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 @@
@@ -36,10 +36,10 @@
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 @@
-{% 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 @@
@@ -64,4 +64,4 @@
});