Skip to content
This repository has been archived by the owner on Nov 14, 2022. It is now read-only.

Add Items (crud, models, endpoints), utils, refactor #15

Merged
merged 2 commits into from
Apr 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,17 @@ After using this generator, your new project (the directory created) will contai

## Release Notes

* PR <a href="https://github.com/tiangolo/full-stack-fastapi-couchbase/pull/15" target="_blank">#15</a>:
* Update CRUD utils to use types better.
* Simplify Pydantic model names, from `UserInCreate` to `UserCreate`, etc.
* Upgrade packages.
* Add new generic "Items" models, crud utils, endpoints, and tests. To facilitate re-using them to create new functionality. As they are simple and generic (not like Users), it's easier to copy-paste and adapt them to each use case.
* Update endpoints/*path operations* to simplify code and use new utilities, prefix and tags in `include_router`.
* Update testing utils.
* Update linting rules, relax vulture to reduce false positives.
* Add full text search for items.
* Update project README.md with tips about how to start with backend.

### 0.2.1

* Fix frontend hijacking /docs in development. Using latest https://github.com/tiangolo/node-frontend with custom Nginx configs in frontend. <a href="https://github.com/tiangolo/full-stack-fastapi-couchbase/pull/14" target="_blank">PR #14</a>.
Expand Down
4 changes: 3 additions & 1 deletion {{cookiecutter.project_slug}}/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ If your Docker is not running in `localhost` (the URLs above wouldn't work) chec

### General workflow

Modify or add Pydantic models in `./backend/app/app/models` and API endpoints in `./backend/app/app/api/`.
Open your editor at `./backend/app/` (instead of the project root: `./`), so that you see an `./app/` directory with your code inside. That way, your editor will be able to find all the imports, etc.

Modify or add Pydantic models in `./backend/app/app/models/`, API endpoints in `./backend/app/app/api/`, CRUD (Create, Read, Update, Delete) utils in `./backend/app/app/crud/`. The easiest might be to copy the ones for Items (models, endpoints, and CRUD utils) and update them to your needs.

Add and modify tasks to the Celery worker in `./backend/app/app/worker.py`.

Expand Down
2 changes: 1 addition & 1 deletion {{cookiecutter.project_slug}}/backend/app/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pyjwt = "*"
python-multipart = "*"
email-validator = "*"
requests = "*"
celery = "==4.2.1"
celery = "~=4.3"
passlib = {extras = ["bcrypt"],version = "*"}
tenacity = "*"
pydantic = "*"
Expand Down
11 changes: 6 additions & 5 deletions {{cookiecutter.project_slug}}/backend/app/app/api/api_v1/api.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from fastapi import APIRouter

from app.api.api_v1.endpoints import role, token, user, utils
from app.api.api_v1.endpoints import items, login, roles, users, utils

api_router = APIRouter()
api_router.include_router(role.router)
api_router.include_router(token.router)
api_router.include_router(user.router)
api_router.include_router(utils.router)
api_router.include_router(login.router, tags=["login"])
api_router.include_router(roles.router, prefix="/roles", tags=["roles"])
api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(utils.router, prefix="/utils", tags=["utils"])
api_router.include_router(items.router, prefix="/items", tags=["items"])
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
from typing import List

from fastapi import APIRouter, Depends, HTTPException

from app import crud
from app.api.utils.security import get_current_active_user
from app.db.database import get_default_bucket
from app.models.item import Item, ItemCreate, ItemUpdate
from app.models.user import UserInDB

router = APIRouter()


@router.get("/", response_model=List[Item])
def read_items(
skip: int = 0,
limit: int = 100,
current_user: UserInDB = Depends(get_current_active_user),
):
"""
Retrieve items.

If superuser, all the items.

If normal user, the items owned by this user.
"""
bucket = get_default_bucket()
if crud.user.is_superuser(current_user):
docs = crud.item.get_multi(bucket, skip=skip, limit=limit)
else:
docs = crud.item.get_multi_by_owner(
bucket=bucket, owner_username=current_user.username, skip=skip, limit=limit
)
return docs


@router.get("/search/", response_model=List[Item])
def search_items(
q: str,
skip: int = 0,
limit: int = 100,
current_user: UserInDB = Depends(get_current_active_user),
):
"""
Search items, use Bleve Query String syntax:
http://blevesearch.com/docs/Query-String-Query/

For typeahead suffix with `*`. For example, a query with: `title:foo*` will match
items containing `football`, `fool proof`, etc.
"""
bucket = get_default_bucket()
if crud.user.is_superuser(current_user):
docs = crud.item.search(bucket=bucket, query_string=q, skip=skip, limit=limit)
else:
docs = crud.item.search_with_owner(
bucket=bucket,
query_string=q,
username=current_user.username,
skip=skip,
limit=limit,
)
return docs


@router.post("/", response_model=Item)
def create_item(
*, item_in: ItemCreate, current_user: UserInDB = Depends(get_current_active_user)
):
"""
Create new item.
"""
bucket = get_default_bucket()
id = crud.utils.generate_new_id()
doc = crud.item.upsert(
bucket=bucket, id=id, doc_in=item_in, owner_username=current_user.username
)
return doc


@router.put("/{id}", response_model=Item)
def update_item(
*,
id: str,
item_in: ItemUpdate,
current_user: UserInDB = Depends(get_current_active_user),
):
"""
Update an item.
"""
bucket = get_default_bucket()
doc = crud.item.get(bucket=bucket, id=id)
if not doc:
raise HTTPException(status_code=404, detail="Item not found")
if not crud.user.is_superuser(current_user) and (
doc.owner_username != current_user.username
):
raise HTTPException(status_code=400, detail="Not enough permissions")
doc = crud.item.update(
bucket=bucket, id=id, doc_in=item_in, owner_username=doc.owner_username
)
return doc


@router.get("/{id}", response_model=Item)
def read_item(id: str, current_user: UserInDB = Depends(get_current_active_user)):
"""
Get item by ID.
"""
bucket = get_default_bucket()
doc = crud.item.get(bucket=bucket, id=id)
if not doc:
raise HTTPException(status_code=404, detail="Item not found")
if not crud.user.is_superuser(current_user) and (
doc.owner_username != current_user.username
):
raise HTTPException(status_code=400, detail="Not enough permissions")
return doc


@router.delete("/{id}", response_model=Item)
def delete_item(id: str, current_user: UserInDB = Depends(get_current_active_user)):
"""
Delete an item by ID.
"""
bucket = get_default_bucket()
doc = crud.item.get(bucket=bucket, id=id)
if not doc:
raise HTTPException(status_code=404, detail="Item not found")
if not crud.user.is_superuser(current_user) and (
doc.owner_username != current_user.username
):
raise HTTPException(status_code=400, detail="Not enough permissions")
doc = crud.item.remove(bucket=bucket, id=id)
return doc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from app.db.database import get_default_bucket
from app.models.msg import Msg
from app.models.token import Token
from app.models.user import User, UserInDB, UserInUpdate
from app.models.user import User, UserInDB, UserUpdate
from app.utils import (
generate_password_reset_token,
send_reset_password_email,
Expand All @@ -20,10 +20,10 @@
router = APIRouter()


@router.post("/login/access-token", response_model=Token, tags=["login"])
@router.post("/login/access-token", response_model=Token)
def login(form_data: OAuth2PasswordRequestForm = Depends()):
"""
OAuth2 compatible token login, get an access token for future requests
OAuth2 compatible token login, get an access token for future requests.
"""
bucket = get_default_bucket()
user = crud.user.authenticate(
Expand All @@ -42,18 +42,18 @@ def login(form_data: OAuth2PasswordRequestForm = Depends()):
}


@router.post("/login/test-token", tags=["login"], response_model=User)
@router.post("/login/test-token", response_model=User)
def test_token(current_user: UserInDB = Depends(get_current_user)):
"""
Test access token
Test access token.
"""
return current_user


@router.post("/password-recovery/{username}", tags=["login"], response_model=Msg)
@router.post("/password-recovery/{username}", response_model=Msg)
def recover_password(username: str):
"""
Password Recovery
Password Recovery.
"""
bucket = get_default_bucket()
user = crud.user.get(bucket, username=username)
Expand All @@ -70,10 +70,10 @@ def recover_password(username: str):
return {"msg": "Password recovery email sent"}


@router.post("/reset-password/", tags=["login"], response_model=Msg)
@router.post("/reset-password/", response_model=Msg)
def reset_password(token: str, new_password: str):
"""
Reset password
Reset password.
"""
username = verify_password_reset_token(token)
if not username:
Expand All @@ -87,6 +87,6 @@ def reset_password(token: str, new_password: str):
)
elif not crud.user.is_active(user):
raise HTTPException(status_code=400, detail="Inactive user")
user_in = UserInUpdate(name=username, password=new_password)
user_in = UserUpdate(name=username, password=new_password)
user = crud.user.update(bucket, username=username, user_in=user_in)
return {"msg": "Password updated successfully"}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
router = APIRouter()


@router.get("/roles/", response_model=Roles)
@router.get("/", response_model=Roles)
def read_roles(current_user: UserInDB = Depends(get_current_active_superuser)):
"""
Retrieve roles
Retrieve roles.
"""
roles = crud.utils.ensure_enums_to_strs(RoleEnum)
return {"roles": roles}
Loading