def geoip_suggestion(request): """ Ajax view to return the localized text for GeoIP locale change suggestion. Takes one parameter from the querystring: * locales - a form encoded list of locales to translate to. Example url: /localize?locales[]=es&locales[]=en-US """ locales = request.GET.getlist('locales[]') response = {'locales': {}} for locale in locales: # English and native names for the language response['locales'][locale] = LOCALES[locale] with uselocale(locale): # This is using our JS-style string formatting. response[locale] = { 'suggestion': _('Would you like to view this page in ' '%(language)s instead?'), 'confirm': _('Yes'), 'cancel': _('No'), } return HttpResponse(json.dumps(response), content_type='application/json')
def hide_tweet(request): """ Hide the tweet with a given ID. Only hides tweets that are not replies and do not have replies. Returns proper HTTP status codes. """ # If feature disabled, bail. if not settings.CC_ALLOW_REMOVE: return HttpResponse(status=418) # I'm a teapot. try: id = int(request.POST.get('id')) except (ValueError, TypeError): return HttpResponseBadRequest(_('Invalid ID.')) try: tweet = Tweet.objects.get(pk=id) except Tweet.DoesNotExist: return HttpResponseNotFound(_('Invalid ID.')) if (tweet.reply_to is not None or Tweet.objects.filter(reply_to=tweet).exists()): return HttpResponseBadRequest(_('Tweets that are replies or have ' 'replies must not be hidden.')) try: tweet.hidden = True tweet.save(force_update=True) except Exception, e: return HttpResponseServerError( _('An error occured: {message}').format(message=e))
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 form_valid(self, form): """Custom form validation to support email changing. If user is already authenticated and reaches this points, it's an email changing procedure. Validate that email is good and save it in the database. Otherwise continue with the default django-browserid verification. """ if not self.request.user.is_authenticated(): return super(BrowserIDVerify, self).form_valid(form) failure_url = urlparams(reverse('phonebook:profile_edit'), bid_login_failed=1) self.assertion = form.cleaned_data['assertion'] self.audience = get_audience(self.request) result = verify(self.assertion, self.audience) if not result: messages.error(self.request, _('Authentication failed.')) return redirect(failure_url) email = result['email'] if User.objects.filter(email=email).exists(): messages.error(self.request, _('Email already exists in the database.')) return redirect('phonebook:logout') user = self.request.user user.email = email user.save() return redirect('phonebook:profile_view', user.username)
def twitter_post(request): """Post a tweet, and return a rendering of it (and any replies).""" try: reply_to_id = int(request.POST.get('reply_to', '')) except ValueError: # L10n: the tweet needs to be a reply to another tweet. return HttpResponseBadRequest(_('Reply-to is empty')) content = request.POST.get('content', '') if len(content) == 0: # L10n: the tweet has no content. return HttpResponseBadRequest(_('Message is empty')) if len(content) > 140: return HttpResponseBadRequest(_('Message is too long')) try: credentials = request.twitter.api.verify_credentials() username = credentials['screen_name'] if username in settings.CC_BANNED_USERS: return render(request, 'customercare/tweets.html', {'tweets': []}) result = request.twitter.api.update_status( status=content, in_reply_to_status_id=reply_to_id) except (TwythonError, TwythonAuthError), e: # L10n: {message} is an error coming from our twitter api library return HttpResponseBadRequest( _('An error occured: {message}').format(message=e))
def file_details(self, file): platform = file.platform_id if self.latest and ( self.addon.status == file.status == amo.STATUS_PUBLIC): url = file.latest_xpi_url() else: url = file.get_url_path(self.src) if platform == amo.PLATFORM_ALL.id: text, os = _('Download Now'), None else: text, os = _('Download'), amo.PLATFORMS[platform] if self.show_eula: # L10n: please keep in the string so → does not wrap. text = jinja2.Markup(_('Continue to Download →')) url = file.eula_url() elif self.accept_eula: text = _('Accept and Download') elif self.show_contrib: # The eula doesn't exist or has been hit already. # L10n: please keep in the string so → does not wrap. text = jinja2.Markup(_('Continue to Download →')) roadblock = reverse('addons.roadblock', args=[self.addon.id]) url = urlparams(roadblock, eula='', version=self.version.version) return text, url, os
def in_app_config(request, addon_id, addon, webapp=True): inapp = addon.premium_type in amo.ADDON_INAPPS if not inapp: messages.error(request, _('Your app is not configured for in-app payments.')) return redirect(reverse('mkt.developers.apps.payments', args=[addon.app_slug])) try: account = addon.app_payment_account except ObjectDoesNotExist: messages.error(request, _('No payment account for this app.')) return redirect(reverse('mkt.developers.apps.payments', args=[addon.app_slug])) seller_config = get_seller_product(account) owner = acl.check_addon_ownership(request, addon) if request.method == 'POST': # Reset the in-app secret for the app. (client.api.generic .product(seller_config['resource_pk']) .patch(data={'secret': generate_key(48)})) messages.success(request, _('Changes successfully saved.')) return redirect(reverse('mkt.developers.apps.in_app_config', args=[addon.app_slug])) return jingo.render(request, 'developers/payments/in-app-config.html', {'addon': addon, 'owner': owner, 'seller_config': seller_config})
def ajax(request): """Query for a user matching a given email.""" if 'q' not in request.GET: raise http.Http404() data = {'status': 0, 'message': ''} email = request.GET.get('q', '').strip() dev_only = request.GET.get('dev', '1') try: dev_only = int(dev_only) except ValueError: dev_only = 1 dev_only = dev_only and settings.MARKETPLACE if not email: data.update(message=_('An email address is required.')) return data user = UserProfile.objects.filter(email=email) if dev_only: user = user.exclude(read_dev_agreement=None) msg = _('A user with that email address does not exist.') msg_dev = _('A user with that email address does not exist, or the user ' 'has not yet accepted the developer agreement.') if user: data.update(status=1, id=user[0].id, name=user[0].name) else: data['message'] = msg_dev if dev_only else msg return escape_all(data)
def setup_viewer(request, file_obj): data = { "file": file_obj, "version": file_obj.version, "addon": file_obj.version.addon, "status": False, "selected": {}, "validate_url": "", } if acl.check_reviewer(request) or acl.check_addon_ownership( request, file_obj.version.addon, viewer=True, ignore_disabled=True ): data["validate_url"] = reverse( "mkt.developers.apps.json_file_validation", args=[file_obj.version.addon.app_slug, file_obj.id] ) if acl.check_reviewer(request): data["file_link"] = { "text": _("Back to review"), "url": reverse("reviewers.apps.review", args=[data["addon"].app_slug]), } else: data["file_link"] = {"text": _("Back to app"), "url": reverse("detail", args=[data["addon"].pk])} return data
def _make_mail(locale, user, context): # Avoid circular import issues from kitsune.users.helpers import display_name is_asker = asker_id == user.id if is_asker: subject = _( u'%s posted an answer to your question "%s"' % (display_name(self.answer.creator), self.instance.title) ) text_template = "questions/email/new_answer_to_asker.ltxt" html_template = "questions/email/new_answer_to_asker.html" else: subject = _(u"Re: %s" % self.instance.title) text_template = "questions/email/new_answer.ltxt" html_template = "questions/email/new_answer.html" mail = email_utils.make_mail( subject=subject, text_template=text_template, html_template=html_template, context_vars=context, from_email="Mozilla Support Forum <*****@*****.**>", to_email=user.email, ) return mail
def emailchange(request, user_id, token, hash): user = get_object_or_404(UserProfile, id=user_id) try: _uid, newemail = EmailResetCode.parse(token, hash) except ValueError: return http.HttpResponse(status=400) if _uid != user.id: # I'm calling this a warning because invalid hashes up to this point # could be any number of things, but this is a targeted attack from # one user account to another log.warning((u"[Tampering] Valid email reset code for UID (%s) " "attempted to change email address for user (%s)") % (_uid, user)) return http.HttpResponse(status=400) user.email = newemail user.save() l = {'user': user, 'newemail': newemail} log.info(u"User (%(user)s) confirmed new email address (%(newemail)s)" % l) messages.success(request, _('Your email address was changed successfully'), _(u'From now on, please use {0} to log in.').format(newemail)) return http.HttpResponseRedirect(reverse('users.edit'))
def parse_xpi(xpi, addon=None, check=True): """Extract and parse an XPI.""" # Extract to /tmp path = tempfile.mkdtemp() try: xpi = get_file(xpi) extract_xpi(xpi, path) xpi_info = Extractor.parse(path) except forms.ValidationError: raise except IOError as e: if len(e.args) < 2: errno, strerror = None, e[0] else: errno, strerror = e log.error('I/O error({0}): {1}'.format(errno, strerror)) raise forms.ValidationError(_('Could not parse install.rdf.')) except Exception: log.error('XPI parse error', exc_info=True) raise forms.ValidationError(_('Could not parse install.rdf.')) finally: rm_local_tmp_dir(path) if check: return check_xpi_info(xpi_info, addon) else: return xpi_info
def api(request): try: access = Access.objects.get(user=request.user) except Access.DoesNotExist: access = None roles = request.amo_user.groups.all() if roles: messages.error(request, _('Users with roles cannot use the API.')) elif not request.amo_user.read_dev_agreement: messages.error(request, _('You must accept the terms of service.')) elif request.method == 'POST': if 'delete' in request.POST: if access: access.delete() messages.success(request, _('API key deleted.')) else: if not access: key = 'mkt:%s:%s' % (request.amo_user.pk, request.amo_user.email) access = Access.objects.create(key=key, user=request.user, secret=generate()) else: access.update(secret=generate()) messages.success(request, _('New API key generated.')) return redirect(reverse('mkt.developers.apps.api')) return jingo.render(request, 'developers/api.html', {'consumer': access, 'profile': profile, 'roles': roles})
def clean_price(self): price_value = self.cleaned_data.get('price') premium_type = self.cleaned_data.get('premium_type') if ((premium_type in amo.ADDON_PREMIUMS or premium_type == amo.ADDON_FREE_INAPP) and not price_value and not self.is_toggling()): raise_required() if not price_value and self.fields['price'].required is False: return None # Special case for a free app - in-app payments must be enabled. # Note: this isn't enforced for tier zero apps. if price_value == 'free': if self.cleaned_data.get('allow_inapp') != 'True': raise ValidationError(_('If app is Free, ' 'in-app payments must be enabled')) return price_value try: price = Price.objects.get(pk=price_value, active=True) except (ValueError, Price.DoesNotExist): raise ValidationError(_('Not a valid choice')) return price
def answer_vote(request, question_id, answer_id): """Vote for Helpful/Not Helpful answers""" answer = get_object_or_404(Answer, pk=answer_id, question=question_id) if answer.question.is_locked: raise PermissionDenied if not answer.has_voted(request): vote = AnswerVote(answer=answer) if 'helpful' in request.REQUEST: vote.helpful = True AnswerMarkedHelpfulAction(answer.creator).save() message = _('Glad to hear it!') else: AnswerMarkedNotHelpfulAction(answer.creator).save() message = _('Sorry to hear that.') if request.user.is_authenticated(): vote.creator = request.user else: vote.anonymous_id = request.anonymous.anonymous_id vote.save() ua = request.META.get('HTTP_USER_AGENT') if ua: vote.add_metadata('ua', ua[:1000]) # 1000 max_length statsd.incr('questions.votes.answer') else: message = _('You already voted on this reply.') if request.is_ajax(): return HttpResponse(json.dumps({'message': message})) return HttpResponseRedirect(answer.get_absolute_url())
def join_group(request, group_pk): """User request to join group.""" group = get_object_or_404(Group, pk=group_pk) profile_to_add = request.user.userprofile # TODO: this duplicates some of the logic in Group.user_can_join(), but we # want to give the user a message that's specific to the reason they can't join. # Can we make this DRYer? if group.has_member(profile_to_add): messages.error(request, _('You are already in this group.')) elif group.has_pending_member(profile_to_add): messages.error(request, _('Your request to join this group is still pending.')) elif group.accepting_new_members == 'no': messages.error(request, _('This group is not accepting requests to join.')) else: if group.accepting_new_members == 'yes': group.add_member(profile_to_add) messages.info(request, _('You have been added to this group.')) elif group.accepting_new_members == 'by_request': group.add_member(profile_to_add, status=GroupMembership.PENDING) messages.info(request, _('Your membership request is pending approval by the group curator.')) return redirect(reverse('groups:show_group', args=[group.url]))
def ajax(request): """Query for a user matching a given email.""" if "q" not in request.GET: raise http.Http404() data = {"status": 0, "message": ""} email = request.GET.get("q", "").strip() dev_only = request.GET.get("dev", "1") try: dev_only = int(dev_only) except ValueError: dev_only = 1 dev_only = dev_only and settings.MARKETPLACE if not email: data.update(message=_("An email address is required.")) return data user = UserProfile.objects.filter(email=email) if dev_only: user = user.exclude(read_dev_agreement=None) msg = _("A user with that email address does not exist.") msg_dev = _( "A user with that email address does not exist, or the user " "has not yet accepted the developer agreement." ) if user: data.update(status=1, id=user[0].id, name=user[0].name) else: data["message"] = msg_dev if dev_only else msg return escape_all(data)
def _transaction_summary(tx_uuid): """Get transaction details from Solitude API.""" contrib = get_object_or_404(Contribution, uuid=tx_uuid) refund_contribs = contrib.get_refund_contribs() refund_contrib = refund_contribs[0] if refund_contribs.exists() else None # Get refund status. refund_status = None if refund_contrib and refund_contrib.refund.status == amo.REFUND_PENDING: try: refund_status = REFUND_STATUSES[ client.api.bango.refund.status.get(data={"uuid": refund_contrib.transaction_id})["status"] ] except HttpServerError: refund_status = _("Currently unable to retrieve refund status.") return { # Solitude data. "refund_status": refund_status, # Zamboni data. "app": contrib.addon, "contrib": contrib, "related": contrib.related, "type": amo.CONTRIB_TYPES.get(contrib.type, _("Incomplete")), # Whitelist what is refundable. "is_refundable": ((contrib.type == amo.CONTRIB_PURCHASE) and not refund_contrib), }
def reply(request, addon, review_id): is_admin = acl.action_allowed(request, 'Addons', 'Edit') is_author = acl.check_addon_ownership(request, addon, dev=True) if not (is_admin or is_author): raise PermissionDenied review = get_object_or_404(Review.objects, pk=review_id, addon=addon) form = ReviewReplyForm(request.POST or None) if form.is_valid(): d = dict(reply_to=review, addon=addon, defaults=dict(user=request.amo_user)) reply, new = Review.objects.get_or_create(**d) for k, v in _review_details(request, addon, form).items(): setattr(reply, k, v) reply.save() action = 'New' if new else 'Edited' if new: amo.log(amo.LOG.ADD_REVIEW, addon, reply) else: amo.log(amo.LOG.EDIT_REVIEW, addon, reply) log.debug('%s reply to %s: %s' % (action, review_id, reply.id)) messages.success(request, _('Your reply was successfully added.') if new else _('Your reply was successfully updated.')) return http.HttpResponse()
def account_settings(request): ctx = {} # Don't use `request.amo_user` because it's too cached. user = request.user if user.is_authenticated(): amo_user = user.get_profile() form = forms.UserEditForm(request.POST or None, instance=amo_user) if request.method == 'POST': if form.is_valid(): form.save() messages.success(request, _('Settings Updated.')) amo.log(amo.LOG.USER_EDITED) response = redirect('account.settings') # TODO: Detect when we're changing the user's locale and region # and bust on '/', bust on '/settings' for everything else. bust_fragments(response, '/') return response else: messages.form_errors(request) ctx = {'form': form, 'amouser': amo_user} else: if request.method == 'POST': messages.success(request, _('Settings Updated.')) return jingo.render(request, 'account/settings.html', ctx)
def reviewers_breadcrumbs(context, queue=None, items=None): """ Wrapper function for ``breadcrumbs``. Prepends 'Editor Tools' breadcrumbs. **queue** Explicit queue type to set. **items** list of [(url, label)] to be inserted after Add-on. """ crumbs = [(reverse('reviewers.home'), _('Reviewer Tools'))] if queue: queues = {'pending': _('Apps'), 'rereview': _('Re-reviews'), 'updates': _('Updates'), 'escalated': _('Escalations'), 'device': _('Device'), 'moderated': _('Moderated Reviews'), 'abuse': _('Abuse Reports'), 'reviewing': _('Reviewing'), 'region': _('Regional Queues')} if items: url = reverse('reviewers.apps.queue_%s' % queue) else: # The Addon is the end of the trail. url = None crumbs.append((url, queues[queue])) if items: crumbs.extend(items) return mkt_breadcrumbs(context, items=crumbs, add_default=True)
def description(self, category): """Description for the feed as a whole""" if category: # L10n: %s is a category name. return _(u'%s Add-ons') % category.name else: return _('Extensions')
def is_valid(self, fatal=True): """ Runs some overall archive checks. fatal: if the archive is not valid and fatal is True, it will raise an error, otherwise it will return False. """ try: zip = zipfile.ZipFile(self.source, self.mode) except (BadZipfile, IOError): if fatal: log.info('Error extracting', exc_info=True) raise return False _info = zip.infolist() for info in _info: if '..' in info.filename or info.filename.startswith('/'): log.error('Extraction error, Invalid archive: %s' % self.source) raise forms.ValidationError(_('Invalid archive.')) if info.file_size > settings.FILE_UNZIP_SIZE_LIMIT: log.error('Extraction error, file too big: %s, %s' % (self.source, info.file_size)) raise forms.ValidationError(_('Invalid archive.')) self.info = _info self.zip = zip return True
def preload_submit(request, addon_id, addon): if request.method == "POST": form = PreloadTestPlanForm(request.POST, request.FILES) if form.is_valid(): # Save test plan file. test_plan = request.FILES["test_plan"] # Figure the type to save it as (cleaned as pdf/xls from the form). filetype = mimetypes.guess_type(test_plan.name)[0] if "pdf" in filetype: filename = "test_plan_%s.pdf" else: filename = "test_plan_%s.xls" # Timestamp. filename = filename % str(time.time()).split(".")[0] save_test_plan(request.FILES["test_plan"], filename, addon) # Log test plan. PreloadTestPlan.objects.filter(addon=addon).update(status=amo.STATUS_DISABLED) PreloadTestPlan.objects.create(addon=addon, filename=filename) messages.success(request, _("Application for preload successfully submitted.")) return redirect(addon.get_dev_url("versions")) else: messages.error(request, _("There was an error with the form.")) else: form = PreloadTestPlanForm() return render(request, "developers/apps/preload/submit.html", {"addon": addon, "form": form})
def get_json_data(self, fileorpath): path = get_filepath(fileorpath) if zipfile.is_zipfile(path): zf = SafeUnzip(path) zf.is_valid() # Raises forms.ValidationError if problems. try: data = zf.extract_path('manifest.webapp') except KeyError: raise forms.ValidationError( _('The file "manifest.webapp" was not found at the root ' 'of the packaged app archive.')) else: file_ = get_file(fileorpath) data = file_.read() file_.close() try: enc_guess = chardet.detect(data) data = strip_bom(data) decoded_data = data.decode(enc_guess['encoding']) except (ValueError, UnicodeDecodeError) as exc: msg = 'Error parsing webapp %r (encoding: %r %.2f%% sure): %s: %s' log.error(msg % (fileorpath, enc_guess['encoding'], enc_guess['confidence'] * 100.0, exc.__class__.__name__, exc)) raise forms.ValidationError( _('Could not decode the webapp manifest file.')) try: return json.loads(decoded_data) except Exception: raise forms.ValidationError( _('The webapp manifest is not valid JSON.'))
def del_image_async(request, image_id): """Delete an image given its object id.""" user = request.user if not user.is_authenticated(): message = _('You are not logged in.') return HttpResponseForbidden( json.dumps({'status': 'error', 'message': message})) try: image = ImageAttachment.objects.get(pk=image_id) except ImageAttachment.DoesNotExist: message = _('The requested image could not be found.') return HttpResponseNotFound( json.dumps({'status': 'error', 'message': message})) if not ((user == image.creator) or (user.has_perm('upload.delete_imageattachment'))): message = _('You do not have permission to do that.') return HttpResponseForbidden( json.dumps({'status': 'error', 'message': message})) image.file.delete() if image.thumbnail: image.thumbnail.delete() image.delete() return HttpResponse(json.dumps({'status': 'success'}))
def sidebar(app): """Populates the sidebar with (categories, types).""" from addons.models import Category if app is None: return [], [] # We muck with query to make order_by and extra_order_by play nice. q = Category.objects.filter(application=app.id, weight__gte=0, type=amo.ADDON_EXTENSION) categories = order_by_translation(q, 'name') categories.query.extra_order_by.insert(0, 'weight') Type = collections.namedtuple('Type', 'id name url') base = urlresolvers.reverse('home') types = [Type(99, _('Collections'), base + 'collections/')] shown_types = { amo.ADDON_PERSONA: urlresolvers.reverse('browse.personas'), amo.ADDON_DICT: urlresolvers.reverse('browse.language-tools'), amo.ADDON_SEARCH: urlresolvers.reverse('browse.search-tools'), amo.ADDON_THEME: urlresolvers.reverse('browse.themes'), } titles = dict(amo.ADDON_TYPES, **{amo.ADDON_DICT: _('Dictionaries & Language Packs')}) for type_, url in shown_types.items(): if type_ in app.types: types.append(Type(type_, titles[type_], url)) return categories, sorted(types, key=lambda x: x.name)
def is_compatible(self): """Returns tuple of compatibility and reasons why if not. Server side conditions for determining compatibility are: * The add-on is an extension (not a theme, app, etc.) * Has not opted in to strict compatibility. * Does not use binary_components in chrome.manifest. Note: The lowest maxVersion compat check needs to be checked separately. Note: This does not take into account the client conditions. """ compat = True reasons = [] if self.addon.type != amo.ADDON_EXTENSION: compat = False # TODO: We may want this. For now we think it may be confusing. # reasons.append(_('Add-on is not an extension.')) if self.files.filter(binary_components=True).exists(): compat = False reasons.append(_("Add-on uses binary components.")) if self.files.filter(strict_compatibility=True).exists(): compat = False reasons.append(_("Add-on has opted into strict compatibility " "checking.")) return (compat, reasons)
def up_image_async(request, model_name, object_pk): """Upload all images in request.FILES.""" # Verify the model agaist our white-list if model_name not in ALLOWED_MODELS: message = _('Model not allowed.') return HttpResponseBadRequest( json.dumps({'status': 'error', 'message': message})) # Get the model m = get_model(*model_name.split('.')) # Then look up the object by pk try: obj = m.objects.get(pk=object_pk) except ObjectDoesNotExist: message = _('Object does not exist.') return HttpResponseNotFound( json.dumps({'status': 'error', 'message': message})) try: file_info = upload_imageattachment(request, obj) except FileTooLargeError as e: return HttpResponseBadRequest( json.dumps({'status': 'error', 'message': e.args[0]})) if isinstance(file_info, dict) and 'thumbnail_url' in file_info: return HttpResponse( json.dumps({'status': 'success', 'file': file_info})) message = _('Invalid or no image received.') return HttpResponseBadRequest( json.dumps({'status': 'error', 'message': message, 'errors': file_info}))
def clean(self): # If lng/lat were provided, make sure they point at a country somewhere... if self.cleaned_data.get('lat') is not None and self.cleaned_data.get('lng') is not None: # We only want to call reverse_geocode if some location data changed. if ('lat' in self.changed_data or 'lng' in self.changed_data or 'saveregion' in self.changed_data or 'savecity' in self.changed_data): self.instance.lat = self.cleaned_data['lat'] self.instance.lng = self.cleaned_data['lng'] self.instance.reverse_geocode() if not self.instance.geo_country: error_msg = _('Location must be inside a country.') self.errors['savecountry'] = self.error_class([error_msg]) del self.cleaned_data['savecountry'] # If the user doesn't want their region/city saved, respect it. if not self.cleaned_data.get('saveregion'): if not self.cleaned_data.get('savecity'): self.instance.geo_region = None else: error_msg = _('Region must also be saved if city is saved.') self.errors['saveregion'] = self.error_class([error_msg]) if not self.cleaned_data.get('savecity'): self.instance.geo_city = None else: self.errors['location'] = self.error_class([_('Search for your country on the map.')]) self.errors['savecountry'] = self.error_class([_('Country cannot be empty.')]) del self.cleaned_data['savecountry'] return self.cleaned_data
class EDIT_DESCRIPTIONS(_LOG): id = 3 action_class = 'edit' format = _(u'{addon} description edited.')
class MANIFEST_UPDATED(_LOG): id = 52 format = _(u'{addon} manifest updated.')
class PAYPAL_FAILED(_LOG): id = 51 format = _(u'{addon} failed checks with PayPal.')
class CHANGE_PASSWORD(_LOG): id = 48 format = _(u'Password changed.')
class BULK_VALIDATION_USER_EMAILED(_LOG): id = 130 format = _(u'Email sent to Author about add-on compatibility.')
class BULK_VALIDATION_EMAILED(_LOG): id = 47 format = _(u'Authors emailed about compatibility of {version}.')
class MAX_APPVERSION_UPDATED(_LOG): id = 46 format = _(u'Application max version for {version} updated.')
class CHANGE_POLICY(_LOG): id = 38 action_class = 'edit' format = _(u'{addon} policy changed.')
class CHANGE_USER_WITH_ROLE(_LOG): """ Expects: author.user, role, addon """ id = 36 # L10n: {0} is a user, {1} is their role format = _(u'{0.name} role changed to {1} for {addon}.') keep = True
class CHANGE_ICON(_LOG): id = 39 action_class = 'edit' format = _(u'{addon} icon changed.')
class REMOVE_RECOMMENDED(_LOG): id = 34 format = _(u'{addon} is no longer featured.') keep = True
class CHANGE_LICENSE(_LOG): """ Expects: license, addon """ id = 37 action_class = 'edit' format = _(u'{addon} is now licensed under {0.name}.')
class REMOVE_RECOMMENDED_CATEGORY(_LOG): id = 32 action_class = 'edit' # L10n: {0} is a category name. format = _(u'{addon} no longer featured in {0}.')
class ADD_APPVERSION(_LOG): id = 35 action_class = 'add' # L10n: {0} is the application, {1} is the version of the app format = _(u'{0} {1} added.')
class EDIT_PROPERTIES(_LOG): """ Expects: addon """ id = 2 action_class = 'edit' format = _(u'{addon} properties edited.')
class ADD_RECOMMENDED(_LOG): id = 33 format = _(u'{addon} is now featured.') keep = True
class REMOVE_FROM_COLLECTION(_LOG): id = 28 action_class = 'collection' format = _(u'{addon} removed from {collection}.')
class ADD_RECOMMENDED_CATEGORY(_LOG): id = 31 action_class = 'edit' # L10n: {0} is a category name. format = _(u'{addon} featured in {0}.')
class REMOVE_TAG(_LOG): id = 26 action_class = 'tag' format = _(u'{tag} removed from {addon}.')
class ADD_REVIEW(_LOG): id = 29 action_class = 'review' format = _(u'{review} for {addon} written.')
class REQUEST_SUPER_REVIEW(_LOG): id = 45 format = _(u'{addon} {version} super review requested.') short = _(u'Super review requested') keep = True review_queue = True
class ADD_TO_COLLECTION(_LOG): id = 27 action_class = 'collection' format = _(u'{addon} added to {collection}.')
class ADD_FILE_TO_VERSION(_LOG): id = 19 action_class = 'add' format = _(u'File {0.name} added to {version} of {addon}.')
class ADD_TAG(_LOG): id = 25 action_class = 'tag' format = _(u'{tag} added to {addon}.')
class ADD_VERSION(_LOG): id = 16 action_class = 'add' format = _(u'{version} added to {addon}.') keep = True
class CREATE_ADDON(_LOG): id = 1 action_class = 'add' format = _(u'{addon} was created.') keep = True
class EDIT_PREVIEW(_LOG): id = 14 action_class = 'edit' format = _(u'Preview edited for {addon}.')
class EDIT_VERSION(_LOG): id = 17 action_class = 'edit' format = _(u'{version} edited for {addon}.')
class ADD_PREVIEW(_LOG): id = 13 action_class = 'add' format = _(u'Preview added to {addon}.')
class DELETE_PREVIEW(_LOG): id = 15 action_class = 'delete' format = _(u'Preview deleted from {addon}.')