diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e9b5805..8051ff0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -49,12 +49,9 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt - - name: Check for missing migrations - run: python manage.py makemigrations --check --dry-run - - name: Wait for MariaDB run: | - for i in {1..30}; do + for i in {1..60}; do if mysqladmin ping -h127.0.0.1 -utestuser -ptestpass --silent; then echo "MariaDB is up!" exit 0 @@ -74,6 +71,15 @@ jobs: echo "DB_PORT=3306" >> $GITHUB_ENV echo "SECRET_KEY=testsecrret" >> $GITHUB_ENV + - name: Check for missing migrations + env: + DJANGO_DB_NAME: ${{ env.DB_NAME }} + DJANGO_DB_USER: ${{ env.DB_USER }} + DJANGO_DB_PASSWORD: ${{ env.DB_PASSWORD }} + DJANGO_DB_HOST: ${{ env.DB_HOST }} + DJANGO_DB_PORT: ${{ env.DB_PORT }} + run: python manage.py makemigrations --check --dry-run + - name: Run Django tests env: DJANGO_DB_NAME: ${{ env.DB_NAME }} diff --git a/README.md b/README.md index f26546d..ef4a0cd 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,16 @@ mypy . docker compose logs -f web ``` +## Pre-commit + +```bash +# depuis un shell avec le venv actif +pre-commit install +pre-commit run --all-files +``` + +Les hooks locaux utilisent le Python du projet (`.venv/bin/python`) pour les checks Django et `mypy`. + ## Arborescence principale - `bde/` : configuration Django (settings, urls, env) diff --git a/bde/generate_robots.py b/bde/generate_robots.py index 769a1d1..4299b08 100644 --- a/bde/generate_robots.py +++ b/bde/generate_robots.py @@ -7,19 +7,21 @@ def get_robots_content() -> str: if env.DEV_MODE: - return "User-Agent: *\nDisallow: /" + return "User-Agent: *\nDisallow: /\nSitemap: /sitemap.xml\n" - return f"""User-agent: * - Disallow: /{env.ADMIN_URL} - Disallow: /logout/ - Disallow: /redirect/ - Disallow: /sso/ - Disallow: /uploads/ - """ + return ( + f"User-agent: *\n" + f"Disallow: /{env.ADMIN_URL}\n" + "Disallow: /logout/\n" + "Disallow: /redirect/\n" + "Disallow: /sso/\n" + "Disallow: /uploads/\n" + "Sitemap: /sitemap.xml\n" + ) BASE_DIR = Path(__file__).resolve().parent.parent -STATICFILES_DIR = BASE_DIR / ("staticfiles" if not env.DEBUG else "static") +STATICFILES_DIR = BASE_DIR / "staticfiles" STATICFILES_DIR.mkdir(exist_ok=True) robots_path = STATICFILES_DIR / "robots.txt" diff --git a/bde/settings.py b/bde/settings.py index 8ed54a4..6530b55 100644 --- a/bde/settings.py +++ b/bde/settings.py @@ -14,6 +14,10 @@ SESSION_COOKIE_SECURE = env.SESSION_COOKIE_SECURE CSRF_COOKIE_SECURE = env.CSRF_COOKIE_SECURE +DEFAULT_SEO_TITLE = "BDE UTT" +DEFAULT_SEO_DESCRIPTION = "Le site du Bureau des Etudiants de l'UTT : evenements, services, partenariats et vie associative." +DEFAULT_SEO_IMAGE = "/static/img/bde.png" + if env.CSRF_TRUSTED_ORIGINS: CSRF_TRUSTED_ORIGINS = env.CSRF_TRUSTED_ORIGINS @@ -24,6 +28,7 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "django.contrib.sitemaps", "mozilla_django_oidc", "core", "members", diff --git a/bde/sitemaps.py b/bde/sitemaps.py new file mode 100644 index 0000000..c1f3028 --- /dev/null +++ b/bde/sitemaps.py @@ -0,0 +1,27 @@ +from django.contrib.sitemaps import Sitemap +from django.urls import reverse + + +class StaticViewSitemap(Sitemap): + changefreq = "weekly" + priority = 0.8 + + def items(self): + return [ + "home", + "contacts", + "events", + "membership", + "partners", + "services", + "members", + "board", + ] + + def location(self, item): + return reverse(item) + + +sitemaps = { + "static": StaticViewSitemap, +} diff --git a/bde/templates/legals.html b/bde/templates/legals.html index 1c4d0e5..4f96e83 100644 --- a/bde/templates/legals.html +++ b/bde/templates/legals.html @@ -22,11 +22,11 @@
BDE UTT
Association loi 1901
-N° RNA :
-N° RCS :
+N° RNA : W103000735
+N° SIRET : 44838667200019
+N° SIREN : 448386672
diff --git a/bde/tests.py b/bde/tests.py new file mode 100644 index 0000000..a7127de --- /dev/null +++ b/bde/tests.py @@ -0,0 +1,18 @@ +from django.test import SimpleTestCase, override_settings + +from bde.generate_robots import get_robots_content + + +class SEOInfrastructureTests(SimpleTestCase): + def test_sitemap_is_available(self): + response = self.client.get("/sitemap.xml") + + self.assertEqual(response.status_code, 200) + self.assertContains(response, "/members/") + self.assertContains(response, "/events/") + + @override_settings(DEBUG=True) + def test_robots_declares_sitemap(self): + robots = get_robots_content() + + self.assertIn("Sitemap: /sitemap.xml", robots) diff --git a/bde/urls.py b/bde/urls.py index 7411b54..90b6cc9 100644 --- a/bde/urls.py +++ b/bde/urls.py @@ -1,10 +1,12 @@ from django.contrib import admin +from django.contrib.sitemaps.views import sitemap from django.urls import path, include, re_path from django.conf import settings from django.conf.urls.static import static from django.views.static import serve from bde.env import EnvConfig +from bde.sitemaps import sitemaps env = EnvConfig() ADMIN_URL = env.ADMIN_URL @@ -22,6 +24,12 @@ serve, {"path": "robots.txt", "document_root": settings.STATIC_ROOT}, ), + path( + "sitemap.xml", + sitemap, + {"sitemaps": sitemaps}, + name="django.contrib.sitemaps.views.sitemap", + ), path("sso/", include("mozilla_django_oidc.urls")), path(ADMIN_URL, admin.site.urls), ] diff --git a/bde/views.py b/bde/views.py index a8bdd2c..1285a23 100644 --- a/bde/views.py +++ b/bde/views.py @@ -21,7 +21,15 @@ def legals(request): return render( request, "legals.html", - {**common_data()}, + { + **common_data( + request, + seo={ + "title": "BDE UTT | Mentions legales", + "description": "Mentions legales du site du Bureau des Etudiants de l'UTT.", + }, + ) + }, ) @@ -29,5 +37,13 @@ def privacy(request): return render( request, "privacy.html", - {**common_data()}, + { + **common_data( + request, + seo={ + "title": "BDE UTT | Politique de confidentialite", + "description": "Politique de confidentialite et traitement des donnees personnelles du site BDE UTT.", + }, + ) + }, ) diff --git a/members/admin.py b/members/admin.py index 6afa392..15582ed 100644 --- a/members/admin.py +++ b/members/admin.py @@ -14,4 +14,4 @@ class UserTeamAdmin(admin.ModelAdmin): @admin.register(UserProfile) class UserProfileAdmin(admin.ModelAdmin): - list_display = ("user", "picture", "description") + list_display = ("user", "picture", "feminine_role", "description") diff --git a/members/migrations/0004_userprofile_feminine_role_alter_userteam_role.py b/members/migrations/0004_userprofile_feminine_role_alter_userteam_role.py new file mode 100644 index 0000000..093fed4 --- /dev/null +++ b/members/migrations/0004_userprofile_feminine_role_alter_userteam_role.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.6 on 2026-03-08 15:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0003_alter_userteam_team'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='feminine_role', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='userteam', + name='role', + field=models.CharField(choices=[('PRESIDENT', 'Président'), ('VICE_PRESIDENT', 'Vice-Président'), ('SECRETARY', 'Secrétaire'), ('TREASURER', 'Trésorier'), ('VICE_TREASURER', 'Vice-Trésorier'), ('MANAGER', 'Responsable'), ('MEMBER', 'Membre')], default='MEMBER', max_length=20), + ), + ] diff --git a/members/migrations/0005_alter_userteam_role.py b/members/migrations/0005_alter_userteam_role.py new file mode 100644 index 0000000..c369582 --- /dev/null +++ b/members/migrations/0005_alter_userteam_role.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.6 on 2026-03-08 23:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0004_userprofile_feminine_role_alter_userteam_role'), + ] + + operations = [ + migrations.AlterField( + model_name='userteam', + name='role', + field=models.CharField(choices=[('PRESIDENT', 'Président'), ('TREASURER', 'Trésorier'), ('VICE_TREASURER', 'Vice-Trésorier'), ('VICE_PRESIDENT', 'Vice-Président'), ('SECRETARY', 'Secrétaire'), ('MANAGER', 'Responsable'), ('MEMBER', 'Membre')], default='MEMBER', max_length=20), + ), + ] diff --git a/members/models.py b/members/models.py index 749f13a..b126af5 100644 --- a/members/models.py +++ b/members/models.py @@ -1,15 +1,42 @@ +from __future__ import annotations + from django.db import models from django.contrib.auth.models import User from django.utils.safestring import mark_safe from utils.models import picture_upload_to -from typing import cast +from typing import Literal, TypedDict, cast + + +RoleCode = Literal[ + "PRESIDENT", + "VICE_PRESIDENT", + "SECRETARY", + "TREASURER", + "VICE_TREASURER", + "MANAGER", + "MEMBER", +] + + +class TeamTemplate(TypedDict): + name: str + description: str + + +class UserTeamTemplate(TypedDict): + first_name: str + last_name: str + role: RoleCode + role_display: str + role_suffix: str + profile: "UserProfile | None" class Team(models.Model): name: models.CharField = models.CharField(max_length=100) description: models.TextField = models.TextField(blank=True) - def to_template(self) -> dict[str, str]: + def to_template(self) -> TeamTemplate: return {"name": self.name, "description": self.description} def __str__(self) -> str: @@ -27,6 +54,7 @@ def _members_picture_upload_to(instance, _) -> str: ) description: models.TextField = models.TextField(blank=True) + feminine_role: models.BooleanField = models.BooleanField(default=False) @property def picture_url(self) -> str: @@ -39,11 +67,19 @@ def __str__(self) -> str: class UserTeam(models.Model): - ROLES = [ + FEMINIZABLE_ROLES: set[RoleCode] = { + "PRESIDENT", + "VICE_PRESIDENT", + "TREASURER", + "VICE_TREASURER", + } + + ROLES: list[tuple[RoleCode, str]] = [ ("PRESIDENT", "Président"), + ("TREASURER", "Trésorier"), + ("VICE_TREASURER", "Vice-Trésorier"), ("VICE_PRESIDENT", "Vice-Président"), ("SECRETARY", "Secrétaire"), - ("TREASURER", "Trésorier"), ("MANAGER", "Responsable"), ("MEMBER", "Membre"), ] @@ -60,13 +96,21 @@ class UserTeam(models.Model): def role_display(self) -> str: return dict(self.ROLES).get(self.role, "Unknown") - def to_template(self) -> dict[str, object | None]: + def role_suffix(self, profile: UserProfile | None) -> str: + if profile and profile.feminine_role and self.role in self.FEMINIZABLE_ROLES: + return "e" + return "" + + def to_template(self) -> UserTeamTemplate: user = cast(User, self.user) + profile = get_userProfile_from_userTeam(self) return { "first_name": user.first_name, "last_name": user.last_name, + "role": self.role, "role_display": self.role_display, - "profile": get_userProfile_from_userTeam(self), + "role_suffix": self.role_suffix(profile), + "profile": profile, } def __str__(self) -> str: diff --git a/members/templates/board/memberCard.html b/members/templates/board/memberCard.html index 5273d21..9376f82 100644 --- a/members/templates/board/memberCard.html +++ b/members/templates/board/memberCard.html @@ -9,7 +9,8 @@ alt="{{member.first_name}} {{member.last_name}}" style="width: 72px; height: 72px; object-fit: cover;">
{{member.role_display}}
+{{ member.role_display }}e
On recrute du monde !
+ {% else %} {% for member in team.members %}{{member.first_name}} {{member.last_name}}
-{{member.role_display}}
+{{ member.role_display }}e