persona creation (bug 640414)
This commit is contained in:
Родитель
7dc9e886b4
Коммит
bb4ef4842c
|
@ -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 × 200 pixels') }}</li>
|
||||
<li>{{ _('300 KB max') }}</li>
|
||||
<li>{{ _('PNG or JPG') }}</li>
|
||||
<li>{{ _('Aligned to top-right') }}</li>
|
||||
</ul>
|
||||
<ul class="errorlist"></ul>
|
||||
<img class="preview">
|
||||
<a href="#" class="reset">
|
||||
{{ _('Select a different header image') }}</a>
|
||||
</li>
|
||||
<li id="persona-footer" class="row">
|
||||
{{ pretty_field(form.footer, label=_('Select a footer image for your Persona.'),
|
||||
tag=None, req=True) }}
|
||||
{{ form.footer_hash }}
|
||||
{{ form.footer_hash.errors }}
|
||||
<ul class="note">
|
||||
<li>{{ _('3000 × 100 pixels') }}</li>
|
||||
<li>{{ _('300 KB max') }}</li>
|
||||
<li>{{ _('PNG or JPG') }}</li>
|
||||
<li>{{ _('Aligned to bottom-left') }}</li>
|
||||
</ul>
|
||||
<ul class="errorlist"></ul>
|
||||
<img class="preview">
|
||||
<a href="#" class="reset">
|
||||
{{ _('Select a different footer image') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ _('Select colors for your Persona.') }}</h3>
|
||||
<ul class="colors">
|
||||
{{ pretty_field(form.textcolor, label=_('Foreground Text'),
|
||||
tooltip=_('This is the color of the tab text.')) }}
|
||||
{{ pretty_field(form.accentcolor, label=_('Background'),
|
||||
tooltip=_('This is the color of the tabs.')) }}
|
||||
</ul>
|
||||
</fieldset>
|
||||
<fieldset id="persona-preview">
|
||||
<legend>{{ _('Preview') }}</legend>
|
||||
<div class="persona persona-large persona-preview">
|
||||
<div class="persona-viewer" data-browsertheme>
|
||||
<div class="details">
|
||||
<span class="title" id="persona-preview-name">
|
||||
{{ _("Your Persona's Name") }}</span>
|
||||
<span class="author">
|
||||
{% set amo_user = request.amo_user %}
|
||||
{% trans user=amo_user.username,
|
||||
profile_url=url('users.profile', amo_user.id) %}
|
||||
by <a href="{{ profile_url }}" target="_blank">{{ user }}</a>
|
||||
{% endtrans %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<p><button>{{ _('Submit Persona') }}</button></p>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -0,0 +1,40 @@
|
|||
{% set addon_type = amo.ADDON_PERSONA %}
|
||||
{% extends "impala/base_side_categories.html" %}
|
||||
|
||||
{% block title %}{{ page_title(_('Submission Complete')) }}{% endblock %}
|
||||
|
||||
{% block primary %}
|
||||
<section class="primary">
|
||||
<h1>{{ _("You're done!") }}</h1>
|
||||
<div class="prose">
|
||||
<p>
|
||||
{% trans %}
|
||||
Your Persona has been submitted to the Review queue.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
{# TODO(cvan): The following is a lie. Personas cannot be installed before being reviewed - yet. #}
|
||||
<p>
|
||||
{% trans %}
|
||||
You'll receive an email once your Persona has been reviewed by an editor.
|
||||
In the meantime, you and your friends can install the Persona directly
|
||||
from its details page:
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<p>
|
||||
<a id="submitted-addon-url" href="{{ addon.get_url_path() }}">
|
||||
{{ addon.get_url_path()|absolutify|display_url }}</a>
|
||||
</p>
|
||||
<h2>{{ _('Next steps:') }}</h2>
|
||||
<ul class="done-next-steps">
|
||||
{# TODO(cvan): This should lead to the Persona Edit page. #}
|
||||
{% set edit_url = url('devhub.addons.edit', addon.slug) %}
|
||||
<li>{{ _('Provide more details about your Persona by <a href="{0}">editing its listing</a>.')|f(edit_url)|safe }}</li>
|
||||
{% set profile_url = url('devhub.addons.profile', addon.slug) %}
|
||||
<li>{{ _('Tell your users why you created this Persona in your <a href="{0}">Developer Profile</a>.')|f(profile_url)|safe }}</li>
|
||||
{% set feed_url = url('devhub.feed', addon.slug) %}
|
||||
<li>{{ _('View and subscribe to your Persona\'s <a href="{0}">activity feed</a> to stay updated on reviews, collections, and more.')|f(feed_url)|safe }}</li>
|
||||
<li>{{ _('View approximate review queue <a href="{0}">wait times</a>.')|f('https://forums.addons.mozilla.org/viewforum.php?f=21')|safe }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -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
|
||||
|
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 3.9 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 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]
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 78 B |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 10 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 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');
|
11
settings.py
11
settings.py
|
@ -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; },
|
||||
|
|
Загрузка…
Ссылка в новой задаче