3 # Copyright (C) 2012 FinalsClub Foundation
7 from django.conf import settings
8 from django.forms import CharField
9 from django.forms import ValidationError
11 from ajax_select.fields import AutoCompleteField, AutoCompleteSelectWidget
12 from ajax_select.fields import AutoCompleteSelectField
13 from ajax_select_cascade.fields import AutoCompleteDependentSelectField
15 # generates DIV errors with appropriate classes
16 from django.utils.safestring import mark_safe
17 from karmaworld.utils.forms import NiceErrorModelForm
18 # supports handling autocomplete fields as a model object or a value
19 from karmaworld.utils.forms import ACFieldModelForm
20 # supports filling in Foreign Key fields with another ModelForm
21 from karmaworld.utils.forms import DependentModelForm
23 from karmaworld.apps.courses.models import Course
24 from karmaworld.apps.courses.models import Professor
25 from karmaworld.apps.courses.models import Department
28 class ProfessorForm(NiceErrorModelForm, ACFieldModelForm):
29 """ Find or create a Professor. """
30 # AutoCompleteField would make sense for these fields because it only
31 # returns the value while AutoCompleteSelectField returns the object.
32 # This way, Javascript on the front end can autofill the other field based
33 # on the autocompletion of one field because the whole object is available.
35 # first argument is ajax channel name, defined in models as LookupChannel.
36 name = AutoCompleteSelectField('professor_object_by_name', help_text='',
37 label="Professor's name",
38 # allows creating a new Professor on the fly
40 widget=AutoCompleteSelectWidget('professor_object_by_name', attrs={'class': 'small-6 columns'}))
41 email = AutoCompleteSelectField('professor_object_by_email', help_text='',
42 label="Professor's email address",
43 # allows creating a new Professor on the fly
49 fields = ('name', 'email')
51 def _clean_distinct_field(self, field, *args, **kwargs):
53 Check to see if Professor model is found before grabbing the field.
54 Ensure that if Professor was already found, the new field corresponds.
56 In theory, Javascript could and should ensure this.
57 In practice, better safe than incoherent.
60 if hasattr(self, 'instance') and self.instance and self.instance.pk:
61 # Professor was already autocompleted. Save that object.
62 oldprof = self.instance
63 # Extract the field value, possibly replacing self.instance
64 value = self._clean_field(field, *args, **kwargs)
65 if oldprof and not value:
66 # This field was not supplied, but another one determined the prof.
67 # Grab field from prof model object.
68 value = getattr(oldprof, field)
69 if oldprof and self.instance != oldprof:
70 # Two different Professor objects have been found across fields.
71 raise ValidationError('It looks like two or more different Professors have been autocompleted.')
74 def clean_name(self, *args, **kwargs):
75 return self._clean_distinct_field('name', *args, **kwargs)
77 def clean_email(self, *args, **kwargs):
78 return self._clean_distinct_field('email', *args, **kwargs)
81 class DepartmentForm(NiceErrorModelForm, ACFieldModelForm):
82 """ Find and create a Department given a School. """
83 # first argument is ajax channel name, defined in models as LookupChannel.
84 school = AutoCompleteSelectField('school_object_by_name',
86 label=mark_safe('School <span class="required-field">(required)</span>'))
87 # first argument is ajax channel name, defined in models as LookupChannel.
88 name = AutoCompleteDependentSelectField(
89 'dept_object_by_name_given_school', help_text='',
90 label=mark_safe('Department name <span class="required-field">(required)</span>'),
91 # autocomplete department based on school
93 # allows creating a new department on the fly
95 widget_id='input_department_name'
101 fields = ('school', 'name')
103 def clean_name(self, *args, **kwargs):
105 Extract the name from the Department object if it already exists.
107 name = self._clean_field('name', *args, **kwargs)
108 # this might be unnecessary
110 raise ValidationError('Cannot create a Department without a name.')
114 class CourseForm(NiceErrorModelForm, DependentModelForm):
115 """ A course form which adds a honeypot and autocompletes some fields. """
116 # first argument is ajax channel name, defined in models as LookupChannel.
117 # note this AJAX field returns a field value, not a course object.
118 name = AutoCompleteField('course_name_by_name', help_text='',
119 label=mark_safe('Course name <span class="required-field">(required)</span>'))
121 def __init__(self, *args, **kwargs):
122 """ Add a dynamically named field. """
123 super(CourseForm, self).__init__(*args, **kwargs)
124 # insert honeypot into a random order on the form.
125 idx = random.randint(0, len(self.fields))
126 self.fields.insert(idx, settings.HONEYPOT_FIELD_NAME,
127 CharField(required=False, label=mark_safe(settings.HONEYPOT_LABEL))
133 fields = ('name', 'url')
135 # pass department data onto DepartmentForm
136 'department': DepartmentForm,
137 # pass professor data onto ProfessorForm
138 'professor': ProfessorForm,
141 def clean(self, *args, **kwargs):
142 """ Honeypot validation. """
144 # Call ModelFormMixin or whoever normally cleans house.
145 cleaned_data = super(CourseForm, self).clean(*args, **kwargs)
148 # parts of this code borrow from
149 # https://github.com/sunlightlabs/django-honeypot
150 hfn = settings.HONEYPOT_FIELD_NAME
151 formhoneypot = cleaned_data.get(hfn, None)
152 if formhoneypot and (formhoneypot != settings.HONEYPOT_VALUE):
153 # Highlight the failure to follow instructions.
154 self._errors[hfn] = [settings.HONEYPOT_ERROR]
155 del cleaned_data[hfn]