passerelle/passerelle/contrib/grenoble_gru/models.py

292 lines
12 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (C) 2018 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 re
from urllib import parse as urlparse
from django.core.cache import cache
from django.db import models
from django.utils import dateparse
from django.utils.http import urlencode
from django.utils.text import slugify
from django.utils.translation import ugettext_lazy as _
from lxml import etree
from passerelle.base.models import BaseResource
from passerelle.utils.api import endpoint
from passerelle.utils.jsonresponse import APIError
from passerelle.views import WrongParameter
RESPONSE_CODES = {
'01': _('Success'),
'02': _('Remote service error'),
'10': _('Authentication failed'),
'11': _('Collectivity not defined'),
'20': _('Invalid input format'),
'21': _('Required field not provided'),
'22': _('Unexpected value (referentials)'),
'23': _('Demand already exists'),
}
def xml2dict(element):
data = {}
for attr in element.keys():
data[attr] = element.get(attr)
if element.text:
return element.text
for child in element:
data[child.tag] = xml2dict(child)
if data:
return data
def check_value(data, field_name, values):
value = data[field_name]
if value not in values:
raise ValueError('%s must be one of %s' % (field_name, values))
return value
def strip_emoji(value):
emoji_pattern = re.compile(
"["
"\U00002700-\U000027BF" # Dingbats
"\U0001F300-\U0001F5FF" # symbols & pictographs
"\U0001F600-\U0001F64F" # emoticons
"\U0001F680-\U0001F6FF" # transport & map symbols
"]+",
)
return emoji_pattern.sub(r'', value)
class GrenobleGRU(BaseResource):
base_url = models.URLField(
max_length=256, blank=False, verbose_name=_('Base URL'), help_text=_('Grenoble GRU API base URL')
)
username = models.CharField(max_length=128, verbose_name=_('Username'))
password = models.CharField(max_length=128, verbose_name=_('Password'))
category = _('Business Process Connectors')
class Meta:
verbose_name = "Grenoble - Gestion des signalements"
@classmethod
def get_verbose_name(cls):
return cls._meta.verbose_name
def request(self, endpoint, payload=None):
payload = payload or {}
# do not alter original payload
payload = payload.copy()
payload['uti_identifiant'] = self.username
payload['uti_motdepasse'] = self.password
url = urlparse.urljoin(self.base_url, endpoint)
return self.requests.post(url, data=payload)
def check_status(self):
response = self.request('ws_typologie_demande.php')
response.raise_for_status()
def build_gru_params(self, data):
types_params = {}
payload = {'id': data['application_id']}
payload['dem_comp'] = types_params['dem_comp'] = data.get('dem_comp', 'Voirie')
payload.update(
{
# applicant informations
'dem_nom': data['applicant_lastname'],
'dem_prenom': data['applicant_firstname'],
'dem_tel': data['applicant_phone'],
'dem_mail': data['applicant_email'],
'dem_reponse': 1 if data.get('applicant_requires_reply') is True else 0,
'dem_moyen_contact': check_value(
data, 'applicant_contact_mode', self.types('//modeContact', True, types_params)
),
'dem_nature': check_value(
data, 'applicant_status', self.types('//natureContact', True, types_params)
),
# intervention informations
'int_type_adresse': check_value(
data, 'intervention_address_type', self.types('//typeAdresse', True, types_params)
),
'int_numerovoie': data['intervention_street_number'],
'int_libellevoie': data['intervention_street_name'],
'int_insee': data['intervention_address_insee'],
'int_secteur': check_value(
data, 'intervention_sector', self.types('//secteur', True, types_params)
),
'int_type_numero': check_value(
data, 'intervention_number_type', self.types('//typeNumero', True, types_params)
),
'int_date_demande': dateparse.parse_datetime(data['intervention_datetime']).strftime(
'%d%m%Y %H:%M'
),
# comments
'obs_demande_urgente': 1 if data.get('urgent_demand') in (True, 'True', 1, '1') else 0,
'obs_type_dysfonctionnement': check_value(
data, 'dysfonction_type', self.types('//typeDysfonctionnement', True, types_params)
),
'obs_motif': check_value(
data, 'intervention_reason', self.types('//motif', True, types_params)
),
'obs_description_probleme': strip_emoji(data.get('comment_description', '')),
}
)
if data['intervention_reason'] == '24':
# code for reason 'Autre' in which case it should be specified
payload['obs_motifautre'] = data.get('intervention_custom_reason', '')
if 'intervention_free_address' in data:
payload['int_adresse_manuelle'] = data['intervention_free_address']
if 'applicant_free_address' in data:
payload['dem_adresse_manuelle'] = data['applicant_free_address']
if 'dem_pav' in data and data['dem_pav']:
payload['dem_pav'] = data['dem_pav']
return payload
def types(self, path, as_list=False, params=None):
params = params or {}
cache_key = 'grenoble-gru-%s' % self.id
if params:
# compute new cache key based on params
params_slug = '-'.join(sorted([slugify('%s-%s' % (k, v)) for k, v in params.items()]))
cache_key = '%s-%s' % (cache_key, params_slug)
xml_content = cache.get(cache_key)
if not xml_content:
xml_content = self.request('ws_typologie_demande.php', params).content
try:
root = etree.fromstring(xml_content)
except etree.XMLSyntaxError as e:
raise APIError('Invalid XML returned: %s' % e)
cache.set(cache_key, xml_content, 3600)
if as_list:
return [el.find('identifiant').text for el in root.xpath(path)]
return {
'data': [
{'id': el.find('identifiant').text, 'text': el.find('libelle').text}
for el in root.xpath(path)
]
}
@endpoint(name='contact-modes', perm='can_access', description=_('Lists contact modes'))
def contact_modes(self, request, *args, **kwargs):
return self.types('//modeContact', params=kwargs)
@endpoint(name='contact-types', perm='can_access', description=_('Lists contact types'))
def contact_types(self, request, *args, **kwargs):
return self.types('//natureContact', params=kwargs)
@endpoint(name='sectors', perm='can_access', description=_('Lists sectors'))
def sectors(self, request, *args, **kwargs):
return self.types('//secteur', params=kwargs)
@endpoint(name='address-types', perm='can_access', description=_('Lists address types'))
def address_types(self, request, *args, **kwargs):
return self.types('//typeAdresse', params=kwargs)
@endpoint(name='number-types', perm='can_access', description=_('Lists number types'))
def number_types(self, request, *args, **kwargs):
return self.types('//typeNumero', params=kwargs)
@endpoint(name='dysfunction-types', perm='can_access', description=_('Lists dysfunction types'))
def dysfunction_types(self, request, *args, **kwargs):
return self.types('//typeDysfonctionnement', params=kwargs)
@endpoint(
name='intervention-descriptions', perm='can_access', description=_('Lists intervention descriptions')
)
def intervention_descriptions(self, request, *args, **kwargs):
return self.types('//descIntervention', params=kwargs)
@endpoint(name='intervention-reasons', perm='can_access', description=_('Lists intervention reasons'))
def intervention_reasons(self, request, *args, **kwargs):
return self.types('//motif', params=kwargs)
@endpoint(perm='can_access', description=_('Lists PAVs'))
def pavs(self, request, *args, **kwargs):
response = self.request('ws_recuperation_pav.php')
data = []
for item in response.json():
if not isinstance(item['adresse'], dict):
continue
item['id'] = str(item['id'])
item['text'] = '{numero_voie}, {nom_voie}, {commune}'.format(**item['adresse'])
data.append(item)
return {'data': data}
@endpoint(name='create-demand', perm='can_access', methods=['post'], description=_('Create a demand'))
def create_demand(self, request, *args, **kwargs):
try:
payload = self.build_gru_params(json.loads(request.body))
except (KeyError, ValueError) as e:
raise APIError(e)
response = self.request('ws_creation_demande.php', payload)
if response.text != '01':
raise APIError(RESPONSE_CODES.get(response.text, _('Unknown error code (%s)') % response.text))
return {'data': 'Demand successfully created'}
@endpoint(
name='demand',
perm='can_access',
methods=['post'],
description=_('Add attachment to a demand'),
pattern=r'(?P<demand_id>[\w-]+)/add-attachment/$',
)
def add_attachment_to_demand(self, request, demand_id, **kwargs):
data = json.loads(request.body)
if 'file' not in data:
raise WrongParameter(['file'], [])
file_data = data['file']
if not isinstance(file_data, dict):
raise APIError('file should be a dict')
if 'filename' not in file_data:
raise WrongParameter(['file[filename]'], [])
if 'content_type' not in file_data:
raise WrongParameter(['file[content_type]'], [])
if 'content' not in file_data:
raise WrongParameter(['file[content]'], [])
# file data should be ordered
file_data = (
('filetype', file_data['content_type']),
('filename', file_data['filename']),
('filecontent', file_data['content']),
)
# file parameters should be urlencoded and sent as 'piece_jointe' param
payload = {'dem_tiers_id': demand_id, 'piece_jointe': urlencode(file_data)}
response = self.request('ws_update_demandePJ.php', payload)
if response.text == '01':
return True
return False
@endpoint(
name='demand', perm='can_access', description=_('Get demand'), pattern=r'(?P<demand_id>[\w-]+)/$'
)
def get_demand(self, request, demand_id, **kwargs):
payload = {'dem_tiers_id': demand_id}
response = self.request('ws_get_demande.php', payload)
try:
demand = etree.fromstring(response.content)
except etree.XMLSyntaxError as e:
raise APIError('Invalid XML returned: %s' % e)
return {'data': xml2dict(demand)}