Merge branch 'master' of github.com:jbalogh/zamboni

This commit is contained in:
Jeremy Orem 2011-08-16 17:22:47 -07:00
Родитель 008368933f 8c0bf7526a
Коммит bf2b6a685a
219 изменённых файлов: 6557 добавлений и 1580 удалений

Просмотреть файл

@ -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
apps/abuse/__init__.py Normal file
Просмотреть файл

53
apps/abuse/models.py Normal file
Просмотреть файл

@ -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 &nbsp; in the string so &rarr; does not wrap.
text = jinja2.Markup(_('Continue to Download&nbsp;&rarr;'))
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 &nbsp; in the string so &rarr; does not wrap.
text = jinja2.Markup(_('Continue to Download&nbsp;&rarr;'))
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 &times; 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 &times; 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 %}>
&#x25C2;&#x25C2;</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') }} &#x25B8;</a>
<a href="{{ pager.url|urlparams(page=pager.paginator.num_pages) }}"
title="{{ _('Jump to last page') }}"
{% if not pager.has_next() %}class="disabled"{% endif %}>
&#x25B8;&#x25B8;</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&hellip;') }}</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):

Двоичные данные
apps/amo/tests/images/persona-footer.jpg Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.9 KiB

Двоичные данные
apps/amo/tests/images/persona-header.jpg Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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 }} &raquo;
{% pluralize %}
See all {{ cnt }} extensions in {{ name }} &raquo;
{% 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]

48
apps/devhub/decorators.py Normal file
Просмотреть файл

@ -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> &middot;
{% elif not addon.is_disabled %}
<a href="#" id="disable-addon">{{ _('Disable Add-on') }}</a> &middot;

Просмотреть файл

@ -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 '&lt;script&gt;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]

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше