Multi-platform: support multiple files per version #201
@ -682,18 +682,22 @@ class Version(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
elif len(files) == 0:
|
elif len(files) == 0:
|
||||||
Oleg-Komarov marked this conversation as resolved
Outdated
|
|||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
raise Exception('FIXME: multiple files accessed via .file property')
|
log.warning('FIXME: multiple files accessed via .file property')
|
||||||
|
return files[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def can_upload_more_files(self):
|
def can_upload_more_files(self):
|
||||||
|
return len(self.get_available_platforms()) > 0
|
||||||
|
|
||||||
|
def get_available_platforms(self):
|
||||||
all_platforms = set(p.slug for p in Platform.objects.all())
|
all_platforms = set(p.slug for p in Platform.objects.all())
|
||||||
for file in self.files.all():
|
for file in self.files.all():
|
||||||
platforms = file.platforms()
|
platforms = file.platforms()
|
||||||
if not platforms:
|
if not platforms:
|
||||||
# no platforms means any platform
|
# no platforms means any platform
|
||||||
return False
|
return set()
|
||||||
all_platforms -= set(platforms)
|
all_platforms -= set(platforms)
|
||||||
return len(all_platforms) > 0
|
return all_platforms
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_listed(self):
|
def is_listed(self):
|
||||||
@ -767,6 +771,16 @@ class Version(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_version_upload_url(self) -> str:
|
||||||
|
return reverse(
|
||||||
|
'extensions:version-upload',
|
||||||
|
kwargs={
|
||||||
|
'type_slug': self.extension.type_slug,
|
||||||
|
'slug': self.extension.slug,
|
||||||
|
'pk': self.pk,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def update_url(self) -> str:
|
def update_url(self) -> str:
|
||||||
return reverse(
|
return reverse(
|
||||||
|
@ -16,11 +16,7 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% with form=form|add_form_classes %}
|
{% with form=form|add_form_classes %}
|
||||||
|
|
||||||
<h2>
|
<h2>v{{ form.instance.version }}</h2>
|
||||||
{% blocktranslate with version=form.instance.version %}
|
|
||||||
v{{ version }}
|
|
||||||
{% endblocktranslate %}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<section class="card p-3">
|
<section class="card p-3">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -39,6 +35,24 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
<section class="card p-3 mt-4">
|
||||||
|
{% with version_files=form.instance.files.all %}
|
||||||
|
{% trans 'Files' %}:
|
||||||
|
<ul>
|
||||||
|
{% for file in version_files %}
|
||||||
|
<li>{{ file }} {% trans 'for platforms:' %} {{ file.platforms|join:", " }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endwith %}
|
||||||
|
{% if form.instance.can_upload_more_files %}
|
||||||
|
<div>
|
||||||
|
<a href="{{ form.instance.get_version_upload_url }}" class="btn btn-primary">
|
||||||
|
<i class="i-upload"></i>
|
||||||
|
{% trans 'Upload files for other platforms' %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="is-sticky">
|
<div class="is-sticky">
|
||||||
|
@ -17,15 +17,19 @@
|
|||||||
<div class="col-md-10 mx-auto">
|
<div class="col-md-10 mx-auto">
|
||||||
<div class="card p-5">
|
<div class="card p-5">
|
||||||
<h1>
|
<h1>
|
||||||
{% if extension %}
|
{% if version %}
|
||||||
|
{% trans 'Upload New File for' %} {{ version }}
|
||||||
|
{% elif extension %}
|
||||||
{% trans 'Upload New' %} {{ extension.get_type_display }} {% trans 'Version' %}
|
{% trans 'Upload New' %} {{ extension.get_type_display }} {% trans 'Version' %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans 'Upload Extension' %}
|
{% trans 'Upload Extension' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
|
{% if extension %}
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ extension.get_absolute_url }}"><strong>{{ extension.name }}</strong></a>
|
<a href="{{ extension.get_absolute_url }}"><strong>{{ extension.name }}</strong></a>
|
||||||
</p>
|
</p>
|
||||||
|
{% endif %}
|
||||||
<hr>
|
<hr>
|
||||||
{% if not extension and drafts %}
|
{% if not extension and drafts %}
|
||||||
<div>
|
<div>
|
||||||
@ -75,7 +79,9 @@
|
|||||||
<button type="submit" class="btn btn-block btn-primary js-agree-with-terms-btn-submit px-5 py-2" disabled>
|
<button type="submit" class="btn btn-block btn-primary js-agree-with-terms-btn-submit px-5 py-2" disabled>
|
||||||
<i class="i-upload"></i>
|
<i class="i-upload"></i>
|
||||||
<span>
|
<span>
|
||||||
{% if extension %}
|
{% if version %}
|
||||||
|
{% trans 'Upload New File' %}
|
||||||
|
{% elif extension %}
|
||||||
{% trans 'Upload New Version' %}
|
{% trans 'Upload New Version' %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans 'Upload Extension' %}
|
{% trans 'Upload Extension' %}
|
||||||
@ -91,7 +97,9 @@
|
|||||||
<button type="submit" class="btn btn-block btn-primary mb-2 px-5 py-2">
|
<button type="submit" class="btn btn-block btn-primary mb-2 px-5 py-2">
|
||||||
<i class="i-upload"></i>
|
<i class="i-upload"></i>
|
||||||
<span>
|
<span>
|
||||||
{% if extension %}
|
{% if version %}
|
||||||
|
{% trans 'Upload New File' %}
|
||||||
|
{% elif extension %}
|
||||||
{% trans 'Upload New Version' %}
|
{% trans 'Upload New Version' %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans 'Upload Extension' %}
|
{% trans 'Upload Extension' %}
|
||||||
|
@ -91,6 +91,11 @@ urlpatterns = [
|
|||||||
public.extension_version_download,
|
public.extension_version_download,
|
||||||
name='version-download',
|
name='version-download',
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
'<slug:slug>/manage/versions/<int:pk>/upload/',
|
||||||
|
manage.UploadVersionFileView.as_view(),
|
||||||
|
name='version-upload',
|
||||||
|
),
|
||||||
path('<slug:slug>/versions/', manage.VersionsView.as_view(), name='versions'),
|
path('<slug:slug>/versions/', manage.VersionsView.as_view(), name='versions'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -281,6 +281,46 @@ class UpdateVersionView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
|
|||||||
return self.get_object().extension.has_maintainer(self.request.user)
|
return self.get_object().extension.has_maintainer(self.request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class UploadVersionFileView(
|
||||||
|
LoginRequiredMixin,
|
||||||
|
MaintainedExtensionMixin,
|
||||||
|
CreateView,
|
||||||
|
):
|
||||||
|
"""Upload a new file for an existing version. Multi-OS support."""
|
||||||
|
|
||||||
|
form_class = FileForm
|
||||||
|
template_name = 'extensions/submit.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx['extension'] = self.extension
|
||||||
|
ctx['version'] = self.version
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs['request'] = self.request
|
||||||
|
kwargs['extension'] = self.extension
|
||||||
|
kwargs['version'] = self.version
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def form_valid(self, form):
|
||||||
|
response = super().form_valid(form)
|
||||||
|
self.version.files.add(self.object)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse(
|
||||||
|
'extensions:new-version-finalise',
|
||||||
|
kwargs={
|
||||||
|
'type_slug': self.extension.type_slug,
|
||||||
|
'slug': self.extension.slug,
|
||||||
|
'pk': self.object.pk,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DraftExtensionView(
|
class DraftExtensionView(
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
MaintainedExtensionMixin,
|
MaintainedExtensionMixin,
|
||||||
|
@ -27,13 +27,18 @@ class ExtensionQuerysetMixin:
|
|||||||
|
|
||||||
|
|
||||||
class MaintainedExtensionMixin:
|
class MaintainedExtensionMixin:
|
||||||
"""Fetch an extension by slug if current user is a maintainer."""
|
"""Fetch an extension by slug if current user is a maintainer.
|
||||||
|
|
||||||
|
If pk is present, also fetch a corresponding version.
|
||||||
|
"""
|
||||||
|
|
||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs):
|
||||||
self.extension = get_object_or_404(
|
self.extension = get_object_or_404(
|
||||||
Extension.objects.authored_by(self.request.user),
|
Extension.objects.authored_by(self.request.user),
|
||||||
slug=self.kwargs['slug'],
|
slug=self.kwargs['slug'],
|
||||||
)
|
)
|
||||||
|
if 'pk' in self.kwargs:
|
||||||
|
self.version = get_object_or_404(self.extension.versions, pk=self.kwargs['pk'])
|
||||||
return super().dispatch(*args, **kwargs)
|
return super().dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from .validators import (
|
from .validators import (
|
||||||
ExtensionIDManifestValidator,
|
ExtensionIDManifestValidator,
|
||||||
ExtensionNameManifestValidator,
|
ExtensionNameManifestValidator,
|
||||||
|
ExtensionVersionManifestValidator,
|
||||||
FileMIMETypeValidator,
|
FileMIMETypeValidator,
|
||||||
ManifestValidator,
|
ManifestValidator,
|
||||||
)
|
)
|
||||||
@ -36,11 +37,15 @@ class FileForm(forms.ModelForm):
|
|||||||
# TODO: surface TOML parsing errors?
|
# TODO: surface TOML parsing errors?
|
||||||
'invalid_manifest_toml': _('Manifest file contains invalid code.'),
|
'invalid_manifest_toml': _('Manifest file contains invalid code.'),
|
||||||
'invalid_missing_init': mark_safe(_('Add-on file missing: <strong>__init__.py</strong>.')),
|
'invalid_missing_init': mark_safe(_('Add-on file missing: <strong>__init__.py</strong>.')),
|
||||||
'missing_or_multiple_theme_xml': mark_safe(_('Themes can only contain <strong>one XML file</strong>.')),
|
'missing_or_multiple_theme_xml': mark_safe(
|
||||||
|
_('Themes can only contain <strong>one XML file</strong>.')
|
||||||
|
),
|
||||||
'invalid_zip_archive': msg_only_zip_files,
|
'invalid_zip_archive': msg_only_zip_files,
|
||||||
'missing_manifest_toml': _('The manifest file is missing.'),
|
'missing_manifest_toml': _('The manifest file is missing.'),
|
||||||
'missing_wheel': _('Python wheel missing: %(path)s'), # TODO <strong>%(path)s</strong>
|
'missing_wheel': _('Python wheel missing: %(path)s'), # TODO <strong>%(path)s</strong>
|
||||||
'forbidden_filepaths': _('Archive contains forbidden files or directories: %(paths)s'), #TODO <strong>%(paths)s</strong>
|
'forbidden_filepaths': _(
|
||||||
|
'Archive contains forbidden files or directories: %(paths)s'
|
||||||
|
), # TODO <strong>%(paths)s</strong>
|
||||||
}
|
}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -71,6 +76,7 @@ class FileForm(forms.ModelForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.request = kwargs.pop('request')
|
self.request = kwargs.pop('request')
|
||||||
self.extension = kwargs.pop('extension', None)
|
self.extension = kwargs.pop('extension', None)
|
||||||
|
self.version = kwargs.pop('version', None)
|
||||||
for field in self.base_fields:
|
for field in self.base_fields:
|
||||||
if field not in {'source', 'agreed_with_terms'}:
|
if field not in {'source', 'agreed_with_terms'}:
|
||||||
self.base_fields[field].required = False
|
self.base_fields[field].required = False
|
||||||
@ -160,6 +166,7 @@ class FileForm(forms.ModelForm):
|
|||||||
ManifestValidator(manifest)
|
ManifestValidator(manifest)
|
||||||
ExtensionIDManifestValidator(manifest, self.extension)
|
ExtensionIDManifestValidator(manifest, self.extension)
|
||||||
ExtensionNameManifestValidator(manifest, self.extension)
|
ExtensionNameManifestValidator(manifest, self.extension)
|
||||||
|
ExtensionVersionManifestValidator(manifest, self.extension, self.version)
|
||||||
|
|
||||||
self.cleaned_data['metadata'] = manifest
|
self.cleaned_data['metadata'] = manifest
|
||||||
self.cleaned_data['type'] = EXTENSION_SLUG_TYPES[manifest['type']]
|
self.cleaned_data['type'] = EXTENSION_SLUG_TYPES[manifest['type']]
|
||||||
|
@ -164,6 +164,44 @@ class ExtensionNameManifestValidator:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionVersionManifestValidator:
|
||||||
|
"""Validates version."""
|
||||||
|
|
||||||
|
def __init__(self, manifest, extension_to_be_updated, version_to_be_updated):
|
||||||
|
|
||||||
|
# If the extension wasn't created yet, any version is valid
|
||||||
|
if not extension_to_be_updated:
|
||||||
|
return
|
||||||
|
|
||||||
|
# check for duplicates in database
|
||||||
|
if not version_to_be_updated:
|
||||||
|
version = manifest.get('version')
|
||||||
|
if extension_to_be_updated.versions.filter(version=version).first():
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
'source': [
|
||||||
|
f'The version {escape(version)} was already uploaded for this '
|
||||||
|
f'extension ({extension_to_be_updated.name})'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
code='invalid',
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# check for platforms
|
||||||
|
if platforms := manifest.get('platforms', None):
|
||||||
|
available_platforms = version_to_be_updated.get_available_platforms()
|
||||||
|
if diff := set(platforms) - available_platforms:
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
'source': [
|
||||||
|
f'{version_to_be_updated} already has files for {", ".join(diff)}'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
code='invalid',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ManifestFieldValidator:
|
class ManifestFieldValidator:
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, *, name: str, value: object, manifest: dict) -> str:
|
def validate(cls, *, name: str, value: object, manifest: dict) -> str:
|
||||||
@ -495,30 +533,6 @@ class SchemaVersionValidator(VersionValidator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class VersionVersionValidator(VersionValidator):
|
|
||||||
example = '1.0.0'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def validate(cls, *, name: str, value: str, manifest: dict) -> str:
|
|
||||||
"""Return error message if cannot validate, otherwise returns nothing"""
|
|
||||||
if err_message := super().validate(name=name, value=value, manifest=manifest):
|
|
||||||
return err_message
|
|
||||||
|
|
||||||
extension = Extension.objects.filter(extension_id=manifest.get("id")).first()
|
|
||||||
|
|
||||||
# If the extension wasn't created yet, any version is valid
|
|
||||||
if not extension:
|
|
||||||
return
|
|
||||||
|
|
||||||
version = extension.versions.filter(version=value).first()
|
|
||||||
|
|
||||||
if version:
|
|
||||||
return mark_safe(
|
|
||||||
f'The version {value} was already uploaded for this extension '
|
|
||||||
f'({extension.name})'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class VersionMinValidator(VersionValidator):
|
class VersionMinValidator(VersionValidator):
|
||||||
example = '4.2.0'
|
example = '4.2.0'
|
||||||
|
|
||||||
@ -608,7 +622,7 @@ class ManifestValidator:
|
|||||||
'schema_version': SchemaVersionValidator,
|
'schema_version': SchemaVersionValidator,
|
||||||
'tagline': TaglineValidator,
|
'tagline': TaglineValidator,
|
||||||
'type': TypeValidator,
|
'type': TypeValidator,
|
||||||
'version': VersionVersionValidator,
|
'version': VersionValidator,
|
||||||
}
|
}
|
||||||
optional_fields = {
|
optional_fields = {
|
||||||
'blender_version_max': VersionMaxValidator,
|
'blender_version_max': VersionMaxValidator,
|
||||||
|
Loading…
Reference in New Issue
Block a user
s/get_available_platforms/get_missing_platforms/
?