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):
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 # find all courses without a department
96 course_list = list(self.course_set.all())
97 # find all courses with a department
98 for department in self.department_set.all():
99 for course in department.course_set.all():
100 course_list.append(course)
101 self.file_count = sum([course.file_count for course in course_list])
104 @register_channel_name('school_object_by_name')
105 class SchoolLookup(AnonLookupChannel):
107 Handles AJAX lookups against the school model's name and alias fields.
111 def get_query(self, q, request):
112 """ Search against both name and alias. """
113 query = models.Q(name__icontains=q) | models.Q(alias__icontains=q)
114 return self.model.objects.filter(query)
117 class DepartmentManager(models.Manager):
118 """ Handle restoring data. """
119 def get_by_natural_key(self, name, school):
121 Return a Department defined by its name and school.
123 return self.get(name=name, school=school)
126 class Department(models.Model):
127 """ Department within a School. """
128 objects = DepartmentManager()
130 name = models.CharField(max_length=255, verbose_name="Department name")
131 school = models.ForeignKey(School) # Should this be optional ever?
132 slug = models.SlugField(max_length=150, null=True)
133 url = models.URLField(max_length=511, blank=True, null=True)
137 The same department name might exist across schools, but only once
140 unique_together = ('name', 'school',)
142 def __unicode__(self):
145 def natural_key(self):
147 A Department is uniquely defined by its school and name.
149 return (self.name, self.school.natural_key())
150 # Requires School to be dumped first
151 natural_key.dependencies = ['courses.school']
153 def save(self, *args, **kwargs):
154 """ Save department and generate a slug if one doesn't exist """
156 self.slug = slugify(unicode(self.name))
157 super(Department, self).save(*args, **kwargs)
160 @register_channel_name('dept_object_by_name_given_school')
161 class DeptGivenSchoolLookup(DependentLookupChannel, AnonLookupChannel):
163 Handles AJAX lookups against the department model's name field given a
168 def get_dependent_query(self, q, request, dependency):
169 """ Search against department name given a school. """
171 return Department.objects.filter(name__icontains=q,
172 school__id=dependency)
174 # If no dependency is submit, return nothing.
178 class ProfessorManager(models.Manager):
179 """ Handle restoring data. """
180 def get_by_natural_key(self, name, email):
182 Return a Professor defined by name and email address.
184 return self.get(name=name, email=email)
187 class Professor(models.Model):
189 Track professors for courses.
191 objects = ProfessorManager()
193 name = models.CharField(max_length=255, verbose_name="Professor's name")
194 email = models.EmailField(blank=True, null=True,
195 verbose_name="Professor's Email")
199 email should be unique, but some professors have no email address
200 in the database. For those cases, the name must be appended for
203 unique_together = ('name', 'email',)
205 def __unicode__(self):
208 def natural_key(self):
210 A Professor is uniquely defined by his/her name and email.
212 return (self.name,self.email)
215 @register_channel_name('professor_object_by_name')
216 class ProfessorLookup(FieldLookupChannel):
218 Handles AJAX lookups against the professor model's name field.
221 field_lookup = 'name'
224 @register_channel_name('professor_object_by_email')
225 class ProfessorEmailLookup(FieldLookupChannel):
227 Handles AJAX lookups against the professor model's email field.
230 field_lookup = 'email'
233 class CourseManager(models.Manager):
234 """ Handle restoring data. """
235 def get_by_natural_key(self, name, dept):
237 Return a Course defined by name and department.
239 return self.get(name=name,department=dept)
242 class Course(models.Model):
243 """ First class object that contains many notes.Note objects """
244 objects = CourseManager()
247 name = models.CharField(max_length=255, verbose_name="Course name")
248 slug = models.SlugField(max_length=150, null=True)
249 # department should remove nullable when school gets yoinked
250 department = models.ForeignKey(Department, blank=True, null=True)
251 # school is an appendix: the kind that gets swollen and should be removed
253 school = models.ForeignKey(School, null=True, blank=True)
254 file_count = models.IntegerField(default=0)
255 thank_count = models.IntegerField(default=0)
257 desc = models.TextField(max_length=511, blank=True, null=True)
258 url = models.URLField(max_length=511, blank=True, null=True,
259 verbose_name="Course URL")
261 # professor should remove nullable when school instructor_* yoinked
262 professor = models.ManyToManyField(Professor, blank=True, null=True)
263 # instructor_* is vestigial
264 instructor_name = models.CharField(max_length=255, blank=True, null=True)
265 instructor_email = models.EmailField(blank=True, null=True)
267 updated_at = models.DateTimeField(default=datetime.datetime.utcnow)
269 created_at = models.DateTimeField(auto_now_add=True)
271 # Number of times this course has been flagged as abusive/spam.
272 flags = models.IntegerField(default=0,null=False)
275 ordering = ['-file_count', 'school', 'name']
276 unique_together = ('name', 'department')
277 unique_together = ('name', 'school')
278 verbose_name = 'course'
279 verbose_name_plural = 'courses'
281 def __unicode__(self):
282 return u"Course {0} in {1}".format(self.name, unicode(self.department))
284 def natural_key(self):
286 A Course is uniquely defined by its name and the department it is in.
288 return (self.name, self.department.natural_key())
289 # Requires dependencies to be dumped first
290 natural_key.dependencies = ['courses.department']
292 def get_absolute_url(self):
293 """ return url based on urls.py definition. """
294 return reverse('course_detail', kwargs={'slug':self.slug})
296 def save(self, *args, **kwargs):
297 """ Save school and generate a slug if one doesn't exist """
298 super(Course, self).save(*args, **kwargs) # generate a self.id
302 def get_updated_at_string(self):
303 """ return the formatted style for datetime strings """
304 return self.updated_at.strftime("%I%p // %a %b %d %Y")
307 self.slug = slugify(u"%s %s" % (self.name, self.id))
308 self.save() # Save the slug
311 def autocomplete_search_fields():
312 return ("name__icontains",)
314 def update_note_count(self):
315 """ Update self.file_count by summing the note_set """
316 self.file_count = self.note_set.count()
320 def update_thank_count(self):
321 """ Update the thank_count by summing the note_set
323 self.thank_count = sum([note.thanks for note in self.note_set.all()])
327 def get_prof_names(self):
328 """ Comma separated list of professor names. """
329 # old style: just use the given name
330 if self.instructor_name:
331 return str(self.instructor_name)
332 # Run through all associated professors and concatenate their names.
333 return ','.join(self.professor.values_list('name', flat=True))
335 def get_prof_emails(self):
336 """ Comma separated list of professor emails. """
337 # old style: just use the given name
338 if self.instructor_email:
339 return str(self.instructor_email)
340 # Run through all associated professors and concatenate their names.
341 return ','.join(self.professor.values_list('email', flat=True))
343 reversion.register(Course)
346 @register_channel_name('course_name_by_name')
347 class CourseNameLookup(FieldLookupChannel):
349 Handles AJAX lookups against the course model's name field.
350 Returns just the matching field values.
353 field_lookup = 'name'
355 def get_query(self, q, request):
356 """ Return only the list of name fields. """
357 # Find the matching objects.
358 results = super(CourseNameLookup, self).get_query(q, request)
359 # Only return the name field, not the object.
360 return results.values_list(self.field_lookup, flat=True)
363 # Enforce unique constraints even when we're using a database like
364 # SQLite that doesn't understand them
365 auto_add_check_unique_together(Course)
366 auto_add_check_unique_together(School)
367 auto_add_check_unique_together(Department)
368 auto_add_check_unique_together(Professor)