application: increment version number on bundle generation (#88373)
gitea/hobo/pipeline/head This commit looks good Details

This commit is contained in:
Lauréline Guérin 2024-03-20 15:51:20 +01:00
parent 159f93a783
commit d123e136ff
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
3 changed files with 240 additions and 81 deletions

View File

@ -15,15 +15,62 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django import forms from django import forms
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from hobo.applications.models import Application, Version from hobo.applications.models import Application, Version
class GenerateForm(forms.Form): class GenerateForm(forms.Form):
number = forms.CharField(label=_('Version Number'), max_length=100) number = forms.RegexField(
label=_('Version Number'),
max_length=100,
regex=r'^\d+\.\d+$',
help_text=_('The version number consists of two numbers separated by a dot. Example: 1.0'),
)
notes = forms.CharField(label=_('Version notes'), widget=forms.Textarea, required=False) notes = forms.CharField(label=_('Version notes'), widget=forms.Textarea, required=False)
def __init__(self, *args, **kwargs):
self.latest_version = kwargs.pop('latest_version')
super().__init__(*args, **kwargs)
if self.latest_version:
try:
old_version = [int(n) for n in self.latest_version.number.split('.')]
except ValueError:
old_version = None
if old_version:
self.initial['number'] = '.'.join(str(n) for n in old_version[:2])
self.initial['notes'] = self.latest_version.notes
def clean_number(self):
number = self.cleaned_data['number']
if not self.latest_version:
return number
try:
old_number = [int(n) for n in self.latest_version.number.split('.')]
except ValueError:
return number
new_number = [int(n) for n in self.cleaned_data['number'].split('.')]
if old_number[:2] > new_number:
raise forms.ValidationError(
_('The version number must be equal to or greater than the previous one.')
)
return number
def get_cleaned_number(self):
number = [int(n) for n in self.cleaned_data['number'].split('.')]
number.append(int(now().strftime('%Y%m%d')))
if not self.latest_version:
return '%s.%s.%s.0' % tuple(number)
try:
old_number = [int(n) for n in self.latest_version.number.split('.')]
except ValueError:
return '%s.%s.%s.0' % tuple(number)
if number != old_number[:3]:
return '%s.%s.%s.0' % tuple(number)
last_part = old_number[3]
return '%s.%s.%s.%s' % (*number, last_part + 1)
class InstallForm(forms.Form): class InstallForm(forms.Form):
bundle = forms.FileField(label=_('Application')) bundle = forms.FileField(label=_('Application'))

View File

@ -390,23 +390,17 @@ class GenerateView(FormView):
form_class = GenerateForm form_class = GenerateForm
template_name = 'hobo/applications/generate.html' template_name = 'hobo/applications/generate.html'
def get_initial(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
self.app = get_object_or_404(Application, slug=self.kwargs['app_slug'], editable=True) self.app = get_object_or_404(Application, slug=self.kwargs['app_slug'], editable=True)
version = self.app.version_set.order_by('last_update_timestamp').last() kwargs['latest_version'] = self.app.version_set.order_by('last_update_timestamp').last()
if version: return kwargs
self.initial['number'] = version.number
self.initial['notes'] = version.notes
return super().get_initial()
def form_valid(self, form): def form_valid(self, form):
app = self.app app = self.app
latest_version = app.version_set.order_by('last_update_timestamp').last() version = Version(application=app)
if latest_version and latest_version.number == form.cleaned_data['number']: version.number = form.get_cleaned_number()
version = latest_version
else:
version = Version(application=app)
version.number = form.cleaned_data['number']
version.notes = form.cleaned_data['notes'] version.notes = form.cleaned_data['notes']
version.save() version.save()

View File

@ -1,5 +1,6 @@
import base64 import base64
import copy import copy
import datetime
import io import io
import json import json
import random import random
@ -9,6 +10,7 @@ import tarfile
import httmock import httmock
import pytest import pytest
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.timezone import now
from httmock import HTTMock from httmock import HTTMock
from pyquery import PyQuery from pyquery import PyQuery
from webtest import Upload from webtest import Upload
@ -323,7 +325,7 @@ def test_create_application(app, admin_user, settings, analyze):
resp = resp.form.submit().follow() resp = resp.form.submit().follow()
assert 'Test Card' in resp.text assert 'Test Card' in resp.text
version = Version.objects.latest('pk') version = Version.objects.latest('pk')
assert version.number == '1.0' assert version.number == '1.0.%s.0' % now().strftime('%Y%m%d')
assert version.notes == 'Foo bar blah.' assert version.notes == 'Foo bar blah.'
resp = resp.click('Download') resp = resp.click('Download')
assert resp.content_type == 'application/x-tar' assert resp.content_type == 'application/x-tar'
@ -332,44 +334,21 @@ def test_create_application(app, admin_user, settings, analyze):
assert b'<carddef/>' in resp.content assert b'<carddef/>' in resp.content
assert b'"icon": null' in resp.content assert b'"icon": null' in resp.content
assert b'"documentation_url": "http://foo.bar"' in resp.content assert b'"documentation_url": "http://foo.bar"' in resp.content
assert b'"version_number": "1.0"' in resp.content assert b'"version_number": "1.0.%s.0"' % now().strftime('%Y%m%d').encode() in resp.content
assert b'"version_notes": "Foo bar blah."' in resp.content assert b'"version_notes": "Foo bar blah."' in resp.content
assert b'"visible": false' in resp.content assert b'"visible": false' in resp.content
resp = app.get('/applications/manifest/test/versions/') resp = app.get('/applications/manifest/test/versions/')
versions = [e.text() for e in resp.pyquery('h3').items()] versions = [e.text() for e in resp.pyquery('h3').items()]
assert versions.count('1.0') == 1 assert versions.count('1.0.%s.0' % now().strftime('%Y%m%d')) == 1
assert resp.text.count('Creating application bundle') == 1 assert resp.text.count('Creating application bundle') == 1
assert 'Compare' not in resp assert 'Compare' not in resp
resp = resp.click(href='/applications/manifest/test/download/%s/' % version.pk) resp = resp.click(href='/applications/manifest/test/download/%s/' % version.pk)
assert resp.content_type == 'application/x-tar' assert resp.content_type == 'application/x-tar'
assert resp.headers['Content-Disposition'] == 'attachment; filename="test-1.0.tar"' assert resp.headers[
assert b'"version_number": "1.0"' in resp.content 'Content-Disposition'
] == 'attachment; filename="test-1.0.%s.0.tar"' % now().strftime('%Y%m%d')
# generate again without changing version number assert b'"version_number": "1.0.%s.0"' % now().strftime('%Y%m%d').encode() in resp.content
resp = app.get('/applications/manifest/test/generate/')
assert resp.form['number'].value == '1.0' # last one
assert resp.form['notes'].value == 'Foo bar blah.' # last one
resp.form['notes'] = 'Foo bar blahha.'
resp = resp.form.submit().follow()
same_version = Version.objects.latest('pk')
assert same_version.number == '1.0'
assert same_version.notes == 'Foo bar blahha.'
assert same_version.pk == version.pk
resp = resp.click('Download')
assert resp.content_type == 'application/x-tar'
assert b'"version_number": "1.0"' in resp.content
assert b'"version_notes": "Foo bar blahha."' in resp.content
resp = app.get('/applications/manifest/test/versions/')
versions = [e.text() for e in resp.pyquery('h3').items()]
assert versions.count('1.0') == 1
assert resp.text.count('Creating application bundle') == 2
assert 'Compare' not in resp
resp = resp.click(href='/applications/manifest/test/download/%s/' % same_version.pk)
assert resp.content_type == 'application/x-tar'
assert resp.headers['Content-Disposition'] == 'attachment; filename="test-1.0.tar"'
assert b'"version_number": "1.0"' in resp.content
# add an icon # add an icon
resp = app.get('/applications/manifest/test/metadata/') resp = app.get('/applications/manifest/test/metadata/')
@ -408,68 +387,88 @@ def test_create_application(app, admin_user, settings, analyze):
assert b'<carddef/>' in resp.content assert b'<carddef/>' in resp.content
assert b'"icon": "foo' in resp.content assert b'"icon": "foo' in resp.content
assert b'"documentation_url": ""' in resp.content assert b'"documentation_url": ""' in resp.content
assert b'"version_number": "2.0"' in resp.content assert b'"version_number": "2.0.%s.0"' % now().strftime('%Y%m%d').encode() in resp.content
assert b'"version_notes": "Foo bar blah. But with an icon."' in resp.content assert b'"version_notes": "Foo bar blah. But with an icon."' in resp.content
assert b'"visible": true' in resp.content assert b'"visible": true' in resp.content
version = Version.objects.latest('pk') version = Version.objects.latest('pk')
assert version.number == '2.0' assert version.number == '2.0.%s.0' % now().strftime('%Y%m%d')
assert version.notes == 'Foo bar blah. But with an icon.' assert version.notes == 'Foo bar blah. But with an icon.'
assert version.pk != same_version.pk
resp = app.get('/applications/manifest/test/versions/') resp = app.get('/applications/manifest/test/versions/')
versions = [e.text() for e in resp.pyquery('h3').items()] versions = [e.text() for e in resp.pyquery('h3').items()]
assert versions.count('1.0') == 1 assert versions.count('1.0.%s.0' % now().strftime('%Y%m%d')) == 1
assert versions.count('2.0') == 1 assert versions.count('2.0.%s.0' % now().strftime('%Y%m%d')) == 1
assert resp.text.count('Compare') == 2 assert resp.text.count('Compare') == 2
assert resp.text.count('Creating application bundle') == 3 assert resp.text.count('Creating application bundle') == 2
resp = resp.click(href='/applications/manifest/test/download/%s/' % same_version.pk) resp = resp.click(
href='/applications/manifest/test/download/%s/' % Version.objects.get(number__startswith='1.0').pk
)
assert resp.content_type == 'application/x-tar' assert resp.content_type == 'application/x-tar'
assert resp.headers['Content-Disposition'] == 'attachment; filename="test-1.0.tar"' assert resp.headers[
assert b'"version_number": "1.0"' in resp.content 'Content-Disposition'
] == 'attachment; filename="test-1.0.%s.0.tar"' % now().strftime('%Y%m%d')
assert b'"version_number": "1.0.%s.0"' % now().strftime('%Y%m%d').encode() in resp.content
resp = app.get('/applications/manifest/test/versions/') resp = app.get('/applications/manifest/test/versions/')
resp = resp.click(href='/applications/manifest/test/download/%s/' % version.pk) resp = resp.click(
href='/applications/manifest/test/download/%s/' % Version.objects.get(number__startswith='2.0').pk
)
assert resp.content_type == 'application/x-tar' assert resp.content_type == 'application/x-tar'
assert resp.headers['Content-Disposition'] == 'attachment; filename="test-2.0.tar"' assert resp.headers[
assert b'"version_number": "2.0"' in resp.content 'Content-Disposition'
] == 'attachment; filename="test-2.0.%s.0.tar"' % now().strftime('%Y%m%d')
assert b'"version_number": "2.0.%s.0"' % now().strftime('%Y%m%d').encode() in resp.content
resp = app.get('/applications/manifest/test/generate/') resp = app.get('/applications/manifest/test/')
assert resp.form['number'].value == '2.0' # last one resp = resp.click('Generate application bundle')
assert resp.form['notes'].value == 'Foo bar blah. But with an icon.' # last one resp.form['number'] = '2.0'
resp.form['number'] = '1.0' # old number resp.form['notes'] = 'Foo bar blah. But with an icon.'
resp = resp.form.submit().follow() resp = resp.form.submit().follow()
new_version = Version.objects.latest('pk') resp = resp.click('Download')
assert new_version.number == '1.0' assert resp.content_type == 'application/x-tar'
assert new_version.notes == 'Foo bar blah. But with an icon.' # uncompressed tar, primitive check of contents
assert new_version.pk != version.pk # new version created assert b'<formdef/>' in resp.content
assert b'<carddef/>' in resp.content
assert b'"icon": "foo' in resp.content
assert b'"documentation_url": ""' in resp.content
assert b'"version_number": "2.0.%s.1"' % now().strftime('%Y%m%d').encode() in resp.content
assert b'"version_notes": "Foo bar blah. But with an icon."' in resp.content
assert b'"visible": true' in resp.content
version = Version.objects.latest('pk')
assert version.number == '2.0.%s.1' % now().strftime('%Y%m%d')
assert version.notes == 'Foo bar blah. But with an icon.'
resp = app.get('/applications/manifest/test/versions/') resp = app.get('/applications/manifest/test/versions/')
versions = [e.text() for e in resp.pyquery('h3').items()] versions = [e.text() for e in resp.pyquery('h3').items()]
assert versions.count('1.0') == 2 assert resp.text.count('Creating application bundle') == 3
assert versions.count('2.0') == 1
assert resp.text.count('Creating application bundle') == 4
assert resp.text.count('Compare') == 3 assert resp.text.count('Compare') == 3
resp = resp.click('Compare', index=0) resp = resp.click('Compare', index=0)
assert 'Compare version 1.0 to:' in resp assert 'Compare version 2.0.%s.1 to:' % now().strftime('%Y%m%d') in resp
version1 = Version.objects.get(number__startswith='1.0')
version2 = Version.objects.filter(number__startswith='2.0').latest('-pk')
version3 = Version.objects.filter(number__startswith='2.0').latest('pk')
assert resp.form['version'].options == [ assert resp.form['version'].options == [
(str(version.pk), False, '2.0'), (str(version2.pk), False, version2.number),
(str(same_version.pk), False, '1.0'), (str(version1.pk), False, version1.number),
] ]
resp = resp.form.submit() resp = resp.form.submit()
assert resp.location.endswith( assert resp.location.endswith(
'/applications/manifest/test/version/compare/?version1=%s&version2=%s' '/applications/manifest/test/version/compare/?version1=%s&version2=%s'
% (new_version.pk, version.pk) % (version3.pk, version2.pk)
) )
resp = resp.follow() resp = resp.follow()
assert 'Version 2.0' in resp assert 'Version %s' % version3.number in resp
assert 'Version 1.0' in resp assert 'Version %s' % version2.number in resp
resp = resp.click('Compare elements definitions') resp = resp.click('Compare elements definitions')
assert 'Version 2.0' in resp assert 'Version %s' % version3.number in resp
assert 'Version 1.0' in resp assert 'Version %s' % version2.number in resp
assert ( assert (
'https://wcs.example.invalid/api/export-import/forms/test-form/redirect/?application=test&version1=2.0&version2=1.0&compare' 'https://wcs.example.invalid/api/export-import/forms/test-form/redirect/?application=test&version1=%s&version2=%s&compare'
% (version2.number, version3.number)
in resp in resp
) )
assert ( assert (
'https://wcs.example.invalid/api/export-import/cards/test-card/redirect/?application=test&version1=2.0&version2=1.0&compare' 'https://wcs.example.invalid/api/export-import/cards/test-card/redirect/?application=test&version1=%s&version2=%s&compare'
% (version2.number, version3.number)
in resp in resp
) )
assert 'Test Form <span class="extra-info">- Form</span>' in resp assert 'Test Form <span class="extra-info">- Form</span>' in resp
@ -479,16 +478,18 @@ def test_create_application(app, admin_user, settings, analyze):
Element.objects.filter(type='cards').delete() Element.objects.filter(type='cards').delete()
resp = app.get( resp = app.get(
'/applications/manifest/test/version/compare/?version1=%s&version2=%s&mode=elements' '/applications/manifest/test/version/compare/?version1=%s&version2=%s&mode=elements'
% (new_version.pk, version.pk) % (version3.pk, version2.pk)
) )
assert 'Version 2.0' in resp assert 'Version %s' % version3.number in resp
assert 'Version 1.0' in resp assert 'Version %s' % version2.number in resp
assert ( assert (
'https://wcs.example.invalid/api/export-import/forms/test-form/redirect/?application=test&version1=2.0&version2=1.0&compare' 'https://wcs.example.invalid/api/export-import/forms/test-form/redirect/?application=test&version1=%s&version2=%s&compare'
% (version2.number, version3.number)
in resp in resp
) )
assert ( assert (
'https://wcs.example.invalid/api/export-import/cards/test-card/redirect/?application=test&version1=2.0&version2=1.0&compare' 'https://wcs.example.invalid/api/export-import/cards/test-card/redirect/?application=test&version1=%s&version2=%s&compare'
% (version2.number, version3.number)
not in resp not in resp
) )
assert 'Test Form <span class="extra-info">- Form</span>' in resp assert 'Test Form <span class="extra-info">- Form</span>' in resp
@ -521,6 +522,123 @@ def test_create_application(app, admin_user, settings, analyze):
app.get('/applications/manifest/test/delete/%s/' % application.relation_set.first().pk, status=404) app.get('/applications/manifest/test/delete/%s/' % application.relation_set.first().pk, status=404)
def test_application_version_number(app, admin_user, settings):
Wcs.objects.create(base_url='https://wcs.example.invalid', slug='foobar', title='Foobar')
settings.KNOWN_SERVICES = {
'wcs': {
'blah': {
# simulate an instance from another collectivity
'title': 'Unknown',
'url': 'https://unknown.example.invalid/',
'orig': 'example.org',
'secret': 'xxx',
},
'foobar': {
'title': 'Foobar',
'url': 'https://wcs.example.invalid/',
'orig': 'example.org',
'secret': 'xxx',
},
}
}
login(app)
application = Application.objects.create(name='Test', slug='test')
resp = app.get('/applications/manifest/test/generate/')
resp.form['number'] = 'X'
resp = resp.form.submit()
assert resp.context['form'].errors == {'number': ['Enter a valid value.']}
resp.form['number'] = 'X.0'
resp = resp.form.submit()
assert resp.context['form'].errors == {'number': ['Enter a valid value.']}
resp.form['number'] = '1.1.1'
resp = resp.form.submit()
assert resp.context['form'].errors == {'number': ['Enter a valid value.']}
resp.form['number'] = '1.1'
with StatefulHTTMock(mocked_http):
resp.form.submit()
last_version = Version.objects.latest('pk')
assert last_version.application == application
assert last_version.number == '1.1.%s.0' % now().strftime('%Y%m%d')
resp = app.get('/applications/manifest/test/generate/')
assert resp.form['number'].value == '1.1'
with StatefulHTTMock(mocked_http):
resp.form.submit()
last_version = Version.objects.latest('pk')
assert last_version.application == application
assert last_version.number == '1.1.%s.1' % now().strftime('%Y%m%d')
resp = app.get('/applications/manifest/test/generate/')
assert resp.form['number'].value == '1.1'
with StatefulHTTMock(mocked_http):
resp.form.submit()
last_version = Version.objects.latest('pk')
assert last_version.application == application
assert last_version.number == '1.1.%s.2' % now().strftime('%Y%m%d')
resp = app.get('/applications/manifest/test/generate/')
resp.form['number'] = '1.0'
resp = resp.form.submit()
assert resp.context['form'].errors == {
'number': ['The version number must be equal to or greater than the previous one.']
}
last_version.number = '1.1.%s.2' % (now() - datetime.timedelta(days=1)).strftime('%Y%m%d')
last_version.save()
resp = app.get('/applications/manifest/test/generate/')
assert resp.form['number'].value == '1.1'
with StatefulHTTMock(mocked_http):
resp.form.submit()
last_version = Version.objects.latest('pk')
assert last_version.application == application
assert last_version.number == '1.1.%s.0' % now().strftime('%Y%m%d')
last_version.number = 'garbage'
last_version.save()
resp = app.get('/applications/manifest/test/generate/')
assert resp.form['number'].value == ''
resp.form['number'] = '1.1'
with StatefulHTTMock(mocked_http):
resp.form.submit()
last_version = Version.objects.latest('pk')
assert last_version.application == application
assert last_version.number == '1.1.%s.0' % now().strftime('%Y%m%d')
last_version.number = '1'
last_version.save()
resp = app.get('/applications/manifest/test/generate/')
assert resp.form['number'].value == '1'
resp.form['number'] = '1.1'
with StatefulHTTMock(mocked_http):
resp.form.submit()
last_version = Version.objects.latest('pk')
assert last_version.application == application
assert last_version.number == '1.1.%s.0' % now().strftime('%Y%m%d')
last_version.number = '1.1'
last_version.save()
resp = app.get('/applications/manifest/test/generate/')
assert resp.form['number'].value == '1.1'
with StatefulHTTMock(mocked_http):
resp.form.submit()
last_version = Version.objects.latest('pk')
assert last_version.application == application
assert last_version.number == '1.1.%s.0' % now().strftime('%Y%m%d')
last_version.number = '1.1.%s.1.1' % now().strftime('%Y%m%d')
last_version.save()
resp = app.get('/applications/manifest/test/generate/')
assert resp.form['number'].value == '1.1'
with StatefulHTTMock(mocked_http):
resp.form.submit()
last_version = Version.objects.latest('pk')
assert last_version.application == application
assert last_version.number == '1.1.%s.2' % now().strftime('%Y%m%d')
def test_manifest_ordering(app, admin_user, settings): def test_manifest_ordering(app, admin_user, settings):
Wcs.objects.create(base_url='https://wcs.example.invalid', slug='foobar', title='Foobar') Wcs.objects.create(base_url='https://wcs.example.invalid', slug='foobar', title='Foobar')