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 ' 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 ' 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')