Projet

Général

Profil

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

Paul Marillonnet, 16 février 2023 12:47

Télécharger (8,78 ko)

Voir les différences:

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

 .../get-phone-attributes-usage-count.py       | 148 ++++++++++++++++++
 tests/test_commands.py                        |  41 ++++-
 2 files changed, 188 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

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

  
25
from authentic2.models import Attribute, AttributeValue
26

  
27
logger = logging.getLogger(__name__)
28

  
29
User = get_user_model()
30

  
31

  
32
class Command(BaseCommand):
33
    help = 'Gather phone attributes usage statistics'
34

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

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

  
48
        phone = None
49
        phone_count = 0
50

  
51
        mobile = None
52
        mobile_count = 0
53

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

  
65
        if user_count:
66
            if phone:
67
                phone_count = AttributeValue.objects.filter(
68
                    content_type=user_ct,
69
                    object_id__isnull=False,
70
                    attribute=phone,
71
                    content__isnull=False,
72
                ).count()
73
            if mobile:
74
                mobile_count = AttributeValue.objects.filter(
75
                    content_type=user_ct,
76
                    object_id__isnull=False,
77
                    attribute=mobile,
78
                    content__isnull=False,
79
                ).count()
80

  
81
            phone_ratio = round(phone_count / user_count, 3)
82
            mobile_ratio = round(mobile_count / user_count, 3)
83

  
84
            logger.info('global count: %s', user_count)
85
            logger.info('phone: %s (%s)', phone_count, phone_ratio)
86
            logger.info('mobile: %s (%s)', mobile_count, mobile_ratio)
87

  
88
        # take a random sample of 500 Users
89
        sample = User.objects.order_by('?')[:500]
90
        sample_ids = sample.values_list('id', flat=True)
91
        sample_count = sample.count()
92
        if sample_count:
93
            with_phone_ids = User.objects.filter(id__in=sample_ids, phone__isnull=False).values_list(
94
                'id', flat=True
95
            )
96
            with_mobile_ids = AttributeValue.objects.filter(
97
                content_type=user_ct,
98
                object_id__in=[u.id for u in sample],
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 / sample_count, 3)
106

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

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

  
126
            logger.info(
127
                'same value for both fields: %s of %s (%s)', same_field_count, sample_count, same_field_ratio
128
            )
129

  
130
            mobile_as_phone_ids = []
131
            for user in sample:
132
                if not user.phone:
133
                    continue
134
                try:
135
                    pn = phonenumbers.parse(user.phone, 'FR')
136
                except phonenumbers.NumberParseException:
137
                    continue
138
                if number_type(pn) == PhoneNumberType.MOBILE:
139
                    mobile_as_phone_ids.append(user.id)
140

  
141
            mobile_as_phone_count = len(mobile_as_phone_ids)
142
            mobile_as_phone_ratio = round(mobile_as_phone_count / sample_count, 3)
143
            logger.info(
144
                'phone field used for mobile: %s of %s (%s)',
145
                mobile_as_phone_count,
146
                sample_count,
147
                mobile_as_phone_ratio,
148
            )
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
    mobile_attr = Attribute.objects.create(
663
        label='Mobile',
664
        name='mobile',
665
        kind='phone_number',
666
    )
667
    Attribute.objects.create(
668
        label='Phone',
669
        name='phone',
670
        kind='phone_number',
671
    )
672

  
673
    user_ct = ContentType.objects.get_for_model(User)
674
    for i in range(300):
675
        user = User.objects.create(
676
            username=f'user_{i}',
677
            first_name='User',
678
            last_name=f'{i}',
679
            email=f'user_{i}@example.com',
680
            phone=f'+33612345{i:03d}' if i % 2 else f'+33112345{i:03d}',
681
        )
682
        AttributeValue.objects.create(
683
            content_type=user_ct,
684
            object_id=user.id,
685
            attribute=mobile_attr,
686
            content=f'+33612345{i:03d}',
687
        )
688

  
689
    call_command('get-phone-attributes-usage-count', '-v1')
690
    assert caplog.records
691
    assert 'global count: 301' == caplog.records[0].message
692
    assert 'phone: 300 (0.997)' == caplog.records[1].message
693
    assert 'mobile: 300 (0.997)' == caplog.records[2].message
694
    assert 'both fields: 300 of 301 (0.997)' == caplog.records[3].message
695
    assert 'same value for both fields: 150 of 301 (0.498)' == caplog.records[4].message
696
    assert 'phone field used for mobile: 150 of 301 (0.498)' == caplog.records[5].message
658
-