applications: add version number & notes (#69654)
This commit is contained in:
parent
fce0d6b80f
commit
d6d9665824
|
@ -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'))
|
||||
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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 %}
|
|
@ -44,7 +44,7 @@
|
|||
<div class="buttons">
|
||||
<a class="pk-button" href="{% url 'application-scandeps' app_slug=app.slug %}">{% trans "Scan dependencies" %}</a>
|
||||
|
||||
<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 %}
|
||||
|
||||
<a class="pk-button" download href="{% url 'application-download' app_slug=app.slug %}">{% trans "Download" %}</a>
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue