export_import: bundle-check endpoint (#87653)

This commit is contained in:
Lauréline Guérin 2024-03-01 13:42:51 +01:00
parent df0e356e75
commit 393a20b87b
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
2 changed files with 376 additions and 3 deletions

View File

@ -19,6 +19,7 @@ import json
import tarfile
from django.contrib.auth.models import Group
from django.contrib.contenttypes.models import ContentType
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
@ -200,7 +201,115 @@ class BundleCheck(GenericAPIView):
permission_classes = (permissions.IsAdminUser,)
def put(self, request, *args, **kwargs):
return Response({'err': 0, 'data': {}})
compare_urls = {
'agendas': 'chrono-manager-agenda-history-compare',
'categories': 'chrono-manager-category-history-compare',
'events_types': 'chrono-manager-events-type-history-compare',
'resources': 'chrono-manager-resource-history-compare',
'unavailability_calendars': 'chrono-manager-unavailability-calendar-history-compare',
}
tar_io = io.BytesIO(request.read())
with tarfile.open(fileobj=tar_io) as tar:
manifest = json.loads(tar.extractfile('manifest.json').read().decode())
application_slug = manifest.get('slug')
application_version = manifest.get('version_number')
if not application_slug or not application_version:
return Response({'data': {}})
differences = []
unknown_elements = []
no_history_elements = []
legacy_elements = []
content_types = ContentType.objects.get_for_models(
*[v for k, v in klasses.items() if k != 'roles']
)
for element in manifest.get('elements'):
component_type = element['type']
if component_type not in klasses or component_type == 'roles':
continue
klass = klasses[component_type]
component_type = klasses_translation.get(component_type, component_type)
try:
component = klass.objects.get(slug=element['slug'])
except klass.DoesNotExist:
unknown_elements.append(
{
'type': component_type,
'slug': element['slug'],
}
)
continue
elements_qs = ApplicationElement.objects.filter(
application__slug=application_slug,
content_type=content_types[klass],
object_id=component.pk,
)
if not elements_qs.exists():
# object exists, but not linked to the application
legacy_elements.append(
{
'type': component.application_component_type,
'slug': str(component.slug),
# information needed here, Relation objects may not exist yet in hobo
'text': component.label,
'url': reverse(
'api-export-import-component-redirect',
kwargs={
'slug': str(component.slug),
'component_type': component.application_component_type,
},
),
}
)
continue
snapshot_for_app = (
klass.get_snapshot_model()
.objects.filter(
instance=component,
application_slug=application_slug,
application_version=application_version,
)
.order_by('timestamp')
.last()
)
if not snapshot_for_app:
# no snapshot for this bundle
no_history_elements.append(
{
'type': element['type'],
'slug': element['slug'],
}
)
continue
last_snapshot = (
klass.get_snapshot_model().objects.filter(instance=component).latest('timestamp')
)
if snapshot_for_app.pk != last_snapshot.pk:
differences.append(
{
'type': element['type'],
'slug': element['slug'],
'url': '%s?version1=%s&version2=%s'
% (
request.build_absolute_uri(
reverse(compare_urls[component_type], args=[component.pk])
),
snapshot_for_app.pk,
last_snapshot.pk,
),
}
)
return Response(
{
'data': {
'differences': differences,
'unknown_elements': unknown_elements,
'no_history_elements': no_history_elements,
'legacy_elements': legacy_elements,
}
}
)
bundle_check = BundleCheck.as_view()
@ -223,7 +332,7 @@ class BundleImport(GenericAPIView):
for element in manifest.get('elements'):
component_type = element['type']
if component_type not in klasses or element['type'] == 'roles':
if component_type not in klasses or component_type == 'roles':
continue
component_type = klasses_translation.get(component_type, component_type)
if component_type not in components:

View File

@ -9,6 +9,7 @@ from django.contrib.contenttypes.models import ContentType
from chrono.agendas.models import Agenda, Category, Desk, EventsType, Resource, UnavailabilityCalendar
from chrono.apps.export_import.models import Application, ApplicationElement
from chrono.apps.snapshot.models import AgendaSnapshot
pytestmark = pytest.mark.django_db
@ -644,4 +645,267 @@ def test_bundle_unlink(app, admin_user, bundle):
def test_bundle_check(app, admin_user):
app.authorization = ('Basic', ('admin', 'admin'))
assert app.put('/api/export-import/bundle-check/').json == {'err': 0, 'data': {}}
bundles = []
for version_number in ['42.0', '42.1']:
bundles.append(create_bundle(app, admin_user, version_number=version_number))
Agenda.objects.all().delete()
Category.objects.all().delete()
Resource.objects.all().delete()
EventsType.objects.all().delete()
UnavailabilityCalendar.objects.all().delete()
incomplete_bundles = []
for manifest_json in [{'slug': 'test'}, {'version_number': '1.0'}]:
tar_io = io.BytesIO()
with tarfile.open(mode='w', fileobj=tar_io) as tar:
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)
incomplete_bundles.append(tar_io.getvalue())
# incorrect bundles, missing information
resp = app.put('/api/export-import/bundle-check/', incomplete_bundles[0])
assert resp.json == {'data': {}}
resp = app.put('/api/export-import/bundle-check/', incomplete_bundles[1])
assert resp.json == {'data': {}}
# not yet imported
resp = app.put('/api/export-import/bundle-check/', bundles[0])
assert resp.json == {
'data': {
'differences': [],
'no_history_elements': [],
'unknown_elements': [
{'slug': 'rdv', 'type': 'agendas'},
{'slug': 'foo', 'type': 'categories'},
{'slug': 'foo', 'type': 'resources'},
{'slug': 'foo', 'type': 'unavailability_calendars'},
{'slug': 'evt', 'type': 'agendas'},
{'slug': 'foo', 'type': 'events_types'},
{'slug': 'virt', 'type': 'agendas'},
{'slug': 'sub', 'type': 'agendas'},
],
'legacy_elements': [],
}
}
# import bundle
resp = app.put('/api/export-import/bundle-import/', bundles[0])
assert Application.objects.count() == 1
assert ApplicationElement.objects.count() == 8
# remove application links
Application.objects.all().delete()
resp = app.put('/api/export-import/bundle-check/', bundles[0])
assert resp.json == {
'data': {
'differences': [],
'no_history_elements': [],
'unknown_elements': [],
'legacy_elements': [
{
'slug': 'rdv',
'text': 'Rdv',
'type': 'agendas',
'url': '/api/export-import/agendas/rdv/redirect/',
},
{
'slug': 'foo',
'text': 'Foo',
'type': 'agendas_categories',
'url': '/api/export-import/agendas_categories/foo/redirect/',
},
{
'slug': 'foo',
'text': 'Foo',
'type': 'resources',
'url': '/api/export-import/resources/foo/redirect/',
},
{
'slug': 'foo',
'text': 'Foo',
'type': 'unavailability_calendars',
'url': '/api/export-import/unavailability_calendars/foo/redirect/',
},
{
'slug': 'evt',
'text': 'Evt',
'type': 'agendas',
'url': '/api/export-import/agendas/evt/redirect/',
},
{
'slug': 'foo',
'text': 'Foo',
'type': 'events_types',
'url': '/api/export-import/events_types/foo/redirect/',
},
{
'slug': 'virt',
'text': 'Virt',
'type': 'agendas',
'url': '/api/export-import/agendas/virt/redirect/',
},
{
'slug': 'sub',
'text': 'Sub',
'type': 'agendas',
'url': '/api/export-import/agendas/sub/redirect/',
},
],
}
}
# import bundle again, recreate links
resp = app.put('/api/export-import/bundle-import/', bundles[0])
assert Application.objects.count() == 1
assert ApplicationElement.objects.count() == 8
# no changes since last import
resp = app.put('/api/export-import/bundle-check/', bundles[0])
assert resp.json == {
'data': {
'differences': [],
'unknown_elements': [],
'no_history_elements': [],
'legacy_elements': [],
}
}
# add local changes
snapshots = {}
for model in [Agenda, Category, EventsType, Resource, UnavailabilityCalendar]:
for instance in model.objects.all():
old_snapshot = model.get_snapshot_model().objects.filter(instance=instance).latest('pk')
instance.take_snapshot(comment='local changes')
new_snapshot = model.get_snapshot_model().objects.filter(instance=instance).latest('pk')
assert new_snapshot.pk > old_snapshot.pk
snapshots[f'{instance.application_component_type}:{instance.slug}'] = (
instance.pk,
old_snapshot.pk,
new_snapshot.pk,
)
# and check
resp = app.put('/api/export-import/bundle-check/', bundles[0])
assert resp.json == {
'data': {
'differences': [
{
'slug': 'rdv',
'type': 'agendas',
'url': 'http://testserver/manage/agendas/%s/history/compare/?version1=%s&version2=%s'
% (
snapshots['agendas:rdv'][0],
snapshots['agendas:rdv'][1],
snapshots['agendas:rdv'][2],
),
},
{
'slug': 'foo',
'type': 'agendas_categories',
'url': 'http://testserver/manage/category/%s/history/compare/?version1=%s&version2=%s'
% (
snapshots['agendas_categories:foo'][0],
snapshots['agendas_categories:foo'][1],
snapshots['agendas_categories:foo'][2],
),
},
{
'slug': 'foo',
'type': 'resources',
'url': 'http://testserver/manage/resource/%s/history/compare/?version1=%s&version2=%s'
% (
snapshots['resources:foo'][0],
snapshots['resources:foo'][1],
snapshots['resources:foo'][2],
),
},
{
'slug': 'foo',
'type': 'unavailability_calendars',
'url': 'http://testserver/manage/unavailability-calendar/%s/history/compare/?version1=%s&version2=%s'
% (
snapshots['unavailability_calendars:foo'][0],
snapshots['unavailability_calendars:foo'][1],
snapshots['unavailability_calendars:foo'][2],
),
},
{
'slug': 'evt',
'type': 'agendas',
'url': 'http://testserver/manage/agendas/%s/history/compare/?version1=%s&version2=%s'
% (
snapshots['agendas:evt'][0],
snapshots['agendas:evt'][1],
snapshots['agendas:evt'][2],
),
},
{
'slug': 'foo',
'type': 'events_types',
'url': 'http://testserver/manage/events-type/%s/history/compare/?version1=%s&version2=%s'
% (
snapshots['events_types:foo'][0],
snapshots['events_types:foo'][1],
snapshots['events_types:foo'][2],
),
},
{
'slug': 'virt',
'type': 'agendas',
'url': 'http://testserver/manage/agendas/%s/history/compare/?version1=%s&version2=%s'
% (
snapshots['agendas:virt'][0],
snapshots['agendas:virt'][1],
snapshots['agendas:virt'][2],
),
},
{
'slug': 'sub',
'type': 'agendas',
'url': 'http://testserver/manage/agendas/%s/history/compare/?version1=%s&version2=%s'
% (
snapshots['agendas:sub'][0],
snapshots['agendas:sub'][1],
snapshots['agendas:sub'][2],
),
},
],
'unknown_elements': [],
'no_history_elements': [],
'legacy_elements': [],
}
}
# update bundle
resp = app.put('/api/export-import/bundle-import/', bundles[1])
# and check
resp = app.put('/api/export-import/bundle-check/', bundles[1])
assert resp.json == {
'data': {
'differences': [],
'unknown_elements': [],
'no_history_elements': [],
'legacy_elements': [],
}
}
# snapshots without application info
AgendaSnapshot.objects.update(application_slug=None, application_version=None)
resp = app.put('/api/export-import/bundle-check/', bundles[1])
assert resp.json == {
'data': {
'differences': [],
'unknown_elements': [],
'no_history_elements': [
{'slug': 'rdv', 'type': 'agendas'},
{'slug': 'evt', 'type': 'agendas'},
{'slug': 'virt', 'type': 'agendas'},
{'slug': 'sub', 'type': 'agendas'},
],
'legacy_elements': [],
}
}