Projet

Général

Profil

0001-commands-get-phone-attributes-usage-count-misc-stati.patch

Paul Marillonnet, 16 février 2023 15:50

Télécharger (8,49 ko)

Voir les différences:

Subject: [PATCH] commands: get phone attributes usage count & misc statistics
 (#73677)

 .../get-phone-attributes-usage-count.py       | 145 ++++++++++++++++++
 tests/test_commands.py                        |  45 +++++-
 2 files changed, 189 insertions(+), 1 deletion(-)
 create mode 100644 src/authentic2/management/commands/get-phone-attributes-usage-count.py
src/authentic2/management/commands/get-phone-attributes-usage-count.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2023 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 logging
18
from datetime import timedelta
19

  
20
import phonenumbers
21
from django.contrib.auth import get_user_model
22
from django.contrib.contenttypes.models import ContentType
23
from django.core.management.base import BaseCommand
24
from django.utils.timezone import now
25
from phonenumbers.phonenumberutil import PhoneNumberType, number_type
26

  
27
from authentic2.models import Attribute, AttributeValue
28

  
29
logger = logging.getLogger(__name__)
30

  
31
User = get_user_model()
32

  
33

  
34
class Command(BaseCommand):
35
    help = 'Gather phone attributes usage statistics'
36

  
37
    verbosity_to_log_level = {
38
        0: logging.CRITICAL,
39
        1: logging.WARNING,
40
        2: logging.INFO,
41
        3: logging.DEBUG,
42
    }
43

  
44
    def handle(self, **options):
45
        # add StreamHandler for console output
46
        handler = logging.StreamHandler()
47
        handler.setLevel(level=self.verbosity_to_log_level[options['verbosity']])
48
        logger.addHandler(handler)
49

  
50
        phone = None
51
        phone_count = 0
52

  
53
        mobile = None
54
        mobile_count = 0
55

  
56
        try:
57
            phone = Attribute.objects.get(name='phone', disabled=False)
58
        except Attribute.DoesNotExist:
59
            pass
60
        try:
61
            mobile = Attribute.objects.get(name='mobile', disabled=False)
62
        except Attribute.DoesNotExist:
63
            pass
64
        user_ct = ContentType.objects.get_for_model(User)
65

  
66
        login_threshold = now() - timedelta(days=365.25)
67
        users = User.objects.filter(
68
            is_active=True,
69
            last_login__gte=login_threshold,
70
            userexternalid__isnull=True,
71
            oidc_account__isnull=True,
72
        )
73
        user_ids = users.values_list('id', flat=True)
74
        user_count = users.count()
75
        if not user_count:
76
            return
77

  
78
        if phone:
79
            phone_count = users.filter(phone__isnull=False).count()
80
        if mobile:
81
            mobile_count = AttributeValue.objects.filter(
82
                content_type=user_ct,
83
                object_id__in=user_ids,
84
                attribute=mobile,
85
                content__isnull=False,
86
            ).count()
87

  
88
        phone_ratio = round(phone_count / user_count, 3)
89
        mobile_ratio = round(mobile_count / user_count, 3)
90

  
91
        logger.info('global count: %s', user_count)
92
        logger.info('phone: %s (%s)', phone_count, phone_ratio)
93
        logger.info('mobile: %s (%s)', mobile_count, mobile_ratio)
94

  
95
        with_phone_ids = users.filter(phone__isnull=False).values_list('id', flat=True)
96
        with_mobile_ids = AttributeValue.objects.filter(
97
            content_type=user_ct,
98
            object_id__in=user_ids,
99
            content__isnull=False,
100
            attribute=mobile,
101
        ).values_list('object_id', flat=True)
102

  
103
        both_fields_ids = set(with_phone_ids).intersection(set(with_mobile_ids))
104
        both_fields_count = len(both_fields_ids)
105
        both_fields_ratio = round(both_fields_count / user_count, 3)
106

  
107
        logger.info('both fields: %s (%s)', both_fields_count, both_fields_ratio)
108

  
109
        same_field_ids = []
110
        for identifier in both_fields_ids:
111
            try:
112
                user = users.get(id=identifier)
113
            except User.DoesNotExist:
114
                continue
115
            try:
116
                atv = AttributeValue.objects.get(content_type=user_ct, object_id=identifier, attribute=mobile)
117
            except AttributeValue.DoesNotExist:
118
                continue
119
            if user.phone and user.phone == atv.to_python():
120
                same_field_ids.append(identifier)
121
        same_field_count = len(same_field_ids)
122
        same_field_ratio = round(same_field_count / user_count, 3)
123

  
124
        logger.info('same value for both fields: %s (%s)', same_field_count, same_field_ratio)
125

  
126
        if not with_phone_ids:
127
            return
128

  
129
        mobile_as_phone_ids = []
130
        for user in users.filter(phone__isnull=False):
131
            try:
132
                pn = phonenumbers.parse(user.phone, 'FR')
133
            except phonenumbers.NumberParseException:
134
                continue
135
            if number_type(pn) == PhoneNumberType.MOBILE:
136
                mobile_as_phone_ids.append(user.id)
137

  
138
        mobile_as_phone_count = len(mobile_as_phone_ids)
139
        mobile_as_phone_ratio = round(mobile_as_phone_count / len(with_phone_ids), 3)
140
        logger.info(
141
            'phone field used for mobile: %s of %s (%s)',
142
            mobile_as_phone_count,
143
            len(with_phone_ids),
144
            mobile_as_phone_ratio,
145
        )
tests/test_commands.py
42 42
from authentic2.a2_rbac.utils import get_default_ou, get_operation
43 43
from authentic2.apps.journal.models import Event
44 44
from authentic2.custom_user.models import DeletedUser
45
from authentic2.models import UserExternalId
45
from authentic2.models import Attribute, AttributeValue, UserExternalId
46 46
from authentic2.utils import crypto
47 47
from authentic2_auth_oidc.models import OIDCAccount, OIDCClaimMapping, OIDCProvider
48 48

  
......
655 655
            assert User.objects.filter(first_name='Mod', last_name='Ified').count() in range(
656 656
                20 - deletion_number, 21
657 657
            )
658

  
659

  
660
def test_get_phone_attributes_usage_count(db, app, admin, settings, caplog, capsys):
661
    # recreated (soon-to-be-deprecated) separate phone/mobile number attributes
662
    admin.last_login = now()
663
    admin.save()
664
    mobile_attr = Attribute.objects.create(
665
        label='Mobile',
666
        name='mobile',
667
        kind='phone_number',
668
    )
669
    Attribute.objects.create(
670
        label='Phone',
671
        name='phone',
672
        kind='phone_number',
673
    )
674

  
675
    user_ct = ContentType.objects.get_for_model(User)
676
    for i in range(900):
677
        user = User.objects.create(
678
            username=f'user_{i}',
679
            first_name='User',
680
            last_name=f'{i}',
681
            email=f'user_{i}@example.com',
682
            phone=f'+33612345{i:03d}' if i % 2 else f'+33112345{i:03d}',
683
            last_login=now() if i % 9 in range(8) else now() - datetime.timedelta(days=700),
684
        )
685

  
686
        AttributeValue.objects.create(
687
            content_type=user_ct,
688
            object_id=user.id,
689
            attribute=mobile_attr,
690
            content=f'+33612345{i:03d}',
691
        )
692

  
693
    call_command('get-phone-attributes-usage-count', '-v1')
694
    assert caplog.records
695
    assert 'global count: 801' == caplog.records[0].message
696
    assert 'phone: 800 (0.999)' == caplog.records[1].message
697
    assert 'mobile: 800 (0.999)' == caplog.records[2].message
698
    assert 'both fields: 800 (0.999)' == caplog.records[3].message
699
    assert 'same value for both fields: 400 (0.499)' == caplog.records[4].message
700
    assert 'phone field used for mobile: 400 of 800 (0.5)' == caplog.records[5].message
658
-