Skip to content

Commit

Permalink
enable user creation and editing (#158)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
sinisaos and dantownsend committed Sep 26, 2022
1 parent 8896694 commit d0fe8ee
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 55 deletions.
129 changes: 74 additions & 55 deletions piccolo_api/crud/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from dataclasses import dataclass, field

import pydantic
from piccolo.apps.user.tables import BaseUser
from piccolo.columns import Column, Where
from piccolo.columns.column_types import Array, ForeignKey, Text, Varchar
from piccolo.columns.operators import (
Expand Down Expand Up @@ -245,11 +246,6 @@ def __init__(
methods=["GET"],
),
Route(path="/new/", endpoint=self.get_new, methods=["GET"]),
Route(
path="/password/",
endpoint=self.update_password,
methods=["PUT"],
),
Route(
path="/{row_id:str}/",
endpoint=self.detail,
Expand Down Expand Up @@ -343,14 +339,6 @@ async def get_schema(self, request: Request) -> JSONResponse:

###########################################################################

async def update_password(self, request: Request) -> Response:
"""
Used to update password fields.
"""
return Response("Coming soon", status_code=501)

###########################################################################

@apply_validators
async def get_ids(self, request: Request) -> Response:
"""
Expand Down Expand Up @@ -807,21 +795,31 @@ async def post_single(
except ValidationError as exception:
return Response(str(exception), status_code=400)

try:
row = self.table(**model.dict())
if self._hook_map:
row = await execute_post_hooks(
hooks=self._hook_map,
hook_type=HookType.pre_save,
row=row,
request=request,
if issubclass(self.table, BaseUser):
try:
user = await self.table.create_user(**model.dict())
json = dump_json({"id": user.id})
return CustomJSONResponse(json, status_code=201)
except Exception as e:
return Response(f"Error: {e}", status_code=400)
else:
try:
row = self.table(**model.dict())
if self._hook_map:
row = await execute_post_hooks(
hooks=self._hook_map,
hook_type=HookType.pre_save,
row=row,
request=request,
)
response = await row.save().run()
json = dump_json(response)
# Returns the id of the inserted row.
return CustomJSONResponse(json, status_code=201)
except ValueError:
return Response(
"Unable to save the resource.", status_code=500
)
response = await row.save().run()
json = dump_json(response)
# Returns the id of the inserted row.
return CustomJSONResponse(json, status_code=201)
except ValueError:
return Response("Unable to save the resource.", status_code=500)

@apply_validators
async def delete_all(
Expand Down Expand Up @@ -854,6 +852,7 @@ async def get_new(self, request: Request) -> CustomJSONResponse:
row = self.table(_ignore_missing=True)
row_dict = row.__dict__
row_dict.pop("id", None)
row_dict.pop("password", None)

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

cls = self.table

try:
if issubclass(cls, BaseUser):
values = {
getattr(cls, key): getattr(model, key) for key in data.keys()
getattr(cls, key): getattr(model, key)
for key in cleaned_data.keys()
}
except AttributeError:
unrecognised_keys = set(data.keys()) - set(model.dict().keys())
return Response(
f"Unrecognised keys - {unrecognised_keys}.", status_code=400
)
if values["password"]:
cls._validate_password(values["password"])
values["password"] = cls.hash_password(values["password"])
else:
values.pop("password")

if self._hook_map:
values = await execute_patch_hooks(
hooks=self._hook_map,
hook_type=HookType.pre_patch,
row_id=row_id,
values=values,
request=request,
)
await cls.update(values).where(cls.email == values["email"]).run()
return Response(status_code=200)
else:
try:
values = {
getattr(cls, key): getattr(model, key)
for key in data.keys()
}
except AttributeError:
unrecognised_keys = set(data.keys()) - set(model.dict().keys())
return Response(
f"Unrecognised keys - {unrecognised_keys}.",
status_code=400,
)

try:
await cls.update(values).where(
cls._meta.primary_key == row_id
).run()
new_row = (
await cls.select(exclude_secrets=self.exclude_secrets)
.where(cls._meta.primary_key == row_id)
.first()
.run()
)
return CustomJSONResponse(self.pydantic_model(**new_row).json())
except ValueError:
return Response("Unable to save the resource.", status_code=500)
if self._hook_map:
values = await execute_patch_hooks(
hooks=self._hook_map,
hook_type=HookType.pre_patch,
row_id=row_id,
values=values,
request=request,
)

try:
await cls.update(values).where(
cls._meta.primary_key == row_id
).run()
new_row = (
await cls.select(exclude_secrets=self.exclude_secrets)
.where(cls._meta.primary_key == row_id)
.first()
.run()
)
return CustomJSONResponse(
self.pydantic_model(**new_row).json()
)
except ValueError:
return Response(
"Unable to save the resource.", status_code=500
)

@apply_validators
async def delete_single(
Expand Down
3 changes: 3 additions & 0 deletions requirements/dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ twine==3.2.0
mypy==0.950
pip-upgrader==1.4.15
wheel==0.37.1

# Version pin FastAPI, so MyPy is more predictable.
fastapi==0.58.0
127 changes: 127 additions & 0 deletions tests/crud/test_crud_endpoints.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from enum import Enum
from unittest import TestCase

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

class TestPatch(TestCase):
def setUp(self):
BaseUser.create_table(if_not_exists=True).run_sync()
Movie.create_table(if_not_exists=True).run_sync()

def tearDown(self):
BaseUser.alter().drop_table().run_sync()
Movie.alter().drop_table().run_sync()

def test_patch_succeeds(self):
Expand Down Expand Up @@ -145,6 +148,94 @@ def test_patch_succeeds(self):
self.assertTrue(len(movies) == 1)
self.assertTrue(movies[0]["name"] == new_name)

def test_patch_user_new_password(self):

client = TestClient(PiccoloCRUD(table=BaseUser, read_only=False))

json = {
"username": "John",
"password": "John123",
"email": "john@test.com",
"active": False,
"admin": False,
"superuser": False,
}

response = client.post("/", json=json)
self.assertEqual(response.status_code, 201)

user = BaseUser.select().first().run_sync()

json = {
"email": "john@test.com",
"password": "123456",
"active": True,
"admin": False,
"superuser": False,
}

response = client.patch(f"/{user['id']}/", json=json)
self.assertEqual(response.status_code, 200)

def test_patch_user_old_password(self):

client = TestClient(PiccoloCRUD(table=BaseUser, read_only=False))

json = {
"username": "John",
"password": "John123",
"email": "john@test.com",
"active": False,
"admin": False,
"superuser": False,
}

response = client.post("/", json=json)
self.assertEqual(response.status_code, 201)

user = BaseUser.select().first().run_sync()

json = {
"email": "john@test.com",
"password": "",
"active": True,
"admin": False,
"superuser": False,
}

response = client.patch(f"/{user['id']}/", json=json)
self.assertEqual(response.status_code, 200)

def test_patch_user_fails(self):

client = TestClient(PiccoloCRUD(table=BaseUser, read_only=False))

json = {
"username": "John",
"password": "John123",
"email": "john@test.com",
"active": False,
"admin": False,
"superuser": False,
}

response = client.post("/", json=json)
self.assertEqual(response.status_code, 201)

user = BaseUser.select().first().run_sync()

json = {
"email": "john@test.com",
"password": "1",
"active": True,
"admin": True,
"superuser": False,
}

with self.assertRaises(ValueError):
response = client.patch(f"/{user['id']}/", json=json)
self.assertEqual(response.content, b"The password is too short.")

def test_patch_fails(self):
"""
Make sure a patch containing the wrong columns is rejected.
Expand Down Expand Up @@ -880,9 +971,11 @@ def test_get_single(self):

class TestPost(TestCase):
def setUp(self):
BaseUser.create_table(if_not_exists=True).run_sync()
Movie.create_table(if_not_exists=True).run_sync()

def tearDown(self):
BaseUser.alter().drop_table().run_sync()
Movie.alter().drop_table().run_sync()

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

def test_post_user_success(self):
client = TestClient(PiccoloCRUD(table=BaseUser, read_only=False))

json = {
"username": "John",
"password": "John123",
"email": "john@test.com",
"active": False,
"admin": False,
"superuser": False,
}

response = client.post("/", json=json)
self.assertEqual(response.status_code, 201)

def test_post_user_fails(self):
client = TestClient(PiccoloCRUD(table=BaseUser, read_only=False))

json = {
"username": "John",
"password": "1",
"email": "john@test.com",
"active": False,
"admin": False,
"superuser": False,
}

response = client.post("/", json=json)
self.assertEqual(
response.content, b"Error: The password is too short."
)
self.assertEqual(response.status_code, 400)

def test_validation_error(self):

"""
Make sure a post returns a validation error with incorrect or missing
data.
Expand Down

0 comments on commit d0fe8ee

Please sign in to comment.