2103 lines
77 KiB
Python
2103 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.encoding import force_text
|
|
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': force_text(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': force_text(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': force_text(e),
|
|
}, status=500)
|
|
|
|
dedup = DedupView.as_view()
|