From d123e136ff2f646e2d273b22b3b277c63ec7b671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= Date: Wed, 20 Mar 2024 15:51:20 +0100 Subject: [PATCH] application: increment version number on bundle generation (#88373) --- hobo/applications/forms.py | 49 ++++++- hobo/applications/views.py | 18 +-- tests/test_application.py | 254 +++++++++++++++++++++++++++---------- 3 files changed, 240 insertions(+), 81 deletions(-) diff --git a/hobo/applications/forms.py b/hobo/applications/forms.py index bb6b200..1a675b0 100644 --- a/hobo/applications/forms.py +++ b/hobo/applications/forms.py @@ -15,15 +15,62 @@ # along with this program. If not, see . 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')) diff --git a/hobo/applications/views.py b/hobo/applications/views.py index 7256b94..f0cfa08 100644 --- a/hobo/applications/views.py +++ b/hobo/applications/views.py @@ -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() diff --git a/tests/test_application.py b/tests/test_application.py index 14663bc..d8a138b 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -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'' 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'' 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'' in resp.content + assert b'' 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 - Form' 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 - Form' 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')