major security overhaul for #420 and #423
[oweals/karmaworld.git] / karmaworld / apps / users / models.py
1 #!/usr/bin/env python
2 # -*- coding:utf8 -*-
3 # Copyright (C) 2013  FinalsClub Foundation
4 import logging
5 import datetime
6 from allauth.account.signals import email_confirmed
7 from django.contrib.auth.models import User
8 from django.db.models import Sum
9 from django.db.models.signals import post_save
10 from django.dispatch import receiver
11 from django.db import models, DatabaseError
12 from django.middleware.transaction import transaction
13 from karmaworld.apps.courses.models import School
14
15 logger = logging.getLogger(__name__)
16
17
18 class UserProfileManager(models.Manager):
19     """ Handle restoring data. """
20     def get_by_natural_key(self, user):
21         return self.get(user=user)
22
23
24 class UserProfile(models.Model):
25     user = models.OneToOneField(User)
26     thanked_notes = models.ManyToManyField('notes.Note', related_name='users_thanked', blank=True, null=True)
27     flagged_notes = models.ManyToManyField('notes.Note', related_name='users_flagged', blank=True, null=True)
28     flagged_courses = models.ManyToManyField('courses.Course', related_name='users_flagged', blank=True, null=True)
29     school = models.ForeignKey(School, blank=True, null=True)
30
31     def natural_key(self):
32         return (self.user,)
33
34     def get_points(self):
35         sum = 0
36         for cls in ALL_KARMA_EVENT_CLASSES:
37             points = cls.objects.filter(user=self.user).aggregate(Sum('points'))['points__sum']
38             if points:
39                 sum += points
40
41         return sum
42
43     def get_id(self):
44         return self.user.id
45
46     def has_staff_status(self):
47         return self.user.is_staff
48
49     NO_BADGE = 0
50     PROSPECT = 1
51     BEGINNER = 2
52     TRAINEE = 3
53     APPRENTICE = 4
54     SCHOLAR = 5
55
56     BADGES = (
57         PROSPECT,
58         BEGINNER,
59         TRAINEE,
60         APPRENTICE,
61         SCHOLAR
62     )
63
64     BADGE_NAMES = {
65         PROSPECT: 'Prospect',
66         BEGINNER: 'Beginner',
67         TRAINEE: 'Trainee',
68         APPRENTICE: 'Apprentice',
69         SCHOLAR: 'Scholar'
70     }
71
72     BADGE_THRESHOLDS = {
73         PROSPECT: 10,
74         BEGINNER: 100,
75         TRAINEE: 200,
76         APPRENTICE: 500,
77         SCHOLAR: 1000
78     }
79
80     def get_badge(self):
81         points = self.get_points()
82         highest_badge = self.NO_BADGE
83         for badge in self.BADGES:
84             if points >= self.BADGE_THRESHOLDS[badge]:
85                 highest_badge = badge
86
87         if highest_badge is not self.NO_BADGE:
88             return self.BADGE_NAMES[highest_badge]
89         else:
90             return None
91
92     def __unicode__(self):
93         return self.user.__unicode__()
94
95
96 @receiver(email_confirmed, weak=True)
97 def give_email_confirm_karma(sender, **kwargs):
98     GenericKarmaEvent.create_event(kwargs['email_address'].user, kwargs['email_address'].email, GenericKarmaEvent.EMAIL_CONFIRMED)
99
100
101 class BaseKarmaEventManager(models.Manager):
102     """ Handle restoring data. """
103     def get_by_natural_key(self, points, user, timestamp):
104         return self.get(user=user, timestamp=timestamp)
105
106
107 class BaseKarmaEvent(models.Model):
108     points    = models.IntegerField()
109     user      = models.ForeignKey(User)
110     timestamp = models.DateTimeField(default=datetime.datetime.utcnow)
111
112     class Meta:
113         abstract = True
114         unique_together = ('points', 'user', 'timestamp')
115
116     def natural_key(self):
117         return (self.user, self.timestamp)
118
119     def get_message(self):
120         raise NotImplemented()
121
122
123 class GenericKarmaEvent(BaseKarmaEvent):
124     NONE = 'none'
125     NOTE_DELETED       = 'upload'
126     EMAIL_CONFIRMED    = 'thanks'
127
128     EVENT_TYPE_CHOICES = (
129         (NONE,               'This should not happen'),
130         (NOTE_DELETED,       'Your note "{m}" was deleted'),
131         (EMAIL_CONFIRMED,    'You confirmed your email address {m}'),
132     )
133
134     POINTS = {
135         NOTE_DELETED: -5,
136         EMAIL_CONFIRMED: 5,
137     }
138
139     event_type = models.CharField(max_length=15, choices=EVENT_TYPE_CHOICES, default=NONE)
140     message = models.CharField(max_length=255)
141
142     @staticmethod
143     def create_event(user, message, type):
144         event = GenericKarmaEvent.objects.create(user=user,
145                                                  points=GenericKarmaEvent.POINTS[type],
146                                                  event_type=type,
147                                                  message=message)
148         event.save()
149
150     def get_message(self):
151         if self.event_type == self.NONE:
152             return self.message
153         else:
154             return self.get_event_type_display().format(m=self.message)
155
156     def __unicode__(self):
157         return unicode(self.user) + ' -- ' + self.get_message()
158
159
160 class NoteKarmaEvent(BaseKarmaEvent):
161     UPLOAD       = 'upload'
162     THANKS       = 'thanks'
163     NOTE_DELETED = 'deleted'
164     GIVE_FLAG    = 'give_flag'
165     GET_FLAGGED  = 'get_flagged'
166     DOWNLOADED_NOTE = 'downloaded'
167     HAD_NOTE_DOWNLOADED = 'was_downloaded'
168     CREATED_KEYWORD = 'created_keyword'
169
170     EVENT_TYPE_CHOICES = (
171         (UPLOAD,       "You uploaded a note"),
172         (THANKS,       "You received a thanks for your note"),
173         (NOTE_DELETED, "Your note was deleted"),
174         (GIVE_FLAG,    "You flagged a note"),
175         (GET_FLAGGED,  "Your note was flagged as spam"),
176         (DOWNLOADED_NOTE,  "You downloaded a note"),
177         (HAD_NOTE_DOWNLOADED,  "Your note was downloaded"),
178         (CREATED_KEYWORD,  "You created a keyword"),
179     )
180     note = models.ForeignKey('notes.Note')
181     event_type = models.CharField(max_length=15, choices=EVENT_TYPE_CHOICES)
182
183     POINTS = {
184         UPLOAD: 5,
185         THANKS: 1,
186         NOTE_DELETED: -5,
187         GIVE_FLAG: -1,
188         GET_FLAGGED: -100,
189         DOWNLOADED_NOTE: -2,
190         HAD_NOTE_DOWNLOADED: 2,
191         CREATED_KEYWORD: 1,
192     }
193
194     def get_message(self):
195         return self.get_event_type_display()
196
197     def __unicode__(self):
198         return unicode(self.user) + ' -- ' + self.get_event_type_display() + ' -- ' + unicode(self.note)
199
200     @staticmethod
201     def create_event(user, note, type):
202         event = NoteKarmaEvent.objects.create(user=user,
203                                       note=note,
204                                       points=NoteKarmaEvent.POINTS[type],
205                                       event_type=type)
206         event.save()
207
208
209 class CourseKarmaEvent(BaseKarmaEvent):
210     GIVE_FLAG    = 'give_flag'
211     EVENT_TYPE_CHOICES = (
212         (GIVE_FLAG,    "You flagged a course"),
213     )
214     course = models.ForeignKey('courses.Course')
215     event_type = models.CharField(max_length=15, choices=EVENT_TYPE_CHOICES)
216
217     POINTS = {
218         GIVE_FLAG: -1,
219     }
220
221     def get_message(self):
222         return self.get_event_type_display()
223
224     def __unicode__(self):
225         return unicode(self.user) + ' -- ' + self.get_event_type_display() + ' -- ' + unicode(self.course)
226
227     @staticmethod
228     def create_event(user, course, type):
229         event = CourseKarmaEvent.objects.create(user=user,
230                                       course=course,
231                                       points=CourseKarmaEvent.POINTS[type],
232                                       event_type=type)
233         event.save()
234
235
236 ALL_KARMA_EVENT_CLASSES = (GenericKarmaEvent, NoteKarmaEvent, CourseKarmaEvent)
237
238
239 def user_display_name(user):
240     """Return the best way to display a user's
241     name to them on the site."""
242     if hasattr(user, 'first_name') and user.first_name and \
243             hasattr(user, 'last_name') and user.last_name:
244         return user.first_name + ' ' + user.last_name
245     elif hasattr(user, 'email') and user.email:
246         return user.email
247     else:
248         return user.username
249
250
251 @receiver(post_save, sender=User, weak=True)
252 def create_user_profile(sender, instance, created, **kwargs):
253     if created:
254         with transaction.commit_on_success():
255             try:
256                 UserProfile.objects.create(user=instance)
257             except DatabaseError:
258                 logger.warn("Could not create UserProfile for user {u}. This is okay if running syncdb.".format(u=instance))
259