diff --git a/README.md b/README.md index 7bf826e..bfeb1ba 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,6 @@ Hier der vereinbarte Arbeitsablauf: 1. Issue erstellen - das geht am einfachsten über die Webseite. Der Titel beschreibt kurz und klar, was angestrebt wird. -2. Merge Request anlegen - dadurch wird der Bransch automatisch angelegt. Dieser kann mit `git pull` geholt und mit `git checkout ` bearbeitet werden. +2. Merge Request anlegen - dadurch wird der Branch automatisch angelegt. Dieser kann mit `git pull` geholt und mit `git checkout ` bearbeitet werden. 3. Nach dem Hochladen kann der Merge in den Master durchgeführt werden. diff --git a/django/core/settings.py b/django/core/settings.py index 162b0f8..10c7c97 100644 --- a/django/core/settings.py +++ b/django/core/settings.py @@ -43,6 +43,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'channels', + 'django_cleanup', ] MIDDLEWARE = [ diff --git a/django/library/admin.py b/django/library/admin.py index 6526f60..7d617a5 100644 --- a/django/library/admin.py +++ b/django/library/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import QivipQuiz, QivipQuestion, QuizCategory, Tag +from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite, QuizCategory # Für das QivipQuiz Modell class QivipQuizAdmin(admin.ModelAdmin): @@ -18,13 +18,13 @@ class QuizCategoryAdmin(admin.ModelAdmin): search_fields = ('name',) prepopulated_fields = {'slug': ('name',)} # Erstelle automatisch den Slug basierend auf dem Namen -# Für das Tag Modell -class TagAdmin(admin.ModelAdmin): - list_display = ('name',) # Zeige nur den Namen des Tags - search_fields = ('name',) + + +class QivipQuizFavoriteAdmin(admin.ModelAdmin): + list_display = ('user', 'favorite', 'quiz') # Welche Felder sollen in der Übersicht angezeigt werden # Registrierung der Modelle im Admin admin.site.register(QivipQuiz, QivipQuizAdmin) admin.site.register(QivipQuestion, QivipQuestionAdmin) admin.site.register(QuizCategory, QuizCategoryAdmin) -admin.site.register(Tag, TagAdmin) +admin.site.register(QivipQuizFavorite, QivipQuizFavoriteAdmin) diff --git a/django/library/forms.py b/django/library/forms.py index c454b71..7cb8773 100644 --- a/django/library/forms.py +++ b/django/library/forms.py @@ -4,12 +4,12 @@ from .models import QivipQuiz, QivipQuestion class QuizForm(forms.ModelForm): class Meta: model = QivipQuiz - fields = ['name', 'description','image', 'status', 'category', 'tags',"difficulty","credits"] + fields = ['name', 'description','image', 'status', 'category','difficulty','credits'] class QuestionForm(forms.ModelForm): class Meta: model = QivipQuestion - fields = ['quiz_id', 'data'] + fields = ['quiz_id', 'data','time_per_question'] from django import forms diff --git a/django/library/migrations/0033_qivipquestion_time_per_question.py b/django/library/migrations/0033_qivipquestion_time_per_question.py new file mode 100644 index 0000000..fd5a7a1 --- /dev/null +++ b/django/library/migrations/0033_qivipquestion_time_per_question.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.7 on 2025-04-08 14:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0032_qivipquiz_average_rating_qivipquiz_rating_count_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='qivipquestion', + name='time_per_question', + field=models.CharField(choices=[('10', 10), ('15', 15), ('30', 30), ('60', 60), ('90', 90), ('120', 120), ('180', 180), ('nicht gesetzt', 'nicht gesetzt')], default=30, help_text='Zeit pro Frage in Sekunden', max_length=13), + ), + ] diff --git a/django/library/migrations/0033_remove_qivipquiz_tags_delete_tag.py b/django/library/migrations/0033_remove_qivipquiz_tags_delete_tag.py new file mode 100644 index 0000000..680c87a --- /dev/null +++ b/django/library/migrations/0033_remove_qivipquiz_tags_delete_tag.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.7 on 2025-04-10 13:59 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0032_qivipquiz_average_rating_qivipquiz_rating_count_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='qivipquiz', + name='tags', + ), + migrations.DeleteModel( + name='Tag', + ), + ] diff --git a/django/library/migrations/0034_alter_qivipquestion_time_per_question.py b/django/library/migrations/0034_alter_qivipquestion_time_per_question.py new file mode 100644 index 0000000..3c50abb --- /dev/null +++ b/django/library/migrations/0034_alter_qivipquestion_time_per_question.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.7 on 2025-04-08 14:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0033_qivipquestion_time_per_question'), + ] + + operations = [ + migrations.AlterField( + model_name='qivipquestion', + name='time_per_question', + field=models.CharField(choices=[('10', 10), ('15', 15), ('30', 30), ('60', 60), ('90', 90), ('120', 120), ('180', 180)], default=30, help_text='Zeit pro Frage in Sekunden', max_length=13), + ), + ] diff --git a/django/library/migrations/0034_alter_qivipquiz_status.py b/django/library/migrations/0034_alter_qivipquiz_status.py new file mode 100644 index 0000000..27a3a46 --- /dev/null +++ b/django/library/migrations/0034_alter_qivipquiz_status.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.7 on 2025-04-10 14:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0033_remove_qivipquiz_tags_delete_tag'), + ] + + operations = [ + migrations.AlterField( + model_name='qivipquiz', + name='status', + field=models.CharField(choices=[('öffentlich', 'Öffentlich'), ('privat', 'Privat')], default='öffentlich', max_length=10), + ), + ] diff --git a/django/library/migrations/0035_alter_qivipquestion_time_per_question.py b/django/library/migrations/0035_alter_qivipquestion_time_per_question.py new file mode 100644 index 0000000..b8825eb --- /dev/null +++ b/django/library/migrations/0035_alter_qivipquestion_time_per_question.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.7 on 2025-04-08 14:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0034_alter_qivipquestion_time_per_question'), + ] + + operations = [ + migrations.AlterField( + model_name='qivipquestion', + name='time_per_question', + field=models.CharField(choices=[('10', 10), ('15', 15), ('30', 30), ('60', 60), ('90', 90), ('120', 120), ('180', 180)], default=30, help_text='Zeit pro Frage in Sekunden', max_length=3), + ), + ] diff --git a/django/library/migrations/0035_qivipquiz_favorite.py b/django/library/migrations/0035_qivipquiz_favorite.py new file mode 100644 index 0000000..756b801 --- /dev/null +++ b/django/library/migrations/0035_qivipquiz_favorite.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.7 on 2025-04-10 14:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0034_alter_qivipquiz_status'), + ] + + operations = [ + migrations.AddField( + model_name='qivipquiz', + name='favorite', + field=models.BooleanField(default=False), + ), + ] diff --git a/django/library/migrations/0036_remove_qivipquiz_favorite_quizfavorite.py b/django/library/migrations/0036_remove_qivipquiz_favorite_quizfavorite.py new file mode 100644 index 0000000..f0efab0 --- /dev/null +++ b/django/library/migrations/0036_remove_qivipquiz_favorite_quizfavorite.py @@ -0,0 +1,32 @@ +# Generated by Django 5.1.7 on 2025-04-10 16:02 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0035_qivipquiz_favorite'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RemoveField( + model_name='qivipquiz', + name='favorite', + ), + migrations.CreateModel( + name='QuizFavorite', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('favorite', models.BooleanField(default=False)), + ('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.qivipquiz')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('user', 'quiz')}, + }, + ), + ] diff --git a/django/library/migrations/0037_rename_quizfavorite_qivipquizfavorite.py b/django/library/migrations/0037_rename_quizfavorite_qivipquizfavorite.py new file mode 100644 index 0000000..6eb61d5 --- /dev/null +++ b/django/library/migrations/0037_rename_quizfavorite_qivipquizfavorite.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.7 on 2025-04-10 16:05 + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0036_remove_qivipquiz_favorite_quizfavorite'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RenameModel( + old_name='QuizFavorite', + new_name='QivipQuizFavorite', + ), + ] diff --git a/django/library/migrations/0038_alter_qivipquizfavorite_quiz.py b/django/library/migrations/0038_alter_qivipquizfavorite_quiz.py new file mode 100644 index 0000000..1efd121 --- /dev/null +++ b/django/library/migrations/0038_alter_qivipquizfavorite_quiz.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.7 on 2025-04-10 17:08 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0037_rename_quizfavorite_qivipquizfavorite'), + ] + + operations = [ + migrations.AlterField( + model_name='qivipquizfavorite', + name='quiz', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to='library.qivipquiz'), + ), + ] diff --git a/django/library/migrations/0039_qivipquizfavorite_favorite_date.py b/django/library/migrations/0039_qivipquizfavorite_favorite_date.py new file mode 100644 index 0000000..a4eb4d8 --- /dev/null +++ b/django/library/migrations/0039_qivipquizfavorite_favorite_date.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.7 on 2025-04-11 12:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0038_alter_qivipquizfavorite_quiz'), + ] + + operations = [ + migrations.AddField( + model_name='qivipquizfavorite', + name='favorite_date', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/django/library/migrations/0040_alter_qivipquiz_description.py b/django/library/migrations/0040_alter_qivipquiz_description.py new file mode 100644 index 0000000..16ac3b0 --- /dev/null +++ b/django/library/migrations/0040_alter_qivipquiz_description.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.7 on 2025-04-11 13:15 + +import library.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0039_qivipquizfavorite_favorite_date'), + ] + + operations = [ + migrations.AlterField( + model_name='qivipquiz', + name='description', + field=models.TextField(default='In dem Quiz geht es um ...', max_length=200, validators=[library.models.QivipQuiz.validate_description], verbose_name='Beschreibung'), + ), + ] diff --git a/django/library/migrations/0041_alter_qivipquiz_category_alter_qivipquiz_difficulty_and_more.py b/django/library/migrations/0041_alter_qivipquiz_category_alter_qivipquiz_difficulty_and_more.py new file mode 100644 index 0000000..32fee6b --- /dev/null +++ b/django/library/migrations/0041_alter_qivipquiz_category_alter_qivipquiz_difficulty_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 5.1.7 on 2025-04-11 13:21 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0040_alter_qivipquiz_description'), + ] + + operations = [ + migrations.AlterField( + model_name='qivipquiz', + name='category', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quiz', to='library.quizcategory', verbose_name='Kategorie'), + ), + migrations.AlterField( + model_name='qivipquiz', + name='difficulty', + field=models.CharField(choices=[('1', '1'), ('2', '2'), ('3', '3'), ('4', '4'), ('5', '5'), ('nicht gesetzt', 'nicht gesetzt')], default='nicht gesetzt', help_text='1: niedrigste Schwierigkeit und 5: höchste Schwierigkeit', max_length=13, verbose_name='Schwierigkeit'), + ), + migrations.AlterField( + model_name='qivipquiz', + name='image', + field=models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/', verbose_name='Bild'), + ), + migrations.AlterField( + model_name='qivipquiz', + name='name', + field=models.CharField(max_length=75, verbose_name='Name des Quizzes'), + ), + ] diff --git a/django/library/migrations/0042_qivipquiz_favorite.py b/django/library/migrations/0042_qivipquiz_favorite.py new file mode 100644 index 0000000..9913a85 --- /dev/null +++ b/django/library/migrations/0042_qivipquiz_favorite.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.7 on 2025-04-11 13:43 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0041_alter_qivipquiz_category_alter_qivipquiz_difficulty_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='qivipquiz', + name='favorite', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quizzes', to='library.qivipquizfavorite'), + ), + ] diff --git a/django/library/migrations/0043_remove_qivipquiz_favorite.py b/django/library/migrations/0043_remove_qivipquiz_favorite.py new file mode 100644 index 0000000..f7dfdfc --- /dev/null +++ b/django/library/migrations/0043_remove_qivipquiz_favorite.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.7 on 2025-04-11 15:25 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0042_qivipquiz_favorite'), + ] + + operations = [ + migrations.RemoveField( + model_name='qivipquiz', + name='favorite', + ), + ] diff --git a/django/library/migrations/0044_remove_qivipquizfavorite_favorite_date_and_more.py b/django/library/migrations/0044_remove_qivipquizfavorite_favorite_date_and_more.py new file mode 100644 index 0000000..c3df207 --- /dev/null +++ b/django/library/migrations/0044_remove_qivipquizfavorite_favorite_date_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.7 on 2025-04-11 16:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0043_remove_qivipquiz_favorite'), + ] + + operations = [ + migrations.RemoveField( + model_name='qivipquizfavorite', + name='favorite_date', + ), + migrations.AddField( + model_name='qivipquiz', + name='favorite_date', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/django/library/migrations/0045_remove_qivipquiz_favorite_date_and_more.py b/django/library/migrations/0045_remove_qivipquiz_favorite_date_and_more.py new file mode 100644 index 0000000..8d92ed4 --- /dev/null +++ b/django/library/migrations/0045_remove_qivipquiz_favorite_date_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.7 on 2025-04-11 16:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0044_remove_qivipquizfavorite_favorite_date_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='qivipquiz', + name='favorite_date', + ), + migrations.AddField( + model_name='qivipquizfavorite', + name='favorite_date', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/django/library/migrations/0046_alter_qivipquizfavorite_favorite_date.py b/django/library/migrations/0046_alter_qivipquizfavorite_favorite_date.py new file mode 100644 index 0000000..e08ca9d --- /dev/null +++ b/django/library/migrations/0046_alter_qivipquizfavorite_favorite_date.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.7 on 2025-04-11 16:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0045_remove_qivipquiz_favorite_date_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='qivipquizfavorite', + name='favorite_date', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/django/library/migrations/0047_merge_20250418_1334.py b/django/library/migrations/0047_merge_20250418_1334.py new file mode 100644 index 0000000..7419abf --- /dev/null +++ b/django/library/migrations/0047_merge_20250418_1334.py @@ -0,0 +1,14 @@ +# Generated by Django 5.1.7 on 2025-04-18 11:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0035_alter_qivipquestion_time_per_question'), + ('library', '0046_alter_qivipquizfavorite_favorite_date'), + ] + + operations = [ + ] diff --git a/django/library/migrations/0048_qivipquestion_image.py b/django/library/migrations/0048_qivipquestion_image.py new file mode 100644 index 0000000..85a5c57 --- /dev/null +++ b/django/library/migrations/0048_qivipquestion_image.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.7 on 2025-04-18 11:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0047_merge_20250418_1334'), + ] + + operations = [ + migrations.AddField( + model_name='qivipquestion', + name='image', + field=models.ImageField(blank=True, height_field=900, max_length=200, null=True, upload_to='question_images/', verbose_name='Bild', width_field=1000), + ), + ] diff --git a/django/library/migrations/0049_alter_qivipquestion_image.py b/django/library/migrations/0049_alter_qivipquestion_image.py new file mode 100644 index 0000000..3ee5e21 --- /dev/null +++ b/django/library/migrations/0049_alter_qivipquestion_image.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.7 on 2025-04-18 19:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0048_qivipquestion_image'), + ] + + operations = [ + migrations.AlterField( + model_name='qivipquestion', + name='image', + field=models.ImageField(blank=True, null=True, upload_to='question_images/', verbose_name='Bild'), + ), + ] diff --git a/django/library/models.py b/django/library/models.py index ceb2eb5..23d46a3 100644 --- a/django/library/models.py +++ b/django/library/models.py @@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError class QivipQuiz(models.Model): STATUS_VALUES = { "öffentlich": "Öffentlich", - "versteckt": "Versteckt", + #"versteckt": "Versteckt", "privat": "Privat", } DIFFICULTY_VALUES = { @@ -19,12 +19,15 @@ class QivipQuiz(models.Model): "nicht gesetzt":"nicht gesetzt", } + + + def validate_description(value): if value.strip() == "In dem Quiz geht es um ...": raise ValidationError("Bitte gib eine aussagekräftige Beschreibung ein.") - image = models.ImageField(max_length=256, upload_to='quiz_images/', blank=True, null=True) # Bild speichern in media/quiz_images/ + image = models.ImageField(max_length=256,verbose_name="Bild", upload_to='quiz_images/', blank=True, null=True) # Bild speichern in media/quiz_images/ uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) user_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='quiz') #creator = models.ForeignKey(User, on_delete=models.SET_NULL,blank=True, null=True, related_name='creator_quiz') @@ -32,11 +35,10 @@ class QivipQuiz(models.Model): creation_date = models.DateTimeField(auto_now_add=True) update_date = models.DateTimeField(auto_now=True) status = models.CharField(max_length=10, choices=list(STATUS_VALUES.items()), default="öffentlich") - category = models.ForeignKey('QuizCategory', related_name='quiz', on_delete=models.CASCADE) - tags = models.ManyToManyField('Tag', blank=True) - name = models.CharField(max_length=75) - description = models.TextField(validators=[validate_description],max_length=200,blank=False, default="In dem Quiz geht es um ...") - difficulty= models.CharField(max_length=13, choices=list(DIFFICULTY_VALUES.items()), default="nicht gesetzt", help_text="1: niedrigste Schwierigkeit und 5: höchste Schwierigkeit") + category = models.ForeignKey('QuizCategory',verbose_name="Kategorie", related_name='quiz', on_delete=models.CASCADE) + name = models.CharField(max_length=75,verbose_name="Name des Quizzes") + description = models.TextField(validators=[validate_description],max_length=200,blank=False, default="In dem Quiz geht es um ...", verbose_name="Beschreibung") + difficulty= models.CharField(verbose_name="Schwierigkeit", max_length=13, choices=list(DIFFICULTY_VALUES.items()), default="nicht gesetzt", help_text="1: niedrigste Schwierigkeit und 5: höchste Schwierigkeit") credits=models.TextField(blank=True, editable=True) average_rating = models.FloatField(default=3.0) rating_count = models.IntegerField(default=0) @@ -44,24 +46,39 @@ class QivipQuiz(models.Model): return self.name +class QivipQuizFavorite(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) # Verknüpfung zu User + quiz = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='favorites') # Verknüpfung zu Quiz + favorite = models.BooleanField(default=False) # Favoritenstatus + favorite_date = models.DateTimeField(null=True, blank=True) + + class Meta: + unique_together = ('user', 'quiz') # Sicherstellen, dass es nur ein Favorit pro User und Quiz gibt # Create your models here. class QivipQuestion(models.Model): + + TIME_VALUES = { + "10": 10, + "15": 15, + "30": 30, + "60": 60, + "90": 90, + "120": 120, + "180": 180, + } uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) quiz_id = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='questions') creation_date = models.DateTimeField(auto_now_add=True) update_date = models.DateTimeField(auto_now=True) data = models.TextField() - + time_per_question = models.CharField(max_length=3, choices=list(TIME_VALUES.items()), default=30, help_text="Zeit pro Frage in Sekunden") + image = models.ImageField(verbose_name="Bild", upload_to="question_images/", blank=True, null=True) def __str__(self): return self.data[:50] -class Tag(models.Model): - name = models.CharField(max_length=100, unique=True) - def __str__(self): - return self.name class QuizRating(models.Model): quiz = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='ratings') diff --git a/django/library/urls.py b/django/library/urls.py index 28b2b76..1721a2b 100644 --- a/django/library/urls.py +++ b/django/library/urls.py @@ -15,5 +15,6 @@ urlpatterns = [ path('question/edit//', views.edit_question, name='edit_question'), path('question/delete//', views.delete_question, name='delete_question'), path('detail/copy//', views.copy_quiz, name='copy_quiz'), + path('favorite_quiz//', views.favorite_quiz, name='favorite_quiz'), ] diff --git a/django/library/views.py b/django/library/views.py index 36d77b9..0f05e0d 100644 --- a/django/library/views.py +++ b/django/library/views.py @@ -1,10 +1,12 @@ +from django.utils import timezone import uuid +from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User from django.urls import reverse from django.contrib import messages -from .models import QivipQuiz, QivipQuestion +from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite from .forms import QuizForm, QuestionForm import json from django.core.paginator import Paginator @@ -17,12 +19,23 @@ from django.contrib.auth.models import User def overview_quiz(request): # Filter form = QuizFilterForm(request.GET) + try: + favorite_quizzes_data = QivipQuizFavorite.objects.filter(user=request.user, favorite=True).order_by("-favorite_date") + favorite_quiz_ids = favorite_quizzes_data.values_list('quiz', flat=True) + favorite_quizzes = QivipQuiz.objects.filter(id__in=favorite_quiz_ids) + except: + favorite_quizzes_data = QivipQuizFavorite.objects.none() + favorite_quizzes = QivipQuizFavorite.objects.none() + favorite_quiz_ids = favorite_quizzes.none() + favorite_quizzes = QivipQuiz.objects.none() # Initialize querysets if request.user.is_authenticated: filter_my_quizzes = QivipQuiz.objects.filter(user_id=request.user).annotate(max_amout_questions=Count('questions')) + favorite_quizzes = favorite_quizzes.filter(id__in=favorite_quiz_ids).annotate(max_amout_questions=Count('questions')) filter_other_quizzes = QivipQuiz.objects.exclude(user_id=request.user) else: + favorite_quizzes = QivipQuiz.objects.none() filter_my_quizzes = QivipQuiz.objects.none() filter_other_quizzes = QivipQuiz.objects.all() @@ -30,11 +43,49 @@ def overview_quiz(request): filter_other_quizzes = filter_other_quizzes.annotate(max_amout_questions=Count('questions')) filter_other_quizzes = filter_other_quizzes.filter(max_amout_questions__gte=1, status='öffentlich') + + + + if form.is_valid(): + search = form.cleaned_data.get('search') username= form.cleaned_data.get('user') max_amout_questions= form.cleaned_data.get('max_amout_questions') min_amout_questions= form.cleaned_data.get('min_amout_questions') + + filters = {} + if username: + try: + user = User.objects.get(username=username) + filters['user_id'] = user.id + except User.DoesNotExist: + favorite_quizzes = QivipQuiz.objects.none() + filter_my_quizzes = QivipQuiz.objects.none() + filter_other_quizzes=QivipQuiz.objects.none() + + if search: + filters['name__icontains'] = search + if min_amout_questions: + filters['max_amout_questions__gte'] = min_amout_questions + if max_amout_questions: + filters['max_amout_questions__lte'] = max_amout_questions + + try: + filter_other_quizzes = filter_other_quizzes.filter(**filters) + except: + filter_other_quizzes=QivipQuiz.objects.none() + try: + filter_my_quizzes = filter_my_quizzes.filter(**filters) + except: + filter_my_quizzes=QivipQuiz.objects.none() + + try: + favorite_quizzes = favorite_quizzes.filter(**filters) + except: + favorite_quizzes=QivipQuiz.objects.none() + + """ if search: # Suche nach Namen filter_other_quizzes = filter_other_quizzes.filter(name__icontains=search) filter_my_quizzes = filter_my_quizzes.filter(name__icontains=search) @@ -63,9 +114,9 @@ def overview_quiz(request): filter_other_quizzes=filter_other_quizzes.exclude(user_id=request.user) except: filter_other_quizzes=filter_other_quizzes + """ - # Pagination for my quizzes my_quizzes_paginator = Paginator(filter_my_quizzes.order_by('-creation_date'), 8) # 8 quizzes per page try: @@ -80,17 +131,64 @@ def overview_quiz(request): except: other_quizzes = other_quizzes_paginator.page(1) + + + try: + favorite_quizzes = favorite_quizzes.filter( + favorites__user=request.user, # Filtere nach dem User + favorites__favorite=True # Filtere nach favorisierten Quizzes + ).order_by('-favorites__favorite_date') + except: + pass + + + # Pagination for favorite_quizzes + favorite_quizzes_paginator = Paginator(favorite_quizzes, 8) # 8 quizzes per page + try: + favorite_quizzes = favorite_quizzes_paginator.page(request.GET.get('my_page', 1)) + except: + favorite_quizzes = favorite_quizzes_paginator.page(1) + context = { 'show_search': True, + 'favorite_quiz_ids': favorite_quiz_ids, + 'favorite_quizzes':favorite_quizzes, 'my_quizzes': my_quizzes, 'other_quizzes': other_quizzes, 'form': form, } return render(request, 'library/overview_quiz.html', context) - +# Neues Quiz erstellen +@login_required +def favorite_quiz(request, pk): + quiz= get_object_or_404(QivipQuiz, pk=pk) + favorite = QivipQuizFavorite.objects.filter(user=request.user, quiz=quiz).first() + + if favorite: + favorite.favorite=not favorite.favorite + + + if favorite.favorite==True: + favorite.favorite_date = timezone.now() + + statement="Das Quiz '"+ favorite.quiz.name+ "' wurde erfolgreich zu den Favoriten hinzugefügt!" + messages.success(request,statement ) + favorite.save() + else: + favorite.favorite_date = None + favorite.delete() + statement="Das Quiz '"+ favorite.quiz.name+ "' wurde erfolgreich von den Favoriten entfernt!" + messages.error(request, statement) + + + else: + QivipQuizFavorite.objects.create(user=request.user, quiz=quiz, favorite=True, favorite_date=timezone.now()) + + return redirect(request.META.get('HTTP_REFERER', 'library:overview_quiz')) + # Neues Quiz erstellen @login_required @@ -102,7 +200,7 @@ def new_quiz(request): quiz.user_id = request.user #quiz.creator = request.user quiz.save() - form.save_m2m() # Speichert die Many-to-Many Beziehungen (Tags) + #form.save_m2m() # Speichert die Many-to-Many Beziehungen (Tags) return redirect('library:detail_quiz', pk=quiz.pk) else: @@ -151,33 +249,7 @@ def detail_quiz(request, pk): 'detail':show_answers, } return render(request, 'library/detail_quiz.html', context) -""" -def modified(request, pk): - original_quiz = get_object_or_404(QivipQuiz, pk=pk) - words=original_quiz.modified_description.split() - word_exist=0 - print(original_quiz.creator.username) - print(request.user.username) - - for word in words: - - if word==request.user.username or word+","==request.user.username: - word_exist=1 - - - if word_exist==0 and len(words)!=0 and request.user.username!=original_quiz.creator.username: - original_quiz.modified_description +=", "+ request.user.username - - - elif word_exist==0 and request.user.username!=original_quiz.creator.username: - original_quiz.modified_description +=" "+ request.user.username - - original_quiz.save() - - return redirect('library:detail_quiz', pk=pk) - -""" @login_required def copy_quiz(request, pk): @@ -206,6 +278,7 @@ def copy_quiz(request, pk): question.data = question.data # `data`-Feld wird explizit übernommen # Verknüpfe mit dem neuen Quiz question.save() + messages.success(request, "Quiz wurde erfolgreich kopiert!") return redirect('library:detail_quiz', pk=new_quiz.pk) @@ -243,6 +316,7 @@ def new_question(request): if request.method == 'POST': question_text = request.POST.get('question', '') + question_image = request.FILES.get('question_image', None) # Handle different question types if question_type == 'true_false': @@ -282,6 +356,8 @@ def new_question(request): question = QivipQuestion() question.data = json.dumps(json_data) question.quiz_id = quiz + question.image = question_image + question.time_per_question = request.POST.get('time_per_question', '30') question.save() #return modified(request, pk=quiz.pk) return redirect('library:detail_quiz', pk=quiz.pk) @@ -296,6 +372,7 @@ def new_question(request): {'value': 'Falsch', 'is_correct': False} ] } + standard_time_per_question = 30 elif question_type == 'multiple_choice': question_data = { 'type': question_type, @@ -305,12 +382,14 @@ def new_question(request): {'value': '', 'is_correct': False} ] } + standard_time_per_question = 30 template_name = f'library/question/question_{question_type}.html' context = { 'question': question_data, 'quiz_id': quiz_id, - 'quiz': quiz + 'quiz': quiz, + 'standard_time_per_question': standard_time_per_question, } return render(request, template_name, context) @@ -326,6 +405,7 @@ def edit_question(request, pk): try: question_data = json.loads(question.data) if question.data else {} question_type = question_data.get('type', 'multiple_choice') + question_image = question.image # For true/false questions, get the correct answer from the options if question_type == 'true_false': @@ -370,7 +450,11 @@ def edit_question(request, pk): if request.method == 'POST': question_text = request.POST.get('question', '') - + try: + question_image = request.FILES['question_image'] + except Exception: + pass + # Handle different question types if question_type == 'true_false': correct_answer = request.POST.get('correct_answer') == 'true' @@ -411,6 +495,8 @@ def edit_question(request, pk): } question.data = json.dumps(json_data) + question.time_per_question = request.POST.get('time_per_question', '30') + question.image = question_image question.save() #return modified(request, question.quiz_id.pk) return redirect('library:detail_quiz', pk=question.quiz_id.pk) @@ -420,7 +506,9 @@ def edit_question(request, pk): context = { 'question': {'data': question_data}, # Wrap in data structure expected by template 'quiz_id': question.quiz_id.pk, - 'quiz': question.quiz_id + 'quiz': question.quiz_id, + 'time_per_question': question.time_per_question, + 'image': question_image, } return render(request, template_name, context) diff --git a/django/play/urls.py b/django/play/urls.py index 81f59e8..261b472 100644 --- a/django/play/urls.py +++ b/django/play/urls.py @@ -12,4 +12,6 @@ urlpatterns = [ path('game//waiting', views.waiting_room, name='waiting'), path('game//scores', views.scores, name='scores'), path('game//finished', views.finished, name='finished'), + path('select_mode/', views.select_mode, name='select_mode'), + path('selected_mode/', views.selected_mode, name='selected_mode'), ] \ No newline at end of file diff --git a/django/play/views.py b/django/play/views.py index 87871b0..4bbd671 100644 --- a/django/play/views.py +++ b/django/play/views.py @@ -1,3 +1,4 @@ +from django.utils import timezone from django.shortcuts import render from django.shortcuts import render, redirect, get_object_or_404, reverse from play.models import QuizGame, QuizGameParticipant @@ -7,6 +8,7 @@ from django.contrib import messages import json + # Create your views here. def lobby(request, join_code): quiz_game = get_object_or_404(QuizGame, join_code=join_code) @@ -16,12 +18,16 @@ def lobby(request, join_code): 'quiz_game': quiz_game, } + mode = request.GET.get('mode','default') + request.session['mode'] = mode if "host_id" in request.COOKIES: host_id = request.COOKIES['host_id'] if host_id == quiz_game.host_id: context['host_id'] = host_id return render(request, 'play/lobby.html', context=context) - + elif mode=="learn": + return redirect('play:create_participant', join_code=join_code) + if not "participant_id" in request.COOKIES: return redirect('play:create_participant', join_code=join_code) @@ -38,46 +44,54 @@ def lobby(request, join_code): context['participant'] = participant return render(request, 'play/lobby.html', context=context) -def create_participant(request, join_code): - quiz_game = get_object_or_404(QuizGame, join_code=join_code) +def select_mode(request,join_code): + return render(request, 'play/select_mode.html', {'join_code': join_code}) - if request.method == 'GET' and "participant_id" in request.COOKIES: - # Prüfe ob der Teilnehmer noch existiert - participant_id = request.COOKIES['participant_id'] - try: - participant = QuizGameParticipant.objects.get(participant_id=participant_id) - if participant.quiz_game.join_code != join_code: - participant.quiz_game = quiz_game - participant.score = 0 # Reset score when joining new game - participant.save() - return render(request, 'play/initialize_participant.html', {'participant': participant, 'join_code': quiz_game.join_code}) - except QuizGameParticipant.DoesNotExist: - # Cookie löschen wenn Teilnehmer nicht mehr existiert - response = HttpResponseRedirect(request.path) - response.delete_cookie('participant_id') +def create_participant(request, join_code): + mode = request.GET.get('mode','default') + request.session['mode'] = mode + quiz_game = get_object_or_404(QuizGame, join_code=join_code) + if mode!="learn": + if request.method == 'GET' and "participant_id" in request.COOKIES: + # Prüfe ob der Teilnehmer noch existiert + participant_id = request.COOKIES['participant_id'] + try: + participant = QuizGameParticipant.objects.get(participant_id=participant_id) + if participant.quiz_game.join_code != join_code: + participant.quiz_game = quiz_game + participant.score = 0 # Reset score when joining new game + participant.save() + return render(request, 'play/initialize_participant.html', {'participant': participant, 'join_code': quiz_game.join_code}) + except QuizGameParticipant.DoesNotExist: + # Cookie löschen wenn Teilnehmer nicht mehr existiert + response = HttpResponseRedirect(request.path) + response.delete_cookie('participant_id') + return response + + if request.method == 'POST': + display_name = request.POST.get('display_name') + if not display_name: + return render(request, 'play/initialize_participant.html', { + 'join_code': join_code, + 'error': 'Bitte geben Sie einen Namen ein' + }) + + participant = QuizGameParticipant() + participant.display_name = display_name + participant.quiz_game = quiz_game + participant.score = 0 # Initialize score + participant.save() + + response = HttpResponseRedirect(reverse('play:lobby', kwargs={'join_code': join_code})) + response.set_cookie('participant_id', participant.participant_id, max_age=3600) return response - if request.method == 'POST': - display_name = request.POST.get('display_name') - if not display_name: - return render(request, 'play/initialize_participant.html', { - 'join_code': join_code, - 'error': 'Bitte geben Sie einen Namen ein' - }) - - participant = QuizGameParticipant() - participant.display_name = display_name - participant.quiz_game = quiz_game - participant.score = 0 # Initialize score - participant.save() - - response = HttpResponseRedirect(reverse('play:lobby', kwargs={'join_code': join_code})) - response.set_cookie('participant_id', participant.participant_id, max_age=3600) - return response - - return render(request, 'play/initialize_participant.html', {'join_code': join_code}) - + return render(request, 'play/initialize_participant.html', {'join_code': join_code}) + else: + messages.error(request, "Beitritt nicht möglich.") + return render(request, 'play/join_game.html') def create_game(request, quiz_id): + try: quiz = QivipQuiz.objects.get(id=quiz_id) game = QuizGame() @@ -85,7 +99,8 @@ def create_game(request, quiz_id): game.host_id = game.generate_unique_id() game.save() - response = HttpResponseRedirect(reverse('play:lobby', kwargs={'join_code': game.join_code})) + + response = HttpResponseRedirect(reverse('play:select_mode', kwargs={'join_code': game.join_code})) response.set_cookie('host_id', game.host_id, max_age=3600) return response @@ -128,7 +143,7 @@ def finished(request, join_code): def scores(request, join_code): quiz_game = get_object_or_404(QuizGame, join_code=join_code) - + # Verify game state if quiz_game.current_state != 'scores': return redirect('play:lobby', join_code=join_code) @@ -146,9 +161,18 @@ def scores(request, join_code): current_question = questions[quiz_game.current_question_index] question_data = json.loads(current_question.data) + try: + my_participant = request.session.get('my_participant-participant_id') + participant = QuizGameParticipant.objects.get(participant_id=my_participant) + my_participant=participant + except: + participant = QuizGameParticipant.objects.none() + return render(request, 'play/game/score_overview.html', { 'quiz_game': quiz_game, 'participants': participants, + 'participant': participant, + 'my_participant':my_participant, 'is_host': is_host, 'host_id': quiz_game.host_id if is_host else None, 'is_last_question': quiz_game.current_question_index + 1 >= len(questions), @@ -210,10 +234,12 @@ def question(request, join_code): return render(request, 'play/game/question_host.html', { 'quiz_game': quiz_game, 'question_data': question_data, + 'current_question':current_question, 'host_id': quiz_game.host_id, 'start_time': int(quiz_game.question_start_time.timestamp() * 1000), 'question_index': quiz_game.current_question_index, - 'total_questions': len(questions) + 'total_questions': len(questions), + 'question_image': current_question.image }) # Handle participant view @@ -223,16 +249,20 @@ def question(request, join_code): participant_id = request.COOKIES['participant_id'] try: participant = QuizGameParticipant.objects.get(participant_id=participant_id) + + except QuizGameParticipant.DoesNotExist: return redirect('play:create_participant', join_code=join_code) + request.session['my_participant-participant_id'] = participant.participant_id return render(request, 'play/game/question_participant.html', { 'quiz_game': quiz_game, 'question_data': question_data, + 'current_question':current_question, 'participant': participant, 'start_time': int(quiz_game.question_start_time.timestamp() * 1000), 'question_index': quiz_game.current_question_index, - 'total_questions': len(questions) + 'total_questions': len(questions), }) def waiting_room(request, join_code): @@ -255,4 +285,43 @@ def waiting_room(request, join_code): return render(request, 'play/game/wait_for_other_players.html', { 'quiz_game': quiz_game, 'participant': participant - }) \ No newline at end of file + }) + + +def selected_mode(request, join_code): + + + mode = request.GET.get('mode','default') + request.session['mode'] = mode + quiz_game = get_object_or_404(QuizGame, join_code=join_code) + quiz = get_object_or_404(QivipQuiz, pk=quiz_game.quiz_id.id) + QuizGameParticipant.objects.filter(quiz_game=quiz_game).delete() + + context = { + 'quiz': quiz, + 'quiz_game': quiz_game, + } + if "host_id" in request.COOKIES: + host_id = request.COOKIES['host_id'] + if host_id == quiz_game.host_id: + context['host_id'] = host_id + + if mode=="learn": + participant = QuizGameParticipant() + if request.user.is_authenticated: + participant.display_name = request.user.username + else: + participant.display_name = "SIE" + + participant.participant_id=host_id + participant.quiz_game = quiz_game + participant.score = 0 # Initialize score + participant.save() + quiz_game.current_state = "question" + quiz_game.question_start_time = timezone.now() + quiz_game.save() + + return redirect('play:question',join_code) + + + return render(request, 'play/lobby.html', context=context) diff --git a/django/templates/base.html b/django/templates/base.html index 3ec89de..416bfd7 100644 --- a/django/templates/base.html +++ b/django/templates/base.html @@ -17,7 +17,18 @@
- + + {% if messages %} + + {% endif %} + {% block extra_js %}{% endblock %} diff --git a/django/templates/library/detail_quiz.html b/django/templates/library/detail_quiz.html index 21a7e3d..f62d2bb 100644 --- a/django/templates/library/detail_quiz.html +++ b/django/templates/library/detail_quiz.html @@ -3,25 +3,54 @@ {% block content %}
-
-

{{ quiz.name }}

-
+ +
+

{{ quiz.name }}

+ +
+ +
{% if quiz.user_id == request.user %} - + - + {% endif %} - Quiz kopieren -
+ +
+ + + + + Quiz kopieren + + + {% if quiz.questions.count != 0 %} + + + + + + Spielen + + {% endif %} +
+
+ +
{% if quiz.description %} @@ -31,7 +60,7 @@ {% if detail == True %} -
+
@@ -42,7 +71,7 @@ {% else %} -
+
@@ -58,18 +87,18 @@
-
+

Fragen

-
+
{% if quiz.user_id == request.user %} + class=" mt-1 bg-blue-100 text-blue-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2"> Multiple Choice {% endif %} {% if quiz.user_id == request.user %} + class=" mt-1 bg-purple-100 text-purple-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2"> Wahr/Falsch {% endif %} @@ -170,12 +199,12 @@
{% if quiz.base_quiz %} -

Kopie von: {{ quiz.base_quiz}}

+

Kopie von: {{ quiz.base_quiz}}

{% endif %}
{% if quiz.base_quiz %} -

Autor: {{ quiz.base_quiz.user_id}}

+

Autor: {{ quiz.base_quiz.user_id}}

{% endif %}
diff --git a/django/templates/library/form.html b/django/templates/library/form.html index 4d7d13d..a7d8da5 100644 --- a/django/templates/library/form.html +++ b/django/templates/library/form.html @@ -11,7 +11,7 @@ Abbrechen
diff --git a/django/templates/library/overview_quiz.html b/django/templates/library/overview_quiz.html index 955cb27..97ca65a 100644 --- a/django/templates/library/overview_quiz.html +++ b/django/templates/library/overview_quiz.html @@ -53,15 +53,15 @@
- + {{ form.min_amout_questions }}
- + {{ form.max_amout_questions }}
- + {{ form.user }}
@@ -77,6 +77,7 @@
+ {% if user.is_authenticated %}
@@ -92,13 +93,42 @@
{% if quiz.image %} -
+
+ +
+
+
+ {% else %}
{% endif %} + {% if user.is_authenticated %} + {% if quiz.id not in favorite_quiz_ids %} +
+ + + + + + +
+ + {% else %} + + {% endif %} + {% endif %}
+ +
@@ -163,6 +193,7 @@
+ {% if quiz.questions.count != 0 %} @@ -171,6 +202,9 @@ Spielen + {% else %} + Fürs Spielen ist min. 1 Frage nötig! + {% endif %}
@@ -179,7 +213,7 @@ - {% if quiz.user == request.user %} + {% if quiz.user_id == request.user %} @@ -228,6 +262,203 @@
{% endif %} + + + +{% if user.is_authenticated %} +
+
+

Meine Favoriten

+
+
+
+ +
+
+ {% for quiz in favorite_quizzes %} +
+ + + + + + +
+ +

+ {{ quiz.name }} +

+ +

{{ quiz.description }}

+ + +
+ +
+ + + + {{ quiz.difficulty }} +
+ + +
+ + + + {{ quiz.questions.count }} Fragen +
+ + +
+ + + + {{ quiz.status }} +
+ + +
+ {% for i in '12345'|make_list %} + {% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %} + + {% else %} + + {% endif %} + {% endfor %} + ({{ quiz.rating_count }}) +
+
+ + + {% if quiz.authors.all %} +
+ + + + + {% for author in quiz.authors.all %} + {{ author.username }}{% if not forloop.last %}, {% endif %} + {% endfor %} + +
+ {% endif %} + + +
+ {% if quiz.questions.count != 0 %} + + + + + + Spielen + + {% else %} + Fürs Spielen ist min. 1 Frage nötig! + {% endif %} + +
+ + + + + + + {% if quiz.user_id == request.user %} + + + + + + + + + + + {% endif %} +
+
+
+
+ {% endfor %} + +
+ + + {% if favorite_quizzes.paginator.num_pages > 1 %} +
+ {% if favorite_quizzes.has_previous %} + + + + + + {% endif %} + + Seite {{ favorite_quizzes.number }} von {{ favorite_quizzes.paginator.num_pages }} + + {% if my_quizzes.has_next %} + + + + + + {% endif %} +
+ {% endif %} +
+{% endif %} + + + + + + + + +
@@ -244,11 +475,39 @@
{% if quiz.image %} -
+
+ +
+
+
+ + {% else %}
{% endif %} + {% if user.is_authenticated %} + {% if quiz.id not in favorite_quiz_ids %} +
+ + + + + + +
+ + {% else %} + + {% endif %} + {% endif %}
@@ -315,6 +574,7 @@ + + {% if image %} + Bild der Frage + {% endif %} +
+ + +
+
@@ -92,6 +100,19 @@
+
+ + +
+
- + {% csrf_token %} @@ -24,6 +24,14 @@
+ {% if image %} + Bild der Frage + {% endif %} +
+ + +
+
@@ -43,6 +51,19 @@
+
+ + +
+
diff --git a/django/templates/partials/_nav.html b/django/templates/partials/_nav.html index 60f8a6f..963ebc8 100644 --- a/django/templates/partials/_nav.html +++ b/django/templates/partials/_nav.html @@ -1,23 +1,23 @@ -