diff --git a/alembic/versions/20230823-110844_add_icon_and_description_to_poi_type__76752a74a304.py b/alembic/versions/20230823-110844_add_icon_and_description_to_poi_type__76752a74a304.py new file mode 100644 index 0000000..2f70ba5 --- /dev/null +++ b/alembic/versions/20230823-110844_add_icon_and_description_to_poi_type__76752a74a304.py @@ -0,0 +1,30 @@ +"""add icon and description to poi type and description to poi + +Revision ID: 76752a74a304 +Revises: 5c43394aa8e0 +Create Date: 2023-08-23 11:08:44.313882 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '76752a74a304' +down_revision = '5c43394aa8e0' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('point_of_interest', sa.Column('description', sa.Text(), nullable=False)) + op.add_column('point_of_interest_type', sa.Column('icon', sa.String(length=128), server_default='marker', nullable=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('point_of_interest_type', 'icon') + op.drop_column('point_of_interest', 'description') + # ### end Alembic commands ### diff --git a/src/database/models.py b/src/database/models.py index ce540bb..afc4d26 100644 --- a/src/database/models.py +++ b/src/database/models.py @@ -81,12 +81,14 @@ class PointOfInterestType(BaseModel): id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(128), nullable=False) + icon: Mapped[str] = mapped_column(String(128), nullable=False, server_default='marker') is_public: Mapped[bool] = mapped_column(Boolean, server_default='0') created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id')) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0') created_by: Mapped['User'] = relationship() + points_of_interest: Mapped[List[PointOfInterest]] = relationship() class PointOfInterest(BaseModel): @@ -94,6 +96,7 @@ class PointOfInterest(BaseModel): id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(128), nullable=False) + description: Mapped[str] = mapped_column(Text, nullable=False) gps_latitude: Mapped[float] = mapped_column(Float, nullable=True) gps_longitude: Mapped[float] = mapped_column(Float, nullable=True) type_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest_type.id"), nullable=True) diff --git a/src/graphql_schema/dataloaders/flight.py b/src/graphql_schema/dataloaders/flight.py index 87c7071..c7560dc 100644 --- a/src/graphql_schema/dataloaders/flight.py +++ b/src/graphql_schema/dataloaders/flight.py @@ -3,7 +3,7 @@ from typing import List, Optional from sqlalchemy import select from strawberry.dataloader import DataLoader from database import async_session -from database.models import Flight, Copilot +from database.models import Flight, Copilot, PointOfInterest class FlightsLoader: @@ -36,3 +36,5 @@ class FlightsLoader: flights_by_copilot_dataloader = DataLoader(load_fn=FlightsLoader(Copilot.id, extra_join=[Flight.copilots]).load, cache=False) flights_by_aircraft_dataloader = DataLoader(load_fn=FlightsLoader(Flight.aircraft_id).load, cache=False) + +flight_by_poi_dataloader = DataLoader(load_fn=FlightsLoader(PointOfInterest.id, extra_join=[Flight.track, PointOfInterest]).load, cache=False) \ No newline at end of file diff --git a/src/graphql_schema/dataloaders/poi.py b/src/graphql_schema/dataloaders/poi.py index 3d8ede1..1559123 100644 --- a/src/graphql_schema/dataloaders/poi.py +++ b/src/graphql_schema/dataloaders/poi.py @@ -3,7 +3,7 @@ from typing import List from sqlalchemy import select from strawberry.dataloader import DataLoader from database import async_session -from database.models import PointOfInterest, FlightTrack +from database.models import PointOfInterest, FlightTrack, PointOfInterestType async def load_flight_track(flight_ids: List[int]): @@ -33,4 +33,13 @@ async def load_poi(ids: List[int]): return [models_by_id.get(id_) for id_ in ids] +async def load_poi_type(ids: List[int]): + async with async_session() as session: + models = (await session.scalars(select(PointOfInterestType).filter(PointOfInterestType.id.in_(ids)))).all() + + models_by_id = {model.id: model for model in models} + return [models_by_id.get(id_) for id_ in ids] + + poi_dataloader = DataLoader(load_fn=load_poi, cache=False) +poi_type_dataloader = DataLoader(load_fn=load_poi_type, cache=False) diff --git a/src/graphql_schema/entities/airport.py b/src/graphql_schema/entities/airport.py index fd66486..e06929a 100644 --- a/src/graphql_schema/entities/airport.py +++ b/src/graphql_schema/entities/airport.py @@ -1,7 +1,8 @@ from typing import List import strawberry -from sqlalchemy import select +from sqlalchemy import select, or_ from database import models +from decorators.endpoints import authenticated_user_only from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type @@ -14,6 +15,10 @@ def get_base_query(user_id: int): return ( select(models.Airport) .filter(models.Airport.deleted.is_(False)) + .filter(or_( + models.Airport.created_by_id == user_id, + models.Airport.created_by_id.is_(None), + )) ) @@ -21,6 +26,7 @@ def get_base_query(user_id: int): class AirportQueries: @strawberry.field + @authenticated_user_only() async def airports(root, info) -> List[Airport]: query = ( get_base_query(info.context.user_id) @@ -30,6 +36,7 @@ class AirportQueries: return (await info.context.db.scalars(query)).all() @strawberry.field + @authenticated_user_only() async def airport(root, info, id: int) -> Airport: query = ( get_base_query(info.context.user_id) diff --git a/src/graphql_schema/entities/poi.py b/src/graphql_schema/entities/poi.py index 4ac2f65..5ce44fa 100644 --- a/src/graphql_schema/entities/poi.py +++ b/src/graphql_schema/entities/poi.py @@ -1,19 +1,35 @@ -from typing import List +from typing import List, Optional, TYPE_CHECKING, Annotated import strawberry from sqlalchemy import select, or_ from database import models from decorators.endpoints import authenticated_user_only +from graphql_schema.dataloaders.flight import flight_by_poi_dataloader from graphql_schema.dataloaders.photos import poi_photos_dataloader +from graphql_schema.dataloaders.poi import poi_type_dataloader +from graphql_schema.entities.helpers.flight import handle_combobox_save from graphql_schema.entities.photo import Photo +from graphql_schema.entities.poi_type import PointOfInterestType from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input +from graphql_schema.types import ComboboxInput +if TYPE_CHECKING: + from .flight import Flight + @strawberry_sqlalchemy_type(models.PointOfInterest) class PointOfInterest: async def load_photos(root): return await poi_photos_dataloader.load(root.id) + async def load_type(root): + return await poi_type_dataloader.load(root.type_id) + + async def load_flights(root): + return await flight_by_poi_dataloader.load(root.id) + + type: Optional[PointOfInterestType] = strawberry.field(resolver=load_type) photos: List[Photo] = strawberry.field(resolver=load_photos) + flights: List[Annotated["Flight", strawberry.lazy('.flight')]] = strawberry.field(resolver=load_flights) def get_base_query(user_id: int, only_my: bool = False): @@ -36,7 +52,7 @@ def get_base_query(user_id: int, only_my: bool = False): @strawberry.type class PointOfInterestQueries: - @strawberry.field + @strawberry.field() @authenticated_user_only() async def points_of_interest(root, info) -> List[PointOfInterest]: query = ( @@ -46,7 +62,7 @@ class PointOfInterestQueries: return (await info.context.db.scalars(query)).all() - @strawberry.field + @strawberry.field() @authenticated_user_only() async def point_of_interest(root, info, id: int) -> PointOfInterest: query = ( @@ -56,16 +72,25 @@ class PointOfInterestQueries: return (await info.context.db.scalars(query)).one() + @strawberry.type class CreatePointOfInterestMutation: - @strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id']) + @strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id']) class CreatePointOfInterestInput: - pass + type: Optional[ComboboxInput] = None @strawberry.mutation @authenticated_user_only() async def create_point_of_interest(root, info, input: CreatePointOfInterestInput) -> PointOfInterest: input_data = input.to_dict() + + input_data['type_id'] = await handle_combobox_save( + info.context.db, + models.PointOfInterestType, + input.type, + info.context.user_id + ) + return await models.PointOfInterest.create( info.context.db, data=dict( @@ -77,22 +102,30 @@ class CreatePointOfInterestMutation: @strawberry.type class EditPointOfInterestMutation: - @strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id']) + @strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id']) class EditPointOfInterestInput: - pass + type: Optional[ComboboxInput] = None @strawberry.mutation @authenticated_user_only() async def edit_point_of_interest(root, info, id: int, input: EditPointOfInterestInput) -> PointOfInterest: # TODO: kontrola organizace - # TODO: kontrola opravneni na akci + input_data = input.to_dict() + + if input.type is not None: + input_data['type_id'] = await handle_combobox_save( + info.context.db, + models.PointOfInterestType, + input.type, + info.context.user_id + ) poi = ( await info.context.db.scalars( get_base_query(info.context.user_id, only_my=True) .filter(models.PointOfInterest.id == id)) ).one() - return await models.PointOfInterest.update(info.context.db, obj=poi, data=input.to_dict()) + return await models.PointOfInterest.update(info.context.db, obj=poi, data=input_data) @strawberry.type diff --git a/src/graphql_schema/entities/poi_type.py b/src/graphql_schema/entities/poi_type.py new file mode 100644 index 0000000..ed45d40 --- /dev/null +++ b/src/graphql_schema/entities/poi_type.py @@ -0,0 +1,118 @@ +from typing import List, Optional +import strawberry +from sqlalchemy import select, or_ +from database import models +from decorators.endpoints import authenticated_user_only +from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type +from graphql_schema.types import ComboboxInput + + +@strawberry_sqlalchemy_type(models.PointOfInterestType) +class PointOfInterestType: + pass + + +def get_base_query(user_id: int, only_my: bool = False): + query = ( + select(models.PointOfInterestType) + .filter(models.PointOfInterestType.deleted.is_(False)) + ) + + if only_my: + query = query.filter(models.PointOfInterestType.created_by_id == user_id) + else: + query = query.filter(or_( + models.PointOfInterestType.created_by_id == user_id, + models.PointOfInterestType.is_public.is_(True) + )) + + return query + + +@strawberry.type +class PointOfInterestTypeQueries: + + @strawberry.field() + @authenticated_user_only() + async def point_of_interest_types(root, info) -> List[PointOfInterestType]: + query = ( + get_base_query(info.context.user_id, only_my=False) + .order_by(models.PointOfInterestType.id.desc()) + ) + + return (await info.context.db.scalars(query)).all() + + @strawberry.field() + @authenticated_user_only() + async def point_of_interest_type(root, info, id: int) -> PointOfInterestType: + query = ( + get_base_query(info.context.user_id) + .filter(models.PointOfInterestType.id == id) + ) + return (await info.context.db.scalars(query)).one() + +# +# @strawberry.type +# class CreatePointOfInterestMutation: +# @strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id']) +# class CreatePointOfInterestInput: +# type: # Optional[ComboboxInput] = None +# +# @strawberry.mutation +# @authenticated_user_only() +# async def create_point_of_interest(root, info, input: CreatePointOfInterestInput) -> PointOfInterest: +# input_data = input.to_dict() +# +# input_data['type_id'] = await handle_combobox_save( +# info.context.db, +# models.PointOfInterestType, +# input.type, +# info.context.user_id +# ) +# +# return await models.PointOfInterest.create( +# info.context.db, +# data=dict( +# **input_data, +# created_by_id=info.context.user_id, +# ) +# ) +# +# +# @strawberry.type +# class EditPointOfInterestMutation: +# @strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id']) +# class EditPointOfInterestInput: +# type: Optional[ComboboxInput] = None +# +# @strawberry.mutation +# @authenticated_user_only() +# async def edit_point_of_interest(root, info, id: int, input: EditPointOfInterestInput) -> PointOfInterest: +# # TODO: kontrola organizace +# input_data = input.to_dict() +# +# if 'type' in input: +# input_data['type_id'] = await handle_combobox_save( +# info.context.db, +# models.PointOfInterestType, +# input.type, +# info.context.user_id +# ) +# +# poi = ( +# await info.context.db.scalars( +# get_base_query(info.context.user_id, only_my=True) +# .filter(models.PointOfInterest.id == id)) +# ).one() +# return await models.PointOfInterest.update(info.context.db, obj=poi, data=input_data) +# +# +# @strawberry.type +# class DeletePointOfInterestMutation: +# +# @strawberry.mutation +# @authenticated_user_only() +# async def delete_point_of_interest(self, info, id: int) -> PointOfInterest: +# poi = get_base_query(info.context.user_id, only_my=True).filter(models.PointOfInterest.id == id).one() +# +# return await models.PointOfInterest.update(info.context.db, obj=poi, data=dict(deleted=True)) diff --git a/src/graphql_schema/query.py b/src/graphql_schema/query.py index 4192722..dda0a3e 100644 --- a/src/graphql_schema/query.py +++ b/src/graphql_schema/query.py @@ -4,6 +4,7 @@ from .entities.airport import AirportQueries from .entities.copilot import CopilotQueries from .entities.flight import FlightQueries from .entities.poi import PointOfInterestQueries +from .entities.poi_type import PointOfInterestTypeQueries from .entities.user import UserQueries # https://github.com/strawberry-graphql/examples/blob/main/fastapi-sqlalchemy/api/schema.py @@ -16,4 +17,5 @@ Query = merge_types('Query', ( CopilotQueries, UserQueries, PointOfInterestQueries, + PointOfInterestTypeQueries, ))