diff --git a/alembic/env.py b/alembic/env.py index 6176ca7..3f66b56 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -53,6 +53,7 @@ def run_migrations_offline() -> None: target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"}, + render_as_batch=True ) with context.begin_transaction(): diff --git a/alembic/script.py.mako b/alembic/script.py.mako index fbc4b07..83185a7 100644 --- a/alembic/script.py.mako +++ b/alembic/script.py.mako @@ -7,6 +7,7 @@ Create Date: ${create_date} """ from typing import Sequence, Union +import backend.db.migration_types from alembic import op import sqlalchemy as sa ${imports if imports else ""} diff --git a/alembic/versions/68d05d045e6e_create_user_table.py b/alembic/versions/4b90a6ac504b_create_users_table.py similarity index 88% rename from alembic/versions/68d05d045e6e_create_user_table.py rename to alembic/versions/4b90a6ac504b_create_users_table.py index f72f08b..9e5e5f1 100644 --- a/alembic/versions/68d05d045e6e_create_user_table.py +++ b/alembic/versions/4b90a6ac504b_create_users_table.py @@ -1,18 +1,19 @@ -"""create user table +"""create users table -Revision ID: 68d05d045e6e +Revision ID: 4b90a6ac504b Revises: -Create Date: 2024-05-24 04:12:25.599139 +Create Date: 2024-06-25 09:14:34.465698 """ from typing import Sequence, Union +import backend.db.migration_types from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision: str = '68d05d045e6e' +revision: str = '4b90a6ac504b' down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -21,7 +22,7 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table('users', - sa.Column('id', sa.UUID(), nullable=False), + sa.Column('id', backend.db.models.guid.GUID(), nullable=False), sa.Column('username', sa.String(), nullable=False), sa.Column('password', sa.String(), nullable=False), sa.Column('name', sa.String(), nullable=True), diff --git a/alembic/versions/7ef4aef5fcda_create_house_and_area_table.py b/alembic/versions/7ef4aef5fcda_create_house_and_area_table.py new file mode 100644 index 0000000..6eae2f2 --- /dev/null +++ b/alembic/versions/7ef4aef5fcda_create_house_and_area_table.py @@ -0,0 +1,70 @@ +"""create house and area table + +Revision ID: 7ef4aef5fcda +Revises: 4b90a6ac504b +Create Date: 2024-06-25 09:20:59.915212 + +""" +from typing import Sequence, Union + +import backend.db.migration_types +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '7ef4aef5fcda' +down_revision: Union[str, None] = '4b90a6ac504b' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('houses', + sa.Column('id', backend.db.models.guid.GUID(), nullable=False), + sa.Column('icon', sa.String(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('address', sa.String(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_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', + sa.Column('id', backend.db.models.guid.GUID(), nullable=False), + sa.Column('house_id', backend.db.models.guid.GUID(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + 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.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 ### + + +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/backend/app.py b/backend/app.py index 50393d5..c491ab7 100644 --- a/backend/app.py +++ b/backend/app.py @@ -15,7 +15,7 @@ settings = get_app_settings() logger = get_logger() description = f""" -fuware is a web application for managing your hours items and tracking them. +fuware is a web application for managing your house items and tracking them. """ @asynccontextmanager diff --git a/backend/db/migration_types.py b/backend/db/migration_types.py new file mode 100644 index 0000000..4e4f58b --- /dev/null +++ b/backend/db/migration_types.py @@ -0,0 +1 @@ +from backend.db.models.guid import GUID # noqa: F401 diff --git a/backend/db/models/__init__.py b/backend/db/models/__init__.py index 9917a30..27ef8ed 100644 --- a/backend/db/models/__init__.py +++ b/backend/db/models/__init__.py @@ -1 +1,2 @@ from .users import * +from .houses import * diff --git a/backend/db/models/guid.py b/backend/db/models/guid.py new file mode 100644 index 0000000..813bfdd --- /dev/null +++ b/backend/db/models/guid.py @@ -0,0 +1,56 @@ +import uuid +from typing import Any + +from sqlalchemy import Dialect +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.types import CHAR, TypeDecorator + + +class GUID(TypeDecorator): + """Platform-independent GUID type. + Uses PostgreSQL's UUID type, otherwise uses + CHAR(32), storing as stringified hex values. + """ + + impl = CHAR + cache_ok = True + + @staticmethod + def generate(): + return uuid.uuid4() + + @staticmethod + def convert_value_to_guid(value: Any, dialect: Dialect) -> str | None: + if value is None: + return value + elif dialect.name == "postgresql": + return str(value) + else: + if not isinstance(value, uuid.UUID): + return "%.32x" % uuid.UUID(value).int + else: + # hexstring + return "%.32x" % value.int + + def load_dialect_impl(self, dialect): + if dialect.name == "postgresql": + return dialect.type_descriptor(UUID()) + else: + return dialect.type_descriptor(CHAR(32)) + + def process_bind_param(self, value, dialect): + return self.convert_value_to_guid(value, dialect) + + def _uuid_value(self, value): + if value is None: + return value + else: + if not isinstance(value, uuid.UUID): + value = uuid.UUID(value) + return value + + def process_result_value(self, value, dialect): + return self._uuid_value(value) + + def sort_key_function(self, value): + return self._uuid_value(value) diff --git a/backend/db/models/houses/__init__.py b/backend/db/models/houses/__init__.py new file mode 100644 index 0000000..6c5960a --- /dev/null +++ b/backend/db/models/houses/__init__.py @@ -0,0 +1 @@ +from .houses import * diff --git a/backend/db/models/houses/houses.py b/backend/db/models/houses/houses.py new file mode 100644 index 0000000..b668a04 --- /dev/null +++ b/backend/db/models/houses/houses.py @@ -0,0 +1,33 @@ +from typing import List +from sqlalchemy import ForeignKey, String +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from backend.db.models.guid import GUID + +from .._model_base import SqlAlchemyBase + +class Houses(SqlAlchemyBase): + __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) + name: Mapped[str | None] = mapped_column(String, index=True, nullable=False) + address: Mapped[str | None] = mapped_column(String, index=True, nullable=False) + + areas: Mapped[List["Areas"]] = relationship() + + def __repr__(self): + return f"{self.__class__.__name__}, name: {self.name}" + +class Areas(SqlAlchemyBase): + __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) + name: Mapped[str | None] = mapped_column(String, index=True, nullable=False) + desc: Mapped[str | None] = mapped_column(String, index=True, nullable=False) + + user: 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 42aff88..28dde12 100644 --- a/backend/db/models/users/users.py +++ b/backend/db/models/users/users.py @@ -1,15 +1,14 @@ -from uuid import uuid4 -from sqlalchemy import Boolean, ForeignKey, String -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy import Boolean, String +from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy.dialects.postgresql import UUID +from backend.db.models.guid import GUID from .._model_base import SqlAlchemyBase -class User(SqlAlchemyBase): +class Users(SqlAlchemyBase): __tablename__ = 'users' - id: Mapped[UUID] = mapped_column(UUID, primary_key=True, default=uuid4, index=True) + id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate, index=True) username: Mapped[str | None] = mapped_column(String, unique=True, index=True, nullable=False) password: Mapped[str | None] = mapped_column(String, index=True, nullable=False) name: Mapped[str | None] = mapped_column(String, index=True, nullable=True) diff --git a/backend/repos/__init__.py b/backend/repos/__init__.py index bdb81bd..584a0fb 100644 --- a/backend/repos/__init__.py +++ b/backend/repos/__init__.py @@ -1 +1,2 @@ from .repository_users import * +from .repository_houses import * diff --git a/backend/repos/repository_houses.py b/backend/repos/repository_houses.py new file mode 100644 index 0000000..2008169 --- /dev/null +++ b/backend/repos/repository_houses.py @@ -0,0 +1,26 @@ +from backend.db.models.houses import Houses +from sqlalchemy.orm import Session + +from backend.schemas.house import HouseCreate + +class RepositoryHouses: + def __init__(self): + self.houses = Houses() + + def get_all(self, skip: int = 0, limit: int = 100): + return self.houses.query.offset(skip).limit(limit).all() + + 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()) + db.add(db_house) + db.commit() + except Exception: + db.rollback() + raise + + db.refresh(db_house) + return db_house diff --git a/backend/repos/repository_users.py b/backend/repos/repository_users.py index d2d15b0..1198e96 100644 --- a/backend/repos/repository_users.py +++ b/backend/repos/repository_users.py @@ -1,6 +1,6 @@ from backend.core.config import get_app_settings from backend.core.security.security import hash_password -from backend.db.models import User +from backend.db.models import Users from backend.schemas import UserCreate, UserProfile from sqlalchemy.orm import Session from uuid import UUID @@ -11,7 +11,7 @@ settings = get_app_settings() class RepositoryUsers: def __init__(self): - self.user = User() + self.user = Users() def get_all(self, skip: int = 0, limit: int = 100): return self.user.query.offset(skip).limit(limit).all() @@ -25,7 +25,7 @@ class RepositoryUsers: def create(self, db: Session, user: UserCreate | UserSeeds): try: password = getattr(user, "password") - db_user = User(**user.dict(exclude={"password"}), password=hash_password(password)) + db_user = Users(**user.dict(exclude={"password"}), password=hash_password(password)) db.add(db_user) db.commit() except Exception: @@ -41,7 +41,7 @@ class RepositoryUsers: return None try: user.update_password() - self.user.query.where(User.id == user_id).update(user.dict(exclude_unset=True, exclude_none=True)) + self.user.query.where(Users.id == user_id).update(user.dict(exclude_unset=True, exclude_none=True)) db.commit() except Exception: db.rollback() diff --git a/backend/routes/__init__.py b/backend/routes/__init__.py index 2b9470e..db5200a 100644 --- a/backend/routes/__init__.py +++ b/backend/routes/__init__.py @@ -1,9 +1,10 @@ from fastapi import APIRouter -from . import (auth, user) +from . import (auth, user, house) router = APIRouter(prefix='/api') router.include_router(auth.router) router.include_router(user.router) +router.include_router(house.router) diff --git a/backend/routes/house/__init__.py b/backend/routes/house/__init__.py new file mode 100644 index 0000000..3cccca0 --- /dev/null +++ b/backend/routes/house/__init__.py @@ -0,0 +1,7 @@ + +from fastapi import APIRouter +from . import house + +router = APIRouter(prefix='/house') + +router.include_router(house.public_router) diff --git a/backend/routes/house/house.py b/backend/routes/house/house.py new file mode 100644 index 0000000..dc050b1 --- /dev/null +++ b/backend/routes/house/house.py @@ -0,0 +1,21 @@ +from typing import Annotated, Any +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from backend.core.config import get_app_settings +from backend.core.dependencies import is_logged_in +from backend.db.db_setup import generate_session +from backend.schemas.common import ReturnValue +from backend.schemas.house import HouseCreate +from backend.schemas.user import ProfileResponse +from backend.services.house import HouseService + +public_router = APIRouter(tags=["Houses: Control"]) +house_service = HouseService() +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]) +def create_house(house: HouseCreate, db: db_dependency, current_user: current_user_token) -> ReturnValue[Any]: + return ReturnValue(status=200, data="created") diff --git a/backend/schemas/__init__.py b/backend/schemas/__init__.py index deffa8e..22447fc 100644 --- a/backend/schemas/__init__.py +++ b/backend/schemas/__init__.py @@ -1,2 +1,3 @@ from .common import * from .user import * +from .house import * diff --git a/backend/schemas/house/__init__.py b/backend/schemas/house/__init__.py new file mode 100644 index 0000000..cacc29c --- /dev/null +++ b/backend/schemas/house/__init__.py @@ -0,0 +1 @@ +from .house import * diff --git a/backend/schemas/house/house.py b/backend/schemas/house/house.py new file mode 100644 index 0000000..bd394c2 --- /dev/null +++ b/backend/schemas/house/house.py @@ -0,0 +1,17 @@ +from backend.schemas.main_model import MainModel + +class HouseBase(MainModel): + pass + +class AreaBase(MainModel): + pass + +class AreaCreate(AreaBase): + name: str + desc: str + +class HouseCreate(HouseBase): + icon: str + name: str + address: str + areas: list[AreaCreate] diff --git a/backend/services/house/__init__.py b/backend/services/house/__init__.py new file mode 100644 index 0000000..40f4d69 --- /dev/null +++ b/backend/services/house/__init__.py @@ -0,0 +1 @@ +from .house_service import * diff --git a/backend/services/house/house_service.py b/backend/services/house/house_service.py new file mode 100644 index 0000000..2022856 --- /dev/null +++ b/backend/services/house/house_service.py @@ -0,0 +1,11 @@ +from sqlalchemy.orm import Session +from backend.repos import RepositoryHouses +from backend.schemas import HouseCreate +from backend.services._base_service import BaseService + +class HouseService(BaseService): + def __init__(self): + self.repos = RepositoryHouses() + + def create(self, db: Session, house: HouseCreate): + return self.repos.create(db=db, house=house) diff --git a/backend/services/user/user_service.py b/backend/services/user/user_service.py index 5a968b7..103c5a3 100644 --- a/backend/services/user/user_service.py +++ b/backend/services/user/user_service.py @@ -31,7 +31,6 @@ class UserService(BaseService): return self.repos.create(db=db, user=user) def check_exist(self, user: UserRequest): - print(f"user: {user}") db_user = self.get_by_username(username=user.username) if not db_user: return False diff --git a/frontend/index.html b/frontend/index.html index edb83bc..ad3f3e7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -15,6 +15,7 @@
+