addons-server/mkt/developers/forms.py

1007 строки
38 KiB
Python

# -*- coding: utf-8 -*-
import json
import os
from datetime import datetime
from urlparse import urlparse, urlunparse
from django import forms
from django.conf import settings
from django.forms.extras.widgets import SelectDateWidget
from django.forms.models import formset_factory, modelformset_factory
from django.template.defaultfilters import filesizeformat
import commonware
import happyforms
import waffle
from product_details import product_details
from quieter_formset.formset import BaseFormSet, BaseModelFormSet
from tower import ugettext as _, ugettext_lazy as _lazy, ungettext as ngettext
import amo
import addons.forms
from access import acl
from addons.forms import icons, IconWidgetRenderer, slug_validator
from addons.models import (Addon, AddonCategory, AddonUser, BlacklistedSlug,
Category, CategorySupervisor, Preview)
from addons.widgets import CategoriesSelectMultiple
from amo import get_user
from amo.fields import SeparatedValuesField
from amo.utils import remove_icons
from files.models import FileUpload
from lib.video import tasks as vtasks
from translations.fields import TransField
from translations.forms import TranslationFormMixin
from translations.models import Translation
from translations.widgets import TransTextarea
import mkt
from mkt.api.models import Access
from mkt.constants import APP_IMAGE_SIZES, MAX_PACKAGED_APP_SIZE
from mkt.constants.ratingsbodies import (ALL_RATINGS, RATINGS_BODIES,
RATINGS_BY_NAME)
from mkt.site.forms import AddonChoiceField
from mkt.webapps.models import (AddonExcludedRegion, ContentRating, ImageAsset,
Webapp)
from . import tasks
log = commonware.log.getLogger('mkt.developers')
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'))
def clean_user(self):
user = self.cleaned_data['user']
if not user.read_dev_agreement:
raise forms.ValidationError(
_('All team members must have read and agreed to the '
'developer agreement.'))
return user
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 team member must be listed.'))
users = [d['user'] for d in data]
if sorted(users) != sorted(set(users)):
raise forms.ValidationError(
_('A team member can only be listed once.'))
AuthorFormSet = modelformset_factory(AddonUser, formset=BaseAuthorFormSet,
form=AuthorForm, can_delete=True, extra=0)
class DeleteForm(happyforms.Form):
reason = forms.CharField(required=False)
def __init__(self, request):
super(DeleteForm, self).__init__(request.POST)
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)
def trap_duplicate(request, manifest_url):
# See if this user has any other apps with the same manifest.
owned = (request.user.get_profile().addonuser_set
.filter(addon__manifest_url=manifest_url))
if not owned:
return
try:
app = owned[0].addon
except Addon.DoesNotExist:
return
error_url = app.get_dev_url()
msg = None
if app.status == amo.STATUS_PUBLIC:
msg = _(u'Oops, looks like you already submitted that manifest '
'for %s, which is currently public. '
'<a href="%s">Edit app</a>')
elif app.status == amo.STATUS_PENDING:
msg = _(u'Oops, looks like you already submitted that manifest '
'for %s, which is currently pending. '
'<a href="%s">Edit app</a>')
elif app.status == amo.STATUS_NULL:
msg = _(u'Oops, looks like you already submitted that manifest '
'for %s, which is currently incomplete. '
'<a href="%s">Resume app</a>')
elif app.status == amo.STATUS_REJECTED:
msg = _(u'Oops, looks like you already submitted that manifest '
'for %s, which is currently rejected. '
'<a href="%s">Edit app</a>')
elif app.status == amo.STATUS_DISABLED:
msg = _(u'Oops, looks like you already submitted that manifest '
'for %s, which is currently disabled by Mozilla. '
'<a href="%s">Edit app</a>')
elif app.disabled_by_user:
msg = _(u'Oops, looks like you already submitted that manifest '
'for %s, which is currently disabled. '
'<a href="%s">Edit app</a>')
if msg:
return msg % (app.name, error_url)
def validate_origin(origin):
"""
Validates that an origin looks like a url.
Returns origin stripped of path, params, query strings, and fragments.
"""
parts = urlparse(origin)
if not parts.scheme.startswith('app'):
raise forms.ValidationError(
_('Origin must start with either "app://".'))
if not '.' in parts.netloc:
raise forms.ValidationError(
_('Origin must be a fully qualified domain name.'))
return urlunparse((parts.scheme, parts.netloc, '', '', '', ''))
def verify_app_domain(manifest_url, exclude=None):
if waffle.switch_is_active('webapps-unique-by-domain'):
domain = Webapp.domain_from_url(manifest_url)
qs = Webapp.objects.filter(app_domain=domain)
if exclude:
qs = qs.exclude(pk=exclude.pk)
if qs.exists():
raise forms.ValidationError(
_('An app already exists on this domain; '
'only one app per domain is allowed.'))
class PreviewForm(happyforms.ModelForm):
caption = TransField(widget=TransTextarea, required=False)
file_upload = forms.FileField(required=False)
upload_hash = forms.CharField(required=False)
# This lets us POST the data URIs of the unsaved previews so we can still
# show them if there were form errors.
unsaved_image_data = forms.CharField(required=False,
widget=forms.HiddenInput)
unsaved_image_type = forms.CharField(required=False,
widget=forms.HiddenInput)
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)
filetype = (os.path.splitext(upload_hash)[1][1:]
.replace('-', '/'))
if filetype in amo.VIDEO_TYPES:
self.instance.update(filetype=filetype)
vtasks.resize_video.delay(upload_path, self.instance,
user=amo.get_user(),
set_modified_on=[self.instance])
else:
self.instance.update(filetype='image/png')
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 ImageAssetForm(happyforms.Form):
file_upload = forms.FileField(required=False)
upload_hash = forms.CharField(required=False)
# This lets us POST the data URIs of the unsaved previews so we can still
# show them if there were form errors.
unsaved_image_data = forms.CharField(required=False,
widget=forms.HiddenInput)
def setup(self, data):
self.size = data.get('size')
self.required = data.get('required')
self.slug = data.get('slug')
self.name = data.get('name')
self.description = data.get('description')
def get_id(self):
return '_'.join(map(str, self.size))
def save(self, addon):
if self.cleaned_data:
if self.cleaned_data['upload_hash']:
if not self.instance:
self.instance, c = ImageAsset.objects.get_or_create(
addon=addon, slug=self.slug)
upload_hash = self.cleaned_data['upload_hash']
upload_path = os.path.join(settings.TMP_PATH, 'image',
upload_hash)
self.instance.update(filetype='image/png')
tasks.resize_imageasset.delay(
upload_path, self.instance.image_path, self.size,
instance=self.instance.pk,
set_modified_on=[self.instance])
def clean(self):
self.cleaned_data = super(ImageAssetForm, self).clean()
if self.required and not self.cleaned_data['upload_hash']:
raise forms.ValidationError(
# L10n: {0} is the name of the image asset type.
_('The {0} image asset is required.').format(self.name))
return self.cleaned_data
class AdminSettingsForm(PreviewForm):
DELETE = forms.BooleanField(required=False)
mozilla_contact = SeparatedValuesField(forms.EmailField, separator=',',
required=False)
app_ratings = forms.MultipleChoiceField(
required=False,
choices=RATINGS_BY_NAME)
class Meta:
model = Preview
fields = ('caption', 'file_upload', 'upload_hash', 'position')
def __init__(self, *args, **kw):
# Get the object for the app's promo `Preview` and pass it to the form.
if kw.get('instance'):
addon = kw.pop('instance')
self.instance = addon
self.promo = addon.get_promo()
# Just consume the request - we don't care.
kw.pop('request', None)
super(AdminSettingsForm, self).__init__(*args, **kw)
if self.instance:
self.initial['mozilla_contact'] = addon.mozilla_contact
rs = []
for r in addon.content_ratings.all():
rating = RATINGS_BODIES[r.ratings_body].ratings[r.rating]
rs.append(ALL_RATINGS.index(rating))
self.initial['app_ratings'] = rs
def clean_caption(self):
return '__promo__'
def clean_position(self):
return -1
def clean_app_ratings(self):
ratings_ids = self.cleaned_data.get('app_ratings')
ratings = [ALL_RATINGS[int(i)] for i in ratings_ids]
ratingsbodies = set([r.ratingsbody for r in ratings])
if len(ratingsbodies) != len(ratings):
raise forms.ValidationError(_('Only one rating from each ratings '
'body may be selected.'))
return ratings_ids
def save(self, addon, commit=True):
if (self.cleaned_data.get('DELETE') and
'upload_hash' not in self.changed_data and self.promo.id):
self.promo.delete()
elif self.promo and 'upload_hash' in self.changed_data:
self.promo.delete()
elif self.cleaned_data.get('upload_hash'):
super(AdminSettingsForm, self).save(addon, True)
contact = self.cleaned_data.get('mozilla_contact')
if contact:
addon.update(mozilla_contact=contact)
ratings = self.cleaned_data.get('app_ratings')
if ratings:
before = set(addon.content_ratings.filter(rating__in=ratings)
.values_list('rating', flat=True))
after = set(int(r) for r in ratings)
addon.content_ratings.exclude(rating__in=after).delete()
for i in after - before:
r = ALL_RATINGS[i]
ContentRating.objects.create(addon=addon, rating=r.id,
ratings_body=r.ratingsbody.id)
else:
addon.content_ratings.all().delete()
uses_flash = self.cleaned_data.get('flash')
af = addon.get_latest_file()
if af is not None:
af.update(uses_flash=bool(uses_flash))
return addon
class BasePreviewFormSet(BaseModelFormSet):
def clean(self):
if any(self.errors):
return
at_least_one = False
for form in self.forms:
if (not form.cleaned_data.get('DELETE') and
form.cleaned_data.get('upload_hash') is not None):
at_least_one = True
if not at_least_one:
raise forms.ValidationError(
_('You must upload at least one screenshot or video.'))
PreviewFormSet = modelformset_factory(Preview, formset=BasePreviewFormSet,
form=PreviewForm, can_delete=True,
extra=1)
class BaseImageAssetFormSet(BaseFormSet):
def __init__(self, *args, **kw):
self.app = kw.pop('app')
super(BaseImageAssetFormSet, self).__init__(*args, **kw)
self.initial = APP_IMAGE_SIZES
# Reconstruct the forms according to the initial data.
self._construct_forms()
for data, form in zip(APP_IMAGE_SIZES, self.forms):
form.setup(data)
form.app = self.app
try:
form.instance = ImageAsset.objects.get(addon=self.app,
slug=form.slug)
except ImageAsset.DoesNotExist:
form.instance = None
def save(self):
for f in self.forms:
f.save(self.app)
ImageAssetFormSet = formset_factory(form=ImageAssetForm,
formset=BaseImageAssetFormSet,
can_delete=False, extra=0)
class NewManifestForm(happyforms.Form):
manifest = forms.URLField(verify_exists=False)
def __init__(self, *args, **kwargs):
self.is_standalone = kwargs.pop('is_standalone', False)
super(NewManifestForm, self).__init__(*args, **kwargs)
def clean_manifest(self):
manifest = self.cleaned_data['manifest']
# Skip checking the domain for the standalone validator.
if not self.is_standalone:
verify_app_domain(manifest)
return manifest
class NewPackagedAppForm(happyforms.Form):
upload = forms.FileField()
def __init__(self, *args, **kwargs):
self.max_size = kwargs.pop('max_size', MAX_PACKAGED_APP_SIZE)
self.user = kwargs.pop('user', get_user())
self.file_upload = None
super(NewPackagedAppForm, self).__init__(*args, **kwargs)
def clean_upload(self):
upload = self.cleaned_data['upload']
if upload.size > self.max_size:
msg = 'Packaged app too large for submission.'
big = json.dumps({
'errors': 1,
'success': False,
'messages': [{
'type': 'error',
'message': [
msg,
'Packages must be less than %s.' %
filesizeformat(self.max_size)],
'tier': 1}]})
# Persist the error with this into FileUpload, but do not persist
# the file contents, which are too large.
self.file_upload = FileUpload.objects.create(
is_webapp=True, user=self.user, validation=big)
# Raise an error so the form is invalid.
raise forms.ValidationError(msg)
else:
self.file_upload = FileUpload.from_post(
upload, upload.name, upload.size, is_webapp=True)
self.file_upload.user = self.user
self.file_upload.save()
class AppFormBasic(addons.forms.AddonFormBase):
"""Form to edit basic app info."""
slug = forms.CharField(max_length=30, widget=forms.TextInput)
manifest_url = forms.URLField(verify_exists=False)
description = TransField(required=True,
label=_lazy(u'Provide a detailed description of your app'),
help_text=_lazy(u'This description will appear on the details page.'),
widget=TransTextarea)
class Meta:
model = Addon
fields = ('slug', 'manifest_url', 'description')
def __init__(self, *args, **kw):
# Force the form to use app_slug if this is a webapp. We want to keep
# this under "slug" so all the js continues to work.
if kw['instance'].is_webapp():
kw.setdefault('initial', {})['slug'] = kw['instance'].app_slug
super(AppFormBasic, self).__init__(*args, **kw)
def _post_clean(self):
# Switch slug to app_slug in cleaned_data and self._meta.fields so
# we can update the app_slug field for webapps.
try:
self._meta.fields = list(self._meta.fields)
slug_idx = self._meta.fields.index('slug')
data = self.cleaned_data
if 'slug' in data:
data['app_slug'] = data.pop('slug')
self._meta.fields[slug_idx] = 'app_slug'
super(AppFormBasic, self)._post_clean()
finally:
self._meta.fields[slug_idx] = 'slug'
def clean_slug(self):
slug = self.cleaned_data['slug']
slug_validator(slug, lower=False)
if slug != self.instance.app_slug:
if Webapp.objects.filter(app_slug=slug).exists():
raise forms.ValidationError(
_('This slug is already in use. Please choose another.'))
if BlacklistedSlug.blocked(slug):
raise forms.ValidationError(_('The slug cannot be "%s". '
'Please choose another.' % slug))
return slug
def clean_manifest_url(self):
manifest_url = self.cleaned_data['manifest_url']
# Only verify if manifest changed.
if 'manifest_url' in self.changed_data:
# Only Admins can edit the manifest_url.
if not acl.action_allowed(self.request, 'Admin', '%'):
return self.instance.manifest_url
verify_app_domain(manifest_url, exclude=self.instance)
return manifest_url
def save(self, addon, commit=False):
# We ignore `commit`, since we need it to be `False` so we can save
# the ManyToMany fields on our own.
addonform = super(AppFormBasic, self).save(commit=False)
addonform.save()
return addonform
class AppFormDetails(addons.forms.AddonFormBase):
default_locale = forms.TypedChoiceField(required=False,
choices=Addon.LOCALES)
homepage = TransField.adapt(forms.URLField)(required=False,
verify_exists=False)
privacy_policy = TransField(widget=TransTextarea(), required=True,
label=_lazy(u"Please specify your app's Privacy Policy"))
class Meta:
model = Addon
fields = ('default_locale', 'homepage', 'privacy_policy')
def clean(self):
# Make sure we have the required translations in the new locale.
required = ['name', 'description']
data = self.cleaned_data
if not self.errors and 'default_locale' in self.changed_data:
fields = dict((k, getattr(self.instance, k + '_id'))
for k in required)
locale = data['default_locale']
ids = filter(None, fields.values())
qs = (Translation.objects.filter(locale=locale, id__in=ids,
localized_string__isnull=False)
.values_list('id', flat=True))
missing = [k for k, v in fields.items() if v not in qs]
if missing:
raise forms.ValidationError(
_('Before changing your default locale you must have a '
'name and description in that locale. '
'You are missing %s.') % ', '.join(map(repr, missing)))
return data
class AppFormMedia(addons.forms.AddonFormBase):
icon_type = forms.CharField(required=False,
widget=forms.RadioSelect(renderer=IconWidgetRenderer, choices=[]))
icon_upload_hash = forms.CharField(required=False)
unsaved_icon_data = forms.CharField(required=False,
widget=forms.HiddenInput)
class Meta:
model = Addon
fields = ('icon_upload_hash', 'icon_type')
def __init__(self, *args, **kwargs):
super(AppFormMedia, self).__init__(*args, **kwargs)
# Add icons here so we only read the directory when
# AppFormMedia is actually being used.
self.fields['icon_type'].widget.choices = icons()
def save(self, addon, commit=True):
if self.cleaned_data['icon_upload_hash']:
upload_hash = self.cleaned_data['icon_upload_hash']
upload_path = os.path.join(settings.TMP_PATH, 'icon', upload_hash)
dirname = addon.get_icon_dir()
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])
return super(AppFormMedia, self).save(commit)
class AppFormSupport(addons.forms.AddonFormBase):
support_url = TransField.adapt(forms.URLField)(required=False,
verify_exists=False)
support_email = TransField.adapt(forms.EmailField)()
class Meta:
model = Addon
fields = ('support_email', 'support_url')
def save(self, addon, commit=True):
i = self.instance
url = addon.support_url.localized_string
(i.get_satisfaction_company,
i.get_satisfaction_product) = addons.forms.get_satisfaction(url)
return super(AppFormSupport, self).save(commit)
class AppAppealForm(happyforms.Form):
"""
If a developer's app is rejected he can make changes and request
another review.
"""
notes = forms.CharField(
label=_lazy(u'Your comments'),
required=False, widget=forms.Textarea(attrs={'rows': 2}))
def __init__(self, *args, **kw):
self.product = kw.pop('product', None)
super(AppAppealForm, self).__init__(*args, **kw)
def save(self):
version = self.product.versions.latest()
notes = self.cleaned_data['notes']
if notes:
amo.log(amo.LOG.WEBAPP_RESUBMIT, self.product, version,
details={'comments': notes})
else:
amo.log(amo.LOG.WEBAPP_RESUBMIT, self.product, version)
# Mark app and file as pending again.
self.product.update(status=amo.WEBAPPS_UNREVIEWED_STATUS)
version.all_files[0].update(status=amo.WEBAPPS_UNREVIEWED_STATUS)
return version
class RegionForm(forms.Form):
regions = forms.MultipleChoiceField(required=False,
label=_lazy(u'Choose the regions your app will be listed in:'),
choices=mkt.regions.REGIONS_CHOICES_NAME[1:],
widget=forms.CheckboxSelectMultiple,
error_messages={'required':
_lazy(u'You must select at least one region.')})
other_regions = forms.BooleanField(required=False, initial=True,
label=_lazy(u'Other and new regions'))
def __init__(self, *args, **kw):
self.product = kw.pop('product', None)
self.request = kw.pop('request', None)
self.region_ids = self.product.get_region_ids()
super(RegionForm, self).__init__(*args, **kw)
is_paid = self._product_is_paid()
# If we have excluded regions, uncheck those.
# Otherwise, default to everything checked.
self.regions_before = self.product.get_region_ids()
# If we have future excluded regions, uncheck box.
self.future_exclusions = self.product.addonexcludedregion.filter(
region=mkt.regions.WORLDWIDE.id)
self.initial = {
'regions': self.regions_before,
'other_regions': not self.future_exclusions.exists() and
not is_paid
}
# Games cannot be listed in Brazil without a content rating.
self.disabled_regions = set()
games = Webapp.category('games')
if (games and
self.product.categories.filter(id=games.id).exists() and
not self.product.content_ratings_in(mkt.regions.BR)):
self.disabled_regions.add(mkt.regions.BR.id)
# If the app is paid, disable regions that use payments.
if is_paid:
self.disabled_regions.add(mkt.regions.WORLDWIDE.id)
self.fields['other_regions'].widget.attrs['disabled'] = 'disabled'
self.fields['other_regions'].label = _(u'Other regions')
self.disabled_regions = list(self.disabled_regions)
def is_toggling(self):
if not self.request or not hasattr(self.request, 'POST'):
return False
value = self.request.POST.get('toggle-paid')
return value if value in ('free', 'paid') else False
def _product_is_paid(self):
return self.product.premium_type in amo.ADDON_PREMIUMS
def has_inappropriate_regions(self):
"""Returns whether the app is listed in regions that it shouldn't
otherwise be registered in."""
inappropriate_regions = (set(mkt.regions.ALL_REGION_IDS)
.difference(self.product.get_possible_price_region_ids())
.union(self.disabled_regions))
return (self._product_is_paid() and
set(self.region_ids).intersection(inappropriate_regions))
def clean(self):
data = self.cleaned_data
if (not data.get('regions') and not data.get('other_regions')
and not self.is_toggling()):
raise forms.ValidationError(
_('You must select at least one region or '
'"Other and new regions."'))
if data.get('regions'):
self.region_ids = [int(r) for r in data['regions']]
if self.has_inappropriate_regions():
raise forms.ValidationError(
_('You have selected a region that is not valid for your '
'price point.'))
return data
def save(self):
# Don't save regions if we are toggling.
if self.is_toggling():
return
before = set(self.regions_before)
after = set(map(int, self.cleaned_data['regions']))
# If the app is paid, disable regions that do not use payments.
if self._product_is_paid():
after &= set(self.product.get_possible_price_region_ids())
# Add new region exclusions.
to_add = before - after
for r in to_add:
g, c = AddonExcludedRegion.objects.get_or_create(
addon=self.product, region=r)
if c:
log.info(u'[Webapp:%s] Excluded from new region (%s).'
% (self.product, r))
# Remove old region exclusions.
to_remove = after - before
for r in to_remove:
self.product.addonexcludedregion.filter(region=r).delete()
log.info(u'[Webapp:%s] No longer exluded from region (%s).'
% (self.product, r))
if self.cleaned_data['other_regions']:
# Developer wants to be visible in future regions, then
# delete excluded regions.
self.future_exclusions.delete()
log.info(u'[Webapp:%s] No longer excluded from future regions.'
% self.product)
else:
# Developer does not want future regions, then
# exclude all future apps.
g, c = AddonExcludedRegion.objects.get_or_create(
addon=self.product, region=mkt.regions.WORLDWIDE.id)
if c:
log.info(u'[Webapp:%s] Excluded from future regions.'
% self.product)
# Disallow games in Brazil without a rating.
games = Webapp.category('games')
if games:
r = mkt.regions.BR
if (self.product.categories.filter(id=games.id) and
self.product.listed_in(r) and
not self.product.content_ratings_in(r)):
g, c = AddonExcludedRegion.objects.get_or_create(
addon=self.product, region=r.id)
if c:
log.info(u'[Webapp:%s] Game excluded from new region '
'(%s).' % (self.product, r.id))
class CategoryForm(happyforms.Form):
categories = forms.ModelMultipleChoiceField(
queryset=Category.objects.filter(
type=amo.ADDON_WEBAPP, region=None, carrier=None),
widget=CategoriesSelectMultiple)
def __init__(self, *args, **kw):
self.request = kw.pop('request', None)
self.product = kw.pop('product', None)
super(CategoryForm, self).__init__(*args, **kw)
supervisor_of = self.special_cats()
self.special_cats = lambda *args: supervisor_of # Caching!
if supervisor_of.exists():
# Update the list of categories with the supervisor categories,
# excluding categories which are region-exclusive to regions that
# the app is unavailable in.
self.fields['categories'].queryset |= supervisor_of
self.cats_before = list(self.product.categories
.values_list('id', flat=True))
self.initial['categories'] = self.cats_before
# If this app is featured, category changes are forbidden.
self.disabled = (
not acl.action_allowed(self.request, 'Addons', 'Edit') and
Webapp.featured(cat=self.cats_before)
)
def special_cats(self):
# Get the list of categories that we're a supervisor for.
sup_of = (CategorySupervisor.objects.filter(user=self.request.user)
.values_list('category_id',
flat=True))
if not sup_of:
# Don't make any more requests if we don't have any permissions.
return Category.objects.none()
# Get the list of regions that the app is excluded from.
exclude_regs = self.product.addonexcludedregion.values_list(
'region', flat=True)
# Find all the supervised categories, excluding categories which are
# unavailable for the current app.
return (Category.objects.filter(id__in=sup_of)
.exclude(region__in=exclude_regs))
def max_categories(self):
return amo.MAX_CATEGORIES + self.special_cats().count()
def clean_categories(self):
if self.disabled:
raise forms.ValidationError(
_('Categories cannot be changed while your app is featured.'))
categories = self.cleaned_data['categories']
set_categories = set(categories.values_list('id', flat=True))
# Supervisored categories don't count towards the max, so subtract
# them out if there are any.
supervisor_of = self.special_cats()
if supervisor_of.exists():
set_categories -= set(supervisor_of.values_list('id', flat=True))
total = len(set_categories)
max_cat = amo.MAX_CATEGORIES
if total > max_cat:
# L10n: {0} is the number of categories.
raise forms.ValidationError(ngettext(
'You can have only {0} category.',
'You can have only {0} categories.',
max_cat).format(max_cat))
return categories
def save(self):
after = list(self.cleaned_data['categories']
.values_list('id', flat=True))
before = self.cats_before
# Add new categories.
to_add = set(after) - set(before)
for c in to_add:
AddonCategory.objects.create(addon=self.product, category_id=c)
# Remove old categories.
to_remove = set(before) - set(after)
for c in to_remove:
self.product.addoncategory_set.filter(category=c).delete()
# Disallow games in Brazil without a rating.
games = Webapp.category('games')
if (games and self.product.listed_in(mkt.regions.BR) and
not self.product.content_ratings_in(mkt.regions.BR)):
r = mkt.regions.BR.id
if games.id in to_add:
g, c = AddonExcludedRegion.objects.get_or_create(
addon=self.product, region=r)
if c:
log.info(u'[Webapp:%s] Game excluded from new region '
'(%s).' % (self.product, r))
elif games.id in to_remove:
self.product.addonexcludedregion.filter(region=r).delete()
log.info(u'[Webapp:%s] Game no longer exluded from region '
'(%s).' % (self.product, r))
class DevAgreementForm(happyforms.Form):
read_dev_agreement = forms.BooleanField(label=_lazy(u'Agree'),
widget=forms.HiddenInput)
def __init__(self, *args, **kw):
self.instance = kw.pop('instance')
super(DevAgreementForm, self).__init__(*args, **kw)
def save(self):
self.instance.read_dev_agreement = datetime.now()
self.instance.save()
class DevNewsletterForm(happyforms.Form):
"""Devhub newsletter subscription form."""
email = forms.EmailField(
error_messages={'required':
_lazy(u'Please enter a valid email address.')},
widget=forms.TextInput(attrs={'required': '',
'placeholder':
_lazy(u'Your email address')}))
email_format = forms.ChoiceField(
widget=forms.RadioSelect(),
choices=(('H', 'HTML'), ('T', _lazy(u'Text'))),
initial='H')
privacy = forms.BooleanField(
error_messages={'required':
_lazy(u'You must agree to the Privacy Policy.')})
country = forms.ChoiceField(label=_lazy(u'Country'))
def __init__(self, locale, *args, **kw):
regions = product_details.get_regions(locale)
regions = sorted(regions.iteritems(), key=lambda x: x[1])
super(DevNewsletterForm, self).__init__(*args, **kw)
self.fields['country'].choices = regions
self.fields['country'].initial = 'us'
class AppFormTechnical(addons.forms.AddonFormBase):
flash = forms.BooleanField(required=False)
class Meta:
model = Addon
fields = 'public_stats',
def __init__(self, *args, **kw):
super(AppFormTechnical, self).__init__(*args, **kw)
self.initial['flash'] = self.instance.uses_flash
def save(self, addon, commit=False):
uses_flash = self.cleaned_data.get('flash')
af = self.instance.get_latest_file()
if af is not None:
af.update(uses_flash=bool(uses_flash))
return super(AppFormTechnical, self).save(commit=True)
class TransactionFilterForm(happyforms.Form):
app = AddonChoiceField(queryset=None, required=False, label=_lazy(u'App'))
transaction_type = forms.ChoiceField(
required=False, label=_lazy(u'Transaction Type'),
choices=[(None, '')] + amo.MKT_TRANSACTION_CONTRIB_TYPES.items())
transaction_id = forms.CharField(
required=False, label=_lazy(u'Transaction ID'))
current_year = datetime.today().year
years = [current_year - x for x in range(current_year - 2012)]
date_from = forms.DateTimeField(
required=False, widget=SelectDateWidget(years=years),
label=_lazy(u'From'))
date_to = forms.DateTimeField(
required=False, widget=SelectDateWidget(years=years),
label=_lazy(u'To'))
def __init__(self, *args, **kwargs):
self.apps = kwargs.pop('apps', [])
super(TransactionFilterForm, self).__init__(*args, **kwargs)
self.fields['app'].queryset = self.apps
class APIConsumerForm(happyforms.ModelForm):
app_name = forms.CharField(required=True)
redirect_uri = forms.CharField(required=True)
class Meta:
model = Access
fields = ('app_name', 'redirect_uri')