Compare commits

...

18 Commits

20 changed files with 431 additions and 2655 deletions

View File

@ -1,2 +1,35 @@
# kholles-web # Colloscope : Votre colloscope. En ligne.
Certifié le dernier colloscope dont vous aurez besoin. Avec ses fonctionalités de synchronisation, il reste
toujours à jour pour vous permettre d'aborder vos colles sereinement, si vous connaissez votre cours (apprentissage
du cours non fourni).
## Soyez le premier informé lors d'une modification
Lorsqu'une colle est modifiée, le modification se propage à l'ensemble des pages visibles par les utilisateurs.
Aucune excuse pour manquer sa colle.
## Échangez vos colles en toute confianc
Vous ne pouvez pas venir à une colle ? Aucun problème : il vous suffit de l'échanger !
Le *Marketplace* intégré vous donne la possibilité de récupérer des colles disponibles.
## Un système interopérable
Vous pouvez synchroniser vos colles avec votre application de calendrier favorite. Il lui suffit de supporter
les liens iCalendar. C'est le cas de l'application Calendrier sur iOS, de OneCalendar sur Android et de
Mozilla Thunderbird sur GNU/Linux et macOS (et Microsoft Windows).
## Pensé par un nerd, pour les nerds.
Un système complet d'API REST vous permet d'intégrer ce colloscope à vos programmes tiers.
## Libre, pour toujours.
Colloscope est distribué avec la licence GNU Affero GPL. Cette licence garantit vos quatre libertés, à savoir :
0. Exécutez le code librement ;
1. Modifiez le code librement ;
2. Distribuez le code librement ;
3. Distribuez librement des versions modifiées du code.

View File

@ -1,4 +1,5 @@
from django.contrib import admin from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from colloscope.models import * from colloscope.models import *
@ -31,18 +32,59 @@ class ColleInline(admin.StackedInline):
@admin.register(Slot) @admin.register(Slot)
class SlotAdmin(admin.ModelAdmin): class SlotAdmin(admin.ModelAdmin):
list_display = ('subject', 'colleur', "term", 'view_day', "time", "duration") list_display = ('subject', 'colleur', "term", 'get_day', "time", "duration")
list_filter = ("subject", "colleur", "term") list_filter = ("subject", "colleur", "term")
inlines = [ColleInline] inlines = [ColleInline]
def view_day(self, obj): def get_day(self, obj):
jours = ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"] jours = ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"]
return jours[obj.day] return jours[obj.day]
view_day.short_description = 'Day' get_day.short_description = _('Day')
class SwapInline(admin.StackedInline):
model = Swap
raw_id_fields = ("colle",)
@admin.register(Colle)
class ColleAdmin(admin.ModelAdmin):
list_display = ('get_subject', 'get_colleur', 'get_room', 'datetime',)
list_filter = ('slot',)
inlines = [SwapInline]
def get_subject(self, obj):
return obj.slot.subject
def get_colleur(self, obj):
return obj.slot.colleur
def get_room(self, obj):
return obj.slot.room
get_subject.short_description = _('Subject')
get_colleur.short_description = _('Colleur')
get_room.short_description = _('Room')
@admin.register(Swap)
class SwapAdmin(admin.ModelAdmin):
list_display = ('get_subject', 'get_colleur', 'get_datetime', 'enroll', 'student')
list_filter = ('enroll', 'student')
def get_subject(self, obj):
return obj.colle.slot.subject
get_subject.short_description = _('Subject')
def get_colleur(self, obj):
return obj.colle.slot.colleur
get_subject.short_description = _('Colleur')
def get_datetime(self, obj):
return obj.colle.datetime
get_subject.short_description = _('Heure')
admin.site.register(Colle)
admin.site.register(Swap)
admin.site.register(Profile) admin.site.register(Profile)
admin.site.register(CalendarLink) admin.site.register(CalendarLink)

View File

@ -9,16 +9,16 @@ from django.db import models
from django.db.models import F, Q, Count, QuerySet, Subquery, OuterRef, Sum from django.db.models import F, Q, Count, QuerySet, Subquery, OuterRef, Sum
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.conf import settings from django.conf import settings
from django.utils.translation import gettext_lazy as _
from discord import Webhook from discord import Webhook
calendar = { calendar = {
"C": [ "C": [
(date(2023, 10, 21), date(2023, 11, 6)), (date(2024, 10, 19), date(2024, 11, 4)),
(date(2023, 12, 23), date(2024, 1, 8)), (date(2024, 12, 21), date(2025, 1, 6)),
(date(2024, 2, 10), date(2024, 2, 26)), (date(2025, 2, 15), date(2025, 3, 3)),
(date(2024, 4, 6), date(2024, 4, 22)), (date(2025, 4, 12), date(2025, 4, 28)),
(date(2024, 5, 6), date(2024, 5, 11)), # pont un peu gratté
] ]
} }
@ -212,7 +212,7 @@ class Group(models.Model):
members = models.ManyToManyField("Student", through="Member") members = models.ManyToManyField("Student", through="Member")
def __str__(self): def __str__(self):
return self.description return f"{self.description} ({self.type})"
"""def get_colles(self): """def get_colles(self):
return Rotation.objects.filter(slot__term=self.term, return Rotation.objects.filter(slot__term=self.term,
@ -270,6 +270,9 @@ class Member(models.Model):
student = models.ForeignKey(Student, on_delete=models.CASCADE) student = models.ForeignKey(Student, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE) group = models.ForeignKey(Group, on_delete=models.CASCADE)
def __str__(self):
return f"{self.student} of {self.group}"
class Colleur(models.Model): class Colleur(models.Model):
gender = models.CharField(max_length=1) gender = models.CharField(max_length=1)
@ -383,6 +386,9 @@ class Colle(models.Model):
# func = async_to_sync(swap.notify) # func = async_to_sync(swap.notify)
# func() # func()
def week_number(self):
return self.slot.term.cls.week_number(self.datetime.date())
def __str__(self): def __str__(self):
return f"Colle {self.slot.subject} ({self.slot.colleur}); {self.datetime} {self.slot.time} {self.slot.room}. Groupe(s) {{{'; '.join(str(groupe) for groupe in self.groups.all())}}}" return f"Colle {self.slot.subject} ({self.slot.colleur}); {self.datetime} {self.slot.time} {self.slot.room}. Groupe(s) {{{'; '.join(str(groupe) for groupe in self.groups.all())}}}"
@ -408,22 +414,7 @@ class Profile(models.Model):
colleur = models.ForeignKey(Colleur, null=True, blank=True, on_delete=models.SET_NULL) colleur = models.ForeignKey(Colleur, null=True, blank=True, on_delete=models.SET_NULL)
def __str__(self): def __str__(self):
return f"Profil {self.user} : {self.student} ; {self.colleur}" return "Student" if self.student is not None else "Colleur"
@staticmethod
def from_request(request, preprocess=lambda query: query):
user = request.user
session = request.session
match session.get("profile"):
case "student":
profil = preprocess(Profile.objects.filter(user=user)).get()
return profil.student
case "colleur":
profil = preprocess(Profile.objects.filter(user=user)).get()
return profil.colleur
case _:
raise ValueError("profil non choisi")
class CalendarLink(models.Model): class CalendarLink(models.Model):

View File

@ -26,7 +26,7 @@ class PDF(FPDF):
row.cell(etu.last_name.upper()) # Nom row.cell(etu.last_name.upper()) # Nom
row.cell(etu.first_name) # Prénom row.cell(etu.first_name) # Prénom
row.cell(etu.colle_group(term).description) # Groupe row.cell(etu.colle_group(term).description) # Groupe
row.cell(etu.group_of_type(term, "td").description) #row.cell(etu.group_of_type(term, "td").description)
#row.cell("??") # LV1 #row.cell("??") # LV1
#row.cell("??") # LV2 #row.cell("??") # LV2
@ -39,9 +39,9 @@ class PDF(FPDF):
with self.table( with self.table(
align="LEFT", align="LEFT",
width=190, width=270,
line_height=3, line_height=3,
col_widths=(2, 1, 1, 3, 1, *(1,)*len(weeks)), col_widths=(1.2, 1.2, 0.8, 2.4, 1, *(1,)*len(weeks)),
num_heading_rows=2 if heading else 0, num_heading_rows=2 if heading else 0,
first_row_as_headings=heading) as table: first_row_as_headings=heading) as table:
@ -55,7 +55,7 @@ class PDF(FPDF):
header2 = table.row() header2 = table.row()
for lundi in lundis: for lundi in lundis:
header2.cell(lundi.strftime("%d/%m/%y"), align="CENTER") header2.cell(lundi.strftime("%d/%m"), align="CENTER")
for i, c in enumerate(slots): for i, c in enumerate(slots):
@ -99,7 +99,7 @@ def generate(term):
pdf.set_line_width(0.1) pdf.set_line_width(0.1)
base_y = pdf.t_margin + 10 base_y = pdf.t_margin + 10
pdf.set_y(base_y) pdf.set_y(base_y)
pdf.liste_eleves(term) #pdf.liste_eleves(term)
pdf.set_y(base_y) pdf.set_y(base_y)
pdf.table_colloscope(term) pdf.table_colloscope(term)
pdf.y += 3 pdf.y += 3
@ -110,12 +110,10 @@ def generate(term):
def handle(request): def handle(request):
try: try:
student = Profile.from_request( student = (Student.objects
request, .select_related("cls")
preprocess=lambda query: query \ .prefetch_related("cls__term_set")
.select_related("student__cls") \ .get(profile__user=request.user))
.prefetch_related("student__cls__term_set")
)
except ValueError: except ValueError:
return redirect("colloscope.select_profile") return redirect("colloscope.select_profile")

View File

@ -0,0 +1,71 @@
{% extends "base.html" %}
{% load static %}
{% load extras %}
{% block title %}Tableau de bord{% endblock title %}
{% block main %}
{% block intro %}
{% endblock %}
{% if colles %}
{% regroup colles by week_number as week_list %}
{% for week in week_list %}
<h3 class="week" id="week-no-{{ week.grouper }}">
<a href="#week-no-{{ week.grouper }}">Semaine {{ week.grouper }}</a>
</h3>
<div class="colle-wrapper">
{% for colle in week.list %}
<div class="colle">
<ul>
<li><i class="fa-solid fa-graduation-cap"></i> {{ colle.slot.subject }}</li>
<li><i class="fa-solid fa-person-chalkboard"></i> {{ colle.slot.colleur }}</li>
<li><i class="fa-solid fa-clock"></i> {{ colle.datetime|date:"l"|title }} {{ colle.datetime|date:"DATETIME_FORMAT" }}</li>
<li>
<details>
<summary><i class="fa-solid fa-users"></i>
{{ colle.groups.all | print_manager | safe }}
({{ colle.volume }} / {{ colle.slot.capacity }})</summary>
{% if colle.final_group.exists %}
<ul>
{% for member in colle.final_group.all %}
<li>{{ member }}</li>
{% endfor %}
</ul>
{% else %}
Personne n'est inscrit à cette colle.
{% endif %}
</details>
</li>
<li><i class="fa-solid fa-earth-americas"></i> {{ colle.slot.room }}</li>
{% if False %}
<li><i class="fa-solid fa-circle-exclamation"></i>
<form
action="{% url "colloscope:withdraw" %}"
method="POST"
onsubmit="return confirm('Êtes-vous sûr de vouloir vous désinscrire de la colle {{ colle }} ');">
{% csrf_token %}
<input type="hidden" name="colle_id" value="{{ colle.id }}">
<button type="submit">Rendre disponible</button>
</form>
</li>
{% endif %}
</ul>
</div>
{% endfor %}
</div>
{% endfor %}
{% else %}
Aucune colle.
{% endif %}
{% endblock main %}

View File

@ -13,30 +13,33 @@
Bienvenue {{ student }}. Votre lycée est {{ term.cls.school.description }}, et votre classe est {{ term.cls.description }}. Bienvenue {{ student }}. Votre lycée est {{ term.cls.school.description }}, et votre classe est {{ term.cls.description }}.
</p> </p>
<p>Période actuelle : {{ term }}. Votre groupe de colle est {{ group }}. <a href="table.html">Consulter le colloscope</a></p> <p>Période actuelle : {{ term }}. Votre groupe de colle est {{ group }}. <a href="{% url "colloscope:table" %}">Consulter le colloscope</a></p>
<h2>Mes colles</h2> <h2>Mes colles</h2>
<p><a href="{{ calendar_link }}"><i class="fa-regular fa-calendar"></i> Exporter en .ics (ceci est un permalien public)</a></p> <p><a href="{{ calendar_link }}"><i class="fa-regular fa-calendar"></i> Exporter en .ics (ceci est un permalien public)</a></p>
<p><a href="{% url "colloscope.marketplace" %}">Accéder au marketplace</a></p> <p><a href="{% url "colloscope:marketplace" %}">Accéder au marketplace</a></p>
{% for n, lundi, colles in colles_per_sem %} {% for n, lundi, colles in colles_per_sem %}
{% if colles %} {% if colles %}
<h3 class="week">Semaine {{n}} ({{lundi}})</h3> <h3 class="week" id="week-no-{{ n }}">
<a href="#week-no-{{ n }}">Semaine {{n}} ({{lundi}})</a>
</h3>
<div class="colle-wrapper"> <div class="colle-wrapper">
{% for colle in colles %} {% for colle in colles %}
<div class="colle"> <div class="colle">
<span class="summary">{{ colle.slot.subject }} ({{ colle.slot.colleur }})</span>
<ul> <ul>
<li><i class="fa-solid fa-clock"></i> Le {{ colle.datetime|date:"l" }} {{ colle.datetime|date:"DATETIME_FORMAT" }}</li> <li><i class="fa-solid fa-graduation-cap"></i> {{ colle.slot.subject }}</li>
<li><i class="fa-solid fa-person-chalkboard"></i> {{ colle.slot.colleur }}</li>
<li><i class="fa-solid fa-clock"></i> {{ colle.datetime|date:"l"|title }} {{ colle.datetime|date:"DATETIME_FORMAT" }}</li>
<li><i class="fa-solid fa-users"></i> {{ colle.groups.all | print_manager | safe }} ({{ colle.volume }} / {{ colle.slot.capacity }})</li> <li><i class="fa-solid fa-users"></i> {{ colle.groups.all | print_manager | safe }} ({{ colle.volume }} / {{ colle.slot.capacity }})</li>
<li><i class="fa-solid fa-earth-americas"></i> {{ colle.slot.room }}</li> <li><i class="fa-solid fa-earth-americas"></i> {{ colle.slot.room }}</li>
<li><i class="fa-solid fa-circle-exclamation"></i> <li><i class="fa-solid fa-circle-exclamation"></i>
<form <form
action="{% url "colloscope.withdraw" %}" action="{% url "colloscope:withdraw" %}"
method="POST" method="POST"
onsubmit="return confirm('Êtes-vous sûr de vouloir vous désinscrire de la colle {{ colle }} ');"> onsubmit="return confirm('Êtes-vous sûr de vouloir vous désinscrire de la colle {{ colle }} ');">
{% csrf_token %} {% csrf_token %}

View File

@ -15,13 +15,14 @@
<div class="colle-wrapper"> <div class="colle-wrapper">
{% for colle in colles %} {% for colle in colles %}
<div class="colle"> <div class="colle">
<span class="summary">{{ colle.slot.subject }} ({{ colle.slot.colleur }})</span>
<ul> <ul>
<li><i class="fa-solid fa-clock"></i> Le {{ colle.datetime|date:"l" }} {{ colle.datetime|date:"DATETIME_FORMAT" }}</li> <li><i class="fa-solid fa-graduation-cap"></i> {{ colle.slot.subject }}</li>
<li><i class="fa-solid fa-person-chalkboard"></i> {{ colle.slot.colleur }}</li>
<li><i class="fa-solid fa-clock"></i> {{ colle.datetime|date:"l"|title }} {{ colle.datetime|date:"DATETIME_FORMAT" }}</li>
<li><i class="fa-solid fa-users"></i> {{ colle.groups.all | print_manager | safe }} ({{ colle.volume }} / {{ colle.slot.capacity }})</li> <li><i class="fa-solid fa-users"></i> {{ colle.groups.all | print_manager | safe }} ({{ colle.volume }} / {{ colle.slot.capacity }})</li>
<li><i class="fa-solid fa-earth-americas"></i> {{ colle.slot.room }}</li> <li><i class="fa-solid fa-earth-americas"></i> {{ colle.slot.room }}</li>
<li><i class="fa-solid fa-circle-exclamation"></i> <li><i class="fa-solid fa-circle-exclamation"></i>
<form action="{% url "colloscope.enroll" %}" <form action="{% url "colloscope:enroll" %}"
method="POST" method="POST"
onsubmit="return confirm('Êtes-vous sûr de vouloir vous inscrire à la colle {{ colle }} ');"> onsubmit="return confirm('Êtes-vous sûr de vouloir vous inscrire à la colle {{ colle }} ');">
{% csrf_token %} {% csrf_token %}

View File

@ -13,12 +13,12 @@
<h1>Colloscope</h1> <h1>Colloscope</h1>
<p> <p>
Lycée : {{ term.cls.school.description }}. Classe : {{ term.cls.description }}. <a href="dashboard.html">Retour au tableau de bord</a> Lycée : {{ term.cls.school.description }}. Classe : {{ term.cls.description }}. <a href="{% url "colloscope:table" %}">Retour au tableau de bord</a>
</p> </p>
<h2>Colloscope : {{ term.description }}</h2> <h2>Colloscope : {{ term.description }}</h2>
<form method="get" action="{% url "colloscope.table" %}"> <form method="get" action="{% url "colloscope:table" %}">
Changer de période&nbsp;: Changer de période&nbsp;:
<select name="term" id="term"> <select name="term" id="term">
{% for p in term.cls.term_set.all %} {% for p in term.cls.term_set.all %}
@ -33,9 +33,9 @@
</form> </form>
{% if request.GET.term %} {% if request.GET.term %}
<a href="export.pdf?term={{ request.GET.term }}" target="_blank">Exporter le colloscope</a> <a href="{% url 'colloscope:export' %}?term={{ request.GET.term }}" target="_blank">Exporter le colloscope</a>
{% else %} {% else %}
<a href="export.pdf" target="_blank">Exporter le colloscope</a> <a href="{% url 'colloscope:export' %}" target="_blank">Exporter le colloscope</a>
{% endif %} {% endif %}
<div class="table-wrapper"> <div class="table-wrapper">

View File

@ -1,14 +1,20 @@
from django.urls import path from django.urls import path
from django.shortcuts import redirect
from . import views from . import views
from .views import ColleListView
urlpatterns = [ urlpatterns = [
path("", views.home_redirect, name="colloscope.home"), path("", lambda req: redirect("colloscope:dashboard"), name="home"),
path("table.html", views.colloscope, name="colloscope.table"), path("table/", views.colloscope, name="table"),
path("dashboard.html", views.dashboard, name="colloscope.dashboard"), path("dashboard/", views.dashboard, name="dashboard"),
path("export.pdf", views.export, name="colloscope.export"), path("export.pdf", views.export, name="export"),
path("export/calendar/<str:key>/calendar.ics", views.icalendar, name="colloscope.calendar.ics"), path("export/calendar/<str:key>/calendar.ics", views.icalendar, name="export-ics"),
path("select_profile", views.select_profile, name="colloscope.select_profile"), path("calendrier.ics",
path("marketplace.html", views.marketplace, name="colloscope.marketplace"), lambda req: redirect("colloscope:export-ics", key=req.GET.get("key")), name="export-ics-old"),
path("action/enroll", views.enroll, name="colloscope.enroll"), path("marketplace/", views.marketplace, name="marketplace"),
path("action/withdraw", views.withdraw, name="colloscope.withdraw"), path("action/enroll/", views.enroll, name="enroll"),
path("action/withdraw/", views.withdraw, name="withdraw"),
path("listing/<int:term>/", ColleListView.as_view(), name="colles"),
path("listing/<int:term>/by_subject/<int:subject>/", ColleListView.as_view(), name="colles_by_subject"),
path("listing/<int:term>/by_colleur/<int:colleur>/", ColleListView.as_view(), name="colles_by_colleur"),
] ]

View File

@ -1,11 +1,14 @@
from uuid import uuid4 from uuid import uuid4
from django import forms from django import forms
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.http import HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect
from django.template import loader from django.template import loader
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.views.generic import ListView
from django.utils.translation import gettext_lazy as _
from colloscope.models import * from colloscope.models import *
from colloscope.pdfexport import handle from colloscope.pdfexport import handle
@ -13,66 +16,41 @@ from colloscope.icalexport import to_calendar
def handler404(request): def handler404(request):
template = loader.get_template("404.html")
context = {} context = {}
return HttpResponse(template.render(context), status=404) return render(request, '404.html', context, status=404)
def home_redirect(request): class ColleListView(ListView):
return redirect("/colloscope/dashboard.html") model = Colle
context_object_name = "colles"
def get_queryset(self):
term = Term.objects.get(pk=self.kwargs.get("term"))
base_query = (term.cls
.current_term()
.query_colles()
.filter(datetime__gte=date.today()))
@login_required if self.kwargs.get("subject") is not None:
def select_profile(request): print(base_query)
user = request.user print(self.kwargs.get("subject"))
session = request.session base_query = base_query.filter(slot__subject__id=self.kwargs.get("subject"))
if self.kwargs.get("colleur") is not None:
base_query = base_query.filter(slot__colleur__id=self.kwargs.get("colleur"))
if not Profile.objects.filter(user=user).exists(): return base_query
profile = Profile(user=user)
profile.save()
else:
profile = Profile.objects.get(user=user)
if profile.student is not None and profile.colleur is None:
session["profile"] = "student"
return redirect("/colloscope/")
elif profile.colleur is not None and profile.student is None:
session["profile"] = "colleur"
return redirect("/colloscope/")
else:
if profile.student is not None:
template = loader.get_template("select_profile.html")
else:
template = loader.get_template("unbound_profile.html")
context = {
"profile": profile,
}
return HttpResponse(template.render(context))
def get_lien_calendrier(student, term):
try:
lien = CalendarLink.objects.get(student=student, term=term)
except CalendarLink.DoesNotExist:
key = uuid4().hex
lien = CalendarLink(key=key, student=student, term=term)
lien.save()
return f"calendrier.ics?key={lien.key}"
@login_required @login_required
def dashboard(request): def dashboard(request):
try: try:
student = Profile.from_request( student = (Student.objects
request, .select_related("cls")
preprocess=lambda query: (query .prefetch_related("cls__term_set")
.select_related("student__cls") .get(profile__user=request.user))
.prefetch_related("student__cls__term_set"))
)
except ValueError: except ValueError:
return redirect("colloscope.select_profile") return redirect("colloscope:select_profile")
if not isinstance(student, Student): if not isinstance(student, Student):
return HttpResponse("pas encore supporté") return HttpResponse("pas encore supporté")
@ -117,17 +95,15 @@ class WithdrawForm(AmendForm):
@login_required @login_required
def marketplace(request): def marketplace(request):
try: try:
student = Profile.from_request( student = (Student.objects
request, .select_related("cls")
preprocess=lambda query: (query .prefetch_related("cls__term_set")
.select_related("student__cls") .get(profile__user=request.user))
.prefetch_related("student__cls__term_set"))
)
except ValueError: except ValueError:
return redirect("colloscope.select_profile") return redirect("colloscope:select_profile")
if not isinstance(student, Student): if not isinstance(student, Student):
return HttpResponse("pas encore supporté") return HttpResponse(_("Not supported yet."))
term = student.cls.current_term() term = student.cls.current_term()
colles = term.query_colles_not_full_excluding_student(student) colles = term.query_colles_not_full_excluding_student(student)
@ -142,14 +118,12 @@ def marketplace(request):
@login_required @login_required
def colloscope(request): def colloscope(request):
try: try:
student = Profile.from_request( student = (Student.objects
request, .select_related("cls")
preprocess=lambda query: (query .prefetch_related("cls__term_set")
.select_related("student__cls") .get(profile__user=request.user))
.prefetch_related("student__cls__term_set"))
)
except ValueError: except ValueError:
return redirect("colloscope.select_profile") return redirect("colloscope:select_profile")
if not isinstance(student, Student): if not isinstance(student, Student):
return HttpResponse("pas encore supporté") return HttpResponse("pas encore supporté")
@ -237,12 +211,10 @@ def icalendar(request, key):
def amend(request, colle_id, do_enroll): def amend(request, colle_id, do_enroll):
try: try:
student = Profile.from_request( student = (Student.objects
request, .select_related("cls")
preprocess=lambda query: (query .prefetch_related("cls__term_set")
.select_related("student__cls") .get(profile__user=request.user))
.prefetch_related("student__cls__term_set"))
)
except ValueError: except ValueError:
return redirect("colloscope.choix_profil") return redirect("colloscope.choix_profil")

View File

@ -52,6 +52,7 @@ INSTALLED_APPS = [
"rest_framework", "rest_framework",
'rest_framework_simplejwt', 'rest_framework_simplejwt',
'colloscope', 'colloscope',
"front",
"drf_spectacular", "drf_spectacular",
] ]
@ -124,6 +125,7 @@ LANGUAGE_CODE = 'fr'
TIME_ZONE = 'Europe/Paris' TIME_ZONE = 'Europe/Paris'
USE_I18N = True USE_I18N = True
USE_L10N = True
USE_TZ = True USE_TZ = True
@ -156,8 +158,8 @@ STATICFILES_FINDERS = [
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_URL = "/accounts/login" LOGIN_URL = "/accounts/login"
LOGIN_REDIRECT_URL = "home" LOGIN_REDIRECT_URL = "colloscope:dashboard"
LOGOUT_REDIRECT_URL = "home" LOGOUT_REDIRECT_URL = "front:index"
DISCORD_NOTIFY_WEBHOOK_URL = "https://discord.com/api/webhooks/YOUR_URL" DISCORD_NOTIFY_WEBHOOK_URL = "https://discord.com/api/webhooks/YOUR_URL"
DISCORD_NOTIFY_WEBHOOK_USERNAME = "Watchdog" DISCORD_NOTIFY_WEBHOOK_USERNAME = "Watchdog"

View File

@ -16,16 +16,15 @@ Including another URLconf
""" """
from django.contrib import admin, auth from django.contrib import admin, auth
from django.urls import include, path from django.urls import include, path
from django.shortcuts import redirect from django.shortcuts import redirect, render
from django.contrib.staticfiles import views as vstatic from django.contrib.staticfiles import views as vstatic
from rest_framework import routers from rest_framework import routers
from rest_framework_simplejwt.views import ( from rest_framework_simplejwt.views import (
TokenObtainPairView, TokenObtainPairView,
TokenRefreshView, TokenRefreshView,
) )
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from colloscope.views import home_redirect
from colloscope.viewsets import * from colloscope.viewsets import *
router = routers.SimpleRouter() router = routers.SimpleRouter()
@ -43,7 +42,9 @@ router.register("calendarlink", CalendarLinkViewset, basename='calendarlink')
urlpatterns = [ urlpatterns = [
path('', home_redirect, name="home"), path('', lambda req: render(req, "index.html", {}), name="home"),
path("__debug__/", include("debug_toolbar.urls")),
path('api-auth/', include('rest_framework.urls')), path('api-auth/', include('rest_framework.urls')),
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
@ -55,7 +56,7 @@ urlpatterns = [
path("oauth2/", include('oauth2_provider.urls', namespace='oauth2_provider')), path("oauth2/", include('oauth2_provider.urls', namespace='oauth2_provider')),
path("favicon.ico", lambda req: vstatic.serve(req, "favicon.ico")), path("favicon.ico", lambda req: vstatic.serve(req, "favicon.ico")),
path('colloscope/', include('colloscope.urls')), path('colloscope/', include(('colloscope.urls', "colloscope"), namespace="colloscope")),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('accounts/', include("django.contrib.auth.urls")), path('accounts/', include("django.contrib.auth.urls")),
] ]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@ defusedxml==0.7.1
discord.py==2.3.2 discord.py==2.3.2
Django==5.0.4 Django==5.0.4
django-cors-headers==4.3.1 django-cors-headers==4.3.1
django-debug-toolbar==4.3.0
django-oauth-toolkit==2.3.0 django-oauth-toolkit==2.3.0
django-smtp-ssl==1.0 django-smtp-ssl==1.0
djangorestframework==3.15.1 djangorestframework==3.15.1

View File

@ -1,68 +1,91 @@
import csv import csv
from datetime import date, time, timedelta from datetime import date, time, timedelta
from colloscope.models import * from colloscope.models import *
from zoneinfo import ZoneInfo
tz = ZoneInfo("Europe/Paris")
def digest_time(h1):
a = h1.split("h")
return time(hour=int(a[0]), minute=int(a[1]) if a[1] != '' else 0)
def scrape(periode, chemin): def scrape(periode, chemin):
with open(chemin, "r") as file: with open(chemin, "r") as file:
reader = csv.reader(file) reader = csv.reader(file)
headers, *colloscope = list(reader) header1, header2, *colloscope = list(reader)
for l in colloscope: for l in colloscope:
print(l) print(l)
for colleur, matiere, jour, heure, *(rotations) in colloscope: for matiere, infos, *(rotations) in colloscope:
nom_colleur = colleur.lstrip("Mme.").title() if len(infos.split()) == 5:
civilite = "M" if colleur.startswith("M.") else "F" civ1, nom_colleur, j1, h1, salle = infos.split()
if not Colleur.objects.filter(nom=nom_colleur, civilite=civilite).exists():
c = Colleur(civilite=civilite, nom=nom_colleur)
c.save()
else: else:
c = Colleur.objects.get(nom=nom_colleur, civilite=civilite) civ1, nom_colleur, j1, h1, *_ = infos.split()
salle = "ns"
if not Subject.objects.filter(classe=periode.classe, libelle=matiere).exists(): civilite = "M" if civ1 == "M." else "F"
m = Subject(classe=periode.classe, libelle=matiere, code=matiere.upper())
m.save()
else:
m = Subject.objects.get(classe=periode.classe, libelle=matiere)
jours_dict = {"dimanche": 0, "lundi": 1, "mardi": 2, "mercredi": 3, "jeudi": 4, "vendredi": 5, "samedi": 6} #print(civilite, nom_colleur, j1, h1, salle, rotations)
j = jours_dict[jour]
h = time(int(heure[:-1]), 0)
if matiere=="InfoTP": colleur_obj, _ = Colleur.objects.get_or_create(
d = timedelta(hours=2) name=nom_colleur,
c2 = 9 gender=civilite,
else: )
d = timedelta(hours=1)
c2 = 3
print(f"--> Traitement de {c=}, {m=}, {j=}, {h=}, {d=}, {c2=}") jours_dict = {"di": 0, "lu": 1, "ma": 2, "me": 3, "je": 4, "ve": 5, "sa": 6}
if not Slot.objects.filter(periode=periode, jour=j, heure=h, duree=d, matiere=m, colleur=c, est_colle=True, capacite=c2).exists(): subject_obj, _ = Subject.objects.get_or_create(
creneau = Slot(periode=periode, jour=j, heure=h, duree=d, salle="nc", matiere=m, colleur=c, est_colle=True, capacite=c2) cls=periode.cls,
creneau.save() description=matiere
else: )
creneau = Slot.objects.get(periode=periode, jour=j, heure=h, duree=d, matiere=m, colleur=c, est_colle=True, capacite=c2)
for i, r in enumerate(rotations): #print(repr(subject_obj))
sem = headers[4+i].split("/") time_ = digest_time(h1)
sem[2] = "20"+sem[2]
sem.reverse()
s = date.fromisoformat("-".join(sem)) + (j-1) * timedelta(days=1) slot_obj, _ = Slot.objects.get_or_create(
term=periode,
day=jours_dict[j1],
time=time_,
duration=timedelta(hours=1),
room=salle,
subject=subject_obj,
colleur=colleur_obj,
type=GroupType.objects.get_or_create(term=periode, description="colle")[0],
capacity=3
)
if not Colle.objects.filter(creneau=creneau, date=s): print("----------------")
rot = Colle(creneau=creneau, date=s) print(slot_obj)
rot.save()
else:
rot = Colle.objects.get(creneau=creneau, date=s) for i, rot in enumerate(rotations):
if rot=="":
continue
groupe, _ = Group.objects.get_or_create(
term=periode,type=GroupType.objects.get_or_create(term=periode, description="colle")[0],
description=rot
)
date_raw = [int(a) for a in header2[i+2].split("/")]
#print(date_raw)
date_ = date(2025 if date_raw[1] < 9 else 2024, date_raw[1], date_raw[0]) + timedelta(days=jours_dict[j1] - 1)
time_processed = datetime(day=date_.day, month=date_.month, year=date_.year, hour=time_.hour, minute=time_.minute, second=time_.second, tzinfo=tz)
#print(time_processed)
#print(f"{i}) {rot}, {header2[i+2]}")
colle_obj, _ = Colle.objects.get_or_create(
slot = slot_obj,
datetime = time_processed
)
colle_obj.groups.add(groupe)
print(colle_obj)
rot.groupes.add(Group.objects.get(libelle=r))
def main(): def main():
periode = Term.objects.get(id=3) periode = Term.objects.get(id=5)
scrape(periode, "colloscope.csv") scrape(periode, "colloscope.csv")
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -7,6 +7,15 @@ body {
margin: 0; margin: 0;
} }
a:link, a:visited {
color: blue;
text-decoration: underline;
}
a:link:active, a:visited:active {
color: darkblue;
}
header .bandeau { header .bandeau {
display: block; display: block;
background-color: #333; background-color: #333;
@ -79,7 +88,7 @@ header .bandeau button:active {
} }
main { main {
margin: 20px auto; margin: 0 auto;
width: clamp(350px, 60%, 1200px); width: clamp(350px, 60%, 1200px);
background-color: white; background-color: white;
padding: 10px; padding: 10px;
@ -129,7 +138,11 @@ nav.semaine .select, nav.semaine .label {
text-align: center; text-align: center;
} }
nav.semaine .select { background-color: dodgerblue; color: white; width: 1em; } nav.semaine .select {
background-color: dodgerblue;
color: white;
width: 1em;
}
nav.semaine .select:hover { background-color: #0077ea; } nav.semaine .select:hover { background-color: #0077ea; }
nav.semaine .label:hover { nav.semaine .label:hover {
@ -144,13 +157,26 @@ p.programme {
footer { footer {
text-align: center; text-align: center;
margin: 20px 0;
} }
.week { .week {
background-color: dodgerblue; background-color: dodgerblue;
color: white; color: white;
padding: 5px; padding: 5px 10px;
border-radius: 5px;
transition: background-color 200ms;
}
.week a, .week a:hover, .week a:active {
color: white;
text-decoration: none;
}
.week:hover {
background-color: #0077ea;
} }
.week.empty { .week.empty {
@ -159,7 +185,7 @@ footer {
.colle-wrapper { .colle-wrapper {
display: grid; display: grid;
gap: 10px; gap: 15px;
} }
@media screen and (min-width: 400px) @media screen and (min-width: 400px)
@ -170,8 +196,15 @@ footer {
} }
.colle { .colle {
border: 1px solid black; padding: 15px;
padding: 10px; border-radius: 8px;
box-shadow: 0 3px 6px rgba(0,0,0,0.04),0 3px 6px rgba(0,0,0,0.0575);
transition: background-color 200ms;
}
.colle:hover {
background-color: #fafafa;
} }
.colle span { .colle span {
@ -180,6 +213,7 @@ footer {
.colle ul { .colle ul {
padding: 0; padding: 0;
margin: 0;
} }
.colle li { .colle li {

@ -1 +1 @@
Subproject commit 9e2427d5ae8e7fafef9fe001394e9566e181f217 Subproject commit 811b224d6a8a644dffd6c34cb54acbbd1a0f4db0

View File

@ -32,20 +32,27 @@
<div class="navbar"> <div class="navbar">
<div class="block"> <div class="block">
<a href="{% url "home" %}">
<div class="link"><i class="fa-solid fa-home"></i> Accueil</div>
</a>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<a href="{% url "colloscope.dashboard" %}"> <a href="{% url "colloscope:dashboard" %}">
<div class="link"><i class="fa-solid fa-rocket"></i> Tableau de bord</div> <div class="link"><i class="fa-solid fa-rocket"></i> Tableau de bord</div>
</a> </a>
<a href="{% url "colloscope.table" %}"> <a href="{% url "colloscope:table" %}">
<div class="link"><i class="fa-solid fa-calendar"></i> Colloscope</div> <div class="link"><i class="fa-solid fa-calendar"></i> Colloscope</div>
</a> </a>
<a href="{% url "colloscope.marketplace" %}"> <a href="{% url "colloscope:marketplace" %}">
<div class="link"><i class="fa-solid fa-shop"></i> Marketplace</div> <div class="link"><i class="fa-solid fa-shop"></i> Marketplace</div>
</a> </a>
{% endif %} {% endif %}
</div> </div>
<div class="block"> <div class="block">
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<div class="link">
<i class="fa-solid fa-user"></i> {{ user.username }} ({{ request.user.profile }})
</div>
<form action="{% url 'logout' %}" method="post"> <form action="{% url 'logout' %}" method="post">
{% csrf_token %} {% csrf_token %}
<button class="link" type="submit" href="{% url "login" %}"> <button class="link" type="submit" href="{% url "login" %}">
@ -66,6 +73,6 @@
{% block main %}{% endblock main %} {% block main %}{% endblock main %}
</main> </main>
<footer> <footer>
{% block footer %}&copy; colles.mp2i-vms.fr 2024 - <a href="https://git.mp2i-vms.fr/mp2i-vms/kholles-web" target="_blank">Code source</a> - <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank">Licence GNU AGPL v3 or later</a>{% endblock footer %} {% block footer %}&copy; colles.mp2i-vms.fr 2024 - <a href="https://git.mp2i-vms.fr/mp2i-vms/colles.mp2i-vms.fr" target="_blank">Code source</a> - <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank">Licence GNU AGPL v3 or later</a>{% endblock footer %}
</footer> </footer>
</body> </body>

50
templates/index.html Normal file
View File

@ -0,0 +1,50 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}Accueil{% endblock %}
{% block main %}
<h1>{% translate "Your colloscope. Online." %}</h1>
<p>
Certifié le dernier colloscope dont vous aurez besoin. Avec ses fonctionalités de synchronisation, il reste
toujours à jour pour vous permettre d'aborder vos colles sereinement, si vous connaissez votre cours (apprentissage
du cours non fourni).
</p>
<h2>Soyez le premier informé lors d'une modification</h2>
<p>
Lorsqu'une colle est modifiée, le modification se propage à l'ensemble des pages visibles par les utilisateurs.
Aucune excuse pour manquer sa colle.
</p>
<h2>Échangez vos colles en toute confiance</h2>
<p>
Vous ne pouvez pas venir à une colle&nbsp;? Aucun problème&nbsp;: il vous suffit de l'échanger&nbsp;!
Le <em>Marketplace</em> intégré vous donne la possibilité de récupérer des colles disponibles.
</p>
<h2>Un système interopérable</h2>
<p>
Vous pouvez synchroniser vos colles avec votre application de calendrier favorite. Il lui suffit de supporter
les liens iCalendar. C'est le cas de l'application Calendrier sur iOS, de OneCalendar sur Android et de
Mozilla Thunderbird sur GNU/Linux et macOS (et Microsoft Windows).
</p>
<h2>Pensé par un nerd, pour les nerds.</h2>
<p>
Un système complet d'API REST vous permet d'intégrer ce colloscope à vos programmes tiers.
</p>
<h2>Libre, pour toujours.</h2>
<p>
Colloscope est distribué avec la licence GNU Affero GPL. Cette licence garantit vos quatre libertés, à savoir&nbsp;:
</p>
<ol start=0>
<li>Exécutez le code librement&nbsp;;</li>
<li>Modifiez le code librement&nbsp;;</li>
<li>Distribuez le code librement&nbsp;;</li>
<li>Distribuez librement des versions modifiées du code.</li>
</ol>
{% endblock %}