diff --git a/alembic/versions/20240429-164009_add_flight_plan_meta_information_790ba00fc021.py b/alembic/versions/20240429-164009_add_flight_plan_meta_information_790ba00fc021.py new file mode 100644 index 0000000..cbf3017 --- /dev/null +++ b/alembic/versions/20240429-164009_add_flight_plan_meta_information_790ba00fc021.py @@ -0,0 +1,58 @@ +"""add flight plan meta information + +Revision ID: 790ba00fc021 +Revises: f7ab4bdac987 +Create Date: 2024-04-29 16:40:09.829317 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '790ba00fc021' +down_revision = 'f7ab4bdac987' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('flight_plan_has_copilot', + sa.Column('flight_plan_id', sa.Integer(), nullable=False), + sa.Column('copilot_id', sa.Integer(), nullable=False), + sa.Column('token', sa.String(length=64), nullable=True), + sa.ForeignKeyConstraint(['copilot_id'], ['copilot.id'], ), + sa.ForeignKeyConstraint(['flight_plan_id'], ['flight_plan.id'], ), + sa.PrimaryKeyConstraint('flight_plan_id', 'copilot_id'), + sa.UniqueConstraint('token') + ) + op.add_column('flight_plan', sa.Column('is_default_name', sa.Boolean(), server_default='0', nullable=False)) + op.add_column('flight_plan', sa.Column('is_editable_by_copilot', sa.Boolean(), server_default='0', nullable=False)) + op.add_column('flight_plan', sa.Column('planned_speed', sa.Integer(), nullable=False)) + op.add_column('flight_plan', sa.Column('planned_takeoff_datetime', sa.DateTime(), nullable=True)) + op.add_column('flight_plan', sa.Column('aircraft_id', sa.Integer(), nullable=True)) + op.alter_column('flight_plan', 'name', + existing_type=mysql.VARCHAR(length=128), + type_=sa.String(length=256), + existing_nullable=False) + op.create_foreign_key(None, 'flight_plan', 'aircraft', ['aircraft_id'], ['id']) + op.drop_column('point_of_interest_type', 'openaip_id') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('point_of_interest_type', sa.Column('openaip_id', mysql.VARCHAR(length=32), nullable=False)) + op.drop_constraint(None, 'flight_plan', type_='foreignkey') + op.alter_column('flight_plan', 'name', + existing_type=sa.String(length=256), + type_=mysql.VARCHAR(length=128), + existing_nullable=False) + op.drop_column('flight_plan', 'aircraft_id') + op.drop_column('flight_plan', 'planned_takeoff_datetime') + op.drop_column('flight_plan', 'planned_speed') + op.drop_column('flight_plan', 'is_editable_by_copilot') + op.drop_column('flight_plan', 'is_default_name') + op.drop_table('flight_plan_has_copilot') + # ### end Alembic commands ### diff --git a/src/database/models.py b/src/database/models.py index efe8720..920c713 100644 --- a/src/database/models.py +++ b/src/database/models.py @@ -59,6 +59,14 @@ flight_has_copilot = Table( Column("token", String(64), unique=True) ) +flight_plan_has_copilot = Table( + "flight_plan_has_copilot", + BaseModel.metadata, + Column("flight_plan_id", ForeignKey("flight_plan.id"), primary_key=True), + Column("copilot_id", ForeignKey("copilot.id"), primary_key=True), + Column("token", String(64), unique=True) +) + copilot_has_photo = Table( "copilot_has_photo", BaseModel.metadata, @@ -93,12 +101,20 @@ class FlightPlan(BaseModel): __tablename__ = "flight_plan" id: Mapped[int] = mapped_column(primary_key=True) - name: Mapped[str] = mapped_column(String(128), nullable=False) + name: Mapped[str] = mapped_column(String(256), nullable=False) + is_default_name: Mapped[bool] = mapped_column(Boolean, server_default='0') is_public: Mapped[bool] = mapped_column(Boolean, server_default='0') + is_editable_by_copilot: Mapped[bool] = mapped_column(Boolean, server_default='0') + planned_speed: Mapped[int] = mapped_column(Integer, nullable=False, default=150) + planned_takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=True) + aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.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') + copilots: Mapped[List['Copilot']] = relationship(secondary=flight_plan_has_copilot) + aircraft: Mapped['Aircraft'] = relationship() + markers: Mapped['FlightPlanMarker'] = relationship() created_by: Mapped['User'] = relationship() @@ -403,6 +419,7 @@ class Copilot(BaseModel): deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0') flights: Mapped[Set['Flight']] = relationship(secondary=flight_has_copilot) + flight_plans: Mapped[Set['FlightPlan']] = relationship(secondary=flight_plan_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]) diff --git a/src/graphql_schema/dataloaders/multi_models.py b/src/graphql_schema/dataloaders/multi_models.py index c86616b..5338ccf 100644 --- a/src/graphql_schema/dataloaders/multi_models.py +++ b/src/graphql_schema/dataloaders/multi_models.py @@ -20,9 +20,25 @@ flight_copilots_dataloader = DataLoader( models.flight_has_copilot.c.flight_id == models.Flight.id, models.flight_has_copilot.c.copilot_id == models.Copilot.id, ], - extra_join=[models.Copilot.flights]).load, + extra_join=[models.Copilot.flights] + ).load, cache=False) + +flight_plan_copilots_dataloader = DataLoader( + load_fn=FlightCopilotDataloader( + models.Copilot, + relationship_column=models.FlightPlan.id, + extra_select=[models.flight_plan_has_copilot.c.token], + # filters=[ + # models.flight_plan_has_copilot.c.flight_plan_id == models.FlightPlan.id, + # models.flight_plan_has_copilot.c.copilot_id == models.Copilot.id, + # ], + extra_join=[models.Copilot.flight_plans] + ).load, + cache=False +) + reporting_points_dataloader = DataLoader( load_fn=MultiModelsDataloader( models.ReportingPoint, diff --git a/src/graphql_schema/entities/resolvers/base.py b/src/graphql_schema/entities/resolvers/base.py index 2a98b91..37f02f1 100644 --- a/src/graphql_schema/entities/resolvers/base.py +++ b/src/graphql_schema/entities/resolvers/base.py @@ -87,7 +87,7 @@ class BaseQueryResolver(BaseResolver): class BaseMutationResolver(BaseResolver): - async def _get_one(self, db: AsyncSession, id: int, created_by_id: int) -> models.BaseModel: + async def _get_one(self, db: AsyncSession, id: int, created_by_id: int): query = self.query_builder.get_simple_query(created_by_id=created_by_id).filter(self.model.id == id) return (await db.scalars(query)).one() diff --git a/src/graphql_schema/entities/resolvers/flight_plan.py b/src/graphql_schema/entities/resolvers/flight_plan.py index a141568..71f3f17 100644 --- a/src/graphql_schema/entities/resolvers/flight_plan.py +++ b/src/graphql_schema/entities/resolvers/flight_plan.py @@ -1,9 +1,17 @@ -from sqlalchemy import delete +import asyncio + +from sqlalchemy import delete, select +from sqlalchemy.dialects.mysql import insert + from database import models +from database.models import flight_plan_has_copilot 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.flight import handle_aircraft_save from graphql_schema.entities.types.mutation_input import CreateFlightPlanInput, EditFlightPlanInput from graphql_schema.entities.types.types import FlightPlan +from utils.str_utils import random_str class FlightPlanMutationResolver(BaseMutationResolver): @@ -41,13 +49,55 @@ class FlightPlanMutationResolver(BaseMutationResolver): await self.save_markers(db, flight_plan, data.markers) return flight_plan + async def save_copilots(self, db, flight_plan_id: int, copilots: list, user_id: int): + await db.execute(delete(flight_plan_has_copilot).filter_by(flight_plan_id=flight_plan_id)) + copilots = await asyncio.gather(*[ + handle_combobox_save(db, models.Copilot, copilot, user_id) for copilot in copilots + ]) + + for copilot_id in copilots: + await db.execute(insert(flight_plan_has_copilot).values( + flight_plan_id=flight_plan_id, + copilot_id=copilot_id, + token=random_str(64) + )) + async def update(self, id: int, data: EditFlightPlanInput, user_id: int) -> FlightPlan: input_data = data.to_dict() async with get_session() as db: - flight_plan = await self._do_update(db, id, input_data) + flight_plan_model = await self._get_one(db, id=id, created_by_id=user_id) - await self.reset_plan_markers(db, flight_plan) - await self.save_markers(db, flight_plan, data.markers) + if data.aircraft is not None: + input_data['aircraft_id'] = await handle_aircraft_save(db, user_id, data.aircraft) + if data.markers is not None: + await self.reset_plan_markers(db, flight_plan_model) + await self.save_markers(db, flight_plan_model, data.markers) + + if flight_plan_model.is_default_name: + if data.markers: + markers = data.markers + else: + markers = (await db.scalars(select(models.FlightPlanMarker).filter(models.FlightPlanMarker.flight_plan_id == id))).all() + + used_markers = evenly_spaced_elements(markers, 5) + input_data['name'] = " - ".join(m.name for m in used_markers) + + if data.copilots is not None: + await self.save_copilots( + db, + flight_plan_id=id, + copilots=data.copilots, + user_id=user_id + ) + + flight_plan = await self._do_update(db, obj=flight_plan_model, data=input_data) return flight_plan + + +def evenly_spaced_elements(lst: list, count: int) -> list: + if count > len(lst): + return lst + interval = (len(lst) - 1) / (count - 1) if count > 1 else 0 + return [lst[int(round(i * interval))] for i in range(count)] diff --git a/src/graphql_schema/entities/types/mutation_input.py b/src/graphql_schema/entities/types/mutation_input.py index 02e7afa..f6f8f16 100644 --- a/src/graphql_schema/entities/types/mutation_input.py +++ b/src/graphql_schema/entities/types/mutation_input.py @@ -5,6 +5,7 @@ import strawberry from strawberry.file_uploads import Upload from database import models from graphql_schema.entities.types.base import BaseGraphqlInputType +from graphql_schema.entities.types.types import Aircraft from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input @@ -36,7 +37,9 @@ class CreateFlightPlanInput(BaseGraphqlInputType): @strawberry_sqlalchemy_input(model=models.FlightPlan, exclude_fields=["id", "markers"], all_optional=True) class EditFlightPlanInput(BaseGraphqlInputType): - markers: list[FlightPlanMarkerInput] + markers: Optional[list[FlightPlanMarkerInput]] = None + copilots: Optional[list[ComboboxInput]] = None + aircraft: Optional[ComboboxInput] = None @strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"], all_optional=True) diff --git a/src/graphql_schema/entities/types/types.py b/src/graphql_schema/entities/types/types.py index a1596db..53f7ad4 100644 --- a/src/graphql_schema/entities/types/types.py +++ b/src/graphql_schema/entities/types/types.py @@ -14,7 +14,8 @@ from graphql_schema.dataloaders.multi_models import ( 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, - photo_copilots_dataloader, photos_aircraft_dataloader, copilots_in_photo_dataloader, flight_plan_markers_dataloader, reporting_points_dataloader + photo_copilots_dataloader, photos_aircraft_dataloader, copilots_in_photo_dataloader, flight_plan_markers_dataloader, + reporting_points_dataloader, flight_plan_copilots_dataloader ) from graphql_schema.dataloaders.single_model import ( poi_dataloader, poi_type_dataloader, event_dataloader, aircraft_dataloader, airport_dataloader, @@ -227,12 +228,18 @@ class FlightPlan: ) return navigation + @authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[]) + async def load_copilots(root): + return await flight_plan_copilots_dataloader.load(root.id) + markers: list[FlightPlanMarker] = strawberry.field( resolver=lambda root: flight_plan_markers_dataloader.load(root.id) ) track: Optional[list[FlightPlanTrack]] = strawberry.field( resolver=resolve_track ) + copilots: List[Copilot] = strawberry.field(resolver=load_copilots) + aircraft: Optional[Aircraft] = strawberry.field(resolver=lambda root: aircraft_dataloader.load(root.aircraft_id)) @strawberry_sqlalchemy_type(models.Copilot)