Refactor Extension and Version: explicit constructors from File #191
@ -125,7 +125,7 @@ and then open `htmlcov/index.html` with your favourite browser.
|
|||||||
|
|
||||||
# Deploy
|
# Deploy
|
||||||
|
|
||||||
See [playbooks](/playbooks/).
|
See [playbooks](playbooks#deploy).
|
||||||
|
|
||||||
# Feature Flags
|
# 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'],
|
'BASE_URL': settings.BLENDER_ID['BASE_URL'],
|
||||||
},
|
},
|
||||||
'canonical_url': request.build_absolute_uri(request.path),
|
'canonical_url': request.build_absolute_uri(request.path),
|
||||||
|
'root_url': request.build_absolute_uri('/'),
|
||||||
'user_is_moderator': user_is_moderator,
|
'user_is_moderator': user_is_moderator,
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
table,
|
table,
|
||||||
.table
|
.table
|
||||||
a
|
|
||||||
text-decoration: underline
|
|
||||||
|
|
||||||
th
|
th
|
||||||
color: var(--color-text-secondary)
|
color: var(--color-text-secondary)
|
||||||
|
|
||||||
|
@ -18,9 +18,40 @@
|
|||||||
<link rel="icon" type="image/png" sizes="16x16" href="{% static 'icons/favicon-16x16.png' %}">
|
<link rel="icon" type="image/png" sizes="16x16" href="{% static 'icons/favicon-16x16.png' %}">
|
||||||
{% endblock head_favicon %}
|
{% endblock head_favicon %}
|
||||||
|
|
||||||
{% block meta %}
|
{% spaceless %}
|
||||||
{% include 'common/components/meta.html' %}
|
{% with default_title="Blender Extensions" %}
|
||||||
{% endblock meta %}
|
{% 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' %}
|
{% 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('cols', None)
|
||||||
bound_field.field.widget.attrs.pop('rows', None)
|
bound_field.field.widget.attrs.pop('rows', None)
|
||||||
return bound_field
|
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 logging
|
||||||
|
import semantic_version
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@ -319,6 +320,26 @@ class VersionForm(forms.ModelForm):
|
|||||||
return self.initial['file']
|
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 VersionDeleteForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = extensions.models.Version
|
model = extensions.models.Version
|
||||||
|
@ -1,30 +1,47 @@
|
|||||||
{% load i18n common %}
|
{% load i18n common %}
|
||||||
|
|
||||||
|
{# 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
|
<a
|
||||||
class="overflow-visible"
|
class="overflow-visible"
|
||||||
href="https://www.blender.org/download/releases/{{ version.blender_version_min|version_without_patch|replace:".,-" }}/"
|
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>
|
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 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 %}
|
|
||||||
{% if version.blender_version_max %}
|
{% if version.blender_version_max %}
|
||||||
{% if version.blender_version_max|version_without_patch != version.blender_version_min|version_without_patch %}
|
{# Check if max version and min version are equal #}
|
||||||
<span class="mx-2">—</span>
|
{% 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
|
<a
|
||||||
href="https://www.blender.org/download/releases/{{ version.blender_version_max|version_without_patch|replace:".,-" }}/"
|
href="https://www.blender.org/download/releases/{{ blender_version_max_major }}-{{ blender_version_max_minor_converted }}/"
|
||||||
title="{{ version.blender_version_max }}">{{ version.blender_version_max|version_without_patch }}</a>
|
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 %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans 'and newer' %}
|
{% trans 'and newer' %}
|
||||||
{% endif %}
|
{% 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>
|
<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 %}
|
|
||||||
|
@ -12,11 +12,10 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if version.blender_version_max %}
|
{% if version.blender_version_max %}
|
||||||
{% if version.blender_version_max|version_without_patch != version.blender_version_min|version_without_patch %}
|
|
||||||
<a
|
<a
|
||||||
href="https://www.blender.org/download/releases/{{ version.blender_version_max|version_without_patch|replace:".,-" }}/"
|
href="https://www.blender.org/download/releases/{{ version.blender_version_max|replace:".,-" }}/"
|
||||||
title="{{ version.blender_version_max }}">{{ version.blender_version_max|version_without_patch }}</a>
|
title="{{ version.blender_version_max }}">{{ version.blender_version_max }}
|
||||||
{% endif %}
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans 'Not set' %}
|
{% trans 'Not set' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -3,5 +3,4 @@
|
|||||||
<a
|
<a
|
||||||
class="overflow-visible"
|
class="overflow-visible"
|
||||||
href="https://www.blender.org/download/releases/{{ version.blender_version_min|version_without_patch|replace:".,-" }}/"
|
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 }}
|
title="{{ version.blender_version_min }}">{{ version.blender_version_min }}</a>
|
||||||
</a>
|
|
||||||
|
@ -3,6 +3,14 @@
|
|||||||
{% load filters %}
|
{% load filters %}
|
||||||
|
|
||||||
{% block page_title %}{{ extension.name }}{% endblock page_title %}
|
{% 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 %}
|
{% block content %}
|
||||||
{% include "files/components/scan_details.html" with suspicious_files=extension.suspicious_files %}
|
{% include "files/components/scan_details.html" with suspicious_files=extension.suspicious_files %}
|
||||||
|
@ -25,7 +25,7 @@ META_DATA = {
|
|||||||
"type": "add-on",
|
"type": "add-on",
|
||||||
"license": [LICENSE_GPL3.slug],
|
"license": [LICENSE_GPL3.slug],
|
||||||
"blender_version_min": "4.2.0",
|
"blender_version_min": "4.2.0",
|
||||||
"blender_version_max": "4.2.0",
|
"blender_version_max": "4.3.0",
|
||||||
"schema_version": "1.0.0",
|
"schema_version": "1.0.0",
|
||||||
"maintainer": "",
|
"maintainer": "",
|
||||||
"tags": [],
|
"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):
|
def test_licenses(self):
|
||||||
data = {
|
data = {
|
||||||
**self.mandatory_fields,
|
**self.mandatory_fields,
|
||||||
|
@ -9,7 +9,7 @@ from teams.models import TeamsUsers
|
|||||||
|
|
||||||
def _create_extension():
|
def _create_extension():
|
||||||
extension = create_version(
|
extension = create_version(
|
||||||
metadata__blender_version_min='2.93.1',
|
metadata__blender_version_min='4.2.0',
|
||||||
metadata__name='Test Add-on',
|
metadata__name='Test Add-on',
|
||||||
metadata__support='https://example.com/issues/',
|
metadata__support='https://example.com/issues/',
|
||||||
metadata__version='1.3.4',
|
metadata__version='1.3.4',
|
||||||
@ -272,10 +272,18 @@ class UpdateVersionViewTest(_BaseTestCase):
|
|||||||
url,
|
url,
|
||||||
{'release_notes': 'text', 'blender_version_max': '4.2.0'},
|
{'release_notes': 'text', 'blender_version_max': '4.2.0'},
|
||||||
)
|
)
|
||||||
# success, redirect
|
# error page (version must be greater), no redirect
|
||||||
self.assertEqual(response2.status_code, 302)
|
self.assertEqual(response2.status_code, 200)
|
||||||
version.refresh_from_db()
|
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):
|
class MyExtensionsTest(_BaseTestCase):
|
||||||
|
@ -16,8 +16,9 @@ from .mixins import (
|
|||||||
from extensions.forms import (
|
from extensions.forms import (
|
||||||
ExtensionDeleteForm,
|
ExtensionDeleteForm,
|
||||||
ExtensionUpdateForm,
|
ExtensionUpdateForm,
|
||||||
VersionDeleteForm,
|
|
||||||
VersionForm,
|
VersionForm,
|
||||||
|
VersionDeleteForm,
|
||||||
|
VersionUpdateForm,
|
||||||
)
|
)
|
||||||
from extensions.models import Extension, Version
|
from extensions.models import Extension, Version
|
||||||
from files.forms import FileForm
|
from files.forms import FileForm
|
||||||
@ -260,9 +261,9 @@ class NewVersionFinalizeView(LoginRequiredMixin, OwnsFileMixin, CreateView):
|
|||||||
class UpdateVersionView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
|
class UpdateVersionView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
|
||||||
"""Update release notes for an existing version."""
|
"""Update release notes for an existing version."""
|
||||||
|
|
||||||
template_name = 'extensions/new_version_finalise.html'
|
form_class = VersionUpdateForm
|
||||||
model = Version
|
model = Version
|
||||||
fields = ['blender_version_max', 'release_notes']
|
template_name = 'extensions/new_version_finalise.html'
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse(
|
return reverse(
|
||||||
|
@ -523,7 +523,36 @@ class VersionMinValidator(VersionValidator):
|
|||||||
|
|
||||||
|
|
||||||
class VersionMaxValidator(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):
|
class TaglineValidator(StringValidator):
|
||||||
|
Loading…
Reference in New Issue
Block a user