Skip to content

Commit 9fc7851

Browse files
committed
Merge pull request #94 from AvaCodeSolutions/feat/23/course-page-component
feat: #23 frontend UI for course page (PR1)
2 parents a19dd56 + f627095 commit 9fc7851

34 files changed

Lines changed: 2650 additions & 86 deletions

File tree

django_email_learning/api/serializers.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,13 @@ class LessonCreate(BaseModel):
174174
type: Literal["lesson"]
175175

176176

177+
class LessonUpdate(BaseModel):
178+
title: Optional[str] = None
179+
content: Optional[str] = None
180+
181+
model_config = ConfigDict(extra="forbid")
182+
183+
177184
class LessonResponse(BaseModel):
178185
id: int
179186
title: str
@@ -282,10 +289,17 @@ def from_seconds(cls, seconds: int) -> "WaitingPeriod":
282289

283290

284291
class CreateCourseContentRequest(BaseModel):
285-
priority: int = Field(gt=0, examples=[1])
292+
priority: int | None = Field(gt=0, examples=[1], default=None)
286293
waiting_period: WaitingPeriod
287294
content: LessonCreate | QuizCreate = Field(discriminator="type")
288295

296+
@property
297+
def required_priority(self) -> int:
298+
if self.priority is not None:
299+
return self.priority
300+
else:
301+
raise ValueError("Priority must be set before converting to Django model.")
302+
289303
def to_django_model(self, course: Course) -> CourseContent:
290304
lesson = None
291305
quiz = None
@@ -319,7 +333,7 @@ def to_django_model(self, course: Course) -> CourseContent:
319333
content_type = "quiz"
320334
course_content = CourseContent.objects.create(
321335
course=course,
322-
priority=self.priority,
336+
priority=self.required_priority,
323337
waiting_period=self.waiting_period.to_seconds(),
324338
lesson=lesson,
325339
quiz=quiz,

django_email_learning/api/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from django_email_learning.api.views import (
44
CourseView,
55
ImapConnectionView,
6+
LessonView,
67
OrganizationsView,
78
SingleCourseView,
89
CourseContentView,
@@ -38,6 +39,11 @@
3839
SingleCourseContentView.as_view(),
3940
name="single_course_content_view",
4041
),
42+
path(
43+
"organizations/<int:organization_id>/lessons/<int:lesson_id>/",
44+
LessonView.as_view(),
45+
name="lesson_view",
46+
),
4147
path("organizations/", OrganizationsView.as_view(), name="organizations_view"),
4248
path("session", UpdateSessionView.as_view(), name="update_session_view"),
4349
path("", page_not_found, name="root"),

django_email_learning/api/views.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
from django.db.utils import IntegrityError
55
from django.http import JsonResponse
66
from django.core.exceptions import ValidationError as DjangoValidationError
7+
from django.db import models
78
from pydantic import ValidationError
89

910
from django_email_learning.api import serializers
1011
from django_email_learning.models import (
1112
Course,
1213
CourseContent,
1314
ImapConnection,
15+
Lesson,
1416
OrganizationUser,
1517
Organization,
1618
)
@@ -68,6 +70,14 @@ def post(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-unt
6870
try:
6971
serializer = serializers.CreateCourseContentRequest.model_validate(payload)
7072
course = Course.objects.get(id=kwargs["course_id"])
73+
if serializer.priority is None:
74+
# Set priority to max existing priority + 1
75+
max_priority = (
76+
CourseContent.objects.filter(course_id=course.id)
77+
.aggregate(max_priority=models.Max("priority"))
78+
.get("max_priority")
79+
)
80+
serializer.priority = (max_priority or 0) + 1
7181
course_content = serializer.to_django_model(course=course)
7282

7383
return JsonResponse(
@@ -130,6 +140,8 @@ def delete(self, request, *args, **kwargs): # type: ignore[no-untyped-def]
130140
except (IntegrityError, ValueError) as e:
131141
return JsonResponse({"error": str(e)}, status=409)
132142

143+
# TODO: Implement POST method for updating course content.
144+
133145

134146
@method_decorator(accessible_for(roles={"admin", "editor"}), name="post")
135147
@method_decorator(accessible_for(roles={"admin", "editor"}), name="delete")
@@ -255,6 +267,29 @@ def post(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-unt
255267
return JsonResponse({"error": str(e)}, status=409)
256268

257269

270+
@method_decorator(accessible_for(roles={"admin", "editor"}), name="post")
271+
class LessonView(View):
272+
def post(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
273+
payload = json.loads(request.body)
274+
try:
275+
serializer = serializers.LessonUpdate.model_validate(payload)
276+
lesson = Lesson.objects.get(id=kwargs["lesson_id"])
277+
if serializer.title is not None:
278+
lesson.title = serializer.title
279+
if serializer.content is not None:
280+
lesson.content = serializer.content
281+
lesson.save()
282+
283+
return JsonResponse(
284+
{},
285+
status=204,
286+
)
287+
except Lesson.DoesNotExist:
288+
return JsonResponse({"error": "Lesson not found"}, status=404)
289+
except ValidationError as e:
290+
return JsonResponse({"error": e.errors()}, status=400)
291+
292+
258293
@method_decorator(is_an_organization_member(), name="post")
259294
class UpdateSessionView(View):
260295
def post(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]

django_email_learning/platform/views.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ class CourseView(BasePlatformView):
8282
def get_context_data(self, **kwargs) -> dict: # type: ignore[no-untyped-def]
8383
context = super().get_context_data(**kwargs)
8484
course = Course.objects.get(pk=self.kwargs["course_id"])
85-
context["page_title"] = course.title
85+
context["course"] = course
86+
context["page_title"] = f"Course: {course.title}"
8687
return context
8788

8889

120 KB
Loading

django_email_learning/templates/platform/base.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{% load django_vite %}
2+
{% load static %}
23
<!doctype html>
34
<html lang="en">
45
<head>
@@ -14,6 +15,7 @@
1415
</script>
1516
<meta charset="UTF-8" />
1617
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
18+
<link rel="icon" type="image/png" href="{% static 'logo.png' %}" />
1719
<title>{% block title %}{{ page_title }}{% endblock %}</title>
1820
</head>
1921
<body>
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
{% extends "platform/base.html" %}
22
{% load django_vite %}
33
{% block extra_head %}
4-
<!-- {% vite_asset 'courses/Courses.jsx' %} -->
4+
<script>
5+
let course_title = "{{ course.title|escapejs }}";
6+
let course_id = "{{ course.id }}";
7+
</script>
8+
{% vite_asset 'course/Course.jsx' %}
59
{% endblock %}

django_service/settings.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565

6666
CORS_ALLOW_CREDENTIALS = True
6767
CSRF_COOKIE_SECURE = False
68-
CSRF_COOKIE_SAMESITE = "None"
68+
# CSRF_COOKIE_SAMESITE = "None"
6969

7070

7171
ROOT_URLCONF = "django_service.urls"
@@ -148,7 +148,14 @@
148148

149149
STATIC_URL = "static/"
150150

151-
STATIC_ROOT = BASE_DIR / "static"
151+
# For development - where Django looks for static files
152+
STATICFILES_DIRS = [
153+
BASE_DIR / "django_service" / "static",
154+
]
155+
156+
# For production - where collectstatic puts files
157+
STATIC_ROOT = BASE_DIR / "staticfiles"
158+
152159

153160
# Default primary key field type
154161
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
120 KB
Loading
94.3 KB
Loading

0 commit comments

Comments
 (0)