Skip to content

Commit e0c5409

Browse files
committed
fix: generate_crud bug
1 parent b5433d5 commit e0c5409

File tree

5 files changed

+82
-65
lines changed

5 files changed

+82
-65
lines changed

easy/controller/admin_auto_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def create_api_controller(
2828
"model_fields": "__all__",
2929
"model_recursive": False,
3030
"model_join": True,
31-
"sensitive_fields": ["password"],
31+
"sensitive_fields": ["password", "token"],
3232
},
3333
)
3434

easy/controller/meta.py

Lines changed: 55 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -53,47 +53,24 @@ def __init__(self, service=None): # type: ignore
5353
self.service = BaseService(model=self.model)
5454
super().__init__(model=self.model)
5555

56-
# Define Controller APIs for auto generation
57-
async def get_obj(self, request: HttpRequest, id: int) -> Any:
58-
"""
59-
GET /{id}
60-
Retrieve a single Object
61-
"""
62-
try:
63-
qs = await self.service.get_obj(id)
64-
except Exception as e: # pragma: no cover
65-
logger.error(f"Get Error - {e}", exc_info=True)
66-
return BaseApiResponse(str(e), message="Get Failed", errno=500)
67-
if qs:
68-
return qs
69-
else:
70-
return BaseApiResponse(message="Not Found", errno=404)
71-
72-
async def del_obj(self, request: HttpRequest, id: int) -> Any:
73-
"""
74-
DELETE /{id}
75-
Delete a single Object
76-
"""
77-
if await self.service.del_obj(id):
78-
return BaseApiResponse("Deleted.", errno=204)
79-
else:
80-
return BaseApiResponse("Not Found.", errno=404)
81-
82-
@paginate
83-
async def get_objs(self, request: HttpRequest, filters: str = None) -> Any:
84-
"""
85-
GET /?maximum={int}&filters={filters_dict}
86-
Retrieve multiple Object (optional: maximum # and filters)
87-
"""
88-
if filters:
89-
return await self.service.get_objs(**json.loads(filters))
90-
return await self.service.get_objs()
91-
9256

9357
class CrudApiMetaclass(ABCMeta):
9458
def __new__(mcs, name: str, bases: Tuple[Type[Any], ...], attrs: dict) -> Any:
59+
def is_private_attrs(attr_name: str) -> Optional[Match[str]]:
60+
return re.match(r"^__[^\d\W]\w*\Z__$", attr_name, re.UNICODE)
61+
62+
parent_attrs = ChainMap(
63+
*[attrs]
64+
+ [
65+
{k: v for (k, v) in vars(base).items() if not (is_private_attrs(k))}
66+
for base in bases
67+
]
68+
)
69+
base_cls_attrs: dict = {}
70+
base_cls_attrs.update(parent_attrs)
71+
9572
# Get configs from Meta
96-
temp_cls: Type = super().__new__(mcs, name, (object,), attrs)
73+
temp_cls: Type = super().__new__(mcs, name, (object,), base_cls_attrs)
9774
temp_opts: ModelOptions = ModelOptions(getattr(temp_cls, "Meta", None))
9875
opts_model: Optional[Type[models.Model]] = temp_opts.model
9976
opts_generate_crud: Optional[bool] = temp_opts.generate_crud
@@ -105,33 +82,56 @@ def __new__(mcs, name: str, bases: Tuple[Type[Any], ...], attrs: dict) -> Any:
10582
Union[str, List[str]]
10683
] = temp_opts.sensitive_fields
10784

108-
def is_private_attrs(attr_name: str) -> Optional[Match[str]]:
109-
return re.match(r"^__[^\d\W]\w*\Z__$", attr_name, re.UNICODE)
85+
# Define Controller APIs for auto generation
86+
async def get_obj(self, request: HttpRequest, id: int) -> Any: # type: ignore
87+
"""
88+
GET /{id}
89+
Retrieve a single Object
90+
"""
91+
try:
92+
qs = await self.service.get_obj(id)
93+
except Exception as e: # pragma: no cover
94+
logger.error(f"Get Error - {e}", exc_info=True)
95+
return BaseApiResponse(str(e), message="Get Failed", errno=500)
96+
if qs:
97+
return qs
98+
else:
99+
return BaseApiResponse(message="Not Found", errno=404)
100+
101+
async def del_obj(self, request: HttpRequest, id: int) -> Any: # type: ignore
102+
"""
103+
DELETE /{id}
104+
Delete a single Object
105+
"""
106+
if await self.service.del_obj(id):
107+
return BaseApiResponse("Deleted.", errno=204)
108+
else:
109+
return BaseApiResponse("Not Found.", errno=404)
110+
111+
@paginate
112+
async def get_objs(self, request: HttpRequest, filters: str = None) -> Any: # type: ignore
113+
"""
114+
GET /?maximum={int}&filters={filters_dict}
115+
Retrieve multiple Object (optional: maximum # and filters)
116+
"""
117+
if filters:
118+
return await self.service.get_objs(**json.loads(filters))
119+
return await self.service.get_objs()
110120

111-
parent_attrs = ChainMap(
112-
*[attrs]
113-
+ [
114-
{k: v for (k, v) in vars(base).items() if not (is_private_attrs(k))}
115-
for base in bases
116-
]
117-
)
118-
base_cls_attrs: dict = {}
119-
base_cls_attrs.update(parent_attrs)
120-
if opts_generate_crud:
121+
if opts_generate_crud and opts_model:
121122
base_cls_attrs.update(
122123
{
123124
"get_obj": http_get("/{id}", summary="Get a single object")(
124-
copy_func(CrudAPI.get_obj) # type: ignore
125+
copy_func(get_obj) # type: ignore
125126
),
126127
"del_obj": http_delete("/{id}", summary="Delete a single object")(
127-
copy_func(CrudAPI.del_obj) # type: ignore
128+
copy_func(del_obj) # type: ignore
128129
),
129130
"get_objs": http_get("/", summary="Get multiple objects")(
130-
copy_func(CrudAPI.get_objs) # type: ignore
131+
copy_func(get_objs) # type: ignore
131132
),
132133
}
133134
)
134-
if opts_generate_crud and opts_model:
135135

136136
class DataSchema(ModelSchema):
137137
class Config:
@@ -173,16 +173,13 @@ async def patch_obj( # type: ignore
173173
f"{opts_model.__name__}__AutoSchema({str(uuid.uuid4())[:4]})"
174174
)
175175

176-
setattr(CrudAPI, "patch_obj", patch_obj)
177-
setattr(CrudAPI, "add_obj", add_obj)
178-
179176
base_cls_attrs.update(
180177
{
181178
"patch_obj": http_patch("/{id}", summary="Patch a single object")(
182-
copy_func(CrudAPI.patch_obj) # type: ignore
179+
copy_func(patch_obj) # type: ignore
183180
),
184181
"add_obj": http_put("/", summary="Create")(
185-
copy_func(CrudAPI.add_obj) # type: ignore
182+
copy_func(add_obj) # type: ignore
186183
),
187184
}
188185
)

tests/demo_app/controllers.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,14 @@ def __init__(self, service: EventService):
186186

187187
class Meta:
188188
model = Event
189+
190+
191+
@api_controller("unittest", permissions=[AdminSitePermission])
192+
class NoCrudAPIController(CrudAPIController):
193+
def __init__(self, service: EventService):
194+
super().__init__(service)
195+
self.service = service
196+
197+
class Meta:
198+
model = Event
199+
generate_crud = False

tests/demo_app/test_async_auto_crud_apis.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
AutoGenCrudSomeFieldsAPIController,
1212
EventSchema,
1313
InheritedRecursiveAPIController,
14+
NoCrudAPIController,
1415
RecursiveAPIController,
1516
)
1617
from tests.demo_app.models import Category, Client, Event, Type
@@ -25,6 +26,19 @@
2526
@pytest.mark.skipif(django.VERSION < (3, 1), reason="requires django 3.1 or higher")
2627
@pytest.mark.django_db
2728
class TestAutoCrudAdminAPI:
29+
async def test_crud_generate_or_not(self, transactional_db, easy_api_client):
30+
client = easy_api_client(NoCrudAPIController)
31+
32+
object_data = dummy_data.copy()
33+
object_data.update(title=f"{object_data['title']}_get")
34+
35+
event = await sync_to_async(Event.objects.create)(**object_data)
36+
37+
with pytest.raises(Exception):
38+
await client.get(
39+
f"/{event.id}",
40+
)
41+
2842
async def test_crud_default_get_all(self, transactional_db, easy_api_client):
2943
client = easy_api_client(AutoGenCrudAPIController)
3044

tests/demo_app/test_auto_api_creation.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,7 @@ async def test_auto_apis(transactional_db, easy_api_client):
3737
if not str(controller_class).endswith("ClientAdminAPIController"):
3838
continue
3939
client = easy_api_client(controller_class)
40-
response = await client.get(
41-
"/",
42-
query=dict(
43-
maximum=100,
44-
),
45-
)
40+
response = await client.get("/")
4641
assert response.status_code == 200
4742
assert response.json()["data"] == []
4843

0 commit comments

Comments
 (0)