3 # Copyright (C) 2012 FinalsClub Foundation
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.
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
22 from karmaworld.utils.ajax_selects import register_channel_name
25 class SchoolManager(models.Manager):
26 """ Handle restoring data. """
27 def get_by_natural_key(self, usde_id):
29 Return a School defined by USDE number.
31 return self.get(usde_id=usde_id)
34 class School(models.Model):
35 """ A grouping that contains many courses """
36 objects = SchoolManager()
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 #')
52 """ Sort School by file_count descending, name abc=> """
53 ordering = ['-file_count','-priority', 'name']
55 def natural_key(self):
57 A School is uniquely defined by USDE number.
59 Name should be unique, but there are some dupes in the DB.
61 return (self.usde_id,)
63 def __unicode__(self):
64 return u'School {0}: {1}'.format(self.usde_id, self.name)
66 def save(self, *args, **kwargs):
67 """ Save school and generate a slug if one doesn't exist """
69 self.slug = slugify(unicode(self.name))
70 super(School, self).save(*args, **kwargs)
73 def autocomplete_search_fields():
74 return ("name__icontains",)
76 def update_note_count(self):
77 """ Update the School.file_count by summing the
78 contained course.file_count
80 self.file_count = sum([course.file_count for course in self.course_set.all()])
84 @register_channel_name('school')
85 class SchoolLookup(LookupChannel):
87 Handles AJAX lookups against the school model's name and value fields.
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)
96 def check_auth(self, request):
97 """ Allow anonymous access. """
98 # By default, Lookups require request.is_staff. Don't require anything!
102 class DepartmentManager(models.Manager):
103 """ Handle restoring data. """
104 def get_by_natural_key(self, name, school):
106 Return a Department defined by its name and school.
108 return self.get(name=name, school=school)
111 class Department(models.Model):
112 """ Department within a School. """
113 objects = DepartmentManager()
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)
122 The same department name might exist across schools, but only once
125 unique_together = ('name', 'school',)
127 def __unicode__(self):
128 return u'Department: {0} at {1}'.format(self.name, unicode(self.school))
130 def natural_key(self):
132 A Department is uniquely defined by its school and name.
134 return (self.name, self.school.natural_key())
135 # Requires School to be dumped first
136 natural_key.dependencies = ['courses.school']
138 def save(self, *args, **kwargs):
139 """ Save department and generate a slug if one doesn't exist """
141 self.slug = slugify(unicode(self.name))
142 super(Department, self).save(*args, **kwargs)
145 @register_channel_name('dept_given_school')
146 class DeptGivenSchoolLookup(DependentLookupChannel):
148 Handles AJAX lookups against the department model's name field given a
153 def get_dependent_query(self, q, request, dependency):
154 """ Search against department name given a school. """
156 return Department.objects.filter(name__icontains=q,
157 school__id=dependency)
161 def check_auth(self, request):
162 """ Allow anonymous access. """
163 # By default, Lookups require request.is_staff. Don't require anything!
167 class ProfessorManager(models.Manager):
168 """ Handle restoring data. """
169 def get_by_natural_key(self, name, email):
171 Return a Professor defined by name and email address.
173 return self.get(name=name,email=email)
176 class Professor(models.Model):
178 Track professors for courses.
180 objects = ProfessorManager()
182 name = models.CharField(max_length=255)
183 email = models.EmailField(blank=True, null=True)
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
191 unique_together = ('name', 'email',)
193 def __unicode__(self):
194 return u'Professor: {0} ({1})'.format(self.name, self.email)
196 def natural_key(self):
198 A Professor is uniquely defined by his/her name and email.
200 return (self.name,self.email)
203 class ProfessorAffiliationManager(models.Manager):
204 """ Handle restoring data. """
205 def get_by_natural_key(self, prof, dept):
207 Return a ProfessorAffiliation defined by prof and department.
209 return self.get(professor=prof,department=dept)
212 class ProfessorAffiliation(models.Model):
214 Track professors for departments. (many-to-many)
216 objects = ProfessorAffiliationManager()
218 professor = models.ForeignKey(Professor)
219 department = models.ForeignKey(Department)
221 def __unicode__(self):
222 return u'Professor {0} working for {1}'.format(unicode(self.professor), unicode(self.department))
226 Many-to-many across both professor and department.
227 However, (prof, dept) as a tuple should only appear once.
229 unique_together = ('professor', 'department',)
231 def natural_key(self):
233 A ProfessorAffiliation is uniquely defined by the prof and department
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']
240 class CourseManager(models.Manager):
241 """ Handle restoring data. """
242 def get_by_natural_key(self, name, dept):
244 Return a Course defined by name and department.
246 return self.get(name=name,department=dept)
249 class Course(models.Model):
250 """ First class object that contains many notes.Note objects """
251 objects = CourseManager()
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
260 school = models.ForeignKey(School, null=True, blank=True)
261 file_count = models.IntegerField(default=0)
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:")
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)
271 updated_at = models.DateTimeField(default=datetime.datetime.utcnow)
273 created_at = models.DateTimeField(auto_now_add=True)
275 # Number of times this course has been flagged as abusive/spam.
276 flags = models.IntegerField(default=0,null=False)
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'
285 def __unicode__(self):
286 return u"Course {0} in {1}".format(self.name, unicode(self.department))
288 def natural_key(self):
290 A Course is uniquely defined by its name and the department it is in.
292 return (self.name, self.department.natural_key())
293 # Requires dependencies to be dumped first
294 natural_key.dependencies = ['courses.department']
296 def get_absolute_url(self):
297 """ return url based on urls.py definition. """
298 return reverse('course_detail', kwargs={'slug':self.slug})
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
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")
311 self.slug = slugify(u"%s %s" % (self.name, self.id))
312 self.save() # Save the slug
315 def autocomplete_search_fields():
316 return ("name__icontains",)
318 def update_note_count(self):
319 """ Update self.file_count by summing the note_set """
320 self.file_count = self.note_set.count()
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
332 reversion.register(Course)
334 class ProfessorTaughtManager(models.Manager):
335 """ Handle restoring data. """
336 def get_by_natural_key(self, prof, course):
338 Return a ProfessorTaught defined by professor and course.
340 return self.get(professor=prof, course=course)
343 class ProfessorTaught(models.Model):
345 Track professors teaching courses. (many-to-many)
347 objects = ProfessorTaughtManager()
349 professor = models.ForeignKey(Professor)
350 course = models.ForeignKey(Course)
352 def __unicode__(self):
353 return u'Professor {0} taught {1}'.format(unicode(self.professor), unicode(self.course))
356 # many-to-many across both fields,
357 # but (prof, course) as a tuple should only appear once.
358 unique_together = ('professor', 'course',)
360 def natural_key(self):
362 A ProfessorTaught is uniquely defined by the prof and course.
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']
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)