diff --git a/.gitignore b/.gitignore index eca2a89b04a3e380907aa885e8b227e4611cec55..33687b8235f99519db5d1a665f0488a6b3274cea 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 8c38f3f3dad51e4585f3984282c2a4bec5349c1e..2780a04ba241bd12fb5e4d74c2938614e6d26c3a 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 86b49bc69390200e161abca3346269d60c54b2f0..8c383e046e915815d4edd30e17443dbef3541083 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 fbab9a6d5e8e818050b04cbb5410e1200dc8b29e..a18eb371e79373e798d973d68b9bd92217b17003 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 8c38f3f3dad51e4585f3984282c2a4bec5349c1e..07ab4440af7dd56c7d4d699d47d669d6d7eafb58 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 30d62f2847985f5046f071af0616c7d1202ac5cd..5518d56dc0e62c11c556ef9ef539c93c16cd98c2 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 549de6b5571b77807fd889b3f0a7027e4ec043b7..1728c286d902f81dc322fa078d7c5cf596f5e5f8 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 0000000000000000000000000000000000000000..39a23e3a523b6e537d839374260b37729410312a --- /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 0000000000000000000000000000000000000000..b605f4d22065fc1135e18a35d91ca1e8e8bd7c00 --- /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 0000000000000000000000000000000000000000..a44a7da2335db04ebf6af88419a1037e406a3ed5 --- /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 0000000000000000000000000000000000000000..0ac154f3704676bb74a69c0a7b9efca67522a895 --- /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 0000000000000000000000000000000000000000..498885384ed5df7c42d5ccca1f2ce5aaea78f7fe --- /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 0000000000000000000000000000000000000000..d1dbc97bc04fa756d92dfa16ea1d921bdafe5519 --- /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 0000000000000000000000000000000000000000..fa169f9602532418b53309a8d50b724d677c6db3 --- /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 0000000000000000000000000000000000000000..4cc5167c10e1548266b5188590873e90b2b52cda --- /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 0000000000000000000000000000000000000000..339cb6c7a1afd75b287cddc44533760af2e8de8e --- /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 81cd2a0163c90d9003b3dbf75ae51760fccff4e3..cd8ea810d5c17886005ab4563b2f1881bf35c022 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 0000000000000000000000000000000000000000..c2f99e340942f8194f69d7b444492a47070f0dba --- /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 0000000000000000000000000000000000000000..004bc535cdee9c938950982e1225a54e937f0e83 --- /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 91ea44a218fbd2f408430959283f0419c921093e..fe8c0d093b6060cc1cc0b1ee0efef281ddfb347d 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 b792cc0ecafa20e96f50afbd350bc29719c51800..701b8e3665a69f161e23733263eaccb07b868d13 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 Binary files /dev/null and b/media/364D1F51-B67A-4FA5-83E6-1F32D62B6A78.gif differ diff --git a/oidc/apps.py b/oidc/apps.py index 742acac1713eae78fd9b35b887dfc5dfe22b1989..eab6c95cfb1797f32823a2978cd8d40d63d52c42 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 0a0e359ec6e997b85d5fb3eb92d734f505a1af8b..97ef6a2a8c0cedbbe9a4fd710bc1a97866e71370 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 8d1c8b69c3fce7bea45c73efd06983e3c419a92f..0000000000000000000000000000000000000000 --- 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 d52da305ca4c673c848f91497ca84cbb9df9970f..219ecfeeed80d9a217ff743e4f5edbc80bfe36e4 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 Binary files /dev/null and b/shared/static/shared/favicon.png differ diff --git a/shared/static/shared/runtime.js b/shared/static/shared/runtime.js index 52a82802fa3960a8c24b6bf124d26ab5b5de92dc..3babfc0573770a7488e1255395385c8cf11c360b 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 7958c7a8505e637ccdcbcb34981615e160c303a0..d11e2fb9672f2cc076b54a7ad91f5139f3366618 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 0000000000000000000000000000000000000000..8f3808cc4ad86b1249ade378e9af0889cc48893e --- /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 6c0fb6de2afb7bbac2f4e1bf0d7660fdb8e9f94b..5b858b7c941e8559b14e5deee0042cf0d001ff6c 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 d675a06c2fc6547886b7d9424d2f105af4a13fd9..6767e3029e739daeff06b8f14ca2d009fa6875ce 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 d2e7cac9cda39bc9169098b90e87182b9559c76e..ecd2efa5a231ee8c87dbcaeeb7b87c64ba72c6bb 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 ffe7ac058a29e556116fd52ec9303f4cea94cb1e..2021daf93c69bde873197f0c7679f1afdb02b2a6 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 0000000000000000000000000000000000000000..642997ed060c4dadb82e46601d903a2bffc59982 --- /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 0000000000000000000000000000000000000000..09afbba68b69868329d1d6dee92d3278f8400cda --- /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 0000000000000000000000000000000000000000..1124c135dfc40f5860167bb6460a483eb2f675cf --- /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 0000000000000000000000000000000000000000..bd559b82f227743e8b30988692a4e12203543d9f --- /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 0000000000000000000000000000000000000000..f0a50e5da161281bb4da47eb3506d46428da29e6 --- /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 0000000000000000000000000000000000000000..c40dbef8760f1b4746102e122802b96936701b31 --- /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 f04e2e2281a18464a737e2ad578fa725a2a67bdb..eca7b683e1e95722296afb5442052409e5912d4e 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