Blame view

flashcards/models.py 12.4 KB
a2d8c4229   Andrew Buss   added feed
1
  from datetime import datetime
9276b7b89   Andrew Buss   Nitpicky cleanup....
2
  from django.contrib.auth.models import AbstractUser, UserManager
5d861cbfb   Rohan Rangray   Wrote tests for F...
3
4
  from django.core.exceptions import PermissionDenied, SuspiciousOperation
  from django.db import IntegrityError
d6a663553   Andrew Buss   Fixed up some mod...
5
  from django.db.models import *
ddd9b2145   Andrew Buss   Fixed usermanager...
6
  from django.utils.timezone import now
18095ed46   Andrew Buss   Integrated django...
7
  from simple_email_confirmation import SimpleEmailConfirmationUserMixin
fe6a4ff63   Rohan Rangray   Replaced Flashcar...
8
  from fields import MaskField
a2d8c4229   Andrew Buss   added feed
9

18095ed46   Andrew Buss   Integrated django...
10

2b6bc762e   Andrew Buss   expanded registra...
11
12
  # Hack to fix AbstractUser before subclassing it
  AbstractUser._meta.get_field('email')._unique = True
ce17f969f   Andrew Buss   Restructured api,...
13
  AbstractUser._meta.get_field('username')._unique = False
18095ed46   Andrew Buss   Integrated django...
14

9f4aa9bfa   Andrew Buss   Added FlashcardRe...
15

ddd9b2145   Andrew Buss   Fixed usermanager...
16
17
18
19
20
21
22
23
  class EmailOnlyUserManager(UserManager):
      """
      A tiny extension of Django's UserManager which correctly creates users
      without usernames (using emails instead).
      """
  
      def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
          """
bf4ae95e4   Andrew Buss   Starting move to ...
24
          Creates and saves a User with the given email and password.
ddd9b2145   Andrew Buss   Fixed usermanager...
25
26
27
28
29
30
31
32
33
34
35
36
          """
          email = self.normalize_email(email)
          user = self.model(email=email,
                            is_staff=is_staff, is_active=True,
                            is_superuser=is_superuser,
                            date_joined=now(), **extra_fields)
          user.set_password(password)
          user.save(using=self._db)
          return user
  
      def create_user(self, email, password=None, **extra_fields):
          return self._create_user(email, password, False, False, **extra_fields)
56c04ca5b   Andrew Buss   Extended UserMana...
37
      def create_superuser(self, email, password, **extra_fields):
ddd9b2145   Andrew Buss   Fixed usermanager...
38
          return self._create_user(email, password, True, True, **extra_fields)
56c04ca5b   Andrew Buss   Extended UserMana...
39
40
41
  
  
  class User(AbstractUser, SimpleEmailConfirmationUserMixin):
ddd9b2145   Andrew Buss   Fixed usermanager...
42
43
44
45
46
      """
      An extension of Django's default user model.
      We use email as the username field, and include enrolled sections here
      """
      objects = EmailOnlyUserManager()
18095ed46   Andrew Buss   Integrated django...
47
48
      USERNAME_FIELD = 'email'
      REQUIRED_FIELDS = []
ddd9b2145   Andrew Buss   Fixed usermanager...
49
      sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
e0cc79368   Andrew Buss   fixed typos in mo...
50

f0b284adb   Rohan Rangray   Refactored Flashc...
51
      def is_in_section(self, section):
fedcc8ded   Rohan Rangray   Wrote tests for F...
52
          return self.sections.filter(pk=section.pk).exists()
f0b284adb   Rohan Rangray   Refactored Flashc...
53
54
55
56
57
58
59
  
      def pull(self, flashcard):
          if not self.is_in_section(flashcard.section):
              raise ValueError("User not in the section this flashcard belongs to")
          user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard)
          user_card.pulled = datetime.now()
          user_card.save()
fedcc8ded   Rohan Rangray   Wrote tests for F...
60
61
62
63
      def get_deck(self, section):
          if not self.is_in_section(section):
              raise ObjectDoesNotExist("User not enrolled in section")
          return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section)
8b6e41f08   Andrew Buss   Added some helper...
64

d6a663553   Andrew Buss   Fixed up some mod...
65
  class UserFlashcard(Model):
e0cc79368   Andrew Buss   fixed typos in mo...
66
67
      """
      Represents the relationship between a user and a flashcard by:
8b6e41f08   Andrew Buss   Added some helper...
68
69
70
      1. A user has a flashcard in their deck
      2. A user used to have a flashcard in their deck
      3. A user has a flashcard hidden from them
e0cc79368   Andrew Buss   fixed typos in mo...
71
      """
2b6bc762e   Andrew Buss   expanded registra...
72
      user = ForeignKey('User')
fe0d37016   Andrew Buss   changed some thin...
73
74
      mask = MaskField(max_length=255, null=True, blank=True, default=None,
                       help_text="The user-specific mask on the card")
bca16d61f   Rohan Rangray   Added the patch m...
75
      pulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user pulled the card")
d6a663553   Andrew Buss   Fixed up some mod...
76
      flashcard = ForeignKey('Flashcard')
8b6e41f08   Andrew Buss   Added some helper...
77
78
  
      class Meta:
d6a663553   Andrew Buss   Fixed up some mod...
79
          # There can be at most one UserFlashcard for each User and Flashcard
8b6e41f08   Andrew Buss   Added some helper...
80
81
          unique_together = (('user', 'flashcard'),)
          index_together = ["user", "flashcard"]
d6a663553   Andrew Buss   Fixed up some mod...
82
          # By default, order by most recently pulled
8b6e41f08   Andrew Buss   Added some helper...
83
          ordering = ['-pulled']
8b6e41f08   Andrew Buss   Added some helper...
84

f0b284adb   Rohan Rangray   Refactored Flashc...
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
  class FlashcardHide(Model):
      """
      Represents the property of a flashcard being hidden by a user.
      Each instance of this class represents a single user hiding a single flashcard.
      If reason is null, the flashcard was just hidden.
      If reason is not null, the flashcard was reported, and reason is the reason why it was reported.
      """
      user = ForeignKey('User')
      flashcard = ForeignKey('Flashcard')
      reason = CharField(max_length=255, blank=True, null=True)
      hidden = DateTimeField(auto_now_add=True)
  
      class Meta:
          # There can only be one FlashcardHide object for each User and Flashcard
          unique_together = (('user', 'flashcard'),)
          index_together = ["user", "flashcard"]
8b6e41f08   Andrew Buss   Added some helper...
101

c663d9897   Andrew Buss   merged
102

d6a663553   Andrew Buss   Fixed up some mod...
103
104
  class Flashcard(Model):
      text = CharField(max_length=255, help_text='The text on the card')
491577131   Andrew Buss   Class -> section....
105
      section = ForeignKey('Section', help_text='The section with which the card is associated')
d6a663553   Andrew Buss   Fixed up some mod...
106
      pushed = DateTimeField(auto_now_add=True, help_text="When the card was first pushed")
c1f4d3dea   Andrew Buss   Fix section flash...
107
      material_date = DateTimeField(default=now, help_text="The date with which the card is associated")
5d861cbfb   Rohan Rangray   Wrote tests for F...
108
      previous = ForeignKey('Flashcard', null=True, blank=True, default=None,
d6a663553   Andrew Buss   Fixed up some mod...
109
110
111
                            help_text="The previous version of this card, if one exists")
      author = ForeignKey(User)
      is_hidden = BooleanField(default=False)
5d861cbfb   Rohan Rangray   Wrote tests for F...
112
113
      hide_reason = CharField(blank=True, null=True, max_length=255,
                              default=None, help_text="Reason for hiding this card")
fe6a4ff63   Rohan Rangray   Replaced Flashcar...
114
      mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card")
8b6e41f08   Andrew Buss   Added some helper...
115

d6a663553   Andrew Buss   Fixed up some mod...
116
117
118
119
120
121
122
123
124
125
126
127
128
129
      class Meta:
          # By default, order by most recently pushed
          ordering = ['-pushed']
  
      def is_hidden_from(self, user):
          """
          A card can be hidden globally, but if a user has the card in their deck,
          this visibility overrides a global hide.
          :param user:
          :return: Whether the card is hidden from the user.
          """
          result = user.userflashcard_set.filter(flashcard=self)
          if not result.exists(): return self.is_hidden
          return result[0].is_hidden()
8b6e41f08   Andrew Buss   Added some helper...
130

5d861cbfb   Rohan Rangray   Wrote tests for F...
131
132
133
134
135
136
137
138
139
      def add_to_deck(self, user):
          if not user.is_in_section(self.section):
              raise PermissionDenied("You don't have the permission to add this card")
          try:
              user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask)
          except IntegrityError:
              raise SuspiciousOperation("The flashcard is already in the user's deck")
          user_flashcard.save()
          return user_flashcard
f0b284adb   Rohan Rangray   Refactored Flashc...
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
      def edit(self, user, new_flashcard):
          """
          Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard.
          Sets up everything correctly so this object, when saved, will result in the appropriate changes.
          :param user: The user editing this card.
          :param new_flashcard: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys.
          """
          if not user.is_in_section(self.section):
              raise PermissionDenied("You don't have the permission to edit this card")
  
          # content_changed is True iff either material_date or text were changed
          content_changed = False
          # create_new is True iff the user editing this card is the author of this card
          # and there are no other users with this card in their decks
          create_new = user != self.author or \
a2d8c4229   Andrew Buss   added feed
155
                       UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists()
f0b284adb   Rohan Rangray   Refactored Flashc...
156
157
158
159
160
161
162
163
  
          if 'material_date' in new_flashcard and self.material_date != new_flashcard['material_date']:
              content_changed |= True
              self.material_date = new_flashcard['material_date']
          if 'text' in new_flashcard and self.text != new_flashcard['text']:
              content_changed |= True
              self.text = new_flashcard['text']
          if create_new and content_changed:
5d861cbfb   Rohan Rangray   Wrote tests for F...
164
165
              mask = self.mask
              pk = self.pk
f0b284adb   Rohan Rangray   Refactored Flashc...
166
167
              self.pk = None
              if 'mask' in new_flashcard:
5d861cbfb   Rohan Rangray   Wrote tests for F...
168
169
170
                  mask = new_flashcard['mask']
              self.mask = mask
              self.previous_id = pk
fedcc8ded   Rohan Rangray   Wrote tests for F...
171
              self.save()
5d861cbfb   Rohan Rangray   Wrote tests for F...
172
          return create_new and content_changed
f0b284adb   Rohan Rangray   Refactored Flashc...
173

8b6e41f08   Andrew Buss   Added some helper...
174
175
      @classmethod
      def cards_visible_to(cls, user):
d6a663553   Andrew Buss   Fixed up some mod...
176
177
178
179
          """
          :param user:
          :return: A queryset with all cards that should be visible to a user.
          """
033fffbf8   Andrew Buss   Refactored flashc...
180
          return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user)
d6a663553   Andrew Buss   Fixed up some mod...
181

8b6e41f08   Andrew Buss   Added some helper...
182

1519ec887   Andrew Buss   Removed reference...
183
  class UserFlashcardQuiz(Model):
d6a663553   Andrew Buss   Fixed up some mod...
184
      """
1519ec887   Andrew Buss   Removed reference...
185
      An event of a user being quizzed on a flashcard.
d6a663553   Andrew Buss   Fixed up some mod...
186
187
      """
      user_flashcard = ForeignKey(UserFlashcard)
c1f4d3dea   Andrew Buss   Fix section flash...
188
      when = DateTimeField(auto_now=True)
d6a663553   Andrew Buss   Fixed up some mod...
189
190
191
192
193
194
      blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked")
      response = CharField(max_length=255, blank=True, null=True, help_text="The user's response")
      correct = NullBooleanField(help_text="The user's self-evaluation of their response")
  
      def status(self):
          """
1519ec887   Andrew Buss   Removed reference...
195
          There are three stages of a quiz object:
d6a663553   Andrew Buss   Fixed up some mod...
196
197
            1. the user has been shown the card
            2. the user has answered the card
08fbe6868   Andrew Buss   started on a sche...
198
            3. the user has self-evaluated their response's correctness
55c1546aa   Andrew Buss   Enabled admindocs...
199

d6a663553   Andrew Buss   Fixed up some mod...
200
201
          :return: string (evaluated, answered, viewed)
          """
08fbe6868   Andrew Buss   started on a sche...
202
          if self.correct is not None: return "evaluated"
d6a663553   Andrew Buss   Fixed up some mod...
203
204
          if self.response: return "answered"
          return "viewed"
ab5a72acd   Rohan Rangray   Registered flashc...
205

18095ed46   Andrew Buss   Integrated django...
206

491577131   Andrew Buss   Class -> section....
207
  class Section(Model):
d6a663553   Andrew Buss   Fixed up some mod...
208
209
      """
      A UCSD course taught by an instructor during a quarter.
491577131   Andrew Buss   Class -> section....
210
      We use the term "section" to avoid collision with the builtin keyword "class"
dc685f192   Andrew Buss   Section search wo...
211
      We index gratuitously to support autofill and because this is primarily read-only
d6a663553   Andrew Buss   Fixed up some mod...
212
      """
2c22131d9   Andrew Buss   Added department ...
213
214
215
216
217
218
      department = CharField(db_index=True, max_length=50)
      department_abbreviation = CharField(db_index=True, max_length=10)
      course_num = CharField(db_index=True, max_length=6)
      course_title = CharField(db_index=True, max_length=50)
      instructor = CharField(db_index=True, max_length=100)
      quarter = CharField(db_index=True, max_length=4)
72bf5f00c   Andrew Buss   Falcon puuuuuuuus...
219

dc685f192   Andrew Buss   Section search wo...
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
      @classmethod
      def search(cls, terms):
          """
          Search all fields of all sections for a particular set of terms
          A matching section must match at least one field on each term
          :param terms:iterable
          :return: Matching QuerySet ordered by department and course number
          """
          final_q = Q()
          for term in terms:
              q = Q(department__icontains=term)
              q |= Q(department_abbreviation__icontains=term)
              q |= Q(course_title__icontains=term)
              q |= Q(course_num__icontains=term)
              q |= Q(instructor__icontains=term)
              final_q &= q
          qs = cls.objects.filter(final_q)
          # Have the database cast the course number to an integer so it will sort properly
          # ECE 35 should rank before ECE 135 even though '135' < '35' lexicographically
52c37d174   Andrew Buss   Strip letters fro...
239
          qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
dc685f192   Andrew Buss   Section search wo...
240
241
          qs = qs.order_by('department_abbreviation', 'course_num_int')
          return qs
7b4e8b793   Andrew Buss   Fixed sections fi...
242
      @property
72bf5f00c   Andrew Buss   Falcon puuuuuuuus...
243
244
245
246
247
248
249
250
251
252
      def is_whitelisted(self):
          """
          :return: whether a whitelist exists for this section
          """
          return self.whitelist.exists()
  
      def is_user_on_whitelist(self, user):
          """
          :return: whether the user is on the waitlist for this section
          """
72bf5f00c   Andrew Buss   Falcon puuuuuuuus...
253
          return self.whitelist.filter(email=user.email).exists()
8b6e41f08   Andrew Buss   Added some helper...
254

d6a663553   Andrew Buss   Fixed up some mod...
255
      class Meta:
be7810aad   Laura Hawkins   working on adding...
256
          ordering = ['-course_title']
491577131   Andrew Buss   Class -> section....
257

0794ea949   Andrew Buss   Improved search f...
258
      @property
7b4e8b793   Andrew Buss   Fixed sections fi...
259
      def lecture_times(self):
fe0d37016   Andrew Buss   changed some thin...
260
          lecture_periods = self.lectureperiod_set.all()
02dee7072   Andrew Buss   fixed autocomplet...
261
          if not lecture_periods.exists(): return ''
fe0d37016   Andrew Buss   changed some thin...
262
          return ''.join(map(lambda x: x.weekday_letter, lecture_periods)) + ' ' + lecture_periods[0].short_start_time
7b4e8b793   Andrew Buss   Fixed sections fi...
263
264
  
      @property
0794ea949   Andrew Buss   Improved search f...
265
      def long_name(self):
a2d8c4229   Andrew Buss   added feed
266
          return '%s %s (%s)' % (self.course_title, self.lecture_times, self.instructor)
0794ea949   Andrew Buss   Improved search f...
267
268
269
270
  
      @property
      def short_name(self):
          return '%s %s' % (self.department_abbreviation, self.course_num)
a2d8c4229   Andrew Buss   added feed
271
272
273
      def get_feed_for_user(self, user):
          qs = Flashcard.objects.filter(section=self).exclude(userflashcard__user=user).order_by('pushed')
          return qs
2c22131d9   Andrew Buss   Added department ...
274
275
      def __unicode__(self):
          return '%s %s: %s (%s %s)' % (
0794ea949   Andrew Buss   Improved search f...
276
              self.department_abbreviation, self.course_num, self.course_title, self.instructor, self.quarter)
2c22131d9   Andrew Buss   Added department ...
277

fe0d37016   Andrew Buss   changed some thin...
278

491577131   Andrew Buss   Class -> section....
279
280
281
282
283
  class LecturePeriod(Model):
      """
      A lecture period for a section
      """
      section = ForeignKey(Section)
ddd9b2145   Andrew Buss   Fixed usermanager...
284
      week_day = IntegerField(help_text="1-indexed day of week, starting at Sunday")
491577131   Andrew Buss   Class -> section....
285
286
      start_time = TimeField()
      end_time = TimeField()
9f4aa9bfa   Andrew Buss   Added FlashcardRe...
287

7b4e8b793   Andrew Buss   Fixed sections fi...
288
289
290
291
292
293
      @property
      def weekday_letter(self):
          return ['?', 'S', 'M', 'T', 'W', 'Th', 'F', 'S', 'S'][self.week_day]
  
      @property
      def short_start_time(self):
dc19eeed5   Andrew Buss   replaced strftime...
294
295
          # lstrip 0 because windows doesn't support %-I
          return self.start_time.strftime('%I %p').lstrip('0')
7b4e8b793   Andrew Buss   Fixed sections fi...
296

9f4aa9bfa   Andrew Buss   Added FlashcardRe...
297
298
      class Meta:
          unique_together = (('section', 'start_time', 'week_day'),)
7b4e8b793   Andrew Buss   Fixed sections fi...
299
          ordering = ['section', 'week_day']
9f4aa9bfa   Andrew Buss   Added FlashcardRe...
300
301
302
  
  
  class WhitelistedAddress(Model):
ddd9b2145   Andrew Buss   Fixed usermanager...
303
304
305
      """
      An email address that has been whitelisted for a section at an instructor's request
      """
9f4aa9bfa   Andrew Buss   Added FlashcardRe...
306
      email = EmailField()
72bf5f00c   Andrew Buss   Falcon puuuuuuuus...
307
      section = ForeignKey(Section, related_name='whitelist')