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/>.
from django import forms
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from hobo.applications.models import Application, Version
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)
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):
bundle = forms.FileField(label=_('Application'))

View File

@ -390,23 +390,17 @@ class GenerateView(FormView):
form_class = GenerateForm
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)
version = self.app.version_set.order_by('last_update_timestamp').last()
if version:
self.initial['number'] = version.number
self.initial['notes'] = version.notes
return super().get_initial()
kwargs['latest_version'] = self.app.version_set.order_by('last_update_timestamp').last()
return kwargs
def form_valid(self, form):
app = self.app
latest_version = app.version_set.order_by('last_update_timestamp').last()
if latest_version and latest_version.number == form.cleaned_data['number']:
version = latest_version
else:
version = Version(application=app)
version.number = form.cleaned_data['number']
version = Version(application=app)
version.number = form.get_cleaned_number()
version.notes = form.cleaned_data['notes']
version.save()

View File

@ -1,5 +1,6 @@
import base64
import copy
import datetime
import io
import json
import random
@ -9,6 +10,7 @@ import tarfile
import httmock
import pytest
from django.contrib.contenttypes.models import ContentType
from django.utils.timezone import now
from httmock import HTTMock
from pyquery import PyQuery
from webtest import Upload
@ -323,7 +325,7 @@ def test_create_application(app, admin_user, settings, analyze):
resp = resp.form.submit().follow()
assert 'Test Card' in resp.text
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.'
resp = resp.click('Download')
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'"icon": null' 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'"visible": false' 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 versions.count('1.0.%s.0' % now().strftime('%Y%m%d')) == 1
assert resp.text.count('Creating application bundle') == 1
assert 'Compare' not in resp
resp = resp.click(href='/applications/manifest/test/download/%s/' % 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
# generate again without changing version number
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
assert resp.headers[
'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
# add an icon
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'"icon": "foo' 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'"visible": true' in resp.content
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.pk != same_version.pk
resp = app.get('/applications/manifest/test/versions/')
versions = [e.text() for e in resp.pyquery('h3').items()]
assert versions.count('1.0') == 1
assert versions.count('2.0') == 1
assert versions.count('1.0.%s.0' % now().strftime('%Y%m%d')) == 1
assert versions.count('2.0.%s.0' % now().strftime('%Y%m%d')) == 1
assert resp.text.count('Compare') == 2
assert resp.text.count('Creating application bundle') == 3
resp = resp.click(href='/applications/manifest/test/download/%s/' % same_version.pk)
assert resp.text.count('Creating application bundle') == 2
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.headers['Content-Disposition'] == 'attachment; filename="test-1.0.tar"'
assert b'"version_number": "1.0"' in resp.content
assert resp.headers[
'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 = 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.headers['Content-Disposition'] == 'attachment; filename="test-2.0.tar"'
assert b'"version_number": "2.0"' in resp.content
assert resp.headers[
'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/')
assert resp.form['number'].value == '2.0' # last one
assert resp.form['notes'].value == 'Foo bar blah. But with an icon.' # last one
resp.form['number'] = '1.0' # old number
resp = app.get('/applications/manifest/test/')
resp = resp.click('Generate application bundle')
resp.form['number'] = '2.0'
resp.form['notes'] = 'Foo bar blah. But with an icon.'
resp = resp.form.submit().follow()
new_version = Version.objects.latest('pk')
assert new_version.number == '1.0'
assert new_version.notes == 'Foo bar blah. But with an icon.'
assert new_version.pk != version.pk # new version created
resp = resp.click('Download')
assert resp.content_type == 'application/x-tar'
# uncompressed tar, primitive check of contents
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/')
versions = [e.text() for e in resp.pyquery('h3').items()]
assert versions.count('1.0') == 2
assert versions.count('2.0') == 1
assert resp.text.count('Creating application bundle') == 4
assert resp.text.count('Creating application bundle') == 3
assert resp.text.count('Compare') == 3
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 == [
(str(version.pk), False, '2.0'),
(str(same_version.pk), False, '1.0'),
(str(version2.pk), False, version2.number),
(str(version1.pk), False, version1.number),
]
resp = resp.form.submit()
assert resp.location.endswith(
'/applications/manifest/test/version/compare/?version1=%s&version2=%s'
% (new_version.pk, version.pk)
% (version3.pk, version2.pk)
)
resp = resp.follow()
assert 'Version 2.0' in resp
assert 'Version 1.0' in resp
assert 'Version %s' % version3.number in resp
assert 'Version %s' % version2.number in resp
resp = resp.click('Compare elements definitions')
assert 'Version 2.0' in resp
assert 'Version 1.0' in resp
assert 'Version %s' % version3.number in resp
assert 'Version %s' % version2.number in resp
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
)
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
)
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()
resp = app.get(
'/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 1.0' in resp
assert 'Version %s' % version3.number in resp
assert 'Version %s' % version2.number in resp
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
)
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
)
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)
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):
Wcs.objects.create(base_url='https://wcs.example.invalid', slug='foobar', title='Foobar')