Skip to content

Commit d0fe8ee

Browse files
enable user creation and editing (#158)
* enable user creation and editing * added exceptions * hashed password in the user creation form is no longer displayed * update user refactoring * remove unused code * remove whitespace * version pin fastapi in dev-requiremnts for now Co-authored-by: Daniel Townsend <dan@dantownsend.co.uk>
1 parent 8896694 commit d0fe8ee

3 files changed

Lines changed: 204 additions & 55 deletions

File tree

piccolo_api/crud/endpoints.py

Lines changed: 74 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from dataclasses import dataclass, field
99

1010
import pydantic
11+
from piccolo.apps.user.tables import BaseUser
1112
from piccolo.columns import Column, Where
1213
from piccolo.columns.column_types import Array, ForeignKey, Text, Varchar
1314
from piccolo.columns.operators import (
@@ -245,11 +246,6 @@ def __init__(
245246
methods=["GET"],
246247
),
247248
Route(path="/new/", endpoint=self.get_new, methods=["GET"]),
248-
Route(
249-
path="/password/",
250-
endpoint=self.update_password,
251-
methods=["PUT"],
252-
),
253249
Route(
254250
path="/{row_id:str}/",
255251
endpoint=self.detail,
@@ -343,14 +339,6 @@ async def get_schema(self, request: Request) -> JSONResponse:
343339

344340
###########################################################################
345341

346-
async def update_password(self, request: Request) -> Response:
347-
"""
348-
Used to update password fields.
349-
"""
350-
return Response("Coming soon", status_code=501)
351-
352-
###########################################################################
353-
354342
@apply_validators
355343
async def get_ids(self, request: Request) -> Response:
356344
"""
@@ -807,21 +795,31 @@ async def post_single(
807795
except ValidationError as exception:
808796
return Response(str(exception), status_code=400)
809797

810-
try:
811-
row = self.table(**model.dict())
812-
if self._hook_map:
813-
row = await execute_post_hooks(
814-
hooks=self._hook_map,
815-
hook_type=HookType.pre_save,
816-
row=row,
817-
request=request,
798+
if issubclass(self.table, BaseUser):
799+
try:
800+
user = await self.table.create_user(**model.dict())
801+
json = dump_json({"id": user.id})
802+
return CustomJSONResponse(json, status_code=201)
803+
except Exception as e:
804+
return Response(f"Error: {e}", status_code=400)
805+
else:
806+
try:
807+
row = self.table(**model.dict())
808+
if self._hook_map:
809+
row = await execute_post_hooks(
810+
hooks=self._hook_map,
811+
hook_type=HookType.pre_save,
812+
row=row,
813+
request=request,
814+
)
815+
response = await row.save().run()
816+
json = dump_json(response)
817+
# Returns the id of the inserted row.
818+
return CustomJSONResponse(json, status_code=201)
819+
except ValueError:
820+
return Response(
821+
"Unable to save the resource.", status_code=500
818822
)
819-
response = await row.save().run()
820-
json = dump_json(response)
821-
# Returns the id of the inserted row.
822-
return CustomJSONResponse(json, status_code=201)
823-
except ValueError:
824-
return Response("Unable to save the resource.", status_code=500)
825823

826824
@apply_validators
827825
async def delete_all(
@@ -854,6 +852,7 @@ async def get_new(self, request: Request) -> CustomJSONResponse:
854852
row = self.table(_ignore_missing=True)
855853
row_dict = row.__dict__
856854
row_dict.pop("id", None)
855+
row_dict.pop("password", None)
857856

858857
# If any email columns have a default value of '', we need to remove
859858
# them, otherwise Pydantic will fail to serialise it, because it's not
@@ -1052,38 +1051,58 @@ async def patch_single(
10521051

10531052
cls = self.table
10541053

1055-
try:
1054+
if issubclass(cls, BaseUser):
10561055
values = {
1057-
getattr(cls, key): getattr(model, key) for key in data.keys()
1056+
getattr(cls, key): getattr(model, key)
1057+
for key in cleaned_data.keys()
10581058
}
1059-
except AttributeError:
1060-
unrecognised_keys = set(data.keys()) - set(model.dict().keys())
1061-
return Response(
1062-
f"Unrecognised keys - {unrecognised_keys}.", status_code=400
1063-
)
1059+
if values["password"]:
1060+
cls._validate_password(values["password"])
1061+
values["password"] = cls.hash_password(values["password"])
1062+
else:
1063+
values.pop("password")
10641064

1065-
if self._hook_map:
1066-
values = await execute_patch_hooks(
1067-
hooks=self._hook_map,
1068-
hook_type=HookType.pre_patch,
1069-
row_id=row_id,
1070-
values=values,
1071-
request=request,
1072-
)
1065+
await cls.update(values).where(cls.email == values["email"]).run()
1066+
return Response(status_code=200)
1067+
else:
1068+
try:
1069+
values = {
1070+
getattr(cls, key): getattr(model, key)
1071+
for key in data.keys()
1072+
}
1073+
except AttributeError:
1074+
unrecognised_keys = set(data.keys()) - set(model.dict().keys())
1075+
return Response(
1076+
f"Unrecognised keys - {unrecognised_keys}.",
1077+
status_code=400,
1078+
)
10731079

1074-
try:
1075-
await cls.update(values).where(
1076-
cls._meta.primary_key == row_id
1077-
).run()
1078-
new_row = (
1079-
await cls.select(exclude_secrets=self.exclude_secrets)
1080-
.where(cls._meta.primary_key == row_id)
1081-
.first()
1082-
.run()
1083-
)
1084-
return CustomJSONResponse(self.pydantic_model(**new_row).json())
1085-
except ValueError:
1086-
return Response("Unable to save the resource.", status_code=500)
1080+
if self._hook_map:
1081+
values = await execute_patch_hooks(
1082+
hooks=self._hook_map,
1083+
hook_type=HookType.pre_patch,
1084+
row_id=row_id,
1085+
values=values,
1086+
request=request,
1087+
)
1088+
1089+
try:
1090+
await cls.update(values).where(
1091+
cls._meta.primary_key == row_id
1092+
).run()
1093+
new_row = (
1094+
await cls.select(exclude_secrets=self.exclude_secrets)
1095+
.where(cls._meta.primary_key == row_id)
1096+
.first()
1097+
.run()
1098+
)
1099+
return CustomJSONResponse(
1100+
self.pydantic_model(**new_row).json()
1101+
)
1102+
except ValueError:
1103+
return Response(
1104+
"Unable to save the resource.", status_code=500
1105+
)
10871106

10881107
@apply_validators
10891108
async def delete_single(

requirements/dev-requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ twine==3.2.0
44
mypy==0.950
55
pip-upgrader==1.4.15
66
wheel==0.37.1
7+
8+
# Version pin FastAPI, so MyPy is more predictable.
9+
fastapi==0.58.0

tests/crud/test_crud_endpoints.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from enum import Enum
22
from unittest import TestCase
33

4+
from piccolo.apps.user.tables import BaseUser
45
from piccolo.columns import Email, ForeignKey, Integer, Secret, Text, Varchar
56
from piccolo.columns.readable import Readable
67
from piccolo.table import Table
@@ -113,9 +114,11 @@ def test_split_params(self):
113114

114115
class TestPatch(TestCase):
115116
def setUp(self):
117+
BaseUser.create_table(if_not_exists=True).run_sync()
116118
Movie.create_table(if_not_exists=True).run_sync()
117119

118120
def tearDown(self):
121+
BaseUser.alter().drop_table().run_sync()
119122
Movie.alter().drop_table().run_sync()
120123

121124
def test_patch_succeeds(self):
@@ -145,6 +148,94 @@ def test_patch_succeeds(self):
145148
self.assertTrue(len(movies) == 1)
146149
self.assertTrue(movies[0]["name"] == new_name)
147150

151+
def test_patch_user_new_password(self):
152+
153+
client = TestClient(PiccoloCRUD(table=BaseUser, read_only=False))
154+
155+
json = {
156+
"username": "John",
157+
"password": "John123",
158+
"email": "john@test.com",
159+
"active": False,
160+
"admin": False,
161+
"superuser": False,
162+
}
163+
164+
response = client.post("/", json=json)
165+
self.assertEqual(response.status_code, 201)
166+
167+
user = BaseUser.select().first().run_sync()
168+
169+
json = {
170+
"email": "john@test.com",
171+
"password": "123456",
172+
"active": True,
173+
"admin": False,
174+
"superuser": False,
175+
}
176+
177+
response = client.patch(f"/{user['id']}/", json=json)
178+
self.assertEqual(response.status_code, 200)
179+
180+
def test_patch_user_old_password(self):
181+
182+
client = TestClient(PiccoloCRUD(table=BaseUser, read_only=False))
183+
184+
json = {
185+
"username": "John",
186+
"password": "John123",
187+
"email": "john@test.com",
188+
"active": False,
189+
"admin": False,
190+
"superuser": False,
191+
}
192+
193+
response = client.post("/", json=json)
194+
self.assertEqual(response.status_code, 201)
195+
196+
user = BaseUser.select().first().run_sync()
197+
198+
json = {
199+
"email": "john@test.com",
200+
"password": "",
201+
"active": True,
202+
"admin": False,
203+
"superuser": False,
204+
}
205+
206+
response = client.patch(f"/{user['id']}/", json=json)
207+
self.assertEqual(response.status_code, 200)
208+
209+
def test_patch_user_fails(self):
210+
211+
client = TestClient(PiccoloCRUD(table=BaseUser, read_only=False))
212+
213+
json = {
214+
"username": "John",
215+
"password": "John123",
216+
"email": "john@test.com",
217+
"active": False,
218+
"admin": False,
219+
"superuser": False,
220+
}
221+
222+
response = client.post("/", json=json)
223+
self.assertEqual(response.status_code, 201)
224+
225+
user = BaseUser.select().first().run_sync()
226+
227+
json = {
228+
"email": "john@test.com",
229+
"password": "1",
230+
"active": True,
231+
"admin": True,
232+
"superuser": False,
233+
}
234+
235+
with self.assertRaises(ValueError):
236+
response = client.patch(f"/{user['id']}/", json=json)
237+
self.assertEqual(response.content, b"The password is too short.")
238+
148239
def test_patch_fails(self):
149240
"""
150241
Make sure a patch containing the wrong columns is rejected.
@@ -880,9 +971,11 @@ def test_get_single(self):
880971

881972
class TestPost(TestCase):
882973
def setUp(self):
974+
BaseUser.create_table(if_not_exists=True).run_sync()
883975
Movie.create_table(if_not_exists=True).run_sync()
884976

885977
def tearDown(self):
978+
BaseUser.alter().drop_table().run_sync()
886979
Movie.alter().drop_table().run_sync()
887980

888981
def test_success(self):
@@ -902,7 +995,41 @@ def test_success(self):
902995
self.assertTrue(movie.name == json["name"])
903996
self.assertTrue(movie.rating == json["rating"])
904997

998+
def test_post_user_success(self):
999+
client = TestClient(PiccoloCRUD(table=BaseUser, read_only=False))
1000+
1001+
json = {
1002+
"username": "John",
1003+
"password": "John123",
1004+
"email": "john@test.com",
1005+
"active": False,
1006+
"admin": False,
1007+
"superuser": False,
1008+
}
1009+
1010+
response = client.post("/", json=json)
1011+
self.assertEqual(response.status_code, 201)
1012+
1013+
def test_post_user_fails(self):
1014+
client = TestClient(PiccoloCRUD(table=BaseUser, read_only=False))
1015+
1016+
json = {
1017+
"username": "John",
1018+
"password": "1",
1019+
"email": "john@test.com",
1020+
"active": False,
1021+
"admin": False,
1022+
"superuser": False,
1023+
}
1024+
1025+
response = client.post("/", json=json)
1026+
self.assertEqual(
1027+
response.content, b"Error: The password is too short."
1028+
)
1029+
self.assertEqual(response.status_code, 400)
1030+
9051031
def test_validation_error(self):
1032+
9061033
"""
9071034
Make sure a post returns a validation error with incorrect or missing
9081035
data.

0 commit comments

Comments
 (0)