mirror of
https://github.com/ae-utbm/sith.git
synced 2026-10-05 18:34:15 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87d247dbb4 | ||
|
|
7ab412a59b | ||
|
|
12fa99eb50 | ||
|
|
5a9ed1342b | ||
|
|
7a0df3d8a7 | ||
|
|
7044a06546 |
No files matched your search
@@ -1,7 +1,7 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.16.10
|
||||
rev: v0.16.0
|
||||
hooks:
|
||||
- id: ruff-check # just check the code, and print the errors
|
||||
- id: ruff-check # actually fix the fixable errors, but print nothing
|
||||
@@ -12,7 +12,7 @@ repos:
|
||||
rev: v0.6.1
|
||||
hooks:
|
||||
- id: biome-check
|
||||
additional_dependencies: ["@biomejs/biome@2.5.15"]
|
||||
additional_dependencies: ["@biomejs/biome@2.5.1"]
|
||||
- repo: https://github.com/rtts/djhtml
|
||||
rev: 3.0.11
|
||||
hooks:
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# 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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -799,6 +799,14 @@ class Preferences(models.Model):
|
||||
),
|
||||
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(
|
||||
_("get a notification for every click"), default=False
|
||||
)
|
||||
|
||||
@@ -42,7 +42,6 @@ details.accordion>.accordion-content {
|
||||
background: #ffffff;
|
||||
color: #333333;
|
||||
padding: 1em 2.2em;
|
||||
margin-top: 0;
|
||||
border: 1px solid #dddddd;
|
||||
border-bottom-right-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
|
||||
@@ -29,5 +29,13 @@
|
||||
</span>
|
||||
{{ form.show_my_stats.errors }}
|
||||
</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 %}">
|
||||
</form>
|
||||
+10
-2
@@ -336,10 +336,14 @@ class UserVisibilityForm(forms.ModelForm):
|
||||
|
||||
__preferences_fields = forms.fields_for_model(
|
||||
Preferences,
|
||||
["show_my_stats"],
|
||||
widgets={"show_my_stats": forms.CheckboxInput(attrs={"class": "switch"})},
|
||||
["show_my_stats", "show_my_timetable"],
|
||||
widgets={
|
||||
"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_timetable = __preferences_fields["show_my_timetable"]
|
||||
|
||||
def __init__(
|
||||
self, *args, initial: dict | None = None, instance: User | None = None, **kwargs
|
||||
@@ -347,12 +351,16 @@ class UserVisibilityForm(forms.ModelForm):
|
||||
if instance:
|
||||
initial = initial or {}
|
||||
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)
|
||||
|
||||
def save(self, commit=True) -> User: # noqa: FBT002
|
||||
instance = super().save(commit=commit)
|
||||
if commit:
|
||||
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()
|
||||
return instance
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-10-02 12:56+0200\n"
|
||||
"POT-Creation-Date: 2026-09-21 13:16+0200\n"
|
||||
"PO-Revision-Date: 2016-07-18\n"
|
||||
"Last-Translator: Maréchal <thomas.girod@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
|
||||
#: election/templates/election/election_detail.jinja
|
||||
#: forum/templates/forum/macros.jinja
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
|
||||
#: pedagogy/templates/pedagogy/guide.jinja sas/templates/sas/album.jinja
|
||||
#: sas/templates/sas/moderation.jinja sas/templates/sas/picture.jinja
|
||||
#: trombi/templates/trombi/detail.jinja
|
||||
@@ -1028,7 +1028,7 @@ msgstr "Outils"
|
||||
#: counter/templates/counter/counter_list.jinja
|
||||
#: election/templates/election/election_detail.jinja
|
||||
#: forum/templates/forum/macros.jinja
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
|
||||
#: pedagogy/templates/pedagogy/guide.jinja sas/templates/sas/album.jinja
|
||||
#: trombi/templates/trombi/detail.jinja
|
||||
#: trombi/templates/trombi/edit_profile.jinja
|
||||
@@ -1285,9 +1285,8 @@ msgstr "Actions"
|
||||
|
||||
#: com/templates/com/mailing_admin.jinja com/templates/com/poster_list.jinja
|
||||
#: core/templates/core/file_detail.jinja
|
||||
#: core/templates/core/file_moderation.jinja
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
|
||||
#: sas/templates/sas/moderation.jinja sas/templates/sas/picture.jinja
|
||||
#: core/templates/core/file_moderation.jinja sas/templates/sas/moderation.jinja
|
||||
#: sas/templates/sas/picture.jinja
|
||||
msgid "Moderate"
|
||||
msgstr "Modérer"
|
||||
|
||||
@@ -1589,6 +1588,7 @@ msgstr "Descendre"
|
||||
|
||||
#: com/templates/com/weekmail_preview.jinja
|
||||
#: core/templates/core/user_account_detail.jinja
|
||||
#: pedagogy/templates/pedagogy/ue_detail.jinja
|
||||
#: trombi/templates/trombi/comment_moderation.jinja
|
||||
#: trombi/templates/trombi/export.jinja
|
||||
msgid "Back"
|
||||
@@ -1933,8 +1933,8 @@ msgstr ""
|
||||
"toujours le voir."
|
||||
|
||||
#: core/models.py
|
||||
msgid "ToS approved at"
|
||||
msgstr "CGU approuvées le"
|
||||
msgid "ToS approved"
|
||||
msgstr "CGU approuvées"
|
||||
|
||||
#: core/models.py
|
||||
msgid "A user with that username already exists"
|
||||
@@ -1943,9 +1943,7 @@ msgstr "Un utilisateur de ce nom d'utilisateur existe déjà"
|
||||
#: core/models.py core/templates/core/macros.jinja
|
||||
#: core/templates/core/user_detail.jinja core/templates/core/user_edit.jinja
|
||||
#: election/templates/election/election_detail.jinja
|
||||
#: forum/templates/forum/macros.jinja
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
|
||||
#: trombi/templates/trombi/user_tools.jinja
|
||||
#: forum/templates/forum/macros.jinja trombi/templates/trombi/user_tools.jinja
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
|
||||
@@ -2376,6 +2374,7 @@ msgstr "Propriétés"
|
||||
|
||||
#: core/templates/core/file_delete_confirm.jinja
|
||||
#: counter/templates/counter/fragments/delete_student_card.jinja
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
|
||||
#, python-format
|
||||
msgid "Are you sure you want to delete \"%(obj)s\"?"
|
||||
msgstr "Êtes-vous sûr de vouloir supprimer \"%(obj)s\" ?"
|
||||
@@ -3908,6 +3907,7 @@ msgid "Emptied"
|
||||
msgstr "Coffre vidé"
|
||||
|
||||
#: counter/templates/counter/cash_summary_list.jinja counter/views/cash.py
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_comment_form.jinja
|
||||
#: pedagogy/templates/pedagogy/moderation.jinja
|
||||
#: trombi/templates/trombi/comment.jinja
|
||||
#: trombi/templates/trombi/user_tools.jinja
|
||||
@@ -5239,7 +5239,6 @@ msgid "reporter"
|
||||
msgstr "signalant"
|
||||
|
||||
#: pedagogy/templates/pedagogy/fragments/comment_report.jinja
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
|
||||
msgid "Report"
|
||||
msgstr "Signaler"
|
||||
|
||||
@@ -5247,46 +5246,45 @@ msgstr "Signaler"
|
||||
msgid "Leave comment"
|
||||
msgstr "Laisser un commentaire"
|
||||
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
|
||||
#: trombi/templates/trombi/export.jinja
|
||||
msgid "Comments"
|
||||
msgstr "Commentaires"
|
||||
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
|
||||
msgid "This comment has been reported"
|
||||
msgstr "Ce commentaire a été signalé"
|
||||
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
|
||||
msgid "It will be hidden until moderated."
|
||||
msgstr "Il sera caché tant qu'il ne sera pas modéré."
|
||||
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_detail/grade.jinja
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_details/grade.jinja
|
||||
msgid "Global grade"
|
||||
msgstr "Note globale"
|
||||
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_detail/comments.jinja
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_detail/grade.jinja
|
||||
#: 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_detail/comments.jinja
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_detail/grade.jinja
|
||||
#: 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_detail/comments.jinja
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_detail/grade.jinja
|
||||
#: 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_detail/comments.jinja
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_detail/grade.jinja
|
||||
msgid "Workload"
|
||||
#: 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_detail/ue_detail.jinja
|
||||
#: pedagogy/templates/pedagogy/ue_detail.jinja
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
|
||||
msgid "This comment has been reported"
|
||||
msgstr "Ce commentaire a été signalé"
|
||||
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_details/comments.jinja
|
||||
msgid "Report this comment"
|
||||
msgstr "Signaler ce commentaire"
|
||||
|
||||
#: pedagogy/templates/pedagogy/fragments/ue_details/form.jinja
|
||||
msgid ""
|
||||
"You already posted a comment on this UE. If you want to comment again, "
|
||||
"please modify or delete your previous comment."
|
||||
@@ -5294,6 +5292,26 @@ msgstr ""
|
||||
"Vous avez déjà commenté cette UE. Si vous voulez de nouveau commenter, "
|
||||
"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
|
||||
msgid "A guide of courses available at UTBM."
|
||||
msgstr "Un guide de tous les cours disponibles à l'UTBM."
|
||||
@@ -5359,52 +5377,24 @@ msgid "UE Details"
|
||||
msgstr "Détails d'UE"
|
||||
|
||||
#: pedagogy/templates/pedagogy/ue_detail.jinja
|
||||
msgid "Category"
|
||||
msgstr "Catégorie"
|
||||
msgid "CM: "
|
||||
msgstr "CM : "
|
||||
|
||||
#: pedagogy/templates/pedagogy/ue_detail.jinja
|
||||
msgid "Credits"
|
||||
msgstr "Crédits"
|
||||
msgid "TD: "
|
||||
msgstr "TD : "
|
||||
|
||||
#: pedagogy/templates/pedagogy/ue_detail.jinja
|
||||
msgid "Lectures"
|
||||
msgstr "Cours magistraux"
|
||||
msgid "TP: "
|
||||
msgstr "TP : "
|
||||
|
||||
#: pedagogy/templates/pedagogy/ue_detail.jinja
|
||||
msgid "Tutorials"
|
||||
msgstr "Travaux dirigés"
|
||||
msgid "TE: "
|
||||
msgstr "TE : "
|
||||
|
||||
#: pedagogy/templates/pedagogy/ue_detail.jinja
|
||||
msgid "Practicals"
|
||||
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"
|
||||
msgid "THE: "
|
||||
msgstr "THE : "
|
||||
|
||||
#: pedagogy/templates/pedagogy/ue_edit.jinja
|
||||
msgid "Edit UE"
|
||||
|
||||
Generated
+355
-385
File diff suppressed because it is too large.
Load diff
+14
-14
@@ -24,24 +24,24 @@
|
||||
"#com:*": "./com/static/bundled/*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^8.0.6",
|
||||
"@babel/preset-env": "^8.0.6",
|
||||
"@biomejs/biome": "^2.5.15",
|
||||
"@babel/core": "^8.0.1",
|
||||
"@babel/preset-env": "^8.0.2",
|
||||
"@biomejs/biome": "^2.5.1",
|
||||
"@hey-api/openapi-ts": "^0.99.0",
|
||||
"@types/alpinejs": "^3.17.0",
|
||||
"@types/alpinejs": "^3.13.11",
|
||||
"@types/alpinejs__sort": "^3.13.0",
|
||||
"@types/cytoscape-cxtmenu": "^3.4.5",
|
||||
"@types/cytoscape-klay": "^3.1.5",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^26.6.4",
|
||||
"@types/node": "^26.2.0",
|
||||
"@typescript/native": "npm:typescript@^7.0.2",
|
||||
"rollup-plugin-visualizer": "^7.1.1",
|
||||
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
||||
"vite": "^8.3.2"
|
||||
"vite": "^8.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ae_utbm/aemark": "^0.1.3",
|
||||
"@alpinejs/sort": "^3.17.4",
|
||||
"@alpinejs/sort": "^3.16.2",
|
||||
"@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
|
||||
"@floating-ui/dom": "^1.8.0",
|
||||
"@fortawesome/fontawesome-free": "^7.3.1",
|
||||
@@ -49,13 +49,13 @@
|
||||
"@fullcalendar/daygrid": "^6.1.21",
|
||||
"@fullcalendar/icalendar": "^6.1.21",
|
||||
"@fullcalendar/list": "^6.1.21",
|
||||
"@sentry/browser": "^11.3.0",
|
||||
"@zip.js/zip.js": "^2.22.0",
|
||||
"3d-force-graph": "^1.80.1",
|
||||
"alpinejs": "^3.17.4",
|
||||
"@sentry/browser": "^10.70.0",
|
||||
"@zip.js/zip.js": "^2.8.57",
|
||||
"3d-force-graph": "^1.80.0",
|
||||
"alpinejs": "^3.16.2",
|
||||
"chart.js": "^4.5.1",
|
||||
"country-flag-emoji-polyfill": "^0.1.10",
|
||||
"cytoscape": "^3.34.3",
|
||||
"cytoscape": "^3.34.1",
|
||||
"cytoscape-cxtmenu": "^3.5.0",
|
||||
"cytoscape-klay": "^3.1.4",
|
||||
"d3-force-3d": "^3.0.6",
|
||||
@@ -66,8 +66,8 @@
|
||||
"js-cookie": "^3.0.8",
|
||||
"lit-html": "^3.3.3",
|
||||
"native-file-system-adapter": "^3.0.1",
|
||||
"temporal-polyfill": "^1.0.5",
|
||||
"three": "^0.186.1",
|
||||
"temporal-polyfill": "^1.0.4",
|
||||
"three": "^0.185.1",
|
||||
"three-spritetext": "^1.10.0",
|
||||
"tom-select": "^2.6.2"
|
||||
}
|
||||
|
||||
@@ -1,22 +1,52 @@
|
||||
@import "core/static/core/devices";
|
||||
@import "core/static/core/colors";
|
||||
|
||||
|
||||
$pedagogy-blue: #1bb9ea;
|
||||
$pedagogy-orange: #ea7900;
|
||||
$pedagogy-hover-blue: #0e97ce;
|
||||
$pedagogy-light-blue: #caf0ff;
|
||||
$pedagogy-white-text: #f0f0f0;
|
||||
|
||||
.pedagogy {
|
||||
.star-not-checked {
|
||||
color: lightgray;
|
||||
&.star-not-checked {
|
||||
color: #f7f7f7;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.star-checked {
|
||||
&.star-checked {
|
||||
color: $pedagogy-orange;
|
||||
margin-bottom: 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 {
|
||||
font-size: 1.1em;
|
||||
|
||||
@@ -34,10 +64,12 @@ $pedagogy-white-text: #f0f0f0;
|
||||
}
|
||||
|
||||
#search_form {
|
||||
|
||||
.radio-guide fieldset {
|
||||
input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
margin-top: 10px;
|
||||
margin-bottom: 0;
|
||||
color: white;
|
||||
@@ -53,79 +85,80 @@ $pedagogy-white-text: #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
&#ue_detail {
|
||||
.ue-infos {
|
||||
display: flex;
|
||||
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%);
|
||||
#ue_detail {
|
||||
color: #062f38;
|
||||
|
||||
.ue-quick-info-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
border-radius: 5px;
|
||||
padding: .75rem 1rem ;
|
||||
grid-gap: .5rem;
|
||||
@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;
|
||||
grid-template-columns: 20% 20% 20% 20% auto;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-areas:
|
||||
"hours-cm hours-td hours-tp hours-te hours-the"
|
||||
"department credit-type semester . .";
|
||||
}
|
||||
|
||||
:first-child {
|
||||
justify-content: right;
|
||||
text-align: right;
|
||||
}
|
||||
.department {
|
||||
grid-area: department;
|
||||
}
|
||||
|
||||
hr {
|
||||
grid-column: span 2;
|
||||
width: 50%;
|
||||
margin: auto;
|
||||
.credit-type {
|
||||
grid-area: credit-type;
|
||||
}
|
||||
|
||||
.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 {
|
||||
.comment-form-content {
|
||||
display: flex;
|
||||
gap: 2em;
|
||||
.leave-comment-grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: 270px auto;
|
||||
grid-template-rows: 100%;
|
||||
grid-template-areas: "stars comment";
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
gap: 1em;
|
||||
flex-direction: column;
|
||||
grid-template-columns: 100%;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-areas:
|
||||
"stars"
|
||||
"comment";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.form-stars {
|
||||
flex: 0;
|
||||
min-width: 250px;
|
||||
display: flex;
|
||||
align-content: flex-start;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem 2em;
|
||||
height: unset;
|
||||
grid-area: stars;
|
||||
}
|
||||
|
||||
.form-comment {
|
||||
flex: 2;
|
||||
grid-area: comment;
|
||||
}
|
||||
|
||||
.ui-accordion-header-icon {
|
||||
@@ -133,18 +166,45 @@ $pedagogy-white-text: #f0f0f0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
.input-stars {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
summary {
|
||||
background: $pedagogy-orange;
|
||||
.ue-details-container {
|
||||
display: grid;
|
||||
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;
|
||||
@media screen and (min-width: $large-devices) {
|
||||
clip-path: polygon(0 0%, 0 100%, 30% 100%, 33% 0);
|
||||
background-color: $pedagogy-blue;
|
||||
padding-right: 10px;
|
||||
|
||||
>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 {
|
||||
@@ -152,60 +212,236 @@ $pedagogy-white-text: #f0f0f0;
|
||||
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 {
|
||||
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;
|
||||
grid-area: comment;
|
||||
|
||||
&.reported {
|
||||
border: #fc8181 1px solid;
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
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;
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
|
||||
img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
.anchor {
|
||||
grid-area: anchor;
|
||||
text-align: right;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.comment-metadata {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.markdown {
|
||||
grid-area: markdown;
|
||||
|
||||
.comment-options {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
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 {
|
||||
min-height: 139px;
|
||||
margin-top: 0;
|
||||
margin-right: 0;
|
||||
padding: 10px;
|
||||
text-align: justify;
|
||||
padding: .5em 20px 1em;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
@@ -11,39 +11,44 @@
|
||||
hx-disabled-elt="find input[type='submit']"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<div class="leave-comment-grid-container">
|
||||
<div class="form-stars">
|
||||
{{ form.non_field_errors() }}
|
||||
{{ form.author.errors }}
|
||||
{{ form.ue.errors }}
|
||||
|
||||
{{ form.author }}
|
||||
{{ form.ue }}
|
||||
<div class="comment-form-content">
|
||||
<div class="form-stars">
|
||||
<fieldset>
|
||||
<legend>{{ form.grade_global.label }} :</legend>
|
||||
|
||||
<div class="input-stars">
|
||||
<label for="{{ form.grade_global.id_for_label }}">{{ form.grade_global.label }} :</label>
|
||||
{{ form.grade_global.errors }}
|
||||
{{ form.grade_global }}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{{ form.grade_utility.label }} :</legend>
|
||||
</div>
|
||||
|
||||
<div class="input-stars">
|
||||
<label for="{{ form.grade_utility.id_for_label }}">{{ form.grade_utility.label }} :</label>
|
||||
{{ form.grade_utility.errors }}
|
||||
{{ form.grade_utility }}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{{ form.grade_interest.label }} :</legend>
|
||||
</div>
|
||||
|
||||
<div class="input-stars">
|
||||
<label for="{{ form.grade_interest.id_for_label }}">{{ form.grade_interest.label }} :</label>
|
||||
{{ form.grade_interest.errors }}
|
||||
{{ form.grade_interest }}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{{ form.grade_teaching.label }} :</legend>
|
||||
</div>
|
||||
|
||||
<div class="input-stars">
|
||||
<label for="{{ form.grade_teaching.id_for_label }}">{{ form.grade_teaching.label }} :</label>
|
||||
{{ form.grade_teaching.errors }}
|
||||
{{ form.grade_teaching }}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{{ form.grade_work_load.label }} :</legend>
|
||||
</div>
|
||||
|
||||
<div class="input-stars">
|
||||
<label for="{{ form.grade_work_load.id_for_label }}">{{ form.grade_work_load.label }} :</label>
|
||||
{{ form.grade_work_load.errors }}
|
||||
{{ form.grade_work_load }}
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-comment">
|
||||
<label for="{{ form.comment.id_for_label }}">{{ form.comment.label }} :</label>
|
||||
@@ -51,7 +56,9 @@
|
||||
{{ form.comment }}
|
||||
</div>
|
||||
</div>
|
||||
<p><input type="submit" class="btn btn-blue"/></p>
|
||||
<div class="buttons">
|
||||
<input type="submit" value="{% trans %}Comment{% endtrans %}" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if form.is_creation %}
|
||||
|
||||
@@ -1,129 +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 -%}
|
||||
<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>
|
||||
@@ -1,20 +0,0 @@
|
||||
<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>
|
||||
@@ -1,25 +0,0 @@
|
||||
{# 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>
|
||||
@@ -0,0 +1,91 @@
|
||||
{% 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>
|
||||
@@ -0,0 +1,10 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,27 @@
|
||||
<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>
|
||||
@@ -1,4 +1,5 @@
|
||||
{% macro display_star(grade) -%}
|
||||
|
||||
{% if grade >= 0 %}
|
||||
{% for i in range(5) %}
|
||||
{% if i <= grade %}
|
||||
@@ -7,7 +8,9 @@
|
||||
<span class="fa-solid fa-star pedagogy star-not-checked grade-with-star"></span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<span class="pedagogy grade-without-star">{{ grade }}/5</span>
|
||||
{% else %}
|
||||
<span class="grade-text"> {% trans %} not rated {% endtrans %} </span>
|
||||
{% endif %}
|
||||
|
||||
{%- endmacro %}
|
||||
@@ -1,92 +1,91 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
{% from "pedagogy/macros.jinja" import display_star %}
|
||||
|
||||
{% if is_fragment %}
|
||||
|
||||
{% block additional_css %}
|
||||
<hx-partial id="ue-grade">
|
||||
{% 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') }}">
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{% block title %}
|
||||
{% trans %}UE Details{% endtrans %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ object.code }}</h1>
|
||||
<h2 class="margin-bottom">{{ object.title }}</h2>
|
||||
<div class="pedagogy" id="ue_detail">
|
||||
<div class="ue-infos">
|
||||
<div class="infos-column">
|
||||
<aside class="info-container">
|
||||
<div class="info-container-item">
|
||||
<div>{% trans %}Category{% endtrans %}</div>
|
||||
<div>{{ object.credit_type }}</div>
|
||||
{% block content %}
|
||||
|
||||
<div class="pedagogy">
|
||||
<div id="ue_detail">
|
||||
<button onclick='(function(){
|
||||
// If comes from the guide page, go back with history
|
||||
if (document.referrer.replace(/\?(.+)/gm,"").endsWith(`{{ url("pedagogy:guide") }}`)){
|
||||
window.history.back();
|
||||
return;
|
||||
}
|
||||
// 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 class="info-container-item">
|
||||
<div>{% trans %}Credits{% endtrans %}</div>
|
||||
<div>{{ object.credits }}</div>
|
||||
<div class="hours-td">
|
||||
<b>{% trans %}TD: {% endtrans %}</b>{{ object.hours_TD }}
|
||||
</div>
|
||||
<hr>
|
||||
<div class="info-container-item">
|
||||
<div>{% trans %}Lectures{% endtrans %}</div>
|
||||
<div>{{ object.hours_CM }}h</div>
|
||||
<div class="hours-tp">
|
||||
<b>{% trans %}TP: {% endtrans %}</b>{{ object.hours_TP }}
|
||||
</div>
|
||||
<div class="info-container-item">
|
||||
<div>{% trans %}Tutorials{% endtrans %}</div>
|
||||
<div>{{ object.hours_TD }}h</div>
|
||||
<div class="hours-te">
|
||||
<b>{% trans %}TE: {% endtrans %}</b>{{ object.hours_TE }}
|
||||
</div>
|
||||
<div class="info-container-item">
|
||||
<div>{% trans %}Practicals{% endtrans %}</div>
|
||||
<div>{{ object.hours_TP }}h</div>
|
||||
<div class="hours-the">
|
||||
<b>{% trans %}THE: {% endtrans %}</b>{{ object.hours_THE }}
|
||||
</div>
|
||||
<div class="info-container-item">
|
||||
<div>{% trans %}Work without supervision{% endtrans %}</div>
|
||||
<div>{{ object.hours_THE }}h</div>
|
||||
|
||||
<div class="department">
|
||||
{{ object.department }}
|
||||
</div>
|
||||
<hr>
|
||||
<div class="info-container-item">
|
||||
<div>{% trans %}Availability{% endtrans %}</div>
|
||||
<div>{{ object.get_semester_display() }}</div>
|
||||
<div class="credit-type">
|
||||
{{ object.credit_type }}
|
||||
</div>
|
||||
<hr>
|
||||
<div class="info-container-item">
|
||||
<div>{% trans %}Manager{% endtrans %}</div>
|
||||
<div>{{ object.manager }}</div>
|
||||
<div class="semester">
|
||||
{{ object.get_semester_display() }}
|
||||
</div>
|
||||
</aside>
|
||||
<aside class="info-container" id="ue-grade">
|
||||
{% include "pedagogy/fragments/ue_detail/grade.jinja" %}
|
||||
</aside>
|
||||
</div>
|
||||
<section class="ue-main-infos">
|
||||
<h3>{% trans %}Objectives{% endtrans %}</h3>
|
||||
<p>{{ object.objectives|markdown }}</p>
|
||||
<h3>{% trans %}Program{% endtrans %}</h3>
|
||||
<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>
|
||||
|
||||
<br>
|
||||
|
||||
<div id="ue-grade">
|
||||
{% include "pedagogy/fragments/ue_details/grade.jinja" %}
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div 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 %}
|
||||
{% include "pedagogy/fragments/ue_details/form.jinja" %}
|
||||
</div>
|
||||
|
||||
<div id="comments">
|
||||
{% include "pedagogy/fragments/ue_detail/comments.jinja" %}
|
||||
{% include "pedagogy/fragments/ue_details/comments.jinja" %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
+7
-10
@@ -88,19 +88,16 @@ class UEDetailView(
|
||||
|
||||
model = UE
|
||||
pk_url_kwarg = "ue_id"
|
||||
template_name = "pedagogy/ue_detail.jinja"
|
||||
permission_required = "pedagogy.view_ue"
|
||||
fragments = {"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"]
|
||||
)
|
||||
fragments = {
|
||||
"add_comment_form": UECommentCreateView,
|
||||
}
|
||||
|
||||
def get_fragment_data(self):
|
||||
return {"add_comment_form": {"ue_id": self.object.id}}
|
||||
return {
|
||||
"add_comment_form": {"ue_id": self.object.id},
|
||||
}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return super().get_context_data(**kwargs) | {
|
||||
|
||||
+14
-14
@@ -20,14 +20,14 @@ license = { text = "GPL-3.0-only" }
|
||||
requires-python = "<4.0,>=3.14"
|
||||
dependencies = [
|
||||
"django>=5.2.17,<6.0.0",
|
||||
"django-ninja>=1.7.1,<2.0.0",
|
||||
"django-ninja>=1.7.0,<2.0.0",
|
||||
"django-ninja-extra>=0.31.7",
|
||||
"Pillow>=12.3.0,<13.0.0",
|
||||
"aemark>=0.1.1",
|
||||
"django-jinja<3.0.0,>=2.11.0",
|
||||
"cryptography>=50.0.2,<51.0.0",
|
||||
"cryptography>=50.0.1,<51.0.0",
|
||||
"django-phonenumber-field>=8.5.0,<9.0.0",
|
||||
"phonenumbers>=9.0.40,<10.0.0",
|
||||
"phonenumbers>=9.0.38,<10.0.0",
|
||||
"reportlab>=5.0.1,<6.0.0",
|
||||
"django-haystack>=3.4.0,<4.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-simple-captcha>=0.7.0,<1.0.0",
|
||||
"python-dateutil>=2.9.0.post0,<3.0.0",
|
||||
"sentry-sdk>=2.71.0,<3.0.0",
|
||||
"sentry-sdk>=2.68.1,<3.0.0",
|
||||
"jinja2>=3.1.6,<4.0.0",
|
||||
"django-countries>=9.1.0,<10.0.0",
|
||||
"django-countries>=9.0.0,<10.0.0",
|
||||
"dict2xml>=1.7.8,<2.0.0",
|
||||
"Sphinx>=9.1.0,<10", # Used by xapian during installation
|
||||
"tomli>=2.4.1,<3.0.0",
|
||||
"django-honeypot>=1.3.0,<2",
|
||||
"pydantic-extra-types>=2.11.1,<3.0.0",
|
||||
"ical>=14.2.0",
|
||||
"redis[hiredis]>=8.1.0,<9",
|
||||
"environs[django]>=15.2.0,<16",
|
||||
"ical>=14.1.1",
|
||||
"redis[hiredis]>=6.4.0,<9.0.0",
|
||||
"environs[django]>=15.1.0,<16",
|
||||
"requests>=2.34.2,<3.0.0",
|
||||
"honcho>=2.0.0",
|
||||
"psutil>=7.2.2,<8.0.0",
|
||||
"celery[redis]>=5.6.3,<7",
|
||||
"celery[redis]>=5.6.2,<7",
|
||||
"django-celery-results>=2.6.0",
|
||||
"django-celery-beat>=2.9.0",
|
||||
]
|
||||
@@ -65,10 +65,10 @@ prod = [
|
||||
dev = [
|
||||
"django-debug-toolbar>=8.0.0,<9",
|
||||
"ipython>=9.17.1,<10.0.0",
|
||||
"pre-commit>=4.6.2,<5.0.0",
|
||||
"ruff>=0.16.10,<1.0.0",
|
||||
"pre-commit>=4.6.1,<5.0.0",
|
||||
"ruff>=0.16.6,<1.0.0",
|
||||
"djhtml>=3.0.11,<4.0.0",
|
||||
"faker>=40.40.0,<41.0.0",
|
||||
"faker>=40.38.0,<41.0.0",
|
||||
"rjsmin>=1.2.5,<2.0.0",
|
||||
]
|
||||
tests = [
|
||||
@@ -76,7 +76,7 @@ tests = [
|
||||
"pytest>=9.1.1,<10.0.0",
|
||||
"pytest-cov>=7.1.0,<8.0.0",
|
||||
"pytest-django>=4.14.0,<5.0.0",
|
||||
"model-bakery>=1.24.1,<2.0.0",
|
||||
"model-bakery>=1.24.0,<2.0.0",
|
||||
"beautifulsoup4>=4.15.0,<5",
|
||||
"lxml>=6.1.3,<7",
|
||||
]
|
||||
@@ -84,7 +84,7 @@ docs = [
|
||||
"mkdocs>=1.6.1,<2.0.0",
|
||||
"mkdocs-material>=9.7.7,<10.0.0",
|
||||
"mkdocstrings>=1.0.6,<2.0.0",
|
||||
"mkdocstrings-python>=2.0.9,<3.0.0",
|
||||
"mkdocstrings-python>=2.0.7,<3.0.0",
|
||||
"mkdocs-include-markdown-plugin>=7.3.0,<8.0.0",
|
||||
]
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type TomSelect from "tom-select";
|
||||
import type { TomOption } from "tom-select/src/types";
|
||||
import type { UserAjaxSelect } from "#core:core/components/ajax-select-index";
|
||||
import { paginated } from "#core:utils/api";
|
||||
import { History } from "#core:utils/history";
|
||||
import {
|
||||
type IdentifiedUserSchema,
|
||||
type ModerationRequestSchema,
|
||||
@@ -227,6 +228,11 @@ document.addEventListener("alpine:init", () => {
|
||||
* Error message when a moderation operation fails
|
||||
**/
|
||||
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() {
|
||||
this.pictures = (
|
||||
@@ -261,10 +267,12 @@ document.addEventListener("alpine:init", () => {
|
||||
if (!event.state || event.state.sasPictureId === undefined) {
|
||||
return;
|
||||
}
|
||||
this.pushstate = History.Replace;
|
||||
this.currentPicture = this.pictures.find(
|
||||
(i: PictureSchema) => i.id === Number.parseInt(event.state.sasPictureId, 10),
|
||||
) as PictureWithIdentifications;
|
||||
});
|
||||
this.pushstate = History.Replace; /* Avoid first url push */
|
||||
await this.updatePicture();
|
||||
},
|
||||
|
||||
@@ -277,11 +285,17 @@ document.addEventListener("alpine:init", () => {
|
||||
* the list of identified users are updated.
|
||||
*/
|
||||
async updatePicture(): Promise<void> {
|
||||
window.history.replaceState(
|
||||
{ sasPictureId: this.currentPicture.id },
|
||||
"",
|
||||
this.currentPicture.sas_url,
|
||||
);
|
||||
const updateArgs = {
|
||||
data: { sasPictureId: this.currentPicture.id },
|
||||
unused: "",
|
||||
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 = "";
|
||||
const index: number = this.pictures.indexOf(this.currentPicture);
|
||||
|
||||
@@ -1 +1,17 @@
|
||||
# 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",)
|
||||
@@ -0,0 +1,27 @@
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,121 @@
|
||||
# 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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1 +1,82 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,35 @@
|
||||
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,4 +1,6 @@
|
||||
import html2canvas from "html2canvas";
|
||||
import { AlertMessage } from "#core:utils/alert-message";
|
||||
import { timetableSaveTimetable } from "#openapi";
|
||||
|
||||
// see https://regex101.com/r/QHSaPM/3
|
||||
const TIMETABLE_ROW_RE: RegExp =
|
||||
@@ -78,15 +80,15 @@ function parseSlots(s: string): TimetableSlot[] {
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("timetableGenerator", () => ({
|
||||
content: DEFAULT_TIMETABLE,
|
||||
error: null as { incorrectRow?: string },
|
||||
error: null as { incorrectRow?: string } | null,
|
||||
displayedWeekdays: [] as WeekDay[],
|
||||
courses: [] as TimetableSlot[],
|
||||
startSlot: 0,
|
||||
endSlot: 0,
|
||||
table: {
|
||||
height: 0,
|
||||
width: 0,
|
||||
},
|
||||
table: { height: 0, width: 0 },
|
||||
edtPublic: false,
|
||||
saving: false,
|
||||
timetableSavedMessage: new AlertMessage(),
|
||||
|
||||
colors: {} as Record<string, string>,
|
||||
colorPalette: [
|
||||
@@ -108,7 +110,8 @@ document.addEventListener("alpine:init", () => {
|
||||
this.courses = parseSlots(this.content);
|
||||
this.error = null;
|
||||
} catch (err) {
|
||||
this.error = { incorrectRow: err?.cause?.row };
|
||||
type ParseRowError = { cause: { row: string } };
|
||||
this.error = { incorrectRow: (err as ParseRowError)?.cause?.row };
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -180,5 +183,36 @@ document.addEventListener("alpine:init", () => {
|
||||
downloadLink.click();
|
||||
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;
|
||||
},
|
||||
}));
|
||||
});
|
||||
@@ -92,12 +92,39 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="margin-bottom btn btn-blue"
|
||||
@click="savePng"
|
||||
x-show="table.height > 0 && table.width > 0"
|
||||
>
|
||||
{% trans %}Save to PNG{% endtrans %}
|
||||
<template x-if="table.height > 0 && table.width > 0">
|
||||
<section>
|
||||
{% if user.has_perm("timetable.add_self_timetable") %}
|
||||
<div id="timetable-save" class="margin-bottom">
|
||||
<h3>{% trans %}Save on your profile{% endtrans %}</h3>
|
||||
<form>
|
||||
<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>
|
||||
<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>
|
||||
{% endblock content %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends 'core/base.jinja' %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -1 +0,0 @@
|
||||
# Create your tests here.
|
||||
Whitespace-only changes.
@@ -0,0 +1,20 @@
|
||||
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})
|
||||
@@ -0,0 +1,32 @@
|
||||
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
|
||||
@@ -1,6 +1,21 @@
|
||||
# Create your views here.
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.views.generic import TemplateView
|
||||
from django.views.generic.detail import DetailView
|
||||
|
||||
from core.models import User
|
||||
|
||||
|
||||
class GeneratorView(TemplateView):
|
||||
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")
|
||||
Reference in new issue
Block a user