diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000..ed0d4d7fac Binary files /dev/null and b/.coverage differ diff --git a/.gitignore b/.gitignore index b4405ebab4..47b985badd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ **/__pycache__ *.pyc venv +.DS_Store +.env \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..1855c44055 --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +.PHONY: test coverage clean run test-clean + +PYTHON = python +COVERAGE = coverage + +test-clean: + rm -f profiles/migrations/0002_copy_data_from_old_models.py + $(PYTHON) manage.py test lettings oc_lettings_site profiles + +test: + $(PYTHON) manage.py test lettings oc_lettings_site profiles + +test-coverage: + $(COVERAGE) run --source='.' manage.py test lettings oc_lettings_site profiles + $(COVERAGE) report + $(COVERAGE) html + +run: + $(PYTHON) manage.py runserver + +clean: + find . -type d -name "__pycache__" -exec rm -r {} + + find . -type f -name "*.pyc" -delete + find . -type f -name "*.pyo" -delete + find . -type f -name "*.pyd" -delete + find . -type f -name ".coverage" -delete + find . -type d -name "*.egg-info" -exec rm -r {} + + find . -type d -name "*.egg" -exec rm -r {} + + find . -type d -name "htmlcov" -exec rm -r {} + + find . -type d -name ".pytest_cache" -exec rm -r {} + + find . -type d -name ".coverage" -exec rm -r {} + + +all: test-clean \ No newline at end of file diff --git a/lettings/__init__.py b/lettings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lettings/admin.py b/lettings/admin.py new file mode 100644 index 0000000000..acd55bc056 --- /dev/null +++ b/lettings/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from .models import Letting, Address + +# Register your models here. +admin.site.register(Letting) +admin.site.register(Address) diff --git a/lettings/apps.py b/lettings/apps.py new file mode 100644 index 0000000000..63dada00c4 --- /dev/null +++ b/lettings/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class LettingsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'lettings' diff --git a/lettings/migrations/0001_initial.py b/lettings/migrations/0001_initial.py new file mode 100644 index 0000000000..bd7d8ded63 --- /dev/null +++ b/lettings/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# Generated by Django 4.2.11 on 2025-05-16 11:59 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Address', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('number', models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(9999)])), + ('street', models.CharField(max_length=64)), + ('city', models.CharField(max_length=64)), + ('state', models.CharField(max_length=2, validators=[django.core.validators.MinLengthValidator(2)])), + ('zip_code', models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(99999)])), + ('country_iso_code', models.CharField(max_length=3, validators=[django.core.validators.MinLengthValidator(3)])), + ], + ), + migrations.CreateModel( + name='Letting', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=256)), + ('address', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='lettings.address')), + ], + ), + ] diff --git a/lettings/migrations/0002_copy_data_from_old_models.py b/lettings/migrations/0002_copy_data_from_old_models.py new file mode 100644 index 0000000000..fd16f02d90 --- /dev/null +++ b/lettings/migrations/0002_copy_data_from_old_models.py @@ -0,0 +1,36 @@ +from django.db import migrations + +def copy_lettings_and_addresses(apps, schema_editor): + OldAddress = apps.get_model('oc_lettings_site', 'Address') + OldLetting = apps.get_model('oc_lettings_site', 'Letting') + NewAddress = apps.get_model('lettings', 'Address') + NewLetting = apps.get_model('lettings', 'Letting') + + old_to_new_address = {} + for old_addr in OldAddress.objects.all(): + new_addr = NewAddress.objects.create( + number=old_addr.number, + street=old_addr.street, + city=old_addr.city, + state=old_addr.state, + zip_code=old_addr.zip_code, + country_iso_code=old_addr.country_iso_code, + ) + old_to_new_address[old_addr.id] = new_addr + + # Copier les lettings + for old_letting in OldLetting.objects.all(): + NewLetting.objects.create( + title=old_letting.title, + address=old_to_new_address.get(old_letting.address_id) + ) + +class Migration(migrations.Migration): + dependencies = [ + ('lettings', '0001_initial'), + ('oc_lettings_site', '__first__'), + ] + + operations = [ + migrations.RunPython(copy_lettings_and_addresses), + ] \ No newline at end of file diff --git a/lettings/migrations/__init__.py b/lettings/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lettings/models.py b/lettings/models.py new file mode 100644 index 0000000000..84f1d51f36 --- /dev/null +++ b/lettings/models.py @@ -0,0 +1,45 @@ +from django.db import models +from django.core.validators import MaxValueValidator, MinLengthValidator + + +class Address(models.Model): + """ + Model representing a physical address. + This model stores : + number, street, city, state, zip code, and country code. + """ + number = models.PositiveIntegerField(validators=[MaxValueValidator(9999)]) + street = models.CharField(max_length=64) + city = models.CharField(max_length=64) + state = models.CharField(max_length=2, validators=[MinLengthValidator(2)]) + zip_code = models.PositiveIntegerField(validators=[MaxValueValidator(99999)]) + country_iso_code = models.CharField(max_length=3, validators=[MinLengthValidator(3)]) + + class Meta: + verbose_name_plural = "Addresses" + + def __str__(self): + """ + Returns a string representation of the address. + Returns: + str: The address in the format 'number street' + """ + return f'{self.number} {self.street}' + + +class Letting(models.Model): + """ + Model representing a property letting. + This model stores title and associated addresses. + """ + title = models.CharField(max_length=256) + address = models.OneToOneField(Address, on_delete=models.CASCADE) + + class Meta: + verbose_name_plural = "Lettings" + + def __str__(self): + """ + Returns the title of the letting. + """ + return self.title diff --git a/templates/lettings_index.html b/lettings/templates/lettings/index.html similarity index 88% rename from templates/lettings_index.html rename to lettings/templates/lettings/index.html index 92857a78d9..0251067b6b 100644 --- a/templates/lettings_index.html +++ b/lettings/templates/lettings/index.html @@ -20,7 +20,7 @@

Lettings

@@ -36,10 +36,10 @@

Lettings

Home - + Profiles -{% endblock %} \ No newline at end of file +{% endblock %} \ No newline at end of file diff --git a/templates/letting.html b/lettings/templates/lettings/letting.html similarity index 94% rename from templates/letting.html rename to lettings/templates/lettings/letting.html index 7e5f3a73fd..3a050e03cc 100644 --- a/templates/letting.html +++ b/lettings/templates/lettings/letting.html @@ -25,17 +25,17 @@

{{ title }}

- + Back Home - + Profiles
-{% endblock %} \ No newline at end of file +{% endblock %} \ No newline at end of file diff --git a/lettings/tests.py b/lettings/tests.py new file mode 100644 index 0000000000..dcbbcc52de --- /dev/null +++ b/lettings/tests.py @@ -0,0 +1,46 @@ +from django.test import TestCase, Client +from django.urls import reverse +from .models import Letting, Address + + +class LettingViewsTest(TestCase): + def setUp(self): + """Set up test data.""" + self.client = Client() + self.address = Address.objects.create( + number=123, + street="Test Street", + city="Test City", + state="TS", + zip_code=12345, + country_iso_code="FR" + ) + self.letting = Letting.objects.create( + title="Test Letting", + address=self.address + ) + + def test_index_view(self): + """Test the index view.""" + url = reverse('lettings:index') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'lettings/index.html') + self.assertIn('lettings_list', response.context) + self.assertEqual(len(response.context['lettings_list']), 1) + self.assertEqual(response.context['lettings_list'][0], self.letting) + + def test_letting_view(self): + """Test the letting detail view.""" + url = reverse('lettings:letting', args=[self.letting.id]) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'lettings/letting.html') + self.assertEqual(response.context['title'], self.letting.title) + self.assertEqual(response.context['address'], self.letting.address) + + def test_letting_view_404(self): + """Test the letting detail view with non-existent letting.""" + url = reverse('lettings:letting', args=[999]) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) diff --git a/lettings/urls.py b/lettings/urls.py new file mode 100644 index 0000000000..b2f0174c74 --- /dev/null +++ b/lettings/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from . import views + +app_name = 'lettings' + +urlpatterns = [ + path('', views.index, name='index'), + path('/', views.letting, name='letting'), +] diff --git a/lettings/views.py b/lettings/views.py new file mode 100644 index 0000000000..b7a4e2c9f9 --- /dev/null +++ b/lettings/views.py @@ -0,0 +1,47 @@ +from django.shortcuts import render, get_object_or_404 +from django.http import Http404 +import logging +from .models import Letting + +logger = logging.getLogger(__name__) + +def index(request): + """ + View function for the lettings list page. + Args: + request: The HTTP request object + Returns: + template 'lettings/index.html' with a list of all lettings + """ + try: + lettings_list = Letting.objects.all() + logger.info(f"Retrieved {len(lettings_list)} lettings for index page") + return render(request, 'lettings/index.html', context={'lettings_list': lettings_list}) + except Exception as e: + logger.error(f"Error retrieving lettings list: {str(e)}", exc_info=True) + raise + +def letting(request, letting_id): + """ + View function for displaying a specific letting's details. + Args: + request: The HTTP request object + letting_id: The ID of the letting to display + Returns: + template 'lettings/letting.html' with the requested letting's details + Raises: + Http404: If the letting with the given ID doesn't exist + """ + try: + letting = get_object_or_404(Letting, id=letting_id) + logger.info(f"Retrieved letting {letting_id}: {letting.title}") + return render(request, 'lettings/letting.html', context={ + 'title': letting.title, + 'address': letting.address, + }) + except Http404: + logger.warning(f"Letting with id {letting_id} not found") + raise + except Exception as e: + logger.error(f"Error retrieving letting {letting_id}: {str(e)}", exc_info=True) + raise diff --git a/oc-lettings-site.sqlite3 b/oc-lettings-site.sqlite3 index 3d885414f9..8258bc7c2e 100644 Binary files a/oc-lettings-site.sqlite3 and b/oc-lettings-site.sqlite3 differ diff --git a/oc_lettings_site/admin.py b/oc_lettings_site/admin.py index 63328c6dd3..65bb1527fd 100644 --- a/oc_lettings_site/admin.py +++ b/oc_lettings_site/admin.py @@ -1,10 +1 @@ -from django.contrib import admin - -from .models import Letting -from .models import Address -from .models import Profile - - -admin.site.register(Letting) -admin.site.register(Address) -admin.site.register(Profile) +# Les modèles ont été déplacés dans les applications 'lettings' et 'profiles'. diff --git a/oc_lettings_site/migrations/0002_remove_letting_address_remove_profile_user_and_more.py b/oc_lettings_site/migrations/0002_remove_letting_address_remove_profile_user_and_more.py new file mode 100644 index 0000000000..ec34d47d68 --- /dev/null +++ b/oc_lettings_site/migrations/0002_remove_letting_address_remove_profile_user_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.11 on 2025-05-16 12:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('oc_lettings_site', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='letting', + name='address', + ), + migrations.RemoveField( + model_name='profile', + name='user', + ), + migrations.DeleteModel( + name='Address', + ), + migrations.DeleteModel( + name='Letting', + ), + migrations.DeleteModel( + name='Profile', + ), + ] diff --git a/oc_lettings_site/models.py b/oc_lettings_site/models.py index ed255e8c11..65bb1527fd 100644 --- a/oc_lettings_site/models.py +++ b/oc_lettings_site/models.py @@ -1,31 +1 @@ -from django.db import models -from django.core.validators import MaxValueValidator, MinLengthValidator -from django.contrib.auth.models import User - - -class Address(models.Model): - number = models.PositiveIntegerField(validators=[MaxValueValidator(9999)]) - street = models.CharField(max_length=64) - city = models.CharField(max_length=64) - state = models.CharField(max_length=2, validators=[MinLengthValidator(2)]) - zip_code = models.PositiveIntegerField(validators=[MaxValueValidator(99999)]) - country_iso_code = models.CharField(max_length=3, validators=[MinLengthValidator(3)]) - - def __str__(self): - return f'{self.number} {self.street}' - - -class Letting(models.Model): - title = models.CharField(max_length=256) - address = models.OneToOneField(Address, on_delete=models.CASCADE) - - def __str__(self): - return self.title - - -class Profile(models.Model): - user = models.OneToOneField(User, on_delete=models.CASCADE) - favorite_city = models.CharField(max_length=64, blank=True) - - def __str__(self): - return self.user.username +# Les modèles ont été déplacés dans les applications 'lettings' et 'profiles'. diff --git a/oc_lettings_site/settings.py b/oc_lettings_site/settings.py index a18bee8106..e6202e2c49 100644 --- a/oc_lettings_site/settings.py +++ b/oc_lettings_site/settings.py @@ -1,10 +1,60 @@ import os - from pathlib import Path +import sentry_sdk +from sentry_sdk.integrations.django import DjangoIntegration +from sentry_sdk.integrations.logging import LoggingIntegration +import logging + +sentry_sdk.init( + dsn = "https://7220363d53b6d31ec048180b5b0074e9@o452613.ingest.us.sentry.io/4509418921263104", + integrations=[ + DjangoIntegration(), + LoggingIntegration( + level=logging.INFO, + event_level=logging.ERROR + ), + ], + traces_sample_rate=1.0, + send_default_pii=True, +) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = Path(__file__).resolve().parent.parent +# Logging config +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', + 'style': '{', + }, + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + }, + }, + 'loggers': { + 'django': { + 'handlers': ['console'], + 'level': 'INFO', + 'propagate': True, + }, + 'lettings': { + 'handlers': ['console'], + 'level': 'INFO', + 'propagate': True, + }, + 'profiles': { + 'handlers': ['console'], + 'level': 'INFO', + 'propagate': True, + }, + }, +} # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ @@ -15,7 +65,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['localhost', '127.0.0.1'] # Application definition @@ -28,6 +78,8 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'lettings', + 'profiles', ] MIDDLEWARE = [ @@ -45,7 +97,7 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'templates')], + 'DIRS': [os.path.join(BASE_DIR, 'oc_lettings_site/templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -111,4 +163,4 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') STATIC_URL = '/static/' -STATICFILES_DIRS = [BASE_DIR / "static",] +STATICFILES_DIRS = [BASE_DIR / "static"] diff --git a/oc_lettings_site/templates/404.html b/oc_lettings_site/templates/404.html new file mode 100644 index 0000000000..4d71daac0c --- /dev/null +++ b/oc_lettings_site/templates/404.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block title %}Page non trouvée{% endblock %} + +{% block content %} +
+

404

+

Oops !

+

La page que vous recherchez n'existe pas.

+ Retour à l'accueil +
+{% endblock %} diff --git a/oc_lettings_site/templates/500.html b/oc_lettings_site/templates/500.html new file mode 100644 index 0000000000..725094beac --- /dev/null +++ b/oc_lettings_site/templates/500.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block title %}Erreur serveur{% endblock %} + +{% block content %} +
+

500

+

Oops !

+

Une erreur inattendue s'est produite.

+

Notre équipe technique a été notifiée et travaille à résoudre le problème.

+ Retour à l'accueil +
+{% endblock %} diff --git a/templates/base.html b/oc_lettings_site/templates/base.html similarity index 97% rename from templates/base.html rename to oc_lettings_site/templates/base.html index ab7addba01..a480792d39 100644 --- a/templates/base.html +++ b/oc_lettings_site/templates/base.html @@ -24,10 +24,10 @@
Logo Orange County Lettings
- + Profiles - + Lettings
@@ -64,4 +64,4 @@ }); - \ No newline at end of file + \ No newline at end of file diff --git a/templates/index.html b/oc_lettings_site/templates/index.html similarity index 90% rename from templates/index.html rename to oc_lettings_site/templates/index.html index 71a8e61a46..cf7f8d8122 100644 --- a/templates/index.html +++ b/oc_lettings_site/templates/index.html @@ -14,13 +14,13 @@

Welcome to Holiday Homes

-{% endblock %} \ No newline at end of file +{% endblock %} \ No newline at end of file diff --git a/oc_lettings_site/tests.py b/oc_lettings_site/tests.py index 3fd62bb718..656ffe7894 100644 --- a/oc_lettings_site/tests.py +++ b/oc_lettings_site/tests.py @@ -1,2 +1,33 @@ +from django.test import TestCase, Client +from django.urls import reverse + + +class OCLettingsSiteViewsTest(TestCase): + def setUp(self): + """Set up test data.""" + self.client = Client() + + def test_index_view(self): + """Test the index view.""" + url = reverse('index') + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'index.html') + + def test_404_view(self): + """Test the 404 error view.""" + url = reverse('test_404') + + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + def test_500_view(self): + """Test the 500 error view.""" + url = reverse('test_500') + with self.assertRaises(ZeroDivisionError): + self.client.get(url) + + def test_dummy(): assert 1 diff --git a/oc_lettings_site/urls.py b/oc_lettings_site/urls.py index f0ff5897ab..7bfd70bd58 100644 --- a/oc_lettings_site/urls.py +++ b/oc_lettings_site/urls.py @@ -1,13 +1,22 @@ from django.contrib import admin -from django.urls import path +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static from . import views +def trigger_error(request): + division_by_zero = 1 / 0 + urlpatterns = [ path('', views.index, name='index'), - path('lettings/', views.lettings_index, name='lettings_index'), - path('lettings//', views.letting, name='letting'), - path('profiles/', views.profiles_index, name='profiles_index'), - path('profiles//', views.profile, name='profile'), + path('lettings/', include('lettings.urls', namespace='lettings')), + path('profiles/', include('profiles.urls', namespace='profiles')), path('admin/', admin.site.urls), -] + # Test URLs for error pages + path('404/', views.test_404, name='test_404'), + path('500/', views.test_500, name='test_500'), + # Test sentry + path('sentry-debug/', trigger_error), + +] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/oc_lettings_site/views.py b/oc_lettings_site/views.py index a72db27074..1579bf3f6b 100644 --- a/oc_lettings_site/views.py +++ b/oc_lettings_site/views.py @@ -1,45 +1,44 @@ from django.shortcuts import render -from .models import Letting, Profile +from django.http import Http404 +import logging +logger = logging.getLogger(__name__) - - -# Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque molestie quam lobortis leo consectetur ullamcorper non id est. Praesent dictum, nulla eget feugiat sagittis, sem mi convallis eros, -# vitae dapibus nisi lorem dapibus sem. Maecenas pharetra purus ipsum, eget consequat ipsum lobortis quis. Phasellus eleifend ex auctor venenatis tempus. -# Aliquam vitae erat ac orci placerat luctus. Nullam elementum urna nisi, pellentesque iaculis enim cursus in. Praesent volutpat porttitor magna, non finibus neque cursus id. def index(request): + """ + View function for the home page. + Args: + request: The HTTP request object + Returns: + Rendered template 'index.html' + """ + try: + logger.info("Rendering home page") + return render(request, 'index.html') + except Exception as e: + logger.error(f"Error rendering home page: {str(e)}", exc_info=True) + raise + +def test_404(request): + """ + Test view that raises a 404 error. + Args: + request: The HTTP request object + Raises: + Http404: Always raises a 404 error for testing purposes + """ + logger.warning("Test 404 error page requested") + raise Http404("Page de test 404") + + +def test_500(request): + """ + Test view that raises a 500 error by performing a division by zero. + Args: + request: The HTTP request object + Returns: + This function never returns as it raises a ZeroDivisionError + """ + logger.error("Test 500 error page requested") + 1 / 0 # Raise ZeroDivisionError return render(request, 'index.html') - -# Aenean leo magna, vestibulum et tincidunt fermentum, consectetur quis velit. Sed non placerat massa. Integer est nunc, pulvinar a -# tempor et, bibendum id arcu. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Cras eget scelerisque -def lettings_index(request): - lettings_list = Letting.objects.all() - context = {'lettings_list': lettings_list} - return render(request, 'lettings_index.html', context) - - -#Cras ultricies dignissim purus, vitae hendrerit ex varius non. In accumsan porta nisl id eleifend. Praesent dignissim, odio eu consequat pretium, purus urna vulputate arcu, vitae efficitur -# lacus justo nec purus. Aenean finibus faucibus lectus at porta. Maecenas auctor, est ut luctus congue, dui enim mattis enim, ac condimentum velit libero in magna. Suspendisse potenti. In tempus a nisi sed laoreet. -# Suspendisse porta dui eget sem accumsan interdum. Ut quis urna pellentesque justo mattis ullamcorper ac non tellus. In tristique mauris eu velit fermentum, tempus pharetra est luctus. Vivamus consequat aliquam libero, eget bibendum lorem. Sed non dolor risus. Mauris condimentum auctor elementum. Donec quis nisi ligula. Integer vehicula tincidunt enim, ac lacinia augue pulvinar sit amet. -def letting(request, letting_id): - letting = Letting.objects.get(id=letting_id) - context = { - 'title': letting.title, - 'address': letting.address, - } - return render(request, 'letting.html', context) - -# Sed placerat quam in pulvinar commodo. Nullam laoreet consectetur ex, sed consequat libero pulvinar eget. Fusc -# faucibus, urna quis auctor pharetra, massa dolor cursus neque, quis dictum lacus d -def profiles_index(request): - profiles_list = Profile.objects.all() - context = {'profiles_list': profiles_list} - return render(request, 'profiles_index.html', context) - -# Aliquam sed metus eget nisi tincidunt ornare accumsan eget lac -# laoreet neque quis, pellentesque dui. Nullam facilisis pharetra vulputate. Sed tincidunt, dolor id facilisis fringilla, eros leo tristique lacus, -# it. Nam aliquam dignissim congue. Pellentesque habitant morbi tristique senectus et netus et males -def profile(request, username): - profile = Profile.objects.get(user__username=username) - context = {'profile': profile} - return render(request, 'profile.html', context) diff --git a/profiles/__init__.py b/profiles/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/profiles/admin.py b/profiles/admin.py new file mode 100644 index 0000000000..123dd95de0 --- /dev/null +++ b/profiles/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from .models import Profile + +# Register your models here. +admin.site.register(Profile) diff --git a/profiles/apps.py b/profiles/apps.py new file mode 100644 index 0000000000..d722976462 --- /dev/null +++ b/profiles/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ProfilesConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'profiles' diff --git a/profiles/migrations/0001_initial.py b/profiles/migrations/0001_initial.py new file mode 100644 index 0000000000..fae9ad6bb1 --- /dev/null +++ b/profiles/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.11 on 2025-05-16 11:59 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +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')), + ('favorite_city', models.CharField(blank=True, max_length=64)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/profiles/migrations/__init__.py b/profiles/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/profiles/models.py b/profiles/models.py new file mode 100644 index 0000000000..9ac020da62 --- /dev/null +++ b/profiles/models.py @@ -0,0 +1,23 @@ +from django.db import models +from django.contrib.auth.models import User + + +class Profile(models.Model): + """ + Model representing a user profile. + This model extends the default Django User model with additional information + about the user's favorite city. + """ + user = models.OneToOneField(User, on_delete=models.CASCADE) + favorite_city = models.CharField(max_length=64, blank=True) + + class Meta: + verbose_name_plural = "Profiles" + + def __str__(self): + """ + Returns the username of the associated user. + Returns: + str: The username of the user + """ + return self.user.username diff --git a/profiles/templates/profiles/index.html b/profiles/templates/profiles/index.html new file mode 100644 index 0000000000..09ee8df3ec --- /dev/null +++ b/profiles/templates/profiles/index.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} +{% block title %}Profiles{% endblock title %} + +{% block content %} +
+
+
+

Profiles

+
+
+
+ +
+
+
+
+ {% if profiles_list %} + + {% else %} +

No profiles are available.

+ {% endif %} +
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/profiles/templates/profiles/profile.html b/profiles/templates/profiles/profile.html new file mode 100644 index 0000000000..c87f86d5f2 --- /dev/null +++ b/profiles/templates/profiles/profile.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} +{% block title %}{{ profile.user.username }}{% endblock title %} + +{% block content %} +
+
+
+

{{ profile.user.username }}

+
+
+
+ +
+
+
+
+

First name : {{ profile.user.first_name }}

+

Last name : {{ profile.user.last_name }}

+

Email : {{ profile.user.email }}

+

Favorite city : {{ profile.favorite_city }}

+
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/profiles/tests.py b/profiles/tests.py new file mode 100644 index 0000000000..2a66ccf7dc --- /dev/null +++ b/profiles/tests.py @@ -0,0 +1,49 @@ +from django.test import TestCase, Client +from django.urls import reverse +from django.contrib.auth.models import User +from .models import Profile + + +class ProfilesViewsTest(TestCase): + def setUp(self): + """Set up test data.""" + self.client = Client() + self.user = User.objects.create_user( + username='testuser', + email='test@example.com', + password='testpass123' + ) + # Create a test profile + self.profile = Profile.objects.create( + user=self.user, + favorite_city='Test City' + ) + + def test_index_view(self): + """Test the profiles list view.""" + + url = reverse('profiles:index') + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'profiles/index.html') + self.assertIn('profiles_list', response.context) + self.assertEqual(len(response.context['profiles_list']), 1) + self.assertEqual(response.context['profiles_list'][0], self.profile) + + def test_profile_view(self): + """Test the profile detail view.""" + + url = reverse('profiles:profile', args=[self.user.username]) + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'profiles/profile.html') + self.assertEqual(response.context['profile'], self.profile) + + def test_profile_view_404(self): + """Test the profile detail view with non-existent username.""" + + url = reverse('profiles:profile', args=['nonexistentuser']) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) diff --git a/profiles/urls.py b/profiles/urls.py new file mode 100644 index 0000000000..9a6d0ee8e3 --- /dev/null +++ b/profiles/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from . import views + +app_name = 'profiles' + +urlpatterns = [ + path('', views.index, name='index'), + path('/', views.profile, name='profile'), +] diff --git a/profiles/views.py b/profiles/views.py new file mode 100644 index 0000000000..2eb48c33cb --- /dev/null +++ b/profiles/views.py @@ -0,0 +1,44 @@ +from django.shortcuts import render, get_object_or_404 +from django.http import Http404 +import logging +from .models import Profile + +logger = logging.getLogger(__name__) + +def index(request): + """ + View function for the profiles list page. + Args: + request: The HTTP request object + Returns: + Rendered template 'profiles/index.html' with a list of all profiles + """ + try: + profiles_list = Profile.objects.all() + logger.info(f"Retrieved {len(profiles_list)} profiles for index page") + return render(request, 'profiles/index.html', context={'profiles_list': profiles_list}) + except Exception as e: + logger.error(f"Error retrieving profiles list: {str(e)}", exc_info=True) + raise + +def profile(request, username): + """ + View function for displaying a specific user's profile. + Args: + request: The HTTP request object + username: The username of the profile to display + Returns: + Rendered template 'profiles/profile.html' with the requested profile + Raises: + Http404: If the profile with the given username doesn't exist + """ + try: + profile = get_object_or_404(Profile, user__username=username) + logger.info(f"Retrieved profile for user {username}") + return render(request, 'profiles/profile.html', context={'profile': profile}) + except Http404: + logger.warning(f"Profile for user {username} not found") + raise + except Exception as e: + logger.error(f"Error retrieving profile for user {username}: {str(e)}", exc_info=True) + raise diff --git a/requirements.txt b/requirements.txt index c48c84ea40..13e02d0e33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -django==3.0 +django==4.2.11 flake8==3.7.0 pytest-django==3.9.0 \ No newline at end of file