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 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
21 from karmaworld.utils.ajax_selects import register_channel_name
24 class SchoolManager(models.Manager):
25 """ Handle restoring data. """
26 def get_by_natural_key(self, usde_id):
28 Return a School defined by USDE number.
30 return self.get(usde_id=usde_id)
33 class School(models.Model):
34 """ A grouping that contains many courses """
35 objects = SchoolManager()
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 #')
51 """ Sort School by file_count descending, name abc=> """
52 ordering = ['-file_count','-priority', 'name']
54 def natural_key(self):
56 A School is uniquely defined by USDE number.
58 Name should be unique, but there are some dupes in the DB.
60 return (self.usde_id,)
62 def __unicode__(self):
63 return u'School {0}: {1}'.format(self.usde_id, self.name)
65 def save(self, *args, **kwargs):
66 """ Save school and generate a slug if one doesn't exist """
68 self.slug = slugify(unicode(self.name))
69 super(School, self).save(*args, **kwargs)
72 def autocomplete_search_fields():
73 return ("name__icontains",)
75 def update_note_count(self):
76 """ Update the School.file_count by summing the
77 contained course.file_count
79 self.file_count = sum([course.file_count for course in self.course_set.all()])
83 @register_channel_name('school')
84 class SchoolLookup(LookupChannel):
86 Handles AJAX lookups against the school model's name and value fields.
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)
95 def check_auth(self, request):
96 """ Allow anonymous access. """
97 # By default, Lookups require request.is_staff. Don't require anything!
101 class DepartmentManager(models.Manager):
102 """ Handle restoring data. """
103 def get_by_natural_key(self, name, school):
105 Return a Department defined by its name and school.
107 return self.get(name=name, school=school)
110 class Department(models.Model):
111 """ Department within a School. """
112 objects = DepartmentManager()
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)
121 The same department name might exist across schools, but only once
124 unique_together = ('name', 'school',)
126 def __unicode__(self):
127 return u'Department: {0} at {1}'.format(self.name, unicode(self.school))
129 def natural_key(self):
131 A Department is uniquely defined by its school and name.
133 return (self.name, self.school.natural_key())
134 # Requires School to be dumped first
135 natural_key.dependencies = ['courses.school']
137 def save(self, *args, **kwargs):
138 """ Save department and generate a slug if one doesn't exist """
140 self.slug = slugify(unicode(self.name))
141 super(Department, self).save(*args, **kwargs)
144 @register_channel_name('dept_given_school')
145 class DeptGivenSchoolLookup(DependentLookupChannel):
147 Handles AJAX lookups against the department model's name field given a
152 def get_dependent_query(self, q, request, dependency):
153 """ Search against department name given a school. """
155 return Department.objects.filter(name__icontains=q,
156 school__id=dependency)
160 def check_auth(self, request):
161 """ Allow anonymous access. """
162 # By default, Lookups require request.is_staff. Don't require anything!
166 class ProfessorManager(models.Manager):
167 """ Handle restoring data. """
168 def get_by_natural_key(self, name, email):
170 Return a Professor defined by name and email address.
172 return self.get(name=name,email=email)
175 class Professor(models.Model):
177 Track professors for courses.
179 objects = ProfessorManager()
181 name = models.CharField(max_length=255)
182 email = models.EmailField(blank=True, null=True)
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
190 unique_together = ('name', 'email',)
192 def __unicode__(self):
193 return u'Professor: {0} ({1})'.format(self.name, self.email)
195 def natural_key(self):
197 A Professor is uniquely defined by his/her name and email.
199 return (self.name,self.email)
202 class ProfessorAffiliationManager(models.Manager):
203 """ Handle restoring data. """
204 def get_by_natural_key(self, prof, dept):
206 Return a ProfessorAffiliation defined by prof and department.
208 return self.get(professor=prof,department=dept)
211 class ProfessorAffiliation(models.Model):
213 Track professors for departments. (many-to-many)
215 objects = ProfessorAffiliationManager()
217 professor = models.ForeignKey(Professor)
218 department = models.ForeignKey(Department)
220 def __unicode__(self):
221 return u'Professor {0} working for {1}'.format(unicode(self.professor), unicode(self.department))
225 Many-to-many across both professor and department.
226 However, (prof, dept) as a tuple should only appear once.
228 unique_together = ('professor', 'department',)
230 def natural_key(self):
232 A ProfessorAffiliation is uniquely defined by the prof and department
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']
239 class CourseManager(models.Manager):
240 """ Handle restoring data. """
241 def get_by_natural_key(self, name, dept):
243 Return a Course defined by name and department.
245 return self.get(name=name,department=dept)
248 class Course(models.Model):
249 """ First class object that contains many notes.Note objects """
250 objects = CourseManager()
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
259 school = models.ForeignKey(School, null=True, blank=True)
260 file_count = models.IntegerField(default=0)
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:")
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)
270 updated_at = models.DateTimeField(default=datetime.datetime.utcnow)
272 created_at = models.DateTimeField(auto_now_add=True)
274 # Number of times this course has been flagged as abusive/spam.
275 flags = models.IntegerField(default=0,null=False)
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'
284 def __unicode__(self):
285 return u"Course {0} in {1}".format(self.name, unicode(self.department))
287 def natural_key(self):
289 A Course is uniquely defined by its name and the department it is in.
291 return (self.name, self.department.natural_key())
292 # Requires dependencies to be dumped first
293 natural_key.dependencies = ['courses.department']
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)
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
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")
310 self.slug = slugify(u"%s %s" % (self.name, self.id))
311 self.save() # Save the slug
314 def autocomplete_search_fields():
315 return ("name__icontains",)
317 def update_note_count(self):
318 """ Update self.file_count by summing the note_set """
319 self.file_count = self.note_set.count()
322 reversion.register(Course)
324 class ProfessorTaughtManager(models.Manager):
325 """ Handle restoring data. """
326 def get_by_natural_key(self, prof, course):
328 Return a ProfessorTaught defined by professor and course.
330 return self.get(professor=prof, course=course)
333 class ProfessorTaught(models.Model):
335 Track professors teaching courses. (many-to-many)
337 objects = ProfessorTaughtManager()
339 professor = models.ForeignKey(Professor)
340 course = models.ForeignKey(Course)
342 def __unicode__(self):
343 return u'Professor {0} taught {1}'.format(unicode(self.professor), unicode(self.course))
346 # many-to-many across both fields,
347 # but (prof, course) as a tuple should only appear once.
348 unique_together = ('professor', 'course',)
350 def natural_key(self):
352 A ProfessorTaught is uniquely defined by the prof and course.
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']
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)