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.first_name}} {{member.last_name}}
-

{{member.role_display}}

+

{{ member.role_display }}e

diff --git a/members/templates/members/cards/team.html b/members/templates/members/cards/team.html index a2325b7..db5a56f 100644 --- a/members/templates/members/cards/team.html +++ b/members/templates/members/cards/team.html @@ -6,8 +6,10 @@ {% block card_body %}
{{team.name}}
+ {% if team.members|length %} {{ team.members|length }} membre{{ team.members|length|pluralize }} + {% endif %}
{% if team.description %} @@ -17,15 +19,20 @@
{{team.name}}

+ {% if not team.members %} +

On recrute du monde !

+ {% else %} {% for member in team.members %}
{{member.first_name}} {{member.last_name}}

{{member.first_name}} {{member.last_name}}

-

{{member.role_display}}

+

{{ member.role_display }}e

{% endfor %} + {% endif %}
{% endblock %} \ No newline at end of file diff --git a/members/tests.py b/members/tests.py index 7bc936d..38a93a1 100644 --- a/members/tests.py +++ b/members/tests.py @@ -45,6 +45,10 @@ def test_common_data(self): response = self.client.get("/members/board/") self.assertIn("partners_qs", response.context) self.assertIn("partners_json", response.context) + self.assertIn("seo_title", response.context) + self.assertIn("seo_description", response.context) + self.assertIn("seo_canonical_url", response.context) + self.assertIn("seo_og_image", response.context) def test_members_context(self): response = self.client.get("/members/board/") @@ -117,6 +121,10 @@ def test_common_data(self): response = self.client.get("/members/") self.assertIn("partners_qs", response.context) self.assertIn("partners_json", response.context) + self.assertIn("seo_title", response.context) + self.assertIn("seo_description", response.context) + self.assertIn("seo_canonical_url", response.context) + self.assertIn("seo_og_image", response.context) def test_teams_context(self): response = self.client.get("/members/") diff --git a/members/views.py b/members/views.py index 4cbc4d5..9e5baf8 100644 --- a/members/views.py +++ b/members/views.py @@ -1,8 +1,12 @@ from django.shortcuts import render from django.contrib.auth.models import User -from .models import Team, UserTeam +from .models import RoleCode, Team, TeamTemplate, UserTeam, UserTeamTemplate from utils.views import common_data -from typing import cast +from typing import TypedDict, cast + + +class TeamWithMembersTemplate(TeamTemplate): + members: list[UserTeamTemplate] def board(request): @@ -11,11 +15,12 @@ def board(request): .filter(team__name="Bureau") .only("user__id", "user__first_name", "user__last_name", "role") ) - role_order = UserTeam.ROLES + role_order: list[RoleCode] = [role for role, _ in UserTeam.ROLES] members: list[UserTeam] = sorted( members_qs, key=lambda m: ( - role_order.index(m.role) if m.role in role_order else len(role_order) + role_order.index(m.role) if m.role in role_order else len(role_order), + cast(User, m.user).last_name, ), ) @@ -29,7 +34,13 @@ def board(request): request, "board/main.html", { - **common_data(), + **common_data( + request, + seo={ + "title": "BDE UTT | Bureau", + "description": "Decouvrez les membres du bureau du BDE UTT et leurs roles.", + }, + ), "members": [ { **member.to_template(), @@ -50,23 +61,40 @@ def members(request): "user", "role", "team" ) - teams_with_members = [ - { - **team.to_template(), - "members": [ - ut.to_template() - for ut in users_team - if cast(Team, ut.team).pk == team.pk - ], - } - for team in teams - ] + role_order: list[RoleCode] = [role for role, _ in UserTeam.ROLES] + + teams_with_members: list[TeamWithMembersTemplate] = sorted( + [ + { + **team.to_template(), + "members": sorted( + [ + ut.to_template() + for ut in users_team + if cast(Team, ut.team).pk == team.pk + ], + key=lambda m: ( + role_order.index(m["role"]), + m["last_name"].lower(), + ), + ), + } + for team in teams + ], + key=lambda t: t["name"].lower(), + ) return render( request, "members/main.html", { - **common_data(), + **common_data( + request, + seo={ + "title": "BDE UTT | Commissions", + "description": "Retrouvez les commissions du BDE UTT et les membres qui les composent.", + }, + ), "teams": teams_with_members, }, ) diff --git a/requirements.txt b/requirements.txt index 22c2c70..c3c4463 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,15 +2,20 @@ asgiref==3.9.1 astroid==3.3.11 certifi==2025.8.3 cffi==2.0.0 +cfgv==3.5.0 charset-normalizer==3.4.3 cryptography==45.0.7 defusedxml==0.7.1 dill==0.4.0 +distlib==0.4.0 Django==5.2.6 django-allauth==65.11.2 +django-browser-reload==1.21.0 django-stubs==5.2.2 django-stubs-ext==5.2.2 +filelock==3.25.0 gunicorn==23.0.0 +identify==2.6.17 idna==3.10 isort==6.0.1 josepy==2.1.0 @@ -19,18 +24,22 @@ mozilla-django-oidc==4.0.1 mypy==1.17.1 mypy_extensions==1.1.0 mysqlclient==2.2.7 +nodeenv==1.10.0 oauthlib==3.3.1 packaging==25.0 pathspec==0.12.1 pillow==11.3.0 platformdirs==4.4.0 +pre_commit==4.5.1 pycparser==2.23 pylint==3.3.8 pylint-django==2.6.1 pylint-plugin-utils==0.9.0 +python-discovery==1.1.1 python-dotenv==1.1.1 python-openid==2.2.5 python3-openid==3.2.0 +PyYAML==6.0.3 requests==2.32.5 requests-oauthlib==2.0.0 sqlparse==0.5.3 @@ -38,4 +47,5 @@ tomlkit==0.13.3 types-PyYAML==6.0.12.20250822 typing_extensions==4.15.0 urllib3==2.5.0 +virtualenv==21.1.0 whitenoise==6.10.0 diff --git a/showcase/templates/home/main.html b/showcase/templates/home/main.html index 5460ee7..ddb953c 100644 --- a/showcase/templates/home/main.html +++ b/showcase/templates/home/main.html @@ -10,8 +10,6 @@ {% include "partials/heroWithPicture.html" %} {% endwith %}{% endwith %}{% endwith %} - -
diff --git a/showcase/templates/services/foyer/main.html b/showcase/templates/services/foyer/main.html index 6b0788e..2dba886 100644 --- a/showcase/templates/services/foyer/main.html +++ b/showcase/templates/services/foyer/main.html @@ -11,7 +11,6 @@ {% include "partials/heroWithPicture.html" %} {% endwith %}{% endwith %}{% endwith %} -
diff --git a/showcase/templates/services/treasury/main.html b/showcase/templates/services/treasury/main.html index fd21340..64f931c 100644 --- a/showcase/templates/services/treasury/main.html +++ b/showcase/templates/services/treasury/main.html @@ -9,8 +9,6 @@

Trésorerie

- -
{% with title="Plateforme de contact Trésorerie" %} {% with subject="relatives à la trésorerie" %} diff --git a/showcase/tests.py b/showcase/tests.py index 4606d27..690e492 100644 --- a/showcase/tests.py +++ b/showcase/tests.py @@ -89,6 +89,14 @@ def _assert_common_data(self, response): self.assertIn("partners_qs", response.context) self.assertIn("partners_json", response.context) self.assertIn("current_year", response.context) + self.assertIn("seo_title", response.context) + self.assertIn("seo_description", response.context) + self.assertIn("seo_canonical_url", response.context) + self.assertIn("seo_og_title", response.context) + self.assertIn("seo_og_description", response.context) + self.assertIn("seo_og_type", response.context) + self.assertIn("seo_og_url", response.context) + self.assertIn("seo_og_image", response.context) partners_qs = response.context["partners_qs"] self.assertEqual( @@ -100,6 +108,9 @@ def _assert_common_data(self, response): self.assertIn("Enabled B", partners_json) self.assertNotIn("Disabled", partners_json) + self.assertTrue(response.context["seo_title"]) + self.assertTrue(response.context["seo_description"]) + def test_static_showcase_views_status_template_and_common_data(self): routes = [ ("/", "home/main.html"), @@ -138,3 +149,37 @@ def test_dynamic_showcase_views_status_template_and_common_data(self): self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, template_name) self._assert_common_data(response) + + def test_dynamic_showcase_views_use_specific_seo_metadata(self): + cases = [ + ( + "/events/integration/", + "BDE UTT | Week-end Integration", + "Programme, infos pratiques et conseils pour le week-end d'integration organise par le BDE UTT.", + ), + ( + "/events/r2d/", + "BDE UTT | R2D", + "Decouvrez la R2D, un evenement phare du BDE UTT et de la vie etudiante.", + ), + ( + "/services/tickets/", + "BDE UTT | Plateforme Tickets", + "Utilisez la plateforme tickets de l'UNG pour demander un support rapide.", + ), + ( + "/services/clubs/", + "BDE UTT | Clubs et Assos", + "Retrouvez les informations sur la gestion des clubs et associations de l'UTT ainsi que les contacts utiles proposes par le BDE.", + ), + ] + + for url, expected_title, expected_description in cases: + with self.subTest(url=url): + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context["seo_title"], expected_title) + self.assertEqual( + response.context["seo_description"], + expected_description, + ) diff --git a/showcase/views.py b/showcase/views.py index 73b2550..a6cbc1e 100644 --- a/showcase/views.py +++ b/showcase/views.py @@ -4,11 +4,88 @@ from .models import UsefulContact, BDEEmail, BDEPhone +EVENTS_SEO_BY_PARAM: dict[str, dict[str, str]] = { + "integration": { + "title": "BDE UTT | Week-end Integration", + "description": "Programme, infos pratiques et conseils pour le week-end d'integration organise par le BDE UTT.", + }, + "r2d": { + "title": "BDE UTT | R2D", + "description": "Decouvrez la R2D, un evenement phare du BDE UTT et de la vie etudiante.", + }, + "sdf": { + "title": "BDE UTT | SDF", + "description": "Informations sur la SDF, un evenement organise par le BDE UTT.", + }, +} + + +SERVICES_SEO_BY_PARAM: dict[str, dict[str, str]] = { + "campus": { + "title": "BDE UTT | Campus", + "description": "La vie de Campus du l'UTT pour simplifier votre quotidien etudiant.", + }, + "clubs": { + "title": "BDE UTT | Clubs et Assos", + "description": "Retrouvez les informations sur la gestion des clubs et associations de l'UTT ainsi que les contacts utiles proposes par le BDE.", + }, + "communication": { + "title": "BDE UTT | Communication", + "description": "Les actions de communication du BDE UTT pour informer les etudiants.", + }, + "foyer": { + "title": "BDE UTT | Foyer", + "description": "Decouvrez le foyer de l'UTT et les services proposes par le BDE.", + }, + "loan": { + "title": "BDE UTT | Pret de materiel", + "description": "Consultez les modalites de pret de materiel proposees par le BDE UTT.", + }, + "tickets": { + "title": "BDE UTT | Plateforme Tickets", + "description": "Utilisez la plateforme tickets de l'UNG pour demander un support rapide.", + }, + "treasury": { + "title": "BDE UTT | Tresorerie", + "description": "Informations de tresorerie et accompagnement financier proposes par le BDE UTT.", + }, + "zeshop": { + "title": "BDE UTT | ZeShop", + "description": "Decouvrez ZeShop, la boutique du BDE UTT et ses produits pour les etudiants.", + }, +} + + +def _seo_for_param( + mapping: dict[str, dict[str, str]], + param: str, + title_prefix: str, + description_prefix: str, +) -> dict[str, str]: + seo = mapping.get(param) + if seo is not None: + return seo + + readable_param = param.replace("-", " ").title() + return { + "title": f"BDE UTT | {title_prefix} {readable_param}", + "description": f"{description_prefix} {readable_param} proposé par le BDE UTT.", + } + + def home(request): return render( request, "home/main.html", - {**common_data()}, + { + **common_data( + request, + seo={ + "title": "BDE UTT | Accueil", + "description": "Bienvenue sur le site du BDE UTT : vie etudiante, evenements, services et actualites associatives.", + }, + ) + }, ) @@ -17,7 +94,13 @@ def contacts(request): request, "contacts/main.html", { - **common_data(), + **common_data( + request, + seo={ + "title": "BDE UTT | Contacts", + "description": "Retrouvez les adresses email et les numeros utiles du Bureau des Etudiants de l'UTT.", + }, + ), "bde_emails": BDEEmail.objects.all(), "bde_phones": BDEPhone.objects.all(), }, @@ -48,14 +131,32 @@ def _get_google_calendar_settings() -> dict: request, "events/main.html", { - **common_data(), + **common_data( + request, + seo={ + "title": "BDE UTT | Evenements", + "description": "Decouvrez les evenements du BDE UTT et consultez le calendrier associatif.", + }, + ), **_get_google_calendar_settings(), }, ) + + param_seo = _seo_for_param( + EVENTS_SEO_BY_PARAM, + param, + "Evenement", + "Informations sur l'evenement", + ) return render( request, f"events/{param}/main.html", - {**common_data()}, + { + **common_data( + request, + seo=param_seo, + ) + }, ) @@ -63,7 +164,15 @@ def membership(request): return render( request, "membership/main.html", - {**common_data()}, + { + **common_data( + request, + seo={ + "title": "BDE UTT | Adhesion", + "description": "Toutes les informations pour adherer au BDE UTT et profiter des services associes.", + }, + ) + }, ) @@ -71,7 +180,15 @@ def partners(request): return render( request, "partners/main.html", - {**common_data()}, + { + **common_data( + request, + seo={ + "title": "BDE UTT | Partenaires", + "description": "Decouvrez les partenaires du BDE UTT et les avantages proposes aux etudiants.", + }, + ) + }, ) @@ -89,13 +206,31 @@ def _get_userful_contacts(param: str) -> dict: return render( request, "services/main.html", - {**common_data()}, + { + **common_data( + request, + seo={ + "title": "BDE UTT | Services", + "description": "Explorez les services du BDE UTT pour faciliter votre vie etudiante.", + }, + ) + }, ) + + param_seo = _seo_for_param( + SERVICES_SEO_BY_PARAM, + param, + "Service", + "Details du service", + ) return render( request, f"services/{param}/main.html", { - **common_data(), + **common_data( + request, + seo=param_seo, + ), **_get_userful_contacts(param), }, ) diff --git a/static/css/main.css b/static/css/main.css index dbb1535..77ecea2 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -51,6 +51,15 @@ nav { text-align: justify; } +.role-suffix { + display: inline-block; + width: 1ch; +} + +.role-suffix--hidden { + visibility: hidden; +} + .width-limited-sm { min-width: 300px; max-width: 500px; diff --git a/templates/admin/login.html b/templates/admin/login.html index 882d80a..e22fa12 100644 --- a/templates/admin/login.html +++ b/templates/admin/login.html @@ -6,8 +6,6 @@ {% endblock %} - - {% block content %}
diff --git a/templates/base.html b/templates/base.html index e604340..262071e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -5,7 +5,32 @@ - {% block title %}BDE UTT{% endblock %} + {% if seo_title %}{{ seo_title }}{% else %}{% block title %}BDE UTT{% endblock %}{% endif %} + + + {% if seo_canonical_url %} + + {% endif %} + + + + + + + {% if seo_og_url %} + + {% endif %} + {% if seo_og_image %} + + {% endif %} + + + + + {% if seo_og_image %} + + {% endif %} + {% block head %}{% endblock %} diff --git a/templates/partials/footer.html b/templates/partials/footer.html index 10b43c7..c8f39c5 100644 --- a/templates/partials/footer.html +++ b/templates/partials/footer.html @@ -11,7 +11,6 @@
Le Bureau des Étudiants de l'Université de Technologie de Troyes.

-
diff --git a/utils/views.py b/utils/views.py index cb065d2..961195b 100644 --- a/utils/views.py +++ b/utils/views.py @@ -1,9 +1,47 @@ from showcase.models import Partner from datetime import datetime import json +from typing import Any +from django.http import HttpRequest +from bde.settings import DEFAULT_SEO_TITLE, DEFAULT_SEO_DESCRIPTION, DEFAULT_SEO_IMAGE -def common_data(): +def _absolute_url(request: HttpRequest | None, path: str) -> str: + if request is None: + return path + return request.build_absolute_uri(path) + + +def build_seo_data( + request: HttpRequest | None = None, + seo: dict[str, Any] | None = None, +) -> dict[str, str]: + seo = seo or {} + + title = str(seo.get("title") or DEFAULT_SEO_TITLE) + description = str(seo.get("description") or DEFAULT_SEO_DESCRIPTION) + canonical_url = str(seo.get("canonical_url") or _absolute_url(request, "")) + og_title = str(seo.get("og_title") or title) + og_description = str(seo.get("og_description") or description) + og_type = str(seo.get("og_type") or "website") + og_image = str(seo.get("og_image") or _absolute_url(request, DEFAULT_SEO_IMAGE)) + + return { + "seo_title": title, + "seo_description": description, + "seo_canonical_url": canonical_url, + "seo_og_title": og_title, + "seo_og_description": og_description, + "seo_og_type": og_type, + "seo_og_url": canonical_url, + "seo_og_image": og_image, + } + + +def common_data( + request: HttpRequest | None = None, + seo: dict[str, Any] | None = None, +): partners_qs = Partner.objects.filter(enable=True).order_by("order") partners_list = [ @@ -20,4 +58,5 @@ def common_data(): "partners_qs": partners_qs, "partners_json": json.dumps(partners_list), "current_year": datetime.now().year, + **build_seo_data(request, seo), }