From bc9eae51e50f72e3b602c26a475171b7082e5924 Mon Sep 17 00:00:00 2001 From: Votre Nom Date: Mon, 14 Apr 2025 14:54:16 +0200 Subject: [PATCH 1/6] Profiles migration done --- oc-lettings-site.sqlite3 | Bin 151552 -> 151552 bytes oc_lettings_site/admin.py | 2 -- .../migrations/0002_delete_profile.py | 23 ++++++++++++++++ oc_lettings_site/models.py | 9 ------ oc_lettings_site/settings.py | 1 + oc_lettings_site/views.py | 5 ++-- profiles/__init__.py | 0 profiles/admin.py | 6 ++++ profiles/apps.py | 5 ++++ profiles/migrations/0001_initial.py | 26 ++++++++++++++++++ profiles/migrations/__init__.py | 0 profiles/models.py | 11 ++++++++ profiles/tests.py | 0 profiles/views.py | 3 ++ 14 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 oc_lettings_site/migrations/0002_delete_profile.py create mode 100644 profiles/__init__.py create mode 100644 profiles/admin.py create mode 100644 profiles/apps.py create mode 100644 profiles/migrations/0001_initial.py create mode 100644 profiles/migrations/__init__.py create mode 100644 profiles/models.py create mode 100644 profiles/tests.py create mode 100644 profiles/views.py diff --git a/oc-lettings-site.sqlite3 b/oc-lettings-site.sqlite3 index 3d885414f9f3ed046704e8b09bdcd5c791c82d42..117363f1381409ed3a24d6135880d8ae11147281 100644 GIT binary patch delta 816 zcmZutO-K}B7@p6W-I@LN=k;e67napomvKjQbbeM%32ha_g}|u2bdYUzM;%(%*xl?B zQ(a?3|LQ#}w+$qVk4?Inr@R6_wv1=^?gZ;1L^ZiGpUG?GvR+s$ zWlDNklO##W%fjRf=EKyQS__Ldkr38+FDHedl#a12l2HFQG4GDrxrR3v<=Z}h=tVthY@p@V=U z*n=H-2bb4$EkQU>l~0NZ6U%$fi2;h7^29CVe@@_&LAV_rrH&8JoVDRtoV5 zuj<-u$18!QP!GcY8W{gW&M##Pi>I!3w@@gCa5k57w0upOeOn|22%7YMih(#pAq+vw zcpeD3NQAI(65}Va`y}!;@$mgx4~i1_yl02`$h5k@xmnjT`I5ZfR3opUXyB)J%fA$; z_=^5s??J`*UXNzco`(v2A!-%lseAmKyZx$Im6uT{=Irf!C?lU`xcb2jM3a6$1lMg05uH}H4!hwz*6OKfcX$0sTyz--HypB$f)T2hjkmtGuSoLQ1OSyw-n zk#lmlz6Ybqk&$j)ruByG+vZfncf>{*hSl#_}}GrIV6#e7Eb>G_F_g4-SQ84o8gt8mSm T&VGRr=p8? Date: Mon, 14 Apr 2025 15:12:36 +0200 Subject: [PATCH 2/6] lettings migrations done. --- .coverage | Bin 0 -> 53248 bytes lettings/__init__.py | 0 lettings/admin.py | 8 ++++ lettings/apps.py | 5 +++ lettings/migrations/0001_initial.py | 41 ++++++++++++++++++ lettings/migrations/__init__.py | 0 lettings/models.py | 22 ++++++++++ lettings/tests.py | 0 lettings/views.py | 3 ++ oc-lettings-site.sqlite3 | Bin 151552 -> 151552 bytes oc_lettings_site/admin.py | 7 --- .../migrations/0003_auto_20250414_1300.py | 20 +++++++++ oc_lettings_site/models.py | 21 --------- oc_lettings_site/settings.py | 1 + oc_lettings_site/views.py | 2 +- 15 files changed, 101 insertions(+), 29 deletions(-) create mode 100644 .coverage create mode 100644 lettings/__init__.py create mode 100644 lettings/admin.py create mode 100644 lettings/apps.py create mode 100644 lettings/migrations/0001_initial.py create mode 100644 lettings/migrations/__init__.py create mode 100644 lettings/models.py create mode 100644 lettings/tests.py create mode 100644 lettings/views.py create mode 100644 oc_lettings_site/migrations/0003_auto_20250414_1300.py diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..dc7ee317cf2da3053b38cc53b78a6c81dc34d0e0 GIT binary patch literal 53248 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 Date: Mon, 14 Apr 2025 15:30:55 +0200 Subject: [PATCH 3/6] Refactoring urls/views/templates. --- .../templates/lettings/index.html | 4 +- .../templates/lettings}/letting.html | 4 +- lettings/tests.py | 15 ++++++ lettings/urls.py | 15 ++++++ lettings/views.py | 40 +++++++++++++- .../templates}/base.html | 4 +- .../templates}/index.html | 4 +- oc_lettings_site/urls.py | 10 ++-- oc_lettings_site/views.py | 53 +++++-------------- .../templates/profiles/index.html | 4 +- .../templates/profiles}/profile.html | 4 +- profiles/urls.py | 14 +++++ profiles/views.py | 36 ++++++++++++- 13 files changed, 148 insertions(+), 59 deletions(-) rename templates/lettings_index.html => lettings/templates/lettings/index.html (89%) rename {templates => lettings/templates/lettings}/letting.html (95%) create mode 100644 lettings/urls.py rename {templates => oc_lettings_site/templates}/base.html (97%) rename {templates => oc_lettings_site/templates}/index.html (92%) rename templates/profiles_index.html => profiles/templates/profiles/index.html (88%) rename {templates => profiles/templates/profiles}/profile.html (96%) create mode 100644 profiles/urls.py diff --git a/templates/lettings_index.html b/lettings/templates/lettings/index.html similarity index 89% rename from templates/lettings_index.html rename to lettings/templates/lettings/index.html index 92857a78d9..a85f3a348e 100644 --- a/templates/lettings_index.html +++ b/lettings/templates/lettings/index.html @@ -20,7 +20,7 @@

Lettings

@@ -36,7 +36,7 @@

Lettings

Home - + Profiles diff --git a/templates/letting.html b/lettings/templates/lettings/letting.html similarity index 95% rename from templates/letting.html rename to lettings/templates/lettings/letting.html index 7e5f3a73fd..252d68035e 100644 --- a/templates/letting.html +++ b/lettings/templates/lettings/letting.html @@ -25,14 +25,14 @@

{{ title }}

diff --git a/lettings/tests.py b/lettings/tests.py index e69de29bb2..cc263c1220 100644 --- a/lettings/tests.py +++ b/lettings/tests.py @@ -0,0 +1,15 @@ +""" +Module for the URL configuration of the lettings application. + +This module maps URL patterns to their corresponding view functions. +""" + +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/urls.py b/lettings/urls.py new file mode 100644 index 0000000000..08dee713d9 --- /dev/null +++ b/lettings/urls.py @@ -0,0 +1,15 @@ +""" +Module for the URL configuration of the lettings application. + +This module maps URL patterns to their corresponding view functions. +""" + +from django.urls import path +from . import views + +app_name = 'lettings' + +urlpatterns = [ + path('', views.index, name='index'), + path('/', views.letting, name='letting'), +] \ No newline at end of file diff --git a/lettings/views.py b/lettings/views.py index 91ea44a218..b95859a12e 100644 --- a/lettings/views.py +++ b/lettings/views.py @@ -1,3 +1,41 @@ +""" +Views for handling lettings display. +""" + from django.shortcuts import render +from lettings.models import Letting + + +def index(request): + """Display a list of lettings. + + Retrieves all Letting objects and renders the 'lettings/index.html' template. + + :param request: The HTTP request object. + :type request: HttpRequest + :return: Rendered template with the list of lettings. + :rtype: HttpResponse + """ + lettings_list = Letting.objects.all() + context = {'lettings_list': lettings_list} + return render(request, 'lettings/index.html', context) + + +def letting(request, letting_id): + """Display details for a specific letting. + + Retrieves a Letting object by its ID and renders the 'lettings/letting.html' template with its details. -# Create your views here. + :param request: The HTTP request object. + :type request: HttpRequest + :param letting_id: The unique identifier of the letting to display. + :type letting_id: int + :return: Rendered template with details for the specified letting. + :rtype: HttpResponse + """ + letting = Letting.objects.get(id=letting_id) + context = { + 'title': letting.title, + 'address': letting.address, + } + return render(request, 'lettings/letting.html', context) \ No newline at end of file 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..403b342755 100644 --- a/templates/base.html +++ b/oc_lettings_site/templates/base.html @@ -24,10 +24,10 @@
Logo Orange County Lettings diff --git a/templates/index.html b/oc_lettings_site/templates/index.html similarity index 92% rename from templates/index.html rename to oc_lettings_site/templates/index.html index 71a8e61a46..fc9a76c7ab 100644 --- a/templates/index.html +++ b/oc_lettings_site/templates/index.html @@ -14,10 +14,10 @@

Welcome to Holiday Homes

diff --git a/oc_lettings_site/urls.py b/oc_lettings_site/urls.py index f0ff5897ab..5c91d408ea 100644 --- a/oc_lettings_site/urls.py +++ b/oc_lettings_site/urls.py @@ -1,13 +1,11 @@ from django.contrib import admin -from django.urls import path - +from django.urls import path, include from . import views + 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')), + path('profiles/', include('profiles.urls')), path('admin/', admin.site.urls), ] diff --git a/oc_lettings_site/views.py b/oc_lettings_site/views.py index 56fd987ec4..4af6a18bf2 100644 --- a/oc_lettings_site/views.py +++ b/oc_lettings_site/views.py @@ -1,44 +1,19 @@ -from django.shortcuts import render -from lettings.models import Letting -from profiles.models import Profile - - -# 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): - return render(request, 'index.html') +""" +Module for the oc_lettings_site views. -# 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) +This module contains view functions for the oc_lettings_site project. +""" +from django.shortcuts import render -#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) +def index(request): + """ + Render the homepage. -# 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) + :param request: The HTTP request object. + :type request: HttpRequest + :return: The rendered homepage. + :rtype: HttpResponse + """ + return render(request, 'index.html') diff --git a/templates/profiles_index.html b/profiles/templates/profiles/index.html similarity index 88% rename from templates/profiles_index.html rename to profiles/templates/profiles/index.html index 4ad1daf92f..563b7a0166 100644 --- a/templates/profiles_index.html +++ b/profiles/templates/profiles/index.html @@ -18,7 +18,7 @@

Profiles

@@ -34,7 +34,7 @@

Profiles

Home - + Lettings
diff --git a/templates/profile.html b/profiles/templates/profiles/profile.html similarity index 96% rename from templates/profile.html rename to profiles/templates/profiles/profile.html index d150d30e63..4b1af37496 100644 --- a/templates/profile.html +++ b/profiles/templates/profiles/profile.html @@ -24,14 +24,14 @@

{{ profile.user.username }}

diff --git a/profiles/urls.py b/profiles/urls.py new file mode 100644 index 0000000000..0a3b5b5d1f --- /dev/null +++ b/profiles/urls.py @@ -0,0 +1,14 @@ +""" +Module for the profiles URL configuration. +This module maps URL patterns to views for the profiles application. +""" + +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 index 91ea44a218..3239ba271b 100644 --- a/profiles/views.py +++ b/profiles/views.py @@ -1,3 +1,37 @@ from django.shortcuts import render +from profiles.models import Profile -# Create your views here. + +def index(request): + """ + Display a list of profiles. + + Retrieves all Profile objects and renders the 'profiles/index.html' template. + + :param request: The HTTP request object. + :type request: HttpRequest + :return: The rendered page with the list of profiles. + :rtype: HttpResponse + """ + profiles_list = Profile.objects.all() + context = {'profiles_list': profiles_list} + return render(request, 'profiles/index.html', context) + + +def profile(request, username): + """ + Display the details of a specific user profile. + + Retrieves the Profile object corresponding to the provided username and + renders the 'profiles/profile.html' template with its details. + + :param request: The HTTP request object. + :type request: HttpRequest + :param username: The username of the profile to display. + :type username: str + :return: The rendered page with the profile details. + :rtype: HttpResponse + """ + profile = Profile.objects.get(user__username=username) + context = {'profile': profile} + return render(request, 'profiles/profile.html', context) From f2fe88bd4f0ab7eab8ec21de8f85d3f46d3dbbb7 Mon Sep 17 00:00:00 2001 From: Votre Nom Date: Sun, 20 Apr 2025 12:55:16 +0200 Subject: [PATCH 4/6] Solved project issues and initialise Sentry. --- .coverage | Bin 53248 -> 53248 bytes .coveragerc | 15 ++++++ .gitignore | 4 ++ lettings/admin.py | 12 +++-- lettings/apps.py | 14 +++++ lettings/models.py | 46 +++++++++++++++- lettings/tests.py | 62 ++++++++++++++++++---- lettings/urls.py | 2 +- lettings/views.py | 78 ++++++++++++++++++---------- manage.py | 15 ++++++ oc_lettings_site/admin.py | 1 - oc_lettings_site/apps.py | 7 +++ oc_lettings_site/asgi.py | 7 +++ oc_lettings_site/models.py | 1 - oc_lettings_site/settings.py | 49 +++++++++++++++-- oc_lettings_site/templates/404.html | 16 ++++++ oc_lettings_site/templates/500.html | 16 ++++++ oc_lettings_site/tests.py | 33 +++++++++++- oc_lettings_site/urls.py | 2 + oc_lettings_site/views.py | 48 +++++++++++++---- oc_lettings_site/wsgi.py | 8 ++- profiles/admin.py | 11 ++-- profiles/apps.py | 9 ++++ profiles/models.py | 23 +++++++- profiles/tests.py | 55 ++++++++++++++++++++ profiles/views.py | 37 ++++++++++--- requirements.txt | 9 +++- 27 files changed, 509 insertions(+), 71 deletions(-) create mode 100644 .coveragerc delete mode 100644 oc_lettings_site/admin.py delete mode 100644 oc_lettings_site/models.py create mode 100644 oc_lettings_site/templates/404.html create mode 100644 oc_lettings_site/templates/500.html diff --git a/.coverage b/.coverage index dc7ee317cf2da3053b38cc53b78a6c81dc34d0e0..2d7ec6eb6849859c15653f7d5dcfc4166294a264 100644 GIT binary patch delta 589 zcmX|3uyD#_Mop*2lh}Z}kBG!K;b%;rDN$l1(S{$o3q0k@!jm9>Lw8b>) z;vke81ReZF-6}D-s8k&Ur*sn?tPTzq(ZNjw?`pp;$MgQ~z31NNxMoP2Az7h?xK#59 zUcfCFgFgLHKhTppslKbWns4{^OerrT&FkU;(>=x2wQA-1Z0gF5in@K1IGv7go|)d!0*z6$WR}a@IOE&yET-GO(=(TErWS0Fk}@aH z54sdPq7*Vi>SnDp6l{eMwqtlJLn$56z&t&dg;u01SY`{8n;T^n!4m^Pn4BUl9J8cS1*TG%Pep-Xf*`0Vi#-CLx{7OgR$B^<~!kgdl+?sdQuZB_LA1X z>ToyR^fC8rz0+$AtQ7h&<^0H4;tmSJd~+Q?PpZcvzWJ)qLGTBD!w>idpWy?n!FzaX QKTOBYc^}%HN8`u-0aZw-BLDyZ delta 2014 zcma)+TWDNW6o${4vuDrjb6aQbH!*Q~!;U1A$*ql=q$yF8n#Agh+UhW*W1NyqlFpe@SVjuTCaPv1Uk|7)$i z*Zw*4i^}|>a+>sZB^XHX1w0QAfY|wX#_h^!QOArsL!c`7qSL+eO#U>9DZJmWVSmry10m zCGT+dHmG3o3IRuzJ1|iw^3xmQ^!8_uZXeFDL!Fu;&G09yPLBkLy84lViAkIUzi=T&go zx=s7V_yGpAGi)!j%uD+3=IiWR?V$0lmO(Fn=_S1c-|FOWn_)XS(L*XSM((EZK~jmx zyygEfbIew!8tPu?UyhGHLD0+CZu?rz240RlOwh_GzeUOLV+5UynyqU~uH)oTr&8kg zNjb(0-bX4RLW#QUx~T`8`Pi294->R9_S+!CS8I~|ubVBqTXM5wm-X9%T_o%XZM&NG zB^(1A&KYfuzC)Q25BJX*96M{Zy_D&M)LjG(j#sh`{-k4YZDN{!iyoCr)QfJ)_!g6?m(!b}9!`%;DJRWPH>X>sgp*`~3Ua$L*mzv>F80iS zC!aOmQRCD_B)Q2e*/', views.letting, name='letting'), -] +class TestLettings(TestCase): + """Test class for lettings app.""" + + def setUp(self): + """Set up test data.""" + self.address = Address.objects.create( + number=123, + street="Test Street", + city="Test City", + state="TS", + zip_code=12345, + country_iso_code="TST" + ) + self.letting = Letting.objects.create( + title="Test Letting", + address=self.address + ) + + def test_address_str_method(self): + """Test the string representation of Address model.""" + assert str(self.address) == "123 Test Street" + + def test_letting_str_method(self): + """Test the string representation of Letting model.""" + assert str(self.letting) == "Test Letting" + + def test_lettings_index_view(self): + """Test the index view of lettings app.""" + url = reverse('lettings:index') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertIn("Lettings", str(response.content)) + self.assertEqual(response.templates[0].name, 'lettings/index.html') + + def test_letting_detail_view(self): + """Test the detail view of a letting.""" + url = reverse('lettings:letting', kwargs={'letting_id': self.letting.id}) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertIn(self.letting.title, str(response.content)) + self.assertEqual(response.templates[0].name, 'lettings/letting.html') + + def test_letting_detail_view_404(self): + """Test the detail view with invalid letting id.""" + url = reverse('lettings:letting', kwargs={'letting_id': 999}) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + self.assertTemplateNotUsed(response, 'lettings/letting.html') diff --git a/lettings/urls.py b/lettings/urls.py index 08dee713d9..cc263c1220 100644 --- a/lettings/urls.py +++ b/lettings/urls.py @@ -12,4 +12,4 @@ urlpatterns = [ path('', views.index, name='index'), path('/', views.letting, name='letting'), -] \ No newline at end of file +] diff --git a/lettings/views.py b/lettings/views.py index b95859a12e..80970101d4 100644 --- a/lettings/views.py +++ b/lettings/views.py @@ -1,41 +1,67 @@ -""" -Views for handling lettings display. -""" +"""Views for lettings app.""" +import logging from django.shortcuts import render -from lettings.models import Letting +from django.http import Http404 +from .models import Letting + +logger = logging.getLogger(__name__) def index(request): - """Display a list of lettings. + """ + Display the list of all lettings. - Retrieves all Letting objects and renders the 'lettings/index.html' template. + Retrieves all Letting objects from the database and renders them in the index template. - :param request: The HTTP request object. - :type request: HttpRequest - :return: Rendered template with the list of lettings. - :rtype: HttpResponse + :param request: The HTTP request object + :type request: django.http.HttpRequest + :return: Rendered template with the list of lettings + :rtype: django.http.HttpResponse + :raises Exception: If there is an error retrieving the lettings list """ - lettings_list = Letting.objects.all() - context = {'lettings_list': lettings_list} - return render(request, 'lettings/index.html', context) + try: + logger.info('Accessing lettings index page') + lettings_list = Letting.objects.all() + logger.debug(f'Found {len(lettings_list)} lettings') + context = {'lettings_list': lettings_list} + return render(request, 'lettings/index.html', context) + except Exception as e: + logger.error(f'Error retrieving lettings list: {str(e)}', exc_info=True) + raise def letting(request, letting_id): - """Display details for a specific letting. + """ + Display details for a specific letting. - Retrieves a Letting object by its ID and renders the 'lettings/letting.html' template with its details. + Retrieves a Letting object by its ID and renders the letting detail template. - :param request: The HTTP request object. - :type request: HttpRequest - :param letting_id: The unique identifier of the letting to display. + :param request: The HTTP request object + :type request: django.http.HttpRequest + :param letting_id: The ID of the letting to display :type letting_id: int - :return: Rendered template with details for the specified letting. - :rtype: HttpResponse + :return: Rendered template with letting details + :rtype: django.http.HttpResponse + :raises Http404: If no letting matches the provided ID + :raises Exception: For any other errors """ - letting = Letting.objects.get(id=letting_id) - context = { - 'title': letting.title, - 'address': letting.address, - } - return render(request, 'lettings/letting.html', context) \ No newline at end of file + try: + logger.info(f'Accessing letting detail page for ID: {letting_id}') + letting = Letting.objects.get(id=letting_id) + logger.debug(f'Retrieved letting: {letting.title}') + context = { + 'title': letting.title, + 'letting': letting, + 'address': letting.address + } + return render(request, 'lettings/letting.html', context) + except Letting.DoesNotExist: + logger.warning(f'Attempted to access non-existent letting with ID: {letting_id}') + raise Http404("Letting does not exist") + except Exception as e: + logger.error( + f'Error retrieving letting details for ID {letting_id}: {str(e)}', + exc_info=True + ) + raise diff --git a/manage.py b/manage.py index c0e27e034a..87332f6b05 100755 --- a/manage.py +++ b/manage.py @@ -1,8 +1,23 @@ +""" +Django's command-line utility for administrative tasks. + +This module contains the main entry point for executing Django administrative commands. +It handles setting up the Django environment and running management commands. +""" + import os import sys def main(): + """ + Sets up the Django environment by configuring the default settings module + and executes the requested management command. + + + :raises ImportError: If Django cannot be imported, likely due to not being installed + or the virtual environment not being activated. + """ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oc_lettings_site.settings') try: from django.core.management import execute_from_command_line diff --git a/oc_lettings_site/admin.py b/oc_lettings_site/admin.py deleted file mode 100644 index 8b13789179..0000000000 --- a/oc_lettings_site/admin.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/oc_lettings_site/apps.py b/oc_lettings_site/apps.py index 6489692f04..fdfbee3f5f 100644 --- a/oc_lettings_site/apps.py +++ b/oc_lettings_site/apps.py @@ -1,3 +1,10 @@ +""" +App configuration for main project app. + +This module defines the OCLettingsSiteConfig class which serves as the main +application configuration for the Django project settings. +""" + from django.apps import AppConfig diff --git a/oc_lettings_site/asgi.py b/oc_lettings_site/asgi.py index 61f2d23ba3..d1233f86bd 100644 --- a/oc_lettings_site/asgi.py +++ b/oc_lettings_site/asgi.py @@ -1,3 +1,10 @@ +""" +ASGI config for oc_lettings_site project. + +This module exposes the ASGI callable as a module-level variable named 'application'. +It is used by Django's development server and any production ASGI deployments. +""" + import os from django.core.asgi import get_asgi_application diff --git a/oc_lettings_site/models.py b/oc_lettings_site/models.py deleted file mode 100644 index 8b13789179..0000000000 --- a/oc_lettings_site/models.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/oc_lettings_site/settings.py b/oc_lettings_site/settings.py index 34eb0b8a17..05b3e51619 100644 --- a/oc_lettings_site/settings.py +++ b/oc_lettings_site/settings.py @@ -1,6 +1,11 @@ import os from pathlib import Path +import sentry_sdk +from sentry_sdk.integrations.django import DjangoIntegration +from dotenv import load_dotenv + +load_dotenv() # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = Path(__file__).resolve().parent.parent @@ -10,12 +15,12 @@ # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'fp$9^593hsriajg$_%=5trot9g!1qa@ew(o-1#@=&4%=hp46(s' +SECRET_KEY = os.getenv('SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['localhost', '0.0.0.0'] # Application definition @@ -113,4 +118,42 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') STATIC_URL = '/static/' -STATICFILES_DIRS = [BASE_DIR / "static",] +STATICFILES_DIRS = [BASE_DIR / "static", ] + +sentry_sdk.init( + dsn=os.getenv("SENTRY_DSN"), + integrations=[DjangoIntegration()], + + traces_sample_rate=1.0, + + enable_tracing=True, + + send_default_pii=True +) + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '{levelname} {asctime} {module} {message}', + 'style': '{', + } + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'verbose' + }, + 'sentry': { + 'level': 'WARNING', + 'class': 'sentry_sdk.integrations.logging.EventHandler', + }, + }, + 'loggers': { + '': { + 'handlers': ['console', 'sentry'], + 'level': 'INFO', + }, + }, +} diff --git a/oc_lettings_site/templates/404.html b/oc_lettings_site/templates/404.html new file mode 100644 index 0000000000..24bedac4e6 --- /dev/null +++ b/oc_lettings_site/templates/404.html @@ -0,0 +1,16 @@ +{% load static %} + + + + + 404 - Page Not Found + + + +
+

404

+

Oops! The page you are looking for does not exist.

+ Back to Home +
+ + \ No newline at end of file diff --git a/oc_lettings_site/templates/500.html b/oc_lettings_site/templates/500.html new file mode 100644 index 0000000000..f461c44155 --- /dev/null +++ b/oc_lettings_site/templates/500.html @@ -0,0 +1,16 @@ +{% load static %} + + + + + 500 - Internal Server Error + + + +
+

500

+

Sorry, something went wrong on our end.

+ Back to Home +
+ + \ No newline at end of file diff --git a/oc_lettings_site/tests.py b/oc_lettings_site/tests.py index 3fd62bb718..fb9f78e99a 100644 --- a/oc_lettings_site/tests.py +++ b/oc_lettings_site/tests.py @@ -1,2 +1,31 @@ -def test_dummy(): - assert 1 +""" +Unit tests for the oc_lettings_site app. + +This module contains test cases for views and urls of the main application. +""" + +from django.test import TestCase +from django.urls import reverse + + +class TestOCLettingsSite(TestCase): + """Test class for oc_lettings_site app.""" + + def test_index_view(self): + """Test the main index view.""" + url = reverse('index') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'index.html') + self.assertIn('Welcome', str(response.content)) + + def test_error_404_view(self): + """Test the 404 error view.""" + response = self.client.get('/nonexistent-page/') + self.assertEqual(response.status_code, 404) + self.assertTemplateUsed(response, '404.html') + + def test_error_500_view(self): + """Test the 500 error view.""" + with self.assertRaises(Exception): + self.client.get(reverse('error_500')) diff --git a/oc_lettings_site/urls.py b/oc_lettings_site/urls.py index 5c91d408ea..c89a50bc35 100644 --- a/oc_lettings_site/urls.py +++ b/oc_lettings_site/urls.py @@ -2,6 +2,8 @@ from django.urls import path, include from . import views +handler404 = 'oc_lettings_site.views.error_404' +handler500 = 'oc_lettings_site.views.error_500' urlpatterns = [ path('', views.index, name='index'), diff --git a/oc_lettings_site/views.py b/oc_lettings_site/views.py index 4af6a18bf2..6fdc868c8e 100644 --- a/oc_lettings_site/views.py +++ b/oc_lettings_site/views.py @@ -1,19 +1,49 @@ -""" -Module for the oc_lettings_site views. - -This module contains view functions for the oc_lettings_site project. -""" - from django.shortcuts import render +import logging + +logger = logging.getLogger(__name__) def index(request): """ - Render the homepage. + Renders the main index page. + + :param request: The HTTP request object. + :type request: HttpRequest + :return: The rendered index page. + :rtype: HttpResponse + """ + try: + logger.info('Accessing main index page') + return render(request, 'index.html') + except Exception as e: + logger.error(f'Error rendering index page: {str(e)}', exc_info=True) + raise + + +def error_404(request, exception): + """ + Render the 404 error page when a page is not found. + + :param request: The HTTP request object. + :type request: HttpRequest + :param exception: The exception raised when the requested page is not found. + :type exception: Exception + :return: The rendered 404 error page. + :rtype: HttpResponse + """ + logger.warning(f"404 error: Page not found - {request.path}", exc_info=True) + return render(request, '404.html', status=404) + + +def error_500(request): + """ + Render the 500 error page when a server error occurs. :param request: The HTTP request object. :type request: HttpRequest - :return: The rendered homepage. + :return: The rendered 500 error page. :rtype: HttpResponse """ - return render(request, 'index.html') + logger.error("500 error: Server error occurred", exc_info=True) + return render(request, '500.html', status=500) diff --git a/oc_lettings_site/wsgi.py b/oc_lettings_site/wsgi.py index d78ca6d669..7d69ccec7c 100644 --- a/oc_lettings_site/wsgi.py +++ b/oc_lettings_site/wsgi.py @@ -1,5 +1,11 @@ -import os +""" +WSGI config for oc_lettings_site project. + +This module exposes the WSGI callable as a module-level variable named ``application``. +It is used by Django's development server and any production WSGI deployments. +""" +import os from django.core.wsgi import get_wsgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oc_lettings_site.settings') diff --git a/profiles/admin.py b/profiles/admin.py index 8e5abd6b6f..05eb20c23b 100644 --- a/profiles/admin.py +++ b/profiles/admin.py @@ -1,6 +1,11 @@ -from django.contrib import admin +""" +Admin configuration for profiles app. -from .models import Profile +This module registers the Profile model with the Django admin interface +to allow management of user profiles through the administration panel. +""" +from django.contrib import admin +from .models import Profile -admin.site.register(Profile) \ No newline at end of file +admin.site.register(Profile) diff --git a/profiles/apps.py b/profiles/apps.py index 5501fdad35..2bb0a2b4ee 100644 --- a/profiles/apps.py +++ b/profiles/apps.py @@ -1,5 +1,14 @@ +""" +Module for the profiles application configuration. +This module contains the configuration class for the profiles app. +""" + from django.apps import AppConfig class ProfilesConfig(AppConfig): + """ + :param AppConfig: Django application's configuration base class. + :type AppConfig: django.apps.AppConfig + """ name = 'profiles' diff --git a/profiles/models.py b/profiles/models.py index c7d43f4f51..f3c9967a51 100644 --- a/profiles/models.py +++ b/profiles/models.py @@ -1,11 +1,30 @@ -from django.db import models +""" +Module for the Profile model. + +This module defines the Profile model used to store additional user information. +""" +from django.db import models from django.contrib.auth.models import User class Profile(models.Model): + """ + Profile model with user informations. + + :param models: Django's ORM models module. + :type models: django.db.models + :return: Instance of Profile representing the user's profile data. + :rtype: Profile + """ user = models.OneToOneField(User, on_delete=models.CASCADE) favorite_city = models.CharField(max_length=64, blank=True) def __str__(self): - return self.user.username \ No newline at end of file + """ + Return the username of the associated User instance. + + :return: The username of the user. + :rtype: str + """ + return self.user.username diff --git a/profiles/tests.py b/profiles/tests.py index e69de29bb2..76f801ce24 100644 --- a/profiles/tests.py +++ b/profiles/tests.py @@ -0,0 +1,55 @@ +""" +Unit tests for the profiles app. + +This module contains test cases for models, views and urls of the profiles application. +""" + +from django.test import TestCase +from django.urls import reverse +from django.contrib.auth.models import User +from .models import Profile + + +class TestProfiles(TestCase): + """Test class for profiles app.""" + + def setUp(self): + """Set up test data.""" + self.user = User.objects.create_user( + username="test_user", + first_name="Test", + last_name="User", + email="test@test.com", + password="test_password" + ) + self.profile = Profile.objects.create( + user=self.user, + favorite_city="Test City" + ) + + def test_profile_str_method(self): + """Test the string representation of Profile model.""" + self.assertEqual(str(self.profile), self.user.username) + + def test_profiles_index_view(self): + """Test the index view of profiles app.""" + url = reverse('profiles:index') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertIn("Profiles", str(response.content)) + self.assertEqual(response.templates[0].name, 'profiles/index.html') + + def test_profile_detail_view(self): + """Test the detail view of a profile.""" + url = reverse('profiles:profile', kwargs={'username': self.user.username}) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertIn(self.user.username, str(response.content)) + self.assertEqual(response.templates[0].name, 'profiles/profile.html') + + def test_profile_detail_view_404(self): + """Test the detail view with invalid username.""" + url = reverse('profiles:profile', kwargs={'username': 'invalid_user'}) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + self.assertTemplateNotUsed(response, 'profiles/profile.html') diff --git a/profiles/views.py b/profiles/views.py index 3239ba271b..b93e666018 100644 --- a/profiles/views.py +++ b/profiles/views.py @@ -1,5 +1,9 @@ +import logging from django.shortcuts import render -from profiles.models import Profile +from django.http import Http404 +from .models import Profile + +logger = logging.getLogger(__name__) def index(request): @@ -13,9 +17,15 @@ def index(request): :return: The rendered page with the list of profiles. :rtype: HttpResponse """ - profiles_list = Profile.objects.all() - context = {'profiles_list': profiles_list} - return render(request, 'profiles/index.html', context) + try: + logger.info('Accessing profiles index page') + profiles_list = Profile.objects.all() + logger.debug(f'Found {len(profiles_list)} profiles') + context = {'profiles_list': profiles_list} + return render(request, 'profiles/index.html', context) + except Exception as e: + logger.error(f'Error retrieving profiles list: {str(e)}', exc_info=True) + raise def profile(request, username): @@ -31,7 +41,20 @@ def profile(request, username): :type username: str :return: The rendered page with the profile details. :rtype: HttpResponse + :raises Http404: If no profile matches the provided username. """ - profile = Profile.objects.get(user__username=username) - context = {'profile': profile} - return render(request, 'profiles/profile.html', context) + try: + logger.info(f'Accessing profile detail page for username: {username}') + profile = Profile.objects.get(user__username=username) + logger.debug(f'Retrieved profile for user: {username}') + context = {'profile': profile} + return render(request, 'profiles/profile.html', context) + except Profile.DoesNotExist: + logger.warning(f'Profile not found with username: {username}') + raise Http404("Profile does not exist") + except Exception as e: + logger.error( + f'Error retrieving profile for username {username}: {str(e)}', + exc_info=True + ) + raise diff --git a/requirements.txt b/requirements.txt index c48c84ea40..03c7d16f45 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,10 @@ django==3.0 flake8==3.7.0 -pytest-django==3.9.0 \ No newline at end of file +pytest-django==3.9.0 +sentry-sdk==2.26.1 +setuptools==75.3.2 +six==1.17.0 +coverage==7.6.1 +python-dotenv==1.0.1 +gunicorn==23.0.0 +whitenoise==6.7.0 \ No newline at end of file From cd179a2780081f67f3b0df5cee6a51c3c9c5fbe1 Mon Sep 17 00:00:00 2001 From: muumz <108881756+mumz0@users.noreply.github.com> Date: Fri, 25 Apr 2025 17:15:19 +0200 Subject: [PATCH 5/6] Create ci.yml --- .github/workflows/CI/ci.yml | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/CI/ci.yml diff --git a/.github/workflows/CI/ci.yml b/.github/workflows/CI/ci.yml new file mode 100644 index 0000000000..62a26bafb4 --- /dev/null +++ b/.github/workflows/CI/ci.yml @@ -0,0 +1,44 @@ +name: Django CI + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: [3.7, 3.8, 3.9] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Lint with flake8 + run: | + flake8 + + - name: Run tests + env: + DJANGO_SETTINGS_MODULE: oc_lettings_site.settings + run: | + pytest + + - name: Collect static files + run: | + python manage.py collectstatic --noinput + + - name: Build Docker image + run: docker build -t oc-lettings-site . From 5bca194337179bc8d5d2ec8006436ef4a984f61f Mon Sep 17 00:00:00 2001 From: Votre Nom Date: Fri, 25 Apr 2025 17:20:17 +0200 Subject: [PATCH 6/6] Add dockerfile, change debug flag. Test to run the CI pipeline. --- .coverage | Bin 53248 -> 0 bytes Dockerfile | 15 +++++++++++++++ oc_lettings_site/settings.py | 8 ++++++-- static/css/styles.css | 30 ------------------------------ 4 files changed, 21 insertions(+), 32 deletions(-) delete mode 100644 .coverage create mode 100644 Dockerfile diff --git a/.coverage b/.coverage deleted file mode 100644 index 2d7ec6eb6849859c15653f7d5dcfc4166294a264..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53248 zcmeI4Uu+{s9mjXQw%2RNo`Z0h5S5IIAQzg%jextONZg-tNNrELww#av33qKzk_GQ> zvVU?HprXzdov6eUZ-7uA;SC8+eF0R#0}@Z|8-fQ452&I-@BmNcf#1yTtmDh2=etxY zDc_a7J3BkS`Tc&M`OS>S>-FcKy6h!_uZNNECj8^Z3Bxpv^PC%oQKxs4-g>hsQPwAP zH20+sOV*9mo$oZ+7mS(ecMbNH<`uRw^TXyx8?Vmz&I^qn*{;*43)nya1V8`;{=W%~ zo|>`Sb93h2vx(aqiX?LTBFgs7N1wiQ;p!!R^}@x=mw1-PPt`eXD=Yj0kHTwwOGJF# z8w&0P1F!EUUa-NFO`%Jtu^7mSj`C=#lLc3myi9ey0ZAodgR0nyJl~CW`PanmvMP{k zOT_gg%b-exw-Lw^{8TQ!#3QjTA`$dOoLO2`}3rf`_tZWWazemWwn)+L^ zaY!OloP>u-9wwWFxwBKZ%^liu?C(T#YUHv-GpRK2wo_3w{xTm+E-N>JaG6UdC;~px zsJ1^fYZ?@l;(EM2q|aMjH%&ry-lfLxuF-Dh`S;Z9_QHaBb6W+9#MAA0Nn9kHiJ&R6 z4+$N4{qVSvy<{>-WG@;HdAP2+jFjeC<4`p3?=u|@!;NG|<6&NqEPY3#O&Uu!@ZK=& zO~#svm{?E;p|L<0TTg?&>?*PJp&#yu$lVaD`PFP#c&7u2LTAmXwl6Il7CJ=;bk~Yx zeKc3K+w=40o}{`bPlS+e$vX_A`jRi)IF0l`@>YYSmKcwcx{u^n$ek6t+P*M< zm^*p7?pmIxjqI}BK6%m{Wj#WUOY&Wra6;ycyTEM|!Sv>DxydF!XuHfN!xt;n_NPxC zX1Fw=yC(P58(%IdlW)!``aTW%@@-9+EOW*tKOuHkcHAM2CR?r-QJ1ALIGX{uEI4qf zu-gmMM8&T>RpBY;p0T}I1>G@HIybH(-j#K1>?|sq^yDIk`K#3FbpN7+@4LYupReRJoYhKW zzwDe+Pd12BA^3OW;CAq*i*_#Hv zuz>&wfB*=900@8p2!H?xfB*=900_MM2v}yxwB`E0#Qtosf6xva2!H?xfB*=900@8p z2!H?xfB*=9!0{w7Q?edl`X?R_Gt-)H=RX2CvwC**VfiEf5_{cXud{z2PX}Q&2!H?x zfB*=900@8p2!H?xfB*=9fF>|wJz(l@0ZMgi-pRiNkoW%^&2Jd&7wiUmoV~a8ht~I7 zUu-?t{9E&9l#LApKmY_l00ck)1V8`;KmY_l;OGQSH!LIWY=*w*_-X2Yz4PR5vKa;| zR~}utEE4(eHSx;lp6+Z#p?nlQ?(BHtT1=0)f3j{F+xH-sMnfrf+L=ln{q2rYJyutn zN?jqBt2<|xjgfVCGsAvYi+1VXYeb>(i+#)~8yXGo89ZE>~AOUmjUgn0a7vs`c9E$|LiBD##6}-lM;I9bcJFu4oo5S3YZ{ zQ{>T6Sy$u~1Re%5pfB*=900@8p2!H?xfB*=9!2d1* z%e0K9y#H^q{}}Yb1_B@e0w4eaAOHd&00JNY0w4eaAn-0EU|CH^J^#<1HP{>MHTDYo zE&CO_#eTwGWZz>iP&PIY009sH0T2KI5C8!X009sH0T2LzqY`K|%BJ&y#;UPs{L`}Q zbaJBp>qi=n%ys@!I%~e%wQDlBwwKmzO1$6RE1xnPyDBrQ!Gc|piArhSF3Uu@)M#0h z_>i?X_j2czPdryKof4hDue3M!)AO@t*>OJhIRCa$d-2?JubD43e_a2ddj6l?GT58! zU+nMfuk26k_v}^nJN6s;N5ET0brA9(00JNY0w4eaAOHd&00JNY0w4ea_f0@9|IJ22 z9qM&;a2$20)zraO-w>G9syeGw)S+Bf2g_22QmIT|2dMl1#)