api: export/import models and views (#86291)
This commit is contained in:
parent
d5d447b9df
commit
439248a7e2
|
@ -45,6 +45,10 @@ class Agenda(models.Model):
|
|||
on_delete=models.SET_NULL,
|
||||
)
|
||||
|
||||
application_component_type = 'lingo_agendas'
|
||||
application_label_singular = _('Agenda (payment)')
|
||||
application_label_plural = _('Agendas (payment)')
|
||||
|
||||
class Meta:
|
||||
ordering = ['label']
|
||||
|
||||
|
@ -60,6 +64,20 @@ class Agenda(models.Model):
|
|||
def base_slug(self):
|
||||
return slugify(self.label)
|
||||
|
||||
def get_dependencies(self):
|
||||
yield self.check_type_group
|
||||
yield self.regie
|
||||
if not settings.KNOWN_SERVICES.get('chrono'):
|
||||
return
|
||||
chrono = list(settings.KNOWN_SERVICES['chrono'].values())[0]
|
||||
chrono_url = chrono.get('url') or ''
|
||||
urls = {
|
||||
'export': f'{chrono_url}api/export-import/agendas/{self.slug}/',
|
||||
'dependencies': f'{chrono_url}api/export-import/agendas/{self.slug}/dependencies/',
|
||||
'redirect': f'{chrono_url}api/export-import/agendas/{self.slug}/redirect/',
|
||||
}
|
||||
yield {'type': 'agendas', 'id': self.slug, 'text': self.label, 'urls': urls}
|
||||
|
||||
def export_json(self):
|
||||
return {
|
||||
'slug': self.slug,
|
||||
|
@ -111,6 +129,10 @@ class CheckTypeGroup(models.Model):
|
|||
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
|
||||
label = models.CharField(_('Label'), max_length=150)
|
||||
|
||||
application_component_type = 'check_type_groups'
|
||||
application_label_singular = _('Check type group')
|
||||
application_label_plural = _('Check type groups')
|
||||
|
||||
class Meta:
|
||||
ordering = ['label']
|
||||
|
||||
|
@ -126,6 +148,9 @@ class CheckTypeGroup(models.Model):
|
|||
def base_slug(self):
|
||||
return slugify(self.label)
|
||||
|
||||
def get_dependencies(self):
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def import_json(cls, data):
|
||||
check_types = data.pop('check_types', [])
|
||||
|
|
|
@ -0,0 +1,317 @@
|
|||
# lingo - payment and billing system
|
||||
# Copyright (C) 2022-2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import io
|
||||
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
|
||||
|
||||
from lingo.agendas.chrono import refresh_agendas
|
||||
from lingo.agendas.models import Agenda, CheckTypeGroup
|
||||
from lingo.export_import.models import Application, ApplicationElement
|
||||
from lingo.invoicing.models import Payer, Regie
|
||||
from lingo.invoicing.utils import import_site as invoicing_import_site
|
||||
from lingo.pricing.models import CriteriaCategory, Pricing
|
||||
from lingo.pricing.utils import import_site as pricing_import_site
|
||||
|
||||
klasses = {
|
||||
klass.application_component_type: klass
|
||||
for klass in [Pricing, CriteriaCategory, Agenda, CheckTypeGroup, CriteriaCategory, Payer, Regie]
|
||||
}
|
||||
klasses['roles'] = Group
|
||||
klasses_translation = {
|
||||
'lingo_agendas': 'agendas', # agendas type is already used in chrono for Agenda
|
||||
}
|
||||
klasses_translation_reverse = {v: k for k, v in klasses_translation.items()}
|
||||
|
||||
|
||||
def import_site(components):
|
||||
# refresh agendas from chrono first
|
||||
refresh_agendas()
|
||||
# then import regies, payers
|
||||
invoicing_import_site(components)
|
||||
# and pricing components
|
||||
pricing_import_site(components)
|
||||
|
||||
|
||||
class Index(GenericAPIView):
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
data = []
|
||||
for klass in klasses.values():
|
||||
if klass == Group:
|
||||
data.append(
|
||||
{
|
||||
'id': 'roles',
|
||||
'text': _('Roles'),
|
||||
'singular': _('Role'),
|
||||
'urls': {
|
||||
'list': request.build_absolute_uri(
|
||||
reverse(
|
||||
'api-export-import-components-list',
|
||||
kwargs={'component_type': 'roles'},
|
||||
)
|
||||
),
|
||||
},
|
||||
'minor': True,
|
||||
}
|
||||
)
|
||||
continue
|
||||
component_type = {
|
||||
'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},
|
||||
)
|
||||
),
|
||||
},
|
||||
}
|
||||
if klass not in [Pricing]:
|
||||
component_type['minor'] = True
|
||||
data.append(component_type)
|
||||
|
||||
return Response({'data': data})
|
||||
|
||||
|
||||
index = Index.as_view()
|
||||
|
||||
|
||||
def get_component_bundle_entry(request, component):
|
||||
if isinstance(component, Group):
|
||||
return {
|
||||
'id': component.role.slug if hasattr(component, 'role') else component.id,
|
||||
'text': component.name,
|
||||
'type': 'roles',
|
||||
'urls': {},
|
||||
# include uuid in object reference, this is not used for applification API but is useful
|
||||
# for authentic creating its role summary page.
|
||||
'uuid': component.role.uuid if hasattr(component, 'role') else None,
|
||||
}
|
||||
return {
|
||||
'id': str(component.slug),
|
||||
'text': component.label,
|
||||
'type': component.application_component_type,
|
||||
'urls': {
|
||||
'export': request.build_absolute_uri(
|
||||
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-component-dependencies',
|
||||
kwargs={
|
||||
'component_type': component.application_component_type,
|
||||
'slug': str(component.slug),
|
||||
},
|
||||
)
|
||||
),
|
||||
'redirect': request.build_absolute_uri(
|
||||
reverse(
|
||||
'api-export-import-component-redirect',
|
||||
kwargs={
|
||||
'component_type': component.application_component_type,
|
||||
'slug': str(component.slug),
|
||||
},
|
||||
)
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class ListComponents(GenericAPIView):
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
klass = klasses[kwargs['component_type']]
|
||||
order_by = 'slug'
|
||||
if klass == Group:
|
||||
order_by = 'name'
|
||||
response = [get_component_bundle_entry(request, x) for x in klass.objects.order_by(order_by)]
|
||||
return Response({'data': response})
|
||||
|
||||
|
||||
list_components = ListComponents.as_view()
|
||||
|
||||
|
||||
class ExportComponent(GenericAPIView):
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
|
||||
def get(self, request, slug, *args, **kwargs):
|
||||
klass = klasses[kwargs['component_type']]
|
||||
serialisation = klass.objects.get(slug=slug).export_json()
|
||||
return Response({'data': serialisation})
|
||||
|
||||
|
||||
export_component = ExportComponent.as_view()
|
||||
|
||||
|
||||
class ComponentDependencies(GenericAPIView):
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
|
||||
def get(self, request, slug, *args, **kwargs):
|
||||
klass = klasses[kwargs['component_type']]
|
||||
component = klass.objects.get(slug=slug)
|
||||
|
||||
def dependency_dict(element):
|
||||
if isinstance(element, dict):
|
||||
return element
|
||||
return get_component_bundle_entry(request, element)
|
||||
|
||||
dependencies = [dependency_dict(x) for x in component.get_dependencies() if x]
|
||||
return Response({'err': 0, 'data': dependencies})
|
||||
|
||||
|
||||
component_dependencies = ComponentDependencies.as_view()
|
||||
|
||||
|
||||
def component_redirect(request, component_type, slug):
|
||||
klass = klasses[component_type]
|
||||
component = get_object_or_404(klass, slug=slug)
|
||||
if klass == Pricing:
|
||||
return redirect(reverse('lingo-manager-pricing-detail', kwargs={'pk': component.pk}))
|
||||
if klass == CriteriaCategory:
|
||||
return redirect(reverse('lingo-manager-pricing-criteria-list'))
|
||||
if klass == Agenda:
|
||||
return redirect(reverse('lingo-manager-agenda-detail', kwargs={'pk': component.pk}))
|
||||
if klass == CheckTypeGroup:
|
||||
return redirect(reverse('lingo-manager-check-type-list'))
|
||||
if klass == Payer:
|
||||
return redirect(reverse('lingo-manager-invoicing-payer-detail', kwargs={'pk': component.pk}))
|
||||
if klass == Regie:
|
||||
return redirect(reverse('lingo-manager-invoicing-regie-detail', kwargs={'pk': component.pk}))
|
||||
raise Http404
|
||||
|
||||
|
||||
class BundleCheck(GenericAPIView):
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
|
||||
def put(self, request, *args, **kwargs):
|
||||
return Response({'err': 0, 'data': {}})
|
||||
|
||||
|
||||
bundle_check = BundleCheck.as_view()
|
||||
|
||||
|
||||
class BundleImport(GenericAPIView):
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
install = True
|
||||
|
||||
def put(self, request, *args, **kwargs):
|
||||
tar_io = io.BytesIO(request.read())
|
||||
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(
|
||||
manifest,
|
||||
tar,
|
||||
editable=not self.install,
|
||||
)
|
||||
|
||||
for element in manifest.get('elements'):
|
||||
component_type = element['type']
|
||||
if component_type not in klasses or element['type'] == 'roles':
|
||||
continue
|
||||
component_type = klasses_translation.get(component_type, component_type)
|
||||
if component_type not in components:
|
||||
components[component_type] = []
|
||||
component_content = (
|
||||
tar.extractfile('%s/%s' % (element['type'], element['slug'])).read().decode()
|
||||
)
|
||||
components[component_type].append(json.loads(component_content).get('data'))
|
||||
# init cache of application elements, from manifest
|
||||
self.application_elements = set()
|
||||
# import agendas
|
||||
self.do_something(components)
|
||||
# create application elements
|
||||
self.link_objects(components)
|
||||
# remove obsolete application elements
|
||||
self.unlink_obsolete_objects()
|
||||
return Response({'err': 0})
|
||||
|
||||
def do_something(self, components):
|
||||
if components:
|
||||
import_site(components)
|
||||
|
||||
def link_objects(self, components):
|
||||
for component_type, component_list in components.items():
|
||||
component_type = klasses_translation_reverse.get(component_type, component_type)
|
||||
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)
|
||||
for element in known_elements:
|
||||
if element.content_object not in self.application_elements:
|
||||
element.delete()
|
||||
|
||||
|
||||
bundle_import = BundleImport.as_view()
|
||||
|
||||
|
||||
class BundleDeclare(BundleImport):
|
||||
install = False
|
||||
|
||||
def do_something(self, components):
|
||||
# no installation on declare
|
||||
pass
|
||||
|
||||
|
||||
bundle_declare = BundleDeclare.as_view()
|
||||
|
||||
|
||||
class BundleUnlink(GenericAPIView):
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if request.POST.get('application'):
|
||||
try:
|
||||
application = Application.objects.get(slug=request.POST['application'])
|
||||
except Application.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
application.delete()
|
||||
|
||||
return Response({'err': 0})
|
||||
|
||||
|
||||
bundle_unlink = BundleUnlink.as_view()
|
|
@ -0,0 +1,7 @@
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = []
|
||||
|
||||
operations = []
|
|
@ -0,0 +1,61 @@
|
|||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('export_import', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Application',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('slug', models.SlugField(max_length=100, unique=True)),
|
||||
('icon', models.FileField(blank=True, null=True, upload_to='applications/icons/')),
|
||||
('description', models.TextField(blank=True)),
|
||||
('documentation_url', models.URLField(blank=True)),
|
||||
('version_number', models.CharField(max_length=100)),
|
||||
('version_notes', models.TextField(blank=True)),
|
||||
('editable', models.BooleanField(default=True)),
|
||||
('visible', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ApplicationElement',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
'application',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to='export_import.application'
|
||||
),
|
||||
),
|
||||
(
|
||||
'content_type',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('application', 'content_type', 'object_id')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,131 @@
|
|||
# lingo - payment and billing system
|
||||
# Copyright (C) 2022-2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import collections
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Application(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
slug = models.SlugField(max_length=100, unique=True)
|
||||
icon = models.FileField(
|
||||
upload_to='applications/icons/',
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
description = models.TextField(blank=True)
|
||||
documentation_url = models.URLField(blank=True)
|
||||
version_number = models.CharField(max_length=100)
|
||||
version_notes = models.TextField(blank=True)
|
||||
editable = models.BooleanField(default=True)
|
||||
visible = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
@classmethod
|
||||
def update_or_create_from_manifest(cls, manifest, tar, editable=False):
|
||||
application, dummy = cls.objects.get_or_create(
|
||||
slug=manifest.get('slug'), defaults={'editable': editable}
|
||||
)
|
||||
application.name = manifest.get('application')
|
||||
application.description = manifest.get('description')
|
||||
application.documentation_url = manifest.get('documentation_url')
|
||||
application.version_number = manifest.get('version_number') or 'unknown'
|
||||
application.version_notes = manifest.get('version_notes')
|
||||
if not editable:
|
||||
application.editable = editable
|
||||
application.visible = manifest.get('visible', True)
|
||||
application.save()
|
||||
icon = manifest.get('icon')
|
||||
if icon:
|
||||
application.icon.save(icon, tar.extractfile(icon), save=True)
|
||||
else:
|
||||
application.icon.delete()
|
||||
return application
|
||||
|
||||
@classmethod
|
||||
def select_for_object_class(cls, object_class):
|
||||
content_type = ContentType.objects.get_for_model(object_class)
|
||||
elements = ApplicationElement.objects.filter(content_type=content_type)
|
||||
return cls.objects.filter(pk__in=elements.values('application'), visible=True).order_by('name')
|
||||
|
||||
@classmethod
|
||||
def populate_objects(cls, object_class, objects):
|
||||
content_type = ContentType.objects.get_for_model(object_class)
|
||||
elements = ApplicationElement.objects.filter(content_type=content_type)
|
||||
elements_by_objects = collections.defaultdict(list)
|
||||
for element in elements:
|
||||
elements_by_objects[element.content_object].append(element)
|
||||
applications_by_ids = {
|
||||
a.pk: a for a in cls.objects.filter(pk__in=elements.values('application'), visible=True)
|
||||
}
|
||||
for obj in objects:
|
||||
applications = []
|
||||
elements = elements_by_objects.get(obj) or []
|
||||
for element in elements:
|
||||
application = applications_by_ids.get(element.application_id)
|
||||
if application:
|
||||
applications.append(application)
|
||||
obj._applications = sorted(applications, key=lambda a: a.name)
|
||||
|
||||
@classmethod
|
||||
def load_for_object(cls, obj):
|
||||
content_type = ContentType.objects.get_for_model(obj.__class__)
|
||||
elements = ApplicationElement.objects.filter(content_type=content_type, object_id=obj.pk)
|
||||
applications_by_ids = {
|
||||
a.pk: a for a in cls.objects.filter(pk__in=elements.values('application'), visible=True)
|
||||
}
|
||||
applications = []
|
||||
for element in elements:
|
||||
application = applications_by_ids.get(element.application_id)
|
||||
if application:
|
||||
applications.append(application)
|
||||
obj._applications = sorted(applications, key=lambda a: a.name)
|
||||
|
||||
def get_objects_for_object_class(self, object_class):
|
||||
content_type = ContentType.objects.get_for_model(object_class)
|
||||
elements = ApplicationElement.objects.filter(content_type=content_type, application=self)
|
||||
return object_class.objects.filter(pk__in=elements.values('object_id'))
|
||||
|
||||
|
||||
class ApplicationElement(models.Model):
|
||||
application = models.ForeignKey(Application, on_delete=models.CASCADE)
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object = GenericForeignKey('content_type', 'object_id')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ['application', 'content_type', 'object_id']
|
||||
|
||||
@classmethod
|
||||
def update_or_create_for_object(cls, application, obj):
|
||||
content_type = ContentType.objects.get_for_model(obj.__class__)
|
||||
element, created = cls.objects.get_or_create(
|
||||
application=application,
|
||||
content_type=content_type,
|
||||
object_id=obj.pk,
|
||||
)
|
||||
if not created:
|
||||
element.save()
|
||||
return element
|
|
@ -0,0 +1,47 @@
|
|||
# lingo - payment and billing system
|
||||
# Copyright (C) 2022-2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from . import api_views
|
||||
|
||||
urlpatterns = [
|
||||
path('export-import/', api_views.index, name='api-export-import'),
|
||||
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',
|
||||
),
|
||||
]
|
|
@ -93,6 +93,10 @@ class Payer(models.Model):
|
|||
)
|
||||
user_fields_mapping = models.JSONField(blank=True, default=dict)
|
||||
|
||||
application_component_type = 'payers'
|
||||
application_label_singular = _('Payer')
|
||||
application_label_plural = _('Payers')
|
||||
|
||||
class Meta:
|
||||
ordering = ['label']
|
||||
|
||||
|
@ -130,6 +134,9 @@ class Payer(models.Model):
|
|||
def base_slug(self):
|
||||
return slugify(self.label)
|
||||
|
||||
def get_dependencies(self):
|
||||
return []
|
||||
|
||||
def export_json(self):
|
||||
return {
|
||||
'label': self.label,
|
||||
|
@ -320,6 +327,10 @@ class Regie(models.Model):
|
|||
cashier_name = models.CharField(_('Cashier name'), max_length=256, blank=True)
|
||||
city_name = models.CharField(_('City name'), max_length=256, blank=True)
|
||||
|
||||
application_component_type = 'regies'
|
||||
application_label_singular = _('Regie')
|
||||
application_label_plural = _('Regies')
|
||||
|
||||
class Meta:
|
||||
ordering = ['label']
|
||||
|
||||
|
@ -335,6 +346,10 @@ class Regie(models.Model):
|
|||
def base_slug(self):
|
||||
return slugify(self.label)
|
||||
|
||||
def get_dependencies(self):
|
||||
yield self.cashier_role
|
||||
yield self.payer
|
||||
|
||||
def export_json(self):
|
||||
return {
|
||||
'label': self.label,
|
||||
|
|
|
@ -102,6 +102,10 @@ class CriteriaCategory(models.Model):
|
|||
label = models.CharField(_('Label'), max_length=150)
|
||||
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
|
||||
|
||||
application_component_type = 'pricing_categories'
|
||||
application_label_singular = _('Criteria category')
|
||||
application_label_plural = _('Criteria categories')
|
||||
|
||||
class Meta:
|
||||
ordering = ['label']
|
||||
|
||||
|
@ -117,6 +121,9 @@ class CriteriaCategory(models.Model):
|
|||
def base_slug(self):
|
||||
return slugify(self.label)
|
||||
|
||||
def get_dependencies(self):
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def import_json(cls, data):
|
||||
criterias = data.pop('criterias', [])
|
||||
|
@ -304,6 +311,10 @@ class Pricing(models.Model):
|
|||
|
||||
pricing_data = models.JSONField(null=True)
|
||||
|
||||
application_component_type = 'pricings'
|
||||
application_label_singular = _('Pricing')
|
||||
application_label_plural = _('Pricings')
|
||||
|
||||
def __str__(self):
|
||||
return self.label
|
||||
|
||||
|
@ -316,6 +327,10 @@ class Pricing(models.Model):
|
|||
def base_slug(self):
|
||||
return slugify(self.label)
|
||||
|
||||
def get_dependencies(self):
|
||||
yield from self.agendas.all()
|
||||
yield from self.categories.all()
|
||||
|
||||
def export_json(self):
|
||||
return {
|
||||
'label': self.label,
|
||||
|
|
|
@ -61,6 +61,7 @@ INSTALLED_APPS = (
|
|||
'lingo.api',
|
||||
'lingo.basket',
|
||||
'lingo.epayment',
|
||||
'lingo.export_import',
|
||||
'lingo.invoicing',
|
||||
'lingo.manager',
|
||||
'lingo.pricing',
|
||||
|
|
|
@ -23,6 +23,7 @@ from .api.urls import urlpatterns as lingo_api_urls
|
|||
from .basket.urls import public_urlpatterns as lingo_basket_public_urls
|
||||
from .epayment.urls import manager_urlpatterns as lingo_epayment_manager_urls
|
||||
from .epayment.urls import public_urlpatterns as lingo_epayment_public_urls
|
||||
from .export_import import urls as export_import_urls
|
||||
from .invoicing.urls import urlpatterns as lingo_invoicing_urls
|
||||
from .manager.urls import urlpatterns as lingo_manager_urls
|
||||
from .pricing.urls import urlpatterns as lingo_pricing_urls
|
||||
|
@ -37,6 +38,7 @@ urlpatterns = [
|
|||
re_path(r'^manage/epayment/', decorated_includes(manager_required, include(lingo_epayment_manager_urls))),
|
||||
path('basket/', include(lingo_basket_public_urls)),
|
||||
path('api/', include(lingo_api_urls)),
|
||||
path('api/', include(export_import_urls)),
|
||||
path('login/', login, name='auth_login'),
|
||||
path('logout/', logout, name='auth_logout'),
|
||||
path('', include(lingo_epayment_public_urls)),
|
||||
|
|
|
@ -0,0 +1,661 @@
|
|||
import datetime
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import tarfile
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from lingo.agendas.models import Agenda, CheckType, CheckTypeGroup
|
||||
from lingo.export_import.models import Application, ApplicationElement
|
||||
from lingo.invoicing.models import Payer, Regie
|
||||
from lingo.pricing.models import CriteriaCategory, Pricing
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_object_types(app, user):
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
resp = app.get('/api/export-import/')
|
||||
assert resp.json == {
|
||||
'data': [
|
||||
{
|
||||
'id': 'pricings',
|
||||
'singular': 'Pricing',
|
||||
'text': 'Pricings',
|
||||
'urls': {'list': 'http://testserver/api/export-import/pricings/'},
|
||||
},
|
||||
{
|
||||
'id': 'pricing_categories',
|
||||
'minor': True,
|
||||
'singular': 'Criteria category',
|
||||
'text': 'Criteria categories',
|
||||
'urls': {'list': 'http://testserver/api/export-import/pricing_categories/'},
|
||||
},
|
||||
{
|
||||
'id': 'lingo_agendas',
|
||||
'minor': True,
|
||||
'singular': 'Agenda (payment)',
|
||||
'text': 'Agendas (payment)',
|
||||
'urls': {'list': 'http://testserver/api/export-import/lingo_agendas/'},
|
||||
},
|
||||
{
|
||||
'id': 'check_type_groups',
|
||||
'minor': True,
|
||||
'singular': 'Check type group',
|
||||
'text': 'Check type groups',
|
||||
'urls': {'list': 'http://testserver/api/export-import/check_type_groups/'},
|
||||
},
|
||||
{
|
||||
'id': 'payers',
|
||||
'minor': True,
|
||||
'singular': 'Payer',
|
||||
'text': 'Payers',
|
||||
'urls': {'list': 'http://testserver/api/export-import/payers/'},
|
||||
},
|
||||
{
|
||||
'id': 'regies',
|
||||
'minor': True,
|
||||
'singular': 'Regie',
|
||||
'text': 'Regies',
|
||||
'urls': {'list': 'http://testserver/api/export-import/regies/'},
|
||||
},
|
||||
{
|
||||
'id': 'roles',
|
||||
'minor': True,
|
||||
'singular': 'Role',
|
||||
'text': 'Roles',
|
||||
'urls': {'list': 'http://testserver/api/export-import/roles/'},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_list(app, user):
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
Pricing.objects.create(
|
||||
label='Foo Bar pricing',
|
||||
date_start=datetime.date(year=2021, month=9, day=1),
|
||||
date_end=datetime.date(year=2022, month=9, day=1),
|
||||
)
|
||||
CriteriaCategory.objects.create(label='Foo Bar Cat')
|
||||
Agenda.objects.create(label='Foo Bar Agenda')
|
||||
CheckTypeGroup.objects.create(label='Foo Bar Group')
|
||||
Payer.objects.create(label='Foo Bar Payer')
|
||||
Regie.objects.create(label='Foo Bar Regie')
|
||||
group = Group.objects.create(name='group1')
|
||||
resp = app.get('/api/export-import/pricings/')
|
||||
assert resp.json == {
|
||||
'data': [
|
||||
{
|
||||
'id': 'foo-bar-pricing',
|
||||
'text': 'Foo Bar pricing',
|
||||
'type': 'pricings',
|
||||
'urls': {
|
||||
'dependencies': 'http://testserver/api/export-import/pricings/foo-bar-pricing/dependencies/',
|
||||
'export': 'http://testserver/api/export-import/pricings/foo-bar-pricing/',
|
||||
'redirect': 'http://testserver/api/export-import/pricings/foo-bar-pricing/redirect/',
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
resp = app.get('/api/export-import/pricing_categories/')
|
||||
assert resp.json == {
|
||||
'data': [
|
||||
{
|
||||
'id': 'foo-bar-cat',
|
||||
'text': 'Foo Bar Cat',
|
||||
'type': 'pricing_categories',
|
||||
'urls': {
|
||||
'dependencies': 'http://testserver/api/export-import/pricing_categories/foo-bar-cat/dependencies/',
|
||||
'export': 'http://testserver/api/export-import/pricing_categories/foo-bar-cat/',
|
||||
'redirect': 'http://testserver/api/export-import/pricing_categories/foo-bar-cat/redirect/',
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
resp = app.get('/api/export-import/lingo_agendas/')
|
||||
assert resp.json == {
|
||||
'data': [
|
||||
{
|
||||
'id': 'foo-bar-agenda',
|
||||
'text': 'Foo Bar Agenda',
|
||||
'type': 'lingo_agendas',
|
||||
'urls': {
|
||||
'dependencies': 'http://testserver/api/export-import/lingo_agendas/foo-bar-agenda/dependencies/',
|
||||
'export': 'http://testserver/api/export-import/lingo_agendas/foo-bar-agenda/',
|
||||
'redirect': 'http://testserver/api/export-import/lingo_agendas/foo-bar-agenda/redirect/',
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
resp = app.get('/api/export-import/check_type_groups/')
|
||||
assert resp.json == {
|
||||
'data': [
|
||||
{
|
||||
'id': 'foo-bar-group',
|
||||
'text': 'Foo Bar Group',
|
||||
'type': 'check_type_groups',
|
||||
'urls': {
|
||||
'dependencies': 'http://testserver/api/export-import/check_type_groups/foo-bar-group/dependencies/',
|
||||
'export': 'http://testserver/api/export-import/check_type_groups/foo-bar-group/',
|
||||
'redirect': 'http://testserver/api/export-import/check_type_groups/foo-bar-group/redirect/',
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
resp = app.get('/api/export-import/payers/')
|
||||
assert resp.json == {
|
||||
'data': [
|
||||
{
|
||||
'id': 'foo-bar-payer',
|
||||
'text': 'Foo Bar Payer',
|
||||
'type': 'payers',
|
||||
'urls': {
|
||||
'dependencies': 'http://testserver/api/export-import/payers/foo-bar-payer/dependencies/',
|
||||
'export': 'http://testserver/api/export-import/payers/foo-bar-payer/',
|
||||
'redirect': 'http://testserver/api/export-import/payers/foo-bar-payer/redirect/',
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
resp = app.get('/api/export-import/regies/')
|
||||
assert resp.json == {
|
||||
'data': [
|
||||
{
|
||||
'id': 'foo-bar-regie',
|
||||
'text': 'Foo Bar Regie',
|
||||
'type': 'regies',
|
||||
'urls': {
|
||||
'dependencies': 'http://testserver/api/export-import/regies/foo-bar-regie/dependencies/',
|
||||
'export': 'http://testserver/api/export-import/regies/foo-bar-regie/',
|
||||
'redirect': 'http://testserver/api/export-import/regies/foo-bar-regie/redirect/',
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
resp = app.get('/api/export-import/roles/')
|
||||
assert resp.json == {
|
||||
'data': [{'id': group.pk, 'text': 'group1', 'type': 'roles', 'urls': {}, 'uuid': None}]
|
||||
}
|
||||
|
||||
|
||||
def test_export_pricing(app, user):
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
Pricing.objects.create(
|
||||
label='Foo Bar pricing',
|
||||
date_start=datetime.date(year=2021, month=9, day=1),
|
||||
date_end=datetime.date(year=2022, month=9, day=1),
|
||||
)
|
||||
resp = app.get('/api/export-import/pricings/foo-bar-pricing/')
|
||||
assert resp.json['data']['label'] == 'Foo Bar pricing'
|
||||
|
||||
|
||||
def test_export_minor_components(app, user):
|
||||
CriteriaCategory.objects.create(label='Foo Bar Cat')
|
||||
Agenda.objects.create(label='Foo Bar Agenda')
|
||||
CheckTypeGroup.objects.create(label='Foo Bar Group')
|
||||
Payer.objects.create(label='Foo Bar Payer')
|
||||
Regie.objects.create(label='Foo Bar Regie')
|
||||
|
||||
resp = app.get('/api/export-import/pricing_categories/foo-bar-cat/')
|
||||
assert resp.json['data']['label'] == 'Foo Bar Cat'
|
||||
resp = app.get('/api/export-import/lingo_agendas/foo-bar-agenda/')
|
||||
assert resp.json['data']['slug'] == 'foo-bar-agenda'
|
||||
resp = app.get('/api/export-import/check_type_groups/foo-bar-group/')
|
||||
assert resp.json['data']['label'] == 'Foo Bar Group'
|
||||
resp = app.get('/api/export-import/payers/foo-bar-payer/')
|
||||
assert resp.json['data']['label'] == 'Foo Bar Payer'
|
||||
resp = app.get('/api/export-import/regies/foo-bar-regie/')
|
||||
assert resp.json['data']['label'] == 'Foo Bar Regie'
|
||||
|
||||
|
||||
def test_pricing_dependencies(app, user):
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
pricing = Pricing.objects.create(
|
||||
label='Foo Bar pricing',
|
||||
date_start=datetime.date(year=2021, month=9, day=1),
|
||||
date_end=datetime.date(year=2022, month=9, day=1),
|
||||
)
|
||||
category1 = CriteriaCategory.objects.create(label='Cat 1')
|
||||
category2 = CriteriaCategory.objects.create(label='Cat 2')
|
||||
agenda1 = Agenda.objects.create(label='Foo bar 1')
|
||||
agenda2 = Agenda.objects.create(label='Foo bar 2')
|
||||
pricing.categories.add(category1, through_defaults={'order': 1})
|
||||
pricing.categories.add(category2, through_defaults={'order': 2})
|
||||
pricing.agendas.add(agenda1, agenda2)
|
||||
|
||||
resp = app.get('/api/export-import/pricings/foo-bar-pricing/dependencies/')
|
||||
assert resp.json == {
|
||||
'data': [
|
||||
{
|
||||
'id': 'foo-bar-1',
|
||||
'text': 'Foo bar 1',
|
||||
'type': 'lingo_agendas',
|
||||
'urls': {
|
||||
'dependencies': 'http://testserver/api/export-import/lingo_agendas/foo-bar-1/dependencies/',
|
||||
'export': 'http://testserver/api/export-import/lingo_agendas/foo-bar-1/',
|
||||
'redirect': 'http://testserver/api/export-import/lingo_agendas/foo-bar-1/redirect/',
|
||||
},
|
||||
},
|
||||
{
|
||||
'id': 'foo-bar-2',
|
||||
'text': 'Foo bar 2',
|
||||
'type': 'lingo_agendas',
|
||||
'urls': {
|
||||
'dependencies': 'http://testserver/api/export-import/lingo_agendas/foo-bar-2/dependencies/',
|
||||
'export': 'http://testserver/api/export-import/lingo_agendas/foo-bar-2/',
|
||||
'redirect': 'http://testserver/api/export-import/lingo_agendas/foo-bar-2/redirect/',
|
||||
},
|
||||
},
|
||||
{
|
||||
'id': 'cat-1',
|
||||
'text': 'Cat 1',
|
||||
'type': 'pricing_categories',
|
||||
'urls': {
|
||||
'dependencies': 'http://testserver/api/export-import/pricing_categories/cat-1/dependencies/',
|
||||
'export': 'http://testserver/api/export-import/pricing_categories/cat-1/',
|
||||
'redirect': 'http://testserver/api/export-import/pricing_categories/cat-1/redirect/',
|
||||
},
|
||||
},
|
||||
{
|
||||
'id': 'cat-2',
|
||||
'text': 'Cat 2',
|
||||
'type': 'pricing_categories',
|
||||
'urls': {
|
||||
'dependencies': 'http://testserver/api/export-import/pricing_categories/cat-2/dependencies/',
|
||||
'export': 'http://testserver/api/export-import/pricing_categories/cat-2/',
|
||||
'redirect': 'http://testserver/api/export-import/pricing_categories/cat-2/redirect/',
|
||||
},
|
||||
},
|
||||
],
|
||||
'err': 0,
|
||||
}
|
||||
|
||||
|
||||
def test_agenda_dependencies(app, user):
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
regie = Regie.objects.create(label='Foo Bar Regie')
|
||||
group = CheckTypeGroup.objects.create(label='Foo Bar Group')
|
||||
Agenda.objects.create(label='Foo Bar Agenda', check_type_group=group, regie=regie)
|
||||
resp = app.get('/api/export-import/lingo_agendas/foo-bar-agenda/dependencies/')
|
||||
assert resp.json == {
|
||||
'data': [
|
||||
{
|
||||
'id': 'foo-bar-group',
|
||||
'text': 'Foo Bar Group',
|
||||
'type': 'check_type_groups',
|
||||
'urls': {
|
||||
'dependencies': 'http://testserver/api/export-import/check_type_groups/foo-bar-group/dependencies/',
|
||||
'export': 'http://testserver/api/export-import/check_type_groups/foo-bar-group/',
|
||||
'redirect': 'http://testserver/api/export-import/check_type_groups/foo-bar-group/redirect/',
|
||||
},
|
||||
},
|
||||
{
|
||||
'id': 'foo-bar-regie',
|
||||
'text': 'Foo Bar Regie',
|
||||
'type': 'regies',
|
||||
'urls': {
|
||||
'dependencies': 'http://testserver/api/export-import/regies/foo-bar-regie/dependencies/',
|
||||
'export': 'http://testserver/api/export-import/regies/foo-bar-regie/',
|
||||
'redirect': 'http://testserver/api/export-import/regies/foo-bar-regie/redirect/',
|
||||
},
|
||||
},
|
||||
{
|
||||
'id': 'foo-bar-agenda',
|
||||
'text': 'Foo Bar Agenda',
|
||||
'type': 'agendas',
|
||||
'urls': {
|
||||
'dependencies': 'http://chrono.example.org/api/export-import/agendas/foo-bar-agenda/dependencies/',
|
||||
'export': 'http://chrono.example.org/api/export-import/agendas/foo-bar-agenda/',
|
||||
'redirect': 'http://chrono.example.org/api/export-import/agendas/foo-bar-agenda/redirect/',
|
||||
},
|
||||
},
|
||||
],
|
||||
'err': 0,
|
||||
}
|
||||
|
||||
|
||||
def test_check_type_group_dependencies(app, user):
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
group = CheckTypeGroup.objects.create(label='Foo Bar Group')
|
||||
CheckType.objects.create(label='Foo reason', group=group, kind='presence')
|
||||
CheckType.objects.create(label='Baz reason', group=group)
|
||||
resp = app.get('/api/export-import/check_type_groups/foo-bar-group/dependencies/')
|
||||
assert resp.json == {
|
||||
'data': [],
|
||||
'err': 0,
|
||||
}
|
||||
|
||||
|
||||
def test_regie_dependencies(app, user):
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
group = Group.objects.create(name='group1')
|
||||
payer = Payer.objects.create(label='Foo Bar Payer')
|
||||
Regie.objects.create(label='Foo Bar Regie', cashier_role=group, payer=payer)
|
||||
resp = app.get('/api/export-import/regies/foo-bar-regie/dependencies/')
|
||||
assert resp.json == {
|
||||
'data': [
|
||||
{'id': group.pk, 'text': 'group1', 'type': 'roles', 'urls': {}, 'uuid': None},
|
||||
{
|
||||
'id': 'foo-bar-payer',
|
||||
'text': 'Foo Bar Payer',
|
||||
'type': 'payers',
|
||||
'urls': {
|
||||
'dependencies': 'http://testserver/api/export-import/payers/foo-bar-payer/dependencies/',
|
||||
'export': 'http://testserver/api/export-import/payers/foo-bar-payer/',
|
||||
'redirect': 'http://testserver/api/export-import/payers/foo-bar-payer/redirect/',
|
||||
},
|
||||
},
|
||||
],
|
||||
'err': 0,
|
||||
}
|
||||
|
||||
|
||||
def test_payer_dependencies(app, user):
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
Payer.objects.create(label='Foo Bar Payer')
|
||||
resp = app.get('/api/export-import/payers/foo-bar-payer/dependencies/')
|
||||
assert resp.json == {
|
||||
'data': [],
|
||||
'err': 0,
|
||||
}
|
||||
|
||||
|
||||
def test_pricing_categories_dependencies(app, user):
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
CriteriaCategory.objects.create(label='Foo Bar Cat')
|
||||
resp = app.get('/api/export-import/pricing_categories/foo-bar-cat/dependencies/')
|
||||
assert resp.json == {
|
||||
'data': [],
|
||||
'err': 0,
|
||||
}
|
||||
|
||||
|
||||
def test_redirect(app, user):
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
pricing = Pricing.objects.create(
|
||||
label='Foo Bar pricing',
|
||||
date_start=datetime.date(year=2021, month=9, day=1),
|
||||
date_end=datetime.date(year=2022, month=9, day=1),
|
||||
)
|
||||
category = CriteriaCategory.objects.create(label='Foo Bar Cat')
|
||||
agenda = Agenda.objects.create(label='Foo Bar Agenda')
|
||||
group = CheckTypeGroup.objects.create(label='Foo Bar Group')
|
||||
payer = Payer.objects.create(label='Foo Bar Payer')
|
||||
regie = Regie.objects.create(label='Foo Bar Regie')
|
||||
|
||||
redirect_url = f'/api/export-import/pricings/{pricing.slug}/redirect/'
|
||||
resp = app.get(redirect_url, status=302)
|
||||
assert resp.location == f'/manage/pricing/{pricing.pk}/'
|
||||
|
||||
redirect_url = f'/api/export-import/pricing_categories/{category.slug}/redirect/'
|
||||
resp = app.get(redirect_url, status=302)
|
||||
assert resp.location == '/manage/pricing/criterias/'
|
||||
|
||||
redirect_url = f'/api/export-import/lingo_agendas/{agenda.slug}/redirect/'
|
||||
resp = app.get(redirect_url, status=302)
|
||||
assert resp.location == f'/manage/pricing/agenda/{agenda.pk}/'
|
||||
|
||||
redirect_url = f'/api/export-import/check_type_groups/{group.slug}/redirect/'
|
||||
resp = app.get(redirect_url, status=302)
|
||||
assert resp.location == '/manage/pricing/check-types/'
|
||||
|
||||
redirect_url = f'/api/export-import/payers/{payer.slug}/redirect/'
|
||||
resp = app.get(redirect_url, status=302)
|
||||
assert resp.location == f'/manage/invoicing/payer/{payer.pk}/'
|
||||
|
||||
redirect_url = f'/api/export-import/regies/{regie.slug}/redirect/'
|
||||
resp = app.get(redirect_url, status=302)
|
||||
assert resp.location == f'/manage/invoicing/regie/{regie.pk}/'
|
||||
|
||||
|
||||
def create_bundle(app, user, visible=True, version_number='42.0'):
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
|
||||
group, _ = CheckTypeGroup.objects.get_or_create(label='Foo Bar Group')
|
||||
payer, _ = Payer.objects.get_or_create(label='Foo Bar Payer')
|
||||
regie, _ = Regie.objects.get_or_create(label='Foo Bar Regie', payer=payer)
|
||||
pricing, _ = Pricing.objects.get_or_create(
|
||||
label='Foo Bar pricing',
|
||||
date_start=datetime.date(year=2021, month=9, day=1),
|
||||
date_end=datetime.date(year=2022, month=9, day=1),
|
||||
)
|
||||
category, _ = CriteriaCategory.objects.get_or_create(label='Foo Bar Cat')
|
||||
agenda, _ = Agenda.objects.get_or_create(label='Foo Bar Agenda', check_type_group=group, regie=regie)
|
||||
pricing.categories.add(category, through_defaults={'order': 1})
|
||||
pricing.agendas.add(agenda)
|
||||
|
||||
components = [
|
||||
(pricing, False),
|
||||
(category, True),
|
||||
(agenda, True),
|
||||
(group, True),
|
||||
(payer, True),
|
||||
(regie, True),
|
||||
]
|
||||
|
||||
tar_io = io.BytesIO()
|
||||
with tarfile.open(mode='w', fileobj=tar_io) as tar:
|
||||
manifest_json = {
|
||||
'application': 'Test',
|
||||
'slug': 'test',
|
||||
'icon': 'foo.png',
|
||||
'description': 'Foo Bar',
|
||||
'documentation_url': 'http://foo.bar',
|
||||
'visible': visible,
|
||||
'version_number': version_number,
|
||||
'version_notes': 'foo bar blah',
|
||||
'elements': [],
|
||||
}
|
||||
for component, auto_dependency in components:
|
||||
manifest_json['elements'].append(
|
||||
{
|
||||
'type': component.application_component_type,
|
||||
'slug': component.slug,
|
||||
'name': component.label,
|
||||
'auto-dependency': auto_dependency,
|
||||
}
|
||||
)
|
||||
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)
|
||||
|
||||
icon_fd = io.BytesIO(
|
||||
b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVQI12NoAAAAggCB3UNq9AAAAABJRU5ErkJggg=='
|
||||
)
|
||||
tarinfo = tarfile.TarInfo('foo.png')
|
||||
tarinfo.size = len(icon_fd.getvalue())
|
||||
tar.addfile(tarinfo, fileobj=icon_fd)
|
||||
|
||||
for component, _ in components:
|
||||
component_export = app.get(
|
||||
'/api/export-import/%s/%s/' % (component.application_component_type, component.slug)
|
||||
).content
|
||||
tarinfo = tarfile.TarInfo('%s/%s' % (component.application_component_type, component.slug))
|
||||
tarinfo.size = len(component_export)
|
||||
tar.addfile(tarinfo, fileobj=io.BytesIO(component_export))
|
||||
bundle = tar_io.getvalue()
|
||||
return bundle
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bundle(app, user):
|
||||
return create_bundle(app, user)
|
||||
|
||||
|
||||
@mock.patch('lingo.export_import.api_views.refresh_agendas')
|
||||
def test_bundle_import(mock_refresh, app, user):
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
|
||||
bundles = []
|
||||
for version_number in ['42.0', '42.1']:
|
||||
bundles.append(create_bundle(app, user, version_number=version_number))
|
||||
|
||||
Pricing.objects.all().delete()
|
||||
CriteriaCategory.objects.all().delete()
|
||||
CheckTypeGroup.objects.all().delete()
|
||||
Regie.objects.all().delete()
|
||||
Payer.objects.all().delete()
|
||||
Agenda.objects.all().delete()
|
||||
agenda = Agenda.objects.create(label='Foo Bar Agenda') # created by agenda refresh
|
||||
|
||||
resp = app.put('/api/export-import/bundle-import/', bundles[0])
|
||||
assert Pricing.objects.all().count() == 1
|
||||
assert resp.json['err'] == 0
|
||||
assert Application.objects.count() == 1
|
||||
application = Application.objects.latest('pk')
|
||||
assert application.slug == 'test'
|
||||
assert application.name == 'Test'
|
||||
assert application.description == 'Foo Bar'
|
||||
assert application.documentation_url == 'http://foo.bar'
|
||||
assert application.version_number == '42.0'
|
||||
assert application.version_notes == 'foo bar blah'
|
||||
assert re.match(r'applications/icons/foo(_\w+)?.png', application.icon.name)
|
||||
assert application.editable is False
|
||||
assert application.visible is True
|
||||
assert ApplicationElement.objects.count() == 6
|
||||
assert mock_refresh.call_args_list == [mock.call()]
|
||||
|
||||
payer = Payer.objects.latest('pk')
|
||||
regie = Regie.objects.latest('pk')
|
||||
assert regie.payer == payer
|
||||
group = CheckTypeGroup.objects.latest('pk')
|
||||
agenda.refresh_from_db()
|
||||
assert agenda.regie == regie
|
||||
assert agenda.check_type_group == group
|
||||
category = CriteriaCategory.objects.latest('pk')
|
||||
last_pricing = Pricing.objects.latest('pk')
|
||||
assert list(last_pricing.agendas.all()) == [agenda]
|
||||
assert list(last_pricing.categories.all()) == [category]
|
||||
|
||||
# check editable flag is kept on install
|
||||
application.editable = True
|
||||
application.save()
|
||||
|
||||
# create link to element not present in manifest: it should be unlinked
|
||||
last_pricing = Pricing.objects.latest('pk')
|
||||
ApplicationElement.objects.create(
|
||||
application=application,
|
||||
content_type=ContentType.objects.get_for_model(Pricing),
|
||||
object_id=last_pricing.pk + 1,
|
||||
)
|
||||
|
||||
# check update
|
||||
resp = app.put('/api/export-import/bundle-import/', bundles[1])
|
||||
assert Pricing.objects.all().count() == 1
|
||||
assert resp.json['err'] == 0
|
||||
assert Application.objects.count() == 1
|
||||
application = Application.objects.latest('pk')
|
||||
assert application.editable is False
|
||||
assert ApplicationElement.objects.count() == 6
|
||||
assert (
|
||||
ApplicationElement.objects.filter(
|
||||
application=application,
|
||||
content_type=ContentType.objects.get_for_model(Pricing),
|
||||
object_id=last_pricing.pk + 1,
|
||||
).exists()
|
||||
is False
|
||||
)
|
||||
|
||||
|
||||
def test_bundle_declare(app, user):
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
|
||||
bundle = create_bundle(app, user, visible=False)
|
||||
resp = app.put('/api/export-import/bundle-declare/', bundle)
|
||||
assert Pricing.objects.all().count() == 1
|
||||
assert resp.json['err'] == 0
|
||||
assert Application.objects.count() == 1
|
||||
application = Application.objects.latest('pk')
|
||||
assert application.slug == 'test'
|
||||
assert application.name == 'Test'
|
||||
assert application.description == 'Foo Bar'
|
||||
assert application.documentation_url == 'http://foo.bar'
|
||||
assert application.version_number == '42.0'
|
||||
assert application.version_notes == 'foo bar blah'
|
||||
assert re.match(r'applications/icons/foo(_\w+)?.png', application.icon.name)
|
||||
assert application.editable is True
|
||||
assert application.visible is False
|
||||
assert ApplicationElement.objects.count() == 6
|
||||
|
||||
bundle = create_bundle(app, user, visible=True)
|
||||
# create link to element not present in manifest: it should be unlinked
|
||||
last_pricing = Pricing.objects.latest('pk')
|
||||
ApplicationElement.objects.create(
|
||||
application=application,
|
||||
content_type=ContentType.objects.get_for_model(Pricing),
|
||||
object_id=last_pricing.pk + 1,
|
||||
)
|
||||
# and remove regie to have unkown references in manifest
|
||||
Regie.objects.all().delete()
|
||||
|
||||
resp = app.put('/api/export-import/bundle-declare/', bundle)
|
||||
assert Application.objects.count() == 1
|
||||
application = Application.objects.latest('pk')
|
||||
assert application.visible is True
|
||||
assert ApplicationElement.objects.count() == 5 # pricing, categorie, agenda, group, payer
|
||||
|
||||
|
||||
def test_bundle_unlink(app, user, bundle):
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
|
||||
application = Application.objects.create(
|
||||
name='Test',
|
||||
slug='test',
|
||||
version_number='42.0',
|
||||
)
|
||||
other_application = Application.objects.create(
|
||||
name='Other Test',
|
||||
slug='other-test',
|
||||
version_number='42.0',
|
||||
)
|
||||
pricing = Pricing.objects.latest('pk')
|
||||
ApplicationElement.objects.create(
|
||||
application=application,
|
||||
content_object=pricing,
|
||||
)
|
||||
ApplicationElement.objects.create(
|
||||
application=application,
|
||||
content_type=ContentType.objects.get_for_model(Pricing),
|
||||
object_id=pricing.pk + 1,
|
||||
)
|
||||
ApplicationElement.objects.create(
|
||||
application=other_application,
|
||||
content_object=pricing,
|
||||
)
|
||||
ApplicationElement.objects.create(
|
||||
application=other_application,
|
||||
content_type=ContentType.objects.get_for_model(Pricing),
|
||||
object_id=pricing.pk + 1,
|
||||
)
|
||||
|
||||
assert Application.objects.count() == 2
|
||||
assert ApplicationElement.objects.count() == 4
|
||||
app.post('/api/export-import/unlink/', {'application': 'test'})
|
||||
assert Application.objects.count() == 1
|
||||
assert ApplicationElement.objects.count() == 2
|
||||
assert ApplicationElement.objects.filter(
|
||||
application=other_application,
|
||||
content_type=ContentType.objects.get_for_model(Pricing),
|
||||
object_id=pricing.pk,
|
||||
).exists()
|
||||
assert ApplicationElement.objects.filter(
|
||||
application=other_application,
|
||||
content_type=ContentType.objects.get_for_model(Pricing),
|
||||
object_id=pricing.pk + 1,
|
||||
).exists()
|
||||
|
||||
# again
|
||||
app.post('/api/export-import/unlink/', {'application': 'test'})
|
||||
assert Application.objects.count() == 1
|
||||
assert ApplicationElement.objects.count() == 2
|
||||
|
||||
|
||||
def test_bundle_check(app, user):
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
assert app.put('/api/export-import/bundle-check/').json == {'err': 0, 'data': {}}
|
Loading…
Reference in New Issue