added forms

This commit is contained in:
Alexander Karpov 2022-12-09 02:33:22 +03:00
parent 5961d7dbe0
commit 145c322fb5
22 changed files with 599 additions and 135 deletions

View File

@ -9,10 +9,9 @@ from passlib.context import CryptContext
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from med_backend.auth.schemas import TokenData, User from med_backend.auth.schemas import TokenData, User
from med_backend.db.dependencies import get_db_session
from med_backend.settings import settings from med_backend.settings import settings
from med_backend.users.crud import get_user_by_email
from ..db.dependencies import get_db_session
from . import crud
SECRET_KEY = config("SECRET") SECRET_KEY = config("SECRET")
JWT_ALGORITHM = settings.JWT_ALGORITHM JWT_ALGORITHM = settings.JWT_ALGORITHM
@ -33,7 +32,7 @@ def get_password_hash(password: str) -> str:
async def get_user(session: AsyncSession, email: str) -> User: async def get_user(session: AsyncSession, email: str) -> User:
db_user = await crud.get_user_by_email(session, email=email) db_user = await get_user_by_email(session, email=email)
if db_user is None: if db_user is None:
raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=404, detail="User not found")
return db_user return db_user
@ -96,16 +95,3 @@ async def get_current_active_user(
if current_user.disabled: if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user") raise HTTPException(status_code=400, detail="Inactive user")
return current_user return current_user
async def get_current_active_manager(
current_user: User = Depends(get_current_user),
) -> User:
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
if not current_user.is_manager:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="You are not allowed to access this info",
)
return current_user

View File

@ -5,7 +5,6 @@ from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from starlette import status from starlette import status
from med_backend.auth.crud import create_user
from med_backend.auth.schemas import Token, User, UserCreate, UserLogin, UserPublicInfo from med_backend.auth.schemas import Token, User, UserCreate, UserLogin, UserPublicInfo
from med_backend.auth.services import ( from med_backend.auth.services import (
ACCESS_TOKEN_EXPIRE_MINUTES, ACCESS_TOKEN_EXPIRE_MINUTES,
@ -14,6 +13,7 @@ from med_backend.auth.services import (
get_current_active_user, get_current_active_user,
) )
from med_backend.db.dependencies import get_db_session from med_backend.db.dependencies import get_db_session
from med_backend.users.crud import create_user
router = APIRouter() router = APIRouter()

View File

@ -0,0 +1,147 @@
from typing import List
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from med_backend.db.base import Base
from med_backend.db.models.users import UserScheme
class FormScheme(Base):
__tablename__ = "forms"
id: int = Column(
Integer,
primary_key=True,
autoincrement=True,
unique=True,
index=True,
)
name: str = Column(String, nullable=False)
# user
user_id: int = Column(Integer, ForeignKey(UserScheme.id), primary_key=True)
user: UserScheme = relationship("UserScheme", foreign_keys="FormScheme.user_id")
questions: List["FormQuestion"] = relationship(
"FormQuestion",
back_populates="form",
)
class FormQuestion(Base):
__tablename__ = "form_questions"
id: int = Column(
Integer,
primary_key=True,
autoincrement=True,
unique=True,
index=True,
)
# form
form_id: int = Column(Integer, ForeignKey(FormScheme.id), primary_key=True)
form: FormScheme = relationship("FormScheme", foreign_keys="FormQuestion.form_id")
type: str = Column(String, default="number")
question: str = Column(String, nullable=False)
ref_min: int = Column(Integer, nullable=True)
ref_max: int = Column(Integer, nullable=True)
class FormAssignment(Base):
__tablename__ = "form_assignment"
id: int = Column(
Integer,
primary_key=True,
autoincrement=True,
unique=True,
index=True,
)
form_id: int = Column(Integer, ForeignKey(FormScheme.id), primary_key=True)
form: FormScheme = relationship("FormScheme", foreign_keys="FormAssignment.form_id")
user_id: int = Column(Integer, ForeignKey(UserScheme.id), primary_key=True)
user: UserScheme = relationship("UserScheme", foreign_keys="FormAssignment.user_id")
class UserRevQuestion(Base):
__tablename__ = "user_form_rev_question"
id: int = Column(
Integer,
primary_key=True,
autoincrement=True,
unique=True,
index=True,
)
# question
question_id: int = Column(Integer, ForeignKey(FormQuestion.id), primary_key=True)
question: FormQuestion = relationship(
"FormQuestion",
foreign_keys="UserRevQuestion.question_id",
)
# user
user_id: int = Column(Integer, ForeignKey(UserScheme.id), primary_key=True)
user: UserScheme = relationship(
"UserScheme",
foreign_keys="UserRevQuestion.user_id",
)
ref_min: int = Column(Integer, nullable=False)
ref_max: int = Column(Integer, nullable=False)
class UserFormSubmission(Base):
__tablename__ = "user_form_submission"
id: int = Column(
Integer,
primary_key=True,
autoincrement=True,
unique=True,
index=True,
)
# form
form_id: int = Column(Integer, ForeignKey(FormScheme.id), primary_key=True)
form: FormScheme = relationship(
"FormScheme",
foreign_keys="UserFormSubmission.form_id",
)
# user
user_id: int = Column(Integer, ForeignKey(UserScheme.id), primary_key=True)
user: UserScheme = relationship(
"UserScheme",
foreign_keys="UserFormSubmission.user_id",
)
answers: List["UserFormFieldSubmission"] = relationship(
"UserFormFieldSubmission",
back_populates="submission",
)
class UserFormFieldSubmission(Base):
__tablename__ = "user_form_field_submission"
id: int = Column(
Integer,
primary_key=True,
autoincrement=True,
unique=True,
index=True,
)
# submission
submission_id: int = Column(
Integer,
ForeignKey(UserFormSubmission.id),
primary_key=True,
)
submission: UserFormSubmission = relationship(
"UserFormSubmission",
foreign_keys="UserFormFieldSubmission.submission_id",
)

View File

@ -11,7 +11,13 @@ class UserScheme(Base):
__tablename__ = "users" __tablename__ = "users"
id: int = Column(Integer, primary_key=True, index=True) id: int = Column(
Integer,
primary_key=True,
autoincrement=True,
unique=True,
index=True,
)
email: EmailStr = Column(String, unique=True, index=True, nullable=False) email: EmailStr = Column(String, unique=True, index=True, nullable=False)
fullname: str = Column(String, default="") fullname: str = Column(String, default="")
hashed_password: str = Column(String) hashed_password: str = Column(String)

View File

@ -0,0 +1,5 @@
"""API for form creation and submitting"""
from med_backend.forms.views import router
__all__ = ["router"]

168
med_backend/forms/crud.py Normal file
View File

@ -0,0 +1,168 @@
from typing import List
from fastapi import HTTPException
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from med_backend.db.models.forms import (
FormAssignment,
FormQuestion,
FormScheme,
UserRevQuestion,
)
from med_backend.forms.schemas import BaseForm, CreateFormField
from med_backend.users.crud import get_user
async def get_forms(
session: AsyncSession,
skip: int = 0,
limit: int = 100,
) -> List[FormScheme] | None:
r = await session.execute(
select(FormScheme).offset(skip).limit(limit),
)
forms = r.scalars().all()
return forms
async def get_form(session: AsyncSession, form_id: int) -> FormScheme | None:
r = await session.execute(select(FormScheme).where(FormScheme.id == form_id))
form = r.scalars().first()
return form
async def filter_form_assigment(
session: AsyncSession,
user_id: int,
skip: int = 0,
limit: int = 100,
) -> List[FormScheme] | None:
r = await session.execute(
select(FormScheme)
.outerjoin(FormAssignment)
.where(FormAssignment.user_id == user_id)
.offset(skip)
.limit(limit),
)
forms = r.scalars().fetchall()
return forms
async def get_questions(session: AsyncSession, form_id: int) -> List[FormQuestion]:
r = await session.execute(
select(FormQuestion).where(FormQuestion.form_id == form_id),
)
questions = r.scalars().all()
return questions
async def create_form(
session: AsyncSession,
form: BaseForm,
user_id: int,
) -> FormScheme:
user = await get_user(session, user_id)
if not user or not user.is_manager:
raise HTTPException(status_code=422, detail="User can't be used")
db_form = FormScheme(name=form.name, user_id=user_id)
session.add(db_form)
await session.commit()
await session.refresh(db_form)
return db_form
async def create_form_field(
session: AsyncSession,
field: CreateFormField,
user_id: int,
form_id: int,
) -> FormQuestion:
user = await get_user(session, user_id)
if not user or not user.is_manager:
raise HTTPException(status_code=422, detail="User can't be used")
form = await get_form(session, form_id)
if not form:
raise HTTPException(status_code=422, detail="Form can't be used")
if user.id != form.user_id:
raise HTTPException(
status_code=401,
detail="You are not allowed to access this form",
)
obj = FormQuestion(
form_id=form_id,
type=field.type,
question=field.question,
ref_min=field.ref_min,
ref_max=field.ref_max,
)
session.add(obj)
await session.commit()
await session.refresh(obj)
return obj
async def create_form_assigment(session: AsyncSession, form_id: int, user_id: int):
user = await get_user(session, user_id)
if not user:
raise HTTPException(status_code=422, detail="User can't be used")
form = await get_form(session, form_id)
if not form:
raise HTTPException(status_code=422, detail="Form can't be used")
assigment = await session.execute(
select(FormAssignment)
.where(FormAssignment.form_id == form_id)
.where(FormAssignment.user_id == user_id),
)
if assigment.scalars().first():
return True
obj = FormAssignment(form_id=form_id, user_id=user_id)
session.add(obj)
await session.commit()
await session.refresh(obj)
return True
async def create_user_form_rev_question(
session: AsyncSession,
field_id: int,
user_id: int,
ref_min: int,
ref_max: int,
):
r = await session.execute(select(FormQuestion).where(FormQuestion.id == field_id))
field = r.scalars().first()
if not field:
raise HTTPException(status_code=422, detail="Such field doesn't exist")
r = await session.execute(
select(UserRevQuestion)
.where(UserRevQuestion.user_id == user_id)
.where(UserRevQuestion.question_id == field_id),
)
rev = r.scalars().first()
if rev:
await session.execute(
update(UserRevQuestion)
.where(UserRevQuestion.id == rev.id)
.values(ref_max=ref_max, ref_min=ref_min),
)
else:
rev = UserRevQuestion(
question_id=field_id,
user_id=user_id,
ref_max=ref_max,
ref_min=ref_min,
)
session.add(rev)
await session.commit()
await session.refresh(rev)
return rev

View File

@ -0,0 +1,64 @@
from typing import List
from pydantic import BaseModel
from med_backend.auth.schemas import UserPublicInfo
class Question(BaseModel):
id: int
type: str
question: str
class BaseFormField(BaseModel):
type: str
question: str
ref_min: int | None
ref_max: int | None
class CreateFormField(BaseFormField):
...
class FormField(BaseFormField):
id: int
class Config:
orm_mode = True
class QuestionRef(BaseModel):
id: int
ref_min: int | None
ref_max: int | None
class FormAssigment(BaseModel):
user_id: int
question_refs: List[QuestionRef]
class BaseForm(BaseModel):
name: str
class FormCreate(BaseForm):
user_id: int
class ListForm(BaseForm):
id: int
class Config:
orm_mode = True
class Form(BaseForm):
id: int
user: UserPublicInfo
questions: List[Question]
class Config:
orm_mode = True

View File

@ -0,0 +1,43 @@
from fastapi import HTTPException
from pydantic import parse_obj_as
from sqlalchemy.ext.asyncio import AsyncSession
from med_backend.auth.schemas import UserPublicInfo
from med_backend.forms.crud import (
create_form_assigment,
create_user_form_rev_question,
get_form,
get_questions,
)
from med_backend.forms.schemas import Form, FormAssigment, Question
async def get_full_form(session: AsyncSession, form_id: int) -> Form:
form = await get_form(session, form_id)
if not form:
raise HTTPException(status_code=404, detail="Form doesn't exist")
questions = await get_questions(session, form_id)
return Form(
id=form_id,
name=form.name,
user=parse_obj_as(UserPublicInfo, form.user),
questions=[
Question(id=q.id, type=q.type, question=q.question) for q in questions
],
)
async def assign_form(session: AsyncSession, data: FormAssigment, form_id: int):
form = await get_form(session, form_id)
if not form:
raise HTTPException(status_code=404, detail="Form doesn't exist")
await create_form_assigment(session, form_id, data.user_id)
for field in data.question_refs:
await create_user_form_rev_question(
session,
field.id,
data.user_id,
field.ref_min,
field.ref_max,
)

109
med_backend/forms/views.py Normal file
View File

@ -0,0 +1,109 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from starlette import status
from med_backend.auth.schemas import User
from med_backend.auth.services import get_current_active_user
from med_backend.db.dependencies import get_db_session
from med_backend.forms import crud, services
from med_backend.forms.schemas import (
BaseForm,
CreateFormField,
Form,
FormAssigment,
FormField,
ListForm,
)
from med_backend.forms.services import assign_form
from med_backend.users.services import get_current_active_manager
router = APIRouter()
@router.get("/all", response_model=list[ListForm])
async def get_all_forms(
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_manager),
session: AsyncSession = Depends(get_db_session),
):
forms = await crud.get_forms(session, skip, limit)
return forms
@router.get("/list", response_model=list[ListForm])
async def get_all_forms(
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_user),
session: AsyncSession = Depends(get_db_session),
):
forms = await crud.filter_form_assigment(session, current_user.id, skip, limit)
return forms
@router.post("/create", response_model=Form)
async def create_form_view(
data: BaseForm,
current_user: User = Depends(get_current_active_manager),
session: AsyncSession = Depends(get_db_session),
) -> Form:
db_form = await crud.create_form(session, data, current_user.id)
form = await services.get_full_form(session, db_form.id)
return form
@router.get("/{form_id}", response_model=Form)
async def get_form(
form_id: int,
current_user: User = Depends(get_current_active_user),
session: AsyncSession = Depends(get_db_session),
) -> Form:
form = await services.get_full_form(session, form_id)
return form
@router.get("/{form_id}/fields", response_model=List[FormField])
async def create_form_field_view(
form_id: int,
current_user: User = Depends(get_current_active_manager),
session: AsyncSession = Depends(get_db_session),
):
form = await services.get_form(session, form_id)
if form.user.id != current_user.id:
raise HTTPException(
status_code=401,
detail="You are not allowed to access this form",
)
fields = await services.get_questions(session, form_id)
return fields
@router.post("/{form_id}/fields", response_model=FormField)
async def create_form_field_view(
form_id: int,
data: CreateFormField,
current_user: User = Depends(get_current_active_manager),
session: AsyncSession = Depends(get_db_session),
):
field = await crud.create_form_field(session, data, current_user.id, form_id)
return field
@router.post("/{form_id}/assign", status_code=status.HTTP_201_CREATED)
async def create_assigment_view(
form_id: int,
data: FormAssigment,
current_user: User = Depends(get_current_active_manager),
session: AsyncSession = Depends(get_db_session),
):
form = await services.get_form(session, form_id)
if form.user.id != current_user.id:
raise HTTPException(
status_code=401,
detail="You are not allowed to access this form",
)
await assign_form(session, data, form_id)
return {"message": "created"}

View File

@ -28,7 +28,7 @@ class Settings(BaseSettings):
with environment variables. with environment variables.
""" """
host: str = "127.0.0.1" host: str = "0.0.0.0"
port: int = 8000 port: int = 8000
# quantity of workers for uvicorn # quantity of workers for uvicorn
workers_count: int = 1 workers_count: int = 1

View File

@ -4,18 +4,17 @@ from fastapi import HTTPException
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from ..db.models.users import UserScheme from med_backend.auth import schemas, services
from . import schemas, services from med_backend.db.models.users import UserScheme
from .schemas import User
async def get_user_by_email(session: AsyncSession, email: str) -> User | None: async def get_user_by_email(session: AsyncSession, email: str) -> schemas.User | None:
r = await session.execute(select(UserScheme).where(UserScheme.email == email)) r = await session.execute(select(UserScheme).where(UserScheme.email == email))
user = r.scalars().first() user = r.scalars().first()
return user return user
async def get_user(session: AsyncSession, pk: int) -> User | None: async def get_user(session: AsyncSession, pk: int) -> schemas.User | None:
r = await session.execute(select(UserScheme).where(UserScheme.id == pk)) r = await session.execute(select(UserScheme).where(UserScheme.id == pk))
user = r.scalars().first() user = r.scalars().first()
return user return user
@ -25,7 +24,7 @@ async def get_users(
session: AsyncSession, session: AsyncSession,
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100,
) -> List[User] | None: ) -> List[schemas.User] | None:
r = await session.execute( r = await session.execute(
select(UserScheme) select(UserScheme)
.where(UserScheme.is_manager == False) .where(UserScheme.is_manager == False)

View File

@ -0,0 +1,42 @@
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from sqlalchemy.ext.asyncio import AsyncSession
from starlette import status
from med_backend.auth import schemas, services
from med_backend.db.dependencies import get_db_session
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
async def get_current_active_manager(
token: str = Depends(oauth2_scheme),
session: AsyncSession = Depends(get_db_session),
) -> schemas.User:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(
token,
services.SECRET_KEY,
algorithms=[services.JWT_ALGORITHM],
)
email: str = payload.get("sub")
if email is None:
raise credentials_exception
token_data = schemas.TokenData(email=email)
except JWTError:
raise credentials_exception
user = await services.get_user(session, token_data.email)
if user is None:
raise credentials_exception
if not user.is_manager:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="You are not allowed to access this info",
)
return user

View File

@ -1,11 +1,11 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from med_backend.auth import crud
from med_backend.auth.schemas import User from med_backend.auth.schemas import User
from med_backend.auth.services import get_current_active_manager
from med_backend.db.dependencies import get_db_session from med_backend.db.dependencies import get_db_session
from med_backend.users import crud
from med_backend.users.schemas import FullUser, ListUser from med_backend.users.schemas import FullUser, ListUser
from med_backend.users.services import get_current_active_manager
router = APIRouter() router = APIRouter()

View File

@ -1,4 +0,0 @@
"""Echo API."""
from med_backend.web.api.echo.views import router
__all__ = ["router"]

View File

@ -1,7 +0,0 @@
from pydantic import BaseModel
class Message(BaseModel):
"""Simple message model."""
message: str

View File

@ -1,18 +0,0 @@
from fastapi import APIRouter
from med_backend.web.api.echo.schema import Message
router = APIRouter()
@router.post("/", response_model=Message)
async def send_echo_message(
incoming_message: Message,
) -> Message:
"""
Sends echo back to user.
:param incoming_message: incoming message.
:returns: message same as the incoming.
"""
return incoming_message

View File

@ -1,4 +0,0 @@
"""API for checking project status."""
from med_backend.web.api.monitoring.views import router
__all__ = ["router"]

View File

@ -1,12 +0,0 @@
from fastapi import APIRouter
router = APIRouter()
@router.get("/health")
def health_check() -> None:
"""
Checks the health of a project.
It returns 200 if the project is healthy.
"""

View File

@ -1,4 +0,0 @@
"""Redis API."""
from med_backend.web.api.redis.views import router
__all__ = ["router"]

View File

@ -1,10 +0,0 @@
from typing import Optional
from pydantic import BaseModel
class RedisValueDTO(BaseModel):
"""DTO for redis values."""
key: str
value: Optional[str] # noqa: WPS110

View File

@ -1,44 +0,0 @@
from fastapi import APIRouter
from fastapi.param_functions import Depends
from redis.asyncio import ConnectionPool, Redis
from med_backend.services.redis.dependency import get_redis_pool
from med_backend.web.api.redis.schema import RedisValueDTO
router = APIRouter()
@router.get("/", response_model=RedisValueDTO)
async def get_redis_value(
key: str,
redis_pool: ConnectionPool = Depends(get_redis_pool),
) -> RedisValueDTO:
"""
Get value from redis.
:param key: redis key, to get data from.
:param redis_pool: redis connection pool.
:returns: information from redis.
"""
async with Redis(connection_pool=redis_pool) as redis:
redis_value = await redis.get(key)
return RedisValueDTO(
key=key,
value=redis_value,
)
@router.put("/")
async def set_redis_value(
redis_value: RedisValueDTO,
redis_pool: ConnectionPool = Depends(get_redis_pool),
) -> None:
"""
Set value in redis.
:param redis_value: new value data.
:param redis_pool: redis connection pool.
"""
if redis_value.value is not None:
async with Redis(connection_pool=redis_pool) as redis:
await redis.set(name=redis_value.key, value=redis_value.value)

View File

@ -1,10 +1,8 @@
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from med_backend import auth, users from med_backend import auth, forms, users
from med_backend.web.api import echo, monitoring
api_router = APIRouter() api_router = APIRouter()
api_router.include_router(monitoring.router)
api_router.include_router(echo.router, prefix="/echo", tags=["echo"])
api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
api_router.include_router(users.router, prefix="/users", tags=["users"]) api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(forms.router, prefix="/forms", tags=["forms"])