annotate bio/models.py @ 849:ff645a692791

For issue #79, use bleach to sanitize both user input markdown & html.
author Brian Neal <bgneal@gmail.com>
date Thu, 30 Oct 2014 19:30:37 -0500
parents 9e803323a0d0
children 0b6bf9c5a982
rev   line source
gremmie@1 1 """
gremmie@1 2 Contains models for the bio application.
gremmie@1 3 I would have picked profile for this application, but that is already taken, apparently.
gremmie@1 4 """
bgneal@277 5 import datetime
gremmie@1 6 import os.path
gremmie@1 7
gremmie@1 8 from django.db import models
bgneal@259 9 from django.contrib.auth.models import User
gremmie@1 10 from django.conf import settings
bgneal@54 11 from django.core.cache import cache
bgneal@277 12 from django.template.loader import render_to_string
gremmie@1 13
bgneal@128 14 from core.markup import SiteMarkup
bgneal@609 15 import bio.flags
bgneal@124 16
bgneal@562 17
bgneal@215 18 # These are the secondary user status enumeration values.
bgneal@215 19 (STA_ACTIVE, # User is a full member in good standing.
bgneal@215 20 STA_RESIGNED, # User has voluntarily asked to be removed.
bgneal@215 21 STA_REMOVED, # User was removed for bad behavior.
bgneal@215 22 STA_SUSPENDED, # User is temporarily suspended; e.g. a stranger tripped
bgneal@215 23 # the spam filter.
bgneal@215 24 STA_SPAMMER, # User has been removed for spamming.
bgneal@215 25 STA_STRANGER, # New member, isn't fully trusted yet. Their comments and
bgneal@215 26 # forum posts are scanned for spam. They can have their
bgneal@215 27 # accounts deactivated by moderators for spamming.
bgneal@215 28 ) = range(6)
bgneal@147 29
bgneal@147 30 USER_STATUS_CHOICES = (
bgneal@147 31 (STA_ACTIVE, "Active"),
bgneal@147 32 (STA_RESIGNED, "Resigned"),
bgneal@147 33 (STA_REMOVED, "Removed"),
bgneal@147 34 (STA_SUSPENDED, "Suspended"),
bgneal@147 35 (STA_SPAMMER, "Spammer"),
bgneal@215 36 (STA_STRANGER, "Stranger")
bgneal@147 37 )
bgneal@147 38
bgneal@204 39
bgneal@204 40 class Badge(models.Model):
bgneal@204 41 """This model represents badges that users can earn."""
bgneal@204 42 image = models.ImageField(upload_to='badges')
bgneal@204 43 name = models.CharField(max_length=64)
bgneal@204 44 description = models.TextField(blank=True)
bgneal@204 45 order = models.IntegerField()
bgneal@204 46 numeric_id = models.IntegerField(db_index=True)
bgneal@204 47
bgneal@204 48 class Meta:
bgneal@204 49 ordering = ('order', )
bgneal@204 50
bgneal@204 51 def __unicode__(self):
bgneal@204 52 return self.name
bgneal@204 53
bgneal@204 54 def get_absolute_url(self):
bgneal@204 55 return self.image.url
bgneal@204 56
bgneal@204 57 def html(self):
bgneal@204 58 """Returns a HTML img tag representation of the badge."""
bgneal@204 59 if self.image:
bgneal@204 60 return u'<img src="%s" alt="%s" title="%s" />' % (
bgneal@204 61 self.get_absolute_url(), self.name, self.name)
bgneal@204 62 return u''
bgneal@204 63 html.allow_tags = True
bgneal@204 64
bgneal@204 65
gremmie@1 66 def avatar_file_path(instance, filename):
bgneal@666 67 # The avatar's name is the username plus the current time plus the file
bgneal@666 68 # extension. The time is used to bust browser caches.
bgneal@666 69 # Get extension, if it exists:
bgneal@560 70 ext = os.path.splitext(filename)[1]
bgneal@560 71 if not ext:
bgneal@560 72 ext = '.jpg'
bgneal@666 73 avatar_name = "{}{}{}".format(instance.user.username,
bgneal@666 74 datetime.datetime.utcnow().strftime('%S%f'), ext)
bgneal@560 75 return os.path.join(settings.AVATAR_DIR, 'users', avatar_name)
gremmie@1 76
gremmie@1 77
gremmie@1 78 class UserProfile(models.Model):
gremmie@1 79 """model to represent additional information about users"""
gremmie@1 80
bgneal@789 81 user = models.OneToOneField(User, related_name='profile')
gremmie@1 82 location = models.CharField(max_length=128, blank=True)
bgneal@609 83 country = models.CharField(max_length=2, blank=True, default='',
bgneal@609 84 choices=bio.flags.FLAG_CHOICES,
bgneal@609 85 help_text='Optional')
gremmie@1 86 birthday = models.DateField(blank=True, null=True,
gremmie@1 87 help_text='Optional; the year is not shown to others')
gremmie@1 88 occupation = models.CharField(max_length=128, blank=True)
gremmie@1 89 interests = models.CharField(max_length=255, blank=True)
gremmie@1 90 profile_text = models.TextField(blank=True)
gremmie@1 91 profile_html = models.TextField(blank=True)
gremmie@1 92 hide_email = models.BooleanField(default=True)
gremmie@1 93 signature = models.TextField(blank=True)
gremmie@1 94 signature_html = models.TextField(blank=True)
gremmie@1 95 avatar = models.ImageField(upload_to=avatar_file_path, blank=True)
bgneal@70 96 time_zone = models.CharField(max_length=64, blank=True,
bgneal@70 97 default='US/Pacific')
bgneal@120 98 use_24_time = models.BooleanField(default=False)
bgneal@96 99 forum_post_count = models.IntegerField(default=0)
bgneal@215 100 status = models.IntegerField(default=STA_STRANGER,
bgneal@147 101 choices=USER_STATUS_CHOICES)
bgneal@147 102 status_date = models.DateTimeField(auto_now_add=True)
bgneal@204 103 badges = models.ManyToManyField(Badge, through="BadgeOwnership")
bgneal@277 104 update_date = models.DateTimeField(db_index=True, blank=True)
bgneal@390 105 auto_favorite = models.BooleanField(default=False)
bgneal@390 106 auto_subscribe = models.BooleanField(default=False)
gremmie@1 107
gremmie@1 108 def __unicode__(self):
gremmie@1 109 return self.user.username
gremmie@1 110
gremmie@1 111 class Meta:
gremmie@1 112 ordering = ('user__username', )
gremmie@1 113
gremmie@1 114 def save(self, *args, **kwargs):
bgneal@562 115 """
bgneal@562 116 Custom profile save() function.
bgneal@562 117 If content_update is True (default), then it is assumed that major
bgneal@562 118 fields are being updated and that the profile_content_update signal
bgneal@562 119 should be signalled. When content_update is False, the update_date is
bgneal@562 120 not updated, expensive markup conversions are not performed, and the
bgneal@562 121 signal is not signalled. This is useful for updating the
bgneal@562 122 forum_post_count, for example.
bgneal@562 123
bgneal@562 124 """
bgneal@562 125 content_update = kwargs.pop('content_update', True)
bgneal@562 126
bgneal@562 127 if content_update:
bgneal@562 128 self.update_date = datetime.datetime.now()
bgneal@562 129 sm = SiteMarkup()
bgneal@562 130 self.profile_html = sm.convert(self.profile_text)
bgneal@562 131 self.signature_html = sm.convert(self.signature)
bgneal@562 132 cache.delete('avatar_' + self.user.username)
bgneal@562 133
gremmie@1 134 super(UserProfile, self).save(*args, **kwargs)
bgneal@562 135
bgneal@562 136 if content_update:
bgneal@562 137 notify_profile_content_update(self)
bgneal@138 138
bgneal@138 139 @models.permalink
bgneal@138 140 def get_absolute_url(self):
bgneal@138 141 return ('bio-view_profile', (), {'username': self.user.username})
bgneal@138 142
bgneal@204 143 def badge_ownership(self):
bgneal@329 144 return BadgeOwnership.objects.filter(profile=self).select_related('badge')
bgneal@204 145
bgneal@215 146 def is_stranger(self):
bgneal@215 147 """Returns True if this user profile status is STA_STRANGER."""
bgneal@215 148 return self.status == STA_STRANGER
bgneal@215 149
bgneal@215 150 def user_is_active(self):
bgneal@215 151 """Returns the profile's user is_active status. This function exists
bgneal@215 152 for the admin.
bgneal@215 153 """
bgneal@215 154 return self.user.is_active
bgneal@215 155 user_is_active.boolean = True
bgneal@215 156 user_is_active.short_description = "Is Active"
bgneal@215 157
bgneal@625 158 def reset(self):
bgneal@363 159 """
bgneal@625 160 Reset profile fields to empty defaults.
bgneal@363 161 This function is useful when a spammer is identified.
bgneal@363 162
bgneal@363 163 """
bgneal@363 164 self.location = ''
bgneal@625 165 self.country = ''
bgneal@625 166 self.birthday = None
bgneal@363 167 self.occupation = ''
bgneal@363 168 self.interests = ''
bgneal@363 169 self.profile_text = ''
bgneal@363 170 self.signature = ''
bgneal@363 171
bgneal@223 172 def search_title(self):
bgneal@223 173 full_name = self.user.get_full_name()
bgneal@223 174 if full_name:
bgneal@223 175 return u"%s (%s)" % (self.user.username, full_name)
bgneal@223 176 return self.user.username
bgneal@223 177
bgneal@223 178 def search_summary(self):
bgneal@277 179 text = render_to_string('search/indexes/bio/userprofile_text.txt',
bgneal@277 180 {'object': self});
bgneal@277 181 return text
bgneal@223 182
bgneal@138 183
bgneal@138 184 class UserProfileFlag(models.Model):
bgneal@138 185 """This model represents a user flagging a profile as inappropriate."""
bgneal@259 186 user = models.ForeignKey(User)
bgneal@138 187 profile = models.ForeignKey(UserProfile)
bgneal@138 188 flag_date = models.DateTimeField(auto_now_add=True)
bgneal@138 189
bgneal@138 190 def __unicode__(self):
bgneal@138 191 return u"%s's profile flagged by %s" % (self.profile.user.username,
bgneal@138 192 self.user.username)
bgneal@138 193
bgneal@138 194 class Meta:
bgneal@138 195 ordering = ('flag_date', )
bgneal@138 196
bgneal@138 197 def get_profile_url(self):
bgneal@138 198 return '<a href="%s">Profile</a>' % self.profile.get_absolute_url()
bgneal@138 199 get_profile_url.allow_tags = True
bgneal@204 200
bgneal@204 201
bgneal@204 202 class BadgeOwnership(models.Model):
bgneal@204 203 """This model represents the ownership of badges by users."""
bgneal@204 204 profile = models.ForeignKey(UserProfile)
bgneal@204 205 badge = models.ForeignKey(Badge)
bgneal@204 206 count = models.IntegerField(default=1)
bgneal@204 207
bgneal@204 208 class Meta:
bgneal@204 209 verbose_name_plural = "badge ownership"
bgneal@204 210 ordering = ('badge__order', )
bgneal@204 211
bgneal@204 212 def __unicode__(self):
bgneal@204 213 if self.count == 1:
bgneal@204 214 return u"%s owns 1 %s" % (self.profile.user.username,
bgneal@204 215 self.badge.name)
bgneal@204 216 else:
bgneal@204 217 return u"%s owns %d %s badges" % (self.profile.user.username,
bgneal@204 218 self.count, self.badge.name)
bgneal@204 219
bgneal@204 220 def badge_count_str(self):
bgneal@204 221 if self.count == 1:
bgneal@204 222 return u"1 %s" % self.badge.name
bgneal@204 223 return u"%d %ss" % (self.count, self.badge.name)
bgneal@562 224
bgneal@562 225 # Put down here to avoid a circular import
bgneal@562 226 from bio.signals import notify_profile_content_update