Projet

Général

Profil

0001-nanterre-d-tecte-et-supprime-les-fiches-inactives-fi.patch

Benjamin Dauvergne, 17 novembre 2018 09:50

Télécharger (18,3 ko)

Voir les différences:

Subject: [PATCH] =?UTF-8?q?nanterre:=20d=C3=A9tecte=20et=20supprime=20les?=
 =?UTF-8?q?=20fiches=20inactives=20(fixes=20#28080)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

 debian/control                                |   1 +
 setup.py                                      |   1 +
 zoo/zoo_nanterre/apps.py                      |   7 +
 zoo/zoo_nanterre/inactivity.py                | 123 ++++++++++++++++++
 zoo/zoo_nanterre/models.py                    |  16 +++
 zoo/zoo_nanterre/templates/admin/index.html   |  11 ++
 .../admin/zoo_data/entity/inactive_index.html |  72 ++++++++++
 zoo/zoo_nanterre/utils.py                     |  35 ++++-
 zoo/zoo_nanterre/views.py                     |  89 ++++++++++++-
 9 files changed, 351 insertions(+), 4 deletions(-)
 create mode 100644 zoo/zoo_nanterre/inactivity.py
 create mode 100644 zoo/zoo_nanterre/templates/admin/zoo_data/entity/inactive_index.html
debian/control
17 17
    python-psycopg2,
18 18
    python-memcache,
19 19
    python-django-mellon,
20
    python-cached-property,
20 21
    uwsgi
21 22
Recommends: nginx, postgresql, memcached
22 23
Description: Maintain a graph of objects
setup.py
108 108
        'python-dateutil',
109 109
        'django-admin-rangefilter',
110 110
        'requests',
111
        'cached-property',
111 112
    ],
112 113
    zip_safe=False,
113 114
    cmdclass={
zoo/zoo_nanterre/apps.py
95 95
                kwargs={'model_admin': model_admin},
96 96
                name='synchronize-federations' + desc['name'],
97 97
            ))
98
        urls.append(url(
99
            r'^inactive/',
100
            model_admin.admin_site.admin_view(
101
                getattr(views, 'inactive_index')),
102
            kwargs={'model_admin': model_admin},
103
            name='inactive-index',
104
        ))
98 105
        return urls
99 106

  
100 107
    def post_migrate(self, *args, **kwargs):
zoo/zoo_nanterre/inactivity.py
1
# zoo - versatile objects management
2
# Copyright (C) 2016  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import datetime
18
import operator
19
try:
20
    from functools import reduce
21
except ImportError:
22
    pass
23

  
24
from django.db.models.query import Q
25
from django.utils.timezone import now
26

  
27
from cached_property import cached_property
28

  
29
from zoo.zoo_data.models import Entity
30

  
31
from . import utils
32

  
33

  
34
class Inactivity(object):
35
    def __init__(self, child_delay=365, adult_delay=365):
36
        self.child_delay = child_delay
37
        self.adult_delay = adult_delay
38

  
39
    @property
40
    def child_threshold(self):
41
        return now() - datetime.timedelta(days=self.child_delay)
42

  
43
    @property
44
    def adult_threshold(self):
45
        return now() - datetime.timedelta(days=self.adult_delay)
46

  
47
    def exclude_newer_than_threshold(self, qs, threshold):
48
        return qs.exclude(
49
            created__created__gte=threshold,
50
            modified__created__gte=threshold,
51
            log__timestamp__gte=threshold)
52

  
53
    @property
54
    def query_no_federation(self):
55
        queries = []
56

  
57
        for app_id in utils.get_applications(rsu_ws_url=True):
58
            query = Q(**{'content__cles_de_federation__%s__isnull' % app_id: True})
59
            query |= Q(**{'content__cles_de_federation__%s__exact' % app_id: ''})
60
            queries.append(query)
61
        return reduce(operator.__and__, queries)
62

  
63
    def filter_no_federation(self, qs):
64
        return qs.filter(self.query_no_federation)
65

  
66
    @property
67
    def entities(self):
68
        qs = Entity.objects.all()
69
        # prefetch to optimize accesors to spouses and siblings
70
        qs = qs.prefetch_related(
71
            'left_relations__schema', 'left_relations__right',
72
            'right_relations__schema', 'right_relations__left',
73
            'right_relations__left__left_relations__left',
74
            'right_relations__left__left_relations__schema',
75
        )
76
        return qs
77

  
78
    @property
79
    def children(self):
80
        return self.entities.filter(content__statut_legal='mineur')
81

  
82
    @property
83
    def adults(self):
84
        return self.entities.filter(content__statut_legal='majeur')
85

  
86
    @cached_property
87
    def deletable_children(self):
88
        potent = self.exclude_newer_than_threshold(self.filter_no_federation(self.children), self.child_threshold)
89
        potent_ids = potent.values_list('id', flat=True)
90

  
91
        def filter_siblings_are_potent():
92
            for child in potent:
93
                for sibling in utils.fratrie(child):
94
                    if sibling.id not in potent_ids:
95
                        break
96
                else:
97
                    yield child.id
98
        potent2_ids = list(filter_siblings_are_potent())
99

  
100
        return potent.filter(id__in=potent2_ids)
101

  
102
    @cached_property
103
    def deletable_adults(self):
104
        deletable_children_ids = self.deletable_children.values_list('id', flat=True)
105
        potent = self.exclude_newer_than_threshold(self.filter_no_federation(self.adults), self.adult_threshold)
106

  
107
        def filter_children_are_deletable():
108
            for adult in potent:
109
                for enfant, rele in utils.enfants(adult):
110
                    if enfant.id not in deletable_children_ids:
111
                        break
112
                else:
113
                    yield adult
114
        potent2 = list(filter_children_are_deletable())
115
        potent2_ids = [adult.id for adult in potent2]
116

  
117
        def filter_spouse_is_deletable():
118
            for adult in potent2:
119
                conjoint = utils.conjoint(adult)[0]
120
                if conjoint and conjoint.id not in potent2_ids:
121
                    continue
122
                yield adult.id
123
        return potent.filter(id__in=filter_spouse_is_deletable())
zoo/zoo_nanterre/models.py
1
# zoo - versatile objects management
2
# Copyright (C) 2016  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
1 17
from django.utils.translation import ugettext_lazy as _
2 18
from django.contrib.postgres.fields import JSONField
3 19
from django.db import models
zoo/zoo_nanterre/templates/admin/index.html
61 61
          <td>&nbsp;</td>
62 62
        </tr>
63 63
      {% endif %}
64
      {% if perms.zoo_data.action2_entity %}
65
        <tr>
66
          <th scope="row">
67
            <a href="{% url "admin:inactive-index" %}">
68
              Fiches inactives
69
            </a>
70
          </th>
71
          <td>&nbsp;</td>
72
          <td>&nbsp;</td>
73
        </tr>
74
      {% endif %}
64 75
    </table>
65 76
  </div>
66 77
</div>
zoo/zoo_nanterre/templates/admin/zoo_data/entity/inactive_index.html
1
{% extends "admin/base_site.html" %}
2
{% load i18n admin_urls static %}
3

  
4
{% block extrastyle %}
5
  {{ block.super }}
6
  <link rel="stylesheet" type="text/css" href="{% static "admin/css/changelists.css" %}" />
7
{% endblock %}
8

  
9
{% block coltype %}flex{% endblock %}
10

  
11

  
12
{% block breadcrumbs %}
13
<div class="breadcrumbs">
14
  <a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
15
  &rsaquo; Fiches inactives
16
</div>
17
{% endblock %}
18

  
19
{% block content %}
20
<div id="content-main">
21
  <ul class="object-tools">
22
    <li><a href="?recompute">Recalculer</a></li>
23
    <li><a href="?csv">Export CSV</a></li>
24
    <li><a href="#" onclick="document.getElementById('delete-button').click()">Supprimer</a></li>
25
    <form method="post" action="?delete" style="display: none">{% csrf_token %}<button id="delete-button">Supprimer</button></form>
26
  </ul>
27
  <div style="float: right">
28
      <p>{{ fiches|length }} fiches. Dernier calcul le {{ timestamp }}. {% if duration %} Durée {{ duration }} secondes.{% endif %}{% if queries %}{{ queries }} requêtes.{% endif %}</p>
29
      <h3>Explication du calcul</h3>
30
      <ul>
31
          <li>Une fiche sans activité est une fiche dont la dernière date de modification et les dernières lignes du journal n'ont pas bougé depuis un certain nombre de jours</li>
32
          <li>Les fiches enfants sans activité depuis {{ child_delay }} jours sont considérées inactives.</li>
33
          <li>Les fiches adultes sans activité depuis {{ adult_delay }} jours sont considérées inactives.</li>
34
          <li>Les fratries (tous les enfants partageant au moins un parent commun) inactives et sans fédérations sont à supprimer.</li>
35
          <li>Les adultes sans liens matrimoniaux, inactifs, sans fédérations et dont tous les enfants sont à supprimer sont à supprimer.</li>
36
          <li>Les couples inactifs, sans fédérations et dont tous les enfants sont à supprimer sont à supprimer.</li>
37
      </ul>
38
      <h3>Configuration</h3>
39
      <ul>
40
          <li>Le délai d'inactivité en jours pour les fiches enfants est configuré par le setting <tt>ZOO_NANTERRE_INACTIVITY_CHILD_DELAY</tt></li>
41
          <li>Le délai d'inactivité en jours pour les fiches adultes est configuré par le setting <tt>ZOO_NANTERRE_INACTIVITY_ADULT_DELAY</tt></li>
42
      </ul>
43
  </div>
44
  <table id="result-list">
45
    {% comment %}Fiches inactives{% endcomment %}
46
    <thead>
47
      <tr>
48
        <th>Identifiant RSU</th>
49
        <th>Prénom</th>
50
        <th>Nom d'usage</th>
51
        <th>Nom de naissance</th>
52
        <th>Date de naissance</th>
53
        <th>Statut légal</th>
54
        <th>Âge</th>
55
      </tr>
56
     </thead>
57
     <tbody>
58
       {% for fiche in fiches %}
59
         <tr>
60
           <td>{{ fiche.id }}</td>
61
           <td>{{ fiche.prenoms }}</td>
62
           <td>{{ fiche.nom_d_usage }}</td>
63
           <td>{{ fiche.nom_de_naissance }}</td>
64
           <td>{{ fiche.date_de_naissance }}</td>
65
           <td>{{ fiche.statut_legal }}</td>
66
           <td>{{ fiche.age }}</td>
67
         </tr>
68
       {% endfor %}
69
     </tbody>
70
  </table>
71
</div>
72
{% endblock %}
zoo/zoo_nanterre/utils.py
20 20
import six
21 21
import functools
22 22
import uuid
23
import io
24
import csv
23 25

  
24 26
import sys
25 27
import re
......
39 41
from django.db.models import Q, F, Value, ExpressionWrapper, CharField, When, Case
40 42
from django.db.models.functions import Least, Greatest, Coalesce, Concat
41 43
from django.db import transaction
42
from django.utils.timezone import now, make_aware
43 44
from django.contrib.auth.hashers import make_password
45
from django.http import HttpResponse
46

  
47
from django.utils.timezone import now, make_aware
48
from django.utils.encoding import force_bytes
44 49

  
45 50
from zoo.zoo_meta.models import EntitySchema, RelationSchema
46 51
from zoo.zoo_data.models import Entity, Relation, Transaction, Log
......
141 146
    return adresses
142 147

  
143 148

  
149
def fratrie(individu):
150
    assert individu.content['statut_legal'] == 'mineur'
151

  
152
    def helper_fratrie():
153
        for parent, relp in parents(individu):
154
            for enfant, rele in enfants(parent):
155
                yield enfant
156
    return set(helper_fratrie())
157

  
158

  
144 159
def adresses_norel(individu):
145 160
    return [adresse for adresse, rel in adresses(individu)]
146 161

  
......
1249 1264
    if c['date_de_naissance']:
1250 1265
        s += u' - ' + c['date_de_naissance']
1251 1266
    return s
1267

  
1268

  
1269
def csv_export_response(rows, filename):
1270
    if sys.version >= (3,):
1271
        with io.StringIO(newline='') as f:
1272
            writer = csv.writer(f)
1273
            for row in rows:
1274
                writer.writerow(map(str, row))
1275
            r = HttpResponse(f.getvalue(), content_type='text/csv')
1276
    else:
1277
        with io.BytesIO() as f:
1278
            writer = csv.writer(f)
1279
            for row in rows:
1280
                writer.writerow(map(force_bytes, row))
1281
            r = HttpResponse(f.getvalue(), content_type='text/csv')
1282
    r['Content-Disposition'] = 'attachment; filename="%s"' % filename
1283
    return r
1284

  
zoo/zoo_nanterre/views.py
17 17
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 18

  
19 19
import csv
20
import itertools
21
import time
20 22

  
21 23
from django.template.response import TemplateResponse
22 24
from django.views.generic import TemplateView
23 25
from django.shortcuts import redirect, get_object_or_404
24
from django.http import Http404, FileResponse
25
from django.db.transaction import non_atomic_requests
26
from django.http import Http404, FileResponse, HttpResponseRedirect
27
from django.db.transaction import non_atomic_requests, atomic
26 28
from django.db import connection
29
from django.conf import settings
30
from django.core.cache import cache
31
from django.utils.timezone import now
32

  
27 33
from django.contrib.auth.decorators import permission_required
34
from django.contrib import messages
28 35

  
29
from zoo.zoo_data.models import Job
36
from zoo.zoo_data.models import Entity
30 37

  
31 38
from . import forms
32 39
from .synchronize_federations import SynchronizeFederationsAction
40
from .inactivity import Inactivity
41
from . import utils
33 42

  
34 43

  
35 44
class Demo(TemplateView):
......
186 195
            raise Http404
187 196
        job.action.set_apply(job)
188 197
    return redirect('admin:synchronize-federations')
198

  
199

  
200
def fiches_inactives():
201
    inactivity = Inactivity(
202
        child_delay=getattr(settings, 'ZOO_NANTERRE_INACTIVITY_CHILD_DELAY', 365),
203
        adult_delay=getattr(settings, 'ZOO_NANTERRE_INACTIVITY_ADULT_DELAY', 365),
204
    )
205
    fiches = []
206
    for child in itertools.chain(inactivity.deletable_children, inactivity.deletable_adults):
207
        utils.PersonSearch.add_age(child)
208
        fiches.append({
209
            'id': child.id,
210
            'prenoms': child.content['prenoms'],
211
            'nom_de_naissance': child.content['nom_de_naissance'],
212
            'nom_d_usage': child.content['nom_d_usage'],
213
            'date_de_naissance': child.content['date_de_naissance'],
214
            'statut_legal': child.content['statut_legal'],
215
            'age': child.age_label,
216
        })
217
    fiches.sort(key=lambda f: f['id'])
218
    return fiches
219

  
220

  
221
@permission_required('zoo_data.action1_entity')
222
@atomic
223
def inactive_index(request, model_admin, *args, **kwargs):
224
    try:
225
        timestamp, fiches = cache.get('fiches-inactives')
226
        if 'recompute' in request.GET:
227
            cache.delete('fiches-inactives')
228
            return HttpResponseRedirect(request.path)
229
        duration = None
230
        queries = None
231
    except (TypeError, ValueError):
232
        try:
233
            connection.force_debug_cursor = True
234
            start = time.time()
235
            fiches = fiches_inactives()
236
            queries = len(connection.queries_log)
237
            duration = time.time() - start
238
            timestamp = now()
239
            cache.set('fiches-inactives', (timestamp, fiches), 365 * 24 * 3600)
240
        finally:
241
            connection.force_debug_cursor = False
242

  
243
    # delete operation
244
    if 'delete' in request.GET and request.method == 'POST':
245
        Entity.objects.filter(id__in=[fiche['id'] for fiche in fiches]).delete()
246
        messages.info(request, '%d fiches ont été supprimées.' % len(fiches))
247
        cache.delete('fiches-inactives')
248
        return HttpResponseRedirect(request.path)
249

  
250
    # download csv export
251
    if 'csv' in request.GET:
252
        header = ['id', 'prenoms', 'nom_d_usage', 'nom_de_naissance',
253
                  'date_de_naissance', 'statut_legal', 'age']
254

  
255
        def rows():
256
            yield header
257
            for fiche in fiches:
258
                yield [fiche[key] for key in header]
259
        return utils.csv_export_response(rows(), 'fiches-inactives-%s.csv' % timestamp)
260

  
261
    context = dict(
262
        model_admin.admin_site.each_context(request),
263
        title='Fiches inactives à supprimer',
264
        fiches=fiches,
265
        timestamp=timestamp,
266
        duration=duration,
267
        queries=queries,
268
        child_delay=getattr(settings, 'ZOO_NANTERRE_INACTIVITY_CHILD_DELAY', 365),
269
        adult_delay=getattr(settings, 'ZOO_NANTERRE_INACTIVITY_ADULT_DELAY', 365),
270
    )
271
    return TemplateResponse(request, "admin/zoo_data/entity/inactive_index.html", context)
189
-