applications: add version number & notes (#69654)

This commit is contained in:
Lauréline Guérin 2022-10-20 14:39:59 +02:00
parent fce0d6b80f
commit d6d9665824
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
9 changed files with 210 additions and 47 deletions

View File

@ -20,6 +20,11 @@ from django.utils.translation import ugettext_lazy as _
from hobo.applications.models import Application
class GenerateForm(forms.Form):
number = forms.CharField(label=_('Version Number'), max_length=100)
notes = forms.CharField(label=_('Version notes'), widget=forms.Textarea, required=False)
class InstallForm(forms.Form):
bundle = forms.FileField(label=_('Application'))

View File

@ -0,0 +1,21 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('applications', '0002_icon'),
]
operations = [
migrations.AddField(
model_name='version',
name='notes',
field=models.TextField(blank=True, verbose_name='Notes'),
),
migrations.AddField(
model_name='version',
name='number',
field=models.CharField(max_length=100, null=True, verbose_name='Number'),
),
]

View File

@ -0,0 +1,23 @@
from django.db import migrations
def forwards(apps, schema_editor):
Application = apps.get_model('applications', 'Application')
for app in Application.objects.all():
for i, version in enumerate(app.version_set.order_by('creation_timestamp')):
if app.editable:
version.number = '%s.0' % (i + 1)
else:
version.number = 'unknown'
version.save(update_fields=['number'])
class Migration(migrations.Migration):
dependencies = [
('applications', '0003_version_num_notes'),
]
operations = [
migrations.RunPython(forwards, reverse_code=migrations.RunPython.noop),
]

View File

@ -0,0 +1,16 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('applications', '0004_version_num_notes'),
]
operations = [
migrations.AlterField(
model_name='version',
name='number',
field=models.CharField(max_length=100, verbose_name='Number'),
),
]

View File

@ -99,6 +99,8 @@ class Relation(models.Model):
class Version(models.Model):
application = models.ForeignKey(Application, on_delete=models.CASCADE)
number = models.CharField(max_length=100, verbose_name=_('Number'))
notes = models.TextField(verbose_name=_('Notes'), blank=True)
bundle = models.FileField(upload_to='applications', blank=True, null=True)
creation_timestamp = models.DateTimeField(default=now)
last_update_timestamp = models.DateTimeField(auto_now=True)

View File

@ -0,0 +1,17 @@
{% extends "hobo/base.html" %}
{% load i18n %}
{% block appbar %}
<h2>{% trans "Generate application bundle" %}</h2>
{% endblock %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<div class="buttons">
<button class="submit-button">{% trans 'Generate' %}</button>
<a class="cancel" href="{% url 'application-manifest' app_slug %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

@ -44,7 +44,7 @@
<div class="buttons">
<a class="pk-button" href="{% url 'application-scandeps' app_slug=app.slug %}">{% trans "Scan dependencies" %}</a>
&nbsp; &nbsp;
<a class="pk-button" href="{% url 'application-generate' app_slug=app.slug %}">{% trans "Generate application bundle" %}</a>
<a class="pk-button" rel="popup" href="{% url 'application-generate' app_slug=app.slug %}">{% trans "Generate application bundle" %}</a>
{% if versions %}
&nbsp; &nbsp;
<a class="pk-button" download href="{% url 'application-download' app_slug=app.slug %}">{% trans "Download" %}</a>

View File

@ -29,7 +29,7 @@ from django.views.generic.edit import CreateView, DeleteView, UpdateView
from hobo.environment.utils import get_installed_services
from .forms import InstallForm, MetadataForm
from .forms import GenerateForm, InstallForm, MetadataForm
from .models import Application, Element, Relation, Version
from .utils import Requests
@ -207,56 +207,88 @@ def scandeps(request, app_slug):
return HttpResponseRedirect(reverse('application-manifest', kwargs={'app_slug': app_slug}))
def generate(request, app_slug):
app = Application.objects.get(slug=app_slug)
elements = scan(app_slug)
class GenerateView(FormView):
form_class = GenerateForm
template_name = 'hobo/applications/generate.html'
version = Version(application=app)
version.save()
def get_initial(self):
self.app = Application.objects.get(slug=self.kwargs['app_slug'])
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()
tar_io = io.BytesIO()
with tarfile.open(mode='w', fileobj=tar_io) as tar:
manifest_json = {
'application': app.name,
'slug': app.slug,
'description': app.description,
'icon': os.path.basename(app.icon.name) if app.icon.name else None,
'elements': [],
}
def form_valid(self, form):
app = self.app
elements = scan(app.slug)
for element, relation in elements.values():
manifest_json['elements'].append(
{
'type': element.type,
'slug': element.slug,
'name': element.name,
'auto-dependency': relation.auto_dependency,
}
)
version = (
app.version_set.filter(number=form.cleaned_data['number'])
.order_by('last_update_timestamp')
.last()
)
if not version:
version = Version(application=app)
version.number = form.cleaned_data['number']
version.notes = form.cleaned_data['notes']
version.save()
response = requests.get(element.cache['urls']['export'])
tarinfo = tarfile.TarInfo('%s/%s' % (element.type, element.slug))
tar_io = io.BytesIO()
with tarfile.open(mode='w', fileobj=tar_io) as tar:
manifest_json = {
'application': app.name,
'slug': app.slug,
'description': app.description,
'icon': os.path.basename(app.icon.name) if app.icon.name else None,
'version_number': version.number,
'version_notes': version.notes,
'elements': [],
}
for element, relation in elements.values():
manifest_json['elements'].append(
{
'type': element.type,
'slug': element.slug,
'name': element.name,
'auto-dependency': relation.auto_dependency,
}
)
response = requests.get(element.cache['urls']['export'])
tarinfo = tarfile.TarInfo('%s/%s' % (element.type, element.slug))
tarinfo.mtime = version.last_update_timestamp.timestamp()
tarinfo.size = int(response.headers['content-length'])
tar.addfile(tarinfo, fileobj=io.BytesIO(response.content))
manifest_fd = io.BytesIO(json.dumps(manifest_json, indent=2).encode())
tarinfo = tarfile.TarInfo('manifest.json')
tarinfo.size = len(manifest_fd.getvalue())
tarinfo.mtime = version.last_update_timestamp.timestamp()
tarinfo.size = int(response.headers['content-length'])
tar.addfile(tarinfo, fileobj=io.BytesIO(response.content))
tar.addfile(tarinfo, fileobj=manifest_fd)
manifest_fd = io.BytesIO(json.dumps(manifest_json, indent=2).encode())
tarinfo = tarfile.TarInfo('manifest.json')
tarinfo.size = len(manifest_fd.getvalue())
tarinfo.mtime = version.last_update_timestamp.timestamp()
tar.addfile(tarinfo, fileobj=manifest_fd)
if app.icon.name:
icon_fd = app.icon.file
tarinfo = tarfile.TarInfo(manifest_json['icon'])
tarinfo.size = icon_fd.size
tarinfo.mtime = version.last_update_timestamp.timestamp()
tar.addfile(tarinfo, fileobj=icon_fd)
if app.icon.name:
icon_fd = app.icon.file
tarinfo = tarfile.TarInfo(manifest_json['icon'])
tarinfo.size = icon_fd.size
tarinfo.mtime = version.last_update_timestamp.timestamp()
tar.addfile(tarinfo, fileobj=icon_fd)
version.bundle.save('%s.tar' % self.kwargs['app_slug'], content=ContentFile(tar_io.getvalue()))
version.save()
version.bundle.save('%s.tar' % app_slug, content=ContentFile(tar_io.getvalue()))
version.save()
return super().form_valid(form)
return HttpResponseRedirect(reverse('application-manifest', kwargs={'app_slug': app_slug}))
def get_context_data(self, **kwargs):
kwargs['app_slug'] = self.kwargs['app_slug']
return super().get_context_data(**kwargs)
def get_success_url(self):
return reverse('application-manifest', kwargs={'app_slug': self.kwargs['app_slug']})
generate = GenerateView.as_view()
def download(request, app_slug):
@ -308,7 +340,12 @@ class Install(FormView):
)
relation.save()
version = Version(application=app)
# always create a new version on install
version = Version(
application=app,
number=manifest.get('version_number') or 'unknown',
notes=manifest.get('version_notes') or '',
)
version.bundle.save('%s.tar' % app.slug, content=ContentFile(tar_io.getvalue()))
version.save()

View File

@ -10,7 +10,7 @@ from httmock import HTTMock
from test_manager import login
from webtest import Upload
from hobo.applications.models import Application, Element, Relation
from hobo.applications.models import Application, Element, Relation, Version
from hobo.environment.models import Authentic, Wcs
pytestmark = pytest.mark.django_db
@ -205,14 +205,37 @@ def test_create_application(app, admin_user, settings, analyze):
resp = resp.click('Scan dependencies').follow()
assert Application.objects.get(slug='test').elements.count() == 2
resp = resp.click('Generate application bundle').follow()
resp = resp.click('Generate application bundle')
resp.form['number'] = '1.0'
resp.form['notes'] = 'Foo bar blah.'
resp = resp.form.submit().follow()
assert 'Test Card' in resp.text
version = Version.objects.latest('pk')
assert version.number == '1.0'
assert version.notes == 'Foo bar blah.'
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": null' in resp.content
assert b'"version_number": "1.0"' in resp.content
assert b'"version_notes": "Foo bar blah."' 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
# add an icon
resp = app.get('/applications/manifest/test/metadata/')
@ -234,13 +257,26 @@ def test_create_application(app, admin_user, settings, analyze):
assert re.match(r'applications/icons/foo(_\w+)?.png', Application.objects.get(slug='test').icon.name)
resp = app.get('/applications/manifest/test/')
resp = resp.click('Generate application bundle').follow()
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()
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'"version_number": "2.0"' in resp.content
assert b'"version_notes": "Foo bar blah. But with an icon."' in resp.content
version = Version.objects.latest('pk')
assert version.number == '2.0'
assert version.notes == 'Foo bar blah. But with an icon.'
assert version.pk != same_version.pk
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
def test_redirect_application_element(app, admin_user, settings):
@ -357,6 +393,8 @@ def get_bundle(with_icon=False):
'slug': 'test',
'icon': 'foo.png' if with_icon else None,
'description': '',
'version_number': '42.0',
'version_notes': 'foo bar blah',
'elements': [
{'type': 'forms', 'slug': 'test', 'name': 'test', 'auto-dependency': False},
{'type': 'blocks', 'slug': 'test', 'name': 'test', 'auto-dependency': True},
@ -410,6 +448,10 @@ def test_deploy_application(app, admin_user, settings, app_bundle, app_bundle_wi
assert re.match(r'applications/icons/foo(_\w+)?.png', app.icon.name)
else:
assert app.icon.name == ''
assert app.version_set.count() == i + 1
version = app.version_set.all()[i]
assert version.number == '42.0'
assert version.notes == 'foo bar blah'
assert app.elements.count() == 3