diff --git a/alembic/versions/20231128-163256_add_photos_to_aircraft_poi_and_copilots_91a9608b509d.py b/alembic/versions/20231128-163256_add_photos_to_aircraft_poi_and_copilots_91a9608b509d.py new file mode 100644 index 0000000..c00d17d --- /dev/null +++ b/alembic/versions/20231128-163256_add_photos_to_aircraft_poi_and_copilots_91a9608b509d.py @@ -0,0 +1,50 @@ +"""add photos to aircraft, poi and copilots + +Revision ID: 91a9608b509d +Revises: d42172840f6c +Create Date: 2023-11-28 16:32:56.725312 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '91a9608b509d' +down_revision = 'd42172840f6c' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('copilot_has_photo', + sa.Column('copilot_id', sa.Integer(), nullable=False), + sa.Column('photo_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['copilot_id'], ['copilot.id'], ), + sa.ForeignKeyConstraint(['photo_id'], ['photo.id'], ), + sa.PrimaryKeyConstraint('copilot_id', 'photo_id') + ) + op.add_column('aircraft', sa.Column('title_photo_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'aircraft', 'photo', ['title_photo_id'], ['id']) + op.add_column('flight', sa.Column('title_photo_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'flight', 'photo', ['title_photo_id'], ['id']) + op.add_column('photo', sa.Column('aircraft_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'photo', 'aircraft', ['aircraft_id'], ['id']) + op.add_column('point_of_interest', sa.Column('title_photo_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'point_of_interest', 'photo', ['title_photo_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'point_of_interest', type_='foreignkey') + op.drop_column('point_of_interest', 'title_photo_id') + op.drop_constraint(None, 'photo', type_='foreignkey') + op.drop_column('photo', 'aircraft_id') + op.drop_constraint(None, 'flight', type_='foreignkey') + op.drop_column('flight', 'title_photo_id') + op.drop_constraint(None, 'aircraft', type_='foreignkey') + op.drop_column('aircraft', 'title_photo_id') + op.drop_table('copilot_has_photo') + # ### end Alembic commands ### diff --git a/alembic/versions/20231202-121425_add_is_public_to_aircraft_e5682ac4355c.py b/alembic/versions/20231202-121425_add_is_public_to_aircraft_e5682ac4355c.py new file mode 100644 index 0000000..bb1ba84 --- /dev/null +++ b/alembic/versions/20231202-121425_add_is_public_to_aircraft_e5682ac4355c.py @@ -0,0 +1,28 @@ +"""add is_public to aircraft + +Revision ID: e5682ac4355c +Revises: 91a9608b509d +Create Date: 2023-12-02 12:14:25.055308 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'e5682ac4355c' +down_revision = '91a9608b509d' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('aircraft', sa.Column('is_public', sa.Boolean(), server_default='0', nullable=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('aircraft', 'is_public') + # ### end Alembic commands ### diff --git a/alembic/versions/20231202-130546_add_title_photo_to_copilot_022bf6fbcd47.py b/alembic/versions/20231202-130546_add_title_photo_to_copilot_022bf6fbcd47.py new file mode 100644 index 0000000..3d4b4d1 --- /dev/null +++ b/alembic/versions/20231202-130546_add_title_photo_to_copilot_022bf6fbcd47.py @@ -0,0 +1,30 @@ +"""add title photo to copilot + +Revision ID: 022bf6fbcd47 +Revises: e5682ac4355c +Create Date: 2023-12-02 13:05:46.267006 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '022bf6fbcd47' +down_revision = 'e5682ac4355c' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('copilot', sa.Column('title_photo_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'copilot', 'photo', ['title_photo_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'copilot', type_='foreignkey') + op.drop_column('copilot', 'title_photo_id') + # ### end Alembic commands ### diff --git a/docker-compose.yml b/docker-compose.yml index 1bb95cf..2ff6426 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,7 @@ services: TZ: "Europe/Prague" API_URL: "http://localhost:8000" APP_ENV: "development" - ALLOW_CORS_ORIGINS: "http://localhost:9000 http://localhost:9001 http://localhost:3000" + ALLOW_CORS_ORIGINS: "http://localhost:9000 http://localhost:9001 http://localhost:9101 http://localhost:3000" SENTRY_DSN: "https://184d1d6bd39f4fbb94804ae84e9afdd5@o472821.ingest.sentry.io/5506983" diff --git a/src/database/models.py b/src/database/models.py index a17b6d8..5e55697 100644 --- a/src/database/models.py +++ b/src/database/models.py @@ -57,6 +57,13 @@ flight_has_copilot = Table( Column("copilot_id", ForeignKey("copilot.id"), primary_key=True), ) +copilot_has_photo = Table( + "copilot_has_photo", + BaseModel.metadata, + Column("copilot_id", ForeignKey("copilot.id"), primary_key=True), + Column("photo_id", ForeignKey("photo.id"), primary_key=True), +) + class Airport(BaseModel): __tablename__ = "airport" @@ -72,7 +79,7 @@ class Airport(BaseModel): created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=True) # automaticky import nebude mit ID # noqa deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0') - metars: Mapped['Metar'] = relationship(back_populates="airport") + metars: Mapped['Metar'] = relationship() created_by: Mapped['User'] = relationship() @@ -97,6 +104,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) + title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True) 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) @@ -105,9 +113,9 @@ class PointOfInterest(BaseModel): created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0') - photos: Mapped[List[Photo]] = relationship() type: Mapped[PointOfInterestType] = relationship() created_by: Mapped['User'] = relationship() + title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id]) class Photo(BaseModel): @@ -116,7 +124,7 @@ class Photo(BaseModel): id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(128), nullable=False) filename: Mapped[str] = mapped_column(String(128), nullable=False) - is_flight_cover: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="0") + is_flight_cover: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="0") # TODO: odstranit description: Mapped[str] = mapped_column(Text, nullable=False) width: Mapped[int] = mapped_column(Integer, nullable=False) height: Mapped[int] = mapped_column(Integer, nullable=False) @@ -125,15 +133,18 @@ class Photo(BaseModel): gps_longitude: Mapped[float] = mapped_column(Float, nullable=True) gps_altitude: Mapped[float] = mapped_column(Float, nullable=True) terrain_elevation: Mapped[float] = mapped_column(Float, nullable=True) + aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=True) point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True) flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False) created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id')) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) flight: Mapped['Flight'] = relationship(foreign_keys=[flight_id]) - point_of_interest: Mapped['PointOfInterest'] = relationship() + point_of_interest: Mapped['PointOfInterest'] = relationship(foreign_keys=[point_of_interest_id]) adjustment: Mapped['PhotoAdjustment'] = relationship(passive_deletes=True) created_by: Mapped['User'] = relationship() + aircraft: Mapped['Aircraft'] = relationship(foreign_keys=[aircraft_id]) + copilots: Mapped[List['Copilot']] = relationship(secondary=copilot_has_photo) class PhotoAdjustment(BaseModel): @@ -153,7 +164,6 @@ class PhotoAdjustment(BaseModel): crop_height: Mapped[float] = mapped_column(Float, nullable=True) updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) - photo: Mapped['Photo'] = relationship() @@ -162,7 +172,9 @@ class Aircraft(BaseModel): id: Mapped[int] = mapped_column(primary_key=True) call_sign: Mapped[str] = mapped_column(String(16), nullable=False) - photo_filename: Mapped[str] = mapped_column(String(128), nullable=True) + photo_filename: Mapped[str] = mapped_column(String(128), nullable=True) # TODO: odstranit + title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True) + is_public: Mapped[bool] = mapped_column(Boolean, server_default='0') manufacturer: Mapped[str] = mapped_column(Text, nullable=False, server_default="") model: Mapped[str] = mapped_column(String(30), nullable=False, server_default="") seats: Mapped[str] = mapped_column(Integer, nullable=False) @@ -190,7 +202,7 @@ class Aircraft(BaseModel): # created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) # # created_by: Mapped['User'] = relationship() -# aircraft: Mapped['Aircraft'] = relationship(back_populates="notes") +# aircraft: Mapped['Aircraft'] = relationship() class Organization(BaseModel): @@ -202,7 +214,7 @@ class Organization(BaseModel): created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0') - users: Mapped[Set['User']] = relationship(back_populates='organizations', secondary=user_is_in_organization) + users: Mapped[Set['User']] = relationship(secondary=user_is_in_organization) created_by: Mapped['User'] = relationship() @@ -261,6 +273,8 @@ class Flight(BaseModel): name: Mapped[str] = mapped_column(String(128), nullable=False) description: Mapped[str] = mapped_column(Text, nullable=False) event_id: Mapped[int] = mapped_column(Integer, ForeignKey("event.id"), nullable=True) + title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True) + takeoff_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=False) landing_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=False) takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False) @@ -285,9 +299,10 @@ class Flight(BaseModel): track: Mapped['FlightTrack'] = relationship() event: Mapped['Event'] = relationship() copilots: Mapped[List['Copilot']] = relationship(secondary=flight_has_copilot) - aircraft: Mapped['Aircraft'] = relationship(back_populates="flights") + aircraft: Mapped['Aircraft'] = relationship() photos: Mapped[List['Photo']] = relationship(foreign_keys=[Photo.flight_id]) created_by: Mapped['User'] = relationship() + title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id]) class Copilot(BaseModel): @@ -295,12 +310,15 @@ class Copilot(BaseModel): id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(128), nullable=False) + title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True) 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') flights: Mapped[Set['Flight']] = relationship(secondary=flight_has_copilot) created_by: Mapped['User'] = relationship() + photos: Mapped[List['Photo']] = relationship(secondary=copilot_has_photo) + title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id]) class Metar(BaseModel): @@ -312,7 +330,7 @@ class Metar(BaseModel): issued_at: Mapped[datetime] = mapped_column(DateTime) deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0') - airport: Mapped['Airport'] = relationship(back_populates="metars") + airport: Mapped['Airport'] = relationship() class License(BaseModel): @@ -325,7 +343,7 @@ class License(BaseModel): created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id')) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) - user: Mapped['User'] = relationship(back_populates="licences") + user: Mapped['User'] = relationship() created_by: Mapped['User'] = relationship() diff --git a/src/database/query_builder.py b/src/database/query_builder.py index 9a7b894..5a504bb 100644 --- a/src/database/query_builder.py +++ b/src/database/query_builder.py @@ -23,12 +23,7 @@ class QueryBuilder: query = query.filter(self.model.deleted.is_(False)) if hasattr(self.model, "created_by_id") and created_by_id: - ownership = [self.model.created_by_id == created_by_id] - - if hasattr(self.model, "is_public"): - ownership.append(self.model.is_public.is_(True)) - - query = query.filter(or_(*ownership)) + query = query.filter(self.model.created_by_id == created_by_id) if order_by: query = query.order_by(*order_by) diff --git a/src/graphql_schema/dataloaders/multi_models.py b/src/graphql_schema/dataloaders/multi_models.py index 6fe3265..6dfc3cc 100644 --- a/src/graphql_schema/dataloaders/multi_models.py +++ b/src/graphql_schema/dataloaders/multi_models.py @@ -110,3 +110,32 @@ flight_track_dataloader = DataLoader( ).load, cache=False ) + +copilots_in_photo_dataloader = DataLoader( + load_fn=MultiModelsDataloader( + models.Copilot, + relationship_column=models.copilot_has_photo.c.photo_id, + extra_join=[models.copilot_has_photo], + order_by=[models.Copilot.name] + ).load, + cache=False +) + +photo_copilots_dataloader = DataLoader( + load_fn=MultiModelsDataloader( + models.Photo, + relationship_column=models.copilot_has_photo.c.copilot_id, + extra_join=[models.copilot_has_photo], + order_by=[models.Photo.exposed_at] + ).load, + cache=False +) + +photos_aircraft_dataloader = DataLoader( + load_fn=MultiModelsDataloader( + models.Photo, + relationship_column=models.Photo.aircraft_id, + order_by=[models.Photo.exposed_at] + ).load, + cache=False +) \ No newline at end of file diff --git a/src/graphql_schema/dataloaders/single_model.py b/src/graphql_schema/dataloaders/single_model.py index 45898a0..b14a42e 100644 --- a/src/graphql_schema/dataloaders/single_model.py +++ b/src/graphql_schema/dataloaders/single_model.py @@ -9,6 +9,7 @@ def create_dataloader(model: Type[models.BaseModel], relationship_column=None, f return DataLoader(load_fn=loader, cache=False) +user_dataloader = create_dataloader(models.User) airport_dataloader = create_dataloader(models.Airport) aircraft_dataloader = create_dataloader(models.Aircraft) event_dataloader = create_dataloader(models.Event) @@ -18,12 +19,6 @@ poi_dataloader = create_dataloader(models.PointOfInterest) poi_type_dataloader = create_dataloader(models.PointOfInterestType) flight_dataloader = create_dataloader(models.Flight) photo_adjustment_dataloader = create_dataloader( - models.PhotoAdjustment, - relationship_column=models.PhotoAdjustment.photo_id -) -photo_dataloader = create_dataloader(models.Photo) -cover_photo_loader = create_dataloader( - models.Photo, - relationship_column=models.Photo.flight_id, - filters=[models.Photo.is_flight_cover.is_(True)] + models.PhotoAdjustment, relationship_column=models.PhotoAdjustment.photo_id ) +photo_dataloader = create_dataloader(models.Photo) \ No newline at end of file diff --git a/src/graphql_schema/entities/aircraft.py b/src/graphql_schema/entities/aircraft.py index f11c729..20d5e9a 100644 --- a/src/graphql_schema/entities/aircraft.py +++ b/src/graphql_schema/entities/aircraft.py @@ -1,5 +1,8 @@ -from typing import List +from typing import List, Optional import strawberry +from fastapi import HTTPException +from starlette.status import HTTP_401_UNAUTHORIZED + from decorators.endpoints import authenticated_user_only from decorators.error_logging import error_logging from .resolvers.aircraft import AircraftMutationResolver, AircraftQueryResolver @@ -20,10 +23,12 @@ class AircraftQueries: @strawberry.field() @error_logging - @authenticated_user_only() - async def aircraft(root, info, id: int) -> Aircraft: + async def aircraft(root, info, id: int, public: Optional[bool] = False) -> Aircraft: + if not info.context.user_id and not public: + raise HTTPException(HTTP_401_UNAUTHORIZED) + return await AircraftQueryResolver().get_one( - id, user_id=info.context.user_id, organization_ids=info.context.organization_ids + id, user_id=info.context.user_id, organization_ids=info.context.organization_ids, public=public ) diff --git a/src/graphql_schema/entities/flight.py b/src/graphql_schema/entities/flight.py index e441f65..218af40 100644 --- a/src/graphql_schema/entities/flight.py +++ b/src/graphql_schema/entities/flight.py @@ -19,19 +19,34 @@ class FlightQueries: @strawberry.field() @error_logging - async def flights(root, info, username: Optional[str] = None) -> List[Flight]: - if not info.context.user_id and not username: + async def flights( + root, info, + username: Optional[str] = None, + public: Optional[bool] = False, + copilot_id: Optional[int] = None, + point_of_interest_id: Optional[int] = None, + aircraft_id: Optional[int] = None, + ) -> List[Flight]: + if not info.context.user_id and not public: raise HTTPException(HTTP_401_UNAUTHORIZED) - return await FlightQueryResolver().get_list(user_id=info.context.user_id, username=username) + return await FlightQueryResolver().get_list( + user_id=info.context.user_id, + username=username, + only_public=public, + copilot_id=copilot_id, + aircraft_id=aircraft_id, + point_of_interest_id=point_of_interest_id + + ) @strawberry.field() @error_logging - async def flight(root, info, id: int, username: Optional[str] = None) -> Flight: - if not info.context.user_id and not username: + async def flight(root, info, id: int, username: Optional[str] = None, public: Optional[bool] = False) -> Flight: + if not info.context.user_id and not public: raise HTTPException(HTTP_401_UNAUTHORIZED) - return await FlightQueryResolver().get_one(id, user_id=info.context.user_id, username=username) + return await FlightQueryResolver().get_one(id, user_id=info.context.user_id, username=username, public=public) @strawberry.type diff --git a/src/graphql_schema/entities/photo.py b/src/graphql_schema/entities/photo.py index 7696d96..9880623 100644 --- a/src/graphql_schema/entities/photo.py +++ b/src/graphql_schema/entities/photo.py @@ -1,10 +1,10 @@ -from typing import List +from typing import List, Optional import strawberry from database import models from decorators.endpoints import authenticated_user_only from decorators.error_logging import error_logging from graphql_schema.entities.resolvers.base import BaseQueryResolver -from graphql_schema.entities.resolvers.photo import PhotoMutationResolver +from graphql_schema.entities.resolvers.photo import PhotoMutationResolver, PhotoQueryResolver from graphql_schema.entities.types.types import Photo from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput @@ -13,8 +13,22 @@ from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadP class PhotoQueries: @strawberry.field() @error_logging - async def photos(root, info) -> List[Photo]: - return await BaseQueryResolver(Photo, models.Photo).get_list(user_id=info.context.user_id) + async def photos( + root, info, + flight_id: Optional[int] = None, + copilot_id: Optional[int] = None, + point_of_interest_id: Optional[int] = None, + aircraft_id: Optional[int] = None, + public: Optional[bool] = False, + ) -> List[Photo]: + return await PhotoQueryResolver().get_list( + public=public, + flight_id=flight_id, + user_id=info.context.user_id, + copilot_id=copilot_id, + aircraft_id=aircraft_id, + point_of_interest_id=point_of_interest_id, + ) @strawberry.field() @error_logging diff --git a/src/graphql_schema/entities/poi.py b/src/graphql_schema/entities/poi.py index 72d3700..4b35454 100644 --- a/src/graphql_schema/entities/poi.py +++ b/src/graphql_schema/entities/poi.py @@ -1,5 +1,7 @@ from typing import List import strawberry +from fastapi import HTTPException +from starlette.status import HTTP_401_UNAUTHORIZED from database import models from decorators.endpoints import authenticated_user_only from database.transaction import get_session @@ -14,14 +16,24 @@ from graphql_schema.entities.types.mutation_input import CreatePointOfInterestIn class PointOfInterestQueries: @strawberry.field() @error_logging - @authenticated_user_only() - async def points_of_interest(root, info) -> List[PointOfInterest]: - return await BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_list(info.context.user_id) + async def points_of_interest(root, info, public: bool = False) -> List[PointOfInterest]: + if not info.context.user_id and not public: + raise HTTPException(HTTP_401_UNAUTHORIZED) + + return await BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_list( + info.context.user_id, + only_public=public + ) @strawberry.field() - @authenticated_user_only() - async def point_of_interest(root, info, id: int) -> PointOfInterest: - return await BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_one(id, info.context.user_id) + async def point_of_interest(root, info, id: int, public: bool = False) -> PointOfInterest: + if not info.context.user_id and not public: + raise HTTPException(HTTP_401_UNAUTHORIZED) + + return await BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_one( + id, info.context.user_id, + only_public=public + ) @strawberry.type diff --git a/src/graphql_schema/entities/resolvers/aircraft.py b/src/graphql_schema/entities/resolvers/aircraft.py index 1fc47a8..8e5bedf 100644 --- a/src/graphql_schema/entities/resolvers/aircraft.py +++ b/src/graphql_schema/entities/resolvers/aircraft.py @@ -48,9 +48,6 @@ class AircraftMutationResolver(BaseMutationResolver): async def create_new(self, data: CreateAircraftInput, user_id: int) -> Aircraft: input_data = data.to_dict() - if data.photo: - input_data['photo_filename'] = await handle_file_upload(data.photo, AIRCRAFT_UPLOAD_DEST_PATH) - if data.organization: async with get_session() as db: input_data['organization_id'] = await handle_combobox_save( @@ -64,13 +61,6 @@ class AircraftMutationResolver(BaseMutationResolver): async def edit(self, id: int, user_id: int, data: EditAircraftInput) -> Aircraft: update_data = data.to_dict() - aircraft = await AircraftQueryResolver().get_one(id, user_id) - - if data.photo: - if aircraft.photo_filename: - delete_file(AIRCRAFT_UPLOAD_DEST_PATH + "/" + aircraft.photo_filename, silent=True) - update_data['photo_filename'] = await handle_file_upload(data.photo, AIRCRAFT_UPLOAD_DEST_PATH) - async with get_session() as db: if data.organization: update_data['organization_id'] = await handle_combobox_save( diff --git a/src/graphql_schema/entities/resolvers/base.py b/src/graphql_schema/entities/resolvers/base.py index e1053c9..d1d101d 100644 --- a/src/graphql_schema/entities/resolvers/base.py +++ b/src/graphql_schema/entities/resolvers/base.py @@ -46,6 +46,9 @@ class BaseQueryResolver(BaseResolver): if only_public and hasattr(self.model, "is_public"): query = query.filter(self.model.is_public.is_(True)) + if kwargs: + query = query.filter_by(**kwargs) + return query async def get_list(self, user_id: Optional[int] = None, **kwargs) -> List[GQL_TYPE]: diff --git a/src/graphql_schema/entities/resolvers/copilot.py b/src/graphql_schema/entities/resolvers/copilot.py index 0297a70..1e0918c 100644 --- a/src/graphql_schema/entities/resolvers/copilot.py +++ b/src/graphql_schema/entities/resolvers/copilot.py @@ -16,12 +16,13 @@ class CopilotQueryResolver(BaseQueryResolver): only_public: Optional[bool] = False, *args, **kwargs ): + pilot_username = kwargs.pop("pilot_username", None) query = super().get_query(user_id, object_id, order_by, only_public, *args, **kwargs) - if kwargs.get("pilot_username"): + if pilot_username: query = ( query.join(models.Copilot.created_by) - .filter(models.User.public_username == kwargs['pilot_username']) + .filter(models.User.public_username == pilot_username) ) return query diff --git a/src/graphql_schema/entities/resolvers/flight.py b/src/graphql_schema/entities/resolvers/flight.py index 512cd0c..f977a27 100644 --- a/src/graphql_schema/entities/resolvers/flight.py +++ b/src/graphql_schema/entities/resolvers/flight.py @@ -21,17 +21,38 @@ class FlightQueryResolver(BaseQueryResolver): def __init__(self): super().__init__(graphql_type=Flight, model=models.Flight) - def get_query(self, user_id: int, object_id: Optional[int] = None, *args, **kwargs): + def get_query( + self, + user_id: Optional[int] = None, + object_id: Optional[int] = None, + only_public: Optional[bool] = False, + *args, + **kwargs + ): query = super().get_query( user_id, object_id, order_by=[models.Flight.takeoff_datetime.desc()], - only_public=not bool(user_id) + only_public=only_public ) + if kwargs.get("aircraft_id"): + query = query.filter(models.Flight.aircraft_id == kwargs['aircraft_id']) + + if kwargs.get("copilot_id"): + query = ( + query.join(models.flight_has_copilot) + .filter(models.flight_has_copilot.c.copilot_id == kwargs['copilot_id']) + ) + + if kwargs.get("point_of_interest_id"): + query = ( + query.join(models.Flight.track) + .filter(models.FlightTrack.point_of_interest_id == kwargs["point_of_interest_id"]) + ) + if kwargs.get('username'): query = ( - query - .join(models.Flight.created_by) + query.join(models.Flight.created_by) .filter(models.User.public_username == kwargs['username']) ) diff --git a/src/graphql_schema/entities/resolvers/photo.py b/src/graphql_schema/entities/resolvers/photo.py index af20dbe..740a78b 100644 --- a/src/graphql_schema/entities/resolvers/photo.py +++ b/src/graphql_schema/entities/resolvers/photo.py @@ -1,15 +1,16 @@ import os import shutil +from typing import Type, Optional, List + from PIL import Image from pydantic import BaseModel -from sqlalchemy import update, delete -from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, delete, insert from background_jobs.elevation import add_terrain_elevation_to_photo from background_jobs.photo import generate_thumbnail, resize_photo from database import models from database.transaction import get_session from graphql_schema.entities.helpers.combobox import handle_combobox_save -from graphql_schema.entities.resolvers.base import BaseMutationResolver +from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver, GQL_TYPE from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput from graphql_schema.entities.types.types import Photo from paths import get_photo_basepath @@ -18,6 +19,40 @@ from utils.image import PhotoEditor, parse_exif_info from utils.upload import handle_file_upload +class PhotoQueryResolver(BaseQueryResolver): + + def __init__(self): + super().__init__(Photo, models.Photo) + + def get_query( + self, + user_id: Optional[int] = None, + object_id: Optional[int] = None, + order_by: Optional[list] = None, + only_public: Optional[bool] = False, + *args, **kwargs + ): + query = super().get_query( + user_id, object_id, order_by, + aircraft_id=kwargs.get("aircraft_id"), + point_of_interest_id=kwargs.get("point_of_interest_id") + ) + + if kwargs.get("public"): + query = ( + query.join(models.Flight, onclause=models.Photo.flight_id == models.Flight.id) + .filter(models.Flight.is_public.is_(True)) + ) + + if kwargs.get("copilot_id"): + query = ( + query.join(models.copilot_has_photo, ) + .filter(models.copilot_has_photo.c.copilot_id == kwargs['copilot_id']) + ) + + return query + + class PhotoDetailInfo(BaseModel): filename: str original_filename: str @@ -29,15 +64,6 @@ class PhotoMutationResolver(BaseMutationResolver): def __init__(self): super().__init__(Photo, models.Photo) - @staticmethod - async def _reset_flight_cover(db: AsyncSession, flight_id: int, ignored_photo_id: int): - (await db.execute( - update(models.Photo) - .filter(models.Photo.flight_id == flight_id) - .filter(models.Photo.id != ignored_photo_id) - .values(is_flight_cover=False)) - ) - @staticmethod def _copy_original(path: str, filename: str): original_filename = "_original_" + filename @@ -105,9 +131,14 @@ class PhotoMutationResolver(BaseMutationResolver): extra_data={"description": ""} ) - if input.is_flight_cover: - # reset other covers - await self._reset_flight_cover(db, photo.flight_id, id) + if input.is_aircraft: + flight = (await db.scalars(select(models.Flight).filter(models.Flight.id == photo.flight_id))).one() + data['aircraft_id'] = flight.aircraft_id + + if input.copilots is not None: + await db.execute(delete(models.copilot_has_photo).filter_by(photo_id=id)) + for copilot in input.copilots: + await db.execute(insert(models.copilot_has_photo).values(photo_id=id, copilot_id=copilot.id)) return await self._do_update(db, obj=photo, data=data) diff --git a/src/graphql_schema/entities/types/mutation_input.py b/src/graphql_schema/entities/types/mutation_input.py index 9cec9a9..ae5413e 100644 --- a/src/graphql_schema/entities/types/mutation_input.py +++ b/src/graphql_schema/entities/types/mutation_input.py @@ -29,7 +29,7 @@ class CreatePointOfInterestInput(BaseGraphqlInputType): type: Optional[ComboboxInput] = None -@strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id']) +@strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id'], all_optional=True) class EditPointOfInterestInput(BaseGraphqlInputType): type: Optional[ComboboxInput] = None @@ -68,11 +68,12 @@ class EditPhotoInput: name: Optional[str] = None description: Optional[str] = None point_of_interest: Optional[ComboboxInput] = None - is_flight_cover: Optional[bool] = None + is_aircraft: Optional[bool] = None + copilots: Optional[List[ComboboxInput]] = None def to_dict(self): return { - key: getattr(self, key) for key in ('name', 'description', 'is_flight_cover') + key: getattr(self, key) for key in ('name', 'description') if getattr(self, key) is not None } @@ -128,11 +129,9 @@ class TrackItemInput: @strawberry_sqlalchemy_input(models.Aircraft, exclude_fields=['id', 'photo_filename']) class CreateAircraftInput(BaseGraphqlInputType): - photo: Optional[Upload] organization: Optional[ComboboxInput] = None -@strawberry_sqlalchemy_input(models.Aircraft, exclude_fields=['photo_filename']) +@strawberry_sqlalchemy_input(models.Aircraft, exclude_fields=['photo_filename'], all_optional=True) class EditAircraftInput(BaseGraphqlInputType): - photo: Optional[Upload] organization: Optional[ComboboxInput] = None diff --git a/src/graphql_schema/entities/types/types.py b/src/graphql_schema/entities/types/types.py index 866f8b0..a0b7a12 100644 --- a/src/graphql_schema/entities/types/types.py +++ b/src/graphql_schema/entities/types/types.py @@ -10,12 +10,12 @@ from graphql_schema.dataloaders.multi_models import ( poi_photos_dataloader, flight_by_poi_dataloader, flight_copilots_dataloader, flight_track_dataloader, photos_dataloader, flights_by_aircraft_dataloader, users_in_organization_dataloader, aircrafts_from_organization_dataloader, user_organizations_dataloader, flights_by_event_dataloader, - flights_by_copilot_dataloader, public_flights_by_event_dataloader, public_flights_by_copilot_dataloader + flights_by_copilot_dataloader, public_flights_by_event_dataloader, public_flights_by_copilot_dataloader, photo_copilots_dataloader, photos_aircraft_dataloader, copilots_in_photo_dataloader ) from graphql_schema.dataloaders.single_model import ( - poi_dataloader, poi_type_dataloader, event_dataloader, aircraft_dataloader, airport_dataloader, cover_photo_loader, + poi_dataloader, poi_type_dataloader, event_dataloader, aircraft_dataloader, airport_dataloader, airport_weather_info_loader, organizations_dataloader, flight_dataloader, photo_adjustment_dataloader, - photo_dataloader + photo_dataloader, user_dataloader ) from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type from paths import ( @@ -76,6 +76,7 @@ class PointOfInterest: type: Optional[PointOfInterestType] = strawberry.field(resolver=lambda root: poi_type_dataloader.load(root.type_id)) photos: List[Photo] = strawberry.field(resolver=lambda root: poi_photos_dataloader.load(root.id)) flights: List[Flight] = strawberry.field(resolver=lambda root: flight_by_poi_dataloader.load(root.id)) + title_photo: Optional[Photo] = strawberry.field(resolver=lambda root: photo_dataloader.load(root.title_photo_id)) @strawberry_sqlalchemy_type(models.Photo) @@ -85,6 +86,8 @@ class Photo: point_of_interest: Optional[PointOfInterest] = strawberry.field( resolver=lambda root: poi_dataloader.load(root.point_of_interest_id) ) + copilots: List[Copilot] = strawberry.field(resolver=lambda root: copilots_in_photo_dataloader.load(root.id)) + is_aircraft: bool = strawberry.field(resolver=lambda root: bool(root.aircraft_id)) flight: Flight = strawberry.field(resolver=lambda root: flight_dataloader.load(root.flight_id)) adjustment: Optional[PhotoAdjustment] = strawberry.field( resolver=lambda root: photo_adjustment_dataloader.load(root.id) @@ -123,12 +126,13 @@ class Flight: async def load_event(root): return await event_dataloader.load(root.event_id) + pilot: User = strawberry.field(resolver=lambda root: user_dataloader.load(root.created_by_id)) copilots: Optional[List[Copilot]] = strawberry.field(resolver=load_copilots) event: Optional[Event] = strawberry.field(resolver=load_event) aircraft: Aircraft = strawberry.field(resolver=lambda root: aircraft_dataloader.load(root.aircraft_id)) takeoff_airport: Airport = strawberry.field(resolver=lambda root: airport_dataloader.load(root.takeoff_airport_id)) landing_airport: Airport = strawberry.field(resolver=lambda root: airport_dataloader.load(root.landing_airport_id)) - cover_photo: Optional[Photo] = strawberry.field(resolver=lambda root: cover_photo_loader.load(root.id)) + title_photo: Optional[Photo] = strawberry.field(resolver=lambda root: photo_dataloader.load(root.title_photo_id)) track: List[FlightTrack] = strawberry.field(resolver=lambda root: flight_track_dataloader.load(root.id)) takeoff_weather_info: Optional[WeatherInfo] = strawberry.field( resolver=lambda root: airport_weather_info_loader.load(root.takeoff_weather_info_id) @@ -153,17 +157,21 @@ class Copilot: return await dataloader.load(root.id) flights: List[Flight] = strawberry.field(resolver=resolve_flights) + photos: List[Photo] = strawberry.field(resolver=lambda root: photo_copilots_dataloader.load(root.id)) + title_photo: Optional[Photo] = strawberry.field(resolver=lambda root: photo_dataloader.load(root.title_photo_id)) @strawberry_sqlalchemy_type(models.Aircraft) class Aircraft: - photo_url: Optional[str] = strawberry.field( - resolver=lambda root: get_public_url(f"aircrafts/{root.photo_filename}") if root.photo_filename else None - ) + # photo_url: Optional[str] = strawberry.field( + # resolver=lambda root: get_public_url(f"aircrafts/{root.photo_filename}") if root.photo_filename else None + # ) flights: List[Flight] = strawberry.field(resolver=lambda root: flights_by_aircraft_dataloader.load(root.id)) organization: Optional[Organization] = strawberry.field( resolver=lambda root: organizations_dataloader.load(root.organization_id) ) + photos: List[Photo] = strawberry.field(resolver=lambda root: photos_aircraft_dataloader.load(root.id)) + title_photo: Optional[Photo] = strawberry.field(resolver=lambda root: photo_dataloader.load(root.title_photo_id)) @strawberry_sqlalchemy_type(models.Organization)