addons-server/apps/devhub/views.py

1837 строки
69 KiB
Python

import collections
import functools
import json
import os
import sys
import time
import traceback
import uuid
from django import http
from django.core.exceptions import PermissionDenied
from django.core.files.storage import default_storage as storage
from django.conf import settings
from django import forms as django_forms
from django.db import transaction
from django.db.models import Count
from django.shortcuts import get_object_or_404, redirect
from django.utils.http import urlquote
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_view_exempt
import commonware.log
import jingo
from PIL import Image
from session_csrf import anonymous_csrf
from tower import ugettext_lazy as _lazy, ugettext as _
import waffle
from waffle.decorators import waffle_switch
from applications.models import Application, AppVersion
import amo
import amo.utils
from amo import messages
from amo.decorators import json_view, login_required, post_required, write
from amo.helpers import loc, urlparams
from amo.utils import escape_all, HttpResponseSendFile, MenuItem
from amo.urlresolvers import reverse
from access import acl
from addons import forms as addon_forms
from addons.decorators import addon_view, can_become_premium
from addons.models import Addon, AddonUser
from addons.views import BaseFilter
from devhub.decorators import dev_required
from devhub.forms import CheckCompatibilityForm
from devhub.models import ActivityLog, BlogPost, RssKey, SubmitStep
from devhub import perf
from editors.helpers import get_position
from files.models import File, FileUpload, Platform
from files.utils import parse_addon
from market.models import AddonPremium, Refund
from paypal.check import Check
import paypal
from search.views import BaseAjaxSearch
from stats.models import Contribution
from translations.models import delete_translation
from users.models import UserProfile
from versions.models import Version
from mkt.webapps.models import Webapp
from zadmin.models import ValidationResult
from . import forms, tasks, feeds, signals
log = commonware.log.getLogger('z.devhub')
paypal_log = commonware.log.getLogger('z.paypal')
# We use a session cookie to make sure people see the dev agreement.
DEV_AGREEMENT_COOKIE = 'yes-I-read-the-dev-agreement'
class AddonFilter(BaseFilter):
opts = (('name', _lazy(u'Name')),
('updated', _lazy(u'Updated')),
('created', _lazy(u'Created')),
('popular', _lazy(u'Downloads')),
('rating', _lazy(u'Rating')))
class AppFilter(BaseFilter):
opts = (('name', _lazy(u'Name')),
('created', _lazy(u'Created')),
('downloads', _lazy(u'Weekly Downloads')),
('rating', _lazy(u'Rating')))
def addon_listing(request, default='name', webapp=False):
"""Set up the queryset and filtering for addon listing for Dashboard."""
Filter = AppFilter if webapp else AddonFilter
if webapp:
qs = Webapp.objects.filter(
id__in=request.amo_user.addons.filter(type=amo.ADDON_WEBAPP))
model = Webapp
else:
qs = request.amo_user.addons.exclude(type=amo.ADDON_WEBAPP)
model = Addon
filter = Filter(request, qs, 'sort', default, model=model)
return filter.qs, filter
def index(request):
if settings.APP_PREVIEW:
# This can be a permanent redirect when we finalize devhub for apps.
return redirect('devhub.apps')
ctx = {'blog_posts': _get_posts()}
if request.amo_user:
user_addons = request.amo_user.addons.exclude(type=amo.ADDON_WEBAPP)
recent_addons = user_addons.order_by('-modified')[:3]
ctx['recent_addons'] = []
for addon in recent_addons:
ctx['recent_addons'].append({'addon': addon,
'position': get_position(addon)})
return jingo.render(request, 'devhub/index.html', ctx)
@login_required
def dashboard(request, webapp=False):
addons, filter = addon_listing(request, webapp=webapp)
addons = amo.utils.paginate(request, addons, per_page=10)
blog_posts = _get_posts()
all_addons = request.amo_user.addons.exclude(type=amo.ADDON_WEBAPP)
data = dict(addons=addons, sorting=filter.field, filter=filter,
items=_get_items(None, all_addons)[:4],
sort_opts=filter.opts, rss=_get_rss_feed(request),
blog_posts=blog_posts, timestamp=int(time.time()),
webapp=webapp)
return jingo.render(request, 'devhub/addons/dashboard.html', data)
@dev_required
def ajax_compat_status(request, addon_id, addon):
if not (addon.accepts_compatible_apps() and addon.current_version):
raise http.Http404()
return jingo.render(request, 'devhub/addons/ajax_compat_status.html',
dict(addon=addon))
@dev_required
def ajax_compat_error(request, addon_id, addon):
if not (addon.accepts_compatible_apps() and addon.current_version):
raise http.Http404()
return jingo.render(request, 'devhub/addons/ajax_compat_error.html',
dict(addon=addon))
@dev_required
def ajax_compat_update(request, addon_id, addon, version_id):
if not addon.accepts_compatible_apps():
raise http.Http404()
version = get_object_or_404(Version, pk=version_id, addon=addon)
compat_form = forms.CompatFormSet(request.POST or None,
queryset=version.apps.all())
if request.method == 'POST' and compat_form.is_valid():
for compat in compat_form.save(commit=False):
compat.version = version
compat.save()
for form in compat_form.forms:
if (isinstance(form, forms.CompatForm) and
'max' in form.changed_data):
_log_max_version_change(addon, version, form.instance)
return jingo.render(request, 'devhub/addons/ajax_compat_update.html',
dict(addon=addon, version=version,
compat_form=compat_form))
def _get_addons(request, addons, addon_id, action):
"""Create a list of ``MenuItem``s for the activity feed."""
items = []
a = MenuItem()
a.selected = (not addon_id)
(a.text, a.url) = (_('All My Add-ons'), reverse('devhub.feed_all'))
if action:
a.url += '?action=' + action
items.append(a)
for addon in addons:
item = MenuItem()
try:
item.selected = (addon_id and addon.id == int(addon_id))
except ValueError:
pass # We won't get here... EVER
url = reverse('devhub.feed', args=[addon.slug])
if action:
url += '?action=' + action
item.text, item.url = addon.name, url
items.append(item)
return items
def _get_posts(limit=5):
return BlogPost.objects.order_by('-date_posted')[0:limit]
def _get_activities(request, action):
url = request.get_full_path()
choices = (None, 'updates', 'status', 'collections', 'reviews')
text = {None: _('All Activity'),
'updates': _('Add-on Updates'),
'status': _('Add-on Status'),
'collections': _('User Collections'),
'reviews': _('User Reviews'),
}
items = []
for c in choices:
i = MenuItem()
i.text = text[c]
i.url, i.selected = urlparams(url, page=None, action=c), (action == c)
items.append(i)
return items
def _get_items(action, addons):
filters = dict(updates=(amo.LOG.ADD_VERSION, amo.LOG.ADD_FILE_TO_VERSION),
status=(amo.LOG.USER_DISABLE, amo.LOG.USER_ENABLE,
amo.LOG.CHANGE_STATUS, amo.LOG.APPROVE_VERSION,),
collections=(amo.LOG.ADD_TO_COLLECTION,
amo.LOG.REMOVE_FROM_COLLECTION,),
reviews=(amo.LOG.ADD_REVIEW,))
filter = filters.get(action)
items = (ActivityLog.objects.for_addons(addons).filter()
.exclude(action__in=amo.LOG_HIDE_DEVELOPER))
if filter:
items = items.filter(action__in=[i.id for i in filter])
return items
def _get_rss_feed(request):
key, __ = RssKey.objects.get_or_create(user=request.amo_user)
return urlparams(reverse('devhub.feed_all'), privaterss=key.key)
def feed(request, addon_id=None):
if request.GET.get('privaterss'):
return feeds.ActivityFeedRSS()(request)
addon_selected = None
if not request.user.is_authenticated():
url = reverse('users.login')
p = urlquote(request.get_full_path())
return http.HttpResponseRedirect('%s?to=%s' % (url, p))
else:
# We exclude apps on AMO.
addons_all = request.amo_user.addons.exclude(type=amo.ADDON_WEBAPP)
if addon_id:
addon = get_object_or_404(Addon.objects.id_or_slug(addon_id))
addons = addon # common query set
try:
key = RssKey.objects.get(addon=addons)
except RssKey.DoesNotExist:
key = RssKey.objects.create(addon=addons)
addon_selected = addon.id
rssurl = urlparams(reverse('devhub.feed', args=[addon_id]),
privaterss=key.key)
if not acl.check_addon_ownership(request, addons, viewer=True,
ignore_disabled=True):
raise PermissionDenied
else:
rssurl = _get_rss_feed(request)
addon = None
addons = addons_all
action = request.GET.get('action')
items = _get_items(action, addons)
activities = _get_activities(request, action)
addon_items = _get_addons(request, addons_all, addon_selected, action)
pager = amo.utils.paginate(request, items, 20)
data = dict(addons=addon_items, pager=pager, activities=activities,
rss=rssurl, addon=addon)
return jingo.render(request, 'devhub/addons/activity.html', data)
@dev_required(webapp=True)
def edit(request, addon_id, addon, webapp=False):
data = {
'page': 'edit',
'addon': addon,
'webapp': webapp,
'valid_slug': addon.slug,
'tags': addon.tags.not_blacklisted().values_list('tag_text', flat=True),
'previews': addon.previews.all(),
}
if webapp and waffle.switch_is_active('marketplace'):
data['device_type_form'] = addon_forms.DeviceTypeForm(
request.POST or None, addon=addon)
if (not webapp and
acl.action_allowed(request, 'Addons', 'Configure')):
data['admin_form'] = forms.AdminForm(instance=addon)
return jingo.render(request, 'devhub/addons/edit.html', data)
@dev_required(owner_for_post=True, webapp=True)
def delete(request, addon_id, addon, webapp=False):
# Database deletes only allowed for free or incomplete addons.
if not addon.can_be_deleted():
if webapp:
msg = loc('App cannot be deleted. Disable this app instead.')
else:
msg = _('Add-on cannot be deleted. Disable this add-on instead.')
messages.error(request, msg)
return redirect(addon.get_dev_url('versions'))
form = forms.DeleteForm(request)
if form.is_valid():
addon.delete('Removed via devhub')
messages.success(request,
loc('App deleted.') if webapp else _('Add-on deleted.'))
return redirect('devhub.%s' % ('apps' if webapp else 'addons'))
else:
if webapp:
msg = loc('Password was incorrect. App was not deleted.')
else:
msg = _('Password was incorrect. Add-on was not deleted.')
messages.error(request, msg)
return redirect(addon.get_dev_url('versions'))
@dev_required
def enable(request, addon_id, addon):
addon.update(disabled_by_user=False)
amo.log(amo.LOG.USER_ENABLE, addon)
return redirect(addon.get_dev_url('versions'))
@dev_required(owner_for_post=True)
def cancel(request, addon_id, addon):
if addon.status in amo.STATUS_UNDER_REVIEW:
if addon.status == amo.STATUS_LITE_AND_NOMINATED:
addon.update(status=amo.STATUS_LITE)
else:
addon.update(status=amo.STATUS_NULL)
amo.log(amo.LOG.CHANGE_STATUS, addon.get_status_display(), addon)
return redirect(addon.get_dev_url('versions'))
@dev_required
@post_required
def disable(request, addon_id, addon):
addon.update(disabled_by_user=True)
amo.log(amo.LOG.USER_DISABLE, addon)
return redirect(addon.get_dev_url('versions'))
@dev_required(owner_for_post=True, webapp=True)
def ownership(request, addon_id, addon, webapp=False):
fs, ctx = [], {}
# Authors.
qs = AddonUser.objects.filter(addon=addon).order_by('position')
user_form = forms.AuthorFormSet(request.POST or None, queryset=qs)
fs.append(user_form)
# Versions.
license_form = forms.LicenseForm(request.POST or None, addon=addon)
if not addon.is_webapp():
ctx.update(license_form.get_context())
if ctx['license_form']: # if addon has a version
fs.append(ctx['license_form'])
# Policy.
policy_form = forms.PolicyForm(request.POST or None, addon=addon)
if not addon.is_webapp():
ctx.update(policy_form=policy_form)
fs.append(policy_form)
if request.method == 'POST' and all([form.is_valid() for form in fs]):
# Authors.
authors = user_form.save(commit=False)
for author in authors:
action = None
if not author.id or author.user_id != author._original_user_id:
action = amo.LOG.ADD_USER_WITH_ROLE
author.addon = addon
elif author.role != author._original_role:
action = amo.LOG.CHANGE_USER_WITH_ROLE
author.save()
if action:
amo.log(action, author.user, author.get_role_display(), addon)
if (author._original_user_id and
author.user_id != author._original_user_id):
amo.log(amo.LOG.REMOVE_USER_WITH_ROLE,
(UserProfile, author._original_user_id),
author.get_role_display(), addon)
for author in user_form.deleted_objects:
amo.log(amo.LOG.REMOVE_USER_WITH_ROLE, author.user,
author.get_role_display(), addon)
if license_form in fs:
license_form.save()
if policy_form in fs:
policy_form.save()
messages.success(request, _('Changes successfully saved.'))
return redirect(addon.get_dev_url('owner'))
ctx.update(addon=addon, webapp=webapp, user_form=user_form)
return jingo.render(request, 'devhub/addons/owner.html', ctx)
@dev_required(owner_for_post=True, webapp=True)
def payments(request, addon_id, addon, webapp=False):
if addon.is_premium():
return _premium(request, addon_id, addon, webapp)
return _voluntary(request, addon_id, addon, webapp)
def _premium(request, addon_id, addon, webapp=False):
premium_form = forms.PremiumForm(request.POST or None,
request=request,
extra={'addon': addon,
'amo_user': request.amo_user,
'dest': 'payment'})
if request.method == 'POST' and premium_form.is_valid():
premium_form.save()
messages.success(request, _('Changes successfully saved.'))
return redirect(addon.get_dev_url('payments'))
return jingo.render(request, 'devhub/payments/premium.html',
dict(addon=addon, webapp=webapp, premium=addon.premium,
form=premium_form))
def _voluntary(request, addon_id, addon, webapp):
charity = None if addon.charity_id == amo.FOUNDATION_ORG else addon.charity
charity_form = forms.CharityForm(request.POST or None, instance=charity,
prefix='charity')
contrib_form = forms.ContribForm(request.POST or None, instance=addon,
initial=forms.ContribForm.initial(addon))
profile_form = forms.ProfileForm(request.POST or None, instance=addon,
required=True)
if request.method == 'POST':
if contrib_form.is_valid():
addon = contrib_form.save(commit=False)
addon.wants_contributions = True
valid = _save_charity(addon, contrib_form, charity_form)
if not addon.has_full_profile():
valid &= profile_form.is_valid()
if valid:
profile_form.save()
if valid:
addon.save()
messages.success(request, _('Changes successfully saved.'))
amo.log(amo.LOG.EDIT_CONTRIBUTIONS, addon)
return redirect(addon.get_dev_url('payments'))
errors = charity_form.errors or contrib_form.errors or profile_form.errors
if errors:
messages.error(request, _('There were errors in your submission.'))
return jingo.render(request, 'devhub/payments/payments.html',
dict(addon=addon, webapp=webapp, errors=errors,
charity_form=charity_form, contrib_form=contrib_form,
profile_form=profile_form))
def _save_charity(addon, contrib_form, charity_form):
recipient = contrib_form.cleaned_data['recipient']
if recipient == 'dev':
addon.charity = None
elif recipient == 'moz':
addon.charity_id = amo.FOUNDATION_ORG
elif recipient == 'org':
if charity_form.is_valid():
addon.charity = charity_form.save()
else:
return False
return True
@write
@dev_required(webapp=True)
def acquire_refund_permission(request, addon_id, addon, webapp=False):
"""This is the callback from Paypal."""
paypal_log.debug('User approved refund for addon: %s' % addon_id)
token = paypal.get_permissions_token(request.GET['request_token'],
request.GET['verification_code'])
paypal_log.debug('Got refund token for addon: %s, token: %s....' %
(addon_id, token[:10]))
# Sadly this is an update on a GET.
addonpremium, created = (AddonPremium.objects
.safer_get_or_create(addon=addon))
paypal_log.debug('AddonPremium %s for: %s' %
('created' if created else 'updated', addon.pk))
addonpremium.update(paypal_permissions_token=token)
paypal_log.debug('AddonPremium saved with token: %s' % addonpremium.pk)
amo.log(amo.LOG.EDIT_PROPERTIES, addon)
dest = 'payments'
if request.GET.get('dest') == 'wizard':
dest = 'market.1'
return redirect(addon.get_dev_url(dest))
@waffle_switch('allow-refund')
@dev_required(webapp=True)
def issue_refund(request, addon_id, addon, webapp=False):
txn_id = request.REQUEST.get('transaction_id')
if not txn_id:
raise http.Http404
form_enabled = True
contribution = get_object_or_404(Contribution, transaction_id=txn_id,
type=amo.CONTRIB_PURCHASE)
if Refund.objects.filter(contribution=contribution).exists():
messages.error(request, _('Refund already processed.'))
form_enabled = False
elif request.method == 'POST':
if 'issue' in request.POST:
try:
results = paypal.refund(contribution.paykey)
except paypal.PaypalError, e:
messages.error(request, _('Refund failed. error: %s') % e)
contribution.record_failed_refund(e)
else:
for res in results:
if res['refundStatus'] == 'ALREADY_REVERSED_OR_REFUNDED':
paypal_log.debug(
'Refund attempt for already-refunded paykey: %s, %s'
% (contribution.paykey, res['receiver.email']))
messages.error(request,
_('Refund was previously issued; '
'no action taken.'))
return redirect(addon.get_dev_url('refunds'))
contribution.mail_approved()
refund = contribution.enqueue_refund(amo.REFUND_APPROVED)
paypal_log.info('Refund %r issued for contribution %r' %
(refund.pk, contribution.pk))
messages.success(request, _('Refund issued.'))
else:
contribution.mail_declined()
# TODO: Consider requiring a rejection reason for declined refunds.
refund = contribution.enqueue_refund(amo.REFUND_DECLINED)
paypal_log.info('Refund %r declined for contribution %r' %
(refund.pk, contribution.pk))
messages.success(request, _('Refund declined.'))
return redirect(addon.get_dev_url('refunds'))
return jingo.render(request, 'devhub/payments/issue-refund.html',
{'enabled': form_enabled,
'contribution': contribution,
'addon': addon,
'webapp': webapp,
'transaction_id': txn_id})
@waffle_switch('allow-refund')
@dev_required(webapp=True)
# TODO: Make sure 'Support' staff can access this.
def refunds(request, addon_id, addon, webapp=False):
ctx = {'addon': addon, 'webapp': webapp}
queues = {
'pending': Refund.objects.pending(addon),
'approved': Refund.objects.approved(addon),
'instant': Refund.objects.instant(addon),
'declined': Refund.objects.declined(addon),
}
# For now set the limit to something stupid so this is stupid easy to QA.
for status, refunds in queues.iteritems():
ctx[status] = amo.utils.paginate(request, refunds, per_page=5)
return jingo.render(request, 'devhub/payments/refunds.html', ctx)
@dev_required
@post_required
def disable_payments(request, addon_id, addon):
addon.update(wants_contributions=False)
return redirect(addon.get_dev_url('payments'))
@dev_required(webapp=True)
@post_required
def remove_profile(request, addon_id, addon, webapp=False):
delete_translation(addon, 'the_reason')
delete_translation(addon, 'the_future')
if addon.wants_contributions:
addon.update(wants_contributions=False)
return redirect(addon.get_dev_url('profile'))
@dev_required(webapp=True)
def profile(request, addon_id, addon, webapp=False):
profile_form = forms.ProfileForm(request.POST or None, instance=addon)
if request.method == 'POST' and profile_form.is_valid():
profile_form.save()
amo.log(amo.LOG.EDIT_PROPERTIES, addon)
messages.success(request, _('Changes successfully saved.'))
return redirect(addon.get_dev_url('profile'))
return jingo.render(request, 'devhub/addons/profile.html',
dict(addon=addon, webapp=webapp,
profile_form=profile_form))
@login_required
@post_required
@json_view
def compat_application_versions(request):
app_id = request.POST['application_id']
f = CheckCompatibilityForm()
return {'choices': f.version_choices_for_app_id(app_id)}
@login_required
def validate_addon(request):
return jingo.render(request, 'devhub/validate_addon.html',
{'title': _('Validate Add-on'),
'upload_url': reverse('devhub.standalone_upload')})
@login_required
def check_addon_compatibility(request):
form = CheckCompatibilityForm()
return jingo.render(request, 'devhub/validate_addon.html',
{'appversion_form': form,
'title': _('Check Add-on Compatibility'),
'upload_url': reverse('devhub.standalone_upload')})
@dev_required
@json_view
def file_perf_tests_start(request, addon_id, addon, file_id):
if not waffle.flag_is_active(request, 'perf-tests'):
raise PermissionDenied
file_ = get_object_or_404(File, pk=file_id)
plats = perf.PLATFORM_MAP.get(file_.platform.id, None)
if plats is None:
log.info('Unsupported performance platform %s for file %s'
% (file_.platform.id, file_))
# TODO(Kumar) provide a message about this
return {'success': False}
for app in perf.ALL_APPS:
for plat in plats:
tasks.start_perf_test_for_file.delay(file_.id, plat, app)
return {'success': True}
def packager_path(name):
return os.path.join(settings.PACKAGER_PATH, '%s.zip' % name)
@anonymous_csrf
def package_addon(request):
basic_form = forms.PackagerBasicForm(request.POST or None)
features_form = forms.PackagerFeaturesForm(request.POST or None)
compat_forms = forms.PackagerCompatFormSet(request.POST or None)
# Process requests, but also avoid short circuiting by using all().
if (request.method == 'POST' and
all([basic_form.is_valid(),
features_form.is_valid(),
compat_forms.is_valid()])):
basic_data = basic_form.cleaned_data
compat_data = compat_forms.cleaned_data
data = {'id': basic_data['id'],
'version': basic_data['version'],
'name': basic_data['name'],
'slug': basic_data['package_name'],
'description': basic_data['description'],
'author_name': basic_data['author_name'],
'contributors': basic_data['contributors'],
'targetapplications': [c for c in compat_data if c['enabled']]}
tasks.packager.delay(data, features_form.cleaned_data)
return redirect('devhub.package_addon_success',
basic_data['package_name'])
return jingo.render(request, 'devhub/package_addon.html',
{'basic_form': basic_form,
'compat_forms': compat_forms,
'features_form': features_form})
def package_addon_success(request, package_name):
"""Return the success page for the add-on packager."""
return jingo.render(request, 'devhub/package_addon_success.html',
{'package_name': package_name})
@json_view
def package_addon_json(request, package_name):
"""Return the URL of the packaged add-on."""
path_ = packager_path(package_name)
if storage.exists(path_):
url = reverse('devhub.package_addon_download', args=[package_name])
return {'download_url': url, 'filename': os.path.basename(path_),
'size': round(storage.open(path_).size / 1024, 1)}
def package_addon_download(request, package_name):
"""Serve a packaged add-on."""
path_ = packager_path(package_name)
if not storage.exists(path_):
raise http.Http404()
return HttpResponseSendFile(request, path_, content_type='application/zip')
@login_required
@post_required
def upload(request, addon_slug=None, is_standalone=False):
filedata = request.FILES['upload']
fu = FileUpload.from_post(filedata, filedata.name, filedata.size)
log.info('FileUpload created: %s' % fu.pk)
if request.user.is_authenticated():
fu.user = request.amo_user
fu.save()
if request.POST.get('app_id') and request.POST.get('version_id'):
app = get_object_or_404(Application, pk=request.POST['app_id'])
ver = get_object_or_404(AppVersion, pk=request.POST['version_id'])
tasks.compatibility_check.delay(fu.pk, app.guid, ver.version)
else:
tasks.validator.delay(fu.pk)
if addon_slug:
return redirect('devhub.upload_detail_for_addon',
addon_slug, fu.pk)
elif is_standalone:
return redirect('devhub.standalone_upload_detail', fu.pk)
else:
return redirect('devhub.upload_detail', fu.pk, 'json')
@login_required
@post_required
@json_view
def upload_manifest(request):
form = forms.NewManifestForm(request.POST)
if form.is_valid():
upload = FileUpload.objects.create()
tasks.fetch_manifest.delay(form.cleaned_data['manifest'], upload.pk)
return redirect('devhub.upload_detail', upload.pk, 'json')
else:
error_text = _('There was an error with the submission.')
if 'manifest' in form.errors:
error_text = ' '.join(form.errors['manifest'])
error_message = {'type': 'error', 'message': error_text, 'tier': 1}
v = {'errors': 1, 'success': False, 'messages': [error_message]}
return make_validation_result(dict(validation=v, error=error_text))
@login_required
@post_required
def standalone_upload(request):
return upload(request, is_standalone=True)
@login_required
@json_view
def standalone_upload_detail(request, uuid):
upload = get_object_or_404(FileUpload.uncached, uuid=uuid)
url = reverse('devhub.standalone_upload_detail', args=[uuid])
return upload_validation_context(request, upload, url=url)
@post_required
@dev_required
def upload_for_addon(request, addon_id, addon):
return upload(request, addon_slug=addon.slug)
@dev_required
@json_view
def upload_detail_for_addon(request, addon_id, addon, uuid):
upload = get_object_or_404(FileUpload.uncached, uuid=uuid)
return json_upload_detail(request, upload, addon_slug=addon.slug)
def make_validation_result(data, is_compatibility=False):
"""Safe wrapper around JSON dict containing a validation result.
Keyword Arguments
**is_compatibility=False**
When True, errors will be summarized as if they were in a regular
validation result.
"""
if not settings.EXPOSE_VALIDATOR_TRACEBACKS:
if data['error']:
# Just expose the message, not the traceback
data['error'] = data['error'].strip().split('\n')[-1].strip()
if data['validation']:
lim = settings.VALIDATOR_MESSAGE_LIMIT
if lim:
del (data['validation']['messages']
[settings.VALIDATOR_MESSAGE_LIMIT:])
ending_tier = data['validation'].get('ending_tier', 0)
for msg in data['validation']['messages']:
if msg['tier'] > ending_tier:
ending_tier = msg['tier']
if msg['tier'] == 0:
# We can't display a message if it's on tier 0.
# Should get fixed soon in bug 617481
msg['tier'] = 1
for k, v in msg.items():
msg[k] = escape_all(v)
if lim:
compatibility_count = 0
if data['validation'].get('compatibility_summary'):
cs = data['validation']['compatibility_summary']
compatibility_count = (cs['errors']
+ cs['warnings']
+ cs['notices'])
else:
cs = {}
leftover_count = (data['validation'].get('errors', 0)
+ data['validation'].get('warnings', 0)
+ data['validation'].get('notices', 0)
+ compatibility_count
- lim)
if leftover_count > 0:
msgtype = 'notice'
if is_compatibility:
if cs.get('errors'):
msgtype = 'error'
elif cs.get('warnings'):
msgtype = 'warning'
else:
if data['validation']['errors']:
msgtype = 'error'
elif data['validation']['warnings']:
msgtype = 'warning'
data['validation']['messages'].append(
{'tier': 1,
'type': msgtype,
'message': (_('Validation generated too many errors/'
'warnings so %s messages were truncated. '
'After addressing the visible messages, '
"you'll be able to see the others.")
% (leftover_count,)),
'compatibility_type': None
})
if is_compatibility:
compat = data['validation']['compatibility_summary']
for k in ('errors', 'warnings', 'notices'):
data['validation'][k] = compat[k]
for msg in data['validation']['messages']:
if msg['compatibility_type']:
msg['type'] = msg['compatibility_type']
data['validation']['ending_tier'] = ending_tier
return data
@dev_required(allow_editors=True)
def file_validation(request, addon_id, addon, file_id):
file = get_object_or_404(File, id=file_id)
v = reverse('devhub.json_file_validation', args=[addon.slug, file.id])
return jingo.render(request, 'devhub/validation.html',
dict(validate_url=v, filename=file.filename,
timestamp=file.created,
addon=addon))
@dev_required(allow_editors=True)
def bulk_compat_result(request, addon_id, addon, result_id):
qs = ValidationResult.objects.exclude(completed=None)
result = get_object_or_404(qs, pk=result_id)
job = result.validation_job
revalidate_url = reverse('devhub.json_bulk_compat_result',
args=[addon.slug, result.id])
return _compat_result(request, revalidate_url,
job.application, job.target_version,
for_addon=result.file.version.addon,
validated_filename=result.file.filename,
validated_ts=result.completed)
def _compat_result(request, revalidate_url, target_app, target_version,
validated_filename=None, validated_ts=None,
for_addon=None):
app_trans = dict((g, unicode(a.pretty)) for g, a in amo.APP_GUIDS.items())
ff_versions = (AppVersion.objects.filter(application=amo.FIREFOX.id,
version_int__gte=4000000000000)
.values_list('application', 'version')
.order_by('version_int'))
tpl = 'https://developer.mozilla.org/en/Firefox_%s_for_developers'
change_links = dict()
for app, ver in ff_versions:
major = ver.split('.')[0] # 4.0b3 -> 4
change_links['%s %s' % (amo.APP_IDS[app].guid, ver)] = tpl % major
return jingo.render(request, 'devhub/validation.html',
dict(validate_url=revalidate_url,
filename=validated_filename,
timestamp=validated_ts,
target_app=target_app,
target_version=target_version,
addon=for_addon,
result_type='compat',
app_trans=app_trans,
version_change_links=change_links))
@json_view
@csrf_view_exempt
@dev_required(allow_editors=True)
def json_file_validation(request, addon_id, addon, file_id):
file = get_object_or_404(File, id=file_id)
if not file.has_been_validated == True:
if request.method != 'POST':
return http.HttpResponseNotAllowed(['POST'])
try:
v_result = tasks.file_validator(file.id)
except Exception, exc:
log.error('file_validator(%s): %s' % (file.id, exc))
error = "\n".join(traceback.format_exception(*sys.exc_info()))
return make_validation_result({'validation': '',
'error': error})
else:
v_result = file.validation
validation = json.loads(v_result.validation)
return make_validation_result(dict(validation=validation,
error=None))
@json_view
@csrf_view_exempt
@post_required
@dev_required(allow_editors=True)
def json_bulk_compat_result(request, addon_id, addon, result_id):
qs = ValidationResult.objects.exclude(completed=None)
result = get_object_or_404(qs, pk=result_id)
if result.task_error:
return make_validation_result({'validation': '',
'error': result.task_error})
else:
validation = json.loads(result.validation)
return make_validation_result(dict(validation=validation, error=None))
@json_view
def json_upload_detail(request, upload, addon_slug=None):
addon = None
if addon_slug:
addon = get_object_or_404(Addon, slug=addon_slug)
result = upload_validation_context(request, upload, addon=addon)
plat_exclude = []
if result['validation']:
if result['validation']['errors'] == 0:
try:
pkg = parse_addon(upload, addon=addon)
app_ids = set([a.id for a in pkg.get('apps', [])])
supported_platforms = []
for app in (amo.MOBILE, amo.ANDROID):
if app.id in app_ids:
supported_platforms.extend(amo.MOBILE_PLATFORMS.keys())
app_ids.remove(app.id)
if len(app_ids):
# Targets any other non-mobile app:
supported_platforms.extend(amo.DESKTOP_PLATFORMS.keys())
s = amo.SUPPORTED_PLATFORMS.keys()
plat_exclude = set(s) - set(supported_platforms)
plat_exclude = [str(p) for p in plat_exclude]
except django_forms.ValidationError, exc:
m = []
for msg in exc.messages:
# Simulate a validation error so the UI displays
# it as such
m.append({'type': 'error',
'message': msg, 'tier': 1})
v = make_validation_result(
dict(error='', validation=dict(messages=m)))
return json_view.error(v)
result['platforms_to_exclude'] = plat_exclude
return result
def upload_validation_context(request, upload, addon_slug=None, addon=None,
url=None):
if addon_slug and not addon:
addon = get_object_or_404(Addon, slug=addon_slug)
if not settings.VALIDATE_ADDONS:
upload.task_error = ''
upload.validation = json.dumps({'errors': 0, 'messages': [],
'metadata': {}, 'notices': 0,
'warnings': 0})
upload.save()
validation = json.loads(upload.validation) if upload.validation else ""
if not url:
if addon:
url = reverse('devhub.upload_detail_for_addon',
args=[addon.slug, upload.uuid])
else:
url = reverse('devhub.upload_detail', args=[upload.uuid, 'json'])
full_report_url = reverse('devhub.upload_detail', args=[upload.uuid])
return make_validation_result(dict(upload=upload.uuid,
validation=validation,
error=upload.task_error, url=url,
full_report_url=full_report_url),
is_compatibility=upload.compat_with_app)
@login_required
def upload_detail(request, uuid, format='html'):
upload = get_object_or_404(FileUpload.uncached, uuid=uuid)
if format == 'json' or request.is_ajax():
return json_upload_detail(request, upload)
validate_url = reverse('devhub.standalone_upload_detail',
args=[upload.uuid])
if upload.compat_with_app:
return _compat_result(request, validate_url,
upload.compat_with_app,
upload.compat_with_appver)
return jingo.render(request, 'devhub/validation.html',
dict(validate_url=validate_url, filename=upload.name,
timestamp=upload.created))
class AddonDependencySearch(BaseAjaxSearch):
# No personas. No webapps.
types = [amo.ADDON_ANY, amo.ADDON_EXTENSION, amo.ADDON_THEME,
amo.ADDON_DICT, amo.ADDON_SEARCH, amo.ADDON_LPAPP]
class AppDependencySearch(BaseAjaxSearch):
# Only webapps.
types = [amo.ADDON_WEBAPP]
@dev_required
@json_view
def ajax_dependencies(request, addon_id, addon):
s = AppDependencySearch if addon.is_webapp() else AddonDependencySearch
return s(request, excluded_ids=[addon_id]).items
@dev_required(webapp=True)
def addons_section(request, addon_id, addon, section, editable=False,
webapp=False):
basic = addon_forms.AppFormBasic if webapp else addon_forms.AddonFormBasic
models = {'basic': basic,
'media': addon_forms.AddonFormMedia,
'details': addon_forms.AddonFormDetails,
'support': addon_forms.AddonFormSupport,
'technical': addon_forms.AddonFormTechnical,
'admin': forms.AdminForm}
if section not in models:
raise http.Http404()
tags = previews = restricted_tags = []
cat_form = dependency_form = device_type_form = None
if section == 'basic':
tags = addon.tags.not_blacklisted().values_list('tag_text', flat=True)
cat_form = addon_forms.CategoryFormSet(request.POST or None,
addon=addon, request=request)
restricted_tags = addon.tags.filter(restricted=True)
if webapp and waffle.switch_is_active('marketplace'):
device_type_form = addon_forms.DeviceTypeForm(request.POST or None,
addon=addon)
elif section == 'media':
previews = forms.PreviewFormSet(request.POST or None,
prefix='files', queryset=addon.previews.all())
elif section == 'technical':
if not webapp:
dependency_form = forms.DependencyFormSet(request.POST or None,
queryset=addon.addons_dependencies.all(), addon=addon,
prefix='dependencies')
# Get the slug before the form alters it to the form data.
valid_slug = addon.slug
if editable:
if request.method == 'POST':
form = models[section](request.POST, request.FILES,
instance=addon, request=request)
if form.is_valid() and (not previews or previews.is_valid()):
addon = form.save(addon)
if previews:
for preview in previews.forms:
preview.save(addon)
editable = False
if section == 'media':
amo.log(amo.LOG.CHANGE_ICON, addon)
else:
amo.log(amo.LOG.EDIT_PROPERTIES, addon)
valid_slug = addon.slug
if cat_form:
if cat_form.is_valid():
cat_form.save()
addon.save()
else:
editable = True
if device_type_form:
if device_type_form.is_valid():
device_type_form.save(addon)
addon.save()
else:
editable = True
if dependency_form:
if dependency_form.is_valid():
dependency_form.save()
else:
editable = True
else:
form = models[section](instance=addon, request=request)
else:
form = False
data = {'addon': addon,
'webapp': webapp,
'form': form,
'editable': editable,
'tags': tags,
'restricted_tags': restricted_tags,
'cat_form': cat_form,
'preview_form': previews,
'dependency_form': dependency_form,
'valid_slug': valid_slug,
'device_type_form': device_type_form}
return jingo.render(request,
'devhub/addons/edit/%s.html' % section, data)
@never_cache
@dev_required
@json_view
def image_status(request, addon_id, addon):
# Default icon needs no checking.
if not addon.icon_type or addon.icon_type.split('/')[0] == 'icon':
icons = True
# Persona icon is handled differently.
elif addon.type == amo.ADDON_PERSONA:
icons = True
else:
icons = storage.exists(os.path.join(addon.get_icon_dir(),
'%s-32.png' % addon.id))
previews = all(storage.exists(p.thumbnail_path)
for p in addon.previews.all())
return {'overall': icons and previews,
'icons': icons,
'previews': previews}
@json_view
def ajax_upload_image(request, upload_type):
errors = []
upload_hash = ''
if 'upload_image' in request.FILES:
upload_preview = request.FILES['upload_image']
upload_preview.seek(0)
upload_hash = uuid.uuid4().hex
loc = os.path.join(settings.TMP_PATH, upload_type, upload_hash)
with storage.open(loc, 'wb') as fd:
for chunk in upload_preview:
fd.write(chunk)
is_icon = upload_type == 'icon'
is_persona = upload_type.startswith('persona_')
check = amo.utils.ImageCheck(upload_preview)
if (not check.is_image() or
upload_preview.content_type not in amo.IMG_TYPES):
if is_icon:
errors.append(_('Icons must be either PNG or JPG.'))
else:
errors.append(_('Images must be either PNG or JPG.'))
if check.is_animated():
if is_icon:
errors.append(_('Icons cannot be animated.'))
else:
errors.append(_('Images cannot be animated.'))
max_size = None
if is_icon:
max_size = settings.MAX_ICON_UPLOAD_SIZE
if is_persona:
max_size = settings.MAX_PERSONA_UPLOAD_SIZE
if max_size and upload_preview.size > max_size:
if is_icon:
errors.append(_('Please use images smaller than %dMB.') % (
max_size / 1024 / 1024 - 1))
if is_persona:
errors.append(_('Images cannot be larger than %dKB.') % (
max_size / 1024))
if check.is_image() and is_persona:
persona, img_type = upload_type.split('_') # 'header' or 'footer'
expected_size = amo.PERSONA_IMAGE_SIZES.get(img_type)[1]
with storage.open(loc, 'rb') as fp:
actual_size = Image.open(fp).size
if actual_size != expected_size:
# L10n: {0} is an image width (in pixels), {1} is a height.
errors.append(_('Image must be exactly {0} pixels wide '
'and {1} pixels tall.')
.format(expected_size[0], expected_size[1]))
else:
errors.append(_('There was an error uploading your preview.'))
if errors:
upload_hash = ''
return {'upload_hash': upload_hash, 'errors': errors}
@dev_required
def upload_image(request, addon_id, addon, upload_type):
return ajax_upload_image(request, upload_type)
@dev_required
def version_edit(request, addon_id, addon, version_id):
version = get_object_or_404(Version, pk=version_id, addon=addon)
version_form = forms.VersionForm(request.POST or None, instance=version)
new_file_form = forms.NewFileForm(request.POST or None,
addon=addon, version=version)
file_form = forms.FileFormSet(request.POST or None, prefix='files',
queryset=version.files.all())
file_history = _get_file_history(version)
data = {'version_form': version_form, 'file_form': file_form}
if addon.accepts_compatible_apps():
# We should be in no-caching land but this one stays cached for some
# reason.
qs = version.apps.all().no_cache()
compat_form = forms.CompatFormSet(request.POST or None, queryset=qs)
data['compat_form'] = compat_form
if (request.method == 'POST' and
all([form.is_valid() for form in data.values()])):
data['version_form'].save()
data['file_form'].save()
for deleted in data['file_form'].deleted_forms:
file = deleted.cleaned_data['id']
amo.log(amo.LOG.DELETE_FILE_FROM_VERSION,
file.filename, file.version, addon)
if 'compat_form' in data:
for compat in data['compat_form'].save(commit=False):
compat.version = version
compat.save()
for form in data['compat_form'].forms:
if (isinstance(form, forms.CompatForm) and
'max' in form.changed_data):
_log_max_version_change(addon, version, form.instance)
messages.success(request, _('Changes successfully saved.'))
return redirect('devhub.versions.edit', addon.slug, version_id)
data.update(addon=addon, version=version, new_file_form=new_file_form,
file_history=file_history)
return jingo.render(request, 'devhub/versions/edit.html', data)
def _log_max_version_change(addon, version, appversion):
details = {'version': version.version,
'target': appversion.version.version,
'application': appversion.application.pk}
amo.log(amo.LOG.MAX_APPVERSION_UPDATED,
addon, version, details=details)
def _get_file_history(version):
file_ids = [f.id for f in version.all_files]
addon = version.addon
file_history = (ActivityLog.objects.for_addons(addon)
.filter(action__in=amo.LOG_REVIEW_QUEUE))
files = dict([(fid, []) for fid in file_ids])
for log in file_history:
details = log.details
current_file_ids = details["files"] if 'files' in details else []
for fid in current_file_ids:
if fid in file_ids:
files[fid].append(log)
return files
@dev_required
@post_required
@transaction.commit_on_success
def version_delete(request, addon_id, addon):
version_id = request.POST.get('version_id')
version = get_object_or_404(Version, pk=version_id, addon=addon)
messages.success(request, _('Version %s deleted.') % version.version)
version.delete()
return redirect(addon.get_dev_url('versions'))
@json_view
@dev_required
@post_required
def version_add(request, addon_id, addon):
form = forms.NewVersionForm(request.POST, addon=addon)
if form.is_valid():
pl = (list(form.cleaned_data['desktop_platforms']) +
list(form.cleaned_data['mobile_platforms']))
v = Version.from_upload(form.cleaned_data['upload'], addon, pl)
log.info('Version created: %s for: %s' %
(v.pk, form.cleaned_data['upload']))
url = reverse('devhub.versions.edit', args=[addon.slug, str(v.id)])
return dict(url=url)
else:
return json_view.error(form.errors)
@json_view
@dev_required
@post_required
def version_add_file(request, addon_id, addon, version_id):
version = get_object_or_404(Version, pk=version_id, addon=addon)
form = forms.NewFileForm(request.POST, addon=addon, version=version)
if not form.is_valid():
return json_view.error(form.errors)
upload = form.cleaned_data['upload']
new_file = File.from_upload(upload, version, form.cleaned_data['platform'],
parse_addon(upload, addon))
storage.delete(upload.path)
file_form = forms.FileFormSet(prefix='files', queryset=version.files.all())
form = [f for f in file_form.forms if f.instance == new_file]
return jingo.render(request, 'devhub/includes/version_file.html',
{'form': form[0], 'addon': addon})
@dev_required(webapp=True)
def version_list(request, addon_id, addon, webapp=False):
qs = addon.versions.order_by('-created').transform(Version.transformer)
versions = amo.utils.paginate(request, qs)
new_file_form = forms.NewVersionForm(None, addon=addon)
data = {'addon': addon,
'webapp': webapp,
'versions': versions,
'new_file_form': new_file_form,
'position': get_position(addon),
'timestamp': int(time.time())}
return jingo.render(request, 'devhub/versions/list.html', data)
@dev_required
def version_bounce(request, addon_id, addon, version):
# Use filter since there could be dupes.
vs = (Version.objects.filter(version=version, addon=addon)
.order_by('-created'))
if vs:
return redirect('devhub.versions.edit', addon.slug, vs[0].id)
else:
raise http.Http404()
@json_view
@dev_required
def version_stats(request, addon_id, addon):
qs = Version.objects.filter(addon=addon)
reviews = (qs.annotate(reviews=Count('reviews'))
.values('id', 'version', 'reviews'))
d = dict((v['id'], v) for v in reviews)
files = qs.annotate(files=Count('files')).values_list('id', 'files')
for id, files in files:
d[id]['files'] = files
return d
Step = collections.namedtuple('Step', 'current max')
def submit_step(outer_step):
"""Wraps the function with a decorator that bounces to the right step."""
def decorator(f):
@functools.wraps(f)
def wrapper(request, *args, **kw):
step = outer_step
webapp = kw.get('webapp', False)
if webapp and step == 7:
# decorator calls this step 7, but it's step 5 for apps
step = 5
max_step = 5 if webapp else 7
# We only bounce on pages with an addon id.
if 'addon' in kw:
addon = kw['addon']
on_step = SubmitStep.objects.filter(addon=addon)
if on_step:
max_step = on_step[0].step
if max_step < step:
# The step was too high, so bounce to the saved step.
return redirect(_step_url(max_step, webapp),
addon.slug)
elif step != max_step:
# We couldn't find a step, so we must be done.
return redirect(_step_url(7, webapp), addon.slug)
kw['step'] = Step(step, max_step)
return f(request, *args, **kw)
# Tell @dev_required that this is a function in the submit flow so it
# doesn't try to redirect into the submit flow.
wrapper.submitting = True
return wrapper
return decorator
@dev_required(webapp=True)
@can_become_premium
def marketplace_paypal(request, addon_id, addon, webapp=False):
"""
Start of the marketplace wizard, none of this means anything until
addon-premium is set, so we'll just save as we go along. Further
we might have the PayPal permissions bounce happen at any time
so we'll need to cope with AddonPremium being incomplete.
"""
form = forms.PremiumForm(request.POST or None,
request=request,
extra={'addon': addon,
'amo_user': request.amo_user,
'dest': 'wizard',
'exclude': ['price']})
if form.is_valid():
form.save()
return redirect(addon.get_dev_url('market.2'))
return jingo.render(request, 'devhub/payments/paypal.html',
{'form': form, 'addon': addon, 'webapp': webapp,
'premium': addon.premium})
@dev_required(webapp=True)
@can_become_premium
def marketplace_pricing(request, addon_id, addon, webapp=False):
form = forms.PremiumForm(request.POST or None,
request=request,
extra={'addon': addon,
'amo_user': request.amo_user,
'dest': 'wizard',
'exclude': ['paypal_id',
'support_email']})
if form.is_valid():
form.save()
if not (form.fields['free'].queryset.count()):
return redirect(addon.get_dev_url('market.4'))
return redirect(addon.get_dev_url('market.3'))
return jingo.render(request, 'devhub/payments/tier.html',
{'form': form, 'addon': addon, 'webapp': webapp,
'premium': addon.premium})
@dev_required(webapp=True)
@can_become_premium
def marketplace_upsell(request, addon_id, addon, webapp=False):
form = forms.PremiumForm(request.POST or None,
request=request,
extra={'addon': addon,
'amo_user': request.amo_user,
'dest': 'wizard',
'exclude': ['price', 'paypal_id',
'support_email']})
if form.is_valid():
form.save()
return redirect(addon.get_dev_url('market.4'))
return jingo.render(request, 'devhub/payments/upsell.html',
{'form': form, 'addon': addon, 'webapp': webapp,
'premium': addon.premium})
@dev_required(webapp=True)
@can_become_premium
def marketplace_confirm(request, addon_id, addon, webapp=False):
if request.method == 'POST':
if (addon.premium and addon.premium.is_complete()
and addon.premium.has_permissions_token()):
if addon.status == amo.STATUS_UNREVIEWED:
addon.status = amo.STATUS_NOMINATED
addon.premium_type = amo.ADDON_PREMIUM
addon.save()
amo.log(amo.LOG.MAKE_PREMIUM, addon)
return redirect(addon.get_dev_url('payments'))
messages.error(request, 'Some required details are missing.')
return redirect(addon.get_dev_url('market.1'))
return jingo.render(request, 'devhub/payments/second-confirm.html',
{'addon': addon, 'webapp': webapp,
'upsell': addon.upsold, 'premium': addon.premium})
def _step_url(step, is_webapp):
url_base = 'devhub.submit%s' % ('_apps' if is_webapp else '')
if is_webapp and str(step).isdigit() and step > 5:
step = 5
return '%s.%s' % (url_base, step)
@login_required
@submit_step(1)
def submit(request, step, webapp=False):
if request.method == 'POST':
response = redirect(_step_url(2, webapp))
response.set_cookie(DEV_AGREEMENT_COOKIE)
return response
return jingo.render(request, 'devhub/addons/submit/start.html',
{'step': step, 'webapp': webapp})
@login_required
@submit_step(2)
def submit_addon(request, step, webapp=False):
if DEV_AGREEMENT_COOKIE not in request.COOKIES:
return redirect(_step_url(1, webapp))
NewItem = forms.NewWebappForm if webapp else forms.NewAddonForm
form = NewItem(request.POST or None)
if request.method == 'POST':
if form.is_valid():
data = form.cleaned_data
if webapp:
p = [Platform.objects.get(id=amo.PLATFORM_ALL.id)]
else:
p = (list(data.get('desktop_platforms', [])) +
list(data.get('mobile_platforms', [])))
addon = Addon.from_upload(data['upload'], p)
if webapp:
tasks.fetch_icon.delay(addon)
AddonUser(addon=addon, user=request.amo_user).save()
SubmitStep.objects.create(addon=addon, step=3)
return redirect(_step_url(3, webapp), addon.slug)
template = 'upload_webapp.html' if webapp else 'upload.html'
return jingo.render(request, 'devhub/addons/submit/%s' % template,
{'step': step, 'webapp': webapp, 'new_addon_form': form})
@dev_required(webapp=True)
@submit_step(3)
def submit_describe(request, addon_id, addon, step, webapp=False):
form_cls = forms.Step3WebappForm if addon.is_webapp() else forms.Step3Form
form = form_cls(request.POST or None, instance=addon, request=request)
cat_form = addon_forms.CategoryFormSet(request.POST or None, addon=addon,
request=request)
device_form = None
if webapp and waffle.switch_is_active('marketplace'):
device_form = addon_forms.DeviceTypeForm(request.POST or None,
addon=addon)
if request.method == 'POST' and form.is_valid() and cat_form.is_valid():
if not device_form or device_form.is_valid():
addon = form.save(addon)
cat_form.save()
if device_form:
device_form.save(addon)
SubmitStep.objects.filter(addon=addon).update(step=4)
return redirect(_step_url(4, webapp), addon.slug)
return jingo.render(request, 'devhub/addons/submit/describe.html',
{'form': form, 'cat_form': cat_form, 'addon': addon,
'step': step, 'webapp': addon.is_webapp(),
'device_form': device_form})
@dev_required(webapp=True)
@submit_step(4)
def submit_media(request, addon_id, addon, step, webapp=False):
form_icon = addon_forms.AddonFormMedia(request.POST or None,
request.FILES or None, instance=addon, request=request)
form_previews = forms.PreviewFormSet(request.POST or None,
prefix='files', queryset=addon.previews.all())
if (request.method == 'POST' and
form_icon.is_valid() and form_previews.is_valid()):
addon = form_icon.save(addon)
for preview in form_previews.forms:
preview.save(addon)
SubmitStep.objects.filter(addon=addon).update(step=5)
# Special handling for webapps, where this is jumping to the done step
if addon.is_webapp():
addon.update(status=amo.WEBAPPS_UNREVIEWED_STATUS)
SubmitStep.objects.filter(addon=addon).delete()
signals.submission_done.send(sender=addon)
return redirect(_step_url(5, webapp), addon.slug)
return jingo.render(request, 'devhub/addons/submit/media.html',
{'form': form_icon, 'addon': addon, 'step': step,
'preview_form': form_previews,
'webapp': addon.is_webapp()})
@dev_required(webapp=True)
@submit_step(5)
def submit_license(request, addon_id, addon, step, webapp=False):
fs, ctx = [], {}
# Versions.
license_form = forms.LicenseForm(request.POST or None, addon=addon)
if not addon.is_webapp():
ctx.update(license_form.get_context())
fs.append(ctx['license_form'])
# Policy.
policy_form = forms.PolicyForm(request.POST or None, addon=addon)
fs.append(policy_form)
if request.method == 'POST' and all([form.is_valid() for form in fs]):
if license_form in fs:
license_form.save(log=False)
policy_form.save()
SubmitStep.objects.filter(addon=addon).update(step=6)
return redirect('devhub.submit.6', addon.slug)
ctx.update(addon=addon, policy_form=policy_form, step=step,
webapp=addon.is_webapp())
return jingo.render(request, 'devhub/addons/submit/license.html', ctx)
@dev_required
@submit_step(6)
def submit_select_review(request, addon_id, addon, step):
review_type_form = forms.ReviewTypeForm(request.POST or None)
updated_status = None
if request.method == 'POST' and review_type_form.is_valid():
updated_status = review_type_form.cleaned_data['review_type']
if updated_status:
addon.update(status=updated_status)
SubmitStep.objects.filter(addon=addon).delete()
signals.submission_done.send(sender=addon)
return redirect('devhub.submit.7', addon.slug)
return jingo.render(request, 'devhub/addons/submit/select-review.html',
{'addon': addon, 'review_type_form': review_type_form,
'step': step})
@dev_required(webapp=True)
@submit_step(7)
def submit_done(request, addon_id, addon, step, webapp=False):
# Bounce to the versions page if they don't have any versions.
if not addon.versions.exists():
return redirect(addon.get_dev_url('versions'))
sp = addon.current_version.supported_platforms
is_platform_specific = sp != [amo.PLATFORM_ALL]
return jingo.render(request, 'devhub/addons/submit/done.html',
{'addon': addon, 'step': step,
'webapp': addon.is_webapp(),
'is_platform_specific': is_platform_specific})
@dev_required
def submit_resume(request, addon_id, addon):
step = SubmitStep.objects.filter(addon=addon)
return _resume(addon, step)
def _resume(addon, step):
if step:
return redirect(_step_url(step[0].step, addon.is_webapp()), addon.slug)
return redirect(addon.get_dev_url('versions'))
@login_required
@dev_required
def submit_bump(request, addon_id, addon, webapp=False):
if not acl.action_allowed(request, 'Admin', 'EditSubmitStep'):
raise PermissionDenied
step = SubmitStep.objects.filter(addon=addon)
step = step[0] if step else None
if request.method == 'POST' and request.POST.get('step'):
new_step = request.POST['step']
if step:
step.step = new_step
else:
step = SubmitStep(addon=addon, step=new_step)
step.save()
return redirect(_step_url('bump', webapp), addon.slug)
return jingo.render(request, 'devhub/addons/submit/bump.html',
dict(addon=addon, step=step))
@login_required
def submit_persona(request):
if not waffle.flag_is_active(request, 'submit-personas'):
raise PermissionDenied
form = addon_forms.NewPersonaForm(data=request.POST or None,
files=request.FILES or None,
request=request)
if request.method == 'POST' and form.is_valid():
addon = form.save()
return redirect('devhub.personas.submit.done', addon.slug)
return jingo.render(request, 'devhub/personas/submit.html',
dict(form=form))
@dev_required
def submit_persona_done(request, addon_id, addon):
if not waffle.flag_is_active(request, 'submit-personas'):
raise PermissionDenied
if addon.is_public():
return redirect(addon.get_url_path())
return jingo.render(request, 'devhub/personas/submit_done.html',
dict(addon=addon))
@dev_required
@post_required
def remove_locale(request, addon_id, addon):
POST = request.POST
if 'locale' in POST and POST['locale'] != addon.default_locale:
addon.remove_locale(POST['locale'])
return http.HttpResponse()
return http.HttpResponseBadRequest()
# You can only request one of the new review tracks.
REQUEST_REVIEW = (amo.STATUS_PUBLIC, amo.STATUS_LITE)
@dev_required
@post_required
def request_review(request, addon_id, addon, status):
status_req = int(status)
if status_req not in addon.can_request_review():
return http.HttpResponseBadRequest()
elif status_req == amo.STATUS_PUBLIC:
if addon.status == amo.STATUS_LITE:
new_status = amo.STATUS_LITE_AND_NOMINATED
else:
new_status = amo.STATUS_NOMINATED
elif status_req == amo.STATUS_LITE:
if addon.status in (amo.STATUS_PUBLIC, amo.STATUS_LITE_AND_NOMINATED):
new_status = amo.STATUS_LITE
else:
new_status = amo.STATUS_UNREVIEWED
addon.update(status=new_status)
msg = {amo.STATUS_LITE: _('Preliminary Review Requested.'),
amo.STATUS_PUBLIC: _('Full Review Requested.')}
messages.success(request, msg[status_req])
amo.log(amo.LOG.CHANGE_STATUS, addon.get_status_display(), addon)
return redirect(addon.get_dev_url('versions'))
# TODO(kumar): Remove when the editor tools are in zamboni.
def validator_redirect(request, version_id):
v = get_object_or_404(Version, id=version_id)
return redirect('devhub.addons.versions', v.addon_id, permanent=True)
@post_required
@addon_view
def admin(request, addon):
if not acl.action_allowed(request, 'Addons', 'Configure'):
raise PermissionDenied
form = forms.AdminForm(request, request.POST or None, instance=addon)
if form.is_valid():
form.save()
return jingo.render(request, 'devhub/addons/edit/admin.html',
{'addon': addon, 'admin_form': form})
def docs(request, doc_name=None, doc_page=None):
filename = ''
all_docs = {'getting-started': [], 'reference': [],
'policies': ['submission', 'reviews', 'maintenance',
'recommended', 'agreement', 'contact'],
'case-studies': ['cooliris', 'stumbleupon',
'download-statusbar'],
'how-to': ['getting-started', 'extension-development',
'thunderbird-mobile', 'theme-development',
'other-addons']}
if waffle.switch_is_active('marketplace'):
all_docs['marketplace'] = ['voluntary']
if doc_name and doc_name in all_docs:
filename = '%s.html' % doc_name
if doc_page and doc_page in all_docs[doc_name]:
filename = '%s-%s.html' % (doc_name, doc_page)
if not filename:
return redirect('devhub.index')
return jingo.render(request, 'devhub/docs/%s' % filename)
def builder(request):
return jingo.render(request, 'devhub/builder.html')
@json_view
@post_required
def check_paypal(request):
if 'email' not in request.POST:
raise http.Http404()
check = Check(paypal_id=request.POST['email'])
check.all()
# TODO(andym): we will want to l10n these messages at some point and
# we'll need to change this to give more detail back to the user than
# a tooltip at a later date.
return {'valid': check.passed, 'message': ' '.join(check.errors)}
def search(request):
query = request.GET.get('q', '')
return jingo.render(request, 'devhub/devhub_search.html', {'query': query})