Esempio n. 1
0
class FeedApp(amo.models.ModelBase):
    """
    Thin wrapper around the Webapp class that allows single apps to be featured
    on the feed.
    """
    app = models.ForeignKey(Webapp)
    description = PurifiedField()

    # Optionally linked to a Preview (screenshot or video).
    preview = models.ForeignKey(Preview, null=True, blank=True)

    # Optionally linked to a pull quote.
    pullquote_rating = models.PositiveSmallIntegerField(null=True, blank=True,
        validators=[validate_rating])
    pullquote_text = PurifiedField(null=True)
    pullquote_attribution = PurifiedField(null=True)

    class Meta:
        db_table = 'mkt_feed_app'

    def clean(self):
        """
        Require `pullquote_text` if `pullquote_rating` or
        `pullquote_attribution` are set.
        """
        if not self.pullquote_text and (self.pullquote_rating or
                                        self.pullquote_attribution):
            raise ValidationError('Pullquote text required if rating or '
                                  'attribution is defined.')
        super(FeedApp, self).clean()
Esempio n. 2
0
class L10nSettings(amo.models.ModelBase):
    """Per-locale L10n Dashboard settings"""
    locale = models.CharField(max_length=30, default='', unique=True)
    motd = PurifiedField()
    team_homepage = models.CharField(max_length=255, default='', null=True)

    class Meta:
        db_table = 'l10n_settings'
Esempio n. 3
0
class FeedApp(amo.models.ModelBase):
    """
    Model for "Custom Featured Apps", a feed item highlighting a single app
    and some additional metadata (e.g. a review or a screenshot).
    """
    app = models.ForeignKey(Webapp)
    feedapp_type = models.CharField(choices=FEEDAPP_TYPES, max_length=30)
    description = PurifiedField()
    slug = SlugField(max_length=30, unique=True)
    background_color = ColorField(null=True)

    # Optionally linked to a Preview (screenshot or video).
    preview = models.ForeignKey(Preview, null=True, blank=True)

    # Optionally linked to a pull quote.
    pullquote_attribution = models.CharField(max_length=50, null=True,
                                             blank=True)
    pullquote_rating = models.PositiveSmallIntegerField(null=True, blank=True,
        validators=[validate_rating])
    pullquote_text = PurifiedField(null=True)

    image_hash = models.CharField(default=None, max_length=8, null=True,
                                  blank=True)

    class Meta:
        db_table = 'mkt_feed_app'

    def clean(self):
        """
        Require `pullquote_text` if `pullquote_rating` or
        `pullquote_attribution` are set.
        """
        if not self.pullquote_text and (self.pullquote_rating or
                                        self.pullquote_attribution):
            raise ValidationError('Pullquote text required if rating or '
                                  'attribution is defined.')
        super(FeedApp, self).clean()

    def image_path(self):
        return os.path.join(settings.FEATURED_APP_BG_PATH,
                            str(self.pk / 1000),
                            'featured_app_%s.png' % (self.pk,))

    @property
    def has_image(self):
        return bool(self.image_hash)
Esempio n. 4
0
class FeedApp(amo.models.ModelBase):
    """
    Thin wrapper around the Webapp class that allows single apps to be featured
    on the feed.
    """
    app = models.ForeignKey(Webapp)
    feedapp_type = models.CharField(choices=FEEDAPP_TYPES, max_length=30)
    description = PurifiedField()
    slug = SlugField(max_length=30)
    background_color = ColorField(null=True)
    has_image = models.BooleanField(default=False)

    # Optionally linked to a Preview (screenshot or video).
    preview = models.ForeignKey(Preview, null=True, blank=True)

    # Optionally linked to a pull quote.
    pullquote_rating = models.PositiveSmallIntegerField(
        null=True, blank=True, validators=[validate_rating])
    pullquote_text = PurifiedField(null=True)
    pullquote_attribution = PurifiedField(null=True)

    class Meta:
        db_table = 'mkt_feed_app'

    def clean(self):
        """
        Require `pullquote_text` if `pullquote_rating` or
        `pullquote_attribution` are set.
        """
        if not self.pullquote_text and (self.pullquote_rating
                                        or self.pullquote_attribution):
            raise ValidationError('Pullquote text required if rating or '
                                  'attribution is defined.')
        super(FeedApp, self).clean()

    def image_path(self):
        return os.path.join(settings.FEATURED_APP_BG_PATH, str(self.pk / 1000),
                            'featured_app_%s.png' % (self.pk, ))
Esempio n. 5
0
class Version(amo.models.ModelBase):
    addon = models.ForeignKey('addons.Addon', related_name='versions')
    license = models.ForeignKey('License', null=True)
    releasenotes = PurifiedField()
    approvalnotes = models.TextField(default='', null=True)
    version = models.CharField(max_length=255, default='0.1')
    version_int = models.BigIntegerField(null=True, editable=False)

    nomination = models.DateTimeField(null=True)
    reviewed = models.DateTimeField(null=True)

    has_info_request = models.BooleanField(default=False)
    has_editor_comment = models.BooleanField(default=False)

    deleted = models.BooleanField(default=False)

    supported_locales = models.CharField(max_length=255)

    objects = VersionManager()
    with_deleted = VersionManager(include_deleted=True)

    class Meta(amo.models.ModelBase.Meta):
        db_table = 'versions'
        ordering = ['-created', '-modified']

    def __init__(self, *args, **kwargs):
        super(Version, self).__init__(*args, **kwargs)
        self.__dict__.update(version_dict(self.version or ''))

    def __unicode__(self):
        return jinja2.escape(self.version)

    def save(self, *args, **kw):
        if not self.version_int and self.version:
            v_int = version_int(self.version)
            # Magic number warning, this is the maximum size
            # of a big int in MySQL to prevent version_int overflow, for
            # people who have rather crazy version numbers.
            # http://dev.mysql.com/doc/refman/5.5/en/numeric-types.html
            if v_int < 9223372036854775807:
                self.version_int = v_int
            else:
                log.error('No version_int written for version %s, %s' %
                          (self.pk, self.version))
        creating = not self.id
        super(Version, self).save(*args, **kw)
        if creating:
            from mkt.webapps.models import AppFeatures
            if self.addon.type == amo.ADDON_WEBAPP:
                AppFeatures.objects.create(version=self)
        return self

    @classmethod
    def from_upload(cls, upload, addon, platforms, send_signal=True):
        data = utils.parse_addon(upload, addon)
        try:
            license = addon.versions.latest().license_id
        except Version.DoesNotExist:
            license = None
        v = cls.objects.create(addon=addon,
                               version=data['version'],
                               license_id=license)
        log.info('New version: %r (%s) from %r' % (v, v.id, upload))
        # appversions
        AV = ApplicationsVersions
        for app in data.get('apps', []):
            AV(version=v, min=app.min, max=app.max,
               application_id=app.id).save()
        if addon.type in [amo.ADDON_SEARCH, amo.ADDON_WEBAPP]:
            # Search extensions and webapps are always for all platforms.
            platforms = [Platform.objects.get(id=amo.PLATFORM_ALL.id)]
        else:
            platforms = cls._make_safe_platform_files(platforms)

        for platform in platforms:
            File.from_upload(upload, v, platform, parse_data=data)

        if addon.type == amo.ADDON_WEBAPP:
            update_supported_locales_single.apply_async(
                args=[addon.id],
                kwargs={'latest': True},
                eta=datetime.datetime.now() +
                datetime.timedelta(seconds=settings.NFS_LAG_DELAY))

        v.disable_old_files()
        # After the upload has been copied to all platforms, remove the upload.
        storage.delete(upload.path)
        if send_signal:
            version_uploaded.send(sender=v)

        # If packaged app and app is blocked, put in escalation queue.
        if (addon.is_webapp() and addon.is_packaged
                and addon.status == amo.STATUS_BLOCKED):
            # To avoid circular import.
            from editors.models import EscalationQueue
            EscalationQueue.objects.create(addon=addon)

        return v

    @classmethod
    def _make_safe_platform_files(cls, platforms):
        """Make file platform translations until all download pages
        support desktop ALL + mobile ALL. See bug 646268.
        """
        pl_set = set([p.id for p in platforms])

        if pl_set == set([amo.PLATFORM_ALL_MOBILE.id, amo.PLATFORM_ALL.id]):
            # Make it really ALL:
            return [Platform.objects.get(id=amo.PLATFORM_ALL.id)]

        has_mobile = any(p in amo.MOBILE_PLATFORMS for p in pl_set)
        has_desktop = any(p in amo.DESKTOP_PLATFORMS for p in pl_set)
        has_all = any(p in (amo.PLATFORM_ALL_MOBILE.id, amo.PLATFORM_ALL.id)
                      for p in pl_set)
        is_mixed = has_mobile and has_desktop
        if (is_mixed and has_all) or has_mobile:
            # Mixing desktop and mobile w/ ALL is not safe;
            # we have to split the files into exact platforms.
            # Additionally, it is not safe to use all-mobile.
            new_plats = []
            for p in platforms:
                if p.id == amo.PLATFORM_ALL_MOBILE.id:
                    new_plats.extend(
                        list(
                            Platform.objects.filter(
                                id__in=amo.MOBILE_PLATFORMS).exclude(
                                    id=amo.PLATFORM_ALL_MOBILE.id)))
                elif p.id == amo.PLATFORM_ALL.id:
                    new_plats.extend(
                        list(
                            Platform.objects.filter(
                                id__in=amo.DESKTOP_PLATFORMS).exclude(
                                    id=amo.PLATFORM_ALL.id)))
                else:
                    new_plats.append(p)
            return new_plats

        # Platforms are safe as is
        return platforms

    @property
    def path_prefix(self):
        return os.path.join(settings.ADDONS_PATH, str(self.addon_id))

    @property
    def mirror_path_prefix(self):
        return os.path.join(settings.MIRROR_STAGE_PATH, str(self.addon_id))

    def license_url(self, impala=False):
        return reverse('addons.license', args=[self.addon.slug, self.version])

    def flush_urls(self):
        return self.addon.flush_urls()

    def get_url_path(self):
        return reverse('addons.versions', args=[self.addon.slug, self.version])

    def delete(self):
        log.info(u'Version deleted: %r (%s)' % (self, self.id))
        amo.log(amo.LOG.DELETE_VERSION, self.addon, str(self.version))
        if settings.MARKETPLACE:
            self.update(deleted=True)
            if self.addon.is_packaged:
                f = self.all_files[0]
                # Unlink signed packages if packaged app.
                storage.delete(f.signed_file_path)
                log.info(u'Unlinked file: %s' % f.signed_file_path)
                storage.delete(f.signed_reviewer_file_path)
                log.info(u'Unlinked file: %s' % f.signed_reviewer_file_path)

        else:
            super(Version, self).delete()

    @property
    def current_queue(self):
        """Return the current queue, or None if not in a queue."""
        from editors.models import (ViewPendingQueue, ViewFullReviewQueue,
                                    ViewPreliminaryQueue)

        if self.addon.status in [
                amo.STATUS_NOMINATED, amo.STATUS_LITE_AND_NOMINATED
        ]:
            return ViewFullReviewQueue
        elif self.addon.status == amo.STATUS_PUBLIC:
            return ViewPendingQueue
        elif self.addon.status in [amo.STATUS_LITE, amo.STATUS_UNREVIEWED]:
            return ViewPreliminaryQueue

        return None

    @amo.cached_property(writable=True)
    def all_activity(self):
        from devhub.models import VersionLog  # yucky
        al = (VersionLog.objects.filter(
            version=self.id).order_by('created').select_related(
                depth=1).no_cache())
        return al

    @amo.cached_property(writable=True)
    def compatible_apps(self):
        """Get a mapping of {APP: ApplicationVersion}."""
        avs = self.apps.select_related(depth=1)
        return self._compat_map(avs)

    @amo.cached_property
    def compatible_apps_ordered(self):
        apps = self.compatible_apps.items()
        return sorted(apps, key=lambda v: v[0].short)

    def compatible_platforms(self):
        """Returns a dict of compatible file platforms for this version.

        The result is based on which app(s) the version targets.
        """
        apps = set([a.application.id for a in self.apps.all()])
        targets_mobile = amo.MOBILE.id in apps
        targets_other = any((a != amo.MOBILE.id) for a in apps)
        all_plats = {}
        if targets_other:
            all_plats.update(amo.DESKTOP_PLATFORMS)
        if targets_mobile:
            all_plats.update(amo.MOBILE_PLATFORMS)
        return all_plats

    @amo.cached_property
    def is_compatible(self):
        """Returns tuple of compatibility and reasons why if not.

        Server side conditions for determining compatibility are:
            * The add-on is an extension (not a theme, app, etc.)
            * Has not opted in to strict compatibility.
            * Does not use binary_components in chrome.manifest.

        Note: The lowest maxVersion compat check needs to be checked
              separately.
        Note: This does not take into account the client conditions.

        """
        compat = True
        reasons = []
        if self.addon.type != amo.ADDON_EXTENSION:
            compat = False
            # TODO: We may want this. For now we think it may be confusing.
            # reasons.append(_('Add-on is not an extension.'))
        if self.files.filter(binary_components=True).exists():
            compat = False
            reasons.append(_('Add-on uses binary components.'))
        if self.files.filter(strict_compatibility=True).exists():
            compat = False
            reasons.append(
                _('Add-on has opted into strict compatibility '
                  'checking.'))
        return (compat, reasons)

    def is_compatible_app(self, app):
        """Returns True if the provided app passes compatibility conditions."""
        appversion = self.compatible_apps.get(app)
        if appversion and app.id in amo.D2C_MAX_VERSIONS:
            return (version_int(appversion.max.version) >= version_int(
                amo.D2C_MAX_VERSIONS.get(app.id, '*')))
        return False

    def compat_override_app_versions(self):
        """Returns the incompatible app versions range(s).

        If not ranges, returns empty list.  Otherwise, this will return all
        the app version ranges that this particular version is incompatible
        with.

        """
        from addons.models import CompatOverride
        cos = CompatOverride.objects.filter(addon=self.addon)
        if not cos:
            return []
        app_versions = []
        for co in cos:
            for range in co.collapsed_ranges():
                if (version_int(range.min) <= version_int(self.version) <=
                        version_int(range.max)):
                    app_versions.extend([(a.min, a.max) for a in range.apps])
        return app_versions

    @amo.cached_property(writable=True)
    def all_files(self):
        """Shortcut for list(self.files.all()).  Heavily cached."""
        return list(self.files.all())

    @amo.cached_property
    def supported_platforms(self):
        """Get a list of supported platform names."""
        return list(set(amo.PLATFORMS[f.platform_id] for f in self.all_files))

    @property
    def status(self):
        if settings.MARKETPLACE and self.deleted:
            return [amo.STATUS_CHOICES[amo.STATUS_DELETED]]
        else:
            return [amo.STATUS_CHOICES[f.status] for f in self.all_files]

    @property
    def statuses(self):
        """Unadulterated statuses, good for an API."""
        return [(f.id, f.status) for f in self.all_files]

    def is_allowed_upload(self):
        """Check that a file can be uploaded based on the files
        per platform for that type of addon."""

        num_files = len(self.all_files)
        if self.addon.type == amo.ADDON_SEARCH:
            return num_files == 0
        elif num_files == 0:
            return True
        elif amo.PLATFORM_ALL in self.supported_platforms:
            return False
        elif amo.PLATFORM_ALL_MOBILE in self.supported_platforms:
            return False
        else:
            compatible = (v for k, v in self.compatible_platforms().items()
                          if k not in (amo.PLATFORM_ALL.id,
                                       amo.PLATFORM_ALL_MOBILE.id))
            return bool(set(compatible) - set(self.supported_platforms))

    @property
    def has_files(self):
        return bool(self.all_files)

    @property
    def is_unreviewed(self):
        return filter(lambda f: f.status in amo.UNREVIEWED_STATUSES,
                      self.all_files)

    @property
    def is_all_unreviewed(self):
        return not bool([
            f
            for f in self.all_files if f.status not in amo.UNREVIEWED_STATUSES
        ])

    @property
    def is_beta(self):
        return filter(lambda f: f.status == amo.STATUS_BETA, self.all_files)

    @property
    def is_lite(self):
        return filter(lambda f: f.status in amo.LITE_STATUSES, self.all_files)

    @property
    def is_jetpack(self):
        return all(f.jetpack_version for f in self.all_files)

    @classmethod
    def _compat_map(cls, avs):
        apps = {}
        for av in avs:
            app_id = av.application_id
            if app_id in amo.APP_IDS:
                apps[amo.APP_IDS[app_id]] = av
        return apps

    @classmethod
    def transformer(cls, versions):
        """Attach all the compatible apps and files to the versions."""
        ids = set(v.id for v in versions)
        if not versions:
            return

        avs = (ApplicationsVersions.objects.filter(
            version__in=ids).select_related(depth=1).no_cache())
        files = (File.objects.filter(
            version__in=ids).select_related('version').no_cache())

        def rollup(xs):
            groups = amo.utils.sorted_groupby(xs, 'version_id')
            return dict((k, list(vs)) for k, vs in groups)

        av_dict, file_dict = rollup(avs), rollup(files)

        for version in versions:
            v_id = version.id
            version.compatible_apps = cls._compat_map(av_dict.get(v_id, []))
            version.all_files = file_dict.get(v_id, [])

    @classmethod
    def transformer_activity(cls, versions):
        """Attach all the activity to the versions."""
        from devhub.models import VersionLog  # yucky

        ids = set(v.id for v in versions)
        if not versions:
            return

        al = (VersionLog.objects.filter(
            version__in=ids).order_by('created').select_related(
                depth=1).no_cache())

        def rollup(xs):
            groups = amo.utils.sorted_groupby(xs, 'version_id')
            return dict((k, list(vs)) for k, vs in groups)

        al_dict = rollup(al)

        for version in versions:
            v_id = version.id
            version.all_activity = al_dict.get(v_id, [])

    def disable_old_files(self):
        if not self.files.filter(status=amo.STATUS_BETA).exists():
            qs = File.objects.filter(
                version__addon=self.addon_id,
                version__lt=self.id,
                version__deleted=False,
                status__in=[amo.STATUS_UNREVIEWED, amo.STATUS_PENDING])
            # Use File.update so signals are triggered.
            for f in qs:
                f.update(status=amo.STATUS_DISABLED)
Esempio n. 6
0
class Collection(amo.models.ModelBase):
    collection_type = models.IntegerField(choices=COLLECTION_TYPES)
    description = PurifiedField()
    name = PurifiedField()
    is_public = models.BooleanField(default=False)
    # FIXME: add better / composite indexes that matches the query we are
    # going to make.
    category = models.ForeignKey(Category, null=True, blank=True)
    region = models.PositiveIntegerField(
        default=None,
        null=True,
        blank=True,
        choices=mkt.regions.REGIONS_CHOICES_ID,
        db_index=True)
    carrier = models.IntegerField(default=None,
                                  null=True,
                                  blank=True,
                                  choices=mkt.carriers.CARRIER_CHOICES,
                                  db_index=True)
    author = models.CharField(max_length=255, default='', blank=True)
    slug = SlugField(blank=True,
                     max_length=30,
                     help_text='Used in collection URLs.')
    default_language = models.CharField(
        max_length=10,
        choices=((to_language(lang), desc)
                 for lang, desc in settings.LANGUAGES.items()),
        default=to_language(settings.LANGUAGE_CODE))
    curators = models.ManyToManyField('users.UserProfile')
    background_color = ColorField(null=True)
    text_color = ColorField(null=True)
    has_image = models.BooleanField(default=False)
    can_be_hero = models.BooleanField(
        default=False,
        help_text=
        ('Indicates whether an operator shelf collection can be displayed with'
         'a hero graphic'))
    _apps = models.ManyToManyField(Webapp,
                                   through='CollectionMembership',
                                   related_name='app_collections')

    objects = amo.models.ManagerBase()
    public = PublicCollectionsManager()

    class Meta:
        db_table = 'app_collections'
        ordering = ('-id', )  # This will change soon since we'll need to be
        # able to order collections themselves, but this
        # helps tests for now.

    def __unicode__(self):
        return self.name.localized_string_clean

    def save(self, **kw):
        self.clean_slug()
        return super(Collection, self).save(**kw)

    @use_master
    def clean_slug(self):
        clean_slug(self, 'slug')

    @classmethod
    def get_fallback(cls):
        return cls._meta.get_field('default_language')

    def image_path(self):
        return os.path.join(settings.COLLECTIONS_ICON_PATH,
                            str(self.pk / 1000),
                            'app_collection_%s.png' % (self.pk, ))

    def apps(self):
        """
        Public apps on the collection, ordered by their position in the
        CollectionMembership model.

        Use this method everytime you want to display apps for a collection to
        an user.
        """
        return self._apps.filter(
            disabled_by_user=False,
            status=amo.STATUS_PUBLIC).order_by('collectionmembership')

    def add_app(self, app, order=None):
        """
        Add an app to this collection. If specified, the app will be created
        with the specified `order`. If not, it will be added to the end of the
        collection.
        """
        qs = CollectionMembership.objects.filter(collection=self)
        if order is None:
            aggregate = qs.aggregate(models.Max('order'))['order__max']
            order = aggregate + 1 if aggregate is not None else 0
        rval = CollectionMembership.objects.create(collection=self,
                                                   app=app,
                                                   order=order)
        # Help django-cache-machine: it doesn't like many 2 many relations,
        # the cache is never invalidated properly when adding a new object.
        CollectionMembership.objects.invalidate(*qs)
        index_webapps.delay([app.pk])
        return rval

    def remove_app(self, app):
        """
        Remove the passed app from this collection, returning a boolean
        indicating whether a successful deletion took place.
        """
        try:
            membership = self.collectionmembership_set.get(app=app)
        except CollectionMembership.DoesNotExist:
            return False
        else:
            membership.delete()
            index_webapps.delay([app.pk])
            return True

    def reorder(self, new_order):
        """
        Passed a list of app IDs, e.g.

        [18, 24, 9]

        will change the order of each item in the collection to match the
        passed order. A ValueError will be raised if each app in the
        collection is not included in the ditionary.
        """
        existing_pks = self.apps().no_cache().values_list('pk', flat=True)
        if set(existing_pks) != set(new_order):
            raise ValueError('Not all apps included')
        for order, pk in enumerate(new_order):
            CollectionMembership.objects.get(collection=self,
                                             app_id=pk).update(order=order)
        index_webapps.delay(new_order)

    def has_curator(self, userprofile):
        """
        Returns boolean indicating whether the passed user profile is a curator
        on this collection.

        ID comparison used instead of directly checking objects to ensure that
        RequestUser or UserProfile objects could be passed.
        """
        return userprofile.id in self.curators.values_list('id', flat=True)

    def add_curator(self, userprofile):
        ret = self.curators.add(userprofile)
        Collection.objects.invalidate(*self.curators.all())
        return ret

    def remove_curator(self, userprofile):
        ret = self.curators.remove(userprofile)
        Collection.objects.invalidate(*self.curators.all())
        return ret
Esempio n. 7
0
class UserProfile(amo.models.OnChangeMixin, amo.models.ModelBase):
    username = models.CharField(max_length=255, default='', unique=True)
    display_name = models.CharField(max_length=255, default='', null=True,
                                    blank=True)

    password = models.CharField(max_length=255, default='')
    email = models.EmailField(unique=True, null=True)

    averagerating = models.CharField(max_length=255, blank=True, null=True)
    bio = PurifiedField(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)
    emailhidden = models.BooleanField(default=True)
    homepage = models.URLField(max_length=255, blank=True, default='',
                               verify_exists=False)
    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)
    resetcode = models.CharField(max_length=255, default='', blank=True)
    resetcode_expires = models.DateTimeField(default=datetime.now, null=True,
                                             blank=True)
    read_dev_agreement = models.BooleanField(default=False)

    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)

    user = models.ForeignKey(DjangoUser, null=True, editable=False, blank=True)

    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)

    def is_anonymous(self):
        return False

    def get_url_path(self):
        if settings.MARKETPLACE:
            return reverse('users.profile', args=[self.username or self.id])
        else:
            # AMO isn't ready for this.
            return reverse('users.profile', args=[self.id])

    def flush_urls(self):
        urls = ['*/user/%d/' % self.id,
                self.picture_url,
                ]

        return urls

    @amo.cached_property
    def addons_listed(self):
        """Public add-ons this user is listed as author of."""
        return self.addons.reviewed().exclude(type=amo.ADDON_WEBAPP).filter(
            addonuser__user=self, addonuser__listed=True)

    @amo.cached_property
    def apps_listed(self):
        """Public apps this user is listed as author of."""
        return self.addons.reviewed().filter(type=amo.ADDON_WEBAPP,
            addonuser__user=self, addonuser__listed=True)

    def my_addons(self, n=8):
        """Returns n addons (anything not a webapp)"""
        qs = self.addons.exclude(type=amo.ADDON_WEBAPP)
        qs = order_by_translation(qs, 'name')
        return qs[:n]

    def my_apps(self, n=8):
        """Returns n apps"""
        qs = self.addons.filter(type=amo.ADDON_WEBAPP)
        qs = order_by_translation(qs, 'name')
        return qs[:n]

    @property
    def picture_dir(self):
        split_id = re.match(r'((\d*?)(\d{0,3}?))\d{1,3}$', str(self.id))
        return os.path.join(settings.USERPICS_PATH, 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):
        if not self.picture_type:
            return settings.MEDIA_URL + '/img/zamboni/anon_user.png'
        else:
            split_id = re.match(r'((\d*?)(\d{0,3}?))\d{1,3}$', str(self.id))
            return settings.USERPICS_URL % (
                split_id.group(2) or 0, split_id.group(1) or 0, self.id,
                int(time.mktime(self.modified.timetuple())))

    @amo.cached_property
    def is_developer(self):
        return self.addonuser_set.exists()

    @amo.cached_property
    def is_app_developer(self):
        return self.addonuser_set.filter(addon__type=amo.ADDON_WEBAPP).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 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 name(self):
        return smart_unicode(self.display_name or self.username)

    welcome_name = name

    @property
    def last_login(self):
        """Make UserProfile look more like auth.User."""
        # Django expects this to be non-null, so fake a login attempt.
        if not self.last_login_attempt:
            self.update(last_login_attempt=datetime.now())
        return self.last_login_attempt

    @amo.cached_property
    def reviews(self):
        """All reviews that are not dev replies."""
        return self._reviews_all.filter(reply_to=None)

    def anonymize(self):
        log.info(u"User (%s: <%s>) is being anonymized." % (self, self.email))
        self.email = None
        self.password = "******"
        self.username = "******" % self.id  # Can't be null
        self.display_name = None
        self.homepage = ""
        self.deleted = True
        self.picture_type = ""
        self.save()

    @transaction.commit_on_success
    def restrict(self):
        from 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 save(self, force_insert=False, force_update=False, using=None):
        # we have to fix stupid things that we defined poorly in remora
        if not self.resetcode_expires:
            self.resetcode_expires = datetime.now()

        delete_user = None
        if self.deleted and self.user:
            delete_user = self.user
            self.user = None
            # Delete user after saving this profile.

        super(UserProfile, self).save(force_insert, force_update, using)

        if self.deleted and delete_user:
            delete_user.delete()

    def check_password(self, raw_password):
        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('$')
        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 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)

    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()

    def create_django_user(self):
        """Make a django.contrib.auth.User for this UserProfile."""
        # Reusing the id will make our life easier, because we can use the
        # OneToOneField as pk for Profile linked back to the auth.user
        # in the future.
        self.user = DjangoUser(id=self.pk)
        self.user.first_name = ''
        self.user.last_name = ''
        self.user.username = self.username
        self.user.email = self.email
        self.user.password = self.password
        self.user.date_joined = self.created

        if self.groups.filter(rules='*:*').count():
            self.user.is_superuser = self.user.is_staff = True

        self.user.save()
        self.save()
        return self.user

    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 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

    def purchase_ids(self):
        """
        I'm special casing this because we use purchase_ids a lot in the site
        and we are not caching empty querysets in cache-machine.
        That means that when the site is first launched we are having a
        lot of empty queries hit.

        We can probably do this in smarter fashion by making cache-machine
        cache empty queries on an as need basis.
        """
        # Circular import
        from amo.utils import memoize
        from market.models import AddonPurchase

        @memoize(prefix='users:purchase-ids')
        def ids(pk):
            return (AddonPurchase.objects.filter(user=pk)
                                 .values_list('addon_id', flat=True)
                                 .filter(type=amo.CONTRIB_PURCHASE)
                                 .order_by('pk'))
        return ids(self.pk)

    def get_preapproval(self):
        """
        Returns the pre approval object for this user, or None if it does
        not exist
        """
        try:
            return self.preapprovaluser
        except ObjectDoesNotExist:
            pass

    def has_preapproval_key(self):
        """
        Returns the pre approval paypal key for this user, or False if the
        pre_approval doesn't exist or the key is blank.
        """
        return bool(getattr(self.get_preapproval(), 'paypal_key', ''))

    def can_view_consumer(self):
        # To view the consumer pages, the user must satisfy either criterion:
        #   * Have submitted an app.
        #   * Is a vouched Mozillian or whitelisted fella.
        return self.is_app_developer or AccessWhitelist.matches(self.email)
Esempio n. 8
0
class Version(amo.models.ModelBase):
    addon = models.ForeignKey('addons.Addon', related_name='versions')
    license = models.ForeignKey('License', null=True)
    releasenotes = PurifiedField()
    approvalnotes = models.TextField(default='', null=True)
    version = models.CharField(max_length=255, default='0.1')
    version_int = models.BigIntegerField(null=True, editable=False)

    nomination = models.DateTimeField(null=True)
    reviewed = models.DateTimeField(null=True)

    has_info_request = models.BooleanField(default=False)
    has_editor_comment = models.BooleanField(default=False)

    class Meta(amo.models.ModelBase.Meta):
        db_table = 'versions'
        ordering = ['-created', '-modified']

    def __init__(self, *args, **kwargs):
        super(Version, self).__init__(*args, **kwargs)
        self.__dict__.update(compare.version_dict(self.version or ''))

    def __unicode__(self):
        return jinja2.escape(self.version)

    def save(self, *args, **kw):
        if not self.version_int and self.version:
            version_int = compare.version_int(self.version)
            # Magic number warning, this is the maximum size
            # of a big int in MySQL to prevent version_int overflow, for
            # people who have rather crazy version numbers.
            # http://dev.mysql.com/doc/refman/5.5/en/numeric-types.html
            if version_int < 9223372036854775807:
                self.version_int = version_int
            else:
                log.error('No version_int written for version %s, %s' %
                          (self.pk, self.version))
        return super(Version, self).save(*args, **kw)

    @classmethod
    def from_upload(cls, upload, addon, platforms, send_signal=True):
        data = utils.parse_addon(upload, addon)
        try:
            license = addon.versions.latest().license_id
        except Version.DoesNotExist:
            license = None
        v = cls.objects.create(addon=addon,
                               version=data['version'],
                               license_id=license)
        log.info('New version: %r (%s) from %r' % (v, v.id, upload))
        # appversions
        AV = ApplicationsVersions
        for app in data.get('apps', []):
            AV(version=v, min=app.min, max=app.max,
               application_id=app.id).save()
        if addon.type == amo.ADDON_SEARCH:
            # Search extensions are always for all platforms.
            platforms = [Platform.objects.get(id=amo.PLATFORM_ALL.id)]
        else:
            platforms = cls._make_safe_platform_files(platforms)

        for platform in platforms:
            File.from_upload(upload, v, platform, parse_data=data)

        v.disable_old_files()
        # After the upload has been copied to all
        # platforms, remove the upload.
        path.path(upload.path).unlink()
        if send_signal:
            version_uploaded.send(sender=v)
        return v

    @classmethod
    def _make_safe_platform_files(cls, platforms):
        """Make file platform translations until all download pages
        support desktop ALL + mobile ALL. See bug 646268.
        """
        pl_set = set([p.id for p in platforms])

        if pl_set == set([amo.PLATFORM_ALL_MOBILE.id, amo.PLATFORM_ALL.id]):
            # Make it really ALL:
            return [Platform.objects.get(id=amo.PLATFORM_ALL.id)]

        has_mobile = any(p in amo.MOBILE_PLATFORMS for p in pl_set)
        has_desktop = any(p in amo.DESKTOP_PLATFORMS for p in pl_set)
        has_all = any(p in (amo.PLATFORM_ALL_MOBILE.id, amo.PLATFORM_ALL.id)
                      for p in pl_set)
        is_mixed = has_mobile and has_desktop
        if (is_mixed and has_all) or has_mobile:
            # Mixing desktop and mobile w/ ALL is not safe;
            # we have to split the files into exact platforms.
            # Additionally, it is not safe to use all-mobile.
            new_plats = []
            for p in platforms:
                if p.id == amo.PLATFORM_ALL_MOBILE.id:
                    new_plats.extend(
                        list(
                            Platform.objects.filter(
                                id__in=amo.MOBILE_PLATFORMS).exclude(
                                    id=amo.PLATFORM_ALL_MOBILE.id)))
                elif p.id == amo.PLATFORM_ALL.id:
                    new_plats.extend(
                        list(
                            Platform.objects.filter(
                                id__in=amo.DESKTOP_PLATFORMS).exclude(
                                    id=amo.PLATFORM_ALL.id)))
                else:
                    new_plats.append(p)
            return new_plats

        # Platforms are safe as is
        return platforms

    @property
    def path_prefix(self):
        return os.path.join(settings.ADDONS_PATH, str(self.addon_id))

    @property
    def mirror_path_prefix(self):
        return os.path.join(settings.MIRROR_STAGE_PATH, str(self.addon_id))

    def license_url(self, impala=False):
        return reverse('addons.license', args=[self.addon.slug, self.version])

    def flush_urls(self):
        return self.addon.flush_urls()

    def get_url_path(self):
        return reverse('addons.versions', args=[self.addon.slug, self.version])

    def delete(self):
        amo.log(amo.LOG.DELETE_VERSION, self.addon, str(self.version))
        super(Version, self).delete()

    @property
    def current_queue(self):
        """Return the current queue, or None if not in a queue."""
        from editors.models import (ViewPendingQueue, ViewFullReviewQueue,
                                    ViewPreliminaryQueue)

        if self.addon.status in [
                amo.STATUS_NOMINATED, amo.STATUS_LITE_AND_NOMINATED
        ]:
            return ViewFullReviewQueue
        elif self.addon.status == amo.STATUS_PUBLIC:
            return ViewPendingQueue
        elif self.addon.status in [amo.STATUS_LITE, amo.STATUS_UNREVIEWED]:
            return ViewPreliminaryQueue

        return None

    @amo.cached_property(writable=True)
    def all_activity(self):
        from devhub.models import VersionLog  # yucky
        al = (VersionLog.objects.filter(
            version=self.id).order_by('created').select_related(
                depth=1).no_cache())
        return al

    @amo.cached_property(writable=True)
    def compatible_apps(self):
        """Get a mapping of {APP: ApplicationVersion}."""
        avs = self.apps.select_related(depth=1)
        return self._compat_map(avs)

    def compatible_platforms(self):
        """Returns a dict of compatible file platforms for this version.

        The result is based on which app(s) the version targets.
        """
        apps = set([a.application.id for a in self.apps.all()])
        targets_mobile = amo.MOBILE.id in apps
        targets_other = any((a != amo.MOBILE.id) for a in apps)
        all_plats = {}
        if targets_other:
            all_plats.update(amo.DESKTOP_PLATFORMS)
        if targets_mobile:
            all_plats.update(amo.MOBILE_PLATFORMS)
        return all_plats

    @amo.cached_property(writable=True)
    def all_files(self):
        """Shortcut for list(self.files.all()).  Heavily cached."""
        return list(self.files.all())

    @amo.cached_property
    def supported_platforms(self):
        """Get a list of supported platform names."""
        return list(set(amo.PLATFORMS[f.platform_id] for f in self.all_files))

    @property
    def status(self):
        status = dict([(f.status, amo.STATUS_CHOICES[f.status])
                       for f in self.all_files])
        return status.values()

    @property
    def statuses(self):
        """Unadulterated statuses, good for an API."""
        return [(f.id, f.status) for f in self.all_files]

    def is_allowed_upload(self):
        """Check that a file can be uploaded based on the files
        per platform for that type of addon."""
        num_files = len(self.all_files)
        if self.addon.type == amo.ADDON_SEARCH:
            return num_files == 0
        elif num_files == 0:
            return True
        elif amo.PLATFORM_ALL in self.supported_platforms:
            return False
        elif amo.PLATFORM_ALL_MOBILE in self.supported_platforms:
            return False
        else:
            compatible = (v for k, v in self.compatible_platforms().items()
                          if k not in (amo.PLATFORM_ALL.id,
                                       amo.PLATFORM_ALL_MOBILE.id))
            return bool(set(compatible) - set(self.supported_platforms))

    @property
    def has_files(self):
        return bool(self.all_files)

    @property
    def is_unreviewed(self):
        return filter(lambda f: f.status in amo.UNREVIEWED_STATUSES,
                      self.all_files)

    @property
    def is_all_unreviewed(self):
        return not bool([
            f
            for f in self.all_files if f.status not in amo.UNREVIEWED_STATUSES
        ])

    @property
    def is_beta(self):
        return filter(lambda f: f.status == amo.STATUS_BETA, self.all_files)

    @property
    def is_lite(self):
        return filter(lambda f: f.status in amo.LITE_STATUSES, self.all_files)

    @property
    def is_jetpack(self):
        return all(f.jetpack_version for f in self.all_files)

    @classmethod
    def _compat_map(cls, avs):
        apps = {}
        for av in avs:
            app_id = av.application_id
            if app_id in amo.APP_IDS:
                apps[amo.APP_IDS[app_id]] = av
        return apps

    @classmethod
    def transformer(cls, versions):
        """Attach all the compatible apps and files to the versions."""
        ids = set(v.id for v in versions)
        if not versions:
            return

        avs = (ApplicationsVersions.objects.filter(
            version__in=ids).select_related(depth=1).no_cache())
        files = (File.objects.filter(
            version__in=ids).select_related('version').no_cache())

        def rollup(xs):
            groups = amo.utils.sorted_groupby(xs, 'version_id')
            return dict((k, list(vs)) for k, vs in groups)

        av_dict, file_dict = rollup(avs), rollup(files)

        for version in versions:
            v_id = version.id
            version.compatible_apps = cls._compat_map(av_dict.get(v_id, []))
            version.all_files = file_dict.get(v_id, [])

    @classmethod
    def transformer_activity(cls, versions):
        """Attach all the activity to the versions."""
        from devhub.models import VersionLog  # yucky

        ids = set(v.id for v in versions)
        if not versions:
            return

        al = (VersionLog.objects.filter(
            version__in=ids).order_by('created').select_related(
                depth=1).no_cache())

        def rollup(xs):
            groups = amo.utils.sorted_groupby(xs, 'version_id')
            return dict((k, list(vs)) for k, vs in groups)

        al_dict = rollup(al)

        for version in versions:
            v_id = version.id
            version.all_activity = al_dict.get(v_id, [])

    def disable_old_files(self):
        if not self.files.filter(status=amo.STATUS_BETA).exists():
            qs = File.objects.filter(version__addon=self.addon_id,
                                     version__lt=self,
                                     status=amo.STATUS_UNREVIEWED)
            # Use File.update so signals are triggered.
            for f in qs:
                f.update(status=amo.STATUS_DISABLED)
Esempio n. 9
0
class FancyModel(amo.models.ModelBase):
    """Mix it up with purified and linkified fields."""
    purified = PurifiedField()
    linkified = LinkifiedField()
Esempio n. 10
0
class UserProfile(amo.models.ModelBase):
    # nickname, firstname, & lastname are deprecated.
    nickname = models.CharField(max_length=255,
                                default='',
                                null=True,
                                blank=True)
    firstname = models.CharField(max_length=255, default='', blank=True)
    lastname = models.CharField(max_length=255, default='', blank=True)

    username = models.CharField(max_length=255, default='', unique=True)
    display_name = models.CharField(max_length=255,
                                    default='',
                                    null=True,
                                    blank=True)

    password = models.CharField(max_length=255, default='')
    email = models.EmailField(unique=True, null=True)

    averagerating = models.CharField(max_length=255, blank=True, null=True)
    bio = PurifiedField(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)
    emailhidden = models.BooleanField(default=False)
    homepage = models.URLField(max_length=255,
                               blank=True,
                               default='',
                               verify_exists=False)
    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)
    resetcode = models.CharField(max_length=255, default='', blank=True)
    resetcode_expires = models.DateTimeField(default=datetime.now,
                                             null=True,
                                             blank=True)
    sandboxshown = models.BooleanField(default=False)
    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)

    user = models.ForeignKey(DjangoUser, null=True, editable=False, blank=True)

    class Meta:
        db_table = 'users'

    def __unicode__(self):
        return '%s: %s' % (self.id, self.display_name or self.username)

    def get_url_path(self):
        return reverse('users.profile', args=[self.id])

    def flush_urls(self):
        urls = [
            '*/user/%d/' % self.id,
            self.picture_url,
        ]

        return urls

    @amo.cached_property
    def addons_listed(self):
        """Public add-ons this user is listed as author of."""
        return self.addons.valid().filter(addonuser__listed=True).distinct()

    @property
    def picture_dir(self):
        split_id = re.match(r'((\d*?)(\d{0,3}?))\d{1,3}$', str(self.id))
        return os.path.join(settings.USERPICS_PATH,
                            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):
        if not self.picture_type:
            return settings.MEDIA_URL + '/img/zamboni/anon_user.png'
        else:
            split_id = re.match(r'((\d*?)(\d{0,3}?))\d{1,3}$', str(self.id))
            return settings.USERPICS_URL % (
                split_id.group(2) or 0, split_id.group(1)
                or 0, self.id, int(time.mktime(self.modified.timetuple())))

    @amo.cached_property
    def is_developer(self):
        return self.addonuser_set.exists()

    @property
    def name(self):
        return self.display_name or self.username

    welcome_name = name

    @property
    def last_login(self):
        """Make UserProfile look more like auth.User."""
        # Django expects this to be non-null, so fake a login attempt.
        if not self.last_login_attempt:
            self.update(last_login_attempt=datetime.now())
        return self.last_login_attempt

    @amo.cached_property
    def reviews(self):
        """All reviews that are not dev replies."""
        return self._reviews_all.filter(reply_to=None)

    def anonymize(self):
        log.info(u"User (%s: <%s>) is being anonymized." % (self, self.email))
        self.email = None
        self.password = "******"
        self.firstname = ""
        self.lastname = ""
        self.nickname = None
        self.username = "******" % self.id  # Can't be null
        self.display_name = None
        self.homepage = ""
        self.deleted = True
        self.picture_type = ""
        self.save()

    def generate_confirmationcode(self):
        if not self.confirmationcode:
            self.confirmationcode = ''.join(
                random.sample(string.letters + string.digits, 60))
        return self.confirmationcode

    def save(self, force_insert=False, force_update=False, using=None):
        # we have to fix stupid things that we defined poorly in remora
        if not self.resetcode_expires:
            self.resetcode_expires = datetime.now()

        # TODO POSTREMORA (maintain remora's view of user names.)
        if not self.firstname or self.lastname or self.nickname:
            self.nickname = self.name

        delete_user = None
        if self.deleted and self.user:
            delete_user = self.user
            self.user = None
            # Delete user after saving this profile.

        super(UserProfile, self).save(force_insert, force_update, using)

        if self.deleted and delete_user:
            delete_user.delete()

    def check_password(self, raw_password):
        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('$')
        return hsh == get_hexdigest(algo, salt, raw_password)

    def set_password(self, raw_password, algorithm='sha512'):
        self.password = create_password(algorithm, raw_password)

    def email_confirmation_code(self):
        from 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)

    def log_login_attempt(self, request, 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()

    def create_django_user(self):
        """Make a django.contrib.auth.User for this UserProfile."""
        # Reusing the id will make our life easier, because we can use the
        # OneToOneField as pk for Profile linked back to the auth.user
        # in the future.
        self.user = DjangoUser(id=self.pk)
        self.user.first_name = ''
        self.user.last_name = ''
        self.user.username = self.email  # f
        self.user.email = self.email
        self.user.password = self.password
        self.user.date_joined = self.created

        if self.groups.filter(rules='*:*').count():
            self.user.is_superuser = self.user.is_staff = True

        self.user.save()
        self.save()
        return self.user

    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 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
Esempio n. 11
0
class Version(amo.models.OnChangeMixin, amo.models.ModelBase):
    addon = models.ForeignKey('addons.Addon', related_name='versions')
    license = models.ForeignKey('License', null=True)
    releasenotes = PurifiedField()
    approvalnotes = models.TextField(default='', null=True)
    version = models.CharField(max_length=255, default='0.1')
    version_int = models.BigIntegerField(null=True, editable=False)

    nomination = models.DateTimeField(null=True)
    reviewed = models.DateTimeField(null=True)

    has_info_request = models.BooleanField(default=False)
    has_editor_comment = models.BooleanField(default=False)

    deleted = models.BooleanField(default=False)

    supported_locales = models.CharField(max_length=255)

    _developer_name = models.CharField(max_length=255,
                                       default='',
                                       editable=False)

    source = models.FileField(upload_to=source_upload_path,
                              null=True,
                              blank=True)

    # The order of those managers is very important: please read the lengthy
    # comment above the Addon managers declaration/instanciation.
    unfiltered = VersionManager(include_deleted=True)
    objects = VersionManager()

    class Meta(amo.models.ModelBase.Meta):
        db_table = 'versions'
        ordering = ['-created', '-modified']

    def __init__(self, *args, **kwargs):
        super(Version, self).__init__(*args, **kwargs)
        self.__dict__.update(version_dict(self.version or ''))

    def __unicode__(self):
        return jinja2.escape(self.version)

    def save(self, *args, **kw):
        if not self.version_int and self.version:
            v_int = version_int(self.version)
            # Magic number warning, this is the maximum size
            # of a big int in MySQL to prevent version_int overflow, for
            # people who have rather crazy version numbers.
            # http://dev.mysql.com/doc/refman/5.5/en/numeric-types.html
            if v_int < 9223372036854775807:
                self.version_int = v_int
            else:
                log.error('No version_int written for version %s, %s' %
                          (self.pk, self.version))
        super(Version, self).save(*args, **kw)
        return self

    @classmethod
    def from_upload(cls,
                    upload,
                    addon,
                    platforms,
                    send_signal=True,
                    source=None,
                    is_beta=False):
        data = utils.parse_addon(upload, addon)
        try:
            license = addon.versions.latest().license_id
        except Version.DoesNotExist:
            license = None
        max_len = cls._meta.get_field_by_name('_developer_name')[0].max_length
        developer = data.get('developer_name', '')[:max_len]
        v = cls.objects.create(addon=addon,
                               version=data['version'],
                               license_id=license,
                               _developer_name=developer,
                               source=source)
        log.info('New version: %r (%s) from %r' % (v, v.id, upload))

        AV = ApplicationsVersions
        for app in data.get('apps', []):
            AV(version=v, min=app.min, max=app.max, application=app.id).save()
        if addon.type == amo.ADDON_SEARCH:
            # Search extensions are always for all platforms.
            platforms = [amo.PLATFORM_ALL.id]
        else:
            platforms = cls._make_safe_platform_files(platforms)

        for platform in platforms:
            File.from_upload(upload,
                             v,
                             platform,
                             parse_data=data,
                             is_beta=is_beta)

        v.disable_old_files()
        # After the upload has been copied to all platforms, remove the upload.
        storage.delete(upload.path)
        if send_signal:
            version_uploaded.send(sender=v)

        # Track the time it took from first upload through validation
        # (and whatever else) until a version was created.
        upload_start = utc_millesecs_from_epoch(upload.created)
        now = datetime.datetime.now()
        now_ts = utc_millesecs_from_epoch(now)
        upload_time = now_ts - upload_start

        log.info('Time for version {version} creation from upload: {delta}; '
                 'created={created}; now={now}'.format(delta=upload_time,
                                                       version=v,
                                                       created=upload.created,
                                                       now=now))
        statsd.timing('devhub.version_created_from_upload', upload_time)

        return v

    @classmethod
    def _make_safe_platform_files(cls, platforms):
        """Make file platform translations until all download pages
        support desktop ALL + mobile ALL. See bug 646268.

        Returns platforms ids.
        """
        pl_set = set(platforms)

        if pl_set == set((amo.PLATFORM_ALL.id, )):
            # Make it really ALL:
            return [amo.PLATFORM_ALL.id]

        has_mobile = amo.PLATFORM_ANDROID in pl_set
        has_desktop = any(p in amo.DESKTOP_PLATFORMS for p in pl_set)
        has_all = amo.PLATFORM_ALL in pl_set
        is_mixed = has_mobile and has_desktop
        if (is_mixed and has_all) or has_mobile:
            # Mixing desktop and mobile w/ ALL is not safe;
            # we have to split the files into exact platforms.
            new_plats = []
            for platform in platforms:
                if platform == amo.PLATFORM_ALL.id:
                    plats = amo.DESKTOP_PLATFORMS.keys()
                    plats.remove(amo.PLATFORM_ALL.id)
                    new_plats.extend(plats)
                else:
                    new_plats.append(platform)
            return new_plats

        # Platforms are safe as is
        return platforms

    @property
    def path_prefix(self):
        return os.path.join(user_media_path('addons'), str(self.addon_id))

    @property
    def mirror_path_prefix(self):
        return os.path.join(user_media_path('addons'), str(self.addon_id))

    def license_url(self, impala=False):
        return reverse('addons.license', args=[self.addon.slug, self.version])

    def flush_urls(self):
        return self.addon.flush_urls()

    def get_url_path(self):
        if not self.addon.is_listed:  # Not listed? Doesn't have a public page.
            return ''
        return reverse('addons.versions', args=[self.addon.slug, self.version])

    def delete(self):
        log.info(u'Version deleted: %r (%s)' % (self, self.id))
        amo.log(amo.LOG.DELETE_VERSION, self.addon, str(self.version))
        super(Version, self).delete()

    @property
    def current_queue(self):
        """Return the current queue, or None if not in a queue."""
        from editors.models import (ViewFullReviewQueue, ViewPendingQueue,
                                    ViewPreliminaryQueue,
                                    ViewUnlistedFullReviewQueue,
                                    ViewUnlistedPendingQueue,
                                    ViewUnlistedPreliminaryQueue)

        if self.addon.status in [
                amo.STATUS_NOMINATED, amo.STATUS_LITE_AND_NOMINATED
        ]:
            return (ViewFullReviewQueue
                    if self.addon.is_listed else ViewUnlistedFullReviewQueue)
        elif self.addon.status == amo.STATUS_PUBLIC:
            return (ViewPendingQueue
                    if self.addon.is_listed else ViewUnlistedPendingQueue)
        elif self.addon.status in [amo.STATUS_LITE, amo.STATUS_UNREVIEWED]:
            return (ViewPreliminaryQueue
                    if self.addon.is_listed else ViewUnlistedPreliminaryQueue)

        return None

    @amo.cached_property(writable=True)
    def all_activity(self):
        from devhub.models import VersionLog  # yucky
        al = (VersionLog.objects.filter(
            version=self.id).order_by('created').select_related(
                'activity_log', 'version').no_cache())
        return al

    @amo.cached_property(writable=True)
    def compatible_apps(self):
        """Get a mapping of {APP: ApplicationVersion}."""
        avs = self.apps.select_related('versions', 'license')
        return self._compat_map(avs)

    @amo.cached_property
    def compatible_apps_ordered(self):
        apps = self.compatible_apps.items()
        return sorted(apps, key=lambda v: v[0].short)

    def compatible_platforms(self):
        """Returns a dict of compatible file platforms for this version.

        The result is based on which app(s) the version targets.
        """
        app_ids = [a.application for a in self.apps.all()]
        targets_mobile = amo.ANDROID.id in app_ids
        targets_other = any((id_ != amo.ANDROID.id) for id_ in app_ids)
        all_plats = {}
        if targets_other:
            all_plats.update(amo.DESKTOP_PLATFORMS)
        if targets_mobile:
            all_plats.update(amo.MOBILE_PLATFORMS)
        return all_plats

    @amo.cached_property
    def is_compatible(self):
        """Returns tuple of compatibility and reasons why if not.

        Server side conditions for determining compatibility are:
            * The add-on is an extension (not a theme, app, etc.)
            * Has not opted in to strict compatibility.
            * Does not use binary_components in chrome.manifest.

        Note: The lowest maxVersion compat check needs to be checked
              separately.
        Note: This does not take into account the client conditions.
        """
        compat = True
        reasons = []
        if self.addon.type != amo.ADDON_EXTENSION:
            compat = False
            # TODO: We may want this. For now we think it may be confusing.
            # reasons.append(_('Add-on is not an extension.'))
        if self.files.filter(binary_components=True).exists():
            compat = False
            reasons.append(_('Add-on uses binary components.'))
        if self.files.filter(strict_compatibility=True).exists():
            compat = False
            reasons.append(
                _('Add-on has opted into strict compatibility '
                  'checking.'))
        return (compat, reasons)

    def is_compatible_app(self, app):
        """Returns True if the provided app passes compatibility conditions."""
        appversion = self.compatible_apps.get(app)
        if appversion and app.id in amo.D2C_MAX_VERSIONS:
            return (version_int(appversion.max.version) >= version_int(
                amo.D2C_MAX_VERSIONS.get(app.id, '*')))
        return False

    def compat_override_app_versions(self):
        """Returns the incompatible app versions range(s).

        If not ranges, returns empty list.  Otherwise, this will return all
        the app version ranges that this particular version is incompatible
        with.
        """
        from addons.models import CompatOverride
        cos = CompatOverride.objects.filter(addon=self.addon)
        if not cos:
            return []
        app_versions = []
        for co in cos:
            for range in co.collapsed_ranges():
                if (version_int(range.min) <= version_int(self.version) <=
                        version_int(range.max)):
                    app_versions.extend([(a.min, a.max) for a in range.apps])
        return app_versions

    @amo.cached_property(writable=True)
    def all_files(self):
        """Shortcut for list(self.files.all()).  Heavily cached."""
        return list(self.files.all())

    @amo.cached_property
    def supported_platforms(self):
        """Get a list of supported platform names."""
        return list(set(amo.PLATFORMS[f.platform] for f in self.all_files))

    @property
    def status(self):
        return [f.STATUS_CHOICES[f.status] for f in self.all_files]

    @property
    def statuses(self):
        """Unadulterated statuses, good for an API."""
        return [(f.id, f.status) for f in self.all_files]

    def is_allowed_upload(self):
        """
        Check that a file can be uploaded based on the files
        per platform for that type of addon.
        """
        num_files = len(self.all_files)
        if self.addon.type == amo.ADDON_SEARCH:
            return num_files == 0
        elif num_files == 0:
            return True
        elif amo.PLATFORM_ALL in self.supported_platforms:
            return False
        else:
            compatible = (v for k, v in self.compatible_platforms().items()
                          if k != amo.PLATFORM_ALL.id)
            return bool(set(compatible) - set(self.supported_platforms))

    def is_public(self):
        # To be public, a version must not be deleted, must belong to a public
        # addon, and all its attached files must have public status.
        try:
            return (not self.deleted and self.addon.is_public()
                    and all(f.status == amo.STATUS_PUBLIC
                            for f in self.all_files))
        except ObjectDoesNotExist:
            return False

    @property
    def has_files(self):
        return bool(self.all_files)

    @property
    def is_unreviewed(self):
        return filter(lambda f: f.status in amo.UNREVIEWED_STATUSES,
                      self.all_files)

    @property
    def is_all_unreviewed(self):
        return not bool([
            f
            for f in self.all_files if f.status not in amo.UNREVIEWED_STATUSES
        ])

    @property
    def is_beta(self):
        return filter(lambda f: f.status == amo.STATUS_BETA, self.all_files)

    @property
    def is_lite(self):
        return filter(lambda f: f.status in amo.LITE_STATUSES, self.all_files)

    @property
    def is_jetpack(self):
        return all(f.jetpack_version for f in self.all_files)

    @classmethod
    def _compat_map(cls, avs):
        apps = {}
        for av in avs:
            app_id = av.application
            if app_id in amo.APP_IDS:
                apps[amo.APP_IDS[app_id]] = av
        return apps

    @classmethod
    def transformer(cls, versions):
        """Attach all the compatible apps and files to the versions."""
        ids = set(v.id for v in versions)
        if not versions:
            return

        # FIXME: find out why we have no_cache() here and try to remove it.
        avs = (ApplicationsVersions.objects.filter(
            version__in=ids).select_related('application', 'apps', 'min_set',
                                            'max_set').no_cache())
        files = File.objects.filter(version__in=ids).no_cache()

        def rollup(xs):
            groups = amo.utils.sorted_groupby(xs, 'version_id')
            return dict((k, list(vs)) for k, vs in groups)

        av_dict, file_dict = rollup(avs), rollup(files)

        for version in versions:
            v_id = version.id
            version.compatible_apps = cls._compat_map(av_dict.get(v_id, []))
            version.all_files = file_dict.get(v_id, [])
            for f in version.all_files:
                f.version = version

    @classmethod
    def transformer_activity(cls, versions):
        """Attach all the activity to the versions."""
        from devhub.models import VersionLog  # yucky

        ids = set(v.id for v in versions)
        if not versions:
            return

        al = (VersionLog.objects.filter(
            version__in=ids).order_by('created').select_related(
                'activity_log', 'version').no_cache())

        def rollup(xs):
            groups = amo.utils.sorted_groupby(xs, 'version_id')
            return dict((k, list(vs)) for k, vs in groups)

        al_dict = rollup(al)

        for version in versions:
            v_id = version.id
            version.all_activity = al_dict.get(v_id, [])

    def disable_old_files(self):
        if not self.files.filter(status=amo.STATUS_BETA).exists():
            qs = File.objects.filter(
                version__addon=self.addon_id,
                version__lt=self.id,
                version__deleted=False,
                status__in=[amo.STATUS_UNREVIEWED, amo.STATUS_PENDING])
            # Use File.update so signals are triggered.
            for f in qs:
                f.update(status=amo.STATUS_DISABLED)

    @property
    def developer_name(self):
        return self._developer_name

    def reset_nomination_time(self, nomination=None):
        if not self.nomination or nomination:
            nomination = nomination or datetime.datetime.now()
            # We need signal=False not to call update_status (which calls us).
            self.update(nomination=nomination, _signal=False)
            # But we need the cache to be flushed.
            Version.objects.invalidate(self)

    @property
    def is_listed(self):
        return self.addon.is_listed

    @property
    def unreviewed_files(self):
        """A File is unreviewed if:
        - its status is in amo.UNDER_REVIEW_STATUSES or
        - its addon status is in amo.UNDER_REVIEW_STATUSES
          and its status is either in amo.UNDER_REVIEW_STATUSES or
          amo.STATUS_LITE
        """
        under_review_or_lite = amo.UNDER_REVIEW_STATUSES + (amo.STATUS_LITE, )
        return self.files.filter(
            models.Q(status__in=amo.UNDER_REVIEW_STATUSES)
            | models.Q(version__addon__status__in=amo.UNDER_REVIEW_STATUSES,
                       status__in=under_review_or_lite))
Esempio n. 12
0
class Version(amo.models.ModelBase):
    addon = models.ForeignKey('addons.Addon', related_name='versions')
    license = models.ForeignKey('License', null=True)
    releasenotes = PurifiedField()
    approvalnotes = models.TextField(default='', null=True)
    version = models.CharField(max_length=255, default='0.1')
    version_int = models.BigIntegerField(null=True, editable=False)

    nomination = models.DateTimeField(null=True)
    reviewed = models.DateTimeField(null=True)

    class Meta(amo.models.ModelBase.Meta):
        db_table = 'versions'
        ordering = ['-created', '-modified']

    def __init__(self, *args, **kwargs):
        super(Version, self).__init__(*args, **kwargs)
        self.__dict__.update(compare.version_dict(self.version or ''))

    def __unicode__(self):
        return jinja2.escape(self.version)

    def save(self, *args, **kw):
        if not self.version_int and self.version:
            version_int = compare.version_int(self.version)
            # Magic number warning, this is the maximum size
            # of a big int in MySQL to prevent version_int overflow, for
            # people who have rather crazy version numbers.
            # http://dev.mysql.com/doc/refman/5.5/en/numeric-types.html
            if version_int < 9223372036854775807:
                self.version_int = version_int
            else:
                log.error('No version_int written for version %s, %s' %
                          (self.pk, self.version))
        return super(Version, self).save(*args, **kw)

    @classmethod
    def from_upload(cls, upload, addon, platforms):
        data = utils.parse_addon(upload.path, addon)
        try:
            license = addon.versions.latest().license_id
        except Version.DoesNotExist:
            license = None
        v = cls.objects.create(addon=addon,
                               version=data['version'],
                               license_id=license)
        log.debug('New version: %r (%s) from %r' % (v, v.id, upload))
        # appversions
        AV = ApplicationsVersions
        for app in data.get('apps', []):
            AV(version=v, min=app.min, max=app.max,
               application_id=app.id).save()
        if addon.type == amo.ADDON_SEARCH:
            # Search extensions are always for all platforms.
            platforms = [Platform.objects.get(id=amo.PLATFORM_ALL.id)]
        else:
            new_plats = []
            # Transform PLATFORM_ALL_MOBILE into specific mobile platform
            # files (e.g. Android, Maemo).
            # TODO(Kumar) Stop doing this when allmobile is supported
            # for downloads. See bug 646268.
            for p in platforms:
                if p.id == amo.PLATFORM_ALL_MOBILE.id:
                    for mobi_p in (set(amo.MOBILE_PLATFORMS.keys()) -
                                   set([amo.PLATFORM_ALL_MOBILE.id])):
                        new_plats.append(Platform.objects.get(id=mobi_p))
                else:
                    new_plats.append(p)
            platforms = new_plats

        for platform in platforms:
            File.from_upload(upload, v, platform, parse_data=data)

        v.disable_old_files()
        # After the upload has been copied to all
        # platforms, remove the upload.
        upload.path.unlink()
        return v

    @property
    def path_prefix(self):
        return os.path.join(settings.ADDONS_PATH, str(self.addon_id))

    @property
    def mirror_path_prefix(self):
        return os.path.join(settings.MIRROR_STAGE_PATH, str(self.addon_id))

    def license_url(self):
        return reverse('addons.license', args=[self.addon.slug, self.version])

    def flush_urls(self):
        return self.addon.flush_urls()

    def get_url_path(self):
        return reverse('addons.versions', args=[self.addon.slug, self.version])

    def delete(self):
        amo.log(amo.LOG.DELETE_VERSION, self.addon, str(self.version))
        super(Version, self).delete()

    @amo.cached_property(writable=True)
    def compatible_apps(self):
        """Get a mapping of {APP: ApplicationVersion}."""
        avs = self.apps.select_related(depth=1)
        return self._compat_map(avs)

    def compatible_platforms(self):
        """Returns a dict of compatible file platforms for this version.

        The result is based on which app(s) the version targets.
        """
        apps = set([a.application.id for a in self.apps.all()])
        targets_mobile = amo.MOBILE.id in apps
        targets_other = any((a != amo.MOBILE.id) for a in apps)
        all_plats = {}
        if targets_other:
            all_plats.update(amo.DESKTOP_PLATFORMS)
        if targets_mobile:
            all_plats.update(amo.MOBILE_PLATFORMS)
        return all_plats

    @amo.cached_property(writable=True)
    def all_files(self):
        """Shortcut for list(self.files.all()).  Heavily cached."""
        return list(self.files.all())

    # TODO(jbalogh): Do we want names or Platforms?
    @amo.cached_property
    def supported_platforms(self):
        """Get a list of supported platform names."""
        return list(set(amo.PLATFORMS[f.platform_id] for f in self.all_files))

    def is_allowed_upload(self):
        """Check that a file can be uploaded based on the files
        per platform for that type of addon."""
        num_files = len(self.all_files)
        if self.addon.type == amo.ADDON_SEARCH:
            return num_files == 0
        elif num_files == 0:
            return True
        elif amo.PLATFORM_ALL in self.supported_platforms:
            return False
        elif amo.PLATFORM_ALL_MOBILE in self.supported_platforms:
            return False
        else:
            compatible = (v for k, v in self.compatible_platforms().items()
                          if k not in (amo.PLATFORM_ALL.id,
                                       amo.PLATFORM_ALL_MOBILE.id))
            return bool(set(compatible) - set(self.supported_platforms))

    @property
    def has_files(self):
        return bool(self.all_files)

    @property
    def is_unreviewed(self):
        return filter(lambda f: f.status in amo.UNREVIEWED_STATUSES,
                      self.all_files)

    @property
    def is_beta(self):
        return filter(lambda f: f.status == amo.STATUS_BETA, self.all_files)

    @property
    def is_lite(self):
        return filter(lambda f: f.status in amo.LITE_STATUSES, self.all_files)

    @classmethod
    def _compat_map(cls, avs):
        apps = {}
        for av in avs:
            app_id = av.application_id
            if app_id in amo.APP_IDS:
                apps[amo.APP_IDS[app_id]] = av
        return apps

    @classmethod
    def transformer(cls, versions):
        """Attach all the compatible apps and files to the versions."""
        ids = set(v.id for v in versions)
        if not versions:
            return

        avs = (ApplicationsVersions.objects.filter(
            version__in=ids).select_related(depth=1).no_cache())
        files = (File.objects.filter(
            version__in=ids).select_related('version').no_cache())

        def rollup(xs):
            groups = amo.utils.sorted_groupby(xs, 'version_id')
            return dict((k, list(vs)) for k, vs in groups)

        av_dict, file_dict = rollup(avs), rollup(files)

        for version in versions:
            v_id = version.id
            version.compatible_apps = cls._compat_map(av_dict.get(v_id, []))
            version.all_files = file_dict.get(v_id, [])

    def disable_old_files(self):
        if not self.files.filter(status=amo.STATUS_BETA).exists():
            qs = File.objects.filter(version__addon=self.addon_id,
                                     version__lt=self,
                                     status=amo.STATUS_UNREVIEWED)
            # Use File.update so signals are triggered.
            for f in qs:
                f.update(status=amo.STATUS_DISABLED)
Esempio n. 13
0
class Collection(amo.models.ModelBase):
    collection_type = models.IntegerField(choices=COLLECTION_TYPES)
    description = PurifiedField()
    name = PurifiedField()
    is_public = models.BooleanField(default=False)
    # FIXME: add better / composite indexes that matches the query we are
    # going to make.
    category = models.ForeignKey(Category, null=True, blank=True)
    region = models.PositiveIntegerField(
        default=None,
        null=True,
        blank=True,
        choices=mkt.regions.REGIONS_CHOICES_ID,
        db_index=True)
    carrier = models.IntegerField(default=None,
                                  null=True,
                                  blank=True,
                                  choices=mkt.carriers.CARRIER_CHOICES,
                                  db_index=True)
    author = models.CharField(max_length=255, default='', blank=True)
    slug = models.SlugField(blank=True,
                            max_length=30,
                            help_text='Used in collection URLs.')
    default_language = models.CharField(
        max_length=10,
        choices=((to_language(lang), desc)
                 for lang, desc in settings.LANGUAGES.items()),
        default=to_language(settings.LANGUAGE_CODE))
    background_color = ColorField(null=True)
    text_color = ColorField(null=True)

    objects = amo.models.ManagerBase()
    public = PublicCollectionsManager()

    class Meta:
        db_table = 'app_collections'
        ordering = ('-id', )  # This will change soon since we'll need to be
        # able to order collections themselves, but this
        # helps tests for now.

    def __unicode__(self):
        return self.name.localized_string_clean

    def save(self, **kw):
        self.clean_slug()
        return super(Collection, self).save(**kw)

    @use_master
    def clean_slug(self):
        clean_slug(self, 'slug')

    @classmethod
    def get_fallback(cls):
        return cls._meta.get_field('default_language')

    def apps(self):
        """
        Return a list containing all apps in this collection.
        """
        return [a.app for a in self.collectionmembership_set.all()]

    def add_app(self, app, order=None):
        """
        Add an app to this collection. If specified, the app will be created
        with the specified `order`. If not, it will be added to the end of the
        collection.
        """
        if not order:
            qs = CollectionMembership.objects.filter(collection=self)
            aggregate = qs.aggregate(models.Max('order'))['order__max']
            order = aggregate + 1 if aggregate is not None else 0
        return CollectionMembership.objects.create(collection=self,
                                                   app=app,
                                                   order=order)

    def remove_app(self, app):
        """
        Remove the passed app from this collection, returning a boolean
        indicating whether a successful deletion took place.
        """
        try:
            membership = self.collectionmembership_set.get(app=app)
        except CollectionMembership.DoesNotExist:
            return False
        else:
            membership.delete()
            return True

    def reorder(self, new_order):
        """
        Passed a list of app IDs, e.g.

        [18, 24, 9]

        will change the order of each item in the collection to match the
        passed order. A ValueError will be raised if each app in the
        collection is not included in the ditionary.
        """
        if set(a.pk for a in self.apps()) != set(new_order):
            raise ValueError('Not all apps included')
        for order, pk in enumerate(new_order):
            CollectionMembership.objects.get(collection=self,
                                             app_id=pk).update(order=order)
Esempio n. 14
0
class Version(amo.models.ModelBase):
    addon = models.ForeignKey('addons.Addon', related_name='versions')
    license = models.ForeignKey('License', null=True)
    releasenotes = PurifiedField()
    approvalnotes = models.TextField(default='', null=True)
    version = models.CharField(max_length=255, default='0.1')
    version_int = models.BigIntegerField(null=True, editable=False)

    nomination = models.DateTimeField(null=True)
    reviewed = models.DateTimeField(null=True)

    has_info_request = models.BooleanField(default=False)
    has_editor_comment = models.BooleanField(default=False)

    deleted = models.BooleanField(default=False)

    supported_locales = models.CharField(max_length=255)

    _developer_name = models.CharField(max_length=255, default='',
                                       editable=False)

    objects = VersionManager()
    with_deleted = VersionManager(include_deleted=True)

    class Meta(amo.models.ModelBase.Meta):
        db_table = 'versions'
        ordering = ['-created', '-modified']

    def __init__(self, *args, **kwargs):
        super(Version, self).__init__(*args, **kwargs)
        self.__dict__.update(version_dict(self.version or ''))

    def __unicode__(self):
        return jinja2.escape(self.version)

    def save(self, *args, **kw):
        if not self.version_int and self.version:
            v_int = version_int(self.version)
            # Magic number warning, this is the maximum size
            # of a big int in MySQL to prevent version_int overflow, for
            # people who have rather crazy version numbers.
            # http://dev.mysql.com/doc/refman/5.5/en/numeric-types.html
            if v_int < 9223372036854775807:
                self.version_int = v_int
            else:
                log.error('No version_int written for version %s, %s' %
                          (self.pk, self.version))
        creating = not self.id
        super(Version, self).save(*args, **kw)
        if creating:
            # To avoid circular import.
            from mkt.webapps.models import AppFeatures
            AppFeatures.objects.create(version=self)
        return self

    @classmethod
    def from_upload(cls, upload, addon, platforms, send_signal=True):
        data = utils.parse_addon(upload, addon)
        try:
            license = addon.versions.latest().license_id
        except Version.DoesNotExist:
            license = None
        max_len = cls._meta.get_field_by_name('_developer_name')[0].max_length
        developer = data.get('developer_name', '')[:max_len]
        v = cls.objects.create(addon=addon, version=data['version'],
                               license_id=license, _developer_name=developer)
        log.info('New version: %r (%s) from %r' % (v, v.id, upload))

        platforms = [Platform.objects.get(id=amo.PLATFORM_ALL.id)]

        # To avoid circular import.
        from mkt.webapps.models import AppManifest

        # Note: This must happen before we call `File.from_upload`.
        manifest = utils.WebAppParser().get_json_data(upload)
        AppManifest.objects.create(
            version=v, manifest=json.dumps(manifest))

        for platform in platforms:
            File.from_upload(upload, v, platform, parse_data=data)

        # Update supported locales from manifest.
        # Note: This needs to happen after we call `File.from_upload`.
        update_supported_locales_single.apply_async(
            args=[addon.id], kwargs={'latest': True},
            eta=datetime.datetime.now() +
                datetime.timedelta(seconds=settings.NFS_LAG_DELAY))

        v.disable_old_files()
        # After the upload has been copied to all platforms, remove the upload.
        storage.delete(upload.path)
        if send_signal:
            version_uploaded.send(sender=v)

        # If packaged app and app is blocked, put in escalation queue.
        if addon.is_packaged and addon.status == amo.STATUS_BLOCKED:
            # To avoid circular import.
            from editors.models import EscalationQueue
            EscalationQueue.objects.create(addon=addon)

        return v

    @property
    def path_prefix(self):
        return os.path.join(settings.ADDONS_PATH, str(self.addon_id))

    def delete(self):
        log.info(u'Version deleted: %r (%s)' % (self, self.id))
        amo.log(amo.LOG.DELETE_VERSION, self.addon, str(self.version))
        self.update(deleted=True)
        # Set file status to disabled.
        f = self.all_files[0]
        f.update(status=amo.STATUS_DISABLED, _signal=False)
        f.hide_disabled_file()

        if self.addon.is_packaged:
            # Unlink signed packages if packaged app.
            storage.delete(f.signed_file_path)
            log.info(u'Unlinked file: %s' % f.signed_file_path)
            storage.delete(f.signed_reviewer_file_path)
            log.info(u'Unlinked file: %s' % f.signed_reviewer_file_path)

    @amo.cached_property(writable=True)
    def all_activity(self):
        from devhub.models import VersionLog  # yucky
        al = (VersionLog.objects.filter(version=self.id).order_by('created')
              .select_related(depth=1).no_cache())
        return al

    @amo.cached_property(writable=True)
    def all_files(self):
        """Shortcut for list(self.files.all()).  Heavily cached."""
        return list(self.files.all())

    @amo.cached_property
    def supported_platforms(self):
        """Get a list of supported platform names."""
        return list(set(amo.PLATFORMS[f.platform_id] for f in self.all_files))

    @property
    def status(self):
        status_choices = amo.MKT_STATUS_FILE_CHOICES

        if self.deleted:
            return [status_choices[amo.STATUS_DELETED]]
        else:
            return [status_choices[f.status] for f in self.all_files]

    @property
    def statuses(self):
        """Unadulterated statuses, good for an API."""
        return [(f.id, f.status) for f in self.all_files]

    def is_public(self):
        # To be public, a version must not be deleted, must belong to a public
        # addon, and all its attached files must have public status.
        try:
            return (not self.deleted and self.addon.is_public() and
                    all(f.status == amo.STATUS_PUBLIC for f in self.all_files))
        except ObjectDoesNotExist:
            return False

    @property
    def has_files(self):
        return bool(self.all_files)

    @classmethod
    def transformer(cls, versions):
        """Attach all the files to the versions."""
        ids = set(v.id for v in versions)
        if not versions:
            return

        # FIXME: find out why we have no_cache() here and try to remove it.
        files = File.objects.filter(version__in=ids).no_cache()

        def rollup(xs):
            groups = amo.utils.sorted_groupby(xs, 'version_id')
            return dict((k, list(vs)) for k, vs in groups)

        file_dict = rollup(files)

        for version in versions:
            v_id = version.id
            version.all_files = file_dict.get(v_id, [])
            for f in version.all_files:
                f.version = version

    @classmethod
    def transformer_activity(cls, versions):
        """Attach all the activity to the versions."""
        from devhub.models import VersionLog  # yucky

        ids = set(v.id for v in versions)
        if not versions:
            return

        al = (VersionLog.objects.filter(version__in=ids).order_by('created')
              .select_related(depth=1).no_cache())

        def rollup(xs):
            groups = amo.utils.sorted_groupby(xs, 'version_id')
            return dict((k, list(vs)) for k, vs in groups)

        al_dict = rollup(al)

        for version in versions:
            v_id = version.id
            version.all_activity = al_dict.get(v_id, [])

    def disable_old_files(self):
        if not self.files.filter(status=amo.STATUS_BETA).exists():
            qs = File.objects.filter(version__addon=self.addon_id,
                                     version__lt=self.id,
                                     version__deleted=False,
                                     status__in=[amo.STATUS_UNREVIEWED,
                                                 amo.STATUS_PENDING])
            # Use File.update so signals are triggered.
            for f in qs:
                f.update(status=amo.STATUS_DISABLED)

    @property
    def developer_name(self):
        return self._developer_name

    @amo.cached_property(writable=True)
    def is_privileged(self):
        """
        Return whether the corresponding addon is privileged by looking at
        the manifest file.

        This is a cached property, to avoid going in the manifest more than
        once for a given instance. It's also directly writable do allow you to
        bypass the manifest fetching if you *know* your app is privileged or
        not already and want to pass the instance to some code that will use
        that property.
        """
        if not self.addon.is_packaged or not self.all_files:
            return False
        data = self.addon.get_manifest_json(file_obj=self.all_files[0])
        return data.get('type') == 'privileged'

    @amo.cached_property
    def manifest(self):
        # To avoid circular import.
        from mkt.webapps.models import AppManifest

        try:
            manifest = self.manifest_json.manifest
        except AppManifest.DoesNotExist:
            manifest = None

        return json.loads(manifest) if manifest else {}