This commit is contained in:
Chris Van 2011-08-08 17:13:03 -07:00
Родитель 7dc9e886b4
Коммит bb4ef4842c
40 изменённых файлов: 2272 добавлений и 181 удалений

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

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

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

@ -564,7 +564,7 @@ class Addon(amo.models.OnChangeMixin, amo.models.ModelBase):
def update_status(self, using=None):
if (self.status == amo.STATUS_NULL or self.is_disabled
or self.is_webapp()):
or self.is_webapp() or self.is_persona()):
return
def logit(reason, old=self.status):
@ -1047,7 +1047,7 @@ class Persona(caching.CachingMixin, models.Model):
author = models.CharField(max_length=32, null=True)
display_username = models.CharField(max_length=32, null=True)
submit = models.DateTimeField(null=True)
approve = models.DateTimeField(null=True)
approve = models.DateTimeField(null=False)
movers = models.FloatField(null=True, db_index=True)
popularity = models.IntegerField(null=False, default=0, db_index=True)

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

@ -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
@ -137,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)
@ -159,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)

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

@ -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 %}

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

@ -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 abuse.models import AbuseReport
from addons.models import Addon, AddonUser, Charity
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):
@ -1087,6 +1092,7 @@ class TestReportAbuse(amo.tests.TestCase):
assert 'spammy' in mail.outbox[0].body
assert AbuseReport.objects.get(addon=15663)
class TestMobile(amo.tests.TestCase):
fixtures = ['addons/featured', 'base/apps', 'base/addon_3615',
'base/featured', 'bandwagon/featured_collections']
@ -1162,8 +1168,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>[^/<>"']+)"""
@ -70,6 +71,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'),

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

@ -21,9 +21,11 @@ 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
from amo.helpers import absolutify
@ -33,6 +35,7 @@ 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
@ -42,11 +45,13 @@ from translations.query import order_by_translation
from translations.helpers import truncate
from versions.models import Version
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)
@ -817,3 +822,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))

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

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

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

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

После

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

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

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

После

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

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

@ -130,6 +130,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)

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

@ -159,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)

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

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

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

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

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

@ -22,23 +22,25 @@ 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
@ -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')),
@ -952,8 +915,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 +932,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 +981,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)

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

@ -58,11 +58,6 @@
}
&.disabled,
&.concealed {
background: #c1c5ca;
.gradient-two-color(#d1d4d7, #c1c5ca);
color: #919497;
.box-shadow(0 3px rgba(0, 0, 0, 0.05), 0 -4px rgba(0, 0, 0, 0.05) inset);
text-shadow: 0 1px 0 rgba(255,255,255,.5);
top: 0;
span {
.sprite-pos(1, 64, 3px);
@ -213,7 +208,18 @@
}
a.link {
text-decoration: none;
color: #0055EE;
color: #05e;
}
}
button[disabled],
.button.disabled,
.button.concealed {
background: #c1c5ca;
.gradient-two-color(#d1d4d7, #c1c5ca);
color: #919497;
.box-shadow(0 3px rgba(0, 0, 0, 0.05), 0 -4px rgba(0, 0, 0, 0.05) inset);
text-shadow: 0 1px 0 rgba(255,255,255,.5);
}
button[disabled] {
pointer-events: none;
}

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

@ -0,0 +1,59 @@
@import 'lib';
.miniColors-trigger {
border: 4px solid @border-blue;
display: inline-block;
height: 18px;
margin: 0 .25em 1px 0;
outline: none;
vertical-align: bottom;
width: 20px;
}
.miniColors-selector {
background: #fff;
border: solid 1px #bbb;
.border-radius(5px);
.box-shadow(0 0 6px rgba(0, 0, 0, .25));
height: 150px;
padding: 5px;
position: absolute;
width: 175px;
}
.miniColors-colors,
.miniColors-hues {
background: center no-repeat;
cursor: crosshair;
height: 150px;
position: absolute;
top: 5px;
}
.miniColors-colors {
background-image: url(../../img/impala/colorpicker/gradient.png);
left: 5px;
width: 150px;
height: 150px;
}
.miniColors-hues {
background-image: url(../../img/impala/colorpicker/rainbow.png);
left: 160px;
width: 20px;
}
.miniColors-colorPicker {
background: url(../../img/impala/colorpicker/circle.gif) center no-repeat;
height: 11px;
position: absolute;
width: 11px;
}
.miniColors-huePicker {
border-bottom: 2px solid #000;
height: 0;
position: absolute;
left: -3px;
width: 26px;
}

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

@ -13,7 +13,9 @@ label {
font-weight: bold;
}
.req {
.req,
.errorlist,
.error {
color: @error-red;
}
@ -87,12 +89,33 @@ textarea {
}
.prettyform {
.char-count {
float: left;
b {
color: @dark-gray;
}
&.error b {
color: darken(@error-red, 10%);
}
}
.note,
.html-support {
color: @medium-gray;
font-size: 11px;
line-height: 1.4;
}
.html-support {
margin: 0;
padding: 0;
float: right;
span {
border-bottom: 1px dotted #bbb;
cursor: help;
}
}
ul {
font-size: 13px;
&.note {
color: @medium-gray;
font-size: 11px;
line-height: 1.4;
li {
display: inline-block;
&:before {
@ -106,7 +129,6 @@ textarea {
}
}
&.errorlist {
color: #c00;
font-size: 12px;
}
}
@ -137,6 +159,9 @@ textarea {
}
.row {
margin: 0 0 15px;
&.c {
float: left;
}
}
h3,
.row > label {
@ -175,26 +200,6 @@ textarea {
width: 400px;
vertical-align: text-top;
}
p.note {
color: #888;
font-size: 0.9em;
font-style: italic;
margin-top: 0;
padding-left: 138px;
clear: both;
width: 300px;
}
sup {
bottom: 4px;
font-size: 0.7em;
position: relative;
&.msg {
color: @red;
}
}
legend + p {
margin: 0 0 1em;
}
footer {
button {
float: right;
@ -250,6 +255,17 @@ textarea {
margin-top: 0;
}
}
sup {
bottom: 4px;
font-size: 0.7em;
position: relative;
&.msg {
color: @red;
}
}
legend + p {
margin: 0 0 1em;
}
}
.checkboxes,
.radios {
@ -273,6 +289,15 @@ textarea {
}
.html-rtl .prettyform {
.row.c {
float: right;
}
.char-count {
float: right;
}
.html-support {
float: left;
}
ul.note li {
&:before {
padding: 0 0 0 3px;

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

@ -13,3 +13,61 @@
font-style: italic;
}
}
#l10n-menu {
float: right;
p {
margin: 0;
}
}
.html-rtl #l10n-menu {
float: left;
}
#change-locale {
padding-right: 16px;
&:after {
border: 4px solid transparent;
border-style: solid;
border-top-color: #003595;
content: "\00a0";
display: inline-block;
position: relative;
top: 12px;
left: 4px;
width: 0;
height: 0;
}
}
#locale-popup section {
display: block;
height: 300px;
line-height: 30px;
overflow-y: auto;
a {
display: block;
&:hover {
background-color: #eefafe;
text-decoration: none;
}
em {
color: #98bfef;
}
}
> div,
> ul {
border-top: 1px dotted #A4CFDE;
margin-bottom: 0;
}
> div:first-child,
> ul:first-child {
border-top: medium none;
}
}
#existing_locales a.remove {
display: block;
float: right;
margin: 4px;
&:hover {
background-color: #2a4364;
}
}

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

@ -33,6 +33,12 @@
transform: @tform;
}
.background-size(@size) {
-moz-background-size: @size;
-wekbkit-background-size: @size;
background-size: @size;
}
.box-shadow(@shadow) {
box-shadow: @shadow;
-moz-box-shadow: @shadow;

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

@ -0,0 +1,219 @@
@import 'lib';
.notice {
.border-radius(5px);
padding: 1em 1em 1em 104px;
background: #ECF5FE;
margin-bottom: 1em;
h3 {
font-style: italic;
font-size: 16px;
}
p {
margin-top: .5em;
}
b {
color: #333;
}
}
ul.license {
position: relative;
top: 3px;
li {
display: block;
float: left;
list-style: none;
margin-right: 2px;
&.text {
font-size: 90%;
line-height: 15px;
margin-left: 4px;
}
}
}
.html-rtl ul.license {
li {
float: right;
margin: 0 0 0 2px;
&.text {
margin: 0 4px 0 0;
}
}
}
.license .icon,
.license.icon {
background: url(../../img/zamboni/licenses.png) no-repeat;
&.cc-attrib { background-position: 0 0; }
&.cc-noderiv { background-position: 0 -65px; }
&.cc-noncom { background-position: 0 -130px; }
&.cc-share { background-position: 0 -195px; }
&.copyr { background-position: 0 -260px; }
}
/* Persona previews */
.persona-large .persona-,
.persona-large p {
.border-radius(6px);
}
.persona-preview {
.persona-viewer {
background: transparent no-repeat right top;
border-bottom: 1px solid rgba(0, 0, 0, 0.4);
display: table;
position: relative;
height: 100px;
width: 100%;
}
p, .persona-viewer {
.border-radius(6px);
}
}
.persona-large {
max-width: 680px;
p {
background-image: url(../../img/zamboni/mobile/loading-white.png);
background-repeat: no-repeat;
background-position: 50% 50%;
.background-size(auto 32px);
color: #fff;
display: none;
font: 18px Georgia, serif;
pointer-events: none;
text-align: center;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5);
vertical-align: middle;
}
.details {
margin: 8px;
a {
color: inherit;
}
.title,
.author {
background: fadeOut(#fff, 40%);
clear: left;
display: block;
float: left;
line-height: 1.3;
padding: 3px 6px;
}
.title {
font-size: 16px;
font-weight: bold;
margin-bottom: 4px;
}
.author {
color: @dark-gray;
}
}
}
.persona-hover .persona-viewer p {
background-color: rgba(0, 0, 0, 0.4);
display: table-cell;
}
#submit-persona {
.note {
display: block;
padding-top: 2px;
}
.addon-cats ul {
.columns(4);
}
#persona-license {
display: none;
}
#persona-license-list {
ul {
margin: .5em 0 0;
}
}
#persona-license,
#persona-license-list {
background-color: darken(#FCFDFE, 10%);
.border-radius(5px);
padding: 1em;
width: 400px;
p {
margin: 0;
}
p.license {
font-size: 12px;
font-weight: bold;
height: auto;
margin: .5em 0;
padding-left: 20px;
width: auto;
}
}
.colors {
.row {
display: inline-block;
margin-right: 2em;
}
.row > label {
font-weight: normal;
}
}
#persona-design {
.preview,
.reset {
display: none;
}
.reset {
font-weight: bold;
}
.preview {
background-color: #ccc;
display: none;
width: 100%;
}
#persona-header .preview.loading {
height: 50px;
}
#persona-footer .preview.loading {
height: 25px;
}
.loading,
.loaded {
display: block;
}
.loading:before {
background: #444 url(../../img/zamboni/mobile/loading-white.png) no-repeat 50% 50%;
.background-size(auto 70%);
content: "\00a0";
display: block;
height: 100%;
-moz-transition: opacity .5s;
opacity: .4;
width: 100%;
}
.loaded:before {
opacity: 0;
}
.error-loading {
border: @error-red 1px solid;
}
}
#persona-preview .persona-viewer {
.background-size(cover);
.gradient-two-color(#ccc, #bbb);
}
}
#submitted-addon-url {
font-size: 1.3em;
padding-left: 2em;
}
.done-next-steps li {
margin-bottom: 0.3em;
}

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

@ -1,19 +1,19 @@
@import 'lib';
#tooltip {
display:none;
display: none;
background: #2A4364;
color: white;
color: #fff;
font-size: 11px;
border: 1px solid #fff;
-moz-border-radius: .8em;
-webkit-border-radius: .8em;
.border-radius(.8em);
max-width: 300px;
text-align: center;
position: absolute;
padding: 1em;
pointer-events: none;
&.error {
background: #6C1a1a;
background: #6c1a1a;
}
span {
display: block;
@ -30,7 +30,7 @@
bottom: -16px;
border: solid transparent;
border-width: 8px 6px;
border-top-color: #2A4364;
border-top-color: #2a4364;
pointer-events: none;
}
&.error:before {
@ -38,3 +38,21 @@
}
}
.tip {
background-color: #ddd;
.border-radius(20px);
color: #fff;
cursor: help;
display: inline-block;
font-size: 14px;
font-weight: bold;
height: 18px;
line-height: 18px;
text-align: center;
text-decoration: none;
width: 18px;
}
.tip:hover {
background-color: #2a4364;
}

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

@ -157,9 +157,11 @@ pre {
.primary {
.prose {
h2, h3 {
margin-top: 1em;
}
h3 {
font: italic 16px/18px @serif-stack;
margin-top: 1em;
}
}
> .seeall {

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

@ -18,21 +18,10 @@
#acct-notify .check {
text-transform: none;
}
#profile-detail {
.html-support, #trans-bio textarea {
width: 430px;
}
.html-support {
font-size: 0.9em;
margin: 0;
padding: 0;
text-align: right;
span {
border-bottom: 1px dotted #bbb;
cursor: help;
}
}
}
.user-notifications {
li {
@ -47,8 +36,10 @@
font-size: 0.6em;
}
}
.note {
padding-left: 0;
p.note {
clear: both;
font-style: italic;
width: 300px;
}
}
}

Двоичные данные
media/img/impala/colorpicker/circle.gif Normal file

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

После

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

Двоичные данные
media/img/impala/colorpicker/gradient.png Normal file

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

После

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

Двоичные данные
media/img/impala/colorpicker/rainbow.png Normal file

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

После

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

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

@ -0,0 +1,178 @@
$(function() {
if (!$('#submit-persona').length) {
return;
}
initCharCount();
initLicense();
initPreview();
});
z.licenses = {
'copyr': {
'id': 7,
'name': gettext('All Rights Reserved')
},
'cc-attrib': {
'id': 9,
'name': gettext('Creative Commons Attribution 3.0'),
'url': 'http://creativecommons.org/licenses/by/3.0/'
},
'cc-attrib cc-noncom': {
'id': 10,
'name': gettext('Creative Commons Attribution-NonCommercial 3.0'),
'url': 'http://creativecommons.org/licenses/by-nc/3.0/'
},
'cc-attrib cc-noncom cc-noderiv': {
'id': 11,
'name': gettext('Creative Commons Attribution-NonCommercial-NoDerivs 3.0'),
'url': 'http://creativecommons.org/licenses/by/3.0/'
},
'cc-attrib cc-noncom cc-share': {
'id': 8,
'name': gettext('Creative Commons Attribution-NonCommercial-Share Alike 3.0'),
'url': 'http://creativecommons.org/licenses/by-nc-sa/3.0/'
},
'cc-attrib cc-noderiv': {
'id': 12,
'name': gettext('Creative Commons Attribution-NoDerivs 3.0'),
'url': 'http://creativecommons.org/licenses/by-nd/3.0/'
},
'cc-attrib cc-share': {
'id': 13,
'name': gettext('Creative Commons Attribution-ShareAlike 3.0'),
'url': 'http://creativecommons.org/licenses/by/3.0/'
}
};
function initLicense() {
var ccClasses = {
'cc-attrib': ['cc-attrib', 'copyr'],
'cc-noncom': ['', 'cc-noncom'],
'cc-noderiv': ['', 'cc-share', 'cc-noderiv']
};
function licenseUpdate() {
var license = '';
$.each(ccClasses, function(k, val) {
v = val[parseInt($('input[name=' + k + ']:checked').val())];
if (v) {
var is_copyr = (v == 'copyr');
if (k == 'cc-attrib') {
// Hide the other radio buttons when copyright is selected.
$('input[name=cc-noncom], input[name=cc-noderiv]').attr('disabled', is_copyr);
}
if (license != ' copyr') {
license += ' ' + v;
}
}
});
license = $.trim(license);
var l = z.licenses[license];
if (!l) {
return;
}
var license_txt = l['name'];
if (l['url']) {
license_txt = format('<a href="{0}" target="_blank">{1}</a>',
l['url'], license_txt);
}
var $p = $('#persona-license');
$p.show();
$p.find('#cc-license').html(license_txt).attr('class', 'license icon ' + license);
$('#id_license').val(l['id']);
}
$('input[name^=cc-]').change(licenseUpdate);
licenseUpdate();
function saveLicense() {
$('#persona-license-list input[value=' + $('#id_license').val() + ']').attr('checked', true);
}
$('#persona-license .select-license').click(_pd(function() {
$('#persona-license-list').show();
$('#cc-chooser').hide();
saveLicense();
}));
saveLicense();
}
function initPreview() {
$('#submit-persona input[type=color]').miniColors({change: updatePersona});
$('#submit-persona').delegate('#id_name', 'change keyup paste blur', function() {
$('#persona-preview-name').text($(this).val() || gettext("Your Persona's Name"));
});
var $d = $('#persona-design'),
upload_finished = function(e) {
$(this).closest('.row').find('.preview').removeClass('loading');
$('#submit-persona button').attr('disabled', false);
updatePersona();
},
upload_start = function(e, file) {
var $p = $(this).closest('.row');
$p.find('.errorlist').html('');
$p.find('.preview').addClass('loading').removeClass('error-loading');
$('#submit-persona button').attr('disabled', true);
},
upload_success = function(e, file, upload_hash) {
var $p = $(this).closest('.row');
$p.find('input[type=hidden]').val(upload_hash);
$p.find('input[type=file], .note').hide();
$p.find('.preview').attr('src', file.dataURL).addClass('loaded');
updatePersona();
$p.find('.preview, .reset').show();
},
upload_errors = function(e, file, errors) {
var $p = $(this).closest('.row'),
$errors = $p.find('.errorlist');
$p.find('.preview').addClass('error-loading');
$.each(errors, function(i, v) {
$errors.append('<li>' + v + '</li>');
});
};
$d.delegate('.reset', 'click', _pd(function() {
var $p = $(this).closest('.row');
$p.find('input[type=hidden]').val('');
$p.find('input[type=file], .note').show();
$p.find('.preview').removeAttr('src').removeClass('loaded');
updatePersona();
$(this).hide();
}));
$d.delegate('input[type=file]', 'upload_finished', upload_finished)
.delegate('input[type=file]', 'upload_start', upload_start)
.delegate('input[type=file]', 'upload_success', upload_success)
.delegate('input[type=file]', 'upload_errors', upload_errors)
.delegate('input[type=file]', 'change', function(e) {
$(this).imageUploader();
});
function updatePersona() {
var previewSrc = $('#persona-header .preview').attr('src'),
$preview = $('#persona-preview .persona-viewer');
if (previewSrc) {
$preview.css('background-image', 'url(' + previewSrc + ')');
} else {
$preview.removeAttr('style');
}
var data = {'id': '0'};
$.each(['name', 'accentcolor', 'textcolor'], function(i, v) {
data[v] = $d.find('#id_' + v).val();
});
// TODO(cvan): We need to link to the CDN-served Persona images since
// Personas cannot reference moz-filedata URIs.
data['header'] = data['headerURL'] = $d.find('#persona-header .preview').attr('src');
data['footer'] = data['footerURL'] = $d.find('#persona-footer .preview').attr('src');
$preview.attr('data-browsertheme', JSON.stringify(data));
var accentcolor = $d.find('#id_accentcolor').siblings('a[data-color]').attr('data-color'),
textcolor = $d.find('#id_textcolor').val();
$preview.find('.title, .author').css({
'background-color': format('rgba({0}, 0.7)', accentcolor),
'color': textcolor
});
}
updatePersona();
}

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

@ -0,0 +1,595 @@
/*
* jQuery miniColors: A small color selector
* Copyright 2011 Cory LaViska for A Beautiful Site, LLC. (http://abeautifulsite.net/)
* Dual licensed under the MIT or GPL Version 2 licenses
*
* Usage:
* 1. Link to jQuery: <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.5.0/jquery.min.js"></script>
* 2. Link to miniColors: <script type="text/javascript" src="jquery.miniColors.js"></script>
* 3. Include miniColors stylesheet: <link type="text/css" rel="stylesheet" href="jquery.miniColors.css" />
* 4. Apply $([selector]).miniColors() to one or more INPUT elements
*
* Options:
* disabled [true|false]
* readonly [true|false]
*
* Specify options on creation:
* $([selector]).miniColors({
* optionName: value,
* optionName: value,
* ...
* });
*
* Methods:
* Call a method using: $([selector]).miniColors('methodName', [value]);
*
* disabled [true|false]
* readonly [true|false]
* value [hex value]
* destroy
*
* Events:
*
* Attach events on creation:
* $([selector]).miniColors({
* change: function(hex, rgb) { ... }
* });
*
* change(hex, rgb) called when the color value changes;
* 'this' will refer to the original input element;
* hex is the string hex value of the selected color;
* rgb is an object with the RGB values
*
* Change log:
*
* - v0.1 (2011-02-24) - Initial release
*
*
* Attribution:
*
* - The color picker icon is based on an icon from the amazing Fugue icon set:
* http://p.yusukekamiyamane.com/
*
* - The gradient image, the hue image, and the math functions are courtesy of
* the eyecon.co jQuery color picker: http://www.eyecon.ro/colorpicker/
*
*/
if (jQuery)(function($) {
$.extend($.fn, {
miniColors: function(o, data) {
var create = function(input, o, data) {
//
// Creates a new instance of the miniColors selector
//
// Determine initial color (defaults to gray)
var color = cleanHex(input.val()) || 'cccccc',
hsb = hex2hsb(color);
// Create trigger
var trigger = $('<a class="miniColors-trigger" style="background-color: #' + color + '" href="#"></a>');
trigger.insertBefore(input);
// Add necessary attributes
input.addClass('miniColors').attr('maxlength', 7).attr('autocomplete', 'off');
// Set input data
input.data('trigger', trigger);
input.data('hsb', hsb);
if (o.change) input.data('change', o.change);
// Handle options
if (o.readonly) input.attr('readonly', true);
if (o.disabled) disable(input);
// Show selector when trigger is clicked
trigger.bind('click.miniColors', function(e) {
e.preventDefault();
input.trigger('focus');
});
// Show selector when input receives focus
input.bind('focus.miniColors', function(e) {
show(input);
});
// Hide on blur
input.bind('blur.miniColors',
function(event) {
var hex = cleanHex(input.val());
input.val(hex ? '#' + hex: '');
});
// Hide when tabbing out of the input
input.bind('keydown.miniColors', function(e) {
if (e.keyCode === 9) hide(input);
});
// Update when color is typed in
input.bind('keyup.miniColors',
function(event) {
// Remove non-hex characters
var filteredHex = input.val().replace(/[^A-F0-9#]/ig, '');
input.val(filteredHex);
if (!setColorFromInput(input)) {
// Reset trigger color when color is invalid
input.data('trigger').css('backgroundColor', '#fff');
}
});
// Handle pasting
input.bind('paste.miniColors', function(e) {
// Short pause to wait for paste to complete
setTimeout(function() {
input.trigger('keyup');
}, 5);
});
};
var destroy = function(input) {
//
// Destroys an active instance of the miniColors selector
//
hide();
input = $(input);
input.data('trigger').remove();
input.removeAttr('autocomplete');
input.removeData('trigger');
input.removeData('selector');
input.removeData('hsb');
input.removeData('huePicker');
input.removeData('colorPicker');
input.removeData('mousebutton');
input.removeData('moving');
input.unbind('click.miniColors');
input.unbind('focus.miniColors');
input.unbind('blur.miniColors');
input.unbind('keyup.miniColors');
input.unbind('keydown.miniColors');
input.unbind('paste.miniColors');
$(document).unbind('mousedown.miniColors');
$(document).unbind('mousemove.miniColors');
};
var enable = function(input) {
//
// Disables the input control and the selector
//
input.attr('disabled', false);
input.data('trigger').css('opacity', 1);
};
var disable = function(input) {
//
// Disables the input control and the selector
//
hide(input);
input.attr('disabled', true);
input.data('trigger').css('opacity', .5);
};
var show = function(input) {
//
// Shows the miniColors selector
//
if (input.attr('disabled')) return false;
// Hide all other instances
hide();
// Generate the selector
var selector = $('<div class="miniColors-selector"></div>');
selector.append('<div class="miniColors-colors" style="background-color: #fff"><div class="miniColors-colorPicker"></div></div>');
selector.append('<div class="miniColors-hues"><div class="miniColors-huePicker"></div></div>');
var top, left;
if (input.is(':visible')) {
top = input.offset().top + input.outerHeight();
left = input.offset().left;
} else {
top = input.data('trigger').offset().top + input.data('trigger').outerHeight();
left = input.data('trigger').offset().left;
}
selector.css({top: top, left: left, display: 'none'}).addClass(input.attr('class'));
// Set background for colors
var hsb = input.data('hsb');
selector.find('.miniColors-colors').css('backgroundColor', '#' + hsb2hex({
h: hsb.h,
s: 100,
b: 100
}));
// Set colorPicker position
var colorPosition = input.data('colorPosition') || getColorPositionFromHSB(hsb);
selector.find('.miniColors-colorPicker').css('top', colorPosition.y + 'px').css('left', colorPosition.x + 'px');
// Set huePicker position
var huePosition = input.data('huePosition') || getHuePositionFromHSB(hsb);
selector.find('.miniColors-huePicker').css('top', huePosition.y + 'px');
// Set input data
input.data('selector', selector);
input.data('huePicker', selector.find('.miniColors-huePicker'));
input.data('colorPicker', selector.find('.miniColors-colorPicker'));
input.data('mousebutton', 0);
$(document.body).append(selector);
selector.fadeIn(100);
// Prevent text selection in IE
selector.bind('selectstart', function() {
return false;
});
$(document).bind('mousedown.miniColors', function(e) {
input.data('mousebutton', 1);
if ($(e.target).parents().andSelf().hasClass('miniColors-colors')) {
e.preventDefault();
input.data('moving', 'colors');
moveColor(input, e);
}
if ($(e.target).parents().andSelf().hasClass('miniColors-hues')) {
e.preventDefault();
input.data('moving', 'hues');
moveHue(input, e);
}
if ($(e.target).parents().andSelf().hasClass('miniColors-selector')) {
e.preventDefault();
return;
}
if ($(e.target).parents().andSelf().hasClass('miniColors')) return;
hide(input);
});
$(document).bind('mouseup.miniColors', function(e) {
input.data('mousebutton', 0);
input.removeData('moving');
});
$(document).bind('mousemove.miniColors', function(e) {
if (input.data('mousebutton') === 1) {
if (input.data('moving') === 'colors') moveColor(input, e);
if (input.data('moving') === 'hues') moveHue(input, e);
}
});
};
var hide = function(input) {
//
// Hides one or more miniColors selectors
//
// Hide all other instances if input isn't specified
if (!input) input = '.miniColors';
$(input).each(function() {
var selector = $(this).data('selector');
$(this).removeData('selector');
$(selector).fadeOut(100,
function() {
$(this).remove();
});
});
$(document).unbind('mousedown.miniColors');
$(document).unbind('mousemove.miniColors');
};
var moveColor = function(input, event) {
var colorPicker = input.data('colorPicker');
colorPicker.hide();
var position = {
x: event.clientX - input.data('selector').find('.miniColors-colors').offset().left + $(document).scrollLeft() - 5,
y: event.clientY - input.data('selector').find('.miniColors-colors').offset().top + $(document).scrollTop() - 5
};
if (position.x <= -5) position.x = -5;
if (position.x >= 144) position.x = 144;
if (position.y <= -5) position.y = -5;
if (position.y >= 144) position.y = 144;
input.data('colorPosition', position);
colorPicker.css('left', position.x).css('top', position.y).show();
// Calculate saturation
var s = Math.round((position.x + 5) * .67);
if (s < 0) s = 0;
if (s > 100) s = 100;
// Calculate brightness
var b = 100 - Math.round((position.y + 5) * .67);
if (b < 0) b = 0;
if (b > 100) b = 100;
// Update HSB values
var hsb = input.data('hsb');
hsb.s = s;
hsb.b = b;
// Set color
setColor(input, hsb, true);
};
var moveHue = function(input, event) {
var huePicker = input.data('huePicker');
huePicker.hide();
var position = {
y: event.clientY - input.data('selector').find('.miniColors-colors').offset().top + $(document).scrollTop() - 1
};
if (position.y <= -1) position.y = -1;
if (position.y >= 149) position.y = 149;
input.data('huePosition', position);
huePicker.css('top', position.y).show();
// Calculate hue
var h = Math.round((150 - position.y - 1) * 2.4);
if (h < 0) h = 0;
if (h > 360) h = 360;
// Update HSB values
var hsb = input.data('hsb');
hsb.h = h;
// Set color
setColor(input, hsb, true);
};
var setColor = function(input, hsb, updateInputValue) {
input.data('hsb', hsb);
var hex = hsb2hex(hsb),
rgb = hex2rgb(hex);
if (updateInputValue) input.val('#' + hex);
input.data('trigger').css('backgroundColor', '#' + hex)
.attr('data-color', [rgb.r, rgb.g, rgb.b]);
if (input.data('selector')) {
input.data('selector')
.find('.miniColors-colors')
.css('backgroundColor', '#' + hsb2hex({
h: hsb.h,
s: 100,
b: 100
}));
}
if (input.data('change')) {
input.data('change').call(input, '#' + hex, hsb2rgb(hsb));
}
};
var setColorFromInput = function(input) {
// Don't update if the hex color is invalid
var hex = cleanHex(input.val());
if (!hex) return false;
// Get HSB equivalent
var hsb = hex2hsb(hex);
// If color is the same, no change required
var currentHSB = input.data('hsb');
if (hsb.h === currentHSB.h && hsb.s === currentHSB.s && hsb.b === currentHSB.b) return true;
// Set colorPicker position
var colorPosition = getColorPositionFromHSB(hsb),
colorPicker = $(input.data('colorPicker'));
colorPicker.css('top', colorPosition.y + 'px').css('left', colorPosition.x + 'px');
// Set huePosition position
var huePosition = getHuePositionFromHSB(hsb),
huePicker = $(input.data('huePicker'));
huePicker.css('top', huePosition.y + 'px');
setColor(input, hsb, false);
return true;
};
var getColorPositionFromHSB = function(hsb) {
var x = Math.ceil(hsb.s / .67);
if (x < 0) x = 0;
if (x > 150) x = 150;
var y = 150 - Math.ceil(hsb.b / .67);
if (y < 0) y = 0;
if (y > 150) y = 150;
return {
x: x - 5,
y: y - 5
};
}
var getHuePositionFromHSB = function(hsb) {
var y = 150 - (hsb.h / 2.4);
if (y < 0) h = 0;
if (y > 150) h = 150;
return {
y: y - 1
};
}
var cleanHex = function(hex) {
//
// Turns a dirty hex string into clean, 6-character hex color
//
hex = hex.replace(/[^A-Fa-f0-9]/, '');
if (hex.length == 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
return hex.length === 6 ? hex: null;
};
var hsb2rgb = function(hsb) {
var rgb = {},
h = Math.round(hsb.h),
s = Math.round(hsb.s * 255 / 100),
v = Math.round(hsb.b * 255 / 100);
if (s == 0) {
rgb.r = rgb.g = rgb.b = v;
} else {
var t1 = v,
t2 = (255 - s) * v / 255,
t3 = (t1 - t2) * (h % 60) / 60;
if (h == 360) h = 0;
if (h < 60) {
rgb.r = t1;
rgb.b = t2;
rgb.g = t2 + t3;
}
else if (h < 120) {
rgb.g = t1;
rgb.b = t2;
rgb.r = t1 - t3;
}
else if (h < 180) {
rgb.g = t1;
rgb.r = t2;
rgb.b = t2 + t3;
}
else if (h < 240) {
rgb.b = t1;
rgb.r = t2;
rgb.g = t1 - t3;
}
else if (h < 300) {
rgb.b = t1;
rgb.g = t2;
rgb.r = t2 + t3;
}
else if (h < 360) {
rgb.r = t1;
rgb.g = t2;
rgb.b = t1 - t3;
}
else {
rgb.r = 0;
rgb.g = 0;
rgb.b = 0;
}
}
return {
r: Math.round(rgb.r),
g: Math.round(rgb.g),
b: Math.round(rgb.b)
};
};
var rgb2hex = function(rgb) {
var hex = [rgb.r.toString(16), rgb.g.toString(16), rgb.b.toString(16)];
$.each(hex, function(nr, val) {
if (val.length == 1) hex[nr] = '0' + val;
});
return hex.join('');
};
var hex2rgb = function(hex) {
var hex = parseInt(((hex.indexOf('#') > -1) ? hex.substring(1) : hex), 16);
return {
r: hex >> 16,
g: (hex & 0x00FF00) >> 8,
b: (hex & 0x0000FF)
};
};
var rgb2hsb = function(rgb) {
var hsb = {
h: 0,
s: 0,
b: 0
};
var min = Math.min(rgb.r, rgb.g, rgb.b),
max = Math.max(rgb.r, rgb.g, rgb.b),
delta = max - min;
hsb.b = max;
hsb.s = max != 0 ? 255 * delta / max: 0;
if (hsb.s != 0) {
if (rgb.r == max) {
hsb.h = (rgb.g - rgb.b) / delta;
} else if (rgb.g == max) {
hsb.h = 2 + (rgb.b - rgb.r) / delta;
} else {
hsb.h = 4 + (rgb.r - rgb.g) / delta;
}
} else {
hsb.h = -1;
}
hsb.h *= 60;
if (hsb.h < 0) {
hsb.h += 360;
}
hsb.s *= 100 / 255;
hsb.b *= 100 / 255;
return hsb;
};
var hex2hsb = function(hex) {
var hsb = rgb2hsb(hex2rgb(hex));
// Zero out hue marker for black, white, and grays (saturation === 0)
if (hsb.s === 0) hsb.h = 360;
return hsb;
};
var hsb2hex = function(hsb) {
return rgb2hex(hsb2rgb(hsb));
};
//
// Handle calls to $([selector]).miniColors()
//
var $this = $(this);
switch (o) {
case 'readonly':
$this.each(function() {
$(this).attr('readonly', data);
});
return $this;
break;
case 'disabled':
$this.each(function() {
if (data) {
disable($(this));
} else {
enable($(this));
}
});
return $(this);
case 'value':
$this.each(function() {
$(this).val(data).trigger('keyup');
});
return $(this);
break;
case 'destroy':
$this.each(function() {
destroy($(this));
});
return $(this);
default:
o = o || {};
$this.each(function() {
// Must be called on an input element
if ($(this)[0].tagName.toLowerCase() !== 'input') return;
// If a trigger is present, the control was already created
if ($(this).data('trigger')) return;
// Create the control
create($(this), o, data);
});
return $(this);
}
}
});
})(jQuery);

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

@ -0,0 +1,49 @@
$(document).ready(function() {
var personaFixture = {
setup: function() {
this.sandbox = tests.createSandbox('#personas');
initLicense();
},
teardown: function() {
this.sandbox.remove();
},
selectRadio: function(name, value) {
var suite = this.sandbox;
$('input[name=' + name + ']', suite).val(value);
$('input[name=' + name + '][value=' + value + ']', suite)
.attr('checked', true).trigger('change');
}
};
module('Personas', personaFixture);
test('License Chooser', function() {
var that = this,
suite = that.sandbox;
function ccTest(values, licenseName, licenseId) {
that.selectRadio('cc-attrib', values[0]);
if (values.length > 1) {
that.selectRadio('cc-noncom', values[1]);
if (values.length > 2) {
that.selectRadio('cc-noderiv', values[2]);
}
}
equals($('#cc-license', suite).text(), licenseName);
equals(parseInt($('#id_license', suite).val()), licenseId);
}
ccTest([0], 'Creative Commons Attribution 3.0', 9);
ccTest([0, 0], 'Creative Commons Attribution 3.0', 9);
ccTest([0, 0, 0], 'Creative Commons Attribution 3.0', 9);
ccTest([0, 0, 0], 'Creative Commons Attribution 3.0', 9);
ccTest([0, 0, 1], 'Creative Commons Attribution-ShareAlike 3.0', 13);
ccTest([0, 0, 2], 'Creative Commons Attribution-NoDerivs 3.0', 12);
ccTest([0, 1, 0], 'Creative Commons Attribution-NonCommercial 3.0', 10);
ccTest([0, 1, 1], 'Creative Commons Attribution-NonCommercial-Share Alike 3.0', 8);
ccTest([0, 1, 2], 'Creative Commons Attribution-NonCommercial-NoDerivs 3.0', 11);
ccTest([1], 'All Rights Reserved', 7);
ccTest([1, 0], 'All Rights Reserved', 7);
ccTest([1, 0, 0], 'All Rights Reserved', 7);
});
});

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

@ -12,6 +12,7 @@
"js/zamboni/upload.js",
"js/zamboni/files.js",
"js/zamboni/contributions.js",
"js/zamboni/password-strength.js"
"js/zamboni/password-strength.js",
"js/impala/persona_creation.js"
]
}

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

@ -0,0 +1,5 @@
ALTER TABLE `personas` CHANGE COLUMN `approve` `approve` datetime NULL;
INSERT INTO `waffle_flag`
(name, everyone, percent, superusers, staff, authenticated, rollout, note) VALUES
('submit-personas',0,NULL,1,0,0,0, 'Allow Personas to be submitted to AMO');

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

@ -452,6 +452,8 @@ MINIFY_BUNDLES = {
'css/impala/collections.less',
'css/impala/tooltips.less',
'css/impala/search.less',
'css/impala/colorpicker.less',
'css/impala/personas.less',
),
'zamboni/discovery-pane': (
'css/zamboni/discovery-pane.css',
@ -575,6 +577,11 @@ MINIFY_BUNDLES = {
'js/zamboni/personas_core.js',
'js/zamboni/personas.js',
# Persona creation
'js/zamboni/upload.js',
'js/lib/jquery.minicolors.js',
'js/impala/persona_creation.js',
# Collections
'js/zamboni/collections.js',
'js/impala/collections.js',
@ -722,6 +729,7 @@ PRIVATE_MIRROR_URL = '/_privatefiles'
ADDON_ICONS_PATH = UPLOADS_PATH + '/addon_icons'
COLLECTIONS_ICON_PATH = UPLOADS_PATH + '/collection_icons'
PREVIEWS_PATH = UPLOADS_PATH + '/previews'
PERSONAS_PATH = UPLOADS_PATH + '/personas'
USERPICS_PATH = UPLOADS_PATH + '/userpics'
PACKAGER_PATH = os.path.join(TMP_PATH, 'packager')
ADDON_ICONS_DEFAULT_PATH = os.path.join(MEDIA_ROOT, 'img/addon-icons')
@ -744,6 +752,8 @@ USERPICS_URL = STATIC_URL + '/img/uploads/userpics/%s/%s/%s.png?modified=%d'
# paths for uploaded extensions
COLLECTION_ICON_URL = ('%s/images/collection_icon/%%s.png?modified=%%s' %
STATIC_URL)
NEW_PERSONAS_IMAGE_URL = (STATIC_URL +
'/img/uploads/personas/%(id)d/%(file)s.jpg')
PERSONAS_IMAGE_URL = ('http://www.getpersonas.com/static/'
'%(tens)d/%(units)d/%(id)d/%(file)s')
PERSONAS_IMAGE_URL_SSL = ('https://www.getpersonas.com/static/'
@ -959,6 +969,7 @@ def read_only_mode(env):
# Uploaded file limits
MAX_ICON_UPLOAD_SIZE = 4 * 1024 * 1024
MAX_PHOTO_UPLOAD_SIZE = MAX_ICON_UPLOAD_SIZE
MAX_PERSONA_UPLOAD_SIZE = 300 * 1024
# RECAPTCHA - copy all three statements to settings_local.py
RECAPTCHA_PUBLIC_KEY = ''

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

@ -30,6 +30,7 @@ def _polite_tmpdir():
# See settings.py for documentation:
NETAPP_STORAGE = _polite_tmpdir()
ADDONS_PATH = _polite_tmpdir()
PERSONAS_PATH = _polite_tmpdir()
GUARDED_ADDONS_PATH = _polite_tmpdir()
UPLOADS_PATH = _polite_tmpdir()
MIRROR_STAGE_PATH = _polite_tmpdir()

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

@ -2,7 +2,7 @@
{% block content %}
<section class="secondary">
{{ side_nav(amo.ADDON_EXTENSION, category=category, impala=True) }}
{{ side_nav(addon_type or amo.ADDON_EXTENSION, category=category, impala=True) }}
</section>
{% block primary %}{% endblock %}
{% endblock %}

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

@ -2,16 +2,38 @@
<abbr class="req" title="{{ _('required') }}">*</abbr>
{%- endmacro %}
{% macro pretty_field(field, label=None, tag='li', req=False) %}
{% macro optional() -%}
<span class="optional">{{ _('(optional)') }}</span>
{%- endmacro %}
{% macro pretty_field(field, label=None, tooltip=None, tag='li', req=False,
opt=False, class='row') %}
{% set req = field.field.required or req %}
<{{ tag }} class="row{{ ' error' if field.errors else '' }}">
{% if tag %}
<{{ tag }} class="{{ class }}{{ ' error' if field.errors }}">
{% endif %}
<label for="{{ field.auto_id }}">
{{- label or field.label }}
{% if req %}
{{ required() -}}
{% endif %}
{% if opt %}
{{ optional() -}}
{% endif %}
{% if tooltip %}
{{ tip(None, tooltip) }}
{% endif %}
</label>
{{ field.as_widget() }}
{{ field.errors }}
</{{ tag }}>
{% endmacro %}
{% if tag %}
</{{ tag }}>
{% endif %}
{% endmacro %}
{% macro tip(name, tip) %}
{% if name %}
<span class="label">{{ name }}</span>
{% endif %}
<span class="tip tooltip" title="{{ tip }}">?</span>
{% endmacro %}

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

@ -304,6 +304,49 @@
<input type="password" data-min-length="8" />
</div>
<div id="personas">
<legend>Persona License</legend>
<input type="hidden" name="license" id="id_license">
<div id="cc-chooser">
<h3>Can others share your Persona, as long as you're given credit?</h3>
<ul>
<li><label><input type="radio" name="cc-attrib" value="0"> Yes</label></li>
<li><label><input type="radio" name="cc-attrib" value="1"> No</label></li>
</ul>
<h3>Can others make commercial use of your Persona?</h3>
<ul>
<li><label><input type="radio" name="cc-noncom" value="0"> Yes</label></li>
<li><label><input type="radio" name="cc-noncom" value="1"> No</label></li>
</ul>
<h3>Can others create derivative works from your Persona?</h3>
<ul>
<li><label><input type="radio" name="cc-noderiv" value="0"> Yes</label></li>
<li><label><input type="radio" name="cc-noderiv" value="1"> Yes, as long as they share alike</label></li>
<li><label><input type="radio" name="cc-noderiv" value="2"> No</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" class="hidden">
<h3>Select a license for your Persona.</h3>
<ul>
<li><label><input type="radio" name="license" value="7"> All Rights Reserved</label></li>
<li><label><input type="radio" name="license" value="9"> Creative Commons Attribution 3.0</label></li>
<li><label><input type="radio" name="license" value="10"> Creative Commons Attribution-NonCommercial 3.0</label></li>
<li><label><input type="radio" name="license" value="11"> Creative Commons Attribution-NonCommercial-NoDerivs 3.0</label></li>
<li><label><input type="radio" name="license" value="8"> Creative Commons Attribution-Noncommercial-Share Alike 3.0</label></li>
<li><label><input type="radio" name="license" value="12"> Creative Commons Attribution-NoDerivs 3.0</label></li>
<li><label><input type="radio" name="license" value="13"> Creative Commons Attribution-ShareAlike 3.0</label></li>
</ul>
</div>
</div>
<script type="text/javascript">
waffle = {
flag: function () { return true; },