passerelle/passerelle/apps/jsondatastore/models.py

191 lines
6.7 KiB
Python

# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2017 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 json
import uuid
from django.db import models
from django.db.models import JSONField
from django.template import Context, Template
from django.utils.translation import gettext_lazy as _
from passerelle.base.models import BaseResource
from passerelle.utils.api import APIError, endpoint
from passerelle.utils.conversion import simplify
def get_hex_uuid():
return uuid.uuid4().hex
def clean_json_data(data):
try:
payload = json.loads(data)
if not isinstance(payload, dict):
raise APIError('payload must be a dict')
return payload
except ValueError:
raise APIError('could not decode body to json')
class JsonData(models.Model):
datastore = models.ForeignKey('JsonDataStore', null=True, on_delete=models.CASCADE)
uuid = models.CharField(_('uuid'), max_length=32, default=get_hex_uuid, editable=False, unique=True)
name_id = models.CharField(max_length=256, blank=True)
content = JSONField(_('Content'))
text = models.CharField(max_length=256, blank=True)
creation_datetime = models.DateTimeField(auto_now_add=True)
last_update_datetime = models.DateTimeField(auto_now=True)
def save(self, *args, **kwargs):
text_value_template = self.datastore.text_value_template
if text_value_template:
template = Template(text_value_template)
context = Context(self.content)
self.text = template.render(context).strip()
return super().save(*args, **kwargs)
def to_json(self):
return {
'id': self.uuid,
'text': self.text,
'content': self.content,
'creation_datetime': self.creation_datetime,
'last_update_datetime': self.last_update_datetime,
}
class JsonDataStore(BaseResource):
category = _('Data Sources')
documentation_url = (
'https://doc-publik.entrouvert.com/admin-fonctionnel/parametrage-avance/json-data-store/'
)
text_value_template = models.CharField(_('Template for "text" key value'), max_length=256, blank=True)
class Meta:
verbose_name = _('JSON Data Store')
@endpoint(
perm='can_access',
name='data',
pattern=r'$',
description=_('Listing'),
long_description=_('More filtering on attributes is possible using "key=val" additionals parameters'),
parameters={
'name_id': {'description': _('Object identifier'), 'example_value': '12345'},
'q': {'description': _('Filter on "text" key value'), 'example_value': 'rue du chateau'},
},
)
def list(self, request, name_id=None, q=None, **kwargs):
objects = JsonData.objects.filter(datastore=self).order_by('text')
if name_id is not None:
objects = objects.filter(name_id=name_id)
if q:
q = simplify(q)
objects = [o for o in objects if q in simplify(o.text)]
for key, value in kwargs.items():
objects = [o for o in objects if o.content.get(key) == value]
return {'data': [x.to_json() for x in objects]}
@endpoint(
perm='can_access',
methods=['post'],
name='data',
pattern=r'create$',
example_pattern='create',
description=_('Create'),
)
def create(self, request, name_id=None, **kwargs):
content = clean_json_data(request.body)
attrs = {
'content': content,
'datastore': self,
}
if name_id is not None:
attrs['name_id'] = name_id
data = JsonData(**attrs)
data.save()
return {'id': data.uuid, 'text': data.text}
def get_data_object(self, uuid, name_id=None):
attrs = {
'uuid': uuid,
'datastore': self,
}
if name_id is not None:
attrs['name_id'] = name_id
return JsonData.objects.get(**attrs)
@endpoint(
perm='can_access',
methods=['get', 'post', 'patch'],
name='data',
pattern=r'(?P<uuid>\w+)/$',
example_pattern='{uuid}/',
description_get=_('Get'),
description_post=_('Replace'),
description_patch=_('Update'),
parameters={'uuid': {'description': _('Object identifier'), 'example_value': '12345'}},
)
def get_or_replace(self, request, uuid, name_id=None):
data = self.get_data_object(uuid, name_id)
if request.method == 'POST':
new_content = clean_json_data(request.body)
data.content = new_content
data.save()
elif request.method == 'PATCH':
new_content = clean_json_data(request.body)
data.content.update(new_content)
data.save()
return data.to_json()
@endpoint(
perm='can_access',
methods=['post'],
name='data',
description=_('Delete'),
pattern=r'(?P<uuid>\w+)/delete$',
example_pattern='{uuid}/delete',
parameters={'uuid': {'description': _('Object identifier'), 'example_value': '12345'}},
)
def delete_(self, request, uuid, name_id=None):
# delete() would collide with Model.delete()
self.get_data_object(uuid, name_id).delete()
return {}
@endpoint(
perm='can_access',
name='data',
pattern=r'by/(?P<attribute>[\w-]+)/$',
example_pattern='by/{attribute}/',
description=_('Get a single object by attribute'),
parameters={
'attribute': {'description': _('Attribute name'), 'example_value': 'code'},
'value': {'description': _('Attribute value'), 'example_value': '12345'},
},
)
def get_by_attribute(self, request, attribute, value, name_id=None):
objects = JsonData.objects.filter(datastore=self)
if name_id is not None:
objects = objects.filter(name_id=name_id)
objects = objects.filter(content__contains={attribute: value})
data = objects.first()
if data is not None:
return data.to_json()
raise APIError('no such object')