Skip to content

Commit 6682f34

Browse files
EnxDevdpgaspar
andauthored
feat(GroupAPI): Add GroupAPI to FAB (#2339)
* feat: - create API endpoint for Groups - add tests for endpoints and roles * refactor: coverage PR comments * refactor: add the error message --------- Co-authored-by: Daniel Vaz Gaspar <danielvazgaspar@gmail.com>
1 parent 989d4b1 commit 6682f34

File tree

11 files changed

+996
-44
lines changed

11 files changed

+996
-44
lines changed

flask_appbuilder/api/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,6 +1268,15 @@ def _init_properties(self) -> None:
12681268
self.edit_query_rel_fields = self.edit_query_rel_fields or dict()
12691269
self.add_query_rel_fields = self.add_query_rel_fields or dict()
12701270

1271+
def _fetch_entities(self, model_class: Model, ids: List[int]):
1272+
if not ids:
1273+
return []
1274+
return (
1275+
self.datamodel.session.query(model_class)
1276+
.filter(model_class.id.in_(ids))
1277+
.all()
1278+
)
1279+
12711280
def merge_add_field_info(self, response: Dict[str, Any], **kwargs: Any) -> None:
12721281
add_columns_info = kwargs.get("add_columns", {})
12731282
response[API_ADD_COLUMNS_RES_KEY] = self._get_fields_info(

flask_appbuilder/security/sqla/apis/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from flask_appbuilder.security.sqla.apis.group import GroupApi # noqa: F401
12
from flask_appbuilder.security.sqla.apis.permission import PermissionApi # noqa: F401
23
from flask_appbuilder.security.sqla.apis.permission_view_menu import ( # noqa: F401
34
PermissionViewMenuApi,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .api import GroupApi # noqa: F401
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
from flask import request
2+
from flask_appbuilder import ModelRestApi
3+
from flask_appbuilder.api import expose, safe
4+
from flask_appbuilder.const import API_RESULT_RES_KEY
5+
from flask_appbuilder.models.sqla.interface import SQLAInterface
6+
from flask_appbuilder.security.decorators import permission_name, protect
7+
from flask_appbuilder.security.sqla.apis.group.schema import (
8+
GroupPostSchema,
9+
GroupPutSchema,
10+
)
11+
from flask_appbuilder.security.sqla.models import Group, Role, User
12+
from marshmallow import ValidationError
13+
from sqlalchemy.exc import IntegrityError
14+
15+
16+
class GroupApi(ModelRestApi):
17+
resource_name = "security/groups"
18+
openapi_spec_tag = "Security Groups"
19+
class_permission_name = "Group"
20+
datamodel = SQLAInterface(Group)
21+
allow_browser_login = True
22+
23+
list_columns = ["id", "name", "label", "description", "roles", "users"]
24+
show_columns = list_columns
25+
edit_columns = ["name", "label", "description", "users", "roles"]
26+
add_columns = edit_columns
27+
search_columns = list_columns
28+
29+
add_model_schema = GroupPostSchema()
30+
edit_model_schema = GroupPutSchema()
31+
32+
@expose("/", methods=["POST"])
33+
@protect()
34+
@safe
35+
@permission_name("post")
36+
def post(self):
37+
"""Create new group
38+
---
39+
post:
40+
requestBody:
41+
description: Model schema
42+
required: true
43+
content:
44+
application/json:
45+
schema:
46+
$ref: '#/components/schemas/GroupPostSchema'
47+
responses:
48+
201:
49+
description: Group created
50+
content:
51+
application/json:
52+
schema:
53+
type: object
54+
properties:
55+
result:
56+
$ref: '#/components/schemas/GroupPostSchema'
57+
400:
58+
$ref: '#/components/responses/400'
59+
401:
60+
$ref: '#/components/responses/401'
61+
422:
62+
$ref: '#/components/responses/422'
63+
500:
64+
$ref: '#/components/responses/500'
65+
"""
66+
try:
67+
item = self.add_model_schema.load(request.json)
68+
model = Group()
69+
roles = []
70+
users = []
71+
72+
for key, value in item.items():
73+
if key == "roles":
74+
roles = self._fetch_entities(Role, value)
75+
missing_role_ids = set(item["roles"]) - {r.id for r in roles}
76+
if missing_role_ids:
77+
return self.response_400(
78+
message={
79+
"roles": [
80+
(
81+
f"Role(s) with ID(s) {sorted(missing_role_ids)} "
82+
"do not exist."
83+
)
84+
]
85+
}
86+
)
87+
elif key == "users":
88+
users = self._fetch_entities(User, value)
89+
missing_user_ids = set(item["users"]) - {u.id for u in users}
90+
if missing_user_ids:
91+
return self.response_400(
92+
message={
93+
"users": [
94+
(
95+
f"User(s) with ID(s) {sorted(missing_user_ids)} "
96+
"do not exist."
97+
)
98+
]
99+
}
100+
)
101+
else:
102+
setattr(model, key, value)
103+
104+
model.roles = roles
105+
model.users = users
106+
107+
self.pre_add(model)
108+
self.datamodel.add(model, raise_exception=True)
109+
110+
return self.response(201, id=model.id)
111+
112+
except ValidationError as error:
113+
return self.response_400(message=error.messages)
114+
except IntegrityError as e:
115+
return self.response_422(message=str(e.orig))
116+
117+
@expose("/<pk>", methods=["PUT"])
118+
@protect()
119+
@safe
120+
@permission_name("put")
121+
def put(self, pk):
122+
"""Edit group
123+
---
124+
put:
125+
parameters:
126+
- in: path
127+
schema:
128+
type: integer
129+
name: pk
130+
requestBody:
131+
description: Model schema
132+
required: true
133+
content:
134+
application/json:
135+
schema:
136+
$ref: '#/components/schemas/GroupPutSchema'
137+
responses:
138+
200:
139+
description: Group updated
140+
content:
141+
application/json:
142+
schema:
143+
type: object
144+
properties:
145+
result:
146+
$ref: '#/components/schemas/GroupPutSchema'
147+
400:
148+
$ref: '#/components/responses/400'
149+
401:
150+
$ref: '#/components/responses/401'
151+
404:
152+
$ref: '#/components/responses/404'
153+
422:
154+
$ref: '#/components/responses/422'
155+
500:
156+
$ref: '#/components/responses/500'
157+
"""
158+
try:
159+
item = self.edit_model_schema.load(request.json)
160+
model = self.datamodel.get(pk, self._base_filters)
161+
if not model:
162+
return self.response_404()
163+
164+
roles = []
165+
users = []
166+
167+
for key, value in item.items():
168+
if key == "roles":
169+
roles = self._fetch_entities(Role, value)
170+
missing_role_ids = set(value) - {r.id for r in roles}
171+
if missing_role_ids:
172+
return self.response_400(
173+
message={
174+
"roles": [
175+
(
176+
f"Role(s) with ID(s) {sorted(missing_role_ids)} "
177+
"do not exist."
178+
)
179+
]
180+
}
181+
)
182+
elif key == "users":
183+
users = self._fetch_entities(User, value)
184+
missing_user_ids = set(value) - {u.id for u in users}
185+
if missing_user_ids:
186+
return self.response_400(
187+
message={
188+
"users": [
189+
(
190+
f"User(s) with ID(s) {sorted(missing_user_ids)} "
191+
"do not exist."
192+
)
193+
]
194+
}
195+
)
196+
else:
197+
setattr(model, key, value)
198+
199+
if "roles" in item.keys():
200+
model.roles = roles
201+
if "users" in item.keys():
202+
model.users = users
203+
self.pre_update(model)
204+
self.datamodel.edit(model, raise_exception=True)
205+
return self.response(
206+
200,
207+
**{API_RESULT_RES_KEY: self.edit_model_schema.dump(item, many=False)},
208+
)
209+
210+
except ValidationError as e:
211+
return self.response_400(message=e.messages)
212+
except IntegrityError as e:
213+
return self.response_422(message=str(e.orig))
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from flask_appbuilder.security.sqla.models import Group
2+
from marshmallow import fields, Schema
3+
from marshmallow.validate import Length
4+
5+
name_description = "Group name"
6+
label_description = "Group label"
7+
description_description = "Group description"
8+
roles_description = "Group roles"
9+
users_description = "Group users"
10+
11+
12+
class GroupPostSchema(Schema):
13+
model_cls = Group
14+
15+
name = fields.String(
16+
required=True,
17+
validate=[Length(1, 100)],
18+
metadata={"description": name_description},
19+
)
20+
label = fields.String(
21+
required=False,
22+
allow_none=True,
23+
validate=[Length(0, 150)],
24+
metadata={"description": label_description},
25+
)
26+
description = fields.String(
27+
required=False,
28+
allow_none=True,
29+
validate=[Length(0, 512)],
30+
metadata={"description": description_description},
31+
)
32+
roles = fields.List(
33+
fields.Integer,
34+
required=False,
35+
metadata={"description": roles_description},
36+
)
37+
users = fields.List(
38+
fields.Integer,
39+
required=False,
40+
metadata={"description": users_description},
41+
)
42+
43+
44+
class GroupPutSchema(Schema):
45+
model_cls = Group
46+
47+
name = fields.String(
48+
required=False,
49+
validate=[Length(1, 100)],
50+
metadata={"description": name_description},
51+
)
52+
label = fields.String(
53+
required=False,
54+
allow_none=True,
55+
validate=[Length(0, 150)],
56+
metadata={"description": label_description},
57+
)
58+
description = fields.String(
59+
required=False,
60+
allow_none=True,
61+
validate=[Length(0, 512)],
62+
metadata={"description": description_description},
63+
)
64+
roles = fields.List(
65+
fields.Integer,
66+
required=False,
67+
metadata={"description": roles_description},
68+
)
69+
users = fields.List(
70+
fields.Integer,
71+
required=False,
72+
metadata={"description": users_description},
73+
)

0 commit comments

Comments
 (0)