applications: compare manifest of two versions (#81283)
gitea/hobo/pipeline/head This commit looks good Details

This commit is contained in:
Lauréline Guérin 2023-09-18 16:34:00 +02:00
parent dd16a73f55
commit 1314fd5f8c
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
9 changed files with 246 additions and 6 deletions

View File

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

View File

@ -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()

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

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

View File

@ -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

View File

@ -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;

View File

@ -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/':