diff --git a/PlacesReviews/PlacesReviews/__init__.py b/PlacesReviews/PlacesReviews/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/PlacesReviews/PlacesReviews/asgi.py b/PlacesReviews/PlacesReviews/asgi.py
new file mode 100644
index 0000000..eedf9c4
--- /dev/null
+++ b/PlacesReviews/PlacesReviews/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for PlacesReviews project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'PlacesReviews.settings')
+
+application = get_asgi_application()
diff --git a/PlacesReviews/PlacesReviews/settings.py b/PlacesReviews/PlacesReviews/settings.py
new file mode 100644
index 0000000..a393baa
--- /dev/null
+++ b/PlacesReviews/PlacesReviews/settings.py
@@ -0,0 +1,129 @@
+"""
+Django settings for PlacesReviews project.
+
+Generated by 'django-admin startproject' using Django 5.1.3.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.1/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/5.1/ref/settings/
+"""
+
+from pathlib import Path
+import os
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'django-insecure-m$f6t*aci2ht%fsj-tax&9)u&!=@uy+93vlmi0mod)%ejdb3j!'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'main',
+ 'places',
+ 'users',
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'PlacesReviews.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'PlacesReviews.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': BASE_DIR / 'db.sqlite3',
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/5.1/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/5.1/howto/static-files/
+
+STATIC_URL = 'static/'
+MEDIA_URL = '/media/'
+MEDIA_ROOT = os.path.join(BASE_DIR, 'main', 'media')
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
diff --git a/PlacesReviews/PlacesReviews/urls.py b/PlacesReviews/PlacesReviews/urls.py
new file mode 100644
index 0000000..015eaef
--- /dev/null
+++ b/PlacesReviews/PlacesReviews/urls.py
@@ -0,0 +1,27 @@
+"""
+URL configuration for PlacesReviews project.
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/5.1/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+from django.contrib import admin
+from django.urls import path, include
+from django.conf.urls.static import static
+from . import settings
+
+urlpatterns = [
+ path('admin/', admin.site.urls),
+ path('', include('main.urls')),
+ path('places/', include('places.urls')),
+ path('users/', include('users.urls')),
+] +static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)
\ No newline at end of file
diff --git a/PlacesReviews/PlacesReviews/wsgi.py b/PlacesReviews/PlacesReviews/wsgi.py
new file mode 100644
index 0000000..eed688e
--- /dev/null
+++ b/PlacesReviews/PlacesReviews/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for PlacesReviews project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'PlacesReviews.settings')
+
+application = get_wsgi_application()
diff --git a/PlacesReviews/db.sqlite3 b/PlacesReviews/db.sqlite3
new file mode 100644
index 0000000..67c2e26
Binary files /dev/null and b/PlacesReviews/db.sqlite3 differ
diff --git a/PlacesReviews/main/__init__.py b/PlacesReviews/main/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/PlacesReviews/main/admin.py b/PlacesReviews/main/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/PlacesReviews/main/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/PlacesReviews/main/apps.py b/PlacesReviews/main/apps.py
new file mode 100644
index 0000000..15882ea
--- /dev/null
+++ b/PlacesReviews/main/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class MainConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'main'
\ No newline at end of file
diff --git a/PlacesReviews/main/media/mplaces/AI-revolution-and-its-impact-on-work-and-economy_2.jpg b/PlacesReviews/main/media/mplaces/AI-revolution-and-its-impact-on-work-and-economy_2.jpg
new file mode 100644
index 0000000..a083e7e
Binary files /dev/null and b/PlacesReviews/main/media/mplaces/AI-revolution-and-its-impact-on-work-and-economy_2.jpg differ
diff --git a/PlacesReviews/main/media/places/Detail_Page.png b/PlacesReviews/main/media/places/Detail_Page.png
new file mode 100644
index 0000000..7224aa3
Binary files /dev/null and b/PlacesReviews/main/media/places/Detail_Page.png differ
diff --git a/PlacesReviews/main/media/places/Detail_Page_a7YYdGh.png b/PlacesReviews/main/media/places/Detail_Page_a7YYdGh.png
new file mode 100644
index 0000000..7224aa3
Binary files /dev/null and b/PlacesReviews/main/media/places/Detail_Page_a7YYdGh.png differ
diff --git a/PlacesReviews/main/media/places/Detail_Page_dQoyRbt.png b/PlacesReviews/main/media/places/Detail_Page_dQoyRbt.png
new file mode 100644
index 0000000..7224aa3
Binary files /dev/null and b/PlacesReviews/main/media/places/Detail_Page_dQoyRbt.png differ
diff --git a/PlacesReviews/main/media/places/Detail_Page_wbKoMoy.png b/PlacesReviews/main/media/places/Detail_Page_wbKoMoy.png
new file mode 100644
index 0000000..7224aa3
Binary files /dev/null and b/PlacesReviews/main/media/places/Detail_Page_wbKoMoy.png differ
diff --git a/PlacesReviews/main/media/places/R.jpg b/PlacesReviews/main/media/places/R.jpg
new file mode 100644
index 0000000..220da34
Binary files /dev/null and b/PlacesReviews/main/media/places/R.jpg differ
diff --git a/PlacesReviews/main/media/places/R_9t2Ty04.jpg b/PlacesReviews/main/media/places/R_9t2Ty04.jpg
new file mode 100644
index 0000000..220da34
Binary files /dev/null and b/PlacesReviews/main/media/places/R_9t2Ty04.jpg differ
diff --git a/PlacesReviews/main/media/places/doodles.png b/PlacesReviews/main/media/places/doodles.png
new file mode 100644
index 0000000..a8efc82
Binary files /dev/null and b/PlacesReviews/main/media/places/doodles.png differ
diff --git a/PlacesReviews/main/media/places/doodles_cdktlfZ.png b/PlacesReviews/main/media/places/doodles_cdktlfZ.png
new file mode 100644
index 0000000..a8efc82
Binary files /dev/null and b/PlacesReviews/main/media/places/doodles_cdktlfZ.png differ
diff --git a/PlacesReviews/main/media/places/macdonald-grodzka.jpg b/PlacesReviews/main/media/places/macdonald-grodzka.jpg
new file mode 100644
index 0000000..b28fce6
Binary files /dev/null and b/PlacesReviews/main/media/places/macdonald-grodzka.jpg differ
diff --git a/PlacesReviews/main/media/places/porsche-logo.jpg b/PlacesReviews/main/media/places/porsche-logo.jpg
new file mode 100644
index 0000000..aa52a5d
Binary files /dev/null and b/PlacesReviews/main/media/places/porsche-logo.jpg differ
diff --git a/PlacesReviews/main/media/places/porsche-logo_CSbnZVp.jpg b/PlacesReviews/main/media/places/porsche-logo_CSbnZVp.jpg
new file mode 100644
index 0000000..aa52a5d
Binary files /dev/null and b/PlacesReviews/main/media/places/porsche-logo_CSbnZVp.jpg differ
diff --git a/PlacesReviews/main/media/places/porsche-logo_H0WRVe8.jpg b/PlacesReviews/main/media/places/porsche-logo_H0WRVe8.jpg
new file mode 100644
index 0000000..aa52a5d
Binary files /dev/null and b/PlacesReviews/main/media/places/porsche-logo_H0WRVe8.jpg differ
diff --git a/PlacesReviews/main/media/places/porsche-logo_J403N53.jpg b/PlacesReviews/main/media/places/porsche-logo_J403N53.jpg
new file mode 100644
index 0000000..aa52a5d
Binary files /dev/null and b/PlacesReviews/main/media/places/porsche-logo_J403N53.jpg differ
diff --git a/PlacesReviews/main/media/places/porsche-logo_TKjwnG2.jpg b/PlacesReviews/main/media/places/porsche-logo_TKjwnG2.jpg
new file mode 100644
index 0000000..aa52a5d
Binary files /dev/null and b/PlacesReviews/main/media/places/porsche-logo_TKjwnG2.jpg differ
diff --git a/PlacesReviews/main/media/places/porsche-logo_iQQB4N8.jpg b/PlacesReviews/main/media/places/porsche-logo_iQQB4N8.jpg
new file mode 100644
index 0000000..aa52a5d
Binary files /dev/null and b/PlacesReviews/main/media/places/porsche-logo_iQQB4N8.jpg differ
diff --git a/PlacesReviews/main/media/places/porsche-logo_mPS71ze.jpg b/PlacesReviews/main/media/places/porsche-logo_mPS71ze.jpg
new file mode 100644
index 0000000..aa52a5d
Binary files /dev/null and b/PlacesReviews/main/media/places/porsche-logo_mPS71ze.jpg differ
diff --git a/PlacesReviews/main/media/places/porsche-logo_pbYG5kI.jpg b/PlacesReviews/main/media/places/porsche-logo_pbYG5kI.jpg
new file mode 100644
index 0000000..aa52a5d
Binary files /dev/null and b/PlacesReviews/main/media/places/porsche-logo_pbYG5kI.jpg differ
diff --git a/PlacesReviews/main/migrations/__init__.py b/PlacesReviews/main/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/PlacesReviews/main/models.py b/PlacesReviews/main/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/PlacesReviews/main/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/PlacesReviews/main/static/images/background.png b/PlacesReviews/main/static/images/background.png
new file mode 100644
index 0000000..8c2b34c
Binary files /dev/null and b/PlacesReviews/main/static/images/background.png differ
diff --git a/PlacesReviews/main/static/main/styles/styles.css b/PlacesReviews/main/static/main/styles/styles.css
new file mode 100644
index 0000000..0c843fd
--- /dev/null
+++ b/PlacesReviews/main/static/main/styles/styles.css
@@ -0,0 +1,177 @@
+/* Body Styling */
+html, body {
+ height: 100%;
+ margin: 0;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+ color: #333;
+ font-family: 'Arial', sans-serif;
+ background-image: url('/static/images/background.png');
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-attachment: fixed;
+ background-position: center;
+}
+
+body::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.8);
+ z-index: -1;
+}
+
+/* Main Content */
+main {
+ flex: 1;
+}
+
+/* Navbar Styling */
+/* Navbar Styling */
+.navbar {
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ background-color: #ffffff;
+ padding: 0.5rem 1rem; /* Adjust padding for smaller height */
+}
+
+.navbar-brand {
+ font-weight: bold;
+ color: #035bb9;
+ font-size: 1.25rem; /* Smaller font size */
+}
+
+.navbar-nav .nav-link {
+ font-weight: 500;
+ color: #333;
+ font-size: 0.9rem; /* Smaller font size for nav links */
+}
+
+.navbar-nav .nav-link.active {
+ color: #035bb9;
+}
+
+/* Header Section (Blue Bar) */
+header {
+ background-color: #035bb9; /* Blue color */
+ color: white;
+ text-align: center;
+ padding: 1rem; /* Adjust padding to reduce height */
+ font-size: 1.5rem; /* Smaller font size for header text */
+
+}
+
+header h1 {
+ font-size: 2.5rem;
+ font-weight: bold;
+}
+
+/* Footer Styling */
+footer {
+ border-top: 1px solid #ddd;
+ background-color: #f8f9fa;
+ color: #666;
+ text-align: center;
+ padding: 20px 0;
+ font-size: 0.9rem;
+}
+
+footer p {
+ margin: 0;
+}
+
+/* Custom Button Styles */
+button.btn {
+ background-color: #035bb9;
+ color: #fff;
+ border: none;
+ padding: 10px 20px;
+ font-size: 0.9rem;
+ border-radius: 5px;
+ transition: background-color 0.3s ease-in-out;
+}
+
+button.btn:hover {
+ background-color: #035bb9;
+}
+
+/* Spacing Helpers */
+.my-5 {
+ margin-top: 50px;
+ margin-bottom: 50px;
+}
+
+.py-4 {
+ padding-top: 20px;
+ padding-bottom: 20px;
+}
+
+
+/* Styling for the registration form */
+.form-container {
+ max-width: 500px;
+ margin: 0 auto;
+ padding: 20px;
+ background-color: #f9f9f9;
+ border-radius: 8px;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+h2 {
+ text-align: center;
+ color: #333;
+ margin-bottom: 20px;
+}
+
+.form-group {
+ margin-bottom: 15px;
+}
+
+label {
+ font-size: 16px;
+ font-weight: bold;
+ color: #555;
+ display: block;
+ margin-bottom: 5px;
+}
+
+input, textarea, select {
+ width: 100%;
+ padding: 10px;
+ margin-top: 5px;
+ border-radius: 4px;
+ border: 1px solid #ccc;
+ font-size: 14px;
+ background-color: #fff;
+}
+
+input[type="file"] {
+ padding: 5px;
+}
+
+.error {
+ color: #ff4d4d;
+ font-size: 12px;
+ margin-top: 5px;
+}
+
+button.submit-btn {
+ width: 100%;
+ padding: 10px;
+ background-color: #007bff;
+ color: white;
+ font-size: 16px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+}
+
+button.submit-btn:hover {
+ background-color: #0056b3;
+}
diff --git a/PlacesReviews/main/templates/main/base.html b/PlacesReviews/main/templates/main/base.html
new file mode 100644
index 0000000..da7dcc5
--- /dev/null
+++ b/PlacesReviews/main/templates/main/base.html
@@ -0,0 +1,79 @@
+{% load static %}
+
+
+
+
+
+
+ {% block title %}Places Reviews{% endblock %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% block content %}
+
+ {% endblock %}
+
+
+
+
+
+
+
+
+
+
diff --git a/PlacesReviews/main/templates/main/home.html b/PlacesReviews/main/templates/main/home.html
new file mode 100644
index 0000000..e8ed3a2
--- /dev/null
+++ b/PlacesReviews/main/templates/main/home.html
@@ -0,0 +1,33 @@
+{% extends 'main/base.html' %}
+
+{% block content %}
+
+ {% if messages %}
+ {% for message in messages %}
+
+ {{ message }}
+
+
+ {% endfor %}
+ {% endif %}
+
Latest Places
+
+ {% for place in places %}
+
+
+

+
+
{{ place.name }}
+
City: {{ place.city }}
+
View Details
+
+
+
+ {% empty %}
+
+ {% endfor %}
+
+
+{% endblock %}
diff --git a/PlacesReviews/main/tests.py b/PlacesReviews/main/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/PlacesReviews/main/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/PlacesReviews/main/urls.py b/PlacesReviews/main/urls.py
new file mode 100644
index 0000000..81ec57e
--- /dev/null
+++ b/PlacesReviews/main/urls.py
@@ -0,0 +1,8 @@
+from django.urls import path
+from . import views
+
+app_name="main"
+
+urlpatterns = [
+ path('', views.home_view, name='home_view'),
+]
\ No newline at end of file
diff --git a/PlacesReviews/main/views.py b/PlacesReviews/main/views.py
new file mode 100644
index 0000000..a648bed
--- /dev/null
+++ b/PlacesReviews/main/views.py
@@ -0,0 +1,24 @@
+from django.shortcuts import render
+from places.models import Place
+
+
+def home_view(request):
+ """
+ Renders the home page with the latest 5 places.
+
+ Retrieves the 5 most recent `Place` objects, ordered by `created_at` in descending order.
+ Handles cases where no places exist or other errors occur.
+
+ Args:
+ request: The HTTP request object.
+
+ Returns:
+ HttpResponse: Renders 'main/home.html' with a context containing `places`.
+ """
+ try:
+ places = Place.objects.order_by('-created_at')[:5]
+ except Exception as e:
+ places = []
+ print(f"An error occurred: {e}")
+
+ return render(request, 'main/home.html', {'places': places})
\ No newline at end of file
diff --git a/PlacesReviews/manage.py b/PlacesReviews/manage.py
new file mode 100644
index 0000000..59fce31
--- /dev/null
+++ b/PlacesReviews/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'PlacesReviews.settings')
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/PlacesReviews/places/__init__.py b/PlacesReviews/places/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/PlacesReviews/places/admin.py b/PlacesReviews/places/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/PlacesReviews/places/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/PlacesReviews/places/apps.py b/PlacesReviews/places/apps.py
new file mode 100644
index 0000000..b3d0c3d
--- /dev/null
+++ b/PlacesReviews/places/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class PlacesConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'places'
diff --git a/PlacesReviews/places/forms.py b/PlacesReviews/places/forms.py
new file mode 100644
index 0000000..e8a602c
--- /dev/null
+++ b/PlacesReviews/places/forms.py
@@ -0,0 +1,15 @@
+from django import forms
+from .models import Place
+
+class PlaceForm(forms.ModelForm):
+ class Meta:
+ model = Place
+ fields = ['name', 'description', 'city', 'category', 'photo']
+ widgets = {
+ 'name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Enter place name'}),
+ 'description': forms.Textarea(attrs={'class': 'form-control', 'placeholder': 'Enter description'}),
+ 'city': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Enter city name'}),
+ 'category': forms.Select(attrs={'class': 'form-control'}),
+ 'photo': forms.ClearableFileInput(attrs={'class': 'form-control'}),
+ }
+
\ No newline at end of file
diff --git a/PlacesReviews/places/migrations/0001_initial.py b/PlacesReviews/places/migrations/0001_initial.py
new file mode 100644
index 0000000..a39b5f3
--- /dev/null
+++ b/PlacesReviews/places/migrations/0001_initial.py
@@ -0,0 +1,34 @@
+# Generated by Django 5.1.3 on 2024-11-28 11:09
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Place',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=255)),
+ ('description', models.TextField()),
+ ('city', models.CharField(max_length=100)),
+ ('photo', models.ImageField(upload_to='places/photos/')),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Bookmark',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('added_at', models.DateTimeField(auto_now_add=True)),
+ ('place', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bookmarks', to='places.place')),
+ ],
+ ),
+ ]
diff --git a/PlacesReviews/places/migrations/0002_place_author_alter_place_city_alter_place_photo.py b/PlacesReviews/places/migrations/0002_place_author_alter_place_city_alter_place_photo.py
new file mode 100644
index 0000000..7b5c117
--- /dev/null
+++ b/PlacesReviews/places/migrations/0002_place_author_alter_place_city_alter_place_photo.py
@@ -0,0 +1,31 @@
+# Generated by Django 5.1.3 on 2024-11-29 20:37
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('places', '0001_initial'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='place',
+ name='author',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.AlterField(
+ model_name='place',
+ name='city',
+ field=models.CharField(max_length=255),
+ ),
+ migrations.AlterField(
+ model_name='place',
+ name='photo',
+ field=models.ImageField(upload_to='places/'),
+ ),
+ ]
diff --git a/PlacesReviews/places/migrations/0003_delete_bookmark.py b/PlacesReviews/places/migrations/0003_delete_bookmark.py
new file mode 100644
index 0000000..8ff272f
--- /dev/null
+++ b/PlacesReviews/places/migrations/0003_delete_bookmark.py
@@ -0,0 +1,16 @@
+# Generated by Django 5.1.3 on 2024-11-29 21:04
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('places', '0002_place_author_alter_place_city_alter_place_photo'),
+ ]
+
+ operations = [
+ migrations.DeleteModel(
+ name='Bookmark',
+ ),
+ ]
diff --git a/PlacesReviews/places/migrations/0004_bookmark.py b/PlacesReviews/places/migrations/0004_bookmark.py
new file mode 100644
index 0000000..ba4ab57
--- /dev/null
+++ b/PlacesReviews/places/migrations/0004_bookmark.py
@@ -0,0 +1,28 @@
+# Generated by Django 5.1.3 on 2024-11-30 19:03
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('places', '0003_delete_bookmark'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Bookmark',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('place', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='places.place')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'unique_together': {('user', 'place')},
+ },
+ ),
+ ]
diff --git a/PlacesReviews/places/migrations/0005_place_category.py b/PlacesReviews/places/migrations/0005_place_category.py
new file mode 100644
index 0000000..cc896a8
--- /dev/null
+++ b/PlacesReviews/places/migrations/0005_place_category.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.1.3 on 2024-11-30 21:32
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('places', '0004_bookmark'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='place',
+ name='category',
+ field=models.CharField(choices=[('Restaurant', 'Restaurant'), ('Entertainment', 'Entertainment'), ('Café', 'Café'), ('Museum', 'Museum'), ('Park', 'Park'), ('Other', 'Other')], default='Other', max_length=50),
+ ),
+ ]
diff --git a/PlacesReviews/places/migrations/0006_alter_place_category.py b/PlacesReviews/places/migrations/0006_alter_place_category.py
new file mode 100644
index 0000000..1a6bdbc
--- /dev/null
+++ b/PlacesReviews/places/migrations/0006_alter_place_category.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.1.3 on 2024-11-30 21:34
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('places', '0005_place_category'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='place',
+ name='category',
+ field=models.CharField(choices=[('Restaurant', 'Restaurant'), ('Entertainment', 'Entertainment'), ('Cafe', 'Cafe'), ('Museum', 'Museum'), ('Park', 'Park'), ('Other', 'Other')], default='Other', max_length=50),
+ ),
+ ]
diff --git a/PlacesReviews/places/migrations/__init__.py b/PlacesReviews/places/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/PlacesReviews/places/models.py b/PlacesReviews/places/models.py
new file mode 100644
index 0000000..9c174db
--- /dev/null
+++ b/PlacesReviews/places/models.py
@@ -0,0 +1,32 @@
+from django.db import models
+
+# Create your models here.
+
+from django.contrib.auth.models import User
+
+class Place(models.Model):
+ CATEGORY_CHOICES = [
+ ('Restaurant', 'Restaurant'),
+ ('Entertainment', 'Entertainment'),
+ ('Cafe', 'Cafe'),
+ ('Museum', 'Museum'),
+ ('Park', 'Park'),
+ ('Other', 'Other'),
+ ]
+
+ name = models.CharField(max_length=255)
+ city = models.CharField(max_length=255)
+ description = models.TextField()
+ category = models.CharField(max_length=50, choices=CATEGORY_CHOICES, default='Other')
+ photo = models.ImageField(upload_to='places/')
+ created_at = models.DateTimeField(auto_now_add=True)
+ author = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
+
+class Bookmark(models.Model):
+ user = models.ForeignKey(User, on_delete=models.CASCADE)
+ place = models.ForeignKey(Place, on_delete=models.CASCADE)
+ created_at = models.DateTimeField(auto_now_add=True)
+
+ class Meta:
+ unique_together = ('user', 'place')
+
diff --git a/PlacesReviews/places/templates/places/add_place.html b/PlacesReviews/places/templates/places/add_place.html
new file mode 100644
index 0000000..8988f87
--- /dev/null
+++ b/PlacesReviews/places/templates/places/add_place.html
@@ -0,0 +1,18 @@
+{% extends 'main/base.html' %}
+
+{% block content %}
+{% if messages %}
+ {% for message in messages %}
+
+ {{ message }}
+
+
+ {% endfor %}
+ {% endif %}
+Add a New Place
+
+{% endblock %}
\ No newline at end of file
diff --git a/PlacesReviews/places/templates/places/all_places.html b/PlacesReviews/places/templates/places/all_places.html
new file mode 100644
index 0000000..270202f
--- /dev/null
+++ b/PlacesReviews/places/templates/places/all_places.html
@@ -0,0 +1,55 @@
+{% extends 'main/base.html' %}
+
+{% block content %}
+
+ {% if messages %}
+ {% for message in messages %}
+
+ {{ message }}
+
+
+ {% endfor %}
+ {% endif %}
+
All Places
+
+
+
+
+
+
+ {% for place in places %}
+
+
+

+
+
{{ place.name }}
+
City: {{ place.city }}
+
Category: {{ place.get_category_display }}
+
View Details
+
+
+
+ {% empty %}
+
+ {% endfor %}
+
+
+{% endblock %}
diff --git a/PlacesReviews/places/templates/places/place_detail.html b/PlacesReviews/places/templates/places/place_detail.html
new file mode 100644
index 0000000..62f19e6
--- /dev/null
+++ b/PlacesReviews/places/templates/places/place_detail.html
@@ -0,0 +1,50 @@
+{% extends 'main/base.html' %}
+
+{% block content %}
+
+
+ {% if messages %}
+ {% for message in messages %}
+
+ {{ message }}
+
+
+ {% endfor %}
+ {% endif %}
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/PlacesReviews/places/templates/places/search.html b/PlacesReviews/places/templates/places/search.html
new file mode 100644
index 0000000..c2a6232
--- /dev/null
+++ b/PlacesReviews/places/templates/places/search.html
@@ -0,0 +1,52 @@
+{% extends 'main/base.html' %}
+
+{% block content %}
+
+
Search Results
+
+
+
+
+
+
+ {% if results %}
+ {% for result in results %}
+
+
+

+
+
+
City: {{ result.city }}
+
Category: {{ result.get_category_display }}
+
+
+
+ {% endfor %}
+ {% else %}
+
+
No results found for "{{ query }}".
+
+ {% endif %}
+
+
+{% endblock %}
diff --git a/PlacesReviews/places/tests.py b/PlacesReviews/places/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/PlacesReviews/places/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/PlacesReviews/places/urls.py b/PlacesReviews/places/urls.py
new file mode 100644
index 0000000..4ab335e
--- /dev/null
+++ b/PlacesReviews/places/urls.py
@@ -0,0 +1,17 @@
+from django.contrib import admin
+from django.urls import path
+from . import views
+
+app_name="places"
+
+
+urlpatterns = [
+ path('', views.all_places_view, name='all_places_view'), # List all places
+ path('/', views.place_detail_view, name='place_detail_view'), # View place details
+ path('add/', views.add_place_view, name='add_place_view'), # Add a new place
+ path('search/', views.search_view, name='search_view'), # Search places
+ path('/delete/', views.delete_place_view, name='delete_place_view'),
+ path('bookmarks/add//', views.add_bookmark_view, name='add_bookmark_view'),
+
+
+]
\ No newline at end of file
diff --git a/PlacesReviews/places/views.py b/PlacesReviews/places/views.py
new file mode 100644
index 0000000..e90bc56
--- /dev/null
+++ b/PlacesReviews/places/views.py
@@ -0,0 +1,179 @@
+from django.shortcuts import render, get_object_or_404, redirect
+from django.contrib.auth.decorators import login_required
+from django.contrib import messages
+from django.db.models import Q
+from .models import Place, Bookmark
+from .forms import PlaceForm
+
+
+def all_places_view(request):
+ """
+ Display a list of all places, with optional filtering by category and search query.
+ """
+ try:
+ category = request.GET.get('category', None)
+ query = request.GET.get('q', None)
+ places = Place.objects.all()
+
+ if query:
+ places = places.filter(Q(name__icontains=query) | Q(city__icontains=query))
+ if category:
+ places = places.filter(category=category)
+
+ # Fetch category choices from the model
+ category_choices = Place.CATEGORY_CHOICES
+
+ return render(request, 'places/all_places.html', {
+ 'places': places,
+ 'category_choices': category_choices,
+ 'query': query,
+ 'category': category,
+ })
+
+ except Place.DoesNotExist:
+ messages.error(request, "An error occurred: Some places could not be found.")
+ return render(request, 'places/all_places.html', {
+ 'places': [],
+ 'category_choices': Place.CATEGORY_CHOICES,
+ 'query': query,
+ 'category': category,
+ })
+
+ except Exception as e:
+ messages.error(request, f"An unexpected error occurred: {e}")
+ return render(request, 'places/all_places.html', {
+ 'places': [],
+ 'category_choices': Place.CATEGORY_CHOICES,
+ 'query': query,
+ 'category': category,
+ })
+
+def place_detail_view(request, pk):
+ """
+ Display details of a specific place.
+ """
+ try:
+ place = Place.objects.get(pk=pk)
+ can_delete = request.user.is_authenticated and (place.author == request.user or request.user.is_staff)
+ is_bookmarked = (
+ request.user.is_authenticated
+ and Bookmark.objects.filter(place=place, user=request.user).exists()
+ )
+ return render(request, 'places/place_detail.html', {
+ 'place': place,
+ 'can_delete': can_delete,
+ 'is_bookmarked': is_bookmarked,
+ })
+ except Place.DoesNotExist:
+ messages.error(request, "The requested place does not exist.")
+ return redirect('places:all_places_view')
+ except Exception as e:
+ messages.error(request, f"An error occurred: {e}")
+ return redirect('places:all_places_view')
+
+
+@login_required
+def add_place_view(request):
+ """
+ Allow authenticated users to add a new place.
+ """
+ try:
+ if request.method == 'POST':
+ form = PlaceForm(request.POST, request.FILES)
+ if form.is_valid():
+ place = form.save(commit=False)
+ place.author = request.user
+ place.save()
+ messages.success(request, "Place added successfully!")
+ return redirect('places:all_places_view')
+ else:
+ messages.error(request, "Failed to add the place. Please try again.")
+ else:
+ form = PlaceForm()
+ return render(request, 'places/add_place.html', {'form': form})
+ except Exception as e:
+ messages.error(request, f"An error occurred: {e}")
+ return redirect('places:all_places_view')
+
+
+@login_required
+def delete_place_view(request, pk):
+ """
+ Allow the author or an admin to delete a place.
+ """
+ try:
+ place = get_object_or_404(Place, pk=pk)
+ if place.author != request.user and not request.user.is_staff:
+ messages.error(request, "You are not authorized to delete this place.")
+ return redirect('places:place_detail_view', pk=pk)
+ place.delete()
+ messages.success(request, "Place deleted successfully!")
+ return redirect('places:all_places_view')
+ except Exception as e:
+ messages.error(request, f"An error occurred: {e}")
+ return redirect('places:all_places_view')
+
+
+def search_view(request):
+ """
+ Search for places by name, city, or category.
+ """
+ try:
+ query = request.GET.get('q', '').strip()
+ category = request.GET.get('category', '').strip()
+ results = Place.objects.all()
+
+ if query:
+ results = results.filter(Q(name__icontains=query) | Q(city__icontains=query))
+
+ if category:
+ results = results.filter(category=category)
+
+ # Fetch category choices from the model
+ category_choices = Place.CATEGORY_CHOICES
+
+ return render(request, 'places/search.html', {
+ 'results': results,
+ 'query': query,
+ 'category': category,
+ 'category_choices': category_choices,
+ })
+
+ except Place.DoesNotExist:
+ messages.error(request, "An error occurred: Some places could not be found.")
+ return render(request, 'places/search.html', {
+ 'results': [],
+ 'query': query,
+ 'category': category,
+ 'category_choices': Place.CATEGORY_CHOICES,
+ })
+
+ except Exception as e:
+ messages.error(request, f"An unexpected error occurred: {e}")
+ return render(request, 'places/search.html', {
+ 'results': [],
+ 'query': query,
+ 'category': category,
+ 'category_choices': Place.CATEGORY_CHOICES,
+ })
+
+
+@login_required
+def add_bookmark_view(request, place_id):
+ """
+ Toggle bookmark for a specific place.
+ """
+ try:
+ place = get_object_or_404(Place, pk=place_id)
+ bookmark = Bookmark.objects.filter(place=place, user=request.user).first()
+
+ if not bookmark:
+ Bookmark.objects.create(user=request.user, place=place)
+ messages.success(request, "Bookmark added successfully!")
+ else:
+ bookmark.delete()
+ messages.warning(request, "Bookmark removed.")
+ return redirect('places:place_detail_view', pk=place_id)
+ except Exception as e:
+ messages.error(request, f"An error occurred: {e}")
+ return redirect('places:place_detail_view', pk=place_id)
\ No newline at end of file
diff --git a/PlacesReviews/users/__init__.py b/PlacesReviews/users/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/PlacesReviews/users/admin.py b/PlacesReviews/users/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/PlacesReviews/users/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/PlacesReviews/users/apps.py b/PlacesReviews/users/apps.py
new file mode 100644
index 0000000..72b1401
--- /dev/null
+++ b/PlacesReviews/users/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class UsersConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'users'
diff --git a/PlacesReviews/users/forms.py b/PlacesReviews/users/forms.py
new file mode 100644
index 0000000..e896e5d
--- /dev/null
+++ b/PlacesReviews/users/forms.py
@@ -0,0 +1,21 @@
+from django import forms
+from django.contrib.auth.forms import UserCreationForm
+from django.contrib.auth.models import User
+from .models import Profile
+
+class CustomUserCreationForm(UserCreationForm):
+ email = forms.EmailField(required=True)
+ bio = forms.CharField(widget=forms.Textarea, required=False)
+ profile_image = forms.ImageField(required=False)
+
+ class Meta:
+ model = User
+ fields = ["username", "email", "password1", "password2"]
+
+
+
+
+# class ProfileUpdateForm(forms.ModelForm):
+# class Meta:
+# model = Profile
+# fields = ['bio', 'profile_image']
\ No newline at end of file
diff --git a/PlacesReviews/users/migrations/0001_initial.py b/PlacesReviews/users/migrations/0001_initial.py
new file mode 100644
index 0000000..86747a1
--- /dev/null
+++ b/PlacesReviews/users/migrations/0001_initial.py
@@ -0,0 +1,26 @@
+# Generated by Django 5.1.3 on 2024-12-02 11:13
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Profile',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('bio', models.TextField(blank=True, null=True)),
+ ('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_images/')),
+ ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ ]
diff --git a/PlacesReviews/users/migrations/__init__.py b/PlacesReviews/users/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/PlacesReviews/users/models.py b/PlacesReviews/users/models.py
new file mode 100644
index 0000000..75209e2
--- /dev/null
+++ b/PlacesReviews/users/models.py
@@ -0,0 +1,11 @@
+from django.db import models
+from django.contrib.auth.models import User
+
+class Profile(models.Model):
+ user = models.OneToOneField(User, on_delete=models.CASCADE)
+ bio = models.TextField(blank=True, null=True)
+ profile_image = models.ImageField(upload_to='profile_images/', blank=True, null=True)
+
+
+ def __str__(self):
+ return f'{self.user.username} Profile'
diff --git a/PlacesReviews/users/templates/users/login.html b/PlacesReviews/users/templates/users/login.html
new file mode 100644
index 0000000..52ee9ef
--- /dev/null
+++ b/PlacesReviews/users/templates/users/login.html
@@ -0,0 +1,45 @@
+{% extends 'main/base.html' %}
+
+{% block content %}
+
+
+ {% if messages %}
+ {% for message in messages %}
+
+ {{ message }}
+
+
+ {% endfor %}
+ {% endif %}
+
+
+
+
+
Login
+
+
+
+ Don't have an account? Register
+
+
+
+
+{% endblock %}
diff --git a/PlacesReviews/users/templates/users/logout.html b/PlacesReviews/users/templates/users/logout.html
new file mode 100644
index 0000000..8fcb830
--- /dev/null
+++ b/PlacesReviews/users/templates/users/logout.html
@@ -0,0 +1,4 @@
+
diff --git a/PlacesReviews/users/templates/users/profile.html b/PlacesReviews/users/templates/users/profile.html
new file mode 100644
index 0000000..d4130ac
--- /dev/null
+++ b/PlacesReviews/users/templates/users/profile.html
@@ -0,0 +1,51 @@
+{% extends 'main/base.html' %}
+
+{% block content %}
+
+
+ {% if messages %}
+ {% for message in messages %}
+
+ {{ message }}
+
+
+ {% endfor %}
+ {% endif %}
+
+
+
{{ profile_user.username }}'s Profile
+
+
+
+
+ {% if can_add_place %}
+
Add a Place
+ {% endif %}
+
+ {% if user.is_authenticated %}
+
View My Bookmarks
+ {% endif %}
+
+
+
+
Places Published
+
+ {% for place in places %}
+
+
+

+
+
{{ place.name }}
+
City: {{ place.city }}
+
View Details
+
+
+
+ {% empty %}
+
+
No places published yet.
+
+ {% endfor %}
+
+
+{% endblock %}
diff --git a/PlacesReviews/users/templates/users/register.html b/PlacesReviews/users/templates/users/register.html
new file mode 100644
index 0000000..d6ebb72
--- /dev/null
+++ b/PlacesReviews/users/templates/users/register.html
@@ -0,0 +1,49 @@
+{% extends "main/base.html" %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/PlacesReviews/users/templates/users/user_bookmarks.html b/PlacesReviews/users/templates/users/user_bookmarks.html
new file mode 100644
index 0000000..a4d94c7
--- /dev/null
+++ b/PlacesReviews/users/templates/users/user_bookmarks.html
@@ -0,0 +1,29 @@
+{% extends 'main/base.html' %}
+
+{% block content %}
+
+
{{ user.username }}'s Bookmarked Places
+
+
+ {% for bookmark in bookmarks %}
+
+
+

+
+
+
City: {{ bookmark.place.city }}
+
+
+
+ {% empty %}
+
+
You have no bookmarked places yet.
+
+ {% endfor %}
+
+
+{% endblock %}
diff --git a/PlacesReviews/users/tests.py b/PlacesReviews/users/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/PlacesReviews/users/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/PlacesReviews/users/urls.py b/PlacesReviews/users/urls.py
new file mode 100644
index 0000000..b947408
--- /dev/null
+++ b/PlacesReviews/users/urls.py
@@ -0,0 +1,16 @@
+from django.urls import path
+from django.contrib.auth.views import LoginView, LogoutView
+from django.contrib.auth.forms import UserCreationForm
+from django.views.generic.edit import CreateView
+from . import views
+
+
+app_name = "users"
+
+urlpatterns = [
+ path('signup/', views.sign_up, name='signup'),
+ path('profile//', views.user_profile_view, name='user_profile'),
+ path('bookmarks/', views.user_bookmarks_view, name='user_bookmarks'),
+ path('signin/', views.sign_in, name='signin'),
+ path('logout/', views.log_out, name='logout'),
+]
diff --git a/PlacesReviews/users/views.py b/PlacesReviews/users/views.py
new file mode 100644
index 0000000..36cdf70
--- /dev/null
+++ b/PlacesReviews/users/views.py
@@ -0,0 +1,143 @@
+from django.shortcuts import render, redirect, get_object_or_404
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import User
+from places.models import Place, Bookmark
+from django.contrib import messages
+from django.http import HttpRequest, Http404
+from django.contrib.auth import authenticate, login, logout
+from .forms import CustomUserCreationForm
+from .models import Profile
+from django.db import transaction
+
+
+
+def sign_up(request):
+ """
+ Handle user registration with a custom form.
+
+ If the form is valid, the user is saved and redirected to the login page.
+ Displays error messages for invalid submissions.
+
+ Args:
+ request: HttpRequest object.
+
+ Returns:
+ HttpResponse: Render 'users/register.html' with the form.
+ """
+ try:
+ if request.method == 'POST':
+ form = CustomUserCreationForm(request.POST)
+ if form.is_valid():
+ form.save()
+ messages.success(request, "Registration successful!")
+ return redirect('users:signin') # After registration, redirect to login page
+
+ else:
+ form = CustomUserCreationForm()
+ except Exception as e:
+ messages.error(request, f"An unexpected error occurred: {e}")
+ form = CustomUserCreationForm()
+
+ return render(request, 'users/register.html', {'form': form})
+
+
+
+
+def user_profile_view(request, username):
+ """
+ Display a user's profile page with their published places.
+
+ Args:
+ request: HttpRequest object.
+ username: Username of the profile to display.
+
+ Returns:
+ HttpResponse: Render 'users/profile.html' with user details and places.
+ """
+ try:
+
+ profile_user = User.objects.filter(username=username).first()
+
+ if not profile_user:
+
+ messages.error(request, f"User with username '{username}' not found.")
+ profile_user, places, can_add_place = None, [], False
+ else:
+ places = Place.objects.filter(author=profile_user)
+ can_add_place = request.user.is_authenticated and request.user == profile_user
+ except Exception as e:
+ messages.error(request, f"An error occurred while fetching the profile: {e}")
+ profile_user, places, can_add_place = None, [], False
+
+ context = {
+ 'profile_user': profile_user,
+ 'places': places,
+ 'can_add_place': can_add_place,
+ }
+ return render(request, 'users/profile.html', context)
+
+
+
+
+@login_required
+def user_bookmarks_view(request):
+ """
+ Display all places bookmarked by the logged-in user.
+
+ Args:
+ request: HttpRequest object.
+
+ Returns:
+ HttpResponse: Render 'users/user_bookmarks.html' with bookmarks.
+ """
+ try:
+ bookmarks = Bookmark.objects.filter(user=request.user).select_related('place')
+ except Bookmark.DoesNotExist:
+ bookmarks = []
+ messages.warning(request, "No bookmarks found.")
+ except Exception as e:
+ bookmarks = []
+ messages.error(request, f"An error occurred: {e}")
+
+ return render(request, 'users/user_bookmarks.html', {'bookmarks': bookmarks})
+
+def sign_in(request: HttpRequest):
+ """
+ Handle user login and authenticate credentials.
+
+ Args:
+ request: HttpRequest object.
+
+ Returns:
+ HttpResponse: Render 'users/signin.html' for login page.
+ """
+ if request.method == "POST":
+ # Authenticate the user
+ user = authenticate(request, username=request.POST["username"], password=request.POST["password"])
+ if user is not None:
+ # Login the user
+ login(request, user)
+ messages.success(request, "Logged in successfully!", "alert-success")
+ # Redirect to the next page or homepage
+ return redirect(request.GET.get("next", "/"))
+ else:
+ messages.error(request, "Invalid credentials. Please try again.", "alert-danger")
+
+ return render(request, "users/login.html")
+
+def log_out(request: HttpRequest):
+ """
+ Logout the user and redirect to the next page.
+
+ Args:
+ request: HttpRequest object.
+
+ Returns:
+ HttpResponse: Redirect to the next page or homepage after logout.
+ """
+ logout(request)
+ messages.success(request, "Logged out successfully!", "alert-warning")
+
+ return redirect(request.GET.get("next", "/"))
+
+