Multi-platform: support multiple files per version #201
@ -682,18 +682,22 @@ class Version(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
||||
elif len(files) == 0:
|
||||
Oleg-Komarov marked this conversation as resolved
Outdated
|
||||
return None
|
||||
else:
|
||||
raise Exception('FIXME: multiple files accessed via .file property')
|
||||
log.warning('FIXME: multiple files accessed via .file property')
|
||||
return files[0]
|
||||
|
||||
@property
|
||||
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())
|
||||
for file in self.files.all():
|
||||
platforms = file.platforms()
|
||||
if not platforms:
|
||||
# no platforms means any platform
|
||||
return False
|
||||
return set()
|
||||
all_platforms -= set(platforms)
|
||||
return len(all_platforms) > 0
|
||||
return all_platforms
|
||||
|
||||
@property
|
||||
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
|
||||
def update_url(self) -> str:
|
||||
return reverse(
|
||||
|
@ -16,11 +16,7 @@
|
||||
{% csrf_token %}
|
||||
{% with form=form|add_form_classes %}
|
||||
|
||||
<h2>
|
||||
{% blocktranslate with version=form.instance.version %}
|
||||
v{{ version }}
|
||||
{% endblocktranslate %}
|
||||
</h2>
|
||||
<h2>v{{ form.instance.version }}</h2>
|
||||
|
||||
<section class="card p-3">
|
||||
<div class="row">
|
||||
@ -39,6 +35,24 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</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 class="col-md-4">
|
||||
<div class="is-sticky">
|
||||
|
@ -17,15 +17,19 @@
|
||||
<div class="col-md-10 mx-auto">
|
||||
<div class="card p-5">
|
||||
<h1>
|
||||
{% if extension %}
|
||||
{% if version %}
|
||||
{% trans 'Upload New File for' %} {{ version }}
|
||||
{% elif extension %}
|
||||
{% trans 'Upload New' %} {{ extension.get_type_display }} {% trans 'Version' %}
|
||||
{% else %}
|
||||
{% trans 'Upload Extension' %}
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% if extension %}
|
||||
<p>
|
||||
<a href="{{ extension.get_absolute_url }}"><strong>{{ extension.name }}</strong></a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<hr>
|
||||
{% if not extension and drafts %}
|
||||
<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>
|
||||
<i class="i-upload"></i>
|
||||
<span>
|
||||
{% if extension %}
|
||||
{% if version %}
|
||||
{% trans 'Upload New File' %}
|
||||
{% elif extension %}
|
||||
{% trans 'Upload New Version' %}
|
||||
{% else %}
|
||||
{% trans 'Upload Extension' %}
|
||||
@ -91,7 +97,9 @@
|
||||
<button type="submit" class="btn btn-block btn-primary mb-2 px-5 py-2">
|
||||
<i class="i-upload"></i>
|
||||
<span>
|
||||
{% if extension %}
|
||||
{% if version %}
|
||||
{% trans 'Upload New File' %}
|
||||
{% elif extension %}
|
||||
{% trans 'Upload New Version' %}
|
||||
{% else %}
|
||||
{% trans 'Upload Extension' %}
|
||||
|
@ -91,6 +91,11 @@ urlpatterns = [
|
||||
public.extension_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'),
|
||||
],
|
||||
),
|
||||
|
@ -281,6 +281,46 @@ class UpdateVersionView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
|
||||
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(
|
||||
LoginRequiredMixin,
|
||||
MaintainedExtensionMixin,
|
||||
|
@ -27,13 +27,18 @@ class ExtensionQuerysetMixin:
|
||||
|
||||
|
||||
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):
|
||||
self.extension = get_object_or_404(
|
||||
Extension.objects.authored_by(self.request.user),
|
||||
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)
|
||||
|
||||
|
||||
|
@ -10,6 +10,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from .validators import (
|
||||
ExtensionIDManifestValidator,
|
||||
ExtensionNameManifestValidator,
|
||||
ExtensionVersionManifestValidator,
|
||||
FileMIMETypeValidator,
|
||||
ManifestValidator,
|
||||
)
|
||||
@ -36,11 +37,15 @@ class FileForm(forms.ModelForm):
|
||||
# TODO: surface TOML parsing errors?
|
||||
'invalid_manifest_toml': _('Manifest file contains invalid code.'),
|
||||
'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,
|
||||
'missing_manifest_toml': _('The manifest file is missing.'),
|
||||
'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>
|
||||
'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>
|
||||
}
|
||||
|
||||
class Meta:
|
||||
@ -71,6 +76,7 @@ class FileForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.request = kwargs.pop('request')
|
||||
self.extension = kwargs.pop('extension', None)
|
||||
self.version = kwargs.pop('version', None)
|
||||
for field in self.base_fields:
|
||||
if field not in {'source', 'agreed_with_terms'}:
|
||||
self.base_fields[field].required = False
|
||||
@ -160,6 +166,7 @@ class FileForm(forms.ModelForm):
|
||||
ManifestValidator(manifest)
|
||||
ExtensionIDManifestValidator(manifest, self.extension)
|
||||
ExtensionNameManifestValidator(manifest, self.extension)
|
||||
ExtensionVersionManifestValidator(manifest, self.extension, self.version)
|
||||
|
||||
self.cleaned_data['metadata'] = manifest
|
||||
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:
|
||||
@classmethod
|
||||
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):
|
||||
example = '4.2.0'
|
||||
|
||||
@ -608,7 +622,7 @@ class ManifestValidator:
|
||||
'schema_version': SchemaVersionValidator,
|
||||
'tagline': TaglineValidator,
|
||||
'type': TypeValidator,
|
||||
'version': VersionVersionValidator,
|
||||
'version': VersionValidator,
|
||||
}
|
||||
optional_fields = {
|
||||
'blender_version_max': VersionMaxValidator,
|
||||
|
Loading…
Reference in New Issue
Block a user
s/get_available_platforms/get_missing_platforms/
?