Refactor Extension and Version: explicit constructors from File #191
@ -125,7 +125,7 @@ and then open `htmlcov/index.html` with your favourite browser.
|
||||
|
||||
# Deploy
|
||||
|
||||
See [playbooks](/playbooks/).
|
||||
See [playbooks](playbooks#deploy).
|
||||
|
||||
# Feature Flags
|
||||
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit b42a40608238c43429e4f4e32231d12fbb16f114
|
||||
Subproject commit 493bc70fd4fecb2b3ff5c4b48ca288e4355015fd
|
@ -18,5 +18,6 @@ def extra_context(request: HttpRequest) -> Dict[str, str]:
|
||||
'BASE_URL': settings.BLENDER_ID['BASE_URL'],
|
||||
},
|
||||
'canonical_url': request.build_absolute_uri(request.path),
|
||||
'root_url': request.build_absolute_uri('/'),
|
||||
'user_is_moderator': user_is_moderator,
|
||||
}
|
||||
|
@ -1,8 +1,5 @@
|
||||
table,
|
||||
.table
|
||||
a
|
||||
text-decoration: underline
|
||||
|
||||
th
|
||||
color: var(--color-text-secondary)
|
||||
|
||||
|
@ -18,9 +18,40 @@
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{% static 'icons/favicon-16x16.png' %}">
|
||||
{% endblock head_favicon %}
|
||||
|
||||
{% block meta %}
|
||||
{% include 'common/components/meta.html' %}
|
||||
{% endblock meta %}
|
||||
{% spaceless %}
|
||||
{% with default_title="Blender Extensions" %}
|
||||
{% with default_author="Blender Foundation" %}
|
||||
|
||||
{% with default_description="Blender Extensions is a web based service developed by Blender Foundation that allows people to share open source add-ons for Blender." %}
|
||||
|
||||
{% if not image_url %}
|
||||
{% absolute_url default_image_path as image_url %}
|
||||
{% endif %}
|
||||
|
||||
{% if url %}{% absolute_url url as url %}{% endif %}
|
||||
|
||||
<link rel="canonical" href="{% firstof url canonical_url %}" />
|
||||
<meta property="og:url" content="{% firstof url canonical_url %}">
|
||||
|
||||
<meta name="author" content="{% firstof author default_author %}">
|
||||
<meta name="theme-color" content="#009eff">
|
||||
<meta property="og:site_name" content="Blender Extensions">
|
||||
<meta property="og:locale" content="en_US">
|
||||
<meta property="og:type" content="website">
|
||||
|
||||
<meta property="og:title" content="{% block og_title %}{% if title %}{{ title }} - {% endif %}{{ default_title }}{% endblock og_title %}">
|
||||
|
||||
<meta name="description" content="{% block page_description %}{% firstof description default_description %}{% endblock page_description %}">
|
||||
<meta property="og:description" content="{% block og_description %}{% firstof description default_description %}{% endblock og_description %}">
|
||||
|
||||
<meta property="og:image" content="{% block og_image %}{{ image_url }}{% endblock og_image %}">
|
||||
<meta property="og:image:alt" content="{% block og_image_alt %}{{ default_title }}{% endblock og_image_alt %}">
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endspaceless %}
|
||||
|
||||
{% block meta %}{% endblock meta %}
|
||||
|
||||
{% stylesheet 'common' %}
|
||||
|
||||
|
@ -1,40 +0,0 @@
|
||||
{% spaceless %}
|
||||
{% load static %}
|
||||
{% load common %}
|
||||
|
||||
{% with default_title="Blender Extensions" %}
|
||||
{% with default_author="Blender Foundation" %}
|
||||
|
||||
{% with default_description="Blender Extensions is a web based service developed by Blender Foundation that allows people to share open source add-ons for Blender." %}
|
||||
|
||||
{% if not image_url %}
|
||||
{% absolute_url default_image_path as image_url %}
|
||||
{% endif %}
|
||||
|
||||
{% if url %}{% absolute_url url as url %}{% endif %}
|
||||
|
||||
<link rel="canonical" href="{% firstof url canonical_url %}" />
|
||||
<meta property="og:url" content="{% firstof url canonical_url %}">
|
||||
|
||||
<meta name="author" content="{% firstof author default_author %}">
|
||||
<meta name="theme-color" content="#009eff">
|
||||
<meta property="og:site_name" content="Blender Extensions">
|
||||
<meta property="og:locale" content="en_US">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
{#<meta name="twitter:site" content="@Blender_Extensions">#}
|
||||
<meta property="og:type" content="website">
|
||||
|
||||
<meta property="og:title" content="{% if title %}{{ title }} - {% endif %}{{ default_title }}">
|
||||
<meta name="twitter:title" content="{% if title %}{{ title }} - {% endif %}{{ default_title }}">
|
||||
|
||||
<meta name="description" content="{% firstof description default_description %}">
|
||||
<meta property="og:description" content="{% firstof description default_description %}">
|
||||
<meta name="twitter:description" content="{% firstof description default_description %}">
|
||||
|
||||
<meta property="og:image" content="{{ image_url }}">
|
||||
<meta name="twitter:image" content="{{ image_url }}">
|
||||
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endspaceless %}
|
@ -207,3 +207,26 @@ def remove_cols_rows(bound_field: BoundField):
|
||||
bound_field.field.widget.attrs.pop('cols', None)
|
||||
bound_field.field.widget.attrs.pop('rows', None)
|
||||
return bound_field
|
||||
|
||||
@register.filter
|
||||
def get_nth(value, n):
|
||||
"""Gets the nth element of a list, where n is a 0-based index."""
|
||||
try:
|
||||
# Keep n as 0-based index and return the nth element
|
||||
return value[int(n)]
|
||||
except (IndexError, ValueError, TypeError):
|
||||
return None
|
||||
|
||||
@register.filter
|
||||
@stringfilter
|
||||
def split(value, key):
|
||||
"""Splits the value by a key and returns a list of parts."""
|
||||
return value.split(key)
|
||||
|
||||
@register.filter
|
||||
def to_int(value):
|
||||
"""Convert a string to an integer."""
|
||||
try:
|
||||
return int(value)
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
|
@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import semantic_version
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
@ -319,6 +320,26 @@ class VersionForm(forms.ModelForm):
|
||||
return self.initial['file']
|
||||
|
||||
|
||||
class VersionUpdateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
fields = ['blender_version_max', 'release_notes']
|
||||
model = extensions.models.Version
|
||||
|
||||
def clean_blender_version_max(self, *args, **kwargs):
|
||||
if 'blender_version_max' in self.data:
|
||||
blender_version_max = self.cleaned_data['blender_version_max']
|
||||
try:
|
||||
max = semantic_version.Version(blender_version_max)
|
||||
if max <= semantic_version.Version(self.instance.blender_version_min):
|
||||
self.add_error(
|
||||
'blender_version_max', _('Must be greater than min blender version')
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
self.add_error('blender_version_max', _('Must be a valid version'))
|
||||
|
||||
return blender_version_max
|
||||
|
||||
|
||||
class VersionDeleteForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = extensions.models.Version
|
||||
|
@ -1,30 +1,47 @@
|
||||
{% load i18n common %}
|
||||
|
||||
<a
|
||||
class="overflow-visible"
|
||||
href="https://www.blender.org/download/releases/{{ version.blender_version_min|version_without_patch|replace:".,-" }}/"
|
||||
title="{{ version.blender_version_min }}">Blender {{ version.blender_version_min|version_without_patch }}</a>
|
||||
{% if is_editable %}
|
||||
<span class="mx-2">—</span>
|
||||
<input name="blender_version_max" class="form-control"
|
||||
value="{{version.blender_version_max|default_if_none:''}}"
|
||||
placeholder="x.x.x"
|
||||
pattern="^([0-9]+\.[0-9]+\.[0-9]+)?$"
|
||||
title="{% trans 'Blender version, e.g. 4.1.0' %}"
|
||||
/>
|
||||
{% for error in form.errors.blender_version_max %}
|
||||
<div class="error">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{# Convert exclusive version to latest supported #}
|
||||
{% with blender_version_max_major=version.blender_version_max|split:"."|get_nth:0 blender_version_max_minor=version.blender_version_max|split:"."|get_nth:1 blender_version_max_patch=version.blender_version_max|split:"."|get_nth:2 blender_version_min_major=version.blender_version_min|split:"."|get_nth:0 blender_version_min_minor=version.blender_version_min|split:"."|get_nth:1 blender_version_min_patch=version.blender_version_min|split:"."|get_nth:2 %}
|
||||
<a
|
||||
class="overflow-visible"
|
||||
href="https://www.blender.org/download/releases/{{ version.blender_version_min|version_without_patch|replace:".,-" }}/"
|
||||
title="{{ version.blender_version_min }}">Blender {% if blender_version_min_patch == "0" %}{{ blender_version_min_major }}.{{ blender_version_min_minor }}{% else %}{{ blender_version_min_major }}.{{ blender_version_min_minor }}.{{ blender_version_min_patch }}{% endif %}</a>
|
||||
|
||||
{% if version.blender_version_max %}
|
||||
{% if version.blender_version_max|version_without_patch != version.blender_version_min|version_without_patch %}
|
||||
<span class="mx-2">—</span>
|
||||
<a
|
||||
href="https://www.blender.org/download/releases/{{ version.blender_version_max|version_without_patch|replace:".,-" }}/"
|
||||
title="{{ version.blender_version_max }}">{{ version.blender_version_max|version_without_patch }}</a>
|
||||
{# Check if max version and min version are equal #}
|
||||
{% if version.blender_version_max == version.blender_version_min or version.blender_version_max < version.blender_version_min %}
|
||||
{# Do nothing #}
|
||||
{# Check if max version minor is higher than min version minor by 1 and if patch version is 0 #}
|
||||
{% elif blender_version_max_minor|to_int|add:"-1" == blender_version_min_minor|to_int and blender_version_max_patch == "0" %}
|
||||
{# Decrement version minor display #}
|
||||
{% with blender_version_max_minor_converted=blender_version_max_minor|add:"-1" %}
|
||||
<span>and newer </span>
|
||||
<a
|
||||
href="https://www.blender.org/download/releases/{{ blender_version_max_major }}-{{ blender_version_max_minor_converted }}/"
|
||||
title="{{ blender_version_max_major }}.{{ blender_version_max_minor_converted }}">{{ blender_version_max_major }}.{{ blender_version_max_minor_converted }}</a>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<span class="mx-2">—</span>
|
||||
|
||||
{% if blender_version_max_patch == "0" %}
|
||||
{# Decrement version minor display #}
|
||||
{% with blender_version_max_minor_converted=blender_version_max_minor|add:"-1" %}
|
||||
<a
|
||||
href="https://www.blender.org/download/releases/{{ blender_version_max_major }}-{{ blender_version_max_minor_converted }}/"
|
||||
title="{{ blender_version_max_major }}.{{ blender_version_max_minor_converted }}.{{ blender_version_max_patch }}">{{ blender_version_max_major }}.{{ blender_version_max_minor_converted }}</a>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{# Decrement version patch display #}
|
||||
{% with blender_version_max_patch_converted=blender_version_max_patch|add:"-1" %}
|
||||
<a
|
||||
href="https://www.blender.org/download/releases/{{ blender_version_max_major }}-{{ blender_version_max_minor }}/"
|
||||
title="{{ blender_version_max_major }}.{{ blender_version_max_minor }}.{{ blender_version_max_patch_converted }}">{{ blender_version_max_major }}.{{ blender_version_max_minor }}.{{ blender_version_max_patch_converted }}</a>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% else %}
|
||||
{% trans 'and newer' %}
|
||||
{% endif %}
|
||||
<a href="{{ version.extension.get_review_url }}?report_compatibility_issue&version={{ version.version }}#id_message" title="{% trans 'Report compatibility issue' %}"><i class="i-flag"></i></a>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<a href="{{ version.extension.get_review_url }}?report_compatibility_issue&version={{ version.version }}#id_message" title="{% trans 'Report compatibility issue' %}"><i class="i-flag"></i></a>
|
||||
|
@ -12,11 +12,10 @@
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% if version.blender_version_max %}
|
||||
{% if version.blender_version_max|version_without_patch != version.blender_version_min|version_without_patch %}
|
||||
<a
|
||||
href="https://www.blender.org/download/releases/{{ version.blender_version_max|version_without_patch|replace:".,-" }}/"
|
||||
title="{{ version.blender_version_max }}">{{ version.blender_version_max|version_without_patch }}</a>
|
||||
{% endif %}
|
||||
href="https://www.blender.org/download/releases/{{ version.blender_version_max|replace:".,-" }}/"
|
||||
title="{{ version.blender_version_max }}">{{ version.blender_version_max }}
|
||||
</a>
|
||||
{% else %}
|
||||
{% trans 'Not set' %}
|
||||
{% endif %}
|
||||
|
@ -3,5 +3,4 @@
|
||||
<a
|
||||
class="overflow-visible"
|
||||
href="https://www.blender.org/download/releases/{{ version.blender_version_min|version_without_patch|replace:".,-" }}/"
|
||||
title="{{ version.blender_version_min }}">{{ version.blender_version_min|version_without_patch }}
|
||||
</a>
|
||||
title="{{ version.blender_version_min }}">{{ version.blender_version_min }}</a>
|
||||
|
@ -3,6 +3,14 @@
|
||||
{% load filters %}
|
||||
|
||||
{% block page_title %}{{ extension.name }}{% endblock page_title %}
|
||||
{% block page_description %}{{ extension.latest_version.tagline }}{% endblock page_description %}
|
||||
|
||||
{% block og_title %}{{ extension.name }}{% endblock og_title %}
|
||||
|
||||
{% block og_description %}{{ extension.latest_version.tagline }}{% endblock og_description %}
|
||||
{% block og_image_alt %}{{ extension.latest_version.tagline }}{% endblock og_image_alt %}
|
||||
|
||||
{# TODO: add og:author override (optional) #}
|
||||
|
||||
{% block content %}
|
||||
{% include "files/components/scan_details.html" with suspicious_files=extension.suspicious_files %}
|
||||
|
@ -25,7 +25,7 @@ META_DATA = {
|
||||
"type": "add-on",
|
||||
"license": [LICENSE_GPL3.slug],
|
||||
"blender_version_min": "4.2.0",
|
||||
"blender_version_max": "4.2.0",
|
||||
"blender_version_max": "4.3.0",
|
||||
"schema_version": "1.0.0",
|
||||
"maintainer": "",
|
||||
"tags": [],
|
||||
@ -426,6 +426,23 @@ class ValidateManifestFields(TestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_blender_version_max(self):
|
||||
data = {
|
||||
**self.mandatory_fields,
|
||||
**self.optional_fields,
|
||||
}
|
||||
|
||||
data['blender_version_min'] = '4.2.0'
|
||||
data['blender_version_max'] = '4.2.1'
|
||||
ManifestValidator(data)
|
||||
|
||||
for bad_version in ['4.2.0', '4.1.99']:
|
||||
data['blender_version_max'] = bad_version
|
||||
with self.assertRaises(ValidationError) as e:
|
||||
ManifestValidator(data)
|
||||
self.assertEqual(1, len(e.exception.messages))
|
||||
self.assertIn('blender_version_max', e.exception.messages[0])
|
||||
|
||||
def test_licenses(self):
|
||||
data = {
|
||||
**self.mandatory_fields,
|
||||
|
@ -9,7 +9,7 @@ from teams.models import TeamsUsers
|
||||
|
||||
def _create_extension():
|
||||
extension = create_version(
|
||||
metadata__blender_version_min='2.93.1',
|
||||
metadata__blender_version_min='4.2.0',
|
||||
metadata__name='Test Add-on',
|
||||
metadata__support='https://example.com/issues/',
|
||||
metadata__version='1.3.4',
|
||||
@ -272,10 +272,18 @@ class UpdateVersionViewTest(_BaseTestCase):
|
||||
url,
|
||||
{'release_notes': 'text', 'blender_version_max': '4.2.0'},
|
||||
)
|
||||
# success, redirect
|
||||
self.assertEqual(response2.status_code, 302)
|
||||
# error page (version must be greater), no redirect
|
||||
self.assertEqual(response2.status_code, 200)
|
||||
version.refresh_from_db()
|
||||
self.assertEqual(version.blender_version_max, '4.2.0')
|
||||
self.assertIsNone(version.blender_version_max)
|
||||
|
||||
response3 = self.client.post(
|
||||
url,
|
||||
{'release_notes': 'text', 'blender_version_max': '4.2.1'},
|
||||
)
|
||||
self.assertEqual(response3.status_code, 302)
|
||||
version.refresh_from_db()
|
||||
self.assertEqual(version.blender_version_max, '4.2.1')
|
||||
|
||||
|
||||
class MyExtensionsTest(_BaseTestCase):
|
||||
|
@ -16,8 +16,9 @@ from .mixins import (
|
||||
from extensions.forms import (
|
||||
ExtensionDeleteForm,
|
||||
ExtensionUpdateForm,
|
||||
VersionDeleteForm,
|
||||
VersionForm,
|
||||
VersionDeleteForm,
|
||||
VersionUpdateForm,
|
||||
)
|
||||
from extensions.models import Extension, Version
|
||||
from files.forms import FileForm
|
||||
@ -260,9 +261,9 @@ class NewVersionFinalizeView(LoginRequiredMixin, OwnsFileMixin, CreateView):
|
||||
class UpdateVersionView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
|
||||
"""Update release notes for an existing version."""
|
||||
|
||||
template_name = 'extensions/new_version_finalise.html'
|
||||
form_class = VersionUpdateForm
|
||||
model = Version
|
||||
fields = ['blender_version_max', 'release_notes']
|
||||
template_name = 'extensions/new_version_finalise.html'
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse(
|
||||
|
@ -523,7 +523,36 @@ class VersionMinValidator(VersionValidator):
|
||||
|
||||
|
||||
class VersionMaxValidator(VersionValidator):
|
||||
example = '4.2.0'
|
||||
example = '4.3.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
|
||||
|
||||
min = None
|
||||
max = None
|
||||
try:
|
||||
min = Version(manifest.get('blender_version_min'))
|
||||
except (ValueError, TypeError):
|
||||
# assuming that VersionMinValidator has caught this and reported properly
|
||||
return
|
||||
|
||||
try:
|
||||
max = Version(value)
|
||||
except (ValueError, TypeError):
|
||||
return mark_safe(
|
||||
f'Manifest value error: <code>{name}</code> should follow a '
|
||||
f'<a href="https://semver.org/" target="_blank">semantic version</a>. '
|
||||
f'e.g., "{cls.example}"'
|
||||
)
|
||||
|
||||
if max <= min:
|
||||
return mark_safe(
|
||||
'Manifest value error: <code>blender_version_max</code> should be greater than '
|
||||
'<code>blender_version_min</code>'
|
||||
)
|
||||
|
||||
|
||||
class TaglineValidator(StringValidator):
|
||||
|
Loading…
Reference in New Issue
Block a user