Skip to content

Commit 6c976ec

Browse files
authored
Merge pull request #113 from AvaCodeSolutions/feat/24/quiz-public-view
feat: #24 #25 Quiz submision template, frontend and API view
2 parents 6270424 + e617b6d commit 6c976ec

40 files changed

Lines changed: 952 additions & 243 deletions

django_email_learning/admin.py

Lines changed: 10 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
from django.contrib import admin
22
from django import forms
3-
from django.http import HttpRequest
43
from .models import (
54
Course,
65
ImapConnection,
7-
Quiz,
8-
Lesson,
9-
Question,
10-
Answer,
11-
CourseContent,
126
Organization,
137
OrganizationUser,
148
BlockedEmail,
9+
Enrollment,
10+
ContentDelivery,
11+
Learner,
12+
DeliverySchedule,
13+
QuizSubmission,
1514
)
1615

1716

@@ -45,51 +44,13 @@ def get_object(self, *args, **kwargs) -> ImapConnection | None: # type: ignore[
4544
return obj
4645

4746

48-
class QuizAdmin(admin.ModelAdmin):
49-
list_display = ("title", "required_score", "is_published")
50-
search_fields = ("title",)
51-
list_filter = ("is_published",)
52-
53-
def get_fields(
54-
self, request: HttpRequest, obj: Quiz | None = None
55-
) -> tuple[str, ...]:
56-
if obj is None:
57-
return ("title", "required_score")
58-
return ("title", "required_score", "is_published")
59-
60-
61-
class AnswerInline(admin.TabularInline):
62-
model = Answer
63-
extra = 1
64-
65-
66-
class QuestionAdmin(admin.ModelAdmin):
67-
inlines = [AnswerInline]
68-
list_display = ("text", "quiz")
69-
search_fields = ("text",)
70-
list_filter = ("quiz",)
71-
72-
73-
class CourseContentAdmin(admin.ModelAdmin):
74-
list_filter = ("course", "type")
75-
list_display = ("course", "priority", "type", "get_content_title")
76-
ordering = ("course", "priority")
77-
78-
def get_content_title(self, obj: CourseContent) -> str | None:
79-
if obj.type == "lesson" and obj.lesson:
80-
return obj.lesson.title
81-
elif obj.type == "quiz" and obj.quiz:
82-
return obj.quiz.title
83-
return None
84-
85-
8647
admin.site.register(Course, CourseAdmin)
8748
admin.site.register(ImapConnection, ImapConnectionAdmin)
88-
admin.site.register(Lesson)
89-
admin.site.register(Quiz, QuizAdmin)
90-
admin.site.register(CourseContent, CourseContentAdmin)
91-
admin.site.register(Question, QuestionAdmin)
92-
admin.site.register(Answer)
9349
admin.site.register(Organization)
9450
admin.site.register(BlockedEmail)
9551
admin.site.register(OrganizationUser)
52+
admin.site.register(Enrollment)
53+
admin.site.register(ContentDelivery)
54+
admin.site.register(Learner)
55+
admin.site.register(DeliverySchedule)
56+
admin.site.register(QuizSubmission)

django_email_learning/migrations/0001_initial.py

Lines changed: 128 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 5.2.8 on 2025-11-29 19:01
1+
# Generated by Django 6.0 on 2026-01-03 12:20
22

33
import django.core.validators
44
import django.db.models.deletion
@@ -32,7 +32,30 @@ class Migration(migrations.Migration):
3232
],
3333
),
3434
migrations.CreateModel(
35-
name="EventTimestamp",
35+
name="Course",
36+
fields=[
37+
(
38+
"id",
39+
models.BigAutoField(
40+
auto_created=True,
41+
primary_key=True,
42+
serialize=False,
43+
verbose_name="ID",
44+
),
45+
),
46+
("title", models.CharField(max_length=200)),
47+
(
48+
"slug",
49+
models.SlugField(
50+
help_text="A short label for the course, used in URLs or email interactive actions. You can not edit it later."
51+
),
52+
),
53+
("description", models.TextField(blank=True, null=True)),
54+
("enabled", models.BooleanField(default=False)),
55+
],
56+
),
57+
migrations.CreateModel(
58+
name="DeliverySchedule",
3659
fields=[
3760
(
3861
"id",
@@ -49,6 +72,7 @@ class Migration(migrations.Migration):
4972
db_index=True, default=django.utils.timezone.now
5073
),
5174
),
75+
("is_delivered", models.BooleanField(db_index=True, default=False)),
5276
],
5377
),
5478
migrations.CreateModel(
@@ -172,7 +196,7 @@ class Migration(migrations.Migration):
172196
},
173197
),
174198
migrations.CreateModel(
175-
name="Course",
199+
name="CourseContent",
176200
fields=[
177201
(
178202
"id",
@@ -183,29 +207,42 @@ class Migration(migrations.Migration):
183207
verbose_name="ID",
184208
),
185209
),
186-
("title", models.CharField(max_length=200)),
210+
("priority", models.IntegerField()),
187211
(
188-
"slug",
189-
models.SlugField(
190-
help_text="A short label for the course, used in URLs or email interactive actions. You can not edit it later."
212+
"type",
213+
models.CharField(
214+
choices=[("lesson", "Lesson"), ("quiz", "Quiz")], max_length=50
215+
),
216+
),
217+
(
218+
"waiting_period",
219+
models.IntegerField(
220+
help_text="Waiting period in seconds after previous content is sent or submited."
221+
),
222+
),
223+
(
224+
"course",
225+
models.ForeignKey(
226+
on_delete=django.db.models.deletion.CASCADE,
227+
to="django_email_learning.course",
191228
),
192229
),
193-
("description", models.TextField(blank=True, null=True)),
194-
("enabled", models.BooleanField(default=False)),
195230
(
196-
"imap_connection",
231+
"lesson",
197232
models.ForeignKey(
198233
blank=True,
199234
null=True,
200-
on_delete=django.db.models.deletion.SET_NULL,
201-
to="django_email_learning.imapconnection",
235+
on_delete=django.db.models.deletion.CASCADE,
236+
to="django_email_learning.lesson",
202237
),
203238
),
204239
(
205-
"organization",
240+
"quiz",
206241
models.ForeignKey(
242+
blank=True,
243+
null=True,
207244
on_delete=django.db.models.deletion.CASCADE,
208-
to="django_email_learning.organization",
245+
to="django_email_learning.quiz",
209246
),
210247
),
211248
],
@@ -271,6 +308,52 @@ class Migration(migrations.Migration):
271308
),
272309
],
273310
),
311+
migrations.CreateModel(
312+
name="ContentDelivery",
313+
fields=[
314+
(
315+
"id",
316+
models.BigAutoField(
317+
auto_created=True,
318+
primary_key=True,
319+
serialize=False,
320+
verbose_name="ID",
321+
),
322+
),
323+
("hash_value", models.CharField(blank=True, max_length=64, null=True)),
324+
(
325+
"course_content",
326+
models.ForeignKey(
327+
on_delete=django.db.models.deletion.CASCADE,
328+
to="django_email_learning.coursecontent",
329+
),
330+
),
331+
(
332+
"delivery_schedules",
333+
models.ManyToManyField(to="django_email_learning.deliveryschedule"),
334+
),
335+
(
336+
"enrollment",
337+
models.ForeignKey(
338+
on_delete=django.db.models.deletion.CASCADE,
339+
to="django_email_learning.enrollment",
340+
),
341+
),
342+
],
343+
options={
344+
"unique_together": {("enrollment", "course_content")},
345+
},
346+
),
347+
migrations.AddField(
348+
model_name="course",
349+
name="imap_connection",
350+
field=models.ForeignKey(
351+
blank=True,
352+
null=True,
353+
on_delete=django.db.models.deletion.SET_NULL,
354+
to="django_email_learning.imapconnection",
355+
),
356+
),
274357
migrations.AddField(
275358
model_name="imapconnection",
276359
name="organization",
@@ -279,6 +362,14 @@ class Migration(migrations.Migration):
279362
to="django_email_learning.organization",
280363
),
281364
),
365+
migrations.AddField(
366+
model_name="course",
367+
name="organization",
368+
field=models.ForeignKey(
369+
on_delete=django.db.models.deletion.CASCADE,
370+
to="django_email_learning.organization",
371+
),
372+
),
282373
migrations.CreateModel(
283374
name="OrganizationUser",
284375
fields=[
@@ -353,91 +444,6 @@ class Migration(migrations.Migration):
353444
to="django_email_learning.quiz",
354445
),
355446
),
356-
migrations.CreateModel(
357-
name="CourseContent",
358-
fields=[
359-
(
360-
"id",
361-
models.BigAutoField(
362-
auto_created=True,
363-
primary_key=True,
364-
serialize=False,
365-
verbose_name="ID",
366-
),
367-
),
368-
("priority", models.IntegerField()),
369-
(
370-
"type",
371-
models.CharField(
372-
choices=[("lesson", "Lesson"), ("quiz", "Quiz")], max_length=50
373-
),
374-
),
375-
(
376-
"waiting_period",
377-
models.IntegerField(
378-
help_text="Waiting period in seconds after previous content is sent or submited."
379-
),
380-
),
381-
(
382-
"course",
383-
models.ForeignKey(
384-
on_delete=django.db.models.deletion.CASCADE,
385-
to="django_email_learning.course",
386-
),
387-
),
388-
(
389-
"lesson",
390-
models.ForeignKey(
391-
blank=True,
392-
null=True,
393-
on_delete=django.db.models.deletion.CASCADE,
394-
to="django_email_learning.lesson",
395-
),
396-
),
397-
(
398-
"quiz",
399-
models.ForeignKey(
400-
blank=True,
401-
null=True,
402-
on_delete=django.db.models.deletion.CASCADE,
403-
to="django_email_learning.quiz",
404-
),
405-
),
406-
],
407-
),
408-
migrations.CreateModel(
409-
name="SentItem",
410-
fields=[
411-
(
412-
"id",
413-
models.BigAutoField(
414-
auto_created=True,
415-
primary_key=True,
416-
serialize=False,
417-
verbose_name="ID",
418-
),
419-
),
420-
("times_sent", models.IntegerField(default=1)),
421-
(
422-
"course_content",
423-
models.ForeignKey(
424-
on_delete=django.db.models.deletion.CASCADE,
425-
to="django_email_learning.coursecontent",
426-
),
427-
),
428-
(
429-
"enrollment",
430-
models.ForeignKey(
431-
on_delete=django.db.models.deletion.CASCADE,
432-
to="django_email_learning.enrollment",
433-
),
434-
),
435-
(
436-
"send_events",
437-
models.ManyToManyField(to="django_email_learning.eventtimestamp"),
438-
),
439-
],
440-
),
441447
migrations.CreateModel(
442448
name="QuizSubmission",
443449
fields=[
@@ -454,10 +460,10 @@ class Migration(migrations.Migration):
454460
("is_passed", models.BooleanField()),
455461
("submitted_at", models.DateTimeField(auto_now_add=True)),
456462
(
457-
"sent_item",
463+
"delivery",
458464
models.ForeignKey(
459465
on_delete=django.db.models.deletion.CASCADE,
460-
to="django_email_learning.sentitem",
466+
to="django_email_learning.contentdelivery",
461467
),
462468
),
463469
],
@@ -476,8 +482,26 @@ class Migration(migrations.Migration):
476482
name="course",
477483
unique_together={("slug", "organization"), ("title", "organization")},
478484
),
479-
migrations.AlterUniqueTogether(
480-
name="sentitem",
481-
unique_together={("enrollment", "course_content")},
485+
migrations.AddConstraint(
486+
model_name="coursecontent",
487+
constraint=models.UniqueConstraint(
488+
condition=models.Q(("quiz__isnull", False)),
489+
fields=("course", "quiz"),
490+
name="unique_quiz_per_course",
491+
),
492+
),
493+
migrations.AddConstraint(
494+
model_name="coursecontent",
495+
constraint=models.UniqueConstraint(
496+
condition=models.Q(("lesson__isnull", False)),
497+
fields=("course", "lesson"),
498+
name="unique_lesson_per_course",
499+
),
500+
),
501+
migrations.AddConstraint(
502+
model_name="coursecontent",
503+
constraint=models.UniqueConstraint(
504+
fields=("course", "priority"), name="unique_priority_per_course"
505+
),
482506
),
483507
]

0 commit comments

Comments
 (0)