diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..a715c9d790 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.git +.env diff --git a/.github/workflows/oc_lettings.yml b/.github/workflows/oc_lettings.yml new file mode 100644 index 0000000000..f07c0629c8 --- /dev/null +++ b/.github/workflows/oc_lettings.yml @@ -0,0 +1,99 @@ +name: GitHub Actions Integration + +on: + push: + branches: + - '**' + +jobs: + compilations_and_tests: + runs-on: ubuntu-latest + if: github.event_name == 'push' + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Install Python and dependencies + run: | + sudo apt update + sudo apt install python3-pip + pip3 install -r requirements.txt + pip3 install pytest pytest-cov + + - name: Run Tests whit Coverage + run: pytest + + - name: Run Linting + run: flake8 + + containerization_and_push: + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + needs: compilations_and_tests + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up AWS credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Build Docker image to ECR and DockerHub + run: | + cd $(git rev-parse --show-toplevel) && COMMIT_HASH=$(git rev-parse --short HEAD) + docker build -t python-oc-lettings-fr:$COMMIT_HASH -f Dockerfile . + + - name: Push Docker image to ECR + run: | + cd $(git rev-parse --show-toplevel) && COMMIT_HASH=$(git rev-parse --short HEAD) + aws ecr-public get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $ECR_URL_REPOSITORY + docker tag python-oc-lettings-fr:$COMMIT_HASH $ECR_URL_REPOSITORY:$COMMIT_HASH + docker push $ECR_URL_REPOSITORY:$COMMIT_HASH + env: + ECR_URL_REPOSITORY: ${{ secrets.ECR_URL_REPOSITORY }} + AWS_REGION: ${{ secrets.AWS_REGION }} + + - name: Push Docker image to DockerHub and to local + run: | + cd $(git rev-parse --show-toplevel) && COMMIT_HASH=$(git rev-parse --short HEAD) + echo "$DOCKERHUB_PASSWORD" | docker login -u $DOCKERHUB_USERNAME --password-stdin + docker tag python-oc-lettings-fr:$COMMIT_HASH $DOCKERHUB_USERNAME/python-oc-lettings-fr:$COMMIT_HASH + docker push $DOCKERHUB_USERNAME/python-oc-lettings-fr:$COMMIT_HASH + docker pull $DOCKERHUB_USERNAME/python-oc-lettings-fr:$COMMIT_HASH + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} + + + build: + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + needs: containerization_and_push + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up AWS credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Update ECS Task Definition + run: | + cd $(git rev-parse --show-toplevel) && COMMIT_HASH=$(git rev-parse --short HEAD) + TASK_DEFINITION=$(aws ecs describe-task-definition --task-definition lettings-oc-docker --region $AWS_REGION) + NEW_TASK_DEFINITION=$(echo "$TASK_DEFINITION" | jq --arg IMAGE $ECR_URL_REPOSITORY:$COMMIT_HASH '.taskDefinition | .containerDefinitions[0].image = $IMAGE | del(.taskDefinitionArn) | del(.revision) | del(.status) | del(.requiresAttributes) | del(.compatibilities) | del(.registeredAt) | del(.registeredBy)') + NEW_TASK_INFO=$(aws ecs register-task-definition --region $AWS_REGION --cli-input-json "$NEW_TASK_DEFINITION") + NEW_REVISION=$(echo $NEW_TASK_INFO | jq '.taskDefinition.revision') + aws ecs update-service --cluster lettings-fr --service python-oc-lettings-fr --task-definition lettings-oc-docker:$NEW_REVISION --force-new-deployment --region $AWS_REGION + env: + ECR_URL_REPOSITORY: ${{ secrets.ECR_URL_REPOSITORY }} + AWS_REGION: ${{ secrets.AWS_REGION }} diff --git a/.gitignore b/.gitignore index b4405ebab4..ac09184bb2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ **/__pycache__ *.pyc venv +.idea \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..4a781d9357 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.10 + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8000 + +CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] diff --git a/README.md b/README.md index c8547803f7..da89ca4c08 100644 --- a/README.md +++ b/README.md @@ -75,3 +75,51 @@ Utilisation de PowerShell, comme ci-dessus sauf : - Pour activer l'environnement virtuel, `.\venv\Scripts\Activate.ps1` - Remplacer `which ` par `(Get-Command ).Path` + +### Déploiement + +Ce projet utilise GitHub Actions pour automatiser le déploiement de l'application sur ECS + +#### Fonctionnement du déploiement + +1. Lorsqu'un push est créé sur n'importe quelle branche, les jobs suivants sont déclenchées : + - compilations_and_tests : compile et teste le code. +2. Lorsqu'une pull request est créée sur la branche master, les jobs suivants sont déclenchés : + - compilations_and_tests : compile et teste le code. + - containerization_and_push : construit une image Docker, puis la pousse vers ECR et DockerHub. + - build : met à jour la tâche ECS avec la nouvelle image Docker et déploie l'application mise à jour. + +#### Configuration requise + +Pour que le déploiement fonctionne correctement, vous devez disposer des éléments suivants : + +1. Un compte AWS avec les autorisations pour Amazon ECS et Amazon ECR. +2. Un compte Docker Hub pour stocker les images Docker. +3. Les secrets GitHub : +- AWS_ACCESS_KEY_ID : votre clé d'accès AWS. +- AWS_SECRET_ACCESS_KEY : votre clé secrète AWS. +- ECR_URL_REPOSITORY: l'url' de votre repository sur ECR. +- AWS_REGION: la region associé a votre compte AWS. +- DOCKERHUB_PASSWORD : le mot de passe de votre compte Docker Hub. +- DOCKERHUB_USERNAME : l'identifiant de votre compte Docker Hub. +4. Un cluster Amazon ECS avec un service comprenant la définition de tâche lettings-oc-docker. + +#### Étapes de déploiement + +1. Assurez-vous davoir la configuration requise. +2. Créez une nouvelle branche à partir de la branche main. +3. Apportez les modifications souhaitées au code et validez-les dans votre nouvelle branche. +4. Créez une pull request à partir de votre branche vers la branche master. +5. Attendez que les GitHub Actions se terminent et vérifiez que le déploiement a réussi. +6. Une fois le déploiement réussi, vous pouvez fusionner la pull request dans la branche main. + +#### Commande unique de déploiement +Pour déployer votre site en une seule commande, suivez les étapes suivantes : + +1. Assurez-vous de vous trouver dans le dossier racine du projet. +2. Assurez-vous de posséder un compte Docker Hub. +3. Tapez la commande suivante dans le terminal : `./deploy.sh` + +Cette commande construira une image Docker de votre site avec le tag du hash du commit actuel, la poussera vers votre compte Docker Hub et la tirera localement pour que vous puissiez exécuter le site en utilisant l'image Docker. + +Vous disposez maintenant dans votre Docker Hub et localement de l'image de votre site actuel. \ No newline at end of file diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000000..5ef9dd9ff7 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +deploy_docker() { + cd $(git rev-parse --show-toplevel) && COMMIT_HASH=$(git rev-parse --short HEAD) + export DOCKER_USERNAME="$1" + export DOCKER_PASSWORD="$2" + docker build -t python-oc-lettings-fr:$COMMIT_HASH -f Dockerfile . + docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD" + docker push $DOCKER_USERNAME/python-oc-lettings-fr:$COMMIT_HASH + docker pull $DOCKER_USERNAME/python-oc-lettings-fr:$COMMIT_HASH +} + +echo "Entrez votre nom d'utilisateur Docker : " +read DOCKER_USERNAME +echo "Entrez votre mot de passe Docker : " +read -s DOCKER_PASSWORD + +deploy_docker "$DOCKER_USERNAME" "$DOCKER_PASSWORD" + diff --git a/oc_lettings_site/migrations/__init__.py b/lettings/__init__.py similarity index 100% rename from oc_lettings_site/migrations/__init__.py rename to lettings/__init__.py diff --git a/lettings/admin.py b/lettings/admin.py new file mode 100644 index 0000000000..a890de2111 --- /dev/null +++ b/lettings/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from .models import Letting, Address + +"""This code registers the Letting and Address models with the Django admin interface.""" + +admin.site.register(Letting) +admin.site.register(Address) diff --git a/lettings/apps.py b/lettings/apps.py new file mode 100644 index 0000000000..57497321c6 --- /dev/null +++ b/lettings/apps.py @@ -0,0 +1,14 @@ +from django.apps import AppConfig + + +class LettingsConfig(AppConfig): + """Configuration class for the 'lettings' app. + + This class defines configuration settings for the 'lettings' app. + It primarily sets the name of the app. + + Attributes: + name (str): The name of the 'lettings' app. + """ + + name = 'lettings' diff --git a/oc_lettings_site/migrations/0001_initial.py b/lettings/migrations/0001_initial.py similarity index 68% rename from oc_lettings_site/migrations/0001_initial.py rename to lettings/migrations/0001_initial.py index 774cf23f58..a95b8cfbf0 100644 --- a/oc_lettings_site/migrations/0001_initial.py +++ b/lettings/migrations/0001_initial.py @@ -1,6 +1,5 @@ -# Generated by Django 3.0 on 2020-06-14 09:35 +# Generated by Django 3.0 on 2024-03-26 13:33 -from django.conf import settings import django.core.validators from django.db import migrations, models import django.db.models.deletion @@ -11,7 +10,6 @@ class Migration(migrations.Migration): initial = True dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -27,20 +25,12 @@ class Migration(migrations.Migration): ('country_iso_code', models.CharField(max_length=3, validators=[django.core.validators.MinLengthValidator(3)])), ], ), - 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)), - ], - ), 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='oc_lettings_site.Address')), + ('address', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='lettings.Address')), ], ), ] diff --git a/lettings/migrations/0002_auto_20240326_1424.py b/lettings/migrations/0002_auto_20240326_1424.py new file mode 100644 index 0000000000..91b46ced2d --- /dev/null +++ b/lettings/migrations/0002_auto_20240326_1424.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0 on 2024-03-26 14:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('lettings', '0001_initial'), + ] + + operations = [ + migrations.RenameModel( + old_name='Address', + new_name='NewAddress', + ), + migrations.RenameModel( + old_name='Letting', + new_name='NewLetting', + ), + ] diff --git a/lettings/migrations/0003_auto_20240326_1507.py b/lettings/migrations/0003_auto_20240326_1507.py new file mode 100644 index 0000000000..150dc182f6 --- /dev/null +++ b/lettings/migrations/0003_auto_20240326_1507.py @@ -0,0 +1,29 @@ +from django.db import migrations + + +def new_tables(apps, schema_editor): + old_address_table = apps.get_model('oc_lettings_site', 'Address') + new_address_table = apps.get_model('lettings', 'NewAddress') + old_letting_table = apps.get_model('oc_lettings_site', 'Letting') + new_letting_table = apps.get_model('lettings', 'NewLetting') + + old_address_datas = old_address_table.objects.all() + old_letting_datas = old_letting_table.objects.all() + + for data in old_address_datas: + new_letting_data = new_address_table(id=data.id, number=data.number, street=data.street, city=data.city, state=data.state, zip_code=data.zip_code, country_iso_code=data.country_iso_code) + new_letting_data.save() + + for data in old_letting_datas: + new_letting_data = new_letting_table(id=data.id, title=data.title, address_id=data.address_id) + new_letting_data.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('lettings', '0002_auto_20240326_1424'), + ] + + operations = [ + migrations.RunPython(new_tables), + ] diff --git a/lettings/migrations/0004_auto_20240327_1335.py b/lettings/migrations/0004_auto_20240327_1335.py new file mode 100644 index 0000000000..e9b0998457 --- /dev/null +++ b/lettings/migrations/0004_auto_20240327_1335.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0 on 2024-03-27 13:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('lettings', '0003_auto_20240326_1507'), + ] + + operations = [ + migrations.RenameModel( + old_name='NewAddress', + new_name='Address', + ), + migrations.RenameModel( + old_name='NewLetting', + new_name='Letting', + ), + ] diff --git a/lettings/migrations/0005_auto_20240328_1224.py b/lettings/migrations/0005_auto_20240328_1224.py new file mode 100644 index 0000000000..ec4841a4e8 --- /dev/null +++ b/lettings/migrations/0005_auto_20240328_1224.py @@ -0,0 +1,17 @@ +# Generated by Django 3.0 on 2024-03-28 12:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('lettings', '0004_auto_20240327_1335'), + ] + + operations = [ + migrations.AlterModelOptions( + name='address', + options={'verbose_name': 'Address', 'verbose_name_plural': 'Addresses'}, + ), + ] 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..c64c670fd5 --- /dev/null +++ b/lettings/models.py @@ -0,0 +1,54 @@ +from django.core.validators import MaxValueValidator, MinLengthValidator +from django.db import models + + +class Address(models.Model): + """Model representing an address. + + This model stores information about a physical address, + including the street number, street name, city, state, + ZIP code, and country ISO code. + + Attributes: + number (PositiveIntegerField): The street number. + street (CharField): The street name. + city (CharField): The city. + state (CharField): The state abbreviation (2 characters). + zip_code (PositiveIntegerField): The ZIP code. + country_iso_code (CharField): The country ISO code (3 characters). + """ + + 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): + """String representation of the address.""" + return f'{self.number} {self.street}' + + class Meta: + """Meta options for the Address model.""" + verbose_name = 'Address' + verbose_name_plural = 'Addresses' + + +class Letting(models.Model): + """Model representing a letting (rental property). + + This model stores information about a letting, including + the title and the associated address. + + Attributes: + title (CharField): The title of the letting. + address (OneToOneField): The associated address of the letting. + """ + + title = models.CharField(max_length=256) + address = models.OneToOneField(Address, on_delete=models.CASCADE) + + def __str__(self): + """String representation of the letting.""" + return self.title diff --git a/lettings/templates/__init__.py b/lettings/templates/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lettings/templates/lettings/__init__.py b/lettings/templates/lettings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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/__init__.py b/lettings/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lettings/tests/test_urls.py b/lettings/tests/test_urls.py new file mode 100644 index 0000000000..7cd3c37103 --- /dev/null +++ b/lettings/tests/test_urls.py @@ -0,0 +1,28 @@ +import pytest + +from django.urls import reverse, resolve +from lettings.models import Address + + +@pytest.mark.django_db +def test_lettings_url(): + """Test the generation and resolution of the URL for the 'letting' view. + + This test ensures that the URL for the 'letting' view is generated correctly + and that it resolves to the expected view function. + + Database setup: + - Creates an Address instance with specific attributes. + + Assertions: + - Verifies that the generated URL matches '/lettings/1/'. + - Verifies that the resolved view name is 'lettings:letting'. + """ + + Address.objects.create(id=1, number=30, street='Oratoire', city='Marseille', + state='France', zip_code='13009', country_iso_code='FRA') + + path = reverse('lettings:letting', kwargs={'letting_id': 1}) + + assert path == "/lettings/1/" + assert resolve(path).view_name == 'lettings:letting' diff --git a/lettings/tests/test_view.py b/lettings/tests/test_view.py new file mode 100644 index 0000000000..8fb8904c44 --- /dev/null +++ b/lettings/tests/test_view.py @@ -0,0 +1,40 @@ +import pytest + +from django.test import Client +from django.urls import reverse +from lettings.models import Address, Letting + + +@pytest.mark.django_db +def test_lettings_view(): + """Test the 'letting' view functionality. + + This test verifies that the 'letting' view displays the correct content + for a given letting object. + + Database setup: + - Creates an Address instance with specific attributes. + - Creates a Letting instance associated with the created Address. + + Steps: + - Makes a GET request to the 'letting' view. + - Retrieves the response content. + - Checks if the expected content is present in the response. + + Assertions: + - Verifies that the response status code is 200 (OK). + - Verifies that the expected content is present in the response. + """ + + client = Client() + address = Address.objects.create(id=1, number=30, street='Oratoire', city='Marseille', + state='France', zip_code='13009', country_iso_code='FRA') + letting = Letting.objects.create(id=1, title="Marseille", address_id=address.id) + + path = reverse('lettings:letting', kwargs={'letting_id': letting.id}) + response = client.get(path) + content = response.content.decode() + expected_content = "

Marseille, France 13009

" + + assert response.status_code == 200 + assert expected_content in content diff --git a/lettings/urls.py b/lettings/urls.py new file mode 100644 index 0000000000..e4358ff221 --- /dev/null +++ b/lettings/urls.py @@ -0,0 +1,22 @@ +from django.urls import path +from . import views + +"""URL configuration for the 'lettings' app. + +This module defines URL patterns for the 'lettings' app, mapping +URL patterns to view functions. + +Attributes: + app_name (str): The namespace for the URLs in this module. + This allows you to refer to these URLs using their namespaced + names like 'lettings:index' or 'lettings:letting'. + urlpatterns (list): A list of `path` objects that define the + URL patterns for the 'lettings' app. +""" + +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..1579a4e469 --- /dev/null +++ b/lettings/views.py @@ -0,0 +1,43 @@ +from django.shortcuts import render +from lettings.models import Letting + + +def index(request): + """Render the index page with a list of lettings. + + This view retrieves all letting objects from the database + and renders the index.html template with the list of lettings. + + Args: + request (HttpRequest): The HTTP request object. + + Returns: + HttpResponse: The rendered HTML response containing the + index page with the list of lettings. + """ + lettings_list = Letting.objects.all() + context = {'lettings_list': lettings_list} + return render(request, 'lettings/index.html', context) + + +def letting(request, letting_id): + """Render the letting details page. + + This view retrieves the letting object with the specified ID + from the database and renders the letting.html template with + the letting details. + + Args: + request (HttpRequest): The HTTP request object. + letting_id (int): The ID of the letting to display. + + Returns: + HttpResponse: The rendered HTML response containing the + letting details page. + """ + letting = Letting.objects.get(id=letting_id) + context = { + 'title': letting.title, + 'address': letting.address, + } + return render(request, 'lettings/letting.html', context) diff --git a/manage.py b/manage.py index c0e27e034a..e18d8f7bf1 100755 --- a/manage.py +++ b/manage.py @@ -3,6 +3,11 @@ def main(): + """Entry point for the Django management command-line interface. + + This function sets the DJANGO_SETTINGS_MODULE environment variable + to specify the settings module for the Django project. + """ 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..d29616b29d 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 deleted file mode 100644 index 6489692f04..0000000000 --- a/oc_lettings_site/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class OCLettingsSiteConfig(AppConfig): - name = 'oc_lettings_site' diff --git a/oc_lettings_site/asgi.py b/oc_lettings_site/asgi.py index 61f2d23ba3..3aa4a4da0f 100644 --- a/oc_lettings_site/asgi.py +++ b/oc_lettings_site/asgi.py @@ -1,7 +1,7 @@ import os - from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oc_lettings_site.settings') +"""This code initializes the ASGI application for the Django project.""" +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oc_lettings_site.settings') application = get_asgi_application() 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..30fab28750 100644 --- a/oc_lettings_site/settings.py +++ b/oc_lettings_site/settings.py @@ -2,6 +2,7 @@ from pathlib import Path + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = Path(__file__).resolve().parent.parent @@ -15,19 +16,21 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ["*"] # Application definition INSTALLED_APPS = [ - 'oc_lettings_site.apps.OCLettingsSiteConfig', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'oc_lettings_site', + 'lettings', + 'profiles' ] MIDDLEWARE = [ @@ -45,7 +48,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 +114,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..909f4bc0f1 --- /dev/null +++ b/oc_lettings_site/templates/404.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% block title %}ERROR 404{% endblock title %} + +{% block content %} + +
+
+
+

404 PAGE NOT FOUND

+
+
+
+ +
+
+
+

"Oops, unfortunately the page you are looking for seems to have taken an unexpected vacation."

+
+
+
+ +
+ +
+ +{% endblock %} diff --git a/oc_lettings_site/templates/500.html b/oc_lettings_site/templates/500.html new file mode 100644 index 0000000000..7661da570d --- /dev/null +++ b/oc_lettings_site/templates/500.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% block title %}ERROR 500{% endblock title %} + +{% block content %} + +
+
+
+

500 INTERNAL SERVER ERROR

+
+
+
+ +
+
+
+

"Oops, our server decided to take a surprise vacation! We're working on bringing it back quickly."

+
+
+
+ +
+ +
+ +{% endblock %} \ 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/tests.py b/oc_lettings_site/tests.py deleted file mode 100644 index 3fd62bb718..0000000000 --- a/oc_lettings_site/tests.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_dummy(): - assert 1 diff --git a/oc_lettings_site/urls.py b/oc_lettings_site/urls.py index f0ff5897ab..2cbab4caa3 100644 --- a/oc_lettings_site/urls.py +++ b/oc_lettings_site/urls.py @@ -1,13 +1,27 @@ from django.contrib import admin -from django.urls import path +from django.urls import path, include from . import views +"""URL configuration for the Django project. + +This module defines the URL patterns for the Django project. +It includes patterns for the index page, admin site, lettings app, +and profiles app. + +Patterns: + - '' : Maps to the index view, which typically serves the homepage. + - 'admin/' : Maps to the Django admin site. + - 'lettings/' : Includes URL patterns from the 'lettings' app, + using the 'lettings' namespace. + - 'profiles/' : Includes URL patterns from the 'profiles' app, + using the 'profiles' namespace. +""" + + 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('admin/', admin.site.urls), + path('lettings/', include('lettings.urls', namespace='lettings')), + path('profiles/', include('profiles.urls', namespace='profiles')), ] diff --git a/oc_lettings_site/views.py b/oc_lettings_site/views.py index a72db27074..bf5fb8fc69 100644 --- a/oc_lettings_site/views.py +++ b/oc_lettings_site/views.py @@ -1,45 +1,15 @@ from django.shortcuts import render -from .models import Letting, 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') + """Render the index page. -# 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 view renders the index.html template, typically serving as the homepage. + Args: + request (HttpRequest): The HTTP request object. -#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) + Returns: + HttpResponse: The rendered HTML response containing the index page. + """ + return render(request, 'index.html') diff --git a/oc_lettings_site/wsgi.py b/oc_lettings_site/wsgi.py index d78ca6d669..97fff2d429 100644 --- a/oc_lettings_site/wsgi.py +++ b/oc_lettings_site/wsgi.py @@ -1,7 +1,7 @@ import os - from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oc_lettings_site.settings') +"""This code initializes the WSGI application for the Django project.""" +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oc_lettings_site.settings') application = get_wsgi_application() 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..dddbc4f049 --- /dev/null +++ b/profiles/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from .models import Profile + +"""This code registers the Profile model with the Django admin interface.""" + +admin.site.register(Profile) diff --git a/profiles/apps.py b/profiles/apps.py new file mode 100644 index 0000000000..9f19935b00 --- /dev/null +++ b/profiles/apps.py @@ -0,0 +1,14 @@ +from django.apps import AppConfig + + +class ProfilesConfig(AppConfig): + """Configuration class for the 'profiles' app. + + This class defines configuration settings for the 'profiles' app. + It primarily sets the name of the app. + + Attributes: + name (str): The name of the 'profiles' app. + """ + + name = 'profiles' diff --git a/profiles/migrations/0001_initial.py b/profiles/migrations/0001_initial.py new file mode 100644 index 0000000000..9613cfed09 --- /dev/null +++ b/profiles/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# Generated by Django 3.0 on 2024-03-26 14:23 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Profile1', + 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)), + ], + ), + ] diff --git a/profiles/migrations/0002_auto_20240326_1424.py b/profiles/migrations/0002_auto_20240326_1424.py new file mode 100644 index 0000000000..f5357aa8b0 --- /dev/null +++ b/profiles/migrations/0002_auto_20240326_1424.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0 on 2024-03-26 14:24 + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('profiles', '0001_initial'), + ] + + operations = [ + migrations.RenameModel( + old_name='Profile1', + new_name='NewProfile', + ), + ] diff --git a/profiles/migrations/0003_auto_20240326_1631.py b/profiles/migrations/0003_auto_20240326_1631.py new file mode 100644 index 0000000000..2ac6989f1b --- /dev/null +++ b/profiles/migrations/0003_auto_20240326_1631.py @@ -0,0 +1,23 @@ +from django.db import migrations + + +def new_tables(apps, schema_editor): + old_profile_table = apps.get_model('oc_lettings_site', 'Profile') + new_profile_table = apps.get_model('profiles', 'NewProfile') + + old_address_datas = old_profile_table.objects.all() + + for data in old_address_datas: + new_letting_data = new_profile_table(id=data.id, favorite_city=data.favorite_city, user_id=data.user_id) + new_letting_data.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0002_auto_20240326_1424'), + ] + + operations = [ + migrations.RunPython(new_tables), + ] diff --git a/profiles/migrations/0004_auto_20240327_1335.py b/profiles/migrations/0004_auto_20240327_1335.py new file mode 100644 index 0000000000..fee95e3446 --- /dev/null +++ b/profiles/migrations/0004_auto_20240327_1335.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0 on 2024-03-27 13:35 + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('profiles', '0003_auto_20240326_1631'), + ] + + operations = [ + migrations.RenameModel( + old_name='NewProfile', + new_name='Profile', + ), + ] 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..cf21101d7f --- /dev/null +++ b/profiles/models.py @@ -0,0 +1,21 @@ +from django.db import models +from django.contrib.auth.models import User + + +class Profile(models.Model): + """Model representing a user profile. + + This model stores additional information about a user, + such as their favorite city. + + Attributes: + user (OneToOneField): The associated user. + favorite_city (CharField): The user's favorite city. + """ + + user = models.OneToOneField(User, on_delete=models.CASCADE) + favorite_city = models.CharField(max_length=64, blank=True) + + def __str__(self): + """String representation of the user profile.""" + return self.user.username diff --git a/profiles/templates/__init__.py b/profiles/templates/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/profiles/templates/profiles/__init__.py b/profiles/templates/profiles/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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/tests/__init__.py b/profiles/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/profiles/tests/test_urls.py b/profiles/tests/test_urls.py new file mode 100644 index 0000000000..24a6b4389e --- /dev/null +++ b/profiles/tests/test_urls.py @@ -0,0 +1,35 @@ +import pytest +from django.contrib.auth.models import User + +from django.urls import reverse, resolve +from profiles.models import Profile + + +@pytest.mark.django_db +def test_profiles_url(): + """Test the URL routing for user profiles. + + This test verifies that the URL for a user's profile is generated correctly + and that it resolves to the expected view function. + + Database setup: + - Creates a User instance with a specific username. + - Creates a Profile instance associated with the created User. + + Steps: + - Generates the URL for the user's profile using the username. + - Checks if the generated URL matches the expected format. + - Resolves the URL and verifies that the associated view name is correct. + + Assertions: + - Verifies that the generated URL matches '/profiles/{username}/'. + - Verifies that the resolved view name is 'profiles:profile'. + """ + + user = User.objects.create_user(username='4meRomance', password='password') + Profile.objects.create(id=1, favorite_city="Marseille", user_id=user.id) + + path = reverse('profiles:profile', kwargs={'username': user.username}) + + assert path == "/profiles/4meRomance/" + assert resolve(path).view_name == 'profiles:profile' diff --git a/profiles/tests/test_view.py b/profiles/tests/test_view.py new file mode 100644 index 0000000000..55b9513e94 --- /dev/null +++ b/profiles/tests/test_view.py @@ -0,0 +1,39 @@ +import pytest + +from django.test import Client +from django.contrib.auth.models import User +from django.urls import reverse +from profiles.models import Profile + + +@pytest.mark.django_db +def test_profiles_view(): + """Test the 'profile' view functionality. + + This test verifies that the 'profile' view displays the correct content + for a user's profile. + + Database setup: + - Creates a User instance with specific attributes. + - Creates a Profile instance associated with the created User. + + Steps: + - Makes a GET request to the 'profile' view. + - Retrieves the response content. + + Assertions: + - Verifies that the response status code is 200 (OK). + - Verifies that the expected content (user email) is present in the response. + """ + client = Client() + user = User.objects.create_user(username='4meRomance', password='password', + email='edygaram@gmail.com') + Profile.objects.create(id=1, favorite_city="Marseille", user_id=user.id) + + path = reverse('profiles:profile', kwargs={'username': user.username}) + response = client.get(path) + content = response.content.decode() + expected_content = "edygaram@gmail.com" + + assert response.status_code == 200 + assert expected_content in content diff --git a/profiles/urls.py b/profiles/urls.py new file mode 100644 index 0000000000..265d7dc61c --- /dev/null +++ b/profiles/urls.py @@ -0,0 +1,22 @@ +from django.urls import path +from . import views + +"""URL configuration for the 'profiles' app. + +This module defines URL patterns for the 'profiles' app, mapping +URL patterns to view functions. + +Attributes: + app_name (str): The namespace for the URLs in this module. + This allows you to refer to these URLs using their namespaced + names like 'profiles:index' or 'profiles:profile'. + urlpatterns (list): A list of `path` objects that define the + URL patterns for the 'profiles' app. +""" + +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..049392b272 --- /dev/null +++ b/profiles/views.py @@ -0,0 +1,38 @@ +from django.shortcuts import render +from profiles.models import Profile + + +def index(request): + """Render the index page with a list of all profiles. + + This view retrieves all profiles from the database and passes + them to the 'profiles/index.html' template to render the index page. + + Args: + request (HttpRequest): The HTTP request object. + + Returns: + HttpResponse: The rendered HTML response containing the index page. + """ + profiles_list = Profile.objects.all() + context = {'profiles_list': profiles_list} + return render(request, 'profiles/index.html', context) + + +def profile(request, username): + """Render the profile page for a specific user. + + This view retrieves the profile associated with the given username + from the database and passes it to the 'profiles/profile.html' template + to render the profile page. + + Args: + request (HttpRequest): The HTTP request object. + username (str): The username of the user whose profile is being viewed. + + Returns: + HttpResponse: The rendered HTML response containing the profile page. + """ + profile = Profile.objects.get(user__username=username) + context = {'profile': profile} + return render(request, 'profiles/profile.html', context) diff --git a/requirements.txt b/requirements.txt index c48c84ea40..663dd67eed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ django==3.0 -flake8==3.7.0 -pytest-django==3.9.0 \ No newline at end of file +flake8==7.0.0 +pytest-django==4.8.0 +pytest~=8.1.1 diff --git a/setup.cfg b/setup.cfg index 9346841bbc..3d17f4b503 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,5 +4,21 @@ exclude = **/migrations/*,venv [tool:pytest] DJANGO_SETTINGS_MODULE = oc_lettings_site.settings -python_files = tests.py -addopts = -v +python_files = test_*.py +python_functions = test_* +addopts = -v --nomigrations + --cov=. + --cov-fail-under=80 + +[coverage:run] +omit = + */migrations/* + */tests/* + */tests.py + */__init__.py + */apps.py + */admin.py + */manage.py + */settings.py + */wsgi.py + */asgi.py