Multi-platform: support multiple files per version #201
@ -1,5 +1,4 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
from urllib.parse import urlencode
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
@ -755,37 +754,36 @@ class Version(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
permissions.append({'slug': slug, 'reason': reason, 'name': all_permission_names[slug]})
|
permissions.append({'slug': slug, 'reason': reason, 'name': all_permission_names[slug]})
|
||||||
return permissions
|
return permissions
|
||||||
|
|
||||||
# FIXME? make dependent on File or platform
|
def get_download_list(self) -> List[dict]:
|
||||||
@property
|
files = list(self.files.all())
|
||||||
def download_name(self) -> str:
|
if len(files) == 1:
|
||||||
"""Return a file name for downloads."""
|
file = files[0]
|
||||||
Oleg-Komarov marked this conversation as resolved
Outdated
|
|||||||
replace_char = f'{self}'.replace('.', '-')
|
return [
|
||||||
return f'{utils.slugify(replace_char)}.zip'
|
{
|
||||||
Oleg-Komarov marked this conversation as resolved
Outdated
Anna Sirota
commented
to avoid making platform into file ext, maybe to avoid making platform into file ext, maybe `-`?
|
|||||||
|
'name': file.download_name(),
|
||||||
# FIXME make dependent on File or platform
|
'url': file.download_url(platform=None),
|
||||||
def download_url(self, append_repository_and_compatibility=True) -> str:
|
'size': file.size_bytes,
|
||||||
filename = f'{self.extension.type_slug_singular}-{self.extension.slug}-v{self.version}.zip'
|
}
|
||||||
download_url = reverse(
|
]
|
||||||
'extensions:version-download',
|
platform2file = {}
|
||||||
kwargs={
|
for file in files:
|
||||||
'type_slug': self.extension.type_slug,
|
platforms = file.platforms()
|
||||||
'slug': self.extension.slug,
|
if not platforms:
|
||||||
'version': self.version,
|
log.warning(
|
||||||
'filename': filename,
|
f'data error: Version pk={self.pk} has multiple files, but File pk={file.pk} '
|
||||||
},
|
f'is not platform-specific'
|
||||||
)
|
)
|
||||||
if append_repository_and_compatibility:
|
for platform in platforms:
|
||||||
params = {
|
platform2file[platform] = file
|
||||||
'repository': '/api/v1/extensions/',
|
return [
|
||||||
'blender_version_min': self.blender_version_min,
|
{
|
||||||
|
'platform': p,
|
||||||
|
'name': file.download_name(),
|
||||||
|
'url': file.download_url(platform=p),
|
||||||
|
'size': file.size_bytes,
|
||||||
}
|
}
|
||||||
if self.blender_version_max:
|
for p, file in platform2file.items()
|
||||||
params['blender_version_max'] = self.blender_version_max
|
]
|
||||||
if platforms := self.platforms.all():
|
|
||||||
params['platforms'] = ','.join([p.slug for p in platforms])
|
|
||||||
query_string = urlencode(params)
|
|
||||||
download_url += f'?{query_string}'
|
|
||||||
return download_url
|
|
||||||
|
|
||||||
def get_delete_url(self) -> str:
|
def get_delete_url(self) -> str:
|
||||||
return reverse(
|
return reverse(
|
||||||
|
@ -256,37 +256,43 @@
|
|||||||
{% block extension_download %}
|
{% block extension_download %}
|
||||||
<section class="ext-detail-download mt-3">
|
<section class="ext-detail-download mt-3">
|
||||||
{% if extension.is_approved %}
|
{% if extension.is_approved %}
|
||||||
|
{% with download_list=latest.get_download_list %}
|
||||||
<div class="btn-group js-btn-install-group">
|
<div class="btn-group js-btn-install-group">
|
||||||
<button class="btn btn-flex btn-accent js-btn-install" data-install-url="{{ request.scheme }}://{{ request.get_host }}{{ latest.download_url }}">
|
<button class="btn btn-flex btn-accent js-btn-install" data-install-url="{{ request.scheme }}://{{ request.get_host }}{{ download_list.0.url }}">
|
||||||
<span>{% trans 'Get' %} {{ extension.get_type_display }}</span>
|
<span>{% trans 'Get' %} {{ extension.get_type_display }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="fade js-btn-install-action">
|
<div class="fade js-btn-install-action">
|
||||||
|
{% for download_item in download_list %}
|
||||||
<div class="btn-install-drag-group js-btn-install-drag-group mb-2 rounded">
|
<div class="btn-install-drag-group js-btn-install-drag-group mb-2 rounded">
|
||||||
<button class="btn btn-flex btn-primary btn-install-drag cursor-move js-btn-install-drag w-100" draggable="true">
|
<button class="btn btn-flex btn-primary btn-install-drag cursor-move js-btn-install-drag w-100" draggable="true">
|
||||||
<i class="i-move"></i>
|
<i class="i-move"></i>
|
||||||
<span>{% trans 'Drag and Drop into Blender' %}</span>
|
<span>{% trans 'Drag and Drop into Blender' %} {{ download_item.platform }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<small class="d-block text-center w-100">
|
<small class="d-block text-center w-100">
|
||||||
{# TODO @front-end: Replace URL of the manual /dev/ with /latest/. #}
|
{# TODO @front-end: Replace URL of the manual /dev/ with /latest/. #}
|
||||||
...or <a class="text-underline text-primary" href="{{ request.scheme }}://{{ request.get_host }}{{ latest.download_url }}" download="{{ latest.download_name }}">download</a>
|
...or <a class="text-underline text-primary" href="{{ request.scheme }}://{{ request.get_host }}{{ download_item.url }}" download="{{ download_item.name }}">download</a>
|
||||||
and <a class="text-underline text-primary" href="https://docs.blender.org/manual/en/dev/editors/preferences/extensions.html#install" target="_blank">Install from Disk</a>
|
and <a class="text-underline text-primary" href="https://docs.blender.org/manual/en/dev/editors/preferences/extensions.html#install" target="_blank">Install from Disk</a>
|
||||||
</small>
|
</small>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# If JavaScript is disabled. #}
|
{# If JavaScript is disabled. #}
|
||||||
<noscript>
|
<noscript>
|
||||||
<style>.js-btn-install-group { display: none;}</style>
|
<style>.js-btn-install-group { display: none;}</style>
|
||||||
<div class="btn-col text-center">
|
<div class="btn-col text-center">
|
||||||
<a class="btn btn-flex btn-accent" href="{{ request.scheme }}://{{ request.get_host }}{{ latest.download_url }}" download="{{ latest.download_name }}">
|
{% for download_item in download_list %}
|
||||||
<i class="i-download"></i><span>{% trans 'Download' %} {{ extension.get_type_display }}</span>
|
<a class="btn btn-flex btn-accent" href="{{ request.scheme }}://{{ request.get_host }}{{ download_item.url }}" download="{{ download_item.name }}">
|
||||||
|
<i class="i-download"></i><span>{% trans 'Download' %} {{ extension.get_type_display }} {{ download_item.platform }}</span>
|
||||||
</a>
|
</a>
|
||||||
<small class="mt-3">...and <a class="text-underline text-primary text-center" href="https://docs.blender.org/manual/en/dev/editors/preferences/extensions.html#install" target="_blank">Install from Disk</a></small>
|
<small class="mt-3">...and <a class="text-underline text-primary text-center" href="https://docs.blender.org/manual/en/dev/editors/preferences/extensions.html#install" target="_blank">Install from Disk</a></small>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|
||||||
{% else %}
|
{% endwith %}
|
||||||
|
{% else %}
|
||||||
<div class="card p-3 mt-3">
|
<div class="card p-3 mt-3">
|
||||||
<p class="text-info">This {{ extension.get_type_display|lower }} is currently under review.</p>
|
<p class="text-info">This {{ extension.get_type_display|lower }} is currently under review.</p>
|
||||||
<p>
|
<p>
|
||||||
@ -296,7 +302,7 @@
|
|||||||
<span>{% trans 'Review Extension' %}</span>
|
<span>{% trans 'Review Extension' %}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
{% endblock extension_download %}
|
{% endblock extension_download %}
|
||||||
</aside>
|
</aside>
|
||||||
|
@ -75,6 +75,7 @@ class ListedExtensionsSerializer(serializers.ModelSerializer):
|
|||||||
platforms = file.platforms()
|
platforms = file.platforms()
|
||||||
if self.platform and (platforms and self.platform not in platforms):
|
if self.platform and (platforms and self.platform not in platforms):
|
||||||
continue
|
continue
|
||||||
|
# TODO? return all matching files (when no self.platform is passed)?
|
||||||
matching_file = file
|
matching_file = file
|
||||||
Oleg-Komarov marked this conversation as resolved
Outdated
Anna Sirota
commented
this looks shared with the logic in this looks shared with the logic in `public` view that selects which file to pick from storage, and should probable become a utility or method somewhere.
|
|||||||
matching_version = v
|
matching_version = v
|
||||||
Oleg-Komarov marked this conversation as resolved
Outdated
Anna Sirota
commented
needs another needs another `break` here
|
|||||||
break
|
break
|
||||||
@ -91,8 +92,10 @@ class ListedExtensionsSerializer(serializers.ModelSerializer):
|
|||||||
'archive_hash': matching_file.original_hash,
|
'archive_hash': matching_file.original_hash,
|
||||||
'archive_size': matching_file.size_bytes,
|
'archive_size': matching_file.size_bytes,
|
||||||
'archive_url': self.request.build_absolute_uri(
|
'archive_url': self.request.build_absolute_uri(
|
||||||
# FIXME download_url is per file
|
matching_file.download_url(
|
||||||
matching_version.download_url(append_repository_and_compatibility=False)
|
platform=self.platform,
|
||||||
|
append_repository_and_compatibility=False,
|
||||||
|
)
|
||||||
),
|
),
|
||||||
'type': EXTENSION_TYPE_SLUGS_SINGULAR.get(instance.type),
|
'type': EXTENSION_TYPE_SLUGS_SINGULAR.get(instance.type),
|
||||||
'blender_version_min': matching_version.blender_version_min,
|
'blender_version_min': matching_version.blender_version_min,
|
||||||
@ -102,7 +105,7 @@ class ListedExtensionsSerializer(serializers.ModelSerializer):
|
|||||||
'maintainer': instance.team and instance.team.name or str(instance.authors.all()[0]),
|
'maintainer': instance.team and instance.team.name or str(instance.authors.all()[0]),
|
||||||
'license': [license_iter.slug for license_iter in matching_version.licenses.all()],
|
'license': [license_iter.slug for license_iter in matching_version.licenses.all()],
|
||||||
'permissions': matching_file.metadata.get('permissions'),
|
'permissions': matching_file.metadata.get('permissions'),
|
||||||
# FIXME? should we instead list platforms of matching_file?
|
# TODO? if listing all version files (see the note above) use matching_file.platforms()
|
||||||
'platforms': [platform.slug for platform in matching_version.platforms.all()],
|
'platforms': [platform.slug for platform in matching_version.platforms.all()],
|
||||||
# TODO: handle copyright
|
# TODO: handle copyright
|
||||||
'tags': [str(tag) for tag in matching_version.tags.all()],
|
'tags': [str(tag) for tag in matching_version.tags.all()],
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
from urllib.parse import urlencode
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from common.model_mixins import CreatedModifiedMixin, TrackChangesMixin
|
from common.model_mixins import CreatedModifiedMixin, TrackChangesMixin
|
||||||
from files.utils import get_sha256, guess_mimetype_from_ext, get_thumbnail_upload_to
|
from files.utils import get_sha256, guess_mimetype_from_ext, get_thumbnail_upload_to
|
||||||
@ -202,6 +204,49 @@ class File(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
'tags': data.get('tags'),
|
'tags': data.get('tags'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def download_name(self) -> str:
|
||||||
|
"""Return a file name for downloads."""
|
||||||
|
version = self.version.first()
|
||||||
|
replace_char = f'{version}'.replace('.', '-')
|
||||||
|
return f'{utils.slugify(replace_char)}.zip'
|
||||||
|
|
||||||
|
def download_url(self, platform=None, append_repository_and_compatibility=True) -> str:
|
||||||
|
filename = self.download_name()
|
||||||
|
version = self.version.first()
|
||||||
|
if platform:
|
||||||
|
download_url = reverse(
|
||||||
|
'extensions:version-platform-download',
|
||||||
|
kwargs={
|
||||||
|
'type_slug': version.extension.type_slug,
|
||||||
|
'slug': version.extension.slug,
|
||||||
|
'version': version.version,
|
||||||
|
'platform': platform,
|
||||||
|
'filename': filename,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
download_url = reverse(
|
||||||
|
'extensions:version-download',
|
||||||
|
kwargs={
|
||||||
|
'type_slug': version.extension.type_slug,
|
||||||
|
'slug': version.extension.slug,
|
||||||
|
'version': version.version,
|
||||||
|
'filename': filename,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if append_repository_and_compatibility:
|
||||||
|
params = {
|
||||||
|
'repository': '/api/v1/extensions/',
|
||||||
|
'blender_version_min': version.blender_version_min,
|
||||||
|
}
|
||||||
|
if version.blender_version_max:
|
||||||
|
params['blender_version_max'] = version.blender_version_max
|
||||||
|
if platforms := self.platforms():
|
||||||
|
params['platforms'] = ','.join(platforms)
|
||||||
|
query_string = urlencode(params)
|
||||||
|
download_url += f'?{query_string}'
|
||||||
|
return download_url
|
||||||
|
|
||||||
def get_thumbnail_of_size(self, size_key: str) -> str:
|
def get_thumbnail_of_size(self, size_key: str) -> str:
|
||||||
"""Return absolute path portion of the URL of a thumbnail of this file.
|
"""Return absolute path portion of the URL of a thumbnail of this file.
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ class TestTasks(TestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
response = self.client.get(version.extension.get_versions_url())
|
response = self.client.get(version.extension.get_versions_url())
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
response = self.client.get(version.download_url())
|
response = self.client.get(version.files.first().download_url())
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
response['Location'],
|
response['Location'],
|
||||||
|
Loading…
Reference in New Issue
Block a user
The
filename
belowfilename = f'{self.extension.type_slug_singular}-{self.extension.slug}-v{self.version}.zip'
looks like a better name: doesn't rely on
self.__str__
.It also looks like these file names should be the same in both places.