Refactor Extension and Version: explicit constructors from File #191

Merged
Oleg-Komarov merged 6 commits from refactor-models into main 2024-06-20 14:40:41 +02:00
16 changed files with 198 additions and 87 deletions
Showing only changes of commit 61525cc9c3 - Show all commits

View File

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

View File

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

View File

@ -1,8 +1,5 @@
table,
.table
a
text-decoration: underline
th
color: var(--color-text-secondary)

View File

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

View File

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

View File

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

View File

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

View File

@ -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">&mdash;</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">&mdash;</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">&mdash;</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 %}
&nbsp;{% 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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