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 %} + + + + + + + + + + + + + + +
+

Discover and share reviews of amazing places.

+
+ + +
+ {% 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 %} + + {% endfor %} + {% endif %} +

Latest Places

+
+ {% for place in places %} +
+
+ {{ place.name }} +
+
{{ place.name }}
+

City: {{ place.city }}

+ View Details +
+
+
+ {% empty %} +
+

No places added yet.

+
+ {% 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 %} + + {% endfor %} + {% endif %} +

Add a New Place

+
+ {% csrf_token %} + {{ form.as_p }} + +
+{% 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 %} + + {% endfor %} + {% endif %} +

All Places

+ + +
+ + + + + + + + +
+ + +
+ {% for place in places %} +
+
+ {{ place.name }} +
+
{{ place.name }}
+

City: {{ place.city }}

+

Category: {{ place.get_category_display }}

+ View Details +
+
+
+ {% empty %} +
+

No places available.

+
+ {% 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 %} + + {% endfor %} + {% endif %} + + +
+

{{ place.name }}

+
+

City:

+

{{ place.city }}

+
+
+

Description:

+

{{ place.description }}

+
+
+ {{ place.name }} +
+ +
+ {% if user.is_authenticated %} + + {% if is_bookmarked %} + Remove Bookmark + {% else %} + Add Bookmark + {% endif %} + + {% endif %} + + {% if can_delete %} +
+ {% csrf_token %} + +
+ {% 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 %} +
+
+ {{ result.name }} +
+
+ + {{ result.name }} + +
+

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 %} + + {% endfor %} + {% endif %} + + +
+
+

Login

+
+ {% csrf_token %} + +
+ + +
{{ form.username.errors }}
+
+ + +
+ + +
{{ form.password.errors }}
+
+ + + +
+ +

+ 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 @@ +
+ {% csrf_token %} + +
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 %} + + {% 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 }} +
+
{{ 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 %} +
+

Register

+
+ {% csrf_token %} + {{ form.non_field_errors }} + +
+ + {{ form.username }} +
{{ form.username.errors }}
+
+ +
+ + {{ form.email }} +
{{ form.email.errors }}
+
+ +
+ + {{ form.password1 }} +
{{ form.password1.errors }}
+
+ +
+ + {{ form.password2 }} +
{{ form.password2.errors }}
+
+ +
+ + {{ form.bio }} +
{{ form.bio.errors }}
+
+ +
+ + {{ form.profile_image }} +
{{ form.profile_image.errors }}
+
+ + +
+
+{% 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 %} +
+
+ {{ bookmark.place.name }} +
+
+ + {{ bookmark.place.name }} + +
+

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", "/")) + +