0001-api-accept-get-update_or_create-parameter-to-user-an.patch
src/authentic2/api_mixins.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2010-2018 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 |
from django.db import transaction |
|
18 | ||
19 |
from rest_framework.serializers import raise_errors_on_nested_writes |
|
20 |
from rest_framework.utils import model_meta |
|
21 | ||
22 | ||
23 |
class GetOrCreateModelSerializer(object): |
|
24 |
def get_or_create(self, keys, validated_data): |
|
25 |
raise_errors_on_nested_writes('get_or_create', self, validated_data) |
|
26 | ||
27 |
ModelClass = self.Meta.model |
|
28 | ||
29 |
# Remove many-to-many relationships from validated_data. |
|
30 |
# They are not valid arguments to the default `.create()` method, |
|
31 |
# as they require that the instance has already been saved. |
|
32 |
info = model_meta.get_field_info(ModelClass) |
|
33 |
many_to_many = {} |
|
34 |
for field_name, relation_info in info.relations.items(): |
|
35 |
if relation_info.to_many and (field_name in validated_data): |
|
36 |
many_to_many[field_name] = validated_data.pop(field_name) |
|
37 | ||
38 |
kwargs = {} |
|
39 |
defaults = kwargs['defaults'] = {} |
|
40 |
missing_keys = set(keys) - set(validated_data) |
|
41 |
if missing_keys: |
|
42 |
raise TypeError('Keys %s are missing' % missing_keys) |
|
43 |
for key, value in validated_data.items(): |
|
44 |
if key in keys: |
|
45 |
kwargs[key] = value |
|
46 |
else: |
|
47 |
defaults[key] = value |
|
48 |
with transaction.atomic(): |
|
49 |
instance, created = self.Meta.model._default_manager.get_or_create(**kwargs) |
|
50 |
if many_to_many and created: |
|
51 |
self.update(instance, many_to_many) |
|
52 |
return instance |
|
53 | ||
54 |
def update_or_create(self, keys, validated_data): |
|
55 |
raise_errors_on_nested_writes('update_or_create', self, validated_data) |
|
56 | ||
57 |
ModelClass = self.Meta.model |
|
58 | ||
59 |
# Remove many-to-many relationships from validated_data. |
|
60 |
# They are not valid arguments to the default `.create()` method, |
|
61 |
# as they require that the instance has already been saved. |
|
62 |
info = model_meta.get_field_info(ModelClass) |
|
63 |
many_to_many = {} |
|
64 |
get_or_create_data = validated_data.copy() |
|
65 |
for field_name, relation_info in info.relations.items(): |
|
66 |
if relation_info.to_many and (field_name in validated_data): |
|
67 |
many_to_many[field_name] = get_or_create_data.pop(field_name) |
|
68 | ||
69 |
kwargs = {} |
|
70 |
defaults = kwargs['defaults'] = {} |
|
71 |
missing_keys = set(keys) - set(get_or_create_data) |
|
72 |
if missing_keys: |
|
73 |
raise TypeError('Keys %s are missing' % missing_keys) |
|
74 |
for key, value in get_or_create_data.items(): |
|
75 |
if key in keys: |
|
76 |
kwargs[key] = value |
|
77 |
else: |
|
78 |
defaults[key] = value |
|
79 |
with transaction.atomic(): |
|
80 |
instance, created = self.Meta.model._default_manager.get_or_create(**kwargs) |
|
81 |
if many_to_many or not created: |
|
82 |
self.update(instance, validated_data) |
|
83 |
return instance |
|
84 | ||
85 |
def create(self, validated_data): |
|
86 |
try: |
|
87 |
keys = self.context['view'].request.GET.getlist('get_or_create') |
|
88 |
except Exception: |
|
89 |
pass |
|
90 |
else: |
|
91 |
if keys: |
|
92 |
return self.get_or_create(keys, validated_data) |
|
93 |
try: |
|
94 |
keys = self.context['view'].request.GET.getlist('update_or_create') |
|
95 |
except Exception: |
|
96 |
pass |
|
97 |
else: |
|
98 |
if keys: |
|
99 |
return self.update_or_create(keys, validated_data) |
|
100 |
return super(GetOrCreateModelSerializer, self).create(validated_data) |
src/authentic2/api_views.py | ||
---|---|---|
46 | 46 | |
47 | 47 |
from .passwords import get_password_checker |
48 | 48 |
from .custom_user.models import User |
49 |
from . import utils, decorators, attribute_kinds, app_settings, hooks |
|
49 |
from . import (utils, decorators, attribute_kinds, app_settings, hooks, |
|
50 |
api_mixins) |
|
50 | 51 |
from .models import Attribute, PasswordReset, Service |
51 | 52 |
from .a2_rbac.utils import get_default_ou |
52 | 53 | |
... | ... | |
321 | 322 |
return request.user.to_json() |
322 | 323 | |
323 | 324 | |
324 |
class BaseUserSerializer(serializers.ModelSerializer): |
|
325 |
class BaseUserSerializer(api_mixins.GetOrCreateModelSerializer, |
|
326 |
serializers.ModelSerializer): |
|
325 | 327 |
ou = serializers.SlugRelatedField( |
326 | 328 |
queryset=get_ou_model().objects.all(), |
327 | 329 |
slug_field='slug', |
... | ... | |
490 | 492 |
exclude = ('date_joined', 'user_permissions', 'groups', 'last_login') |
491 | 493 | |
492 | 494 | |
493 |
class RoleSerializer(serializers.ModelSerializer): |
|
495 |
class RoleSerializer(api_mixins.GetOrCreateModelSerializer, serializers.ModelSerializer):
|
|
494 | 496 |
ou = serializers.SlugRelatedField( |
495 | 497 |
many=False, |
496 | 498 |
required=False, |
tests/test_api.py | ||
---|---|---|
22 | 22 |
import uuid |
23 | 23 | |
24 | 24 | |
25 |
from django.core.urlresolvers import reverse
|
|
25 |
from django.contrib.auth.hashers import check_password
|
|
26 | 26 |
from django.contrib.auth import get_user_model |
27 | 27 |
from django.contrib.contenttypes.models import ContentType |
28 |
from authentic2.a2_rbac.utils import get_default_ou |
|
29 |
from django_rbac.utils import get_role_model, get_ou_model |
|
30 |
from django_rbac.models import SEARCH_OP |
|
31 |
from authentic2.models import Service |
|
32 | 28 |
from django.core import mail |
33 |
from django.contrib.auth.hashers import check_password
|
|
29 |
from django.core.urlresolvers import reverse
|
|
34 | 30 | |
35 |
from authentic2_idp_oidc.models import OIDCClient |
|
31 |
from django_rbac.models import SEARCH_OP |
|
32 |
from django_rbac.utils import get_role_model, get_ou_model |
|
33 | ||
34 |
from authentic2.a2_rbac.models import Role |
|
35 |
from authentic2.a2_rbac.utils import get_default_ou |
|
36 |
from authentic2.models import Service |
|
36 | 37 | |
37 | 38 |
from utils import login, basic_authorization_header, get_link_from_mail |
38 | 39 | |
39 | 40 |
pytestmark = pytest.mark.django_db |
40 | 41 | |
42 |
User = get_user_model() |
|
43 | ||
41 | 44 | |
42 | 45 |
def test_api_user_simple(logged_app): |
43 | 46 |
resp = logged_app.get('/api/user/') |
... | ... | |
1146 | 1149 |
assert response.json['checks'][3]['result'] is True |
1147 | 1150 |
assert response.json['checks'][4]['label'] == 'must contain "ok"' |
1148 | 1151 |
assert response.json['checks'][4]['result'] is True |
1152 | ||
1153 | ||
1154 |
def test_api_users_get_or_create(settings, app, admin): |
|
1155 |
app.authorization = ('Basic', (admin.username, admin.username)) |
|
1156 |
# test missing first_name |
|
1157 |
payload = { |
|
1158 |
'email': 'john.doe@example.net', |
|
1159 |
'first_name': 'John', |
|
1160 |
'last_name': 'Doe', |
|
1161 |
} |
|
1162 |
resp = app.post_json('/api/users/?get_or_create=email', params=payload, status=201) |
|
1163 |
id = resp.json['id'] |
|
1164 |
assert User.objects.get(id=id).first_name == 'John' |
|
1165 |
assert User.objects.get(id=id).last_name == 'Doe' |
|
1166 | ||
1167 |
resp = app.post_json('/api/users/?get_or_create=email', params=payload, status=201) |
|
1168 |
assert id == resp.json['id'] |
|
1169 |
assert User.objects.get(id=id).first_name == 'John' |
|
1170 |
assert User.objects.get(id=id).last_name == 'Doe' |
|
1171 | ||
1172 |
payload['first_name'] = 'Jane' |
|
1173 |
resp = app.post_json('/api/users/?update_or_create=email', params=payload, status=201) |
|
1174 |
assert id == resp.json['id'] |
|
1175 |
assert User.objects.get(id=id).first_name == 'Jane' |
|
1176 |
assert User.objects.get(id=id).last_name == 'Doe' |
|
1177 | ||
1178 | ||
1179 |
def test_api_users_get_or_create_multi_key(settings, app, admin): |
|
1180 |
app.authorization = ('Basic', (admin.username, admin.username)) |
|
1181 |
# test missing first_name |
|
1182 |
payload = { |
|
1183 |
'email': 'john.doe@example.net', |
|
1184 |
'first_name': 'John', |
|
1185 |
'last_name': 'Doe', |
|
1186 |
} |
|
1187 |
resp = app.post_json('/api/users/?get_or_create=first_name&get_or_create=last_name', params=payload, status=201) |
|
1188 |
id = resp.json['id'] |
|
1189 |
assert User.objects.get(id=id).first_name == 'John' |
|
1190 |
assert User.objects.get(id=id).last_name == 'Doe' |
|
1191 | ||
1192 |
resp = app.post_json('/api/users/?get_or_create=first_name&get_or_create=last_name', params=payload, status=201) |
|
1193 |
assert id == resp.json['id'] |
|
1194 |
assert User.objects.get(id=id).first_name == 'John' |
|
1195 |
assert User.objects.get(id=id).last_name == 'Doe' |
|
1196 | ||
1197 |
payload['email'] = 'john.doe@example2.net' |
|
1198 |
resp = app.post_json('/api/users/?update_or_create=first_name&update_or_create=last_name', params=payload, status=201) |
|
1199 |
assert id == resp.json['id'] |
|
1200 |
assert User.objects.get(id=id).email == 'john.doe@example2.net' |
|
1201 | ||
1202 | ||
1203 |
def test_api_roles_get_or_create(settings, ou1, app, admin): |
|
1204 |
app.authorization = ('Basic', (admin.username, admin.username)) |
|
1205 |
# test missing first_name |
|
1206 |
payload = { |
|
1207 |
'ou_slug': 'ou1', |
|
1208 |
'name': 'Role 1', |
|
1209 |
'slug': 'role-1', |
|
1210 |
} |
|
1211 |
resp = app.post_json('/api/roles/?get_or_create=slug', params=payload, status=201) |
|
1212 |
uuid = resp.json['uuid'] |
|
1213 |
assert Role.objects.get(uuid=uuid).name == 'Role 1' |
|
1214 |
assert Role.objects.get(uuid=uuid).slug == 'role-1' |
|
1215 | ||
1216 |
resp = app.post_json('/api/roles/?get_or_create=slug', params=payload, status=201) |
|
1217 |
assert uuid == resp.json['uuid'] |
|
1218 | ||
1219 |
payload['name'] = 'Role 2' |
|
1220 |
resp = app.post_json('/api/roles/?update_or_create=slug', params=payload, status=201) |
|
1221 |
assert uuid == resp.json['uuid'] |
|
1222 |
assert Role.objects.get(uuid=uuid).name == 'Role 2' |
|
1223 |
assert Role.objects.get(uuid=uuid).slug == 'role-1' |
|
1149 |
- |