zoo/zoo/zoo_nanterre/api_views.py

2102 lines
77 KiB
Python

# -*- coding: utf-8 -*-
#
# zoo - data management system
# 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 logging
import isodate
import copy
import re
import time
import traceback
from dateutil.relativedelta import relativedelta
import datetime
from decimal import Decimal, InvalidOperation
from django.conf import settings
from django.shortcuts import get_object_or_404
from django.db.models.query import Q
from django.db.transaction import non_atomic_requests, atomic
from django.urls import reverse
from django.http import Http404, HttpResponse
from django.utils.timezone import now
from django.utils.http import urlencode
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import serializers
from zoo.zoo_meta.models import EntitySchema, RelationSchema
from zoo.zoo_data.models import Entity, Relation, Transaction, Log
from . import utils, fragments, saga, qf, models
logger = logging.getLogger(__name__)
def flatten_errors(serializer_errors):
errors = []
for k, v in serializer_errors.items():
errors.append(u'%s: %s' % (k, v))
return errors
def individu_to_text(individu, short=False):
d = individu.content
text = d['nom_de_naissance'] + ' '
if d.get('nom_d_usage'):
text += '(' + d['nom_d_usage'] + ') '
text += d['prenoms']
if not short:
date = isodate.parse_date(d['date_de_naissance'])
text += ' - %02d/%02d/%04d - ' % (date.day, date.month, date.year)
text += d['genre']
if d.get('statut_legal'):
text += '/' + d['statut_legal']
return text
def adresse_to_text(adresse):
d = adresse.content
text = '%(streetnumber)s%(streetnumberext)s %(streetname)s, ' % d
for ext in ('at', 'ext1', 'ext2'):
if d.get(ext):
text += '%s, ' % d[ext]
text += '%(zipcode)s %(city)s' % d
if d.get('country') != 'FR':
text += ' (%(country)s)' % d
return text
def individu_to_response(individu, add_text=False, add_conjoint=True, add_enfant=True,
add_parents=True):
'''Serialize a person'''
d = individu.content.copy()
d['id'] = individu.id
if hasattr(individu, 'age_label'):
d['age_label'] = individu.age_label
if hasattr(individu, 'age'):
d['age'] = individu.age
if hasattr(individu, 'similarity'):
d['score'] = individu.similarity
if hasattr(individu, 'adresses'):
d['adresses'] = individu.adresses
if hasattr(individu, 'responsabilite_legale'):
d['responsabilite_legale'] = individu.responsabilite_legale
if add_enfant and hasattr(individu, 'enfants'):
d['enfants'] = [individu_to_response(enfant) for enfant in individu.enfants]
if add_parents and hasattr(individu, 'parents'):
d['parents'] = [individu_to_response(parent) for parent in individu.parents]
if add_conjoint and hasattr(individu, 'union'):
d[utils.UNION_REL] = individu_to_response(individu.union)
d['union_statut'] = individu.union_statut
if add_text:
d['text'] = individu_to_text(individu)
d['date_de_creation'] = individu.created.created.isoformat()
if individu.modified:
d['date_de_modification'] = individu.modified.created.isoformat()
else:
d['date_de_modification'] = d['date_de_creation']
return d
class TransactionalView(APIView):
def dispatch(self, request, *args, **kwargs):
if request.method not in ['GET', 'HEAD', 'OPTIONS']:
with atomic():
return super(TransactionalView, self).dispatch(request, *args, **kwargs)
else:
return super(TransactionalView, self).dispatch(request, *args, **kwargs)
def initial(self, request, *args, **kwargs):
if request.method not in ['GET', 'HEAD', 'OPTIONS']:
self.transaction = Transaction.get_transaction()
self.transaction.content = {}
super(TransactionalView, self).initial(request, *args, **kwargs)
def handle_exception(self, exc):
if hasattr(self, 'transaction'):
content = {
'request': self.request.data,
'status_code': 500,
'$exc_detail': str(exc),
'$exc_tb': traceback.format_exc(),
}
self.transaction.content = content
self.transaction.save()
return super(TransactionalView, self).handle_exception(exc)
def finalize_response(self, request, response, *args, **kwargs):
if hasattr(self, 'transaction') and not getattr(response, 'exception', False):
content = {
'url': request.build_absolute_uri(),
'request': request.data,
'response': response.data,
'status_code': response.status_code,
}
self.transaction.content.update(content)
self.transaction.save()
return super(TransactionalView, self).finalize_response(request, response, *args, **kwargs)
class SearchView(APIView):
def get(self, request, format=None):
try:
limit = int(request.GET.get('limit', ''))
except ValueError:
limit = 100
try:
offset = int(request.GET.get('offset', ''))
except ValueError:
offset = 0
try:
threshold = float(request.GET.get('threshold', ''))
except ValueError:
threshold = getattr(settings, 'ZOO_NANTERRE_SEARCH_THRESHOLD', 0.13)
search = utils.PersonSearch(limit=threshold, base_limit=threshold)
if 'q' in request.GET:
search = search.search_query(request.GET['q'])
else:
prenom = request.GET.get('prenom')
nom = request.GET.get('nom')
date_de_naissance = request.GET.get('date_de_naissance')
cle = request.GET.get('cle')
email = request.GET.get('email', '').strip()
name_id = request.GET.get('NameID')
if prenom or nom:
search = search.search_name(u'%s %s' % (prenom, nom))
if date_de_naissance and search.match_birthdate(date_de_naissance):
search = search.search_birthdate(date_de_naissance)
if cle:
search = search.search_identifier(cle)
if email:
search = search.search_email(email)
if name_id:
search = search.search_identifier(name_id, key='authentic')
for key in request.GET:
if key.startswith('cle_'):
cle = request.GET[key]
search = search.search_identifier(cle, key=key[4:])
if 'statut_legal' in request.GET:
search.search_statut_legal(request.GET['statut_legal'])
data = [individu_to_response(person, add_text=True)
for person in search[offset:offset + limit]]
return Response({
'err': 0,
'offset': offset,
'limit': limit,
'count': len(data),
'data': data,
'meta': {
'applications': utils.PersonSearch.applications(),
}
})
search = non_atomic_requests(SearchView.as_view())
class IndividuViewMixin(object):
def get_individu(self, identifier, **kwargs):
qs = Entity.objects.prefetch_related(
'left_relations__schema', 'left_relations__right',
'right_relations__schema', 'right_relations__left',
)
try:
identifier = int(identifier)
except ValueError:
return get_object_or_404(qs, schema__slug=utils.INDIVIDU_ENT,
content__cles_de_federation__authentic=identifier,
**kwargs)
else:
return get_object_or_404(qs, schema__slug=utils.INDIVIDU_ENT, id=identifier, **kwargs)
class ReseauView(IndividuViewMixin, TransactionalView):
def get(self, request, identifier, format=None):
individu = self.get_individu(identifier)
utils.PersonSearch.decorate_individu(individu)
return Response({
'err': 0,
'data': individu_to_response(individu),
'meta': {
'applications': utils.PersonSearch.applications(),
}
})
@atomic
def post(self, request, identifier, format=None):
individu = self.get_individu(identifier)
utils.PersonSearch.decorate_individu(individu)
serializer = CreateIndividuSerializer(data=request.data, partial=True)
if not serializer.is_valid():
return Response({
'err': 1,
'errors': flatten_errors(serializer.errors),
}, status=400)
if (serializer.validated_data.get(utils.ADRESSE_ENT)
and individu.content['statut_legal'] == 'mineur'):
return Response({
'err': 1,
'errors': [
u'l\'adresse est interdite pour un mineur',
],
}, status=400)
if serializer.validated_data.get('cles_de_federation'):
errors = []
for key, value in serializer.validated_data.get('cles_de_federation').items():
if value:
for other in Entity.objects.exclude(id=individu.id).filter(
**{'content__cles_de_federation__%s' % key: value}):
errors.append(u'la clé %s %s est déjà utilisée par l\'individu #%s' % (
key, value, other.id))
if errors:
return Response({
'err': 1,
'errors': errors,
}, status=400)
transaction = self.transaction
v = serializer.validated_data
identite_modifie = False
contact_modifie = False
cles_modifies = False
if 'prenoms' in v:
individu.content['prenoms'] = v['prenoms'].upper()
identite_modifie = True
if 'nom_de_naissance' in v:
individu.content['nom_de_naissance'] = v['nom_de_naissance'].upper()
identite_modifie = True
if 'nom_d_usage' in v:
individu.content['nom_d_usage'] = v['nom_d_usage'].upper()
identite_modifie = True
if 'date_de_naissance' in v:
individu.content['date_de_naissance'] = v['date_de_naissance'].isoformat()
identite_modifie = True
if 'genre' in v:
individu.content['genre'] = v['genre']
identite_modifie = True
if 'cles_de_federation' in v:
cles_de_federation = individu.content.setdefault('cles_de_federation', {})
for name in settings.ZOO_NANTERRE_APPLICATIONS:
if v['cles_de_federation'] and name in v['cles_de_federation']:
key = v['cles_de_federation'][name]
if not key: # remove key from individu
if name in cles_de_federation:
del cles_de_federation[name]
cles_modifies = True
else:
cles_de_federation[name] = key
cles_modifies = True
if 'email' in v:
individu.content['email'] = v['email']
contact_modifie = True
if 'telephones' in v:
individu.content['telephones'] = v['telephones']
contact_modifie = True
messages = []
if identite_modifie:
messages += list(fragments.MiseAJourIdentite.pour_chaque_application(
[individu],
meta=serializer.journal_meta,
transaction=transaction))
if contact_modifie:
messages += list(fragments.MiseAJourInformationsContact.pour_chaque_application(
[individu],
meta=serializer.journal_meta,
transaction=transaction))
if individu.content['statut_legal'] == 'majeur' and utils.ADRESSE_ENT in v:
adresse = utils.adresse(individu)
if not adresse:
return Response({
'err': 1,
'errors': ['Erreur interne: individu sans adresse ou avec plus d\'une adresse'],
})
utils.upper_dict(v['adresse'])
adresse.content = v['adresse']
adresse.modified = individu.modified
adresse.save()
habitants = Entity.objects.filter(left_relations__right=adresse)
messages += list(fragments.SignalementChangementAdresse.pour_chaque_application(
habitants,
meta=serializer.journal_meta,
transaction=transaction))
if identite_modifie or contact_modifie or cles_modifies:
individu.modified = transaction
individu.save()
# no need to update children and husband/wife adresses,
# they sould already have the same address
utils.journalize(
individu,
meta=serializer.journal_meta,
transaction=transaction,
text=u'Mise à jour des informations')
response = {
'err': 0,
'data': individu_to_response(individu),
}
if messages:
response['messages'] = messages
return Response(response)
reseau = non_atomic_requests(ReseauView.as_view())
class ReseauListView(IndividuViewMixin, APIView):
def get(self, request, identifier, format=None):
# permet de ne voir que le conjoint ou que les enfants
conjoint = request.GET.get('conjoint') is not None
enfants = request.GET.get('enfants') is not None
foyer = request.GET.get('foyer') is not None
# les deux options ensemble s'annulent
if int(conjoint) + int(enfants) + int(foyer) > 1:
conjoint = False
enfants = False
foyer = False
individu = self.get_individu(identifier)
utils.PersonSearch.decorate_individu(individu)
data = []
# on ajoute l'individu visé que si on n'a pas demandé à ne voir que le conjoint ou que les
# enfants
if not conjoint and not enfants:
data.append(individu_to_response(individu, add_parents=False, add_enfant=False,
add_conjoint=False, add_text=True))
if hasattr(individu, 'union') and not enfants:
data.append(individu_to_response(individu.union, add_text=True))
if hasattr(individu, 'enfants') and not conjoint:
enfants = set(individu.enfants)
if foyer and hasattr(individu, 'union'):
utils.PersonSearch.decorate_individu(individu.union)
if hasattr(individu.union, 'enfants'):
enfants.update(individu.union.enfants)
for enfant in enfants:
utils.PersonSearch.add_age(enfant)
# ordonne les enfants du plus agé au plus jeune
for enfant in sorted(enfants, key=lambda e: e.age, reverse=True):
data.append(individu_to_response(enfant, add_text=True))
return Response({
'err': 0,
'data': data,
})
reseau_liste = ReseauListView.as_view()
class JournalSerializerMixin(serializers.Serializer):
def __init__(self, instance=None, data=None, **kwargs):
meta = self.journal_meta = {}
if data:
data = data.copy()
for key in data:
if key.startswith('journal_'):
meta[key[8:]] = data[key]
super(JournalSerializerMixin, self).__init__(instance=instance, data=data, **kwargs)
class AdresseSerializer(serializers.Serializer):
at = serializers.CharField(allow_blank=True)
streetnumber = serializers.CharField(allow_blank=True)
streetnumberext = serializers.CharField(allow_blank=True)
streetname = serializers.CharField()
ext1 = serializers.CharField(allow_blank=True)
ext2 = serializers.CharField(allow_blank=True)
streetmatriculation = serializers.CharField(allow_blank=True)
zipcode = serializers.CharField(allow_blank=True)
inseecode = serializers.CharField(allow_blank=True)
city = serializers.CharField()
country = serializers.CharField()
adresse_inconnnue = serializers.BooleanField(default=False, required=False)
class TelephoneSerializer(serializers.Serializer):
numero = serializers.RegexField('^[0-9 .-]*$')
type = serializers.ChoiceField(
choices=[
'maison',
'mobile',
'pro',
'autre',
])
class CreateIndividuSerializer(JournalSerializerMixin):
genre = serializers.ChoiceField(
choices=[
'femme',
'homme',
'autre',
])
prenoms = serializers.CharField(max_length=128)
nom_d_usage = serializers.CharField(max_length=128, allow_blank=True)
nom_de_naissance = serializers.CharField(max_length=128)
date_de_naissance = serializers.DateField()
email = serializers.EmailField(allow_blank=True)
adresse = AdresseSerializer()
telephones = TelephoneSerializer(many=True)
cles_de_federation = serializers.DictField(child=serializers.CharField(allow_blank=True), required=False,
default=None)
class CreateIndividu(TransactionalView):
def post(self, request):
serializer = CreateIndividuSerializer(data=request.data)
transaction = self.transaction
if not serializer.is_valid():
return Response({
'err': 1,
'errors': flatten_errors(serializer.errors),
}, status=400)
if serializer.validated_data.get('cles_de_federation'):
return Response({
'err': 1,
'errors': [
'clés de fédération non admises lors de la création d\'un individu'
],
}, status=400)
schema = EntitySchema.objects.get(slug=utils.INDIVIDU_ENT)
schema_adresse = EntitySchema.objects.get(slug=utils.ADRESSE_ENT)
habite_schema = RelationSchema.objects.get(slug=utils.HABITE_REL)
v = serializer.validated_data
v['adresse_inconnnue'] = False # new individual always have known adresses
cles_de_federation = {}
individu = Entity(
schema=schema,
created=transaction,
content={
'prenoms': v['prenoms'].upper(),
'nom_d_usage': v['nom_d_usage'].upper(),
'nom_de_naissance': v['nom_de_naissance'].upper(),
'date_de_naissance': v['date_de_naissance'].isoformat(),
'telephones': v['telephones'],
'genre': v['genre'],
'statut_legal': 'majeur',
'cles_de_federation': cles_de_federation,
'email': v['email'],
})
content = v[utils.ADRESSE_ENT].copy()
utils.upper_dict(content)
content['adresse_inconnnue'] = False
individu.save()
adresse = Entity.objects.create(
schema=schema_adresse,
created=individu.created,
content=content,
)
Relation.objects.create(
created=individu.created,
left=individu,
right=adresse,
schema=habite_schema,
content={
'principale': False,
}
)
individu.adresses = [adresse.content]
utils.journalize(
individu,
transaction=transaction,
meta=serializer.journal_meta,
text=u'Création de l\'individu')
return Response({
'err': 0,
'data': individu_to_response(individu),
})
create_individu = CreateIndividu.as_view()
class EnfantSerializer(serializers.Serializer):
genre = serializers.ChoiceField(
choices=[
'femme',
'homme',
'autre',
])
prenoms = serializers.CharField(max_length=128)
nom_de_naissance = serializers.CharField(max_length=128)
date_de_naissance = serializers.DateField()
email = serializers.EmailField(allow_blank=True)
telephones = TelephoneSerializer(many=True)
class DeclarationResponsabiliteLegaleSerializer(JournalSerializerMixin):
statut = serializers.ChoiceField(
choices=[
'parent',
'tiers_de_confiance',
'representant_personne_morale_qualifiee',
])
enfant_id = serializers.IntegerField(required=False)
enfant = EnfantSerializer(required=False)
class DeclarationResponsabiliteLegale(IndividuViewMixin, TransactionalView):
def post(self, request, identifier, format=None):
individu = self.get_individu(identifier, content__statut_legal='majeur')
serializer = DeclarationResponsabiliteLegaleSerializer(data=request.data)
if not serializer.is_valid():
return Response({
'err': 1,
'errors': flatten_errors(serializer.errors),
}, status=400)
v = serializer.validated_data
if v.get('enfant_id') and v.get('enfant'):
return Response({
'err': 1,
'errors': [
u'vous ne devez pas fournir les paramètres enfant_id et enfant',
]
}, status=400)
if not v.get('enfant_id') and not v.get('enfant'):
return Response({
'err': 1,
'errors': [
u'vous devez fournir un des paramètres enfant_id ou enfant',
]
}, status=400)
transaction = self.transaction
individu_schema = EntitySchema.objects.get(slug=utils.INDIVIDU_ENT)
responsabilite_legale_schema = RelationSchema.objects.get(
slug=utils.RESPONSABILITE_LEGALE_REL)
habite_schema = RelationSchema.objects.get(slug=utils.HABITE_REL)
adresse = list(utils.adresses(individu))[0][0]
if v.get('enfant_id'): # existing child
try:
enfant = Entity.objects.get(
schema=individu_schema,
content__statut_legal='mineur',
id=v['enfant_id'])
except Entity.DoesNotExist:
return Response({
'err': 1,
'errors': [
u'enfant_id %s: identifiant inconnu' % v['enfant_id'],
]
}, status=400)
else: # new child
enfant = Entity.objects.create(
created=transaction,
schema=individu_schema,
content={
'prenoms': v['enfant']['prenoms'].upper(),
'nom_de_naissance': v['enfant']['nom_de_naissance'].upper(),
'nom_d_usage': '',
'email': v['enfant']['email'],
'date_de_naissance': v['enfant']['date_de_naissance'].isoformat(),
'genre': v['enfant']['genre'],
'telephones': v['enfant']['telephones'],
'statut_legal': 'mineur',
'cles_de_federation': {},
})
# verify child has not already two parents
if v['statut'] == 'parent':
if enfant.right_relations.filter(
content__statut='parent',
schema__slug=utils.RESPONSABILITE_LEGALE_REL).count() > 1:
return Response({
'err': 1,
'errors': [
u'enfant_id %s: cet enfant a déjà deux parents' % enfant.id,
]
}, status=400)
# verify child is not already linked to this parent
if enfant.right_relations.filter(schema__slug=utils.RESPONSABILITE_LEGALE_REL,
left=individu).exists():
return Response({
'err': 1,
'errors': [
u'enfant_id %s: cet enfant a déjà cet adulte pour responsable légal' % enfant.id,
]
}, status=400)
relation = enfant.right_relations.create(
created=transaction,
schema=responsabilite_legale_schema,
content={
'statut': v['statut'],
},
left=individu)
# if the child does not already live at this address, link him to it
if not enfant.left_relations.filter(schema=habite_schema, right=adresse):
enfant.left_relations.create(
created=transaction,
schema=habite_schema,
content={
'principale': False,
},
right=adresse)
utils.journalize(
individu, meta=serializer.journal_meta,
transaction=transaction,
text=u'Déclaration de responsabilité légale',
enfant_id=enfant.id,
enfant_text=individu_to_text(enfant))
utils.journalize(
enfant, meta=serializer.journal_meta,
transaction=transaction,
text=u'Déclaration de responsabilité légale',
adulte_id=individu.id,
adulte_text=individu_to_text(individu))
messages = list(fragments.DeclarationResponsabiliteLegaleEnfant.pour_chaque_application(
relation,
meta=serializer.journal_meta,
transaction=transaction))
response = {
'err': 0,
'data': individu_to_response(enfant),
}
if messages:
response['messages'] = messages
return Response(response)
declaration_responsabilite_legale = DeclarationResponsabiliteLegale.as_view()
class DeclarationUnionSerializer(JournalSerializerMixin):
individu_id_1 = serializers.IntegerField()
individu_id_2 = serializers.IntegerField()
adresse_commune = serializers.ChoiceField(choices=[1, 2])
statut = serializers.ChoiceField(
choices=['pacs/mariage', 'unionlibre'])
class DeclarationUnion(TransactionalView):
def post(self, request, format=None):
errors = []
serializer = DeclarationUnionSerializer(data=request.data)
if not serializer.is_valid():
return Response({
'err': 1,
'errors': flatten_errors(serializer.errors),
}, status=400)
v = serializer.validated_data
try:
individu1 = Entity.objects.get(id=v['individu_id_1'],
schema__slug=utils.INDIVIDU_ENT)
except Entity.DoesNotExist:
individu1 = None
try:
individu2 = Entity.objects.get(id=v['individu_id_2'],
schema__slug=utils.INDIVIDU_ENT)
except Entity.DoesNotExist:
individu2 = None
if not individu1:
errors.append(u'individu_id_1: identifiant inconnu')
if not individu2:
errors.append(u'individu_id_2: identifiant inconnu')
if errors:
return Response({
'err': 1,
'errors': errors,
})
# vérification sur la majorité
if (not individu1.content['statut_legal'] == 'majeur' or
not individu2.content['statut_legal'] == 'majeur'):
return Response({
'err': 1,
'errors': [
u'les deux individus ne sont pas tous les deux majeurs',
],
})
qs = Relation.objects.filter(schema__slug=utils.UNION_REL)
already1 = qs.filter(Q(left=individu1) | Q(right=individu1)).exists()
already2 = qs.filter(Q(left=individu2) | Q(right=individu2)).exists()
if already1:
errors.append(u'individu_id_1: cet individu est déjà dans une relation maritale')
if already2:
errors.append(u'individu_id_2: cet individu est déjà dans une relation maritale')
if errors:
return Response({
'err': 1,
'errors': errors,
})
transaction = self.transaction
union_schema = RelationSchema.objects.get(slug=utils.UNION_REL)
relation = Relation.objects.create(
created=transaction,
schema=union_schema,
left=individu1,
right=individu2,
content={
'statut': v['statut'],
})
adresse1 = utils.adresse(individu1)
adresse2 = utils.adresse(individu2)
if not adresse1 or not adresse2:
return Response({
'err': 1,
'errors': ['Erreur interne: individus sans adresses ou avec plus d\'une adresse'],
})
assert adresse1 != adresse2, 'people should not have the same address'
if v['adresse_commune'] == 1:
qs = Entity.objects.filter(left_relations__right=adresse1)
Relation.objects.exclude(left__in=qs).filter(right=adresse2) \
.update(right=adresse1, modified=transaction)
adresse2.delete()
else:
qs = Entity.objects.filter(left_relations__right=adresse2)
Relation.objects.exclude(left__in=qs).filter(right=adresse1) \
.update(right=adresse2, modified=transaction)
adresse1.delete()
utils.journalize(
individu1, meta=serializer.journal_meta,
text=u'Déclaration d\'union',
transaction=transaction,
left_id=individu1.id,
left_text=individu_to_text(individu1),
right_id=individu2.id,
right_text=individu_to_text(individu2))
utils.journalize(
individu2, meta=serializer.journal_meta,
text=u'Déclaration d\'union',
transaction=transaction,
left_id=individu1.id,
left_text=individu_to_text(individu1),
right_id=individu2.id,
right_text=individu_to_text(individu2))
messages = list(fragments.DeclarationUnion.pour_chaque_application(
relation,
meta=serializer.journal_meta,
transaction=transaction))
response = {'err': 0}
if messages:
response['messages'] = messages
return Response(response)
declaration_union = DeclarationUnion.as_view()
class JournalView(IndividuViewMixin, APIView):
FILTER_RE = re.compile('^[a-z_]*$')
def get(self, request, identifier, format=None):
individu = self.get_individu(identifier)
filters = {}
for key in request.GET:
if key.startswith('filter_') and self.FILTER_RE.match(key):
value = request.GET.getlist(key)
if len(value) > 1:
filters['content__' + key[7:] + '__in'] = value
else:
filters['content__' + key[7:]] = value[0]
try:
limit = int(request.GET.get('limit', ''))
except ValueError:
limit = 10
try:
cookie = request.GET.get('cookie', '')
timestamp, last_id = cookie.split('_', 1)
timestamp = isodate.parse_datetime(timestamp)
last_id = int(last_id)
except:
cookie = None
limit = min(limit, 100)
qs = Log.objects.filter(entity=individu)
qs = qs.order_by('-timestamp', 'id')
if filters:
qs = qs.filter(**filters)
if cookie:
qs = qs.filter(Q(timestamp__lt=timestamp) |
Q(timestamp=timestamp, id__gt=last_id))
qs = qs[:limit + 1]
data = [
{
'id': log.id,
'timestamp': log.timestamp.isoformat(),
'content': log.content
} for log in qs[:limit]
]
content = {
'err': 0,
'data': data,
}
if len(qs) > limit:
timestamp = qs[limit-1].timestamp.isoformat()
last_id = qs[limit-1].id
cookie = '%s_%s' % (timestamp, last_id)
more_url = request.build_absolute_uri(
reverse('rsu-api-journal', kwargs={'identifier': individu.id}))
more_url += '?' + urlencode({'limit': limit, 'cookie': cookie})
content['cookie'] = cookie
content['more'] = more_url
return Response(content)
def post(self, request, identifier, format=None):
qs = Entity.objects.prefetch_related(
'left_relations__schema', 'left_relations__right',
'right_relations__schema', 'right_relations__left',
)
individu = get_object_or_404(qs, schema__slug=utils.INDIVIDU_ENT, id=identifier)
Log.objects.create(
entity=individu,
content=request.data)
return Response({
'err': 0,
})
journal = JournalView.as_view()
class DeclarationAdressePrincipaleSerializer(JournalSerializerMixin):
adresse_principale = serializers.IntegerField()
class DeclarationAdressePrincipaleView(IndividuViewMixin, TransactionalView):
def get(self, request, identifier, format=None):
individu = self.get_individu(identifier)
adresses = []
for i, (adresse, rel) in enumerate(list(utils.adresses(individu)), 1):
adresses.append({
'id': str(i),
'text': adresse_to_text(adresse),
'rel': rel.content,
'adresse': adresse.content,
})
return Response({'err': 0, 'data': adresses})
def post(self, request, identifier, format=None):
individu = self.get_individu(identifier)
serializer = DeclarationAdressePrincipaleSerializer(data=request.data)
if not serializer.is_valid():
return Response({
'err': 1,
'errors': flatten_errors(serializer.errors),
}, status=400)
if individu.content['statut_legal'] != 'mineur':
return Response({
'err': 1,
'errors': [
u'cet individu n\'est pas mineur'
]
})
adresses = list(utils.adresses(individu))
idx = serializer.validated_data['adresse_principale']
if not (1 <= idx <= len(adresses)):
return Response({
'err': 1,
'errors': [
u'identifiant d\'adresse inconnu'
]
})
transaction = self.transaction
for i, (adresse, rel) in enumerate(adresses):
if i + 1 == idx:
if not rel.content['principale']:
rel.content['principale'] = True
rel.modified = transaction
rel.save()
else:
if rel.content['principale']:
rel.content['principale'] = False
rel.modified = transaction
rel.save()
messages = list(fragments.SignalementChangementAdresse.pour_chaque_application(
[individu],
meta=serializer.journal_meta,
transaction=transaction))
utils.journalize(
individu, meta=serializer.journal_meta,
text=u'Déclaration d\'adresse principale',
adresse_idx=idx,
transaction=transaction)
response = {
'err': 0
}
if messages:
response['messages'] = messages
return Response(response)
declaration_adresse_principale = DeclarationAdressePrincipaleView.as_view()
class ChangementDeSituationMaritaleSerializer(JournalSerializerMixin):
statut = serializers.ChoiceField(
choices=['pacs/mariage', 'unionlibre'])
class ChangementDeSituationMaritaleView(IndividuViewMixin, TransactionalView):
def post(self, request, identifier, format=None):
individu = self.get_individu(identifier)
if individu.content['statut_legal'] != 'majeur':
return Response({
'err': 1,
'errors': [
u'cet individu n\'est pas majeur',
]
})
conjoint, conjoint_rel = utils.conjoint(individu)
if not conjoint:
return Response({
'err': 1,
'errors': [
u"cet individu n'a pas actuellement de relation maritale",
]
})
serializer = ChangementDeSituationMaritaleSerializer(data=request.data)
if not serializer.is_valid():
return Response({
'err': 1,
'errors': serializer.errors,
}, status=400)
if conjoint_rel.content['statut'] == serializer.validated_data['statut']:
return Response({
'err': 1,
'errors': [
u'la relation maritale est déjà de ce type'
]
})
old_statut = conjoint_rel.content['statut']
conjoint_rel.content['statut'] = serializer.validated_data['statut']
conjoint_rel.modified = self.transaction
conjoint_rel.save()
utils.journalize(
individu,
meta=serializer.journal_meta,
text=u'Changement de situation maritale',
old_statut=old_statut,
statut=conjoint_rel.content['statut'],
transaction=self.transaction)
utils.journalize(
conjoint,
meta=serializer.journal_meta,
text=u'Changement de situation maritale',
old_statut=old_statut,
statut=conjoint_rel.content['statut'],
transaction=self.transaction)
messages = list(fragments.DeclarationUnion.pour_chaque_application(
conjoint_rel,
meta=serializer.journal_meta,
transaction=self.transaction))
response = {
'err': 0
}
if messages:
response['messages'] = messages
return Response(response)
changement_de_situation_maritale = ChangementDeSituationMaritaleView.as_view()
class SeparationView(IndividuViewMixin, TransactionalView):
def enfants_communs(self, individu, conjoint):
children = set(enfant for enfant, rel in utils.enfants(individu))
children &= set(enfant for enfant, rel in utils.enfants(conjoint))
return sorted(children, key=lambda e: e.id)
def get(self, request, identifier, format=None):
individu = self.get_individu(identifier)
if individu.content['statut_legal'] != 'majeur':
return Response({
'err': 1,
'errors': [
u'cet individu n\'est pas majeur',
]
})
conjoint, conjoint_rel = utils.conjoint(individu)
if not conjoint:
return Response({
'err': 1,
'errors': [
u'cet individu n\'a pas actuellement de relation maritale',
]
})
return Response({
'err': 0,
'data': {
utils.UNION_REL: individu_to_response(conjoint),
'union_statut': conjoint_rel.content['statut'],
'enfants': [individu_to_response(enfant, add_text=True) for enfant in
self.enfants_communs(individu, conjoint)],
},
})
def post(self, request, identifier, format=None):
individu = self.get_individu(identifier)
serializer = JournalSerializerMixin(data=request.data)
serializer.is_valid()
utils.PersonSearch.decorate_individu(individu)
if individu.content['statut_legal'] != 'majeur':
return Response({
'err': 1,
'errors': [
u'cet individu n\'est pas majeur',
]
})
conjoint, conjoint_rel = utils.conjoint(individu)
if not conjoint:
return Response({
'err': 1,
'errors': [
u'cet individu n\'a pas actuellement de relation maritale',
]
})
enfants_communs = self.enfants_communs(individu, conjoint)
adresse_1 = utils.adresse(individu)
if not adresse_1:
return Response({
'err': 1,
'errors': ['Erreur interne: individu sans adresse ou avec plus d\'une adresse'],
})
errors = []
enfant_ids = [enfant.id for enfant in enfants_communs]
adresse_principale_1 = request.data.get('adresse_principale_1', [])
adresse_principale_2 = request.data.get('adresse_principale_2', [])
if not isinstance(adresse_principale_1, list):
errors.append(u'adresse_principale_1: doit être une liste d\'identifiants.')
if not isinstance(adresse_principale_2, list):
errors.append(u'adresse_principale_2: doit être une liste d\'identifiants.')
for key in adresse_principale_1:
if key not in enfant_ids:
errors.append(u'adresse_principale_1: l\'enfant %s n\'est pas commun' % key)
for key in adresse_principale_2:
if key not in enfant_ids:
errors.append(u'adresse_principale_2: l\'enfant %s n\'est pas commun' % key)
if set(adresse_principale_1) & set(adresse_principale_2):
errors.append(u'un enfant ne peut pas avoir deux adresses principales')
if errors:
return Response({
'err': 1,
'errors': errors,
})
transaction = self.transaction
# lie le conjoint à une nouvelle adresse copie de la première
adresse_2 = copy.copy(adresse_1)
adresse_2.id = None
adresse_2.save()
assert adresse_2.id and adresse_1.id != adresse_2.id
c = (Relation.objects.filter(left=conjoint, right=adresse_1, schema__slug=utils.HABITE_REL)
.update(right=adresse_2, modified=transaction))
assert c == 1
# mise à jour de l'adresse des enfants du conjoint avec sa nouvelle adresse
enfants_du_conjoint = [enfant for enfant, rel in utils.enfants(conjoint) if enfant not in
enfants_communs]
c = Relation.objects.filter(
left__in=enfants_du_conjoint,
right=adresse_1,
schema__slug=utils.HABITE_REL).update(
right=adresse_2,
modified=transaction)
# crée la nouvelle relation utils.HABITE_REL et pose le flag principale si demandé
habite_schema = RelationSchema.objects.get(slug=utils.HABITE_REL)
for enfant in enfants_communs:
rel_adresse_1 = Relation.objects.get(left=enfant, right=adresse_1)
rel_adresse_2 = Relation(
left=enfant, right=adresse_2,
schema=habite_schema,
created=transaction,
content={
'principale': False,
})
if enfant.id in adresse_principale_1:
rel_adresse_1.content['principale'] = True
else:
rel_adresse_1.content['principale'] = False
rel_adresse_2.content['principale'] = True
rel_adresse_1.modified = transaction
rel_adresse_1.save()
rel_adresse_2.save()
# supprime la relation
relation = Relation.objects.filter(
left__in=[individu, conjoint],
right__in=[individu, conjoint],
schema__slug=utils.UNION_REL).select_related().get()
relation.delete()
utils.journalize(
individu,
meta=serializer.journal_meta,
text=u'Déclaration de séparation',
transaction=transaction,
conjoint_id=conjoint.id,
conjoint_text=individu_to_text(conjoint))
utils.journalize(
conjoint,
meta=serializer.journal_meta,
text=u'Déclaration de séparation',
transaction=transaction,
individu_id=individu.id,
individu_text=individu_to_text(individu))
messages = list(fragments.DeclarationSeparation.pour_chaque_application(
relation,
meta=serializer.journal_meta,
transaction=transaction))
response = {
'err': 0,
}
if messages:
response['messages'] = messages
return Response(response)
separation = SeparationView.as_view()
class DeclarationDeDecesSerializer(JournalSerializerMixin):
date_de_deces = serializers.DateField()
class DeclarationDeDecesView(IndividuViewMixin, TransactionalView):
def post(self, request, identifier, format=None):
individu = self.get_individu(identifier)
if individu.content.get('date_de_deces'):
return Response({
'err': 1,
'errors': [
u'cette personne est déjà déclaré décédée.',
]
})
# check de l'adresse
adresse = utils.adresse(individu)
if not adresse:
return Response({
'err': 1,
'errors': ['Erreur interne: individu sans adresse ou avec plus d\'une adresse'],
})
serializer = DeclarationDeDecesSerializer(data=request.data)
if not serializer.is_valid():
return Response({
'err': 1,
'errors': flatten_errors(serializer.errors),
}, status=400)
transaction = self.transaction
# on stocke la date du décés
individu.content['date_de_deces'] = serializer.validated_data['date_de_deces'].isoformat()
individu.modified = transaction
individu.save()
# on supprime les relations
Relation.objects.filter(schema__slug=utils.RESPONSABILITE_LEGALE_REL,
left=individu).delete()
Relation.objects.filter(schema__slug=utils.RESPONSABILITE_LEGALE_REL,
right=individu).delete()
Relation.objects.filter(schema__slug=utils.UNION_REL, left=individu).delete()
Relation.objects.filter(schema__slug=utils.UNION_REL, right=individu).delete()
# si c'est un adulte, gestion de son adresse
if individu.content['statut_legal'] == 'majeur':
# si l'adresse est partagée avec un autre adulte on l'individualise
if adresse.right_relations.filter(left__content__statut_legal='majeur').count() > 1:
new_adresse = copy.copy(adresse)
new_adresse.created = transaction
new_adresse.id = None
new_adresse.save()
assert new_adresse.id and new_adresse.id != adresse.id
c = Relation.objects.filter(schema__slug=utils.HABITE_REL, left=individu,
right=adresse).update(right=new_adresse)
assert c == 1
# sinon l'adresse n'est partagée qu'avec des enfants : on les débranche
else:
Relation.objects.filter(
schema__slug=utils.HABITE_REL, right=adresse).exclude(
left=individu).delete()
utils.journalize(
individu,
meta=serializer.journal_meta,
text=u'Déclaration de décés',
transaction=transaction)
return Response({
'err': 0,
})
declaration_de_deces = DeclarationDeDecesView.as_view()
class SuppressionLienDeResponsabiliteView(IndividuViewMixin, TransactionalView):
def post(self, request, identifier, identifier_enfant, format=None):
individu = self.get_individu(identifier)
enfant = self.get_individu(identifier_enfant)
# check de l'adresse
adresse = utils.adresse(individu)
if not adresse:
return Response({
'err': 1,
'errors': ['Erreur interne: individu sans adresse ou avec plus d\'une adresse'],
})
try:
relation = Relation.objects.get(left=individu, right=enfant,
schema__slug=utils.RESPONSABILITE_LEGALE_REL)
except Relation.DoesNotExist:
return Response({
'err': 1,
'errors': [
u'cet adulte n\'a pas de responsabilité légale sur cet enfant',
]
}, status=400)
# supression de la relation
relation = Relation.objects.filter(
left=individu, right=enfant,
schema__slug=utils.RESPONSABILITE_LEGALE_REL).select_related().get()
relation.delete()
# suppression éventuelle de l'adresse commune entre l'enfant et l'adulte
# si elle n'appartient pas aussi à un autre adulte ayant des responsabilités
# légales sur le même enfant
other_individu_ids = [rel.left_id for rel in
Relation.objects.filter(left__content__statut_legal='majeur',
right=adresse,
schema__slug=utils.HABITE_REL)]
if not Relation.objects.filter(left_id__in=other_individu_ids,
right=enfant,
schema__slug=utils.RESPONSABILITE_LEGALE_REL).exists():
Relation.objects.filter(left=enfant, right=adresse).delete()
serializer = JournalSerializerMixin(data=request.data)
serializer.is_valid()
utils.journalize(
individu,
meta=serializer.journal_meta,
text=u'Supression de lien de responsabilité légale',
statut=relation.content['statut'],
enfant_id=enfant.id,
enfant_text=individu_to_text(enfant),
transaction=self.transaction)
utils.journalize(
enfant,
meta=serializer.journal_meta,
text=u'Supression de lien de responsabilité légale',
statut=relation.content['statut'],
adulte_id=individu.id,
adulte_text=individu_to_text(individu),
transaction=self.transaction)
messages = list(fragments.SuppressionResponsabiliteEnfant.pour_chaque_application(
relation,
meta=serializer.journal_meta,
transaction=self.transaction))
response = {
'err': 0,
}
if messages:
response['messages'] = messages
return Response(response)
suppression_lien_de_responsabilite = SuppressionLienDeResponsabiliteView.as_view()
class SynchronisationSerializer(JournalSerializerMixin):
applications = serializers.ListField(
child=serializers.CharField())
individus = serializers.ListField(
child=serializers.IntegerField())
class Synchronisation(TransactionalView):
def post(self, request, format=None):
qs = Entity.objects.prefetch_related(
'left_relations__schema', 'left_relations__right',
'right_relations__schema', 'right_relations__left',
).filter(schema__slug=utils.INDIVIDU_ENT)
serializer = SynchronisationSerializer(data=request.data)
if not serializer.is_valid():
return Response({
'err': 1,
'errors': flatten_errors(serializer.errors),
}, status=400)
individus = []
for application in serializer.validated_data['applications']:
app_dfn = utils.get_application(application)
if not app_dfn or 'rsu_ws_url' not in app_dfn:
return Response({
'err': 1,
'errors': [
u'l\'application "%s" est invalide.' % application,
]
}, status=400)
for individu_id in serializer.validated_data['individus']:
try:
individu = qs.get(id=individu_id)
except Entity.DoesNotExist:
return Response({
'err': 1,
'errors': [
u'l\'individu %s est inconnu.' % individu_id,
]
}, status=400)
individus.append(individu)
errors = list(fragments.synchronize(
serializer.validated_data['applications'],
individus,
meta=serializer.journal_meta,
transaction=self.transaction))
if errors:
return Response({
'err': 1,
'errors': errors,
})
return Response({
'err': 0,
})
synchronization = Synchronisation.as_view()
class SuppressionIndividu(IndividuViewMixin, TransactionalView):
def post(self, request, identifier, format=None):
individu = self.get_individu(identifier)
# vérifie l'absence de relation et de fédérations
errors = []
parent_count = len(list(utils.parents(individu)))
if parent_count:
errors.append(u'cet individu a encore %d lien(s) avec un parent' % parent_count)
enfant_count = len(list(utils.enfants(individu)))
if enfant_count:
errors.append(u'cet individu a encore %d lien(s) avec un enfant' % enfant_count)
conjoint, rel = utils.conjoint(individu)
if conjoint:
errors.append(u'cet individu a un conjoint')
uuid_authentic = None
cles_de_federation = individu.content['cles_de_federation']
for application in cles_de_federation:
if application == 'authentic':
continue
elif application.startswith('saga'):
continue
else:
errors.append(u'cet individu a encore une clé de fédération avec %s' % application)
if errors:
return Response({
'err': 1,
'errors': errors,
})
adresses = utils.adresses(individu)
individu.delete()
# supprimer les adresses orphelines
for adresse, rel in adresses:
# left_relations ne devrait pas exister, mais dans le doute...
if not adresse.right_relations.exists() and not adresse.left_relations.exists():
adresse.delete()
response = {'err': 0}
if cles_de_federation:
response['cles_de_federation'] = cles_de_federation
response['messages'] = []
if 'authentic' in cles_de_federation:
response['messages'].append(
u'cette fiche avait une clé de fédération sur le portail internet: %s' %
cles_de_federation['authentic'])
if 'saga_tiers' in cles_de_federation:
response['messages'].append(
u'cette fiche avait une clé de fédération SAGA: '
u'tiers %s métier %s' %
(cles_de_federation['saga_tiers'],
cles_de_federation.get('saga', 'Aucune')))
return Response(response)
suppression_individu = SuppressionIndividu.as_view()
class Federation(IndividuViewMixin, APIView):
permission_classes = ()
def get(self, request, identifier, application, format=None):
app_dfn = utils.get_application(application)
if not app_dfn:
raise Http404
apikey = app_dfn.get('apikey')
if not apikey:
return Response({'err': 1, 'errors': ['accès interdit']}, status=403)
if request.GET.get('apikey') != apikey:
return Response({'err': 1, 'errors': ['apikey invalide']}, status=401)
individu = self.get_individu(identifier)
return Response({'err': 0, 'cle_de_federation': individu.content['cles_de_federation'].get(application)})
federation = Federation.as_view()
class SagaTiers(APIView):
permission_classes = ()
def get(self, request, application, identifier, format=None):
app_dfn = utils.get_application(application)
if not app_dfn or not app_dfn.get('rsu_ws_url'):
raise Http404('unknown application')
qs = Entity.objects.filter(**{'content__cles_de_federation__%s' % application: identifier})
qs = qs.select_for_update()
try:
individu = qs.get()
except Entity.DoesNotExist:
raise Http404('unknown identifier')
if 'saga_tiers' not in individu.content['cles_de_federation']:
individu.content['cles_de_federation']['saga_tiers'] = (
'RG%013d' % utils.get_next_saga_sequence())
individu.save()
return Response({
'code': individu.content['cles_de_federation']['saga_tiers']
})
saga_tiers = SagaTiers.as_view()
class SagaMixin(object):
individu = None
def dispatch(self, *args, **kwargs):
self.logger = logging.getLogger('zoo_nanterre.saga')
return super(SagaMixin, self).dispatch(*args, **kwargs)
def error_response(self, error, status=200):
logger = logging.getLogger('zoo_nanterre.saga')
if self.individu:
logger.warning(u'SAGA: %s pour %s', error, self.individu)
else:
logger.warning(u'SAGA: %s', error)
return Response({
'err': 1,
'errors': [
error
]
}, status=status)
def get_ws(self):
app_dfn = utils.get_application('saga')
if not app_dfn or 'url' not in app_dfn:
return None, u'URL de l\'application SAGA n\'est pas configurée'
ws = self.ws = saga.Saga(app_dfn['url'],
timeout=app_dfn.get('timeout'),
base_uri=app_dfn.get('base_uri'),
num_service=app_dfn.get('num_service'))
return ws, None
def get_factures(self, identifier, etats=None):
self.individu = self.get_individu(identifier)
conjoint, rel = utils.conjoint(self.individu)
# on ignore les conjoints en union libre
if conjoint and rel.content['statut'] != 'pacs/mariage':
conjoint, rel = None, None
debut = datetime.date.today() - relativedelta(months=6)
start_time = time.time()
factures, error = self.get_facture_for_individu(self.individu, debut=debut)
if error:
if conjoint:
factures = []
else:
return None, error
if conjoint:
timeout = settings.ZOO_NANTERRE_RSU_TIMEOUT - int(time.time() - start_time)
if timeout > 0:
factures_conjoint, error_conjoint = self.get_facture_for_individu(conjoint,
debut=debut,
timeout=timeout)
if not error_conjoint:
factures.extend(factures_conjoint)
elif error:
return None, error
elif error:
return None, error
# filtrer les factures par état
if etats:
factures = [facture for facture in factures if facture.etat in etats]
# retrier les facture par date d'emission
factures.sort(key=lambda f: f.date_facture)
return factures, None
def get_facture_for_individu(self, individu, debut=None, timeout=None):
if 'saga_tiers' not in individu.content['cles_de_federation']:
return [], None
code_tiers = individu.content['cles_de_federation']['saga_tiers']
ws, error = self.get_ws()
if error:
return None, error
federation = individu.content['cles_de_federation'].get('saga')
if not federation:
# obtention d'un verrou sur l'entité avant mise à jour
individu = Entity.objects.select_for_update().get(id=individu.id)
federation = individu.content['cles_de_federation'].get('saga')
if not federation:
federation, error = ws.resolve_code_tiers(code_tiers)
if error:
return None, error
individu.content['cles_de_federation']['saga'] = federation
individu.save()
if timeout is None:
timeout = settings.ZOO_NANTERRE_RSU_TIMEOUT
factures, error = ws.factures(federation, debut=debut, timeout=timeout)
if error:
return None, error
# définir le redevable sur les factures
redevable = individu_to_text(individu, short=True)
for facture in factures:
facture.extra['redevable'] = redevable
return factures, None
class SagaFactures(SagaMixin, IndividuViewMixin, APIView):
def get(self, request, identifier, format=None):
etats = set(request.GET.getlist('etats', []))
if 'tresorerie' in etats:
etats.update([u'dépassée', 'transmise'])
factures, error = self.get_factures(identifier, etats=etats)
if error:
return self.error_response(error)
def to_json(o):
d = {}
for field in o._fields:
if field == 'creances':
d['creances'] = [to_json(creance) for creance in o.creances]
else:
d[field] = getattr(o, field)
return d
return Response({
'err': 0,
'data': [
to_json(facture) for facture in factures
],
})
saga_factures = SagaFactures.as_view()
class TransactionSagaSerializer(serializers.Serializer):
num_factures = serializers.ListField(child=serializers.CharField())
urlretour_asynchrone = serializers.URLField()
urlretour_synchrone = serializers.URLField()
email = serializers.EmailField(required=False)
class SagaTransaction(SagaFactures):
def post(self, request, identifier, format=None):
serializer = TransactionSagaSerializer(data=request.data)
if not serializer.is_valid():
return self.error_response(flatten_errors(serializer.errors), status=400)
data = serializer.validated_data
factures, error = self.get_factures(identifier)
if error:
return self.error_response(error)
factures_a_payer = []
for facture in factures:
if facture.num in data['num_factures']:
factures_a_payer.append(facture)
if not factures_a_payer:
return self.error_response(u'numéro(s) de facture inconnu')
data = serializer.validated_data
email = data.get('email') or self.individu.content['email'] or ''
url, error = self.ws.transaction(
factures=factures_a_payer,
urlretour_asynchrone=data['urlretour_asynchrone'],
urlretour_synchrone=data['urlretour_synchrone'],
email=email)
if error:
return self.error_response(error)
self.logger.info(u'transaction de paiement pour la personne %s pour les factures %s: %s',
identifier, data['num_factures'], url)
return Response({
'err': 0,
'data': {
'url': url,
},
})
saga_transaction = SagaTransaction.as_view()
class RetourSagaSerializer(serializers.Serializer):
idop = serializers.CharField()
class SagaRetourAsynchrone(SagaMixin, APIView):
def post(self, request, format=None):
serializer = RetourSagaSerializer(data=request.data)
if not serializer.is_valid():
return self.error_response(flatten_errors(serializer.errors))
idop = serializer.validated_data['idop']
ws, error = self.get_ws()
if error:
self.logger.error(u'SAGA: retour asynchrone avec idop %s, erreur "%s"', idop, error)
return self.error_response(error)
result, error = ws.page_retour_asynchrone(idop)
if error:
self.logger.error(u'SAGA: retour asynchrone avec idop %s, erreur "%s"', idop, error)
return self.error_response(error)
self.logger.info(u'SAGA: retour asynchrone avec idop %s, %s', idop, result)
return Response({
'err': 0,
'data': result,
})
saga_retour_asynchrone = SagaRetourAsynchrone.as_view()
class SagaRetourSynchrone(SagaMixin, APIView):
def post(self, request, format=None):
serializer = RetourSagaSerializer(data=request.data)
if not serializer.is_valid():
return self.error_response(flatten_errors(serializer.errors))
idop = serializer.validated_data['idop']
ws, error = self.get_ws()
if error:
self.logger.error(u'SAGA: retour synchrone avec idop %s, erreur "%s"', idop, error)
return self.error_response(error)
result, error = ws.page_retour_synchrone(idop)
if error:
self.logger.error(u'SAGA: retour synchrone avec idop %s, erreur "%s"', idop, error)
return self.error_response(error)
self.logger.info(u'SAGA: retour synchrone avec idop %s, %s', idop, result)
return Response({
'err': 0,
'data': result,
})
saga_retour_synchrone = SagaRetourSynchrone.as_view()
class QFLireQuotientsValide(APIView):
def get(self, request, format=None):
ws = qf.QF()
response, error = ws.lire_quotients_valides(now())
if error:
logger.warning(u'lire-quotients-valide %s', error)
return Response({
'err': 1,
'errors': [error],
})
return Response({
'err': 0,
'data': [
{
'id': data['nature-qf'],
'text': data['libelle'],
'annee_imposition': data['annee_imposition'],
} for data in response
]
})
qf_lire_quotiens_valides = QFLireQuotientsValide.as_view()
class QFSimuleSerializer(JournalSerializerMixin):
nature_qf = serializers.IntegerField()
annee_imposition = serializers.IntegerField()
rfr = serializers.IntegerField()
nb_parts = serializers.DecimalField(max_digits=5, decimal_places=2)
monoparentalite = serializers.BooleanField()
annee_imposition_concubin = serializers.IntegerField(required=False)
rfr_concubin = serializers.IntegerField(required=False)
nb_parts_concubin = serializers.DecimalField(max_digits=5, decimal_places=2, required=False)
class QFSimuler(APIView):
def post(self, request, format=None):
serializer = QFSimuleSerializer(data=request.data)
if not serializer.is_valid():
return Response({
'err': 1,
'errors': flatten_errors(serializer.errors),
}, status=400)
ws = qf.QF()
d = serializer.validated_data
result, error = ws.simuler_qf(**d)
if error:
logger.warning('qf-simuler %s', error)
return Response({
'err': 1,
'errors': [error],
})
return Response({
'err': 0,
'data': result,
})
qf_simuler = QFSimuler.as_view()
class QFCalculer(IndividuViewMixin, TransactionalView):
def post(self, request, identifier, format=None):
serializer = QFSimuleSerializer(data=request.data)
if not serializer.is_valid():
return Response({
'err': 1,
'errors': flatten_errors(serializer.errors),
}, status=400)
d = serializer.validated_data.copy()
individu = self.get_individu(identifier)
d['individu'] = individu
result, error = qf.CalculQF.calcul_qf(meta=serializer.journal_meta,
transaction=self.transaction, **d)
if error:
logger.warning(u'qf-calculer identifier: %s request: %s erreur: %s', identifier,
serializer.validated_data, error)
return Response({
'err': 1,
'errors': [error],
})
return Response({
'err': 0,
'data': result,
})
def get(self, request, identifier, format=None):
individu = self.get_individu(identifier)
ws = qf.QF()
# si pas de clé, on teste que le service fonctionne
if not individu.content['cles_de_federation'].get('implicit'):
result, error = ws.lire_quotients_valides(now())
result = [] # on oublie le résultat, on voulait juste vérifier qu'implicit répondait
else:
result, error = ws.lire_quotient_familial(individu, now())
if error:
logger.warning(u'lire-quotients-valides identifier: %s erreur: %s', identifier, error)
return Response({
'err': 1,
'errors': [error],
})
return Response({
'err': 0,
'data': result,
})
qf_calculer = QFCalculer.as_view()
class QFEditerCarte(IndividuViewMixin, APIView):
def get(self, request, identifier, id_qf, format=None):
individu = self.get_individu(identifier)
ws = qf.QF()
result, error = ws.editer_carte(individu, int(id_qf))
if error:
logger.warning(u'editer-carte identifier: %s id-qf: %s erreur: %s', identifier, id_qf,
error)
return Response({
'err': 1,
'errors': [error],
}, headers={'x-error-code': '1'})
response = HttpResponse(result, content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="qf-%s-%s.pdf"' % (
id_qf, now().isoformat())
return response
qf_editer_carte = QFEditerCarte.as_view()
class DoublonMixin(object):
def get_queryset(self):
qs = models.Duplicate.objects.all()
qs = qs.prefetch_related(
'first__left_relations__schema', 'first__left_relations__right',
'first__right_relations__schema', 'first__right_relations__left',
'second__left_relations__schema', 'second__left_relations__right',
'second__right_relations__schema', 'second__right_relations__left',
)
return qs
def duplicate_to_response(self, duplicate):
utils.PersonSearch.decorate_individu(duplicate.first)
utils.PersonSearch.decorate_individu(duplicate.second)
return {
'id': duplicate.id,
'created': duplicate.created.isoformat(),
'state': duplicate.get_state_display(),
'state_id': duplicate.state,
'score': int(duplicate.score * 100),
'individu_1': individu_to_response(duplicate.first, add_text=True),
'individu_2': individu_to_response(duplicate.second, add_text=True),
'content': duplicate.content,
}
def get_duplicate(self, doublon_id):
try:
p = [int(x) for x in doublon_id.split()]
assert len(p) == 2
p = utils.pair_sort(*p)
return self.get_queryset().get(first_id=p[0], second_id=p[1])
except (AssertionError, ValueError, TypeError, models.Duplicate.DoesNotExist):
return None
class DoublonsView(DoublonMixin, APIView):
def get(self, request, format=None):
false_positive = 'false_positive' in request.GET
dedup = 'dedup' in request.GET
try:
limit = int(request.GET.get('limit', ''))
except ValueError:
limit = 10
try:
cookie = request.GET.get('cookie', '')
cookie = cookie.split('_', 1)
score = Decimal(cookie[0])
last_id = int(cookie[1])
except:
cookie = None
limit = max(10, min(limit, 100))
qs = self.get_queryset()
if false_positive:
qs = qs.filter(state=models.Duplicate.STATE_FALSE_POSITIVE)
elif dedup:
qs = qs.filter(state=models.Duplicate.STATE_DEDUP)
else:
qs = qs.filter(state=models.Duplicate.STATE_NEW)
if 'score_min' in request.GET:
try:
score_min = Decimal(request.GET['score_min'])
except InvalidOperation:
pass
else:
qs = qs.filter(score__gte=score_min/100)
if 'score_max' in request.GET:
try:
score_max = Decimal(request.GET['score_max'])
except InvalidOperation:
pass
else:
qs = qs.filter(score__lte=score_max/100)
# recherche ciblée
if 'id' in request.GET:
try:
individu_id = int(request.GET['id'])
except ValueError:
pass
else:
qs = qs.filter(Q(first_id=individu_id) | Q(second_id=individu_id))
if cookie:
qs = qs.filter(
Q(score__lt=score) | Q(score=score, id__gt=last_id)
)
qs = qs.prefetch_related(
'first__left_relations__schema', 'first__left_relations__right',
'first__right_relations__schema', 'first__right_relations__left',
'second__left_relations__schema', 'second__left_relations__right',
'second__right_relations__schema', 'second__right_relations__left',
)
data = [self.duplicate_to_response(d) for d in qs[:limit + 1]]
content = {
'err': 0,
'data': data[:limit],
}
params = {'limit': limit}
if 'score_min' in request.GET:
params['score_min'] = request.GET['score_min']
if 'score_max' in request.GET:
params['score_max'] = request.GET['score_max']
if false_positive:
params['false_positive'] = false_positive
if dedup:
params['dedup'] = dedup
content.update(params)
if len(data) > limit:
score = qs[limit - 1].score
max_id = qs[limit - 1].id
cookie = '%s_%s' % (score, max_id)
content['cookie'] = params['cookie'] = cookie
content['more'] = request.build_absolute_uri(reverse('rsu-api-doublons')) + '?' + urlencode(params)
return Response(content)
doublons = DoublonsView.as_view()
class DoublonView(DoublonMixin, APIView):
def get(self, request, doublon_id, format=None):
duplicate = self.get_duplicate(doublon_id)
if not duplicate:
return Response({
'err': 1
}, status=404)
return Response({
'err': 0,
'data': self.duplicate_to_response(duplicate),
})
doublon = DoublonView.as_view()
class DoublonActionView(DoublonMixin, TransactionalView):
def action(self, duplicate):
pass
def post(self, request, doublon_id, format=None):
duplicate = self.get_duplicate(doublon_id)
if not duplicate:
return Response({
'err': 1
}, status=404)
return self.action(request, duplicate)
class FalsePositiveView(DoublonActionView):
def action(self, request, duplicate):
serializer = JournalSerializerMixin(data=request.data)
try:
duplicate.false_positive()
utils.journalize(
duplicate.first,
transaction=self.transaction,
meta=serializer.journal_meta,
faux_positif=duplicate.second.id,
text=u'Déclaré comme non doublon de #%d' % duplicate.second.id)
utils.journalize(
duplicate.second,
transaction=self.transaction,
meta=serializer.journal_meta,
faux_positif=duplicate.first.id,
text=u'Déclaré comme non doublon de #%d' % duplicate.first.id)
return Response({
'err': 0,
})
except AssertionError as e:
return Response({
'err': 1,
'errors': str(e),
}, status=500)
false_positive = FalsePositiveView.as_view()
class DedupSerializer(JournalSerializerMixin):
choice = serializers.IntegerField(min_value=1, max_value=2)
class DedupView(DoublonActionView):
def action(self, request, duplicate):
serializer = DedupSerializer(data=request.data)
if not serializer.is_valid():
return Response({
'err': 1,
'errors': flatten_errors(serializer.errors),
}, status=400)
try:
keep, forget = duplicate.dedup(serializer.validated_data['choice'])
utils.journalize(
keep,
meta=serializer.journal_meta,
transaction=self.transaction,
forget=forget.id,
text=u'Dédoublonnage, en remplacement de %s #%s' % (
individu_to_text(forget), forget.id))
utils.journalize(
keep,
meta=serializer.journal_meta,
transaction=self.transaction,
keep=forget.id,
text=u'Dédoublonnage, remplacé par %s #%s' % (
individu_to_text(keep), keep.id))
return Response({
'err': 0,
})
except AssertionError as e:
return Response({
'err': 1,
'errors': str(e),
}, status=500)
dedup = DedupView.as_view()