diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000000..7123bd2f3a
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,15 @@
+[run]
+source =
+ lettings
+ profiles
+ oc_lettings_site
+
+omit =
+ */tests/*
+ */tests.py
+ */migrations/*
+ */apps.py
+ manage.py
+ oc_lettings_site/wsgi.py
+ oc_lettings_site/asgi.py
+ */__init__.py
\ No newline at end of file
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 .
diff --git a/.gitignore b/.gitignore
index b4405ebab4..5b9beac8af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,7 @@
**/__pycache__
*.pyc
venv
+.env
+htmlcov
+pytest_cache
+.coverage
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000..75acf6ecbd
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,15 @@
+FROM python:3.8-slim
+
+WORKDIR /app
+
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY . .
+
+ENV DJANGO_SETTINGS_MODULE=oc_lettings_site.settings
+ENV PYTHONUNBUFFERED=1
+
+RUN python manage.py collectstatic --noinput
+
+CMD ["gunicorn", "oc_lettings_site.wsgi:application", "--bind", "0.0.0.0:8000"]
\ 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..1beb00eb83
--- /dev/null
+++ b/lettings/admin.py
@@ -0,0 +1,12 @@
+"""
+Admin configuration for lettings app.
+
+This module registers the Letting and Address models with the Django admin interface
+to allow management of lettings and addresses through the administration panel.
+"""
+
+from django.contrib import admin
+from .models import Letting, Address
+
+admin.site.register(Letting)
+admin.site.register(Address)
diff --git a/lettings/apps.py b/lettings/apps.py
new file mode 100644
index 0000000000..ce1841f6c6
--- /dev/null
+++ b/lettings/apps.py
@@ -0,0 +1,19 @@
+"""
+Module for the lettings application configuration.
+
+This module defines the configuration class for the lettings app.
+"""
+
+from django.apps import AppConfig
+
+
+class LettingsConfig(AppConfig):
+ """
+ Configuration class for the lettings application.
+ This class is used to set up the application and its settings.
+ It inherits from Django's AppConfig base class.
+
+ :param AppConfig: Django's application configuration base class.
+ :type AppConfig: django.apps.AppConfig
+ """
+ name = 'lettings'
diff --git a/lettings/migrations/0001_initial.py b/lettings/migrations/0001_initial.py
new file mode 100644
index 0000000000..a806513804
--- /dev/null
+++ b/lettings/migrations/0001_initial.py
@@ -0,0 +1,41 @@
+# Generated by Django 3.0 on 2025-04-14 12:57
+
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.SeparateDatabaseAndState(
+ state_operations=[
+ migrations.CreateModel(
+ name='Address',
+ fields=[
+ ('id', models.AutoField(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.AutoField(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')),
+ ],
+ ),
+ ],
+ database_operations=[],
+ ),
+ ]
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..c99e7592e3
--- /dev/null
+++ b/lettings/models.py
@@ -0,0 +1,66 @@
+"""
+Module for lettings models.
+
+This module defines the Address and Letting models used to represent lettings data.
+"""
+
+from django.db import models
+from django.core.validators import MaxValueValidator, MinLengthValidator
+
+
+class Address(models.Model):
+ """
+ Represents a physical address.
+
+ This model stores address details such as number, street,
+ city, state, zip code, and country ISO code.
+
+ :param models: Django's ORM models module.
+ :type models: django.db.models
+ :return: An instance of Address representing a physical address.
+ :rtype: Address
+ """
+ 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 a string representation of the address.
+
+ :return: A string combining the number and street.
+ :rtype: str
+ """
+ return f'{self.number} {self.street}'
+
+ class Meta:
+ """
+ This class defines the verbose name and plural form for the Address model.
+ """
+ verbose_name_plural = "Addresses"
+
+
+class Letting(models.Model):
+ """
+ Represents a letting.
+
+ This model stores information about a letting, including its title and the associated address.
+
+ :param models: Django's ORM models module.
+ :type models: django.db.models
+ :return: An instance of Letting representing a letting record.
+ :rtype: Letting
+ """
+ title = models.CharField(max_length=256)
+ address = models.OneToOneField(Address, on_delete=models.CASCADE)
+
+ def __str__(self):
+ """Return the title of the letting.
+
+ :return: The title of the letting.
+ :rtype: str
+ """
+ return self.title
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 @@
@@ -36,7 +36,7 @@
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 @@
diff --git a/lettings/tests.py b/lettings/tests.py
new file mode 100644
index 0000000000..7cc5ebcf9a
--- /dev/null
+++ b/lettings/tests.py
@@ -0,0 +1,59 @@
+"""
+Unit tests for the lettings app.
+
+This module contains test cases for models, views and urls of the lettings application.
+"""
+
+from django.test import TestCase
+from django.urls import reverse
+from .models import Address, 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
new file mode 100644
index 0000000000..cc263c1220
--- /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'),
+]
diff --git a/lettings/views.py b/lettings/views.py
new file mode 100644
index 0000000000..80970101d4
--- /dev/null
+++ b/lettings/views.py
@@ -0,0 +1,67 @@
+"""Views for lettings app."""
+
+import logging
+from django.shortcuts import render
+from django.http import Http404
+from .models import Letting
+
+logger = logging.getLogger(__name__)
+
+
+def index(request):
+ """
+ Display the list of all lettings.
+
+ Retrieves all Letting objects from the database and renders them in the index template.
+
+ :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
+ """
+ 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.
+
+ Retrieves a Letting object by its ID and renders the letting detail template.
+
+ :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 letting details
+ :rtype: django.http.HttpResponse
+ :raises Http404: If no letting matches the provided ID
+ :raises Exception: For any other errors
+ """
+ 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.sqlite3 b/oc-lettings-site.sqlite3
index 3d885414f9..6bf566e9dc 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
deleted file mode 100644
index 63328c6dd3..0000000000
--- a/oc_lettings_site/admin.py
+++ /dev/null
@@ -1,10 +0,0 @@
-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)
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/migrations/0002_delete_profile.py b/oc_lettings_site/migrations/0002_delete_profile.py
new file mode 100644
index 0000000000..57552c45b7
--- /dev/null
+++ b/oc_lettings_site/migrations/0002_delete_profile.py
@@ -0,0 +1,23 @@
+from django.db import migrations
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('oc_lettings_site', '0001_initial'),
+ ('profiles', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.SeparateDatabaseAndState(
+ state_operations=[
+ migrations.DeleteModel(
+ name='Profile',
+ ),
+ ],
+ database_operations=[
+ migrations.AlterModelTable(
+ name='Profile',
+ table='profiles_profile',
+ ),
+ ],
+ )
+ ]
\ No newline at end of file
diff --git a/oc_lettings_site/migrations/0003_auto_20250414_1300.py b/oc_lettings_site/migrations/0003_auto_20250414_1300.py
new file mode 100644
index 0000000000..fe69ca77af
--- /dev/null
+++ b/oc_lettings_site/migrations/0003_auto_20250414_1300.py
@@ -0,0 +1,20 @@
+from django.db import migrations
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('oc_lettings_site', '0002_delete_profile'),
+ ('lettings', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.SeparateDatabaseAndState(
+ state_operations=[
+ migrations.DeleteModel(name='Address'),
+ migrations.DeleteModel(name='Letting'),
+ ],
+ database_operations=[
+ migrations.AlterModelTable(name='Address', table='lettings_address'),
+ migrations.AlterModelTable(name='Letting', table='lettings_letting'),
+ ],
+ ),
+ ]
\ No newline at end of file
diff --git a/oc_lettings_site/models.py b/oc_lettings_site/models.py
deleted file mode 100644
index ed255e8c11..0000000000
--- a/oc_lettings_site/models.py
+++ /dev/null
@@ -1,31 +0,0 @@
-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
diff --git a/oc_lettings_site/settings.py b/oc_lettings_site/settings.py
index a18bee8106..f965650b46 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,18 +15,20 @@
# 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
+DEBUG = False
-ALLOWED_HOSTS = []
+ALLOWED_HOSTS = ['localhost', '127.0.0.1', '0.0.0.0']
# Application definition
INSTALLED_APPS = [
'oc_lettings_site.apps.OCLettingsSiteConfig',
+ 'profiles.apps.ProfilesConfig',
+ 'lettings.apps.LettingsConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@@ -31,6 +38,7 @@
]
MIDDLEWARE = [
+ 'whitenoise.middleware.WhiteNoiseMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
@@ -42,6 +50,9 @@
ROOT_URLCONF = 'oc_lettings_site.urls'
+STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
+WHITENOISE_MANIFEST_STRICT = False
+
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
@@ -111,4 +122,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/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 @@
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 @@
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 f0ff5897ab..c89a50bc35 100644
--- a/oc_lettings_site/urls.py
+++ b/oc_lettings_site/urls.py
@@ -1,13 +1,13 @@
from django.contrib import admin
-from django.urls import path
-
+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'),
- 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 a72db27074..6fdc868c8e 100644
--- a/oc_lettings_site/views.py
+++ b/oc_lettings_site/views.py
@@ -1,45 +1,49 @@
from django.shortcuts import render
-from .models import Letting, Profile
+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):
- 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)
+ """
+ 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 500 error page.
+ :rtype: HttpResponse
+ """
+ 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/__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..05eb20c23b
--- /dev/null
+++ b/profiles/admin.py
@@ -0,0 +1,11 @@
+"""
+Admin configuration for profiles app.
+
+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)
diff --git a/profiles/apps.py b/profiles/apps.py
new file mode 100644
index 0000000000..2bb0a2b4ee
--- /dev/null
+++ b/profiles/apps.py
@@ -0,0 +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/migrations/0001_initial.py b/profiles/migrations/0001_initial.py
new file mode 100644
index 0000000000..85660989f9
--- /dev/null
+++ b/profiles/migrations/0001_initial.py
@@ -0,0 +1,26 @@
+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.SeparateDatabaseAndState(
+ state_operations=[
+ migrations.CreateModel(
+ name='Profile',
+ fields=[
+ ('id', models.AutoField(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)),
+ ],
+ ),
+ ],
+ database_operations=[],
+ ),
+ ]
\ No newline at end of file
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..f3c9967a51
--- /dev/null
+++ b/profiles/models.py
@@ -0,0 +1,30 @@
+"""
+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 the username of the associated User instance.
+
+ :return: The username of the user.
+ :rtype: str
+ """
+ return self.user.username
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 @@
@@ -34,7 +34,7 @@
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 @@
diff --git a/profiles/tests.py b/profiles/tests.py
new file mode 100644
index 0000000000..76f801ce24
--- /dev/null
+++ 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/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
new file mode 100644
index 0000000000..b93e666018
--- /dev/null
+++ b/profiles/views.py
@@ -0,0 +1,60 @@
+import logging
+from django.shortcuts import render
+from django.http import Http404
+from .models import Profile
+
+logger = logging.getLogger(__name__)
+
+
+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
+ """
+ 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):
+ """
+ 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
+ :raises Http404: If no profile matches the provided username.
+ """
+ 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
diff --git a/static/css/styles.css b/static/css/styles.css
index 53c1b18d5f..b81b2be6c5 100644
--- a/static/css/styles.css
+++ b/static/css/styles.css
@@ -18599,14 +18599,6 @@ body {
background-position: center bottom;
}
-.card-waves .card-body {
- background-image: url("../assets/img/backgrounds/bg-waves.svg");
-}
-
-.card-angles .card-body {
- background-image: url("../assets/img/backgrounds/bg-angles.svg");
-}
-
.card-flag {
position: absolute;
font-size: 0.7rem;
@@ -20055,12 +20047,6 @@ section {
cursor: pointer;
}
-.device[data-device=iPhoneX][data-orientation=portrait][data-color=black] {
- padding-bottom: 198.89807163%;
- background-image: url("../assets/img/device-mockups/iPhoneX/portrait.png");
- z-index: initial;
-}
-
.device[data-device=iPhoneX][data-orientation=portrait][data-color=black] .screen {
top: 3.254847645%;
left: 7.162534435%;
@@ -20070,12 +20056,6 @@ section {
z-index: 0;
}
-.device[data-device=iPhoneX][data-orientation=landscape][data-color=black] {
- padding-bottom: 50.27700831%;
- background-image: url("../assets/img/device-mockups/iPhoneX/landscape.png");
- z-index: initial;
-}
-
.device[data-device=iPhoneX][data-orientation=landscape][data-color=black] .screen {
top: 7.162534435%;
left: 3.254847645%;
@@ -20096,11 +20076,6 @@ section {
height: 93.6288088643%;
}
-.device[data-device=iPhoneX][data-orientation=portrait][data-color=black]::after {
- content: "";
- background-image: url("../assets/img/device-mockups/iPhoneX/portrait_black.png");
-}
-
.device[data-device=iPhoneX][data-orientation=portrait][data-color=black] .button {
display: none;
top: 0%;
@@ -20120,11 +20095,6 @@ section {
height: 87.3278236915%;
}
-.device[data-device=iPhoneX][data-orientation=landscape][data-color=black]::after {
- content: "";
- background-image: url("../assets/img/device-mockups/iPhoneX/landscape_black.png");
-}
-
.device[data-device=iPhoneX][data-orientation=landscape][data-color=black] .button {
display: none;
top: 0%;