From 265ee30eb1d706f5e3fa16ca04481cbbe0d09743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Mogu=C3=A9rou?= Date: Fri, 19 Apr 2024 21:20:33 +0200 Subject: [PATCH] auth --- colloscope/admin.py | 2 +- .../0007_profil_delete_utilisateur.py | 28 ++++++++ colloscope/models.py | 19 +++-- colloscope/pdfexport.py | 2 - colloscope/templates/base.html | 26 ------- colloscope/templates/choix_profil.html | 12 +--- colloscope/templates/colloscope.html | 2 + colloscope/templates/profil_non_associe.html | 13 ++++ colloscope/templates/trunk.html | 4 ++ colloscope/test.pdf | Bin 5327 -> 0 bytes colloscope/urls.py | 7 +- colloscope/views.py | 67 +++++++++++++++--- kholles_web/urls.py | 6 +- static/main.css | 38 +++++++++- static/table.css | 4 ++ {colloscope/templates => templates}/404.html | 0 templates/base.html | 38 ++++++++++ templates/registration/login.html | 44 ++++++++++++ test.pdf | Bin 6766 -> 6766 bytes 19 files changed, 254 insertions(+), 58 deletions(-) create mode 100644 colloscope/migrations/0007_profil_delete_utilisateur.py delete mode 100644 colloscope/templates/base.html create mode 100644 colloscope/templates/profil_non_associe.html create mode 100644 colloscope/templates/trunk.html delete mode 100644 colloscope/test.pdf rename {colloscope/templates => templates}/404.html (100%) create mode 100644 templates/base.html create mode 100644 templates/registration/login.html diff --git a/colloscope/admin.py b/colloscope/admin.py index d0e2529..7040222 100644 --- a/colloscope/admin.py +++ b/colloscope/admin.py @@ -13,4 +13,4 @@ admin.site.register(Colleur) admin.site.register(Creneau) admin.site.register(Rotation) admin.site.register(Amendement) -admin.site.register(Utilisateur) +admin.site.register(Profil) diff --git a/colloscope/migrations/0007_profil_delete_utilisateur.py b/colloscope/migrations/0007_profil_delete_utilisateur.py new file mode 100644 index 0000000..e1feeeb --- /dev/null +++ b/colloscope/migrations/0007_profil_delete_utilisateur.py @@ -0,0 +1,28 @@ +# Generated by Django 5.0.4 on 2024-04-19 17:07 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('colloscope', '0006_remove_rotation_semaine_rotation_date'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Profil', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('colleur', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='colloscope.colleur')), + ('etudiant', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='colloscope.etudiant')), + ('utilisateur', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, unique=True)), + ], + ), + migrations.DeleteModel( + name='Utilisateur', + ), + ] diff --git a/colloscope/models.py b/colloscope/models.py index 9df10fd..5e25cc4 100644 --- a/colloscope/models.py +++ b/colloscope/models.py @@ -2,6 +2,8 @@ from datetime import date, datetime, timedelta from django.db import models from django.db.models import F, Q +from django.contrib.auth.models import User + calendrier = { "C" : [ @@ -123,6 +125,10 @@ class Periode(models.Model): """ return range(self.classe.no_semaine(self.debut), self.classe.no_semaine(self.fin)+1) + def query_rotations(self): + return Rotation.objects.filter(creneau__periode=self) + + def __str__(self): return self.libelle @@ -318,9 +324,10 @@ class Amendement(models.Model): etudiant = models.ForeignKey(Etudiant, on_delete=models.CASCADE) -class Utilisateur(models.Model): - username = models.CharField(max_length=100) - password = models.CharField(max_length=300) - timestamp = models.DateTimeField(auto_now_add=True) - etudiant = models.ForeignKey(Etudiant, on_delete=models.SET_NULL, null=True) - colleur = models.ForeignKey(Colleur, on_delete=models.SET_NULL, null=True) +class Profil(models.Model): + utilisateur = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) + etudiant = models.ForeignKey(Etudiant, null=True, on_delete=models.SET_NULL) + colleur = models.ForeignKey(Colleur, null=True, on_delete=models.SET_NULL) + + def __str__(self): + return f"Profil {self.utilisateur} : {self.etudiant} ; {self.colleur}" diff --git a/colloscope/pdfexport.py b/colloscope/pdfexport.py index 1157dbb..cf2f62b 100644 --- a/colloscope/pdfexport.py +++ b/colloscope/pdfexport.py @@ -106,8 +106,6 @@ def generate(periode): pdf.y += 3 pdf.table_colloscope(periode, heading=False, est_colle=False) - pdf.output("test.pdf") - return pdf def main(): diff --git a/colloscope/templates/base.html b/colloscope/templates/base.html deleted file mode 100644 index c93282b..0000000 --- a/colloscope/templates/base.html +++ /dev/null @@ -1,26 +0,0 @@ -{% load static %} - - - - - - {% block title %}{% endblock title %} - - {% block head %}{% endblock head %} - - -
- {% if request.session.overseer is not None %} -
- Vous voyez ce site en tant que... Désactiver -
- {% endif %} - {% block header %}{% endblock header %} -
-
- {% block main %}{% endblock main %} -
- - diff --git a/colloscope/templates/choix_profil.html b/colloscope/templates/choix_profil.html index 12b734b..c4bdfa2 100644 --- a/colloscope/templates/choix_profil.html +++ b/colloscope/templates/choix_profil.html @@ -8,18 +8,10 @@ {% block main %} -{% if user.etudiant is not None and user.colleur is not None %} Vous êtes connecté. Votre compte correspond à deux profils : -{% elif user.colleur is not None and user.etudiant is None %} -Connection en tant que {{ user.colleur }}... classes : {{ user.colleur.get_classes }} -{% elif user.etudiant is not None and user.colleur is None %} -Connection en tant que {{ user.etudiant }} : classe {{ user.etudiant.classe }} -{% else %} -Vous êtes connecté, mais votre compte n'est associé à aucun profil. Veuillez contacter le webmestre à l'adresse valentin@mp2i-vms.fr. -{% endif %} {% endblock main %} diff --git a/colloscope/templates/colloscope.html b/colloscope/templates/colloscope.html index 72696b2..5ff4976 100644 --- a/colloscope/templates/colloscope.html +++ b/colloscope/templates/colloscope.html @@ -22,6 +22,7 @@

Colloscope : {{ periode.libelle }}

+
@@ -78,6 +79,7 @@ {% endfor %}
+

Par groupes

diff --git a/colloscope/templates/profil_non_associe.html b/colloscope/templates/profil_non_associe.html new file mode 100644 index 0000000..cb6cc4f --- /dev/null +++ b/colloscope/templates/profil_non_associe.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block title %}Sélection du profil{% endblock title %} + +{% block header %} +

Sélection du profil

+{% endblock header %} + +{% block main %} + +Vous êtes connecté, mais votre compte n'est associé à aucun profil. Veuillez contacter le webmestre à l'adresse valentin@mp2i-vms.fr. + +{% endblock main %} diff --git a/colloscope/templates/trunk.html b/colloscope/templates/trunk.html new file mode 100644 index 0000000..78f0b9c --- /dev/null +++ b/colloscope/templates/trunk.html @@ -0,0 +1,4 @@ +{% extends "base.html" %} + +{% block header %} +{% endblock header %} diff --git a/colloscope/test.pdf b/colloscope/test.pdf deleted file mode 100644 index a3a217c8e39e55104989b4945d3161f8e4d7e44f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5327 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 diff --git a/colloscope/urls.py b/colloscope/urls.py index 24355a6..70df17a 100644 --- a/colloscope/urls.py +++ b/colloscope/urls.py @@ -2,7 +2,8 @@ from django.urls import path from . import views urlpatterns = [ - path("", views.colloscope, name="colloscope"), - path("export", views.export, name="export"), - path("choix_profil", views.choix_profil, name="choix_profil") + path("", views.home_redirect, name="colloscope.home"), + path("table.html", views.colloscope, name="colloscope.table"), + path("export.pdf", views.export, name="colloscope.export"), + path("choix_profil", views.choix_profil, name="colloscope.choix_profil"), ] diff --git a/colloscope/views.py b/colloscope/views.py index 0573fa2..05b0a29 100644 --- a/colloscope/views.py +++ b/colloscope/views.py @@ -1,7 +1,10 @@ from datetime import timedelta +from django.shortcuts import redirect from django.http import HttpResponse from django.template import loader +from django.contrib.auth.decorators import login_required + from colloscope.models import * from colloscope.table import table_colloscope from colloscope.pdfexport import main # /!\ temporaire @@ -14,18 +17,59 @@ def handler404(request): return HttpResponse(template.render(context)) +def home_redirect(request): + return redirect("/colloscope/table.html") + + +@login_required def choix_profil(request): - template = loader.get_template("choix_profil.html") + user = request.user + session = request.session - utilisateur = Utilisateur.objects.get(id=1) - - context = { - "user": utilisateur, - } - return HttpResponse(template.render(context)) + if not Profil.objects.filter(utilisateur=user).exists(): + profil = Profil(utilisateur=user) + profil.save() + else: + profil = Profil.objects.get(utilisateur=user) + if profil.etudiant is not None and profil.colleur is None: + session["profil"] = "etudiant" + return redirect("/colloscope/") + elif profil.colleur is not None and profil.etudiant is None: + session["profil"] = "colleur" + return redirect("/colloscope/") + else: + if profil.etudiant is not None: + template = loader.get_template("choix_profil.html") + else: + template = loader.get_template("profil_non_associe.html") + + context = { + "profil": profil, + } + return HttpResponse(template.render(context)) + + +@login_required def colloscope(request): + user = request.user + session = request.session + + match session.get("profil"): + case "etudiant": + profil = Profil.objects \ + .select_related("etudiant__classe") \ + .prefetch_related("etudiant__classe__periode_set") \ + .get(utilisateur=user) + etudiant = profil.etudiant + case "colleur": + return HttpResponse("pas (encore) supporté") + case _: + return redirect("/colloscope/choix_profil") + + + """ periode_str = request.GET.get("periode", "") if periode_str=="": periode = Periode.objects.get(id=3) @@ -38,8 +82,14 @@ def colloscope(request): response = HttpResponse(template.render(context, request)) response.status_code = 404 return response + """ + + periode = etudiant.classe.periode_set.order_by("-debut").first() + + creneaux = Creneau.objects \ + .filter(periode=periode, est_colle=True) \ + .prefetch_related("rotation_set") - creneaux = Creneau.objects.filter(periode=periode, est_colle=True) semaines = periode.range_semaines() rotations = [ (c, []) for c in creneaux ] for c, l in rotations: @@ -77,6 +127,7 @@ def colloscope(request): return HttpResponse(template.render(context, request)) + def export(request): return HttpResponse(bytes(main().output()), content_type="application/pdf") diff --git a/kholles_web/urls.py b/kholles_web/urls.py index 166821d..497c974 100644 --- a/kholles_web/urls.py +++ b/kholles_web/urls.py @@ -14,10 +14,14 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ -from django.contrib import admin +from django.contrib import admin, auth from django.urls import include, path +from colloscope.views import home_redirect + urlpatterns = [ + path('', home_redirect, name="home"), path('colloscope/', include('colloscope.urls')), path('admin/', admin.site.urls), + path('comptes/', include("django.contrib.auth.urls")), ] diff --git a/static/main.css b/static/main.css index 309f005..f275b84 100644 --- a/static/main.css +++ b/static/main.css @@ -1,10 +1,46 @@ +* { + box-sizing: border-box; +} + body { font-family: sans-serif; + background-color: #fafafa; margin: 0; } +header .bandeau { + display: block; + background-color: #333; + color: white; + padding: 10px; +} + +header .bandeau form { + display: inline; +} + +header .bandeau button { + border: none; + background-color: gold; + color: black; + padding: 5px 10px; + margin: 0 5px; + border-radius: 3px; +} + +header .bandeau button:hover { + background-color: goldenrod; +} + +header .bandeau button:active { + background-color: darkgoldenrod; +} + main { - margin: 5px; + margin: auto; + width: clamp(350px, 60%, 1200px); + background-color: white; + padding: 10px; } h1 { diff --git a/static/table.css b/static/table.css index f420455..ee3edad 100644 --- a/static/table.css +++ b/static/table.css @@ -1,3 +1,7 @@ +.table-wrapper { + overflow-x: auto; +} + table { border: 1px solid #eee; border-collapse: collapse; diff --git a/colloscope/templates/404.html b/templates/404.html similarity index 100% rename from colloscope/templates/404.html rename to templates/404.html diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..14ad01f --- /dev/null +++ b/templates/base.html @@ -0,0 +1,38 @@ +{% load static %} + + + + + + {% block title %}{% endblock title %} + + {% block head %}{% endblock head %} + + +
+ {% if request.user.is_authenticated %} +
+ Vous êtes connecté avec le compte {{ user.username }}. + {% if request.session.profil == "etudiant" %} + Profil actuel : étudiant. + {% elif request.session.profil == "colleur" %} + Profil actuel : colleur. + {% else %} + Pas de profil. + {% endif %} +
+ {% csrf_token %} + +
+
+ {% endif %} + + {% block header %}{% endblock header %} +
+
+ {% block main %}{% endblock main %} +
+
+ {% block footer %}© UKS 2024{% endblock footer %} +
+ diff --git a/templates/registration/login.html b/templates/registration/login.html new file mode 100644 index 0000000..7e9e882 --- /dev/null +++ b/templates/registration/login.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} + +{% block title %}Se connecter{% endblock title %} + +{% block main %} + +

Se connecter

+ +{% if form.errors %} +

Your username and password didn't match. Please try again.

+{% endif %} + +{% if next %} + {% if user.is_authenticated %} +

Your account doesn't have access to this page. To proceed, + please login with an account that has access.

+ {% else %} +

Please login to see this page.

+ {% endif %} +{% endif %} + +
+{% csrf_token %} + + + + + + + + + +
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
+ + + +
+ +{# Assumes you set up the password_reset view in your URLconf #} +

Mot de passe oublié ?

+ +{% endblock main %} + + diff --git a/test.pdf b/test.pdf index f6509b6cae935e75732c6a583a7166606dac82d5..e512fc85a5c5a3a41b9c070d96ba8b4350d2813a 100644 GIT binary patch delta 118 zcmaE7^3G(#QweTUBMSqIC___qBa6)+BorBA%}h)!jLaQPU7TEvmEGnreN=@T3Gch*hQdM>JcjE#8e-j>6 delta 118 zcmaE7^3G(#QweS(14C1@C_^K41H;W9BorBAEzQi_Tn#PU44ll(jGPVKj18Pjjob_j rUCi9v3|&p!>}&|Ch~=`g<0>vmEGnreN=@T3Gch*hQdM>JcjE#8kVPK6