add ability to edit themes (bug 679059, bug 848038)

This commit is contained in:
Chris Van 2013-03-13 16:36:57 -07:00
Родитель 1dcb320a19
Коммит 985f6b52f9
28 изменённых файлов: 942 добавлений и 454 удалений

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

@ -129,6 +129,12 @@ class AddonFormBase(TranslationFormMixin, happyforms.ModelForm):
def clean_tags(self):
return clean_tags(self.request, self.cleaned_data['tags'])
def get_tags(self, addon):
if acl.action_allowed(self.request, 'Addons', 'Edit'):
return [t.tag_text for t in addon.tags.all()]
else:
return [t.tag_text for t in addon.tags.filter(restricted=False)]
class AddonFormBasic(AddonFormBase):
name = TransField(max_length=50)
@ -149,10 +155,6 @@ class AddonFormBasic(AddonFormBase):
super(AddonFormBasic, self).__init__(*args, **kw)
# Theme summary optional.
if kw['instance'].is_persona():
self.fields['summary'].required = False
self.fields['tags'].initial = ', '.join(self.get_tags(self.instance))
# Do not simply append validators, as validators will persist between
# instances.
@ -161,12 +163,6 @@ class AddonFormBasic(AddonFormBase):
name_validators.append(validate_name)
self.fields['name'].validators = name_validators
def get_tags(self, addon):
if acl.action_allowed(self.request, 'Addons', 'Edit'):
return [t.tag_text for t in addon.tags.all()]
else:
return [t.tag_text for t in addon.tags.filter(restricted=False)]
def save(self, addon, commit=False):
tags_new = self.cleaned_data['tags']
tags_old = [slugify(t, spaces=True) for t in self.get_tags(addon)]
@ -490,7 +486,29 @@ class AbuseForm(happyforms.Form):
del self.fields['recaptcha']
class NewPersonaForm(AddonFormBase):
class ThemeFormBase(AddonFormBase):
def __init__(self, *args, **kwargs):
super(ThemeFormBase, self).__init__(*args, **kwargs)
cats = Category.objects.filter(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]
for field in ('header', 'footer'):
self.fields[field].widget.attrs = {
'data-upload-url': reverse('devhub.personas.upload_persona',
args=['persona_%s' % field]),
'data-allowed-types': 'image/jpeg|image/png'
}
def clean_name(self):
return clean_name(self.cleaned_data['name'])
def clean_slug(self):
return clean_slug(self.cleaned_data['slug'], self.instance)
class ThemeForm(ThemeFormBase):
name = forms.CharField(max_length=50)
slug = forms.CharField(max_length=30)
category = forms.ModelChoiceField(queryset=Category.objects.all(),
@ -517,25 +535,6 @@ class NewPersonaForm(AddonFormBase):
model = Addon
fields = ('name', 'slug', 'summary', 'tags')
def __init__(self, *args, **kwargs):
super(NewPersonaForm, self).__init__(*args, **kwargs)
cats = Category.objects.filter(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]
for field in ('header', 'footer'):
self.fields[field].widget.attrs = {
'data-upload-url': reverse('devhub.personas.upload_persona',
args=['persona_%s' % field]),
'data-allowed-types': 'image/jpeg|image/png'
}
def clean_name(self):
return clean_name(self.cleaned_data['name'])
def clean_slug(self):
return clean_slug(self.cleaned_data['slug'], self.instance)
def save(self, commit=False):
from addons.tasks import (create_persona_preview_images,
save_persona_image)
@ -598,6 +597,106 @@ class NewPersonaForm(AddonFormBase):
return addon
class EditThemeForm(AddonFormBase):
name = forms.CharField(max_length=50)
slug = forms.CharField(max_length=30)
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)
accentcolor = ColorField(required=False)
textcolor = ColorField(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.')})
class Meta:
model = Addon
fields = ('name', 'slug', 'summary', 'tags')
def __init__(self, *args, **kw):
self.request = kw.pop('request')
super(AddonFormBase, self).__init__(*args, **kw)
addon = self.instance
persona = addon.persona
# Do not simply append validators, as validators will persist between
# instances.
self.fields['name'].validators = list(self.fields['name'].validators)
self.fields['name'].validators.append(lambda x: clean_name(x, addon))
self.initial['tags'] = ', '.join(self.get_tags(addon))
if persona.accentcolor:
self.initial['accentcolor'] = '#' + persona.accentcolor
if persona.textcolor:
self.initial['textcolor'] = '#' + persona.textcolor
self.initial['license'] = persona.license_id
cats = sorted(Category.objects.filter(type=amo.ADDON_PERSONA,
weight__gte=0),
key=lambda x: x.name)
self.fields['category'].choices = [(c.id, c.name) for c in cats]
try:
self.initial['category'] = addon.categories.values_list(
'id', flat=True)[0]
except IndexError:
pass
def save(self):
addon = self.instance
persona = addon.persona
data = self.cleaned_data
# Update Persona-specific data.
persona_data = {
'license_id': int(data['license']),
'accentcolor': data['accentcolor'].lstrip('#'),
'textcolor': data['textcolor'].lstrip('#'),
'display_username': self.request.amo_user.username
}
changed = False
for k, v in persona_data.iteritems():
if v != getattr(persona, k):
changed = True
setattr(persona, k, v)
if changed:
persona.save()
# Update name, slug, and summary.
if self.changed_data:
amo.log(amo.LOG.EDIT_PROPERTIES, addon)
# Save the Addon object.
super(EditThemeForm, self).save()
tags_new = data['tags']
tags_old = [slugify(t, spaces=True) for t in self.get_tags(addon)]
# Add new tags.
for t in set(tags_new) - set(tags_old):
Tag(tag_text=t).save_tag(addon)
# Remove old tags.
for t in set(tags_old) - set(tags_new):
Tag(tag_text=t).remove_tag(addon)
# Update category.
try:
old_cat = (addon.addoncategory_set
.exclude(category_id=data['category'].id))[0]
except IndexError:
# This should never happen, but maybe a category got deleted.
addon.addoncategory_set.create(category=data['category'])
else:
old_cat.category = data['category']
old_cat.save()
return data
class ThemeLicenseForm(happyforms.Form):
license = forms.TypedChoiceField(choices=amo.PERSONA_LICENSES_IDS,
coerce=int, empty_value=None, widget=forms.HiddenInput,

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

@ -485,9 +485,12 @@ class Addon(amo.models.OnChangeMixin, amo.models.ModelBase):
return reverse(view_name % (prefix, action),
args=[self.app_slug] + args)
else:
view_name = '%s.%s' if prefix_only else '%s.addons.%s'
return reverse(view_name % (prefix, action),
args=[self.slug] + args)
type_ = 'themes' if self.type == amo.ADDON_PERSONA else 'addons'
if not prefix_only:
prefix += '.%s' % type_
view_name = '{prefix}.{action}'.format(prefix=prefix,
action=action)
return reverse(view_name, args=[self.slug] + args)
def get_detail_url(self, action='detail', args=[]):
if self.is_webapp():
@ -609,7 +612,10 @@ class Addon(amo.models.OnChangeMixin, amo.models.ModelBase):
def latest_version(self):
"""Returns the absolutely newest non-beta version. """
if self.type == amo.ADDON_PERSONA:
return
try:
return self.versions.all()[0]
except IndexError:
return
if not self._latest_version:
try:
v = (self.versions.exclude(files__status=amo.STATUS_BETA)
@ -748,8 +754,6 @@ class Addon(amo.models.OnChangeMixin, amo.models.ModelBase):
@property
def current_version(self):
"Returns the current_version field or updates it if needed."
if self.type == amo.ADDON_PERSONA:
return
try:
if not self._current_version:
self.update_version()

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

@ -178,9 +178,9 @@ def persona_detail(request, addon, template=None):
persona = addon.persona
# this persona's categories
categories = addon.categories.filter(application=request.APP.id)
categories = addon.categories.all()
category_personas = None
if categories:
if categories.exists():
qs = Addon.objects.public().filter(categories=categories[0])
category_personas = _category_personas(qs, limit=6)
@ -188,7 +188,7 @@ def persona_detail(request, addon, template=None):
'addon': addon,
'persona': persona,
'categories': categories,
'author_personas': persona.authors_other_addons(request.APP)[:3],
'author_personas': persona.authors_other_addons()[:3],
'category_personas': category_personas,
}

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

@ -87,7 +87,7 @@ def global_settings(request):
if waffle.flag_is_active(request, 'submit-personas'):
# TODO(cvan)(fligtar): Do we want this here?
tools_links.append({'text': _('Submit a New Theme'),
'href': reverse('devhub.personas.submit')})
'href': reverse('devhub.themes.submit')})
tools_links.append({'text': _('Developer Hub'),
'href': reverse('devhub.index')})

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

@ -84,7 +84,7 @@ class TestCommon(amo.tests.TestCase):
expected = [
('Tools', '#'),
('Submit a New Add-on', reverse('devhub.submit.1')),
('Submit a New Theme', reverse('devhub.personas.submit')),
('Submit a New Theme', reverse('devhub.themes.submit')),
('Developer Hub', reverse('devhub.index')),
]
check_links(expected, pq(r.content)('#aux-nav .tools a'))
@ -104,7 +104,7 @@ class TestCommon(amo.tests.TestCase):
('Tools', '#'),
('Manage My Add-ons', reverse('devhub.addons')),
('Submit a New Add-on', reverse('devhub.submit.1')),
('Submit a New Theme', reverse('devhub.personas.submit')),
('Submit a New Theme', reverse('devhub.themes.submit')),
('Developer Hub', reverse('devhub.index')),
]
check_links(expected, pq(r.content)('#aux-nav .tools a'))
@ -119,7 +119,7 @@ class TestCommon(amo.tests.TestCase):
expected = [
('Tools', '#'),
('Submit a New Add-on', reverse('devhub.submit.1')),
('Submit a New Theme', reverse('devhub.personas.submit')),
('Submit a New Theme', reverse('devhub.themes.submit')),
('Developer Hub', reverse('devhub.index')),
('Editor Tools', reverse('editors.home')),
]
@ -139,7 +139,7 @@ class TestCommon(amo.tests.TestCase):
('Tools', '#'),
('Manage My Add-ons', reverse('devhub.addons')),
('Submit a New Add-on', reverse('devhub.submit.1')),
('Submit a New Theme', reverse('devhub.personas.submit')),
('Submit a New Theme', reverse('devhub.themes.submit')),
('Developer Hub', reverse('devhub.index')),
('Editor Tools', reverse('editors.home')),
]
@ -157,7 +157,7 @@ class TestCommon(amo.tests.TestCase):
expected = [
('Tools', '#'),
('Submit a New Add-on', reverse('devhub.submit.1')),
('Submit a New Theme', reverse('devhub.personas.submit')),
('Submit a New Theme', reverse('devhub.themes.submit')),
('Developer Hub', reverse('devhub.index')),
('Editor Tools', reverse('editors.home')),
('Localizer Tools', '/localizers'),
@ -181,7 +181,7 @@ class TestCommon(amo.tests.TestCase):
('Tools', '#'),
('Manage My Add-ons', reverse('devhub.addons')),
('Submit a New Add-on', reverse('devhub.submit.1')),
('Submit a New Theme', reverse('devhub.personas.submit')),
('Submit a New Theme', reverse('devhub.themes.submit')),
('Developer Hub', reverse('devhub.index')),
('Editor Tools', reverse('editors.home')),
('Localizer Tools', '/localizers'),

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

@ -27,7 +27,7 @@
Your browser, your style! Dress it up with a design of your own!
{% endtrans %}
</p>
<p><a href="{{ url('devhub.personas.submit') }}" class="button">{{ _('Submit a New Theme') }}</a></p>
<p><a href="{{ url('devhub.themes.submit') }}" class="button">{{ _('Submit a New Theme') }}</a></p>
<p><a href="{{ url('devhub.docs', 'themes') }}" class="learn-more">{{ _('Learn more') }}</a></p>
</div>
{% endif %}

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

@ -52,13 +52,13 @@
</div>
{% else %}
<section class="secondary devhub-sidebar">
<p id="submit-addon" class="cta">
<p id="submit-addon" class="submit-cta">
<a href="{{ url('devhub.submit.1') }}"
class="button prominent">{{ _('Submit a New Add-on') }}</a>
</p>
{% if waffle.flag('submit-personas') %}
<p class="submit-theme cta">
<a href="{{ url('devhub.personas.submit') }}"
<p class="submit-theme submit-cta">
<a href="{{ url('devhub.themes.submit') }}"
class="button prominent">{{ _('Submit a New Theme') }}</a>
</p>
{% endif %}

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

@ -29,33 +29,27 @@
{% include 'devhub/addons/edit/basic.html' %}
</div>
{% if theme %}
<div class="edit-addon-section" id="edit-addon-license">
{% include 'devhub/addons/edit/license.html' %}
</div>
{% else %}
<div class="edit-addon-section" id="edit-addon-media"
data-checkurl="{{ url('devhub.ajax.image.status', addon.slug) }}">
{% include 'devhub/addons/edit/media.html' %}
</div>
<div class="edit-addon-section" id="edit-addon-media"
data-checkurl="{{ url('devhub.ajax.image.status', addon.slug) }}">
{% include 'devhub/addons/edit/media.html' %}
</div>
<div class="edit-addon-section" id="edit-addon-details">
{% include 'devhub/addons/edit/details.html' %}
</div>
<div class="edit-addon-section" id="edit-addon-details">
{% include 'devhub/addons/edit/details.html' %}
</div>
<div class="edit-addon-section" id="edit-addon-support">
{% include 'devhub/addons/edit/support.html' %}
</div>
<div class="edit-addon-section" id="edit-addon-support">
{% include 'devhub/addons/edit/support.html' %}
</div>
<div class="edit-addon-section" id="edit-addon-technical">
{% include 'devhub/addons/edit/technical.html' %}
</div>
<div class="edit-addon-section" id="edit-addon-technical">
{% include 'devhub/addons/edit/technical.html' %}
</div>
{% if admin_form %}
<div class="edit-addon-section" id="edit-addon-admin">
{% include 'devhub/addons/edit/admin.html' %}
</div>
{% endif %}
{% if admin_form %}
<div class="edit-addon-section" id="edit-addon-admin">
{% include 'devhub/addons/edit/admin.html' %}
</div>
{% endif %}
</div>

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

@ -34,13 +34,8 @@
{{ tip('App URL',
'Choose a short, unique URL slug for your app.') }}
{% else %}
{% if theme %}
{{ tip(_("Theme URL"),
_("Choose a short, unique URL slug for your theme.")) }}
{% else %}
{{ tip(_("Add-on URL"),
_("Choose a short, unique URL slug for your add-on.")) }}
{% endif %}
{{ tip(_("Add-on URL"),
_("Choose a short, unique URL slug for your add-on.")) }}
{% endif %}
</th>
<td id="slug_edit">
@ -133,7 +128,7 @@
{% endif %}
</p>
{% else %}
{{ select_cats(amo.MAX_CATEGORIES, form, webapp, theme) }}
{{ select_cats(amo.MAX_CATEGORIES, form, webapp) }}
{% endif %}
{% endfor %}
{% else %}

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

@ -1,16 +1,15 @@
{% set title = _('Delete Theme') if addon.is_persona() else _('Delete Add-on') %}
<form method="post" action="{{ addon.get_dev_url('delete') }}">
{{ csrf() }}
<h3>
{% if webapp %}
{{ loc('Delete App') }}
{% else %}
{{ _('Delete Add-on') }}
{% endif %}
{{ title }}
</h3>
{% if addon.highest_status or addon.status %}
<p class="warning">
{% if webapp %}
{{ loc('Deleting your app will permanently remove it from the site.') }}
{% elif addon.is_persona() %}
{{ _('Deleting your theme will permanently remove it from the site.') }}
{% else %}
{% trans %}
Deleting your add-on will permanently remove it from the site and
@ -30,8 +29,7 @@
</p>
<input type="hidden" name="addon_id" class="addon_id" value="{{ addon.id }}">
<p>
<button class="delete-button" type="submit">
{{ loc('Delete App') if webapp else _('Delete Add-on') }}</button>
<button class="delete-button" type="submit">{{ title }}</button>
{{ _('or') }} <a href="#" class="close cancel">{{ _('Cancel') }}</a>
</p>
<a href="#" class="close">{{ _('Close') }}</a>

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

@ -1,6 +1,23 @@
<h5>{{ _('Actions') }}</h5>
<ul>
{% if addon.is_incomplete() %}
{% if check_addon_ownership(request, addon, dev=True) %}
<li>
<a href="{{ addon.get_dev_url() }}" class="tooltip"
title="{{ _("Edit details about this theme's listing.") }}">
{{ _('Edit Listing') }}</a>
</li>
{% endif %}
{% if addon.is_public() %}
<li>
<a href="{{ addon.get_url_path() }}" class="tooltip"
title="{{ _("View this theme's public listing page.") }}">
{{ _('View Listing') }}</a>
</li>
{% endif %}
<li>
<a href="{{ url('devhub.feed', addon.slug) }}">
{{ _('View Recent Changes') }}</a>
</li>
{% if check_addon_ownership(request, addon) and addon.can_be_deleted() %}
<li>
<a href="#" class="delete-addon tooltip"
@ -10,25 +27,4 @@
</div>
</li>
{% endif %}
{% else %}
{% if check_addon_ownership(request, addon, dev=True) %}
<li>
<a href="{{ addon.get_dev_url() }}" class="tooltip"
title="{{ _("Edit details about this theme's listing.") }}">
{{ _('Edit Listing') }}</a>
</li>
{% endif %}
<li>
<a href="#" class="more-actions">{{ _('More') }}</a>
<div class="more-actions-popup popup hidden">
{% set view_urls = ((addon.get_url_path(), _('View Add-on Listing')),
(url('devhub.feed', addon.slug), _('View Recent Changes'))) %}
<ul>
{% for url, title in view_urls %}
<li><a href="{{ url }}">{{ title }}</a></li>
{% endfor %}
</ul>
</div>
</li>
</ul>
{% endif %}

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

@ -12,7 +12,7 @@
<a href="{{ url('devhub.docs', 'themes') }}">{{ _('How to create a theme') }}</a>
</li>
<li>
<a href="{{ url('devhub.personas.submit') }}">{{ _('Submit your theme') }}</a>
<a href="{{ url('devhub.themes.submit') }}">{{ _('Submit your theme') }}</a>
</li>
<li>
<a href="{{ url('devhub.docs', 'themes', 'faq') }}">{{ _('Frequently Asked Questions') }}</a>
@ -38,7 +38,7 @@
{% endtrans %}
</p>
<p>
{% trans submit_url=url('devhub.personas.submit') %}
{% trans submit_url=url('devhub.themes.submit') %}
<a href="{{ submit_url }}">Finished Your Design? You can submit it right now!</a>
{% endtrans %}
</p>
@ -251,7 +251,7 @@
</ul>
<p class="screenshot"><img src="{{ MEDIA_URL }}img/docs/themes/submission-step.jpg"></p>
<p class="call-to-submit"><a href="{{ url('devhub.personas.submit') }}" class="button prominent">{{ _('Submit Your Theme Now') }}</a></p>
<p class="call-to-submit"><a href="{{ url('devhub.themes.submit') }}" class="button prominent">{{ _('Submit Your Theme Now') }}</a></p>
</div>
{{ sidebar() }}
{% endblock %}

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

@ -1,16 +1,12 @@
{% if theme %}
{% set urls = [(addon.get_dev_url(), _('Edit Listing'))] %}
{% else %}
{% set urls = [
(addon.get_dev_url(), _('Edit Listing')),
(addon.get_dev_url('owner'), _('Manage Authors') if webapp
else _('Manage Authors & License')),
(addon.get_dev_url('profile'), _('Manage Developer Profile')),
(addon.get_dev_url('payments'), _('Manage Payments')),
(addon.get_dev_url('versions'), _('Manage App Status') if webapp
else _('Manage Status & Versions')),
] %}
{% endif %}
{% set urls = [
(addon.get_dev_url(), _('Edit Listing')),
(addon.get_dev_url('owner'), _('Manage Authors') if webapp
else _('Manage Authors & License')),
(addon.get_dev_url('profile'), _('Manage Developer Profile')),
(addon.get_dev_url('payments'), _('Manage Payments')),
(addon.get_dev_url('versions'), _('Manage App Status') if webapp
else _('Manage Status & Versions')),
] %}
{% if addon.is_premium() and waffle.switch('allow-refund') %}
{% do urls.insert(4, (addon.get_dev_url('refunds'), loc('Manage Refunds'))) %}
{% endif %}

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

@ -60,23 +60,17 @@
</tr>
{% endmacro %}
{% macro select_cats(max, form, webapp=False, theme=False) %}
{% macro select_cats(max, form, webapp=False) %}
<div class="addon-app-cats">
<label>
{% if webapp %}
{{ loc('Select <b>up to {0}</b> categories for this app:')|f(max)|safe }}
{% else %}
{% if theme %}
{{ ngettext('Select <b>up to {0}</b> {1} category for this theme:',
'Select <b>up to {0}</b> {1} categories for this theme:',
max)|f(max, form.app.pretty if form.app else '')|safe }}
{% else %}
{# L10n: {0} is the maximum number of add-on categories allowed.
{1} is the application name. #}
{{ ngettext('Select <b>up to {0}</b> {1} category for this add-on:',
'Select <b>up to {0}</b> {1} categories for this add-on:',
max)|f(max, form.app.pretty if form.app else '')|safe }}
{% endif %}
{# L10n: {0} is the maximum number of add-on categories allowed.
{1} is the application name. #}
{{ ngettext('Select <b>up to {0}</b> {1} category for this add-on:',
'Select <b>up to {0}</b> {1} categories for this add-on:',
max)|f(max, form.app.pretty if form.app else '')|safe }}
{% endif %}
</label>
{{ form.application }}

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

@ -70,7 +70,7 @@
{% endtrans %}
</p>
{% endif %}
{% if not item.addon.is_incomplete() and not item.addon.is_disabled %}
{% if not item.addon.is_persona() and not item.addon.is_incomplete() and not item.addon.is_disabled %}
<p class="upload-new-version">
<a href="{{ item.addon.get_dev_url('versions') }}#version-upload">
{{ _('Upload New Version') }}</a>

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

@ -0,0 +1,238 @@
{% extends "devhub/base_impala.html" %}
{% from "includes/forms.html" import pretty_field, tip %}
{% from "devhub/includes/macros.html" import some_html_tip %}
{% set title = _('Edit Listing') %}
{% block title %}
{{ dev_page_title(title, addon) }}
{% endblock %}
{% block content %}
<section class="primary full">
{{ dev_breadcrumbs(addon) }}
<h1>{{ title }}</h1>
<div class="island swagger theme-info c">
<div class="addon-name-and-icon c">
<img class="addon-icon" src="{{ addon.icon_url }}">
<strong class="addon-name">{{ addon.name }}</strong>
</div>
<div>
{{ persona_preview(addon.persona, linked=False) }}
<div class="info">
<p>
<strong>{{ _('Status:') }}</strong>
<span class="{{ status_class(addon) }}"><b>{{ amo.STATUS_CHOICES[addon.status] }}</b></span>
</p>
{% set position = get_position(addon) %}
{% if position %}
<p>
<strong>{{ _('Queue Position:') }}</strong>
{% trans position=position.pos|numberfmt,
total=position.total|numberfmt %}
{{ position }} of {{ total }}
{% endtrans %}
</p>
{% endif %}
{% if addon.is_public() %}
<p><a href="{{ addon.get_url_path() }}">{{ _('View Listing') }}</a></p>
{% endif %}
<p><a href="{{ url('devhub.feed', addon.slug) }}">
{{ _('View Recent Changes') }}</a></p>
</div>
</div>
</div>
<div class="island prettyform" id="submit-persona">
<form method="post">
{{ csrf() }}
{{ form.unsaved_data }}
<fieldset>
<legend>{{ _('Theme Details') }}</legend>
<ul>
{{ pretty_field(form.name, label=_('Give your Theme a name.'), validate=True) }}
<li class="row">
{% set before_slug = ('<span class="url-prefix">%s/addon/</span>' % settings.SITE_URL)|safe %}
{{ pretty_field(form.slug, label=_('Supply a pretty URL for your detail page.'), tag=None, before=before_slug, validate=True) }}
<span class="note">
{{ _('Please use only letters, numbers, underscores, and dashes in your URL.') }}
</span>
</li>
{{ pretty_field(form.category, label=_('Select the category that best describes your Theme.'),
class='row radios addon-cats', validate=True) }}
<li class="row">
{{ pretty_field(form.tags, label=_('Add some tags to describe your Theme.'), tag=None, opt=True, validate=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: dark, cinema, noir. Limit 20.') }}
</span>
</li>
<li class="row c">
{{ pretty_field(form.summary, label=_('Describe your Theme.'),
tooltip=_("A short explanation of your theme's
basic functionality that is displayed in
search and browse listings, as well as at
the top of your theme's details page."),
tag=None, opt=True, validate=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>{{ _('Theme License') }}</legend>
<div id="cc-chooser">
{{ form.license }}
{{ form.license.errors }}
<h3>{{ _("Can others share your Theme, as long as you're given credit?") }}</h3>
<ul class="radios">
<li>
<label>
<input type="radio" name="cc-attrib" value="0" data-cc="cc-attrib">
{{ _('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" data-cc="copyr">
{{ _('No') }}
{{ tip(None, _('The licensor permits others to copy, distribute, '
'display, and perform the work for non-commercial '
'purposes only.')) }}
</label>
</li>
</ul>
<h3 class="noncc">{{ _('Can others make commercial use of your Theme?') }}</h3>
<ul class="noncc 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" data-cc="cc-noncom">
{{ _('No') }}
{{ tip(None, _('The licensor permits others to copy, distribute, '
'display, and perform the work for non-commercial '
'purposes only.')) }}
</label>
</li>
</ul>
<h3 class="noncc">{{ _('Can others create derivative works from your Theme?') }}</h3>
<ul class="noncc 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" data-cc="cc-share">
{{ _('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" data-cc="cc-noderiv">
{{ _('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 Theme 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">
<h3>{{ _('Select a license for your Theme.') }}</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>{{ _('Theme Design') }}</legend>
<ul>
<li id="persona-header" class="row">
<h3>{{ _('Header') }}</h3>
<img class="preview loaded" src="{{ addon.persona.header_url }}">
</li>
<li id="persona-footer" class="row">
<h3>{{ _('Footer') }}</h3>
<img class="preview loaded" src="{{ addon.persona.footer_url }}">
</li>
</ul>
<h3>{{ _('Select colors for your Theme.') }}</h3>
<ul class="colors">
{{ pretty_field(form.textcolor, label=_('Foreground Text'),
tooltip=_('This is the color of the tab text.'),
validate=True) }}
{{ pretty_field(form.accentcolor, label=_('Background'),
tooltip=_('This is the color of the tabs.'),
validate=True) }}
</ul>
</fieldset>
<fieldset id="persona-preview">
<legend>{{ _('Preview') }}</legend>
<div class="persona persona-large persona-preview">
<div class="persona-viewer" data-browsertheme="{{ addon.persona.json_data }}"
style="background-image:url({{ addon.persona.header_url }})">
<div class="details">
<span class="title" id="persona-preview-name">
{{ _("Your Theme'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>
<div class="listing-footer">
<button class="button prominent" disabled type="submit">{{ _('Save Changes') }}</button>
{% if check_addon_ownership(request, addon) and addon.can_be_deleted() %}
<a href="#" class="button prominent delete-addon">{{ _('Delete Theme') }}</a>
<div class="modal-delete modal hidden">
{% include 'devhub/addons/listing/delete_form.html' %}
</div>
{% endif %}
</div>
</form>
</div>
</section>
{% endblock %}

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

@ -66,14 +66,14 @@
</fieldset>
<fieldset>
<legend>{{ _('Theme License') }}</legend>
<div id="cc-chooser"{% if form.license.value() %} class="hidden"{% endif %}>
<div id="cc-chooser">
{{ form.license }}
{{ form.license.errors }}
<h3>{{ _("Can others share your Theme, as long as you're given credit?") }}</h3>
<ul class="radios">
<li>
<label>
<input type="radio" name="cc-attrib" value="0">
<input type="radio" name="cc-attrib" value="0" data-cc="cc-attrib">
{{ _('Yes') }}
{{ tip(None, _('The licensor permits others to copy, distribute, '
'display, and perform the work, including for '
@ -82,7 +82,7 @@
</li>
<li>
<label>
<input type="radio" name="cc-attrib" value="1">
<input type="radio" name="cc-attrib" value="1" data-cc="copyr">
{{ _('No') }}
{{ tip(None, _('The licensor permits others to copy, distribute, '
'display, and perform the work for non-commercial '
@ -103,7 +103,7 @@
</li>
<li>
<label>
<input type="radio" name="cc-noncom" value="1">
<input type="radio" name="cc-noncom" value="1" data-cc="cc-noncom">
{{ _('No') }}
{{ tip(None, _('The licensor permits others to copy, distribute, '
'display, and perform the work for non-commercial '
@ -124,7 +124,7 @@
</li>
<li>
<label>
<input type="radio" name="cc-noderiv" value="1">
<input type="radio" name="cc-noderiv" value="1" data-cc="cc-share">
{{ _('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 '
@ -133,7 +133,7 @@
</li>
<li>
<label>
<input type="radio" name="cc-noderiv" value="2">
<input type="radio" name="cc-noderiv" value="2" data-cc="cc-noderiv">
{{ _('No') }}
{{ tip(None, _('The licensor permits others to copy, distribute and '
'transmit only unaltered copies of the work — not '
@ -149,7 +149,7 @@
</p>
</div>
</div>
<div id="persona-license-list"{% if not form.license.value() %} class="hidden"{% endif %}>
<div id="persona-license-list">
<h3>{{ _('Select a license for your Theme.') }}</h3>
<ul class="radios">
{% for license in amo.PERSONA_LICENSES %}

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

@ -15,12 +15,13 @@ from amo.tests.test_helpers import get_image_path
from amo.urlresolvers import reverse
import paypal
from applications.models import AppVersion
from addons.forms import NewPersonaForm
from addons.models import Addon, Category, Charity
from addons.forms import EditThemeForm, ThemeForm
from addons.models import Addon, Category, Charity, Persona
from devhub import forms
from files.helpers import copyfileobj
from files.models import FileUpload
from market.models import AddonPremium
from tags.models import Tag
from users.models import UserProfile
from versions.models import ApplicationsVersions, License
@ -181,7 +182,7 @@ class TestPremiumForm(amo.tests.TestCase):
eq_(['price'], form.errors.keys())
class TestNewPersonaForm(amo.tests.TestCase):
class TestThemeForm(amo.tests.TestCase):
fixtures = ['base/apps', 'base/user_2519']
def setUp(self):
@ -214,7 +215,7 @@ class TestNewPersonaForm(amo.tests.TestCase):
return data
def post(self, **kw):
self.form = NewPersonaForm(self.get_dict(**kw), request=self.request)
self.form = ThemeForm(self.get_dict(**kw), request=self.request)
return self.form
def test_name_unique(self):
@ -394,3 +395,91 @@ class TestNewPersonaForm(amo.tests.TestCase):
full_dst=[os.path.join(dst, 'preview.png'),
os.path.join(dst, 'icon.png')],
set_modified_on=[addon])
class TestEditThemeForm(amo.tests.TestCase):
fixtures = ['base/apps', 'base/user_2519']
def setUp(self):
self.populate()
self.request = mock.Mock()
self.request.groups = ()
self.request.amo_user = mock.Mock()
self.request.amo_user.username = 'swagyismymiddlename'
self.request.amo_user.is_authenticated.return_value = True
def populate(self):
self.instance = Addon.objects.create(type=amo.ADDON_PERSONA,
status=amo.STATUS_PUBLIC, slug='swag-overload',
name='Bands Make Me Dance', summary='tha summary')
self.cat = Category.objects.create(
type=amo.ADDON_PERSONA, name='xxxx')
self.instance.addoncategory_set.create(category=self.cat)
self.license = License.objects.create(id=amo.LICENSE_CC_BY.id)
Persona.objects.create(persona_id=0, addon_id=self.instance.id,
license_id=self.license.id, accentcolor='C0FFEE',
textcolor='EFFFFF')
Tag(tag_text='sw').save_tag(self.instance)
Tag(tag_text='ag').save_tag(self.instance)
def get_dict(self, **kw):
data = {
'accentcolor': '#C0FFEE',
'category': self.cat.id,
'license': self.license.id,
'name': self.instance.name.id,
'slug': self.instance.slug,
'summary': self.instance.summary.id,
'tags': 'ag, sw',
'textcolor': '#EFFFFF'
}
data.update(**kw)
return data
def test_initial(self):
self.form = EditThemeForm(None, request=self.request,
instance=self.instance)
eq_(self.form.initial, self.get_dict())
def test_success(self):
other_cat = Category.objects.create(type=amo.ADDON_PERSONA)
other_license = License.objects.create(id=amo.LICENSE_CC_BY_NC_SA.id)
data = {
'accentcolor': '#EFF0FF',
'category': other_cat.id,
'license': other_license.id,
'name': 'All Day I Dream About Swag',
'slug': 'swag-lifestyle',
'summary': 'ADIDAS',
'tags': 'ag',
'textcolor': '#CACACA'
}
self.form = EditThemeForm(data, request=self.request,
instance=self.instance)
eq_(self.form.initial, self.get_dict())
eq_(self.form.data, data)
eq_(self.form.is_valid(), True, self.form.errors)
self.form.save()
self.instance = self.instance.reload()
eq_(unicode(self.instance.persona.accentcolor),
data['accentcolor'].lstrip('#'))
eq_(self.instance.categories.all()[0].id, data['category'])
eq_(self.instance.persona.license.id, data['license'])
eq_(unicode(self.instance.name), data['name'])
eq_(unicode(self.instance.summary), data['summary'])
self.assertSetEqual(
self.instance.tags.values_list('tag_text', flat=True),
[data['tags']])
eq_(unicode(self.instance.persona.textcolor),
data['textcolor'].lstrip('#'))
def test_name_unique(self):
data = self.get_dict(name='Bands Make You Dance')
Addon.objects.create(type=amo.ADDON_PERSONA,
status=amo.STATUS_PUBLIC, name=data['name'])
self.form = EditThemeForm(data, request=self.request,
instance=self.instance)
eq_(self.form.is_valid(), False)
eq_(self.form.errors,
{'name': ['This name is already in use. Please choose another.']})

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

@ -18,7 +18,7 @@ class TestSubmitPersona(amo.tests.TestCase):
def setUp(self):
assert self.client.login(username='regular@mozilla.com',
password='password')
self.url = reverse('devhub.personas.submit')
self.url = reverse('devhub.themes.submit')
def get_img_urls(self):
return (

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

@ -43,14 +43,12 @@ def marketplace_patterns(prefix):
name='devhub.%s.market.4' % prefix),
)
# These will all start with /theme/<theme_slug>/
# These will all start with /theme/<slug>/
theme_detail_patterns = patterns('',
url('^$', lambda r,
addon_id: redirect('devhub.themes.edit', addon_id,
permanent=True)),
url('^edit$', views.edit, name='devhub.themes.edit'),
url('^edit_(?P<section>[^/]+)(?:/(?P<editable>[^/]+))?$',
views.addons_section, name='devhub.themes.section'),
addon_id: redirect('devhub.themes.edit', addon_id, permanent=True)),
url('^edit$', views.edit_theme, name='devhub.themes.edit'),
url('^delete$', views.delete, name='devhub.themes.delete'),
)
# These will all start with /app/<app_slug>/
@ -247,10 +245,10 @@ urlpatterns = decorate(write, patterns('',
url('^ajax/addon/%s/' % ADDON_ID, include(ajax_patterns)),
# Themes submission.
url('^theme/submit/?$', views.submit_persona,
name='devhub.personas.submit'),
url('^theme/%s/submit/done$' % ADDON_ID, views.submit_persona_done,
name='devhub.personas.submit.done'),
url('^theme/submit/?$', views.submit_theme,
name='devhub.themes.submit'),
url('^theme/%s/submit/done$' % ADDON_ID, views.submit_theme_done,
name='devhub.themes.submit.done'),
url('^theme/submit/upload/'
'(?P<upload_type>persona_header|persona_footer)$',
views.ajax_upload_image, name='devhub.personas.upload_persona'),

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

@ -25,7 +25,7 @@ from PIL import Image
from session_csrf import anonymous_csrf
from tower import ugettext_lazy as _lazy, ugettext as _
import waffle
from waffle.decorators import waffle_switch
from waffle.decorators import waffle_flag, waffle_switch
from applications.models import Application, AppVersion
import amo
@ -295,19 +295,14 @@ def feed(request, addon_id=None):
return jingo.render(request, 'devhub/addons/activity.html', data)
@dev_required(webapp=True, theme=True)
def edit(request, addon_id, addon, webapp=False, theme=False):
url_prefix = 'addons'
if webapp:
url_prefix = 'apps'
elif theme:
url_prefix = 'themes'
@dev_required(webapp=True)
def edit(request, addon_id, addon, webapp=False):
url_prefix = 'apps' if webapp else 'addons'
data = {
'page': 'edit',
'addon': addon,
'webapp': webapp,
'theme': theme,
'url_prefix': url_prefix,
'valid_slug': addon.slug,
'tags': addon.tags.not_blacklisted().values_list('tag_text', flat=True),
@ -321,6 +316,20 @@ def edit(request, addon_id, addon, webapp=False, theme=False):
return jingo.render(request, 'devhub/addons/edit.html', data)
@dev_required(theme=True)
@waffle_flag('submit-personas')
def edit_theme(request, addon_id, addon, theme=False):
form = addon_forms.EditThemeForm(data=request.POST or None,
request=request, instance=addon)
if request.method == 'POST' and form.is_valid():
form.save()
return redirect('devhub.themes.edit', addon.reload().slug)
return jingo.render(request, 'devhub/personas/edit.html', {
'addon': addon,
'form': form
})
@dev_required(owner_for_post=True, webapp=True)
def delete(request, addon_id, addon, webapp=False):
# Database deletes only allowed for free or incomplete addons.
@ -1096,12 +1105,11 @@ def ajax_dependencies(request, addon_id, addon):
return s(request, excluded_ids=[addon_id]).items
@dev_required(webapp=True, theme=True)
@dev_required(webapp=True)
def addons_section(request, addon_id, addon, section, editable=False,
webapp=False, theme=False):
webapp=False):
basic = addon_forms.AppFormBasic if webapp else addon_forms.AddonFormBasic
models = {'basic': basic,
'license': addon_forms.ThemeLicenseForm,
'media': addon_forms.AddonFormMedia,
'details': addon_forms.AddonFormDetails,
'support': addon_forms.AddonFormSupport,
@ -1173,15 +1181,10 @@ def addons_section(request, addon_id, addon, section, editable=False,
else:
form = False
url_prefix = 'addons'
if webapp:
url_prefix = 'apps'
elif theme:
url_prefix = 'themes'
url_prefix = 'apps' if webapp else 'addons'
data = {'addon': addon,
'webapp': webapp,
'theme': theme,
'url_prefix': url_prefix,
'form': form,
'editable': editable,
@ -1794,23 +1797,21 @@ def submit_bump(request, addon_id, addon, webapp=False):
@login_required
def submit_persona(request):
if not waffle.flag_is_active(request, 'submit-personas'):
raise PermissionDenied
form = addon_forms.NewPersonaForm(data=request.POST or None,
files=request.FILES or None,
request=request)
@waffle_flag('submit-personas')
def submit_theme(request):
form = addon_forms.ThemeForm(data=request.POST or None,
files=request.FILES or None,
request=request)
if request.method == 'POST' and form.is_valid():
addon = form.save()
return redirect('devhub.personas.submit.done', addon.slug)
return redirect('devhub.themes.submit.done', addon.slug)
return jingo.render(request, 'devhub/personas/submit.html',
dict(form=form))
@dev_required(theme=True)
def submit_persona_done(request, addon_id, addon, theme):
if not waffle.flag_is_active(request, 'submit-personas'):
raise PermissionDenied
@waffle_flag('submit-personas')
def submit_theme_done(request, addon_id, addon, theme):
if addon.is_public():
return redirect(addon.get_url_path())
return jingo.render(request, 'devhub/personas/submit_done.html',

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

@ -11,6 +11,7 @@ from tower import ugettext as _, ugettext_lazy as _lazy, ungettext as ngettext
import amo
from addons.helpers import new_context
from addons.models import Addon
from amo.helpers import absolutify, breadcrumbs, page_title
from amo.urlresolvers import reverse
from amo.utils import send_mail as amo_send_mail
@ -319,22 +320,36 @@ def send_mail(template, subject, emails, context, perm_setting=None):
use_blacklist=False, perm_setting=perm_setting)
@register.function
def get_position(addon):
version = addon.latest_version
if addon.is_persona() and addon.is_pending():
pending_themes = (Addon.objects
.filter(status=amo.STATUS_PENDING,
type=amo.ADDON_PERSONA)
.order_by('created').values_list('id', flat=True))
for idx, pending in enumerate(pending_themes, start=1):
if pending == addon.id:
position = idx
break
total = pending_themes.count()
return {'mins': 1 * total, 'pos': position, 'total': total}
if not version:
return False
else:
version = addon.latest_version
q = version.current_queue
if not q:
return False
if not version:
return False
mins_query = q.objects.filter(id=addon.id)
if mins_query.count() > 0:
mins = mins_query[0].waiting_time_min
pos = q.objects.having('waiting_time_min >=', mins).count()
total = q.objects.count()
return dict(mins=mins, pos=pos, total=total)
q = version.current_queue
if not q:
return False
mins_query = q.objects.filter(id=addon.id)
if mins_query.count() > 0:
mins = mins_query[0].waiting_time_min
pos = q.objects.having('waiting_time_min >=', mins).count()
total = q.objects.count()
return dict(mins=mins, pos=pos, total=total)
return False

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

@ -19,9 +19,12 @@
}
}
#submit-addon, .submit-theme {
margin-bottom: 1.5em;
p.submit-cta {
margin-bottom: 1em;
padding: 0;
+ p.submit-cta {
margin-bottom: 1.5em;
}
.button {
display: block;
font-size: 13px;

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

@ -77,10 +77,8 @@ ul.license {
.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);
background: url(../../img/zamboni/mobile/loading-white.png) center no-repeat;
background-size: auto 32px;
color: #fff;
display: none;
font: 18px Georgia, serif;
@ -119,7 +117,7 @@ ul.license {
display: table-cell;
}
#submit-persona, #addon-edit-license {
#submit-persona {
.note {
display: block;
padding-top: 2px;
@ -127,31 +125,27 @@ ul.license {
.addon-cats label + 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);
border-radius: 5px;
display: none;
padding: 1em;
width: 400px;
p {
margin: 0;
&.license {
font-size: 12px;
font-weight: bold;
height: auto;
margin: .5em 0;
padding-left: 20px;
width: auto;
}
}
p.license {
font-size: 12px;
font-weight: bold;
height: auto;
margin: .5em 0;
padding-left: 20px;
width: auto;
}
}
#persona-license-list {
margin-top: 1em;
}
.colors {
li.row {
@ -172,7 +166,6 @@ ul.license {
}
.preview {
background-color: #ccc;
display: none;
width: 100%;
}
#persona-header .preview.loading {
@ -186,13 +179,13 @@ ul.license {
display: block;
}
.loading:before {
background: #444 url(../../img/zamboni/mobile/loading-white.png) no-repeat 50% 50%;
.background-size(auto 70%);
background: #444 url(../../img/zamboni/mobile/loading-white.png) center no-repeat;
background-size: auto 70%;
content: "\00a0";
display: block;
height: 100%;
-moz-transition: opacity .5s;
opacity: .4;
transition: opacity .5s;
width: 100%;
}
.loaded:before {
@ -203,7 +196,7 @@ ul.license {
}
}
#persona-preview .persona-viewer {
.background-size(cover);
background-size: cover;
.gradient-two-color(#ccc, #bbb);
}
}
@ -213,6 +206,14 @@ ul.license {
}
}
#persona-header img {
max-height: 200px;
}
#persona-footer img {
max-height: 100px;
}
.done-next-steps li {
margin-bottom: 0.3em;
}
@ -235,10 +236,7 @@ ul.license {
top: 2px;
}
}
#persona-license-list {
display: none;
}
#persona-license, #persona-license-list {
#persona-license {
margin-bottom: 13px;
}
}
@ -261,3 +259,57 @@ ul.license {
margin: 0 -1.1em -1.1em -1.1em;
}
}
.theme-info {
float: left;
.addon-icon {
border: 1px solid #ccc;
display: inline-block;
height: 32px;
width: 32px;
}
.addon-name-and-icon,
.addon-name-and-icon + div {
float: left;
}
.addon-name {
font: 24px/36px @sans-stack;
padding-left: 8px;
vertical-align: top;
}
.persona {
float: left;
margin-top: 1em;
}
.info {
float: left;
margin-left: 1.5em;
}
[data-browsertheme] {
background: #fff;
border: 1px solid #eee;
border-radius: 6px;
height: 100px;
width: 680px;
}
}
.listing-footer {
overflow: hidden;
padding: 0;
position: relative;
.button {
float: left;
margin: 0;
}
.delete-addon {
float: right;
font-size: 12px;
font-weight: 300;
padding: 12px 16px;
&:not([disabled]) {
background: #e33;
color: #fff;
}
}
}

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

@ -51,6 +51,11 @@ a {
float: left;
margin-bottom: 15px;
padding: 14px 14px 16px;
&.swagger {
.box-shadow(0 -2px 0 rgba(223, 223, 223, .3) inset, 0 0 1px rgba(0, 0, 0, 0.1));
.gradient-two-color(#FCFCFC, #F4F4F4);
border-color: @light-gray;
}
> section {
border-bottom: 1px solid #C9DDF2;
border-top: 1px solid transparent;

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

@ -1,123 +1,14 @@
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() {
(function() {
if (!$('#submit-persona, #addon-edit-license').length) {
return;
}
function checkValid(form) {
if (form) {
$(form).find('button[type=submit]').attr('disabled', !form.checkValidity());
$(form).find('button[type=submit]:not(.delete)').attr('disabled', !form.checkValidity());
}
}
$(document).delegate('input, select, textarea', 'change keyup paste', function(e) {
checkValid(e.target.form);
});
$('form').each(function() {
checkValid(this);
});
initLicense();
if (!$('#addon-edit-license').length) {
initCharCount();
initPreview();
}
});
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[+$('input[name=' + k + ']:checked').val()];
if (v) {
var is_copyr = (v == 'copyr');
if (k == 'cc-attrib') {
var $noncc = $('.noncc');
// Hide the other radio buttons when copyright is selected.
$noncc.toggleClass('disabled', is_copyr);
if ($noncc.find('input[type=radio]:not(:checked)').length == 5) {
$('input[name="cc-noncom"][value=1]').prop('checked', true);
$('input[name="cc-noderiv"][value=2]').prop('checked', true);
}
}
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() {
function hex2rgb(hex) {
var hex = parseInt(((hex.indexOf('#') > -1) ? hex.substring(1) : hex), 16);
return {
@ -127,128 +18,247 @@ function initPreview() {
};
}
var POST = {};
function loadUnsaved() {
return JSON.parse($('input[name="unsaved_data"]').val() || '{}');
}
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'),
$errors = $p.find('.errorlist');
if ($errors.length == 2) {
$errors.eq(0).remove();
}
$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');
POST[upload_hash] = file.dataURL; // Remember this as "posted" data.
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>');
});
};
function postUnsaved(data) {
$('input[name="unsaved_data"]').val(JSON.stringify(data));
}
$d.delegate('.reset', 'click', _pd(function() {
var $this = $(this),
$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();
var licensesByClass = {
'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/'
}
};
// Build an object for lookups by id: {{7: 'copyr'}, {9: 'cc-attrib'}, ...}.
var licenseClassesById = _.object(_.map(licensesByClass, function(v, k) {
return [v.id, k];
}));
$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();
// Validate the form.
var $form = $('#submit-persona form');
$form.delegate('input, select, textarea', 'change keyup paste', function(e) {
checkValid(e.target.form);
});
checkValid($form[0]);
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');
initLicense();
initCharCount();
initPreview();
function initLicense() {
var $licenseField = $('#id_license');
function licenseUpdate(updateList) {
var licenseClass;
if ($('input[data-cc="copyr"]:checked').length) {
licenseClass = 'copyr';
} else {
licenseClass = $('input[data-cc]:checked').map(function() {
return $(this).data('cc');
}).toArray().join(' ');
}
var license = licensesByClass[licenseClass];
if (license) {
var licenseTxt = license['name'];
if (license['url']) {
licenseTxt = format('<a href="{0}" target="_blank">{1}</a>',
license['url'], licenseTxt);
}
var $p = $('#persona-license');
$p.show().find('#cc-license').html(licenseTxt).attr('class', 'license icon ' + licenseClass);
$licenseField.val(license['id']);
if (updateList) {
updateLicenseList();
}
}
}
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').attr('data-rgb'),
textcolor = $d.find('#id_textcolor').val();
$preview.find('.title, .author').css({
'background-color': format('rgba({0}, .7)', accentcolor),
'color': textcolor
});
}
var $color = $('#submit-persona input[type=color]');
$color.change(function() {
var $this = $(this),
val = $this.val();
if (val.indexOf('#') === 0) {
var rgb = hex2rgb(val);
$this.attr('data-rgb', format('{0},{1},{2}', rgb.r, rgb.g, rgb.b));
}
updatePersona();
}).trigger('change');
// Check for native `input[type=color]` support (i.e., WebKit).
if ($color[0].type === 'color') {
$('.miniColors-trigger').hide();
} else {
$color.miniColors({
change: function() {
$color.trigger('change');
updatePersona();
// Hide the other license options when the copyright license is selected.
$form.delegate('input[name="cc-attrib"]', 'change', function() {
var $noncc = $('.noncc');
$noncc.toggleClass('disabled', $(this).data('cc') == 'copyr');
if ($noncc.find('input[type=radio]:not(:checked)').length == 5) {
$('input[name="cc-noncom"][value=1], input[name="cc-noderiv"][value=2]').prop('checked', true);
}
});
$('input[data-cc="copyr"]').trigger('change');
// Whenever a radio field changes, update the license.
$('input[name^="cc-"]').change(licenseUpdate);
licenseUpdate();
if ($licenseField.val()) {
$('input[type=radio][data-cc="' + licenseClassesById[$licenseField.val()] + '"]').prop('checked', true);
licenseUpdate();
}
$form.delegate('input[type=radio][name=license]', 'change', function() {
// Upon selecting license from advanced menu, change it in the Q/A format.
$('.noncc.disabled').removeClass('disabled');
$('input[name^="cc-"]').prop('checked', false);
$('input[type=radio][data-cc~="' + licenseClassesById[$(this).val()].split(' ') + '"]').prop('checked', true);
licenseUpdate(false);
});
function updateLicenseList() {
$('#persona-license-list input[value="' + $licenseField.val() + '"]').prop('checked', true);
}
$('#persona-license .select-license').click(_pd(function() {
$('#persona-license-list').toggle();
updateLicenseList();
}));
updateLicenseList();
}
$('#id_name').bind('change keyup paste blur', _.throttle(function() {
$('#persona-preview-name').text($(this).val() || gettext("Your Theme's Name"));
slugify();
}, 250)).trigger('change');
$('#submit-persona').submit(function() {
postUnsaved(POST);
});
var POST = {};
POST = loadUnsaved();
_.each(POST, function(v, k) {
$('input[value="' + k + '"]').siblings('input[type=file]').trigger('upload_success', [{dataURL: v}, k]);
});
}
function initPreview() {
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'),
$errors = $p.find('.errorlist');
if ($errors.length == 2) {
$errors.eq(0).remove();
}
$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');
POST[upload_hash] = file.dataURL; // Remember this as "posted" data.
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 $this = $(this),
$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();
}));
function postUnsaved(data) {
$('input[name="unsaved_data"]').val(JSON.stringify(data));
}
$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').attr('data-rgb'),
textcolor = $d.find('#id_textcolor').val();
$preview.find('.title, .author').css({
'background-color': format('rgba({0}, .7)', accentcolor),
'color': textcolor
});
}
function loadUnsaved() {
return JSON.parse($('input[name="unsaved_data"]').val() || '{}');
}
var $color = $('#submit-persona input[type=color]');
$color.change(function() {
var $this = $(this),
val = $this.val();
if (val.indexOf('#') === 0) {
var rgb = hex2rgb(val);
$this.attr('data-rgb', format('{0},{1},{2}', rgb.r, rgb.g, rgb.b));
}
updatePersona();
}).trigger('change');
// Check for native `input[type=color]` support (i.e., WebKit).
if ($color[0].type === 'color') {
$('.miniColors-trigger').hide();
} else {
$color.miniColors({
change: function() {
$color.trigger('change');
updatePersona();
}
});
}
$('#id_name').bind('change keyup paste blur', _.throttle(function() {
$('#persona-preview-name').text($(this).val() || gettext("Your Theme's Name"));
slugify();
}, 250)).trigger('change');
$('#submit-persona').submit(function() {
postUnsaved(POST);
});
POST = loadUnsaved();
_.each(POST, function(v, k) {
$('input[value="' + k + '"]').siblings('input[type=file]').trigger('upload_success', [{dataURL: v}, k]);
});
}
})();

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

@ -373,7 +373,7 @@ $(document).ready(function() {
$('.modal-delete').each(function() {
var el = $(this);
el.modal(el.closest('li').find('.delete-addon'), {
el.modal(el.parent().find('.delete-addon'), {
width: 400,
callback: function(obj) {
fixPasswordField(this);

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

@ -0,0 +1 @@
update waffle_switch_amo set active = 1 where name = 'soft_delete';