The activities in the Kukui Cup competition are fairly complex objects. They're complicated enough that the basic Django form validations will not work without a fair bit of tweaking. Some of the fields in an activity are either optional or required depending on the values of other fields. Here's a few things we need outside of the basic form processing:
- If an activity is an event (is_event = True), then it must have an event date.
- If the confirmation type is either "confirmation code" or "image upload", then a prompt is required. Examples would be "Enter the confirmation code you received at the event" or "Upload a photo of yourself holding a CFL and an incandescent light bulb".
- If the confirmation type is "question and answer", then at least one question and answer is required.
- Publication date must be before the expiration date.
1, 2, and 4 are pretty straightforward, especially since I had already taken care of 1. Here's the new activity admin form.
Number 3 is a little tricky, because there can be one or many question and answer pairs. I was already aware of inline forms from the Django tutorial. So the first step was to add in inline forms for the questions and answers.
model = Activity
# Data that has passed validation.
cleaned_data = self.cleaned_data
#1 Check that an event has an event date.
is_event = cleaned_data.get("is_event")
event_date = cleaned_data.get("event_date")
has_date = cleaned_data.has_key("event_date") #Check if this is in the data dict.
if is_event and has_date and not event_date:
self._errors["event_date"] = ErrorList([u"Events require an event date."])
#2 Check the verification type.
confirm_type = cleaned_data.get("confirm_type")
prompt = cleaned_data.get("confirm_prompt")
if confirm_type != "text" and len(prompt) == 0:
self._errors["confirm_prompt"] = ErrorList([u"This confirmation type requires a confirmation prompt."])
#4 Publication date must be before the expiration date.
if cleaned_data.has_key("pub_date") and cleaned_data.has_key("expire_date"):
pub_date = cleaned_data.get("pub_date")
expire_date = cleaned_data.get("expire_date")
if pub_date >= expire_date:
self._errors["expire_date"] = ErrorList([u"The expiration date must be after the pub date."])
Question and answer fields in the admin form.
Easy enough, but now I have to implement the validation. These questions and answers should only be provided if the confirm type is "text". I did some research and figured out that I needed to extend the BaseInlineFormSet class to provide my custom validation behavior.
I chose to raise the validation error if the confirm type is not text because I don't want any question and answers saved for activities that do not need it. This should take care of most of the activity admin interface, but the requirements can always change.
"""Custom formset model to override validation."""
"""Validates the form data and checks if the activity confirmation type is text."""
# Form that represents the activity.
activity_form = self.instance
# Count the number of questions.
count = 0
for form in self.forms:
count += 1
if activity_form.confirm_type == "text" and count == 0:
raise ValidationError("At least one question is required if the activity's confirmation type is text.")
elif activity_form.confirm_type != "text" and count > 0:
raise ValidationError("Questions are not required for this confirmation type.")
model = TextPromptQuestion
extra = 3
formset = TextQuestionInlineFormSet
There are other students working on my Kukui Cup implementation. Their goal is to redesign the interface through the use of HTML and CSS. We don't want just one redesign; we want them to attempt many redesigns so that we can evaluate each one. So, instead of having them hack the settings.py file to change themes, I added a drop down form at the top so that they can change the CSS files easily. This also required moving the original files into folders to create a "default" theme. I also created custom template tags so that any file that ends with ".css" is imported in the header.
This should make it easy for the other students to see how the interface changes as they develop their own CSS.
"""Renders the CSS import header statements for a template."""
return_string = ""
css_dir = os.path.join(settings.PROJECT_ROOT, "media", settings.KUKUI_CSS_THEME, "css")
items = (item for item in os.listdir(css_dir) if string.find(item, "css") >= 0)
for item in items:
return_string += "<link rel=\"stylesheet\" href=\"/site_media/static/" + settings.KUKUI_CSS_THEME
return_string += "/css/" + item + "\" />\n"