application: increment version number on bundle generation (#88373) #119

Merged
lguerin merged 1 commits from wip/88373-application-version-number into main 2024-03-21 10:21:59 +01:00
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')))
pmarillonnet marked this conversation as resolved Outdated

Dans la doc du format mangé par strftime (https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior) on dirait que %M c’est les minutes de l’heure en cours, sur deux chiffres entre 00 et 59. Je crois que c’est %m qu’on veut ici.

Dans la doc du format mangé par `strftime` (https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior) on dirait que `%M` c’est les minutes de l’heure en cours, sur deux chiffres entre 00 et 59. Je crois que c’est `%m` qu’on veut ici.

zut, oui :)

zut, oui :)
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()
pmarillonnet marked this conversation as resolved Outdated

Pas compris pourquoi on prend le soin d’aller chercher la version dernièrement modifiée alors que le soin apporté à la cohérence des numéros de version fait qu’il suffirait d’aller prendre celle dont le champ number est le plus élevé (?)

Pas compris pourquoi on prend le soin d’aller chercher la version dernièrement modifiée alors que le soin apporté à la cohérence des numéros de version fait qu’il suffirait d’aller prendre celle dont le champ `number` est le plus élevé (?)

C'est pareil, la dernière modifiée est désormais aussi celle qui a le numéro de version le plus élevé. Avant c'était saisie libre on pouvait très bien revenir en arrière (et mettre "toto" si on voulait)

C'est pareil, la dernière modifiée est désormais aussi celle qui a le numéro de version le plus élevé. Avant c'était saisie libre on pouvait très bien revenir en arrière (et mettre "toto" si on voulait)

Ok et donc j’imagine qu’il y a encore en base des numéros saisis librement et qui échappent à cette conservation du tri <horodate de dernière modification> ↔ <numéro de version>. Ok.

Ok et donc j’imagine qu’il y a encore en base des numéros saisis librement et qui échappent à cette conservation du tri <horodate de dernière modification> ↔ <numéro de version>. Ok.
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')