Skip to content

Commit 3b71a3b

Browse files
authored
Merge pull request #8 from freemindcore/feat/more-configuration
Feat/more configuration
2 parents f06d849 + 6eb419f commit 3b71a3b

File tree

6 files changed

+119
-57
lines changed

6 files changed

+119
-57
lines changed

easy/controller/admin_auto_api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def create_api_controller(
2424
(object,),
2525
{
2626
"model": model,
27+
"generate_crud": True,
2728
"model_fields": "__all__",
2829
"model_recursive": False,
2930
"model_join": True,

easy/controller/base.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,22 @@ class CrudAPIController(metaclass=CrudApiMetaclass):
2424
2525
Configuration:
2626
model: django model
27+
generate_crud: whether to create crud api, default to True
28+
model_exclude: fields to be excluded in Schema, it will ignore model_fields
2729
model_fields: fields to be included in Schema, default to "__all__"
28-
model_exclude: fields to be excluded in Schema
2930
model_join: retrieve all m2m fields, default to True
3031
model_recursive: recursively retrieve FK/OneToOne models, default to False
3132
sensitive_fields: fields to be ignored
33+
34+
Example:
35+
class Meta
36+
model = Event
37+
generate_crud = False
38+
model_exclude = ["field1", "field2"]
39+
model_fields = ["field1", "field2"]
40+
model_join = False
41+
model_recursive = True
42+
sensitive_fields = ["token", "money"]
3243
"""
3344

3445
...

easy/controller/meta.py

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def __init__(self, service=None): # type: ignore
3939
self.model,
4040
"__Meta",
4141
{
42+
"generate_crud": getattr(_meta, "generate_crud", True),
4243
"model_exclude": getattr(_meta, "model_exclude", None),
4344
"model_fields": getattr(_meta, "model_fields", "__all__"),
4445
"model_recursive": getattr(_meta, "model_recursive", False),
@@ -95,6 +96,7 @@ def __new__(mcs, name: str, bases: Tuple[Type[Any], ...], attrs: dict) -> Any:
9596
temp_cls: Type = super().__new__(mcs, name, (object,), attrs)
9697
temp_opts: ModelOptions = ModelOptions(getattr(temp_cls, "Meta", None))
9798
opts_model: Optional[Type[models.Model]] = temp_opts.model
99+
opts_generate_crud: Optional[bool] = temp_opts.generate_crud
98100
opts_fields_exclude: Optional[str] = temp_opts.model_exclude
99101
opts_fields: Optional[str] = temp_opts.model_fields
100102
opts_recursive: Optional[bool] = temp_opts.model_recursive
@@ -115,8 +117,21 @@ def is_private_attrs(attr_name: str) -> Optional[Match[str]]:
115117
)
116118
base_cls_attrs: dict = {}
117119
base_cls_attrs.update(parent_attrs)
118-
119-
if opts_model:
120+
if opts_generate_crud:
121+
base_cls_attrs.update(
122+
{
123+
"get_obj": http_get("/{id}", summary="Get a single object")(
124+
copy_func(CrudAPI.get_obj) # type: ignore
125+
),
126+
"del_obj": http_delete("/{id}", summary="Delete a single object")(
127+
copy_func(CrudAPI.del_obj) # type: ignore
128+
),
129+
"get_objs": http_get("/", summary="Get multiple objects")(
130+
copy_func(CrudAPI.get_objs) # type: ignore
131+
),
132+
}
133+
)
134+
if opts_generate_crud and opts_model:
120135

121136
class DataSchema(ModelSchema):
122137
class Config:
@@ -163,31 +178,15 @@ async def patch_obj( # type: ignore
163178

164179
base_cls_attrs.update(
165180
{
166-
"patch_obj_api": http_patch(
167-
"/{id}", summary="Patch a single object"
168-
)(
181+
"patch_obj": http_patch("/{id}", summary="Patch a single object")(
169182
copy_func(CrudAPI.patch_obj) # type: ignore
170183
),
171-
"add_obj_api": http_put("/", summary="Create")(
184+
"add_obj": http_put("/", summary="Create")(
172185
copy_func(CrudAPI.add_obj) # type: ignore
173186
),
174187
}
175188
)
176189

177-
base_cls_attrs.update(
178-
{
179-
"get_obj_api": http_get("/{id}", summary="Get a single object")(
180-
copy_func(CrudAPI.get_obj) # type: ignore
181-
),
182-
"del_obj_api": http_delete("/{id}", summary="Delete a single object")(
183-
copy_func(CrudAPI.del_obj) # type: ignore
184-
),
185-
"get_objs_api": http_get("/", summary="Get multiple objects")(
186-
copy_func(CrudAPI.get_objs) # type: ignore
187-
),
188-
}
189-
)
190-
191190
new_cls: Type = super().__new__(
192191
mcs,
193192
name,
@@ -203,6 +202,7 @@ async def patch_obj( # type: ignore
203202
opts_model,
204203
"__Meta",
205204
{
205+
"generate_crud": opts_generate_crud,
206206
"model_exclude": opts_fields_exclude,
207207
"model_fields": opts_fields,
208208
"model_recursive": opts_recursive,
@@ -218,19 +218,18 @@ async def patch_obj( # type: ignore
218218
class ModelOptions:
219219
def __init__(self, options: object = None):
220220
"""
221-
Configuration:
222-
model: django model
223-
model_fields: fields to be included in Schema, default to "__all__"
224-
model_exclude: fields to be excluded in Schema
225-
model_join: retrieve all m2m fields, default to True
226-
model_recursive: recursively retrieve FK/OneToOne models, default to False
227-
sensitive_fields: fields to be ignored
221+
Configuration reader
228222
"""
229223
self.model: Optional[Type[models.Model]] = getattr(options, "model", None)
230-
self.model_fields: Optional[Union[str]] = getattr(options, "model_fields", None)
224+
self.generate_crud: Optional[Union[bool]] = getattr(
225+
options, "generate_crud", True
226+
)
231227
self.model_exclude: Optional[Union[str]] = getattr(
232228
options, "model_exclude", None
233229
)
230+
self.model_fields: Optional[Union[str]] = getattr(
231+
options, "model_fields", "__all__"
232+
)
234233
self.model_join: Optional[Union[bool]] = getattr(options, "model_join", True)
235234
self.model_recursive: Optional[Union[bool]] = getattr(
236235
options, "model_recursive", False

easy/domain/serializers.py

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,19 @@ def serialize_model_instance(
4141
"""Serializes Django model instance to dictionary"""
4242
out = {}
4343
for field in obj._meta.get_fields():
44-
if self.is_one_relationship(field):
45-
out.update(self.serialize_foreign_key(obj, field, referrers + (obj,)))
44+
if self.show_field(obj, field.name):
45+
if self.is_one_relationship(field):
46+
out.update(
47+
self.serialize_foreign_key(obj, field, referrers + (obj,))
48+
)
4649

47-
elif self.is_many_relationship(field):
48-
out.update(self.serialize_many_relationship(obj, referrers + (obj,)))
50+
elif self.is_many_relationship(field):
51+
out.update(
52+
self.serialize_many_relationship(obj, referrers + (obj,))
53+
)
4954

50-
else:
51-
out.update(self.serialize_value_field(obj, field))
55+
else:
56+
out.update(self.serialize_value_field(obj, field))
5257
return out
5358

5459
def serialize_queryset(
@@ -73,9 +78,8 @@ def serialize_foreign_key(
7378
except Exception as exc: # pragma: no cover
7479
logger.error(f"serialize_foreign_key error - {obj}", exc_info=exc)
7580
return {field.name: None}
76-
if hasattr(obj, "__Meta") and getattr(obj, "__Meta").get(
77-
"model_recursive", False
78-
):
81+
82+
if self.get_configuration(obj, "model_recursive", default=False):
7983
return {
8084
field.name: self.serialize_model_instance(related_instance, referrers)
8185
}
@@ -94,9 +98,7 @@ def serialize_many_relationship(
9498
for k, v in obj._prefetched_objects_cache.items(): # type: ignore
9599
field_name = k if hasattr(obj, k) else k + "_set"
96100
if v:
97-
if hasattr(obj, "__Meta") and getattr(obj, "__Meta").get(
98-
"model_join", True
99-
):
101+
if self.get_configuration(obj, "model_join", default=True):
100102
out[field_name] = self.serialize_queryset(v, referrers + (obj,))
101103
else:
102104
out[field_name] = [o.pk for o in v]
@@ -107,20 +109,59 @@ def serialize_many_relationship(
107109
return out
108110

109111
@staticmethod
110-
def serialize_value_field(obj: models.Model, field: Any) -> Dict[Any, Any]:
112+
def get_configuration(obj: models.Model, _name: str, default: Any = None) -> Any:
113+
_value = default if default else None
114+
if hasattr(obj, "__Meta"):
115+
_value = getattr(obj, "__Meta").get(_name, None)
116+
return _value
117+
118+
def get_model_fields_list(self, obj: models.Model) -> Any:
119+
model_fields = self.get_configuration(obj, "model_fields")
120+
return model_fields
121+
122+
def get_model_exclude_list(self, obj: models.Model) -> Any:
123+
return self.get_configuration(obj, "model_exclude")
124+
125+
def get_sensitive_list(self, obj: models.Model) -> Any:
126+
return self.get_configuration(obj, "sensitive_fields")
127+
128+
def get_final_excluded_list(self, obj: models.Model) -> Any:
129+
total_excluded_list = []
130+
sensitive_list: List = ["password", "token"]
131+
excluded_list = []
132+
133+
sensitive_fields = self.get_sensitive_list(obj)
134+
if sensitive_fields:
135+
sensitive_list.extend(sensitive_fields)
136+
sensitive_list = list(set(sensitive_list))
137+
138+
excluded_fields = self.get_model_exclude_list(obj)
139+
if excluded_fields:
140+
excluded_list.extend(excluded_fields)
141+
excluded_list = list(set(excluded_list))
142+
143+
total_excluded_list.extend(sensitive_list)
144+
total_excluded_list.extend(excluded_list)
145+
return list(set(total_excluded_list))
146+
147+
def show_field(self, obj: models.Model, field_name: str) -> bool:
148+
model_exclude_list = self.get_model_exclude_list(obj)
149+
if model_exclude_list:
150+
if field_name in self.get_final_excluded_list(obj):
151+
return False
152+
else:
153+
if field_name in self.get_final_excluded_list(obj):
154+
return False
155+
model_fields_list = self.get_model_fields_list(obj)
156+
if model_fields_list != "__all__":
157+
if field_name not in model_fields_list:
158+
return False
159+
return True
160+
161+
def serialize_value_field(self, obj: models.Model, field: Any) -> Dict[Any, Any]:
111162
"""
112163
Serializes regular 'jsonable' field (Char, Int, etc.) of Django model instance
113164
"""
114-
sensitive_list: List = [
115-
"password",
116-
]
117-
if hasattr(obj, "__Meta"):
118-
sensitive_fields = getattr(obj, "__Meta").get("sensitive_fields", None)
119-
if sensitive_fields:
120-
sensitive_list.extend(sensitive_fields)
121-
sensitive_list = list(set(sensitive_list))
122-
if field.name in sensitive_list:
123-
return {}
124165
return {field.name: getattr(obj, field.name)}
125166

126167
def serialize_data(self, data: Any) -> Any:

tests/demo_app/controllers.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ def __init__(self, service: EventService):
2929

3030
class Meta:
3131
model = Event
32-
model_fields = "__all__"
3332
model_join = True
3433

3534

@@ -90,6 +89,10 @@ class AutoGenCrudSomeFieldsAPIController(CrudAPIController):
9089

9190
class Meta:
9291
model = Client
92+
model_fields = [
93+
"key",
94+
"name",
95+
]
9396

9497

9598
@api_controller("unittest")

tests/demo_app/test_async_auto_crud_apis.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,6 @@ async def test_crud_default_get_all(self, transactional_db, easy_api_client):
9090

9191
response = await client.get(
9292
"/",
93-
query=dict(
94-
maximum=100,
95-
),
9693
)
9794
assert response.status_code == 200
9895

@@ -179,18 +176,26 @@ async def test_crud_default_create_some_fields(
179176
):
180177
client = easy_api_client(AutoGenCrudSomeFieldsAPIController)
181178

179+
category = await sync_to_async(Category.objects.create)(
180+
title="Category for Unit Testings"
181+
)
182+
182183
client_type = await sync_to_async(Client.objects.create)(
183184
name="Client for Unit Testings",
184185
key="Type",
185-
category=None,
186+
category=category,
186187
password="DUMMY_PASSWORD",
187188
)
188189

189190
response = await client.get(
190191
f"/{client_type.id}",
191192
)
193+
192194
assert response.status_code == 200
193195
assert response.json()["data"]["key"] == "Type"
196+
with pytest.raises(KeyError):
197+
print(response.json()["data"]["password"])
198+
print(response.json()["data"]["category"])
194199

195200
async def test_crud_default_patch(self, transactional_db, easy_api_client):
196201
client = easy_api_client(AutoGenCrudAPIController)
@@ -244,7 +249,9 @@ async def test_crud_default_patch(self, transactional_db, easy_api_client):
244249
)
245250
assert response.status_code == 200
246251
data = response.json().get("data")
252+
from pprint import pprint
247253

254+
pprint(vars(response))
248255
assert len(data["owner"]) == 2
249256
assert len(data["lead_owner"]) == 0
250257
assert data["owner"][0]["name"] == "Client E for Unit Testings"

0 commit comments

Comments
 (0)