Skip to content

Commit 24845e8

Browse files
authored
Merge pull request #72 from AvaCodeSolutions/feat/20/api-list-course-content
feat: #20 Add GET method for course content API to list quiz and less…
2 parents 8346796 + 3f1f202 commit 24845e8

6 files changed

Lines changed: 121 additions & 2 deletions

File tree

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,6 @@ make pre-commit
9797

9898
- **Type Safety**: All Python code must pass MyPy type checking
9999
- **Code Style**: Use Ruff for formatting and linting
100-
- **Test Coverage**: Maintain minimum 75% test coverage
100+
- **Test Coverage**: Maintain minimum 80% test coverage
101101
- **Security**: Code is scanned with Bandit for security issues
102102
- **Commit Messages**: Follow [Conventional Commits](https://www.conventionalcommits.org/) specification

django_email_learning/api/serializers.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,3 +342,17 @@ def serialize_waiting_period(self, waiting_period: int) -> dict:
342342
return WaitingPeriod.from_seconds(waiting_period).model_dump()
343343

344344
model_config = ConfigDict(from_attributes=True)
345+
346+
347+
class CourseContentSummaryResponse(BaseModel):
348+
id: int
349+
title: str
350+
priority: int
351+
waiting_period: int
352+
type: str
353+
354+
@field_serializer("waiting_period")
355+
def serialize_waiting_period(self, waiting_period: int) -> dict:
356+
return WaitingPeriod.from_seconds(waiting_period).model_dump()
357+
358+
model_config = ConfigDict(from_attributes=True)

django_email_learning/api/views.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def get(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-unty
6060

6161

6262
@method_decorator(accessible_for(roles={"admin", "editor"}), name="post")
63+
@method_decorator(accessible_for(roles={"admin", "editor", "viewer"}), name="get")
6364
class CourseContentView(View):
6465
def post(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
6566
payload = json.loads(request.body)
@@ -81,6 +82,21 @@ def post(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-unt
8182
except DjangoValidationError as e:
8283
return JsonResponse({"error": e.messages}, status=400)
8384

85+
def get(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
86+
try:
87+
course = Course.objects.get(id=kwargs["course_id"])
88+
course_contents = course.coursecontent_set.all().order_by("priority")
89+
response_list = []
90+
for content in course_contents:
91+
response_list.append(
92+
serializers.CourseContentSummaryResponse.model_validate(
93+
content
94+
).model_dump()
95+
)
96+
return JsonResponse({"course_contents": response_list}, status=200)
97+
except Course.DoesNotExist:
98+
return JsonResponse({"error": "Course not found"}, status=404)
99+
84100

85101
@method_decorator(accessible_for(roles={"admin", "editor"}), name="post")
86102
@method_decorator(accessible_for(roles={"admin", "editor"}), name="delete")

django_email_learning/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,14 @@ def __str__(self) -> str:
227227
return f"{self.priority} - Quiz: {self.quiz.title}"
228228
return f"{self.course.title} content #{self.priority}"
229229

230+
@property
231+
def title(self) -> str:
232+
if self.type == "lesson" and self.lesson:
233+
return self.lesson.title
234+
elif self.type == "quiz" and self.quiz:
235+
return self.quiz.title
236+
return "Untitled Content"
237+
230238
def _validate_content(self) -> None:
231239
if self.type == "lesson" and not self.lesson:
232240
raise ValidationError("Lesson must be provided for lesson content.")

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ addopts = [
103103
"--cov-report=term-missing", # Show missing lines in terminal
104104
"--cov-report=html:htmlcov", # Generate HTML report
105105
"--cov-report=xml", # Generate XML report (for CI)
106-
"--cov-fail-under=75", # Fail if coverage below 80%
106+
"--cov-fail-under=80", # Fail if coverage below 80%
107107
]
108108
markers = [
109109
"slow: marks tests as slow (deselect with '-m \"not slow\"')",

tests/api/test_views/test_course_content_view.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,3 +263,84 @@ def test_invalid_quiz_content_no_correct_answer(superadmin_client, create_course
263263
)
264264
assert response.status_code == 400
265265
assert "error" in response.json()
266+
267+
268+
@pytest.mark.parametrize(
269+
"client", ["superadmin", "editor", "viewer"], indirect=["client"]
270+
)
271+
def test_list_course_content_access(client, create_course):
272+
url = get_url()
273+
response = client.get(url)
274+
assert response.status_code == 200
275+
data = response.json()
276+
assert isinstance(data["course_contents"], list)
277+
assert len(data["course_contents"]) == 0
278+
279+
280+
def test_anonymous_user_cannot_list_course_content(anonymous_client, create_course):
281+
url = get_url()
282+
response = anonymous_client.get(url)
283+
assert response.status_code == 401
284+
285+
286+
def test_list_course_content_with_existing_contents(superadmin_client, create_course):
287+
url = get_url()
288+
# Create a lesson content
289+
lesson_payload = {
290+
"content": {
291+
"title": LESSON_TITLE,
292+
"content": LESSON_CONTENT,
293+
"type": "lesson",
294+
},
295+
"priority": 1,
296+
"waiting_period": {"period": 2, "type": "days"},
297+
}
298+
superadmin_client.post(
299+
url, json.dumps(lesson_payload), content_type="application/json"
300+
)
301+
302+
# Create a quiz content
303+
quiz_payload = {
304+
"content": {
305+
"type": "quiz",
306+
"title": "Quiz 1",
307+
"required_score": 70,
308+
"questions": [
309+
{
310+
"text": "What is Python?",
311+
"priority": 1,
312+
"answers": [
313+
{"text": "A programming language", "is_correct": True},
314+
{"text": "A snake", "is_correct": False},
315+
],
316+
}
317+
],
318+
},
319+
"priority": 2,
320+
"waiting_period": {"period": 1, "type": "hours"},
321+
}
322+
superadmin_client.post(
323+
url, json.dumps(quiz_payload), content_type="application/json"
324+
)
325+
326+
# Now list the course contents
327+
response = superadmin_client.get(url)
328+
assert response.status_code == 200
329+
data = response.json()
330+
assert isinstance(data["course_contents"], list)
331+
assert len(data["course_contents"]) == 2
332+
assert data["course_contents"][0]["type"] == "lesson"
333+
assert data["course_contents"][1]["type"] == "quiz"
334+
assert data["course_contents"][0]["priority"] == 1
335+
assert data["course_contents"][1]["priority"] == 2
336+
assert data["course_contents"][0]["id"] is not None
337+
assert data["course_contents"][1]["id"] is not None
338+
assert data["course_contents"][0]["title"] == LESSON_TITLE
339+
assert data["course_contents"][1]["title"] == "Quiz 1"
340+
assert data["course_contents"][0]["waiting_period"] == {"period": 2, "type": "days"}
341+
assert data["course_contents"][1]["waiting_period"] == {
342+
"period": 1,
343+
"type": "hours",
344+
}
345+
assert "lesson" not in data["course_contents"][0]
346+
assert "quiz" not in data["course_contents"][1]

0 commit comments

Comments
 (0)