810 lines
29 KiB
Python
810 lines
29 KiB
Python
# passerelle - uniform access to multiple data sources and services
|
|
# 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 collections
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import uuid
|
|
from datetime import date, datetime, time, timedelta
|
|
from urllib import parse as urlparse
|
|
|
|
from django.core.cache import cache
|
|
from django.db import models, transaction
|
|
from django.db.models import JSONField
|
|
from django.utils import dateformat, dateparse
|
|
from django.utils.encoding import force_bytes
|
|
from django.utils.translation import gettext_lazy as _
|
|
from requests.exceptions import RequestException
|
|
|
|
from passerelle.base.models import BaseResource
|
|
from passerelle.contrib.planitech import mste
|
|
from passerelle.utils.api import endpoint
|
|
from passerelle.utils.jsonresponse import APIError
|
|
|
|
DEFAULT_MIN_CAPACITY = 0
|
|
DEFAULT_MAX_CAPACITY = 100000
|
|
|
|
|
|
CREATE_RESERVATION_SCHEMA = {
|
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
|
"title": "Planitech createreservation",
|
|
"description": "",
|
|
"type": "object",
|
|
"required": [
|
|
"date",
|
|
"start_time",
|
|
"end_time",
|
|
"place_id",
|
|
"price",
|
|
"name_id",
|
|
"first_name",
|
|
"last_name",
|
|
"email",
|
|
"activity_id",
|
|
"object",
|
|
"type_id",
|
|
"vat_rate",
|
|
],
|
|
"properties": {
|
|
"date": {
|
|
"description": "Date",
|
|
"type": "string",
|
|
},
|
|
"start_time": {
|
|
"description": "Start time",
|
|
"type": "string",
|
|
},
|
|
"end_time": {
|
|
"description": "End time",
|
|
"type": "string",
|
|
},
|
|
"place_id": {
|
|
"description": "Place identifier",
|
|
"type": "number",
|
|
},
|
|
"price": {
|
|
"description": "Price",
|
|
"type": "number",
|
|
},
|
|
"name_id": {
|
|
"description": "Publik user nameID",
|
|
"type": "string",
|
|
},
|
|
"first_name": {
|
|
"description": "First name",
|
|
"type": "string",
|
|
},
|
|
"last_name": {
|
|
"description": "Last name",
|
|
"type": "string",
|
|
},
|
|
"email": {
|
|
"description": "Email",
|
|
"type": "string",
|
|
},
|
|
"activity_id": {
|
|
"description": "Activity identifier",
|
|
"type": "number",
|
|
},
|
|
"object": {
|
|
"description": "Object",
|
|
"type": "string",
|
|
},
|
|
"type_id": {
|
|
"description": "Rerservation type identifier",
|
|
"type": "number",
|
|
},
|
|
"vat_rate": {
|
|
"description": "VAT rate",
|
|
"type": "number",
|
|
},
|
|
"price_code": {
|
|
"description": "User price code",
|
|
"type": "string",
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
GET_RESERVATION_PRICE_SCHEMA = {
|
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
|
"title": "Planitech getreservationprice",
|
|
"description": "",
|
|
"type": "object",
|
|
"required": [
|
|
"date",
|
|
"start_time",
|
|
"end_time",
|
|
"place_id",
|
|
"name_id",
|
|
"first_name",
|
|
"last_name",
|
|
"email",
|
|
"activity_id",
|
|
"type_id",
|
|
],
|
|
"properties": {
|
|
"date": {
|
|
"description": "Date",
|
|
"type": "string",
|
|
},
|
|
"start_time": {
|
|
"description": "Start time",
|
|
"type": "string",
|
|
},
|
|
"end_time": {
|
|
"description": "End time",
|
|
"type": "string",
|
|
},
|
|
"place_id": {
|
|
"description": "Place identifier",
|
|
"type": "number",
|
|
},
|
|
"name_id": {
|
|
"description": "Publik user nameID",
|
|
"type": "string",
|
|
},
|
|
"first_name": {
|
|
"description": "First name",
|
|
"type": "string",
|
|
},
|
|
"last_name": {
|
|
"description": "Last name",
|
|
"type": "string",
|
|
},
|
|
"email": {
|
|
"description": "Email",
|
|
"type": "string",
|
|
},
|
|
"activity_id": {
|
|
"description": "Activity identifier",
|
|
"type": "number",
|
|
},
|
|
"type_id": {
|
|
"description": "Rerservation type identifier",
|
|
"type": "number",
|
|
},
|
|
"price_code": {
|
|
"description": "User price code",
|
|
"type": "string",
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
RESERVATION_STATUS = {"confirmed": 3, "invalid": 0, " pre-reservation": 1, "standard": 2}
|
|
|
|
UPDATE_RESERVATION_SCHEMA = {
|
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
|
"title": "Planitech updatereservation",
|
|
"description": "",
|
|
"type": "object",
|
|
"required": ["reservation_id", "status"],
|
|
"properties": {
|
|
"reservation_id": {
|
|
"description": "Reservation Identifier",
|
|
"type": "number",
|
|
},
|
|
"status": {
|
|
"description": "Status of the reservation",
|
|
"type": "string",
|
|
"enum": list(RESERVATION_STATUS.keys()),
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def parse_date(date_str):
|
|
date_obj = dateparse.parse_date(date_str)
|
|
if date_obj is None:
|
|
raise APIError("Invalid date format: %s" % date_str)
|
|
return date_obj
|
|
|
|
|
|
def parse_time(time_str):
|
|
timeobj = dateparse.parse_time(time_str)
|
|
if timeobj is None:
|
|
raise APIError("Invalid time format: %s" % time_str)
|
|
return timeobj
|
|
|
|
|
|
def compute_hash(content, hardness, salt):
|
|
sha = hashlib.new('sha512', force_bytes(salt + content))
|
|
for idx in range(hardness):
|
|
sha = hashlib.new('sha512', sha.digest())
|
|
return sha.hexdigest().upper()
|
|
|
|
|
|
def date_to_datetime(date_str):
|
|
date_obj = parse_date(date_str)
|
|
if date_obj is None:
|
|
raise APIError("Invalid date string: %s" % date_str)
|
|
return datetime.combine(date_obj, time(hour=12))
|
|
|
|
|
|
def get_duration(start_time_str, end_time_str):
|
|
start_time = parse_time(start_time_str)
|
|
end_time = parse_time(end_time_str)
|
|
return time_to_minutes(end_time) - time_to_minutes(start_time)
|
|
|
|
|
|
def get_salt(salt):
|
|
return re.match(r'<(.*?)>', salt).groups()[0]
|
|
|
|
|
|
def combine_date_time(date_p, time_str):
|
|
if not isinstance(date_p, date):
|
|
date_p = parse_date(date_p)
|
|
time_obj = parse_time(time_str)
|
|
return datetime.combine(date_p, time_obj)
|
|
|
|
|
|
def time_to_minutes(timeobj):
|
|
return float(timeobj.hour * 60 + timeobj.minute)
|
|
|
|
|
|
def get_extensions(post_data):
|
|
extensions = collections.defaultdict(dict)
|
|
|
|
for field_name, field_value in post_data.items():
|
|
if field_name.startswith('extension_'):
|
|
try:
|
|
extension_id, extension_property = field_name[10:].split('_', 1)
|
|
except ValueError:
|
|
raise APIError("Wrong extension format for : '%s'" % field_name)
|
|
extensions[extension_id][extension_property] = field_value
|
|
|
|
res = []
|
|
for extension in extensions.values():
|
|
for key in ('name', 'value'):
|
|
if key not in extension:
|
|
raise APIError("Missing '%s' in extension" % key)
|
|
res.append(
|
|
{'name': extension['name'], 'value': extension['value'], 'type': extension.get('type', 'string')}
|
|
)
|
|
|
|
return res
|
|
|
|
|
|
class PlanitechConnector(BaseResource):
|
|
url = models.URLField(
|
|
max_length=400,
|
|
verbose_name=_('Planitec API endpoint'),
|
|
help_text=_('URL of the Planitec API endpoint'),
|
|
)
|
|
username = models.CharField(max_length=128, verbose_name=_('Service username'))
|
|
password = models.CharField(max_length=128, verbose_name=_('Service password'), null=True, blank=True)
|
|
verify_cert = models.BooleanField(default=True, verbose_name=_('Check HTTPS Certificate validity'))
|
|
custom_fields = JSONField(_('Custom places fields'), blank=True, null=True)
|
|
price_code = models.CharField(max_length=128, verbose_name=_('Price code'), blank=True)
|
|
|
|
category = _('Business Process Connectors')
|
|
|
|
class Meta:
|
|
verbose_name = _('Planitec')
|
|
|
|
def _call_planitech(self, session_meth, endpoint, params=None):
|
|
if not getattr(self, '_planitech_session', False):
|
|
self._login()
|
|
self._planitech_session = True
|
|
|
|
kwargs = {}
|
|
if params is not None:
|
|
kwargs['data'] = json.dumps(mste.encode(params))
|
|
response = session_meth(urlparse.urljoin(self.url, endpoint), **kwargs)
|
|
if response.status_code != 200:
|
|
error_msg = "Planitech error %s" % response.status_code
|
|
try:
|
|
data = mste.decode(response.json())
|
|
if hasattr(data, 'get'):
|
|
error = data.get('errors')
|
|
if error:
|
|
error_msg += " - %s" % error
|
|
except TypeError:
|
|
pass
|
|
raise APIError(error_msg)
|
|
return mste.decode(response.json())
|
|
|
|
def _get_places_fields(self):
|
|
if self.custom_fields:
|
|
return self.custom_fields.get('places', [])
|
|
return []
|
|
|
|
def _raw_get_places_referential(self, refresh_cache=False):
|
|
cache_key = 'planitech-%s-places' % self.id
|
|
ref = cache.get(cache_key)
|
|
if ref is None or refresh_cache:
|
|
data = self._call_planitech(self.requests.get, 'getPlacesList')
|
|
ref = {}
|
|
for place in data['placesList']:
|
|
place_id = int(place['identifier'])
|
|
ref[place_id] = {'identifier': place_id, 'label': place['label']}
|
|
|
|
extensionAttributes = {'capacity': {'name': 'TOTCAP', 'type': 'int'}}
|
|
|
|
for custom_field in self._get_places_fields():
|
|
field_name = custom_field['name']
|
|
extensionAttributes[field_name] = {'name': field_name, 'type': custom_field['type']}
|
|
|
|
data = self._call_planitech(
|
|
self.requests.post,
|
|
'getPlacesInfo',
|
|
{
|
|
"placeIdentifiers": [float(key) for key in ref],
|
|
"extensionAttributes": extensionAttributes,
|
|
},
|
|
)
|
|
for place in data['requestedPlaces']:
|
|
|
|
place_id = int(place['identifier'])
|
|
ref[place_id]['street_number'] = place.get('streetNumber')
|
|
ref[place_id]['address'] = place.get('address1')
|
|
ref[place_id]['city'] = place.get('city')
|
|
ref[place_id]['zipcode'] = place.get('zipCode')
|
|
|
|
# Custom attributes
|
|
for attr_name, attr_def in extensionAttributes.items():
|
|
attr_value = place.get(attr_name)
|
|
if attr_value is not None and attr_def['type'] == 'int':
|
|
attr_value = int(attr_value)
|
|
ref[place_id][attr_name] = attr_value
|
|
|
|
cache.set(cache_key, ref)
|
|
return ref
|
|
|
|
def _get_places_referential(
|
|
self, min_capacity=DEFAULT_MIN_CAPACITY, max_capacity=DEFAULT_MAX_CAPACITY, **kwargs
|
|
):
|
|
|
|
ref = self._raw_get_places_referential()
|
|
res = {}
|
|
|
|
try:
|
|
min_capacity, max_capacity = int(min_capacity), int(max_capacity)
|
|
except (ValueError, TypeError):
|
|
raise APIError("min_capacity and max_capacity must be integers")
|
|
|
|
for place_id, place_data in ref.items():
|
|
# Filter on capacity
|
|
if min_capacity != DEFAULT_MIN_CAPACITY or max_capacity != DEFAULT_MAX_CAPACITY:
|
|
if place_data.get('capacity') is None:
|
|
continue
|
|
if place_data['capacity'] < min_capacity or place_data['capacity'] > max_capacity:
|
|
continue
|
|
|
|
# Filter on custom fields
|
|
skip = False
|
|
for filter_name, filter_value in kwargs.items():
|
|
for field in self._get_places_fields():
|
|
if filter_name == field['name']:
|
|
if field['type'] == 'int':
|
|
filter_value = int(filter_value)
|
|
if place_data.get(filter_name) != filter_value:
|
|
skip = True
|
|
break
|
|
if skip:
|
|
continue
|
|
res[place_id] = place_data
|
|
|
|
return res
|
|
|
|
def _login(self):
|
|
try:
|
|
auth_url = urlparse.urljoin(self.url, 'auth')
|
|
response = self.requests.get(auth_url, headers={'MH-LOGIN': self.username})
|
|
response.raise_for_status()
|
|
part1, salt1, part2, salt2, _ = re.split(r'(\<.*?\>)', response.text)
|
|
hardness1 = int(part1.split(':')[1])
|
|
hardness2 = int(part2.split(':')[1])
|
|
salt1 = get_salt(salt1)
|
|
salt2 = get_salt(salt2)
|
|
tmp_hash = compute_hash(self.password, hardness1, salt1)
|
|
hash_pass = compute_hash(tmp_hash, hardness2, salt2)
|
|
response = self.requests.get(auth_url, headers={'MH-PASSWORD': hash_pass})
|
|
response.raise_for_status()
|
|
# the last response should have set a cookie which will be used for authentication
|
|
except RequestException as e:
|
|
raise APIError("Authentication to Planitec failed: %s" % str(e))
|
|
|
|
def update_or_create_user(self, post_data):
|
|
dyn_price_code = post_data.get('price_code')
|
|
price_code = dyn_price_code or self.price_code
|
|
|
|
with transaction.atomic():
|
|
pairing, created = Pairing.objects.get_or_create(
|
|
resource=self,
|
|
name_id=post_data['name_id'],
|
|
defaults={'external_id': uuid.uuid4().hex, 'price_code': price_code},
|
|
)
|
|
if created:
|
|
# Create planitec user
|
|
params = {
|
|
"externalUserIdentifier": pairing.external_id,
|
|
"name": post_data['last_name'],
|
|
"firstName": post_data['first_name'],
|
|
"mail": post_data['email'],
|
|
"pricingCode": price_code,
|
|
}
|
|
data = self._call_planitech(self.requests.post, 'createPerson', params)
|
|
if data.get('creationStatus') != 'OK':
|
|
raise APIError("Person creation failed: %s" % data.get('creationStatus'))
|
|
|
|
elif dyn_price_code and pairing.price_code != dyn_price_code:
|
|
# Update planitec user
|
|
pairing.price_code = dyn_price_code
|
|
pairing.save()
|
|
params = {'externalUserIdentifier': pairing.external_id, 'pricingCode': dyn_price_code}
|
|
data = self._call_planitech(self.requests.post, 'updatePerson', params)
|
|
if data.get('modificationStatus') != 'OK':
|
|
raise APIError("Person update failed: %s" % data.get('modificationStatus'))
|
|
|
|
return pairing
|
|
|
|
@endpoint(
|
|
perm='can_access',
|
|
post={
|
|
'description': _('Get reservation price'),
|
|
'request_body': {'schema': {'application/json': GET_RESERVATION_PRICE_SCHEMA}},
|
|
},
|
|
)
|
|
def getreservationprice(self, request, post_data):
|
|
start_datetime = combine_date_time(post_data['date'], post_data['start_time'])
|
|
end_datetime = combine_date_time(post_data['date'], post_data['end_time'])
|
|
|
|
pairing = self.update_or_create_user(post_data)
|
|
|
|
params = {
|
|
"activityID": mste.Uint32(post_data['activity_id']),
|
|
"contractorExternalIdentifier": pairing.external_id,
|
|
"end": end_datetime,
|
|
"isWeekly": False,
|
|
"places": [float(post_data['place_id'])],
|
|
"start": start_datetime,
|
|
"typeID": mste.Uint32(post_data['type_id']),
|
|
}
|
|
data = self._call_planitech(self.requests.post, 'getFutureReservationPrice', params)
|
|
if data.get('calculationStatus') != 'OK':
|
|
raise APIError("Get reservation price failed: %s" % data.get('calculationStatus'))
|
|
price = data.get('calculatedPrice', False)
|
|
if price is False:
|
|
raise APIError("Get reservation price failed: no price")
|
|
return {'data': {'price': int(price), 'raw_data': data}}
|
|
|
|
@endpoint(
|
|
perm='can_access',
|
|
post={
|
|
'description': _('Create reservation'),
|
|
'request_body': {'schema': {'application/json': CREATE_RESERVATION_SCHEMA}},
|
|
},
|
|
)
|
|
def createreservation(self, request, post_data):
|
|
start_datetime = combine_date_time(post_data['date'], post_data['start_time'])
|
|
end_datetime = combine_date_time(post_data['date'], post_data['end_time'])
|
|
request_date = datetime.now()
|
|
|
|
pairing = self.update_or_create_user(post_data)
|
|
|
|
params = {
|
|
"activityID": mste.Uint32(post_data['activity_id']),
|
|
"contractorExternalIdentifier": pairing.external_id,
|
|
"end": end_datetime,
|
|
"isWeekly": False,
|
|
"object": post_data['object'],
|
|
"places": [float(post_data['place_id'])],
|
|
"price": mste.Uint32(post_data['price']),
|
|
"requestDate": request_date,
|
|
"start": start_datetime,
|
|
"typeID": mste.Uint32(post_data['type_id']),
|
|
"vatRate": mste.Uint32(post_data['vat_rate']),
|
|
}
|
|
extensions = get_extensions(post_data)
|
|
if extensions:
|
|
params['extensions'] = extensions
|
|
|
|
data = self._call_planitech(self.requests.post, 'createReservation', params)
|
|
if data.get('creationStatus') != 'OK':
|
|
raise APIError("Reservation creation failed: %s" % data.get('creationStatus'))
|
|
reservation_id = data.get('reservationIdentifier')
|
|
if not reservation_id:
|
|
raise APIError("Reservation creation failed: no reservation ID")
|
|
return {'data': {'reservation_id': int(reservation_id), 'raw_data': data}}
|
|
|
|
def hourly(self):
|
|
self._raw_get_places_referential(refresh_cache=True)
|
|
|
|
def _date_display(self, raw_data):
|
|
available_dates = set()
|
|
for place in raw_data.get('availablePlaces', []):
|
|
for freegap in place.get('freeGaps', []):
|
|
available_dates.add(freegap[0].date())
|
|
res = []
|
|
available_dates = list(available_dates)
|
|
available_dates.sort()
|
|
for date_obj in available_dates:
|
|
date_text = dateformat.format(date_obj, 'l d F Y')
|
|
short_text = dateformat.format(date_obj, 'd/m/Y')
|
|
res.append({"id": date_obj.isoformat(), "text": date_text, "short_text": short_text})
|
|
return res
|
|
|
|
def _place_display(self, raw_data):
|
|
available_places = []
|
|
for place in raw_data.get('availablePlaces', []):
|
|
available_places.append(int(place['placeIdentifier']))
|
|
places_ref = self._raw_get_places_referential()
|
|
res = []
|
|
for place in available_places:
|
|
res.append({"id": place, "text": places_ref[place]['label']})
|
|
return res
|
|
|
|
def _full_display(self, raw_data, places_id):
|
|
places_ref = self._raw_get_places_referential()
|
|
res = {'date': self._date_display(raw_data), 'place': self._place_display(raw_data)}
|
|
all_dates = [d['id'] for d in res['date']]
|
|
full = []
|
|
for place_id in places_id:
|
|
place_data = {'id': place_id, 'text': places_ref[place_id]['label'], 'dates': []}
|
|
place_dates = []
|
|
for place in raw_data.get('availablePlaces', []):
|
|
if place_id == int(place['placeIdentifier']):
|
|
for freegap in place.get('freeGaps', []):
|
|
place_dates.append(freegap[0].date().isoformat())
|
|
break
|
|
for d in all_dates:
|
|
available = d in place_dates
|
|
place_data['dates'].append({'id': d, 'available': available})
|
|
assert len(place_data['dates']) == len(all_dates)
|
|
full.append(place_data)
|
|
|
|
res['full'] = full
|
|
return res
|
|
|
|
@endpoint(
|
|
description_get=_('Get days available for reservation'),
|
|
methods=['get'],
|
|
perm='can_access',
|
|
parameters={
|
|
'min_capacity': {
|
|
'description': _('Minimum capacity'),
|
|
'example_value': '1',
|
|
},
|
|
'max_capacity': {
|
|
'description': _('Maximum capacity'),
|
|
'example_value': '10',
|
|
},
|
|
'start_date': {
|
|
'description': _('Start date'),
|
|
'type': 'date',
|
|
'example_value': '2018-10-10',
|
|
},
|
|
'start_days': {
|
|
'description': _('Start days'),
|
|
'example_value': '2',
|
|
},
|
|
'end_date': {
|
|
'description': _('End date'),
|
|
'type': 'date',
|
|
'example_value': '2018-12-10',
|
|
},
|
|
'start_time': {
|
|
'description': _('Start time'),
|
|
'example_value': '10:00',
|
|
},
|
|
'end_time': {
|
|
'description': _('End time'),
|
|
'example_value': '18:00',
|
|
},
|
|
'end_days': {
|
|
'description': _('End days'),
|
|
'example_value': '10',
|
|
},
|
|
'weekdays': {
|
|
'description': _(
|
|
'Week days, comma separated list of integers beetween 0 (sunday) and 6 (saturday)'
|
|
),
|
|
'example_value': 'true',
|
|
'type': 'string',
|
|
},
|
|
'place_id': {
|
|
'description': _('Place identifier'),
|
|
'example_value': '2',
|
|
'type': 'int',
|
|
},
|
|
'display': {
|
|
'description': _('Display'),
|
|
'example_value': 'date',
|
|
},
|
|
},
|
|
)
|
|
def getfreegaps(
|
|
self,
|
|
request,
|
|
display,
|
|
start_time,
|
|
end_time,
|
|
min_capacity=DEFAULT_MIN_CAPACITY,
|
|
start_date=None,
|
|
start_days=None,
|
|
end_date=None,
|
|
end_days=None,
|
|
max_capacity=DEFAULT_MAX_CAPACITY,
|
|
weekdays=None,
|
|
place_id=None,
|
|
**kwargs,
|
|
):
|
|
|
|
# Additional parameters check
|
|
valid_displays = ['date', 'place', 'full']
|
|
if display not in valid_displays:
|
|
raise APIError("Valid display are: %s" % ", ".join(valid_displays))
|
|
if start_date is None and start_days is None:
|
|
raise APIError("start_date or start_days is required")
|
|
|
|
# Starting date computation
|
|
if start_date is not None:
|
|
utc_start_datetime = combine_date_time(start_date, start_time)
|
|
else:
|
|
date_obj = (datetime.now() + timedelta(int(start_days))).date()
|
|
utc_start_datetime = combine_date_time(date_obj, start_time)
|
|
|
|
# Ending date computation
|
|
if end_date is None and end_days is None:
|
|
utc_end_datetime = (utc_start_datetime + timedelta(days=1)).replace(hour=0, minute=0)
|
|
elif end_date is not None:
|
|
utc_end_datetime = combine_date_time(end_date, '00:00')
|
|
elif end_days is not None:
|
|
date_obj = (datetime.now() + timedelta(int(end_days))).date()
|
|
utc_end_datetime = combine_date_time(date_obj, '00:00')
|
|
|
|
duration = get_duration(start_time, end_time)
|
|
|
|
# Places restriction
|
|
if place_id is not None:
|
|
places_id = [int(place_id)]
|
|
else:
|
|
places_id = self._get_places_referential(
|
|
min_capacity=min_capacity, max_capacity=max_capacity, **kwargs
|
|
).keys()
|
|
|
|
params = {
|
|
"placeIdentifiers": [float(p_id) for p_id in places_id],
|
|
"startingDate": utc_start_datetime,
|
|
"endingDate": utc_end_datetime,
|
|
"requestedStartingTime": float(0),
|
|
"requestedEndingTime": duration,
|
|
}
|
|
|
|
if weekdays is not None:
|
|
reservation_days = []
|
|
for day in [d.strip() for d in weekdays.split(',') if d.strip()]:
|
|
try:
|
|
day = mste.Uint32(day)
|
|
if not 0 <= day <= 6:
|
|
raise ValueError()
|
|
reservation_days.append(mste.Uint32(day))
|
|
except (ValueError, TypeError):
|
|
raise APIError('weekdays must be a comma separated list of integers beetween 0 and 6')
|
|
if reservation_days:
|
|
params['reservationDays'] = reservation_days
|
|
|
|
raw_data = self._call_planitech(self.requests.post, 'getFreeGaps', params)
|
|
|
|
if display == 'date':
|
|
return {'data': self._date_display(raw_data)}
|
|
|
|
if display == 'place':
|
|
return {'data': self._place_display(raw_data)}
|
|
|
|
if display == 'full':
|
|
return {'data': self._full_display(raw_data, places_id)}
|
|
|
|
def generic_call(self, endpoint, data_key):
|
|
raw_data = self._call_planitech(self.requests.post, endpoint)
|
|
data = raw_data[data_key]
|
|
for item in data:
|
|
if 'identifier' in item:
|
|
item['identifier'] = int(item['identifier'])
|
|
return data
|
|
|
|
@endpoint(description_get=_('Get activities'), methods=['get'], perm='can_access')
|
|
def getactivities(self, request):
|
|
return {'data': self.generic_call('getActivities', 'activities')}
|
|
|
|
@endpoint(description_get=_('Get activity types'), methods=['get'], perm='can_access')
|
|
def getactivitytypes(self, request):
|
|
return {'data': self.generic_call('getActivityTypes', 'types')}
|
|
|
|
@endpoint(description_get=_('Get place'), methods=['get'], perm='can_access')
|
|
def getplace(self, request, id):
|
|
try:
|
|
id_ = int(id)
|
|
except ValueError:
|
|
raise APIError('ID must be an integer')
|
|
ref = self._raw_get_places_referential()
|
|
if id_ not in ref:
|
|
raise APIError('No place with ID %s' % id_)
|
|
return {'data': ref[int(id_)]}
|
|
|
|
@endpoint(description_get=_('Get places referential'), methods=['get'], perm='can_access')
|
|
def getplacesreferential(self, request, **kwargs):
|
|
return {'data': self._get_places_referential(**kwargs)}
|
|
|
|
@endpoint(description_get=_('Get reservation infos'), methods=['get'], perm='can_access')
|
|
def getreservationsinfo(self, request, reservation_id):
|
|
params = {'reservationIdentifiers': [float(reservation_id)]}
|
|
data = self._call_planitech(self.requests.post, 'getReservationsInfo', params)
|
|
data = data.get('requestedReservations', [])
|
|
|
|
res = {}
|
|
# Delete circular references that are not json serializable
|
|
if data:
|
|
res = data[0]
|
|
if 'activity' in res and 'reservations' in res['activity']:
|
|
del res['activity']['reservations']
|
|
if 'contractor' in res and 'reservations' in res['contractor']:
|
|
del res['contractor']['reservations']
|
|
|
|
return {'data': res}
|
|
|
|
@endpoint(description_get=_('Get reservation types'), methods=['get'], perm='can_access')
|
|
def getreservationtypes(self, request):
|
|
return {'data': self.generic_call('getReservationTypes', 'types')}
|
|
|
|
@endpoint(description_get=_('Get users'), methods=['get'], perm='can_access')
|
|
def getusers(self, request):
|
|
return {'data': self.generic_call('getUsersList', 'usersList')}
|
|
|
|
@endpoint(
|
|
methods=['post'],
|
|
perm='can_access',
|
|
post={
|
|
'description': _('Update reservation'),
|
|
'request_body': {'schema': {'application/json': UPDATE_RESERVATION_SCHEMA}},
|
|
},
|
|
)
|
|
def updatereservation(self, request, post_data):
|
|
params = {
|
|
"reservationIdentifier": mste.Uint32(post_data['reservation_id']),
|
|
"situation": mste.Uint32(RESERVATION_STATUS[post_data['status']]),
|
|
}
|
|
extensions = get_extensions(post_data)
|
|
if extensions:
|
|
params['extensions'] = extensions
|
|
|
|
data = self._call_planitech(self.requests.post, 'updateReservation', params)
|
|
if data.get('modificationStatus') != 'OK':
|
|
raise APIError("Update reservation failed: %s" % data.get('modificationStatus'))
|
|
return {'data': {'raw_data': data}}
|
|
|
|
def check_status(self):
|
|
auth_url = urlparse.urljoin(self.url, 'auth')
|
|
response = self.requests.get(auth_url, headers={'MH-LOGIN': self.username})
|
|
response.raise_for_status()
|
|
|
|
|
|
class Pairing(models.Model):
|
|
class Meta:
|
|
unique_together = (
|
|
('resource', 'name_id'),
|
|
('resource', 'external_id'),
|
|
)
|
|
|
|
resource = models.ForeignKey(PlanitechConnector, on_delete=models.CASCADE)
|
|
name_id = models.CharField(blank=False, max_length=256)
|
|
external_id = models.CharField(blank=False, max_length=256)
|
|
created = models.DateTimeField(auto_now_add=True)
|
|
price_code = models.CharField(max_length=128, verbose_name=_('Price code'), blank=True)
|