remove add-on packager from marketplace devhub (bug 733126)
This commit is contained in:
Родитель
fb629575ea
Коммит
9b1b99dbfb
|
@ -1,15 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django.forms.models import modelformset_factory
|
||||
from django.forms.formsets import formset_factory, BaseFormSet
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.encoding import force_unicode
|
||||
|
||||
import commonware
|
||||
import happyforms
|
||||
|
@ -26,11 +23,11 @@ from addons.models import (Addon, AddonDependency, AddonUpsell, AddonUser,
|
|||
from amo.helpers import loc
|
||||
from amo.forms import AMOModelForm
|
||||
from amo.urlresolvers import reverse
|
||||
from amo.utils import raise_required, slugify
|
||||
from amo.utils import raise_required
|
||||
|
||||
from applications.models import Application, AppVersion
|
||||
from files.models import File, FileUpload, Platform
|
||||
from files.utils import parse_addon, VERSION_RE
|
||||
from files.utils import parse_addon
|
||||
from market.models import AddonPremium, Price, AddonPaymentData
|
||||
from mkt.site.forms import AddonChoiceField, APP_UPSELL_CHOICES
|
||||
from payments.models import InappConfig
|
||||
|
@ -749,208 +746,6 @@ class AdminForm(happyforms.ModelForm):
|
|||
}
|
||||
|
||||
|
||||
class InlineRadioRenderer(forms.widgets.RadioFieldRenderer):
|
||||
|
||||
def render(self):
|
||||
return mark_safe(''.join(force_unicode(w) for w in self))
|
||||
|
||||
|
||||
class PackagerBasicForm(forms.Form):
|
||||
name = forms.CharField(min_length=5, max_length=50,
|
||||
help_text=_lazy(u'Give your add-on a name. The most successful '
|
||||
'add-ons give some indication of their function in '
|
||||
'their name.'))
|
||||
description = forms.CharField(required=False, widget=forms.Textarea,
|
||||
help_text=_lazy(u'Briefly describe your add-on in one sentence. '
|
||||
'This appears in the Add-ons Manager.'))
|
||||
version = forms.CharField(max_length=32,
|
||||
help_text=_lazy(u'Enter your initial version number. Depending on the '
|
||||
'number of releases and your preferences, this is '
|
||||
'usually 0.1 or 1.0'))
|
||||
id = forms.CharField(
|
||||
help_text=_lazy(u'Each add-on requires a unique ID in the form of a '
|
||||
'UUID or an email address, such as '
|
||||
'addon-name@developer.com. The email address does not '
|
||||
'have to be valid.'))
|
||||
package_name = forms.CharField(min_length=5, max_length=50,
|
||||
help_text=_lazy(u'The package name of your add-on used within the '
|
||||
'browser. This should be a short form of its name '
|
||||
'(for example, Test Extension might be '
|
||||
'test_extension).'))
|
||||
author_name = forms.CharField(
|
||||
help_text=_lazy(u'Enter the name of the person or entity to be '
|
||||
'listed as the author of this add-on.'))
|
||||
contributors = forms.CharField(required=False, widget=forms.Textarea,
|
||||
help_text=_lazy(u'Enter the names of any other contributors to this '
|
||||
'extension, one per line.'))
|
||||
|
||||
def clean_name(self):
|
||||
name = self.cleaned_data['name']
|
||||
addons.forms.clean_name(name)
|
||||
name_regex = re.compile('(mozilla|firefox|thunderbird)', re.I)
|
||||
if name_regex.match(name):
|
||||
raise forms.ValidationError(
|
||||
_('Add-on names should not contain Mozilla trademarks.'))
|
||||
return name
|
||||
|
||||
def clean_package_name(self):
|
||||
slug = self.cleaned_data['package_name']
|
||||
if slugify(slug, ok='_', lower=False, delimiter='_') != slug:
|
||||
raise forms.ValidationError(
|
||||
_('Enter a valid package name consisting of letters, numbers, '
|
||||
'or underscores.'))
|
||||
if Addon.objects.filter(slug=slug).exists():
|
||||
raise forms.ValidationError(
|
||||
_('This package name is already in use.'))
|
||||
if BlacklistedSlug.blocked(slug):
|
||||
raise forms.ValidationError(
|
||||
_(u'The package name cannot be: %s.' % slug))
|
||||
return slug
|
||||
|
||||
def clean_id(self):
|
||||
id_regex = re.compile(
|
||||
"""(\{[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}\} | # GUID
|
||||
[a-z0-9-\.\+_]*\@[a-z0-9-\._]+) # Email format""",
|
||||
re.I | re.X)
|
||||
|
||||
if not id_regex.match(self.cleaned_data['id']):
|
||||
raise forms.ValidationError(
|
||||
_('The add-on ID must be a UUID string or an email '
|
||||
'address.'))
|
||||
return self.cleaned_data['id']
|
||||
|
||||
def clean_version(self):
|
||||
if not VERSION_RE.match(self.cleaned_data['version']):
|
||||
raise forms.ValidationError(_('The version string is invalid.'))
|
||||
return self.cleaned_data['version']
|
||||
|
||||
|
||||
class PackagerCompatForm(forms.Form):
|
||||
enabled = forms.BooleanField(required=False)
|
||||
min_ver = forms.ModelChoiceField(AppVersion.objects.none(),
|
||||
empty_label=None, required=False,
|
||||
label=_lazy(u'Minimum'))
|
||||
max_ver = forms.ModelChoiceField(AppVersion.objects.none(),
|
||||
empty_label=None, required=False,
|
||||
label=_lazy(u'Maximum'))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PackagerCompatForm, self).__init__(*args, **kwargs)
|
||||
if not self.initial:
|
||||
return
|
||||
|
||||
self.app = self.initial['application']
|
||||
qs = (AppVersion.objects.filter(application=self.app.id)
|
||||
.order_by('-version_int'))
|
||||
|
||||
self.fields['enabled'].label = self.app.pretty
|
||||
if self.app == amo.FIREFOX:
|
||||
self.fields['enabled'].widget.attrs['checked'] = True
|
||||
|
||||
# Don't allow version ranges as the minimum version.
|
||||
self.fields['min_ver'].queryset = qs.filter(~Q(version__contains='*'))
|
||||
self.fields['max_ver'].queryset = qs.all()
|
||||
|
||||
# Unreasonably hardcode a reasonable default minVersion.
|
||||
if self.app in (amo.FIREFOX, amo.MOBILE, amo.THUNDERBIRD):
|
||||
try:
|
||||
self.fields['min_ver'].initial = qs.filter(
|
||||
version=settings.DEFAULT_MINVER)[0]
|
||||
except (IndexError, AttributeError):
|
||||
pass
|
||||
|
||||
def clean_min_ver(self):
|
||||
if self.cleaned_data['enabled'] and not self.cleaned_data['min_ver']:
|
||||
raise_required()
|
||||
return self.cleaned_data['min_ver']
|
||||
|
||||
def clean_max_ver(self):
|
||||
if self.cleaned_data['enabled'] and not self.cleaned_data['max_ver']:
|
||||
raise_required()
|
||||
return self.cleaned_data['max_ver']
|
||||
|
||||
def clean(self):
|
||||
if self.errors:
|
||||
return
|
||||
|
||||
data = self.cleaned_data
|
||||
|
||||
if data['enabled']:
|
||||
min_ver = data['min_ver']
|
||||
max_ver = data['max_ver']
|
||||
if not (min_ver and max_ver):
|
||||
raise forms.ValidationError(_('Invalid version range.'))
|
||||
|
||||
if min_ver.version_int > max_ver.version_int:
|
||||
raise forms.ValidationError(
|
||||
_('Min version must be less than Max version.'))
|
||||
|
||||
# Pass back the app name and GUID.
|
||||
data['min_ver'] = str(min_ver)
|
||||
data['max_ver'] = str(max_ver)
|
||||
data['name'] = self.app.pretty
|
||||
data['guid'] = self.app.guid
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class PackagerCompatBaseFormSet(BaseFormSet):
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
super(PackagerCompatBaseFormSet, self).__init__(*args, **kw)
|
||||
self.initial = [{'application': a} for a in amo.APP_USAGE]
|
||||
self._construct_forms()
|
||||
|
||||
def clean(self):
|
||||
if any(self.errors):
|
||||
return
|
||||
if (not self.forms or not
|
||||
any(f.cleaned_data.get('enabled') for f in self.forms
|
||||
if f.app == amo.FIREFOX)):
|
||||
# L10n: {0} is Firefox.
|
||||
raise forms.ValidationError(
|
||||
_(u'{0} is a required target application.')
|
||||
.format(amo.FIREFOX.pretty))
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
PackagerCompatFormSet = formset_factory(PackagerCompatForm,
|
||||
formset=PackagerCompatBaseFormSet, extra=0)
|
||||
|
||||
|
||||
class PackagerFeaturesForm(forms.Form):
|
||||
about_dialog = forms.BooleanField(
|
||||
required=False,
|
||||
label=_lazy(u'About dialog'),
|
||||
help_text=_lazy(u'Creates a standard About dialog for your '
|
||||
'extension'))
|
||||
preferences_dialog = forms.BooleanField(
|
||||
required=False,
|
||||
label=_lazy(u'Preferences dialog'),
|
||||
help_text=_lazy(u'Creates an example Preferences window'))
|
||||
toolbar = forms.BooleanField(
|
||||
required=False,
|
||||
label=_lazy(u'Toolbar'),
|
||||
help_text=_lazy(u'Creates an example toolbar for your extension'))
|
||||
toolbar_button = forms.BooleanField(
|
||||
required=False,
|
||||
label=_lazy(u'Toolbar button'),
|
||||
help_text=_lazy(u'Creates an example button on the browser '
|
||||
'toolbar'))
|
||||
main_menu_command = forms.BooleanField(
|
||||
required=False,
|
||||
label=_lazy(u'Main menu command'),
|
||||
help_text=_lazy(u'Creates an item on the Tools menu'))
|
||||
context_menu_command = forms.BooleanField(
|
||||
required=False,
|
||||
label=_lazy(u'Context menu command'),
|
||||
help_text=_lazy(u'Creates a context menu item for images'))
|
||||
sidebar_support = forms.BooleanField(
|
||||
required=False,
|
||||
label=_lazy(u'Sidebar support'),
|
||||
help_text=_lazy(u'Creates an example sidebar panel'))
|
||||
|
||||
|
||||
class CheckCompatibilityForm(happyforms.Form):
|
||||
application = forms.ChoiceField(
|
||||
label=_lazy(u'Application'),
|
||||
|
|
|
@ -22,7 +22,7 @@ import validator.constants as validator_constants
|
|||
|
||||
import amo
|
||||
from amo.decorators import write, set_modified_on
|
||||
from amo.utils import guard, resize_image, remove_icons, strip_bom
|
||||
from amo.utils import resize_image, remove_icons, strip_bom
|
||||
from addons.models import Addon
|
||||
from applications.management.commands import dump_apps
|
||||
from applications.models import Application, AppVersion
|
||||
|
@ -280,32 +280,6 @@ def convert_purified(ids, **kw):
|
|||
addon.save()
|
||||
|
||||
|
||||
@task
|
||||
def packager(data, feature_set, **kw):
|
||||
"""Build an add-on based on input data."""
|
||||
log.info('[1@None] Packaging add-on')
|
||||
|
||||
from mkt.developers.views import packager_path
|
||||
dest = packager_path(data['slug'])
|
||||
|
||||
with guard(u'mkt.developers.packager.%s' % dest) as locked:
|
||||
if locked:
|
||||
log.error(u'Packaging in progress: %s' % dest)
|
||||
return
|
||||
|
||||
with statsd.timer('mkt.developers.packager'):
|
||||
from packager.main import packager
|
||||
log.info('Starting packaging: %s' % dest)
|
||||
features = set([k for k, v in feature_set.items() if v])
|
||||
try:
|
||||
packager(data, dest, features)
|
||||
except Exception, err:
|
||||
log.error(u'Failed to package add-on: %s' % err)
|
||||
raise
|
||||
if os.path.exists(dest):
|
||||
log.info(u'Package saved: %s' % dest)
|
||||
|
||||
|
||||
def failed_validation(*messages):
|
||||
"""Return a validation object that looks like the add-on validator."""
|
||||
m = []
|
||||
|
|
|
@ -1,111 +0,0 @@
|
|||
{% extends "developers/base.html" %}
|
||||
{% from "includes/forms.html" import pretty_field %}
|
||||
|
||||
{% set title = _('Add-on Packager') %}
|
||||
{% block title %}{{ hub_page_title(title) }}{% endblock %}
|
||||
{% block bodyclass %}packager inverse {{ super() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="primary">
|
||||
<header>
|
||||
{{ hub_breadcrumbs(addon, items=[(None, title)]) }}
|
||||
<h2>{{ title }}</h2>
|
||||
</header>
|
||||
<form method="post" id="packager" class="item" action="">
|
||||
{{ csrf() }}
|
||||
<p class="summary">
|
||||
{% trans %}
|
||||
Enter some basic information about your add-on below and select which
|
||||
interface components to start with, and your custom-built add-on will be
|
||||
ready for download.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend>{{ _('Describe your add-on') }}</legend>
|
||||
<p>
|
||||
{% trans %}
|
||||
First, you need to enter some basic information about your add-on. This
|
||||
will be displayed in the Add-ons Manager when your extension is installed.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
{{ basic_form.non_field_errors() }}
|
||||
<ul>
|
||||
{{ pretty_field(basic_form.name, _('Add-on Name'), req=True) }}
|
||||
{{ pretty_field(basic_form.description, _('Description')) }}
|
||||
{{ pretty_field(basic_form.version, _('Add-on Version'), req=True) }}
|
||||
{{ pretty_field(basic_form.id, _('Unique ID'), req=True) }}
|
||||
{{ pretty_field(basic_form.package_name, _('Package Name'), req=True) }}
|
||||
</ul>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{{ _("Who's working on your add-on?") }}</legend>
|
||||
<p>
|
||||
{% trans %}
|
||||
Tell us about the people or companies behind this add-on. This information
|
||||
appears in the add-on's About dialog.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<ul>
|
||||
{{ pretty_field(basic_form.author_name, _('Primary Author'), req=True) }}
|
||||
{{ pretty_field(basic_form.contributors, _('Contributors')) }}
|
||||
</ul>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{{ _('Where will your add-on run?') }}</legend>
|
||||
<p>
|
||||
{% trans %}
|
||||
Select the applications and versions that your add-on will support. The
|
||||
versions that you select will be the only versions that your add-on will
|
||||
be installable on. Make sure you only select applications and versions
|
||||
that you intend to test your add-on with.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<div id="supported-apps">
|
||||
<label>{{ _('Supported Applications and Versions') }}</label>
|
||||
{{ compat_forms.management_form }}
|
||||
{{ compat_forms.non_form_errors() }}
|
||||
<ul class="choices">
|
||||
{% for compat in compat_forms %}
|
||||
<li class="row">
|
||||
{{ pretty_field(compat.enabled, tag=None, class='app ' + compat.app.short) }}
|
||||
{{ pretty_field(compat.min_ver, tag='span') }}
|
||||
{{ pretty_field(compat.max_ver, tag='span') }}
|
||||
{{ compat.non_field_errors() }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{{ _('Choose pre-built features') }}</legend>
|
||||
<p>
|
||||
{% trans %}
|
||||
Get started quickly by selecting user interface components to include in
|
||||
your add-on package. We'll include documented code for each item you
|
||||
select so your extension will work out of the box.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<ul class="choices">
|
||||
{% for feature in features_form %}
|
||||
{{ pretty_field(feature) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</fieldset>
|
||||
<footer>
|
||||
<button>{{ _('Submit and Build') }}</button>
|
||||
</footer>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<aside class="secondary">
|
||||
<div class="highlight">
|
||||
<h3>{{ _('Advanced Building') }}</h3>
|
||||
<p>
|
||||
{% trans url="http://www.mozdev.org/projects/wizard/" %}
|
||||
Want to create XULRunner applications, XPCOM components, and other advanced
|
||||
skeletons? Visit the <a href="{{ url }}">MozDev Project Wizard</a>.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
{% endblock content %}
|
|
@ -1,41 +0,0 @@
|
|||
{% extends "developers/base.html" %}
|
||||
|
||||
{% set title = _('Add-on Packager') %}
|
||||
{% block title %}{{ hub_page_title(title) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<header>
|
||||
{{ hub_breadcrumbs(addon, items=[(None, title)]) }}
|
||||
<h2>{{ title }}</h2>
|
||||
<h3>{{ _('Add-on packaged successfully!') }}</h3>
|
||||
</header>
|
||||
<section>
|
||||
<p>
|
||||
{% trans %}
|
||||
Use the download link below to save a copy of your extension's compressed
|
||||
source. To install as an extension in Firefox or another compatible
|
||||
application, simply rename the <kbd>.zip</kbd> extension to <kbd>.xpi</kbd>.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<div id="packager-download"
|
||||
data-downloadurl="{{ url('mkt.developers.package_addon_json', package_name) }}">
|
||||
<span>{{ _('Please wait…') }}</span>
|
||||
<noscript>
|
||||
{% trans %}
|
||||
JavaScript needs to be enabled to fetch the packaged add-on.
|
||||
{% endtrans %}
|
||||
</noscript>
|
||||
</div>
|
||||
<h3>{{ _('What do I do next?') }}</h3>
|
||||
<p>
|
||||
{% trans gs_url=url('mkt.developers.docs', 'getting-started'),
|
||||
ht_url=url('mkt.developers.docs', 'how-to') %}
|
||||
Now that you've got an extension skeleton, the fun part begins: hacking on
|
||||
the extension to make it do what you want. If you need some help, check out
|
||||
our <a href="{{ gs_url }}">Getting Started</a> page for the basics, or
|
||||
visit the <a href="{{ ht_url }}">How-to Library</a> for tutorials and
|
||||
documentation.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
</section>
|
||||
{% endblock content %}
|
|
@ -9,8 +9,6 @@ from mkt.developers.decorators import use_apps
|
|||
from webapps.urls import APP_SLUG
|
||||
from . import views
|
||||
|
||||
PACKAGE_NAME = '(?P<package_name>[_\w]+)'
|
||||
|
||||
|
||||
def paypal_patterns(prefix):
|
||||
return patterns('',
|
||||
|
@ -149,16 +147,6 @@ ajax_patterns = patterns('',
|
|||
name='mkt.developers.file_perf_tests_start'),
|
||||
)
|
||||
|
||||
packager_patterns = patterns('',
|
||||
url('^$', views.package_addon, name='mkt.developers.package_addon'),
|
||||
url('^download/%s.zip$' % PACKAGE_NAME, views.package_addon_download,
|
||||
name='mkt.developers.package_addon_download'),
|
||||
url('^json/%s$' % PACKAGE_NAME, views.package_addon_json,
|
||||
name='mkt.developers.package_addon_json'),
|
||||
url('^success/%s$' % PACKAGE_NAME, views.package_addon_success,
|
||||
name='mkt.developers.package_addon_success'),
|
||||
)
|
||||
|
||||
urlpatterns = decorate(write, patterns('',
|
||||
url('^$', views.index, name='mkt.developers.index'),
|
||||
|
||||
|
@ -177,9 +165,6 @@ urlpatterns = decorate(write, patterns('',
|
|||
views.compat_application_versions,
|
||||
name='mkt.developers.compat_application_versions'),
|
||||
|
||||
# Add-on packager
|
||||
url('^tools/package/', include(packager_patterns)),
|
||||
|
||||
# Redirect to /addons/ at the base.
|
||||
url('^addon$',
|
||||
lambda r: redirect('mkt.developers.addons', permanent=True)),
|
||||
|
|
|
@ -30,7 +30,7 @@ from amo import messages
|
|||
from amo.decorators import (json_view, login_required, no_login_required,
|
||||
post_required, write)
|
||||
from amo.helpers import loc
|
||||
from amo.utils import escape_all, HttpResponseSendFile
|
||||
from amo.utils import escape_all
|
||||
from amo.urlresolvers import reverse
|
||||
from access import acl
|
||||
from addons import forms as addon_forms
|
||||
|
@ -603,68 +603,6 @@ def file_perf_tests_start(request, addon_id, addon, file_id):
|
|||
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('mkt.developers.package_addon_success',
|
||||
basic_data['package_name'])
|
||||
|
||||
return jingo.render(request, 'developers/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, 'developers/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 os.path.isfile(path_):
|
||||
url = reverse('mkt.developers.package_addon_download',
|
||||
args=[package_name])
|
||||
return {'download_url': url, 'filename': os.path.basename(path_),
|
||||
'size': round(os.path.getsize(path_) / 1024, 1)}
|
||||
|
||||
|
||||
def package_addon_download(request, package_name):
|
||||
"""Serve a packaged add-on."""
|
||||
path_ = packager_path(package_name)
|
||||
if not os.path.isfile(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):
|
||||
|
|
Загрузка…
Ссылка в новой задаче