applications: compare manifest of two versions (#81283) #74
|
@ -17,7 +17,7 @@
|
|||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from hobo.applications.models import Application
|
||||
from hobo.applications.models import Application, Version
|
||||
|
||||
|
||||
class GenerateForm(forms.Form):
|
||||
|
@ -43,3 +43,18 @@ class MetadataForm(forms.ModelForm):
|
|||
if hasattr(value, 'content_type') and value.content_type not in ('image/jpeg', 'image/png'):
|
||||
raise forms.ValidationError(_('The icon must be in JPEG or PNG format.'))
|
||||
return value
|
||||
|
||||
|
||||
class VersionSelectForm(forms.Form):
|
||||
version = forms.ModelChoiceField(queryset=Version.objects.none(), empty_label=None)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.application = kwargs.pop('application')
|
||||
self.version = kwargs.pop('version')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['version'].label = _('Compare version %s to') % self.version.number
|
||||
self.fields['version'].queryset = (
|
||||
Version.objects.filter(application=self.application)
|
||||
.exclude(pk=self.version.pk)
|
||||
.order_by('-last_update_timestamp')
|
||||
)
|
||||
|
|
|
@ -299,6 +299,9 @@ class Version(models.Model):
|
|||
def __repr__(self):
|
||||
return '<Version %s>' % self.application.slug
|
||||
|
||||
def __str__(self):
|
||||
return self.number
|
||||
|
||||
def create_bundle(self, job=None):
|
||||
app = self.application
|
||||
elements = app.scandeps()
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
{% extends "hobo/applications/versions.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'application-version-compare' app_slug=app.slug %}">{% trans "Compare versions" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans "Compare versions" %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p class="version-description">{{ fromdesc|safe }} ➔ {{ todesc|safe }}</p>
|
||||
<div class="diff">
|
||||
{{ diff_serialization|safe }}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,22 @@
|
|||
{% extends "hobo/applications/versions.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'application-version-select' app_slug=app.slug version_pk=version.pk%}">{% trans "Compare" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans "Compare" %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<div class="buttons">
|
||||
<button class="submit-button">{% trans 'Show differences' %}</button>
|
||||
<a class="cancel" href="{% url 'application-versions' app_slug=app.slug %}">{% trans 'Cancel' %}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -37,8 +37,10 @@
|
|||
</ul>
|
||||
{% endif %}
|
||||
<div class="buttons">
|
||||
<a class="button" download href="{% url 'application-download' app_slug=app.slug version_pk=version.pk %}"
|
||||
>{% trans "Download" %}</a>
|
||||
<a class="button" download href="{% url 'application-download' app_slug=app.slug version_pk=version.pk %}">{% trans "Download" %}</a>
|
||||
{% if object_list|length > 1 %}
|
||||
<a class="button" rel=popup href="{% url 'application-version-select' app_slug=app.slug version_pk=version.pk %}">{% trans "Compare" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
|
@ -27,6 +27,16 @@ urlpatterns = [
|
|||
re_path(r'^manifest/(?P<app_slug>[\w-]+)/update/$', views.update, name='application-update'),
|
||||
re_path(r'^manifest/(?P<app_slug>[\w-]+)/refresh/$', views.refresh, name='application-refresh'),
|
||||
re_path(r'^manifest/(?P<app_slug>[\w-]+)/versions/$', views.versions, name='application-versions'),
|
||||
re_path(
|
||||
r'^manifest/(?P<app_slug>[\w-]+)/version/(?P<version_pk>\d+)/select/$',
|
||||
views.version_select,
|
||||
name='application-version-select',
|
||||
),
|
||||
re_path(
|
||||
r'^manifest/(?P<app_slug>[\w-]+)/version/compare/$',
|
||||
views.version_compare,
|
||||
name='application-version-compare',
|
||||
),
|
||||
re_path(r'^manifest/(?P<app_slug>[\w-]+)/metadata/$', views.metadata, name='application-metadata'),
|
||||
re_path(r'^manifest/(?P<app_slug>[\w-]+)/scandeps/$', views.scandeps, name='application-scandeps'),
|
||||
re_path(r'^manifest/(?P<app_slug>[\w-]+)/generate/$', views.generate, name='application-generate'),
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import dataclasses
|
||||
import difflib
|
||||
import io
|
||||
import json
|
||||
import tarfile
|
||||
|
@ -22,18 +23,19 @@ import tarfile
|
|||
from django.contrib import messages
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db.models import Prefetch
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.text import slugify
|
||||
from django.utils.timezone import now
|
||||
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 hobo.environment.models import Variable
|
||||
|
||||
from .forms import GenerateForm, InstallForm, MetadataForm
|
||||
from .forms import GenerateForm, InstallForm, MetadataForm, VersionSelectForm
|
||||
from .models import (
|
||||
STATUS_CHOICES,
|
||||
Application,
|
||||
|
@ -128,6 +130,106 @@ class VersionsView(ListView):
|
|||
versions = VersionsView.as_view()
|
||||
|
||||
|
||||
class VersionSelectView(FormView):
|
||||
template_name = 'hobo/applications/version_select.html'
|
||||
form_class = VersionSelectForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.application = get_object_or_404(Application, slug=self.kwargs['app_slug'])
|
||||
self.version = get_object_or_404(Version, pk=self.kwargs['version_pk'], application=self.application)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['application'] = self.application
|
||||
kwargs['version'] = self.version
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs['app'] = self.application
|
||||
kwargs['version'] = self.version
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
return HttpResponseRedirect(
|
||||
'%s?version1=%s&version2=%s'
|
||||
% (
|
||||
reverse('application-version-compare', kwargs={'app_slug': self.application.slug}),
|
||||
self.version.pk,
|
||||
form.cleaned_data['version'].pk,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
version_select = VersionSelectView.as_view()
|
||||
|
||||
|
||||
class VersionCompareView(DetailView):
|
||||
model = Application
|
||||
template_name = 'hobo/applications/version_compare.html'
|
||||
slug_url_kwarg = 'app_slug'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs['app'] = self.object
|
||||
|
||||
id1 = self.request.GET.get('version1')
|
||||
id2 = self.request.GET.get('version2')
|
||||
if not id1 or not id2:
|
||||
raise Http404
|
||||
|
||||
version1 = get_object_or_404(Version, pk=id1, application=self.object)
|
||||
version2 = get_object_or_404(Version, pk=id2, application=self.object)
|
||||
|
||||
if version1.last_update_timestamp > version2.last_update_timestamp:
|
||||
version1, version2 = version2, version1
|
||||
|
||||
kwargs['version1'] = version1
|
||||
kwargs['version2'] = version2
|
||||
kwargs['fromdesc'] = self.get_version_desc(version1)
|
||||
kwargs['todesc'] = self.get_version_desc(version2)
|
||||
kwargs.update(self.get_compare_context(version1, version2))
|
||||
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_manifest(self, version):
|
||||
bundle = version.bundle.read()
|
||||
tar_io = io.BytesIO(bundle)
|
||||
with tarfile.open(fileobj=tar_io) as tar:
|
||||
manifest = json.loads(tar.extractfile('manifest.json').read().decode())
|
||||
# sort elements, to compare ordered lists
|
||||
elements = manifest.get('elements') or []
|
||||
object_types = get_object_types()
|
||||
types = [o['id'] for o in object_types]
|
||||
manifest['elements'] = sorted(
|
||||
elements, key=lambda a: (a['auto-dependency'], types.index(a['type']), slugify(a['name']))
|
||||
)
|
||||
return manifest
|
||||
|
||||
def get_compare_context(self, version1, version2):
|
||||
manifest1 = self.get_manifest(version1)
|
||||
s1 = json.dumps(manifest1, sort_keys=True, indent=2)
|
||||
manifest2 = self.get_manifest(version2)
|
||||
s2 = json.dumps(manifest2, sort_keys=True, indent=2)
|
||||
diff_serialization = difflib.HtmlDiff(wrapcolumn=160).make_table(
|
||||
fromlines=s1.splitlines(True),
|
||||
tolines=s2.splitlines(True),
|
||||
)
|
||||
|
||||
return {
|
||||
'diff_serialization': diff_serialization,
|
||||
}
|
||||
|
||||
def get_version_desc(self, version):
|
||||
return '{name} {number} ({timestamp})'.format(
|
||||
name=_('Version'),
|
||||
number=version.number,
|
||||
timestamp=date_format(localtime(version.last_update_timestamp), format='DATETIME_FORMAT'),
|
||||
)
|
||||
|
||||
|
||||
version_compare = VersionCompareView.as_view()
|
||||
|
||||
|
||||
class MetadataView(UpdateView):
|
||||
template_name = 'hobo/applications/edit-metadata.html'
|
||||
model = Application
|
||||
|
|
|
@ -379,10 +379,60 @@ ul.objects-list.application-content {
|
|||
}
|
||||
.buttons {
|
||||
justify-content: flex-end;
|
||||
.button {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.version-description {
|
||||
font-size: 80%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
table.diff {
|
||||
background: white;
|
||||
border: 1px solid #f3f3f3;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
colgroup, thead, tbody, td {
|
||||
border: 1px solid #f3f3f3;
|
||||
}
|
||||
tbody tr:nth-child(even) {
|
||||
background: #fdfdfd;
|
||||
}
|
||||
th, td {
|
||||
max-width: 30vw;
|
||||
/* it will not actually limit width as the table is set to
|
||||
* expand to 100% but it will prevent one side getting wider
|
||||
*/
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: top;
|
||||
}
|
||||
.diff_header {
|
||||
background: #f7f7f7;
|
||||
}
|
||||
td.diff_header {
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
color: #606060;
|
||||
}
|
||||
.diff_next {
|
||||
display: none;
|
||||
}
|
||||
.diff_add {
|
||||
background-color: #aaffaa;
|
||||
}
|
||||
.diff_chg {
|
||||
background-color: #ffff77;
|
||||
}
|
||||
.diff_sub {
|
||||
background-color: #ffaaaa;
|
||||
}
|
||||
}
|
||||
|
||||
div.app-parameters--values label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
|
|
|
@ -332,6 +332,7 @@ def test_create_application(app, admin_user, settings, analyze):
|
|||
versions = [e.text() for e in resp.pyquery('h3').items()]
|
||||
assert versions.count('1.0') == 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"'
|
||||
|
@ -356,6 +357,7 @@ def test_create_application(app, admin_user, settings, analyze):
|
|||
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"'
|
||||
|
@ -410,6 +412,7 @@ def test_create_application(app, admin_user, settings, analyze):
|
|||
versions = [e.text() for e in resp.pyquery('h3').items()]
|
||||
assert versions.count('1.0') == 1
|
||||
assert versions.count('2.0') == 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.content_type == 'application/x-tar'
|
||||
|
@ -435,6 +438,21 @@ def test_create_application(app, admin_user, settings, analyze):
|
|||
assert versions.count('1.0') == 2
|
||||
assert versions.count('2.0') == 1
|
||||
assert resp.text.count('Creating application bundle') == 4
|
||||
assert resp.text.count('Compare') == 3
|
||||
resp = resp.click('Compare', index=0)
|
||||
assert 'Compare version 1.0 to:' in resp
|
||||
assert resp.form['version'].options == [
|
||||
(str(version.pk), False, '2.0'),
|
||||
(str(same_version.pk), False, '1.0'),
|
||||
]
|
||||
resp = resp.form.submit()
|
||||
assert resp.location.endswith(
|
||||
'/applications/manifest/test/version/compare/?version1=%s&version2=%s'
|
||||
% (new_version.pk, version.pk)
|
||||
)
|
||||
resp = resp.follow()
|
||||
assert 'Version 2.0' in resp
|
||||
assert 'Version 1.0' in resp
|
||||
|
||||
def response_content(url, request):
|
||||
if url.path == '/api/export-import/bundle-declare/':
|
||||
|
|
Loading…
Reference in New Issue