class UserProfile(OnChangeMixin, ModelBase, AbstractBaseUser): objects = UserManager() USERNAME_FIELD = 'username' REQUIRED_FIELDS = ['email'] username = models.CharField(max_length=255, default='', unique=True) display_name = models.CharField(max_length=255, default='', null=True, blank=True) email = models.EmailField(unique=True, null=True) averagerating = models.CharField(max_length=255, blank=True, null=True) bio = NoLinksField(short=False) confirmationcode = models.CharField(max_length=255, default='', blank=True) deleted = models.BooleanField(default=False) display_collections = models.BooleanField(default=False) display_collections_fav = models.BooleanField(default=False) homepage = models.URLField(max_length=255, blank=True, default='') location = models.CharField(max_length=255, blank=True, default='') notes = models.TextField(blank=True, null=True) notifycompat = models.BooleanField(default=True) notifyevents = models.BooleanField(default=True) occupation = models.CharField(max_length=255, default='', blank=True) # This is essentially a "has_picture" flag right now picture_type = models.CharField(max_length=75, default='', blank=True) read_dev_agreement = models.DateTimeField(null=True, blank=True) last_login_ip = models.CharField(default='', max_length=45, editable=False) last_login_attempt = models.DateTimeField(null=True, editable=False) last_login_attempt_ip = models.CharField(default='', max_length=45, editable=False) failed_login_attempts = models.PositiveIntegerField(default=0, editable=False) is_verified = models.BooleanField(default=True) region = models.CharField(max_length=11, null=True, blank=True, editable=False) lang = models.CharField(max_length=5, null=True, blank=True, default=settings.LANGUAGE_CODE) t_shirt_requested = models.DateTimeField(blank=True, null=True, default=None, editable=False) fxa_id = models.CharField(blank=True, null=True, max_length=128) class Meta: db_table = 'users' def __init__(self, *args, **kw): super(UserProfile, self).__init__(*args, **kw) if self.username: self.username = smart_unicode(self.username) def __unicode__(self): return u'%s: %s' % (self.id, self.display_name or self.username) @property def is_superuser(self): return self.groups.filter(rules='*:*').exists() @property def is_staff(self): from olympia.access import acl return acl.action_allowed_user(self, 'Admin', '%') def has_perm(self, perm, obj=None): return self.is_superuser def has_module_perms(self, app_label): return self.is_superuser backend = 'olympia.users.backends.AmoUserBackend' def is_anonymous(self): return False @staticmethod def create_user_url(id_, username=None, url_name='profile', src=None, args=None): """ We use <username> as the slug, unless it contains gross characters - in which case use <id> as the slug. """ from olympia.amo.utils import urlparams chars = '/<>"\'' if not username or any(x in chars for x in username): username = id_ args = args or [] url = reverse('users.%s' % url_name, args=[username] + args) return urlparams(url, src=src) def get_user_url(self, name='profile', src=None, args=None): return self.create_user_url(self.id, self.username, name, src, args) def get_url_path(self, src=None): return self.get_user_url('profile', src=src) def flush_urls(self): urls = [ '*/user/%d/' % self.id, self.picture_url, ] return urls @amo.cached_property(writable=True) def groups_list(self): """List of all groups the user is a member of, as a cached property.""" return list(self.groups.all()) @amo.cached_property def addons_listed(self): """Public add-ons this user is listed as author of.""" return self.addons.reviewed().filter(addonuser__user=self, addonuser__listed=True) @property def num_addons_listed(self): """Number of public add-ons this user is listed as author of.""" return self.addons.reviewed().filter(addonuser__user=self, addonuser__listed=True).count() def my_addons(self, n=8, with_unlisted=False): """Returns n addons""" addons = self.addons if with_unlisted: addons = self.addons.model.with_unlisted.filter(authors=self) qs = order_by_translation(addons, 'name') return qs[:n] @property def picture_dir(self): from olympia.amo.helpers import user_media_path split_id = re.match(r'((\d*?)(\d{0,3}?))\d{1,3}$', str(self.id)) return os.path.join(user_media_path('userpics'), split_id.group(2) or '0', split_id.group(1) or '0') @property def picture_path(self): return os.path.join(self.picture_dir, str(self.id) + '.png') @property def picture_url(self): from olympia.amo.helpers import user_media_url if not self.picture_type: return settings.STATIC_URL + '/img/zamboni/anon_user.png' else: split_id = re.match(r'((\d*?)(\d{0,3}?))\d{1,3}$', str(self.id)) modified = int(time.mktime(self.modified.timetuple())) path = "/".join([ split_id.group(2) or '0', split_id.group(1) or '0', "%s.png?modified=%s" % (self.id, modified) ]) return user_media_url('userpics') + path @amo.cached_property def is_developer(self): return self.addonuser_set.exists() @amo.cached_property def is_addon_developer(self): return self.addonuser_set.exclude( addon__type=amo.ADDON_PERSONA).exists() @amo.cached_property def is_artist(self): """Is this user a Personas Artist?""" return self.addonuser_set.filter( addon__type=amo.ADDON_PERSONA).exists() @amo.cached_property def needs_tougher_password(user): from olympia.access import acl return (acl.action_allowed_user(user, 'Admin', '%') or acl.action_allowed_user(user, 'Addons', 'Edit') or acl.action_allowed_user(user, 'Addons', 'Review') or acl.action_allowed_user(user, 'Apps', 'Review') or acl.action_allowed_user(user, 'Personas', 'Review') or acl.action_allowed_user(user, 'Users', 'Edit')) @property def source(self): if not self.pk: return None elif self.fxa_migrated(): return 'fxa' else: return 'amo' def fxa_migrated(self): """Return whether the user has a Firefox Accounts id set or not. When this is True the user must log in through Firefox Accounts.""" return bool(self.fxa_id) @property def name(self): if self.display_name: return smart_unicode(self.display_name) elif self.has_anonymous_username(): # L10n: {id} will be something like "13ad6a", just a random number # to differentiate this user from other anonymous users. return _('Anonymous user {id}').format( id=self._anonymous_username_id()) else: return smart_unicode(self.username) welcome_name = name def _anonymous_username_id(self): if self.has_anonymous_username(): return self.username.split('-')[1][:6] def anonymize_username(self): """Set an anonymous username.""" if self.pk: log.info('Anonymizing username for {}'.format(self.pk)) else: log.info('Generating username for {}'.format(self.email)) self.username = '******'.format(os.urandom(16).encode('hex')) return self.username def has_anonymous_username(self): return re.match('^anonymous-[0-9a-f]{32}$', self.username) def has_anonymous_display_name(self): return not self.display_name and self.has_anonymous_username() @amo.cached_property def reviews(self): """All reviews that are not dev replies.""" qs = self._reviews_all.filter(reply_to=None) # Force the query to occur immediately. Several # reviews-related tests hang if this isn't done. return qs def anonymize(self): log.info(u"User (%s: <%s>) is being anonymized." % (self, self.email)) self.email = None self.password = "******" self.fxa_id = None self.username = "******" % self.id # Can't be null self.display_name = None self.homepage = "" self.deleted = True self.picture_type = "" self.save() @transaction.atomic def restrict(self): from olympia.amo.utils import send_mail log.info(u'User (%s: <%s>) is being restricted and ' 'its user-generated content removed.' % (self, self.email)) g = Group.objects.get(rules='Restricted:UGC') GroupUser.objects.create(user=self, group=g) self.reviews.all().delete() self.collections.all().delete() t = loader.get_template('users/email/restricted.ltxt') send_mail(_('Your account has been restricted'), t.render(Context({})), None, [self.email], use_blacklist=False) def unrestrict(self): log.info(u'User (%s: <%s>) is being unrestricted.' % (self, self.email)) GroupUser.objects.filter(user=self, group__rules='Restricted:UGC').delete() def generate_confirmationcode(self): if not self.confirmationcode: self.confirmationcode = ''.join( random.sample(string.letters + string.digits, 60)) return self.confirmationcode def set_unusable_password(self): self.password = '' def has_usable_password(self): """Override AbstractBaseUser.has_usable_password.""" # We also override the check_password method, and don't rely on # settings.PASSWORD_HASHERS, and don't use "set_unusable_password", so # we want to bypass most of AbstractBaseUser.has_usable_password # checks. return bool(self.password) # Not None and not empty. def check_password(self, raw_password): if not self.has_usable_password(): return False if '$' not in self.password: valid = (get_hexdigest('md5', '', raw_password) == self.password) if valid: # Upgrade an old password. self.set_password(raw_password) self.save() return valid algo, salt, hsh = self.password.split('$') # Complication due to getpersonas account migration; we don't # know if passwords were utf-8 or latin-1 when hashed. If you # can prove that they are one or the other, you can delete one # of these branches. if '+base64' in algo and isinstance(raw_password, unicode): if hsh == get_hexdigest(algo, salt, raw_password.encode('utf-8')): return True else: try: return hsh == get_hexdigest(algo, salt, raw_password.encode('latin1')) except UnicodeEncodeError: return False else: return hsh == get_hexdigest(algo, salt, raw_password) def set_password(self, raw_password, algorithm='sha512'): self.password = create_password(algorithm, raw_password) # Can't do CEF logging here because we don't have a request object. def email_confirmation_code(self): from olympia.amo.utils import send_mail log.debug("Sending account confirmation code for user (%s)", self) url = "%s%s" % (settings.SITE_URL, reverse('users.confirm', args=[self.id, self.confirmationcode])) domain = settings.DOMAIN t = loader.get_template('users/email/confirm.ltxt') c = { 'domain': domain, 'url': url, } send_mail(_("Please confirm your email address"), t.render(Context(c)), None, [self.email], use_blacklist=False, real_email=True) def log_login_attempt(self, successful): """Log a user's login attempt""" self.last_login_attempt = datetime.now() self.last_login_attempt_ip = commonware.log.get_remote_addr() if successful: log.debug(u"User (%s) logged in successfully" % self) self.failed_login_attempts = 0 self.last_login_ip = commonware.log.get_remote_addr() else: log.debug(u"User (%s) failed to log in" % self) if self.failed_login_attempts < 16777216: self.failed_login_attempts += 1 self.save(update_fields=[ 'last_login_ip', 'last_login_attempt', 'last_login_attempt_ip', 'failed_login_attempts' ]) def mobile_collection(self): return self.special_collection(amo.COLLECTION_MOBILE, defaults={ 'slug': 'mobile', 'listed': False, 'name': _('My Mobile Add-ons') }) def favorites_collection(self): return self.special_collection(amo.COLLECTION_FAVORITES, defaults={ 'slug': 'favorites', 'listed': False, 'name': _('My Favorite Add-ons') }) def special_collection(self, type_, defaults): from olympia.bandwagon.models import Collection c, new = Collection.objects.get_or_create(author=self, type=type_, defaults=defaults) if new: # Do an extra query to make sure this gets transformed. c = Collection.objects.using('default').get(id=c.id) return c @contextmanager def activate_lang(self): """ Activate the language for the user. If none is set will go to the site default which is en-US. """ lang = self.lang if self.lang else settings.LANGUAGE_CODE old = get_language() activate(lang) yield activate(old) def remove_locale(self, locale): """Remove the given locale for the user.""" Translation.objects.remove_for(self, locale) @classmethod def get_fallback(cls): return cls._meta.get_field('lang') def addons_for_collection_type(self, type_): """Return the addons for the given special collection type.""" from olympia.bandwagon.models import CollectionAddon qs = CollectionAddon.objects.filter(collection__author=self, collection__type=type_) return qs.values_list('addon', flat=True) @amo.cached_property def mobile_addons(self): return self.addons_for_collection_type(amo.COLLECTION_MOBILE) @amo.cached_property def favorite_addons(self): return self.addons_for_collection_type(amo.COLLECTION_FAVORITES) @amo.cached_property def watching(self): return self.collectionwatcher_set.values_list('collection', flat=True)
class UserProfile(OnChangeMixin, ModelBase, AbstractBaseUser): objects = UserManager() USERNAME_FIELD = 'username' REQUIRED_FIELDS = ['email'] username = models.CharField(max_length=255, default='', unique=True) display_name = models.CharField(max_length=255, default='', null=True, blank=True) email = models.EmailField(unique=True, null=True, max_length=75) averagerating = models.CharField(max_length=255, blank=True, null=True) bio = NoLinksField(short=False) deleted = models.BooleanField(default=False) display_collections = models.BooleanField(default=False) display_collections_fav = models.BooleanField(default=False) homepage = models.URLField(max_length=255, blank=True, default='') location = models.CharField(max_length=255, blank=True, default='') notes = models.TextField(blank=True, null=True) notifycompat = models.BooleanField(default=True) notifyevents = models.BooleanField(default=True) occupation = models.CharField(max_length=255, default='', blank=True) # This is essentially a "has_picture" flag right now picture_type = models.CharField(max_length=75, default='', blank=True) read_dev_agreement = models.DateTimeField(null=True, blank=True) last_login_ip = models.CharField(default='', max_length=45, editable=False) last_login_attempt = models.DateTimeField(null=True, editable=False) last_login_attempt_ip = models.CharField(default='', max_length=45, editable=False) failed_login_attempts = models.PositiveIntegerField(default=0, editable=False) is_verified = models.BooleanField(default=True) region = models.CharField(max_length=11, null=True, blank=True, editable=False) lang = models.CharField(max_length=5, null=True, blank=True, default=settings.LANGUAGE_CODE) fxa_id = models.CharField(blank=True, null=True, max_length=128) class Meta: db_table = 'users' def __init__(self, *args, **kw): super(UserProfile, self).__init__(*args, **kw) if self.username: self.username = force_text(self.username) def __unicode__(self): return u'%s: %s' % (self.id, self.display_name or self.username) @property def is_superuser(self): return self.groups.filter(rules='*:*').exists() @property def is_staff(self): from olympia.access import acl return acl.action_allowed_user(self, 'Admin', '%') def has_perm(self, perm, obj=None): return self.is_superuser def has_module_perms(self, app_label): return self.is_superuser backend = 'django.contrib.auth.backends.ModelBackend' def is_anonymous(self): return False @staticmethod def create_user_url(id_, username=None, url_name='profile', src=None, args=None): """ We use <username> as the slug, unless it contains gross characters - in which case use <id> as the slug. """ from olympia.amo.utils import urlparams chars = '/<>"\'' if not username or any(x in chars for x in username): username = id_ args = args or [] url = reverse('users.%s' % url_name, args=[username] + args) return urlparams(url, src=src) def get_user_url(self, name='profile', src=None, args=None): return self.create_user_url(self.id, self.username, name, src, args) def get_url_path(self, src=None): return self.get_user_url('profile', src=src) @amo.cached_property(writable=True) def groups_list(self): """List of all groups the user is a member of, as a cached property.""" return list(self.groups.all()) @amo.cached_property def addons_listed(self): """Public add-ons this user is listed as author of.""" return self.addons.reviewed().filter( addonuser__user=self, addonuser__listed=True) @property def num_addons_listed(self): """Number of public add-ons this user is listed as author of.""" return self.addons.reviewed().filter( addonuser__user=self, addonuser__listed=True).count() def my_addons(self, n=8, with_unlisted=False): """Returns n addons""" addons = self.addons if with_unlisted: addons = self.addons.model.with_unlisted.filter(authors=self) qs = order_by_translation(addons, 'name') return qs[:n] @property def picture_dir(self): from olympia.amo.helpers import user_media_path split_id = re.match(r'((\d*?)(\d{0,3}?))\d{1,3}$', str(self.id)) return os.path.join(user_media_path('userpics'), split_id.group(2) or '0', split_id.group(1) or '0') @property def picture_path(self): return os.path.join(self.picture_dir, str(self.id) + '.png') @property def picture_url(self): from olympia.amo.helpers import user_media_url if not self.picture_type: return settings.STATIC_URL + '/img/zamboni/anon_user.png' else: split_id = re.match(r'((\d*?)(\d{0,3}?))\d{1,3}$', str(self.id)) modified = int(time.mktime(self.modified.timetuple())) path = "/".join([ split_id.group(2) or '0', split_id.group(1) or '0', "%s.png?modified=%s" % (self.id, modified) ]) return user_media_url('userpics') + path @amo.cached_property def is_developer(self): return self.addonuser_set.exists() @amo.cached_property def is_addon_developer(self): return self.addonuser_set.exclude( addon__type=amo.ADDON_PERSONA).exists() @amo.cached_property def is_artist(self): """Is this user a Personas Artist?""" return self.addonuser_set.filter( addon__type=amo.ADDON_PERSONA).exists() @property def name(self): if self.display_name: return force_text(self.display_name) elif self.has_anonymous_username(): # L10n: {id} will be something like "13ad6a", just a random number # to differentiate this user from other anonymous users. return _('Anonymous user {id}').format( id=self._anonymous_username_id()) else: return force_text(self.username) welcome_name = name def get_full_name(self): return self.name def _anonymous_username_id(self): if self.has_anonymous_username(): return self.username.split('-')[1][:6] def anonymize_username(self): """Set an anonymous username.""" if self.pk: log.info('Anonymizing username for {}'.format(self.pk)) else: log.info('Generating username for {}'.format(self.email)) self.username = '******'.format(os.urandom(16).encode('hex')) return self.username def has_anonymous_username(self): return re.match('^anonymous-[0-9a-f]{32}$', self.username) def has_anonymous_display_name(self): return not self.display_name and self.has_anonymous_username() @amo.cached_property def reviews(self): """All reviews that are not dev replies.""" qs = self._reviews_all.filter(reply_to=None) # Force the query to occur immediately. Several # reviews-related tests hang if this isn't done. return qs def anonymize(self): log.info(u"User (%s: <%s>) is being anonymized." % (self, self.email)) self.email = None self.fxa_id = None self.username = "******" % self.id # Can't be null self.display_name = None self.homepage = "" self.deleted = True self.picture_type = "" self.save() @transaction.atomic def restrict(self): from olympia.amo.utils import send_mail log.info(u'User (%s: <%s>) is being restricted and ' 'its user-generated content removed.' % (self, self.email)) g = Group.objects.get(rules='Restricted:UGC') GroupUser.objects.create(user=self, group=g) self.reviews.all().delete() self.collections.all().delete() t = loader.get_template('users/email/restricted.ltxt') send_mail(_('Your account has been restricted'), t.render(Context({})), None, [self.email], use_deny_list=False) def unrestrict(self): log.info(u'User (%s: <%s>) is being unrestricted.' % (self, self.email)) GroupUser.objects.filter(user=self, group__rules='Restricted:UGC').delete() def set_unusable_password(self): raise NotImplementedError('cannot set unusable password') def set_password(self, password): raise NotImplementedError('cannot set password') def check_password(self, password): raise NotImplementedError('cannot check password') def log_login_attempt(self, successful): """Log a user's login attempt""" self.last_login_attempt = datetime.now() self.last_login_attempt_ip = commonware.log.get_remote_addr() if successful: log.debug(u"User (%s) logged in successfully" % self) self.failed_login_attempts = 0 self.last_login_ip = commonware.log.get_remote_addr() else: log.debug(u"User (%s) failed to log in" % self) if self.failed_login_attempts < 16777216: self.failed_login_attempts += 1 self.save(update_fields=['last_login_ip', 'last_login_attempt', 'last_login_attempt_ip', 'failed_login_attempts']) def mobile_collection(self): return self.special_collection( amo.COLLECTION_MOBILE, defaults={'slug': 'mobile', 'listed': False, 'name': _('My Mobile Add-ons')}) def favorites_collection(self): return self.special_collection( amo.COLLECTION_FAVORITES, defaults={'slug': 'favorites', 'listed': False, 'name': _('My Favorite Add-ons')}) def special_collection(self, type_, defaults): from olympia.bandwagon.models import Collection c, new = Collection.objects.get_or_create( author=self, type=type_, defaults=defaults) if new: # Do an extra query to make sure this gets transformed. c = Collection.objects.using('default').get(id=c.id) return c @contextmanager def activate_lang(self): """ Activate the language for the user. If none is set will go to the site default which is en-US. """ lang = self.lang if self.lang else settings.LANGUAGE_CODE old = get_language() activate(lang) yield activate(old) def remove_locale(self, locale): """Remove the given locale for the user.""" Translation.objects.remove_for(self, locale) @classmethod def get_fallback(cls): return cls._meta.get_field('lang') def addons_for_collection_type(self, type_): """Return the addons for the given special collection type.""" from olympia.bandwagon.models import CollectionAddon qs = CollectionAddon.objects.filter( collection__author=self, collection__type=type_) return qs.values_list('addon', flat=True) @amo.cached_property def mobile_addons(self): return self.addons_for_collection_type(amo.COLLECTION_MOBILE) @amo.cached_property def favorite_addons(self): return self.addons_for_collection_type(amo.COLLECTION_FAVORITES) @amo.cached_property def watching(self): return self.collectionwatcher_set.values_list('collection', flat=True)