Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,6 @@ make pre-commit

- **Type Safety**: All Python code must pass MyPy type checking
- **Code Style**: Use Ruff for formatting and linting
- **Test Coverage**: Maintain minimum 75% test coverage
- **Test Coverage**: Maintain minimum 80% test coverage
- **Security**: Code is scanned with Bandit for security issues
- **Commit Messages**: Follow [Conventional Commits](https://www.conventionalcommits.org/) specification
14 changes: 14 additions & 0 deletions django_email_learning/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,3 +342,17 @@ def serialize_waiting_period(self, waiting_period: int) -> dict:
return WaitingPeriod.from_seconds(waiting_period).model_dump()

model_config = ConfigDict(from_attributes=True)


class CourseContentSummaryResponse(BaseModel):
id: int
title: str
priority: int
waiting_period: int
type: str

@field_serializer("waiting_period")
def serialize_waiting_period(self, waiting_period: int) -> dict:
return WaitingPeriod.from_seconds(waiting_period).model_dump()

model_config = ConfigDict(from_attributes=True)
16 changes: 16 additions & 0 deletions django_email_learning/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def get(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-unty


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

def get(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
try:
course = Course.objects.get(id=kwargs["course_id"])
course_contents = course.coursecontent_set.all().order_by("priority")
response_list = []
for content in course_contents:
response_list.append(
serializers.CourseContentSummaryResponse.model_validate(
content
).model_dump()
)
return JsonResponse({"course_contents": response_list}, status=200)
except Course.DoesNotExist:
return JsonResponse({"error": "Course not found"}, status=404)


@method_decorator(accessible_for(roles={"admin", "editor"}), name="post")
@method_decorator(accessible_for(roles={"admin", "editor"}), name="delete")
Expand Down
8 changes: 8 additions & 0 deletions django_email_learning/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,14 @@ def __str__(self) -> str:
return f"{self.priority} - Quiz: {self.quiz.title}"
return f"{self.course.title} content #{self.priority}"

@property
def title(self) -> str:
if self.type == "lesson" and self.lesson:
return self.lesson.title
elif self.type == "quiz" and self.quiz:
return self.quiz.title
return "Untitled Content"

def _validate_content(self) -> None:
if self.type == "lesson" and not self.lesson:
raise ValidationError("Lesson must be provided for lesson content.")
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ addopts = [
"--cov-report=term-missing", # Show missing lines in terminal
"--cov-report=html:htmlcov", # Generate HTML report
"--cov-report=xml", # Generate XML report (for CI)
"--cov-fail-under=75", # Fail if coverage below 80%
"--cov-fail-under=80", # Fail if coverage below 80%
]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
Expand Down
81 changes: 81 additions & 0 deletions tests/api/test_views/test_course_content_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,84 @@ def test_invalid_quiz_content_no_correct_answer(superadmin_client, create_course
)
assert response.status_code == 400
assert "error" in response.json()


@pytest.mark.parametrize(
"client", ["superadmin", "editor", "viewer"], indirect=["client"]
)
def test_list_course_content_access(client, create_course):
url = get_url()
response = client.get(url)
assert response.status_code == 200
data = response.json()
assert isinstance(data["course_contents"], list)
assert len(data["course_contents"]) == 0


def test_anonymous_user_cannot_list_course_content(anonymous_client, create_course):
url = get_url()
response = anonymous_client.get(url)
assert response.status_code == 401


def test_list_course_content_with_existing_contents(superadmin_client, create_course):
url = get_url()
# Create a lesson content
lesson_payload = {
"content": {
"title": LESSON_TITLE,
"content": LESSON_CONTENT,
"type": "lesson",
},
"priority": 1,
"waiting_period": {"period": 2, "type": "days"},
}
superadmin_client.post(
url, json.dumps(lesson_payload), content_type="application/json"
)

# Create a quiz content
quiz_payload = {
"content": {
"type": "quiz",
"title": "Quiz 1",
"required_score": 70,
"questions": [
{
"text": "What is Python?",
"priority": 1,
"answers": [
{"text": "A programming language", "is_correct": True},
{"text": "A snake", "is_correct": False},
],
}
],
},
"priority": 2,
"waiting_period": {"period": 1, "type": "hours"},
}
superadmin_client.post(
url, json.dumps(quiz_payload), content_type="application/json"
)

# Now list the course contents
response = superadmin_client.get(url)
assert response.status_code == 200
data = response.json()
assert isinstance(data["course_contents"], list)
assert len(data["course_contents"]) == 2
assert data["course_contents"][0]["type"] == "lesson"
assert data["course_contents"][1]["type"] == "quiz"
assert data["course_contents"][0]["priority"] == 1
assert data["course_contents"][1]["priority"] == 2
assert data["course_contents"][0]["id"] is not None
assert data["course_contents"][1]["id"] is not None
assert data["course_contents"][0]["title"] == LESSON_TITLE
assert data["course_contents"][1]["title"] == "Quiz 1"
assert data["course_contents"][0]["waiting_period"] == {"period": 2, "type": "days"}
assert data["course_contents"][1]["waiting_period"] == {
"period": 1,
"type": "hours",
}
assert "lesson" not in data["course_contents"][0]
assert "quiz" not in data["course_contents"][1]