Initial commit
This commit is contained in:
+172
@@ -0,0 +1,172 @@
|
||||
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
|
||||
Reference in New Issue
Block a user