addons-server/apps/devhub/forms.py

1148 строки
44 KiB
Python
Исходник Обычный вид История

# -*- coding: utf-8 -*-
import os
2011-05-26 03:44:34 +04:00
import re
2010-10-11 22:24:41 +04:00
import socket
from jinja2 import Markup
2010-09-30 07:12:18 +04:00
from django import forms
from django.conf import settings
2010-10-19 04:16:43 +04:00
from django.db.models import Q
2011-04-07 02:05:11 +04:00
from django.forms.models import modelformset_factory
2011-05-26 03:44:34 +04:00
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
2010-09-30 07:12:18 +04:00
2011-11-15 02:29:56 +04:00
import commonware
2010-10-05 23:24:31 +04:00
import happyforms
2010-10-08 07:08:41 +04:00
from tower import ugettext as _, ugettext_lazy as _lazy
2011-04-07 02:05:11 +04:00
from quieter_formset.formset import BaseModelFormSet
2012-02-04 00:56:47 +04:00
import waffle
2010-09-30 07:12:18 +04:00
import amo
2010-11-23 04:45:12 +03:00
import addons.forms
2010-10-11 22:24:41 +04:00
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
2011-10-02 10:08:10 +04:00
from amo.utils import raise_required, slugify
2010-10-19 04:16:43 +04:00
from applications.models import Application, AppVersion
from files.models import File, FileUpload, Platform
2011-05-26 03:44:34 +04:00
from files.utils import parse_addon, VERSION_RE
from market.models import AddonPremium, Price
2010-10-05 23:24:31 +04:00
from translations.widgets import TranslationTextarea, TranslationTextInput
2010-11-30 03:37:00 +03:00
from translations.fields import TransTextarea, TransField
from translations.models import delete_translation, Translation
2010-11-30 02:31:53 +03:00
from translations.forms import TranslationFormMixin
2010-10-19 04:16:43 +04:00
from versions.models import License, Version, ApplicationsVersions
from mkt.webapps.models import Webapp
2010-12-31 04:02:31 +03:00
from . import tasks
2010-09-30 07:12:18 +04:00
2011-11-15 02:29:56 +04:00
paypal_log = commonware.log.getLogger('z.paypal')
2010-09-30 07:12:18 +04:00
2010-10-05 23:24:31 +04:00
class AuthorForm(happyforms.ModelForm):
2010-09-30 07:12:18 +04:00
2012-02-04 00:56:47 +04:00
# 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'))
2010-09-30 07:12:18 +04:00
class Meta:
model = AddonUser
2010-10-09 06:25:15 +04:00
exclude = ('addon')
2010-09-30 07:12:18 +04:00
2010-10-19 04:16:43 +04:00
class BaseModelFormSet(BaseModelFormSet):
"""
Override the parent's is_valid to prevent deleting all forms.
"""
2010-09-30 07:12:18 +04:00
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.
2010-10-19 04:16:43 +04:00
rv = super(BaseModelFormSet, self).is_valid()
2010-09-30 07:12:18 +04:00
return rv and not any(self.errors) and not bool(self.non_form_errors())
2010-10-19 04:16:43 +04:00
class BaseAuthorFormSet(BaseModelFormSet):
2010-09-30 07:12:18 +04:00
def clean(self):
if any(self.errors):
return
2010-10-13 00:53:05 +04:00
# 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)])
2010-09-30 07:12:18 +04:00
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.'))
2010-10-13 00:53:05 +04:00
users = [d['user'] for d in data]
if sorted(users) != sorted(set(users)):
raise forms.ValidationError(
_('An author can only be listed once.'))
2010-09-30 07:12:18 +04:00
AuthorFormSet = modelformset_factory(AddonUser, formset=BaseAuthorFormSet,
form=AuthorForm, can_delete=True, extra=0)
2010-10-05 23:24:31 +04:00
2010-11-15 21:56:20 +03:00
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
2010-10-05 23:24:31 +04:00
class Meta:
model = License
fields = ('builtin', 'name', 'text')
2010-10-05 23:24:31 +04:00
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
2010-10-07 02:37:36 +04:00
2010-12-27 19:20:55 +03:00
class PolicyForm(TranslationFormMixin, AMOModelForm):
2010-10-07 02:37:36 +04:00
"""Form for editing the add-ons EULA and privacy policy."""
2010-10-09 06:25:15 +04:00
has_eula = forms.BooleanField(required=False,
label=_lazy(u'This add-on has an End User License Agreement'))
2010-12-27 19:20:55 +03:00
eula = TransField(widget=TransTextarea(), required=False,
label=_lazy(u"Please specify your add-on's "
2010-11-19 06:31:05 +03:00
"End User License Agreement:"))
has_priv = forms.BooleanField(
required=False, label=_lazy(u"This add-on has a Privacy Policy"))
2010-12-27 19:20:55 +03:00
privacy_policy = TransField(widget=TransTextarea(), required=False,
label=_lazy(u"Please specify your add-on's Privacy Policy:"))
2010-10-07 02:37:36 +04:00
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)))
2010-10-07 02:37:36 +04:00
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')):
2010-10-07 02:37:36 +04:00
if not self.cleaned_data[k]:
delete_translation(self.instance, field)
2010-10-08 07:08:41 +04:00
if 'privacy_policy' in self.changed_data:
amo.log(amo.LOG.CHANGE_POLICY, self.addon, self.instance)
return ob
2010-10-08 07:08:41 +04:00
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?")
2010-10-11 02:51:45 +04:00
2010-11-30 02:31:53 +03:00
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)
2010-10-11 02:51:45 +04:00
2010-10-08 07:08:41 +04:00
class CharityForm(happyforms.ModelForm):
2010-12-31 00:30:32 +03:00
url = Charity._meta.get_field('url').formfield(verify_exists=False)
2010-10-08 07:08:41 +04:00
class Meta:
model = Charity
2012-03-06 22:41:21 +04:00
fields = ('name', 'url', 'paypal')
2010-10-08 07:08:41 +04:00
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)
2010-11-30 02:31:53 +03:00
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')))
2010-10-08 07:08:41 +04:00
recipient = forms.ChoiceField(choices=RECIPIENTS,
widget=forms.RadioSelect(attrs={'class': 'recipient'}))
2010-12-02 04:27:30 +03:00
thankyou_note = TransField(widget=TransTextarea(), required=False)
2010-10-08 07:08:41 +04:00
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'})
}
2010-10-08 07:08:41 +04:00
@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)
2010-12-02 04:27:30 +03:00
# thankyou_note is a dict since it's a Translation.
if not (data.get('enable_thankyou') and
any(data.get('thankyou_note').values())):
2010-11-30 02:31:53 +03:00
data['thankyou_note'] = {}
data['enable_thankyou'] = False
return data
2010-10-08 07:08:41 +04:00
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
2010-10-08 07:08:41 +04:00
def check_paypal_id(paypal_id):
2010-10-08 07:08:41 +04:00
if not paypal_id:
raise forms.ValidationError(
_('PayPal ID required to accept contributions.'))
2010-10-11 22:24:41 +04:00
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)
2010-12-29 03:51:43 +03:00
approvalnotes = forms.CharField(
widget=TranslationTextarea(attrs={'rows': 4}), required=False)
class Meta:
model = Version
fields = ('releasenotes', 'approvalnotes')
2010-10-19 04:16:43 +04:00
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)]
2010-10-19 04:16:43 +04:00
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.'))
2010-10-19 04:16:43 +04:00
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)])
2010-10-19 04:16:43 +04:00
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)
2010-10-21 21:05:30 +04:00
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())
2010-11-23 04:45:12 +03:00
def clean(self):
if not self.errors:
xpi = parse_addon(self.cleaned_data['upload'])
addons.forms.clean_name(xpi['name'])
self._clean_all_platforms()
2010-11-23 04:45:12 +03:00
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.'))
2010-11-23 04:45:12 +03:00
class NewVersionForm(NewAddonForm):
2010-11-23 04:45:12 +03:00
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
2010-10-21 21:05:30 +04:00
class FileForm(happyforms.ModelForm):
platform = File._meta.get_field('platform').formfield(empty_label=None)
class Meta:
model = File
2011-01-05 04:33:24 +03:00
fields = ('platform',)
2010-10-21 21:05:30 +04:00
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
2010-10-21 21:05:30 +04:00
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:
2010-12-21 01:45:02 +03:00
platforms = [f['platform'].id for f in files]
if amo.PLATFORM_ALL.id in platforms and len(files) > 1:
raise forms.ValidationError(
2011-04-14 21:47:51 +04:00
_('The platform All cannot be combined '
2010-12-21 01:45:02 +03:00
'with specific platforms.'))
if sorted(platforms) != sorted(set(platforms)):
raise forms.ValidationError(
_('A platform can only be chosen once.'))
2010-10-21 21:05:30 +04:00
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)]
2011-01-11 21:54:42 +03:00
review_type = forms.TypedChoiceField(
choices=_choices, widget=forms.HiddenInput,
2011-01-11 21:54:42 +03:00
coerce=int, empty_value=None,
2011-06-17 11:29:57 +04:00
error_messages={'required': _lazy(u'A review type must be selected.')})
class Step3Form(addons.forms.AddonFormBasic):
description = TransField(widget=TransTextarea, required=False)
2010-12-31 04:02:31 +03:00
class Meta:
model = Addon
fields = ('name', 'slug', 'summary', 'tags', 'description',
'homepage', 'support_email', 'support_url')
2010-12-31 04:02:31 +03:00
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)
2010-12-31 04:02:31 +03:00
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
2011-01-06 02:29:35 +03:00
if self.cleaned_data.get('DELETE'):
# Existing preview.
if self.instance.id:
self.instance.delete()
# User has no desire to save this preview.
return
2010-12-31 04:02:31 +03:00
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])
2010-12-31 04:02:31 +03:00
class Meta:
model = Preview
2011-01-11 22:53:03 +03:00
fields = ('caption', 'file_upload', 'upload_hash', 'id', 'position')
2010-12-31 04:02:31 +03:00
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))
2011-05-26 03:44:34 +04:00
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.'))
2011-05-26 03:44:34 +04:00
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):
2011-05-26 03:44:34 +04:00
raise forms.ValidationError(
_('Add-on names should not contain Mozilla trademarks.'))
return name
2011-05-26 03:44:34 +04:00
def clean_package_name(self):
slug = self.cleaned_data['package_name']
2011-10-04 06:20:46 +04:00
if slugify(slug, ok='_', lower=False, delimiter='_') != slug:
2011-10-02 10:08:10 +04:00
raise forms.ValidationError(
_('Enter a valid package name consisting of letters, numbers, '
'or underscores.'))
if Addon.objects.filter(slug=slug).exists():
2011-10-02 10:08:10 +04:00
raise forms.ValidationError(
_('This package name is already in use.'))
if BlacklistedSlug.blocked(slug):
2011-10-02 10:08:10 +04:00
raise forms.ValidationError(
_(u'The package name cannot be: %s.' % slug))
return slug
2011-05-26 03:44:34 +04:00
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 '
2011-05-26 03:44:34 +04:00
'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.'))
2011-05-26 03:44:34 +04:00
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'))
2011-05-26 03:44:34 +04:00
max_ver = forms.ModelChoiceField(AppVersion.objects.none(),
empty_label=None, required=False,
label=_lazy(u'Maximum'))
2011-05-26 03:44:34 +04:00
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
2011-05-26 03:44:34 +04:00
# 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']
2011-05-26 03:44:34 +04:00
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.'))
2011-05-26 03:44:34 +04:00
if min_ver.version_int > max_ver.version_int:
raise forms.ValidationError(
2011-05-26 03:44:34 +04:00
_('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
2011-05-26 03:44:34 +04:00
return data
class PackagerCompatBaseFormSet(BaseFormSet):
2011-05-26 03:44:34 +04:00
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.
2011-05-26 03:44:34 +04:00
raise forms.ValidationError(
_(u'{0} is a required target application.')
.format(amo.FIREFOX.pretty))
return self.cleaned_data
2011-05-26 03:44:34 +04:00
PackagerCompatFormSet = formset_factory(PackagerCompatForm,
formset=PackagerCompatBaseFormSet, extra=0)
2011-05-26 03:44:34 +04:00
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'))
2011-05-26 03:44:34 +04:00
preferences_dialog = forms.BooleanField(
required=False,
label=_lazy(u'Preferences dialog'),
help_text=_lazy(u'Creates an example Preferences window'))
2011-05-26 03:44:34 +04:00
toolbar = forms.BooleanField(
required=False,
label=_lazy(u'Toolbar'),
help_text=_lazy(u'Creates an example toolbar for your extension'))
2011-05-26 03:44:34 +04:00
toolbar_button = forms.BooleanField(
required=False,
label=_lazy(u'Toolbar button'),
help_text=_lazy(u'Creates an example button on the browser '
'toolbar'))
2011-05-26 03:44:34 +04:00
main_menu_command = forms.BooleanField(
required=False,
label=_lazy(u'Main menu command'),
help_text=_lazy(u'Creates an item on the Tools menu'))
2011-05-26 03:44:34 +04:00
context_menu_command = forms.BooleanField(
required=False,
label=_lazy(u'Context menu command'),
help_text=_lazy(u'Creates a context menu item for images'))
2011-05-26 03:44:34 +04:00
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:')),
)
2011-12-09 00:19:31 +04:00
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
2011-10-06 22:26:39 +04:00
.exclude(pk=self.addon.pk)
2011-12-09 00:19:31 +04:00
.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']):
2011-12-09 00:19:31 +04:00
# 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)