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
20 changes: 20 additions & 0 deletions courses/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
UserTaskAnswerFile,
UserTaskAnswerOption,
)
from .models.content import looks_like_image_file

# Admin-only captions for sections in app index
CourseModule._meta.verbose_name = "Модуль"
Expand Down Expand Up @@ -148,6 +149,25 @@ class Meta:
model = CourseTask
fields = "__all__"

def clean(self):
cleaned_data = super().clean()
image_upload = cleaned_data.get("image_upload")
attachment_upload = cleaned_data.get("attachment_upload")
if image_upload and not looks_like_image_file(
mime_type=getattr(image_upload, "content_type", ""),
extension=getattr(image_upload, "name", "").rsplit(".", 1)[-1],
):
self.add_error(
"image_upload",
"В поле изображения можно загрузить только файл изображения.",
)

# Preserve the fact that a file was provided so model validation
# doesn't add a second "required image" error for the same field.
self.instance._has_pending_image_upload = bool(image_upload)
self.instance._has_pending_attachment_upload = bool(attachment_upload)
return cleaned_data


class CourseModuleAdminForm(forms.ModelForm):
avatar_upload = forms.FileField(
Expand Down
52 changes: 48 additions & 4 deletions courses/models/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@
)
from .course import Course

ALLOWED_IMAGE_EXTENSIONS = {
"bmp",
"gif",
"jpg",
"jpeg",
"png",
"svg",
"webp",
}


def looks_like_image_file(*, mime_type: str | None = None, extension: str | None = None) -> bool:
normalized_mime_type = (mime_type or "").strip().lower()
if normalized_mime_type.startswith("image/"):
return True

normalized_extension = (extension or "").strip().lower().lstrip(".")
return normalized_extension in ALLOWED_IMAGE_EXTENSIONS


class CourseModule(models.Model):
course = models.ForeignKey(
Expand Down Expand Up @@ -273,11 +292,36 @@ def __str__(self):
def _require_non_blank(value: str | None) -> bool:
return bool(value and value.strip())

def _has_image_source(self) -> bool:
return self.image_file_id is not None or getattr(
self,
"_has_pending_image_upload",
False,
)

def _has_attachment_source(self) -> bool:
return self.attachment_file_id is not None or getattr(
self,
"_has_pending_attachment_upload",
False,
)

def _has_valid_image_file(self) -> bool:
if self.image_file_id is None:
return False
return looks_like_image_file(
mime_type=self.image_file.mime_type,
extension=self.image_file.extension,
)

def clean(self):
super().clean()

errors = {}

if self.image_file_id is not None and not self._has_valid_image_file():
errors["image_file"] = "В поле изображения можно выбрать только файл изображения."

if self.task_kind == CourseTaskKind.INFORMATIONAL:
if self.informational_type == CourseTaskInformationalType.VIDEO_TEXT:
if not self._require_non_blank(self.body_text):
Expand All @@ -292,7 +336,7 @@ def clean(self):
errors["body_text"] = (
"Поле обязательно для типа 'Текст и изображение'."
)
if self.image_file_id is None:
if not self._has_image_source():
errors["image_file"] = (
"Поле обязательно для типа 'Текст и изображение'."
)
Expand All @@ -303,15 +347,15 @@ def clean(self):
errors["body_text"] = (
"Поле обязательно для типа вопроса 'Изображение и текст'."
)
if self.image_file_id is None:
if not self._has_image_source():
errors["image_file"] = (
"Поле обязательно для типа вопроса 'Изображение и текст'."
)
elif self.question_type == CourseTaskQuestionType.VIDEO:
if not self._require_non_blank(self.video_url):
errors["video_url"] = "Поле обязательно для типа вопроса 'Видео'."
elif self.question_type == CourseTaskQuestionType.IMAGE:
if self.image_file_id is None:
if not self._has_image_source():
errors["image_file"] = (
"Поле обязательно для типа вопроса 'Изображение'."
)
Expand All @@ -320,7 +364,7 @@ def clean(self):
errors["body_text"] = (
"Поле обязательно для типа вопроса 'Текст с файлом'."
)
if self.attachment_file_id is None:
if not self._has_attachment_source():
errors["attachment_file"] = (
"Поле обязательно для типа вопроса 'Текст с файлом'."
)
Expand Down
3 changes: 3 additions & 0 deletions courses/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class CourseAnalyticsStubSerializer(serializers.Serializer):

class CourseCardSerializer(serializers.Serializer):
id = serializers.IntegerField()
partner_program_id = serializers.IntegerField(allow_null=True)
title = serializers.CharField()
access_type = serializers.ChoiceField(choices=CourseAccessType.choices)
status = serializers.ChoiceField(choices=CourseContentStatus.choices)
Expand All @@ -45,6 +46,7 @@ class CourseCardSerializer(serializers.Serializer):

class CourseDetailSerializer(serializers.Serializer):
id = serializers.IntegerField()
partner_program_id = serializers.IntegerField(allow_null=True)
title = serializers.CharField()
description = serializers.CharField(allow_blank=True)
access_type = serializers.ChoiceField(choices=CourseAccessType.choices)
Expand Down Expand Up @@ -127,6 +129,7 @@ class CourseModuleStructureSerializer(serializers.Serializer):

class CourseStructureSerializer(serializers.Serializer):
course_id = serializers.IntegerField()
partner_program_id = serializers.IntegerField(allow_null=True)
progress_status = serializers.ChoiceField(choices=ProgressStatus.choices)
percent = serializers.IntegerField(min_value=0, max_value=100)
modules = CourseModuleStructureSerializer(many=True)
Expand Down
3 changes: 3 additions & 0 deletions courses/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ def get(self, request):
data.append(
{
"id": course.id,
"partner_program_id": course.partner_program_id,
"title": course.title,
"access_type": course.access_type,
"status": course.status,
Expand Down Expand Up @@ -122,6 +123,7 @@ def get(self, request, pk: int):
serializer = CourseDetailSerializer(
data={
"id": course.id,
"partner_program_id": course.partner_program_id,
"title": course.title,
"description": course.description,
"access_type": course.access_type,
Expand Down Expand Up @@ -255,6 +257,7 @@ def get(self, request, pk: int):
serializer = CourseStructureSerializer(
data={
"course_id": course.id,
"partner_program_id": course.partner_program_id,
"progress_status": course_progress_payload["status"],
"percent": course_progress_payload["percent"],
"modules": modules_payload,
Expand Down
Loading