Files
2026-07-29 10:47:40 +00:00

173 lines
5.9 KiB
Python

from django import forms
from django.core.exceptions import ValidationError
import bleach
from .models import ExamSet, Question, QuestionGroup, Section
RICH_TEXT_TAGS = ["p", "br", "strong", "em", "u", "h2", "h3", "ul", "ol", "li", "blockquote", "a"]
RICH_TEXT_ATTRIBUTES = {"a": ["href", "title", "target", "rel"]}
class SectionAdminForm(forms.ModelForm):
class Meta:
model = Section
fields = "__all__"
widgets = {
"passage_text": forms.Textarea(
attrs={
"data-rich-text-editor": "passage",
"rows": 18,
}
)
}
def clean_passage_text(self):
value = self.cleaned_data.get("passage_text") or ""
return bleach.clean(
value,
tags=RICH_TEXT_TAGS,
attributes=RICH_TEXT_ATTRIBUTES,
protocols=["http", "https", "mailto"],
strip=True,
)
class QuestionAdminForm(forms.ModelForm):
class Meta:
model = Question
fields = "__all__"
widgets = {
"prompt": forms.Textarea(
attrs={
"data-rich-text-editor": "prompt",
"rows": 7,
}
)
}
def clean_prompt(self):
value = self.cleaned_data.get("prompt") or ""
return bleach.clean(
value,
tags=RICH_TEXT_TAGS,
attributes=RICH_TEXT_ATTRIBUTES,
protocols=["http", "https", "mailto"],
strip=True,
)
class QuestionGroupAdminForm(forms.ModelForm):
class Meta:
model = QuestionGroup
fields = "__all__"
widgets = {"instructions": forms.Textarea(attrs={"rows": 4}), "layout_html": forms.Textarea(attrs={"rows": 14})}
def clean_layout_html(self):
value = self.cleaned_data.get("layout_html") or ""
allowed = RICH_TEXT_TAGS + ["div", "section", "table", "thead", "tbody", "tr", "th", "td", "caption", "span"]
return bleach.clean(value, tags=allowed, attributes={}, strip=True)
class MultipleFileInput(forms.ClearableFileInput):
allow_multiple_selected = True
class MultipleFileField(forms.FileField):
def clean(self, data, initial=None):
single_file_clean = super().clean
if isinstance(data, (list, tuple)):
return [single_file_clean(item, initial) for item in data]
return [single_file_clean(data, initial)]
class HtmlExamImportForm(forms.Form):
title = forms.CharField(
label="Test title",
max_length=200,
help_text="Use a clear name such as Academic Reading Mock Test 2.",
)
description = forms.CharField(
required=False,
widget=forms.Textarea(attrs={"rows": 3}),
)
section_type = forms.ChoiceField(
label="Skill",
choices=Section.SECTION_TYPES,
initial="reading",
help_text="The test category and every uploaded section will use this skill.",
)
html_files = MultipleFileField(
widget=MultipleFileInput(attrs={"accept": ".html,.htm,text/html"}),
help_text="Choose one complete HTML or HTM test file.",
)
publish = forms.BooleanField(
label="Publish immediately",
required=False,
help_text="Draft is recommended until questions and answers have been reviewed.",
)
def clean_html_files(self):
files = self.cleaned_data["html_files"]
if len(files) != 1:
raise ValidationError("Upload exactly one complete HTML file.")
for uploaded in files:
if not uploaded.name.lower().endswith((".html", ".htm")):
raise ValidationError(f"{uploaded.name} is not an HTML file.")
if uploaded.size > 2 * 1024 * 1024:
raise ValidationError(f"{uploaded.name} is larger than 2 MB.")
return files
class ExcelExamImportForm(forms.Form):
excel_file = forms.FileField(
label="Excel workbook",
widget=forms.ClearableFileInput(attrs={"accept": ".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}),
help_text="Upload a completed IELTS_Test_Import_Template.xlsx file (maximum 5 MB).",
)
publish = forms.BooleanField(
label="Publish immediately",
required=False,
help_text="Draft is recommended until you open and review the imported test.",
)
def clean_excel_file(self):
uploaded = self.cleaned_data["excel_file"]
if not uploaded.name.lower().endswith(".xlsx"):
raise ValidationError("Upload an .xlsx workbook.")
if uploaded.size > 5 * 1024 * 1024:
raise ValidationError("The workbook is larger than 5 MB.")
return uploaded
class ListeningTestSetupForm(forms.Form):
title = forms.CharField(
label="Listening test title",
max_length=200,
help_text="For example: Listening Practice Test 1.",
)
description = forms.CharField(
required=False,
widget=forms.Textarea(attrs={"rows": 3}),
help_text="A short note students will see before starting the test.",
)
audio_file = forms.FileField(
label="Listening audio",
widget=forms.ClearableFileInput(attrs={"accept": "audio/mpeg,audio/mp4,audio/wav,audio/ogg,.mp3,.m4a,.wav,.ogg"}),
help_text="MP3 is recommended. Maximum file size: 30 MB.",
)
time_limit_minutes = forms.IntegerField(
label="Time limit (minutes)",
min_value=1,
initial=30,
help_text="Use 30 minutes for a standard IELTS Listening practice test.",
)
def clean_audio_file(self):
uploaded = self.cleaned_data["audio_file"]
if not uploaded.name.lower().endswith((".mp3", ".m4a", ".wav", ".ogg")):
raise ValidationError("Upload an MP3, M4A, WAV, or OGG audio file.")
if uploaded.size > 30 * 1024 * 1024:
raise ValidationError("The audio file must be 30 MB or smaller.")
return uploaded