Merge branch 'master' of github.com:jbalogh/zamboni
This commit is contained in:
Коммит
bf2b6a685a
10
STYLE.rst
10
STYLE.rst
|
@ -10,6 +10,16 @@ Python
|
|||
- see https://wiki.mozilla.org/Webdev:Python
|
||||
|
||||
|
||||
Markup
|
||||
------
|
||||
- ``<!DOCTYPE html>``
|
||||
- double-quote attributes
|
||||
- Soft tab (2 space) indentation
|
||||
- Title-Case ``<label>``s
|
||||
- "Display Name" vs "Display name"
|
||||
- to clearfix, use the class ``c`` on an element
|
||||
|
||||
|
||||
JavaScript
|
||||
----------
|
||||
- Soft tabs (4 space) indentation
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
import amo.models
|
||||
import amo.utils
|
||||
from addons.models import Addon
|
||||
from users.models import UserProfile
|
||||
|
||||
|
||||
log = logging.getLogger('z.abuse')
|
||||
|
||||
|
||||
class AbuseReport(amo.models.ModelBase):
|
||||
# NULL if the reporter is anonymous.
|
||||
reporter = models.ForeignKey(UserProfile, null=True,
|
||||
related_name='abuse_reported')
|
||||
ip_address = models.CharField(max_length=255, default='0.0.0.0')
|
||||
# An abuse report can be for an addon or a user. Only one of these should
|
||||
# be null.
|
||||
addon = models.ForeignKey(Addon, null=True, related_name='abuse_reports')
|
||||
user = models.ForeignKey(UserProfile, null=True,
|
||||
related_name='abuse_reports')
|
||||
message = models.TextField()
|
||||
|
||||
class Meta:
|
||||
db_table = 'abuse_reports'
|
||||
|
||||
def send(self):
|
||||
obj = self.addon or self.user
|
||||
if self.reporter:
|
||||
user_name = '%s (%s)' % (self.reporter.name, self.reporter.email)
|
||||
else:
|
||||
user_name = 'An anonymous user'
|
||||
subject = 'Abuse Report for %s' % obj.name
|
||||
msg = u'%s reported abuse for %s (%s%s).\n\n%s' % (
|
||||
user_name, obj.name, settings.SITE_URL, obj.get_url_path(),
|
||||
self.message)
|
||||
amo.utils.send_mail(subject, msg, recipient_list=(settings.FLIGTAR,))
|
||||
|
||||
|
||||
def send_abuse_report(request, obj, message):
|
||||
report = AbuseReport(ip_address=request.META.get('REMOTE_ADDR'),
|
||||
message=message)
|
||||
if request.user.is_authenticated():
|
||||
report.reporter = request.amo_user
|
||||
if isinstance(obj, Addon):
|
||||
report.addon = obj
|
||||
elif isinstance(obj, UserProfile):
|
||||
report.user = obj
|
||||
report.save()
|
||||
report.send()
|
|
@ -201,15 +201,14 @@ class InstallButton(object):
|
|||
if self.show_eula:
|
||||
# L10n: please keep in the string so → does not wrap.
|
||||
text = jinja2.Markup(_('Continue to Download →'))
|
||||
url = file.eula_url(impala=self.impala)
|
||||
url = file.eula_url(impala=True)
|
||||
elif self.accept_eula:
|
||||
text = _('Accept and Download')
|
||||
elif self.show_contrib:
|
||||
# The eula doesn't exist or has been hit already.
|
||||
# L10n: please keep in the string so → does not wrap.
|
||||
text = jinja2.Markup(_('Continue to Download →'))
|
||||
u = '%saddons.roadblock' % ('i_' if self.impala else '')
|
||||
roadblock = reverse(u, args=[self.addon.id])
|
||||
roadblock = reverse('addons.roadblock', args=[self.addon.id])
|
||||
url = urlparams(roadblock, eula='', version=self.version.version)
|
||||
|
||||
return text, url, os
|
||||
|
|
|
@ -1,29 +1,36 @@
|
|||
from datetime import datetime
|
||||
import os
|
||||
import path
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.forms.formsets import formset_factory
|
||||
|
||||
import commonware.log
|
||||
import happyforms
|
||||
import path
|
||||
from tower import ugettext as _, ungettext as ngettext
|
||||
from quieter_formset.formset import BaseFormSet
|
||||
from tower import ugettext as _, ugettext_lazy as _lazy, ungettext as ngettext
|
||||
|
||||
from access import acl
|
||||
import amo
|
||||
import captcha.fields
|
||||
from amo.fields import ColorField
|
||||
from amo.urlresolvers import reverse
|
||||
from amo.utils import slug_validator, slugify, sorted_groupby, remove_icons
|
||||
from addons.models import (Addon, AddonCategory, BlacklistedSlug,
|
||||
Category, ReverseNameLookup)
|
||||
from addons.models import (Addon, AddonCategory, AddonUser, BlacklistedSlug,
|
||||
Category, Persona, ReverseNameLookup)
|
||||
from addons.widgets import IconWidgetRenderer, CategoriesSelectMultiple
|
||||
from applications.models import Application
|
||||
from devhub import tasks
|
||||
from devhub import tasks as devhub_tasks
|
||||
from tags.models import Tag
|
||||
from translations.fields import TransField, TransTextarea
|
||||
from translations.forms import TranslationFormMixin
|
||||
from translations.models import Translation
|
||||
from translations.widgets import TranslationTextInput
|
||||
from versions.models import Version
|
||||
|
||||
log = commonware.log.getLogger('z.addons')
|
||||
|
||||
|
||||
def clean_name(name, instance=None):
|
||||
|
@ -36,6 +43,55 @@ def clean_name(name, instance=None):
|
|||
return name
|
||||
|
||||
|
||||
def clean_tags(request, tags):
|
||||
target = [slugify(t, spaces=True, lower=True) for t in tags.split(',')]
|
||||
target = set(filter(None, target))
|
||||
|
||||
min_len = amo.MIN_TAG_LENGTH
|
||||
max_len = Tag._meta.get_field('tag_text').max_length
|
||||
max_tags = amo.MAX_TAGS
|
||||
total = len(target)
|
||||
|
||||
blacklisted = (Tag.objects.values_list('tag_text', flat=True)
|
||||
.filter(tag_text__in=target, blacklisted=True))
|
||||
if blacklisted:
|
||||
# L10n: {0} is a single tag or a comma-separated list of tags.
|
||||
msg = ngettext('Invalid tag: {0}', 'Invalid tags: {0}',
|
||||
len(blacklisted)).format(', '.join(blacklisted))
|
||||
raise forms.ValidationError(msg)
|
||||
|
||||
restricted = (Tag.objects.values_list('tag_text', flat=True)
|
||||
.filter(tag_text__in=target, restricted=True))
|
||||
if not acl.action_allowed(request, 'Admin', 'EditAnyAddon'):
|
||||
if restricted:
|
||||
# L10n: {0} is a single tag or a comma-separated list of tags.
|
||||
msg = ngettext('"{0}" is a reserved tag and cannot be used.',
|
||||
'"{0}" are reserved tags and cannot be used.',
|
||||
len(restricted)).format('", "'.join(restricted))
|
||||
raise forms.ValidationError(msg)
|
||||
else:
|
||||
# Admin's restricted tags don't count towards the limit.
|
||||
total = len(target - set(restricted))
|
||||
|
||||
if total > max_tags:
|
||||
num = total - max_tags
|
||||
msg = ngettext('You have {0} too many tags.',
|
||||
'You have {0} too many tags.', num).format(num)
|
||||
raise forms.ValidationError(msg)
|
||||
|
||||
if any(t for t in target if len(t) > max_len):
|
||||
raise forms.ValidationError(_('All tags must be %s characters '
|
||||
'or less after invalid characters are removed.' % max_len))
|
||||
|
||||
if any(t for t in target if len(t) < min_len):
|
||||
msg = ngettext("All tags must be at least {0} character.",
|
||||
"All tags must be at least {0} characters.",
|
||||
min_len).format(min_len)
|
||||
raise forms.ValidationError(msg)
|
||||
|
||||
return target
|
||||
|
||||
|
||||
class AddonFormBase(TranslationFormMixin, happyforms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
|
@ -90,53 +146,7 @@ class AddonFormBasic(AddonFormBase):
|
|||
return addonform
|
||||
|
||||
def clean_tags(self):
|
||||
target = [slugify(t, spaces=True, lower=True)
|
||||
for t in self.cleaned_data['tags'].split(',')]
|
||||
target = set(filter(None, target))
|
||||
|
||||
min_len = amo.MIN_TAG_LENGTH
|
||||
max_len = Tag._meta.get_field('tag_text').max_length
|
||||
max_tags = amo.MAX_TAGS
|
||||
total = len(target)
|
||||
|
||||
blacklisted = (Tag.objects.values_list('tag_text', flat=True)
|
||||
.filter(tag_text__in=target, blacklisted=True))
|
||||
if blacklisted:
|
||||
# L10n: {0} is a single tag or a comma-separated list of tags.
|
||||
msg = ngettext('Invalid tag: {0}', 'Invalid tags: {0}',
|
||||
len(blacklisted)).format(', '.join(blacklisted))
|
||||
raise forms.ValidationError(msg)
|
||||
|
||||
restricted = (Tag.objects.values_list('tag_text', flat=True)
|
||||
.filter(tag_text__in=target, restricted=True))
|
||||
if not acl.action_allowed(self.request, 'Admin', 'EditAnyAddon'):
|
||||
if restricted:
|
||||
# L10n: {0} is a single tag or a comma-separated list of tags.
|
||||
msg = ngettext('"{0}" is a reserved tag and cannot be used.',
|
||||
'"{0}" are reserved tags and cannot be used.',
|
||||
len(restricted)).format('", "'.join(restricted))
|
||||
raise forms.ValidationError(msg)
|
||||
else:
|
||||
# Admin's restricted tags don't count towards the limit.
|
||||
total = len(target - set(restricted))
|
||||
|
||||
if total > max_tags:
|
||||
num = total - max_tags
|
||||
msg = ngettext('You have {0} too many tags.',
|
||||
'You have {0} too many tags.', num).format(num)
|
||||
raise forms.ValidationError(msg)
|
||||
|
||||
if any(t for t in target if len(t) > max_len):
|
||||
raise forms.ValidationError(_('All tags must be %s characters '
|
||||
'or less after invalid characters are removed.' % max_len))
|
||||
|
||||
if any(t for t in target if len(t) < min_len):
|
||||
msg = ngettext("All tags must be at least {0} character.",
|
||||
"All tags must be at least {0} characters.",
|
||||
min_len).format(min_len)
|
||||
raise forms.ValidationError(msg)
|
||||
|
||||
return target
|
||||
return clean_tags(self.request, self.cleaned_data['tags'])
|
||||
|
||||
def clean_slug(self):
|
||||
target = self.cleaned_data['slug']
|
||||
|
@ -285,9 +295,9 @@ class AddonFormMedia(AddonFormBase):
|
|||
destination = os.path.join(dirname, '%s' % addon.id)
|
||||
|
||||
remove_icons(destination)
|
||||
tasks.resize_icon.delay(upload_path, destination,
|
||||
amo.ADDON_ICON_SIZES,
|
||||
set_modified_on=[addon])
|
||||
devhub_tasks.resize_icon.delay(upload_path, destination,
|
||||
amo.ADDON_ICON_SIZES,
|
||||
set_modified_on=[addon])
|
||||
|
||||
return super(AddonFormMedia, self).save(commit)
|
||||
|
||||
|
@ -410,3 +420,104 @@ class AbuseForm(happyforms.Form):
|
|||
if (not self.request.user.is_anonymous() or
|
||||
not settings.RECAPTCHA_PRIVATE_KEY):
|
||||
del self.fields['recaptcha']
|
||||
|
||||
|
||||
class NewPersonaForm(AddonFormBase):
|
||||
name = forms.CharField(max_length=50)
|
||||
category = forms.ModelChoiceField(queryset=Category.objects.all(),
|
||||
widget=forms.widgets.RadioSelect)
|
||||
summary = forms.CharField(widget=forms.Textarea(attrs={'rows': 4}),
|
||||
max_length=250, required=False)
|
||||
tags = forms.CharField(required=False)
|
||||
|
||||
license = forms.TypedChoiceField(choices=amo.PERSONA_LICENSES_IDS,
|
||||
coerce=int, empty_value=None, widget=forms.HiddenInput,
|
||||
error_messages={'required': _lazy(u'A license must be selected.')})
|
||||
header = forms.FileField(required=False)
|
||||
header_hash = forms.CharField(widget=forms.HiddenInput)
|
||||
footer = forms.FileField(required=False)
|
||||
footer_hash = forms.CharField(widget=forms.HiddenInput)
|
||||
accentcolor = ColorField(required=False)
|
||||
textcolor = ColorField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Addon
|
||||
fields = ('name', 'summary', 'tags')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NewPersonaForm, self).__init__(*args, **kwargs)
|
||||
cats = Category.objects.filter(application=amo.FIREFOX.id,
|
||||
type=amo.ADDON_PERSONA, weight__gte=0)
|
||||
cats = sorted(cats, key=lambda x: x.name)
|
||||
self.fields['category'].choices = [(c.id, c.name) for c in cats]
|
||||
self.fields['header'].widget.attrs['data-upload-url'] = reverse(
|
||||
'personas.upload_persona', args=['persona_header'])
|
||||
self.fields['footer'].widget.attrs['data-upload-url'] = reverse(
|
||||
'personas.upload_persona', args=['persona_footer'])
|
||||
|
||||
def clean_name(self):
|
||||
return clean_name(self.cleaned_data['name'])
|
||||
|
||||
def clean_tags(self):
|
||||
return clean_tags(self.request, self.cleaned_data['tags'])
|
||||
|
||||
def save(self, commit=False):
|
||||
from .tasks import create_persona_preview_image, save_persona_image
|
||||
# We ignore `commit`, since we need it to be `False` so we can save
|
||||
# the ManyToMany fields on our own.
|
||||
addon = super(NewPersonaForm, self).save(commit=False)
|
||||
addon.status = amo.STATUS_UNREVIEWED
|
||||
addon.type = amo.ADDON_PERSONA
|
||||
addon.save()
|
||||
addon._current_version = Version.objects.create(addon=addon,
|
||||
version='0')
|
||||
addon.save()
|
||||
amo.log(amo.LOG.CREATE_ADDON, addon)
|
||||
log.debug('New persona %r uploaded' % addon)
|
||||
|
||||
data = self.cleaned_data
|
||||
|
||||
header = data['header_hash']
|
||||
footer = data['footer_hash']
|
||||
|
||||
header = os.path.join(settings.TMP_PATH, 'persona_header', header)
|
||||
footer = os.path.join(settings.TMP_PATH, 'persona_footer', footer)
|
||||
dst = os.path.join(settings.PERSONAS_PATH, str(addon.id))
|
||||
|
||||
# Save header, footer, and preview images.
|
||||
save_persona_image(src=header, dst=dst, img_basename='header.jpg')
|
||||
save_persona_image(src=footer, dst=dst, img_basename='footer.jpg')
|
||||
create_persona_preview_image(src=header, dst=dst,
|
||||
img_basename='preview.jpg',
|
||||
set_modified_on=[addon])
|
||||
|
||||
# Save user info.
|
||||
user = self.request.amo_user
|
||||
AddonUser(addon=addon, user=user).save()
|
||||
|
||||
p = Persona()
|
||||
p.persona_id = 0
|
||||
p.addon = addon
|
||||
p.header = 'header.jpg'
|
||||
p.footer = 'footer.jpg'
|
||||
if data['accentcolor']:
|
||||
p.accentcolor = data['accentcolor'].lstrip('#')
|
||||
if data['textcolor']:
|
||||
p.textcolor = data['textcolor'].lstrip('#')
|
||||
p.license_id = data['license']
|
||||
p.submit = datetime.now()
|
||||
p.author = user.name
|
||||
p.display_username = user.username
|
||||
p.save()
|
||||
|
||||
# Save tags.
|
||||
for t in data['tags']:
|
||||
Tag(tag_text=t).save_tag(addon)
|
||||
|
||||
# Save categories.
|
||||
tb_c = Category.objects.get(application=amo.THUNDERBIRD.id,
|
||||
name__id=data['category'].name_id)
|
||||
AddonCategory(addon=addon, category=data['category']).save()
|
||||
AddonCategory(addon=addon, category=tb_c).save()
|
||||
|
||||
return addon
|
||||
|
|
|
@ -200,8 +200,11 @@ def sidebar_listing(context, addon):
|
|||
|
||||
@register.filter
|
||||
@jinja2.contextfilter
|
||||
def addon_hovercard(context, addon):
|
||||
return addon_grid(context, [addon], cols=1)
|
||||
@register.inclusion_tag('addons/impala/addon_hovercard.html')
|
||||
def addon_hovercard(context, addon, lazyload=False):
|
||||
vital_summary = context.get('vital_summary') or 'rating'
|
||||
vital_more = context.get('vital_more') or 'adu'
|
||||
return new_context(**locals())
|
||||
|
||||
|
||||
@register.filter
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# -*- coding: utf8 -*-
|
||||
import collections
|
||||
import itertools
|
||||
import json
|
||||
|
@ -223,12 +224,15 @@ class Addon(amo.models.OnChangeMixin, amo.models.ModelBase):
|
|||
|
||||
authors = models.ManyToManyField('users.UserProfile', through='AddonUser',
|
||||
related_name='addons')
|
||||
|
||||
categories = models.ManyToManyField('Category', through='AddonCategory')
|
||||
|
||||
dependencies = models.ManyToManyField('self', symmetrical=False,
|
||||
through='AddonDependency',
|
||||
related_name='addons')
|
||||
premium_type = models.PositiveIntegerField(
|
||||
choices=amo.ADDON_PREMIUM_TYPES.items(),
|
||||
default=amo.ADDON_FREE)
|
||||
manifest_url = models.URLField(max_length=255, blank=True, null=True,
|
||||
verify_exists=False)
|
||||
|
||||
_current_version = models.ForeignKey(Version, related_name='___ignore',
|
||||
db_column='current_version', null=True, on_delete=models.SET_NULL)
|
||||
|
@ -245,6 +249,22 @@ class Addon(amo.models.OnChangeMixin, amo.models.ModelBase):
|
|||
class Meta:
|
||||
db_table = '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 = Addon._meta._type_idx
|
||||
except AttributeError:
|
||||
type_idx = (idx for idx, f in enumerate(Addon._meta.fields)
|
||||
if f.attname == 'type').next()
|
||||
Addon._meta._type_idx = type_idx
|
||||
if ((len(args) == len(Addon._meta.fields)
|
||||
and args[type_idx] == amo.ADDON_WEBAPP)
|
||||
or kw and kw.get('type') == amo.ADDON_WEBAPP):
|
||||
cls = Webapp
|
||||
return super(Addon, cls).__new__(cls, *args, **kw)
|
||||
|
||||
def __unicode__(self):
|
||||
return u'%s: %s' % (self.id, self.name)
|
||||
|
||||
|
@ -330,11 +350,13 @@ class Addon(amo.models.OnChangeMixin, amo.models.ModelBase):
|
|||
@classmethod
|
||||
def from_upload(cls, upload, platforms):
|
||||
from files.utils import parse_addon
|
||||
data = parse_addon(upload.path)
|
||||
data = parse_addon(upload)
|
||||
fields = cls._meta.get_all_field_names()
|
||||
addon = Addon(**dict((k, v) for k, v in data.items() if k in fields))
|
||||
addon.status = amo.STATUS_NULL
|
||||
addon.default_locale = to_language(translation.get_language())
|
||||
if addon.is_webapp():
|
||||
addon.manifest_url = upload.name
|
||||
addon.save()
|
||||
Version.from_upload(upload, addon, platforms)
|
||||
amo.log(amo.LOG.CREATE_ADDON, addon)
|
||||
|
@ -356,6 +378,8 @@ class Addon(amo.models.OnChangeMixin, amo.models.ModelBase):
|
|||
return urls
|
||||
|
||||
def get_url_path(self, impala=False):
|
||||
if settings.IMPALA_ADDON_DETAILS:
|
||||
return reverse('addons.detail', args=[self.slug])
|
||||
u = '%saddons.detail' % ('i_' if impala else '')
|
||||
return reverse(u, args=[self.slug])
|
||||
|
||||
|
@ -541,7 +565,8 @@ class Addon(amo.models.OnChangeMixin, amo.models.ModelBase):
|
|||
self.id, size, int(time.mktime(self.modified.timetuple())))
|
||||
|
||||
def update_status(self, using=None):
|
||||
if self.status == amo.STATUS_NULL or self.is_disabled or self.is_app():
|
||||
if (self.status == amo.STATUS_NULL or self.is_disabled
|
||||
or self.is_webapp() or self.is_persona()):
|
||||
return
|
||||
|
||||
def logit(reason, old=self.status):
|
||||
|
@ -645,7 +670,7 @@ class Addon(amo.models.OnChangeMixin, amo.models.ModelBase):
|
|||
"""
|
||||
Return other addons by the author(s) of this addon
|
||||
"""
|
||||
return (MiniAddon.objects.valid().exclude(id=self.id)
|
||||
return (Addon.objects.valid().exclude(id=self.id)
|
||||
.filter(addonuser__listed=True,
|
||||
authors__in=self.listed_authors).distinct())
|
||||
|
||||
|
@ -706,7 +731,7 @@ class Addon(amo.models.OnChangeMixin, amo.models.ModelBase):
|
|||
def is_persona(self):
|
||||
return self.type == amo.ADDON_PERSONA
|
||||
|
||||
def is_app(self):
|
||||
def is_webapp(self):
|
||||
return self.type == amo.ADDON_WEBAPP
|
||||
|
||||
@property
|
||||
|
@ -730,6 +755,18 @@ class Addon(amo.models.OnChangeMixin, amo.models.ModelBase):
|
|||
def is_incomplete(self):
|
||||
return self.status == amo.STATUS_NULL
|
||||
|
||||
def can_become_premium(self):
|
||||
"""Not all addons can become premium."""
|
||||
return (self.status in amo.PREMIUM_STATUSES
|
||||
and self.highest_status in amo.PREMIUM_STATUSES
|
||||
and self.type in [amo.ADDON_EXTENSION, amo.ADDON_WEBAPP])
|
||||
|
||||
def is_premium(self):
|
||||
return self.premium_type == amo.ADDON_PREMIUM
|
||||
|
||||
def can_be_purchased(self):
|
||||
return self.is_premium() and self.status in amo.REVIEWED_STATUSES
|
||||
|
||||
@classmethod
|
||||
def featured_random(cls, app, lang):
|
||||
return FeaturedManager.featured_ids(app, lang)
|
||||
|
@ -900,14 +937,23 @@ class Addon(amo.models.OnChangeMixin, amo.models.ModelBase):
|
|||
"""For language packs, gets the contents of localepicker."""
|
||||
if (self.type == amo.ADDON_LPAPP and self.status == amo.STATUS_PUBLIC
|
||||
and self.current_version):
|
||||
files = (self.current_version.files
|
||||
.filter(platform__in=amo.MOBILE_PLATFORMS.keys()))
|
||||
try:
|
||||
file = (self.current_version
|
||||
.files.get(platform=amo.PLATFORM_ALL.id))
|
||||
return file.get_localepicker()
|
||||
except File.DoesNotExist:
|
||||
return unicode(files[0].get_localepicker(), 'utf-8')
|
||||
except IndexError:
|
||||
pass
|
||||
return ''
|
||||
|
||||
@amo.cached_property
|
||||
def upsell(self):
|
||||
"""Return the upsell or add-on, or None if there isn't one."""
|
||||
try:
|
||||
# We set unique_together on the model, so there will only be one.
|
||||
return self._upsell_from.all()[0]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
|
||||
@receiver(dbsignals.post_save, sender=Addon,
|
||||
dispatch_uid='addons.update.name.table')
|
||||
|
@ -993,24 +1039,6 @@ def watch_disabled(old_attr={}, new_attr={}, instance=None, sender=None, **kw):
|
|||
f.hide_disabled_file()
|
||||
|
||||
|
||||
class MiniAddonManager(AddonManager):
|
||||
|
||||
def get_query_set(self):
|
||||
qs = super(MiniAddonManager, self).get_query_set()
|
||||
return qs.only_translations()
|
||||
|
||||
|
||||
class MiniAddon(Addon):
|
||||
"""A smaller lightweight version of Addon suitable for the
|
||||
update script or other areas that don't need all the transforms.
|
||||
This class exists to give the addon a different key for cache machine."""
|
||||
|
||||
objects = MiniAddonManager()
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
|
||||
class Persona(caching.CachingMixin, models.Model):
|
||||
"""Personas-specific additions to the add-on model."""
|
||||
addon = models.OneToOneField(Addon)
|
||||
|
@ -1400,3 +1428,21 @@ def freezer(sender, instance, **kw):
|
|||
# Adjust the hotness of the FrozenAddon.
|
||||
if instance.addon_id:
|
||||
Addon.objects.get(id=instance.addon_id).update(hotness=0)
|
||||
|
||||
|
||||
class AddonUpsell(amo.models.ModelBase):
|
||||
free = models.ForeignKey(Addon, related_name='_upsell_from')
|
||||
premium = models.ForeignKey(Addon, related_name='_upsell_to')
|
||||
text = PurifiedField()
|
||||
|
||||
class Meta:
|
||||
db_table = 'addon_upsell'
|
||||
unique_together = ('free', 'premium')
|
||||
|
||||
def __unicode__(self):
|
||||
return u'Free: %s to Premium: %s' % (self.free, self.premium)
|
||||
|
||||
|
||||
# webapps.models imports addons.models to get Addon, so we need to keep the
|
||||
# Webapp import down here.
|
||||
from webapps.models import Webapp
|
||||
|
|
|
@ -20,7 +20,7 @@ def extract(addon):
|
|||
"""Extract indexable attributes from an add-on."""
|
||||
attrs = ('id', 'created', 'last_updated', 'weekly_downloads',
|
||||
'bayesian_rating', 'average_daily_users', 'status', 'type',
|
||||
'is_disabled')
|
||||
'is_disabled', 'premium_type')
|
||||
d = dict(zip(attrs, attrgetter(*attrs)(addon)))
|
||||
# Coerce the Translation into a string.
|
||||
d['name_sort'] = unicode(addon.name).lower()
|
||||
|
@ -43,10 +43,10 @@ def extract(addon):
|
|||
d['app'] = [app.id for app in addon.compatible_apps.keys()]
|
||||
# Boost by the number of users on a logarithmic scale. The maximum boost
|
||||
# (11,000,000 users for adblock) is about 5x.
|
||||
d['_boost'] = addon.average_daily_users ** .1
|
||||
d['_boost'] = addon.average_daily_users ** .2
|
||||
# Double the boost if the add-on is public.
|
||||
if addon.status == amo.STATUS_PUBLIC:
|
||||
d['_boost'] = max(d['_boost'], 1) * 2
|
||||
d['_boost'] = max(d['_boost'], 1) * 4
|
||||
return d
|
||||
|
||||
|
||||
|
|
|
@ -7,9 +7,10 @@ from django.db import connection, transaction
|
|||
|
||||
from celeryutils import task
|
||||
import elasticutils
|
||||
from PIL import Image
|
||||
|
||||
import amo
|
||||
from amo.decorators import write
|
||||
from amo.decorators import set_modified_on, write
|
||||
from amo.utils import sorted_groupby
|
||||
from tags.models import Tag
|
||||
from translations.models import Translation
|
||||
|
@ -40,8 +41,10 @@ def update_last_updated(addon_id):
|
|||
else:
|
||||
q = 'exp'
|
||||
qs = queries[q].filter(pk=addon_id).using('default')
|
||||
pk, t = qs.values_list('id', 'last_updated')[0]
|
||||
Addon.objects.filter(pk=pk).update(last_updated=t)
|
||||
res = qs.values_list('id', 'last_updated')
|
||||
if res:
|
||||
pk, t = res[0]
|
||||
Addon.objects.filter(pk=pk).update(last_updated=t)
|
||||
|
||||
|
||||
@transaction.commit_on_success
|
||||
|
@ -135,7 +138,8 @@ def attach_translations(addons):
|
|||
ids.update((getattr(addon, field.attname, None), addon)
|
||||
for field in fields)
|
||||
ids.pop(None, None)
|
||||
qs = (Translation.objects.filter(id__in=ids, localized_string__isnull=False)
|
||||
qs = (Translation.objects
|
||||
.filter(id__in=ids, localized_string__isnull=False)
|
||||
.values_list('id', 'locale', 'localized_string'))
|
||||
for id, translations in sorted_groupby(qs, lambda x: x[0]):
|
||||
ids[id].translations[id] = [(locale, string)
|
||||
|
@ -157,3 +161,52 @@ def unindex_addons(ids, **kw):
|
|||
for addon in ids:
|
||||
log.info('Removing addon [%s] from search index.' % addon)
|
||||
Addon.unindex(addon)
|
||||
|
||||
|
||||
@task
|
||||
def delete_persona_image(dst, **kw):
|
||||
log.info('[1@None] Deleting persona image: %s.' % dst)
|
||||
if not dst.startswith(settings.PERSONAS_PATH):
|
||||
log.error("Someone tried deleting something they shouldn't: %s" % dst)
|
||||
return
|
||||
try:
|
||||
os.remove(dst)
|
||||
except Exception, e:
|
||||
log.error('Error deleting persona image: %s' % e)
|
||||
|
||||
|
||||
@task
|
||||
@set_modified_on
|
||||
def create_persona_preview_image(src, dst, img_basename, **kw):
|
||||
"""Creates a 680x100 thumbnail used for the Persona preview."""
|
||||
log.info('[1@None] Resizing persona image: %s' % dst)
|
||||
if not os.path.exists(dst):
|
||||
os.makedirs(dst)
|
||||
try:
|
||||
preview, full = amo.PERSONA_IMAGE_SIZES['header']
|
||||
new_w, new_h = preview
|
||||
orig_w, orig_h = full
|
||||
i = Image.open(src)
|
||||
# Crop image from the right.
|
||||
i = i.crop((orig_w - (new_h * 2), 0, orig_w, orig_h))
|
||||
i = i.resize(preview, Image.ANTIALIAS)
|
||||
i.load()
|
||||
i.save(os.path.join(dst, img_basename))
|
||||
return True
|
||||
except Exception, e:
|
||||
log.error('Error saving persona image: %s' % e)
|
||||
|
||||
|
||||
@task
|
||||
@set_modified_on
|
||||
def save_persona_image(src, dst, img_basename, **kw):
|
||||
"""Creates a JPG of a Persona header/footer image."""
|
||||
log.info('[1@None] Saving persona image: %s' % dst)
|
||||
if not os.path.exists(dst):
|
||||
os.makedirs(dst)
|
||||
try:
|
||||
i = Image.open(src)
|
||||
i.save(os.path.join(dst, img_basename))
|
||||
return True
|
||||
except Exception, e:
|
||||
log.error('Error saving persona image: %s' % e)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
{% from "addons/macros.html" import vital %}
|
||||
{% cache addons %}
|
||||
<ul class="listing-grid c {{ columns }}">
|
||||
{% for page in pages %}
|
||||
|
@ -6,37 +5,7 @@
|
|||
<section>
|
||||
{% for addon in page %}
|
||||
<li>
|
||||
<div class="item addon">
|
||||
<a href="{{ addon.get_url_path(impala=True)|urlparams(src=dl_src) }}">
|
||||
<div class="icon">
|
||||
{% if first_page %}
|
||||
<img src="{{ addon.icon_url }}">
|
||||
{% else %}
|
||||
<img data-defer-src="{{ addon.icon_url }}"
|
||||
src="{{ media('img/addon-icons/default-32.png') }}">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="summary">
|
||||
<h3>{{ addon.name }}</h3>
|
||||
{% with cat = addon.get_category(APP.id) %}
|
||||
{% if cat %}
|
||||
<div class="category more-info">{{ cat }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{{ vital(addon, vital_summary) }}
|
||||
</div>
|
||||
</a>
|
||||
<div class="more">
|
||||
{{ install_button(addon, impala=True, src=src) }}
|
||||
{{ addon.summary|truncate(250)|nl2br }}
|
||||
<div class="byline">
|
||||
{% trans users=users_list(addon.listed_authors, size=2) %}
|
||||
by {{ users }}
|
||||
{% endtrans %}
|
||||
</div>
|
||||
{{ vital(addon, vital_more) }}
|
||||
</div>
|
||||
</div>
|
||||
{{ addon|addon_hovercard(lazyload=first_page) }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
{% from "addons/macros.html" import vital %}
|
||||
<div class="addon hovercard">
|
||||
<a href="{{ addon.get_url_path(impala=True)|urlparams(src=dl_src) }}">
|
||||
<div class="icon">
|
||||
{% if lazyload %}
|
||||
<img src="{{ addon.icon_url }}">
|
||||
{% else %}
|
||||
<img data-defer-src="{{ addon.icon_url }}"
|
||||
src="{{ media('img/addon-icons/default-32.png') }}">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="summary">
|
||||
<h3>{{ addon.name }}</h3>
|
||||
{% with cat = addon.get_category(APP.id) %}
|
||||
{% if cat %}
|
||||
<div class="category more-info">{{ cat }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{{ vital(addon, vital_summary) }}
|
||||
</div>
|
||||
</a>
|
||||
<div class="more">
|
||||
{{ install_button(addon, impala=True) }}
|
||||
{{ addon.summary|truncate(250)|nl2br }}
|
||||
<div class="byline">
|
||||
{% trans users=users_list(addon.listed_authors, size=2) %}
|
||||
by {{ users }}
|
||||
{% endtrans %}
|
||||
</div>
|
||||
{{ vital(addon, vital_more) }}
|
||||
</div>
|
||||
</div>
|
|
@ -30,8 +30,7 @@
|
|||
{# L10n: Click Contribute button OR Install button #}
|
||||
<span class="continue">{{ _('or') }}</span>
|
||||
{% set ver = version or None %}
|
||||
{{ install_button(addon, impala=True, version=ver, show_contrib=False,
|
||||
src=src) }}
|
||||
{{ install_button(addon, impala=True, version=ver, show_contrib=False) }}
|
||||
{% endif %}
|
||||
</div>{# /aux #}
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
so we pull in reviews and links to other add-ons with js.
|
||||
This view returns a chunk of HTML that's injected into the DOM. #}
|
||||
{% set version = addon.current_version %}
|
||||
{% set reviews = reviews[:3] %}
|
||||
|
||||
<aside class="secondary addon-reviews c">
|
||||
{% if reviews %}
|
||||
<div>
|
||||
|
@ -37,7 +39,7 @@
|
|||
<ul>
|
||||
{% for category in categories %}
|
||||
<li>
|
||||
<a href="{{ category.get_url_path(impala=True) }}">
|
||||
<a href="{{ category.get_url_path() }}">
|
||||
{{ category }}
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ impala_breadcrumbs([(addon.type_url(impala=True), amo.ADDON_TYPES[addon.type]),
|
||||
{{ impala_breadcrumbs([(addon.type_url(), amo.ADDON_TYPES[addon.type]),
|
||||
(None, addon.name)]) }}
|
||||
<aside class="secondary addon-vitals">
|
||||
{{ addon.average_rating|stars(large=True) }}
|
||||
|
@ -50,7 +50,7 @@
|
|||
{% endif %}
|
||||
</aside>
|
||||
|
||||
{% set version = addon.current_version if not addon.is_app() else None %}
|
||||
{% set version = addon.current_version if not addon.is_webapp() else None %}
|
||||
|
||||
{# All this depends on the addon or version, and nothing needs the user,
|
||||
so we can cache it all against the addon. #}
|
||||
|
@ -72,8 +72,7 @@
|
|||
</hgroup>
|
||||
<p id="addon-summary" {{ addon.summary|locale_html }}>{{ addon.summary|nl2br }}</p>
|
||||
{% if version %}
|
||||
{{ big_install_button(addon, show_warning=False,
|
||||
impala=True, src='dp-btn-primary') }}
|
||||
{{ big_install_button(addon, show_warning=False, impala=True) }}
|
||||
{% endif %}
|
||||
{% if addon.is_featured(APP, LANG) %}
|
||||
<div class="banner-box">
|
||||
|
@ -166,7 +165,7 @@
|
|||
<li>{{ version.created|datetime }}</li>
|
||||
<li>
|
||||
{% if version.license %}
|
||||
{% trans url = version.license_url(impala=True),
|
||||
{% trans url = version.license_url(),
|
||||
name = version.license if version.license.builtin else _('Custom License') %}
|
||||
Released under <a href="{{ url }}">{{ name }}</a>
|
||||
{% endtrans %}
|
||||
|
@ -178,14 +177,16 @@
|
|||
|
||||
<section class="primary island c">
|
||||
<h2>{{ _('About this Add-on') }}</h2>
|
||||
<div class="prose">
|
||||
{% if addon.description %}
|
||||
<p id="addon-description" {{ addon.description|locale_html }}>{{ addon.description|nl2br }}</p>
|
||||
<p id="addon-description" class="prose" {{ addon.description|locale_html }}>{{ addon.description|nl2br }}</p>
|
||||
{% else %}
|
||||
<p id="addon-description" {{ addon.description|locale_html }}>{{ addon.summary|nl2br }}</p>
|
||||
<p id="addon-description" class="prose" {{ addon.description|locale_html }}>{{ addon.summary|nl2br }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="more-webpage" class="primary c" data-more-url="{{ addon.get_url_path(impala=True) }}"></div>
|
||||
<div id="more-webpage" class="primary c" data-more-url="{{ url('addons.detail_more', addon.slug) }}"></div>
|
||||
|
||||
{% if version or addon.developer_comments or addon.show_beta %}
|
||||
<section class="primary island c">
|
||||
|
@ -202,7 +203,7 @@
|
|||
{% if version %}
|
||||
<section id="detail-relnotes" class="expando">
|
||||
<h2>{{ _('Version Information') }}<a class="toggle" href="#detail-relnotes"><b></b></a></h2>
|
||||
<div class="content">
|
||||
<div class="content prose">
|
||||
{{ version_detail(addon, version, src="addon-detail-version", impala=True) }}
|
||||
</div>
|
||||
</section>
|
||||
|
@ -233,7 +234,7 @@
|
|||
addon.current_beta_version.version) }}</dt>
|
||||
<dd>{{ install_button(addon, version=addon.current_beta_version,
|
||||
show_warning=False, impala=True,
|
||||
src='dp-btn-devchannel') }}</dd>
|
||||
src=request.GET.get('src', 'dp-btn-devchannel')) }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -52,8 +52,8 @@
|
|||
{% if page == 'installed' %}
|
||||
<h1 class="addon"{{ addon.name|locale_html }}>{{ title }}</h1>
|
||||
{% else %}
|
||||
{{ impala_breadcrumbs([(addon.type_url(), amo.ADDON_TYPES[addon.type]),
|
||||
(addon.get_url_path(), addon.name),
|
||||
{{ impala_breadcrumbs([(addon.type_url(impala=True), amo.ADDON_TYPES[addon.type]),
|
||||
(addon.get_url_path(impala=True), addon.name),
|
||||
(None, title)]) }}
|
||||
<h1>{{ title }}</h1>
|
||||
{% endif %}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
{% extends "impala/base.html" %}
|
||||
|
||||
{% block title %}{{ page_title(addon.name) }}{% endblock %}
|
||||
{% block bodyclass %}gutter{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ impala_breadcrumbs([(addon.type_url(impala=True), amo.ADDON_TYPES[addon.type]),
|
||||
(None, addon.name)]) }}
|
||||
|
||||
<aside class="secondary">
|
||||
{{ addon|sidebar_listing }}
|
||||
</aside>
|
||||
|
||||
<div class="primary">
|
||||
<div class="notification-box error">
|
||||
{% if addon.disabled_by_user %}
|
||||
{{ _('This add-on has been removed by its author.') }}
|
||||
{% elif addon.status == amo.STATUS_DISABLED %}
|
||||
{{ _('This add-on has been disabled by an administrator.') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
|
@ -6,10 +6,10 @@
|
|||
{{ page_title(_('End-User License Agreement for {0}')|f(addon.name)) }}
|
||||
{% endblock %}
|
||||
|
||||
{% set detail_url = url('i_addons.detail', addon.slug) %}
|
||||
{% set detail_url = addon.get_url_path(impala=True) %}
|
||||
|
||||
{% block primary %}
|
||||
<section class="primary">
|
||||
<section class="primary" id="eula">
|
||||
<hgroup class="hero">
|
||||
{# L10n: EULA stand for End User License Agreement #}
|
||||
{{ impala_breadcrumbs([(addon.type_url(impala=True), amo.ADDON_TYPES[addon.type]),
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<section>
|
||||
{% for addon in page %}
|
||||
<li>
|
||||
<div class="item addon">
|
||||
<div class="hovercard addon">
|
||||
<a href="{{ addon.get_url_path(impala=True)|urlparams(src=dl_src) }}">
|
||||
<div class="summary">
|
||||
<h3>{{ addon.name }}</h3>
|
||||
|
@ -21,7 +21,7 @@
|
|||
</div>
|
||||
</a>
|
||||
<div class="more">
|
||||
{{ install_button(addon, impala=True, src=src) }}
|
||||
{{ install_button(addon, impala=True) }}
|
||||
{{ addon.summary|truncate(250)|nl2br }}
|
||||
<div class="byline">
|
||||
{% trans users=users_list(addon.listed_authors, size=2) %}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<hgroup class="hero">
|
||||
{# L10n: The License for this add-on. #}
|
||||
{{ impala_breadcrumbs([(addon.type_url(impala=True), amo.ADDON_TYPES[addon.type]),
|
||||
(url('i_addons.detail', addon.slug), addon.name),
|
||||
(addon.get_url_path(impala=True), addon.name),
|
||||
(None, _('License'))]) }}
|
||||
{{ addon_heading(addon, version) }}
|
||||
</hgroup>
|
||||
|
|
|
@ -21,16 +21,16 @@
|
|||
{{ impala_reviews_link(addon) }}
|
||||
<div class="adu">
|
||||
{% if addon.type == amo.ADDON_SEARCH %}
|
||||
{% with num=addon.average_daily_users %}
|
||||
{# L10n: {0} is the number of users. #}
|
||||
{{ ngettext('{0} user', '{0} users', num)|f(num|numberfmt) }}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with num=addon.weekly_downloads %}
|
||||
{# L10n: {0} is the number of downloads. #}
|
||||
{{ ngettext('{0} weekly download', '{0} weekly downloads',
|
||||
num)|f(num|numberfmt) }}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with num=addon.average_daily_users %}
|
||||
{# L10n: {0} is the number of users. #}
|
||||
{{ ngettext('{0} user', '{0} users', num)|f(num|numberfmt) }}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if show_date in ('created', 'new', 'newest', 'updated') %}
|
||||
|
@ -53,5 +53,7 @@
|
|||
{{ install_button(addon, impala=True, collection=collection) }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% include 'search/no_results.html' %}
|
||||
{% endfor %}
|
||||
{% endcache %}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<section>
|
||||
{% for addon in page %}
|
||||
<li>
|
||||
<div class="persona item">
|
||||
<div class="persona hovercard">
|
||||
<a href="{{ addon.get_url_path() }}">
|
||||
<div class="persona-preview">
|
||||
<img {{ 'src' if first_page else 'data-defer-src' }}="{{ addon.persona.thumb_url }}"
|
||||
|
|
|
@ -0,0 +1,211 @@
|
|||
{% set addon_type = amo.ADDON_PERSONA %}
|
||||
{% extends "impala/base_side_categories.html" %}
|
||||
|
||||
{% from "includes/forms.html" import pretty_field, tip %}
|
||||
{% from "devhub/includes/macros.html" import some_html_tip %}
|
||||
|
||||
{% set title = _('Create a New Persona') %}
|
||||
|
||||
{% block title %}{{ page_title(title) }}{% endblock %}
|
||||
|
||||
{% block primary %}
|
||||
<section class="primary">
|
||||
{{ impala_breadcrumbs([(url('browse.personas'), _('Personas')),
|
||||
(None, _('Create'))]) }}
|
||||
<h1>{{ title }}</h1>
|
||||
{% include "messages.html" %}
|
||||
<div class="island hero prettyform" id="submit-persona">
|
||||
<form action="" method="post">
|
||||
{{ csrf() }}
|
||||
<fieldset>
|
||||
<legend>{{ _('Persona Details') }}</legend>
|
||||
<ul>
|
||||
{{ pretty_field(form.name, label=_('Give your Persona a name.'), req=True) }}
|
||||
{{ pretty_field(form.category, label=_('Select the category that best describes your Persona.'),
|
||||
req=True, class='row radios addon-cats') }}
|
||||
<li class="row">
|
||||
{{ pretty_field(form.tags, label=_('Add some tags to describe your Persona.'), tag=None, opt=True) }}
|
||||
<span class="note">
|
||||
{{ ngettext('Comma-separated, minimum of {0} character.',
|
||||
'Comma-separated, minimum of {0} characters.',
|
||||
amo.MIN_TAG_LENGTH)|f(amo.MIN_TAG_LENGTH) }}
|
||||
{{ _('Example: pop, hen, yum. Limit 20.') }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="row c">
|
||||
{{ pretty_field(form.summary, label=_('Describe your Persona.'),
|
||||
tooltip=_("A short explanation of your add-on's
|
||||
basic functionality that is displayed in
|
||||
search and browse listings, as well as at
|
||||
the top of your add-on's details page."),
|
||||
tag=None, opt=True) }}
|
||||
<div class="note">
|
||||
{{ some_html_tip() }}
|
||||
<span class="char-count" data-for="{{ form.summary.auto_id }}"
|
||||
data-maxlength="{{ form.summary.field.max_length }}"></span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{{ _('Persona License') }}</legend>
|
||||
<div id="cc-chooser"{% if form.license.value() %} class="hidden"{% endif %}>
|
||||
{{ form.license }}
|
||||
{{ form.license.errors }}
|
||||
<h3>{{ _("Can others share your Persona, as long as you're given credit?") }}</h3>
|
||||
<ul class="radios">
|
||||
<li>
|
||||
<label>
|
||||
<input type="radio" name="cc-attrib" value="0">
|
||||
{{ _('Yes') }}
|
||||
{{ tip(None, _('The licensor permits others to copy, distribute, '
|
||||
'display, and perform the work, including for '
|
||||
'commercial purposes.')) }}
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label>
|
||||
<input type="radio" name="cc-attrib" value="1">
|
||||
{{ _('No') }}
|
||||
{{ tip(None, _('The licensor permits others to copy, distribute, '
|
||||
'display, and perform the work for non-commercial '
|
||||
'purposes only.')) }}
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ _('Can others make commercial use of your Persona?') }}</h3>
|
||||
<ul class="radios">
|
||||
<li>
|
||||
<label>
|
||||
<input type="radio" name="cc-noncom" value="0">
|
||||
{{ _('Yes') }}
|
||||
{{ tip(None, _('The licensor permits others to copy, distribute, '
|
||||
'display, and perform the work, including for '
|
||||
'commercial purposes.')) }}
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label>
|
||||
<input type="radio" name="cc-noncom" value="1">
|
||||
{{ _('No') }}
|
||||
{{ tip(None, _('The licensor permits others to copy, distribute, '
|
||||
'display, and perform the work for non-commercial '
|
||||
'purposes only.')) }}
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ _('Can others create derivative works from your Persona?') }}</h3>
|
||||
<ul class="radios">
|
||||
<li>
|
||||
<label>
|
||||
<input type="radio" name="cc-noderiv" value="0">
|
||||
{{ _('Yes') }}
|
||||
{{ tip(None, _('The licensor permits others to copy, distribute, '
|
||||
'display and perform the work, as well as make '
|
||||
'derivative works based on it.')) }}
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label>
|
||||
<input type="radio" name="cc-noderiv" value="1">
|
||||
{{ _('Yes, as long as they share alike') }}
|
||||
{{ tip(None, _('The licensor permits others to distribute derivative'
|
||||
'works only under the same license or one compatible '
|
||||
"with the one that governs the licensor's work.")) }}
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label>
|
||||
<input type="radio" name="cc-noderiv" value="2">
|
||||
{{ _('No') }}
|
||||
{{ tip(None, _('The licensor permits others to copy, distribute and '
|
||||
'transmit only unaltered copies of the work — not '
|
||||
'derivative works based on it.')) }}
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
<div id="persona-license">
|
||||
<p>{{ _('Your Persona will be released under the following license:') }}</p>
|
||||
<p id="cc-license" class="license icon"></p>
|
||||
<p class="select-license">
|
||||
<a href="#">{{ _('Select a different license.') }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="persona-license-list"{% if not form.license.value() %} class="hidden"{% endif %}>
|
||||
<h3>{{ _('Select a license for your Persona.') }}</h3>
|
||||
<ul class="radios">
|
||||
{% for license in amo.PERSONA_LICENSES %}
|
||||
<li><label><input type="radio" name="license" value="{{ license.id }}">
|
||||
{{ license.name }}</label></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset id="persona-design">
|
||||
<legend>{{ _('Persona Design') }}</legend>
|
||||
<ul>
|
||||
<li id="persona-header" class="row">
|
||||
{{ pretty_field(form.header, label=_('Select a header image for your Persona.'),
|
||||
tag=None, req=True) }}
|
||||
{{ form.header_hash }}
|
||||
{{ form.header_hash.errors }}
|
||||
<ul class="note">
|
||||
<li>{{ _('3000 × 200 pixels') }}</li>
|
||||
<li>{{ _('300 KB max') }}</li>
|
||||
<li>{{ _('PNG or JPG') }}</li>
|
||||
<li>{{ _('Aligned to top-right') }}</li>
|
||||
</ul>
|
||||
<ul class="errorlist"></ul>
|
||||
<img class="preview">
|
||||
<a href="#" class="reset">
|
||||
{{ _('Select a different header image') }}</a>
|
||||
</li>
|
||||
<li id="persona-footer" class="row">
|
||||
{{ pretty_field(form.footer, label=_('Select a footer image for your Persona.'),
|
||||
tag=None, req=True) }}
|
||||
{{ form.footer_hash }}
|
||||
{{ form.footer_hash.errors }}
|
||||
<ul class="note">
|
||||
<li>{{ _('3000 × 100 pixels') }}</li>
|
||||
<li>{{ _('300 KB max') }}</li>
|
||||
<li>{{ _('PNG or JPG') }}</li>
|
||||
<li>{{ _('Aligned to bottom-left') }}</li>
|
||||
</ul>
|
||||
<ul class="errorlist"></ul>
|
||||
<img class="preview">
|
||||
<a href="#" class="reset">
|
||||
{{ _('Select a different footer image') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ _('Select colors for your Persona.') }}</h3>
|
||||
<ul class="colors">
|
||||
{{ pretty_field(form.textcolor, label=_('Foreground Text'),
|
||||
tooltip=_('This is the color of the tab text.')) }}
|
||||
{{ pretty_field(form.accentcolor, label=_('Background'),
|
||||
tooltip=_('This is the color of the tabs.')) }}
|
||||
</ul>
|
||||
</fieldset>
|
||||
<fieldset id="persona-preview">
|
||||
<legend>{{ _('Preview') }}</legend>
|
||||
<div class="persona persona-large persona-preview">
|
||||
<div class="persona-viewer" data-browsertheme>
|
||||
<div class="details">
|
||||
<span class="title" id="persona-preview-name">
|
||||
{{ _("Your Persona's Name") }}</span>
|
||||
<span class="author">
|
||||
{% set amo_user = request.amo_user %}
|
||||
{% trans user=amo_user.username,
|
||||
profile_url=url('users.profile', amo_user.id) %}
|
||||
by <a href="{{ profile_url }}" target="_blank">{{ user }}</a>
|
||||
{% endtrans %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<p><button>{{ _('Submit Persona') }}</button></p>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -0,0 +1,40 @@
|
|||
{% set addon_type = amo.ADDON_PERSONA %}
|
||||
{% extends "impala/base_side_categories.html" %}
|
||||
|
||||
{% block title %}{{ page_title(_('Submission Complete')) }}{% endblock %}
|
||||
|
||||
{% block primary %}
|
||||
<section class="primary">
|
||||
<h1>{{ _("You're done!") }}</h1>
|
||||
<div class="prose">
|
||||
<p>
|
||||
{% trans %}
|
||||
Your Persona has been submitted to the Review queue.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
{# TODO(cvan): The following is a lie. Personas cannot be installed before being reviewed - yet. #}
|
||||
<p>
|
||||
{% trans %}
|
||||
You'll receive an email once your Persona has been reviewed by an editor.
|
||||
In the meantime, you and your friends can install the Persona directly
|
||||
from its details page:
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<p>
|
||||
<a id="submitted-addon-url" href="{{ addon.get_url_path() }}">
|
||||
{{ addon.get_url_path()|absolutify|display_url }}</a>
|
||||
</p>
|
||||
<h2>{{ _('Next steps:') }}</h2>
|
||||
<ul class="done-next-steps">
|
||||
{# TODO(cvan): This should lead to the Persona Edit page. #}
|
||||
{% set edit_url = url('devhub.addons.edit', addon.slug) %}
|
||||
<li>{{ _('Provide more details about your Persona by <a href="{0}">editing its listing</a>.')|f(edit_url)|safe }}</li>
|
||||
{% set profile_url = url('devhub.addons.profile', addon.slug) %}
|
||||
<li>{{ _('Tell your users why you created this Persona in your <a href="{0}">Developer Profile</a>.')|f(profile_url)|safe }}</li>
|
||||
{% set feed_url = url('devhub.feed', addon.slug) %}
|
||||
<li>{{ _('View and subscribe to your Persona\'s <a href="{0}">activity feed</a> to stay updated on reviews, collections, and more.')|f(feed_url)|safe }}</li>
|
||||
<li>{{ _('View approximate review queue <a href="{0}">wait times</a>.')|f('https://forums.addons.mozilla.org/viewforum.php?f=21')|safe }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -7,7 +7,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% set version = addon.current_version %}
|
||||
{% set detail_url = url('i_addons.detail', addon.slug) %}
|
||||
{% set detail_url = addon.get_url_path(impala=True) %}
|
||||
|
||||
{% block primary %}
|
||||
<section class="primary">
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
{{ csrf() }}
|
||||
{{ field(review_form.body, _('Review:'), **attrs) }}
|
||||
{{ field(review_form.rating, _('Rating:'), **attrs) }}
|
||||
<input type="submit" value="{{ _('Submit review') }}" {{ attrs|xmlattr }}>
|
||||
<p><input type="submit" value="{{ _('Submit review') }}" {{ attrs|xmlattr }}></p>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
|
|
|
@ -40,10 +40,12 @@
|
|||
{% else %}
|
||||
<div class="review no-reviews">
|
||||
<h3>
|
||||
{% trans url = url('reviews.add', addon.slug) %}
|
||||
This add-on has not yet been reviewed.
|
||||
<a href="{{ url }}">Be the first!</a>
|
||||
{% endtrans %}
|
||||
{{ _('This add-on has not yet been reviewed.') }}
|
||||
{% if not addon.has_author(amo_user) %}
|
||||
<a href="{{ url('reviews.add', addon.slug) }}">
|
||||
{{ ('Be the first!') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime, timedelta
|
||||
import itertools
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from urlparse import urlparse
|
||||
|
||||
from django import forms
|
||||
|
@ -19,8 +20,9 @@ from amo import set_user
|
|||
from amo.helpers import absolutify
|
||||
from amo.signals import _connect, _disconnect
|
||||
from addons.models import (Addon, AddonCategory, AddonDependency,
|
||||
AddonRecommendation, AddonType, BlacklistedGuid,
|
||||
Category, Charity, FrozenAddon, Persona, Preview)
|
||||
AddonRecommendation, AddonType, AddonUpsell,
|
||||
BlacklistedGuid, Category, Charity, FrozenAddon,
|
||||
Persona, Preview)
|
||||
from applications.models import Application, AppVersion
|
||||
from devhub.models import ActivityLog
|
||||
from files.models import File, Platform
|
||||
|
@ -30,6 +32,7 @@ from translations.models import TranslationSequence, Translation
|
|||
from users.models import UserProfile
|
||||
from versions.models import ApplicationsVersions, Version
|
||||
from versions.compare import version_int
|
||||
from webapps.models import Webapp
|
||||
|
||||
|
||||
class TestAddonManager(amo.tests.TestCase):
|
||||
|
@ -132,6 +135,27 @@ class TestAddonManagerFeatured(amo.tests.TestCase):
|
|||
assert not f.exists()
|
||||
|
||||
|
||||
class TestNewAddonVsWebapp(amo.tests.TestCase):
|
||||
|
||||
def test_addon_from_kwargs(self):
|
||||
a = Addon(type=amo.ADDON_EXTENSION)
|
||||
assert isinstance(a, Addon)
|
||||
|
||||
def test_webapp_from_kwargs(self):
|
||||
w = Addon(type=amo.ADDON_WEBAPP)
|
||||
assert isinstance(w, Webapp)
|
||||
|
||||
def test_addon_from_db(self):
|
||||
a = Addon.objects.create(type=amo.ADDON_EXTENSION)
|
||||
assert isinstance(a, Addon)
|
||||
assert isinstance(Addon.objects.get(id=a.id), Addon)
|
||||
|
||||
def test_webapp_from_db(self):
|
||||
a = Addon.objects.create(type=amo.ADDON_WEBAPP)
|
||||
assert isinstance(a, Webapp)
|
||||
assert isinstance(Addon.objects.get(id=a.id), Webapp)
|
||||
|
||||
|
||||
class TestAddonModels(amo.tests.TestCase):
|
||||
fixtures = ['base/apps',
|
||||
'base/collections',
|
||||
|
@ -1299,6 +1323,14 @@ class TestAddonFromUpload(UploadTest):
|
|||
eq_(addon.description, None)
|
||||
eq_(addon.slug, 'xpi-name')
|
||||
|
||||
def test_manifest_url(self):
|
||||
path = os.path.join(settings.ROOT,
|
||||
'apps/devhub/tests/addons/mozball.webapp')
|
||||
upload = self.get_upload(abspath=path)
|
||||
addon = Addon.from_upload(upload, [self.platform])
|
||||
assert addon.is_webapp()
|
||||
eq_(addon.manifest_url, upload.name)
|
||||
|
||||
def test_xpi_version(self):
|
||||
addon = Addon.from_upload(self.get_upload('extension.xpi'),
|
||||
[self.platform])
|
||||
|
@ -1499,6 +1531,10 @@ class TestSearchSignals(amo.tests.ESTestCase):
|
|||
|
||||
class TestLanguagePack(TestLanguagePack):
|
||||
|
||||
def setUp(self):
|
||||
super(TestLanguagePack, self).setUp()
|
||||
self.platform = Platform.objects.create(id=amo.PLATFORM_ANDROID.id)
|
||||
|
||||
def test_extract(self):
|
||||
File.objects.create(platform=self.platform, version=self.version,
|
||||
filename=self.xpi_path('langpack-localepicker'))
|
||||
|
@ -1521,3 +1557,57 @@ class TestLanguagePack(TestLanguagePack):
|
|||
File.objects.create(platform=self.mac, version=self.version,
|
||||
filename=self.xpi_path('langpack'))
|
||||
eq_(self.addon.get_localepicker(), '')
|
||||
|
||||
|
||||
class TestMarketplace(amo.tests.ESTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.addon = Addon(type=amo.ADDON_EXTENSION)
|
||||
|
||||
def test_is_premium(self):
|
||||
assert not self.addon.is_premium()
|
||||
self.addon.update(premium_type=amo.ADDON_PREMIUM)
|
||||
assert self.addon.is_premium()
|
||||
|
||||
def test_can_be_premium_status(self):
|
||||
for status in amo.STATUS_CHOICES.keys():
|
||||
self.addon.update(status=status)
|
||||
if status in amo.PREMIUM_STATUSES:
|
||||
assert self.addon.can_become_premium()
|
||||
else:
|
||||
assert not self.addon.can_become_premium()
|
||||
|
||||
def test_can_be_premium_type(self):
|
||||
for type in amo.ADDON_TYPES.keys():
|
||||
self.addon.update(type=type)
|
||||
if type in [amo.ADDON_EXTENSION, amo.ADDON_WEBAPP]:
|
||||
assert self.addon.can_become_premium()
|
||||
else:
|
||||
assert not self.addon.can_become_premium()
|
||||
|
||||
def test_can_not_be_purchased(self):
|
||||
assert not self.addon.can_be_purchased()
|
||||
|
||||
def test_can_still_not_be_purchased(self):
|
||||
self.addon.update(premium_type=amo.ADDON_PREMIUM)
|
||||
assert not self.addon.can_be_purchased()
|
||||
|
||||
def test_can_be_purchased(self):
|
||||
for status in amo.REVIEWED_STATUSES:
|
||||
self.addon.update(premium_type=amo.ADDON_PREMIUM,
|
||||
status=status)
|
||||
assert self.addon.can_be_purchased()
|
||||
|
||||
class TestAddonUpsell(amo.tests.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.one = Addon.objects.create(type=amo.ADDON_EXTENSION, name='free')
|
||||
self.two = Addon.objects.create(type=amo.ADDON_EXTENSION,
|
||||
name='premium')
|
||||
self.upsell = AddonUpsell.objects.create(free=self.one,
|
||||
premium=self.two, text='yup')
|
||||
|
||||
def test_create_upsell(self):
|
||||
eq_(self.one.upsell.premium, self.two)
|
||||
eq_(self.one.upsell.text, 'yup')
|
||||
eq_(self.two.upsell, None)
|
||||
|
|
|
@ -9,7 +9,7 @@ import amo.tests
|
|||
class TestFeaturedManager(amo.tests.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
patcher = mock.patch('addons.utils.FeaturedManager.get_objects')
|
||||
patcher = mock.patch('addons.utils.FeaturedManager._get_objects')
|
||||
self.objects_mock = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
|
@ -21,7 +21,7 @@ class TestFeaturedManager(amo.tests.TestCase):
|
|||
(3, 9, None, 1), # A different type.
|
||||
(4, 1, 'ja', 1), # Restricted locale.
|
||||
(5, 1, 'ja', 1),
|
||||
(5, 1, 'en-US', 1), # Same add-on, different locale.
|
||||
(5, 1, 'en-Us', 1), # Same add-on, different locale.
|
||||
(6, 1, None, 18), # Different app.
|
||||
]
|
||||
self.objects_mock.return_value = [dict(zip(self.fields, v))
|
||||
|
@ -60,20 +60,22 @@ class TestFeaturedManager(amo.tests.TestCase):
|
|||
class TestCreaturedManager(amo.tests.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
patcher = mock.patch('addons.utils.CreaturedManager.get_objects')
|
||||
patcher = mock.patch('addons.utils.CreaturedManager._get_objects')
|
||||
self.objects_mock = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
self.category = mock.Mock()
|
||||
self.category.id = 1
|
||||
self.category.application_id = 1
|
||||
|
||||
self.fields = ['category', 'addon', 'feature_locales']
|
||||
self.fields = ['category', 'addon', 'locales', 'app']
|
||||
self.values = [
|
||||
(1, 1, None), # No locales.
|
||||
(1, 2, ''), # Make sure empty string is ok.
|
||||
(2, 3, None), # Something from a different category.
|
||||
(1, 4, 'ja'), # Check locales with no comma.
|
||||
(1, 5, 'ja,en'), # Locales with a comma.
|
||||
(1, 1, None, 1), # No locales.
|
||||
(1, 2, '', 1), # Make sure empty string is ok.
|
||||
(2, 3, None, 1), # Something from a different category.
|
||||
(1, 4, 'JA', 1), # Check locales with no comma.
|
||||
(1, 5, 'ja,en', 1), # Locales with a comma.
|
||||
(1, 6, '', 9), # Make sure empty string is ok.
|
||||
]
|
||||
self.objects_mock.return_value = [dict(zip(self.fields, v))
|
||||
for v in self.values]
|
||||
|
|
|
@ -3,6 +3,7 @@ from cStringIO import StringIO
|
|||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
from django import test
|
||||
|
@ -14,13 +15,17 @@ from django.utils.encoding import iri_to_uri
|
|||
from mock import patch
|
||||
from nose.tools import eq_
|
||||
from pyquery import PyQuery as pq
|
||||
from PIL import Image
|
||||
import waffle
|
||||
|
||||
import amo
|
||||
import amo.tests
|
||||
from amo.helpers import absolutify
|
||||
from amo.tests.test_helpers import get_image_path
|
||||
from amo.urlresolvers import reverse
|
||||
from amo.tests.test_helpers import AbuseBase
|
||||
from addons.models import Addon, AddonUser, Charity
|
||||
from abuse.models import AbuseReport
|
||||
from addons import cron
|
||||
from addons.models import Addon, AddonUser, Charity, Category
|
||||
from files.models import File
|
||||
from paypal.tests import other_error
|
||||
from stats.models import Contribution
|
||||
|
@ -28,7 +33,7 @@ from translations.helpers import truncate
|
|||
from translations.query import order_by_translation
|
||||
from users.helpers import users_list
|
||||
from users.models import UserProfile
|
||||
from versions.models import Version
|
||||
from versions.models import License, Version
|
||||
|
||||
|
||||
def norm(s):
|
||||
|
@ -785,6 +790,18 @@ class TestDetailPage(amo.tests.TestCase):
|
|||
eq_(res.status_code, 404)
|
||||
assert 'disabled by an administrator' in res.content
|
||||
|
||||
def test_ready_to_buy(self):
|
||||
addon = Addon.objects.get(id=3615)
|
||||
addon.update(premium_type=amo.ADDON_PREMIUM,
|
||||
status=amo.STATUS_PUBLIC)
|
||||
eq_(self.client.get(reverse('addons.detail', args=[addon.slug])), 404)
|
||||
|
||||
def test_not_ready_to_buy(self):
|
||||
addon = Addon.objects.get(id=3615)
|
||||
addon.update(premium_type=amo.ADDON_PREMIUM,
|
||||
status=amo.STATUS_NOMINATED)
|
||||
eq_(self.client.get(reverse('addons.detail', args=[addon.slug])), 404)
|
||||
|
||||
|
||||
class TestStatus(amo.tests.TestCase):
|
||||
fixtures = ['base/apps', 'base/addon_3615']
|
||||
|
@ -1029,7 +1046,7 @@ class TestAddonSharing(amo.tests.TestCase):
|
|||
assert iri_to_uri(summary) in r['Location']
|
||||
|
||||
|
||||
class TestReportAbuse(AbuseBase, amo.tests.TestCase):
|
||||
class TestReportAbuse(amo.tests.TestCase):
|
||||
fixtures = ['addons/persona',
|
||||
'base/apps',
|
||||
'base/addon_3615',
|
||||
|
@ -1039,6 +1056,29 @@ class TestReportAbuse(AbuseBase, amo.tests.TestCase):
|
|||
settings.RECAPTCHA_PRIVATE_KEY = 'something'
|
||||
self.full_page = reverse('addons.abuse', args=['a3615'])
|
||||
|
||||
@patch('captcha.fields.ReCaptchaField.clean')
|
||||
def test_abuse_anonymous(self, clean):
|
||||
clean.return_value = ""
|
||||
self.client.post(self.full_page, {'text': 'spammy'})
|
||||
eq_(len(mail.outbox), 1)
|
||||
assert 'spammy' in mail.outbox[0].body
|
||||
report = AbuseReport.objects.get(addon=3615)
|
||||
eq_(report.message, 'spammy')
|
||||
eq_(report.reporter, None)
|
||||
|
||||
def test_abuse_anonymous_fails(self):
|
||||
r = self.client.post(self.full_page, {'text': 'spammy'})
|
||||
assert 'recaptcha' in r.context['abuse_form'].errors
|
||||
|
||||
def test_abuse_logged_in(self):
|
||||
self.client.login(username='regular@mozilla.com', password='password')
|
||||
self.client.post(self.full_page, {'text': 'spammy'})
|
||||
eq_(len(mail.outbox), 1)
|
||||
assert 'spammy' in mail.outbox[0].body
|
||||
report = AbuseReport.objects.get(addon=3615)
|
||||
eq_(report.message, 'spammy')
|
||||
eq_(report.reporter.email, 'regular@mozilla.com')
|
||||
|
||||
def test_abuse_name(self):
|
||||
addon = Addon.objects.get(pk=3615)
|
||||
addon.name = 'Bmrk.ru Социальные закладки'
|
||||
|
@ -1047,6 +1087,7 @@ class TestReportAbuse(AbuseBase, amo.tests.TestCase):
|
|||
self.client.login(username='regular@mozilla.com', password='password')
|
||||
self.client.post(self.full_page, {'text': 'spammy'})
|
||||
assert 'spammy' in mail.outbox[0].body
|
||||
assert AbuseReport.objects.get(addon=addon)
|
||||
|
||||
def test_abuse_persona(self):
|
||||
addon_url = reverse('addons.detail', args=['a15663'])
|
||||
|
@ -1061,6 +1102,7 @@ class TestReportAbuse(AbuseBase, amo.tests.TestCase):
|
|||
self.assertRedirects(r, addon_url)
|
||||
eq_(len(mail.outbox), 1)
|
||||
assert 'spammy' in mail.outbox[0].body
|
||||
assert AbuseReport.objects.get(addon=15663)
|
||||
|
||||
|
||||
class TestMobile(amo.tests.TestCase):
|
||||
|
@ -1138,8 +1180,208 @@ class TestMobileDetails(TestMobile):
|
|||
expires = datetime.strptime(response['Expires'], fmt)
|
||||
assert (expires - datetime.now()).days >= 365
|
||||
|
||||
|
||||
def test_unicode_redirect(self):
|
||||
url = '/en-US/firefox/addon/2848?xx=\xc2\xbcwhscheck\xc2\xbe'
|
||||
response = test.Client().get(url)
|
||||
eq_(response.status_code, 301)
|
||||
|
||||
|
||||
class TestSubmitPersona(amo.tests.TestCase):
|
||||
fixtures = ['base/apps', 'base/addon_5579', 'base/users']
|
||||
|
||||
def setUp(self):
|
||||
super(TestSubmitPersona, self).setUp()
|
||||
self.client.login(username='regular@mozilla.com', password='password')
|
||||
self.category = self.create_category()
|
||||
self.url = reverse('personas.submit')
|
||||
cron.build_reverse_name_lookup()
|
||||
patcher = patch.object(waffle, 'flag_is_active')
|
||||
patcher.start()
|
||||
|
||||
def create_category(self):
|
||||
Category.objects.create(application_id=amo.THUNDERBIRD.id,
|
||||
type=amo.ADDON_PERSONA)
|
||||
return Category.objects.create(application_id=amo.FIREFOX.id,
|
||||
type=amo.ADDON_PERSONA)
|
||||
|
||||
def get_dict(self, **kw):
|
||||
License.objects.create(id=amo.LICENSE_CC_BY.id)
|
||||
data = dict(name='new name', category=self.category.id,
|
||||
accentcolor='#003366', textcolor='#C0FFEE',
|
||||
summary='new summary',
|
||||
tags='tag1, tag2, tag3',
|
||||
license=amo.LICENSE_CC_BY.id)
|
||||
data.update(**kw)
|
||||
return data
|
||||
|
||||
def test_submit_name_unique(self):
|
||||
"""Make sure name is unique."""
|
||||
r = self.client.post(self.url, self.get_dict(name='Cooliris'))
|
||||
self.assertFormError(r, 'form', 'name',
|
||||
'This add-on name is already in use. Please choose another.')
|
||||
|
||||
def test_submit_name_unique_strip(self):
|
||||
"""Make sure we can't sneak in a name by adding a space or two."""
|
||||
r = self.client.post(self.url, self.get_dict(name=' Cooliris '))
|
||||
self.assertFormError(r, 'form', 'name',
|
||||
'This add-on name is already in use. Please choose another.')
|
||||
|
||||
def test_submit_name_unique_case(self):
|
||||
"""Make sure unique names aren't case sensitive."""
|
||||
r = self.client.post(self.url, self.get_dict(name='cooliris'))
|
||||
self.assertFormError(r, 'form', 'name',
|
||||
'This add-on name is already in use. Please choose another.')
|
||||
|
||||
def test_submit_name_required(self):
|
||||
"""Make sure name is required."""
|
||||
r = self.client.post(self.url, self.get_dict(name=''))
|
||||
eq_(r.status_code, 200)
|
||||
self.assertFormError(r, 'form', 'name', 'This field is required.')
|
||||
|
||||
def test_submit_name_length(self):
|
||||
"""Make sure the name isn't too long."""
|
||||
r = self.client.post(self.url, self.get_dict(name='a' * 51))
|
||||
eq_(r.status_code, 200)
|
||||
self.assertFormError(r, 'form', 'name',
|
||||
'Ensure this value has at most 50 characters (it has 51).')
|
||||
|
||||
def test_submit_summary_optional(self):
|
||||
"""Make sure summary is required."""
|
||||
r = self.client.post(self.url, self.get_dict(summary=''))
|
||||
eq_(r.status_code, 200)
|
||||
assert 'summary' not in r.context['form'].errors, (
|
||||
'Expected no summary errors')
|
||||
|
||||
def test_submit_summary_length(self):
|
||||
"""Summary is too long."""
|
||||
r = self.client.post(self.url, self.get_dict(summary='a' * 251))
|
||||
eq_(r.status_code, 200)
|
||||
self.assertFormError(r, 'form', 'summary',
|
||||
'Ensure this value has at most 250 characters (it has 251).')
|
||||
|
||||
def test_submit_categories_required(self):
|
||||
r = self.client.post(self.url, self.get_dict(category=''))
|
||||
eq_(r.context['form'].errors['category'], ['This field is required.'])
|
||||
|
||||
def test_license_required(self):
|
||||
r = self.client.post(self.url, self.get_dict(license=''))
|
||||
self.assertFormError(r, 'form', 'license',
|
||||
'A license must be selected.')
|
||||
|
||||
def test_header_hash_required(self):
|
||||
r = self.client.post(self.url, self.get_dict(header_hash=''))
|
||||
self.assertFormError(r, 'form', 'header_hash',
|
||||
'This field is required.')
|
||||
|
||||
def test_footer_hash_required(self):
|
||||
r = self.client.post(self.url, self.get_dict(footer_hash=''))
|
||||
self.assertFormError(r, 'form', 'footer_hash',
|
||||
'This field is required.')
|
||||
|
||||
def test_accentcolor_optional(self):
|
||||
r = self.client.post(self.url, self.get_dict(accentcolor=''))
|
||||
assert 'accentcolor' not in r.context['form'].errors, (
|
||||
'Expected no accentcolor errors')
|
||||
|
||||
def test_accentcolor_invalid(self):
|
||||
r = self.client.post(self.url, self.get_dict(accentcolor='#BALLIN'))
|
||||
self.assertFormError(r, 'form', 'accentcolor',
|
||||
'This must be a valid hex color code, such as #000000.')
|
||||
|
||||
def test_textcolor_optional(self):
|
||||
r = self.client.post(self.url, self.get_dict(textcolor=''))
|
||||
assert 'textcolor' not in r.context['form'].errors, (
|
||||
'Expected no textcolor errors')
|
||||
|
||||
def test_textcolor_invalid(self):
|
||||
r = self.client.post(self.url, self.get_dict(textcolor='#BALLIN'))
|
||||
self.assertFormError(r, 'form', 'textcolor',
|
||||
'This must be a valid hex color code, such as #000000.')
|
||||
|
||||
def get_img_urls(self):
|
||||
return (reverse('personas.upload_persona', args=['persona_header']),
|
||||
reverse('personas.upload_persona', args=['persona_footer']))
|
||||
|
||||
def test_img_urls(self):
|
||||
r = self.client.get(self.url)
|
||||
doc = pq(r.content)
|
||||
header_url, footer_url = self.get_img_urls()
|
||||
eq_(doc('#id_header').attr('data-upload-url'), header_url)
|
||||
eq_(doc('#id_footer').attr('data-upload-url'), footer_url)
|
||||
|
||||
def test_img_size(self):
|
||||
img = get_image_path('mozilla.png')
|
||||
for url, img_type in zip(self.get_img_urls(), ('header', 'footer')):
|
||||
r_ajax = self.client.post(url, {'upload_image': open(img, 'rb')})
|
||||
r_json = json.loads(r_ajax.content)
|
||||
w, h = amo.PERSONA_IMAGE_SIZES.get(img_type)[1]
|
||||
eq_(r_json['errors'], ['Image must be exactly %s pixels wide '
|
||||
'and %s pixels tall.' % (w, h)])
|
||||
|
||||
def test_img_wrongtype(self):
|
||||
img = open('%s/js/impala/global.js' % settings.MEDIA_ROOT, 'rb')
|
||||
for url in self.get_img_urls():
|
||||
r_ajax = self.client.post(url, {'upload_image': img})
|
||||
r_json = json.loads(r_ajax.content)
|
||||
eq_(r_json['errors'], ['Images must be either PNG or JPG.'])
|
||||
|
||||
def test_success(self):
|
||||
data = self.get_dict()
|
||||
header_url, footer_url = self.get_img_urls()
|
||||
|
||||
img = open(get_image_path('persona-header.jpg'), 'rb')
|
||||
r_ajax = self.client.post(header_url, {'upload_image': img})
|
||||
data.update(header_hash=json.loads(r_ajax.content)['upload_hash'])
|
||||
|
||||
img = open(get_image_path('persona-footer.jpg'), 'rb')
|
||||
r_ajax = self.client.post(footer_url, {'upload_image': img})
|
||||
data.update(footer_hash=json.loads(r_ajax.content)['upload_hash'])
|
||||
|
||||
r = self.client.post(self.url, data)
|
||||
addon = Addon.objects.exclude(id=5579)[0]
|
||||
persona = addon.persona
|
||||
self.assertRedirects(
|
||||
r, reverse('personas.submit.done', args=[addon.slug]), 302)
|
||||
|
||||
# Test for correct Addon and Persona values.
|
||||
eq_(unicode(addon.name), data['name'])
|
||||
|
||||
eq_(sorted(addon.categories.values_list('id', flat=True)),
|
||||
sorted(Category.objects.values_list('id', flat=True)))
|
||||
|
||||
tags = ', '.join(sorted(addon.tags.values_list('tag_text', flat=True)))
|
||||
eq_(tags, data['tags'])
|
||||
|
||||
eq_(persona.persona_id, 0)
|
||||
eq_(persona.license_id, data['license'])
|
||||
|
||||
eq_(persona.accentcolor, data['accentcolor'].lstrip('#'))
|
||||
eq_(persona.textcolor, data['textcolor'].lstrip('#'))
|
||||
|
||||
user = UserProfile.objects.get(pk=999)
|
||||
eq_(persona.author, user.name)
|
||||
eq_(persona.display_username, user.username)
|
||||
|
||||
v = addon.versions.all()
|
||||
eq_(len(v), 1)
|
||||
eq_(v[0].version, '0')
|
||||
|
||||
# Test for header, footer, and preview images.
|
||||
dst = os.path.join(settings.PERSONAS_PATH, str(addon.id))
|
||||
|
||||
img = os.path.join(dst, 'header.jpg')
|
||||
eq_(persona.footer, 'footer.jpg')
|
||||
eq_(os.path.exists(img), True)
|
||||
eq_(Image.open(img).size, (3000, 200))
|
||||
eq_(amo.PERSONA_IMAGE_SIZES['header'][1], (3000, 200))
|
||||
|
||||
img = os.path.join(dst, 'footer.jpg')
|
||||
eq_(persona.footer, 'footer.jpg')
|
||||
eq_(os.path.exists(img), True)
|
||||
eq_(Image.open(img).size, (3000, 100))
|
||||
eq_(amo.PERSONA_IMAGE_SIZES['footer'][1], (3000, 100))
|
||||
|
||||
img = os.path.join(dst, 'preview.jpg')
|
||||
eq_(os.path.exists(img), True)
|
||||
eq_(Image.open(img).size, (680, 100))
|
||||
eq_(amo.PERSONA_IMAGE_SIZES['header'][0], (680, 100))
|
||||
|
|
|
@ -2,6 +2,7 @@ from django.conf.urls.defaults import patterns, url, include
|
|||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.shortcuts import redirect
|
||||
|
||||
from devhub.views import ajax_upload_image
|
||||
from . import views
|
||||
|
||||
ADDON_ID = r"""(?P<addon_id>[^/<>"']+)"""
|
||||
|
@ -10,6 +11,7 @@ ADDON_ID = r"""(?P<addon_id>[^/<>"']+)"""
|
|||
# These will all start with /addon/<addon_id>/
|
||||
detail_patterns = patterns('',
|
||||
url('^$', views.addon_detail, name='addons.detail'),
|
||||
url('^more$', views.addon_detail, name='addons.detail_more'),
|
||||
url('^eula/(?P<file_id>\d+)?$', views.eula, name='addons.eula'),
|
||||
url('^license/(?P<version>[^/]+)?', views.license, name='addons.license'),
|
||||
url('^privacy/', views.privacy, name='addons.privacy'),
|
||||
|
@ -70,6 +72,14 @@ urlpatterns = patterns('',
|
|||
# Impala deets.
|
||||
url('^i/addon/%s/' % ADDON_ID, include(impala_detail_patterns)),
|
||||
|
||||
# Personas submission.
|
||||
url('^i/personas/%s/submit/done$' % ADDON_ID, views.submit_persona_done,
|
||||
name='personas.submit.done'),
|
||||
url('^i/personas/submit$', views.submit_persona, name='personas.submit'),
|
||||
url('^i/personas/submit/upload/'
|
||||
'(?P<upload_type>persona_header|persona_footer)$',
|
||||
ajax_upload_image, name='personas.upload_persona'),
|
||||
|
||||
# Accept extra junk at the end for a cache-busting build id.
|
||||
url('^addons/buttons.js(?:/.+)?$', 'addons.buttons.js'),
|
||||
|
||||
|
|
|
@ -7,9 +7,10 @@ from django.conf import settings
|
|||
from django.utils.encoding import smart_str
|
||||
|
||||
import commonware.log
|
||||
import lru_cache
|
||||
import redisutils
|
||||
|
||||
from amo.utils import sorted_groupby
|
||||
from amo.utils import sorted_groupby, memoize
|
||||
from translations.models import Translation
|
||||
|
||||
safe_key = lambda x: hashlib.md5(smart_str(x).lower().strip()).hexdigest()
|
||||
|
@ -106,26 +107,37 @@ class FeaturedManager(object):
|
|||
by_app = classmethod(lambda cls, x: '%s:%s' % (cls.prefix + 'byapp', x))
|
||||
by_type = classmethod(lambda cls, x: '%s:%s' % (cls.prefix + 'bytype', x))
|
||||
by_locale = classmethod(lambda cls, x: '%s:%s' %
|
||||
(cls.prefix + 'bylocale', x))
|
||||
(cls.prefix + 'bylocale', x and x.lower()))
|
||||
|
||||
@classmethod
|
||||
def redis(cls):
|
||||
return redisutils.connections['master']
|
||||
|
||||
@classmethod
|
||||
def get_objects(cls):
|
||||
def _get_objects(cls):
|
||||
fields = ['addon', 'type', 'locale', 'application']
|
||||
if settings.NEW_FEATURES:
|
||||
from bandwagon.models import FeaturedCollection
|
||||
vals = FeaturedCollection.objects.values_list(
|
||||
'collection__addons', 'collection__addons__type',
|
||||
'locale', 'application')
|
||||
vals = (FeaturedCollection.objects
|
||||
.filter(collection__addons__isnull=False)
|
||||
.values_list('collection__addons',
|
||||
'collection__addons__type', 'locale',
|
||||
'application'))
|
||||
else:
|
||||
from addons.models import Addon
|
||||
vals = Addon.objects.valid().values_list(
|
||||
'id', 'type', 'feature__locale', 'feature__application')
|
||||
vals = (Addon.objects.valid().filter(feature__isnull=False)
|
||||
.values_list('id', 'type', 'feature__locale',
|
||||
'feature__application'))
|
||||
return [dict(zip(fields, val)) for val in vals]
|
||||
|
||||
@classmethod
|
||||
def get_objects(cls):
|
||||
rv = cls._get_objects()
|
||||
for d in rv:
|
||||
if d['locale']:
|
||||
d['locale'] = d['locale'].lower()
|
||||
return rv
|
||||
|
||||
@classmethod
|
||||
def build(cls):
|
||||
qs = list(cls.get_objects())
|
||||
|
@ -138,7 +150,7 @@ class FeaturedManager(object):
|
|||
by_locale = sorted_groupby(qs, itemgetter('locale'))
|
||||
by_app = sorted_groupby(qs, itemgetter('application'))
|
||||
|
||||
pipe = cls.redis().pipeline()
|
||||
pipe = cls.redis().pipeline(transaction=False)
|
||||
pipe.delete(cls.by_id)
|
||||
for row in qs:
|
||||
pipe.sadd(cls.by_id, row['addon'])
|
||||
|
@ -150,10 +162,13 @@ class FeaturedManager(object):
|
|||
name = prefixer(key)
|
||||
pipe.delete(name)
|
||||
for row in rows:
|
||||
pipe.sadd(name, row['addon'])
|
||||
if row['addon']:
|
||||
pipe.sadd(name, row['addon'])
|
||||
pipe.execute()
|
||||
|
||||
@classmethod
|
||||
@lru_cache.lru_cache(maxsize=100)
|
||||
@memoize(prefix, time=60 * 10)
|
||||
def featured_ids(cls, app, lang=None, type=None):
|
||||
redis = cls.redis()
|
||||
base = (cls.by_id, cls.by_app(app.id))
|
||||
|
@ -174,60 +189,82 @@ class FeaturedManager(object):
|
|||
|
||||
class CreaturedManager(object):
|
||||
prefix = 'addons:creatured'
|
||||
by_cat = classmethod(lambda cls, cat: '%s:%s' % (cls.prefix, cat))
|
||||
by_locale = classmethod(lambda cls, cat, locale: '%s:%s:%s' %
|
||||
(cls.prefix, cat, locale))
|
||||
|
||||
@classmethod
|
||||
def by_cat(cls, cat, app):
|
||||
return '%s:%s:%s' % (cls.prefix, cat, app)
|
||||
|
||||
@classmethod
|
||||
def by_locale(cls, cat, app, locale):
|
||||
return '%s:%s:%s:%s' % (cls.prefix, cat, app, locale.lower())
|
||||
|
||||
@classmethod
|
||||
def redis(cls):
|
||||
return redisutils.connections['master']
|
||||
|
||||
@classmethod
|
||||
def get_objects(cls):
|
||||
fields = ['category', 'addon', 'feature_locales']
|
||||
def _get_objects(cls):
|
||||
fields = ['category', 'addon', 'locales', 'app']
|
||||
if settings.NEW_FEATURES:
|
||||
from bandwagon.models import FeaturedCollection
|
||||
vals = FeaturedCollection.objects.values_list(
|
||||
'collection__addons__category', 'collection__addons', 'locale')
|
||||
vals = (FeaturedCollection.objects
|
||||
.filter(collection__addons__isnull=False)
|
||||
.values_list('collection__addons__category',
|
||||
'collection__addons', 'locale',
|
||||
'application'))
|
||||
else:
|
||||
from addons.models import AddonCategory
|
||||
vals = (AddonCategory.objects.filter(feature=True)
|
||||
.values_list('category', 'addon', 'feature_locales'))
|
||||
.values_list('category', 'addon', 'feature_locales',
|
||||
'category__application'))
|
||||
return [dict(zip(fields, val)) for val in vals]
|
||||
|
||||
@classmethod
|
||||
def get_objects(cls):
|
||||
rv = cls._get_objects()
|
||||
for d in rv:
|
||||
if d['locales']:
|
||||
d['locales'] = d['locales'].lower()
|
||||
return rv
|
||||
|
||||
@classmethod
|
||||
def build(cls):
|
||||
qs = list(cls.get_objects())
|
||||
# Expand any comma-separated lists of locales.
|
||||
for row in list(qs):
|
||||
# Normalize empty strings to None.
|
||||
if row['feature_locales'] == '':
|
||||
row['feature_locales'] = None
|
||||
if row['feature_locales']:
|
||||
if row['locales'] == '':
|
||||
row['locales'] = None
|
||||
if row['locales']:
|
||||
qs.remove(row)
|
||||
for locale in row['feature_locales'].split(','):
|
||||
for locale in row['locales'].split(','):
|
||||
d = dict(row)
|
||||
d['feature_locales'] = locale.strip()
|
||||
d['locales'] = locale.strip()
|
||||
qs.append(d)
|
||||
|
||||
pipe = cls.redis().pipeline()
|
||||
for category, rows in sorted_groupby(qs, itemgetter('category')):
|
||||
locale_getter = itemgetter('feature_locales')
|
||||
pipe = cls.redis().pipeline(transaction=False)
|
||||
catapp = itemgetter('category', 'app')
|
||||
for (category, app), rows in sorted_groupby(qs, catapp):
|
||||
locale_getter = itemgetter('locales')
|
||||
for locale, rs in sorted_groupby(rows, locale_getter):
|
||||
if locale:
|
||||
name = cls.by_locale(category, locale)
|
||||
name = cls.by_locale(category, app, locale)
|
||||
else:
|
||||
name = cls.by_cat(category)
|
||||
name = cls.by_cat(category, app)
|
||||
pipe.delete(name)
|
||||
for row in rs:
|
||||
pipe.sadd(name, row['addon'])
|
||||
if row['addon']:
|
||||
pipe.sadd(name, row['addon'])
|
||||
pipe.execute()
|
||||
|
||||
@classmethod
|
||||
@lru_cache.lru_cache(maxsize=100)
|
||||
@memoize(prefix, time=60 * 10)
|
||||
def creatured_ids(cls, category, lang):
|
||||
redis = cls.redis()
|
||||
all_ = redis.smembers(cls.by_cat(category.id))
|
||||
per_locale = redis.smembers(cls.by_locale(category.id, lang))
|
||||
all_ = redis.smembers(cls.by_cat(category.id, category.application_id))
|
||||
locale_key = cls.by_locale(category.id, category.application_id, lang)
|
||||
per_locale = redis.smembers(locale_key)
|
||||
others = list(all_ - per_locale)
|
||||
per_locale = list(per_locale)
|
||||
random.shuffle(others)
|
||||
|
|
|
@ -21,17 +21,21 @@ import commonware.log
|
|||
import session_csrf
|
||||
from tower import ugettext as _, ugettext_lazy as _lazy
|
||||
from mobility.decorators import mobilized, mobile_template
|
||||
import waffle
|
||||
|
||||
import amo
|
||||
from amo import messages
|
||||
from amo.decorators import login_required
|
||||
from amo.forms import AbuseForm
|
||||
from amo.utils import sorted_groupby, randslice, send_abuse_report
|
||||
from amo.utils import sorted_groupby, randslice
|
||||
from amo.helpers import absolutify
|
||||
from amo.models import manual_order
|
||||
from amo import urlresolvers
|
||||
from amo.urlresolvers import reverse
|
||||
from abuse.models import send_abuse_report
|
||||
from addons.utils import FeaturedManager
|
||||
from bandwagon.models import Collection, CollectionFeature, CollectionPromo
|
||||
from devhub.decorators import dev_required
|
||||
import paypal
|
||||
from reviews.forms import ReviewForm
|
||||
from reviews.models import Review, GroupedRating
|
||||
|
@ -40,12 +44,14 @@ from stats.models import GlobalStat, Contribution
|
|||
from translations.query import order_by_translation
|
||||
from translations.helpers import truncate
|
||||
from versions.models import Version
|
||||
from .models import Addon, MiniAddon, Persona, FrozenAddon
|
||||
from .models import Addon, Persona, FrozenAddon
|
||||
from .forms import NewPersonaForm
|
||||
from .decorators import addon_view_factory
|
||||
|
||||
log = commonware.log.getLogger('z.addons')
|
||||
paypal_log = commonware.log.getLogger('z.paypal')
|
||||
addon_view = addon_view_factory(qs=Addon.objects.valid)
|
||||
addon_unreviewed_view = addon_view_factory(qs=Addon.objects.unreviewed)
|
||||
addon_disabled_view = addon_view_factory(qs=Addon.objects.valid_and_disabled)
|
||||
|
||||
|
||||
|
@ -69,7 +75,8 @@ def author_addon_clicked(f):
|
|||
@addon_disabled_view
|
||||
def addon_detail(request, addon):
|
||||
"""Add-ons details page dispatcher."""
|
||||
if addon.disabled_by_user or addon.status == amo.STATUS_DISABLED:
|
||||
if (addon.disabled_by_user or addon.status == amo.STATUS_DISABLED
|
||||
or (addon.is_premium() and not addon.can_be_purchased())):
|
||||
return jingo.render(request, 'addons/disabled.html',
|
||||
{'addon': addon}, status=404)
|
||||
|
||||
|
@ -96,8 +103,14 @@ def addon_detail(request, addon):
|
|||
'addons.detail', args=[addon.slug]))
|
||||
|
||||
|
||||
@addon_view
|
||||
@addon_disabled_view
|
||||
def impala_addon_detail(request, addon):
|
||||
"""Add-ons details page dispatcher."""
|
||||
if (addon.disabled_by_user or addon.status == amo.STATUS_DISABLED
|
||||
or (addon.is_premium() and not addon.can_be_purchased())):
|
||||
return jingo.render(request, 'addons/impala/disabled.html',
|
||||
{'addon': addon}, status=404)
|
||||
|
||||
"""Add-ons details page dispatcher."""
|
||||
# addon needs to have a version and be valid for this app.
|
||||
if addon.type in request.APP.types:
|
||||
|
@ -119,7 +132,10 @@ def impala_addon_detail(request, addon):
|
|||
prefixer = urlresolvers.get_url_prefix()
|
||||
prefixer.app = new_app.short
|
||||
return http.HttpResponsePermanentRedirect(reverse(
|
||||
'i_addons.detail', args=[addon.slug]))
|
||||
'addons.detail', args=[addon.slug]))
|
||||
|
||||
if settings.IMPALA_ADDON_DETAILS:
|
||||
addon_detail = impala_addon_detail
|
||||
|
||||
|
||||
def extension_detail(request, addon):
|
||||
|
@ -148,7 +164,7 @@ def extension_detail(request, addon):
|
|||
tags = addon.tags.not_blacklisted()
|
||||
|
||||
# addon recommendations
|
||||
recommended = MiniAddon.objects.valid().filter(
|
||||
recommended = Addon.objects.valid().filter(
|
||||
recommended_for__addon=addon)[:5]
|
||||
|
||||
# popular collections this addon is part of
|
||||
|
@ -190,7 +206,7 @@ def impala_extension_detail(request, addon):
|
|||
addon.get_satisfaction_company)
|
||||
|
||||
# Other add-ons from the same author(s).
|
||||
author_addons = (Addon.objects.valid().exclude(id=addon.id)
|
||||
author_addons = (Addon.objects.valid().exclude(id=addon.id).distinct()
|
||||
.filter(addonuser__listed=True,
|
||||
authors__in=addon.listed_authors))[:6]
|
||||
|
||||
|
@ -224,10 +240,16 @@ def impala_extension_detail(request, addon):
|
|||
return jingo.render(request, 'addons/impala/details.html', ctx)
|
||||
|
||||
|
||||
@mobilized(extension_detail)
|
||||
def extension_detail(request, addon):
|
||||
return jingo.render(request, 'addons/mobile/details.html',
|
||||
{'addon': addon})
|
||||
if settings.IMPALA_ADDON_DETAILS:
|
||||
@mobilized(impala_extension_detail)
|
||||
def impala_extension_detail(request, addon):
|
||||
return jingo.render(request, 'addons/mobile/details.html',
|
||||
{'addon': addon})
|
||||
else:
|
||||
@mobilized(extension_detail)
|
||||
def extension_detail(request, addon):
|
||||
return jingo.render(request, 'addons/mobile/details.html',
|
||||
{'addon': addon})
|
||||
|
||||
|
||||
def _category_personas(qs, limit):
|
||||
|
@ -427,6 +449,10 @@ def impala_home(request):
|
|||
'src': 'homepage', 'collections': collections})
|
||||
|
||||
|
||||
if settings.IMPALA_HOMEPAGE:
|
||||
home = impala_home
|
||||
|
||||
|
||||
@mobilized(home)
|
||||
@cache_page(60 * 10)
|
||||
def home(request):
|
||||
|
@ -515,6 +541,7 @@ def eula(request, addon, file_id=None):
|
|||
return jingo.render(request, 'addons/eula.html',
|
||||
{'addon': addon, 'version': version})
|
||||
|
||||
|
||||
@addon_view
|
||||
def impala_eula(request, addon, file_id=None):
|
||||
if not addon.eula:
|
||||
|
@ -702,10 +729,10 @@ def contribute_url_params(business, addon_id, item_name, return_url,
|
|||
|
||||
lang = translation.get_language()
|
||||
try:
|
||||
paypal_lang = settings.PAYPAL_COUNTRYMAP[lang]
|
||||
paypal_lang = amo.PAYPAL_COUNTRYMAP[lang]
|
||||
except KeyError:
|
||||
lang = lang.split('-')[0]
|
||||
paypal_lang = settings.PAYPAL_COUNTRYMAP.get(lang, 'US')
|
||||
paypal_lang = amo.PAYPAL_COUNTRYMAP.get(lang, 'US')
|
||||
|
||||
# Get all the data elements that will be URL params
|
||||
# on the Paypal redirect URL.
|
||||
|
@ -791,8 +818,7 @@ def license_redirect(request, version):
|
|||
def report_abuse(request, addon):
|
||||
form = AbuseForm(request.POST or None, request=request)
|
||||
if request.method == "POST" and form.is_valid():
|
||||
url = reverse('addons.detail', args=[addon.slug])
|
||||
send_abuse_report(request, addon, url, form.cleaned_data['text'])
|
||||
send_abuse_report(request, addon, form.cleaned_data['text'])
|
||||
messages.success(request, _('Abuse reported.'))
|
||||
return redirect('addons.detail', addon.slug)
|
||||
else:
|
||||
|
@ -804,3 +830,27 @@ def report_abuse(request, addon):
|
|||
def persona_redirect(request, persona_id):
|
||||
persona = get_object_or_404(Persona, persona_id=persona_id)
|
||||
return redirect('addons.detail', persona.addon.slug, permanent=True)
|
||||
|
||||
|
||||
@login_required
|
||||
def submit_persona(request):
|
||||
if not waffle.flag_is_active(request, 'submit-personas'):
|
||||
return http.HttpResponseForbidden()
|
||||
form = NewPersonaForm(data=request.POST or None,
|
||||
files=request.FILES or None, request=request)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
addon = form.save()
|
||||
messages.success(request, _('Persona successfully added.'))
|
||||
return redirect('personas.submit.done', addon.slug)
|
||||
return jingo.render(request, 'addons/impala/personas/submit.html',
|
||||
dict(form=form))
|
||||
|
||||
|
||||
@dev_required
|
||||
def submit_persona_done(request, addon_id, addon):
|
||||
if not waffle.flag_is_active(request, 'submit-personas'):
|
||||
return http.HttpResponseForbidden()
|
||||
if addon.status != amo.STATUS_UNREVIEWED:
|
||||
return http.HttpResponseRedirect(addon.get_url_path(impala=True))
|
||||
return jingo.render(request, 'addons/impala/personas/submit_done.html',
|
||||
dict(addon=addon))
|
||||
|
|
|
@ -12,8 +12,9 @@ from constants.applications import *
|
|||
from constants.base import *
|
||||
from constants.licenses import *
|
||||
from constants.platforms import *
|
||||
from .log import (LOG, LOG_BY_ID, LOG_EDITORS, LOG_HIDE_DEVELOPER,
|
||||
LOG_KEEP, LOG_REVIEW_QUEUE, LOG_REVIEW_EMAIL_USER, log)
|
||||
from .log import (LOG, LOG_BY_ID, LOG_ADMINS, LOG_EDITORS,
|
||||
LOG_HIDE_DEVELOPER, LOG_KEEP, LOG_REVIEW_QUEUE,
|
||||
LOG_REVIEW_EMAIL_USER, log)
|
||||
|
||||
logger_log = commonware.log.getLogger('z.amo')
|
||||
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
import re
|
||||
|
||||
from django.forms import fields
|
||||
from django.db import models
|
||||
from django.core import exceptions
|
||||
|
||||
from tower import ugettext as _
|
||||
|
||||
from amo.widgets import ColorWidget
|
||||
|
||||
|
||||
class DecimalCharField(models.DecimalField):
|
||||
"""Like the standard django DecimalField but stored in a varchar
|
||||
|
@ -47,3 +54,19 @@ class DecimalCharField(models.DecimalField):
|
|||
if value is None:
|
||||
return value
|
||||
return self.format_number(value)
|
||||
|
||||
|
||||
class ColorField(fields.CharField):
|
||||
|
||||
widget = ColorWidget
|
||||
|
||||
def __init__(self, max_length=7, min_length=None, *args, **kwargs):
|
||||
super(ColorField, self).__init__(max_length, min_length, *args,
|
||||
**kwargs)
|
||||
|
||||
def clean(self, value):
|
||||
super(ColorField, self).clean(value)
|
||||
if value and not re.match('^\#([0-9a-fA-F]{6})$', value):
|
||||
raise exceptions.ValidationError(
|
||||
_(u'This must be a valid hex color code, such as #000000.'))
|
||||
return value
|
||||
|
|
|
@ -270,7 +270,7 @@ def impala_breadcrumbs(context, items=list(), add_default=True, crumb_size=40):
|
|||
"""
|
||||
if add_default and not context.get('WEBAPPS'):
|
||||
app = context['request'].APP
|
||||
crumbs = [(urlresolvers.reverse('i_home'), page_name(app))]
|
||||
crumbs = [(urlresolvers.reverse('home'), page_name(app))]
|
||||
else:
|
||||
crumbs = []
|
||||
|
||||
|
|
|
@ -393,11 +393,47 @@ class CUSTOM_HTML(_LOG):
|
|||
format = '{0}'
|
||||
|
||||
|
||||
class OBJECT_ADDED(_LOG):
|
||||
id = 100
|
||||
format = _(u'Created: {0}.')
|
||||
admin_event = True
|
||||
action_class = None
|
||||
|
||||
|
||||
class OBJECT_EDITED(_LOG):
|
||||
id = 101
|
||||
format = _(u'Edited field: {2} set to: {0}.')
|
||||
admin_event = True
|
||||
action_class = None
|
||||
|
||||
|
||||
class OBJECT_DELETED(_LOG):
|
||||
id = 102
|
||||
format = _(u'Deleted: {1}.')
|
||||
admin_event = True
|
||||
action_class = None
|
||||
|
||||
|
||||
class ADMIN_USER_EDITED(_LOG):
|
||||
id = 103
|
||||
format = _(u'User {user} edited, reason: {1}')
|
||||
admin_event = True
|
||||
action_class = None
|
||||
|
||||
|
||||
class ADMIN_USER_ANONYMIZED(_LOG):
|
||||
id = 104
|
||||
format = _(u'User {user} anonymized.')
|
||||
admin_event = True
|
||||
action_class = None
|
||||
|
||||
|
||||
LOGS = [x for x in vars().values()
|
||||
if isclass(x) and issubclass(x, _LOG) and x != _LOG]
|
||||
|
||||
LOG_BY_ID = dict((l.id, l) for l in LOGS)
|
||||
LOG = AttributeDict((l.__name__, l) for l in LOGS)
|
||||
LOG_ADMINS = [l.id for l in LOGS if hasattr(l, 'admin_event')]
|
||||
LOG_KEEP = [l.id for l in LOGS if hasattr(l, 'keep')]
|
||||
LOG_EDITORS = [l.id for l in LOGS if hasattr(l, 'editor_event')]
|
||||
LOG_REVIEW_QUEUE = [l.id for l in LOGS if hasattr(l, 'review_queue')]
|
||||
|
@ -406,7 +442,8 @@ LOG_REVIEW_QUEUE = [l.id for l in LOGS if hasattr(l, 'review_queue')]
|
|||
LOG_REVIEW_EMAIL_USER = [l.id for l in LOGS if hasattr(l, 'review_email_user')]
|
||||
# Logs *not* to show to the developer.
|
||||
LOG_HIDE_DEVELOPER = [l.id for l in LOGS
|
||||
if getattr(l, 'hide_developer', False)]
|
||||
if (getattr(l, 'hide_developer', False)
|
||||
or l.id in LOG_ADMINS)]
|
||||
|
||||
|
||||
def log(action, *args, **kw):
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
</p>
|
||||
<p class="rel">
|
||||
<a href="{{ pager.url|urlparams(page=1) }}"
|
||||
title="{{ _('Jump to first page') }}"
|
||||
{% if not pager.has_previous() %}class="disabled"{% endif %}>
|
||||
◂◂</a>
|
||||
<a href="{{ pager.url|urlparams(page=pager.previous_page_number()) }}"
|
||||
|
@ -21,6 +22,7 @@
|
|||
class="button{% if not pager.has_next() %} disabled{% endif %}">
|
||||
{{ _('Next') }} ▸</a>
|
||||
<a href="{{ pager.url|urlparams(page=pager.paginator.num_pages) }}"
|
||||
title="{{ _('Jump to last page') }}"
|
||||
{% if not pager.has_next() %}class="disabled"{% endif %}>
|
||||
▸▸</a>
|
||||
</p>
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
('rating', _('Top Rated')),
|
||||
) %}
|
||||
|
||||
{% macro section(title, base_url, extras, categories) %}
|
||||
<li>
|
||||
{% macro section(id, title, base_url, extras, categories) %}
|
||||
<li id="{{ id }}">
|
||||
<a href="{{ base_url }}">{{ title }}</a>
|
||||
<ul class="two-col">
|
||||
{% for sort, title in extras %}
|
||||
|
@ -20,12 +20,12 @@
|
|||
|
||||
<nav id="site-nav" class="menu-nav c">
|
||||
<ul>
|
||||
{{ section(_('Extensions'), url('browse.extensions'), extras, extensions) }}
|
||||
{{ section(_('Personas'), url('browse.personas'), extras, personas) }}
|
||||
{{ section('extensions', _('Extensions'), url('browse.extensions'), extras, extensions) }}
|
||||
{{ section('personas', _('Personas'), url('browse.personas'), extras, personas) }}
|
||||
{% if themes %}
|
||||
{{ section(_('Themes'), url('browse.themes'), extras, themes) }}
|
||||
{{ section('themes', _('Themes'), url('browse.themes'), extras, themes) }}
|
||||
{% endif %}
|
||||
<li>
|
||||
<li id="collections">
|
||||
<a href="#">{{ _('Collections') }}</a>
|
||||
<ul>
|
||||
{% with base_url = url('collections.list') %}
|
||||
|
@ -43,7 +43,7 @@
|
|||
{{ _('My Favorite Add-ons') }}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<li id="more">
|
||||
<a href="#">{{ _('More…') }}</a>
|
||||
<ul>
|
||||
<li><a href="{{ url('browse.language-tools') }}">
|
||||
|
|
|
@ -42,7 +42,8 @@
|
|||
<li><a href="#responses">All Responses</a></li>
|
||||
<li><a href="#errors">Redirects and Errors</a></li>
|
||||
<li><a href="#celery">Celery</a></li>
|
||||
<li><a href="#blocklist">Blocklist Perf</a></li>
|
||||
<li><a href="#guid-search">GUID Search Perf</a></li>
|
||||
<li><a href="#homepage">Homepage</a></li>
|
||||
</ul>
|
||||
<ul id=sizes>
|
||||
<li><a href="{{ request.get_full_path()|urlparams(width=300, height=None) }}">
|
||||
|
@ -70,9 +71,15 @@
|
|||
{% call grid() %}
|
||||
target=sumSeries({{ site }}.celery.tasks.pending.*.*.*)&target=nonNegativeDerivative(sumSeries({{ site }}.celery.tasks.total.*.*.*))&target=nonNegativeDerivative(sumSeries({{ site }}.celery.tasks.failed.*.*.*))
|
||||
{% endcall %}
|
||||
<h2 id="blocklist">Blocklist Perf</h2>
|
||||
|
||||
<h2 id="guid-search">GUID Search Perf</h2>
|
||||
{% call grid() %}
|
||||
&target=stats.timers.{{ site }}.view.api.views.guid_search.GET.lower&target=stats.timers.{{ site }}.view.api.views.guid_search.GET.mean&target=stats.timers.{{ site }}.view.api.views.guid_search.GET.upper_90&target=scale%28stats.timers.{{ site }}.view.api.views.guid_search.GET.count%2C%200.01%29
|
||||
target=stats.timers.{{ site }}.view.api.views.guid_search.GET.lower&target=stats.timers.{{ site }}.view.api.views.guid_search.GET.mean&target=stats.timers.{{ site }}.view.api.views.guid_search.GET.upper_90&target=scale%28stats.timers.{{ site }}.view.api.views.guid_search.GET.count%2C%200.01%29
|
||||
{% endcall %}
|
||||
|
||||
<h2 id="homepage">Homepage</h2>
|
||||
{% call grid() %}
|
||||
target=stats.timers.{{ site }}.view.addons.views.home.GET.count&target=stats.timers.{{ site }}.view.addons.views.home.GET.lower&target=stats.timers.{{ site }}.view.addons.views.home.GET.mean&target=stats.timers.{{ site }}.view.addons.views.home.GET.upper_90
|
||||
{% endcall %}
|
||||
|
||||
<script>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from datetime import datetime, timedelta
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
|
||||
|
@ -72,7 +73,22 @@ class TestCase(RedisTest, test_utils.TestCase):
|
|||
def _pre_setup(self):
|
||||
super(TestCase, self)._pre_setup()
|
||||
from addons.cron import reset_featured_addons
|
||||
from addons.utils import FeaturedManager, CreaturedManager
|
||||
reset_featured_addons()
|
||||
# Clear the in-process caches.
|
||||
FeaturedManager.featured_ids.clear()
|
||||
CreaturedManager.creatured_ids.clear()
|
||||
|
||||
|
||||
class AMOPaths(object):
|
||||
"""Mixin for getting common AMO Paths."""
|
||||
|
||||
def file_fixture_path(self, name):
|
||||
path = 'apps/files/fixtures/files/%s' % name
|
||||
return os.path.join(settings.ROOT, path)
|
||||
|
||||
def xpi_path(self, name):
|
||||
return self.file_fixture_path(name + '.xpi')
|
||||
|
||||
|
||||
def close_to_now(dt):
|
||||
|
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 3.9 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 22 KiB |
|
@ -304,25 +304,6 @@ class TestLicenseLink(amo.tests.TestCase):
|
|||
eq_(s, ex)
|
||||
|
||||
|
||||
class AbuseBase:
|
||||
@patch('captcha.fields.ReCaptchaField.clean')
|
||||
def test_abuse_anonymous(self, clean):
|
||||
clean.return_value = ""
|
||||
self.client.post(self.full_page, {'text': 'spammy'})
|
||||
eq_(len(mail.outbox), 1)
|
||||
assert 'spammy' in mail.outbox[0].body
|
||||
|
||||
def test_abuse_anonymous_fails(self):
|
||||
r = self.client.post(self.full_page, {'text': 'spammy'})
|
||||
assert 'recaptcha' in r.context['abuse_form'].errors
|
||||
|
||||
def test_abuse_logged_in(self):
|
||||
self.client.login(username='regular@mozilla.com', password='password')
|
||||
self.client.post(self.full_page, {'text': 'spammy'})
|
||||
eq_(len(mail.outbox), 1)
|
||||
assert 'spammy' in mail.outbox[0].body
|
||||
|
||||
|
||||
def get_image_path(name):
|
||||
return os.path.join(settings.ROOT, 'apps', 'amo', 'tests', 'images', name)
|
||||
|
||||
|
|
|
@ -59,6 +59,8 @@ class SendMailTest(test.TestCase):
|
|||
assert success, "Email wasn't sent"
|
||||
eq_(len(mail.outbox), 1)
|
||||
|
||||
eq_(mail.outbox[0].body.count('users/unsubscribe'), 1) # bug 676601
|
||||
|
||||
def test_user_setting_checked(self):
|
||||
user = UserProfile.objects.all()[0]
|
||||
to = user.email
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import csp.views
|
||||
from waffle.views import wafflejs
|
||||
|
||||
from django.conf.urls.defaults import patterns, url, include
|
||||
from django.views.decorators.cache import never_cache
|
||||
|
@ -19,6 +20,7 @@ services_patterns = patterns('',
|
|||
|
||||
urlpatterns = patterns('',
|
||||
url('^robots.txt$', views.robots, name='robots.txt'),
|
||||
url(r'^wafflejs$', wafflejs, name='wafflejs'),
|
||||
('^services/', include(services_patterns)),
|
||||
|
||||
url('^opensearch.xml$', 'api.views.render_xml',
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import functools
|
||||
import hashlib
|
||||
import itertools
|
||||
import operator
|
||||
|
@ -24,7 +25,6 @@ from django.utils.translation import trans_real
|
|||
from django.utils.functional import Promise
|
||||
from django.utils.encoding import smart_str, smart_unicode
|
||||
|
||||
import bleach
|
||||
from easy_thumbnails import processors
|
||||
import html5lib
|
||||
from html5lib.serializer.htmlserializer import HTMLSerializer
|
||||
|
@ -34,10 +34,12 @@ from PIL import Image, ImageFile, PngImagePlugin
|
|||
import amo.search
|
||||
from amo import ADDON_ICON_SIZES
|
||||
from amo.urlresolvers import reverse
|
||||
from . import logger_log as log
|
||||
from translations.models import Translation
|
||||
from users.models import UserNotification
|
||||
import users.notifications as notifications
|
||||
from users.utils import UnsubscribeCode
|
||||
|
||||
from . import logger_log as log
|
||||
|
||||
|
||||
def urlparams(url_, hash=None, **query):
|
||||
|
@ -123,7 +125,8 @@ def paginate(request, queryset, per_page=20, count=None):
|
|||
|
||||
|
||||
def send_mail(subject, message, from_email=None, recipient_list=None,
|
||||
fail_silently=False, use_blacklist=True, perm_setting=None):
|
||||
fail_silently=False, use_blacklist=True, perm_setting=None,
|
||||
connection=None):
|
||||
"""
|
||||
A wrapper around django.core.mail.send_mail.
|
||||
|
||||
|
@ -148,14 +151,6 @@ def send_mail(subject, message, from_email=None, recipient_list=None,
|
|||
recipient_list = [e for e in recipient_list
|
||||
if e and perms.setdefault(e, d)]
|
||||
|
||||
# Add footer
|
||||
template = loader.get_template('amo/emails/unsubscribe.ltxt')
|
||||
from amo.helpers import absolutify
|
||||
context = {'message': message, 'perm_setting': perm_setting.label,
|
||||
'unsubscribe': absolutify(reverse('users.edit_impala')),
|
||||
'SITE_URL': settings.SITE_URL}
|
||||
message = template.render(Context(context, autoescape=False))
|
||||
|
||||
# Prune blacklisted emails.
|
||||
if use_blacklist:
|
||||
white_list = []
|
||||
|
@ -169,8 +164,27 @@ def send_mail(subject, message, from_email=None, recipient_list=None,
|
|||
|
||||
try:
|
||||
if white_list:
|
||||
result = django_send_mail(subject, message, from_email, white_list,
|
||||
fail_silently=False)
|
||||
if settings.IMPALA_EDIT and perm_setting:
|
||||
template = loader.get_template('amo/emails/unsubscribe.ltxt')
|
||||
for recipient in white_list:
|
||||
# Add unsubscribe link to footer
|
||||
token, hash = UnsubscribeCode.create(recipient)
|
||||
from amo.helpers import absolutify
|
||||
url = absolutify(reverse('users.unsubscribe',
|
||||
args=[token, hash, perm_setting.short]))
|
||||
context = {'message': message, 'unsubscribe': url,
|
||||
'perm_setting': perm_setting.label,
|
||||
'SITE_URL': settings.SITE_URL}
|
||||
send_message = template.render(Context(context,
|
||||
autoescape=False))
|
||||
|
||||
result = django_send_mail(subject, send_message, from_email,
|
||||
[recipient], fail_silently=False,
|
||||
connection=connection)
|
||||
else:
|
||||
result = django_send_mail(subject, message, from_email,
|
||||
white_list, fail_silently=False,
|
||||
connection=connection)
|
||||
else:
|
||||
result = True
|
||||
except Exception as e:
|
||||
|
@ -438,24 +452,6 @@ class MenuItem():
|
|||
url, text, selected, children = ('', '', False, [])
|
||||
|
||||
|
||||
def send_abuse_report(request, obj, url, message):
|
||||
"""Send email about an abusive addon/user/relationship."""
|
||||
if request.user.is_anonymous():
|
||||
user_name = 'An anonymous user'
|
||||
else:
|
||||
user_name = '%s (%s)' % (request.amo_user.name,
|
||||
request.amo_user.email)
|
||||
|
||||
subject = 'Abuse Report for %s' % obj.name
|
||||
msg = u'%s reported abuse for %s (%s%s).\n\n%s'
|
||||
msg = msg % (user_name, obj.name, settings.SITE_URL, url, message)
|
||||
msg += '\n\nhttp://translate.google.com/#auto|en|%s' % message
|
||||
|
||||
log.debug('Abuse reported by %s for %s: %s.' %
|
||||
(smart_str(user_name), obj.id, smart_str(obj.name)))
|
||||
send_mail(subject, msg, recipient_list=(settings.FLIGTAR,))
|
||||
|
||||
|
||||
def to_language(locale):
|
||||
"""Like django's to_language, but en_US comes out as en-US."""
|
||||
# A locale looks like en_US or fr.
|
||||
|
@ -506,9 +502,10 @@ def memoize(prefix, time=60):
|
|||
key based on stringing args and kwargs. Keep args simple.
|
||||
"""
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
key = hashlib.md5()
|
||||
for arg in itertools.chain(args, kwargs):
|
||||
for arg in itertools.chain(args, sorted(kwargs.items())):
|
||||
key.update(str(arg))
|
||||
key = '%s:memoize:%s:%s' % (settings.CACHE_PREFIX,
|
||||
prefix, key.hexdigest())
|
||||
|
|
|
@ -136,6 +136,7 @@ def monitor(request, format=None):
|
|||
settings.COLLECTIONS_ICON_PATH,
|
||||
settings.PACKAGER_PATH,
|
||||
settings.PREVIEWS_PATH,
|
||||
settings.PERSONAS_PATH,
|
||||
settings.USERPICS_PATH,
|
||||
settings.SPHINX_CATALOG_PATH,
|
||||
settings.SPHINX_LOG_PATH,
|
||||
|
|
|
@ -11,5 +11,21 @@ class EmailWidget(Input):
|
|||
|
||||
def render(self, name, value, attrs=None):
|
||||
attrs = attrs or {}
|
||||
attrs['placeholder'] = self.placeholder
|
||||
if self.placeholder:
|
||||
attrs['placeholder'] = self.placeholder
|
||||
return super(EmailWidget, self).render(name, value, attrs)
|
||||
|
||||
|
||||
class ColorWidget(Input):
|
||||
"""HTML5 color type."""
|
||||
input_type = 'color'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.placeholder = kwargs.pop('placeholder', None)
|
||||
return super(ColorWidget, self).__init__(*args, **kwargs)
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
attrs = attrs or {}
|
||||
if self.placeholder:
|
||||
attrs['placeholder'] = self.placeholder
|
||||
return super(ColorWidget, self).render(name, value, attrs)
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
from functools import partial
|
||||
|
||||
import commonware.log
|
||||
import jingo
|
||||
from piston.authentication.oauth import OAuthAuthentication, views
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
|
||||
from access.middleware import ACLMiddleware
|
||||
from users.models import UserProfile
|
||||
from zadmin import jinja_for_django
|
||||
|
||||
# This allows the views in piston.authentication.oauth to cope with
|
||||
|
@ -15,6 +17,9 @@ jfd = lambda a, b, c: jinja_for_django(a, b, context_instance=c)
|
|||
views.render_to_response = jfd
|
||||
|
||||
|
||||
log = commonware.log.getLogger('z.api')
|
||||
|
||||
|
||||
class AMOOAuthAuthentication(OAuthAuthentication):
|
||||
"""^^^MOO!!! Adds amo_user to the request object."""
|
||||
|
||||
|
@ -31,6 +36,25 @@ class AMOOAuthAuthentication(OAuthAuthentication):
|
|||
rv = super(AMOOAuthAuthentication, self).is_authenticated(request)
|
||||
|
||||
if rv and request.user:
|
||||
# The user is there, but we need to alter the user to be
|
||||
# a user specified in the request. Specifically chose this
|
||||
# term to avoid conflict with user, which could be used elsewhere.
|
||||
if self.two_legged and 'authenticate_as' in request.REQUEST:
|
||||
pk = request.REQUEST.get('authenticate_as')
|
||||
try:
|
||||
profile = UserProfile.objects.get(pk=pk)
|
||||
except UserProfile.DoesNotExist:
|
||||
log.warning('Cannot find user: %s' % pk)
|
||||
return False
|
||||
|
||||
if profile.deleted or profile.confirmationcode:
|
||||
log.warning('Tried to use deleted or unconfirmed user: %s'
|
||||
% pk)
|
||||
return False
|
||||
|
||||
log.info('Authenticating as: %s' % pk)
|
||||
request.user = profile.user
|
||||
|
||||
# If that worked and request.user got set, setup AMO specific bits.
|
||||
ACLMiddleware().process_request(request)
|
||||
else:
|
||||
|
@ -43,6 +67,7 @@ class AMOOAuthAuthentication(OAuthAuthentication):
|
|||
return rv
|
||||
|
||||
def _challenge(self, request):
|
||||
response = jingo.render(request, 'piston/oauth/challenge.html', status=401)
|
||||
response = jingo.render(request, 'piston/oauth/challenge.html',
|
||||
status=401)
|
||||
response['WWW-Authenticate'] = 'OAuth realm="API"'
|
||||
return response
|
||||
|
|
|
@ -3,16 +3,20 @@ import functools
|
|||
from django.db import transaction
|
||||
|
||||
import commonware.log
|
||||
import happyforms
|
||||
from piston.handler import AnonymousBaseHandler, BaseHandler
|
||||
from piston.utils import rc, throttle
|
||||
from piston.utils import rc
|
||||
from tower import ugettext as _
|
||||
|
||||
import amo
|
||||
from access import acl
|
||||
from addons.forms import AddonForm
|
||||
from addons.models import Addon
|
||||
from addons.models import Addon, AddonUser
|
||||
from apps.devhub.forms import ReviewTypeForm
|
||||
from amo.utils import paginate
|
||||
from devhub.forms import LicenseForm
|
||||
from perf.forms import PerformanceForm
|
||||
from perf.models import Performance
|
||||
from perf.models import (Performance, PerformanceAppVersions,
|
||||
PerformanceOSVersion)
|
||||
from users.models import UserProfile
|
||||
from versions.forms import XPIForm
|
||||
from versions.models import Version, ApplicationsVersions
|
||||
|
@ -76,34 +80,34 @@ class UserHandler(BaseHandler):
|
|||
|
||||
|
||||
class AddonsHandler(BaseHandler):
|
||||
allowed_methods = ('POST', 'PUT', 'DELETE')
|
||||
allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
|
||||
model = Addon
|
||||
|
||||
fields = ('id', 'name', 'eula', 'guid')
|
||||
fields = ('id', 'name', 'eula', 'guid', 'status')
|
||||
exclude = ('highest_status', 'icon_type')
|
||||
|
||||
# Custom handler so translated text doesn't look weird
|
||||
@classmethod
|
||||
def name(cls, addon):
|
||||
return addon.name.localized_string
|
||||
return addon.name.localized_string if addon.name else ''
|
||||
|
||||
# We need multiple validation, so don't use @validate decorators.
|
||||
@transaction.commit_on_success
|
||||
def create(self, request):
|
||||
license_form = LicenseForm(request.POST)
|
||||
|
||||
if not license_form.is_valid():
|
||||
return _form_error(license_form)
|
||||
|
||||
new_file_form = XPIForm(request, request.POST, request.FILES)
|
||||
|
||||
if not new_file_form.is_valid():
|
||||
return _xpi_form_error(new_file_form, request)
|
||||
|
||||
license = license_form.save()
|
||||
# License Form can be optional
|
||||
license = None
|
||||
if 'builtin' in request.POST:
|
||||
license_form = LicenseForm(request.POST)
|
||||
if not license_form.is_valid():
|
||||
return _form_error(license_form)
|
||||
license = license_form.save()
|
||||
|
||||
a = new_file_form.create_addon(license=license)
|
||||
return a
|
||||
return new_file_form.create_addon(license=license)
|
||||
|
||||
@check_addon_and_version
|
||||
def update(self, request, addon):
|
||||
|
@ -118,6 +122,31 @@ class AddonsHandler(BaseHandler):
|
|||
addon.delete(msg='Deleted via API')
|
||||
return rc.DELETED
|
||||
|
||||
def read(self, request, addon_id=None):
|
||||
"""
|
||||
Returns authors who can update an addon (not Viewer role) for addons
|
||||
that have not been admin disabled. Optionally provide an addon id.
|
||||
"""
|
||||
if not request.user.is_authenticated():
|
||||
return rc.BAD_REQUEST
|
||||
ids = (AddonUser.objects.values_list('addon_id', flat=True)
|
||||
.filter(user=request.amo_user,
|
||||
role__in=[amo.AUTHOR_ROLE_DEV,
|
||||
amo.AUTHOR_ROLE_OWNER]))
|
||||
qs = (Addon.objects.filter(id__in=ids)
|
||||
.exclude(status=amo.STATUS_DISABLED)
|
||||
.no_transforms())
|
||||
if addon_id:
|
||||
try:
|
||||
return qs.get(id=addon_id)
|
||||
except Addon.DoesNotExist:
|
||||
rc.NOT_HERE
|
||||
|
||||
paginator = paginate(request, qs)
|
||||
return {'objects': paginator.object_list,
|
||||
'num_pages': paginator.paginator.num_pages,
|
||||
'count': paginator.paginator.count}
|
||||
|
||||
|
||||
class ApplicationsVersionsHandler(AnonymousBaseHandler):
|
||||
model = ApplicationsVersions
|
||||
|
@ -182,46 +211,42 @@ class VersionsHandler(BaseHandler, BaseVersionHandler):
|
|||
anonymous = AnonymousVersionsHandler
|
||||
|
||||
@check_addon_and_version
|
||||
@throttle(5, 60 * 60) # 5 new versions an hour
|
||||
def create(self, request, addon):
|
||||
# This has license data
|
||||
license_form = LicenseForm(request.POST)
|
||||
|
||||
if not license_form.is_valid():
|
||||
return _form_error(license_form)
|
||||
|
||||
new_file_form = XPIForm(request, request.POST, request.FILES,
|
||||
addon=addon)
|
||||
|
||||
if not new_file_form.is_valid():
|
||||
return _xpi_form_error(new_file_form, request)
|
||||
|
||||
license = license_form.save()
|
||||
license = None
|
||||
if 'builtin' in request.POST:
|
||||
license_form = LicenseForm(request.POST)
|
||||
if not license_form.is_valid():
|
||||
return _form_error(license_form)
|
||||
license = license_form.save()
|
||||
|
||||
v = new_file_form.create_version(license=license)
|
||||
return v
|
||||
|
||||
@check_addon_and_version
|
||||
@throttle(10, 60 * 60)
|
||||
def update(self, request, addon, version):
|
||||
# This has license data.
|
||||
license_form = LicenseForm(request.POST)
|
||||
|
||||
if license_form.is_valid():
|
||||
license = license_form.save()
|
||||
else:
|
||||
license = version.license
|
||||
|
||||
new_file_form = XPIForm(request, request.PUT, request.FILES,
|
||||
version=version)
|
||||
|
||||
if not new_file_form.is_valid():
|
||||
return _xpi_form_error(new_file_form, request)
|
||||
|
||||
license = None
|
||||
if 'builtin' in request.POST:
|
||||
license_form = LicenseForm(request.POST)
|
||||
if not license_form.is_valid():
|
||||
return _form_error(license_form)
|
||||
license = license_form.save()
|
||||
|
||||
v = new_file_form.update_version(license)
|
||||
return v
|
||||
|
||||
@check_addon_and_version
|
||||
@throttle(10, 60 * 60) # allow 10 deletes an hour
|
||||
def delete(self, request, addon, version):
|
||||
version.delete()
|
||||
return rc.DELETED
|
||||
|
@ -231,25 +256,69 @@ class VersionsHandler(BaseHandler, BaseVersionHandler):
|
|||
return addon.versions.all()
|
||||
|
||||
|
||||
class PerformanceHandler(BaseHandler):
|
||||
allowed_methods = ('PUT', 'POST')
|
||||
model = Performance
|
||||
fields = ('addon', 'average', 'appversion', 'osversion', 'test')
|
||||
class AMOBaseHandler(BaseHandler):
|
||||
"""
|
||||
A generic Base Handler that automates create, delete, read and update.
|
||||
For list, we use a pagination handler rather than just returning all.
|
||||
For list, if an id is given, only one object is returned.
|
||||
For delete and update the id of the record is required.
|
||||
"""
|
||||
|
||||
def create(self, request):
|
||||
form = PerformanceForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return rc.CREATED
|
||||
return _form_error(form)
|
||||
def get_form(self, *args, **kw):
|
||||
class Form(happyforms.ModelForm):
|
||||
class Meta:
|
||||
model = self.model
|
||||
return Form(*args, **kw)
|
||||
|
||||
def update(self, request, addon_id):
|
||||
def delete(self, request, id):
|
||||
try:
|
||||
perf = Performance.objects.get(addon=addon_id)
|
||||
return self.model.objects.get(pk=id).delete()
|
||||
except Performance.DoesNotExist:
|
||||
return rc.NOT_HERE
|
||||
form = PerformanceForm(request.POST, instance=perf)
|
||||
|
||||
def create(self, request):
|
||||
form = self.get_form(request.POST)
|
||||
if form.is_valid():
|
||||
return form.save()
|
||||
return _form_error(form)
|
||||
|
||||
def read(self, request, id=None):
|
||||
if id:
|
||||
try:
|
||||
return self.model.objects.get(pk=id)
|
||||
except Performance.DoesNotExist:
|
||||
return rc.NOT_HERE
|
||||
else:
|
||||
paginator = paginate(request, self.model.objects.all())
|
||||
return {'objects': paginator.object_list,
|
||||
'num_pages': paginator.paginator.num_pages,
|
||||
'count': paginator.paginator.count}
|
||||
|
||||
def update(self, request, id):
|
||||
try:
|
||||
obj = self.model.objects.get(pk=id)
|
||||
except Performance.DoesNotExist:
|
||||
return rc.NOT_HERE
|
||||
form = self.get_form(request.POST, instance=obj)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return rc.ALL_OK
|
||||
return _form_error(form)
|
||||
|
||||
|
||||
class PerformanceHandler(AMOBaseHandler):
|
||||
allowed_methods = ('DELETE', 'GET', 'POST', 'PUT')
|
||||
model = Performance
|
||||
fields = ('id', 'addon', 'average', 'appversion', 'osversion', 'test')
|
||||
|
||||
|
||||
class PerformanceAppHandler(AMOBaseHandler):
|
||||
allowed_methods = ('DELETE', 'GET', 'POST', 'PUT')
|
||||
model = PerformanceAppVersions
|
||||
fields = ('id', 'app', 'version')
|
||||
|
||||
|
||||
class PerformanceOSHandler(AMOBaseHandler):
|
||||
allowed_methods = ('DELETE', 'GET', 'POST', 'PUT')
|
||||
model = PerformanceOSVersion
|
||||
fields = ('id', 'os', 'version', 'name')
|
||||
|
|
|
@ -24,6 +24,7 @@ from addons.models import (Addon, AppSupport, Feature, Preview)
|
|||
from addons.utils import FeaturedManager
|
||||
from applications.models import Application
|
||||
from bandwagon.models import Collection, CollectionAddon, FeaturedCollection
|
||||
from files.models import File
|
||||
from files.tests.test_models import UploadTest
|
||||
from search.tests import SphinxTestCase
|
||||
from search.utils import stop_sphinx
|
||||
|
@ -492,26 +493,6 @@ class ListTest(TestCase):
|
|||
response = make_call('list')
|
||||
self.assertContains(response, '<addon id', 3)
|
||||
|
||||
def test_randomness(self):
|
||||
"""
|
||||
This tests that we're sufficiently random when recommending addons.
|
||||
|
||||
We can test for this by querying /list/recommended a number of times
|
||||
until we get two response.contents that do not match.
|
||||
"""
|
||||
response = make_call('list/recommended')
|
||||
all_identical = True
|
||||
|
||||
for i in range(99):
|
||||
cache.clear()
|
||||
current_request = make_call('list/recommended')
|
||||
if current_request.content != response.content:
|
||||
all_identical = False
|
||||
break
|
||||
|
||||
assert not all_identical, (
|
||||
"All 100 requests returned the exact same response.")
|
||||
|
||||
def test_type_filter(self):
|
||||
"""
|
||||
This tests that list filtering works.
|
||||
|
@ -731,7 +712,7 @@ class SearchTest(SphinxTestCase):
|
|||
|
||||
|
||||
class LanguagePacks(UploadTest):
|
||||
fixtures = ['addons/listed', 'base/apps']
|
||||
fixtures = ['addons/listed', 'base/apps', 'base/platforms']
|
||||
|
||||
def setUp(self):
|
||||
self.url = reverse('api.language', args=['1.5'])
|
||||
|
@ -766,12 +747,27 @@ class LanguagePacks(UploadTest):
|
|||
res = self.client.get(self.tb_url)
|
||||
self.assertNotContains(res, "<strings><![CDATA[")
|
||||
|
||||
def setup_localepicker(self, platform):
|
||||
self.addon.update(type=amo.ADDON_LPAPP, status=amo.STATUS_PUBLIC)
|
||||
version = self.addon.versions.all()[0]
|
||||
File.objects.create(version=version, platform_id=platform)
|
||||
|
||||
def test_search_wrong_platform(self):
|
||||
self.setup_localepicker(amo.PLATFORM_MAC.id)
|
||||
eq_(self.addon.get_localepicker(), '')
|
||||
|
||||
@patch('apps.files.models.File.get_localepicker')
|
||||
def test_search_right_platform(self, get_localepicker):
|
||||
get_localepicker.return_value = 'some data'
|
||||
self.setup_localepicker(amo.PLATFORM_ANDROID.id)
|
||||
eq_(self.addon.get_localepicker(), 'some data')
|
||||
|
||||
@patch('apps.addons.models.Addon.get_localepicker')
|
||||
def test_localepicker(self, get_localepicker):
|
||||
get_localepicker.return_value = 'some value'
|
||||
get_localepicker.return_value = unicode('title=اختر لغة', 'utf8')
|
||||
self.addon.update(type=amo.ADDON_LPAPP, status=amo.STATUS_PUBLIC)
|
||||
res = self.client.get(self.url)
|
||||
self.assertContains(res, dedent("""
|
||||
<strings><![CDATA[
|
||||
some value
|
||||
title=اختر لغة
|
||||
]]></strings>"""))
|
||||
|
|
|
@ -40,10 +40,12 @@ from piston.models import Consumer
|
|||
import amo
|
||||
from amo.tests import TestCase
|
||||
from amo.urlresolvers import reverse
|
||||
from addons.models import Addon, BlacklistedGuid
|
||||
from api.authentication import AMOOAuthAuthentication
|
||||
from addons.models import Addon, AddonUser, BlacklistedGuid
|
||||
from devhub.models import ActivityLog
|
||||
from perf.models import (Performance, PerformanceAppVersions,
|
||||
PerformanceOSVersion)
|
||||
from test_utils import RequestFactory
|
||||
from translations.models import Translation
|
||||
from versions.models import AppVersion, Version
|
||||
|
||||
|
@ -87,8 +89,10 @@ class OAuthClient(Client):
|
|||
signature_method = oauth.SignatureMethod_HMAC_SHA1()
|
||||
|
||||
def get(self, url, consumer=None, token=None, callback=False,
|
||||
verifier=None):
|
||||
verifier=None, params=None):
|
||||
url = get_absolute_url(url)
|
||||
if params:
|
||||
url = '%s?%s' % (url, urllib.urlencode(params))
|
||||
req = oauth.Request(method='GET', url=url,
|
||||
parameters=_get_args(consumer, callback=callback,
|
||||
verifier=verifier))
|
||||
|
@ -182,6 +186,7 @@ class BaseOAuth(TestCase):
|
|||
|
||||
def setUp(self):
|
||||
self.editor = User.objects.get(email='editor@mozilla.com')
|
||||
self.admin = User.objects.get(email='admin@mozilla.com')
|
||||
consumers = []
|
||||
for status in ('accepted', 'pending', 'canceled', ):
|
||||
c = Consumer(name='a', status=status, user=self.editor)
|
||||
|
@ -217,6 +222,46 @@ class BaseOAuth(TestCase):
|
|||
r = client.get('oauth.request_token', c, callback=True)
|
||||
eq_(r.content, 'Invalid Consumer.')
|
||||
|
||||
@patch('piston.authentication.oauth.OAuthAuthentication.is_authenticated')
|
||||
def _test_auth(self, pk, is_authenticated, two_legged=True):
|
||||
request = RequestFactory().get('/en-US/firefox/2/api/2/user/',
|
||||
data={'authenticate_as': pk})
|
||||
request.user = None
|
||||
|
||||
def alter_request(*args, **kw):
|
||||
request.user = self.admin
|
||||
return True
|
||||
is_authenticated.return_value = True
|
||||
is_authenticated.side_effect = alter_request
|
||||
|
||||
auth = AMOOAuthAuthentication()
|
||||
auth.two_legged = two_legged
|
||||
auth.is_authenticated(request)
|
||||
return request
|
||||
|
||||
def test_login_nonexistant(self):
|
||||
eq_(self.admin, self._test_auth(9999).user)
|
||||
|
||||
def test_login_deleted(self):
|
||||
# If _test_auth returns self.admin, that means the user was
|
||||
# not altered to the user set in authenticate_as.
|
||||
self.editor.get_profile().update(deleted=True)
|
||||
pk = self.editor.get_profile().pk
|
||||
eq_(self.admin, self._test_auth(pk).user)
|
||||
|
||||
def test_login_unconfirmed(self):
|
||||
self.editor.get_profile().update(confirmationcode='something')
|
||||
pk = self.editor.get_profile().pk
|
||||
eq_(self.admin, self._test_auth(pk).user)
|
||||
|
||||
def test_login_works(self):
|
||||
pk = self.editor.get_profile().pk
|
||||
eq_(self.editor, self._test_auth(pk).user)
|
||||
|
||||
def test_login_three_legged(self):
|
||||
pk = self.editor.get_profile().pk
|
||||
eq_(self.admin, self._test_auth(pk, two_legged=False).user)
|
||||
|
||||
|
||||
def activitylog_count(type=None):
|
||||
qs = ActivityLog.objects
|
||||
|
@ -268,6 +313,17 @@ class TestAddon(BaseOAuth):
|
|||
r = self.make_create_request(self.create_data)
|
||||
eq_(r.status_code, 401)
|
||||
|
||||
def test_create_user_altered(self):
|
||||
data = self.create_data
|
||||
data['authenticate_as'] = self.editor.get_profile().pk
|
||||
r = self.make_create_request(data)
|
||||
eq_(r.status_code, 200)
|
||||
|
||||
id = json.loads(r.content)['id']
|
||||
ad = Addon.objects.get(pk=id)
|
||||
eq_(len(ad.authors.all()), 1)
|
||||
eq_(ad.authors.all()[0].pk, self.editor.get_profile().pk)
|
||||
|
||||
def test_create(self):
|
||||
# License (req'd): MIT, GPLv2, GPLv3, LGPLv2.1, LGPLv3, MIT, BSD, Other
|
||||
# Custom License (if other, req'd)
|
||||
|
@ -282,12 +338,17 @@ class TestAddon(BaseOAuth):
|
|||
assert Addon.objects.get(pk=id)
|
||||
|
||||
def test_create_nolicense(self):
|
||||
data = {}
|
||||
|
||||
data = self.create_data.copy()
|
||||
del data['builtin']
|
||||
r = self.make_create_request(data)
|
||||
eq_(r.status_code, 400, r.content)
|
||||
eq_(r.content, 'Bad Request: '
|
||||
'Invalid data provided: This field is required. (builtin)')
|
||||
eq_(r.status_code, 200, r.content)
|
||||
eq_(Addon.objects.count(), 1)
|
||||
|
||||
def test_create_status(self):
|
||||
r = self.make_create_request(self.create_data)
|
||||
eq_(r.status_code, 200, r.content)
|
||||
eq_(json.loads(r.content)['status'], 0)
|
||||
eq_(Addon.objects.count(), 1)
|
||||
|
||||
def test_delete(self):
|
||||
data = self.create_addon()
|
||||
|
@ -454,48 +515,65 @@ class TestAddon(BaseOAuth):
|
|||
|
||||
eq_(r.status_code, 400, r.content)
|
||||
|
||||
def test_update_version_bad_license(self):
|
||||
def test_create_version_no_license(self):
|
||||
data = self.create_addon()
|
||||
id = data['id']
|
||||
a = Addon.objects.get(pk=id)
|
||||
v = a.versions.get()
|
||||
path = 'apps/files/fixtures/files/extension-0.2.xpi'
|
||||
data = dict(
|
||||
release_notes='fukyeah',
|
||||
license_type='FFFF',
|
||||
platform='windows',
|
||||
xpi=open(os.path.join(settings.ROOT, path)),
|
||||
)
|
||||
r = client.put(('api.version', id, v.id), self.accepted_consumer,
|
||||
self.token, data=data, content_type=MULTIPART_CONTENT)
|
||||
data = self.version_data.copy()
|
||||
del data['builtin']
|
||||
r = client.post(('api.versions', id,), self.accepted_consumer,
|
||||
self.token, data=data)
|
||||
|
||||
eq_(r.status_code, 200, r.content)
|
||||
data = json.loads(r.content)
|
||||
id = data['id']
|
||||
v = Version.objects.get(pk=id)
|
||||
eq_(str(v.license.text), 'This is FREE!')
|
||||
assert not v.license
|
||||
|
||||
def test_update_version(self):
|
||||
# Create an addon
|
||||
def create_for_update(self):
|
||||
data = self.create_addon()
|
||||
id = data['id']
|
||||
|
||||
# verify version
|
||||
a = Addon.objects.get(pk=id)
|
||||
v = a.versions.get()
|
||||
|
||||
eq_(v.version, '0.1')
|
||||
return a, v, 'apps/files/fixtures/files/extension-0.2.xpi'
|
||||
|
||||
path = 'apps/files/fixtures/files/extension-0.2.xpi'
|
||||
def test_update_version_no_license(self):
|
||||
a, v, path = self.create_for_update()
|
||||
data = dict(
|
||||
release_notes='fukyeah',
|
||||
license_type='bsd',
|
||||
platform='windows',
|
||||
xpi=open(os.path.join(settings.ROOT, path)),
|
||||
)
|
||||
r = client.put(('api.version', a.id, v.id), self.accepted_consumer,
|
||||
self.token, data=data, content_type=MULTIPART_CONTENT)
|
||||
eq_(r.status_code, 200, r.content)
|
||||
v = a.versions.get()
|
||||
eq_(v.version, '0.2')
|
||||
eq_(v.license, None)
|
||||
|
||||
def test_update_version_bad_license(self):
|
||||
a, v, path = self.create_for_update()
|
||||
data = dict(
|
||||
release_notes='fukyeah',
|
||||
builtin=3,
|
||||
platform='windows',
|
||||
xpi=open(os.path.join(settings.ROOT, path)),
|
||||
)
|
||||
r = client.put(('api.version', a.id, v.id), self.accepted_consumer,
|
||||
self.token, data=data, content_type=MULTIPART_CONTENT)
|
||||
eq_(r.status_code, 400, r.content)
|
||||
|
||||
def test_update_version(self):
|
||||
a, v, path = self.create_for_update()
|
||||
data = dict(
|
||||
release_notes='fukyeah',
|
||||
builtin=2,
|
||||
platform='windows',
|
||||
xpi=open(os.path.join(settings.ROOT, path)),
|
||||
)
|
||||
log_count = activitylog_count()
|
||||
# upload new version
|
||||
r = client.put(('api.version', id, v.id), self.accepted_consumer,
|
||||
r = client.put(('api.version', a.id, v.id), self.accepted_consumer,
|
||||
self.token, data=data, content_type=MULTIPART_CONTENT)
|
||||
eq_(r.status_code, 200, r.content[:1000])
|
||||
|
||||
|
@ -505,6 +583,7 @@ class TestAddon(BaseOAuth):
|
|||
v = a.versions.get()
|
||||
eq_(v.version, '0.2')
|
||||
eq_(str(v.releasenotes), 'fukyeah')
|
||||
eq_(str(v.license.builtin), '2')
|
||||
|
||||
def test_update_version_bad_xpi(self):
|
||||
data = self.create_addon()
|
||||
|
@ -518,7 +597,6 @@ class TestAddon(BaseOAuth):
|
|||
|
||||
data = dict(
|
||||
release_notes='fukyeah',
|
||||
license_type='bsd',
|
||||
platform='windows',
|
||||
)
|
||||
|
||||
|
@ -580,6 +658,51 @@ class TestAddon(BaseOAuth):
|
|||
eq_(expect, val,
|
||||
'Got "%s" was expecting "%s" for "%s".' % (val, expect, attr,))
|
||||
|
||||
def test_no_addons(self):
|
||||
r = client.get('api.addons', self.accepted_consumer, self.token)
|
||||
eq_(json.loads(r.content)['count'], 0)
|
||||
|
||||
def test_no_user(self):
|
||||
addon = Addon.objects.create(type=amo.ADDON_EXTENSION)
|
||||
AddonUser.objects.create(addon=addon, user=self.admin.get_profile(),
|
||||
role=amo.AUTHOR_ROLE_DEV)
|
||||
r = client.get('api.addons', self.accepted_consumer, self.token)
|
||||
eq_(json.loads(r.content)['count'], 0)
|
||||
|
||||
def test_my_addons_only(self):
|
||||
for num in range(0, 2):
|
||||
addon = Addon.objects.create(type=amo.ADDON_EXTENSION)
|
||||
AddonUser.objects.create(addon=addon, user=self.editor.get_profile(),
|
||||
role=amo.AUTHOR_ROLE_DEV)
|
||||
r = client.get('api.addons', self.accepted_consumer, self.token,
|
||||
params={'authenticate_as': self.editor.pk})
|
||||
j = json.loads(r.content)
|
||||
eq_(j['count'], 1)
|
||||
eq_(j['objects'][0]['id'], addon.id)
|
||||
|
||||
def test_one_addon(self):
|
||||
addon = Addon.objects.create(type=amo.ADDON_EXTENSION)
|
||||
AddonUser.objects.create(addon=addon, user=self.editor.get_profile(),
|
||||
role=amo.AUTHOR_ROLE_DEV)
|
||||
r = client.get(('api.addon', addon.pk), self.accepted_consumer,
|
||||
self.token, params={'authenticate_as': self.editor.pk})
|
||||
eq_(json.loads(r.content)['id'], addon.pk)
|
||||
|
||||
def test_my_addons_role(self):
|
||||
addon = Addon.objects.create(type=amo.ADDON_EXTENSION)
|
||||
AddonUser.objects.create(addon=addon, user=self.editor.get_profile(),
|
||||
role=amo.AUTHOR_ROLE_VIEWER)
|
||||
r = client.get('api.addons', self.accepted_consumer, self.token)
|
||||
eq_(json.loads(r.content)['count'], 0)
|
||||
|
||||
def test_my_addons_disabled(self):
|
||||
addon = Addon.objects.create(type=amo.ADDON_EXTENSION,
|
||||
status=amo.STATUS_DISABLED)
|
||||
AddonUser.objects.create(addon=addon, user=self.editor.get_profile(),
|
||||
role=amo.AUTHOR_ROLE_DEV)
|
||||
r = client.get('api.addons', self.accepted_consumer, self.token)
|
||||
eq_(json.loads(r.content)['count'], 0)
|
||||
|
||||
|
||||
class TestPerformance(BaseOAuth):
|
||||
fixtures = BaseOAuth.fixtures + ['base/addon_3615']
|
||||
|
@ -596,7 +719,8 @@ class TestPerformance(BaseOAuth):
|
|||
def test_create_perf(self):
|
||||
res = client.post('api.performance', self.accepted_consumer,
|
||||
self.token, data=self.data)
|
||||
assert res.status_code == 201, res.status_code
|
||||
assert 'id' in json.loads(res.content)
|
||||
assert res.status_code == 200, res.status_code
|
||||
eq_(Performance.objects.count(), 1)
|
||||
eq_(Performance.objects.all()[0].average, 0.1)
|
||||
|
||||
|
@ -605,26 +729,32 @@ class TestPerformance(BaseOAuth):
|
|||
self.token, data=self.data)
|
||||
data = self.data.copy()
|
||||
data['average'] = 0.2
|
||||
res = client.put(('api.performance', 3615), self.accepted_consumer,
|
||||
self.token, data=data)
|
||||
pk = Performance.objects.all()[0].pk
|
||||
res = client.put(('api.performance', pk), self.accepted_consumer,
|
||||
self.token, data=data)
|
||||
assert res.status_code == 200, res.status_code
|
||||
eq_(Performance.objects.count(), 1)
|
||||
eq_(Performance.objects.all()[0].average, 0.2)
|
||||
|
||||
def test_create_perf_wrong(self):
|
||||
data = self.data.copy()
|
||||
data['addon'] = '3616'
|
||||
res = client.post('api.performance', self.accepted_consumer,
|
||||
self.token, data=data)
|
||||
assert res.status_code == 400, res.status_code
|
||||
assert 'addon' in res.content
|
||||
eq_(Performance.objects.count(), 0)
|
||||
|
||||
def test_update_perf_wrong(self):
|
||||
def test_delete_perf(self):
|
||||
client.post('api.performance', self.accepted_consumer,
|
||||
self.token, data=self.data)
|
||||
data = self.data.copy()
|
||||
data['average'] = 0.2
|
||||
res = client.put(('api.performance', 3616), self.accepted_consumer,
|
||||
self.token, data=data)
|
||||
assert res.status_code == 410, res.status_code
|
||||
pk = Performance.objects.all()[0].pk
|
||||
client.delete(('api.performance', pk),
|
||||
self.accepted_consumer, self.token)
|
||||
eq_(Performance.objects.count(), 0)
|
||||
|
||||
def test_paginate(self):
|
||||
for x in range(0, 25):
|
||||
PerformanceAppVersions.objects.create(app='1', version=x)
|
||||
res = client.get('api.performance.app', self.accepted_consumer,
|
||||
self.token)
|
||||
data = json.loads(res.content)
|
||||
eq_(data['objects'][0]['version'], '24')
|
||||
eq_(data['count'], 26)
|
||||
eq_(data['num_pages'], 2)
|
||||
|
||||
res = client.get('api.performance.app', self.accepted_consumer,
|
||||
self.token, params={'page': 2})
|
||||
data = json.loads(res.content)
|
||||
eq_(data['objects'][0]['version'], '4')
|
||||
|
|
|
@ -69,6 +69,17 @@ user_resource = Resource(handler=handlers.UserHandler, **ad)
|
|||
addons_resource = Resource(handler=handlers.AddonsHandler, **ad)
|
||||
version_resource = Resource(handler=handlers.VersionsHandler, **ad)
|
||||
performance_resource = Resource(handler=handlers.PerformanceHandler, **ad)
|
||||
app_resource = Resource(handler=handlers.PerformanceAppHandler, **ad)
|
||||
os_resource = Resource(handler=handlers.PerformanceOSHandler, **ad)
|
||||
|
||||
performance_patterns = patterns('',
|
||||
url(r'^$', performance_resource, name='api.performance'),
|
||||
url(r'^(?P<id>\d+)$', performance_resource, name='api.performance'),
|
||||
url(r'^app/$', app_resource, name='api.performance.app'),
|
||||
url(r'^app/(?P<id>\d+)$', app_resource, name='api.performance.app'),
|
||||
url(r'^os/$', os_resource, name='api.performance.os'),
|
||||
url(r'^os/(?P<id>\d+)$', os_resource, name='api.performance.os'),
|
||||
)
|
||||
|
||||
piston_patterns = patterns('',
|
||||
url(r'^user/$', user_resource, name='api.user'),
|
||||
|
@ -78,9 +89,7 @@ piston_patterns = patterns('',
|
|||
name='api.versions'),
|
||||
url(r'^addon/%s/version/(?P<version_id>\d+)$' % ADDON_ID,
|
||||
version_resource, name='api.version'),
|
||||
url(r'^performance/$', performance_resource, name='api.performance'),
|
||||
url(r'^performance/%s$' % ADDON_ID, performance_resource,
|
||||
name='api.performance'),
|
||||
url(r'^performance/', include(performance_patterns)),
|
||||
)
|
||||
|
||||
urlpatterns = patterns('',
|
||||
|
|
|
@ -17,6 +17,7 @@ import amo.models
|
|||
import sharing.utils as sharing
|
||||
from amo.utils import sorted_groupby
|
||||
from amo.urlresolvers import reverse
|
||||
from product_details import product_details
|
||||
from addons.models import Addon, AddonRecommendation
|
||||
from applications.models import Application
|
||||
from stats.models import CollectionShareCountTotal
|
||||
|
@ -558,3 +559,18 @@ class FeaturedCollection(amo.models.ModelBase):
|
|||
def __unicode__(self):
|
||||
return u'%s (%s: %s)' % (self.collection, self.application,
|
||||
self.locale)
|
||||
|
||||
|
||||
class MonthlyPick(amo.models.ModelBase):
|
||||
LOCALES = (('', u'(Default Locale)'),) + tuple(
|
||||
(i, product_details.languages[i]['native'])
|
||||
for i in settings.AMO_LANGUAGES)
|
||||
|
||||
addon = models.ForeignKey(Addon)
|
||||
blurb = models.TextField()
|
||||
image = models.URLField()
|
||||
locale = models.CharField(max_length=10, choices=LOCALES, unique=True,
|
||||
null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'monthly_pick'
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
{{ breadcrumbs(crumbs) }}
|
||||
{% endwith %}
|
||||
<hgroup>
|
||||
<h2 class="collection">
|
||||
<h2 class="collection" data-collectionid="{{ collection.id }}">
|
||||
<img src="{{ c.icon_url }}" class="icon">
|
||||
<span>{{ c.name }}</span>
|
||||
{% if not c.listed %}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<section>
|
||||
{% for collection in page %}
|
||||
<li>
|
||||
<div class="item collection addon">
|
||||
<div class="hovercard collection addon">
|
||||
<a href="{{ collection.get_url_path() }}">
|
||||
<div class="icon">
|
||||
{% if first_page %}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{{ page_title(category.name if category else _('Extensions')) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block bodyclass %}collections listing s-{{ sorting }}{% endblock %}
|
||||
{% block bodyclass %}collections s-{{ sorting }}{% endblock %}
|
||||
|
||||
{% set sort = {'featured': 'featured',
|
||||
'created': 'newest',
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
{% if request.GET %}
|
||||
{{ impala_breadcrumbs([(url('i_collections.list'), _('Collections')),
|
||||
(None, filter.title)]) }}
|
||||
<h1>{{ title }}</h1>
|
||||
{% else %}
|
||||
{{ impala_breadcrumbs([(None, _('Collections'))]) }}
|
||||
<masthead class="hero" id="collections-landing">
|
||||
|
@ -28,11 +29,8 @@
|
|||
</p>
|
||||
</masthead>
|
||||
{% endif %}
|
||||
<div class="island hero c">
|
||||
<div class="island hero c listing">
|
||||
<header>
|
||||
{% if request.GET %}
|
||||
<h1>{{ title }}</h1>
|
||||
{% endif %}
|
||||
{{ impala_addon_listing_header(url_base, filter.opts, sorting) }}
|
||||
</header>
|
||||
{% if sorting != 'featured' %}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<h2><a href="{{ base_url }}">{{ _('Collections') }}</a></h2>
|
||||
<ul>
|
||||
{% for sort, title in filter.opts %}
|
||||
<li class="s-{{ sort }}"><em><a href="{{ base_url|urlparams(sort=sort) }}">{{ title }}</a></em></li>
|
||||
<li class="s-{{ sort }}"><a href="{{ base_url|urlparams(sort=sort) }}">{{ title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if request.user.is_authenticated() %}
|
||||
|
@ -30,14 +30,14 @@
|
|||
<section id="recently-viewed">
|
||||
<h3>{{ _('Recently Viewed') }}</h3>
|
||||
</section>
|
||||
<section>
|
||||
<h3>{{ _('Add-on Collector') }}</h3>
|
||||
<p>
|
||||
{% trans app=request.APP.pretty %}
|
||||
Get updates on followed collections or manage your own collections
|
||||
directly from {{ app }} with this add-on.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
{{ addon_collector|addon_hovercard }}
|
||||
</section>
|
||||
</nav>
|
||||
<section>
|
||||
<h3>{{ _('Add-on Collector') }}</h3>
|
||||
<p>
|
||||
{% trans app=request.APP.pretty %}
|
||||
Get updates on followed collections or manage your own collections
|
||||
directly from {{ app }} with this add-on.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
{{ addon_collector|addon_hovercard }}
|
||||
</section>
|
||||
|
|
|
@ -50,6 +50,12 @@ class BlocklistItemTest(BlocklistTest):
|
|||
self.app = BlocklistApp.objects.create(blitem=self.item,
|
||||
guid=amo.FIREFOX.guid)
|
||||
|
||||
def stupid_unicode_test(self):
|
||||
junk = u'\xc2\x80\x15\xc2\x80\xc3'
|
||||
url = reverse('blocklist', args=[3, amo.FIREFOX.guid, junk])
|
||||
# Just make sure it doesn't fail.
|
||||
eq_(self.client.get(url).status_code, 200)
|
||||
|
||||
def test_content_type(self):
|
||||
response = self.client.get(self.fx4_url)
|
||||
eq_(response['Content-Type'], 'text/xml')
|
||||
|
|
|
@ -10,6 +10,7 @@ from django.conf import settings
|
|||
from django.db.models import Q, signals as db_signals
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.cache import patch_cache_control
|
||||
from django.utils.encoding import smart_str
|
||||
|
||||
import jingo
|
||||
import redisutils
|
||||
|
@ -28,7 +29,7 @@ BlItem = collections.namedtuple('BlItem', 'rows os modified block_id')
|
|||
def blocklist(request, apiver, app, appver):
|
||||
key = 'blocklist:%s:%s:%s' % (apiver, app, appver)
|
||||
# Use md5 to make sure the memcached key is clean.
|
||||
key = hashlib.md5(key).hexdigest()
|
||||
key = hashlib.md5(smart_str(key)).hexdigest()
|
||||
response = cache.get(key)
|
||||
if response is None:
|
||||
response = _blocklist(request, apiver, app, appver)
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{{ page_title(category.name if category else _('Extensions')) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block bodyclass %}listing s-{{ sorting }}{% endblock %}
|
||||
{% block bodyclass %}s-{{ sorting }}{% endblock %}
|
||||
|
||||
{% if category %}
|
||||
{% block extrahead %}
|
||||
|
@ -24,6 +24,7 @@
|
|||
{% set sort = {'featured': 'featured',
|
||||
'created': 'newest',
|
||||
'popular': 'popular',
|
||||
'users': 'users',
|
||||
'rating': 'averagerating'}.get(sorting, 'updated') %}
|
||||
|
||||
{% if category %}
|
||||
|
|
|
@ -19,6 +19,9 @@
|
|||
<h1>{{ category.name }}</h1>
|
||||
{{ addons[:3]|featured_grid(src='cb-hc-featured',
|
||||
dl_src='cb-dl-featured') }}
|
||||
<div class="banner-box">
|
||||
<div class="banner featured">{{ _('Featured') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@ -38,5 +41,13 @@
|
|||
vital_summary=vital[0], vital_more=vital[1]) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<h2 class="seeall"><a href="{{ url('i_browse.extensions',
|
||||
category.slug)|urlparams(sort='users') }}">
|
||||
{% trans cnt = category.count, name = category.name %}
|
||||
See the {{ cnt }} extension in {{ name }} »
|
||||
{% pluralize %}
|
||||
See all {{ cnt }} extensions in {{ name }} »
|
||||
{% endtrans %}
|
||||
</a></h2>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -21,16 +21,16 @@
|
|||
{% else %}
|
||||
{{ impala_breadcrumbs([(None, _('Extensions'))]) }}
|
||||
{% endif %}
|
||||
<div class="island hero c">
|
||||
{% with heading = {'featured': _('Featured Extensions'),
|
||||
'created': _('Newest Extensions'),
|
||||
'users': _('Most Popular Extensions'),
|
||||
'popular': _('Most Popular Extensions'),
|
||||
'rating': _('Top Rated Extensions'),
|
||||
'updated': _('Last Updated Extensions')}[sorting] %}
|
||||
<h1>{{ category.name if category else heading }}</h1>
|
||||
{% endwith %}
|
||||
<div class="island hero c listing">
|
||||
<header>
|
||||
{% with heading = {'featured': _('Featured Extensions'),
|
||||
'created': _('Newest Extensions'),
|
||||
'users': _('Most Popular Extensions'),
|
||||
'popular': _('Most Popular Extensions'),
|
||||
'rating': _('Top Rated Extensions'),
|
||||
'updated': _('Last Updated Extensions')}[sorting] %}
|
||||
<h1>{{ category.name if category else heading }}</h1>
|
||||
{% endwith %}
|
||||
<a href="{{ feed_url }}" class="feed">{{ _('Subscribe') }}</a>
|
||||
{{ impala_addon_listing_header(url_base, extras, sorting) }}
|
||||
</header>
|
||||
|
|
|
@ -366,6 +366,8 @@ class TestFeaturedLocale(amo.tests.TestCase):
|
|||
cache.clear()
|
||||
FeaturedManager.redis().flushall()
|
||||
reset_featured_addons()
|
||||
FeaturedManager.featured_ids.clear()
|
||||
CreaturedManager.creatured_ids.clear()
|
||||
|
||||
def list_featured(self, content):
|
||||
# Not sure we want to get into testing randomness
|
||||
|
@ -736,6 +738,7 @@ class BaseSearchToolsTest(amo.tests.TestCase):
|
|||
s.addoncategory_set.add(AddonCategory(addon=limon, feature=True))
|
||||
s.addoncategory_set.add(AddonCategory(addon=readit, feature=True))
|
||||
s.save()
|
||||
reset_featured_addons()
|
||||
|
||||
|
||||
class TestSearchToolsPages(BaseSearchToolsTest):
|
||||
|
|
|
@ -11,10 +11,12 @@ from product_details import product_details
|
|||
from mobility.decorators import mobile_template
|
||||
from tower import ugettext_lazy as _lazy
|
||||
|
||||
import amo
|
||||
import amo.models
|
||||
from amo.models import manual_order
|
||||
from addons.models import Addon, Category, AddonCategory
|
||||
from amo.urlresolvers import reverse
|
||||
from addons.models import Addon, Category, AddonCategory
|
||||
from addons.utils import FeaturedManager, CreaturedManager
|
||||
from addons.views import BaseFilter, ESBaseFilter
|
||||
from translations.query import order_by_translation
|
||||
|
||||
|
@ -245,6 +247,7 @@ def category_landing(request, category, is_impala=False):
|
|||
template = 'browse/category_landing.html'
|
||||
return jingo.render(request, template,
|
||||
{'category': category, 'filter': filter,
|
||||
'sorting': filter.field,
|
||||
'search_cat': '%s,0' % category.type})
|
||||
|
||||
|
||||
|
@ -368,37 +371,20 @@ class SearchToolsFilter(AddonFilter):
|
|||
('popular', _lazy(u'Downloads')),
|
||||
('rating', _lazy(u'Rating')))
|
||||
|
||||
def filter(self, field):
|
||||
"""Get the queryset for the given field."""
|
||||
# Ensure that we can combine distinct filters
|
||||
# (like the featured filter)
|
||||
this_filter = self._filter(field)
|
||||
if this_filter.query.distinct:
|
||||
base_qs = self.base_queryset.distinct()
|
||||
else:
|
||||
base_qs = self.base_queryset
|
||||
return this_filter & base_qs
|
||||
|
||||
def filter_featured(self):
|
||||
# Featured search add-ons in all locales:
|
||||
featured_search = Q(
|
||||
type=amo.ADDON_SEARCH,
|
||||
feature__application=self.request.APP.id)
|
||||
APP, LANG = self.request.APP, self.request.LANG
|
||||
ids = FeaturedManager.featured_ids(APP, LANG, amo.ADDON_SEARCH)
|
||||
|
||||
# Featured in the search-tools category:
|
||||
featured_search_cat = Q(
|
||||
type__in=(amo.ADDON_EXTENSION, amo.ADDON_SEARCH),
|
||||
addoncategory__category__application=self.request.APP.id,
|
||||
addoncategory__category__slug='search-tools',
|
||||
addoncategory__feature=True)
|
||||
try:
|
||||
search_cat = Category.objects.get(slug='search-tools',
|
||||
application=APP.id)
|
||||
others = CreaturedManager.creatured_ids(search_cat, LANG)
|
||||
ids.extend(o for o in others if o not in ids)
|
||||
except Category.DoesNotExist:
|
||||
pass
|
||||
|
||||
q = Addon.objects.valid().filter(
|
||||
featured_search | featured_search_cat)
|
||||
|
||||
# Need to make the query distinct because
|
||||
# one addon can be in multiple categories (see
|
||||
# addoncategory join above)
|
||||
return q.distinct()
|
||||
return manual_order(Addon.objects.valid(), ids, 'addons.id')
|
||||
|
||||
|
||||
class SearchExtensionsFilter(AddonFilter):
|
||||
|
|
|
@ -42,7 +42,7 @@ def compatibility_report():
|
|||
for addon in Addon.objects.filter(id__in=chunk):
|
||||
doc = docs[addon.id]
|
||||
doc.update(id=addon.id, slug=addon.slug, binary=addon.binary,
|
||||
name=unicode(addon.name))
|
||||
name=unicode(addon.name), created=addon.created)
|
||||
doc.setdefault('usage', {})[app.id] = updates[addon.id]
|
||||
|
||||
if app not in addon.compatible_apps:
|
||||
|
|
|
@ -50,6 +50,9 @@ LITE_STATUSES = (STATUS_LITE, STATUS_LITE_AND_NOMINATED)
|
|||
MIRROR_STATUSES = (STATUS_PUBLIC, STATUS_BETA,
|
||||
STATUS_LITE, STATUS_LITE_AND_NOMINATED)
|
||||
|
||||
# An add-on in one of these statuses can become premium.
|
||||
PREMIUM_STATUSES = (STATUS_NULL,) + STATUS_UNDER_REVIEW
|
||||
|
||||
# Types of administrative review queues for an add-on:
|
||||
ADMIN_REVIEW_FULL = 1
|
||||
ADMIN_REVIEW_PRELIM = 2
|
||||
|
@ -93,6 +96,7 @@ ADDON_TYPE = {
|
|||
ADDON_PLUGIN: _(u'Plugin'),
|
||||
ADDON_LPAPP: _(u'Language Pack (Application)'),
|
||||
ADDON_PERSONA: _(u'Persona'),
|
||||
ADDON_WEBAPP: _(u'App'),
|
||||
}
|
||||
|
||||
# Plural
|
||||
|
@ -136,6 +140,14 @@ ADDON_SLUGS_UPDATE = {
|
|||
ADDON_PLUGIN: 'plugin',
|
||||
}
|
||||
|
||||
ADDON_FREE = 0
|
||||
ADDON_PREMIUM = 1
|
||||
|
||||
ADDON_PREMIUM_TYPES = {
|
||||
ADDON_FREE: 'free',
|
||||
ADDON_PREMIUM: 'premium',
|
||||
}
|
||||
|
||||
# Edit addon information
|
||||
MAX_TAGS = 20
|
||||
MIN_TAG_LENGTH = 2
|
||||
|
@ -147,6 +159,15 @@ ADDON_ICON_SIZES = [32, 48, 64]
|
|||
# Preview upload sizes [thumb, full]
|
||||
ADDON_PREVIEW_SIZES = [(200, 150), (700, 525)]
|
||||
|
||||
# Persona image sizes [preview, full]
|
||||
PERSONA_IMAGE_SIZES = {
|
||||
'header': [(680, 100), (3000, 200)],
|
||||
'footer': [None, (3000, 100)],
|
||||
}
|
||||
|
||||
# Accepted image MIME-types
|
||||
IMG_TYPES = ('image/png', 'image/jpeg', 'image/jpg')
|
||||
|
||||
# These types don't maintain app compatibility in the db. Instead, we look at
|
||||
# APP.types and APP_TYPE_SUPPORT to figure out where they are compatible.
|
||||
NO_COMPAT = (ADDON_SEARCH, ADDON_PERSONA)
|
||||
|
@ -212,3 +233,44 @@ VERSION_SEARCH = re.compile('\.(\d+)$')
|
|||
|
||||
# Editor Tools
|
||||
EDITOR_VIEWING_INTERVAL = 8 # How often we ping for "who's watching?"
|
||||
|
||||
# Paypal is an awful place that doesn't understand locales. Instead they have
|
||||
# country codes. This maps our locales to their codes.
|
||||
PAYPAL_COUNTRYMAP = {
|
||||
'af': 'ZA', 'ar': 'EG', 'ca': 'ES', 'cs': 'CZ', 'cy': 'GB', 'da': 'DK',
|
||||
'de': 'DE', 'de-AT': 'AT', 'de-CH': 'CH', 'el': 'GR', 'en-GB': 'GB',
|
||||
'eu': 'BS', 'fa': 'IR', 'fi': 'FI', 'fr': 'FR', 'he': 'IL', 'hu': 'HU',
|
||||
'id': 'ID', 'it': 'IT', 'ja': 'JP', 'ko': 'KR', 'mn': 'MN', 'nl': 'NL',
|
||||
'pl': 'PL', 'ro': 'RO', 'ru': 'RU', 'sk': 'SK', 'sl': 'SI', 'sq': 'AL',
|
||||
'sr': 'CS', 'tr': 'TR', 'uk': 'UA', 'vi': 'VI',
|
||||
}
|
||||
|
||||
# Source, PayPal docs, PP_AdaptivePayments.PDF
|
||||
PAYPAL_CURRENCIES = {
|
||||
'AUD': _('Australian Dollar'),
|
||||
'BRL': _('Brazilian Real'),
|
||||
'CAD': _('Canadian Dollar'),
|
||||
'CZK': _('Czech Koruna'),
|
||||
'DKK': _('Danish Krone'),
|
||||
'EUR': _('Euro'),
|
||||
'HKD': _('Hong Kong Dollar'),
|
||||
'HUF': _('Hungararian Forint'),
|
||||
'ILS': _('Israeli New Sheqel'),
|
||||
'JPY': _('Japanese Yen'),
|
||||
'MYR': _('Malaysian Ringgit'),
|
||||
'MXN': _('Mexican Peso'),
|
||||
'NOK': _('Norwegian Krone'),
|
||||
'NZD': _('New Zealand Dollar'),
|
||||
'PHP': _('Philippine Peso'),
|
||||
'PLN': _('Polish Zloty'),
|
||||
'GBP': _('Pound Sterling'),
|
||||
'SGD': _('Singapore Dollar'),
|
||||
'SEK': _('Swedish Krona'),
|
||||
'CHF': _('Swiss Franc'),
|
||||
'TWD': _('Taiwan New Dollar'),
|
||||
'THB': _('Thai Baht'),
|
||||
'USD': _('U.S. Dollar'),
|
||||
}
|
||||
|
||||
OTHER_CURRENCIES = PAYPAL_CURRENCIES.copy()
|
||||
del OTHER_CURRENCIES['USD']
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from lib.licenses import license_text
|
||||
from tower import ugettext_lazy as _
|
||||
|
||||
|
||||
# Built-in Licenses
|
||||
class _LicenseBase(object):
|
||||
"""Base class for built-in licenses."""
|
||||
|
@ -85,14 +86,72 @@ class LICENSE_COPYRIGHT(_LicenseBase):
|
|||
|
||||
class LICENSE_CC_BY_NC_SA(_LicenseBase):
|
||||
id = 8
|
||||
name = _(u'Creative Commons Attribution-Noncommercial-Share Alike 3.0')
|
||||
name = _(u'Creative Commons Attribution-NonCommercial-Share Alike 3.0')
|
||||
linktext = _(u'Some rights reserved')
|
||||
url = 'http://creativecommons.org/licenses/by-nc-sa/3.0/'
|
||||
shortname = None
|
||||
icons = ('cc-attrib', 'cc-noncom', 'cc-share')
|
||||
on_form = False
|
||||
|
||||
|
||||
# TODO(cvan): Need migrations for these licenses.
|
||||
class LICENSE_CC_BY(_LicenseBase):
|
||||
id = 9
|
||||
name = _(u'Creative Commons Attribution 3.0')
|
||||
linktext = _(u'Some rights reserved')
|
||||
url = 'http://creativecommons.org/licenses/by/3.0/'
|
||||
shortname = None
|
||||
icons = ('cc-attrib',)
|
||||
on_form = False
|
||||
|
||||
|
||||
class LICENSE_CC_BY_NC(_LicenseBase):
|
||||
id = 10
|
||||
name = _(u'Creative Commons Attribution-NonCommercial 3.0')
|
||||
linktext = _(u'Some rights reserved')
|
||||
url = 'http://creativecommons.org/licenses/by-nc/3.0/'
|
||||
shortname = None
|
||||
icons = ('cc-attrib', 'cc-noncom')
|
||||
on_form = False
|
||||
|
||||
|
||||
class LICENSE_CC_BY_NC_ND(_LicenseBase):
|
||||
id = 11
|
||||
name = _(u'Creative Commons Attribution-NonCommercial-NoDerivs 3.0')
|
||||
linktext = _(u'Some rights reserved')
|
||||
url = 'http://creativecommons.org/licenses/by-nc-nd/3.0/'
|
||||
shortname = None
|
||||
icons = ('cc-attrib', 'cc-noncom', 'cc-noderiv')
|
||||
on_form = False
|
||||
|
||||
|
||||
class LICENSE_CC_BY_ND(_LicenseBase):
|
||||
id = 12
|
||||
name = _(u'Creative Commons Attribution-NoDerivs 3.0')
|
||||
linktext = _(u'Some rights reserved')
|
||||
url = 'http://creativecommons.org/licenses/by-nd/3.0/'
|
||||
shortname = None
|
||||
icons = ('cc-attrib', 'cc-noderiv')
|
||||
on_form = False
|
||||
|
||||
|
||||
class LICENSE_CC_BY_SA(_LicenseBase):
|
||||
id = 13
|
||||
name = _(u'Creative Commons Attribution-ShareAlike 3.0')
|
||||
linktext = _(u'Some rights reserved')
|
||||
url = 'http://creativecommons.org/licenses/by-sa/3.0/'
|
||||
shortname = None
|
||||
icons = ('cc-attrib', 'cc-share')
|
||||
on_form = False
|
||||
|
||||
|
||||
PERSONA_LICENSES = (LICENSE_CC_BY, LICENSE_CC_BY_NC,
|
||||
LICENSE_CC_BY_NC_ND, LICENSE_CC_BY_NC_SA, LICENSE_CC_BY_ND,
|
||||
LICENSE_CC_BY_SA)
|
||||
LICENSES = (LICENSE_CUSTOM, LICENSE_COPYRIGHT, LICENSE_MPL, LICENSE_GPL2,
|
||||
LICENSE_GPL3, LICENSE_LGPL21, LICENSE_LGPL3, LICENSE_MIT,
|
||||
LICENSE_BSD, LICENSE_CC_BY_NC_SA)
|
||||
LICENSE_BSD) + PERSONA_LICENSES
|
||||
LICENSE_IDS = dict((license.id, license) for license in LICENSES)
|
||||
|
||||
PERSONA_LICENSES = (LICENSE_COPYRIGHT,) + PERSONA_LICENSES
|
||||
PERSONA_LICENSES_IDS = [(l.id, l) for l in PERSONA_LICENSES]
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import functools
|
||||
|
||||
from django import http
|
||||
|
||||
from amo.decorators import login_required
|
||||
from access import acl
|
||||
from addons.decorators import addon_view
|
||||
from devhub.models import SubmitStep
|
||||
|
||||
|
||||
def dev_required(owner_for_post=False, allow_editors=False):
|
||||
"""Requires user to be add-on owner or admin.
|
||||
|
||||
When allow_editors is True, an editor can view the page.
|
||||
"""
|
||||
def decorator(f):
|
||||
@addon_view
|
||||
@login_required
|
||||
@functools.wraps(f)
|
||||
def wrapper(request, addon, *args, **kw):
|
||||
from devhub.views import _resume
|
||||
fun = lambda: f(request, addon_id=addon.id, addon=addon, *args,
|
||||
**kw)
|
||||
if allow_editors:
|
||||
if acl.action_allowed(request, 'Editors', '%'):
|
||||
return fun()
|
||||
# Require an owner or dev for POST requests.
|
||||
if request.method == 'POST':
|
||||
if acl.check_addon_ownership(request, addon,
|
||||
dev=not owner_for_post):
|
||||
return fun()
|
||||
# Ignore disabled so they can view their add-on.
|
||||
elif acl.check_addon_ownership(request, addon, viewer=True,
|
||||
ignore_disabled=True):
|
||||
step = SubmitStep.objects.filter(addon=addon)
|
||||
# Redirect to the submit flow if they're not done.
|
||||
if not getattr(f, 'submitting', False) and step:
|
||||
return _resume(addon, step)
|
||||
return fun()
|
||||
return http.HttpResponseForbidden()
|
||||
return wrapper
|
||||
# The arg will be a function if they didn't pass owner_for_post.
|
||||
if callable(owner_for_post):
|
||||
f = owner_for_post
|
||||
owner_for_post = False
|
||||
return decorator(f)
|
||||
else:
|
||||
return decorator
|
|
@ -377,7 +377,7 @@ class NewAddonForm(happyforms.Form):
|
|||
|
||||
def clean(self):
|
||||
if not self.errors:
|
||||
xpi = parse_addon(self.cleaned_data['upload'].path)
|
||||
xpi = parse_addon(self.cleaned_data['upload'])
|
||||
addons.forms.clean_name(xpi['name'])
|
||||
self._clean_all_platforms()
|
||||
return self.cleaned_data
|
||||
|
@ -396,7 +396,7 @@ class NewVersionForm(NewAddonForm):
|
|||
|
||||
def clean(self):
|
||||
if not self.errors:
|
||||
xpi = parse_addon(self.cleaned_data['upload'].path, self.addon)
|
||||
xpi = parse_addon(self.cleaned_data['upload'], self.addon)
|
||||
if self.addon.versions.filter(version=xpi['version']):
|
||||
raise forms.ValidationError(
|
||||
_('Version %s already exists') % xpi['version'])
|
||||
|
@ -445,7 +445,7 @@ class NewFileForm(happyforms.Form):
|
|||
|
||||
# Check for errors in the xpi.
|
||||
if not self.errors:
|
||||
xpi = parse_addon(self.cleaned_data['upload'].path, self.addon)
|
||||
xpi = parse_addon(self.cleaned_data['upload'], self.addon)
|
||||
if xpi['version'] != self.version.version:
|
||||
raise forms.ValidationError(_("Version doesn't match"))
|
||||
return self.cleaned_data
|
||||
|
@ -460,7 +460,8 @@ class FileForm(happyforms.ModelForm):
|
|||
|
||||
def __init__(self, *args, **kw):
|
||||
super(FileForm, self).__init__(*args, **kw)
|
||||
if kw['instance'].version.addon.type == amo.ADDON_SEARCH:
|
||||
if kw['instance'].version.addon.type in (amo.ADDON_SEARCH,
|
||||
amo.ADDON_WEBAPP):
|
||||
del self.fields['platform']
|
||||
else:
|
||||
compat = kw['instance'].version.compatible_platforms()
|
||||
|
@ -761,3 +762,7 @@ class CheckCompatibilityForm(happyforms.Form):
|
|||
def clean_app_version(self):
|
||||
v = self.cleaned_data['app_version']
|
||||
return AppVersion.objects.get(pk=int(v))
|
||||
|
||||
|
||||
class NewWebappForm(happyforms.Form):
|
||||
manifest = forms.URLField()
|
||||
|
|
|
@ -155,7 +155,7 @@ def status_class(addon):
|
|||
amo.STATUS_LITE_AND_NOMINATED: 'lite-nom',
|
||||
amo.STATUS_PURGATORY: 'purgatory',
|
||||
}
|
||||
if addon.disabled_by_user:
|
||||
if addon.disabled_by_user and addon.status != amo.STATUS_DISABLED:
|
||||
cls = 'disabled'
|
||||
else:
|
||||
cls = classes.get(addon.status, 'none')
|
||||
|
|
|
@ -153,6 +153,12 @@ class ActivityLogManager(amo.models.ManagerBase):
|
|||
.values_list('activity_log', flat=True))
|
||||
return self.filter(pk__in=list(vals))
|
||||
|
||||
def for_developer(self):
|
||||
return self.exclude(action__in=amo.LOG_ADMINS + amo.LOG_HIDE_DEVELOPER)
|
||||
|
||||
def admin_events(self):
|
||||
return self.filter(action__in=amo.LOG_ADMINS)
|
||||
|
||||
def editor_events(self):
|
||||
return self.filter(action__in=amo.LOG_EDITORS)
|
||||
|
||||
|
@ -187,7 +193,7 @@ class SafeFormatter(string.Formatter):
|
|||
|
||||
|
||||
class ActivityLog(amo.models.ModelBase):
|
||||
TYPES = [(value, key) for key, value in amo.LOG.items()]
|
||||
TYPES = sorted([(value.id, key) for key, value in amo.LOG.items()])
|
||||
user = models.ForeignKey('users.UserProfile', null=True)
|
||||
action = models.SmallIntegerField(choices=TYPES, db_index=True)
|
||||
_arguments = models.TextField(blank=True, db_column='arguments')
|
||||
|
@ -216,7 +222,7 @@ class ActivityLog(amo.models.ModelBase):
|
|||
for item in d:
|
||||
# item has only one element.
|
||||
model_name, pk = item.items()[0]
|
||||
if model_name in ('str', 'int'):
|
||||
if model_name in ('str', 'int', 'null'):
|
||||
objs.append(pk)
|
||||
else:
|
||||
(app_label, model_name) = model_name.split('.')
|
||||
|
|
|
@ -2,21 +2,25 @@ import json
|
|||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import sys
|
||||
import traceback
|
||||
import urllib2
|
||||
import urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from amo.utils import slugify
|
||||
from celeryutils import task
|
||||
|
||||
from addons.models import Addon
|
||||
from celeryutils import task
|
||||
from tower import ugettext as _
|
||||
|
||||
import amo
|
||||
from amo.decorators import write, set_modified_on
|
||||
from amo.utils import resize_image
|
||||
from files.models import FileUpload, File, FileValidation
|
||||
from amo.utils import slugify, resize_image
|
||||
from addons.models import Addon
|
||||
from applications.management.commands import dump_apps
|
||||
from applications.models import Application, AppVersion
|
||||
from files.models import FileUpload, File, FileValidation
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
@ -280,3 +284,54 @@ def packager(data, feature_set, **kw):
|
|||
shutil.move(xpi_path, packager_path(data['uuid']))
|
||||
except IOError:
|
||||
log.error('Error unlocking add-on: %s' % xpi_path)
|
||||
|
||||
|
||||
def failed_validation(*messages):
|
||||
"""Return a validation object that looks like the add-on validator."""
|
||||
return json.dumps({'errors': 1, 'success': False, 'messages': messages})
|
||||
|
||||
|
||||
def _fetch_manifest(url):
|
||||
try:
|
||||
response = urllib2.urlopen(url, timeout=5)
|
||||
except urllib2.URLError, e:
|
||||
# Unpack the URLError to try and find a useful message.
|
||||
if isinstance(e.reason, socket.timeout):
|
||||
raise Exception(_('Connection to "%s" timed out.') % url)
|
||||
elif isinstance(e.reason, socket.gaierror):
|
||||
raise Exception(_('Could not contact host at "%s".') % url)
|
||||
else:
|
||||
raise Exception(str(e.reason))
|
||||
|
||||
content_type = 'application/x-web-app-manifest+json'
|
||||
if not response.headers.get('Content-Type', '').startswith(content_type):
|
||||
if 'Content-Type' in response.headers:
|
||||
raise Exception(_('Your manifest must be served with the HTTP '
|
||||
'header "Content-Type: %s". We saw "%s".')
|
||||
% (content_type, response.headers['Content-Type']))
|
||||
else:
|
||||
raise Exception(_('Your manifest must be served with the HTTP '
|
||||
'header "Content-Type: %s".') % content_type)
|
||||
|
||||
# Read one extra byte. Reject if it's too big so we don't have issues
|
||||
# downloading huge files.
|
||||
content = response.read(settings.MAX_WEBAPP_UPLOAD_SIZE + 1)
|
||||
if len(content) > settings.MAX_WEBAPP_UPLOAD_SIZE:
|
||||
raise Exception(_('Your manifest must be less than %s bytes.')
|
||||
% settings.MAX_WEBAPP_UPLOAD_SIZE)
|
||||
return content
|
||||
|
||||
|
||||
@task
|
||||
def fetch_manifest(url, upload_pk=None, **kw):
|
||||
log.info(u'[1@None] Fetching manifest: %s.' % url)
|
||||
upload = FileUpload.objects.get(pk=upload_pk)
|
||||
try:
|
||||
content = _fetch_manifest(url)
|
||||
except Exception, e:
|
||||
# Drop a message in the validation slot and bail.
|
||||
return upload.update(validation=failed_validation(e.message))
|
||||
|
||||
upload.add_file([content], url, len(content))
|
||||
# Send the upload to the validator.
|
||||
validator(upload.pk)
|
||||
|
|
|
@ -1,135 +0,0 @@
|
|||
{% extends "devhub/base.html" %}
|
||||
|
||||
{% set title = _('Manage Payments') %}
|
||||
{% block title %}{{ dev_page_title(title, addon) }}{% endblock %}
|
||||
|
||||
{% set can_edit = (addon.status == amo.STATUS_PUBLIC
|
||||
and check_addon_ownership(request, addon)) %}
|
||||
{% block bodyclass %}
|
||||
{{ super() }}{% if not can_edit %} no-edit{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<header>
|
||||
{{ dev_breadcrumbs(addon, items=[(None, title)]) }}
|
||||
{{ l10n_menu(addon.default_locale) }}
|
||||
<h2>{{ title }}</h2>
|
||||
</header>
|
||||
<section class="primary payments devhub-form" role="main">
|
||||
|
||||
{% if not can_edit or not addon.has_full_profile() %}
|
||||
<div class="notification-box warning">
|
||||
<h2>
|
||||
{% trans url=url('devhub.addons.profile', addon.slug) %}
|
||||
Payments are only available for fully reviewed add-ons with a <a href="{{ url }}">completed developer profile</a>.
|
||||
{% endtrans %}
|
||||
</h2>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% set contrib = addon.takes_contributions and addon.has_full_profile() or errors %}
|
||||
{% if contrib and not errors %}
|
||||
<div id="status-bar">
|
||||
<p>
|
||||
{{ _('You are currently requesting <b>contributions</b> from users')|safe }}
|
||||
<br>
|
||||
<span class="light">
|
||||
{% trans url=url('addons.about', addon.slug),
|
||||
url_full=url('addons.about', addon.slug, host=settings.SITE_URL) %}
|
||||
Your contribution page: <a href="{{ url }}">{{ url_full }}</a>
|
||||
{% endtrans %}
|
||||
</span>
|
||||
</p>
|
||||
<form method="post" action="{{ url('devhub.addons.payments.disable', addon.slug) }}">
|
||||
{{ csrf() }}
|
||||
<button type="submit">{{ _('Disable Contributions') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% elif not errors %}
|
||||
<div class="intro">
|
||||
<h3>{{ _('Contributions') }}</h3>
|
||||
<p>{{ _('Voluntary contributions provide a way for users to support your add-on financially. With contributions, you can:') }}</p>
|
||||
<ul>
|
||||
<li>{{ _('Ask for contributions in most places your add-on appears') }}</li>
|
||||
<li>{{ _('Allow users to make payments with a credit card or PayPal account') }}</li>
|
||||
<li>{{ _('Receive contributions in your PayPal account or send them to an organization of your choice') }}</li>
|
||||
</ul>
|
||||
<div class="button-wrapper">
|
||||
<a href="#setup" id="do-setup" class="button prominent">{{ _('Set up Contributions') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="setup" class="{{ 'hidden' if not contrib }}">
|
||||
<h3>{{ _('Contributions') if contrib else _('Set up Contributions') }}</h3>
|
||||
<p class="{{ 'hidden' if contrib }}">{{ _('Fill in the fields below to begin asking for voluntary contributions from users.') }}</p>
|
||||
<form id="payments-setup-form" method="post" action="{{ url('devhub.addons.payments', addon.slug) }}">
|
||||
{{ csrf() }}
|
||||
{% set values = contrib_form.data if contrib_form.is_bound else contrib_form.initial %}
|
||||
<div>
|
||||
{{ contrib_form.non_field_errors() }}
|
||||
{{ contrib_form.recipient.errors }}
|
||||
<b>{{ _('Who will receive contributions to this add-on?') }}</b>
|
||||
{{ contrib_form.recipient }}
|
||||
<div id="org-org" class="brform paypal {{ 'hidden' if (values.recipient != 'org') }}">
|
||||
{{ charity_form.non_field_errors() }}
|
||||
{{ charity_form.name.errors }}
|
||||
<label for="id_charity-name">{{ _('What is the name of the organization?') }}</label>
|
||||
{{ charity_form.name }}
|
||||
{{ charity_form.url.errors }}
|
||||
<label for="id_charity-url">{{ _('What is the URL of the organization?') }}</label>
|
||||
{{ charity_form.url }}
|
||||
{{ charity_form.paypal.errors }}
|
||||
<label for="id_charity-paypal">{{ _('What is the PayPal ID of the organization?') }}</label>
|
||||
{{ charity_form.paypal }}
|
||||
</div>
|
||||
</div>
|
||||
<div id="org-dev" class="brform paypal {{ 'hidden' if (values.recipient != 'dev') }}">
|
||||
{{ contrib_form.paypal_id.errors }}
|
||||
<label for="id_paypal_id">{{ _('What is your PayPal ID?') }}</label>
|
||||
<div>{{ contrib_form.paypal_id }} <a class="extra" href="{{ settings.PAYPAL_CGI_URL + '?cmd=_registration-run' }}">{{ _('Sign up for Paypal') }}</a></div>
|
||||
</div>
|
||||
<div class="brform">
|
||||
{{ contrib_form.suggested_amount.errors }}
|
||||
<label for="id_suggested_amount">{{ _('What is your suggested contribution?') }}</label>
|
||||
<div class="extra">{{ contrib_form.suggested_amount.help_text }}</div>
|
||||
<div>{{ contrib_form.suggested_amount }} USD {{ _('(Example: 3.99)') }}</div>
|
||||
</div>
|
||||
<div class="nag">
|
||||
{{ contrib_form.annoying.errors }}
|
||||
<b>{{ _('When should users be asked for contributions?') }}</b>
|
||||
<div class="extra">{{ contrib_form.annoying.help_text }}</div>
|
||||
{{ contrib_form.annoying }}
|
||||
</div>
|
||||
<div>
|
||||
<b>{{ _('Send a thank-you note?') }}</b><br>
|
||||
{{ contrib_form.enable_thankyou }}
|
||||
<label for="{{ contrib_form.enable_thankyou.auto_id }}">
|
||||
{{ _("We'll automatically email users who contribute to your add-on with this message.") }}</label>
|
||||
<div class="thankyou-note {{ 'hidden' if not contrib_form.initial.enable_thankyou }}">
|
||||
<div class="extra">{% trans %}
|
||||
We recommend thanking the user and telling them how much you appreciate their
|
||||
support. You might also want to tell them about what's next for your add-on and
|
||||
about any other add-ons you've made for them to try.
|
||||
{% endtrans %}</div>
|
||||
{{ contrib_form.thankyou_note.errors }}
|
||||
{{ contrib_form.thankyou_note }}
|
||||
<label data-for="thankyou_note"></label>
|
||||
</div>
|
||||
</div>
|
||||
{% if not addon.has_full_profile() %}
|
||||
{% with slim=True %}
|
||||
{% include "devhub/includes/addons_create_profile.html" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
<button type="submit">{{ _('Save Changes') if contrib else _('Activate Contributions') }}</button>
|
||||
<span class="{{ 'hidden' if contrib }}">
|
||||
{% trans %}
|
||||
or <a id="setup-cancel" href="#">Cancel</a>
|
||||
{% endtrans %}
|
||||
</span>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% include "devhub/includes/addons_edit_nav.html" %}
|
||||
{% endblock %}
|
|
@ -7,32 +7,35 @@
|
|||
<h3>{{ _('Submission Process') }}</h3>
|
||||
</hgroup>
|
||||
<ol class="submit-addon-progress">
|
||||
{% for text in _('Getting Started'),
|
||||
_('Upload your add-on'),
|
||||
_('Describe your add-on'),
|
||||
_('Add images'),
|
||||
_('Select a license'),
|
||||
_('Select a review process'),
|
||||
_("You're done!"): %}
|
||||
<li {% if step.current == loop.index %}class="current"{% endif %}>
|
||||
{% if step.current < HAS_ADDON %}
|
||||
{% if loop.index <= step.current %}
|
||||
<a href="{{ url('devhub.submit.%s' % loop.index) }}">{{ text }}</a>
|
||||
{% for text, show_for_webapp
|
||||
in (_('Getting Started'), True),
|
||||
(_('Upload your add-on'), True),
|
||||
(_('Describe your add-on'), True),
|
||||
(_('Add images'), True),
|
||||
(_('Select a license'), True),
|
||||
(_('Select a review process'), False),
|
||||
(_("You're done!"), True): %}
|
||||
{% if not addon or not addon.is_webapp() or show_for_webapp: %}
|
||||
<li {% if step.current == loop.index %}class="current"{% endif %}>
|
||||
{% if step.current < HAS_ADDON %}
|
||||
{% if loop.index <= step.current %}
|
||||
<a href="{{ url('devhub.submit.%s' % loop.index) }}">{{ text }}</a>
|
||||
{% else %}
|
||||
{{ text }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ text }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{# 1. We have an addon, so don't link to non-addon steps.
|
||||
2. Don't link steps above the max step the addon has reached.
|
||||
3. If step.max == MAX the addon is done, so we only show the final page. #}
|
||||
{% if loop.index < HAS_ADDON or loop.index > step.max or step.max == MAX %}
|
||||
{{ text }}
|
||||
{% else %}
|
||||
<a href="{{ url('devhub.submit.%s' % loop.index, addon.slug) }}">
|
||||
{{ text }}</a>
|
||||
{# 1. We have an addon, so don't link to non-addon steps.
|
||||
2. Don't link steps above the max step the addon has reached.
|
||||
3. If step.max == MAX the addon is done, so we only show the final page. #}
|
||||
{% if loop.index < HAS_ADDON or loop.index > step.max or step.max == MAX %}
|
||||
{{ text }}
|
||||
{% else %}
|
||||
<a href="{{ url('devhub.submit.%s' % loop.index, addon.slug) }}">
|
||||
{{ text }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,19 @@
|
|||
{% block title %}{{ dev_page_title(_('Step 2'), addon) }}{% endblock %}
|
||||
|
||||
{% block primary %}
|
||||
|
||||
|
||||
{% if waffle.flag('accept-webapps') %}
|
||||
<form method="post" id="upload-webapp" action="{{ url('devhub.upload_webapp') }}">
|
||||
{{ csrf() }}
|
||||
<label>Submit a webapp manifest by URL:
|
||||
<input type="url" id="upload-webapp" name="manifest"
|
||||
data-upload-url="{{ url('devhub.upload_webapp') }}">
|
||||
</label>
|
||||
<button type="submit">Alright then!</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" id="create-addon" class="item" action="">
|
||||
{{ csrf() }}
|
||||
<h3>{{ _('Step 2. Upload Your Add-on') }}</h3>
|
||||
|
|
|
@ -103,6 +103,8 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{# TODO(Kumar) move to base template? #}
|
||||
<script src="{{ url('wafflejs') }}"></script>
|
||||
{{ js('zamboni/devhub') }}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{% extends "devhub/base.html" %}
|
||||
|
||||
{% block title %}{{ docs_page_title(_('Marketplace')) }}{% endblock %}
|
|
@ -1,9 +1,4 @@
|
|||
{% macro tip(name, tip) %}
|
||||
{% if name %}
|
||||
<span class="label">{{ name }}</span>
|
||||
{% endif %}
|
||||
<span class="tip tooltip" title="{{ tip }}">?</span>
|
||||
{% endmacro %}
|
||||
{% extends "includes/forms.html" %}
|
||||
|
||||
{% macro some_html_tip() %}
|
||||
<p class="html-support">
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<div id="marketplace-confirm" class="hidden">
|
||||
<h3>{{ _('Mozilla Marketplace') }}</h3>
|
||||
<p>{% trans doc_url=url('devhub.docs', doc_name='marketplace'),
|
||||
agree_url=url('devhub.docs', doc_name='policies', doc_page='agreement') %}
|
||||
Thanks for your interest in selling your add-on in the Mozilla Marketplace.
|
||||
Please be sure to read our <a href="{{ doc_url }}">Marketplace Documentation and Policies</a>
|
||||
before continuing, as well as the <a href="{{ agree_url }}">Developer Agreement</a> you agreed
|
||||
to when first submitting this add-on.
|
||||
{% endtrans %}</p>
|
||||
<p>{% trans %}Please confirm this is the premium add-on you wish to sell.{% endtrans %}</p>
|
||||
<div class="indent">
|
||||
<h4>{{ addon.name }}</h4>
|
||||
<p>{{ addon.guid }}</p>
|
||||
</div>
|
||||
<p>{% trans %}If this is the free version of your add-on, please create a new UUID
|
||||
for your premium add-on and submit it separately before enrolling in Mozilla Marketplace.
|
||||
{% endtrans %}</p>
|
||||
<button id="marketplace-submit" type="submit">{{ _('Sell this Add-on') }}</button>
|
||||
{% trans %}
|
||||
or <a id="marketplace-cancel" href="#">Cancel</a>
|
||||
{% endtrans %}
|
||||
</div>
|
|
@ -0,0 +1,92 @@
|
|||
{% extends "devhub/base.html" %}
|
||||
|
||||
{% set title = _('Manage Payments') %}
|
||||
{% block title %}{{ dev_page_title(title, addon) }}{% endblock %}
|
||||
|
||||
{% set can_edit = (addon.status == amo.STATUS_PUBLIC
|
||||
and check_addon_ownership(request, addon)) %}
|
||||
{% block bodyclass %}
|
||||
{{ super() }}{% if not can_edit %} no-edit{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<header>
|
||||
{{ dev_breadcrumbs(addon, items=[(None, title)]) }}
|
||||
{{ l10n_menu(addon.default_locale) }}
|
||||
<h2>{{ title }}</h2>
|
||||
</header>
|
||||
<section class="primary payments devhub-form" role="main">
|
||||
|
||||
{% if not can_edit or not addon.has_full_profile() %}
|
||||
<div class="notification-box warning">
|
||||
<h2>
|
||||
{% trans url=url('devhub.addons.profile', addon.slug) %}
|
||||
Payments are only available for fully reviewed add-ons with a <a href="{{ url }}">completed developer profile</a>.
|
||||
{% endtrans %}
|
||||
</h2>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% set contrib = addon.takes_contributions and addon.has_full_profile() or errors %}
|
||||
{% if contrib and not errors %}
|
||||
<div id="status-bar">
|
||||
<p>
|
||||
{{ _('You are currently requesting <b>contributions</b> from users')|safe }}
|
||||
<br>
|
||||
<span class="light">
|
||||
{% trans url=url('addons.about', addon.slug),
|
||||
url_full=url('addons.about', addon.slug, host=settings.SITE_URL) %}
|
||||
Your contribution page: <a href="{{ url }}">{{ url_full }}</a>
|
||||
{% endtrans %}
|
||||
</span>
|
||||
</p>
|
||||
<form method="post" action="{{ url('devhub.addons.payments.disable', addon.slug) }}">
|
||||
{{ csrf() }}
|
||||
<button type="submit">{{ _('Disable Contributions') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% elif not errors %}
|
||||
<div class="intro">
|
||||
<h3>{{ _('Voluntary Contributions') }}</h3>
|
||||
<p>{{ _('Add-ons enrolled in our contributions program can request voluntary finanical support from users.') }}</p>
|
||||
<ul>
|
||||
<li>{{ _('Encourage users to support your add-on through your Developer Profile') }}</li>
|
||||
<li>{{ _('Choose when and how users are asked to contribute') }}</li>
|
||||
<li>{{ _('Receive contributions in your PayPal account or send them to an organization of your choice') }}</li>
|
||||
</ul>
|
||||
<div class="button-wrapper">
|
||||
<a href="#setup" id="do-setup" class="button prominent">{{ _('Set up Contributions') }}</a>
|
||||
</div>
|
||||
{% if waffle.switch('marketplace') %}
|
||||
<div class="learn-more">
|
||||
{% trans doc_url=url('devhub.docs', doc_name='marketplace', doc_page='voluntary') %}or <a href="{{ doc_url }}">learn more</a>{% endtrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if waffle.switch('marketplace') and addon.can_become_premium() %}
|
||||
<div class="intro">
|
||||
<h3>{{ _('Mozilla Marketplace') }}</h3>
|
||||
<p>{{ _('Premium add-ons can be sold in our marketplace by themselves or as an upgrade to a free version of your add-on.') }}</p>
|
||||
<ul>
|
||||
<li>{{ _('Set your price and place your add-on for sale') }}</li>
|
||||
<li>{{ _('Users pay with their credit card or PayPal account all over the world') }}</li>
|
||||
<li>{{ _('Receive funds immediately in your PayPal account') }}</li>
|
||||
<li>{{ _("Promote your premium add-on on your free add-on's details page") }}</li>
|
||||
</ul>
|
||||
<div class="button-wrapper">
|
||||
<a href="#marketplace-confirm" id="do-marketplace" class="button prominent">{{ _('Enroll in Marketplace') }}</a>
|
||||
</div>
|
||||
<div class="learn-more">
|
||||
{% trans doc_url=url('devhub.docs', doc_name='marketplace') %}or <a href="{{ doc_url }}">learn more</a>{% endtrans %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% include "devhub/payments/voluntary.html" %}
|
||||
{% if waffle.switch('marketplace') and addon.can_become_premium() %}
|
||||
{% include "devhub/payments/marketplace-confirm.html" %}
|
||||
{% endif %}
|
||||
</section>
|
||||
{% include "devhub/includes/addons_edit_nav.html" %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,70 @@
|
|||
<div id="setup" class="{{ 'hidden' if not contrib }}">
|
||||
<h3>{{ _('Contributions') if contrib else _('Set up Contributions') }}</h3>
|
||||
<p class="{{ 'hidden' if contrib }}">{{ _('Fill in the fields below to begin asking for voluntary contributions from users.') }}</p>
|
||||
<form id="payments-setup-form" method="post" action="{{ url('devhub.addons.payments', addon.slug) }}">
|
||||
{{ csrf() }}
|
||||
{% set values = contrib_form.data if contrib_form.is_bound else contrib_form.initial %}
|
||||
<div>
|
||||
{{ contrib_form.non_field_errors() }}
|
||||
{{ contrib_form.recipient.errors }}
|
||||
<b>{{ _('Who will receive contributions to this add-on?') }}</b>
|
||||
{{ contrib_form.recipient }}
|
||||
<div id="org-org" class="brform paypal {{ 'hidden' if (values.recipient != 'org') }}">
|
||||
{{ charity_form.non_field_errors() }}
|
||||
{{ charity_form.name.errors }}
|
||||
<label for="id_charity-name">{{ _('What is the name of the organization?') }}</label>
|
||||
{{ charity_form.name }}
|
||||
{{ charity_form.url.errors }}
|
||||
<label for="id_charity-url">{{ _('What is the URL of the organization?') }}</label>
|
||||
{{ charity_form.url }}
|
||||
{{ charity_form.paypal.errors }}
|
||||
<label for="id_charity-paypal">{{ _('What is the PayPal ID of the organization?') }}</label>
|
||||
{{ charity_form.paypal }}
|
||||
</div>
|
||||
</div>
|
||||
<div id="org-dev" class="brform paypal {{ 'hidden' if (values.recipient != 'dev') }}">
|
||||
{{ contrib_form.paypal_id.errors }}
|
||||
<label for="id_paypal_id">{{ _('What is your PayPal ID?') }}</label>
|
||||
<div>{{ contrib_form.paypal_id }} <a class="extra" href="{{ settings.PAYPAL_CGI_URL + '?cmd=_registration-run' }}">{{ _('Sign up for Paypal') }}</a></div>
|
||||
</div>
|
||||
<div class="brform">
|
||||
{{ contrib_form.suggested_amount.errors }}
|
||||
<label for="id_suggested_amount">{{ _('What is your suggested contribution?') }}</label>
|
||||
<div class="extra">{{ contrib_form.suggested_amount.help_text }}</div>
|
||||
<div>{{ contrib_form.suggested_amount }} USD {{ _('(Example: 3.99)') }}</div>
|
||||
</div>
|
||||
<div class="nag">
|
||||
{{ contrib_form.annoying.errors }}
|
||||
<b>{{ _('When should users be asked for contributions?') }}</b>
|
||||
<div class="extra">{{ contrib_form.annoying.help_text }}</div>
|
||||
{{ contrib_form.annoying }}
|
||||
</div>
|
||||
<div>
|
||||
<b>{{ _('Send a thank-you note?') }}</b><br>
|
||||
{{ contrib_form.enable_thankyou }}
|
||||
<label for="{{ contrib_form.enable_thankyou.auto_id }}">
|
||||
{{ _("We'll automatically email users who contribute to your add-on with this message.") }}</label>
|
||||
<div class="thankyou-note {{ 'hidden' if not contrib_form.initial.enable_thankyou }}">
|
||||
<div class="extra">{% trans %}
|
||||
We recommend thanking the user and telling them how much you appreciate their
|
||||
support. You might also want to tell them about what's next for your add-on and
|
||||
about any other add-ons you've made for them to try.
|
||||
{% endtrans %}</div>
|
||||
{{ contrib_form.thankyou_note.errors }}
|
||||
{{ contrib_form.thankyou_note }}
|
||||
<label data-for="thankyou_note"></label>
|
||||
</div>
|
||||
</div>
|
||||
{% if not addon.has_full_profile() %}
|
||||
{% with slim=True %}
|
||||
{% include "devhub/includes/addons_create_profile.html" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
<button type="submit">{{ _('Save Changes') if contrib else _('Activate Contributions') }}</button>
|
||||
<span class="{{ 'hidden' if contrib }}">
|
||||
{% trans %}
|
||||
or <a id="setup-cancel" href="#">Cancel</a>
|
||||
{% endtrans %}
|
||||
</span>
|
||||
</form>
|
||||
</div>
|
|
@ -16,7 +16,7 @@
|
|||
<h3>{{ _('Current Status') }}</h3>
|
||||
<div class="item" id="version-status">
|
||||
<div>
|
||||
{% if addon.disabled_by_user %}
|
||||
{% if addon.disabled_by_user and addon.status != amo.STATUS_DISABLED %}
|
||||
{{ status(_('You have <b>disabled</b> this add-on.')|safe) }}
|
||||
{{ _("Your add-on's listing is disabled and is not showing anywhere in our gallery or update service. You may re-enable it at any time below.") }}
|
||||
{% elif addon.status == amo.STATUS_NULL %}
|
||||
|
@ -79,7 +79,7 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
{% if check_addon_ownership(request, addon) %}
|
||||
{% if addon.disabled_by_user %}
|
||||
{% if addon.disabled_by_user and addon.status != amo.STATUS_DISABLED %}
|
||||
<a href="{{ url('devhub.addons.enable', addon.slug) }}" id="enable-addon">{{ _('Enable Add-on') }}</a> ·
|
||||
{% elif not addon.is_disabled %}
|
||||
<a href="#" id="disable-addon">{{ _('Disable Add-on') }}</a> ·
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"version": "1.0",
|
||||
"name": "MozillaBall",
|
||||
"description": "Exciting Open Web development action!",
|
||||
"icons": {
|
||||
"16": "/img/icon-16.png",
|
||||
"48": "/img/icon-48.png",
|
||||
"128": "/img/icon-128.png"
|
||||
},
|
||||
"widget": {
|
||||
"path": "/widget.html",
|
||||
"width": 100,
|
||||
"height": 200
|
||||
},
|
||||
"developer": {
|
||||
"name": "Mozilla Labs",
|
||||
"url": "http://mozillalabs.com"
|
||||
},
|
||||
"installs_allowed_from": [
|
||||
"https://appstore.mozillalabs.com"
|
||||
],
|
||||
"locales": {
|
||||
"es": {
|
||||
"description": "¡Acción abierta emocionante del desarrollo del Web!",
|
||||
"developer": {
|
||||
"url": "http://es.mozillalabs.com/"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"description": "Azione aperta emozionante di sviluppo di fotoricettore!",
|
||||
"developer": {
|
||||
"url": "http://it.mozillalabs.com/"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default_locale": "en"
|
||||
}
|
|
@ -281,6 +281,16 @@ class TestActivityLogCount(amo.tests.TestCase):
|
|||
eq_(result[0]['approval_count'], 1)
|
||||
eq_(result[0]['user'], self.user.pk)
|
||||
|
||||
def test_log_admin(self):
|
||||
amo.log(amo.LOG['OBJECT_EDITED'], Addon.objects.get())
|
||||
eq_(len(ActivityLog.objects.admin_events()), 1)
|
||||
eq_(len(ActivityLog.objects.for_developer()), 0)
|
||||
|
||||
def test_log_not_admin(self):
|
||||
amo.log(amo.LOG['EDIT_VERSION'], Addon.objects.get())
|
||||
eq_(len(ActivityLog.objects.admin_events()), 0)
|
||||
eq_(len(ActivityLog.objects.for_developer()), 1)
|
||||
|
||||
|
||||
class TestBlogPosts(amo.tests.TestCase):
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import json
|
||||
import os
|
||||
import path
|
||||
import shutil
|
||||
import socket
|
||||
import tempfile
|
||||
import urllib2
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
@ -13,7 +16,7 @@ import amo.tests
|
|||
from addons.models import Addon
|
||||
from amo.tests.test_helpers import get_image_path
|
||||
from files.models import FileUpload
|
||||
from devhub.tasks import flag_binary, resize_icon, validator
|
||||
from devhub.tasks import flag_binary, resize_icon, validator, fetch_manifest
|
||||
|
||||
|
||||
def test_resize_icon_shrink():
|
||||
|
@ -140,3 +143,95 @@ class TestFlagBinary(amo.tests.TestCase):
|
|||
_mock.side_effect = RuntimeError()
|
||||
flag_binary([self.addon.pk])
|
||||
eq_(Addon.objects.get(pk=self.addon.pk).binary, False)
|
||||
|
||||
|
||||
class TestFetchManifest(amo.tests.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.upload = FileUpload.objects.create()
|
||||
self.content_type = 'application/x-web-app-manifest+json'
|
||||
|
||||
patcher = mock.patch('devhub.tasks.urllib2.urlopen')
|
||||
self.urlopen_mock = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
@mock.patch('devhub.tasks.validator')
|
||||
def test_success_add_file(self, validator_mock):
|
||||
response_mock = mock.Mock()
|
||||
response_mock.read.return_value = 'woo'
|
||||
response_mock.headers = {'Content-Type': self.content_type}
|
||||
self.urlopen_mock.return_value = response_mock
|
||||
|
||||
fetch_manifest('http://xx.com/manifest.json', self.upload.pk)
|
||||
upload = FileUpload.objects.get(pk=self.upload.pk)
|
||||
eq_(upload.name, 'http://xx.com/manifest.json')
|
||||
eq_(open(upload.path).read(), 'woo')
|
||||
|
||||
@mock.patch('devhub.tasks.validator')
|
||||
def test_success_call_validator(self, validator_mock):
|
||||
response_mock = mock.Mock()
|
||||
response_mock.read.return_value = 'woo'
|
||||
ct = self.content_type + '; charset=utf-8'
|
||||
response_mock.headers = {'Content-Type': ct}
|
||||
self.urlopen_mock.return_value = response_mock
|
||||
|
||||
fetch_manifest('http://xx.com/manifest.json', self.upload.pk)
|
||||
assert validator_mock.called
|
||||
|
||||
def check_validation(self, msg):
|
||||
upload = FileUpload.objects.get(pk=self.upload.pk)
|
||||
validation = json.loads(upload.validation)
|
||||
eq_(validation['errors'], 1)
|
||||
eq_(validation['success'], False)
|
||||
eq_(len(validation['messages']), 1)
|
||||
eq_(validation['messages'][0], msg)
|
||||
|
||||
def test_connection_error(self):
|
||||
reason = socket.gaierror(8, 'nodename nor servname provided')
|
||||
self.urlopen_mock.side_effect = urllib2.URLError(reason)
|
||||
fetch_manifest('url', self.upload.pk)
|
||||
self.check_validation('Could not contact host at "url".')
|
||||
|
||||
def test_url_timeout(self):
|
||||
reason = socket.timeout('too slow')
|
||||
self.urlopen_mock.side_effect = urllib2.URLError(reason)
|
||||
fetch_manifest('url', self.upload.pk)
|
||||
self.check_validation('Connection to "url" timed out.')
|
||||
|
||||
def test_other_url_error(self):
|
||||
reason = Exception('Some other failure.')
|
||||
self.urlopen_mock.side_effect = urllib2.URLError(reason)
|
||||
fetch_manifest('url', self.upload.pk)
|
||||
self.check_validation('Some other failure.')
|
||||
|
||||
def test_no_content_type(self):
|
||||
response_mock = mock.Mock()
|
||||
response_mock.read.return_value = 'woo'
|
||||
response_mock.headers = {}
|
||||
self.urlopen_mock.return_value = response_mock
|
||||
|
||||
fetch_manifest('http://xx.com/manifest.json', self.upload.pk)
|
||||
self.check_validation(
|
||||
'Your manifest must be served with the HTTP header '
|
||||
'"Content-Type: application/x-web-app-manifest+json".')
|
||||
|
||||
def test_bad_content_type(self):
|
||||
response_mock = mock.Mock()
|
||||
response_mock.read.return_value = 'woo'
|
||||
response_mock.headers = {'Content-Type': 'x'}
|
||||
self.urlopen_mock.return_value = response_mock
|
||||
|
||||
fetch_manifest('http://xx.com/manifest.json', self.upload.pk)
|
||||
self.check_validation(
|
||||
'Your manifest must be served with the HTTP header '
|
||||
'"Content-Type: application/x-web-app-manifest+json". We saw "x".')
|
||||
|
||||
def test_response_too_large(self):
|
||||
response_mock = mock.Mock()
|
||||
content = 'x' * (settings.MAX_WEBAPP_UPLOAD_SIZE + 1)
|
||||
response_mock.read.return_value = content
|
||||
response_mock.headers = {'Content-Type': self.content_type}
|
||||
self.urlopen_mock.return_value = response_mock
|
||||
|
||||
fetch_manifest('http://xx.com/manifest.json', self.upload.pk)
|
||||
self.check_validation('Your manifest must be less than 2097152 bytes.')
|
||||
|
|
|
@ -669,6 +669,40 @@ class TestPaymentsProfile(amo.tests.TestCase):
|
|||
eq_(self.get_addon().wants_contributions, False)
|
||||
|
||||
|
||||
class TestMarketplace(amo.tests.TestCase):
|
||||
fixtures = ['base/apps', 'base/users', 'base/addon_3615']
|
||||
|
||||
def setUp(self):
|
||||
self.addon = Addon.objects.get(id=3615)
|
||||
self.url = reverse('devhub.addons.payments', args=[self.addon.slug])
|
||||
assert self.client.login(username='del@icio.us', password='password')
|
||||
|
||||
self.marketplace = (waffle.models.Switch.objects
|
||||
.get_or_create(name='marketplace')[0])
|
||||
self.marketplace.active = True
|
||||
self.marketplace.save()
|
||||
|
||||
def tearDown(self):
|
||||
self.marketplace.active = False
|
||||
self.marketplace.save()
|
||||
|
||||
@mock.patch('addons.models.Addon.can_become_premium')
|
||||
def test_ask_page(self, can_become_premium):
|
||||
can_become_premium.return_value = True
|
||||
res = self.client.get(self.url)
|
||||
eq_(res.status_code, 200)
|
||||
doc = pq(res.content)
|
||||
eq_(len(doc('div.intro')), 2)
|
||||
|
||||
@mock.patch('addons.models.Addon.can_become_premium')
|
||||
def test_cant_become_premium(self, can_become_premium):
|
||||
can_become_premium.return_value = False
|
||||
res = self.client.get(self.url)
|
||||
eq_(res.status_code, 200)
|
||||
doc = pq(res.content)
|
||||
eq_(len(doc('div.intro')), 1)
|
||||
|
||||
|
||||
class TestDelete(amo.tests.TestCase):
|
||||
fixtures = ('base/apps', 'base/users', 'base/addon_3615',
|
||||
'base/addon_5579',)
|
||||
|
@ -681,7 +715,7 @@ class TestDelete(amo.tests.TestCase):
|
|||
def get_addon(self):
|
||||
return Addon.objects.no_cache().get(id=3615)
|
||||
|
||||
def test_post_nopw(self):
|
||||
def test_post_not(self):
|
||||
r = self.client.post(self.url, follow=True)
|
||||
eq_(pq(r.content)('.notification-box').text(),
|
||||
'Password was incorrect. Add-on was not deleted.')
|
||||
|
@ -1726,20 +1760,22 @@ class TestActivityFeed(amo.tests.TestCase):
|
|||
r = self.client.get(reverse('devhub.feed', args=[addon.slug]))
|
||||
eq_(r.status_code, 302)
|
||||
|
||||
def add_comment(self):
|
||||
def add_hidden_log(self, action=amo.LOG.COMMENT_VERSION):
|
||||
addon = Addon.objects.get(id=3615)
|
||||
amo.set_user(UserProfile.objects.get(email='del@icio.us'))
|
||||
amo.log(amo.LOG.COMMENT_VERSION, addon, addon.versions.all()[0])
|
||||
amo.log(action, addon, addon.versions.all()[0])
|
||||
return addon
|
||||
|
||||
def test_feed_hidden(self):
|
||||
addon = self.add_comment()
|
||||
addon = self.add_hidden_log()
|
||||
self.add_hidden_log(amo.LOG.OBJECT_ADDED)
|
||||
res = self.client.get(reverse('devhub.feed', args=[addon.slug]))
|
||||
doc = pq(res.content)
|
||||
eq_(len(doc('#recent-activity p')), 1)
|
||||
|
||||
def test_addons_hidden(self):
|
||||
self.add_comment()
|
||||
self.add_hidden_log()
|
||||
self.add_hidden_log(amo.LOG.OBJECT_ADDED)
|
||||
res = self.client.get(reverse('devhub.addons'))
|
||||
doc = pq(res.content)
|
||||
eq_(len(doc('#dashboard-sidebar div.recent-activity li.item')), 0)
|
||||
|
@ -2381,6 +2417,24 @@ class TestSubmitStep6(TestSubmitBase):
|
|||
eq_(self.get_version().nomination.timetuple()[0:5],
|
||||
nomdate.timetuple()[0:5])
|
||||
|
||||
def test_skip_step_for_webapp(self):
|
||||
self.get_addon().update(type=amo.ADDON_WEBAPP)
|
||||
assert self.get_addon().is_webapp()
|
||||
|
||||
r = self.client.get(self.url, follow=True)
|
||||
doc = pq(r.content)
|
||||
|
||||
eq_(r.redirect_chain[0][1], 302)
|
||||
assert r.redirect_chain[0][0].endswith('7')
|
||||
|
||||
addon = self.get_addon()
|
||||
status = (amo.STATUS_PENDING if settings.WEBAPPS_RESTRICTED
|
||||
else amo.STATUS_LITE)
|
||||
eq_(addon.status, status)
|
||||
|
||||
# Make sure the 7th step isn't shown
|
||||
eq_(doc('.submit-addon-progress li').length, 6)
|
||||
|
||||
|
||||
class TestSubmitStep7(TestSubmitBase):
|
||||
|
||||
|
@ -3124,7 +3178,7 @@ class TestUploadErrors(UploadTest):
|
|||
"notices": 0,
|
||||
"message_tree": {},
|
||||
"messages": [],
|
||||
"metadata": {}
|
||||
"metadata": {},
|
||||
})
|
||||
|
||||
def xpi(self):
|
||||
|
@ -3174,7 +3228,7 @@ class TestUploadErrors(UploadTest):
|
|||
eq_(res.status_code, 200)
|
||||
doc = pq(res.content)
|
||||
|
||||
# javascript: upoad file:
|
||||
# javascript: upload file:
|
||||
upload_url = doc('#upload-addon').attr('data-upload-url')
|
||||
with self.xpi() as f:
|
||||
res = self.client.post(upload_url, {'upload': f}, follow=True)
|
||||
|
@ -3247,17 +3301,7 @@ class TestVersionXSS(UploadTest):
|
|||
assert '<script>alert' in r.content
|
||||
|
||||
|
||||
class TestCreateAddon(BaseUploadTest,
|
||||
amo.tests.TestCase):
|
||||
fixtures = ['base/apps', 'base/users', 'base/platforms']
|
||||
|
||||
def setUp(self):
|
||||
super(TestCreateAddon, self).setUp()
|
||||
self.upload = self.get_upload('extension.xpi')
|
||||
self.url = reverse('devhub.submit.2')
|
||||
assert self.client.login(username='regular@mozilla.com',
|
||||
password='password')
|
||||
self.client.post(reverse('devhub.submit.1'))
|
||||
class UploadAddon(object):
|
||||
|
||||
def post(self, desktop_platforms=[amo.PLATFORM_ALL], mobile_platforms=[],
|
||||
expect_errors=False):
|
||||
|
@ -3272,6 +3316,18 @@ class TestCreateAddon(BaseUploadTest,
|
|||
eq_(r.context['new_addon_form'].errors.as_text(), '')
|
||||
return r
|
||||
|
||||
|
||||
class TestCreateAddon(BaseUploadTest, UploadAddon, amo.tests.TestCase):
|
||||
fixtures = ['base/apps', 'base/users', 'base/platforms']
|
||||
|
||||
def setUp(self):
|
||||
super(TestCreateAddon, self).setUp()
|
||||
self.upload = self.get_upload('extension.xpi')
|
||||
self.url = reverse('devhub.submit.2')
|
||||
assert self.client.login(username='regular@mozilla.com',
|
||||
password='password')
|
||||
self.client.post(reverse('devhub.submit.1'))
|
||||
|
||||
def assert_json_error(self, *args):
|
||||
UploadTest().assert_json_error(self, *args)
|
||||
|
||||
|
@ -3312,6 +3368,54 @@ class TestCreateAddon(BaseUploadTest,
|
|||
[u'xpi_name-0.1-linux.xpi', u'xpi_name-0.1-mac.xpi'])
|
||||
|
||||
|
||||
class TestCreateWebApp(BaseUploadTest, UploadAddon, amo.tests.TestCase):
|
||||
fixtures = ['base/apps', 'base/users', 'base/platforms']
|
||||
|
||||
def setUp(self):
|
||||
super(TestCreateWebApp, self).setUp()
|
||||
manifest = os.path.join(settings.ROOT, 'apps', 'devhub', 'tests',
|
||||
'addons', 'mozball.webapp')
|
||||
self.upload = self.get_upload(abspath=manifest)
|
||||
self.url = reverse('devhub.submit.2')
|
||||
assert self.client.login(username='regular@mozilla.com',
|
||||
password='password')
|
||||
self.client.post(reverse('devhub.submit.1'))
|
||||
|
||||
def post(self, desktop_platforms=[], mobile_platforms=[], **kw):
|
||||
return super(TestCreateWebApp, self).post(**kw)
|
||||
|
||||
def post_addon(self):
|
||||
eq_(Addon.objects.count(), 0)
|
||||
self.post()
|
||||
return Addon.objects.get()
|
||||
|
||||
def test_post_addon_redirect(self):
|
||||
r = self.post()
|
||||
addon = Addon.objects.get()
|
||||
self.assertRedirects(r, reverse('devhub.submit.3', args=[addon.slug]))
|
||||
|
||||
def test_addon_from_uploaded_manifest(self):
|
||||
addon = self.post_addon()
|
||||
eq_(addon.type, amo.ADDON_WEBAPP)
|
||||
eq_(addon.guid, None)
|
||||
eq_(unicode(addon.name), 'MozillaBall')
|
||||
eq_(addon.slug, 'app-%s' % addon.id)
|
||||
eq_(addon.app_slug, 'mozillaball')
|
||||
eq_(addon.summary, u'Exciting Open Web development action!')
|
||||
eq_(Translation.objects.get(id=addon.summary.id, locale='it'),
|
||||
u'Azione aperta emozionante di sviluppo di fotoricettore!')
|
||||
|
||||
def test_version_from_uploaded_manifest(self):
|
||||
addon = self.post_addon()
|
||||
eq_(addon.current_version.version, '1.0')
|
||||
|
||||
def test_file_from_uploaded_manifest(self):
|
||||
addon = self.post_addon()
|
||||
files = addon.current_version.files.all()
|
||||
eq_(len(files), 1)
|
||||
eq_(files[0].status, amo.STATUS_PUBLIC)
|
||||
|
||||
|
||||
class TestDeleteAddon(amo.tests.TestCase):
|
||||
fixtures = ['base/apps', 'base/users', 'base/addon_3615']
|
||||
|
||||
|
|
|
@ -3,9 +3,11 @@ import json
|
|||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import traceback
|
||||
|
||||
from django.conf import settings
|
||||
from django import forms
|
||||
|
||||
import mock
|
||||
from nose.plugins.attrib import attr
|
||||
|
@ -22,7 +24,7 @@ from amo.urlresolvers import reverse
|
|||
from applications.models import AppVersion, Application
|
||||
from files.models import File, FileUpload, FileValidation
|
||||
from files.tests.test_models import UploadTest as BaseUploadTest
|
||||
from files.utils import parse_addon
|
||||
from files.utils import parse_addon, WebAppParser
|
||||
from users.models import UserProfile
|
||||
from zadmin.models import ValidationResult
|
||||
|
||||
|
@ -68,7 +70,7 @@ class TestUploadErrors(BaseUploadTest):
|
|||
def test_dupe_uuid(self, flag_is_active):
|
||||
flag_is_active.return_value = True
|
||||
addon = Addon.objects.get(pk=3615)
|
||||
d = parse_addon(self.get_upload('extension.xpi').path)
|
||||
d = parse_addon(self.get_upload('extension.xpi'))
|
||||
addon.update(guid=d['guid'])
|
||||
|
||||
dupe_xpi = self.get_upload('extension.xpi')
|
||||
|
@ -547,7 +549,7 @@ class TestUploadCompatCheck(BaseUploadTest):
|
|||
flag_is_active.return_value = True
|
||||
addon = Addon.objects.get(pk=3615)
|
||||
dupe_xpi = self.get_upload('extension.xpi')
|
||||
d = parse_addon(dupe_xpi.path)
|
||||
d = parse_addon(dupe_xpi)
|
||||
# Set up a duplicate upload:
|
||||
addon.update(guid=d['guid'])
|
||||
data = self.upload(filename=dupe_xpi.path)
|
||||
|
@ -600,3 +602,46 @@ class TestUploadCompatCheck(BaseUploadTest):
|
|||
data = self.upload()
|
||||
eq_(data['validation']['messages'][0]['type'], 'error')
|
||||
eq_(data['validation']['messages'][1]['type'], 'warning')
|
||||
|
||||
|
||||
class TestWebApps(amo.tests.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.webapp_path = os.path.join(os.path.dirname(__file__),
|
||||
'addons', 'mozball.webapp')
|
||||
self.tmp_files = []
|
||||
|
||||
def tearDown(self):
|
||||
for tmp in self.tmp_files:
|
||||
os.unlink(tmp)
|
||||
|
||||
def webapp(self, data=None, contents='', suffix='.webapp'):
|
||||
fp, tmp = tempfile.mkstemp(suffix=suffix)
|
||||
self.tmp_files.append(tmp)
|
||||
with open(tmp, 'w') as f:
|
||||
f.write(json.dumps(data) if data else contents)
|
||||
return tmp
|
||||
|
||||
def test_parse(self):
|
||||
wp = WebAppParser().parse(self.webapp_path)
|
||||
eq_(wp['guid'], None)
|
||||
eq_(wp['type'], amo.ADDON_WEBAPP)
|
||||
eq_(wp['summary']['en-us'], u'Exciting Open Web development action!')
|
||||
eq_(wp['summary']['es'],
|
||||
u'¡Acción abierta emocionante del desarrollo del Web!')
|
||||
eq_(wp['summary']['it'],
|
||||
u'Azione aperta emozionante di sviluppo di fotoricettore!')
|
||||
eq_(wp['version'], '1.0')
|
||||
eq_(wp['default_locale'], 'en-us')
|
||||
|
||||
def test_no_locales(self):
|
||||
wp = WebAppParser().parse(self.webapp(dict(name='foo', version='1.0',
|
||||
description='summary')))
|
||||
eq_(wp['summary']['en-us'], u'summary')
|
||||
|
||||
def test_syntax_error(self):
|
||||
with self.assertRaises(forms.ValidationError) as exc:
|
||||
WebAppParser().parse(self.webapp(contents='}]'))
|
||||
m = exc.exception.messages[0]
|
||||
assert m.startswith('Could not parse webapp manifest'), (
|
||||
'Unexpected: %s' % m)
|
||||
|
|
|
@ -42,14 +42,18 @@ class TestVersion(amo.tests.TestCase):
|
|||
doc = self.get_doc()
|
||||
assert doc('#version-status')
|
||||
|
||||
self.addon.status = amo.STATUS_DISABLED
|
||||
self.addon.save()
|
||||
self.addon.update(status=amo.STATUS_DISABLED, disabled_by_user=True)
|
||||
doc = self.get_doc()
|
||||
assert doc('#version-status .status-admin-disabled')
|
||||
eq_(doc('#version-status strong').text(),
|
||||
'This add-on has been disabled by Mozilla .')
|
||||
|
||||
self.addon.update(disabled_by_user=True)
|
||||
self.addon.update(disabled_by_user=False)
|
||||
doc = self.get_doc()
|
||||
eq_(doc('#version-status strong').text(),
|
||||
'This add-on has been disabled by Mozilla .')
|
||||
|
||||
self.addon.update(status=amo.STATUS_PUBLIC, disabled_by_user=True)
|
||||
doc = self.get_doc()
|
||||
eq_(doc('#version-status strong').text(),
|
||||
'You have disabled this add-on.')
|
||||
|
|
|
@ -156,6 +156,7 @@ urlpatterns = decorate(write, patterns('',
|
|||
name='devhub.standalone_upload'),
|
||||
url('^standalone-upload/([^/]+)$', views.standalone_upload_detail,
|
||||
name='devhub.standalone_upload_detail'),
|
||||
url('^upload-webapp$', views.upload_webapp, name='devhub.upload_webapp'),
|
||||
|
||||
# URLs for a single add-on.
|
||||
url('^addon/%s/' % ADDON_ID, include(detail_patterns)),
|
||||
|
|
|
@ -22,27 +22,29 @@ import bleach
|
|||
import commonware.log
|
||||
import jingo
|
||||
import jinja2
|
||||
from tower import ugettext_lazy as _lazy, ugettext as _
|
||||
from PIL import Image
|
||||
from session_csrf import anonymous_csrf
|
||||
from tower import ugettext_lazy as _lazy, ugettext as _
|
||||
import waffle
|
||||
|
||||
from applications.models import Application, AppVersion
|
||||
import amo
|
||||
import amo.utils
|
||||
from amo import messages, urlresolvers
|
||||
from amo.decorators import json_view, login_required, post_required
|
||||
from amo.helpers import urlparams
|
||||
from amo.utils import MenuItem
|
||||
from amo.urlresolvers import reverse
|
||||
from amo.decorators import json_view, login_required, post_required
|
||||
from access import acl
|
||||
from addons import forms as addon_forms
|
||||
from addons.decorators import addon_view
|
||||
from addons.models import Addon, AddonUser
|
||||
from addons.views import BaseFilter
|
||||
from devhub.decorators import dev_required
|
||||
from devhub.forms import CheckCompatibilityForm
|
||||
from devhub.models import ActivityLog, BlogPost, RssKey, SubmitStep
|
||||
from editors.helpers import get_position
|
||||
from files.models import File, FileUpload
|
||||
from files.models import File, FileUpload, Platform
|
||||
from files.utils import parse_addon
|
||||
from translations.models import delete_translation
|
||||
from users.models import UserProfile
|
||||
|
@ -59,45 +61,6 @@ log = commonware.log.getLogger('z.devhub')
|
|||
DEV_AGREEMENT_COOKIE = 'yes-I-read-the-dev-agreement'
|
||||
|
||||
|
||||
def dev_required(owner_for_post=False, allow_editors=False):
|
||||
"""Requires user to be add-on owner or admin.
|
||||
|
||||
When allow_editors is True, an editor can view the page.
|
||||
"""
|
||||
def decorator(f):
|
||||
@addon_view
|
||||
@login_required
|
||||
@functools.wraps(f)
|
||||
def wrapper(request, addon, *args, **kw):
|
||||
fun = lambda: f(request, addon_id=addon.id, addon=addon, *args,
|
||||
**kw)
|
||||
if allow_editors:
|
||||
if acl.action_allowed(request, 'Editors', '%'):
|
||||
return fun()
|
||||
# Require an owner or dev for POST requests.
|
||||
if request.method == 'POST':
|
||||
if acl.check_addon_ownership(request, addon,
|
||||
dev=not owner_for_post):
|
||||
return fun()
|
||||
# Ignore disabled so they can view their add-on.
|
||||
elif acl.check_addon_ownership(request, addon, viewer=True,
|
||||
ignore_disabled=True):
|
||||
step = SubmitStep.objects.filter(addon=addon)
|
||||
# Redirect to the submit flow if they're not done.
|
||||
if not getattr(f, 'submitting', False) and step:
|
||||
return _resume(addon, step)
|
||||
return fun()
|
||||
return http.HttpResponseForbidden()
|
||||
return wrapper
|
||||
# The arg will be a function if they didn't pass owner_for_post.
|
||||
if callable(owner_for_post):
|
||||
f = owner_for_post
|
||||
owner_for_post = False
|
||||
return decorator(f)
|
||||
else:
|
||||
return decorator
|
||||
|
||||
|
||||
class AddonFilter(BaseFilter):
|
||||
opts = (('name', _lazy(u'Name')),
|
||||
('updated', _lazy(u'Updated')),
|
||||
|
@ -449,7 +412,7 @@ def payments(request, addon_id, addon):
|
|||
errors = charity_form.errors or contrib_form.errors or profile_form.errors
|
||||
if errors:
|
||||
messages.error(request, _('There were errors in your submission.'))
|
||||
return jingo.render(request, 'devhub/addons/payments.html',
|
||||
return jingo.render(request, 'devhub/payments/payments.html',
|
||||
dict(addon=addon, charity_form=charity_form, errors=errors,
|
||||
contrib_form=contrib_form, profile_form=profile_form))
|
||||
|
||||
|
@ -618,6 +581,26 @@ def upload(request, addon_slug=None, is_standalone=False):
|
|||
return redirect('devhub.upload_detail', fu.pk, 'json')
|
||||
|
||||
|
||||
@login_required
|
||||
@post_required
|
||||
def upload_webapp(request):
|
||||
form = forms.NewWebappForm(request.POST)
|
||||
if form.is_valid():
|
||||
# Eventually we'll pass a pointer to the FileUpload back to the client
|
||||
# and wait for the tasks to finish.
|
||||
upload = FileUpload.objects.create()
|
||||
# TODO: make this .delay()'d, communicate asynchronously.
|
||||
tasks.fetch_manifest(form.cleaned_data['manifest'], upload.pk)
|
||||
# Get the new data.
|
||||
upload = FileUpload.objects.get(pk=upload.pk)
|
||||
# TODO: when we go async reuse the submit_addon() code.
|
||||
platform = Platform.objects.get(id=amo.PLATFORM_ALL.id)
|
||||
addon = Addon.from_upload(upload, [platform])
|
||||
AddonUser(addon=addon, user=request.amo_user).save()
|
||||
SubmitStep.objects.create(addon=addon, step=3)
|
||||
return redirect('devhub.submit.3', addon.slug)
|
||||
|
||||
|
||||
@login_required
|
||||
@post_required
|
||||
def standalone_upload(request):
|
||||
|
@ -787,8 +770,8 @@ def json_upload_detail(request, upload, addon_slug=None):
|
|||
if result['validation']:
|
||||
if result['validation']['errors'] == 0:
|
||||
try:
|
||||
apps = parse_addon(upload.path, addon=addon).get('apps', [])
|
||||
app_ids = set([a.id for a in apps])
|
||||
pkg = parse_addon(upload, addon=addon)
|
||||
app_ids = set([a.id for a in pkg.get('apps', [])])
|
||||
supported_platforms = []
|
||||
if amo.MOBILE.id in app_ids:
|
||||
supported_platforms.extend(amo.MOBILE_PLATFORMS.keys())
|
||||
|
@ -952,8 +935,7 @@ def image_status(request, addon_id, addon):
|
|||
|
||||
|
||||
@json_view
|
||||
@dev_required
|
||||
def upload_image(request, addon_id, addon, upload_type):
|
||||
def ajax_upload_image(request, upload_type):
|
||||
errors = []
|
||||
upload_hash = ''
|
||||
|
||||
|
@ -970,19 +952,46 @@ def upload_image(request, addon_id, addon, upload_type):
|
|||
for chunk in upload_preview:
|
||||
fd.write(chunk)
|
||||
|
||||
is_icon = upload_type in ('icon', 'preview')
|
||||
is_persona = upload_type.startswith('persona_')
|
||||
|
||||
check = amo.utils.ImageCheck(upload_preview)
|
||||
if (not check.is_image() or
|
||||
upload_preview.content_type not in
|
||||
('image/png', 'image/jpeg', 'image/jpg')):
|
||||
errors.append(_('Icons must be either PNG or JPG.'))
|
||||
upload_preview.content_type not in amo.IMG_TYPES):
|
||||
if is_icon:
|
||||
errors.append(_('Icons must be either PNG or JPG.'))
|
||||
else:
|
||||
errors.append(_('Images must be either PNG or JPG.'))
|
||||
|
||||
if check.is_animated():
|
||||
errors.append(_('Icons cannot be animated.'))
|
||||
if is_icon:
|
||||
errors.append(_('Icons cannot be animated.'))
|
||||
else:
|
||||
errors.append(_('Images cannot be animated.'))
|
||||
|
||||
if (upload_type == 'icon' and
|
||||
upload_preview.size > settings.MAX_ICON_UPLOAD_SIZE):
|
||||
errors.append(_('Please use images smaller than %dMB.') %
|
||||
(settings.MAX_ICON_UPLOAD_SIZE / 1024 / 1024 - 1))
|
||||
max_size = None
|
||||
if is_icon:
|
||||
max_size = settings.MAX_ICON_UPLOAD_SIZE
|
||||
if is_persona:
|
||||
max_size = settings.MAX_PERSONA_UPLOAD_SIZE
|
||||
|
||||
if max_size and upload_preview.size > max_size:
|
||||
if is_icon:
|
||||
errors.append(_('Please use images smaller than %dMB.') % (
|
||||
max_size / 1024 / 1024 - 1))
|
||||
if is_persona:
|
||||
errors.append(_('Images cannot be larger than %dKB.') % (
|
||||
max_size / 1024))
|
||||
|
||||
if check.is_image() and is_persona:
|
||||
persona, img_type = upload_type.split('_') # 'header' or 'footer'
|
||||
expected_size = amo.PERSONA_IMAGE_SIZES.get(img_type)[1]
|
||||
actual_size = Image.open(loc).size
|
||||
if actual_size != expected_size:
|
||||
# L10n: {0} is an image width (in pixels), {1} is a height.
|
||||
errors.append(_('Image must be exactly {0} pixels wide '
|
||||
'and {1} pixels tall.')
|
||||
.format(expected_size[0], expected_size[1]))
|
||||
else:
|
||||
errors.append(_('There was an error uploading your preview.'))
|
||||
|
||||
|
@ -992,6 +1001,11 @@ def upload_image(request, addon_id, addon, upload_type):
|
|||
return {'upload_hash': upload_hash, 'errors': errors}
|
||||
|
||||
|
||||
@dev_required
|
||||
def upload_image(request, addon_id, addon, upload_type):
|
||||
return ajax_upload_image(request, upload_type)
|
||||
|
||||
|
||||
@dev_required
|
||||
def version_edit(request, addon_id, addon, version_id):
|
||||
version = get_object_or_404(Version, pk=version_id, addon=addon)
|
||||
|
@ -1257,12 +1271,20 @@ def submit_license(request, addon_id, addon, step):
|
|||
@submit_step(6)
|
||||
def submit_select_review(request, addon_id, addon, step):
|
||||
review_type_form = forms.ReviewTypeForm(request.POST or None)
|
||||
if request.method == 'POST' and review_type_form.is_valid():
|
||||
addon.status = review_type_form.cleaned_data['review_type']
|
||||
addon.save()
|
||||
updated_status = None
|
||||
|
||||
if addon.is_webapp():
|
||||
updated_status = (amo.STATUS_PENDING if settings.WEBAPPS_RESTRICTED
|
||||
else amo.STATUS_LITE)
|
||||
elif request.method == 'POST' and review_type_form.is_valid():
|
||||
updated_status = review_type_form.cleaned_data['review_type']
|
||||
|
||||
if updated_status:
|
||||
addon.update(status=updated_status)
|
||||
SubmitStep.objects.filter(addon=addon).delete()
|
||||
signals.submission_done.send(sender=addon)
|
||||
return redirect('devhub.submit.7', addon.slug)
|
||||
|
||||
return jingo.render(request, 'devhub/addons/submit/select-review.html',
|
||||
{'addon': addon, 'review_type_form': review_type_form,
|
||||
'step': step})
|
||||
|
@ -1382,6 +1404,9 @@ def docs(request, doc_name=None, doc_page=None):
|
|||
'how-to': ['getting-started', 'extension-development',
|
||||
'thunderbird-mobile', 'theme-development',
|
||||
'other-addons']}
|
||||
|
||||
if waffle.switch_is_active('marketplace'):
|
||||
all_docs['marketplace'] = ['voluntary']
|
||||
|
||||
if doc_name and doc_name in all_docs:
|
||||
filename = '%s.html' % doc_name
|
||||
|
|
|
@ -2,11 +2,10 @@ import caching.base as caching
|
|||
import jingo
|
||||
import jinja2
|
||||
from tower import ugettext_lazy as _
|
||||
import waffle
|
||||
|
||||
from addons.models import Addon
|
||||
import amo
|
||||
from api.views import addon_filter
|
||||
from bandwagon.models import Collection
|
||||
from bandwagon.models import Collection, MonthlyPick as MP
|
||||
from .models import BlogCacheRyf
|
||||
|
||||
|
||||
|
@ -70,14 +69,15 @@ class MonthlyPick(TemplatePromo):
|
|||
slug = 'Monthly Pick'
|
||||
template = 'discovery/modules/monthly.html'
|
||||
|
||||
# TODO A bit of hardcoding here to support an alternative locale. This
|
||||
# will all be redone with the mango bugs: http://bugzil.la/[t:mango]
|
||||
def context(self):
|
||||
return {
|
||||
'addon': Addon.objects.get(id=6416),
|
||||
'addon_de': Addon.objects.get(id=146384),
|
||||
'module_context': 'discovery'
|
||||
}
|
||||
try:
|
||||
pick = MP.objects.filter(locale=self.request.LANG)[0]
|
||||
except IndexError:
|
||||
try:
|
||||
pick = MP.objects.filter(locale__isnull=True)[0]
|
||||
except IndexError:
|
||||
pick = None
|
||||
return {'pick': pick, 'module_context': 'discovery'}
|
||||
|
||||
|
||||
class GoMobile(TemplatePromo):
|
||||
|
@ -95,22 +95,29 @@ class CollectionPromo(PromoModule):
|
|||
|
||||
def __init__(self, *args, **kw):
|
||||
super(CollectionPromo, self).__init__(*args, **kw)
|
||||
self.collection = Collection.objects.get(pk=self.pk)
|
||||
try:
|
||||
self.collection = Collection.objects.get(pk=self.pk)
|
||||
except Collection.DoesNotExist:
|
||||
self.collection = None
|
||||
|
||||
def get_descriptions(self):
|
||||
return {}
|
||||
|
||||
def get_addons(self):
|
||||
addons = self.collection.addons.all()
|
||||
addons = self.collection.addons.filter(status=amo.STATUS_PUBLIC)
|
||||
kw = dict(addon_type='ALL', limit=self.limit, app=self.request.APP,
|
||||
platform=self.platform, version=self.version, shuffle=True)
|
||||
platform=self.platform, version=self.version)
|
||||
f = lambda: addon_filter(addons, **kw)
|
||||
return caching.cached_with(addons, f, repr(kw))
|
||||
|
||||
def render(self, module_context='discovery'):
|
||||
c = dict(promo=self, addons=self.get_addons(),
|
||||
module_context=module_context,
|
||||
if module_context == 'home':
|
||||
self.platform = 'ALL'
|
||||
self.version = None
|
||||
c = dict(promo=self, module_context=module_context,
|
||||
descriptions=self.get_descriptions())
|
||||
if self.collection:
|
||||
c.update(addons=self.get_addons())
|
||||
return jinja2.Markup(
|
||||
jingo.render_to_string(self.request, self.template, c))
|
||||
|
||||
|
@ -214,7 +221,7 @@ class TravelCollection(CollectionPromo):
|
|||
|
||||
class SchoolCollection(CollectionPromo):
|
||||
slug = 'School'
|
||||
pk = 2128026 # TODO(push): Change this to 2133887.
|
||||
pk = 2133887
|
||||
id = 'school'
|
||||
cls = 'promo'
|
||||
title = _(u'A+ add-ons for School')
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
{% if sidebar %}
|
||||
<span class="htruncate">{{ addon.name }}</span>
|
||||
{% else %}
|
||||
<h3 class="vtruncate">{{ addon.name }}</h3>
|
||||
<h3 class="htruncate">{{ addon.name }}</h3>
|
||||
<p class="desc vtruncate">{{ addon.summary }}</p>
|
||||
{% endif %}
|
||||
</a>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% set collection = promo.collection %}
|
||||
{%- if addons -%}
|
||||
<li class="panel">
|
||||
<div id="{{ promo.id }}" class="feature promo-collection {{ promo.cls }}">
|
||||
<hgroup>
|
||||
|
@ -25,7 +26,7 @@
|
|||
<a href="{{ services_url('discovery.addons.detail', addon.slug,
|
||||
src='discovery-promo') }}" target="_self">
|
||||
{% else %}
|
||||
<a href="{{ url('i_addons.detail', addon.slug,
|
||||
<a href="{{ url('addons.detail', addon.slug,
|
||||
src='hp-dl-promo') }}" target="_self">
|
||||
{% endif %}
|
||||
{% if addon.type == amo.ADDON_PERSONA %}
|
||||
|
@ -42,3 +43,4 @@
|
|||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
{%- endif -%}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{%- if pick -%}
|
||||
<li class="panel">
|
||||
<div id="monthly" class="feature promo">
|
||||
<hgroup>
|
||||
|
@ -5,31 +6,19 @@
|
|||
</hgroup>
|
||||
<div class="wrap">
|
||||
<div>
|
||||
{% if LANG == 'de' %}
|
||||
<h3>{{ addon_de.name }}</h3>
|
||||
<div>
|
||||
<p>Surfen und sparen: Wenn Sie einen Online-Shop besuchen, zeigt Ihnen dieses
|
||||
Add-on, ob für die Seite Gutscheine zur Verfügung stehen.</p>
|
||||
{{ install_button(addon_de, src='discovery-promo') }}
|
||||
</div>
|
||||
{# TODO: Change this screenshot when changing the monthly pick. #}
|
||||
<img src="{{ addon_de.thumbnail_url }}">
|
||||
{% else %}
|
||||
<img src="{{ addon.thumbnail_url }}">
|
||||
<h3>{{ addon.name }}</h3>
|
||||
<div>
|
||||
<p>{% trans %}
|
||||
Include results from your social networks and favorite websites whenever
|
||||
you search the web.
|
||||
{% endtrans %}</p>
|
||||
{% if module_context == 'discovery' %}
|
||||
{{ install_button(addon, src='discovery-promo') }}
|
||||
{% else %}
|
||||
{{ install_button(addon, src='hp-btn-promo', impala=True) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<img src="{{ pick.image }}">
|
||||
{% set addon = pick.addon %}
|
||||
<h3>{{ addon.name }}</h3>
|
||||
<div>
|
||||
<p>{{ pick.blurb }}</p>
|
||||
{% if module_context == 'discovery' %}
|
||||
{{ install_button(addon, src='discovery-promo') }}
|
||||
{% else %}
|
||||
{{ install_button(addon, src='hp-btn-promo', impala=True) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{%- endif -%}
|
||||
|
|
|
@ -14,7 +14,7 @@ import addons.signals
|
|||
from amo.urlresolvers import reverse
|
||||
from addons.models import Addon
|
||||
from applications.models import Application, AppVersion
|
||||
from bandwagon.models import Collection
|
||||
from bandwagon.models import Collection, MonthlyPick
|
||||
from bandwagon.tests.test_models import TestRecommendations as Recs
|
||||
from discovery import views
|
||||
from discovery.forms import DiscoveryModuleForm
|
||||
|
@ -275,7 +275,7 @@ class TestPane(amo.tests.TestCase):
|
|||
url = reverse('discovery.addons.detail', args=[7661])
|
||||
assert a.attr('href').endswith(url + '?src=discovery-featured'), (
|
||||
'Unexpected add-on details URL')
|
||||
eq_(li.find('h3.vtruncate').text(), unicode(addon.name))
|
||||
eq_(li.find('h3').text(), unicode(addon.name))
|
||||
eq_(li.find('img').attr('src'), addon.icon_url)
|
||||
|
||||
addon = Addon.objects.get(id=2464)
|
||||
|
@ -285,7 +285,7 @@ class TestPane(amo.tests.TestCase):
|
|||
url = reverse('discovery.addons.detail', args=[2464])
|
||||
assert a.attr('href').endswith(url + '?src=discovery-featured'), (
|
||||
'Unexpected add-on details URL')
|
||||
eq_(li.find('h3.vtruncate').text(), unicode(addon.name))
|
||||
eq_(li.find('h3').text(), unicode(addon.name))
|
||||
eq_(li.find('img').attr('src'), addon.icon_url)
|
||||
|
||||
@mock.patch.object(settings, 'NEW_FEATURES', False)
|
||||
|
@ -419,3 +419,29 @@ class TestDownloadSources(amo.tests.TestCase):
|
|||
'?src=discovery-upandcoming')
|
||||
assert doc('#install li:eq(1)').find('a').attr('href').endswith(
|
||||
'?src=discovery-upandcoming')
|
||||
|
||||
|
||||
class TestMonthlyPick(amo.tests.TestCase):
|
||||
fixtures = ['base/apps', 'base/addon_3615', 'discovery/discoverymodules']
|
||||
|
||||
def setUp(self):
|
||||
self.url = reverse('discovery.pane', args=['3.7a1pre', 'Darwin'])
|
||||
self.addon = Addon.objects.get(id=3615)
|
||||
DiscoveryModule.objects.create(
|
||||
app=Application.objects.get(id=amo.FIREFOX.id), ordering=4,
|
||||
module='Monthly Pick')
|
||||
|
||||
def test_monthlypick(self):
|
||||
MonthlyPick.objects.create(addon=self.addon, blurb='BOOP',
|
||||
image='http://mozilla.com')
|
||||
r = self.client.get(self.url)
|
||||
pick = pq(r.content)('#monthly')
|
||||
eq_(pick.find('h3').text(), unicode(self.addon.name))
|
||||
eq_(pick.find('img').attr('src'), 'http://mozilla.com')
|
||||
eq_(pick.find('.wrap > div > div > p').text(), 'BOOP')
|
||||
eq_(pick.find('p.install-button a').attr('href')
|
||||
.endswith('?src=discovery-promo'), True)
|
||||
|
||||
def test_no_monthlypick(self):
|
||||
r = self.client.get(self.url)
|
||||
eq_(pq(r.content)('#monthly').length, 0)
|
||||
|
|
|
@ -92,7 +92,8 @@ def get_modules(request, platform, version):
|
|||
|
||||
def get_featured_personas(request):
|
||||
categories, filter, base, category = personas_listing(request)
|
||||
ids = FeaturedManager.featured_ids(request.APP, type=amo.ADDON_PERSONA)
|
||||
ids = FeaturedManager.featured_ids(request.APP, request.LANG,
|
||||
type=amo.ADDON_PERSONA)
|
||||
return manual_order(base, ids)[:6]
|
||||
|
||||
|
||||
|
|
|
@ -397,4 +397,6 @@ class RawSQLModel(object):
|
|||
if value is None:
|
||||
# for NULL fields, ala left joins
|
||||
return []
|
||||
return [cast(i) for i in value.split(sep)]
|
||||
# Cope with a value like ...1261530,1261530, which occurs because of:
|
||||
# 1 line(s) were cut by GROUP_CONCAT()
|
||||
return [cast(i) for i in value.split(sep) if i]
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче