Prevent blank professors from being created, while still allowing on-the-fly professo...
[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=mark_safe('Professor\'s name <span class="required-field">(required)</span>'),
38         # allows creating a new Professor on the fly
39         required=False)
40     email = AutoCompleteSelectField('professor_object_by_email', help_text='',
41         label="Professor's email address",
42         # allows creating a new Professor on the fly
43         required=False)
44
45     class Meta:
46         model = Professor
47         # order the fields
48         fields = ('name', 'email')
49
50     def _clean_distinct_field(self, field, value_required=False, *args, **kwargs):
51         """
52         Check to see if Professor model is found before grabbing the field.
53         Ensure that if Professor was already found, the new field corresponds.
54
55         In theory, Javascript could and should ensure this.
56         In practice, better safe than incoherent.
57         """
58         oldprof = None
59         if hasattr(self, 'instance') and self.instance and self.instance.pk:
60             # Professor was already autocompleted. Save that object.
61             oldprof = self.instance
62         # Extract the field value, possibly replacing self.instance
63         value = self._clean_field(field, *args, **kwargs)
64         if value_required and not value:
65             raise ValidationError('This field is required.')
66         if oldprof and not value:
67             # This field was not supplied, but another one determined the prof.
68             # Grab field from prof model object.
69             value = getattr(oldprof, field)
70         if oldprof and self.instance != oldprof:
71             # Two different Professor objects have been found across fields.
72             raise ValidationError('It looks like two or more different Professors have been autocompleted.')
73
74         return value
75
76     def clean_name(self, *args, **kwargs):
77         return self._clean_distinct_field('name', value_required=True, *args, **kwargs)
78
79     def clean_email(self, *args, **kwargs):
80         return self._clean_distinct_field('email', *args, **kwargs)
81
82
83 class DepartmentForm(NiceErrorModelForm, ACFieldModelForm):
84     """ Find and create a Department given a School. """
85     # first argument is ajax channel name, defined in models as LookupChannel.
86     school = AutoCompleteSelectField('school_object_by_name',
87                                      help_text='',
88                                      label=mark_safe('School <span class="required-field">(required)</span>'))
89     # first argument is ajax channel name, defined in models as LookupChannel.
90     name = AutoCompleteDependentSelectField(
91         'dept_object_by_name_given_school', help_text='',
92         label=mark_safe('Department name <span class="required-field">(required)</span>'),
93         # autocomplete department based on school
94         dependsOn=school,
95         # allows creating a new department on the fly
96         required=False,
97         widget_id='input_department_name'
98     )
99
100     class Meta:
101         model = Department
102         # order the fields
103         fields = ('school', 'name')
104
105     def clean_name(self, *args, **kwargs):
106         """
107         Extract the name from the Department object if it already exists.
108         """
109         name = self._clean_field('name', *args, **kwargs)
110         # this might be unnecessary
111         if not name:
112             raise ValidationError('Cannot create a Department without a name.')
113         return name
114
115
116 class CourseForm(NiceErrorModelForm, DependentModelForm):
117     """ A course form which adds a honeypot and autocompletes some fields. """
118     # first argument is ajax channel name, defined in models as LookupChannel.
119     # note this AJAX field returns a field value, not a course object.
120     name = AutoCompleteField('course_name_by_name', help_text='',
121         label=mark_safe('Course name <span class="required-field">(required)</span>'))
122
123     def __init__(self, *args, **kwargs):
124         """ Add a dynamically named field. """
125         super(CourseForm, self).__init__(*args, **kwargs)
126         # insert honeypot into a random order on the form.
127         idx = random.randint(0, len(self.fields))
128         self.fields.insert(idx, settings.HONEYPOT_FIELD_NAME,
129             CharField(required=False, label=mark_safe(settings.HONEYPOT_LABEL))
130         )
131
132     class Meta:
133         model = Course
134         # order the fields
135         fields = ('name', 'url')
136         model_fields = {
137             # pass department data onto DepartmentForm
138             'department': DepartmentForm,
139             # pass professor data onto ProfessorForm
140             'professor': ProfessorForm,
141         }
142
143     def clean(self, *args, **kwargs):
144         """ Honeypot validation. """
145
146         # Call ModelFormMixin or whoever normally cleans house.
147         cleaned_data = super(CourseForm, self).clean(*args, **kwargs)
148
149         # Check the honeypot
150         # parts of this code borrow from
151         # https://github.com/sunlightlabs/django-honeypot
152         hfn = settings.HONEYPOT_FIELD_NAME
153         formhoneypot = cleaned_data.get(hfn, None)
154         if formhoneypot and (formhoneypot != settings.HONEYPOT_VALUE):
155             # Highlight the failure to follow instructions.
156             self._errors[hfn] = [settings.HONEYPOT_ERROR]
157             del cleaned_data[hfn]
158         return cleaned_data