1148 строки
44 KiB
Python
1148 строки
44 KiB
Python
# -*- coding: utf-8 -*-
|
|
import os
|
|
import re
|
|
import socket
|
|
|
|
from jinja2 import Markup
|
|
|
|
from django import forms
|
|
from django.conf import settings
|
|
from django.db.models import Q
|
|
from django.forms.models import modelformset_factory
|
|
from django.forms.formsets import formset_factory, BaseFormSet
|
|
from django.utils.safestring import mark_safe
|
|
from django.utils.encoding import force_unicode
|
|
from django.contrib import messages
|
|
|
|
import commonware
|
|
import happyforms
|
|
from tower import ugettext as _, ugettext_lazy as _lazy
|
|
from quieter_formset.formset import BaseModelFormSet
|
|
import waffle
|
|
|
|
import amo
|
|
import addons.forms
|
|
import paypal
|
|
from addons.models import (Addon, AddonDependency, AddonUpsell, AddonUser,
|
|
BlacklistedSlug, Charity, Preview)
|
|
from amo.helpers import loc
|
|
from amo.forms import AMOModelForm
|
|
from amo.urlresolvers import reverse
|
|
from amo.utils import raise_required, slugify
|
|
|
|
from applications.models import Application, AppVersion
|
|
from files.models import File, FileUpload, Platform
|
|
from files.utils import parse_addon, VERSION_RE
|
|
from market.models import AddonPremium, Price
|
|
from translations.widgets import TranslationTextarea, TranslationTextInput
|
|
from translations.fields import TransTextarea, TransField
|
|
from translations.models import delete_translation, Translation
|
|
from translations.forms import TranslationFormMixin
|
|
from versions.models import License, Version, ApplicationsVersions
|
|
from mkt.webapps.models import Webapp
|
|
from . import tasks
|
|
|
|
paypal_log = commonware.log.getLogger('z.paypal')
|
|
|
|
|
|
class AuthorForm(happyforms.ModelForm):
|
|
|
|
# TODO: Remove this whole __init__ when the 'allow-refund' flag goes away.
|
|
def __init__(self, *args, **kwargs):
|
|
super(AuthorForm, self).__init__(*args, **kwargs)
|
|
self.fields['role'].choices = (
|
|
(c, s) for c, s in amo.AUTHOR_CHOICES
|
|
if c != amo.AUTHOR_ROLE_SUPPORT or
|
|
waffle.switch_is_active('allow-refund'))
|
|
|
|
class Meta:
|
|
model = AddonUser
|
|
exclude = ('addon')
|
|
|
|
|
|
class BaseModelFormSet(BaseModelFormSet):
|
|
"""
|
|
Override the parent's is_valid to prevent deleting all forms.
|
|
"""
|
|
|
|
def is_valid(self):
|
|
# clean() won't get called in is_valid() if all the rows are getting
|
|
# deleted. We can't allow deleting everything.
|
|
rv = super(BaseModelFormSet, self).is_valid()
|
|
return rv and not any(self.errors) and not bool(self.non_form_errors())
|
|
|
|
|
|
class BaseAuthorFormSet(BaseModelFormSet):
|
|
|
|
def clean(self):
|
|
if any(self.errors):
|
|
return
|
|
# cleaned_data could be None if it's the empty extra form.
|
|
data = filter(None, [f.cleaned_data for f in self.forms
|
|
if not f.cleaned_data.get('DELETE', False)])
|
|
if not any(d['role'] == amo.AUTHOR_ROLE_OWNER for d in data):
|
|
raise forms.ValidationError(_('Must have at least one owner.'))
|
|
if not any(d['listed'] for d in data):
|
|
raise forms.ValidationError(
|
|
_('At least one author must be listed.'))
|
|
users = [d['user'] for d in data]
|
|
if sorted(users) != sorted(set(users)):
|
|
raise forms.ValidationError(
|
|
_('An author can only be listed once.'))
|
|
|
|
|
|
AuthorFormSet = modelformset_factory(AddonUser, formset=BaseAuthorFormSet,
|
|
form=AuthorForm, can_delete=True, extra=0)
|
|
|
|
|
|
class DeleteForm(happyforms.Form):
|
|
password = forms.CharField()
|
|
|
|
def __init__(self, request):
|
|
self.user = request.amo_user
|
|
super(DeleteForm, self).__init__(request.POST)
|
|
|
|
def clean_password(self):
|
|
data = self.cleaned_data
|
|
if not self.user.check_password(data['password']):
|
|
raise forms.ValidationError(_('Password incorrect.'))
|
|
|
|
|
|
class LicenseChoiceRadio(forms.widgets.RadioFieldRenderer):
|
|
|
|
def __iter__(self):
|
|
for i, choice in enumerate(self.choices):
|
|
yield LicenseRadioInput(self.name, self.value, self.attrs.copy(),
|
|
choice, i)
|
|
|
|
|
|
class LicenseRadioInput(forms.widgets.RadioInput):
|
|
|
|
def __init__(self, name, value, attrs, choice, index):
|
|
super(LicenseRadioInput, self).__init__(name, value, attrs, choice,
|
|
index)
|
|
license = choice[1] # Choice is a tuple (object.id, object).
|
|
link = u'<a class="xx extra" href="%s" target="_blank">%s</a>'
|
|
if hasattr(license, 'url'):
|
|
details = link % (license.url, _('Details'))
|
|
self.choice_label = mark_safe(self.choice_label + details)
|
|
|
|
|
|
class LicenseForm(AMOModelForm):
|
|
builtin = forms.TypedChoiceField(choices=[], coerce=int,
|
|
widget=forms.RadioSelect(attrs={'class': 'license'},
|
|
renderer=LicenseChoiceRadio))
|
|
name = forms.CharField(widget=TranslationTextInput(),
|
|
label=_("What is your license's name?"),
|
|
required=False, initial=_('Custom License'))
|
|
text = forms.CharField(widget=TranslationTextarea(), required=False,
|
|
label=_('Provide the text of your license.'))
|
|
|
|
def __init__(self, *args, **kw):
|
|
addon = kw.pop('addon', None)
|
|
self.version = None
|
|
if addon:
|
|
qs = addon.versions.order_by('-version')[:1]
|
|
self.version = qs[0] if qs else None
|
|
if self.version:
|
|
kw['instance'], kw['initial'] = self.version.license, None
|
|
# Clear out initial data if it's a builtin license.
|
|
if getattr(kw['instance'], 'builtin', None):
|
|
kw['initial'] = {'builtin': kw['instance'].builtin}
|
|
kw['instance'] = None
|
|
|
|
super(LicenseForm, self).__init__(*args, **kw)
|
|
|
|
cs = [(x.builtin, x)
|
|
for x in License.objects.builtins().filter(on_form=True)]
|
|
cs.append((License.OTHER, _('Other')))
|
|
self.fields['builtin'].choices = cs
|
|
|
|
class Meta:
|
|
model = License
|
|
fields = ('builtin', 'name', 'text')
|
|
|
|
def clean_name(self):
|
|
name = self.cleaned_data['name']
|
|
return name.strip() or _('Custom License')
|
|
|
|
def clean(self):
|
|
data = self.cleaned_data
|
|
if self.errors:
|
|
return data
|
|
elif data['builtin'] == License.OTHER and not data['text']:
|
|
raise forms.ValidationError(
|
|
_('License text is required when choosing Other.'))
|
|
return data
|
|
|
|
def get_context(self):
|
|
"""Returns a view context dict having keys license_urls, license_form,
|
|
and license_other_val.
|
|
"""
|
|
license_urls = dict(License.objects.builtins()
|
|
.values_list('builtin', 'url'))
|
|
return dict(license_urls=license_urls, version=self.version,
|
|
license_form=self.version and self,
|
|
license_other_val=License.OTHER)
|
|
|
|
def save(self, *args, **kw):
|
|
"""Save all form data.
|
|
|
|
This will only create a new license if it's not one of the builtin
|
|
ones.
|
|
|
|
Keyword arguments
|
|
|
|
**log=True**
|
|
Set to False if you do not want to log this action for display
|
|
on the developer dashboard.
|
|
"""
|
|
log = kw.pop('log', True)
|
|
changed = self.changed_data
|
|
|
|
builtin = self.cleaned_data['builtin']
|
|
if builtin != License.OTHER:
|
|
license = License.objects.get(builtin=builtin)
|
|
else:
|
|
# Save the custom license:
|
|
license = super(LicenseForm, self).save(*args, **kw)
|
|
|
|
if self.version:
|
|
if changed or license != self.version.license:
|
|
self.version.update(license=license)
|
|
if log:
|
|
amo.log(amo.LOG.CHANGE_LICENSE, license,
|
|
self.version.addon)
|
|
return license
|
|
|
|
|
|
class PolicyForm(TranslationFormMixin, AMOModelForm):
|
|
"""Form for editing the add-ons EULA and privacy policy."""
|
|
has_eula = forms.BooleanField(required=False,
|
|
label=_lazy(u'This add-on has an End User License Agreement'))
|
|
eula = TransField(widget=TransTextarea(), required=False,
|
|
label=_lazy(u"Please specify your add-on's "
|
|
"End User License Agreement:"))
|
|
has_priv = forms.BooleanField(
|
|
required=False, label=_lazy(u"This add-on has a Privacy Policy"))
|
|
privacy_policy = TransField(widget=TransTextarea(), required=False,
|
|
label=_lazy(u"Please specify your add-on's Privacy Policy:"))
|
|
|
|
def __init__(self, *args, **kw):
|
|
self.addon = kw.pop('addon', None)
|
|
if not self.addon:
|
|
raise ValueError('addon keyword arg cannot be None')
|
|
kw['instance'] = self.addon
|
|
kw['initial'] = dict(has_priv=self._has_field('privacy_policy'),
|
|
has_eula=self._has_field('eula'))
|
|
super(PolicyForm, self).__init__(*args, **kw)
|
|
|
|
def _has_field(self, name):
|
|
# If there's a eula in any language, this addon has a eula.
|
|
n = getattr(self.addon, u'%s_id' % name)
|
|
return any(map(bool, Translation.objects.filter(id=n)))
|
|
|
|
class Meta:
|
|
model = Addon
|
|
fields = ('eula', 'privacy_policy')
|
|
|
|
def save(self, commit=True):
|
|
ob = super(PolicyForm, self).save(commit)
|
|
for k, field in (('has_eula', 'eula'),
|
|
('has_priv', 'privacy_policy')):
|
|
if not self.cleaned_data[k]:
|
|
delete_translation(self.instance, field)
|
|
|
|
if 'privacy_policy' in self.changed_data:
|
|
amo.log(amo.LOG.CHANGE_POLICY, self.addon, self.instance)
|
|
|
|
return ob
|
|
|
|
|
|
def ProfileForm(*args, **kw):
|
|
# If the add-on takes contributions, then both fields are required.
|
|
addon = kw['instance']
|
|
fields_required = (kw.pop('required', False) or
|
|
bool(addon.takes_contributions))
|
|
if addon.is_webapp():
|
|
the_reason_label = _('Why did you make this app?')
|
|
the_future_label = _("What's next for this app?")
|
|
else:
|
|
the_reason_label = _('Why did you make this add-on?')
|
|
the_future_label = _("What's next for this add-on?")
|
|
|
|
class _Form(TranslationFormMixin, happyforms.ModelForm):
|
|
the_reason = TransField(widget=TransTextarea(),
|
|
required=fields_required,
|
|
label=the_reason_label)
|
|
the_future = TransField(widget=TransTextarea(),
|
|
required=fields_required,
|
|
label=the_future_label)
|
|
|
|
class Meta:
|
|
model = Addon
|
|
fields = ('the_reason', 'the_future')
|
|
|
|
return _Form(*args, **kw)
|
|
|
|
|
|
class CharityForm(happyforms.ModelForm):
|
|
url = Charity._meta.get_field('url').formfield(verify_exists=False)
|
|
|
|
class Meta:
|
|
model = Charity
|
|
fields = ('name', 'url', 'paypal')
|
|
|
|
def clean_paypal(self):
|
|
check_paypal_id(self.cleaned_data['paypal'])
|
|
return self.cleaned_data['paypal']
|
|
|
|
def save(self, commit=True):
|
|
# We link to the charity row in contrib stats, so we force all charity
|
|
# changes to create a new row so we don't forget old charities.
|
|
if self.changed_data and self.instance.id:
|
|
self.instance.id = None
|
|
return super(CharityForm, self).save(commit)
|
|
|
|
|
|
class ContribForm(TranslationFormMixin, happyforms.ModelForm):
|
|
RECIPIENTS = (('dev', _lazy(u'The developers of this add-on')),
|
|
('moz', _lazy(u'The Mozilla Foundation')),
|
|
('org', _lazy(u'An organization of my choice')))
|
|
|
|
recipient = forms.ChoiceField(choices=RECIPIENTS,
|
|
widget=forms.RadioSelect(attrs={'class': 'recipient'}))
|
|
thankyou_note = TransField(widget=TransTextarea(), required=False)
|
|
|
|
class Meta:
|
|
model = Addon
|
|
fields = ('paypal_id', 'suggested_amount', 'annoying',
|
|
'enable_thankyou', 'thankyou_note')
|
|
widgets = {
|
|
'annoying': forms.RadioSelect(),
|
|
'suggested_amount': forms.TextInput(attrs={'class': 'short'}),
|
|
'paypal_id': forms.TextInput(attrs={'size': '50'})
|
|
}
|
|
|
|
@staticmethod
|
|
def initial(addon):
|
|
if addon.charity:
|
|
recip = 'moz' if addon.charity_id == amo.FOUNDATION_ORG else 'org'
|
|
else:
|
|
recip = 'dev'
|
|
return {'recipient': recip,
|
|
'annoying': addon.annoying or amo.CONTRIB_PASSIVE}
|
|
|
|
def clean(self):
|
|
if self.instance.upsell:
|
|
raise forms.ValidationError(_('You cannot setup Contributions for '
|
|
'an add-on that is linked to a premium '
|
|
'add-on in the Marketplace.'))
|
|
|
|
data = self.cleaned_data
|
|
try:
|
|
if not self.errors and data['recipient'] == 'dev':
|
|
check_paypal_id(data['paypal_id'])
|
|
except forms.ValidationError, e:
|
|
self.errors['paypal_id'] = self.error_class(e.messages)
|
|
# thankyou_note is a dict since it's a Translation.
|
|
if not (data.get('enable_thankyou') and
|
|
any(data.get('thankyou_note').values())):
|
|
data['thankyou_note'] = {}
|
|
data['enable_thankyou'] = False
|
|
return data
|
|
|
|
def clean_suggested_amount(self):
|
|
amount = self.cleaned_data['suggested_amount']
|
|
if amount is not None and amount <= 0:
|
|
msg = _(u'Please enter a suggested amount greater than 0.')
|
|
raise forms.ValidationError(msg)
|
|
if amount > settings.MAX_CONTRIBUTION:
|
|
msg = _(u'Please enter a suggested amount less than ${0}.').format(
|
|
settings.MAX_CONTRIBUTION)
|
|
raise forms.ValidationError(msg)
|
|
return amount
|
|
|
|
|
|
def check_paypal_id(paypal_id):
|
|
if not paypal_id:
|
|
raise forms.ValidationError(
|
|
_('PayPal ID required to accept contributions.'))
|
|
try:
|
|
valid, msg = paypal.check_paypal_id(paypal_id)
|
|
if not valid:
|
|
raise forms.ValidationError(msg)
|
|
except socket.error:
|
|
raise forms.ValidationError(_('Could not validate PayPal id.'))
|
|
|
|
|
|
class VersionForm(happyforms.ModelForm):
|
|
releasenotes = TransField(
|
|
widget=TransTextarea(), required=False)
|
|
approvalnotes = forms.CharField(
|
|
widget=TranslationTextarea(attrs={'rows': 4}), required=False)
|
|
|
|
class Meta:
|
|
model = Version
|
|
fields = ('releasenotes', 'approvalnotes')
|
|
|
|
|
|
class ApplicationChoiceField(forms.ModelChoiceField):
|
|
|
|
def label_from_instance(self, obj):
|
|
return obj.id
|
|
|
|
|
|
class AppVersionChoiceField(forms.ModelChoiceField):
|
|
|
|
def label_from_instance(self, obj):
|
|
return obj.version
|
|
|
|
|
|
class CompatForm(happyforms.ModelForm):
|
|
application = ApplicationChoiceField(Application.objects.all(),
|
|
widget=forms.HiddenInput)
|
|
min = AppVersionChoiceField(AppVersion.objects.none())
|
|
max = AppVersionChoiceField(AppVersion.objects.none())
|
|
|
|
class Meta:
|
|
model = ApplicationsVersions
|
|
fields = ('application', 'min', 'max')
|
|
|
|
def __init__(self, *args, **kw):
|
|
super(CompatForm, self).__init__(*args, **kw)
|
|
if self.initial:
|
|
app = self.initial['application']
|
|
else:
|
|
app = self.data[self.add_prefix('application')]
|
|
self.app = amo.APPS_ALL[int(app)]
|
|
qs = AppVersion.objects.filter(application=app).order_by('version_int')
|
|
self.fields['min'].queryset = qs.filter(~Q(version__contains='*'))
|
|
self.fields['max'].queryset = qs.all()
|
|
|
|
def clean(self):
|
|
min = self.cleaned_data.get('min')
|
|
max = self.cleaned_data.get('max')
|
|
if not (min and max and min.version_int <= max.version_int):
|
|
raise forms.ValidationError(_('Invalid version range.'))
|
|
return self.cleaned_data
|
|
|
|
|
|
class BaseCompatFormSet(BaseModelFormSet):
|
|
|
|
def __init__(self, *args, **kw):
|
|
super(BaseCompatFormSet, self).__init__(*args, **kw)
|
|
# We always want a form for each app, so force extras for apps
|
|
# the add-on does not already have.
|
|
qs = kw['queryset'].values_list('application', flat=True)
|
|
apps = [a for a in amo.APP_USAGE if a.id not in qs]
|
|
self.initial = ([{} for _ in qs] +
|
|
[{'application': a.id} for a in apps])
|
|
self.extra = len(amo.APP_GUIDS) - len(self.forms)
|
|
self._construct_forms()
|
|
|
|
def clean(self):
|
|
if any(self.errors):
|
|
return
|
|
apps = filter(None, [f.cleaned_data for f in self.forms
|
|
if not f.cleaned_data.get('DELETE', False)])
|
|
if not apps:
|
|
raise forms.ValidationError(
|
|
_('Need at least one compatible application.'))
|
|
|
|
|
|
CompatFormSet = modelformset_factory(
|
|
ApplicationsVersions, formset=BaseCompatFormSet,
|
|
form=CompatForm, can_delete=True, extra=0)
|
|
|
|
|
|
def verify_app_domain(manifest_url):
|
|
if waffle.switch_is_active('webapps-unique-by-domain'):
|
|
domain = Webapp.domain_from_url(manifest_url)
|
|
if Addon.objects.filter(app_domain=domain).exists():
|
|
raise forms.ValidationError(
|
|
_('An app already exists on this domain, '
|
|
'only one app per domain is allowed.'))
|
|
|
|
|
|
class NewWebappForm(happyforms.Form):
|
|
upload = forms.ModelChoiceField(widget=forms.HiddenInput,
|
|
queryset=FileUpload.objects.filter(valid=True),
|
|
error_messages={'invalid_choice': _lazy('There was an error with your '
|
|
'upload. Please try again.')})
|
|
|
|
def clean_upload(self):
|
|
upload = self.cleaned_data['upload']
|
|
verify_app_domain(upload.name) # JS puts manifest URL here
|
|
return upload
|
|
|
|
|
|
class NewAddonForm(happyforms.Form):
|
|
upload = forms.ModelChoiceField(widget=forms.HiddenInput,
|
|
queryset=FileUpload.objects.filter(valid=True),
|
|
error_messages={'invalid_choice': _lazy('There was an error with your '
|
|
'upload. Please try again.')})
|
|
desktop_platforms = forms.ModelMultipleChoiceField(
|
|
queryset=Platform.objects,
|
|
widget=forms.CheckboxSelectMultiple(attrs={'class': 'platform'}),
|
|
initial=[amo.PLATFORM_ALL.id],
|
|
required=False)
|
|
desktop_platforms.choices = ((p.id, p.name)
|
|
for p in amo.DESKTOP_PLATFORMS.values())
|
|
mobile_platforms = forms.ModelMultipleChoiceField(
|
|
queryset=Platform.objects,
|
|
widget=forms.CheckboxSelectMultiple(attrs={'class': 'platform'}),
|
|
required=False)
|
|
mobile_platforms.choices = ((p.id, p.name)
|
|
for p in amo.MOBILE_PLATFORMS.values())
|
|
|
|
def clean(self):
|
|
if not self.errors:
|
|
xpi = parse_addon(self.cleaned_data['upload'])
|
|
addons.forms.clean_name(xpi['name'])
|
|
self._clean_all_platforms()
|
|
return self.cleaned_data
|
|
|
|
def _clean_all_platforms(self):
|
|
if (not self.cleaned_data['desktop_platforms']
|
|
and not self.cleaned_data['mobile_platforms']):
|
|
raise forms.ValidationError(_('Need at least one platform.'))
|
|
|
|
|
|
class NewVersionForm(NewAddonForm):
|
|
|
|
def __init__(self, *args, **kw):
|
|
self.addon = kw.pop('addon')
|
|
super(NewVersionForm, self).__init__(*args, **kw)
|
|
|
|
def clean(self):
|
|
if not self.errors:
|
|
xpi = parse_addon(self.cleaned_data['upload'], self.addon)
|
|
if self.addon.versions.filter(version=xpi['version']):
|
|
raise forms.ValidationError(
|
|
_(u'Version %s already exists') % xpi['version'])
|
|
self._clean_all_platforms()
|
|
return self.cleaned_data
|
|
|
|
|
|
class NewFileForm(happyforms.Form):
|
|
upload = forms.ModelChoiceField(widget=forms.HiddenInput,
|
|
queryset=FileUpload.objects.filter(valid=True),
|
|
error_messages={'invalid_choice': _lazy('There was an error with your '
|
|
'upload. Please try again.')})
|
|
platform = File._meta.get_field('platform').formfield(empty_label=None,
|
|
widget=forms.RadioSelect(attrs={'class': 'platform'}))
|
|
platform.choices = sorted((p.id, p.name)
|
|
for p in amo.SUPPORTED_PLATFORMS.values())
|
|
|
|
def __init__(self, *args, **kw):
|
|
self.addon = kw.pop('addon')
|
|
self.version = kw.pop('version')
|
|
super(NewFileForm, self).__init__(*args, **kw)
|
|
# Reset platform choices to just those compatible with target app.
|
|
field = self.fields['platform']
|
|
field.choices = sorted((k, v.name) for k, v in
|
|
self.version.compatible_platforms().items())
|
|
# Don't allow platforms we already have.
|
|
to_exclude = set(File.objects.filter(version=self.version)
|
|
.values_list('platform', flat=True))
|
|
# Don't allow platform=ALL if we already have platform files.
|
|
if len(to_exclude):
|
|
to_exclude.add(amo.PLATFORM_ALL.id)
|
|
|
|
# Always exclude PLATFORM_ALL_MOBILE because it's not supported for
|
|
# downloads yet. The developer can choose Android + Maemo for now.
|
|
# TODO(Kumar) Allow this option when it's supported everywhere.
|
|
# See bug 646268.
|
|
to_exclude.add(amo.PLATFORM_ALL_MOBILE.id)
|
|
|
|
field.choices = [p for p in field.choices if p[0] not in to_exclude]
|
|
field.queryset = Platform.objects.filter(id__in=dict(field.choices))
|
|
|
|
def clean(self):
|
|
if not self.version.is_allowed_upload():
|
|
raise forms.ValidationError(
|
|
_('You cannot upload any more files for this version.'))
|
|
|
|
# Check for errors in the xpi.
|
|
if not self.errors:
|
|
xpi = parse_addon(self.cleaned_data['upload'], self.addon)
|
|
if xpi['version'] != self.version.version:
|
|
raise forms.ValidationError(_("Version doesn't match"))
|
|
return self.cleaned_data
|
|
|
|
|
|
class FileForm(happyforms.ModelForm):
|
|
platform = File._meta.get_field('platform').formfield(empty_label=None)
|
|
|
|
class Meta:
|
|
model = File
|
|
fields = ('platform',)
|
|
|
|
def __init__(self, *args, **kw):
|
|
super(FileForm, self).__init__(*args, **kw)
|
|
if kw['instance'].version.addon.type in (amo.ADDON_SEARCH,
|
|
amo.ADDON_WEBAPP):
|
|
del self.fields['platform']
|
|
else:
|
|
compat = kw['instance'].version.compatible_platforms()
|
|
# TODO(Kumar) Allow PLATFORM_ALL_MOBILE when it's supported.
|
|
# See bug 646268.
|
|
if amo.PLATFORM_ALL_MOBILE.id in compat:
|
|
del compat[amo.PLATFORM_ALL_MOBILE.id]
|
|
pid = int(kw['instance'].platform_id)
|
|
plats = [(p.id, p.name) for p in compat.values()]
|
|
if pid not in compat:
|
|
plats.append([pid, amo.PLATFORMS[pid].name])
|
|
self.fields['platform'].choices = plats
|
|
|
|
def clean_DELETE(self):
|
|
if any(self.errors):
|
|
return
|
|
delete = self.cleaned_data['DELETE']
|
|
|
|
if (delete and not self.instance.version.is_all_unreviewed):
|
|
error = _('You cannot delete a file once the review process has '
|
|
'started. You must delete the whole version.')
|
|
raise forms.ValidationError(error)
|
|
|
|
return delete
|
|
|
|
|
|
class BaseFileFormSet(BaseModelFormSet):
|
|
|
|
def clean(self):
|
|
if any(self.errors):
|
|
return
|
|
files = [f.cleaned_data for f in self.forms
|
|
if not f.cleaned_data.get('DELETE', False)]
|
|
|
|
if self.forms and 'platform' in self.forms[0].fields:
|
|
platforms = [f['platform'].id for f in files]
|
|
|
|
if amo.PLATFORM_ALL.id in platforms and len(files) > 1:
|
|
raise forms.ValidationError(
|
|
_('The platform All cannot be combined '
|
|
'with specific platforms.'))
|
|
|
|
if sorted(platforms) != sorted(set(platforms)):
|
|
raise forms.ValidationError(
|
|
_('A platform can only be chosen once.'))
|
|
|
|
|
|
FileFormSet = modelformset_factory(File, formset=BaseFileFormSet,
|
|
form=FileForm, can_delete=True, extra=0)
|
|
|
|
|
|
class ReviewTypeForm(forms.Form):
|
|
_choices = [(k, amo.STATUS_CHOICES[k]) for k in
|
|
(amo.STATUS_UNREVIEWED, amo.STATUS_NOMINATED)]
|
|
review_type = forms.TypedChoiceField(
|
|
choices=_choices, widget=forms.HiddenInput,
|
|
coerce=int, empty_value=None,
|
|
error_messages={'required': _lazy(u'A review type must be selected.')})
|
|
|
|
|
|
class Step3Form(addons.forms.AddonFormBasic):
|
|
description = TransField(widget=TransTextarea, required=False)
|
|
|
|
class Meta:
|
|
model = Addon
|
|
fields = ('name', 'slug', 'summary', 'tags', 'description',
|
|
'homepage', 'support_email', 'support_url')
|
|
|
|
|
|
class Step3WebappForm(Step3Form):
|
|
"""Form to override certain fields for webapps"""
|
|
name = TransField(max_length=128)
|
|
homepage = TransField.adapt(forms.URLField)(required=False,
|
|
verify_exists=False)
|
|
support_url = TransField.adapt(forms.URLField)(required=False,
|
|
verify_exists=False)
|
|
support_email = TransField.adapt(forms.EmailField)(required=False)
|
|
|
|
|
|
class PreviewForm(happyforms.ModelForm):
|
|
caption = TransField(widget=TransTextarea, required=False)
|
|
file_upload = forms.FileField(required=False)
|
|
upload_hash = forms.CharField(required=False)
|
|
|
|
def save(self, addon, commit=True):
|
|
if self.cleaned_data:
|
|
self.instance.addon = addon
|
|
if self.cleaned_data.get('DELETE'):
|
|
# Existing preview.
|
|
if self.instance.id:
|
|
self.instance.delete()
|
|
# User has no desire to save this preview.
|
|
return
|
|
|
|
super(PreviewForm, self).save(commit=commit)
|
|
if self.cleaned_data['upload_hash']:
|
|
upload_hash = self.cleaned_data['upload_hash']
|
|
upload_path = os.path.join(settings.TMP_PATH, 'preview',
|
|
upload_hash)
|
|
tasks.resize_preview.delay(upload_path, self.instance,
|
|
set_modified_on=[self.instance])
|
|
|
|
class Meta:
|
|
model = Preview
|
|
fields = ('caption', 'file_upload', 'upload_hash', 'id', 'position')
|
|
|
|
|
|
class BasePreviewFormSet(BaseModelFormSet):
|
|
|
|
def clean(self):
|
|
if any(self.errors):
|
|
return
|
|
|
|
|
|
PreviewFormSet = modelformset_factory(Preview, formset=BasePreviewFormSet,
|
|
form=PreviewForm, can_delete=True,
|
|
extra=1)
|
|
|
|
|
|
class AdminForm(happyforms.ModelForm):
|
|
type = forms.ChoiceField(choices=amo.ADDON_TYPE.items())
|
|
|
|
# Request is needed in other ajax forms so we're stuck here.
|
|
def __init__(self, request=None, *args, **kw):
|
|
super(AdminForm, self).__init__(*args, **kw)
|
|
|
|
class Meta:
|
|
model = Addon
|
|
fields = ('trusted', 'type', 'guid',
|
|
'target_locale', 'locale_disambiguation')
|
|
widgets = {
|
|
'guid': forms.TextInput(attrs={'size': '50'})
|
|
}
|
|
|
|
|
|
class InlineRadioRenderer(forms.widgets.RadioFieldRenderer):
|
|
|
|
def render(self):
|
|
return mark_safe(''.join(force_unicode(w) for w in self))
|
|
|
|
|
|
class PackagerBasicForm(forms.Form):
|
|
name = forms.CharField(min_length=5, max_length=50,
|
|
help_text=_lazy(u'Give your add-on a name. The most successful '
|
|
'add-ons give some indication of their function in '
|
|
'their name.'))
|
|
description = forms.CharField(required=False, widget=forms.Textarea,
|
|
help_text=_lazy(u'Briefly describe your add-on in one sentence. '
|
|
'This appears in the Add-ons Manager.'))
|
|
version = forms.CharField(max_length=32,
|
|
help_text=_lazy(u'Enter your initial version number. Depending on the '
|
|
'number of releases and your preferences, this is '
|
|
'usually 0.1 or 1.0'))
|
|
id = forms.CharField(
|
|
help_text=_lazy(u'Each add-on requires a unique ID in the form of a '
|
|
'UUID or an email address, such as '
|
|
'addon-name@developer.com. The email address does not '
|
|
'have to be valid.'))
|
|
package_name = forms.CharField(min_length=5, max_length=50,
|
|
help_text=_lazy(u'The package name of your add-on used within the '
|
|
'browser. This should be a short form of its name '
|
|
'(for example, Test Extension might be '
|
|
'test_extension).'))
|
|
author_name = forms.CharField(
|
|
help_text=_lazy(u'Enter the name of the person or entity to be '
|
|
'listed as the author of this add-on.'))
|
|
contributors = forms.CharField(required=False, widget=forms.Textarea,
|
|
help_text=_lazy(u'Enter the names of any other contributors to this '
|
|
'extension, one per line.'))
|
|
|
|
def clean_name(self):
|
|
name = self.cleaned_data['name']
|
|
addons.forms.clean_name(name)
|
|
name_regex = re.compile('(mozilla|firefox|thunderbird)', re.I)
|
|
if name_regex.match(name):
|
|
raise forms.ValidationError(
|
|
_('Add-on names should not contain Mozilla trademarks.'))
|
|
return name
|
|
|
|
def clean_package_name(self):
|
|
slug = self.cleaned_data['package_name']
|
|
if slugify(slug, ok='_', lower=False, delimiter='_') != slug:
|
|
raise forms.ValidationError(
|
|
_('Enter a valid package name consisting of letters, numbers, '
|
|
'or underscores.'))
|
|
if Addon.objects.filter(slug=slug).exists():
|
|
raise forms.ValidationError(
|
|
_('This package name is already in use.'))
|
|
if BlacklistedSlug.blocked(slug):
|
|
raise forms.ValidationError(
|
|
_(u'The package name cannot be: %s.' % slug))
|
|
return slug
|
|
|
|
def clean_id(self):
|
|
id_regex = re.compile(
|
|
"""(\{[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}\} | # GUID
|
|
[a-z0-9-\.\+_]*\@[a-z0-9-\._]+) # Email format""",
|
|
re.I | re.X)
|
|
|
|
if not id_regex.match(self.cleaned_data['id']):
|
|
raise forms.ValidationError(
|
|
_('The add-on ID must be a UUID string or an email '
|
|
'address.'))
|
|
return self.cleaned_data['id']
|
|
|
|
def clean_version(self):
|
|
if not VERSION_RE.match(self.cleaned_data['version']):
|
|
raise forms.ValidationError(_('The version string is invalid.'))
|
|
return self.cleaned_data['version']
|
|
|
|
|
|
class PackagerCompatForm(forms.Form):
|
|
enabled = forms.BooleanField(required=False)
|
|
min_ver = forms.ModelChoiceField(AppVersion.objects.none(),
|
|
empty_label=None, required=False,
|
|
label=_lazy(u'Minimum'))
|
|
max_ver = forms.ModelChoiceField(AppVersion.objects.none(),
|
|
empty_label=None, required=False,
|
|
label=_lazy(u'Maximum'))
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(PackagerCompatForm, self).__init__(*args, **kwargs)
|
|
if not self.initial:
|
|
return
|
|
|
|
self.app = self.initial['application']
|
|
qs = (AppVersion.objects.filter(application=self.app.id)
|
|
.order_by('-version_int'))
|
|
|
|
self.fields['enabled'].label = self.app.pretty
|
|
if self.app == amo.FIREFOX:
|
|
self.fields['enabled'].widget.attrs['checked'] = True
|
|
|
|
# Don't allow version ranges as the minimum version.
|
|
self.fields['min_ver'].queryset = qs.filter(~Q(version__contains='*'))
|
|
self.fields['max_ver'].queryset = qs.all()
|
|
|
|
# Unreasonably hardcode a reasonable default minVersion.
|
|
if self.app in (amo.FIREFOX, amo.MOBILE, amo.THUNDERBIRD):
|
|
try:
|
|
self.fields['min_ver'].initial = qs.filter(
|
|
version=settings.DEFAULT_MINVER)[0]
|
|
except (IndexError, AttributeError):
|
|
pass
|
|
|
|
def clean_min_ver(self):
|
|
if self.cleaned_data['enabled'] and not self.cleaned_data['min_ver']:
|
|
raise_required()
|
|
return self.cleaned_data['min_ver']
|
|
|
|
def clean_max_ver(self):
|
|
if self.cleaned_data['enabled'] and not self.cleaned_data['max_ver']:
|
|
raise_required()
|
|
return self.cleaned_data['max_ver']
|
|
|
|
def clean(self):
|
|
if self.errors:
|
|
return
|
|
|
|
data = self.cleaned_data
|
|
|
|
if data['enabled']:
|
|
min_ver = data['min_ver']
|
|
max_ver = data['max_ver']
|
|
if not (min_ver and max_ver):
|
|
raise forms.ValidationError(_('Invalid version range.'))
|
|
|
|
if min_ver.version_int > max_ver.version_int:
|
|
raise forms.ValidationError(
|
|
_('Min version must be less than Max version.'))
|
|
|
|
# Pass back the app name and GUID.
|
|
data['min_ver'] = str(min_ver)
|
|
data['max_ver'] = str(max_ver)
|
|
data['name'] = self.app.pretty
|
|
data['guid'] = self.app.guid
|
|
|
|
return data
|
|
|
|
|
|
class PackagerCompatBaseFormSet(BaseFormSet):
|
|
|
|
def __init__(self, *args, **kw):
|
|
super(PackagerCompatBaseFormSet, self).__init__(*args, **kw)
|
|
self.initial = [{'application': a} for a in amo.APP_USAGE]
|
|
self._construct_forms()
|
|
|
|
def clean(self):
|
|
if any(self.errors):
|
|
return
|
|
if (not self.forms or not
|
|
any(f.cleaned_data.get('enabled') for f in self.forms
|
|
if f.app == amo.FIREFOX)):
|
|
# L10n: {0} is Firefox.
|
|
raise forms.ValidationError(
|
|
_(u'{0} is a required target application.')
|
|
.format(amo.FIREFOX.pretty))
|
|
return self.cleaned_data
|
|
|
|
|
|
PackagerCompatFormSet = formset_factory(PackagerCompatForm,
|
|
formset=PackagerCompatBaseFormSet, extra=0)
|
|
|
|
|
|
class PackagerFeaturesForm(forms.Form):
|
|
about_dialog = forms.BooleanField(
|
|
required=False,
|
|
label=_lazy(u'About dialog'),
|
|
help_text=_lazy(u'Creates a standard About dialog for your '
|
|
'extension'))
|
|
preferences_dialog = forms.BooleanField(
|
|
required=False,
|
|
label=_lazy(u'Preferences dialog'),
|
|
help_text=_lazy(u'Creates an example Preferences window'))
|
|
toolbar = forms.BooleanField(
|
|
required=False,
|
|
label=_lazy(u'Toolbar'),
|
|
help_text=_lazy(u'Creates an example toolbar for your extension'))
|
|
toolbar_button = forms.BooleanField(
|
|
required=False,
|
|
label=_lazy(u'Toolbar button'),
|
|
help_text=_lazy(u'Creates an example button on the browser '
|
|
'toolbar'))
|
|
main_menu_command = forms.BooleanField(
|
|
required=False,
|
|
label=_lazy(u'Main menu command'),
|
|
help_text=_lazy(u'Creates an item on the Tools menu'))
|
|
context_menu_command = forms.BooleanField(
|
|
required=False,
|
|
label=_lazy(u'Context menu command'),
|
|
help_text=_lazy(u'Creates a context menu item for images'))
|
|
sidebar_support = forms.BooleanField(
|
|
required=False,
|
|
label=_lazy(u'Sidebar support'),
|
|
help_text=_lazy(u'Creates an example sidebar panel'))
|
|
|
|
|
|
class CheckCompatibilityForm(happyforms.Form):
|
|
application = forms.ChoiceField(
|
|
label=_lazy(u'Application'),
|
|
choices=[(a.id, a.pretty) for a in amo.APP_USAGE])
|
|
app_version = forms.ChoiceField(
|
|
label=_lazy(u'Version'),
|
|
choices=[('', _lazy(u'Select an application first'))])
|
|
|
|
def __init__(self, *args, **kw):
|
|
super(CheckCompatibilityForm, self).__init__(*args, **kw)
|
|
w = self.fields['application'].widget
|
|
# Get the URL after the urlconf has loaded.
|
|
w.attrs['data-url'] = reverse('devhub.compat_application_versions')
|
|
|
|
def version_choices_for_app_id(self, app_id):
|
|
versions = AppVersion.objects.filter(application__id=app_id)
|
|
return [(v.id, v.version) for v in versions]
|
|
|
|
def clean_application(self):
|
|
app_id = int(self.cleaned_data['application'])
|
|
app = Application.objects.get(pk=app_id)
|
|
self.cleaned_data['application'] = app
|
|
choices = self.version_choices_for_app_id(app_id)
|
|
self.fields['app_version'].choices = choices
|
|
return self.cleaned_data['application']
|
|
|
|
def clean_app_version(self):
|
|
v = self.cleaned_data['app_version']
|
|
return AppVersion.objects.get(pk=int(v))
|
|
|
|
|
|
class NewManifestForm(happyforms.Form):
|
|
manifest = forms.URLField(verify_exists=False)
|
|
|
|
def clean_manifest(self):
|
|
manifest = self.cleaned_data['manifest']
|
|
verify_app_domain(manifest)
|
|
return manifest
|
|
|
|
|
|
UPSELL_CHOICES = (
|
|
(0, _("I don't have a free add-on to associate.")),
|
|
(1, _('This is a premium upgrade to:')),
|
|
)
|
|
APP_UPSELL_CHOICES = (
|
|
(0, _("I don't have a free app to associate.")),
|
|
(1, _('This is a premium upgrade to:')),
|
|
)
|
|
|
|
|
|
class PremiumForm(happyforms.Form):
|
|
"""
|
|
The premium details for an addon, which is unfortunately
|
|
distributed across a few models.
|
|
"""
|
|
paypal_id = forms.CharField()
|
|
price = forms.ModelChoiceField(queryset=Price.objects.active(),
|
|
label=_('Add-on price'),
|
|
empty_label=None)
|
|
do_upsell = forms.TypedChoiceField(coerce=lambda x: bool(int(x)),
|
|
choices=UPSELL_CHOICES,
|
|
widget=forms.RadioSelect(),
|
|
required=False)
|
|
free = forms.ModelChoiceField(queryset=Addon.objects.none(),
|
|
required=False,
|
|
empty_label='')
|
|
text = forms.CharField(widget=forms.Textarea(), required=False)
|
|
support_email = forms.EmailField()
|
|
|
|
def __init__(self, *args, **kw):
|
|
self.extra = kw.pop('extra')
|
|
self.request = kw.pop('request')
|
|
self.addon = self.extra['addon']
|
|
kw['initial'] = {
|
|
'paypal_id': self.addon.paypal_id,
|
|
'support_email': self.addon.support_email,
|
|
'do_upsell': 0,
|
|
}
|
|
if self.addon.premium:
|
|
kw['initial']['price'] = self.addon.premium.price
|
|
|
|
upsell = self.addon.upsold
|
|
if upsell:
|
|
kw['initial'].update({
|
|
'text': upsell.text,
|
|
'free': upsell.free,
|
|
'do_upsell': 1,
|
|
})
|
|
|
|
super(PremiumForm, self).__init__(*args, **kw)
|
|
if self.addon.is_webapp():
|
|
self.fields['price'].label = loc('App price')
|
|
self.fields['do_upsell'].choices = APP_UPSELL_CHOICES
|
|
self.fields['free'].queryset = (self.extra['amo_user'].addons
|
|
.exclude(pk=self.addon.pk)
|
|
.filter(premium_type=amo.ADDON_FREE,
|
|
type=self.addon.type))
|
|
|
|
# For the wizard, we need to remove some fields.
|
|
for field in self.extra.get('exclude', []):
|
|
del self.fields[field]
|
|
|
|
def _show_token_msg(self, message):
|
|
"""Display warning for an invalid PayPal refund token."""
|
|
url = paypal.get_permission_url(self.addon,
|
|
self.extra.get('dest', 'payment'),
|
|
['REFUND'])
|
|
msg = _(' <a href="%s">Visit PayPal to grant permission'
|
|
' for refunds on your behalf.</a>') % url
|
|
messages.warning(self.request, '%s %s' % (message, Markup(msg)))
|
|
raise forms.ValidationError(message)
|
|
|
|
def clean_paypal_id(self):
|
|
paypal_id = self.cleaned_data['paypal_id']
|
|
# Check it's a valid paypal id.
|
|
check_paypal_id(paypal_id)
|
|
|
|
if (self.addon.paypal_id and self.addon.paypal_id != paypal_id
|
|
and self.addon.premium
|
|
and self.addon.premium.has_permissions_token()):
|
|
# If a user changes their paypal id, then we need
|
|
# to nuke the token, but don't do this when it's is blank.
|
|
self.addon.premium.paypal_permissions_token = ''
|
|
self.addon.premium.save()
|
|
|
|
return paypal_id
|
|
|
|
def clean(self):
|
|
paypal_id = self.cleaned_data.get('paypal_id', '')
|
|
if paypal_id:
|
|
# If we're going to prompt for refund permission, we need to
|
|
# record the PayPal ID first.
|
|
self.addon.paypal_id = paypal_id
|
|
self.addon.save()
|
|
# Check if third-party refund token is properly set up.
|
|
no_token = (not self.addon.premium or
|
|
not self.addon.premium.has_permissions_token())
|
|
invalid_token = (self.addon.premium and
|
|
not self.addon.premium.has_valid_permissions_token())
|
|
if no_token or invalid_token:
|
|
# L10n: We require PayPal users to have a third-party token set up.
|
|
self._show_token_msg(loc('PayPal third-party refund token has not '
|
|
'been set up or has recently expired.'))
|
|
return self.cleaned_data
|
|
|
|
def clean_text(self):
|
|
if self.cleaned_data['do_upsell'] and not self.cleaned_data['text']:
|
|
raise_required()
|
|
return self.cleaned_data['text']
|
|
|
|
def clean_free(self):
|
|
if self.cleaned_data['do_upsell'] and not self.cleaned_data['free']:
|
|
raise_required()
|
|
return self.cleaned_data['free']
|
|
|
|
def save(self):
|
|
if 'paypal_id' in self.cleaned_data:
|
|
self.addon.paypal_id = self.cleaned_data['paypal_id']
|
|
self.addon.support_email = self.cleaned_data['support_email']
|
|
self.addon.save()
|
|
|
|
if 'price' in self.cleaned_data:
|
|
premium = self.addon.premium
|
|
if not premium:
|
|
premium = AddonPremium()
|
|
premium.addon = self.addon
|
|
premium.price = self.cleaned_data['price']
|
|
premium.save()
|
|
|
|
upsell = self.addon.upsold
|
|
if (self.cleaned_data['do_upsell'] and
|
|
self.cleaned_data['text'] and self.cleaned_data['free']):
|
|
|
|
# Check if this app was already a premium version for another app.
|
|
if upsell and upsell.free != self.cleaned_data['free']:
|
|
upsell.delete()
|
|
|
|
if not upsell:
|
|
upsell = AddonUpsell(premium=self.addon)
|
|
upsell.text = self.cleaned_data['text']
|
|
upsell.free = self.cleaned_data['free']
|
|
upsell.save()
|
|
elif not self.cleaned_data['do_upsell'] and upsell:
|
|
upsell.delete()
|
|
|
|
|
|
def DependencyFormSet(*args, **kw):
|
|
addon_parent = kw.pop('addon')
|
|
|
|
# Add-ons: Required add-ons cannot include apps nor personas.
|
|
# Apps: Required apps cannot include any add-ons.
|
|
qs = Addon.objects.reviewed().exclude(id=addon_parent.id)
|
|
if addon_parent.is_webapp():
|
|
qs = qs.filter(type=amo.ADDON_WEBAPP)
|
|
else:
|
|
qs = qs.exclude(type__in=[amo.ADDON_PERSONA, amo.ADDON_WEBAPP])
|
|
|
|
class _Form(happyforms.ModelForm):
|
|
addon = forms.CharField(required=False, widget=forms.HiddenInput)
|
|
dependent_addon = forms.ModelChoiceField(qs, widget=forms.HiddenInput)
|
|
|
|
class Meta:
|
|
model = AddonDependency
|
|
fields = ('addon', 'dependent_addon')
|
|
|
|
def clean_addon(self):
|
|
return addon_parent
|
|
|
|
class _FormSet(BaseModelFormSet):
|
|
|
|
def clean(self):
|
|
if any(self.errors):
|
|
return
|
|
form_count = len([f for f in self.forms
|
|
if not f.cleaned_data.get('DELETE', False)])
|
|
if form_count > 3:
|
|
if addon_parent.is_webapp():
|
|
error = loc('There cannot be more than 3 required apps.')
|
|
else:
|
|
error = _('There cannot be more than 3 required add-ons.')
|
|
raise forms.ValidationError(error)
|
|
|
|
FormSet = modelformset_factory(AddonDependency, formset=_FormSet,
|
|
form=_Form, extra=0, can_delete=True)
|
|
return FormSet(*args, **kw)
|