for #294 : replaced manual school_list JSON with module, moved template to Django...
[oweals/karmaworld.git] / karmaworld / apps / courses / models.py
1 #!/usr/bin/env python
2 # -*- coding:utf8 -*-
3 # Copyright (C) 2012  FinalsClub Foundation
4
5 """
6     Models for the courses django app.
7     Handles courses, and their related models
8     Courses are the first class object, they contain notes.
9     Courses have a manytoone relation to schools.
10 """
11 import datetime
12 import reversion
13
14 from django.db import models
15 from django.utils.text import slugify
16 from karmaworld.settings.manual_unique_together import auto_add_check_unique_together
17 from ajax_select import LookupChannel
18 from ajax_select_cascade import DependentLookupChannel
19 from ajax_select_cascade import register_channel_name
20
21 from karmaworld.utils.ajax_selects import register_channel_name
22
23
24 class SchoolManager(models.Manager):
25     """ Handle restoring data. """
26     def get_by_natural_key(self, usde_id):
27         """
28         Return a School defined by USDE number.
29         """
30         return self.get(usde_id=usde_id)
31
32
33 class School(models.Model):
34     """ A grouping that contains many courses """
35     objects     = SchoolManager()
36
37     name        = models.CharField(max_length=255)
38     slug        = models.SlugField(max_length=150, null=True)
39     location    = models.CharField(max_length=255, blank=True, null=True)
40     url         = models.URLField(max_length=511, blank=True)
41     # Facebook keeps a unique identifier for all schools
42     facebook_id = models.BigIntegerField(blank=True, null=True)
43     # United States Department of Education institution_id
44     usde_id     = models.BigIntegerField(blank=True, null=True, unique=True)
45     file_count  = models.IntegerField(default=0)
46     priority    = models.BooleanField(default=0)
47     alias       = models.CharField(max_length=255, null=True, blank=True)
48     hashtag     = models.CharField(max_length=16, null=True, blank=True, unique=True, help_text='School abbreviation without #')
49
50     class Meta:
51         """ Sort School by file_count descending, name abc=> """
52         ordering = ['-file_count','-priority', 'name']
53
54     def natural_key(self):
55         """
56         A School is uniquely defined by USDE number.
57
58         Name should be unique, but there are some dupes in the DB.
59         """
60         return (self.usde_id,)
61
62     def __unicode__(self):
63         return u'School {0}: {1}'.format(self.usde_id, self.name)
64
65     def save(self, *args, **kwargs):
66         """ Save school and generate a slug if one doesn't exist """
67         if not self.slug:
68             self.slug = slugify(unicode(self.name))
69         super(School, self).save(*args, **kwargs)
70
71     @staticmethod
72     def autocomplete_search_fields():
73         return ("name__icontains",)
74
75     def update_note_count(self):
76         """ Update the School.file_count by summing the
77             contained course.file_count
78         """
79         self.file_count = sum([course.file_count for course in self.course_set.all()])
80         self.save()
81
82
83 @register_channel_name('school')
84 class SchoolLookup(LookupChannel):
85     """
86     Handles AJAX lookups against the school model's name and value fields.
87     """
88     model = School
89
90     def get_query(self, q, request):
91         """ Search against both name and alias. """
92         query = models.Q(name__icontains=q) | models.Q(alias__icontains=q)
93         return School.objects.filter(query)
94
95     def check_auth(self, request):
96         """ Allow anonymous access. """
97         # By default, Lookups require request.is_staff. Don't require anything!
98         pass
99
100
101 class DepartmentManager(models.Manager):
102     """ Handle restoring data. """
103     def get_by_natural_key(self, name, school):
104         """
105         Return a Department defined by its name and school.
106         """
107         return self.get(name=name, school=school)
108
109
110 class Department(models.Model):
111     """ Department within a School. """
112     objects     = DepartmentManager()
113
114     name        = models.CharField(max_length=255)
115     school      = models.ForeignKey(School) # Should this be optional ever?
116     slug        = models.SlugField(max_length=150, null=True)
117     url         = models.URLField(max_length=511, blank=True, null=True)
118
119     class Meta:
120         """
121         The same department name might exist across schools, but only once
122         per school.
123         """
124         unique_together = ('name', 'school',)
125
126     def __unicode__(self):
127         return u'Department: {0} at {1}'.format(self.name, unicode(self.school))
128
129     def natural_key(self):
130         """
131         A Department is uniquely defined by its school and name.
132         """
133         return (self.name, self.school.natural_key())
134     # Requires School to be dumped first
135     natural_key.dependencies = ['courses.school']
136
137     def save(self, *args, **kwargs):
138         """ Save department and generate a slug if one doesn't exist """
139         if not self.slug:
140             self.slug = slugify(unicode(self.name))
141         super(Department, self).save(*args, **kwargs)
142
143
144 @register_channel_name('dept_given_school')
145 class DeptGivenSchoolLookup(DependentLookupChannel):
146     """
147     Handles AJAX lookups against the department model's name field given a
148     school.
149     """
150     model = Department
151
152     def get_dependent_query(self, q, request, dependency):
153         """ Search against department name given a school. """
154         if dependency:
155             return Department.objects.filter(name__icontains=q,
156                                              school__id=dependency)
157         else:
158             return []
159
160     def check_auth(self, request):
161         """ Allow anonymous access. """
162         # By default, Lookups require request.is_staff. Don't require anything!
163         pass
164
165
166 class ProfessorManager(models.Manager):
167     """ Handle restoring data. """
168     def get_by_natural_key(self, name, email):
169         """
170         Return a Professor defined by name and email address.
171         """
172         return self.get(name=name,email=email)
173
174
175 class Professor(models.Model):
176     """
177     Track professors for courses.
178     """
179     objects = ProfessorManager()
180
181     name = models.CharField(max_length=255)
182     email = models.EmailField(blank=True, null=True)
183
184     class Meta:
185         """
186         email should be unique, but some professors have no email address
187         in the database. For those cases, the name must be appended for
188         uniqueness.
189         """
190         unique_together = ('name', 'email',)
191
192     def __unicode__(self):
193         return u'Professor: {0} ({1})'.format(self.name, self.email)
194
195     def natural_key(self):
196         """
197         A Professor is uniquely defined by his/her name and email.
198         """
199         return (self.name,self.email)
200
201
202 class ProfessorAffiliationManager(models.Manager):
203     """ Handle restoring data. """
204     def get_by_natural_key(self, prof, dept):
205         """
206         Return a ProfessorAffiliation defined by prof and department.
207         """
208         return self.get(professor=prof,department=dept)
209
210
211 class ProfessorAffiliation(models.Model):
212     """
213     Track professors for departments. (many-to-many)
214     """
215     objects    = ProfessorAffiliationManager()
216
217     professor  = models.ForeignKey(Professor)
218     department = models.ForeignKey(Department)
219
220     def __unicode__(self):
221         return u'Professor {0} working for {1}'.format(unicode(self.professor), unicode(self.department))
222
223     class Meta:
224         """
225         Many-to-many across both professor and department.
226         However, (prof, dept) as a tuple should only appear once.
227         """
228         unique_together = ('professor', 'department',)
229
230     def natural_key(self):
231         """
232         A ProfessorAffiliation is uniquely defined by the prof and department
233         """
234         return (self.professor.natural_key(), self.department.natural_key())
235     # Requires dependencies to be dumped first
236     natural_key.dependencies = ['courses.professor','courses.department']
237
238
239 class CourseManager(models.Manager):
240     """ Handle restoring data. """
241     def get_by_natural_key(self, name, dept):
242         """
243         Return a Course defined by name and department.
244         """
245         return self.get(name=name,department=dept)
246
247
248 class Course(models.Model):
249     """ First class object that contains many notes.Note objects """
250     objects     = CourseManager()
251
252     # Core metadata
253     name        = models.CharField(max_length=255, verbose_name="Course:")
254     slug        = models.SlugField(max_length=150, null=True)
255     # department should remove nullable when school gets yoinked
256     department  = models.ForeignKey(Department, blank=True, null=True)
257     # school is an appendix: the kind that gets swollen and should be removed
258     # (vistigial)
259     school      = models.ForeignKey(School, null=True, blank=True)
260     file_count  = models.IntegerField(default=0)
261
262     desc        = models.TextField(max_length=511, blank=True, null=True)
263     url         = models.URLField(max_length=511, blank=True, null=True,
264                                   verbose_name="Course URL:")
265
266     # instructor_* is vestigial, replaced by Professor+ProfessorTaught models.
267     instructor_name     = models.CharField(max_length=255, blank=True, null=True)
268     instructor_email    = models.EmailField(blank=True, null=True)
269
270     updated_at      = models.DateTimeField(default=datetime.datetime.utcnow)
271
272     created_at      = models.DateTimeField(auto_now_add=True)
273
274     # Number of times this course has been flagged as abusive/spam.
275     flags           = models.IntegerField(default=0,null=False)
276
277     class Meta:
278         ordering = ['-file_count', 'school', 'name']
279         unique_together = ('name', 'department')
280         unique_together = ('name', 'school')
281         verbose_name = 'course'
282         verbose_name_plural = 'courses'
283
284     def __unicode__(self):
285         return u"Course {0} in {1}".format(self.name, unicode(self.department))
286
287     def natural_key(self):
288         """
289         A Course is uniquely defined by its name and the department it is in.
290         """
291         return (self.name, self.department.natural_key())
292     # Requires dependencies to be dumped first
293     natural_key.dependencies = ['courses.department']
294
295     def get_absolute_url(self):
296         """ return url based on school slug and self slug """
297         return u"/{0}/{1}".format(self.school.slug, self.slug)
298
299     def save(self, *args, **kwargs):
300         """ Save school and generate a slug if one doesn't exist """
301         super(Course, self).save(*args, **kwargs) # generate a self.id
302         if not self.slug:
303             self.set_slug()
304
305     def get_updated_at_string(self):
306         """ return the formatted style for datetime strings """
307         return self.updated_at.strftime("%I%p // %a %b %d %Y")
308
309     def set_slug(self):
310         self.slug = slugify(u"%s %s" % (self.name, self.id))
311         self.save() # Save the slug
312
313     @staticmethod
314     def autocomplete_search_fields():
315         return ("name__icontains",)
316
317     def update_note_count(self):
318         """ Update self.file_count by summing the note_set """
319         self.file_count = self.note_set.count()
320         self.save()
321
322 reversion.register(Course)
323
324 class ProfessorTaughtManager(models.Manager):
325     """ Handle restoring data. """
326     def get_by_natural_key(self, prof, course):
327         """
328         Return a ProfessorTaught defined by professor and course.
329         """
330         return self.get(professor=prof, course=course)
331
332
333 class ProfessorTaught(models.Model):
334     """
335     Track professors teaching courses. (many-to-many)
336     """
337     objects   = ProfessorTaughtManager()
338
339     professor = models.ForeignKey(Professor)
340     course    = models.ForeignKey(Course)
341
342     def __unicode__(self):
343         return u'Professor {0} taught {1}'.format(unicode(self.professor), unicode(self.course))
344
345     class Meta:
346         # many-to-many across both fields,
347         # but (prof, course) as a tuple should only appear once.
348         unique_together = ('professor', 'course',)
349
350     def natural_key(self):
351         """
352         A ProfessorTaught is uniquely defined by the prof and course.
353         """
354         return (self.professor.natural_key(), self.course.natural_key())
355     # Requires dependencies to be dumped first
356     natural_key.dependencies = ['courses.professor','courses.course']
357
358
359 # Enforce unique constraints even when we're using a database like
360 # SQLite that doesn't understand them
361 auto_add_check_unique_together(Course)
362 auto_add_check_unique_together(Department)
363 auto_add_check_unique_together(Professor)
364 auto_add_check_unique_together(ProfessorAffiliation)
365 auto_add_check_unique_together(ProfessorTaught)