Improve form layout
[oweals/karmaworld.git] / karmaworld / apps / courses / forms.py
1 #!/usr/bin/env python
2 # -*- coding:utf8 -*-
3 # Copyright (C) 2012  FinalsClub Foundation
4
5 import random
6
7 from django.conf import settings
8 from django.forms import CharField
9 from django.forms import ValidationError
10
11 from ajax_select.fields import AutoCompleteField, AutoCompleteSelectWidget
12 from ajax_select.fields import AutoCompleteSelectField
13 from ajax_select_cascade.fields import AutoCompleteDependentSelectField
14
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
22
23 from karmaworld.apps.courses.models import Course
24 from karmaworld.apps.courses.models import Professor
25 from karmaworld.apps.courses.models import Department
26
27
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.
34
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
39         required=False,
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
44         required=False)
45
46     class Meta:
47         model = Professor
48         # order the fields
49         fields = ('name', 'email')
50
51     def _clean_distinct_field(self, field, *args, **kwargs):
52         """
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.
55
56         In theory, Javascript could and should ensure this.
57         In practice, better safe than incoherent.
58         """
59         oldprof = None
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.')
72         return value
73
74     def clean_name(self, *args, **kwargs):
75         return self._clean_distinct_field('name', *args, **kwargs)
76
77     def clean_email(self, *args, **kwargs):
78         return self._clean_distinct_field('email', *args, **kwargs)
79
80
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',
85                                      help_text='',
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
92         dependsOn=school,
93         # allows creating a new department on the fly
94         required=False,
95         widget_id='input_department_name'
96     )
97
98     class Meta:
99         model = Department
100         # order the fields
101         fields = ('school', 'name')
102
103     def clean_name(self, *args, **kwargs):
104         """
105         Extract the name from the Department object if it already exists.
106         """
107         name = self._clean_field('name', *args, **kwargs)
108         # this might be unnecessary
109         if not name:
110             raise ValidationError('Cannot create a Department without a name.')
111         return name
112
113
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>'))
120
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))
128         )
129
130     class Meta:
131         model = Course
132         # order the fields
133         fields = ('name', 'url')
134         model_fields = {
135             # pass department data onto DepartmentForm
136             'department': DepartmentForm,
137             # pass professor data onto ProfessorForm
138             'professor': ProfessorForm,
139         }
140
141     def clean(self, *args, **kwargs):
142         """ Honeypot validation. """
143
144         # Call ModelFormMixin or whoever normally cleans house.
145         cleaned_data = super(CourseForm, self).clean(*args, **kwargs)
146
147         # Check the honeypot
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]
156         return cleaned_data