예제 #1
0
class PriceCurrency(amo.models.ModelBase):
    # The carrier for this currency.
    carrier = models.IntegerField(choices=CARRIER_CHOICES, blank=True,
                                  null=True)
    currency = models.CharField(max_length=10,
                                choices=do_dictsort(amo.OTHER_CURRENCIES))
    price = models.DecimalField(max_digits=10, decimal_places=2)

    # The payments provider for this tier.
    provider = models.IntegerField(choices=PROVIDER_CHOICES, blank=True,
                                   null=True)

    # The payment methods allowed for this tier.
    method = models.IntegerField(choices=PAYMENT_METHOD_CHOICES,
                                 default=PAYMENT_METHOD_ALL)

    # These are the regions as defined in mkt/constants/regions.
    region = models.IntegerField(default=1)  # Default to worldwide.
    tier = models.ForeignKey(Price)

    class Meta:
        db_table = 'price_currency'
        verbose_name = 'Price currencies'
        unique_together = ('tier', 'currency', 'carrier', 'region',
                           'provider')

    def __unicode__(self):
        return u'%s, %s: %s' % (self.tier, self.currency, self.price)
예제 #2
0
파일: forms.py 프로젝트: waseem18/zamboni
class AppFormDetails(AddonFormBase):
    LOCALES = [(translation.to_locale(k).replace('_', '-'), v)
               for k, v in do_dictsort(settings.LANGUAGES)]

    default_locale = forms.TypedChoiceField(required=False, choices=LOCALES)
    homepage = TransField.adapt(forms.URLField)(required=False)
    privacy_policy = TransField(
        widget=TransTextarea(), required=True,
        label=_lazy(u"Please specify your app's Privacy Policy"))

    class Meta:
        model = Webapp
        fields = ('default_locale', 'homepage', 'privacy_policy')

    def clean(self):
        # Make sure we have the required translations in the new locale.
        required = ['name', 'description']
        data = self.cleaned_data
        if not self.errors and 'default_locale' in self.changed_data:
            fields = dict((k, getattr(self.instance, k + '_id'))
                          for k in required)
            locale = data['default_locale']
            ids = filter(None, fields.values())
            qs = (Translation.objects.filter(locale=locale, id__in=ids,
                                             localized_string__isnull=False)
                  .values_list('id', flat=True))
            missing = [k for k, v in fields.items() if v not in qs]
            if missing:
                raise forms.ValidationError(
                    _('Before changing your default locale you must have a '
                      'name and description in that locale. '
                      'You are missing %s.') % ', '.join(map(repr, missing)))
        return data
예제 #3
0
class Refund(ModelBase):
    # This refers to the original object with `type=mkt.CONTRIB_PURCHASE`.
    contribution = models.OneToOneField(Contribution)

    # Pending => 0
    # Approved => 1
    # Instantly Approved => 2
    # Declined => 3
    # Failed => 4
    status = models.PositiveIntegerField(default=mkt.REFUND_PENDING,
                                         choices=do_dictsort(
                                             mkt.REFUND_STATUSES),
                                         db_index=True)

    refund_reason = models.TextField(default='', blank=True)
    rejection_reason = models.TextField(default='', blank=True)

    # Date `created` should always be date `requested` for pending refunds,
    # but let's just stay on the safe side. We might change our minds.
    requested = models.DateTimeField(null=True, db_index=True)
    approved = models.DateTimeField(null=True, db_index=True)
    declined = models.DateTimeField(null=True, db_index=True)
    user = models.ForeignKey('users.UserProfile')

    objects = RefundManager()

    class Meta:
        db_table = 'refunds'

    def __unicode__(self):
        return u'%s (%s)' % (self.contribution, self.get_status_display())
예제 #4
0
class PriceCurrency(amo.models.ModelBase):
    currency = models.CharField(max_length=10,
                                choices=do_dictsort(amo.OTHER_CURRENCIES))
    price = models.DecimalField(max_digits=5, decimal_places=2)
    tier = models.ForeignKey(Price)

    class Meta:
        db_table = 'price_currency'
        verbose_name = 'Price currencies'

    def __unicode__(self):
        return u'%s, %s: %s' % (self.tier, self.currency, self.price)
예제 #5
0
class AddonPurchase(amo.models.ModelBase):
    addon = models.ForeignKey('addons.Addon')
    user = models.ForeignKey(UserProfile)
    type = models.PositiveIntegerField(default=amo.CONTRIB_PURCHASE,
                                       choices=do_dictsort(amo.CONTRIB_TYPES),
                                       db_index=True)

    class Meta:
        db_table = 'addon_purchase'
        unique_together = ('addon', 'user')

    def __unicode__(self):
        return u'%s: %s' % (self.addon, self.user)
예제 #6
0
class AddonPurchase(ModelBase):
    addon = models.ForeignKey('webapps.Webapp')
    type = models.PositiveIntegerField(default=mkt.CONTRIB_PURCHASE,
                                       choices=do_dictsort(mkt.CONTRIB_TYPES),
                                       db_index=True)
    user = models.ForeignKey(UserProfile)
    uuid = models.CharField(max_length=255, db_index=True, unique=True)

    class Meta:
        db_table = 'addon_purchase'
        unique_together = ('addon', 'user')

    def __unicode__(self):
        return u'%s: %s' % (self.addon, self.user)
예제 #7
0
class Category(ModelBase):
    name = TranslatedField()
    slug = models.SlugField(max_length=50, help_text='Used in Category URLs.')
    type = models.PositiveIntegerField(db_column='addontype_id',
                                       choices=do_dictsort(base.ADDON_TYPE))
    application = models.ForeignKey('applications.Application', null=True,
                                    blank=True)
    count = models.IntegerField('Addon count', default=0)
    weight = models.IntegerField(default=0,
        help_text='Category weight used in sort ordering')
    misc = models.BooleanField(default=False)

    addons = models.ManyToManyField(AddonBase, through='AddonCategoryBase')

    class Meta:
        db_table = 'categories'
        verbose_name_plural = 'Categories'
        app_label = 'addons'

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

    def flush_urls(self):
        urls = ['*%s' % self.get_url_path(), ]
        return urls

    def get_url_path(self):
        try:
            type = base.ADDON_SLUGS[self.type]
        except KeyError:
            type = base.ADDON_SLUGS[base.ADDON_EXTENSION]
        if settings.MARKETPLACE and self.type == base.ADDON_PERSONA:
            #TODO: (davor) this is a temp stub. Return category URL when done.
            return urlresolvers.reverse('themes.browse', args=[self.slug])
        return urlresolvers.reverse('browse.%s' % type, args=[self.slug])

    @staticmethod
    def transformer(addons):
        qs = (Category.uncached.filter(addons__in=addons)
              .extra(select={'addon_id': 'addons_categories.addon_id'}))
        cats = dict((addon_id, list(cs))
                    for addon_id, cs in sorted_groupby(qs, 'addon_id'))
        for addon in addons:
            addon.all_categories = cats.get(addon.id, [])
예제 #8
0
class PriceCurrency(amo.models.ModelBase):
    currency = models.CharField(max_length=10,
                                choices=do_dictsort(amo.OTHER_CURRENCIES))
    price = models.DecimalField(max_digits=10, decimal_places=2)
    tier = models.ForeignKey(Price)

    class Meta:
        db_table = 'price_currency'
        verbose_name = 'Price currencies'
        unique_together = ('tier', 'currency')

    def get_price_locale(self):
        """Return the price as a nicely localised string for the locale."""
        lang = translation.get_language()
        locale = get_locale_from_lang(lang)
        return numbers.format_currency(self.price, self.currency,
                                       locale=locale)

    def __unicode__(self):
        return u'%s, %s: %s' % (self.tier, self.currency, self.price)
예제 #9
0
class Contribution(amo.models.ModelBase):
    # TODO(addon): figure out what to do when we delete the add-on.
    addon = models.ForeignKey('addons.Addon')
    amount = models.DecimalField(max_digits=9, decimal_places=2, null=True)
    currency = models.CharField(max_length=3,
                                choices=do_dictsort(amo.PAYPAL_CURRENCIES),
                                default=amo.CURRENCY_DEFAULT)
    source = models.CharField(max_length=255, null=True)
    client_data = models.ForeignKey('stats.ClientData', null=True)
    source_locale = models.CharField(max_length=10, null=True)
    # This is the external id that you can communicate to the world.
    uuid = models.CharField(max_length=255, null=True, db_index=True)
    comment = models.CharField(max_length=255)
    # This is the internal transaction id between us and a provider,
    # for example paypal or solitude.
    transaction_id = models.CharField(max_length=255, null=True, db_index=True)
    paykey = models.CharField(max_length=255, null=True)
    post_data = StatsDictField(null=True)

    # Voluntary Contribution specific.
    charity = models.ForeignKey('addons.Charity', null=True)
    annoying = models.PositiveIntegerField(
        default=0,
        choices=amo.CONTRIB_CHOICES,
    )
    is_suggested = models.BooleanField(default=False)
    suggested_amount = models.DecimalField(max_digits=9,
                                           decimal_places=2,
                                           null=True)

    class Meta:
        db_table = 'stats_contributions'

    def __unicode__(self):
        return u'%s: %s' % (self.addon.name, self.amount)

    @property
    def date(self):
        try:
            return datetime.date(self.created.year, self.created.month,
                                 self.created.day)
        except AttributeError:
            # created may be None
            return None

    @property
    def contributor(self):
        try:
            return u'%s %s' % (self.post_data['first_name'],
                               self.post_data['last_name'])
        except (TypeError, KeyError):
            # post_data may be None or missing a key
            return None

    @property
    def email(self):
        try:
            return self.post_data['payer_email']
        except (TypeError, KeyError):
            # post_data may be None or missing a key
            return None

    def _switch_locale(self):
        if self.source_locale:
            lang = self.source_locale
        else:
            lang = self.addon.default_locale
        tower.activate(lang)
        return Locale(translation.to_locale(lang))

    def mail_thankyou(self, request=None):
        """
        Mail a thankyou note for a completed contribution.

        Raises a ``ContributionError`` exception when the contribution
        is not complete or email addresses are not found.
        """
        locale = self._switch_locale()

        # Thankyous must be enabled.
        if not self.addon.enable_thankyou:
            # Not an error condition, just return.
            return

        # Contribution must be complete.
        if not self.transaction_id:
            raise ContributionError('Transaction not complete')

        # Send from support_email, developer's email, or default.
        from_email = settings.DEFAULT_FROM_EMAIL
        if self.addon.support_email:
            from_email = str(self.addon.support_email)

        # We need the contributor's email.
        to_email = self.post_data['payer_email']
        if not to_email:
            raise ContributionError('Empty payer email')

        # Make sure the url uses the right language.
        # Setting a prefixer would be nicer, but that requires a request.
        url_parts = self.addon.meet_the_dev_url().split('/')
        url_parts[1] = locale.language

        subject = _('Thanks for contributing to {addon_name}').format(
            addon_name=self.addon.name)

        # Send the email.
        send_mail_jinja(
            subject,
            'stats/contribution-thankyou-email.ltxt', {
                'thankyou_note':
                bleach.clean(unicode(self.addon.thankyou_note), strip=True),
                'addon_name':
                self.addon.name,
                'learn_url':
                '%s%s?src=emailinfo' %
                (settings.SITE_URL, '/'.join(url_parts)),
                'domain':
                settings.DOMAIN
            },
            from_email, [to_email],
            fail_silently=True,
            perm_setting='dev_thanks')

    @staticmethod
    def post_save(sender, instance, **kwargs):
        from . import tasks
        tasks.addon_total_contributions.delay(instance.addon_id)

    def get_amount_locale(self, locale=None):
        """Localise the amount paid into the current locale."""
        if not locale:
            lang = translation.get_language()
            locale = get_locale_from_lang(lang)
        return numbers.format_currency(self.amount or 0,
                                       self.currency or 'USD',
                                       locale=locale)
예제 #10
0
class Contribution(amo.models.ModelBase):
    # TODO(addon): figure out what to do when we delete the add-on.
    addon = models.ForeignKey('addons.Addon')
    amount = models.DecimalField(max_digits=9, decimal_places=2, null=True)
    currency = models.CharField(max_length=3,
                                choices=do_dictsort(amo.PAYPAL_CURRENCIES),
                                default=amo.CURRENCY_DEFAULT)
    source = models.CharField(max_length=255, null=True)
    source_locale = models.CharField(max_length=10, null=True)
    # This is the external id that you can communicate to the world.
    uuid = models.CharField(max_length=255, null=True, db_index=True)
    comment = models.CharField(max_length=255)
    # This is the internal transaction id between us and a provider,
    # for example paypal or solitude.
    transaction_id = models.CharField(max_length=255, null=True, db_index=True)
    paykey = models.CharField(max_length=255, null=True)
    post_data = StatsDictField(null=True)

    # Voluntary Contribution specific.
    charity = models.ForeignKey('addons.Charity', null=True)
    annoying = models.PositiveIntegerField(
        default=0,
        choices=amo.CONTRIB_CHOICES,
    )
    is_suggested = models.BooleanField(default=False)
    suggested_amount = models.DecimalField(max_digits=9,
                                           decimal_places=2,
                                           null=True)

    class Meta:
        db_table = 'stats_contributions'

    def __unicode__(self):
        return u'%s: %s' % (self.addon.name, self.amount)

    @property
    def date(self):
        try:
            return datetime.date(self.created.year, self.created.month,
                                 self.created.day)
        except AttributeError:
            # created may be None
            return None

    @property
    def contributor(self):
        try:
            return u'%s %s' % (self.post_data['first_name'],
                               self.post_data['last_name'])
        except (TypeError, KeyError):
            # post_data may be None or missing a key
            return None

    @property
    def email(self):
        try:
            return self.post_data['payer_email']
        except (TypeError, KeyError):
            # post_data may be None or missing a key
            return None

    def _switch_locale(self):
        if self.source_locale:
            lang = self.source_locale
        else:
            lang = self.addon.default_locale
        activate(lang)
        return Locale(to_locale(lang))

    def get_amount_locale(self, locale=None):
        """Localise the amount paid into the current locale."""
        if not locale:
            lang = get_language()
            locale = get_locale_from_lang(lang)
        return numbers.format_currency(self.amount or 0,
                                       self.currency or 'USD',
                                       locale=locale)
예제 #11
0
파일: models.py 프로젝트: tmp0230/zamboni
class Contribution(caching.base.CachingMixin, models.Model):
    # TODO(addon): figure out what to do when we delete the add-on.
    addon = models.ForeignKey('addons.Addon')
    amount = DecimalCharField(max_digits=9, decimal_places=2,
                              nullify_invalid=True, null=True)
    currency = models.CharField(max_length=3,
                                choices=do_dictsort(amo.PAYPAL_CURRENCIES),
                                default=amo.CURRENCY_DEFAULT)
    source = models.CharField(max_length=255, null=True)
    source_locale = models.CharField(max_length=10, null=True)

    created = models.DateTimeField(auto_now_add=True)
    uuid = models.CharField(max_length=255, null=True)
    comment = models.CharField(max_length=255)
    transaction_id = models.CharField(max_length=255, null=True)
    paykey = models.CharField(max_length=255, null=True)
    post_data = StatsDictField(null=True)

    # Voluntary Contribution specific.
    charity = models.ForeignKey('addons.Charity', null=True)
    annoying = models.PositiveIntegerField(default=0,
                                           choices=amo.CONTRIB_CHOICES,)
    is_suggested = models.BooleanField()
    suggested_amount = DecimalCharField(max_digits=254, decimal_places=2,
                                        nullify_invalid=True, null=True)

    # Marketplace specific.
    # TODO(andym): figure out what to do when we delete the user.
    user = models.ForeignKey('users.UserProfile', blank=True, null=True)
    type = models.PositiveIntegerField(default=amo.CONTRIB_TYPE_DEFAULT,
                                       choices=do_dictsort(amo.CONTRIB_TYPES))
    price_tier = models.ForeignKey('market.Price', blank=True, null=True,
                                   on_delete=models.PROTECT)
    # If this is a refund or a chargeback, which charge did it relate to.
    related = models.ForeignKey('self', blank=True, null=True,
                                on_delete=models.PROTECT)

    objects = models.Manager()
    stats = StatsManager('created')

    class Meta:
        db_table = 'stats_contributions'

    def __unicode__(self):
        return u'%s: %s' % (self.addon.name, self.amount)

    def flush_urls(self):
        return ['*/addon/%d/statistics/contributions*' % self.addon_id, ]

    @property
    def date(self):
        try:
            return datetime.date(self.created.year,
                                 self.created.month, self.created.day)
        except AttributeError:
            # created may be None
            return None

    @property
    def contributor(self):
        try:
            return u'%s %s' % (self.post_data['first_name'],
                               self.post_data['last_name'])
        except (TypeError, KeyError):
            # post_data may be None or missing a key
            return None

    @property
    def email(self):
        try:
            return self.post_data['payer_email']
        except (TypeError, KeyError):
            # post_data may be None or missing a key
            return None

    def mail_thankyou(self, request=None):
        """
        Mail a thankyou note for a completed contribution.

        Raises a ``ContributionError`` exception when the contribution
        is not complete or email addresses are not found.
        """

        # Setup l10n before loading addon.
        if self.source_locale:
            lang = self.source_locale
        else:
            lang = self.addon.default_locale
        tower.activate(lang)

        # Thankyous must be enabled.
        if not self.addon.enable_thankyou:
            # Not an error condition, just return.
            return

        # Contribution must be complete.
        if not self.transaction_id:
            raise ContributionError('Transaction not complete')

        # Send from support_email, developer's email, or default.
        from_email = settings.DEFAULT_FROM_EMAIL
        if self.addon.support_email:
            from_email = str(self.addon.support_email)
        else:
            try:
                author = self.addon.listed_authors[0]
                if author.email and not author.emailhidden:
                    from_email = author.email
            except (IndexError, TypeError):
                # This shouldn't happen, but the default set above is still ok.
                pass

        # We need the contributor's email.
        to_email = self.post_data['payer_email']
        if not to_email:
            raise ContributionError('Empty payer email')

        # Make sure the url uses the right language.
        # Setting a prefixer would be nicer, but that requires a request.
        url_parts = self.addon.meet_the_dev_url().split('/')
        url_parts[1] = lang

        # Buildup the email components.
        t = loader.get_template('stats/contribution-thankyou-email.ltxt')
        c = {
            'thankyou_note': self.addon.thankyou_note,
            'addon_name': self.addon.name,
            'learn_url': '%s%s?src=emailinfo' % (settings.SITE_URL,
                                                 '/'.join(url_parts)),
            'domain': settings.DOMAIN,
        }
        body = t.render(Context(c))
        subject = _('Thanks for contributing to {addon_name}').format(
                    addon_name=self.addon.name)

        # Send the email
        if send_mail(subject, body, from_email, [to_email],
                     fail_silently=True, perm_setting='dev_thanks'):
            # Clear out contributor identifying information.
            del(self.post_data['payer_email'])
            self.save()

    @staticmethod
    def post_save(sender, instance, **kwargs):
        from . import tasks
        tasks.addon_total_contributions.delay(instance.addon_id)

    def get_amount_locale(self, locale=None):
        """Localise the amount paid into the current locale."""
        if not locale:
            lang = translation.get_language()
            locale = Locale(translation.to_locale(lang))
        return numbers.format_currency(self.amount or 0,
                                       self.currency or 'USD',
                                       locale=locale)
예제 #12
0
class Contribution(amo.models.ModelBase):
    # TODO(addon): figure out what to do when we delete the add-on.
    addon = models.ForeignKey('addons.Addon')
    amount = DecimalCharField(max_digits=9, decimal_places=2,
                              nullify_invalid=True, null=True)
    currency = models.CharField(max_length=3,
                                choices=do_dictsort(amo.PAYPAL_CURRENCIES),
                                default=amo.CURRENCY_DEFAULT)
    source = models.CharField(max_length=255, null=True)
    client_data = models.ForeignKey('stats.ClientData', null=True)
    source_locale = models.CharField(max_length=10, null=True)
    # This is the external id that you can communicate to the world.
    uuid = models.CharField(max_length=255, null=True, db_index=True)
    comment = models.CharField(max_length=255)
    # This is the internal transaction id between us and a provider,
    # for example paypal or solitude.
    transaction_id = models.CharField(max_length=255, null=True, db_index=True)
    paykey = models.CharField(max_length=255, null=True)
    post_data = StatsDictField(null=True)

    # Voluntary Contribution specific.
    charity = models.ForeignKey('addons.Charity', null=True)
    annoying = models.PositiveIntegerField(default=0,
                                           choices=amo.CONTRIB_CHOICES,)
    is_suggested = models.BooleanField(default=False)
    suggested_amount = DecimalCharField(max_digits=254, decimal_places=2,
                                        nullify_invalid=True, null=True)

    # Marketplace specific.
    # TODO(andym): figure out what to do when we delete the user.
    user = models.ForeignKey('users.UserProfile', blank=True, null=True)
    type = models.PositiveIntegerField(default=amo.CONTRIB_TYPE_DEFAULT,
                                       choices=do_dictsort(amo.CONTRIB_TYPES))
    price_tier = models.ForeignKey('market.Price', blank=True, null=True,
                                   on_delete=models.PROTECT)
    # If this is a refund or a chargeback, which charge did it relate to.
    related = models.ForeignKey('self', blank=True, null=True,
                                on_delete=models.PROTECT)

    class Meta:
        db_table = 'stats_contributions'

    def __unicode__(self):
        return u'%s: %s' % (self.addon.name, self.amount)

    @property
    def date(self):
        try:
            return datetime.date(self.created.year,
                                 self.created.month, self.created.day)
        except AttributeError:
            # created may be None
            return None

    @property
    def contributor(self):
        try:
            return u'%s %s' % (self.post_data['first_name'],
                               self.post_data['last_name'])
        except (TypeError, KeyError):
            # post_data may be None or missing a key
            return None

    @property
    def email(self):
        try:
            return self.post_data['payer_email']
        except (TypeError, KeyError):
            # post_data may be None or missing a key
            return None

    def handle_chargeback(self, reason):
        """
        Hook to handle a payment chargeback.

        When a chargeback is received from a PayPal IPN
        for this contribution, the hook is called.

        reason is either 'reversal' or 'refund'
        """
        # Sigh. AMO does not have inapp_pay installed and it does not have
        # the database tables. Since both mkt and AMO share this code we
        # need to hide it from AMO.
        if ('mkt.inapp_pay' in settings.INSTALLED_APPS
            and self.inapp_payment.count()):
            self.inapp_payment.get().handle_chargeback(reason)

    def _switch_locale(self):
        if self.source_locale:
            lang = self.source_locale
        else:
            lang = self.addon.default_locale
        tower.activate(lang)
        return Locale(translation.to_locale(lang))

    def _mail(self, template, subject, context):
        template = env.get_template(template)
        body = template.render(context)
        send_mail(subject, body, settings.MARKETPLACE_EMAIL,
                  [self.user.email], fail_silently=True)

    def mail_chargeback(self):
        """Send to the purchaser of an add-on about reversal from Paypal."""
        locale = self._switch_locale()
        amt = numbers.format_currency(abs(self.amount), self.currency,
                                      locale=locale)
        self._mail('users/support/emails/chargeback.txt',
                   # L10n: the adddon name.
                   _(u'%s payment reversal' % self.addon.name),
                   {'name': self.addon.name, 'amount': amt})

    def record_failed_refund(self, e, user):
        self.enqueue_refund(amo.REFUND_FAILED, user,
                            rejection_reason=str(e))
        self._switch_locale()
        self._mail('users/support/emails/refund-failed.txt',
                   # L10n: the addon name.
                   _(u'%s refund failed' % self.addon.name),
                   {'name': self.addon.name})
        send_mail_jinja(
            'Refund failed', 'devhub/email/refund-failed.txt',
            {'name': self.user.email,
             'error': str(e)},
            settings.MARKETPLACE_EMAIL,
            [str(self.addon.support_email)], fail_silently=True)

    def mail_approved(self):
        """The developer has approved a refund."""
        locale = self._switch_locale()
        amt = numbers.format_currency(abs(self.amount), self.currency,
                                      locale=locale)
        self._mail('users/support/emails/refund-approved.txt',
                   # L10n: the adddon name.
                   _(u'%s refund approved' % self.addon.name),
                   {'name': self.addon.name, 'amount': amt})

    def mail_declined(self):
        """The developer has declined a refund."""
        self._switch_locale()
        self._mail('users/support/emails/refund-declined.txt',
                   # L10n: the adddon name.
                   _(u'%s refund declined' % self.addon.name),
                   {'name': self.addon.name})

    def mail_thankyou(self, request=None):
        """
        Mail a thankyou note for a completed contribution.

        Raises a ``ContributionError`` exception when the contribution
        is not complete or email addresses are not found.
        """
        locale = self._switch_locale()

        # Thankyous must be enabled.
        if not self.addon.enable_thankyou:
            # Not an error condition, just return.
            return

        # Contribution must be complete.
        if not self.transaction_id:
            raise ContributionError('Transaction not complete')

        # Send from support_email, developer's email, or default.
        from_email = settings.DEFAULT_FROM_EMAIL
        if self.addon.support_email:
            from_email = str(self.addon.support_email)
        else:
            try:
                author = self.addon.listed_authors[0]
                if author.email and not author.emailhidden:
                    from_email = author.email
            except (IndexError, TypeError):
                # This shouldn't happen, but the default set above is still ok.
                pass

        # We need the contributor's email.
        to_email = self.post_data['payer_email']
        if not to_email:
            raise ContributionError('Empty payer email')

        # Make sure the url uses the right language.
        # Setting a prefixer would be nicer, but that requires a request.
        url_parts = self.addon.meet_the_dev_url().split('/')
        url_parts[1] = locale.language

        subject = _('Thanks for contributing to {addon_name}').format(
                    addon_name=self.addon.name)

        # Send the email.
        send_mail_jinja(
            subject, 'stats/contribution-thankyou-email.ltxt',
            {'thankyou_note': bleach.clean(unicode(self.addon.thankyou_note),
                                           strip=True),
             'addon_name': self.addon.name,
             'learn_url': '%s%s?src=emailinfo' % (settings.SITE_URL,
                                                 '/'.join(url_parts)),
             'domain': settings.DOMAIN},
            from_email, [to_email], fail_silently=True,
            perm_setting='dev_thanks')

    def enqueue_refund(self, status, user, refund_reason=None,
                       rejection_reason=None):
        """Keep track of a contribution's refund status."""
        from market.models import Refund
        refund, c = Refund.objects.safer_get_or_create(contribution=self,
                                                       user=user)
        refund.status = status

        # Determine which timestamps to update.
        timestamps = []
        if status in (amo.REFUND_PENDING, amo.REFUND_APPROVED_INSTANT,
                      amo.REFUND_FAILED):
            timestamps.append('requested')
        if status in (amo.REFUND_APPROVED, amo.REFUND_APPROVED_INSTANT):
            timestamps.append('approved')
        elif status == amo.REFUND_DECLINED:
            timestamps.append('declined')
        for ts in timestamps:
            setattr(refund, ts, datetime.datetime.now())

        if refund_reason:
            refund.refund_reason = refund_reason
        if rejection_reason:
            refund.rejection_reason = rejection_reason
        refund.save()
        return refund

    @staticmethod
    def post_save(sender, instance, **kwargs):
        from . import tasks
        tasks.addon_total_contributions.delay(instance.addon_id)

    def get_amount_locale(self, locale=None):
        """Localise the amount paid into the current locale."""
        if not locale:
            lang = translation.get_language()
            locale = get_locale_from_lang(lang)
        return numbers.format_currency(self.amount or 0,
                                       self.currency or 'USD',
                                       locale=locale)

    def get_refund_url(self):
        return urlparams(self.addon.get_dev_url('issue_refund'),
                         transaction_id=self.transaction_id)

    def get_absolute_refund_url(self):
        return absolutify(self.get_refund_url())

    def is_instant_refund(self, period=settings.PAYPAL_REFUND_INSTANT):
        if self.type != amo.CONTRIB_PURCHASE:
            return False
        limit = datetime.timedelta(seconds=period)
        return datetime.datetime.now() < (self.created + limit)

    def get_refund_contribs(self):
        """Get related set of refund contributions."""
        return Contribution.objects.filter(
            related=self, type=amo.CONTRIB_REFUND).order_by('-modified')

    def is_refunded(self):
        """
        If related has been set, then this transaction has been refunded or
        charged back. This is a bit expensive, so refrain from using on listing
        pages.
        """
        return (Contribution.objects.filter(related=self,
                                            type__in=[amo.CONTRIB_REFUND,
                                                      amo.CONTRIB_CHARGEBACK])
                                    .exists())
예제 #13
0
from django.conf import settings
from django.utils.translation import trans_real

from jinja2.filters import do_dictsort

LOCALES = [(trans_real.to_locale(k).replace('_', '-'), v)
           for k, v in do_dictsort(settings.LANGUAGES)]
예제 #14
0
class InAppProductSerializer(serializers.ModelSerializer):
    _locales = [(translation.to_locale(k).replace('_', '-').lower(), v)
                for k, v in do_dictsort(settings.LANGUAGES)]

    app = serializers.SlugRelatedField(read_only=True, slug_field='app_slug',
                                       source='webapp')
    guid = serializers.CharField(read_only=True)
    include_inactive = serializers.BooleanField(read_only=True)
    logo_url = serializers.CharField(
        validators=[URLValidator(schemes=['http', 'https'])],
        required=False)
    name = NameField()
    default_locale = serializers.ChoiceField(choices=_locales)
    price_id = serializers.PrimaryKeyRelatedField(source='price',
                                                  queryset=Price.objects)

    class Meta:
        model = InAppProduct
        fields = ['active', 'guid', 'app', 'price_id', 'name',
                  'default_locale', 'logo_url', 'include_inactive']

    def validate(self, attrs):
        default_name = attrs['name'].get(attrs['default_locale'], None)
        if ((attrs['default_locale'] not in attrs['name']) or
                not default_name):
            raise ValidationError(
                'no localization for default_locale {d} in "name"'
                .format(d=repr(attrs['default_locale'])))
        return attrs

    def validate_logo_url(self, logo_url):

        # This message is shown for all image errors even though it may
        # not be correct. This is to prevent leaking info that could
        # lead to port scanning, DOS'ing or other vulnerabilities.
        msg = _('Product logo must be a 64x64 image. '
                'Check that the URL is correct.')
        tmp_dest = StringIO()
        try:
            res = requests.get(
                logo_url, timeout=3,
                headers={'User-Agent': settings.MARKETPLACE_USER_AGENT})
            res.raise_for_status()
            payload = 0
            read_size = 100000
            for chunk in res.iter_content(read_size):
                payload += len(chunk)
                if payload > settings.MAX_INAPP_IMAGE_SIZE:
                    log.info('clean_logo_url: payload exceeded allowed '
                             'size: {url}: '.format(url=logo_url))
                    raise ValidationError(msg)
                tmp_dest.write(chunk)
        except ValidationError:
            raise
        except Exception, exc:
            log.info('clean_logo_url: exception fetching {url}: '
                     '{exc.__class__.__name__}: {exc}'
                     .format(url=logo_url, exc=exc))
            raise ValidationError(msg)

        tmp_dest.seek(0)
        try:
            img = Image.open(tmp_dest)
            img.verify()
        except Exception, exc:
            log.info('clean_logo_url: Error loading/verifying {url}: '
                     '{exc.__class__.__name__}: {exc}'
                     .format(url=logo_url, exc=exc))
            raise ValidationError(msg)
예제 #15
0
class Contribution(ModelBase):
    addon = models.ForeignKey('webapps.Webapp', blank=True, null=True)
    # For in-app purchases this links to the product.
    inapp_product = models.ForeignKey('inapp.InAppProduct',
                                      blank=True, null=True)
    amount = models.DecimalField(max_digits=9, decimal_places=2, blank=True,
                                 null=True)
    currency = models.CharField(max_length=3,
                                choices=do_dictsort(mkt.PAYPAL_CURRENCIES),
                                default=mkt.CURRENCY_DEFAULT)
    source = models.CharField(max_length=255, null=True)
    source_locale = models.CharField(max_length=10, null=True)
    # This is the external id that you can communicate to the world.
    uuid = models.CharField(max_length=255, null=True, db_index=True)
    comment = models.CharField(max_length=255)
    # This is the internal transaction id between us and a provider,
    # for example paypal or solitude.
    transaction_id = models.CharField(max_length=255, null=True, db_index=True)
    paykey = models.CharField(max_length=255, null=True)

    # Marketplace specific.
    # TODO(andym): figure out what to do when we delete the user.
    user = models.ForeignKey('users.UserProfile', blank=True, null=True)
    type = models.PositiveIntegerField(default=mkt.CONTRIB_TYPE_DEFAULT,
                                       choices=do_dictsort(mkt.CONTRIB_TYPES),
                                       db_index=True)
    price_tier = models.ForeignKey('prices.Price', blank=True, null=True,
                                   on_delete=models.PROTECT)
    # If this is a refund or a chargeback, which charge did it relate to.
    related = models.ForeignKey('self', blank=True, null=True,
                                on_delete=models.PROTECT)

    class Meta:
        db_table = 'stats_contributions'

    def __unicode__(self):
        return (u'<{cls} {pk}; app: {app}; in-app: {inapp}; amount: {amount}>'
                .format(app=self.addon, amount=self.amount, pk=self.pk,
                        inapp=self.inapp_product, cls=self.__class__.__name__))

    @property
    def date(self):
        try:
            return datetime.date(self.created.year,
                                 self.created.month, self.created.day)
        except AttributeError:
            # created may be None
            return None

    def _switch_locale(self):
        if self.source_locale:
            lang = self.source_locale
        else:
            lang = self.addon.default_locale
        translation.activate(lang)
        return Locale(translation.to_locale(lang))

    def _mail(self, template, subject, context):
        template = env.get_template(template)
        body = template.render(context)
        send_mail(subject, body, settings.MARKETPLACE_EMAIL,
                  [self.user.email], fail_silently=True)

    def is_inapp_simulation(self):
        """True if this purchase is for a simulated in-app product."""
        return self.inapp_product and self.inapp_product.simulate

    def enqueue_refund(self, status, user, refund_reason=None,
                       rejection_reason=None):
        """Keep track of a contribution's refund status."""
        from mkt.prices.models import Refund
        refund, c = Refund.objects.safer_get_or_create(contribution=self,
                                                       user=user)
        refund.status = status

        # Determine which timestamps to update.
        timestamps = []
        if status in (mkt.REFUND_PENDING, mkt.REFUND_APPROVED_INSTANT,
                      mkt.REFUND_FAILED):
            timestamps.append('requested')
        if status in (mkt.REFUND_APPROVED, mkt.REFUND_APPROVED_INSTANT):
            timestamps.append('approved')
        elif status == mkt.REFUND_DECLINED:
            timestamps.append('declined')
        for ts in timestamps:
            setattr(refund, ts, datetime.datetime.now())

        if refund_reason:
            refund.refund_reason = refund_reason
        if rejection_reason:
            refund.rejection_reason = rejection_reason
        refund.save()
        return refund

    def get_amount_locale(self, locale=None):
        """Localise the amount paid into the current locale."""
        if not locale:
            lang = translation.get_language()
            locale = get_locale_from_lang(lang)
        return numbers.format_currency(self.amount or 0,
                                       self.currency or 'USD',
                                       locale=locale)

    def get_refund_url(self):
        return urlparams(self.addon.get_dev_url('issue_refund'),
                         transaction_id=self.transaction_id)

    def get_absolute_refund_url(self):
        return absolutify(self.get_refund_url())

    def get_refund_contribs(self):
        """Get related set of refund contributions."""
        return Contribution.objects.filter(
            related=self, type=mkt.CONTRIB_REFUND).order_by('-modified')

    def is_refunded(self):
        """
        If related has been set, then this transaction has been refunded or
        charged back. This is a bit expensive, so refrain from using on listing
        pages.
        """
        return (Contribution.objects.filter(related=self,
                                            type__in=[mkt.CONTRIB_REFUND,
                                                      mkt.CONTRIB_CHARGEBACK])
                                    .exists())
예제 #16
0
class AddonBase(OnChangeMixin, ModelBase):
    STATUS_CHOICES = base.STATUS_CHOICES.items()
    LOCALES = [(translation.to_locale(k).replace('_', '-'), v) for k, v in
               do_dictsort(settings.LANGUAGES)]

    guid = models.CharField(max_length=255, unique=True, null=True)
    slug = models.CharField(max_length=30, unique=True, null=True)
    # This column is only used for webapps, so they can have a slug namespace
    # separate from addons and personas.
    app_slug = models.CharField(max_length=30, unique=True, null=True)
    name = TranslatedField()
    default_locale = models.CharField(max_length=10,
                                      default=settings.LANGUAGE_CODE,
                                      db_column='defaultlocale')

    type = models.PositiveIntegerField(db_column='addontype_id')
    status = models.PositiveIntegerField(
        choices=STATUS_CHOICES, db_index=True, default=0)
    highest_status = models.PositiveIntegerField(
        choices=STATUS_CHOICES, default=0,
        help_text="An upper limit for what an author can change.",
        db_column='higheststatus')
    icon_type = models.CharField(max_length=25, blank=True,
                                 db_column='icontype')
    homepage = TranslatedField()
    support_email = TranslatedField(db_column='supportemail')
    support_url = TranslatedField(db_column='supporturl')
    description = PurifiedField(short=False)

    summary = LinkifiedField()
    developer_comments = PurifiedField(db_column='developercomments')
    eula = PurifiedField()
    privacy_policy = PurifiedField(db_column='privacypolicy')
    the_reason = PurifiedField()
    the_future = PurifiedField()

    average_rating = models.FloatField(max_length=255, default=0, null=True,
                                       db_column='averagerating')
    bayesian_rating = models.FloatField(default=0, db_index=True,
                                        db_column='bayesianrating')
    total_reviews = models.PositiveIntegerField(default=0,
                                                db_column='totalreviews')
    weekly_downloads = models.PositiveIntegerField(
            default=0, db_column='weeklydownloads', db_index=True)
    total_downloads = models.PositiveIntegerField(
            default=0, db_column='totaldownloads')
    hotness = models.FloatField(default=0, db_index=True)

    average_daily_downloads = models.PositiveIntegerField(default=0)
    average_daily_users = models.PositiveIntegerField(default=0)
    share_count = models.PositiveIntegerField(default=0, db_index=True,
                                              db_column='sharecount')
    last_updated = models.DateTimeField(db_index=True, null=True,
        help_text='Last time this add-on had a file/version update')
    ts_slowness = models.FloatField(db_index=True, null=True,
        help_text='How much slower this add-on makes browser ts tests. '
                  'Read as {addon.ts_slowness}% slower.')

    disabled_by_user = models.BooleanField(default=False, db_index=True,
                                           db_column='inactive')
    trusted = models.BooleanField(default=False)
    view_source = models.BooleanField(default=True, db_column='viewsource')
    public_stats = models.BooleanField(default=False, db_column='publicstats')
    prerelease = models.BooleanField(default=False)
    admin_review = models.BooleanField(default=False, db_column='adminreview')
    admin_review_type = models.PositiveIntegerField(
                                    choices=base.ADMIN_REVIEW_TYPES.items(),
                                    default=base.ADMIN_REVIEW_FULL)
    site_specific = models.BooleanField(default=False,
                                        db_column='sitespecific')
    external_software = models.BooleanField(default=False,
                                            db_column='externalsoftware')
    dev_agreement = models.BooleanField(default=False,
                            help_text="Has the dev agreement been signed?")
    auto_repackage = models.BooleanField(default=True,
        help_text='Automatically upgrade jetpack add-on to a new sdk version?')
    outstanding = models.BooleanField(default=False)

    nomination_message = models.TextField(null=True,
                                          db_column='nominationmessage')
    target_locale = models.CharField(
        max_length=255, db_index=True, blank=True, null=True,
        help_text="For dictionaries and language packs")
    locale_disambiguation = models.CharField(
        max_length=255, blank=True, null=True,
        help_text="For dictionaries and language packs")

    wants_contributions = models.BooleanField(default=False)
    paypal_id = models.CharField(max_length=255, blank=True)
    charity = models.ForeignKey('Charity', null=True)
    # TODO(jbalogh): remove nullify_invalid once remora dies.
    suggested_amount = DecimalCharField(
        max_digits=8, decimal_places=2, nullify_invalid=True, blank=True,
        null=True, help_text=_(u'Users have the option of contributing more '
                               'or less than this amount.'))

    total_contributions = DecimalCharField(max_digits=8, decimal_places=2,
                                           nullify_invalid=True, blank=True,
                                           null=True)

    annoying = models.PositiveIntegerField(
        choices=base.CONTRIB_CHOICES, default=0,
        help_text=_(u"Users will always be asked in the Add-ons"
                     " Manager (Firefox 4 and above)"))
    enable_thankyou = models.BooleanField(default=False,
        help_text="Should the thankyou note be sent to contributors?")
    thankyou_note = TranslatedField()

    get_satisfaction_company = models.CharField(max_length=255, blank=True,
                                                null=True)
    get_satisfaction_product = models.CharField(max_length=255, blank=True,
                                                null=True)

    authors = models.ManyToManyField(UserProfileBase, through='AddonUser',
                                     related_name='addons')
    categories = models.ManyToManyField('Category', through='AddonCategoryBase')
    dependencies = models.ManyToManyField('self', symmetrical=False,
                                          through='AddonDependency',
                                          related_name='addons')
    premium_type = models.PositiveIntegerField(
                                    choices=base.ADDON_PREMIUM_TYPES.items(),
                                    default=base.ADDON_FREE)
    manifest_url = models.URLField(max_length=255, blank=True, null=True,
                                   verify_exists=False)
    app_domain = models.CharField(max_length=255, blank=True, null=True,
                                  db_index=True)

    _current_version = models.ForeignKey(VersionBase, related_name='___ignore',
            db_column='current_version', null=True, on_delete=models.SET_NULL)
    # This is for Firefox only.
    _backup_version = models.ForeignKey(VersionBase, related_name='___backup',
            db_column='backup_version', null=True, on_delete=models.SET_NULL)
    _latest_version = None
    make_public = models.DateTimeField(null=True)
    mozilla_contact = models.EmailField()

    # Whether the app is packaged or not (aka hosted).
    is_packaged = models.BooleanField(default=False, db_index=True)

    # This gets overwritten in the transformer.
    share_counts = collections.defaultdict(int)

    class Meta:
        db_table = 'addons'
        app_label = 'addons'

    @staticmethod
    def __new__(cls, *args, **kw):
        # # Return a Webapp instead of an Addon if the `type` column says this is
        # # really a webapp.
        # try:
        #     type_idx = AddonBase._meta._type_idx
        # except AttributeError:
        #     type_idx = (idx for idx, f in enumerate(AddonBase._meta.fields)
        #                 if f.attname == 'type').next()
        #     AddonBase._meta._type_idx = type_idx
        # if ((len(args) == len(AddonBase._meta.fields)
        #      and args[type_idx] == base.ADDON_WEBAPP)
        #     or kw and kw.get('type') == base.ADDON_WEBAPP):
        #     from gelato.models.webapp import Webapp
        #     cls = Webapp
        return super(AddonBase, cls).__new__(cls, *args, **kw)

    def __unicode__(self):
        return u'%s: %s' % (self.id, self.name)

    def __init__(self, *args, **kw):
        super(AddonBase, self).__init__(*args, **kw)
        self._first_category = {}


    @property
    def premium(self):
        """
        Returns the premium object which will be gotten by the transformer,
        if its not there, try and get it. Will return None if there's nothing
        there.
        """
        if not hasattr(self, '_premium'):
            try:
                self._premium = self.addonpremium
            except AddonPremium.DoesNotExist:
                self._premium = None
        return self._premium
예제 #17
0
class Refund(amo.models.ModelBase):
    # This refers to the original object with `type=amo.CONTRIB_PURCHASE`.
    contribution = models.OneToOneField('stats.Contribution')

    # Pending => 0
    # Approved => 1
    # Instantly Approved => 2
    # Declined => 3
    # Failed => 4
    status = models.PositiveIntegerField(default=amo.REFUND_PENDING,
        choices=do_dictsort(amo.REFUND_STATUSES), db_index=True)

    refund_reason = models.TextField(default='', blank=True)
    rejection_reason = models.TextField(default='', blank=True)

    # Date `created` should always be date `requested` for pending refunds,
    # but let's just stay on the safe side. We might change our minds.
    requested = models.DateTimeField(null=True, db_index=True)
    approved = models.DateTimeField(null=True, db_index=True)
    declined = models.DateTimeField(null=True, db_index=True)
    user = models.ForeignKey('users.UserProfile')

    objects = RefundManager()

    class Meta:
        db_table = 'refunds'

    def __unicode__(self):
        return u'%s (%s)' % (self.contribution, self.get_status_display())

    @staticmethod
    def post_save(sender, instance, **kwargs):
        from amo.tasks import find_refund_escalations
        find_refund_escalations(instance.contribution.addon_id)

    @classmethod
    def recent_refund_ratio(cls, addon_id, since):
        """
        Returns the ratio of purchases to refunds since the given datetime.
        """
        cursor = connection.cursor()
        purchases = AddonPurchase.objects.filter(
            addon=addon_id, type=amo.CONTRIB_PURCHASE).count()

        if purchases == 0:
            return 0.0

        params = [addon_id, since]
        # Hardcoded statuses for simplicity, but they are:
        # amo.REFUND_PENDING, amo.REFUND_APPROVED, amo.REFUND_APPROVED_INSTANT
        sql = '''
            SELECT COUNT(DISTINCT sc.user_id) AS num
            FROM refunds
            LEFT JOIN stats_contributions AS sc
                ON refunds.contribution_id = sc.id
            WHERE sc.addon_id = %s
            AND refunds.status IN (0,1,2)
            AND refunds.created > %s
        '''
        cursor.execute(sql, params)
        row = cursor.fetchone()
        if row:
            return row[0] / float(purchases)
        return 0.0
예제 #18
0
class Contribution(amo.models.ModelBase):
    addon = models.ForeignKey('addons.Addon')
    # For in-app purchases this links to the product.
    inapp_product = models.ForeignKey('inapp.InAppProduct',
                                      blank=True,
                                      null=True)
    amount = DecimalCharField(max_digits=9,
                              decimal_places=2,
                              nullify_invalid=True,
                              null=True)
    currency = models.CharField(max_length=3,
                                choices=do_dictsort(amo.PAYPAL_CURRENCIES),
                                default=amo.CURRENCY_DEFAULT)
    source = models.CharField(max_length=255, null=True)
    source_locale = models.CharField(max_length=10, null=True)
    # This is the external id that you can communicate to the world.
    uuid = models.CharField(max_length=255, null=True, db_index=True)
    comment = models.CharField(max_length=255)
    # This is the internal transaction id between us and a provider,
    # for example paypal or solitude.
    transaction_id = models.CharField(max_length=255, null=True, db_index=True)
    paykey = models.CharField(max_length=255, null=True)
    post_data = StatsDictField(null=True)

    # Voluntary Contribution specific.
    charity = models.ForeignKey('addons.Charity', null=True)
    annoying = models.PositiveIntegerField(
        default=0,
        choices=amo.CONTRIB_CHOICES,
    )
    is_suggested = models.BooleanField(default=False)
    suggested_amount = DecimalCharField(max_digits=254,
                                        decimal_places=2,
                                        nullify_invalid=True,
                                        null=True)

    # Marketplace specific.
    # TODO(andym): figure out what to do when we delete the user.
    user = models.ForeignKey('users.UserProfile', blank=True, null=True)
    type = models.PositiveIntegerField(default=amo.CONTRIB_TYPE_DEFAULT,
                                       choices=do_dictsort(amo.CONTRIB_TYPES))
    price_tier = models.ForeignKey('prices.Price',
                                   blank=True,
                                   null=True,
                                   on_delete=models.PROTECT)
    # If this is a refund or a chargeback, which charge did it relate to.
    related = models.ForeignKey('self',
                                blank=True,
                                null=True,
                                on_delete=models.PROTECT)

    class Meta:
        db_table = 'stats_contributions'

    def __unicode__(self):
        return u'%s: %s' % (self.addon.name, self.amount)

    @property
    def date(self):
        try:
            return datetime.date(self.created.year, self.created.month,
                                 self.created.day)
        except AttributeError:
            # created may be None
            return None

    @property
    def contributor(self):
        try:
            return u'%s %s' % (self.post_data['first_name'],
                               self.post_data['last_name'])
        except (TypeError, KeyError):
            # post_data may be None or missing a key
            return None

    @property
    def email(self):
        try:
            return self.post_data['payer_email']
        except (TypeError, KeyError):
            # post_data may be None or missing a key
            return None

    def _switch_locale(self):
        if self.source_locale:
            lang = self.source_locale
        else:
            lang = self.addon.default_locale
        tower.activate(lang)
        return Locale(translation.to_locale(lang))

    def _mail(self, template, subject, context):
        template = env.get_template(template)
        body = template.render(context)
        send_mail(subject,
                  body,
                  settings.MARKETPLACE_EMAIL, [self.user.email],
                  fail_silently=True)

    def record_failed_refund(self, e, user):
        self.enqueue_refund(amo.REFUND_FAILED, user, rejection_reason=str(e))
        self._switch_locale()
        self._mail(
            'users/support/emails/refund-failed.txt',
            # L10n: the addon name.
            _(u'%s refund failed' % self.addon.name),
            {'name': self.addon.name})
        send_mail_jinja('Refund failed',
                        'purchase/email/refund-failed.txt', {
                            'name': self.user.email,
                            'error': str(e)
                        },
                        settings.MARKETPLACE_EMAIL,
                        [str(self.addon.support_email)],
                        fail_silently=True)

    def mail_approved(self):
        """The developer has approved a refund."""
        locale = self._switch_locale()
        amt = numbers.format_currency(abs(self.amount),
                                      self.currency,
                                      locale=locale)
        self._mail(
            'users/support/emails/refund-approved.txt',
            # L10n: the adddon name.
            _(u'%s refund approved' % self.addon.name),
            {
                'name': self.addon.name,
                'amount': amt
            })

    def mail_declined(self):
        """The developer has declined a refund."""
        self._switch_locale()
        self._mail(
            'users/support/emails/refund-declined.txt',
            # L10n: the adddon name.
            _(u'%s refund declined' % self.addon.name),
            {'name': self.addon.name})

    def enqueue_refund(self,
                       status,
                       user,
                       refund_reason=None,
                       rejection_reason=None):
        """Keep track of a contribution's refund status."""
        from mkt.prices.models import Refund
        refund, c = Refund.objects.safer_get_or_create(contribution=self,
                                                       user=user)
        refund.status = status

        # Determine which timestamps to update.
        timestamps = []
        if status in (amo.REFUND_PENDING, amo.REFUND_APPROVED_INSTANT,
                      amo.REFUND_FAILED):
            timestamps.append('requested')
        if status in (amo.REFUND_APPROVED, amo.REFUND_APPROVED_INSTANT):
            timestamps.append('approved')
        elif status == amo.REFUND_DECLINED:
            timestamps.append('declined')
        for ts in timestamps:
            setattr(refund, ts, datetime.datetime.now())

        if refund_reason:
            refund.refund_reason = refund_reason
        if rejection_reason:
            refund.rejection_reason = rejection_reason
        refund.save()
        return refund

    def get_amount_locale(self, locale=None):
        """Localise the amount paid into the current locale."""
        if not locale:
            lang = translation.get_language()
            locale = get_locale_from_lang(lang)
        return numbers.format_currency(self.amount or 0,
                                       self.currency or 'USD',
                                       locale=locale)

    def get_refund_url(self):
        return urlparams(self.addon.get_dev_url('issue_refund'),
                         transaction_id=self.transaction_id)

    def get_absolute_refund_url(self):
        return absolutify(self.get_refund_url())

    def get_refund_contribs(self):
        """Get related set of refund contributions."""
        return Contribution.objects.filter(
            related=self, type=amo.CONTRIB_REFUND).order_by('-modified')

    def is_refunded(self):
        """
        If related has been set, then this transaction has been refunded or
        charged back. This is a bit expensive, so refrain from using on listing
        pages.
        """
        return (Contribution.objects.filter(
            related=self,
            type__in=[amo.CONTRIB_REFUND, amo.CONTRIB_CHARGEBACK]).exists())
예제 #19
0
### Here be dragons.
# Django decided to require that ForeignKeys be unique.  That's generally
# reasonable, but Translations break that in their quest for all things unholy.
# Here we monkeypatch the error collector Django uses in validation to skip any
# messages generated by Translations. (Django #13284)
import re

from django.conf import settings
from django.core.management import validation
from django.utils.translation import trans_real

from jinja2.filters import do_dictsort

Parent = validation.ModelErrorCollection


class ModelErrorCollection(Parent):
    skip = ("Field 'id' under model '\w*Translation' must "
            "have a unique=True constraint.")

    def add(self, context, error):
        if not re.match(self.skip, error):
            Parent.add(self, context, error)

validation.ModelErrorCollection = ModelErrorCollection


LOCALES = [(trans_real.to_locale(k).replace('_', '-'), v) for k, v in
           do_dictsort(settings.LANGUAGES)]