Courses have a manytoone relation to schools.
"""
import datetime
+import reversion
from django.db import models
-from django.template import defaultfilters
+from django.utils.text import slugify
+from django.core.urlresolvers import reverse
from karmaworld.settings.manual_unique_together import auto_add_check_unique_together
+from ajax_select import LookupChannel
+from ajax_select_cascade import DependentLookupChannel
+from ajax_select_cascade import register_channel_name
+
+from karmaworld.utils.ajax_selects import register_channel_name
+
+
+class SchoolManager(models.Manager):
+ """ Handle restoring data. """
+ def get_by_natural_key(self, usde_id):
+ """
+ Return a School defined by USDE number.
+ """
+ return self.get(usde_id=usde_id)
class School(models.Model):
""" A grouping that contains many courses """
+ objects = SchoolManager()
+
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=150, null=True)
location = models.CharField(max_length=255, blank=True, null=True)
# Facebook keeps a unique identifier for all schools
facebook_id = models.BigIntegerField(blank=True, null=True)
# United States Department of Education institution_id
- usde_id = models.BigIntegerField(blank=True, null=True)
+ usde_id = models.BigIntegerField(blank=True, null=True, unique=True)
file_count = models.IntegerField(default=0)
priority = models.BooleanField(default=0)
alias = models.CharField(max_length=255, null=True, blank=True)
""" Sort School by file_count descending, name abc=> """
ordering = ['-file_count','-priority', 'name']
+ def natural_key(self):
+ """
+ A School is uniquely defined by USDE number.
+
+ Name should be unique, but there are some dupes in the DB.
+ """
+ return (self.usde_id,)
def __unicode__(self):
- return self.name
+ return u'School {0}: {1}'.format(self.usde_id, self.name)
def save(self, *args, **kwargs):
""" Save school and generate a slug if one doesn't exist """
if not self.slug:
- self.slug = defaultfilters.slugify(self.name)
+ self.slug = slugify(unicode(self.name))
super(School, self).save(*args, **kwargs)
@staticmethod
self.save()
+@register_channel_name('school')
+class SchoolLookup(LookupChannel):
+ """
+ Handles AJAX lookups against the school model's name and value fields.
+ """
+ model = School
+
+ def get_query(self, q, request):
+ """ Search against both name and alias. """
+ query = models.Q(name__icontains=q) | models.Q(alias__icontains=q)
+ return School.objects.filter(query)
+
+ def check_auth(self, request):
+ """ Allow anonymous access. """
+ # By default, Lookups require request.is_staff. Don't require anything!
+ pass
+
+
+class DepartmentManager(models.Manager):
+ """ Handle restoring data. """
+ def get_by_natural_key(self, name, school):
+ """
+ Return a Department defined by its name and school.
+ """
+ return self.get(name=name, school=school)
+
+
+class Department(models.Model):
+ """ Department within a School. """
+ objects = DepartmentManager()
+
+ name = models.CharField(max_length=255)
+ school = models.ForeignKey(School) # Should this be optional ever?
+ slug = models.SlugField(max_length=150, null=True)
+ url = models.URLField(max_length=511, blank=True, null=True)
+
+ class Meta:
+ """
+ The same department name might exist across schools, but only once
+ per school.
+ """
+ unique_together = ('name', 'school',)
+
+ def __unicode__(self):
+ return u'Department: {0} at {1}'.format(self.name, unicode(self.school))
+
+ def natural_key(self):
+ """
+ A Department is uniquely defined by its school and name.
+ """
+ return (self.name, self.school.natural_key())
+ # Requires School to be dumped first
+ natural_key.dependencies = ['courses.school']
+
+ def save(self, *args, **kwargs):
+ """ Save department and generate a slug if one doesn't exist """
+ if not self.slug:
+ self.slug = slugify(unicode(self.name))
+ super(Department, self).save(*args, **kwargs)
+
+
+@register_channel_name('dept_given_school')
+class DeptGivenSchoolLookup(DependentLookupChannel):
+ """
+ Handles AJAX lookups against the department model's name field given a
+ school.
+ """
+ model = Department
+
+ def get_dependent_query(self, q, request, dependency):
+ """ Search against department name given a school. """
+ if dependency:
+ return Department.objects.filter(name__icontains=q,
+ school__id=dependency)
+ else:
+ return []
+
+ def check_auth(self, request):
+ """ Allow anonymous access. """
+ # By default, Lookups require request.is_staff. Don't require anything!
+ pass
+
+
+class ProfessorManager(models.Manager):
+ """ Handle restoring data. """
+ def get_by_natural_key(self, name, email):
+ """
+ Return a Professor defined by name and email address.
+ """
+ return self.get(name=name,email=email)
+
+
+class Professor(models.Model):
+ """
+ Track professors for courses.
+ """
+ objects = ProfessorManager()
+
+ name = models.CharField(max_length=255)
+ email = models.EmailField(blank=True, null=True)
+
+ class Meta:
+ """
+ email should be unique, but some professors have no email address
+ in the database. For those cases, the name must be appended for
+ uniqueness.
+ """
+ unique_together = ('name', 'email',)
+
+ def __unicode__(self):
+ return u'Professor: {0} ({1})'.format(self.name, self.email)
+
+ def natural_key(self):
+ """
+ A Professor is uniquely defined by his/her name and email.
+ """
+ return (self.name,self.email)
+
+
+class ProfessorAffiliationManager(models.Manager):
+ """ Handle restoring data. """
+ def get_by_natural_key(self, prof, dept):
+ """
+ Return a ProfessorAffiliation defined by prof and department.
+ """
+ return self.get(professor=prof,department=dept)
+
+
+class ProfessorAffiliation(models.Model):
+ """
+ Track professors for departments. (many-to-many)
+ """
+ objects = ProfessorAffiliationManager()
+
+ professor = models.ForeignKey(Professor)
+ department = models.ForeignKey(Department)
+
+ def __unicode__(self):
+ return u'Professor {0} working for {1}'.format(unicode(self.professor), unicode(self.department))
+
+ class Meta:
+ """
+ Many-to-many across both professor and department.
+ However, (prof, dept) as a tuple should only appear once.
+ """
+ unique_together = ('professor', 'department',)
+
+ def natural_key(self):
+ """
+ A ProfessorAffiliation is uniquely defined by the prof and department
+ """
+ return (self.professor.natural_key(), self.department.natural_key())
+ # Requires dependencies to be dumped first
+ natural_key.dependencies = ['courses.professor','courses.department']
+
+
+class CourseManager(models.Manager):
+ """ Handle restoring data. """
+ def get_by_natural_key(self, name, dept):
+ """
+ Return a Course defined by name and department.
+ """
+ return self.get(name=name,department=dept)
+
+
class Course(models.Model):
""" First class object that contains many notes.Note objects """
+ objects = CourseManager()
+
# Core metadata
- name = models.CharField(max_length=255)
+ name = models.CharField(max_length=255, verbose_name="Course:")
slug = models.SlugField(max_length=150, null=True)
- school = models.ForeignKey(School) # Should this be optional ever?
+ # department should remove nullable when school gets yoinked
+ department = models.ForeignKey(Department, blank=True, null=True)
+ # school is an appendix: the kind that gets swollen and should be removed
+ # (vistigial)
+ school = models.ForeignKey(School, null=True, blank=True)
file_count = models.IntegerField(default=0)
desc = models.TextField(max_length=511, blank=True, null=True)
- url = models.URLField(max_length=511, blank=True, null=True)
- academic_year = models.IntegerField(blank=True, null=True,
- default=datetime.datetime.now().year)
+ url = models.URLField(max_length=511, blank=True, null=True,
+ verbose_name="Course URL:")
+ # instructor_* is vestigial, replaced by Professor+ProfessorTaught models.
instructor_name = models.CharField(max_length=255, blank=True, null=True)
instructor_email = models.EmailField(blank=True, null=True)
# Number of times this course has been flagged as abusive/spam.
flags = models.IntegerField(default=0,null=False)
-
class Meta:
ordering = ['-file_count', 'school', 'name']
- unique_together = ('school', 'name', 'instructor_name')
+ unique_together = ('name', 'department')
+ unique_together = ('name', 'school')
verbose_name = 'course'
verbose_name_plural = 'courses'
def __unicode__(self):
- return u"{0}: {1}".format(self.name, self.school)
+ return u"Course {0} in {1}".format(self.name, unicode(self.department))
+
+ def natural_key(self):
+ """
+ A Course is uniquely defined by its name and the department it is in.
+ """
+ return (self.name, self.department.natural_key())
+ # Requires dependencies to be dumped first
+ natural_key.dependencies = ['courses.department']
def get_absolute_url(self):
- """ return url based on school slug and self slug """
- return u"/{0}/{1}".format(self.school.slug, self.slug)
+ """ return url based on urls.py definition. """
+ return reverse('course_detail', kwargs={'slug':self.slug})
def save(self, *args, **kwargs):
""" Save school and generate a slug if one doesn't exist """
super(Course, self).save(*args, **kwargs) # generate a self.id
if not self.slug:
- self.slug = defaultfilters.slugify("%s %s" % (self.name, self.id))
- self.save() # Save the slug
+ self.set_slug()
def get_updated_at_string(self):
""" return the formatted style for datetime strings """
return self.updated_at.strftime("%I%p // %a %b %d %Y")
+ def set_slug(self):
+ self.slug = slugify(u"%s %s" % (self.name, self.id))
+ self.save() # Save the slug
+
@staticmethod
def autocomplete_search_fields():
return ("name__icontains",)
self.file_count = self.note_set.count()
self.save()
+ def get_popularity(self):
+ """ Aggregate popularity of notes contained within. """
+ # Run an efficient GROUP BY aggregation within the database.
+ # It returns {'fieldname': #}, where fieldname is set in the left hand
+ # side of the aggregate kwarg. Call the field x and retrieve the dict
+ # value using that key.
+ # The value might be None, return zero in that case with shortcut logic.
+ return self.note_set.aggregate(x=models.Sum('thanks'))['x'] or 0
+
+reversion.register(Course)
+
+class ProfessorTaughtManager(models.Manager):
+ """ Handle restoring data. """
+ def get_by_natural_key(self, prof, course):
+ """
+ Return a ProfessorTaught defined by professor and course.
+ """
+ return self.get(professor=prof, course=course)
+
+
+class ProfessorTaught(models.Model):
+ """
+ Track professors teaching courses. (many-to-many)
+ """
+ objects = ProfessorTaughtManager()
+
+ professor = models.ForeignKey(Professor)
+ course = models.ForeignKey(Course)
+
+ def __unicode__(self):
+ return u'Professor {0} taught {1}'.format(unicode(self.professor), unicode(self.course))
+
+ class Meta:
+ # many-to-many across both fields,
+ # but (prof, course) as a tuple should only appear once.
+ unique_together = ('professor', 'course',)
+
+ def natural_key(self):
+ """
+ A ProfessorTaught is uniquely defined by the prof and course.
+ """
+ return (self.professor.natural_key(), self.course.natural_key())
+ # Requires dependencies to be dumped first
+ natural_key.dependencies = ['courses.professor','courses.course']
+
# Enforce unique constraints even when we're using a database like
# SQLite that doesn't understand them
auto_add_check_unique_together(Course)
-
+auto_add_check_unique_together(Department)
+auto_add_check_unique_together(Professor)
+auto_add_check_unique_together(ProfessorAffiliation)
+auto_add_check_unique_together(ProfessorTaught)