Projet

Général

Profil

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

Benjamin Dauvergne, 16 novembre 2018 14:47

Télécharger (15,7 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 |  64 +++++++++
 zoo/zoo_nanterre/utils.py                     |  10 ++
 zoo/zoo_nanterre/views.py                     |  79 ++++++++++-
 9 files changed, 309 insertions(+), 3 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
  </ul>
25
  <div style="float: right">
26
      <p>{{ fiches|length }} fiches. Dernier calcul le {{ timestamp }}. {% if duration %} Durée {{ duration }} secondes.{% endif %}{% if queries %}{{ queries }} requêtes.{% endif %}</p>
27
      <h3>Explication du calcul</h3>
28
      <ul>
29
          <li>Les fiches enfants sans activité depuis {{ child_delay }} jours sont considérées inactives.</li>
30
          <li>Les fiches adultes sans activité depuis {{ adult_delay }} jours sont considérées inactives.</li>
31
          <li>Les fratries (tous les enfants partageant au moins un parent commun) inactives et sans fédérations sont à supprimer.</li>
32
          <li>Les adultes sans liens matrimoniaux inactifs, sans fédérations et dont tous les enfants sont à supprimer, sont à supprimer.</li>
33
          <li>Les couples inactifs, sans fédérations et dont tous les enfants sont à supprimer, sont à supprimer.</li>
34
      </ul>
35
  </div>
36
  <table id="result-list">
37
    {% comment %}Fiches inactives{% endcomment %}
38
    <thead>
39
      <tr>
40
        <th>Identifiant RSU</th>
41
        <th>Prénom</th>
42
        <th>Nom d'usage</th>
43
        <th>Nom de naissance</th>
44
        <th>Date de naissance</th>
45
        <th>Statut légal</th>
46
        <th>Âge</th>
47
      </tr>
48
     </thead>
49
     <tbody>
50
       {% for fiche in fiches %}
51
         <tr>
52
           <td>{{ fiche.id }}</td>
53
           <td>{{ fiche.prenoms }}</td>
54
           <td>{{ fiche.nom_d_usage }}</td>
55
           <td>{{ fiche.nom_de_naissance }}</td>
56
           <td>{{ fiche.date_de_naissance }}</td>
57
           <td>{{ fiche.statut_legal }}</td>
58
           <td>{{ fiche.age }}</td>
59
         </tr>
60
       {% endfor %}
61
     </tbody>
62
  </table>
63
</div>
64
{% endblock %}
zoo/zoo_nanterre/utils.py
141 141
    return adresses
142 142

  
143 143

  
144
def fratrie(individu):
145
    assert individu.content['statut_legal'] == 'mineur'
146

  
147
    def helper_fratrie():
148
        for parent, relp in parents(individu):
149
            for enfant, rele in enfants(parent):
150
                yield enfant
151
    return set(helper_fratrie())
152

  
153

  
144 154
def adresses_norel(individu):
145 155
    return [adresse for adresse, rel in adresses(individu)]
146 156

  
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
22
import io
20 23

  
21 24
from django.template.response import TemplateResponse
22 25
from django.views.generic import TemplateView
23 26
from django.shortcuts import redirect, get_object_or_404
24
from django.http import Http404, FileResponse
27
from django.http import Http404, FileResponse, HttpResponseRedirect, HttpResponse
25 28
from django.db.transaction import non_atomic_requests
26 29
from django.db import connection
27 30
from django.contrib.auth.decorators import permission_required
28

  
29
from zoo.zoo_data.models import Job
31
from django.conf import settings
32
from django.core.cache import cache
33
from django.utils.timezone import now
30 34

  
31 35
from . import forms
32 36
from .synchronize_federations import SynchronizeFederationsAction
37
from .inactivity import Inactivity
38
from . import utils
33 39

  
34 40

  
35 41
class Demo(TemplateView):
......
186 192
            raise Http404
187 193
        job.action.set_apply(job)
188 194
    return redirect('admin:synchronize-federations')
195

  
196

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

  
217

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

  
239
    if 'csv' in request.GET:
240
        with io.BytesIO() as f:
241
            writer = csv.writer(f)
242
            header = ['id', 'prenoms', 'nom_d_usage', 'nom_de_naissance',
243
                      'date_de_naissance', 'statut_legal', 'age']
244
            writer.writerow(header)
245
            for fiche in fiches:
246
                writer.writerow([unicode(fiche[key]).encode('utf-8') for key in header])
247
            r = HttpResponse(f.getvalue(), content_type='text/csv')
248
            r['Content-Disposition'] = 'attachment; filename="fiches-inactives-%s.csv"' % timestamp
249
            return r
250

  
251
    context = dict(
252
        model_admin.admin_site.each_context(request),
253
        title='Fiches inactives à supprimer',
254
        fiches=fiches,
255
        timestamp=timestamp,
256
        duration=duration,
257
        queries=queries,
258
        child_delay=getattr(settings, 'ZOO_NANTERRE_INACTIVITY_CHILD_DELAY', 365),
259
        adult_delay=getattr(settings, 'ZOO_NANTERRE_INACTIVITY_ADULT_DELAY', 365),
260
    )
261
    return TemplateResponse(request, "admin/zoo_data/entity/inactive_index.html", context)
189
-