From 5df81d160d9e01a7b8fac1919dc3affdf45c8093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Valenta?= <git@imaniti.org> Date: Tue, 18 Apr 2023 13:22:26 +0200 Subject: [PATCH] finish data structure, make views --- .gitignore | 1 + groups/admin.py | 11 +- groups/apps.py | 5 +- groups/models.py | 3 +- lectures/admin.py | 61 ++++ lectures/apps.py | 5 +- lectures/migrations/0001_initial.py | 165 ++++++++--- ...me_event_lecturelector_lecture_and_more.py | 41 +++ ...003_alter_lecturematerial_file_and_more.py | 23 ++ ...004_alter_lecturematerial_file_and_more.py | 23 ++ lectures/migrations/0005_lecturegroup.py | 27 ++ ...006_alter_lecturegroup_options_and_more.py | 39 +++ .../migrations/0007_alter_lecture_options.py | 17 ++ .../migrations/0008_alter_lecture_options.py | 17 ++ ...ve_lecturegroup_lectures_lecture_groups.py | 22 ++ .../0010_alter_lecturegroup_options.py | 17 ++ lectures/models.py | 69 +++-- lectures/templates/lectures/index.html | 6 + lectures/urls.py | 8 + lectures/views.py | 52 +++- manage.py | 4 +- .../364D1F51-B67A-4FA5-83E6-1F32D62B6A78.gif | Bin 0 -> 19263 bytes oidc/apps.py | 1 + oidc/auth.py | 2 + shared/static/shared/.gitkeep | 1 - shared/static/shared/base.js | 2 +- shared/static/shared/favicon.png | Bin 0 -> 9781 bytes shared/static/shared/runtime.js | 2 +- shared/static/shared/style.css | 263 ++++++++++++++++++ shared/templates/shared/includes/base.html | 198 +++++++++++++ ucebnice/settings/base.py | 2 +- ucebnice/urls.py | 2 + users/admin.py | 7 +- users/migrations/0001_initial.py | 112 ++++++-- users/migrations/0002_user_rsvp_lectures.py | 19 ++ .../0003_alter_user_rsvp_lectures.py | 19 ++ .../0004_alter_user_rsvp_lectures.py | 19 ++ users/migrations/0005_alter_user_email.py | 18 ++ users/migrations/0006_user_sso_username.py | 19 ++ .../0007_alter_user_rsvp_lectures.py | 19 ++ users/models.py | 18 ++ 41 files changed, 1255 insertions(+), 84 deletions(-) create mode 100644 lectures/migrations/0002_lecturematerial_rename_event_lecturelector_lecture_and_more.py create mode 100644 lectures/migrations/0003_alter_lecturematerial_file_and_more.py create mode 100644 lectures/migrations/0004_alter_lecturematerial_file_and_more.py create mode 100644 lectures/migrations/0005_lecturegroup.py create mode 100644 lectures/migrations/0006_alter_lecturegroup_options_and_more.py create mode 100644 lectures/migrations/0007_alter_lecture_options.py create mode 100644 lectures/migrations/0008_alter_lecture_options.py create mode 100644 lectures/migrations/0009_remove_lecturegroup_lectures_lecture_groups.py create mode 100644 lectures/migrations/0010_alter_lecturegroup_options.py create mode 100644 lectures/templates/lectures/index.html create mode 100644 lectures/urls.py create mode 100644 media/364D1F51-B67A-4FA5-83E6-1F32D62B6A78.gif delete mode 100644 shared/static/shared/.gitkeep create mode 100644 shared/static/shared/favicon.png create mode 100644 shared/templates/shared/includes/base.html create mode 100644 users/migrations/0002_user_rsvp_lectures.py create mode 100644 users/migrations/0003_alter_user_rsvp_lectures.py create mode 100644 users/migrations/0004_alter_user_rsvp_lectures.py create mode 100644 users/migrations/0005_alter_user_email.py create mode 100644 users/migrations/0006_user_sso_username.py create mode 100644 users/migrations/0007_alter_user_rsvp_lectures.py diff --git a/.gitignore b/.gitignore index eca2a89..33687b8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ __pycache__/ webpack-stats.json staticfiles node_modules +shared/static/shared/*.{js,css,txt} diff --git a/groups/admin.py b/groups/admin.py index 8c38f3f..2780a04 100644 --- a/groups/admin.py +++ b/groups/admin.py @@ -1,3 +1,12 @@ from django.contrib import admin -# Register your models here. +from shared.admin import MarkdownxGuardedModelAdmin + +from .models import Group + + +class GroupAdmin(MarkdownxGuardedModelAdmin): + autocomplete_fields = ("lectures",) + + +admin.site.register(Group, GroupAdmin) diff --git a/groups/apps.py b/groups/apps.py index 86b49bc..8c383e0 100644 --- a/groups/apps.py +++ b/groups/apps.py @@ -2,5 +2,6 @@ from django.apps import AppConfig class GroupsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'groups' + default_auto_field = "django.db.models.BigAutoField" + name = "groups" + verbose_name = "Skupiny" diff --git a/groups/models.py b/groups/models.py index fbab9a6..a18eb37 100644 --- a/groups/models.py +++ b/groups/models.py @@ -12,8 +12,7 @@ class Group(models.Model): lectures = models.ManyToManyField( "lectures.Lecture", blank=True, - null=True, - verbose_name="Události", + verbose_name="Lekce", ) class Meta: diff --git a/lectures/admin.py b/lectures/admin.py index 8c38f3f..07ab444 100644 --- a/lectures/admin.py +++ b/lectures/admin.py @@ -1,3 +1,64 @@ from django.contrib import admin +from shared.admin import MarkdownxGuardedModelAdmin + +from .models import ( + Lecture, + LectureGroup, + LectureLector, + LectureMaterial, + LectureRecording, +) + # Register your models here. + + +class IndexHiddenModelAdmin(MarkdownxGuardedModelAdmin): + def has_module_permission(self, request): + return False + + +class LectureGroupAdmin(MarkdownxGuardedModelAdmin): + autocomplete_fields = ("user_groups",) + search_fields = ("name",) + + +class LectureLectorInline(admin.TabularInline): + model = LectureLector + extra = 1 + + +class LectureRecordingInline(admin.TabularInline): + model = LectureRecording + extra = 1 + + +class LectureMaterialInline(admin.StackedInline): + model = LectureMaterial + extra = 1 + + +class LectureAdmin(MarkdownxGuardedModelAdmin): + inlines = ( + LectureLectorInline, + LectureRecordingInline, + LectureMaterialInline, + ) + + autocomplete_fields = ("groups",) + search_fields = ("name", "description") + list_display = ( + "name", + "timestamp", + ) + + +for model in ( + LectureLector, + LectureMaterial, + LectureRecording, +): + admin.site.register(model, IndexHiddenModelAdmin) + +admin.site.register(Lecture, LectureAdmin) +admin.site.register(LectureGroup, LectureGroupAdmin) diff --git a/lectures/apps.py b/lectures/apps.py index 30d62f2..5518d56 100644 --- a/lectures/apps.py +++ b/lectures/apps.py @@ -2,5 +2,6 @@ from django.apps import AppConfig class LecturesConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'lectures' + default_auto_field = "django.db.models.BigAutoField" + name = "lectures" + verbose_name = "Lekce" diff --git a/lectures/migrations/0001_initial.py b/lectures/migrations/0001_initial.py index 549de6b..1728c28 100644 --- a/lectures/migrations/0001_initial.py +++ b/lectures/migrations/0001_initial.py @@ -1,70 +1,167 @@ # Generated by Django 4.1.4 on 2023-04-17 10:56 -from django.db import migrations, models import django.db.models.deletion import markdownx.models +from django.db import migrations, models class Migration(migrations.Migration): - initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Lecture', + name="Lecture", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('timestamp', models.DateTimeField(blank=True, null=True, verbose_name='Datum a čas konání')), - ('type', models.CharField(choices=[('recommended', 'Doporučené'), ('optional', 'Volitelné')], max_length=11, verbose_name='Typ')), - ('name', models.CharField(max_length=128, verbose_name='Název')), - ('description', markdownx.models.MarkdownxField(blank=True, help_text='Můžeš použít markdown.', null=True, verbose_name='Popis')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "timestamp", + models.DateTimeField( + blank=True, null=True, verbose_name="Datum a čas konání" + ), + ), + ( + "type", + models.CharField( + choices=[ + ("recommended", "Doporučené"), + ("optional", "Volitelné"), + ], + max_length=11, + verbose_name="Typ", + ), + ), + ("name", models.CharField(max_length=128, verbose_name="Název")), + ( + "description", + markdownx.models.MarkdownxField( + blank=True, + help_text="Můžeš použít markdown.", + null=True, + verbose_name="Popis", + ), + ), ], options={ - 'verbose_name': 'Lekce', - 'verbose_name_plural': 'Lekce', + "verbose_name": "Lekce", + "verbose_name_plural": "Lekce", }, ), migrations.CreateModel( - name='Material', + name="Material", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=128, verbose_name='Název')), - ('link', models.URLField(blank=True, max_length=256, null=True, verbose_name='Odkaz')), - ('file', models.FileField(blank=True, null=True, upload_to='', verbose_name='Soubor')), - ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='materials', to='lectures.lecture', verbose_name='Událost')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=128, verbose_name="Název")), + ( + "link", + models.URLField( + blank=True, max_length=256, null=True, verbose_name="Odkaz" + ), + ), + ( + "file", + models.FileField( + blank=True, null=True, upload_to="", verbose_name="Soubor" + ), + ), + ( + "event", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="materials", + to="lectures.lecture", + verbose_name="Událost", + ), + ), ], options={ - 'verbose_name': 'Materiál', - 'verbose_name_plural': 'Materiály', + "verbose_name": "Materiál", + "verbose_name_plural": "Materiály", }, ), migrations.CreateModel( - name='LectureRecording', + name="LectureRecording", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=128, verbose_name='Název')), - ('link', models.URLField(blank=True, max_length=256, null=True, verbose_name='Odkaz')), - ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recordings', to='lectures.lecture', verbose_name='Událost')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=128, verbose_name="Název")), + ( + "link", + models.URLField( + blank=True, max_length=256, null=True, verbose_name="Odkaz" + ), + ), + ( + "event", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="recordings", + to="lectures.lecture", + verbose_name="Událost", + ), + ), ], options={ - 'verbose_name': 'Nahrávka', - 'verbose_name_plural': 'Nahrávky', + "verbose_name": "Nahrávka", + "verbose_name_plural": "Nahrávky", }, ), migrations.CreateModel( - name='LectureLector', + name="LectureLector", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=128, verbose_name='Jméno')), - ('link', models.URLField(blank=True, max_length=256, null=True, verbose_name='Odkaz')), - ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lectors', to='lectures.lecture', verbose_name='Událost')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=128, verbose_name="Jméno")), + ( + "link", + models.URLField( + blank=True, max_length=256, null=True, verbose_name="Odkaz" + ), + ), + ( + "event", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="lectors", + to="lectures.lecture", + verbose_name="Událost", + ), + ), ], options={ - 'verbose_name': 'Lektor', - 'verbose_name_plural': 'Lektoři', + "verbose_name": "Lektor", + "verbose_name_plural": "Lektoři", }, ), ] diff --git a/lectures/migrations/0002_lecturematerial_rename_event_lecturelector_lecture_and_more.py b/lectures/migrations/0002_lecturematerial_rename_event_lecturelector_lecture_and_more.py new file mode 100644 index 0000000..39a23e3 --- /dev/null +++ b/lectures/migrations/0002_lecturematerial_rename_event_lecturelector_lecture_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 4.1.4 on 2023-04-18 07:33 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('lectures', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='LectureMaterial', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128, verbose_name='Název')), + ('link', models.URLField(blank=True, max_length=256, null=True, verbose_name='Odkaz')), + ('file', models.FileField(blank=True, null=True, upload_to='', verbose_name='Soubor')), + ('lecture', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='materials', to='lectures.lecture', verbose_name='Událost')), + ], + options={ + 'verbose_name': 'Materiál', + 'verbose_name_plural': 'Materiály', + }, + ), + migrations.RenameField( + model_name='lecturelector', + old_name='event', + new_name='lecture', + ), + migrations.RenameField( + model_name='lecturerecording', + old_name='event', + new_name='lecture', + ), + migrations.DeleteModel( + name='Material', + ), + ] diff --git a/lectures/migrations/0003_alter_lecturematerial_file_and_more.py b/lectures/migrations/0003_alter_lecturematerial_file_and_more.py new file mode 100644 index 0000000..b605f4d --- /dev/null +++ b/lectures/migrations/0003_alter_lecturematerial_file_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.4 on 2023-04-18 07:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('lectures', '0002_lecturematerial_rename_event_lecturelector_lecture_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='lecturematerial', + name='file', + field=models.FileField(blank=True, help_text='Zadej prosím pouze odkaz, nebo soubor.', null=True, upload_to='', verbose_name='Soubor'), + ), + migrations.AlterField( + model_name='lecturematerial', + name='link', + field=models.URLField(blank=True, help_text='Zadej prosím pouze odkaz, nebo soubor.', max_length=256, null=True, verbose_name='Odkaz'), + ), + ] diff --git a/lectures/migrations/0004_alter_lecturematerial_file_and_more.py b/lectures/migrations/0004_alter_lecturematerial_file_and_more.py new file mode 100644 index 0000000..a44a7da --- /dev/null +++ b/lectures/migrations/0004_alter_lecturematerial_file_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.4 on 2023-04-18 07:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('lectures', '0003_alter_lecturematerial_file_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='lecturematerial', + name='file', + field=models.FileField(blank=True, help_text='Pokud máš vložený soubor, nemůžeš definovat odkaz.', null=True, upload_to='', verbose_name='Soubor'), + ), + migrations.AlterField( + model_name='lecturematerial', + name='link', + field=models.URLField(blank=True, help_text='Pokud máš zadaný odkaz, nemůžeš definovat soubor.', max_length=256, null=True, verbose_name='Odkaz'), + ), + ] diff --git a/lectures/migrations/0005_lecturegroup.py b/lectures/migrations/0005_lecturegroup.py new file mode 100644 index 0000000..0ac154f --- /dev/null +++ b/lectures/migrations/0005_lecturegroup.py @@ -0,0 +1,27 @@ +# Generated by Django 4.1.4 on 2023-04-18 09:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('lectures', '0004_alter_lecturematerial_file_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='LectureGroup', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128, verbose_name='Jméno')), + ('lectures', models.ManyToManyField(blank=True, related_name='groups', to='lectures.lecture', verbose_name='Lekce')), + ('user_groups', models.ManyToManyField(to='auth.group', verbose_name='Uživatelské skupiny')), + ], + options={ + 'verbose_name': 'Skupina', + 'verbose_name_plural': 'Skupiny', + }, + ), + ] diff --git a/lectures/migrations/0006_alter_lecturegroup_options_and_more.py b/lectures/migrations/0006_alter_lecturegroup_options_and_more.py new file mode 100644 index 0000000..4988853 --- /dev/null +++ b/lectures/migrations/0006_alter_lecturegroup_options_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 4.1.4 on 2023-04-18 10:12 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('lectures', '0005_lecturegroup'), + ] + + operations = [ + migrations.AlterModelOptions( + name='lecturegroup', + options={'verbose_name': 'Výuková skupina', 'verbose_name_plural': 'Výukové skupiny'}, + ), + migrations.AlterField( + model_name='lecturegroup', + name='user_groups', + field=models.ManyToManyField(blank=True, help_text='Pokud nedefinuješ žádné, lekce ve skupině jsou dostupné všem.', to='auth.group', verbose_name='Uživatelské skupiny'), + ), + migrations.AlterField( + model_name='lecturelector', + name='lecture', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lectors', to='lectures.lecture', verbose_name='Lekce'), + ), + migrations.AlterField( + model_name='lecturematerial', + name='lecture', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='materials', to='lectures.lecture', verbose_name='Lekce'), + ), + migrations.AlterField( + model_name='lecturerecording', + name='lecture', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recordings', to='lectures.lecture', verbose_name='Lekce'), + ), + ] diff --git a/lectures/migrations/0007_alter_lecture_options.py b/lectures/migrations/0007_alter_lecture_options.py new file mode 100644 index 0000000..d1dbc97 --- /dev/null +++ b/lectures/migrations/0007_alter_lecture_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.4 on 2023-04-18 10:20 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('lectures', '0006_alter_lecturegroup_options_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='lecture', + options={'ordering': ('-timestamp',), 'verbose_name': 'Lekce', 'verbose_name_plural': 'Lekce'}, + ), + ] diff --git a/lectures/migrations/0008_alter_lecture_options.py b/lectures/migrations/0008_alter_lecture_options.py new file mode 100644 index 0000000..fa169f9 --- /dev/null +++ b/lectures/migrations/0008_alter_lecture_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.4 on 2023-04-18 10:21 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('lectures', '0007_alter_lecture_options'), + ] + + operations = [ + migrations.AlterModelOptions( + name='lecture', + options={'ordering': ('-timestamp', '-name'), 'verbose_name': 'Lekce', 'verbose_name_plural': 'Lekce'}, + ), + ] diff --git a/lectures/migrations/0009_remove_lecturegroup_lectures_lecture_groups.py b/lectures/migrations/0009_remove_lecturegroup_lectures_lecture_groups.py new file mode 100644 index 0000000..4cc5167 --- /dev/null +++ b/lectures/migrations/0009_remove_lecturegroup_lectures_lecture_groups.py @@ -0,0 +1,22 @@ +# Generated by Django 4.1.4 on 2023-04-18 10:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('lectures', '0008_alter_lecture_options'), + ] + + operations = [ + migrations.RemoveField( + model_name='lecturegroup', + name='lectures', + ), + migrations.AddField( + model_name='lecture', + name='groups', + field=models.ManyToManyField(blank=True, related_name='lectures', to='lectures.lecturegroup', verbose_name='Výukové skupiny'), + ), + ] diff --git a/lectures/migrations/0010_alter_lecturegroup_options.py b/lectures/migrations/0010_alter_lecturegroup_options.py new file mode 100644 index 0000000..339cb6c --- /dev/null +++ b/lectures/migrations/0010_alter_lecturegroup_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.4 on 2023-04-18 10:29 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('lectures', '0009_remove_lecturegroup_lectures_lecture_groups'), + ] + + operations = [ + migrations.AlterModelOptions( + name='lecturegroup', + options={'ordering': ('name',), 'verbose_name': 'Výuková skupina', 'verbose_name_plural': 'Výukové skupiny'}, + ), + ] diff --git a/lectures/models.py b/lectures/models.py index 81cd2a0..cd8ea81 100644 --- a/lectures/models.py +++ b/lectures/models.py @@ -1,14 +1,38 @@ from datetime import timedelta +from django.contrib.auth.models import Group from django.core.exceptions import ValidationError from django.db import models from django.utils import timezone from markdownx.models import MarkdownxField +from shared.models import NameStrMixin + # Create your models here. -class Lecture(models.Model): +class LectureGroup(NameStrMixin, models.Model): + name = models.CharField( + max_length=128, + verbose_name="Jméno", + ) + + user_groups = models.ManyToManyField( + Group, + blank=True, + verbose_name="Uživatelské skupiny", + help_text=("Pokud nedefinuješ žádné, lekce ve skupině jsou dostupné všem."), + ) + + class Meta: + verbose_name = "Výuková skupina" + verbose_name_plural = "Výukové skupiny" + ordering = ("name",) + + +class Lecture(NameStrMixin, models.Model): + is_current_treshold = timedelta(hours=8) + timestamp = models.DateTimeField( verbose_name="Datum a čas konání", blank=True, @@ -19,6 +43,13 @@ class Lecture(models.Model): RECOMMENDED = "recommended", "Doporučené" OPTIONAL = "optional", "Volitelné" + groups = models.ManyToManyField( + LectureGroup, + blank=True, + related_name="lectures", + verbose_name="Výukové skupiny", + ) + type = models.CharField( choices=TypeChoices.choices, max_length=11, @@ -40,19 +71,20 @@ class Lecture(models.Model): @property def is_current(self) -> bool: # On or after the current time, minus 8 hours - return self.timestamp >= (timezone.now() - timedelta(hours=8)) + return self.timestamp >= (timezone.now() - self.is_current_treshold) class Meta: verbose_name = "Lekce" verbose_name_plural = verbose_name + ordering = ("-timestamp", "-name") -class LectureLector(models.Model): - event = models.ForeignKey( +class LectureLector(NameStrMixin, models.Model): + lecture = models.ForeignKey( "Lecture", on_delete=models.CASCADE, related_name="lectors", - verbose_name="Událost", + verbose_name="Lekce", ) name = models.CharField( @@ -72,12 +104,12 @@ class LectureLector(models.Model): verbose_name_plural = "Lektoři" -class LectureRecording(models.Model): - event = models.ForeignKey( +class LectureRecording(NameStrMixin, models.Model): + lecture = models.ForeignKey( "Lecture", on_delete=models.CASCADE, related_name="recordings", - verbose_name="Událost", + verbose_name="Lekce", ) name = models.CharField( @@ -97,12 +129,12 @@ class LectureRecording(models.Model): verbose_name_plural = "Nahrávky" -class Material(models.Model): - event = models.ForeignKey( +class LectureMaterial(NameStrMixin, models.Model): + lecture = models.ForeignKey( "Lecture", on_delete=models.CASCADE, related_name="materials", - verbose_name="Událost", + verbose_name="Lekce", ) name = models.CharField( @@ -115,25 +147,28 @@ class Material(models.Model): blank=True, null=True, verbose_name="Odkaz", + help_text="Pokud máš zadaný odkaz, nemůžeš definovat soubor.", ) file = models.FileField( blank=True, null=True, verbose_name="Soubor", + help_text="Pokud máš vložený soubor, nemůžeš definovat odkaz.", ) def clean(self) -> None: BOTH_FILE_AND_LINK_DEFINED_ERROR = ( - "Definuj prosím pouze odkaz, nebo soubor. " - "Nemůžeš mít oboje najednou." + "Definuj prosím pouze odkaz, nebo soubor. Nemůžeš mít oboje najednou." ) if self.file and self.link: - raise ValidationError({ - "link": BOTH_FILE_AND_LINK_DEFINED_ERROR, - "file": BOTH_FILE_AND_LINK_DEFINED_ERROR, - }) + raise ValidationError( + { + "link": BOTH_FILE_AND_LINK_DEFINED_ERROR, + "file": BOTH_FILE_AND_LINK_DEFINED_ERROR, + } + ) class Meta: verbose_name = "Materiál" diff --git a/lectures/templates/lectures/index.html b/lectures/templates/lectures/index.html new file mode 100644 index 0000000..c2f99e3 --- /dev/null +++ b/lectures/templates/lectures/index.html @@ -0,0 +1,6 @@ +{% extends "shared/includes/base.html" %} +{% load markdownify %} + +{% block content %} + a +{% endblock %} diff --git a/lectures/urls.py b/lectures/urls.py new file mode 100644 index 0000000..004bc53 --- /dev/null +++ b/lectures/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from . import models, views + +app_name = "lectures" +urlpatterns = [ + path("", views.view_avilable_groups, name="view_avilable_groups"), +] diff --git a/lectures/views.py b/lectures/views.py index 91ea44a..fe8c0d0 100644 --- a/lectures/views.py +++ b/lectures/views.py @@ -1,3 +1,51 @@ -from django.shortcuts import render +from django.db import models +from django.shortcuts import get_object_or_404, render +from guardian.shortcuts import get_objects_for_user -# Create your views here. +from .models import LectureGroup + + +def view_avilable_groups(request): + lecture_groups = ( + get_objects_for_user(request.user, "lectures.view_lecturegroup") + .filter(user_groups__in=request.user.groups.all()) + .distinct() + .all() + ) + + return render( + request, + "lectures/view_groups.html", + { + "title": "Výukové skupiny", + "site_url": "https://ucebnice.pirati.cz", # TODO + "description": "Kurzy a školení zaměřené na politickou práci a organizaci kampaní.", + "header_name": "Pirátský e-Learning", + "lecture_groups": lecture_groups, + }, + ) + + +def view_lectures(request, group_id: int): + group = get_object_or_404( + get_objects_for_user(request.user, "lectures.view_lecturegroup"), + id=group_id, + ) + + lectures = ( + get_objects_for_user(request.user, "lectures.view_lecture") + .filter(groups=group) + .all() + ) + + return render( + request, + "lectures/view_lectures.html", + { + "title": f"Výuka pro {group.name}", + "site_url": "https://ucebnice.pirati.cz", # TODO + "description": f"e-Learningová výuka pro skupinu {group.name}.", + "header_name": group.name, + "lectures": lectures, + }, + ) diff --git a/manage.py b/manage.py index b792cc0..701b8e3 100755 --- a/manage.py +++ b/manage.py @@ -6,7 +6,7 @@ import sys def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ucebnice.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ucebnice.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/media/364D1F51-B67A-4FA5-83E6-1F32D62B6A78.gif b/media/364D1F51-B67A-4FA5-83E6-1F32D62B6A78.gif new file mode 100644 index 0000000000000000000000000000000000000000..676aff60c51e1b4301828c99f241174cf3b7ab71 GIT binary patch literal 19263 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 literal 0 HcmV?d00001 diff --git a/oidc/apps.py b/oidc/apps.py index 742acac..eab6c95 100644 --- a/oidc/apps.py +++ b/oidc/apps.py @@ -4,3 +4,4 @@ from django.apps import AppConfig class OidcConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "oidc" + verbose_name = "OpenID Přihlašování" diff --git a/oidc/auth.py b/oidc/auth.py index 0a0e359..97ef6a2 100644 --- a/oidc/auth.py +++ b/oidc/auth.py @@ -53,6 +53,8 @@ class UcebniceOIDCAuthenticationBackend(PiratesOIDCAuthenticationBackend): access_token, options={"verify_signature": False} ) + user.sso_username = decoded_access_token["preferred_username"] + user.email = decoded_access_token["email"] user_groups = user.groups.all() self._remove_old_user_groups( diff --git a/shared/static/shared/.gitkeep b/shared/static/shared/.gitkeep deleted file mode 100644 index 8d1c8b6..0000000 --- a/shared/static/shared/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/shared/static/shared/base.js b/shared/static/shared/base.js index d52da30..219ecfe 100644 --- a/shared/static/shared/base.js +++ b/shared/static/shared/base.js @@ -1 +1 @@ -(self.webpackChunkucebnice=self.webpackChunkucebnice||[]).push([[348],{208:()=>{}},e=>{e(e.s=208)}]); \ No newline at end of file +(self.webpackChunkucebnice=self.webpackChunkucebnice||[]).push([[348],{208:()=>{}},e=>{e(e.s=208)}]); diff --git a/shared/static/shared/favicon.png b/shared/static/shared/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..819126c749b3335134893d13561fd74a007e8f2a GIT binary patch literal 9781 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 I=akR{0K8|&eE<Le literal 0 HcmV?d00001 diff --git a/shared/static/shared/runtime.js b/shared/static/shared/runtime.js index 52a8280..3babfc0 100644 --- a/shared/static/shared/runtime.js +++ b/shared/static/shared/runtime.js @@ -1 +1 @@ -(()=>{"use strict";var r,e={},n={};function o(r){var t=n[r];if(void 0!==t)return t.exports;var i=n[r]={exports:{}};return e[r](i,i.exports,o),i.exports}o.m=e,r=[],o.O=(e,n,t,i)=>{if(!n){var a=1/0;for(l=0;l<r.length;l++){for(var[n,t,i]=r[l],s=!0,c=0;c<n.length;c++)(!1&i||a>=i)&&Object.keys(o.O).every((r=>o.O[r](n[c])))?n.splice(c--,1):(s=!1,i<a&&(a=i));if(s){r.splice(l--,1);var f=t();void 0!==f&&(e=f)}}return e}i=i||0;for(var l=r.length;l>0&&r[l-1][2]>i;l--)r[l]=r[l-1];r[l]=[n,t,i]},o.o=(r,e)=>Object.prototype.hasOwnProperty.call(r,e),(()=>{var r={666:0};o.O.j=e=>0===r[e];var e=(e,n)=>{var t,i,[a,s,c]=n,f=0;if(a.some((e=>0!==r[e]))){for(t in s)o.o(s,t)&&(o.m[t]=s[t]);if(c)var l=c(o)}for(e&&e(n);f<a.length;f++)i=a[f],o.o(r,i)&&r[i]&&r[i][0](),r[i]=0;return o.O(l)},n=self.webpackChunkucebnice=self.webpackChunkucebnice||[];n.forEach(e.bind(null,0)),n.push=e.bind(null,n.push.bind(n))})()})(); \ No newline at end of file +(()=>{"use strict";var r,e={},n={};function o(r){var t=n[r];if(void 0!==t)return t.exports;var i=n[r]={exports:{}};return e[r](i,i.exports,o),i.exports}o.m=e,r=[],o.O=(e,n,t,i)=>{if(!n){var a=1/0;for(l=0;l<r.length;l++){for(var[n,t,i]=r[l],s=!0,c=0;c<n.length;c++)(!1&i||a>=i)&&Object.keys(o.O).every((r=>o.O[r](n[c])))?n.splice(c--,1):(s=!1,i<a&&(a=i));if(s){r.splice(l--,1);var f=t();void 0!==f&&(e=f)}}return e}i=i||0;for(var l=r.length;l>0&&r[l-1][2]>i;l--)r[l]=r[l-1];r[l]=[n,t,i]},o.o=(r,e)=>Object.prototype.hasOwnProperty.call(r,e),(()=>{var r={666:0};o.O.j=e=>0===r[e];var e=(e,n)=>{var t,i,[a,s,c]=n,f=0;if(a.some((e=>0!==r[e]))){for(t in s)o.o(s,t)&&(o.m[t]=s[t]);if(c)var l=c(o)}for(e&&e(n);f<a.length;f++)i=a[f],o.o(r,i)&&r[i]&&r[i][0](),r[i]=0;return o.O(l)},n=self.webpackChunkucebnice=self.webpackChunkucebnice||[];n.forEach(e.bind(null,0)),n.push=e.bind(null,n.push.bind(n))})()})(); diff --git a/shared/static/shared/style.css b/shared/static/shared/style.css index 7958c7a..d11e2fb 100644 --- a/shared/static/shared/style.css +++ b/shared/static/shared/style.css @@ -520,18 +520,173 @@ html { --tw-backdrop-sepia: ; } +.container { + width: 100%; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + .static { position: static; } +.my-4 { + margin-top: 1rem; + margin-bottom: 1rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.mb-6 { + margin-bottom: 1.5rem; +} + +.ml-8 { + margin-left: 2rem; +} + .block { display: block; } +.inline-block { + display: inline-block; +} + .flex { display: flex; } +.w-32 { + width: 8rem; +} + +.w-8 { + width: 2rem; +} + +.cursor-pointer { + cursor: pointer; +} + +.flex-col { + flex-direction: column; +} + +.items-center { + align-items: center; +} + +.gap-2 { + gap: 0.5rem; +} + +.space-x-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.5rem * var(--tw-space-x-reverse)); + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-x-4 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(1rem * var(--tw-space-x-reverse)); + margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-y-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)); +} + +.self-start { + align-self: flex-start; +} + +.border-r { + border-right-width: 1px; +} + +.py-4 { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.py-8 { + padding-top: 2rem; + padding-bottom: 2rem; +} + +.pb-4 { + padding-bottom: 1rem; +} + +.pb-6 { + padding-bottom: 1.5rem; +} + +.pl-4 { + padding-left: 1rem; +} + +.pr-8 { + padding-right: 2rem; +} + +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.font-bold { + font-weight: 700; +} + +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + .transition { transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; @@ -539,3 +694,111 @@ html { transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; } + +.hover\:text-white:hover { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +@media (min-width: 640px) { + .sm\:flex-row { + flex-direction: row; + } + + .sm\:space-x-4 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(1rem * var(--tw-space-x-reverse)); + margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); + } + + .sm\:space-y-0 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0px * var(--tw-space-y-reverse)); + } +} + +@media (min-width: 768px) { + .md\:w-40 { + width: 10rem; + } + + .md\:flex-row { + flex-direction: row; + } + + .md\:space-x-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.5rem * var(--tw-space-x-reverse)); + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); + } + + .md\:space-y-0 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0px * var(--tw-space-y-reverse)); + } +} + +@media (min-width: 1024px) { + .lg\:my-0 { + margin-top: 0px; + margin-bottom: 0px; + } + + .lg\:mb-0 { + margin-bottom: 0px; + } + + .lg\:flex-col { + flex-direction: column; + } + + .lg\:items-end { + align-items: flex-end; + } + + .lg\:space-x-0 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0px * var(--tw-space-x-reverse)); + margin-left: calc(0px * calc(1 - var(--tw-space-x-reverse))); + } + + .lg\:space-y-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)); + } + + .lg\:py-16 { + padding-top: 4rem; + padding-bottom: 4rem; + } + + .lg\:py-24 { + padding-top: 6rem; + padding-bottom: 6rem; + } + + .lg\:text-right { + text-align: right; + } +} + +@media (min-width: 1280px) { + .xl\:flex-row { + flex-direction: row; + } + + .xl\:space-x-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.5rem * var(--tw-space-x-reverse)); + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); + } + + .xl\:space-y-0 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0px * var(--tw-space-y-reverse)); + } +} diff --git a/shared/templates/shared/includes/base.html b/shared/templates/shared/includes/base.html new file mode 100644 index 0000000..8f3808c --- /dev/null +++ b/shared/templates/shared/includes/base.html @@ -0,0 +1,198 @@ +{% load static %} +{% load render_bundle from webpack_loader %} + +<!DOCTYPE html> +<html lang="cs"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + + <meta name="title" content="{{ title }} | Učebnice"> + <meta name="description" content="{{ description }}"> + + {% comment %}Open Graph / Facebook{% endcomment %} + <meta property="og:type" content="website"> + <meta property="og:url" content="{{ site_url }}"> + <meta property="og:title" content="{{ title }} | Učebnice"> + <meta property="og:description" content="{{ description }}"> + {% comment %}<meta property="og:image" content="">{% endcomment %} + + {% comment %}Twitter{% endcomment %} + <meta property="twitter:card" content="app"> + <meta property="twitter:url" content="{{ site_url }}"> + <meta property="twitter:title" content="{{ title }} | Učebnice"> + <meta property="twitter:description" content="{{ description }}"> + {% comment %}<meta property="twitter:image" content="">{% endcomment %} + + <link + rel="icon" + type="image/png" + href="{% static "shared/favicon.png" %}" + > + + <link + href="{% static "shared/style.css" %}" + rel="stylesheet" + media="all" + > + + <link + href="https://styleguide.pirati.cz/2.12.x/css/styles.css" + rel="stylesheet" + media="all" + > + <link + href="https://styleguide.pirati.cz/2.12.x/css/pattern-scaffolding.css" + rel="stylesheet" + media="all" + > + + {% render_bundle "base" %} + + <title>{{ title }} | Učebnice</title> + </head> + <body> + <nav class="navbar navbar--simple __js-root"> + <ui-app inline-template> + <ui-navbar inline-template> + <div> + <div class="container container--default navbar__content navbar__content--initialized"> + <div class="navbar__brand flex items-center pr-8 my-4 lg:my-0"> + <a href="{% url "lectures:view_avilable_groups" %}"> + <img src="https://styleguide.pirati.cz/2.12.x/images/logo-round-white.svg" class="w-8"> + </a> + <div class="pl-4 font-bold text-xl border-r border-grey-300 pr-8"> + <a href="{% url "lectures:view_avilable_groups" %}">Učebnice</a> + </div> + {% if header_name %} + <div class="ml-8"> + {{ header_name }} + </div> + {% endif %} + </div> + + <div class="navbar__main navbar__section navbar__section--expandable container-padding--zero lg:container-padding--auto"> + <ul class="navbar-menu text-white"> + {% if user.is_staff %} + <li class="navbar-menu__item"> + <a + href="{% url "admin:index" %}" + data-href="{% url "admin:index" %}" + class="navbar-menu__link flex items-center gap-2" + > + <i class="ico--power text-sm"></i>Administrace + </a> + </li> + {% endif %} + </ul> + </div> + + <div class="navbar__actions navbar__section navbar__section--expandable container-padding--zero lg:container-padding--auto self-start flex flex-col sm:flex-row lg:flex-col sm:space-x-4 space-y-2 sm:space-y-0 lg:space-y-2 xl:flex-row xl:space-x-2 xl:space-y-0"> + {% if user and not user.is_anonymous %} + <div class="flex items-center space-x-4"> + <span class="head-heavy-2xs">{{ user.get_username }}</span> + <div class="avatar avatar--2xs"> + <img + src="https://a.pirati.cz/piratar/100/{{ user.sso_username }}.jpg" + alt="Tvůj profilový obrázek" + > + </div> + <form action="{% url "oidc_logout" %}" method="POST"> + {% csrf_token %} + <button + class="text-grey-200 hover:text-white __tooltipped" + type="submit" + aria-label="Odhlásit se" + ><i class="ico--log-out"></i></button> + </form> + </div> + {% else %} + <a + class="btn btn--white btn--hoveractive cursor-pointer" + href="{% url "oidc_authentication_init" %}" + > + <div class="btn__body">Přihlásit se</div> + </a> + {% endif %} + </div> + </div> + </div> + </ui-navbar> + </ui-app> + </nav> + + <div class="container container--default py-8 lg:py-24"> + <main> + {% block content %}{% endblock %} + </main> + </div> + + <footer class="footer bg-grey-700 text-white __js-root"> + <div> + <div class="footer__main py-4 lg:py-16 container container--default"> + <section class="footer__brand"> + <a href="https://www.pirati.cz"> + <img + src="https://styleguide.pirati.cz/2.12.x/images/logo-full-white.svg" + alt="Logo Pirátské strany" + class="w-32 md:w-40 pb-6" + > + </a> + <p class="para mb-4 text-grey-200"> + <span class="copyleft inline-block">©</span> {% now "Y" %} Piráti. + Všechna práva vyhlazena. + Sdílejte a nechte ostatní sdílet za stejných podmínek. + </p> + <p class="para mb-6 lg:mb-0 text-grey-200"> + <a href="https://www.pirati.cz/ochrana-osobnich-udaju/"> + <span class="text-grey-200">Zásady ochrany osobních údajů</span> + </a> + </p> + </section> + <section class="footer__social lg:text-right"> + <div class="mb-4"> + <div class="social-icon-group space-x-2 text-white pb-4"> + <a href="https://www.pirati.cz" class="social-icon"> + <i class="ico--home"></i> + </a> + <a href="https://www.facebook.com/ceska.piratska.strana/" class="social-icon"> + <i class="ico--facebook"></i> + </a> + <a href="https://twitter.com/PiratskaStrana" class="social-icon"> + <i class="ico--twitter"></i> + </a> + <a href="https://www.youtube.com/user/CeskaPiratskaStrana" class="social-icon"> + <i class="ico--youtube"></i> + </a> + <a href="https://www.instagram.com/pirati.cz/" class="social-icon"> + <i class="ico--instagram"></i> + </a> + <a href="https://www.flickr.com/photos/pirati/" class="social-icon"> + <i class="ico--flickr"></i> + </a> + </div> + </div> + <div class="flex flex-col md:flex-row lg:flex-col lg:items-end space-y-2 md:space-y-0 md:space-x-2 lg:space-x-0 lg:space-y-2"> + <a href="https://dary.pirati.cz" class="btn btn--icon btn--cyan-200 btn--hoveractive text-lg btn--fullwidth sm:btn--autowidth"> + <div class="btn__body-wrap"> + <div class="btn__body">Přispěj</div> + <div class="btn__icon "><i class="ico--pig"></i></div> + </div> + </a> + <a href="https://nalodeni.pirati.cz" class="btn btn--icon btn--blue-300 btn--hoveractive text-lg btn--fullwidth sm:btn--autowidth"> + <div class="btn__body-wrap"> + <div class="btn__body ">Naloď se</div> + <div class="btn__icon "><i class="ico--anchor"></i></div> + </div> + </a> + </div> + </section> + </div> + </div> + </footer> + + <script + src="https://styleguide.pirati.cz/2.12.x/js/main.bundle.js" + ></script> + </body> +</html> diff --git a/ucebnice/settings/base.py b/ucebnice/settings/base.py index 6c0fb6d..5b858b7 100644 --- a/ucebnice/settings/base.py +++ b/ucebnice/settings/base.py @@ -139,7 +139,7 @@ LOGOUT_REDIRECT_URL = "/" OIDC_RP_CLIENT_ID = env.str("OIDC_RP_CLIENT_ID") OIDC_RP_CLIENT_SECRET = env.str("OIDC_RP_CLIENT_SECRET") OIDC_RP_REALM_URL = env.str("OIDC_RP_REALM_URL") -OIDC_RP_SCOPES = "openid profile groups" +OIDC_RP_SCOPES = "openid profile email groups" OIDC_RP_SIGN_ALGO = "RS256" OIDC_RP_RESOURCE_ACCESS_CLIENT = env.str( "OIDC_RESOURCE_ACCESS_CLIENT", OIDC_RP_CLIENT_ID diff --git a/ucebnice/urls.py b/ucebnice/urls.py index d675a06..6767e30 100644 --- a/ucebnice/urls.py +++ b/ucebnice/urls.py @@ -24,5 +24,7 @@ from pirates.urls import urlpatterns as pirates_urlpatterns import ucebnice.admin urlpatterns = [ + path("", include("lectures.urls")), + path("markdownx/", include("markdownx.urls")), path("admin/", admin.site.urls), ] + pirates_urlpatterns diff --git a/users/admin.py b/users/admin.py index d2e7cac..ecd2efa 100644 --- a/users/admin.py +++ b/users/admin.py @@ -4,4 +4,9 @@ from shared.admin import MarkdownxGuardedModelAdmin from .models import User -admin.site.register(User, MarkdownxGuardedModelAdmin) + +class UserAdmin(MarkdownxGuardedModelAdmin): + autocomplete_fields = ("rsvp_lectures",) + + +admin.site.register(User, UserAdmin) diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py index ffe7ac0..2021daf 100644 --- a/users/migrations/0001_initial.py +++ b/users/migrations/0001_initial.py @@ -1,36 +1,114 @@ # Generated by Django 4.1.4 on 2023-04-14 18:14 -from django.db import migrations, models import django.utils.timezone +from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), + ("auth", "0012_alter_user_first_name_max_length"), ] operations = [ migrations.CreateModel( - name='User', + name="User", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('sso_id', models.CharField(error_messages={'unique': 'A user with that SSO ID already exists.'}, max_length=150, unique=True, verbose_name='SSO ID')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "sso_id", + models.CharField( + error_messages={ + "unique": "A user with that SSO ID already exists." + }, + max_length=150, + unique=True, + verbose_name="SSO ID", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), ], options={ - 'verbose_name': 'Uživatel', - 'verbose_name_plural': 'Uživatelé', + "verbose_name": "Uživatel", + "verbose_name_plural": "Uživatelé", }, ), ] diff --git a/users/migrations/0002_user_rsvp_lectures.py b/users/migrations/0002_user_rsvp_lectures.py new file mode 100644 index 0000000..642997e --- /dev/null +++ b/users/migrations/0002_user_rsvp_lectures.py @@ -0,0 +1,19 @@ +# Generated by Django 4.1.4 on 2023-04-18 06:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('lectures', '0001_initial'), + ('users', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='rsvp_lectures', + field=models.ManyToManyField(blank=True, null=True, to='lectures.lecture', verbose_name='Zaregistrované události'), + ), + ] diff --git a/users/migrations/0003_alter_user_rsvp_lectures.py b/users/migrations/0003_alter_user_rsvp_lectures.py new file mode 100644 index 0000000..09afbba --- /dev/null +++ b/users/migrations/0003_alter_user_rsvp_lectures.py @@ -0,0 +1,19 @@ +# Generated by Django 4.1.4 on 2023-04-18 06:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('lectures', '0001_initial'), + ('users', '0002_user_rsvp_lectures'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='rsvp_lectures', + field=models.ManyToManyField(blank=True, to='lectures.lecture', verbose_name='Zaregistrované události'), + ), + ] diff --git a/users/migrations/0004_alter_user_rsvp_lectures.py b/users/migrations/0004_alter_user_rsvp_lectures.py new file mode 100644 index 0000000..1124c13 --- /dev/null +++ b/users/migrations/0004_alter_user_rsvp_lectures.py @@ -0,0 +1,19 @@ +# Generated by Django 4.1.4 on 2023-04-18 07:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('lectures', '0001_initial'), + ('users', '0003_alter_user_rsvp_lectures'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='rsvp_lectures', + field=models.ManyToManyField(blank=True, to='lectures.lecture', verbose_name='Zaregistrované lekce'), + ), + ] diff --git a/users/migrations/0005_alter_user_email.py b/users/migrations/0005_alter_user_email.py new file mode 100644 index 0000000..bd559b8 --- /dev/null +++ b/users/migrations/0005_alter_user_email.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.4 on 2023-04-18 07:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0004_alter_user_rsvp_lectures'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-mailová adresa'), + ), + ] diff --git a/users/migrations/0006_user_sso_username.py b/users/migrations/0006_user_sso_username.py new file mode 100644 index 0000000..f0a50e5 --- /dev/null +++ b/users/migrations/0006_user_sso_username.py @@ -0,0 +1,19 @@ +# Generated by Django 4.1.4 on 2023-04-18 08:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0005_alter_user_email'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='sso_username', + field=models.CharField(default='', max_length=128, verbose_name='Username z SSO'), + preserve_default=False, + ), + ] diff --git a/users/migrations/0007_alter_user_rsvp_lectures.py b/users/migrations/0007_alter_user_rsvp_lectures.py new file mode 100644 index 0000000..c40dbef --- /dev/null +++ b/users/migrations/0007_alter_user_rsvp_lectures.py @@ -0,0 +1,19 @@ +# Generated by Django 4.1.4 on 2023-04-18 09:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('lectures', '0005_lecturegroup'), + ('users', '0006_user_sso_username'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='rsvp_lectures', + field=models.ManyToManyField(blank=True, related_name='rsvp_users', to='lectures.lecture', verbose_name='Zaregistrované lekce'), + ), + ] diff --git a/users/models.py b/users/models.py index f04e2e2..eca7b68 100644 --- a/users/models.py +++ b/users/models.py @@ -17,6 +17,24 @@ class Group: class User(pirates_models.AbstractUser): + sso_username = models.CharField( + max_length=128, + verbose_name="Username z SSO", + ) + + email = models.EmailField( + blank=True, + null=True, + verbose_name="E-mailová adresa", + ) + + rsvp_lectures = models.ManyToManyField( + "lectures.Lecture", + blank=True, + related_name="rsvp_users", + verbose_name="Zaregistrované lekce", + ) + def set_unusable_password(self) -> None: # Purely for compatibility with Guardian pass -- GitLab