]> git.openstreetmap.org Git - osqa.git/blobdiff - forum/models/user.py
Merge pull request #49 from udacity/login_logout_links
[osqa.git] / forum / models / user.py
index f027ffa350296f90f78f0644e0b0ebcacb53f26c..131ae5491d06c013f5b317c794ace47a611cb07e 100644 (file)
@@ -1,30 +1,25 @@
 from base import *
 from base import *
+from utils import PickledObjectField
+from django.conf import settings as django_settings
+from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.auth.models import User as DjangoUser, AnonymousUser as DjangoAnonymousUser
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.auth.models import User as DjangoUser, AnonymousUser as DjangoAnonymousUser
-from django.db.models import Q
-try:
-    from hashlib import md5
-except:
-    from md5 import new as md5
+from django.db.models import Q, Manager
+from django.core.urlresolvers import get_script_prefix
+
+from django.utils.encoding import smart_unicode
+
+from forum.settings import TRUNCATE_LONG_USERNAMES, TRUNCATE_USERNAMES_LONGER_THAN
 
 import string
 from random import Random
 
 from django.utils.translation import ugettext as _
 
 import string
 from random import Random
 
 from django.utils.translation import ugettext as _
-import django.dispatch
-
-
-QUESTIONS_PER_PAGE_CHOICES = (
-   (10, u'10'),
-   (30, u'30'),
-   (50, u'50'),
-)
-
-class UserManager(CachedManager):
-    def get_site_owner(self):
-        return self.all().order_by('date_joined')[0]
+import logging
 
 class AnonymousUser(DjangoAnonymousUser):
 
 class AnonymousUser(DjangoAnonymousUser):
+    reputation = 0
+    
     def get_visible_answers(self, question):
         return question.answers.filter_state(deleted=False)
 
     def get_visible_answers(self, question):
         return question.answers.filter_state(deleted=False)
 
@@ -36,6 +31,9 @@ class AnonymousUser(DjangoAnonymousUser):
 
     def can_vote_down(self):
         return False
 
     def can_vote_down(self):
         return False
+    
+    def can_vote_count_today(self):
+        return 0
 
     def can_flag_offensive(self, post=None):
         return False
 
     def can_flag_offensive(self, post=None):
         return False
@@ -57,6 +55,12 @@ class AnonymousUser(DjangoAnonymousUser):
 
     def can_convert_to_comment(self, answer):
         return False
 
     def can_convert_to_comment(self, answer):
         return False
+    
+    def can_convert_to_question(self, answer):
+        return False
+    
+    def can_convert_comment_to_answer(self, comment):
+        return False
 
     def can_accept_answer(self, answer):
         return False
 
     def can_accept_answer(self, answer):
         return False
@@ -88,20 +92,48 @@ class AnonymousUser(DjangoAnonymousUser):
     def can_upload_files(self):
         return False
 
     def can_upload_files(self):
         return False
 
+    def is_a_super_user_or_staff(self):
+        return False
+
 def true_if_is_super_or_staff(fn):
     def decorated(self, *args, **kwargs):
         return self.is_superuser or self.is_staff or fn(self, *args, **kwargs)
 def true_if_is_super_or_staff(fn):
     def decorated(self, *args, **kwargs):
         return self.is_superuser or self.is_staff or fn(self, *args, **kwargs)
+
     return decorated
 
     return decorated
 
+def false_if_validation_required_to(item):
+    def decorator(fn):
+        def decorated(self, *args, **kwargs):
+            if item in settings.REQUIRE_EMAIL_VALIDATION_TO and not self.email_isvalid:
+                return False
+            else:
+                return fn(self, *args, **kwargs)
+        return decorated
+    return decorator
+
+class UserManager(CachedManager):
+    def get(self, *args, **kwargs):
+        if not len(args) and len(kwargs) == 1 and 'username' in kwargs:
+            matching_users = self.filter(username=kwargs['username'])
+            
+            if len(matching_users) == 1:
+                return matching_users[0]
+            elif len(matching_users) > 1:
+                for user in matching_users:
+                    if user.username == kwargs['username']:
+                        return user
+                return matching_users[0]
+        return super(UserManager, self).get(*args, **kwargs)
+
 class User(BaseModel, DjangoUser):
     is_approved = models.BooleanField(default=False)
     email_isvalid = models.BooleanField(default=False)
 
 class User(BaseModel, DjangoUser):
     is_approved = models.BooleanField(default=False)
     email_isvalid = models.BooleanField(default=False)
 
-    reputation = models.PositiveIntegerField(default=0)
+    reputation = models.IntegerField(default=0)
     gold = models.PositiveIntegerField(default=0)
     silver = models.PositiveIntegerField(default=0)
     bronze = models.PositiveIntegerField(default=0)
     gold = models.PositiveIntegerField(default=0)
     silver = models.PositiveIntegerField(default=0)
     bronze = models.PositiveIntegerField(default=0)
-    
+
     last_seen = models.DateTimeField(default=datetime.datetime.now)
     real_name = models.CharField(max_length=100, blank=True)
     website = models.URLField(max_length=200, blank=True)
     last_seen = models.DateTimeField(default=datetime.datetime.now)
     real_name = models.CharField(max_length=100, blank=True)
     website = models.URLField(max_length=200, blank=True)
@@ -113,18 +145,61 @@ class User(BaseModel, DjangoUser):
 
     vote_up_count = DenormalizedField("actions", canceled=False, action_type="voteup")
     vote_down_count = DenormalizedField("actions", canceled=False, action_type="votedown")
 
     vote_up_count = DenormalizedField("actions", canceled=False, action_type="voteup")
     vote_down_count = DenormalizedField("actions", canceled=False, action_type="votedown")
-   
+
     objects = UserManager()
 
     def __unicode__(self):
     objects = UserManager()
 
     def __unicode__(self):
-        return self.username
+        return smart_unicode(self.username)
 
     @property
 
     @property
-    def gravatar(self):
-        return md5(self.email).hexdigest()
+    def prop(self):
+        prop = self.__dict__.get('_prop', None)
+
+        if prop is None:
+            prop = UserPropertyDict(self)
+            self._prop = prop
+
+        return prop
+
+    @property
+    def is_siteowner(self):
+        #todo: temporary thing, for now lets just assume that the site owner will always be the first user of the application
+        return self.id == 1
+
+
+    def _decorated_name(self):
+        username = smart_unicode(self.username)
+
+        if len(username) > TRUNCATE_USERNAMES_LONGER_THAN and TRUNCATE_LONG_USERNAMES:
+            username = '%s...' % username[:TRUNCATE_USERNAMES_LONGER_THAN-3]
+
+        if settings.SHOW_STATUS_DIAMONDS:
+            if self.is_superuser:
+                return u"%s \u2666\u2666" % username
+
+            if self.is_staff:
+                return u"%s \u2666" % username
+
+        return username
+
+    @property
+    def decorated_name(self):
+        return self._decorated_name()
 
 
+    @property
+    def last_activity(self):
+        try:
+            return self.actions.order_by('-action_date')[0].action_date
+        except:
+            return self.last_seen
+
+    @property
+    def gravatar(self):
+        return md5(self.email.lower()).hexdigest()
+    
     def save(self, *args, **kwargs):
     def save(self, *args, **kwargs):
-        if self.reputation < 0:
+        # If the community doesn't allow negative reputation, set it to 0
+        if not settings.ALLOW_NEGATIVE_REPUTATION and self.reputation < 0:
             self.reputation = 0
 
         new = not bool(self.id)
             self.reputation = 0
 
         new = not bool(self.id)
@@ -135,27 +210,53 @@ class User(BaseModel, DjangoUser):
             sub_settings = SubscriptionSettings(user=self)
             sub_settings.save()
 
             sub_settings = SubscriptionSettings(user=self)
             sub_settings.save()
 
+    @models.permalink
+    def get_profile_url(self):
+        keyword_arguments = {
+            'slug': slugify(smart_unicode(self.username))
+        }
+        if settings.INCLUDE_ID_IN_USER_URLS:
+            keyword_arguments.update({
+                'id': self.id,
+            })
+        return ('user_profile', (), keyword_arguments)
+
     def get_absolute_url(self):
     def get_absolute_url(self):
-        return self.get_profile_url()
+        root_relative_url = self.get_profile_url()
+        relative_url = root_relative_url[len(get_script_prefix()):]
+        return '%s/%s' % (django_settings.APP_URL, relative_url)
 
 
-    def get_messages(self):
-        messages = []
-        for m in self.message_set.all():
-            messages.append(m.message)
-        return messages
+    @models.permalink
+    def get_asked_url(self):
+        return ('user_questions', (), {'mode': _('asked-by'), 'user': self.id, 'slug': slugify(smart_unicode(self.username))})
 
 
-    def delete_messages(self):
-        self.message_set.all().delete()
+    @models.permalink
+    def get_user_subscriptions_url(self):
+        keyword_arguments = {
+            'slug': slugify(smart_unicode(self.username))
+        }
+        if settings.INCLUDE_ID_IN_USER_URLS:
+            keyword_arguments.update({
+                'id': self.id,
+            })
+        return ('user_subscriptions', (), keyword_arguments)
 
     @models.permalink
 
     @models.permalink
-    def get_profile_url(self):
-        return ('user_profile', (), {'id': self.id, 'slug': slugify(self.username)})
+    def get_answered_url(self):
+        return ('user_questions', (), {'mode': _('answered-by'), 'user': self.id, 'slug': slugify(self.username)})
 
 
-    def get_absolute_url(self):
-        return self.get_profile_url()
+    def get_subscribed_url(self):
+        try:
+            # Try to retrieve the Subscribed User URL.
+            url = reverse('user_questions',
+                           kwargs={'mode': _('subscribed-by'), 'user': self.id, 'slug': slugify(smart_unicode(self.username))})
+            return url
+        except Exception, e:
+            # If some Exception has been raised, don't forget to log it.
+            logging.error("Error retrieving a subscribed user URL: %s" % e)
 
     def get_profile_link(self):
 
     def get_profile_link(self):
-        profile_link = u'<a href="%s">%s</a>' % (self.get_profile_url(),self.username)
+        profile_link = u'<a href="%s">%s</a>' % (self.get_profile_url(), self.username)
         return mark_safe(profile_link)
 
     def get_visible_answers(self, question):
         return mark_safe(profile_link)
 
     def get_visible_answers(self, question):
@@ -164,11 +265,12 @@ class User(BaseModel, DjangoUser):
     def get_vote_count_today(self):
         today = datetime.date.today()
         return self.actions.filter(canceled=False, action_type__in=("voteup", "votedown"),
     def get_vote_count_today(self):
         today = datetime.date.today()
         return self.actions.filter(canceled=False, action_type__in=("voteup", "votedown"),
-                action_date__gte=(today - datetime.timedelta(days=1))).count()
+                                   action_date__gte=(today - datetime.timedelta(days=1))).count()
 
     def get_reputation_by_upvoted_today(self):
         today = datetime.datetime.now()
 
     def get_reputation_by_upvoted_today(self):
         today = datetime.datetime.now()
-        sum = self.reputes.filter(reputed_at__range=(today - datetime.timedelta(days=1), today)).aggregate(models.Sum('value'))
+        sum = self.reputes.filter(reputed_at__range=(today - datetime.timedelta(days=1), today)).aggregate(
+                models.Sum('value'))
         #todo: redo this, maybe transform in the daily cap
         #if sum.get('value__sum', None) is not None: return sum['value__sum']
         return 0
         #todo: redo this, maybe transform in the daily cap
         #if sum.get('value__sum', None) is not None: return sum['value__sum']
         return 0
@@ -176,7 +278,23 @@ class User(BaseModel, DjangoUser):
     def get_flagged_items_count_today(self):
         today = datetime.date.today()
         return self.actions.filter(canceled=False, action_type="flag",
     def get_flagged_items_count_today(self):
         today = datetime.date.today()
         return self.actions.filter(canceled=False, action_type="flag",
-                action_date__gte=(today - datetime.timedelta(days=1))).count()
+                                   action_date__gte=(today - datetime.timedelta(days=1))).count()
+    
+    def can_vote_count_today(self):
+        votes_today = settings.MAX_VOTES_PER_DAY
+        
+        if settings.USER_REPUTATION_TO_MAX_VOTES:
+            votes_today = votes_today + int(self.reputation)
+        
+        return votes_today
+    
+    def can_use_canned_comments(self):
+        # The canned comments feature is available only for admins and moderators,
+        # and only if the "Use canned comments" setting is activated in the administration.
+        if (self.is_superuser or self.is_staff) and settings.USE_CANNED_COMMENTS:
+            return True
+        else:
+            return False
 
     @true_if_is_super_or_staff
     def can_view_deleted_post(self, post):
 
     @true_if_is_super_or_staff
     def can_view_deleted_post(self, post):
@@ -190,6 +308,7 @@ class User(BaseModel, DjangoUser):
     def can_vote_down(self):
         return self.reputation >= int(settings.REP_TO_VOTE_DOWN)
 
     def can_vote_down(self):
         return self.reputation >= int(settings.REP_TO_VOTE_DOWN)
 
+    @false_if_validation_required_to('flag')
     def can_flag_offensive(self, post=None):
         if post is not None and post.author == self:
             return False
     def can_flag_offensive(self, post=None):
         if post is not None and post.author == self:
             return False
@@ -202,9 +321,10 @@ class User(BaseModel, DjangoUser):
         return self.reputation >= int(settings.REP_TO_VIEW_FLAGS)
 
     @true_if_is_super_or_staff
         return self.reputation >= int(settings.REP_TO_VIEW_FLAGS)
 
     @true_if_is_super_or_staff
+    @false_if_validation_required_to('comment')
     def can_comment(self, post):
         return self == post.author or self.reputation >= int(settings.REP_TO_COMMENT
     def can_comment(self, post):
         return self == post.author or self.reputation >= int(settings.REP_TO_COMMENT
-        ) or (post.__class__.__name__ == "Answer" and self == post.question.author)
+                ) or (post.__class__.__name__ == "Answer" and self == post.question.author)
 
     @true_if_is_super_or_staff
     def can_like_comment(self, comment):
 
     @true_if_is_super_or_staff
     def can_like_comment(self, comment):
@@ -219,12 +339,28 @@ class User(BaseModel, DjangoUser):
     def can_delete_comment(self, comment):
         return self == comment.author or self.reputation >= int(settings.REP_TO_DELETE_COMMENTS)
 
     def can_delete_comment(self, comment):
         return self == comment.author or self.reputation >= int(settings.REP_TO_DELETE_COMMENTS)
 
+    def can_convert_comment_to_answer(self, comment):
+        # We need to know what is the comment parent node type.
+        comment_parent_type = comment.parent.node_type
+
+        # If the parent is not a question or an answer this comment cannot be converted to an answer.
+        if comment_parent_type != "question" and comment_parent_type != "answer":
+            return False
+
+        return (comment.parent.node_type in ('question', 'answer')) and (self.is_superuser or self.is_staff or (
+            self == comment.author) or (self.reputation >= int(settings.REP_TO_CONVERT_COMMENTS_TO_ANSWERS)))
+
     def can_convert_to_comment(self, answer):
     def can_convert_to_comment(self, answer):
-        return (not answer.marked) and (self.is_superuser or self.is_staff or answer.author == self or self.reputation >= int(settings.REP_TO_CONVERT_TO_COMMENT))
+        return (not answer.marked) and (self.is_superuser or self.is_staff or answer.author == self or self.reputation >= int
+                (settings.REP_TO_CONVERT_TO_COMMENT))
+    
+    def can_convert_to_question(self, node):
+        return (not node.marked) and (self.is_superuser or self.is_staff or node.author == self or self.reputation >= int
+                (settings.REP_TO_CONVERT_TO_QUESTION))
 
     @true_if_is_super_or_staff
     def can_accept_answer(self, answer):
 
     @true_if_is_super_or_staff
     def can_accept_answer(self, answer):
-        return self == answer.question.author
+        return self == answer.question.author and (settings.USERS_CAN_ACCEPT_OWN or answer.author != answer.question.author)
 
     @true_if_is_super_or_staff
     def can_create_tags(self):
 
     @true_if_is_super_or_staff
     def can_create_tags(self):
@@ -233,7 +369,8 @@ class User(BaseModel, DjangoUser):
     @true_if_is_super_or_staff
     def can_edit_post(self, post):
         return self == post.author or self.reputation >= int(settings.REP_TO_EDIT_OTHERS
     @true_if_is_super_or_staff
     def can_edit_post(self, post):
         return self == post.author or self.reputation >= int(settings.REP_TO_EDIT_OTHERS
-        ) or (post.nis.wiki and self.reputation >= int(settings.REP_TO_EDIT_WIKI))
+                                                             ) or (post.nis.wiki and self.reputation >= int(
+                settings.REP_TO_EDIT_WIKI))
 
     @true_if_is_super_or_staff
     def can_wikify(self, post):
 
     @true_if_is_super_or_staff
     def can_wikify(self, post):
@@ -254,45 +391,162 @@ class User(BaseModel, DjangoUser):
 
     @true_if_is_super_or_staff
     def can_reopen_question(self, question):
 
     @true_if_is_super_or_staff
     def can_reopen_question(self, question):
-        return self == question.author and self.reputation >= settings.REP_TO_REOPEN_OWN
+        # Check whether the setting to Unify close and reopen permissions has been activated
+        if bool(settings.UNIFY_PERMISSIONS_TO_CLOSE_AND_REOPEN):
+            # If we unify close to reopen check whether the user has permissions to close.
+            # If he has -- he can reopen his question too.
+            can_reopen = (
+                self == question.author and self.reputation >= int(settings.REP_TO_CLOSE_OWN)
+            ) or self.reputation >= int(settings.REP_TO_CLOSE_OTHERS)
+        else:
+            # Check whether the user is the author and has the required permissions to reopen
+            can_reopen = self == question.author and self.reputation >= int(settings.REP_TO_REOPEN_OWN)
+        return can_reopen
 
     @true_if_is_super_or_staff
     def can_delete_post(self, post):
         if post.node_type == "comment":
             return self.can_delete_comment(post)
 
     @true_if_is_super_or_staff
     def can_delete_post(self, post):
         if post.node_type == "comment":
             return self.can_delete_comment(post)
-            
+
         return (self == post.author and (post.__class__.__name__ == "Answer" or
         return (self == post.author and (post.__class__.__name__ == "Answer" or
-            not post.answers.exclude(author=self).count()))
+        not post.answers.exclude(author__id=self.id).count()))
 
     @true_if_is_super_or_staff
     def can_upload_files(self):
         return self.reputation >= int(settings.REP_TO_UPLOAD)
 
 
     @true_if_is_super_or_staff
     def can_upload_files(self):
         return self.reputation >= int(settings.REP_TO_UPLOAD)
 
+    @true_if_is_super_or_staff
+    def is_a_super_user_or_staff(self):
+        return False
+
+    def email_valid_and_can_ask(self):
+        return 'ask' not in settings.REQUIRE_EMAIL_VALIDATION_TO or self.email_isvalid
+
+    def email_valid_and_can_answer(self):
+        return 'answer' not in settings.REQUIRE_EMAIL_VALIDATION_TO or self.email_isvalid
+
     def check_password(self, old_passwd):
         self.__dict__.update(self.__class__.objects.filter(id=self.id).values('password')[0])
         return DjangoUser.check_password(self, old_passwd)
 
     def check_password(self, old_passwd):
         self.__dict__.update(self.__class__.objects.filter(id=self.id).values('password')[0])
         return DjangoUser.check_password(self, old_passwd)
 
+    @property
+    def suspension(self):
+        if self.__dict__.get('_suspension_dencache_', False) != None:
+            try:
+                self.__dict__['_suspension_dencache_'] = self.reputes.get(action__action_type="suspend", action__canceled=False).action
+            except ObjectDoesNotExist:
+                self.__dict__['_suspension_dencache_'] = None
+            except MultipleObjectsReturned:
+                logging.error("Multiple suspension actions found for user %s (%s)" % (self.username, self.id))
+                self.__dict__['_suspension_dencache_'] = self.reputes.filter(action__action_type="suspend", action__canceled=False
+                                                                             ).order_by('-action__action_date')[0].action
+
+        return self.__dict__['_suspension_dencache_']
+
+    def _pop_suspension_cache(self):
+        self.__dict__.pop('_suspension_dencache_', None)
+
+    def is_suspended(self):
+        if not self.is_active:
+            suspension = self.suspension
+
+            if suspension and suspension.extra.get('bantype', None) == 'forxdays' and (
+            datetime.datetime.now() > suspension.action_date + datetime.timedelta(
+                    days=int(suspension.extra.get('forxdays', 365)))):
+                suspension.cancel()
+            else:
+                return True
+
+        return False
+
+    class Meta:
+        app_label = 'forum'
+
+class UserProperty(BaseModel):
+    user = models.ForeignKey(User, related_name='properties')
+    key = models.CharField(max_length=16)
+    value = PickledObjectField()
 
     class Meta:
         app_label = 'forum'
 
     class Meta:
         app_label = 'forum'
+        unique_together = ('user', 'key')
+
+    def cache_key(self):
+        return self._generate_cache_key("%s:%s" % (self.user.id, self.key))
+
+    @classmethod
+    def infer_cache_key(cls, querydict):
+        if 'user' in querydict and 'key' in querydict:
+            cache_key = cls._generate_cache_key("%s:%s" % (querydict['user'].id, querydict['key']))
+            if len(cache_key) > django_settings.CACHE_MAX_KEY_LENGTH:
+                cache_key = cache_key[:django_settings.CACHE_MAX_KEY_LENGTH]
+            return cache_key
+
+        return None
+
+class UserPropertyDict(object):
+    def __init__(self, user):
+        self.__dict__['_user'] = user
+
+    def __get_property(self, name):
+        if self.__dict__.get('__%s__' % name, None):
+            return self.__dict__['__%s__' % name]
+        try:
+            user = self.__dict__['_user']
+            prop = UserProperty.objects.get(user=user, key=name)
+            self.__dict__['__%s__' % name] = prop
+            self.__dict__[name] = prop.value
+            return prop
+        except:
+            return None
+
+
+    def __getattr__(self, name):
+        if self.__dict__.get(name, None):
+            return self.__dict__[name]
+
+        prop = self.__get_property(name)
+
+        if prop:
+            return prop.value
+        else:
+            return None
+
+    def __setattr__(self, name, value):
+        current = self.__get_property(name)
+
+        if value is not None:
+            if current:
+                current.value = value
+                self.__dict__[name] = value
+                current.save(full_save=True)
+            else:
+                user = self.__dict__['_user']
+                prop = UserProperty(user=user, value=value, key=name)
+                prop.save()
+                self.__dict__[name] = value
+                self.__dict__['__%s__' % name] = prop
+        else:
+            if current:
+                current.delete()
+                del self.__dict__[name]
+                del self.__dict__['__%s__' % name]
+
 
 class SubscriptionSettings(models.Model):
 
 class SubscriptionSettings(models.Model):
-    user = models.OneToOneField(User, related_name='subscription_settings')
+    user = models.OneToOneField(User, related_name='subscription_settings', editable=False)
 
     enable_notifications = models.BooleanField(default=True)
 
     #notify if
     member_joins = models.CharField(max_length=1, default='n')
 
     enable_notifications = models.BooleanField(default=True)
 
     #notify if
     member_joins = models.CharField(max_length=1, default='n')
-    new_question = models.CharField(max_length=1, default='d')
+    new_question = models.CharField(max_length=1, default='n')
     new_question_watched_tags = models.CharField(max_length=1, default='i')
     subscribed_questions = models.CharField(max_length=1, default='i')
     new_question_watched_tags = models.CharField(max_length=1, default='i')
     subscribed_questions = models.CharField(max_length=1, default='i')
-    
+
     #auto_subscribe_to
     all_questions = models.BooleanField(default=False)
     all_questions_watched_tags = models.BooleanField(default=False)
     #auto_subscribe_to
     all_questions = models.BooleanField(default=False)
     all_questions_watched_tags = models.BooleanField(default=False)
-    questions_asked = models.BooleanField(default=True)
-    questions_answered = models.BooleanField(default=True)
-    questions_commented = models.BooleanField(default=False)
     questions_viewed = models.BooleanField(default=False)
 
     #notify activity on subscribed
     questions_viewed = models.BooleanField(default=False)
 
     #notify activity on subscribed
@@ -302,6 +556,8 @@ class SubscriptionSettings(models.Model):
     notify_comments = models.BooleanField(default=False)
     notify_accepted = models.BooleanField(default=False)
 
     notify_comments = models.BooleanField(default=False)
     notify_accepted = models.BooleanField(default=False)
 
+    send_digest = models.BooleanField(default=True)
+
     class Meta:
         app_label = 'forum'
 
     class Meta:
         app_label = 'forum'
 
@@ -324,7 +580,7 @@ class ValidationHashManager(models.Manager):
             obj.save()
         except:
             return None
             obj.save()
         except:
             return None
-            
+
         return obj
 
     def validate(self, hash, user, type, hash_data=[]):
         return obj
 
     def validate(self, hash, user, type, hash_data=[]):
@@ -352,7 +608,7 @@ class ValidationHashManager(models.Manager):
         return False
 
 class ValidationHash(models.Model):
         return False
 
 class ValidationHash(models.Model):
-    hash_code = models.CharField(max_length=255,unique=True)
+    hash_code = models.CharField(max_length=255, unique=True)
     seed = models.CharField(max_length=12)
     expiration = models.DateTimeField(default=one_day_from_now)
     type = models.CharField(max_length=12)
     seed = models.CharField(max_length=12)
     expiration = models.DateTimeField(default=one_day_from_now)
     type = models.CharField(max_length=12)
@@ -368,7 +624,7 @@ class ValidationHash(models.Model):
         return self.hash_code
 
 class AuthKeyUserAssociation(models.Model):
         return self.hash_code
 
 class AuthKeyUserAssociation(models.Model):
-    key = models.CharField(max_length=255,null=False,unique=True)
+    key = models.CharField(max_length=255, null=False, unique=True)
     provider = models.CharField(max_length=64)
     user = models.ForeignKey(User, related_name="auth_keys")
     added_at = models.DateTimeField(default=datetime.datetime.now)
     provider = models.CharField(max_length=64)
     user = models.ForeignKey(User, related_name="auth_keys")
     added_at = models.DateTimeField(default=datetime.datetime.now)