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