export_import: bundle-check endpoint (#87653)
This commit is contained in:
parent
0a0864a4a3
commit
bcd58204c8
|
@ -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,114 @@ class BundleCheck(GenericAPIView):
|
|||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
|
||||
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%s?version1=%s&version2=%s'
|
||||
% (
|
||||
request.build_absolute_uri('/')[:-1],
|
||||
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 +331,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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -612,4 +613,267 @@ def test_bundle_unlink(app, user, bundle):
|
|||
|
||||
def test_bundle_check(app, user):
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
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, 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': [],
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue