api: make import/export endpoints generic for other kinds of objects (#82713)

This commit is contained in:
Lauréline Guérin 2023-10-23 15:01:05 +02:00
parent a940ee3961
commit f5e3f625d2
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
3 changed files with 115 additions and 71 deletions

View File

@ -308,6 +308,10 @@ class Agenda(models.Model):
validators=[MaxValueValidator(59)],
)
application_component_type = 'agendas'
application_label_singular = _('Agenda')
application_label_plural = _('Agendas')
class Meta:
ordering = ['label']

View File

@ -19,9 +19,9 @@ import json
import tarfile
from django.contrib.auth.models import Group
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from rest_framework import permissions
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
@ -30,76 +30,104 @@ from chrono.agendas.models import Agenda
from chrono.apps.export_import.models import Application, ApplicationElement
from chrono.manager.utils import import_site
klasses = {klass.application_component_type: klass for klass in [Agenda]}
class Index(GenericAPIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def get(self, request, *args, **kwargs):
response = [
{
'id': 'agendas',
'text': _('Agendas'),
'singular': _('Agenda'),
'urls': {
'list': request.build_absolute_uri(reverse('api-export-import-agendas-list')),
data = []
for klass in klasses.values():
data.append(
{
'id': klass.application_component_type,
'text': klass.application_label_plural,
'singular': klass.application_label_singular,
'urls': {
'list': request.build_absolute_uri(
reverse(
'api-export-import-components-list',
kwargs={'component_type': klass.application_component_type},
)
),
},
},
},
]
)
return Response({'data': response})
return Response({'data': data})
index = Index.as_view()
def get_agenda_bundle_entry(request, agenda, order):
def get_component_bundle_entry(request, component):
return {
'id': str(agenda.slug),
'text': agenda.label,
'type': 'agendas',
'id': str(component.slug),
'text': component.label,
'type': component.application_component_type,
'urls': {
'export': request.build_absolute_uri(
reverse('api-export-import-agenda-export', kwargs={'slug': str(agenda.slug)})
reverse(
'api-export-import-component-export',
kwargs={
'component_type': component.application_component_type,
'slug': str(component.slug),
},
)
),
'dependencies': request.build_absolute_uri(
reverse('api-export-import-agenda-dependencies', kwargs={'slug': str(agenda.slug)})
reverse(
'api-export-import-component-dependencies',
kwargs={
'component_type': component.application_component_type,
'slug': str(component.slug),
},
)
),
'redirect': request.build_absolute_uri(
reverse('api-export-import-agenda-redirect', kwargs={'slug': str(agenda.slug)})
reverse(
'api-export-import-component-redirect',
kwargs={
'component_type': component.application_component_type,
'slug': str(component.slug),
},
)
),
},
}
class ListAgendas(GenericAPIView):
class ListComponents(GenericAPIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def get(self, request, *args, **kwargs):
response = [
get_agenda_bundle_entry(request, x, i) for i, x in enumerate(Agenda.objects.order_by('slug'))
]
klass = klasses[kwargs['component_type']]
response = [get_component_bundle_entry(request, x) for x in klass.objects.order_by('slug')]
return Response({'data': response})
list_agendas = ListAgendas.as_view()
list_components = ListComponents.as_view()
class ExportAgenda(GenericAPIView):
class ExportComponent(GenericAPIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def get(self, request, slug, *args, **kwargs):
serialisation = Agenda.objects.get(slug=slug).export_json()
klass = klasses[kwargs['component_type']]
serialisation = klass.objects.get(slug=slug).export_json()
return Response({'data': serialisation})
export_agenda = ExportAgenda.as_view()
export_component = ExportComponent.as_view()
class AgendaDependencies(GenericAPIView):
class ComponentDependencies(GenericAPIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def get(self, request, slug, *args, **kwargs):
agenda = Agenda.objects.get(slug=slug)
klass = klasses[kwargs['component_type']]
component = klass.objects.get(slug=slug)
def dependency_dict(element):
if isinstance(element, Group):
@ -112,20 +140,21 @@ class AgendaDependencies(GenericAPIView):
# for authentic creating its role summary page.
'uuid': element.role.uuid if hasattr(element, 'role') else None,
}
elif isinstance(element, Agenda):
return get_agenda_bundle_entry(request, element, 0)
return element
return get_component_bundle_entry(request, element)
dependencies = [dependency_dict(x) for x in agenda.get_dependencies() if x]
dependencies = [dependency_dict(x) for x in component.get_dependencies() if x]
return Response({'err': 0, 'data': dependencies})
agenda_dependencies = AgendaDependencies.as_view()
component_dependencies = ComponentDependencies.as_view()
def agenda_redirect(request, slug):
agenda = get_object_or_404(Agenda, slug=slug)
return redirect(reverse('chrono-manager-agenda-view', kwargs={'pk': agenda.pk}))
def component_redirect(request, component_type, slug):
klass = klasses[component_type]
component = get_object_or_404(klass, slug=slug)
if klass == Agenda:
return redirect(reverse('chrono-manager-agenda-view', kwargs={'pk': component.pk}))
raise Http404
class BundleCheck(GenericAPIView):
@ -144,7 +173,7 @@ class BundleImport(GenericAPIView):
def put(self, request, *args, **kwargs):
tar_io = io.BytesIO(request.read())
agendas = []
components = {}
with tarfile.open(fileobj=tar_io) as tar:
manifest = json.loads(tar.extractfile('manifest.json').read().decode())
self.application = Application.update_or_create_from_manifest(
@ -154,34 +183,41 @@ class BundleImport(GenericAPIView):
)
for element in manifest.get('elements'):
if element.get('type') != 'agendas':
if element['type'] not in klasses or element['type'] == 'roles':
continue
agendas.append(
json.loads(tar.extractfile(f'agendas/{element["slug"]}').read().decode()).get('data')
if element['type'] not in components:
components[element['type']] = []
component_content = (
tar.extractfile('%s/%s' % (element['type'], element['slug'])).read().decode()
)
components[element['type']].append(json.loads(component_content).get('data'))
# init cache of application elements, from manifest
self.application_elements = set()
# import agendas
self.do_something(agendas)
self.do_something(components)
# create application elements
self.link_objects(agendas)
self.link_objects(components)
# remove obsolete application elements
self.unlink_obsolete_objects()
return Response({'err': 0})
def do_something(self, agendas):
if agendas:
import_site({'agendas': agendas})
def do_something(self, components):
if components:
import_site(components)
def link_objects(self, agendas):
for agenda in agendas:
try:
existing_agenda = Agenda.objects.get(slug=agenda['slug'])
except Agenda.DoesNotExist:
pass
else:
element = ApplicationElement.update_or_create_for_object(self.application, existing_agenda)
self.application_elements.add(element.content_object)
def link_objects(self, components):
for component_type, component_list in components.items():
klass = klasses[component_type]
for component in component_list:
try:
existing_component = klass.objects.get(slug=component['slug'])
except klass.DoesNotExist:
pass
else:
element = ApplicationElement.update_or_create_for_object(
self.application, existing_component
)
self.application_elements.add(element.content_object)
def unlink_obsolete_objects(self):
known_elements = ApplicationElement.objects.filter(application=self.application)
@ -196,7 +232,7 @@ bundle_import = BundleImport.as_view()
class BundleDeclare(BundleImport):
install = False
def do_something(self, agendas):
def do_something(self, components):
# no installation on declare
pass

View File

@ -20,24 +20,28 @@ from . import api_views
urlpatterns = [
path('export-import/', api_views.index, name='api-export-import'),
path('export-import/agendas/', api_views.list_agendas, name='api-export-import-agendas-list'),
path(
'export-import/agendas/<slug:slug>/',
api_views.export_agenda,
name='api-export-import-agenda-export',
),
path(
'export-import/agendas/<slug:slug>/dependencies/',
api_views.agenda_dependencies,
name='api-export-import-agenda-dependencies',
),
path(
'export-import/agendas/<slug:slug>/redirect/',
api_views.agenda_redirect,
name='api-export-import-agenda-redirect',
),
path('export-import/bundle-check/', api_views.bundle_check),
path('export-import/bundle-declare/', api_views.bundle_declare),
path('export-import/bundle-import/', api_views.bundle_import),
path('export-import/unlink/', api_views.bundle_unlink),
path(
'export-import/<slug:component_type>/',
api_views.list_components,
name='api-export-import-components-list',
),
path(
'export-import/<slug:component_type>/<slug:slug>/',
api_views.export_component,
name='api-export-import-component-export',
),
path(
'export-import/<slug:component_type>/<slug:slug>/dependencies/',
api_views.component_dependencies,
name='api-export-import-component-dependencies',
),
path(
'export-import/<slug:component_type>/<slug:slug>/redirect/',
api_views.component_redirect,
name='api-export-import-component-redirect',
),
]