Multi-platform: support multiple files per version #201

Merged
Oleg-Komarov merged 43 commits from multi-os into main 2024-07-09 16:27:46 +02:00
8 changed files with 147 additions and 40 deletions
Showing only changes of commit 3bf3d9678b - Show all commits

View File

@ -682,18 +682,22 @@ class Version(CreatedModifiedMixin, TrackChangesMixin, models.Model):
elif len(files) == 0:
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(

View File

@ -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">

View File

@ -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' %}

View File

@ -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'),
],
),

View File

@ -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,

View File

@ -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)

View File

@ -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']]

View File

@ -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,