diff --git a/alembic/versions/7ef4aef5fcda_create_house_and_area_table.py b/alembic/versions/0fbca538155d_create_house_and_area_table.py similarity index 76% rename from alembic/versions/7ef4aef5fcda_create_house_and_area_table.py rename to alembic/versions/0fbca538155d_create_house_and_area_table.py index 6eae2f2..ccaeac4 100644 --- a/alembic/versions/7ef4aef5fcda_create_house_and_area_table.py +++ b/alembic/versions/0fbca538155d_create_house_and_area_table.py @@ -1,8 +1,8 @@ """create house and area table -Revision ID: 7ef4aef5fcda +Revision ID: 0fbca538155d Revises: 4b90a6ac504b -Create Date: 2024-06-25 09:20:59.915212 +Create Date: 2024-06-30 05:38:20.062935 """ from typing import Sequence, Union @@ -13,7 +13,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision: str = '7ef4aef5fcda' +revision: str = '0fbca538155d' down_revision: Union[str, None] = '4b90a6ac504b' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -28,11 +28,10 @@ def upgrade() -> None: sa.Column('address', sa.String(), nullable=False), sa.Column('created_at', sa.DateTime(), nullable=True), sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('deleted_at', sa.DateTime(), nullable=True), sa.PrimaryKeyConstraint('id') ) - op.create_index(op.f('ix_houses_address'), 'houses', ['address'], unique=False) op.create_index(op.f('ix_houses_created_at'), 'houses', ['created_at'], unique=False) - op.create_index(op.f('ix_houses_icon'), 'houses', ['icon'], unique=False) op.create_index(op.f('ix_houses_id'), 'houses', ['id'], unique=False) op.create_index(op.f('ix_houses_name'), 'houses', ['name'], unique=False) op.create_table('areas', @@ -42,12 +41,11 @@ def upgrade() -> None: sa.Column('desc', sa.String(), nullable=False), sa.Column('created_at', sa.DateTime(), nullable=True), sa.Column('updated_at', sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(['house_id'], ['houses.id'], ), + sa.Column('deleted_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['house_id'], ['houses.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_index(op.f('ix_areas_created_at'), 'areas', ['created_at'], unique=False) - op.create_index(op.f('ix_areas_desc'), 'areas', ['desc'], unique=False) - op.create_index(op.f('ix_areas_house_id'), 'areas', ['house_id'], unique=False) op.create_index(op.f('ix_areas_id'), 'areas', ['id'], unique=False) op.create_index(op.f('ix_areas_name'), 'areas', ['name'], unique=False) # ### end Alembic commands ### @@ -57,14 +55,10 @@ def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.drop_index(op.f('ix_areas_name'), table_name='areas') op.drop_index(op.f('ix_areas_id'), table_name='areas') - op.drop_index(op.f('ix_areas_house_id'), table_name='areas') - op.drop_index(op.f('ix_areas_desc'), table_name='areas') op.drop_index(op.f('ix_areas_created_at'), table_name='areas') op.drop_table('areas') op.drop_index(op.f('ix_houses_name'), table_name='houses') op.drop_index(op.f('ix_houses_id'), table_name='houses') - op.drop_index(op.f('ix_houses_icon'), table_name='houses') op.drop_index(op.f('ix_houses_created_at'), table_name='houses') - op.drop_index(op.f('ix_houses_address'), table_name='houses') op.drop_table('houses') # ### end Alembic commands ### diff --git a/alembic/versions/4b90a6ac504b_create_users_table.py b/alembic/versions/4b90a6ac504b_create_users_table.py index 9e5e5f1..2fe315f 100644 --- a/alembic/versions/4b90a6ac504b_create_users_table.py +++ b/alembic/versions/4b90a6ac504b_create_users_table.py @@ -1,7 +1,7 @@ """create users table Revision ID: 4b90a6ac504b -Revises: +Revises: Create Date: 2024-06-25 09:14:34.465698 """ @@ -27,7 +27,7 @@ def upgrade() -> None: sa.Column('password', sa.String(), nullable=False), sa.Column('name', sa.String(), nullable=True), sa.Column('is_admin', sa.Boolean(), nullable=True), - sa.Column('is_lock', sa.Boolean(), nullable=True), + sa.Column('is_lock', sa.DateTime(), nullable=True), sa.Column('created_at', sa.DateTime(), nullable=True), sa.Column('updated_at', sa.DateTime(), nullable=True), sa.PrimaryKeyConstraint('id') diff --git a/backend/app.py b/backend/app.py index c491ab7..3ed125a 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,6 +1,7 @@ from collections.abc import AsyncGenerator from contextlib import asynccontextmanager from fastapi import FastAPI, Request, HTTPException +from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware @@ -70,6 +71,11 @@ async def unicorn_exception_handler(request: Request, exc: HTTPException): content={"status": exc.status_code, "data": exc.detail}, ) +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + print(exc) + return JSONResponse(status_code=422, content={"status": 422, "data": str(exc)}) + def api_routers(): app.include_router(router) diff --git a/backend/core/dependencies/dependencies.py b/backend/core/dependencies/dependencies.py index ab21bf5..2aa3cf6 100644 --- a/backend/core/dependencies/dependencies.py +++ b/backend/core/dependencies/dependencies.py @@ -30,7 +30,7 @@ async def is_logged_in(token: str = Depends(oauth2_scheme_soft_fail)) -> bool: user = user_service.get_by_id(user_id) if not user: raise credentials_exception - if user.is_lock is True: + if user.is_lock is not None: raise HTTPException(status_code=status.HTTP_423_LOCKED, detail=MessageCode.ACCOUNT_LOCK) except Exception: return credentials_exception diff --git a/backend/core/message_code.py b/backend/core/message_code.py index 11abb5a..1f0eb8c 100644 --- a/backend/core/message_code.py +++ b/backend/core/message_code.py @@ -1,6 +1,9 @@ class MessageCode(): + CREATE_USER_SUCCESS: str = 'CREATE_USER_SUCCESS' CREATED_USER: str = 'CREATED_USER' WRONG_INPUT: str = 'LOGIN_WRONG' ACCOUNT_LOCK: str = 'USER_LOCK' REFRESH_TOKEN_EXPIRED: str = 'REFRESH_TOKEN_EXPIRED' + CREATE_HOUSE_FAIL: str = 'CREATE_HOUSE_FAIL' + CREATE_HOUSE_SUCCESS: str = 'CREATE_HOUSE_SUCCESS' diff --git a/backend/db/models/_model_base.py b/backend/db/models/_model_base.py index d89b830..fca5bc7 100644 --- a/backend/db/models/_model_base.py +++ b/backend/db/models/_model_base.py @@ -18,3 +18,6 @@ class SqlAlchemyBase(Model): @classmethod def normalize(cls, val: str) -> str: return unidecode(val).lower().strip() + +class DeleteMixin: + deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) diff --git a/backend/db/models/houses/houses.py b/backend/db/models/houses/houses.py index b668a04..4f3b099 100644 --- a/backend/db/models/houses/houses.py +++ b/backend/db/models/houses/houses.py @@ -4,30 +4,30 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship from backend.db.models.guid import GUID -from .._model_base import SqlAlchemyBase +from .._model_base import SqlAlchemyBase, DeleteMixin -class Houses(SqlAlchemyBase): +class Houses(SqlAlchemyBase, DeleteMixin): __tablename__ = 'houses' id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate, index=True) - icon: Mapped[str | None] = mapped_column(String, index=True, nullable=False) + icon: Mapped[str | None] = mapped_column(String, nullable=False) name: Mapped[str | None] = mapped_column(String, index=True, nullable=False) - address: Mapped[str | None] = mapped_column(String, index=True, nullable=False) + address: Mapped[str | None] = mapped_column(String, nullable=False) - areas: Mapped[List["Areas"]] = relationship() + areas: Mapped[List["Areas"]] = relationship("Areas", back_populates="house", cascade="all, delete", passive_deletes=True) def __repr__(self): return f"{self.__class__.__name__}, name: {self.name}" -class Areas(SqlAlchemyBase): +class Areas(SqlAlchemyBase, DeleteMixin): __tablename__ = 'areas' id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate, index=True) - house_id: Mapped[GUID] = mapped_column(GUID, ForeignKey('houses.id'), index=True, nullable=False) + house_id: Mapped[GUID] = mapped_column(GUID, ForeignKey('houses.id', ondelete='CASCADE'), nullable=False) name: Mapped[str | None] = mapped_column(String, index=True, nullable=False) - desc: Mapped[str | None] = mapped_column(String, index=True, nullable=False) + desc: Mapped[str | None] = mapped_column(String, nullable=False) - user: Mapped['Houses'] = relationship(back_populates="areas") + house: Mapped['Houses'] = relationship(back_populates="areas") def __repr__(self): return f"{self.__class__.__name__}, name: {self.name}" diff --git a/backend/db/models/users/users.py b/backend/db/models/users/users.py index 28dde12..3862947 100644 --- a/backend/db/models/users/users.py +++ b/backend/db/models/users/users.py @@ -1,4 +1,6 @@ -from sqlalchemy import Boolean, String +from datetime import datetime + +from sqlalchemy import Boolean, String, DateTime from sqlalchemy.orm import Mapped, mapped_column from backend.db.models.guid import GUID @@ -13,7 +15,7 @@ class Users(SqlAlchemyBase): password: Mapped[str | None] = mapped_column(String, index=True, nullable=False) name: Mapped[str | None] = mapped_column(String, index=True, nullable=True) is_admin: Mapped[bool | None] = mapped_column(Boolean, default=False) - is_lock: Mapped[bool | None] = mapped_column(Boolean, default=False) + is_lock: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) def __repr__(self): return f"{self.__class__.__name__}, name: {self.name}, username: {self.username}" diff --git a/backend/repos/repository_houses.py b/backend/repos/repository_houses.py index 2008169..1381304 100644 --- a/backend/repos/repository_houses.py +++ b/backend/repos/repository_houses.py @@ -1,4 +1,4 @@ -from backend.db.models.houses import Houses +from backend.db.models.houses import Houses, Areas from sqlalchemy.orm import Session from backend.schemas.house import HouseCreate @@ -6,16 +6,26 @@ from backend.schemas.house import HouseCreate class RepositoryHouses: def __init__(self): self.houses = Houses() + self.areas = Areas() def get_all(self, skip: int = 0, limit: int = 100): - return self.houses.query.offset(skip).limit(limit).all() + return self.houses.query.filter_by(deleted_at=None).offset(skip).limit(limit).all() + + def get_all_areas(self, house_id: str, skip: int = 0, limit: int = 100): + return self.areas.query.filter_by(deleted_at=None, house_id=house_id).offset(skip).limit(limit).all() + + def get_count_all(self, skip: int = 0, limit: int = 100): + return self.houses.query.filter_by(deleted_at=None).offset(skip).limit(limit).count() def get_by_id(self, house_id: str): return self.houses.query.filter_by(id=house_id).one() def create(self, db: Session, house: HouseCreate): try: - db_house = Houses(**house.dict()) + areas = getattr(house, "areas") + db_house = Houses(**house.dict(exclude={"areas"})) + for area in areas: + db_house.areas.append(Areas(**area.dict())) db.add(db_house) db.commit() except Exception: diff --git a/backend/repos/seeder/init_users.py b/backend/repos/seeder/init_users.py index 46e7e80..b21f6c6 100644 --- a/backend/repos/seeder/init_users.py +++ b/backend/repos/seeder/init_users.py @@ -16,21 +16,18 @@ def dev_users() -> list[dict]: "password": "admin", "name": "Sam", "is_admin": True, - "is_lock": False, }, { "username": "duy", "password": "admin", "name": "Duy", "is_admin": True, - "is_lock": False, }, { "username": "sam1", "password": "admin", "name": "Sam1", "is_admin": False, - "is_lock": False, }, ] diff --git a/backend/routes/house/house.py b/backend/routes/house/house.py index dc050b1..102f844 100644 --- a/backend/routes/house/house.py +++ b/backend/routes/house/house.py @@ -1,11 +1,13 @@ from typing import Annotated, Any -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from backend.core.config import get_app_settings from backend.core.dependencies import is_logged_in +from backend.core.message_code import MessageCode from backend.db.db_setup import generate_session from backend.schemas.common import ReturnValue from backend.schemas.house import HouseCreate +from backend.schemas.house.house import HousesListResponse from backend.schemas.user import ProfileResponse from backend.services.house import HouseService @@ -16,6 +18,16 @@ settings = get_app_settings() db_dependency = Annotated[Session, Depends(generate_session)] current_user_token = Annotated[ProfileResponse, Depends(is_logged_in)] -@public_router.get("/create", response_model=ReturnValue[Any]) +@public_router.post("/create", response_model=ReturnValue[Any]) def create_house(house: HouseCreate, db: db_dependency, current_user: current_user_token) -> ReturnValue[Any]: - return ReturnValue(status=200, data="created") + try: + house_service.create(db=db, house=house) + except Exception: + raise HTTPException(status_code=400, detail=MessageCode.CREATE_HOUSE_FAIL) + return ReturnValue(status=200, data=MessageCode.CREATE_HOUSE_SUCCESS) + +@public_router.get("/all", response_model=ReturnValue[HousesListResponse]) +async def get_all_house(page: int, pageSize: int, current_user: current_user_token) -> ReturnValue[HousesListResponse]: + housesCount = house_service.get_all_count(skip=page-1, limit=pageSize) + houses = house_service.get_all(skip=page-1, limit=pageSize) + return ReturnValue(status=200, data={'total': housesCount, 'list': list(houses)}) diff --git a/backend/routes/user/user.py b/backend/routes/user/user.py index 93ec7bf..22372b2 100644 --- a/backend/routes/user/user.py +++ b/backend/routes/user/user.py @@ -23,7 +23,7 @@ def register_user(user: UserCreate, db: db_dependency) -> ReturnValue[Any]: if db_user: raise HTTPException(status_code=400, detail=MessageCode.CREATED_USER) user_service.create(db=db, user=user) - return ReturnValue(status=200, data="created") + return ReturnValue(status=200, data=MessageCode.CREATE_USER_SUCCESS) @public_router.get("/me", response_model=ReturnValue[ProfileResponse]) def get_user(current_user: current_user_token) -> ReturnValue[Any]: diff --git a/backend/schemas/house/house.py b/backend/schemas/house/house.py index bd394c2..68e71a1 100644 --- a/backend/schemas/house/house.py +++ b/backend/schemas/house/house.py @@ -1,3 +1,7 @@ +from datetime import datetime +from uuid import UUID + +from pydantic import ConfigDict from backend.schemas.main_model import MainModel class HouseBase(MainModel): @@ -15,3 +19,14 @@ class HouseCreate(HouseBase): name: str address: str areas: list[AreaCreate] + model_config = ConfigDict(from_attributes=True) + +class HousesList(HouseCreate): + id: UUID + created_at: datetime + updated_at: datetime + deleted_at: datetime | None + +class HousesListResponse(MainModel): + total: int + list: list[HousesList] diff --git a/backend/schemas/user/user.py b/backend/schemas/user/user.py index 5a606e7..6ed3cd9 100644 --- a/backend/schemas/user/user.py +++ b/backend/schemas/user/user.py @@ -27,13 +27,12 @@ class UserProfile(MainModel): class UserSeeds(UserCreate): is_admin: bool - is_lock: bool class PrivateUser(UserBase): id: UUID name: str is_admin: bool - is_lock: bool + is_lock: datetime created_at: datetime updated_at: datetime model_config = ConfigDict(from_attributes=True) diff --git a/backend/services/house/house_service.py b/backend/services/house/house_service.py index 2022856..c5d4c13 100644 --- a/backend/services/house/house_service.py +++ b/backend/services/house/house_service.py @@ -9,3 +9,9 @@ class HouseService(BaseService): def create(self, db: Session, house: HouseCreate): return self.repos.create(db=db, house=house) + + def get_all(self, skip: int = 0, limit: int = 100): + return self.repos.get_all(skip=skip, limit=limit) + + def get_all_count(self, skip: int = 0, limit: int = 100): + return self.repos.get_count_all(skip=skip, limit=limit) diff --git a/frontend/src/api/house.js b/frontend/src/api/house.js new file mode 100644 index 0000000..4318421 --- /dev/null +++ b/frontend/src/api/house.js @@ -0,0 +1,16 @@ +import { protocol } from './index' +import { GET_HOUSES_LIST, POST_HOUSE_CREATE } from './url' + +export const postCreateHouse = (payload) => { + return protocol.post(POST_HOUSE_CREATE, payload) +} + +export const getAllHouse = ({ page, pageSize }) => { + return protocol.get( + GET_HOUSES_LIST({ + page: page(), + pageSize: pageSize(), + }), + {}, + ) +} diff --git a/frontend/src/api/url.js b/frontend/src/api/url.js index d2402e0..cbded8a 100644 --- a/frontend/src/api/url.js +++ b/frontend/src/api/url.js @@ -3,3 +3,6 @@ export const POST_LOGOUT = '/api/auth/logout' export const POST_REFRESH = '/api/auth/refresh' export const GET_USER_PROFILE = '/api/user/me' export const PUT_UPDATE_USER_PROFILE = '/api/user/update-profile' +export const POST_HOUSE_CREATE = '/api/house/create' +export const GET_HOUSES_LIST = ({ page, pageSize }) => + `/api/house/all?page=${page}&pageSize=${pageSize}` diff --git a/frontend/src/components/AreaAdd/AreaAdd.jsx b/frontend/src/components/AreaAdd/AreaAdd.jsx index 4af99e5..50ab582 100644 --- a/frontend/src/components/AreaAdd/AreaAdd.jsx +++ b/frontend/src/components/AreaAdd/AreaAdd.jsx @@ -4,7 +4,6 @@ import Textarea from '@components/common/Textarea' import { createForm } from '@felte/solid' import { validator } from '@felte/validator-yup' import useLanguage from '@hooks/useLanguage' -import useToast from '@hooks/useToast' import { IconAddressBook, IconCirclePlus, @@ -33,7 +32,6 @@ export default function AreaAdd(props) { const [openModal, setOpenModal] = createSignal(false) const [data, setData] = createSignal([]) const { language, isRequired } = useLanguage() - const notify = useToast() const { form, reset, errors } = createForm({ extend: [validator({ schema: areaSchema(language, isRequired) })], onSubmit: async (values) => { @@ -54,8 +52,6 @@ export default function AreaAdd(props) { setData((prev) => [...prev.slice(0, index), ...prev.slice(index + 1)]) } - // console.log(error()) - return (
{props.description}