290 lines
11 KiB
Python
290 lines
11 KiB
Python
# ANTS-Hub - Copyright (C) Entr'ouvert
|
|
|
|
import base64
|
|
import binascii
|
|
import collections
|
|
import datetime
|
|
import functools
|
|
import json
|
|
import os
|
|
|
|
import jsonschema
|
|
from django.core.exceptions import ValidationError
|
|
from django.db.transaction import atomic
|
|
from django.http import JsonResponse
|
|
from django.utils.timezone import now
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from django.views.generic import View
|
|
|
|
from ants_hub.data.models import Collectivite, Horaire, HoraireList, Lieu, Raccordement, RendezVous, TypeDeRdv
|
|
|
|
with open(os.path.join(os.path.dirname(__file__), '..', 'static', 'schemas', 'rdv-disponibles.json')) as fd:
|
|
RENDEZ_VOUS_DISPONIBLES_SCHEMA = json.load(fd)
|
|
|
|
|
|
def authenticate(func):
|
|
@functools.wraps(func)
|
|
def wrapper(request, *args, **kwargs):
|
|
header = request.headers.get('Authorization', '').encode()
|
|
if not header:
|
|
return JsonResponse("Missing Authorization header", status=401, safe=False)
|
|
auth = header.split()
|
|
if not auth or auth[0].lower() != b'basic' or len(auth) != 2:
|
|
return JsonResponse("Invalid Authorization header", status=401, safe=False)
|
|
|
|
try:
|
|
auth_decoded = base64.b64decode(auth[1]).decode()
|
|
except (TypeError, UnicodeDecodeError, binascii.Error):
|
|
return JsonResponse("Invalid Authorization header", status=401, safe=False)
|
|
|
|
apikey = auth_decoded.split(':', 1)[0]
|
|
raccordement = Raccordement.objects.get_by_apikey(apikey)
|
|
|
|
if not raccordement:
|
|
return JsonResponse("Invalid API key", status=401, safe=False)
|
|
request.raccordement = raccordement
|
|
return func(request, *args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
|
|
@authenticate
|
|
def ping(request):
|
|
return JsonResponse({'err': 0})
|
|
|
|
|
|
class RendezVousDisponibleView(View):
|
|
def get(self, request):
|
|
collectivites_data = []
|
|
for collectivite in request.raccordement.collectivites.prefetch_related('lieux').order_by('created'):
|
|
lieux_data = []
|
|
collectivite_data = {
|
|
'id': collectivite.source_id,
|
|
'nom': collectivite.nom,
|
|
'url': collectivite.url,
|
|
'created': collectivite.created.isoformat(),
|
|
'last_update': collectivite.created.isoformat(),
|
|
'lieux': lieux_data,
|
|
'nombre_de_lieux': collectivite.lieux.count(),
|
|
'nombre_de_pre_demandes_actives': RendezVous.objects.filter(
|
|
date__lt=now(), lieu__collectivite=collectivite
|
|
).count(),
|
|
}
|
|
for key in ['logo_url', 'rdv_url', 'gestion_url', 'annulation_url']:
|
|
if getattr(collectivite, key, None):
|
|
collectivite_data[key] = getattr(collectivite, key)
|
|
for lieu in collectivite.lieux.order_by('created'):
|
|
lieu_data = {
|
|
'id': lieu.source_id,
|
|
'nom': lieu.nom,
|
|
'numero_rue': lieu.numero_rue,
|
|
'code_postal': lieu.code_postal,
|
|
'ville': lieu.ville,
|
|
'longitude': lieu.longitude,
|
|
'latitude': lieu.latitude,
|
|
'nombre_de_pre_demandes_actives': lieu.rdvs.filter(date__lt=now()).count(),
|
|
'nombre_de_jours_avec_rdv_disponibles': lieu.plages.filter(date__lt=now())
|
|
.distinct('date')
|
|
.count(),
|
|
}
|
|
for key in ['url', 'logo_url', 'rdv_url', 'gestion_url', 'annulation_url']:
|
|
if getattr(lieu, key, None):
|
|
lieu_data[key] = getattr(collectivite, key)
|
|
lieux_data.append(lieu_data)
|
|
collectivites_data.append(collectivite_data)
|
|
|
|
return JsonResponse(
|
|
{
|
|
'err': 0,
|
|
'collectivites': collectivites_data,
|
|
}
|
|
)
|
|
|
|
collectivites_created = 0
|
|
collectivites_updated = 0
|
|
lieux_created = 0
|
|
lieux_updated = 0
|
|
plage_created = 0
|
|
plage_deleted = 0
|
|
rdv_created = 0
|
|
rdv_deleted = 0
|
|
|
|
def handle_rdv_payload(self, lieu, rdv):
|
|
try:
|
|
identifiant_predemande = rdv.pop('id')
|
|
date = datetime.datetime.fromisoformat(rdv.pop('date'))
|
|
annule = bool(rdv.pop('annule', False))
|
|
except ValueError as e:
|
|
raise ValidationError('rdv %s invalid: %s' % (rdv, e))
|
|
if annule:
|
|
_, count_by_model = lieu.rdvs.filter(
|
|
identifiant_predemande=identifiant_predemande, date=date
|
|
).delete()
|
|
self.rdv_deleted += count_by_model['data.RendezVous']
|
|
return
|
|
_, created = lieu.rdvs.update_or_create(
|
|
identifiant_predemande=identifiant_predemande, date=date, defaults=rdv
|
|
)
|
|
self.rdv_created += bool(created)
|
|
|
|
def handle_plages_payload(self, lieu, plages):
|
|
by_date_and_type_and_personnes = collections.defaultdict(set)
|
|
full = set()
|
|
for plage in plages:
|
|
try:
|
|
date = datetime.date.fromisoformat(plage['date'])
|
|
types_rdv = [TypeDeRdv.from_label(typ) for typ in plage['types_rdv']]
|
|
if not types_rdv:
|
|
types_rdv = [TypeDeRdv.CNI]
|
|
personnes = plage.get('personnes', 1)
|
|
if not plage.get('heure_debut'):
|
|
full.update((date, t, personnes) for t in types_rdv)
|
|
continue
|
|
heure_debut = datetime.time.fromisoformat(plage['heure_debut'])
|
|
heure_fin = datetime.time.fromisoformat(plage['heure_fin'])
|
|
duree = int(plage['duree'])
|
|
if heure_fin < heure_debut:
|
|
raise ValueError('heure de fin inférieure à heure de début')
|
|
if datetime.datetime.combine(date, heure_fin) - datetime.datetime.combine(
|
|
date, heure_debut
|
|
) < datetime.timedelta(minutes=duree):
|
|
raise ValueError('différence heure de début et de fin inférieure à la durée')
|
|
for t in types_rdv:
|
|
by_date_and_type_and_personnes[(date, t, personnes)].add((heure_debut, heure_fin, duree))
|
|
except ValueError as e:
|
|
raise ValidationError('plage %s: %s' % (plage, e))
|
|
|
|
for x in set(by_date_and_type_and_personnes) | full:
|
|
date, type_rdv, personnes = x
|
|
_, count_by_model = lieu.plages.filter(date=date, type_de_rdv=type_rdv).delete()
|
|
if x in full:
|
|
self.plage_deleted += count_by_model['data.Plage']
|
|
continue
|
|
values = by_date_and_type_and_personnes.get(x)
|
|
durees = list({duree for heure_debut, heure_fin, duree in values})
|
|
if len(durees) > 1:
|
|
raise ValidationError(
|
|
'plages %s: la durée varie pour les mêmes rendez-vous du même lieu' % values
|
|
)
|
|
duree = durees[0]
|
|
horaires = HoraireList()
|
|
for start, end, _ in values:
|
|
try:
|
|
horaires.append(Horaire(start=start, end=end))
|
|
except ValueError:
|
|
raise ValidationError('plages %s-%s: les périodes se chevauchent' % (date, values))
|
|
|
|
lieu.plages.create(
|
|
date=date, type_de_rdv=type_rdv, personnes=personnes, horaires=horaires, duree=duree
|
|
)
|
|
self.plage_created += 1
|
|
|
|
def handle_lieu_payload(self, collectivite, payload):
|
|
source_id = payload.pop('id')
|
|
plages = payload.pop('plages', [])
|
|
rdvs = payload.pop('rdvs', [])
|
|
try:
|
|
lieu = collectivite.lieux.get(source_id=source_id)
|
|
updated = False
|
|
for key in payload:
|
|
if getattr(lieu, key) != payload[key]:
|
|
setattr(lieu, key, payload[key])
|
|
updated = True
|
|
lieu.full_clean()
|
|
if updated:
|
|
lieu.save()
|
|
self.lieux_updated += 1
|
|
except Lieu.DoesNotExist:
|
|
lieu = Lieu(collectivite=collectivite, source_id=source_id, **payload)
|
|
lieu.full_clean()
|
|
lieu.save()
|
|
self.lieux_created += 1
|
|
|
|
self.handle_plages_payload(lieu, plages)
|
|
for rdv in rdvs:
|
|
self.handle_rdv_payload(lieu, rdv)
|
|
|
|
def handle_collectivite_payload(self, raccordement, payload):
|
|
source_id = payload.pop('id')
|
|
lieux_payload = payload.pop('lieux', [])
|
|
|
|
try:
|
|
collectivite = raccordement.collectivites.get(source_id=source_id)
|
|
updated = False
|
|
for key in payload:
|
|
if getattr(collectivite, key) != payload[key]:
|
|
setattr(collectivite, key, payload[key])
|
|
updated = True
|
|
collectivite.full_clean()
|
|
if updated:
|
|
collectivite.save()
|
|
self.collectivites_updated += 1
|
|
except Collectivite.DoesNotExist:
|
|
collectivite = Collectivite(raccordement=raccordement, source_id=source_id, **payload)
|
|
collectivite.full_clean()
|
|
collectivite.save()
|
|
self.collectivites_created += 1
|
|
for lieu_payload in lieux_payload:
|
|
self.handle_lieu_payload(collectivite, lieu_payload)
|
|
|
|
def post(self, request):
|
|
try:
|
|
payload = json.loads(request.body)
|
|
except ValueError:
|
|
return JsonResponse({'err': 'invalid-json'}, status=400)
|
|
try:
|
|
jsonschema.validate(payload, RENDEZ_VOUS_DISPONIBLES_SCHEMA)
|
|
except jsonschema.ValidationError as e:
|
|
return JsonResponse({'err': 'invalid-json', 'detail': e.message}, status=400)
|
|
try:
|
|
with atomic():
|
|
# prevent concurrent updates to the data of the same raccordement
|
|
request.raccordement.lock()
|
|
for collectivite in payload.get('collectivites', []):
|
|
self.handle_collectivite_payload(request.raccordement, collectivite)
|
|
except (ValueError, ValidationError) as e:
|
|
return JsonResponse(
|
|
{
|
|
'err': 1,
|
|
'error': str(e),
|
|
}
|
|
)
|
|
|
|
return JsonResponse(
|
|
{
|
|
'err': 0,
|
|
'data': {
|
|
'collectivites_created': self.collectivites_created,
|
|
'collectivites_updated': self.collectivites_updated,
|
|
'lieux_created': self.lieux_created,
|
|
'lieux_updated': self.lieux_updated,
|
|
'plage_created': self.plage_created,
|
|
'plage_deleted': self.plage_deleted,
|
|
'rdv_created': self.rdv_created,
|
|
'rdv_deleted': self.rdv_deleted,
|
|
},
|
|
}
|
|
)
|
|
|
|
|
|
rendez_vous_disponibles = csrf_exempt(authenticate(RendezVousDisponibleView.as_view()))
|
|
|
|
|
|
@csrf_exempt
|
|
@authenticate
|
|
def predemandes(request):
|
|
if request.method == 'GET':
|
|
return get_predemandes(request)
|
|
elif request.method == 'POST':
|
|
return post_predemandes(request)
|
|
return JsonResponse({'err': 'method-no-allowed'}, status=405)
|
|
|
|
|
|
def get_predemandes(request):
|
|
return JsonResponse({'err': 0})
|
|
|
|
|
|
@atomic
|
|
def post_predemandes(request):
|
|
return JsonResponse({'err': 0})
|