10 Commits
36 changed files with 1275 additions and 1869 deletions

No files matched your search

+2 -2
View File
@@ -1,7 +1,7 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.16.0 rev: v0.16.10
hooks: hooks:
- id: ruff-check # just check the code, and print the errors - id: ruff-check # just check the code, and print the errors
- id: ruff-check # actually fix the fixable errors, but print nothing - id: ruff-check # actually fix the fixable errors, but print nothing
@@ -12,7 +12,7 @@ repos:
rev: v0.6.1 rev: v0.6.1
hooks: hooks:
- id: biome-check - id: biome-check
additional_dependencies: ["@biomejs/biome@2.5.1"] additional_dependencies: ["@biomejs/biome@2.5.15"]
- repo: https://github.com/rtts/djhtml - repo: https://github.com/rtts/djhtml
rev: 3.0.11 rev: 3.0.11
hooks: hooks:
@@ -1,22 +0,0 @@
# Generated by Django 5.2.17 on 2026-09-24 14:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("core", "0050_alter_sithfile_moderator")]
operations = [
migrations.AddField(
model_name="preferences",
name="show_my_timetable",
field=models.BooleanField(
default=True,
help_text=(
"Allow subscribers to see your timetable and "
"to compare it to theirs (if you have filled it)"
),
verbose_name="show your timetable to others",
),
),
]
-8
View File
@@ -799,14 +799,6 @@ class Preferences(models.Model):
), ),
default=False, default=False,
) )
show_my_timetable = models.BooleanField(
_("show your timetable to others"),
help_text=_(
"Allow subscribers to see your timetable and "
"to compare it to theirs (if you have filled it)"
),
default=True,
)
notify_on_click = models.BooleanField( notify_on_click = models.BooleanField(
_("get a notification for every click"), default=False _("get a notification for every click"), default=False
) )
+1
View File
@@ -42,6 +42,7 @@ details.accordion>.accordion-content {
background: #ffffff; background: #ffffff;
color: #333333; color: #333333;
padding: 1em 2.2em; padding: 1em 2.2em;
margin-top: 0;
border: 1px solid #dddddd; border: 1px solid #dddddd;
border-bottom-right-radius: 3px; border-bottom-right-radius: 3px;
border-bottom-left-radius: 3px; border-bottom-left-radius: 3px;
@@ -29,13 +29,5 @@
</span> </span>
{{ form.show_my_stats.errors }} {{ form.show_my_stats.errors }}
</fieldset> </fieldset>
<fieldset class="form-group">
{{ form.show_my_timetable }}
{{ form.show_my_timetable.label_tag() }}
<span class="helptext">
{{ form.show_my_timetable.help_text }}
</span>
{{ form.show_my_timetable.errors }}
</fieldset>
<input type="submit" class="btn btn-blue" value="{% trans %}Save{% endtrans %}"> <input type="submit" class="btn btn-blue" value="{% trans %}Save{% endtrans %}">
</form> </form>
+2 -10
View File
@@ -336,14 +336,10 @@ class UserVisibilityForm(forms.ModelForm):
__preferences_fields = forms.fields_for_model( __preferences_fields = forms.fields_for_model(
Preferences, Preferences,
["show_my_stats", "show_my_timetable"], ["show_my_stats"],
widgets={ widgets={"show_my_stats": forms.CheckboxInput(attrs={"class": "switch"})},
"show_my_stats": forms.CheckboxInput(attrs={"class": "switch"}),
"show_my_timetable": forms.CheckboxInput(attrs={"class": "switch"}),
},
) )
show_my_stats = __preferences_fields["show_my_stats"] show_my_stats = __preferences_fields["show_my_stats"]
show_my_timetable = __preferences_fields["show_my_timetable"]
def __init__( def __init__(
self, *args, initial: dict | None = None, instance: User | None = None, **kwargs self, *args, initial: dict | None = None, instance: User | None = None, **kwargs
@@ -351,16 +347,12 @@ class UserVisibilityForm(forms.ModelForm):
if instance: if instance:
initial = initial or {} initial = initial or {}
initial["show_my_stats"] = instance.preferences.show_my_stats initial["show_my_stats"] = instance.preferences.show_my_stats
initial["show_my_timetable"] = instance.preferences.show_my_timetable
super().__init__(*args, initial=initial, instance=instance, **kwargs) super().__init__(*args, initial=initial, instance=instance, **kwargs)
def save(self, commit=True) -> User: # noqa: FBT002 def save(self, commit=True) -> User: # noqa: FBT002
instance = super().save(commit=commit) instance = super().save(commit=commit)
if commit: if commit:
instance.preferences.show_my_stats = self.cleaned_data["show_my_stats"] instance.preferences.show_my_stats = self.cleaned_data["show_my_stats"]
instance.preferences.show_my_timetable = self.cleaned_data[
"show_my_timetable"
]
instance.preferences.save() instance.preferences.save()
return instance return instance
+82 -72
View File
@@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-21 13:16+0200\n" "POT-Creation-Date: 2026-10-02 12:56+0200\n"
"PO-Revision-Date: 2016-07-18\n" "PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n" "Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -832,7 +832,7 @@ msgstr "Méthode de paiement"
#: counter/templates/counter/last_ops.jinja #: counter/templates/counter/last_ops.jinja
#: election/templates/election/election_detail.jinja #: election/templates/election/election_detail.jinja
#: forum/templates/forum/macros.jinja #: forum/templates/forum/macros.jinja
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja #: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
#: pedagogy/templates/pedagogy/guide.jinja sas/templates/sas/album.jinja #: pedagogy/templates/pedagogy/guide.jinja sas/templates/sas/album.jinja
#: sas/templates/sas/moderation.jinja sas/templates/sas/picture.jinja #: sas/templates/sas/moderation.jinja sas/templates/sas/picture.jinja
#: trombi/templates/trombi/detail.jinja #: trombi/templates/trombi/detail.jinja
@@ -1028,7 +1028,7 @@ msgstr "Outils"
#: counter/templates/counter/counter_list.jinja #: counter/templates/counter/counter_list.jinja
#: election/templates/election/election_detail.jinja #: election/templates/election/election_detail.jinja
#: forum/templates/forum/macros.jinja #: forum/templates/forum/macros.jinja
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja #: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
#: pedagogy/templates/pedagogy/guide.jinja sas/templates/sas/album.jinja #: pedagogy/templates/pedagogy/guide.jinja sas/templates/sas/album.jinja
#: trombi/templates/trombi/detail.jinja #: trombi/templates/trombi/detail.jinja
#: trombi/templates/trombi/edit_profile.jinja #: trombi/templates/trombi/edit_profile.jinja
@@ -1285,8 +1285,9 @@ msgstr "Actions"
#: com/templates/com/mailing_admin.jinja com/templates/com/poster_list.jinja #: com/templates/com/mailing_admin.jinja com/templates/com/poster_list.jinja
#: core/templates/core/file_detail.jinja #: core/templates/core/file_detail.jinja
#: core/templates/core/file_moderation.jinja sas/templates/sas/moderation.jinja #: core/templates/core/file_moderation.jinja
#: sas/templates/sas/picture.jinja #: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
#: sas/templates/sas/moderation.jinja sas/templates/sas/picture.jinja
msgid "Moderate" msgid "Moderate"
msgstr "Modérer" msgstr "Modérer"
@@ -1588,7 +1589,6 @@ msgstr "Descendre"
#: com/templates/com/weekmail_preview.jinja #: com/templates/com/weekmail_preview.jinja
#: core/templates/core/user_account_detail.jinja #: core/templates/core/user_account_detail.jinja
#: pedagogy/templates/pedagogy/ue_detail.jinja
#: trombi/templates/trombi/comment_moderation.jinja #: trombi/templates/trombi/comment_moderation.jinja
#: trombi/templates/trombi/export.jinja #: trombi/templates/trombi/export.jinja
msgid "Back" msgid "Back"
@@ -1933,8 +1933,8 @@ msgstr ""
"toujours le voir." "toujours le voir."
#: core/models.py #: core/models.py
msgid "ToS approved" msgid "ToS approved at"
msgstr "CGU approuvées" msgstr "CGU approuvées le"
#: core/models.py #: core/models.py
msgid "A user with that username already exists" msgid "A user with that username already exists"
@@ -1943,7 +1943,9 @@ msgstr "Un utilisateur de ce nom d'utilisateur existe déjà"
#: core/models.py core/templates/core/macros.jinja #: core/models.py core/templates/core/macros.jinja
#: core/templates/core/user_detail.jinja core/templates/core/user_edit.jinja #: core/templates/core/user_detail.jinja core/templates/core/user_edit.jinja
#: election/templates/election/election_detail.jinja #: election/templates/election/election_detail.jinja
#: forum/templates/forum/macros.jinja trombi/templates/trombi/user_tools.jinja #: forum/templates/forum/macros.jinja
#: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
#: trombi/templates/trombi/user_tools.jinja
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -2374,7 +2376,6 @@ msgstr "Propriétés"
#: core/templates/core/file_delete_confirm.jinja #: core/templates/core/file_delete_confirm.jinja
#: counter/templates/counter/fragments/delete_student_card.jinja #: counter/templates/counter/fragments/delete_student_card.jinja
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
#, python-format #, python-format
msgid "Are you sure you want to delete \"%(obj)s\"?" msgid "Are you sure you want to delete \"%(obj)s\"?"
msgstr "Êtes-vous sûr de vouloir supprimer \"%(obj)s\" ?" msgstr "Êtes-vous sûr de vouloir supprimer \"%(obj)s\" ?"
@@ -3907,7 +3908,6 @@ msgid "Emptied"
msgstr "Coffre vidé" msgstr "Coffre vidé"
#: counter/templates/counter/cash_summary_list.jinja counter/views/cash.py #: counter/templates/counter/cash_summary_list.jinja counter/views/cash.py
#: pedagogy/templates/pedagogy/fragments/ue_comment_form.jinja
#: pedagogy/templates/pedagogy/moderation.jinja #: pedagogy/templates/pedagogy/moderation.jinja
#: trombi/templates/trombi/comment.jinja #: trombi/templates/trombi/comment.jinja
#: trombi/templates/trombi/user_tools.jinja #: trombi/templates/trombi/user_tools.jinja
@@ -5239,6 +5239,7 @@ msgid "reporter"
msgstr "signalant" msgstr "signalant"
#: pedagogy/templates/pedagogy/fragments/comment_report.jinja #: pedagogy/templates/pedagogy/fragments/comment_report.jinja
#: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
msgid "Report" msgid "Report"
msgstr "Signaler" msgstr "Signaler"
@@ -5246,45 +5247,46 @@ msgstr "Signaler"
msgid "Leave comment" msgid "Leave comment"
msgstr "Laisser un commentaire" msgstr "Laisser un commentaire"
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja #: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
#: trombi/templates/trombi/export.jinja #: trombi/templates/trombi/export.jinja
msgid "Comments" msgid "Comments"
msgstr "Commentaires" msgstr "Commentaires"
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja #: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
msgid "Global grade"
msgstr "Note globale"
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
msgid "Utility"
msgstr "Utilité"
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
msgid "Interest"
msgstr "Intérêt"
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
msgid "Teaching"
msgstr "Enseignement"
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
msgid "Work load"
msgstr "Charge de travail"
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
msgid "This comment has been reported" msgid "This comment has been reported"
msgstr "Ce commentaire a été signalé" msgstr "Ce commentaire a été signalé"
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja #: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
msgid "Report this comment" msgid "It will be hidden until moderated."
msgstr "Signaler ce commentaire" msgstr "Il sera caché tant qu'il ne sera pas modéré."
#: pedagogy/templates/pedagogy/fragments/ue_details/form.jinja #: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
#: pedagogy/templates/pedagogy/fragments/ue_detail/grade.jinja
msgid "Global grade"
msgstr "Note globale"
#: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
#: pedagogy/templates/pedagogy/fragments/ue_detail/grade.jinja
msgid "Utility"
msgstr "Utilité"
#: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
#: pedagogy/templates/pedagogy/fragments/ue_detail/grade.jinja
msgid "Interest"
msgstr "Intérêt"
#: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
#: pedagogy/templates/pedagogy/fragments/ue_detail/grade.jinja
msgid "Teaching"
msgstr "Enseignement"
#: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
#: pedagogy/templates/pedagogy/fragments/ue_detail/grade.jinja
msgid "Workload"
msgstr "Charge de travail"
#: pedagogy/templates/pedagogy/fragments/ue_detail/ue_detail.jinja
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "" msgid ""
"You already posted a comment on this UE. If you want to comment again, " "You already posted a comment on this UE. If you want to comment again, "
"please modify or delete your previous comment." "please modify or delete your previous comment."
@@ -5292,26 +5294,6 @@ msgstr ""
"Vous avez déjà commenté cette UE. Si vous voulez de nouveau commenter, " "Vous avez déjà commenté cette UE. Si vous voulez de nouveau commenter, "
"veuillez modifier ou supprimer votre commentaire précédent." "veuillez modifier ou supprimer votre commentaire précédent."
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
msgid "Objectives"
msgstr "Objectifs"
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
msgid "Program"
msgstr "Programme"
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
msgid "Earned skills"
msgstr "Compétences acquises"
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
msgid "Key concepts"
msgstr "Concepts clefs"
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
msgid "UE manager: "
msgstr "Gestionnaire d'UE : "
#: pedagogy/templates/pedagogy/guide.jinja #: pedagogy/templates/pedagogy/guide.jinja
msgid "A guide of courses available at UTBM." msgid "A guide of courses available at UTBM."
msgstr "Un guide de tous les cours disponibles à l'UTBM." msgstr "Un guide de tous les cours disponibles à l'UTBM."
@@ -5377,24 +5359,52 @@ msgid "UE Details"
msgstr "Détails d'UE" msgstr "Détails d'UE"
#: pedagogy/templates/pedagogy/ue_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "CM: " msgid "Category"
msgstr "CM : " msgstr "Catégorie"
#: pedagogy/templates/pedagogy/ue_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "TD: " msgid "Credits"
msgstr "TD : " msgstr "Crédits"
#: pedagogy/templates/pedagogy/ue_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "TP: " msgid "Lectures"
msgstr "TP : " msgstr "Cours magistraux"
#: pedagogy/templates/pedagogy/ue_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "TE: " msgid "Tutorials"
msgstr "TE : " msgstr "Travaux dirigés"
#: pedagogy/templates/pedagogy/ue_detail.jinja #: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "THE: " msgid "Practicals"
msgstr "THE : " msgstr "Travaux pratiques"
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Work without supervision"
msgstr "Travail hors encadrement"
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Availability"
msgstr "Disponibilité"
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Manager"
msgstr "Responsable"
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Objectives"
msgstr "Objectifs"
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Program"
msgstr "Programme"
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Earned skills"
msgstr "Compétences acquises"
#: pedagogy/templates/pedagogy/ue_detail.jinja
msgid "Key concepts"
msgstr "Concepts clefs"
#: pedagogy/templates/pedagogy/ue_edit.jinja #: pedagogy/templates/pedagogy/ue_edit.jinja
msgid "Edit UE" msgid "Edit UE"
+385 -355
View File
File diff suppressed because it is too large. Load diff
+14 -14
View File
@@ -24,24 +24,24 @@
"#com:*": "./com/static/bundled/*" "#com:*": "./com/static/bundled/*"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^8.0.1", "@babel/core": "^8.0.6",
"@babel/preset-env": "^8.0.2", "@babel/preset-env": "^8.0.6",
"@biomejs/biome": "^2.5.1", "@biomejs/biome": "^2.5.15",
"@hey-api/openapi-ts": "^0.99.0", "@hey-api/openapi-ts": "^0.99.0",
"@types/alpinejs": "^3.13.11", "@types/alpinejs": "^3.17.0",
"@types/alpinejs__sort": "^3.13.0", "@types/alpinejs__sort": "^3.13.0",
"@types/cytoscape-cxtmenu": "^3.4.5", "@types/cytoscape-cxtmenu": "^3.4.5",
"@types/cytoscape-klay": "^3.1.5", "@types/cytoscape-klay": "^3.1.5",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/node": "^26.2.0", "@types/node": "^26.6.4",
"@typescript/native": "npm:typescript@^7.0.2", "@typescript/native": "npm:typescript@^7.0.2",
"rollup-plugin-visualizer": "^7.1.1", "rollup-plugin-visualizer": "^7.1.1",
"typescript": "npm:@typescript/typescript6@^6.0.2", "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.2.2" "vite": "^8.3.2"
}, },
"dependencies": { "dependencies": {
"@ae_utbm/aemark": "^0.1.3", "@ae_utbm/aemark": "^0.1.3",
"@alpinejs/sort": "^3.16.2", "@alpinejs/sort": "^3.17.4",
"@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0", "@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
"@floating-ui/dom": "^1.8.0", "@floating-ui/dom": "^1.8.0",
"@fortawesome/fontawesome-free": "^7.3.1", "@fortawesome/fontawesome-free": "^7.3.1",
@@ -49,13 +49,13 @@
"@fullcalendar/daygrid": "^6.1.21", "@fullcalendar/daygrid": "^6.1.21",
"@fullcalendar/icalendar": "^6.1.21", "@fullcalendar/icalendar": "^6.1.21",
"@fullcalendar/list": "^6.1.21", "@fullcalendar/list": "^6.1.21",
"@sentry/browser": "^10.70.0", "@sentry/browser": "^11.3.0",
"@zip.js/zip.js": "^2.8.57", "@zip.js/zip.js": "^2.22.0",
"3d-force-graph": "^1.80.0", "3d-force-graph": "^1.80.1",
"alpinejs": "^3.16.2", "alpinejs": "^3.17.4",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"country-flag-emoji-polyfill": "^0.1.10", "country-flag-emoji-polyfill": "^0.1.10",
"cytoscape": "^3.34.1", "cytoscape": "^3.34.3",
"cytoscape-cxtmenu": "^3.5.0", "cytoscape-cxtmenu": "^3.5.0",
"cytoscape-klay": "^3.1.4", "cytoscape-klay": "^3.1.4",
"d3-force-3d": "^3.0.6", "d3-force-3d": "^3.0.6",
@@ -66,8 +66,8 @@
"js-cookie": "^3.0.8", "js-cookie": "^3.0.8",
"lit-html": "^3.3.3", "lit-html": "^3.3.3",
"native-file-system-adapter": "^3.0.1", "native-file-system-adapter": "^3.0.1",
"temporal-polyfill": "^1.0.4", "temporal-polyfill": "^1.0.5",
"three": "^0.185.1", "three": "^0.186.1",
"three-spritetext": "^1.10.0", "three-spritetext": "^1.10.0",
"tom-select": "^2.6.2" "tom-select": "^2.6.2"
} }
+110 -346
View File
@@ -1,52 +1,22 @@
@import "core/static/core/devices"; @import "core/static/core/devices";
@import "core/static/core/colors"; @import "core/static/core/colors";
$pedagogy-blue: #1bb9ea;
$pedagogy-orange: #ea7900; $pedagogy-orange: #ea7900;
$pedagogy-hover-blue: #0e97ce;
$pedagogy-light-blue: #caf0ff;
$pedagogy-white-text: #f0f0f0; $pedagogy-white-text: #f0f0f0;
.pedagogy { .pedagogy {
&.star-not-checked { .star-not-checked {
color: #f7f7f7; color: lightgray;
margin-bottom: 0; margin-bottom: 0;
margin-top: 0; margin-top: 0;
} }
&.star-checked { .star-checked {
color: $pedagogy-orange; color: $pedagogy-orange;
margin-bottom: 0; margin-bottom: 0;
margin-top: 0; margin-top: 0;
} }
&.grade-without-star {
display: none;
}
@media screen and (max-width: $large-devices) {
&.star-not-checked {
margin-left: 5px;
margin-right: 5px;
}
&.star-checked {
margin-left: 5px;
margin-right: 5px;
}
}
@media screen and (max-width: $small-devices) {
&.grade-without-star {
display: block;
}
&.grade-with-star {
display: none;
}
}
#ue-list { #ue-list {
font-size: 1.1em; font-size: 1.1em;
@@ -64,12 +34,10 @@ $pedagogy-white-text: #f0f0f0;
} }
#search_form { #search_form {
.radio-guide fieldset { .radio-guide fieldset {
input[type="checkbox"] { input[type="checkbox"] {
display: none; display: none;
} }
margin-top: 10px; margin-top: 10px;
margin-bottom: 0; margin-bottom: 0;
color: white; color: white;
@@ -85,80 +53,79 @@ $pedagogy-white-text: #f0f0f0;
} }
} }
#ue_detail { &#ue_detail {
color: #062f38; .ue-infos {
display: flex;
.ue-quick-info-container { gap: 2em;
flex-direction: row;
@media screen and (max-width: 700px) {
flex-direction: column;
}
}
.infos-column {
display: flex;
flex-direction: column;
gap: 1em;
min-width: 300px;
}
.info-container {
// Darken the background color just enough to make this part
// stand out a little, while not being too perceptible
background: darken($white-color, 1%);
display: grid; display: grid;
grid-template-columns: 20% 20% 20% 20% auto; grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto; border-radius: 5px;
grid-template-areas: padding: .75rem 1rem ;
"hours-cm hours-td hours-tp hours-te hours-the" grid-gap: .5rem;
"department credit-type semester . ."; @include shadow;
.info-container-item {
display: grid;
grid-column: span 2;
grid-template-rows: subgrid;
grid-template-columns: subgrid;
grid-column-gap: 1rem;
div {
display: flex;
align-items: center;
} }
.department { :first-child {
grid-area: department; justify-content: right;
text-align: right;
}
} }
.credit-type { hr {
grid-area: credit-type; grid-column: span 2;
} width: 50%;
margin: auto;
.semester {
grid-area: semester;
}
.hours-cm {
grid-area: hours-cm;
}
.hours-td {
grid-area: hours-td;
}
.hours-tp {
grid-area: hours-tp;
}
.hours-te {
grid-area: hours-te;
}
.hours-the {
grid-area: hours-the;
}
.leave-comment-not-allowed {
p {
text-align: center;
color: red;
} }
} }
.leave-comment { .leave-comment {
.leave-comment-grid-container { .comment-form-content {
display: grid; display: flex;
grid-template-columns: 270px auto; gap: 2em;
grid-template-rows: 100%;
grid-template-areas: "stars comment";
@media screen and (max-width: $large-devices) { @media screen and (max-width: $large-devices) {
grid-template-columns: 100%; gap: 1em;
grid-template-rows: auto auto; flex-direction: column;
grid-template-areas:
"stars"
"comment";
} }
} }
.form-stars { .form-stars {
grid-area: stars; flex: 0;
min-width: 250px;
display: flex;
align-content: flex-start;
justify-content: center;
flex-wrap: wrap;
gap: 1rem 2em;
height: unset;
} }
.form-comment { .form-comment {
grid-area: comment; flex: 2;
} }
.ui-accordion-header-icon { .ui-accordion-header-icon {
@@ -166,45 +133,18 @@ $pedagogy-white-text: #f0f0f0;
margin-right: 10px; margin-right: 10px;
} }
.input-stars { input[type="submit"] {
margin-top: 20px; display: block;
} margin-left: auto;
} }
.ue-details-container { summary {
display: grid; background: $pedagogy-orange;
grid-template-columns: 150px 130px auto;
grid-template-rows: 156px 1fr;
grid-template-areas:
"grade grade-stars ue-infos"
". . ue-infos";
@media screen and (max-width: $large-devices) {
grid-template-columns: 50% 50%;
grid-template-rows: auto auto;
grid-template-areas:
"grade grade-stars"
"ue-infos ue-infos";
}
}
.grade {
grid-area: grade;
color: $pedagogy-white-text; color: $pedagogy-white-text;
background-color: $pedagogy-blue; @media screen and (min-width: $large-devices) {
padding-right: 10px; clip-path: polygon(0 0%, 0 100%, 30% 100%, 33% 0);
>p {
text-align: right;
font-weight: bold;
} }
} }
.grade-stars {
grid-area: grade-stars;
color: $pedagogy-white-text;
background-color: $pedagogy-blue;
font-weight: bold;
} }
.ue-infos { .ue-infos {
@@ -212,236 +152,60 @@ $pedagogy-white-text: #f0f0f0;
padding-left: 10px; padding-left: 10px;
} }
.comment-container {
display: grid;
grid-template-columns: 300px auto;
grid-template-rows: auto auto auto;
grid-template-areas:
"grade-block comment"
"grade-block info"
"comment-end-bar comment-end-bar";
margin-bottom: 30px;
margin-top: 10px;
@media screen and (max-width: $large-devices) {
grid-template-columns: auto;
grid-template-rows: auto auto auto auto;
grid-template-areas:
"grade-block"
"comment"
"info"
"comment-end-bar";
}
.grade-block {
grid-area: grade-block;
width: 300px;
display: grid;
grid-template-columns: 150px 150px;
grid-template-rows: 156px auto;
grid-template-areas:
"grade-type grade-stars"
"grade-extension grade-extension";
grid-gap: 15px;
clip-path: polygon(0 0, 0 100%, 100% 100%, 100% 30px, 270px 0);
align-items: start;
background-color: $pedagogy-blue;
@media screen and (max-width: $large-devices) {
grid-template-columns: 50% auto;
grid-template-rows: auto;
grid-template-areas: "grade-type grade-stars";
width: auto;
clip-path: none;
align-content: space-evenly;
align-items: end;
}
.grade-extension {
grid-area: grade-extension;
background-color: $pedagogy-blue;
}
.grade-type {
grid-area: grade-type;
>p {
color: $pedagogy-white-text;
font-weight: bold;
text-align: right;
}
}
.grade-stars {
grid-area: grade-stars;
}
}
.comment { .comment {
grid-area: comment; padding: 15px;
margin: 20px;
display: flex;
flex-direction: column;
gap: 10px;
background: $primary-neutral-light-color;
border-radius: 5px;
border: 1px darken($secondary-neutral-light-color, 10%) solid;
display: grid; &.reported {
grid-template-columns: auto; border: #fc8181 1px solid;
grid-template-rows: auto auto;
grid-template-areas:
"anchor"
"markdown";
@media screen and (max-width: $large-devices) {
border-left: solid;
border-right: solid;
border-color: $pedagogy-blue;
} }
.anchor { .comment-header {
grid-area: anchor; display: flex;
text-align: right; gap: 20px;
margin-right: 15px;
img {
width: 50px;
height: 50px;
border-radius: 50%;
} }
.markdown { .comment-metadata {
grid-area: markdown; display: flex;
flex-direction: column;
justify-content: center;
}
min-height: 139px; .comment-options {
margin-top: 0; flex: 2;
margin-right: 0; display: flex;
padding: 10px; justify-content: right;
gap: 15px;
}
}
.comment-stars {
display: flex;
justify-content: space-evenly;
flex-wrap: wrap;
gap: 1em 2em;
padding: 1.5em;
border-top: 0.0625rem grey dotted;
border-bottom: 0.0625rem grey dotted;
}
.comment-content {
text-align: justify; text-align: justify;
overflow: auto; padding: .5em 20px 1em;
}
}
.info {
grid-area: info;
padding-bottom: 10px;
@media screen and (max-width: $large-devices) {
border-left: solid;
border-right: solid;
border-color: $pedagogy-blue;
}
.status-reported {
color: red;
float: left;
padding-left: 10px;
}
.action {
float: right;
margin-top: 0;
}
}
.comment-end-bar {
grid-area: comment-end-bar;
display: grid;
grid-template-columns: 33% auto auto;
grid-template-rows: 2.5em;
grid-template-areas: "author date report";
background-color: $pedagogy-blue;
margin-top: -1px;
@media screen and (max-width: $large-devices) {
grid-template-columns: auto;
grid-template-rows: auto auto auto;
grid-template-areas:
"report"
"date"
"author";
margin-top: 0;
text-align: center;
}
.author {
grid-area: author;
padding-top: 6px;
padding-left: 20px;
background-color: $pedagogy-orange;
clip-path: polygon(0 10px, 0 100%, 350px 200%, 300px 10px);
@media screen and (max-width: $large-devices) {
clip-path: none;
padding: 0;
padding-bottom: 7px;
}
a {
color: $pedagogy-white-text;
font-weight: bold;
}
a:hover {
color: $pedagogy-hover-blue;
}
}
.date {
grid-area: date;
color: $pedagogy-white-text;
@media screen and (max-width: $large-devices) {
padding-bottom: 7px;
}
}
.report {
grid-area: report;
justify-self: right;
padding-right: 30px;
padding-left: 30px;
a {
color: $pedagogy-white-text;
cursor: pointer;
}
a:hover {
color: $pedagogy-hover-blue;
}
@media screen and (max-width: $large-devices) {
text-align: center;
justify-self: inherit;
padding-bottom: 7px;
background-color: $white-color;
border-left: solid;
border-right: solid;
border-color: $pedagogy-blue;
a {
color: $black-color;
} }
} }
} }
}
}
}
}
details.accordion summary {
background: $pedagogy-orange !important;
color: $pedagogy-white-text !important;
clip-path: polygon(0 0%, 0 100%, 30% 100%, 33% 0);
@media screen and (max-width: $large-devices) {
clip-path: none;
}
}
details.accordion>.accordion-content {
background-color: $white-color;
border-color: $pedagogy-orange;
border-right: none;
} }
.buttons { .buttons {
@@ -11,44 +11,39 @@
hx-disabled-elt="find input[type='submit']" hx-disabled-elt="find input[type='submit']"
> >
{% csrf_token %} {% csrf_token %}
<div class="leave-comment-grid-container">
<div class="form-stars">
{{ form.non_field_errors() }} {{ form.non_field_errors() }}
{{ form.author.errors }} {{ form.author.errors }}
{{ form.ue.errors }} {{ form.ue.errors }}
{{ form.author }} {{ form.author }}
{{ form.ue }} {{ form.ue }}
<div class="comment-form-content">
<div class="input-stars"> <div class="form-stars">
<label for="{{ form.grade_global.id_for_label }}">{{ form.grade_global.label }} :</label> <fieldset>
<legend>{{ form.grade_global.label }} :</legend>
{{ form.grade_global.errors }} {{ form.grade_global.errors }}
{{ form.grade_global }} {{ form.grade_global }}
</div> </fieldset>
<fieldset>
<div class="input-stars"> <legend>{{ form.grade_utility.label }} :</legend>
<label for="{{ form.grade_utility.id_for_label }}">{{ form.grade_utility.label }} :</label>
{{ form.grade_utility.errors }} {{ form.grade_utility.errors }}
{{ form.grade_utility }} {{ form.grade_utility }}
</div> </fieldset>
<fieldset>
<div class="input-stars"> <legend>{{ form.grade_interest.label }} :</legend>
<label for="{{ form.grade_interest.id_for_label }}">{{ form.grade_interest.label }} :</label>
{{ form.grade_interest.errors }} {{ form.grade_interest.errors }}
{{ form.grade_interest }} {{ form.grade_interest }}
</div> </fieldset>
<fieldset>
<div class="input-stars"> <legend>{{ form.grade_teaching.label }} :</legend>
<label for="{{ form.grade_teaching.id_for_label }}">{{ form.grade_teaching.label }} :</label>
{{ form.grade_teaching.errors }} {{ form.grade_teaching.errors }}
{{ form.grade_teaching }} {{ form.grade_teaching }}
</div> </fieldset>
<fieldset>
<div class="input-stars"> <legend>{{ form.grade_work_load.label }} :</legend>
<label for="{{ form.grade_work_load.id_for_label }}">{{ form.grade_work_load.label }} :</label>
{{ form.grade_work_load.errors }} {{ form.grade_work_load.errors }}
{{ form.grade_work_load }} {{ form.grade_work_load }}
</div> </fieldset>
</div> </div>
<div class="form-comment"> <div class="form-comment">
<label for="{{ form.comment.id_for_label }}">{{ form.comment.label }} :</label> <label for="{{ form.comment.id_for_label }}">{{ form.comment.label }} :</label>
@@ -56,9 +51,7 @@
{{ form.comment }} {{ form.comment }}
</div> </div>
</div> </div>
<div class="buttons"> <p><input type="submit" class="btn btn-blue"/></p>
<input type="submit" value="{% trans %}Comment{% endtrans %}" />
</div>
</form> </form>
{% if form.is_creation %} {% if form.is_creation %}
@@ -0,0 +1,129 @@
{% from "pedagogy/macros.jinja" import display_star %}
{% from "core/macros.jinja" import user_profile_link %}
{% if comments %}
<h2>{% trans %}Comments{% endtrans %}</h2>
<br>
{% endif %}
<section>
{%- for comment in comments -%}
<article
id="comment-{{ comment.id }}"
class="comment {% if comment.is_reported %}reported{% endif %}"
>
{% if comment.is_reported %}
<div class="alert alert-red">
<div class="alert-main">
<h5 class="alert-title margin-bottom">
{% trans %}This comment has been reported{% endtrans %}
</h5>
<p>{% trans %}It will be hidden until moderated.{% endtrans %}</p>
{% if user.has_perm("pedagogy.view_uecommentreport") %}
{% for report in comment.reports.all() %}
<div class="markdown margin-bottom">
<blockquote>
<em>{{ report.reporter.get_display_name() }}</em>
{{ report.reason|markdown }}
</blockquote>
</div>
{% endfor %}
{% endif %}
</div>
{% if comment.author_id == user.id or user.has_perm("pedagogy.delete_uecomment") %}
<div class="alert-aside">
<a
href="{{ url("pedagogy:comment_delete", comment_id=comment.id) }}"
class="btn btn-red"
>
<i class="fa fa-trash"></i>{% trans %}Delete{% endtrans %}
</a>
<a href="{{ url("pedagogy:moderation") }}" class="btn btn-green">
<i class="fa fa-check"></i>{% trans %}Moderate{% endtrans %}
</a>
</div>
{% endif %}
</div>
{% endif %}
<div class="comment-header">
<img
{% if comment.author.avatar_pict %}
src="{{ comment.author.avatar_pict.get_download_url() }}"
{% elif comment.author.profile_pict %}
src="{{ comment.author.profile_pict.get_download_url() }}"
{% else %}
src="{{ static("core/img/unknown.jpg") }}"
{% endif %}
alt="{% trans %}Profile{% endtrans %}"
/>
<div class="comment-metadata">
<a href="{{ comment.author.get_absolute_url() }}">
<strong>{{ comment.author.get_display_name() }}</strong>
</a>
<time datetime="{{ comment.publish_date.isoformat(timespec="seconds") }}">
{{ comment.publish_date|localtime|date(DATETIME_FORMAT) }}
{{ comment.publish_date|localtime|time(DATETIME_FORMAT) }}
</time>
</div>
<div class="comment-options">
{% if comment.author_id == user.id or user.has_perm("pedagogy.change_uecomment") %}
<a
class="clickable"
hx-get="{{ url('pedagogy:comment_update', comment_id=comment.id) }}"
hx-swap="outerHTML"
hx-target="#comment-{{ comment.id }}"
tooltip="{% trans %}Edit{% endtrans %}"
>
<i class="fa fa-pencil edit-action"></i>
</a>
{% endif %}
{% if not comment.is_reported %}
<a
class="clickable"
hx-get="{{ url('pedagogy:comment_report', comment_id=comment.id) }}"
hx-swap="outerHTML"
hx-target="#comment-{{ comment.id }}"
tooltip="{% trans %}Report{% endtrans %}"
>
<i class="fa fa-exclamation delete-action"></i>
</a>
{% if comment.author_id == user.id or user.has_perm("pedagogy.delete_uecomment") %}
<a
href="{{ url("pedagogy:comment_delete", comment_id=comment.id) }}"
tooltip="{% trans %}Delete{% endtrans %}"
>
<i class="fa fa-trash-can delete-action"></i>
</a>
{% endif %}
{% endif %}
</div>
</div>
<div class="comment-stars">
<div>
<span>{% trans %}Global grade{% endtrans %}</span>
<span>{{ display_star(comment.grade_global) }}</span>
</div>
<div>
<span>{% trans %}Utility{% endtrans %}</span>
<span>{{ display_star(comment.grade_utility) }}</span>
</div>
<div>
<span>{% trans %}Interest{% endtrans %}</span>
<span>{{ display_star(comment.grade_interest) }}</span>
</div>
<div>
<span>{% trans %}Teaching{% endtrans %}</span>
<span>{{ display_star(comment.grade_teaching) }}</span>
</div>
<div>
<span>{% trans %}Workload{% endtrans %}</span>
<span>{{ display_star(comment.grade_work_load) }}</span>
</div>
</div>
<div class="comment-content">
{{ comment.comment|markdown }}
</div>
</article>
{%- endfor -%}
</section>
@@ -0,0 +1,20 @@
<div class="info-container-item">
<span>{% trans %}Global grade{% endtrans %}</span>
<span>{{ display_star(object.grade_global_average) }}</span>
</div>
<div class="info-container-item">
<span>{% trans %}Utility{% endtrans %}</span>
<span>{{ display_star(object.grade_utility_average) }}</span>
</div>
<div class="info-container-item">
<span>{% trans %}Interest{% endtrans %}</span>
<span>{{ display_star(object.grade_interest_average) }}</span>
</div>
<div class="info-container-item">
<span>{% trans %}Teaching{% endtrans %}</span>
<span>{{ display_star(object.grade_teaching_average) }}</span>
</div>
<div class="info-container-item">
<span>{% trans %}Workload{% endtrans %}</span>
<span>{{ display_star(object.grade_work_load_average) }}</span>
</div>
@@ -0,0 +1,25 @@
{# A few hx-partials for swapping a few parts of the main ue_detail.jinja #}
{% from "pedagogy/macros.jinja" import display_star %}
<hx-partial id="ue-grade">
{% include "pedagogy/fragments/ue_detail/grade.jinja" %}
</hx-partial>
<hx-partial id="comment-form">
{% if object.has_user_already_commented(user) %}
<em>
{% trans trimmed %}
You already posted a comment on this UE.
If you want to comment again, please modify or delete your previous comment.
{% endtrans %}
</em>
<br>
{% elif user.has_perm("pedagogy.add_uecomment") %}
{{ add_comment_form }}
{% endif %}
</hx-partial>
<hx-partial id="comments" hx-swap="innerMorph">
{% include "pedagogy/fragments/ue_detail/comments.jinja" %}
</hx-partial>
@@ -1,91 +0,0 @@
{% from "pedagogy/macros.jinja" import display_star %}
{% from "core/macros.jinja" import user_profile_link %}
{% if comments %}
<h2>{% trans %}Comments{% endtrans %}</h2>
<br>
{% endif %}
<section>
{% for comment in comments %}
<div id="comment-{{ comment.id }}" class="comment-container">
<div class="grade-block">
<div class="grade-type">
<p>{% trans %}Global grade{% endtrans %}</p>
<p>{% trans %}Utility{% endtrans %}</p>
<p>{% trans %}Interest{% endtrans %}</p>
<p>{% trans %}Teaching{% endtrans %}</p>
<p>{% trans %}Work load{% endtrans %}</p>
</div>
<div class="grade-stars">
<p>{{ display_star(comment.grade_global) }}</p>
<p>{{ display_star(comment.grade_utility) }}</p>
<p>{{ display_star(comment.grade_interest) }}</p>
<p>{{ display_star(comment.grade_teaching) }}</p>
<p>{{ display_star(comment.grade_work_load) }}</p>
</div>
<div class="grade-extension"></div>
</div>
<div class="comment">
<div class="anchor">
<a href="{{ url('pedagogy:ue_detail', ue_id=ue.id) }}#comment-{{ comment.id }}"><i class="fa fa-paragraph"></i></a>
</div>
{{ comment.comment|markdown }}
</div>
<div class="info">
{% if comment.is_reported %}
<p class="status-reported">
{% trans %}This comment has been reported{% endtrans %}
</p>
{% endif %}
{% if comment.author_id == user.id or user.has_perm("pedagogy.change_comment") %}
<button
class="btn btn-orange action"
hx-get="{{ url('pedagogy:comment_update', comment_id=comment.id) }}"
hx-swap="outerHTML"
hx-target="#comment-{{ comment.id }}"
>
<i class="fa fa-pencil"></i> {% trans %}Edit{% endtrans %}
</button>
{% endif %}
{% if comment.author_id == user.id or user.has_perm("pedagogy.delete_comment") %}
<form class="action"
hx-post="{{ url('pedagogy:comment_delete', comment_id=comment.id) }}"
hx-confirm='{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}'
hx-swap="outerHTML"
hx-target="#comment-{{ comment.id }}"
>
{% csrf_token %}
<button class="btn btn-red action">
<i class="fa fa-trash-can"></i> {% trans %}Delete{% endtrans %}
</button>
</form>
{% endif %}
</div>
<div class="comment-end-bar">
<div class="report">
<p>
<a
hx-get="{{ url('pedagogy:comment_report', comment_id=comment.id) }}"
hx-swap="outerHTML"
hx-target="#comment-{{ comment.id }}"
>
{% trans %}Report this comment{% endtrans %}
</a>
</p>
</div>
<div class="date"><p>{{ comment.publish_date.strftime('%d/%m/%Y') }}</p></div>
<div class="author"><p>{{ user_profile_link(comment.author) }}</p></div>
</div>
</div>
{% endfor %}
</section>
@@ -1,10 +0,0 @@
{% if object.has_user_already_commented(user) %}
<div class="leave-comment-not-allowed">
<p>{% trans %}You already posted a comment on this UE. If you want to comment again, please modify or delete your previous comment.{% endtrans %}</p>
</div>
<br>
{% endif %}
{% if not object.has_user_already_commented(user) and user.has_perm("pedagogy.add_uecomment") %}
{{ add_comment_form }}
{% endif %}
@@ -1,27 +0,0 @@
<div class="ue-details-container">
<div class="grade">
<p>{% trans %}Global grade{% endtrans %}</p>
<p>{% trans %}Utility{% endtrans %}</p>
<p>{% trans %}Interest{% endtrans %}</p>
<p>{% trans %}Teaching{% endtrans %}</p>
<p>{% trans %}Work load{% endtrans %}</p>
</div>
<div class="grade-stars">
<p>{{ display_star(object.grade_global_average) }}</p>
<p>{{ display_star(object.grade_utility_average) }}</p>
<p>{{ display_star(object.grade_interest_average) }}</p>
<p>{{ display_star(object.grade_teaching_average) }}</p>
<p>{{ display_star(object.grade_work_load_average) }}</p>
</div>
<div class="ue-infos">
<p><b>{% trans %}Objectives{% endtrans %}</b></p>
<p>{{ object.objectives|markdown }}</p>
<p><b>{% trans %}Program{% endtrans %}</b></p>
<p>{{ object.program|markdown }}</p>
<p><b>{% trans %}Earned skills{% endtrans %}</b></p>
<p>{{ object.skills|markdown }}</p>
<p><b>{% trans %}Key concepts{% endtrans %}</b></p>
<p>{{ object.key_concepts|markdown }}</p>
<p><b>{% trans %}UE manager: {% endtrans %}</b>{{ object.manager }}</p>
</div>
</div>
-3
View File
@@ -1,5 +1,4 @@
{% macro display_star(grade) -%} {% macro display_star(grade) -%}
{% if grade >= 0 %} {% if grade >= 0 %}
{% for i in range(5) %} {% for i in range(5) %}
{% if i <= grade %} {% if i <= grade %}
@@ -8,9 +7,7 @@
<span class="fa-solid fa-star pedagogy star-not-checked grade-with-star"></span> <span class="fa-solid fa-star pedagogy star-not-checked grade-with-star"></span>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<span class="pedagogy grade-without-star">{{ grade }}/5</span>
{% else %} {% else %}
<span class="grade-text"> {% trans %} not rated {% endtrans %} </span> <span class="grade-text"> {% trans %} not rated {% endtrans %} </span>
{% endif %} {% endif %}
{%- endmacro %} {%- endmacro %}
+66 -65
View File
@@ -1,91 +1,92 @@
{% extends "core/base.jinja" %}
{% from "pedagogy/macros.jinja" import display_star %} {% from "pedagogy/macros.jinja" import display_star %}
{% if is_fragment %}
<hx-partial id="ue-grade"> {% block additional_css %}
{% include "pedagogy/fragments/ue_details/grade.jinja" %}
</hx-partial>
<hx-partial id="comment-form">
{% include "pedagogy/fragments/ue_details/form.jinja" %}
</hx-partial>
<hx-partial id="comments" hx-swap="innerMorph">
{% include "pedagogy/fragments/ue_details/comments.jinja" %}
</hx-partial>
{% else %}
{% extends "core/base.jinja" %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static('pedagogy/css/pedagogy.scss') }}"> <link rel="stylesheet" href="{{ static('pedagogy/css/pedagogy.scss') }}">
{% endblock %} {% endblock %}
{% block title %} {% block title %}
{% trans %}UE Details{% endtrans %} {% trans %}UE Details{% endtrans %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1>{{ object.code }}</h1>
<div class="pedagogy"> <h2 class="margin-bottom">{{ object.title }}</h2>
<div id="ue_detail"> <div class="pedagogy" id="ue_detail">
<button onclick='(function(){ <div class="ue-infos">
// If comes from the guide page, go back with history <div class="infos-column">
if (document.referrer.replace(/\?(.+)/gm,"").endsWith(`{{ url("pedagogy:guide") }}`)){ <aside class="info-container">
window.history.back(); <div class="info-container-item">
return; <div>{% trans %}Category{% endtrans %}</div>
} <div>{{ object.credit_type }}</div>
// Simply goes to the guide page
window.location.href = `{{ url("pedagogy:guide") }}`;
})()' hidden>{% trans %}Back{% endtrans %}</button>
<h1>{{ object.code }} - {{ object.title }}</h1>
<br>
<div class="ue-quick-info-container">
<div class="hours-cm">
<b>{% trans %}CM: {% endtrans %}</b>{{ object.hours_CM }}
</div> </div>
<div class="hours-td"> <div class="info-container-item">
<b>{% trans %}TD: {% endtrans %}</b>{{ object.hours_TD }} <div>{% trans %}Credits{% endtrans %}</div>
<div>{{ object.credits }}</div>
</div> </div>
<div class="hours-tp"> <hr>
<b>{% trans %}TP: {% endtrans %}</b>{{ object.hours_TP }} <div class="info-container-item">
<div>{% trans %}Lectures{% endtrans %}</div>
<div>{{ object.hours_CM }}h</div>
</div> </div>
<div class="hours-te"> <div class="info-container-item">
<b>{% trans %}TE: {% endtrans %}</b>{{ object.hours_TE }} <div>{% trans %}Tutorials{% endtrans %}</div>
<div>{{ object.hours_TD }}h</div>
</div> </div>
<div class="hours-the"> <div class="info-container-item">
<b>{% trans %}THE: {% endtrans %}</b>{{ object.hours_THE }} <div>{% trans %}Practicals{% endtrans %}</div>
<div>{{ object.hours_TP }}h</div>
</div> </div>
<div class="info-container-item">
<div class="department"> <div>{% trans %}Work without supervision{% endtrans %}</div>
{{ object.department }} <div>{{ object.hours_THE }}h</div>
</div> </div>
<div class="credit-type"> <hr>
{{ object.credit_type }} <div class="info-container-item">
<div>{% trans %}Availability{% endtrans %}</div>
<div>{{ object.get_semester_display() }}</div>
</div> </div>
<div class="semester"> <hr>
{{ object.get_semester_display() }} <div class="info-container-item">
<div>{% trans %}Manager{% endtrans %}</div>
<div>{{ object.manager }}</div>
</div> </div>
</aside>
<aside class="info-container" id="ue-grade">
{% include "pedagogy/fragments/ue_detail/grade.jinja" %}
</aside>
</div> </div>
<section class="ue-main-infos">
<br> <h3>{% trans %}Objectives{% endtrans %}</h3>
<p>{{ object.objectives|markdown }}</p>
<div id="ue-grade"> <h3>{% trans %}Program{% endtrans %}</h3>
{% include "pedagogy/fragments/ue_details/grade.jinja" %} <p>{{ object.program|markdown }}</p>
<h3>{% trans %}Earned skills{% endtrans %}</h3>
<p>{{ object.skills|markdown }}</p>
<h3>{% trans %}Key concepts{% endtrans %}</h3>
<p>{{ object.key_concepts|markdown }}</p>
</section>
</div> </div>
<br> <br>
<div id="comment-form"> <div id="comment-form">
{% include "pedagogy/fragments/ue_details/form.jinja" %} {% if object.has_user_already_commented(user) %}
<em>
{% trans trimmed %}
You already posted a comment on this UE.
If you want to comment again, please modify or delete your previous comment.
{% endtrans %}
</em>
<br>
{% elif user.has_perm("pedagogy.add_uecomment") %}
{{ add_comment_form }}
{% endif %}
</div> </div>
<div id="comments"> <div id="comments">
{% include "pedagogy/fragments/ue_details/comments.jinja" %} {% include "pedagogy/fragments/ue_detail/comments.jinja" %}
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% endif %}
+10 -7
View File
@@ -88,16 +88,19 @@ class UEDetailView(
model = UE model = UE
pk_url_kwarg = "ue_id" pk_url_kwarg = "ue_id"
template_name = "pedagogy/ue_detail.jinja"
permission_required = "pedagogy.view_ue" permission_required = "pedagogy.view_ue"
fragments = { fragments = {"add_comment_form": UECommentCreateView}
"add_comment_form": UECommentCreateView,
} def get_template_names(self) -> list[str]:
is_fragment = self.request.headers.get("HX-Request", False)
return (
["pedagogy/fragments/ue_detail/ue_detail.jinja"]
if is_fragment
else ["pedagogy/ue_detail.jinja"]
)
def get_fragment_data(self): def get_fragment_data(self):
return { return {"add_comment_form": {"ue_id": self.object.id}}
"add_comment_form": {"ue_id": self.object.id},
}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | { return super().get_context_data(**kwargs) | {
+14 -14
View File
@@ -20,14 +20,14 @@ license = { text = "GPL-3.0-only" }
requires-python = "<4.0,>=3.14" requires-python = "<4.0,>=3.14"
dependencies = [ dependencies = [
"django>=5.2.17,<6.0.0", "django>=5.2.17,<6.0.0",
"django-ninja>=1.7.0,<2.0.0", "django-ninja>=1.7.1,<2.0.0",
"django-ninja-extra>=0.31.7", "django-ninja-extra>=0.31.7",
"Pillow>=12.3.0,<13.0.0", "Pillow>=12.3.0,<13.0.0",
"aemark>=0.1.1", "aemark>=0.1.1",
"django-jinja<3.0.0,>=2.11.0", "django-jinja<3.0.0,>=2.11.0",
"cryptography>=50.0.1,<51.0.0", "cryptography>=50.0.2,<51.0.0",
"django-phonenumber-field>=8.5.0,<9.0.0", "django-phonenumber-field>=8.5.0,<9.0.0",
"phonenumbers>=9.0.38,<10.0.0", "phonenumbers>=9.0.40,<10.0.0",
"reportlab>=5.0.1,<6.0.0", "reportlab>=5.0.1,<6.0.0",
"django-haystack>=3.4.0,<4.0.0", "django-haystack>=3.4.0,<4.0.0",
"xapian-haystack>=4.0.0,<5.0.0", "xapian-haystack>=4.0.0,<5.0.0",
@@ -35,21 +35,21 @@ dependencies = [
"django-ordered-model>=3.7.4,<4.0.0", "django-ordered-model>=3.7.4,<4.0.0",
"django-simple-captcha>=0.7.0,<1.0.0", "django-simple-captcha>=0.7.0,<1.0.0",
"python-dateutil>=2.9.0.post0,<3.0.0", "python-dateutil>=2.9.0.post0,<3.0.0",
"sentry-sdk>=2.68.1,<3.0.0", "sentry-sdk>=2.71.0,<3.0.0",
"jinja2>=3.1.6,<4.0.0", "jinja2>=3.1.6,<4.0.0",
"django-countries>=9.0.0,<10.0.0", "django-countries>=9.1.0,<10.0.0",
"dict2xml>=1.7.8,<2.0.0", "dict2xml>=1.7.8,<2.0.0",
"Sphinx>=9.1.0,<10", # Used by xapian during installation "Sphinx>=9.1.0,<10", # Used by xapian during installation
"tomli>=2.4.1,<3.0.0", "tomli>=2.4.1,<3.0.0",
"django-honeypot>=1.3.0,<2", "django-honeypot>=1.3.0,<2",
"pydantic-extra-types>=2.11.1,<3.0.0", "pydantic-extra-types>=2.11.1,<3.0.0",
"ical>=14.1.1", "ical>=14.2.0",
"redis[hiredis]>=6.4.0,<9.0.0", "redis[hiredis]>=8.1.0,<9",
"environs[django]>=15.1.0,<16", "environs[django]>=15.2.0,<16",
"requests>=2.34.2,<3.0.0", "requests>=2.34.2,<3.0.0",
"honcho>=2.0.0", "honcho>=2.0.0",
"psutil>=7.2.2,<8.0.0", "psutil>=7.2.2,<8.0.0",
"celery[redis]>=5.6.2,<7", "celery[redis]>=5.6.3,<7",
"django-celery-results>=2.6.0", "django-celery-results>=2.6.0",
"django-celery-beat>=2.9.0", "django-celery-beat>=2.9.0",
] ]
@@ -65,10 +65,10 @@ prod = [
dev = [ dev = [
"django-debug-toolbar>=8.0.0,<9", "django-debug-toolbar>=8.0.0,<9",
"ipython>=9.17.1,<10.0.0", "ipython>=9.17.1,<10.0.0",
"pre-commit>=4.6.1,<5.0.0", "pre-commit>=4.6.2,<5.0.0",
"ruff>=0.16.6,<1.0.0", "ruff>=0.16.10,<1.0.0",
"djhtml>=3.0.11,<4.0.0", "djhtml>=3.0.11,<4.0.0",
"faker>=40.38.0,<41.0.0", "faker>=40.40.0,<41.0.0",
"rjsmin>=1.2.5,<2.0.0", "rjsmin>=1.2.5,<2.0.0",
] ]
tests = [ tests = [
@@ -76,7 +76,7 @@ tests = [
"pytest>=9.1.1,<10.0.0", "pytest>=9.1.1,<10.0.0",
"pytest-cov>=7.1.0,<8.0.0", "pytest-cov>=7.1.0,<8.0.0",
"pytest-django>=4.14.0,<5.0.0", "pytest-django>=4.14.0,<5.0.0",
"model-bakery>=1.24.0,<2.0.0", "model-bakery>=1.24.1,<2.0.0",
"beautifulsoup4>=4.15.0,<5", "beautifulsoup4>=4.15.0,<5",
"lxml>=6.1.3,<7", "lxml>=6.1.3,<7",
] ]
@@ -84,7 +84,7 @@ docs = [
"mkdocs>=1.6.1,<2.0.0", "mkdocs>=1.6.1,<2.0.0",
"mkdocs-material>=9.7.7,<10.0.0", "mkdocs-material>=9.7.7,<10.0.0",
"mkdocstrings>=1.0.6,<2.0.0", "mkdocstrings>=1.0.6,<2.0.0",
"mkdocstrings-python>=2.0.7,<3.0.0", "mkdocstrings-python>=2.0.9,<3.0.0",
"mkdocs-include-markdown-plugin>=7.3.0,<8.0.0", "mkdocs-include-markdown-plugin>=7.3.0,<8.0.0",
] ]
+5 -19
View File
@@ -2,7 +2,6 @@ import type TomSelect from "tom-select";
import type { TomOption } from "tom-select/src/types"; import type { TomOption } from "tom-select/src/types";
import type { UserAjaxSelect } from "#core:core/components/ajax-select-index"; import type { UserAjaxSelect } from "#core:core/components/ajax-select-index";
import { paginated } from "#core:utils/api"; import { paginated } from "#core:utils/api";
import { History } from "#core:utils/history";
import { import {
type IdentifiedUserSchema, type IdentifiedUserSchema,
type ModerationRequestSchema, type ModerationRequestSchema,
@@ -228,11 +227,6 @@ document.addEventListener("alpine:init", () => {
* Error message when a moderation operation fails * Error message when a moderation operation fails
**/ **/
moderationError: "", moderationError: "",
/**
* Method of pushing new url to the browser history
* Used by popstate event and always reset to it's default value when used
**/
pushstate: History.Push,
async init() { async init() {
this.pictures = ( this.pictures = (
@@ -267,12 +261,10 @@ document.addEventListener("alpine:init", () => {
if (!event.state || event.state.sasPictureId === undefined) { if (!event.state || event.state.sasPictureId === undefined) {
return; return;
} }
this.pushstate = History.Replace;
this.currentPicture = this.pictures.find( this.currentPicture = this.pictures.find(
(i: PictureSchema) => i.id === Number.parseInt(event.state.sasPictureId, 10), (i: PictureSchema) => i.id === Number.parseInt(event.state.sasPictureId, 10),
) as PictureWithIdentifications; ) as PictureWithIdentifications;
}); });
this.pushstate = History.Replace; /* Avoid first url push */
await this.updatePicture(); await this.updatePicture();
}, },
@@ -285,17 +277,11 @@ document.addEventListener("alpine:init", () => {
* the list of identified users are updated. * the list of identified users are updated.
*/ */
async updatePicture(): Promise<void> { async updatePicture(): Promise<void> {
const updateArgs = { window.history.replaceState(
data: { sasPictureId: this.currentPicture.id }, { sasPictureId: this.currentPicture.id },
unused: "", "",
url: this.currentPicture.sas_url, this.currentPicture.sas_url,
}; );
if (this.pushstate === History.Replace) {
window.history.replaceState(updateArgs.data, updateArgs.unused, updateArgs.url);
this.pushstate = History.Push;
} else {
window.history.pushState(updateArgs.data, updateArgs.unused, updateArgs.url);
}
this.moderationError = ""; this.moderationError = "";
const index: number = this.pictures.indexOf(this.currentPicture); const index: number = this.pictures.indexOf(this.currentPicture);
-16
View File
@@ -1,17 +1 @@
# Register your models here. # Register your models here.
from django.contrib import admin
from timetable.models import Timetable, TimetableSlot
class TimetableSlotInline(admin.TabularInline):
model = TimetableSlot
@admin.register(Timetable)
class TimetableAdmin(admin.ModelAdmin):
list_display = ("user", "semester")
search_fields = ("user__nick_name", "user__first_name", "user__last_name")
autocomplete_fields = ("user",)
inlines = (TimetableSlotInline,)
list_select_related = ("user",)
-27
View File
@@ -1,27 +0,0 @@
from ninja.security import SessionAuth
from ninja_extra import ControllerBase, api_controller, route
from core.utils import get_semester_code
from timetable.models import Timetable, TimetableSlot
from timetable.schemas import CreateTimetableSchema, TimetableSchema
@api_controller("/edt", urls_namespace="timetable")
class TimetableController(ControllerBase):
@route.put("", auth=SessionAuth(), response={200: TimetableSchema}, url_name="save")
def save_timetable(self, data: CreateTimetableSchema):
user = self.context.request.user
timetable, created = Timetable.objects.get_or_create(
semester=get_semester_code(), user=user
)
if not created:
timetable.slots.all().delete()
if user.preferences.show_my_timetable != data.is_viewable:
user.preferences.show_my_timetable = data.is_viewable
user.preferences.save()
slots = TimetableSlot.objects.bulk_create(
[TimetableSlot(timetable=timetable, **s.model_dump()) for s in data.slots]
)
return TimetableSchema(
id=timetable.id, semester=timetable.semester, slots=slots
)
-121
View File
@@ -1,121 +0,0 @@
# Generated by Django 5.2.17 on 2026-09-24 21:03
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import core.utils
class Migration(migrations.Migration):
initial = True
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
operations = [
migrations.CreateModel(
name="Timetable",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"semester",
models.CharField(
blank=True,
default=core.utils.get_semester_code,
validators=[
django.core.validators.RegexValidator("(A|P)\\d{2}")
],
verbose_name="semester",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="user",
),
),
],
options={"verbose_name": "timetable", "verbose_name_plural": "timetables"},
),
migrations.CreateModel(
name="TimetableSlot",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"weekday",
models.SmallIntegerField(
choices=[
(1, "Monday"),
(2, "Tuesday"),
(3, "Wednesday"),
(4, "Thursday"),
(5, "Friday"),
(6, "Saturday"),
(7, "Sunday"),
],
verbose_name="weekday",
),
),
(
"start_at",
models.PositiveSmallIntegerField(
validators=[django.core.validators.MaxValueValidator(95)],
verbose_name="start at",
),
),
(
"end_at",
models.PositiveSmallIntegerField(
validators=[django.core.validators.MaxValueValidator(95)],
verbose_name="end at",
),
),
(
"timetable",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="slots",
to="timetable.timetable",
verbose_name="timetable",
),
),
],
options={
"verbose_name": "timetable slot",
"verbose_name_plural": "timetable slots",
"permissions": [("add_self_timetable", "Can add its own timetable")],
},
),
migrations.AddConstraint(
model_name="timetable",
constraint=models.UniqueConstraint(
fields=("user", "semester"), name="timetable_unique_user_semester"
),
),
migrations.AddConstraint(
model_name="timetableslot",
constraint=models.CheckConstraint(
condition=models.Q(("end_at__gt", models.F("start_at"))),
name="timetable_slot_end_after_start",
),
),
]
-81
View File
@@ -1,82 +1 @@
# Create your models here. # Create your models here.
from datetime import timedelta
from django.core.validators import MaxValueValidator, RegexValidator
from django.db import models
from django.db.models import F, Q
from django.utils.translation import gettext_lazy as _
from core.models import User
from core.utils import get_semester_code
class Timetable(models.Model):
user = models.ForeignKey(User, verbose_name=_("user"), on_delete=models.CASCADE)
semester = models.CharField(
_("semester"),
default=get_semester_code,
validators=[RegexValidator(r"(A|P)\d{2}")],
blank=True,
)
class Meta:
verbose_name = _("timetable")
verbose_name_plural = _("timetables")
constraints = [
models.UniqueConstraint(
fields=("user", "semester"), name="timetable_unique_user_semester"
)
]
permissions = [("add_self_timetable", "Can add its own timetable")]
def __str__(self) -> str:
return f"{self.user} {self.semester}"
class TimetableSlot(models.Model):
MINUTES_PER_SLOT = 15
class WeekDay(models.IntegerChoices):
MONDAY = 1
TUESDAY = 2
WEDNESDAY = 3
THURSDAY = 4
FRIDAY = 5
SATURDAY = 6
SUNDAY = 7
timetable = models.ForeignKey(
Timetable,
verbose_name=_("timetable"),
related_name="slots",
on_delete=models.CASCADE,
)
weekday = models.SmallIntegerField(_("weekday"), choices=WeekDay)
# there are 4 * 24 = 96 quarters in a day, and we start counting at 0
start_at = models.PositiveSmallIntegerField(
_("start at"), validators=[MaxValueValidator(95)]
)
end_at = models.PositiveSmallIntegerField(
_("end at"), validators=[MaxValueValidator(95)]
)
class Meta:
verbose_name = _("timetable slot")
verbose_name_plural = _("timetable slots")
constraints = [
models.CheckConstraint(
condition=Q(end_at__gt=F("start_at")),
name="timetable_slot_end_after_start",
)
]
def __str__(self) -> str:
return f"{self.start_hour} - {self.end_hour} - {self.get_weekday_display()}"
@property
def start_hour(self):
return timedelta(minutes=self.start_at * self.MINUTES_PER_SLOT)
@property
def end_hour(self):
return timedelta(minutes=self.end_at * self.MINUTES_PER_SLOT)
-35
View File
@@ -1,35 +0,0 @@
from typing import Annotated
from annotated_types import Ge, Le
from ninja import ModelSchema, Schema
from pydantic import Field
from timetable.models import Timetable, TimetableSlot
class TimetableSlotSchema(Schema):
weekday: TimetableSlot.WeekDay = Field(
description=(
f"{TimetableSlot.WeekDay.MONDAY} is monday, "
f"{TimetableSlot.WeekDay.TUESDAY} is tuesday, etc."
)
)
start_at: Annotated[int, Ge(0), Le(96)]
end_at: Annotated[int, Ge(0), Le(96)]
class Meta:
model = TimetableSlot
fields = ["start_at", "end_at"]
class CreateTimetableSchema(Schema):
is_viewable: bool
slots: list[TimetableSlotSchema]
class TimetableSchema(ModelSchema):
slots: list[TimetableSlotSchema]
class Meta:
model = Timetable
fields = ["id", "semester"]
@@ -1,6 +1,4 @@
import html2canvas from "html2canvas"; import html2canvas from "html2canvas";
import { AlertMessage } from "#core:utils/alert-message";
import { timetableSaveTimetable } from "#openapi";
// see https://regex101.com/r/QHSaPM/3 // see https://regex101.com/r/QHSaPM/3
const TIMETABLE_ROW_RE: RegExp = const TIMETABLE_ROW_RE: RegExp =
@@ -80,15 +78,15 @@ function parseSlots(s: string): TimetableSlot[] {
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("timetableGenerator", () => ({ Alpine.data("timetableGenerator", () => ({
content: DEFAULT_TIMETABLE, content: DEFAULT_TIMETABLE,
error: null as { incorrectRow?: string } | null, error: null as { incorrectRow?: string },
displayedWeekdays: [] as WeekDay[], displayedWeekdays: [] as WeekDay[],
courses: [] as TimetableSlot[], courses: [] as TimetableSlot[],
startSlot: 0, startSlot: 0,
endSlot: 0, endSlot: 0,
table: { height: 0, width: 0 }, table: {
edtPublic: false, height: 0,
saving: false, width: 0,
timetableSavedMessage: new AlertMessage(), },
colors: {} as Record<string, string>, colors: {} as Record<string, string>,
colorPalette: [ colorPalette: [
@@ -110,8 +108,7 @@ document.addEventListener("alpine:init", () => {
this.courses = parseSlots(this.content); this.courses = parseSlots(this.content);
this.error = null; this.error = null;
} catch (err) { } catch (err) {
type ParseRowError = { cause: { row: string } }; this.error = { incorrectRow: err?.cause?.row };
this.error = { incorrectRow: (err as ParseRowError)?.cause?.row };
return; return;
} }
@@ -183,36 +180,5 @@ document.addEventListener("alpine:init", () => {
downloadLink.click(); downloadLink.click();
downloadLink.remove(); downloadLink.remove();
}, },
async saveSith() {
this.saving = true;
const res = await timetableSaveTimetable({
body: {
// biome-ignore lint/style/useNamingConvention: api is snake case
is_viewable: this.edtPublic,
slots: this.courses.map((c) =>
Object.assign({}, c, {
// biome-ignore lint/style/useNamingConvention: api is snake case
start_at: c.startSlot,
// biome-ignore lint/style/useNamingConvention: api is snake case
end_at: c.endSlot,
weekday: WEEKDAYS.indexOf(c.weekday) + 1,
}),
),
},
});
if (res.response?.ok) {
this.timetableSavedMessage.display(
gettext("This timetable has been saved in your profile"),
{ success: true },
);
} else {
this.timetableSavedMessage.display(
interpolate(gettext("Error %d: timetable save failed"), res.response?.status),
{ success: false },
);
}
this.saving = false;
},
})); }));
}); });
+6 -33
View File
@@ -92,39 +92,12 @@
</div> </div>
</div> </div>
</div> </div>
<template x-if="table.height > 0 && table.width > 0"> <button
<section> class="margin-bottom btn btn-blue"
{% if user.has_perm("timetable.add_self_timetable") %} @click="savePng"
<div id="timetable-save" class="margin-bottom"> x-show="table.height > 0 && table.width > 0"
<h3>{% trans %}Save on your profile{% endtrans %}</h3> >
<form> {% trans %}Save to PNG{% endtrans %}
<fieldset>
<input type="checkbox" class="switch" id="edt-public-switch-id" x-model="edtPublic">
<label for="edt-public-switch-id">{% trans %}Make this timetable public{% endtrans %}</label>
<span class="helptext">
{% trans trimmed url=url("core:user_prefs", user_id=user.id) %}
You can change this at any time in <a href="{{ url }}">your preferences</a>
{% endtrans %}
</span>
</fieldset>
<button class="btn btn-blue" :disabled="saving" @click.prevent="saveSith">
{% trans %}Save timetable{% endtrans %}
</button> </button>
<p
class="alert"
:class="timetableSavedMessage.success ? 'alert-green' : 'alert-red'"
x-show="timetableSavedMessage.open"
x-transition.duration.500ms
x-text="timetableSavedMessage.content"
></p>
</form>
</div>
{% endif %}
<h3 class="margin-bottom">{% trans %}Export{% endtrans %}</h3>
<button class="margin-bottom btn btn-blue" @click="savePng">
{% trans %}Export to PNG{% endtrans %}
</button>
</section>
</template>
</div> </div>
{% endblock content %} {% endblock content %}
@@ -1,6 +0,0 @@
{% extends 'core/base.jinja' %}
{% block content %}
{% endblock %}
+1
View File
@@ -0,0 +1 @@
# Create your tests here.
View File
Whitespace-only changes.
-20
View File
@@ -1,20 +0,0 @@
from django.test import TestCase
from django.urls import reverse
from core.baker_recipes import subscriber_user
from timetable.models import TimetableSlot
class TestSaveTimetable(TestCase):
@classmethod
def setUpTestData(cls) -> None:
cls.user = subscriber_user.make()
cls.payload = {
"is_viewable": True,
"slots": [
{"weekday": TimetableSlot.WeekDay.MONDAY, "start_at": 32, "end_at": 40},
{"weekday": TimetableSlot.WeekDay.MONDAY, "start_at": 41, "end_at": 49},
{"weekday": TimetableSlot.WeekDay.FRIDAY, "start_at": 52, "end_at": 64},
],
}
cls.url = reverse("api:timetable:save", kwargs={"user_id": cls.user.id})
-32
View File
@@ -1,32 +0,0 @@
import pytest
from bs4 import BeautifulSoup
from django.contrib.auth.models import Permission
from django.test import Client
from django.urls import reverse
from model_bakery import baker
from core.baker_recipes import subscriber_user
from core.models import User
# class TestGenerator(TestCase)
@pytest.mark.django_db
@pytest.mark.parametrize(
"can_save", [True, False]
)
def test_generator_ok(client: Client, can_save):
user = baker.make(User)
if can_save:
user.user_permissions.add(Permission.objects.get(codename="add_self_timetable"))
client.force_login(user)
res = client.get(reverse("timetable:generator"))
assert res.status_code == 200
soup = BeautifulSoup(res.text, "lxml")
elem = soup.find(id="timetable-save")
if can_save:
assert elem is not None
else:
assert elem is None
-15
View File
@@ -1,21 +1,6 @@
# Create your views here. # Create your views here.
from django.contrib.auth.mixins import UserPassesTestMixin
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.views.generic.detail import DetailView
from core.models import User
class GeneratorView(TemplateView): class GeneratorView(TemplateView):
template_name = "timetable/generator.jinja" template_name = "timetable/generator.jinja"
class UserTimetableView(UserPassesTestMixin, DetailView):
model = User
pk_url_kwarg = "user_id"
template_name = "timetable/user_timetable.jinja"
def test_func(self) -> bool | None:
return self.request.user.id == self.kwargs[
"user_id"
] or self.request.user.has_perm("timetable.view_timetable")
Generated
+379 -345
View File
File diff suppressed because it is too large. Load diff