From 6f888390e4a981d5dd1b07a3e1af20f3e4453e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Mogu=C3=A9rou?= Date: Mon, 15 Apr 2024 03:02:49 +0200 Subject: [PATCH] =?UTF-8?q?G=C3=A9n=C3=A9ration=20du=20pdf=20=C3=A0=20part?= =?UTF-8?q?ir=20de=20la=20base=20de=20donn=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ode_options_remove_appartenance_periode.py | 21 +++++ .../migrations/0003_remove_creneau_classe.py | 17 ++++ colloscope/migrations/0004_matiere_classe.py | 20 +++++ colloscope/models.py | 73 ++++++++++++++++-- colloscope/pdfexport.py | 60 +++++++------- test.pdf | Bin 3890 -> 4520 bytes 6 files changed, 151 insertions(+), 40 deletions(-) create mode 100644 colloscope/migrations/0002_alter_periode_options_remove_appartenance_periode.py create mode 100644 colloscope/migrations/0003_remove_creneau_classe.py create mode 100644 colloscope/migrations/0004_matiere_classe.py diff --git a/colloscope/migrations/0002_alter_periode_options_remove_appartenance_periode.py b/colloscope/migrations/0002_alter_periode_options_remove_appartenance_periode.py new file mode 100644 index 0000000..74077ac --- /dev/null +++ b/colloscope/migrations/0002_alter_periode_options_remove_appartenance_periode.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0.4 on 2024-04-15 00:12 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('colloscope', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='periode', + options={'ordering': ['debut']}, + ), + migrations.RemoveField( + model_name='appartenance', + name='periode', + ), + ] diff --git a/colloscope/migrations/0003_remove_creneau_classe.py b/colloscope/migrations/0003_remove_creneau_classe.py new file mode 100644 index 0000000..2b41ab9 --- /dev/null +++ b/colloscope/migrations/0003_remove_creneau_classe.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.4 on 2024-04-15 00:14 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('colloscope', '0002_alter_periode_options_remove_appartenance_periode'), + ] + + operations = [ + migrations.RemoveField( + model_name='creneau', + name='classe', + ), + ] diff --git a/colloscope/migrations/0004_matiere_classe.py b/colloscope/migrations/0004_matiere_classe.py new file mode 100644 index 0000000..24a29e8 --- /dev/null +++ b/colloscope/migrations/0004_matiere_classe.py @@ -0,0 +1,20 @@ +# Generated by Django 5.0.4 on 2024-04-15 00:17 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('colloscope', '0003_remove_creneau_classe'), + ] + + operations = [ + migrations.AddField( + model_name='matiere', + name='classe', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='colloscope.classe'), + preserve_default=False, + ), + ] diff --git a/colloscope/models.py b/colloscope/models.py index f09f7f3..26b2d3a 100644 --- a/colloscope/models.py +++ b/colloscope/models.py @@ -1,5 +1,17 @@ +from math import ceil +from datetime import date, datetime, timedelta + from django.db import models -from django.db.models import F +from django.db.models import F, Q + +calendrier = { + "C" : [ + ( date(2023, 10, 21), date(2023, 11, 6) ), + ( date(2023, 12, 23), date(2024, 1, 8) ), + ( date(2024, 2, 10), date(2024, 2, 26) ), + ( date(2024, 4, 6), date(2024, 4, 22) ), + ] +} class Lycee(models.Model): uai = models.CharField(max_length=10) @@ -19,7 +31,40 @@ class Periode(models.Model): debut = models.DateField() fin = models.DateField() + class Meta: + ordering = ["debut"] + + + def no_semaine(self, jour): + zone = self.classe.lycee.vacances + vacances = calendrier[zone] + jour0 = self.classe.jour_zero + + n = 1 + ((jour - jour0).days)//7 + for debut, fin in vacances: + if jour > debut: + n -= 2 + return n + + def range_semaines(self): + return range(self.no_semaine(self.debut), self.no_semaine(self.fin)+1) + + def date_debut_sem(self, n): + zone = self.classe.lycee.vacances + vacances = calendrier[zone] + jour0 = self.classe.jour_zero + + jour = jour0 + (n-1) * timedelta(weeks=1) + + for debut, _ in vacances: + if jour >= debut: + jour += 2*timedelta(weeks=1) + + return jour + + class Matiere(models.Model): + classe = models.ForeignKey(Classe, on_delete=models.CASCADE) libelle = models.CharField(max_length=100) code = models.CharField(max_length=20) @@ -35,6 +80,9 @@ class Groupe(models.Model): critere = models.ForeignKey(Critere, null=True, on_delete=models.CASCADE) libelle = models.CharField(max_length=100) + def membres(self): + return Etudiant.objects.filter(id__in=Appartenance.objects.filter(groupe=self)) + class Etudiant(models.Model): class Meta: ordering=["classe", "nom", "prenom"] @@ -46,19 +94,18 @@ class Etudiant(models.Model): #lv2 = models.ForeignKey(Matiere, on_delete=models.CASCADE) def appartient(self, groupe, periode): - return Appartenance.objects.filter(periode=periode, etudiant=self, groupe=groupe).exists() + return Appartenance.objects.filter(etudiant=self, groupe=groupe).exists() def groupe_du_critere(self, periode, critere): if isinstance(critere, str): critere = Critere.objects.get(periode=periode, libelle=critere) - return Appartenance.objects.get(periode=periode, etudiant=self, groupe__critere=critere).groupe + return Appartenance.objects.get(groupe__periode=periode, etudiant=self, groupe__critere=critere).groupe def groupe_de_colle(self, periode): return self.groupe_du_critere(periode, "colle") class Appartenance(models.Model): - periode = models.ForeignKey(Periode, on_delete=models.CASCADE) etudiant = models.ForeignKey(Etudiant, on_delete=models.CASCADE) groupe = models.ForeignKey(Groupe, on_delete=models.CASCADE) @@ -67,12 +114,11 @@ class Colleur(models.Model): nom = models.CharField(max_length=100) class Creneau(models.Model): - classe = models.ForeignKey(Classe, on_delete=models.CASCADE) + periode = models.ForeignKey(Periode, on_delete=models.CASCADE) jour = models.IntegerField() heure = models.TimeField() duree = models.DurationField() salle = models.CharField(max_length=20) - periode = models.ForeignKey(Periode, on_delete=models.CASCADE) matiere = models.ForeignKey(Matiere, on_delete=models.CASCADE) colleur = models.ForeignKey(Colleur, on_delete=models.CASCADE) est_colle = models.BooleanField() @@ -83,6 +129,21 @@ class Rotation(models.Model): groupes = models.ManyToManyField(Groupe) semaine = models.IntegerField() + def effectif(self): + n_base = sum(len(groupe.membres()) for groupe in self.groupes.all()) + n_plus = len(Amendement.objects.filter(est_positif=True, rotation=self)) + n_moins = len(Amendement.objects.filter(est_positif=False, rotation=self)) + + return n_base + n_plus - n_moins + + def est_pleine(self): + eff = self.effectif() + + if eff>self.creneau.capacite: + raise models.IntegrityError + else: + return eff==self.creneau.capacite + class Amendement(models.Model): est_positif = models.BooleanField() rotation = models.ForeignKey(Rotation, on_delete=models.CASCADE) diff --git a/colloscope/pdfexport.py b/colloscope/pdfexport.py index 087f29e..3cf3134 100644 --- a/colloscope/pdfexport.py +++ b/colloscope/pdfexport.py @@ -1,29 +1,11 @@ from datetime import date, timedelta -from colloscope.models import Etudiant, Critere, Classe +from colloscope.models import Etudiant, Critere, Classe, Rotation, Periode, Creneau from fpdf import FPDF from fpdf.fonts import FontFace from fpdf.enums import TableCellFillMode -calendrier_zoneC = date(2023, 9, 18), [ - ( date(2023, 10, 21), date(2023, 11, 6) ), - ( date(2023, 12, 23), date(2024, 1, 8) ), - ( date(2024, 2, 10), date(2024, 2, 26) ), - ( date(2024, 4, 6), date(2024, 4, 22) ), -] - -def jour_of_sem(n, cal): - sem_1, vac = cal - - jour = sem_1 + (n-1) * timedelta(weeks=1) - - for (debut, fin) in vac: - if jour >= debut: - jour += 2*timedelta(weeks=1) - - return jour - """ etudiants = [ ['Aboujaib', 'Alexandre', 4, 'A', '', ''], @@ -124,7 +106,12 @@ class PDF(FPDF): row.cell("??") # LV2 - def table_colloscope(self, creneaux, semaines, rotations): + def table_colloscope(self, periode): + semaines = periode.range_semaines() + lundis = [ periode.date_debut_sem(n) for n in semaines ] + creneaux = Creneau.objects.filter(periode=periode) + jours = ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"] + with self.table( align="LEFT", width=190, @@ -140,24 +127,29 @@ class PDF(FPDF): header.cell(str(sem), align="CENTER") header2 = table.row() - for sem in semaines: - header2.cell(jour_of_sem(sem, calendrier_zoneC).strftime("%d/%m/%y"), align="CENTER") + for lundi in lundis: + header2.cell(lundi.strftime("%d/%m/%y"), align="CENTER") - for i, tr in enumerate(creneaux): - matiere, jour, heure, colleur, salle = tr + + for i, c in enumerate(creneaux): + matiere = c.matiere + jour = c.jour + heure = c.heure + colleur = c.colleur + salle = c.salle row = table.row() - row.cell(matiere) - row.cell(jour) - row.cell(heure) - row.cell(colleur) + row.cell(matiere.libelle) + row.cell(jours[jour]) + row.cell(heure.strftime("%H:%M")) + row.cell("{} {}".format("M." if colleur.civilite=="M" else "Mme", colleur.nom.upper())) row.cell(salle) for s in semaines: - for rot in rotations: - if rot[2] == i and rot[0] == s: - row.cell(str(rot[1]), align="CENTER") - break + if Rotation.objects.filter(creneau=c, semaine=s).exists(): + rot = Rotation.objects.get(creneau=c, semaine=s) + groupes = rot.groupes + row.cell(", ".join(g.libelle for g in groupes.all()), align="CENTER") else: row.cell() @@ -195,8 +187,8 @@ def generate(periode): base_y = pdf.t_margin + 10 pdf.set_y(base_y) pdf.liste_eleves(periode) - #pdf.set_y(base_y) - #pdf.table_colloscope(creneaux, semaines, rotations) + pdf.set_y(base_y) + pdf.table_colloscope(periode) #pdf.y += 3 #pdf.table_travaux() diff --git a/test.pdf b/test.pdf index 12c6f1a24cf2e0443b9b5562b3b23ed18a71c7df..ed61cd46651f737bb88e5d337268bab5a079ef93 100644 GIT binary patch delta 3647 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 F{RaXn2rB>p delta 3013 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 mZH+)WINGEC?;Ae