Compare commits

...

13 Commits
v3.8 ... main

Author SHA1 Message Date
Frédéric Péters 48359c4612 translation update
gitea/hobo/pipeline/head This commit looks good Details
2024-05-10 09:09:27 +02:00
Frédéric Péters 5dc90ddfef applications: close <optgroup> tags in filters (#90324)
gitea/hobo/pipeline/head This commit looks good Details
2024-05-10 08:28:05 +02:00
Frédéric Péters cace8739d3 applications: only display filters when there are elements (#90324) 2024-05-10 08:28:05 +02:00
Frédéric Péters 0938ac24a0 applications: give a background to application filter zone (#90324) 2024-05-10 08:28:05 +02:00
Frédéric Péters bf1d7f6bbf applications: limit filter to element types that are used (#90324) 2024-05-10 08:28:05 +02:00
Frédéric Péters 63e98dbd84 misc: enable support for applification of chrono/combo/lingo (#90385)
gitea/hobo/pipeline/head Build queued... Details
2024-05-07 10:14:00 +02:00
Lauréline Guérin 8ba4bc9bea
translation update
gitea/hobo/pipeline/head This commit looks good Details
2024-04-30 09:26:37 +02:00
Lauréline Guérin adc499222b
application: check icon from bundle before install (#88251)
gitea/hobo/pipeline/head This commit looks good Details
2024-04-26 10:54:40 +02:00
Lauréline Guérin 9a9f7b978d application: missing manifest in bundle on install (#88069)
gitea/hobo/pipeline/head This commit looks good Details
2024-04-26 10:49:47 +02:00
Lauréline Guérin 467825f0ba application: javascript for component filtering (#86612)
gitea/hobo/pipeline/head This commit looks good Details
2024-04-26 10:48:50 +02:00
Lauréline Guérin 25be449635
application: post bundle (#89032)
gitea/hobo/pipeline/head This commit looks good Details
2024-04-04 14:24:09 +02:00
Lauréline Guérin cfdd2e8b9c
translation update
gitea/hobo/pipeline/head This commit looks good Details
2024-03-21 13:45:12 +01:00
Lauréline Guérin d123e136ff
application: increment version number on bundle generation (#88373)
gitea/hobo/pipeline/head This commit looks good Details
2024-03-21 10:08:44 +01:00
9 changed files with 467 additions and 127 deletions

View File

@ -13,7 +13,7 @@ recursive-include hobo/maintenance/templates *.html *.txt
recursive-include hobo/locale *.po *.mo
recursive-include hobo/environment/locale *.po *.mo
recursive-include hobo/agent/authentic2/locale *.po *.mo
recursive-include tests *.py *.json
recursive-include tests *.py *.json *.jpeg
include hobo/multitenant/README
include MANIFEST.in
include COPYING

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

@ -127,14 +127,7 @@ class Application(models.Model):
@classmethod
def get_supported_modules(cls):
modules = ['wcs']
if settings.HOBO_APPLICATION_COMBO_SUPPORT:
modules.append('combo')
if settings.HOBO_APPLICATION_CHRONO_SUPPORT:
modules.append('chrono')
if settings.HOBO_APPLICATION_LINGO_SUPPORT:
modules.append('lingo')
return modules
return ['wcs', 'combo', 'chrono', 'lingo']
def save(self, *args, **kwargs):
if not self.slug:
@ -419,7 +412,7 @@ class Version(models.Model):
if service_objects[service['url']].secondary:
continue
url = urllib.parse.urljoin(service['url'], target_url)
response = requests.put(url, data=bundle_content)
response = requests.post(url, files=[('bundle', bundle_content)])
if not response.ok:
raise DeploymentError(exception_message % (service_id, response.status_code))
if not job:
@ -458,7 +451,7 @@ class Version(models.Model):
if service_objects[service['url']].secondary:
continue
url = urllib.parse.urljoin(service['url'], target_url)
response = requests.put(url, data=bundle_content)
response = requests.post(url, files=[('bundle', bundle_content)])
if not response.ok:
raise DeploymentError(exception_message % (service_id, response.status_code))
if not job:

View File

@ -31,9 +31,67 @@
{% block content %}
{% if relations %}
<div class="section padded">
<label>
{% trans "Filter label:" %}
<input type="search" id="name-filter">
</label>
<label>
{% trans "Filter type:" %}
<select id="type-filter">
<option value="">----------</option>
{% for component_type in component_types %}
{% ifchanged component_type.service.title %}
{% if forloop.counter0 > 1 %}</optgroup>{% endif %}
<optgroup label="{{ component_type.service.title }}">
{% endifchanged %}
<option value="{{ component_type.id }}">{{ component_type.text }}</option>
{% endfor %}
</optgroup>
</select>
</label>
</div>
{% endif %}
<script>
$('#name-filter, #type-filter').on('change blur keyup', function() {
const name_val = $('#name-filter').val().toLowerCase();
const type_val = $('#type-filter').val();
if (!name_val && !type_val) {
$('.application-content li').show();
} else {
$('.application-content li').each(function(idx, elem) {
var slugged_text = $(elem).attr('data-slugged-text');
var type = $(elem).attr('data-type');
if (name_val && type_val) {
if (slugged_text.indexOf(name_val) > -1 && type == type_val) {
$(elem).show();
} else {
$(elem).hide();
}
} else if (name_val) {
if (slugged_text.indexOf(name_val) > -1) {
$(elem).show();
} else {
$(elem).hide();
}
} else if (type_val) {
if (type == type_val) {
$(elem).show();
} else {
$(elem).hide();
}
} else {
$(elem).hide();
}
});
}
});
</script>
<ul class="objects-list single-links application-content">
{% for relation in relations %}
<li {% if relation.auto_dependency %}class="auto-dependency"{% endif %}>
<li data-slugged-text="{{ relation.element.name|slugify }}" data-type="{{ relation.element.type }}" {% if relation.auto_dependency %}class="auto-dependency"{% endif %}>
<a {% if relation.element.get_redirect_url %}href="{{ relation.element.get_redirect_url }}"{% endif %}>
{% if relation.error %}<span class="tag tag-error">{{ relation.get_error_status_display }}</span>{% endif %}
{{ relation.element.name }} <span class="extra-info">- {{ relation.element.type_label }}</span>

View File

@ -32,6 +32,7 @@ from django.utils.timezone import localtime, now
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, FormView, ListView, RedirectView, TemplateView
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from PIL import Image, UnidentifiedImageError
from hobo.environment.models import Variable
@ -89,8 +90,16 @@ class ManifestView(TemplateView):
context['last_version'] = context['app'].version_set.order_by('last_update_timestamp').last()
context['types_by_service'] = {}
used_types = {x.element.type for x in context['relations']}
type_labels = {}
object_types = get_object_types()
context['component_types'] = [x for x in object_types if x['id'] in used_types]
if 'roles' in used_types:
roles_dict = [x for x in context['component_types'] if x['id'] == 'roles'][0]
roles_dict['service'] = {'title': _('Others')}
context['component_types'] = [x for x in context['component_types'] if x['id'] != 'roles']
context['component_types'].append(roles_dict)
types = [o['id'] for o in object_types]
for object_type in object_types:
type_labels[object_type['id']] = object_type['singular']
@ -390,23 +399,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()
@ -456,13 +459,20 @@ class Install(FormView):
tar_io = io.BytesIO(self.request.FILES['bundle'].read())
try:
with tarfile.open(fileobj=tar_io) as tar:
manifest = json.loads(tar.extractfile('manifest.json').read().decode())
try:
manifest = json.loads(tar.extractfile('manifest.json').read().decode())
except KeyError:
form.add_error('bundle', _('Invalid tar file, missing manifest.'))
return self.form_invalid(form)
if self.application and self.application.slug != manifest.get('slug'):
form.add_error(
'bundle',
_('Can not update this application, wrong slug (%s).') % manifest.get('slug'),
)
return self.form_invalid(form)
icon = manifest.get('icon')
if icon:
Image.open(tar.extractfile(icon))
app, created = Application.objects.get_or_create(
slug=manifest.get('slug'), defaults={'name': manifest.get('application')}
)
@ -475,7 +485,6 @@ class Install(FormView):
# overwriting a local application and keep on developing it.
app.editable = False
app.save()
icon = manifest.get('icon')
if icon:
app.icon.save(icon, tar.extractfile(icon), save=True)
else:
@ -483,6 +492,9 @@ class Install(FormView):
except tarfile.TarError:
form.add_error('bundle', _('Invalid tar file.'))
return self.form_invalid(form)
except UnidentifiedImageError:
form.add_error('bundle', _('Invalid icon file.'))
return self.form_invalid(form)
# always create a new version on install or if previous version has not the same number
version_number = manifest.get('version_number') or 'unknown'

View File

@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: hobo 0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-12-15 10:40+0100\n"
"PO-Revision-Date: 2023-12-15 10:56+0100\n"
"POT-Creation-Date: 2024-05-10 07:08+0000\n"
"PO-Revision-Date: 2024-05-10 09:09+0200\n"
"Last-Translator: Frederic Peters <fpeters@entrouvert.com>\n"
"Language: French\n"
"MIME-Version: 1.0\n"
@ -20,10 +20,21 @@ msgstr ""
msgid "Version Number"
msgstr "Numéro de version"
#: hobo/applications/forms.py
msgid ""
"The version number consists of two numbers separated by a dot. Example: 1.0"
msgstr ""
"Le numéro de version est formé de deux nombres séparés par un point. "
"Exemple: 1.0"
#: hobo/applications/forms.py
msgid "Version notes"
msgstr "Notes de version"
#: hobo/applications/forms.py
msgid "The version number must be equal to or greater than the previous one."
msgstr "Le numéro de version doit être supérieur ou égal au précédent."
#: hobo/applications/forms.py
msgid "Application"
msgstr "Application"
@ -429,6 +440,14 @@ msgstr "Ces composants seront réinstallés si vous lancez linstallation."
msgid "See all versions"
msgstr "Voir toutes les versions"
#: hobo/applications/templates/hobo/applications/manifest.html
msgid "Filter label:"
msgstr "Filtrer le libellé :"
#: hobo/applications/templates/hobo/applications/manifest.html
msgid "Filter type:"
msgstr "Filtrer le type :"
#: hobo/applications/templates/hobo/applications/manifest.html
msgid "remove"
msgstr "supprimer"
@ -558,6 +577,10 @@ msgstr "Télécharger"
msgid "No versions generated yet."
msgstr "Pas encore de version générée."
#: hobo/applications/views.py
msgid "Others"
msgstr "Autres"
#: hobo/applications/views.py
#, python-format
msgid "Unknown (%s)"
@ -579,6 +602,10 @@ msgstr "Analyse des dépendances"
msgid "Creating application bundle"
msgstr "Création de lapplication"
#: hobo/applications/views.py
msgid "Invalid tar file, missing manifest."
msgstr "Fichier tar invalide, manifest manquant."
#: hobo/applications/views.py
#, python-format
msgid "Can not update this application, wrong slug (%s)."
@ -590,6 +617,10 @@ msgstr ""
msgid "Invalid tar file."
msgstr "Fichier tar invalide."
#: hobo/applications/views.py
msgid "Invalid icon file."
msgstr "Fichier icon invalide."
#: hobo/applications/views.py
msgid "Check installation"
msgstr "Vérification de linstallation"
@ -1116,11 +1147,11 @@ msgstr "Lindexation par des robots nest pas autorisée."
msgid "Contents of robots.txt file:"
msgstr "Contenu du fichier robots.txt :"
#: hobo/settings.py
#: hobo/settings.py multitenants.py
msgid "User Portal"
msgstr "Portail usager"
#: hobo/settings.py
#: hobo/settings.py multitenants.py
msgid "Agent Portal"
msgstr "Portail agent"

View File

@ -243,15 +243,6 @@ HOBO_SERVICES_DISABLED = [
# List of service to show in the create service menu, it overrides HOBO_SERVICES_DISABLED.
HOBO_SERVICES_ENABLED = []
# Support for combo elements in applications
HOBO_APPLICATION_COMBO_SUPPORT = False
# Support for chrono elements in applications
HOBO_APPLICATION_CHRONO_SUPPORT = False
# Support for lingo elements in applications
HOBO_APPLICATION_LINGO_SUPPORT = False
# Phone prefixes by country for phone number as authentication identifier
PHONE_COUNTRY_CODES = {
'32': {'region': 'BE', 'region_desc': _('Belgium')},

BIN
tests/data/black.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

View File

@ -1,7 +1,8 @@
import base64
import copy
import datetime
import io
import json
import os
import random
import re
import tarfile
@ -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
@ -29,6 +31,8 @@ from .test_manager import login
pytestmark = pytest.mark.django_db
TESTS_DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
WCS_AVAILABLE_OBJECTS = {
'data': [
@ -216,7 +220,7 @@ def mocked_http(url, request):
if url.path == '/api/export-import/bundle-import/':
# alter WCS_AVAILABLE_FORMS response with newly installed forms
with tarfile.open(mode='r', fileobj=io.BytesIO(request.body)) as tar:
with tarfile.open(mode='r', fileobj=io.BytesIO(request.original.files[0][1])) as tar:
manifest_json = json.load(tar.extractfile('manifest.json'))
for element in manifest_json.get('elements'):
if element['type'] not in [x['id'] for x in WCS_AVAILABLE_OBJECTS['data']]:
@ -323,7 +327,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,59 +336,31 @@ 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/')
resp.form['icon'] = Upload(
'foo.png',
base64.decodebytes(
b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVQI12NoAAAAggCB3UNq9AAAAABJRU5ErkJggg=='
),
'image/png',
)
with open(os.path.join(TESTS_DATA_DIR, 'black.jpeg'), mode='rb') as icon_fd:
resp.form['icon'] = Upload('foo.jpeg', icon_fd.read(), 'image/jpeg')
resp.form['documentation_url'] = '' # and reset documentation_url
resp.form['visible'] = True
resp = resp.form.submit().follow()
application.refresh_from_db()
assert re.match(r'applications/icons/foo(_\w+)?.png', application.icon.name)
assert re.match(r'applications/icons/foo(_\w+)?.jpeg', application.icon.name)
assert application.documentation_url == ''
assert application.visible is True
@ -394,7 +370,7 @@ def test_create_application(app, admin_user, settings, analyze):
resp = resp.form.submit()
assert 'The icon must be in JPEG or PNG format' in resp
application.refresh_from_db()
assert re.match(r'applications/icons/foo(_\w+)?.png', application.icon.name)
assert re.match(r'applications/icons/foo(_\w+)?.jpeg', application.icon.name)
resp = app.get('/applications/manifest/test/')
resp = resp.click('Generate application bundle')
@ -408,68 +384,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 +475,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 +519,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')
@ -913,7 +1028,7 @@ def get_bundle(with_icon=False):
manifest_json = {
'application': 'Test',
'slug': 'test',
'icon': 'foo.png' if with_icon else None,
'icon': 'foo.jpeg' if with_icon else None,
'description': '',
'documentation_url': 'http://foo.bar',
'version_number': '43.0' if with_icon else '42.0',
@ -939,12 +1054,10 @@ def get_bundle(with_icon=False):
tarinfo.size = len(manifest_fd.getvalue())
tar.addfile(tarinfo, fileobj=manifest_fd)
if with_icon:
icon_fd = io.BytesIO(
b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVQI12NoAAAAggCB3UNq9AAAAABJRU5ErkJggg=='
)
tarinfo = tarfile.TarInfo('foo.png')
tarinfo.size = len(icon_fd.getvalue())
tar.addfile(tarinfo, fileobj=icon_fd)
with open(os.path.join(TESTS_DATA_DIR, 'black.jpeg'), mode='rb') as icon_fd:
tarinfo = tarfile.TarInfo(manifest_json['icon'])
tarinfo.size = 558
tar.addfile(tarinfo, fileobj=icon_fd)
return tar_io.getvalue()
@ -999,7 +1112,7 @@ def test_deploy_application(app, admin_user, settings, app_bundle, app_bundle_wi
application = Application.objects.get(slug='test')
assert application.name == 'Test'
if bundle == app_bundle_with_icon:
assert re.match(r'applications/icons/foo(_\w+)?.png', application.icon.name)
assert re.match(r'applications/icons/foo(_\w+)?.jpeg', application.icon.name)
else:
assert application.icon.name == ''
assert application.documentation_url == 'http://foo.bar'
@ -1182,6 +1295,55 @@ def test_deploy_application(app, admin_user, settings, app_bundle, app_bundle_wi
resp = resp.form.submit()
assert resp.context['form'].errors == {'bundle': ['Invalid tar file.']}
# missing manifest
tar_io = io.BytesIO()
with tarfile.open(mode='w', fileobj=tar_io) as tar:
foo_fd = io.BytesIO(json.dumps({'foo': 'bar'}, indent=2).encode())
tarinfo = tarfile.TarInfo('foo.json')
tarinfo.size = len(foo_fd.getvalue())
tar.addfile(tarinfo, fileobj=foo_fd)
resp = app.get('/applications/')
if action == 'Update':
with StatefulHTTMock(mocked_http2):
resp = resp.click(href='manifest/test/')
resp = resp.click(action)
resp.form['bundle'] = Upload('app.tar', tar_io.getvalue(), 'application/x-tar')
resp = resp.form.submit()
assert resp.context['form'].errors == {'bundle': ['Invalid tar file, missing manifest.']}
# bad icon file
tar_io = io.BytesIO()
with tarfile.open(mode='w', fileobj=tar_io) as tar:
manifest_json = {
'application': 'Test',
'slug': 'test',
'icon': 'foo.png',
'description': '',
'documentation_url': 'http://foo.bar',
'version_number': '42.0',
'version_notes': 'foo bar blah',
'elements': [],
}
manifest_fd = io.BytesIO(json.dumps(manifest_json, indent=2).encode())
tarinfo = tarfile.TarInfo('manifest.json')
tarinfo.size = len(manifest_fd.getvalue())
tar.addfile(tarinfo, fileobj=manifest_fd)
icon_fd = io.BytesIO(b'garbage')
tarinfo = tarfile.TarInfo('foo.png')
tarinfo.size = len(icon_fd.getvalue())
tar.addfile(tarinfo, fileobj=icon_fd)
bundle = tar_io.getvalue()
resp = app.get('/applications/')
if action == 'Update':
with StatefulHTTMock(mocked_http2):
resp = resp.click(href='manifest/test/')
resp = resp.click(action)
resp.form['bundle'] = Upload('app.tar', bundle, 'application/x-tar')
resp = resp.form.submit()
assert resp.context['form'].errors == {'bundle': ['Invalid icon file.']}
def test_update_application(app, admin_user, settings, app_bundle):
Wcs.objects.create(base_url='https://wcs.example.invalid', slug='foobar', title='Foobar')
@ -1910,3 +2072,49 @@ def test_non_editable_application_metadata(app, admin_user):
resp.pyquery('.meta .license').text()
== 'License: GNU Affero General Public License v3 or later (AGPLv3+)'
)
def test_manifest_filter(app, admin_user, settings):
Wcs.objects.create(base_url='https://wcs.example.invalid', slug='foobar', title='Foobar')
settings.KNOWN_SERVICES = {
'wcs': {
'foobar': {
'title': 'Foobar',
'url': 'https://wcs.example.invalid/',
'orig': 'example.org',
'secret': 'xxx',
}
}
}
application = Application.objects.create(name='Test', slug='test')
objects = [
# type, slug, auto_dependency
('forms', 'bar', False),
('workflows', 'foo', True),
('roles', 'baz', True),
]
for _type, slug, auto_dependency in objects:
element = Element.objects.create(
type=_type,
slug=slug,
name=slug.title(),
cache={},
)
Relation.objects.create(application=application, element=element, auto_dependency=auto_dependency)
login(app)
with StatefulHTTMock(mocked_http):
resp = app.get('/applications/manifest/test/')
assert [
(x.tag, x.attrib.get('label') or x.text)
for x in resp.pyquery('#type-filter optgroup, #type-filter option')
] == [
('option', '----------'),
('optgroup', 'Foobar'),
('option', 'Forms'),
('option', 'Workflows'),
('optgroup', 'Others'),
('option', 'Roles'),
]