292 lines
12 KiB
Python
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)}
|