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
23 class AnonLookupChannel(LookupChannel):
24 def check_auth(self, request):
25 """ Allow anonymous access. """
26 # By default, Lookups require request.is_staff. Don't require anything!
30 class FieldLookupChannel(AnonLookupChannel):
31 def get_query(self, q, request):
33 Case insensitive contain search against the given field.
34 Returns model objects with matching field.
36 kwargs = { str(self.field_lookup) + '__icontains': q }
37 return self.model.objects.filter(**kwargs)
40 class SchoolManager(models.Manager):
41 """ Handle restoring data. """
42 def get_by_natural_key(self, usde_id):
44 Return a School defined by USDE number.
46 return self.get(usde_id=usde_id)
49 class School(models.Model):
50 """ A grouping that contains many courses """
51 objects = SchoolManager()
53 name = models.CharField(max_length=255)
54 slug = models.SlugField(max_length=150, null=True)
55 location = models.CharField(max_length=255, blank=True, null=True)
56 url = models.URLField(max_length=511, blank=True)
57 # Facebook keeps a unique identifier for all schools
58 facebook_id = models.BigIntegerField(blank=True, null=True)
59 # United States Department of Education institution_id
60 usde_id = models.BigIntegerField(blank=True, null=True, unique=True)
61 file_count = models.IntegerField(default=0)
62 priority = models.BooleanField(default=0)
63 alias = models.CharField(max_length=255, null=True, blank=True)
64 hashtag = models.CharField(max_length=16, null=True, blank=True, unique=True, help_text='School abbreviation without #')
67 """ Sort School by file_count descending, name abc=> """
68 ordering = ['-file_count','-priority', 'name']
70 def natural_key(self):
72 A School is uniquely defined by USDE number.
74 Name should be unique, but there are some dupes in the DB.
76 return (self.usde_id,)
78 def __unicode__(self):
79 return u'School {0}: {1}'.format(self.usde_id, self.name)
81 def save(self, *args, **kwargs):
82 """ Save school and generate a slug if one doesn't exist """
84 self.slug = slugify(unicode(self.name))
85 super(School, self).save(*args, **kwargs)
88 def autocomplete_search_fields():
89 return ("name__icontains",)
91 def update_note_count(self):
92 """ Update the School.file_count by summing the
93 contained course.file_count
95 self.file_count = sum([course.file_count for course in self.course_set.all()])
99 @register_channel_name('school_object_by_name')
100 class SchoolLookup(AnonLookupChannel):
102 Handles AJAX lookups against the school model's name and alias fields.
106 def get_query(self, q, request):
107 """ Search against both name and alias. """
108 query = models.Q(name__icontains=q) | models.Q(alias__icontains=q)
109 return self.model.objects.filter(query)
112 class DepartmentManager(models.Manager):
113 """ Handle restoring data. """
114 def get_by_natural_key(self, name, school):
116 Return a Department defined by its name and school.
118 return self.get(name=name, school=school)
121 class Department(models.Model):
122 """ Department within a School. """
123 objects = DepartmentManager()
125 name = models.CharField(max_length=255, verbose_name="Department name")
126 school = models.ForeignKey(School) # Should this be optional ever?
127 slug = models.SlugField(max_length=150, null=True)
128 url = models.URLField(max_length=511, blank=True, null=True)
132 The same department name might exist across schools, but only once
135 unique_together = ('name', 'school',)
137 def __unicode__(self):
138 return u'Department: {0} at {1}'.format(self.name, unicode(self.school))
140 def natural_key(self):
142 A Department is uniquely defined by its school and name.
144 return (self.name, self.school.natural_key())
145 # Requires School to be dumped first
146 natural_key.dependencies = ['courses.school']
148 def save(self, *args, **kwargs):
149 """ Save department and generate a slug if one doesn't exist """
151 self.slug = slugify(unicode(self.name))
152 super(Department, self).save(*args, **kwargs)
155 @register_channel_name('dept_object_by_name_given_school')
156 class DeptGivenSchoolLookup(DependentLookupChannel, AnonLookupChannel):
158 Handles AJAX lookups against the department model's name field given a
163 def get_dependent_query(self, q, request, dependency):
164 """ Search against department name given a school. """
166 return Department.objects.filter(name__icontains=q,
167 school__id=dependency)
169 # If no dependency is submit, return nothing.
173 class ProfessorManager(models.Manager):
174 """ Handle restoring data. """
175 def get_by_natural_key(self, name, email):
177 Return a Professor defined by name and email address.
179 return self.get(name=name,email=email)
182 class Professor(models.Model):
184 Track professors for courses.
186 objects = ProfessorManager()
188 name = models.CharField(max_length=255, verbose_name="Professor's name")
189 email = models.EmailField(blank=True, null=True,
190 verbose_name="Professor's Email")
194 email should be unique, but some professors have no email address
195 in the database. For those cases, the name must be appended for
198 unique_together = ('name', 'email',)
200 def __unicode__(self):
201 return u'Professor: {0} ({1})'.format(self.name, self.email)
203 def natural_key(self):
205 A Professor is uniquely defined by his/her name and email.
207 return (self.name,self.email)
210 @register_channel_name('professor_object_by_name')
211 class ProfessorLookup(FieldLookupChannel):
213 Handles AJAX lookups against the professor model's name field.
216 field_lookup = 'name'
219 @register_channel_name('professor_object_by_email')
220 class ProfessorEmailLookup(FieldLookupChannel):
222 Handles AJAX lookups against the professor model's email field.
225 field_lookup = 'email'
228 class CourseManager(models.Manager):
229 """ Handle restoring data. """
230 def get_by_natural_key(self, name, dept):
232 Return a Course defined by name and department.
234 return self.get(name=name,department=dept)
237 class Course(models.Model):
238 """ First class object that contains many notes.Note objects """
239 objects = CourseManager()
242 name = models.CharField(max_length=255, verbose_name="Course name")
243 slug = models.SlugField(max_length=150, null=True)
244 # department should remove nullable when school gets yoinked
245 department = models.ForeignKey(Department, blank=True, null=True)
246 # school is an appendix: the kind that gets swollen and should be removed
248 school = models.ForeignKey(School, null=True, blank=True)
249 file_count = models.IntegerField(default=0)
251 desc = models.TextField(max_length=511, blank=True, null=True)
252 url = models.URLField(max_length=511, blank=True, null=True,
253 verbose_name="Course URL")
255 # professor should remove nullable when school instructor_* yoinked
256 professor = models.ManyToManyField(Professor, blank=True, null=True)
257 # instructor_* is vestigial
258 instructor_name = models.CharField(max_length=255, blank=True, null=True)
259 instructor_email = models.EmailField(blank=True, null=True)
261 updated_at = models.DateTimeField(default=datetime.datetime.utcnow)
263 created_at = models.DateTimeField(auto_now_add=True)
265 # Number of times this course has been flagged as abusive/spam.
266 flags = models.IntegerField(default=0,null=False)
269 ordering = ['-file_count', 'school', 'name']
270 unique_together = ('name', 'department')
271 unique_together = ('name', 'school')
272 verbose_name = 'course'
273 verbose_name_plural = 'courses'
275 def __unicode__(self):
276 return u"Course {0} in {1}".format(self.name, unicode(self.department))
278 def natural_key(self):
280 A Course is uniquely defined by its name and the department it is in.
282 return (self.name, self.department.natural_key())
283 # Requires dependencies to be dumped first
284 natural_key.dependencies = ['courses.department']
286 def get_absolute_url(self):
287 """ return url based on urls.py definition. """
288 return reverse('course_detail', kwargs={'slug':self.slug})
290 def save(self, *args, **kwargs):
291 """ Save school and generate a slug if one doesn't exist """
292 super(Course, self).save(*args, **kwargs) # generate a self.id
296 def get_updated_at_string(self):
297 """ return the formatted style for datetime strings """
298 return self.updated_at.strftime("%I%p // %a %b %d %Y")
301 self.slug = slugify(u"%s %s" % (self.name, self.id))
302 self.save() # Save the slug
305 def autocomplete_search_fields():
306 return ("name__icontains",)
308 def update_note_count(self):
309 """ Update self.file_count by summing the note_set """
310 self.file_count = self.note_set.count()
313 def get_popularity(self):
314 """ Aggregate popularity of notes contained within. """
315 # Run an efficient GROUP BY aggregation within the database.
316 # It returns {'fieldname': #}, where fieldname is set in the left hand
317 # side of the aggregate kwarg. Call the field x and retrieve the dict
318 # value using that key.
319 # The value might be None, return zero in that case with shortcut logic.
320 return self.note_set.aggregate(x=models.Sum('thanks'))['x'] or 0
322 def get_prof_names(self):
323 """ Comma separated list of professor names. """
324 # old style: just use the given name
325 if self.instructor_name:
326 return str(self.instructor_name)
327 # Run through all associated professors and concatenate their names.
328 return ','.join(self.professor.values_list('name', flat=True))
330 def get_prof_emails(self):
331 """ Comma separated list of professor emails. """
332 # old style: just use the given name
333 if self.instructor_email:
334 return str(self.instructor_email)
335 # Run through all associated professors and concatenate their names.
336 return ','.join(self.professor.values_list('email', flat=True))
338 reversion.register(Course)
341 @register_channel_name('course_name_by_name')
342 class CourseNameLookup(FieldLookupChannel):
344 Handles AJAX lookups against the course model's name field.
345 Returns just the matching field values.
348 field_lookup = 'name'
350 def get_query(self, q, request):
351 """ Return only the list of name fields. """
352 # Find the matching objects.
353 results = super(CourseNameLookup, self).get_query(q, request)
354 # Only return the name field, not the object.
355 return results.values_list(self.field_lookup, flat=True)
358 # Enforce unique constraints even when we're using a database like
359 # SQLite that doesn't understand them
360 auto_add_check_unique_together(Course)
361 auto_add_check_unique_together(School)
362 auto_add_check_unique_together(Department)
363 auto_add_check_unique_together(Professor)