Merge pull request #314 from FinalsClub/208-edit-course-properties-main
[oweals/karmaworld.git] / karmaworld / apps / courses / models.py
1 #!/usr/bin/env python
2 # -*- coding:utf8 -*-
3 # Copyright (C) 2012  FinalsClub Foundation
4
5 """
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.
10 """
11 import datetime
12 import reversion
13
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
18
19 class SchoolManager(models.Manager):
20     """ Handle restoring data. """
21     def get_by_natural_key(self, usde_id):
22         """
23         Return a School defined by USDE number.
24         """
25         return self.get(usde_id=usde_id)
26
27
28 class School(models.Model):
29     """ A grouping that contains many courses """
30     objects     = SchoolManager()
31
32     name        = models.CharField(max_length=255)
33     slug        = models.SlugField(max_length=150, null=True)
34     location    = models.CharField(max_length=255, blank=True, null=True)
35     url         = models.URLField(max_length=511, blank=True)
36     # Facebook keeps a unique identifier for all schools
37     facebook_id = models.BigIntegerField(blank=True, null=True)
38     # United States Department of Education institution_id
39     usde_id     = models.BigIntegerField(blank=True, null=True, unique=True)
40     file_count  = models.IntegerField(default=0)
41     priority    = models.BooleanField(default=0)
42     alias       = models.CharField(max_length=255, null=True, blank=True)
43     hashtag     = models.CharField(max_length=16, null=True, blank=True, unique=True, help_text='School abbreviation without #')
44
45     class Meta:
46         """ Sort School by file_count descending, name abc=> """
47         ordering = ['-file_count','-priority', 'name']
48
49     def natural_key(self):
50         """
51         A School is uniquely defined by USDE number.
52
53         Name should be unique, but there are some dupes in the DB.
54         """
55         return (self.usde_id,)
56
57     def __unicode__(self):
58         return u'School {0}: {1}'.format(self.usde_id, self.name)
59
60     def save(self, *args, **kwargs):
61         """ Save school and generate a slug if one doesn't exist """
62         if not self.slug:
63             self.slug = slugify(unicode(self.name))
64         super(School, self).save(*args, **kwargs)
65
66     @staticmethod
67     def autocomplete_search_fields():
68         return ("name__icontains",)
69
70     def update_note_count(self):
71         """ Update the School.file_count by summing the
72             contained course.file_count
73         """
74         self.file_count = sum([course.file_count for course in self.course_set.all()])
75         self.save()
76
77
78 class DepartmentManager(models.Manager):
79     """ Handle restoring data. """
80     def get_by_natural_key(self, name, school):
81         """
82         Return a Department defined by its name and school.
83         """
84         return self.get(name=name, school=school)
85
86
87 class Department(models.Model):
88     """ Department within a School. """
89     objects     = DepartmentManager()
90
91     name        = models.CharField(max_length=255)
92     school      = models.ForeignKey(School) # Should this be optional ever?
93     slug        = models.SlugField(max_length=150, null=True)
94     url         = models.URLField(max_length=511, blank=True, null=True)
95
96     class Meta:
97         """
98         The same department name might exist across schools, but only once
99         per school.
100         """
101         unique_together = ('name', 'school',)
102
103     def __unicode__(self):
104         return u'Department: {0} at {1}'.format(self.name, unicode(self.school))
105
106     def natural_key(self):
107         """
108         A Department is uniquely defined by its school and name.
109         """
110         return (self.name, self.school.natural_key())
111     # Requires School to be dumped first
112     natural_key.dependencies = ['courses.school']
113
114     def save(self, *args, **kwargs):
115         """ Save department and generate a slug if one doesn't exist """
116         if not self.slug:
117             self.slug = slugify(unicode(self.name))
118         super(Department, self).save(*args, **kwargs)
119
120
121 class ProfessorManager(models.Manager):
122     """ Handle restoring data. """
123     def get_by_natural_key(self, name, email):
124         """
125         Return a Professor defined by name and email address.
126         """
127         return self.get(name=name,email=email)
128
129
130 class Professor(models.Model):
131     """
132     Track professors for courses.
133     """
134     objects = ProfessorManager()
135
136     name = models.CharField(max_length=255)
137     email = models.EmailField(blank=True, null=True)
138
139     class Meta:
140         """
141         email should be unique, but some professors have no email address
142         in the database. For those cases, the name must be appended for
143         uniqueness.
144         """
145         unique_together = ('name', 'email',)
146
147     def __unicode__(self):
148         return u'Professor: {0} ({1})'.format(self.name, self.email)
149
150     def natural_key(self):
151         """
152         A Professor is uniquely defined by his/her name and email.
153         """
154         return (self.name,self.email)
155
156
157 class ProfessorAffiliationManager(models.Manager):
158     """ Handle restoring data. """
159     def get_by_natural_key(self, prof, dept):
160         """
161         Return a ProfessorAffiliation defined by prof and department.
162         """
163         return self.get(professor=prof,department=dept)
164
165
166 class ProfessorAffiliation(models.Model):
167     """
168     Track professors for departments. (many-to-many)
169     """
170     objects    = ProfessorAffiliationManager()
171
172     professor  = models.ForeignKey(Professor)
173     department = models.ForeignKey(Department)
174
175     def __unicode__(self):
176         return u'Professor {0} working for {1}'.format(unicode(self.professor), unicode(self.department))
177
178     class Meta:
179         """
180         Many-to-many across both professor and department.
181         However, (prof, dept) as a tuple should only appear once.
182         """
183         unique_together = ('professor', 'department',)
184
185     def natural_key(self):
186         """
187         A ProfessorAffiliation is uniquely defined by the prof and department
188         """
189         return (self.professor.natural_key(), self.department.natural_key())
190     # Requires dependencies to be dumped first
191     natural_key.dependencies = ['courses.professor','courses.department']
192
193
194 class CourseManager(models.Manager):
195     """ Handle restoring data. """
196     def get_by_natural_key(self, name, dept):
197         """
198         Return a Course defined by name and department.
199         """
200         return self.get(name=name,department=dept)
201
202
203 class Course(models.Model):
204     """ First class object that contains many notes.Note objects """
205     objects     = CourseManager()
206
207     # Core metadata
208     name        = models.CharField(max_length=255)
209     slug        = models.SlugField(max_length=150, null=True)
210     # department should remove nullable when school gets yoinked
211     department  = models.ForeignKey(Department, blank=True, null=True)
212     # school is an appendix: the kind that gets swollen and should be removed
213     # (vistigial)
214     school      = models.ForeignKey(School, null=True, blank=True)
215     file_count  = models.IntegerField(default=0)
216
217     desc        = models.TextField(max_length=511, blank=True, null=True)
218     url         = models.URLField(max_length=511, blank=True, null=True)
219
220     # instructor_* is vestigial, replaced by Professor+ProfessorTaught models.
221     instructor_name     = models.CharField(max_length=255, blank=True, null=True)
222     instructor_email    = models.EmailField(blank=True, null=True)
223
224     updated_at      = models.DateTimeField(default=datetime.datetime.utcnow)
225
226     created_at      = models.DateTimeField(auto_now_add=True)
227
228     # Number of times this course has been flagged as abusive/spam.
229     flags           = models.IntegerField(default=0,null=False)
230
231     class Meta:
232         ordering = ['-file_count', 'school', 'name']
233         unique_together = ('name', 'department')
234         unique_together = ('name', 'school')
235         verbose_name = 'course'
236         verbose_name_plural = 'courses'
237
238     def __unicode__(self):
239         return u"Course {0} in {1}".format(self.name, unicode(self.department))
240
241     def natural_key(self):
242         """
243         A Course is uniquely defined by its name and the department it is in.
244         """
245         return (self.name, self.department.natural_key())
246     # Requires dependencies to be dumped first
247     natural_key.dependencies = ['courses.department']
248
249     def get_absolute_url(self):
250         """ return url based on school slug and self slug """
251         return u"/{0}/{1}".format(self.school.slug, self.slug)
252
253     def save(self, *args, **kwargs):
254         """ Save school and generate a slug if one doesn't exist """
255         super(Course, self).save(*args, **kwargs) # generate a self.id
256         if not self.slug:
257             self.set_slug()
258
259     def get_updated_at_string(self):
260         """ return the formatted style for datetime strings """
261         return self.updated_at.strftime("%I%p // %a %b %d %Y")
262
263     def set_slug(self):
264         self.slug = slugify(u"%s %s" % (self.name, self.id))
265         self.save() # Save the slug
266
267     @staticmethod
268     def autocomplete_search_fields():
269         return ("name__icontains",)
270
271     def update_note_count(self):
272         """ Update self.file_count by summing the note_set """
273         self.file_count = self.note_set.count()
274         self.save()
275
276 reversion.register(Course)
277
278 class ProfessorTaughtManager(models.Manager):
279     """ Handle restoring data. """
280     def get_by_natural_key(self, prof, course):
281         """
282         Return a ProfessorTaught defined by professor and course.
283         """
284         return self.get(professor=prof, course=course)
285
286
287 class ProfessorTaught(models.Model):
288     """
289     Track professors teaching courses. (many-to-many)
290     """
291     objects   = ProfessorTaughtManager()
292
293     professor = models.ForeignKey(Professor)
294     course    = models.ForeignKey(Course)
295
296     def __unicode__(self):
297         return u'Professor {0} taught {1}'.format(unicode(self.professor), unicode(self.course))
298
299     class Meta:
300         # many-to-many across both fields,
301         # but (prof, course) as a tuple should only appear once.
302         unique_together = ('professor', 'course',)
303
304     def natural_key(self):
305         """
306         A ProfessorTaught is uniquely defined by the prof and course.
307         """
308         return (self.professor.natural_key(), self.course.natural_key())
309     # Requires dependencies to be dumped first
310     natural_key.dependencies = ['courses.professor','courses.course']
311
312
313 # Enforce unique constraints even when we're using a database like
314 # SQLite that doesn't understand them
315 auto_add_check_unique_together(Course)
316 auto_add_check_unique_together(Department)
317 auto_add_check_unique_together(Professor)
318 auto_add_check_unique_together(ProfessorAffiliation)
319 auto_add_check_unique_together(ProfessorTaught)