Projet

Général

Profil

0005-utils-add-NaturalKeyRelatedField-class-62013.patch

Benjamin Dauvergne, 13 mai 2022 17:27

Télécharger (8,86 ko)

Voir les différences:

Subject: [PATCH 5/9] utils: add NaturalKeyRelatedField class (#62013)

 src/authentic2/utils/api.py |  79 +++++++++++++++++++++
 tests/test_utils_api.py     | 134 ++++++++++++++++++++++++++++++++++++
 2 files changed, 213 insertions(+)
 create mode 100644 src/authentic2/utils/api.py
 create mode 100644 tests/test_utils_api.py
src/authentic2/utils/api.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2022 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

  
18
from django.db import models
19
from rest_framework import exceptions, serializers
20

  
21

  
22
class NaturalKeyRelatedField(serializers.RelatedField):
23
    def to_representation(self, value):
24
        if value is None:
25
            return None
26
        return self._instance_to_natural_key(value)
27

  
28
    def to_internal_value(self, data):
29
        if data is None:
30
            return None
31
        return self._natural_key_to_instance(self.get_queryset(), data)
32

  
33
    def _instance_to_natural_key(self, instance):
34
        model = type(instance)
35
        fields = set()
36
        for natural_key_description in model._meta.natural_key:
37
            for name in natural_key_description:
38
                name = name.split('__')[0]
39
                fields.add(name)
40
        raw = {name: getattr(instance, name) for name in fields}
41
        return {
42
            name: self._instance_to_natural_key(value) if isinstance(value, models.Model) else value
43
            for name, value in raw.items()
44
        }
45

  
46
    def _natural_key_to_instance(self, queryset, data):
47
        if data is None:
48
            return data
49

  
50
        model = queryset.model
51
        natural_keys = {}
52
        for name, value in data.items():
53
            field = model._meta.get_field(name)
54
            if field.related_model:
55
                qs = field.related_model._base_manager
56
                natural_keys[name] = self._natural_key_to_instance(qs, value)
57
            else:
58
                natural_keys[name] = value
59
        for natural_key_description in model._meta.natural_key:
60
            lookups = {}
61
            for name in natural_key_description:
62
                real_name = name.split('__')[0]
63
                if real_name not in natural_keys:
64
                    break
65
                value = natural_keys[real_name]
66
                if name.endswith('__isnull'):
67
                    if value is not None:
68
                        break
69
                    lookups[name] = True
70
                else:
71
                    lookups[name] = value
72
            else:
73
                try:
74
                    return queryset.get(**lookups)
75
                except model.DoesNotExist:
76
                    pass
77
                except model.MultipleObjectsReturned:
78
                    raise exceptions.ValidationError('multiple objects returned')
79
        raise exceptions.ValidationError('object not found')
tests/test_utils_api.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2022 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 pytest
18
from rest_framework.exceptions import ValidationError
19

  
20
from authentic2.a2_rbac.models import OrganizationalUnit as OU
21
from authentic2.a2_rbac.models import Role
22
from authentic2.models import Service
23
from authentic2.utils.api import NaturalKeyRelatedField
24
from tests.utils import scoped_db_fixture
25

  
26

  
27
class TestNaturalKeyRelatedField:
28
    @scoped_db_fixture(scope='class', autouse=True)
29
    def fixture(self):
30
        class Namespace:
31
            ou = OU.objects.create(name='ou', uuid='1' * 32)
32
            service = Service.objects.create(name='service', slug='service', ou=ou)
33
            role = Role.objects.create(name='role', ou=ou, uuid='2' * 32)
34
            ou2 = OU.objects.create(name='ou2', uuid='3' * 32)
35

  
36
        yield Namespace
37

  
38
    def test_to_representation(self, db, fixture):
39
        assert NaturalKeyRelatedField(read_only=True).to_representation(fixture.role) == {
40
            'name': 'role',
41
            'ou': {'name': 'ou', 'slug': 'ou', 'uuid': '1' * 32},
42
            'service': None,
43
            'slug': 'role',
44
            'uuid': '2' * 32,
45
        }
46

  
47
    def test_to_representation_service(self, db, fixture):
48
        fixture.role.service = fixture.service
49
        fixture.role.save()
50
        assert NaturalKeyRelatedField(read_only=True).to_representation(fixture.role) == {
51
            'name': 'role',
52
            'ou': {'name': 'ou', 'slug': 'ou', 'uuid': '1' * 32},
53
            'service': {
54
                'slug': 'service',
55
                'ou': {'name': 'ou', 'slug': 'ou', 'uuid': '1' * 32},
56
            },
57
            'slug': 'role',
58
            'uuid': '2' * 32,
59
        }
60

  
61
    @pytest.mark.parametrize(
62
        'value',
63
        [
64
            {'uuid': '2' * 32},
65
            {'name': 'role'},
66
            {'slug': 'role'},
67
            {'name': 'role', 'ou': {'name': 'ou'}},
68
            {'slug': 'role', 'ou': {'slug': 'ou'}},
69
            {'slug': 'role', 'ou': {'uuid': '1' * 32}},
70
        ],
71
        ids=[
72
            'by uuid',
73
            'by name',
74
            'by slug',
75
            'by name and ou by name',
76
            'by name and ou by slug',
77
            'by name and ou by uuid',
78
        ],
79
    )
80
    def test_to_internal_value_role(self, value, db, fixture):
81
        assert NaturalKeyRelatedField(queryset=Role.objects.all()).to_internal_value(value) == fixture.role
82

  
83
    @pytest.mark.parametrize(
84
        'value',
85
        [
86
            {'name': 'role'},
87
            {'slug': 'role'},
88
        ],
89
        ids=['by name', 'by slug'],
90
    )
91
    def test_to_internal_value_role_ambiguous(self, value, db, fixture):
92
        Role.objects.create(slug='role', name='role', ou=fixture.ou2)
93
        with pytest.raises(ValidationError, match='multiple'):
94
            assert (
95
                NaturalKeyRelatedField(queryset=Role.objects.all()).to_internal_value(value) == fixture.role
96
            )
97

  
98
    @pytest.mark.parametrize(
99
        'value',
100
        [
101
            {'name': 'role', 'ou': {'slug': 'ou'}},
102
            {'slug': 'role', 'ou': {'slug': 'ou'}},
103
        ],
104
        ids=['by name and ou', 'by slug and ou'],
105
    )
106
    def test_to_internal_value_role_unique(self, value, db, fixture):
107
        Role.objects.create(slug='role', name='role', ou=fixture.ou2)
108
        assert NaturalKeyRelatedField(queryset=Role.objects.all()).to_internal_value(value) == fixture.role
109

  
110
    @pytest.mark.parametrize(
111
        'value',
112
        [
113
            {'uuid': '2' * 32},
114
            {'name': 'role'},
115
            {'slug': 'role'},
116
            {'name': 'role', 'ou': {'name': 'ou'}},
117
            {'slug': 'role', 'ou': {'slug': 'ou'}},
118
            {'slug': 'role', 'ou': {'uuid': '1' * 32}},
119
        ],
120
        ids=[
121
            'by uuid',
122
            'by name',
123
            'by slug',
124
            'by name and ou by name',
125
            'by name and ou by slug',
126
            'by name and ou by uuid',
127
        ],
128
    )
129
    def test_to_internal_value_role_not_found(self, value, db, fixture):
130
        Role.objects.all().delete()
131
        with pytest.raises(ValidationError, match='not found'):
132
            assert (
133
                NaturalKeyRelatedField(queryset=Role.objects.all()).to_internal_value(value) == fixture.role
134
            )
0
-