173 lines
5.9 KiB
Python
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
|