Esempio n. 1
0
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)
Esempio n. 2
0
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)