Refaktoring, pridani letiste pro mezipristani

This commit is contained in:
Michal Kváček
2023-10-07 21:36:56 +02:00
parent 2422f26f10
commit b355d0b439
25 changed files with 581 additions and 566 deletions
@@ -0,0 +1,38 @@
"""add airports to track
Revision ID: 956d295689bf
Revises: 8b0c020dc0c4
Create Date: 2023-09-28 22:43:52.286599
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '956d295689bf'
down_revision = '8b0c020dc0c4'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('flight_track', sa.Column('airport_id', sa.Integer(), nullable=True))
op.add_column('flight_track', sa.Column('landing_duration', sa.Integer(), nullable=True))
op.alter_column('flight_track', 'point_of_interest_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=True)
op.create_foreign_key(None, 'flight_track', 'airport', ['airport_id'], ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'flight_track', type_='foreignkey')
op.alter_column('flight_track', 'point_of_interest_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=False)
op.drop_column('flight_track', 'landing_duration')
op.drop_column('flight_track', 'airport_id')
# ### end Alembic commands ###
+2 -2
View File
@@ -5,7 +5,7 @@ from external.elevation import elevation_api
from external.gpx_parser import GPXParser from external.gpx_parser import GPXParser
async def add_terrain_elevation_to_flight(flight: dict, gpx_filename: str): async def add_terrain_elevation_to_flight(flight_id: int, gpx_filename: str):
path = "/app/uploads/tracks" # TODO vytahnout do configu path = "/app/uploads/tracks" # TODO vytahnout do configu
gpx_parser = GPXParser(f"{path}/{gpx_filename}") gpx_parser = GPXParser(f"{path}/{gpx_filename}")
@@ -20,7 +20,7 @@ async def add_terrain_elevation_to_flight(flight: dict, gpx_filename: str):
async with get_session() as db: async with get_session() as db:
await models.Flight.update( await models.Flight.update(
db, {"gpx_track_filename": output_name, "has_terrain_elevation": True}, db, {"gpx_track_filename": output_name, "has_terrain_elevation": True},
id=flight['id']) id=flight_id)
except ClientResponseError as e: except ClientResponseError as e:
print(e) print(e)
+4 -1
View File
@@ -187,11 +187,14 @@ class FlightTrack(BaseModel):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False) flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False)
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=False) point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
landing_duration: Mapped[int] = mapped_column(Integer, nullable=True)
order: Mapped[int] = mapped_column(Integer) order: Mapped[int] = mapped_column(Integer)
flight: Mapped['Flight'] = relationship() flight: Mapped['Flight'] = relationship()
point_of_interest: Mapped['PointOfInterest'] = relationship() point_of_interest: Mapped['PointOfInterest'] = relationship()
airport: Mapped['Airport'] = relationship()
class WeatherInfo(BaseModel): class WeatherInfo(BaseModel):
+1 -1
View File
@@ -23,7 +23,7 @@ class GPXParser:
@cached() @cached()
async def get_times(self): async def get_times(self):
nodes = self.run_xpath("//gpx:time") nodes = self.run_xpath("//gpx:trkpt/gpx:time")
return [datetime.fromisoformat(node.text).astimezone() for node in nodes] return [datetime.fromisoformat(node.text).astimezone() for node in nodes]
@cached() @cached()
+3
View File
@@ -57,3 +57,6 @@ class Weather:
result_data['datetime'] = datetime.datetime.strptime(data['hourly']['time'][idx], "%Y-%m-%dT%H:%M") result_data['datetime'] = datetime.datetime.strptime(data['hourly']['time'][idx], "%Y-%m-%dT%H:%M")
return result_data return result_data
weather_api = Weather()
+6 -21
View File
@@ -1,38 +1,23 @@
from typing import List, Optional, Annotated, TYPE_CHECKING from typing import List, Optional, TYPE_CHECKING
import strawberry import strawberry
from strawberry.file_uploads import Upload from strawberry.file_uploads import Upload
from database import models from database import models
from decorators.endpoints import authenticated_user_only from decorators.endpoints import authenticated_user_only
from dependencies.db import get_session from dependencies.db import get_session
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
from upload_utils import handle_file_upload, delete_file, get_public_url from upload_utils import handle_file_upload, delete_file
from graphql_schema.entities.helpers.combobox import handle_combobox_save from graphql_schema.entities.helpers.combobox import handle_combobox_save
from .resolvers.aircraft import get_aircraft_resolver from .resolvers.aircraft import get_aircraft_resolver
from .resolvers.base import get_list, get_one from .resolvers.base import get_list, get_one
from ..dataloaders.multi_models import flights_by_aircraft_dataloader from graphql_schema.entities.types.mutation_input import ComboboxInput
from ..dataloaders.single_model import organizations_dataloader from graphql_schema.entities.types.types import Aircraft
from ..types import ComboboxInput
if TYPE_CHECKING: if TYPE_CHECKING:
from .flight import Flight pass
from .organization import Organization
AIRCRAFT_UPLOAD_DEST_PATH = "/app/uploads/aircrafts/" AIRCRAFT_UPLOAD_DEST_PATH = "/app/uploads/aircrafts/"
@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
)
flights: List[Annotated["Flight", strawberry.lazy('.flight')]] = strawberry.field(
resolver=lambda root: flights_by_aircraft_dataloader.load(root.id)
)
organization: Optional[Annotated["Organization", strawberry.lazy(".organization")]] = strawberry.field(
resolver=lambda root: organizations_dataloader.load(root.organization_id)
)
@strawberry.type @strawberry.type
class AircraftQueries: class AircraftQueries:
@strawberry.field() @strawberry.field()
+4 -11
View File
@@ -1,21 +1,14 @@
from typing import List, Annotated, TYPE_CHECKING from typing import List, TYPE_CHECKING
import strawberry import strawberry
from database import models from database import models
from decorators.endpoints import authenticated_user_only from decorators.endpoints import authenticated_user_only
from dependencies.db import get_session from dependencies.db import get_session
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
from .resolvers.base import get_base_resolver, get_list, get_one from .resolvers.base import get_base_resolver, get_list, get_one
from ..dataloaders.multi_models import flights_by_copilot_dataloader from graphql_schema.entities.types.types import Copilot
if TYPE_CHECKING: if TYPE_CHECKING:
from .flight import Flight pass
@strawberry_sqlalchemy_type(models.Copilot)
class Copilot:
flights: List[Annotated["Flight", strawberry.lazy('.flight')]] = strawberry.field(
resolver=lambda root: flights_by_copilot_dataloader.load(root.id)
)
@strawberry.type @strawberry.type
+4 -11
View File
@@ -1,21 +1,14 @@
from typing import List, Annotated, TYPE_CHECKING from typing import List, TYPE_CHECKING
import strawberry import strawberry
from database import models from database import models
from decorators.endpoints import authenticated_user_only from decorators.endpoints import authenticated_user_only
from dependencies.db import get_session from dependencies.db import get_session
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
from .resolvers.base import get_base_resolver, get_list, get_one from .resolvers.base import get_base_resolver, get_list, get_one
from ..dataloaders.multi_models import flights_by_event_dataloader from graphql_schema.entities.types.types import Event
if TYPE_CHECKING: if TYPE_CHECKING:
from .flight import Flight pass
@strawberry_sqlalchemy_type(models.Event)
class Event:
flights: List[Annotated["Flight", strawberry.lazy('.flight')]] = strawberry.field(
resolver=lambda root: flights_by_event_dataloader.load(root.id)
)
@strawberry.type @strawberry.type
+12 -241
View File
@@ -1,138 +1,19 @@
import asyncio import asyncio
from datetime import timedelta, datetime from typing import List, Optional
from typing import List, Optional, Annotated, TYPE_CHECKING
import strawberry import strawberry
from fastapi import HTTPException from fastapi import HTTPException
from sqlalchemy import select, insert, delete from sqlalchemy import select
from starlette.status import HTTP_401_UNAUTHORIZED from starlette.status import HTTP_401_UNAUTHORIZED
from strawberry.file_uploads import Upload
from background_jobs.elevation import add_terrain_elevation_to_flight
from database import models from database import models
from database.models import flight_has_copilot
from decorators.endpoints import authenticated_user_only from decorators.endpoints import authenticated_user_only
from decorators.error_logging import error_logging from decorators.error_logging import error_logging
from dependencies.db import get_session from dependencies.db import get_session
from external.gpx_parser import GPXParser from graphql_schema.entities.resolvers.flight import (
from graphql_schema.entities.aircraft import Aircraft handle_aircraft_save, handle_weather_info, FlightMutationResolver, get_airport,
from graphql_schema.entities.airport import Airport
from graphql_schema.entities.photo import Photo
from graphql_schema.entities.poi import PointOfInterest
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input
from upload_utils import get_public_url
from .helpers.flight import (
handle_aircraft_save, handle_track_edit, handle_copilots_edit, handle_weather_info, get_airports,
handle_upload_gpx, handle_airport_changed, handle_combobox_save
) )
from graphql_schema.entities.types.mutation_input import EditFlightInput, CreateFlightInput
from .resolvers.base import get_list, get_one from .resolvers.base import get_list, get_one
from ..dataloaders.multi_models import flight_copilots_dataloader, flight_track_dataloader, photos_dataloader from graphql_schema.entities.types.types import Flight
from ..dataloaders.single_model import (
poi_dataloader, event_dataloader, aircraft_dataloader, airport_dataloader, cover_photo_loader,
airport_weather_info_loader
)
from ..types import ComboboxInput
if TYPE_CHECKING:
from .copilot import Copilot
from .event import Event
@strawberry_sqlalchemy_type(models.FlightTrack)
class FlightTrack:
point_of_interest: PointOfInterest = strawberry.field(
resolver=lambda root: poi_dataloader.load(root.point_of_interest_id)
)
@strawberry_sqlalchemy_type(models.WeatherInfo)
class WeatherInfo:
pass
@strawberry.type
class Point:
lat: float
lng: float
@strawberry.type
class GPXTrack:
coordinates: List[Point]
speed: List[float]
altitude: List[float]
magnetic_variation: List[float]
terrain_elevation: List[float]
time: List[datetime]
max_speed: float
avg_speed: float
max_altitude: float
avg_altitude: float
@strawberry_sqlalchemy_type(models.Flight)
class Flight:
def duration_min_calculated(root):
if root.duration_total:
return root.duration_total
if root.takeoff_datetime and root.landing_datetime:
diff: timedelta = root.landing_datetime - root.takeoff_datetime
return diff.seconds / 60
return 0
async def load_gpx_track(root):
if not root.gpx_track_filename:
return None
try:
gpx_parser = GPXParser(f"/app/uploads/tracks/{root.gpx_track_filename}")
except OSError:
return None
return GPXTrack(
coordinates=[Point(**point) for point in await gpx_parser.get_coordinates()],
speed=await gpx_parser.get_speed(),
altitude=await gpx_parser.get_altitude(),
terrain_elevation=await gpx_parser.get_terrain_elevation(),
time=await gpx_parser.get_times(),
max_speed=await gpx_parser.get_max_speed(),
avg_speed=await gpx_parser.get_avg_speed(),
max_altitude=await gpx_parser.get_max_altitude(),
avg_altitude=await gpx_parser.get_avg_altitude(),
magnetic_variation=await gpx_parser.get_magnetic_variation(),
)
def load_gpx_track_url(root):
if not root.gpx_track_filename:
return None
return get_public_url(f"/tracks/{root.gpx_track_filename}")
@authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
async def load_copilots(root):
return await flight_copilots_dataloader.load(root.id)
@authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
async def load_event(root):
return await event_dataloader.load(root.event_id)
duration_min_calculated: int = strawberry.field(resolver=duration_min_calculated)
copilots: Optional[List[Annotated["Copilot", strawberry.lazy(".copilot")]]] = strawberry.field(resolver=load_copilots) # noqa
event: Optional[Annotated["Event", strawberry.lazy(".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))
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)
)
landing_weather_info: Optional[WeatherInfo] = strawberry.field(
resolver=lambda root: airport_weather_info_loader.load(root.landing_weather_info_id)
)
photos: List[Photo] = strawberry.field(resolver=lambda root: photos_dataloader.load(root.id))
gpx_track_url: Optional[str] = strawberry.field(resolver=load_gpx_track_url) # TODO: odstranit
gpx_track: Optional[GPXTrack] = strawberry.field(resolver=load_gpx_track)
def get_base_query(user_id: Optional[int], username: Optional[str] = None, is_auth: bool = False): def get_base_query(user_id: Optional[int], username: Optional[str] = None, is_auth: bool = False):
@@ -186,25 +67,15 @@ class FlightQueries:
@strawberry.type @strawberry.type
class CreateFlightMutation: class FlightMutation:
@strawberry_sqlalchemy_input(models.Flight, exclude_fields=[
"id", "aircraft_id", "landing_airport_id", "takeoff_airport_id", "weather_info_takeoff_id",
"weather_info_landing_id", "with_instructor", "has_terrain_elevation"
])
class CreateFlightInput:
aircraft: ComboboxInput
landing_airport: ComboboxInput
takeoff_airport: ComboboxInput
@strawberry.mutation @strawberry.mutation
@authenticated_user_only() @authenticated_user_only()
async def create_flight(self, info, input: CreateFlightInput) -> Flight: async def create_flight(self, info, input: CreateFlightInput) -> Flight:
data = input.to_dict() data = input.to_dict()
async with get_session() as db: async with (get_session() as db):
takeoff_airport, landing_airport = await get_airports( takeoff_airport = await get_airport(db, input.takeoff_airport, info.context.user_id)
db, input.takeoff_airport, input.landing_airport, info.context.user_id, landing_airport = await get_airport(db, input.landing_airport, info.context.user_id)
)
aircraft_id = await handle_aircraft_save(db, info.context.user_id, input.aircraft) aircraft_id = await handle_aircraft_save(db, info.context.user_id, input.aircraft)
weather_takeoff, weather_landing = await asyncio.gather( weather_takeoff, weather_landing = await asyncio.gather(
@@ -225,112 +96,12 @@ class CreateFlightMutation:
}) })
return Flight(**flight.as_dict()) return Flight(**flight.as_dict())
@strawberry.type
class EditFlightMutation:
@strawberry_sqlalchemy_input(models.Flight, exclude_fields=[
"id", "aircraft_id", "deleted", "landing_airport_id", "takeoff_airport_id",
"takeoff_weather_info_id", "landing_weather_info_id", "gpx_track_filename"
], all_optional=True)
class EditFlightInput:
gpx_track: Optional[Upload] = None # TODO: poresit validaci uploadovaneho souboru!
track: Optional[List[ComboboxInput]] = None
copilots: Optional[List[ComboboxInput]] = None
aircraft: Optional[ComboboxInput] = None
landing_airport: Optional[ComboboxInput] = None
takeoff_airport: Optional[ComboboxInput] = None
event: Optional[ComboboxInput] = None
@strawberry.mutation @strawberry.mutation
@authenticated_user_only() @authenticated_user_only()
async def edit_flight(self, info, id: int, input: EditFlightInput) -> Flight: async def edit_flight(self, info, id: int, input: EditFlightInput) -> Flight:
user_id = info.context.user_id return await FlightMutationResolver.update(info.context, id, input)
async with get_session() as db:
flight = (await db.scalars(
get_base_query(user_id=user_id, is_auth=bool(user_id)).filter(models.Flight.id == id)
)).one()
data = input.to_dict()
# TODO: nahravani a zpracovani presunout mimo DB transakci!
if input.gpx_track is not None:
data['gpx_track_filename'] = await handle_upload_gpx(flight, input.gpx_track)
info.context.background_tasks.add_task(
add_terrain_elevation_to_flight, flight=flight.as_dict(), gpx_filename=data['gpx_track_filename']
)
if input.takeoff_airport and input.landing_airport:
takeoff_airport, landing_airport = await get_airports(
db, input.takeoff_airport, input.landing_airport, info.context.user_id,
)
if (
(input.takeoff_airport and input.takeoff_airport.id != flight.takeoff_airport_id) or
(data.get('takeoff_datetime') and data.get('takeoff_datetime') != flight.takeoff_datetime)
):
await handle_airport_changed(
db,
flight,
takeoff_airport,
type_="takeoff",
input_datetime=data.get('takeoff_datetime')
)
if (
(input.landing_airport and input.landing_airport.id != flight.landing_airport_id) or
(data.get('landing_datetime') and data.get('landing_datetime') != flight.takeoff_datetime)
):
await handle_airport_changed(
db,
flight,
landing_airport,
type_="landing",
input_datetime=data.get('landing_datetime')
)
# ///////////// konec editace s letistem - je to hnusny
if input.aircraft is not None:
data['aircraft_id'] = await handle_aircraft_save(db, user_id, input.aircraft)
if input.event is not None:
data['event_id'] = await handle_combobox_save(
db,
model=models.Event,
input=input.event,
extra_data={"description": "", "is_public": False},
user_id=info.context.user_id
)
if input.track is not None:
await handle_track_edit(db=db, flight=flight, track=input.track, user_id=user_id)
copilots = await handle_copilots_edit(db, input.copilots or [], user_id)
await db.execute(delete(flight_has_copilot).filter_by(flight_id=flight.id))
for copilot_id in copilots:
await db.execute(insert(flight_has_copilot).values(flight_id=flight.id, copilot_id=copilot_id))
updated_flight = await models.Flight.update(db, obj=flight, data=data)
return Flight(**updated_flight.as_dict())
@strawberry.type
class DeleteFlightMutation:
@strawberry.mutation @strawberry.mutation
@authenticated_user_only() @authenticated_user_only()
async def delete_flight(self, info, id: int) -> Flight: async def delete_flight(self, info, id: int) -> Flight:
async with get_session() as db: return await FlightMutationResolver.delete(info.context.user_id, id)
flight = (
(await db.scalars(
get_base_query(user_id=info.context.user_id, is_auth=True)
.filter(models.Flight.id == id))
)
.one()
)
updated_flight = await models.Flight.update(db, obj=flight, data=dict(deleted=True))
return Flight(**updated_flight.as_dict())
@@ -1,7 +1,7 @@
from typing import Type, Optional from typing import Type, Optional
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from database import models from database import models
from graphql_schema.types import ComboboxInput from graphql_schema.entities.types.mutation_input import ComboboxInput
async def handle_combobox_save( async def handle_combobox_save(
@@ -1,156 +0,0 @@
import asyncio
from datetime import datetime
from typing import List, Literal, Optional, Tuple
from sqlalchemy import select, delete
from sqlalchemy.ext.asyncio import AsyncSession
from strawberry.file_uploads import Upload
from database import models
from external.weather import Weather
from graphql_schema.entities.helpers.combobox import handle_combobox_save
from graphql_schema.types import ComboboxInput
from upload_utils import delete_file, handle_file_upload
weather_api = Weather()
async def handle_weather_info(
db: AsyncSession, date_time: datetime, airport: models.Airport
) -> Optional[models.WeatherInfo]:
if not airport.gps_latitude or not airport.gps_longitude:
return None
try:
weather = await weather_api.get_weather_for_hour(
date_time.astimezone(),
gps=(airport.gps_latitude, airport.gps_longitude)
)
except Exception as e:
print(e)
return None
model = models.WeatherInfo(**{
"datetime": weather['datetime'],
"qnh": weather['pressure_msl'],
"temperature_surface": weather['temperature_2m'],
"dewpoint_surface": weather['dewpoint_2m'],
"rain": weather['rain'],
"cloudcover_total": weather['cloudcover'],
"cloudcover_low": weather['cloudcover_low'],
"wind_speed_surface": weather['windspeed_10m'],
"wind_direction_surface": weather['winddirection_10m'],
})
db.add(model)
return model
async def handle_track_edit(db: AsyncSession, flight: models.Flight, track: List[ComboboxInput], user_id: int):
await db.execute(delete(models.FlightTrack).filter(models.FlightTrack.flight_id == flight.id))
existing_poi_ids = [i.id for i in track if i.id]
poi_query = (
select(models.PointOfInterest)
.filter(models.PointOfInterest.created_by_id == user_id)
.filter(models.PointOfInterest.id.in_(existing_poi_ids))
)
pois = (await db.scalars(poi_query)).all()
poi_map = {poi.id: poi for poi in pois}
order = 0
for item in track:
poi_object = None
if item.id:
poi_object = poi_map.get(item.id)
if not poi_object:
poi_object = await models.PointOfInterest.create(
db,
data=dict(created_by_id=user_id, name=item.name, description="")
)
await db.flush()
await models.FlightTrack.create(
db,
data={
"flight_id": flight.id,
"point_of_interest_id": poi_object.id,
"order": order
}
)
order += 1
async def handle_aircraft_save(db: AsyncSession, user_id: int, aircraft: ComboboxInput):
return await handle_combobox_save(
db, models.Aircraft, aircraft, user_id,
name_column="call_sign",
extra_data={
"description": "",
"model": "",
"seats": 2,
"manufacturer": "",
})
async def get_airports(
db, takeoff_airport: ComboboxInput, landing_airport: ComboboxInput, user_id: int
) -> Tuple[models.Airport, models.Airport]:
takeoff_airport_id = await handle_combobox_save(
db, models.Airport, takeoff_airport, user_id,
name_column="icao_code", extra_data={"name": takeoff_airport.name}
)
if landing_airport.id != takeoff_airport_id or landing_airport.name != takeoff_airport.name:
landing_airport_id = await handle_combobox_save(
db, models.Airport, landing_airport, user_id,
name_column="icao_code",
extra_data={"name": landing_airport.name}
)
else:
landing_airport_id = takeoff_airport_id
takeoff_airport = (await db.scalars(
select(models.Airport).filter(models.Airport.id == takeoff_airport_id)
)).one()
if takeoff_airport_id == landing_airport_id:
landing_airport = takeoff_airport
else:
landing_airport = (await db.scalars(
select(models.Airport).filter(models.Airport.id == landing_airport_id)
)).one()
return takeoff_airport, landing_airport
async def handle_airport_changed(
db, flight: models.Flight, airport: models.Airport, type_: Literal['takeoff', 'landing'],
input_datetime: Optional[datetime]
):
flight_datetime = getattr(flight, f"{type_}_datetime")
if input_datetime and input_datetime != flight_datetime:
existing_weather_id = getattr(flight, f"{type_}_weather_info_id")
if existing_weather_id:
# db.delete(delete())
pass
weather = await handle_weather_info(db, input_datetime, airport)
await db.flush()
if weather:
setattr(flight, f"{type_}_weather_info_id", weather.id)
setattr(flight, f"{type_}_airport_id", airport.id)
setattr(flight, f"{type_}_datetime", input_datetime)
async def handle_upload_gpx(flight: models.Flight, gpx_track: Upload):
path = "/app/uploads/tracks"
if flight.gpx_track_filename:
delete_file(path + "/" + flight.gpx_track_filename, silent=True)
return await handle_file_upload(gpx_track, path)
async def handle_copilots_edit(db: AsyncSession, copilots: List[ComboboxInput], user_id: int) -> tuple:
cors = [handle_combobox_save(db, models.Copilot, copilot, user_id) for copilot in copilots]
return await asyncio.gather(*cors)
+4 -15
View File
@@ -1,4 +1,4 @@
from typing import List, Annotated, TYPE_CHECKING from typing import List, TYPE_CHECKING
import strawberry import strawberry
from sqlalchemy import delete from sqlalchemy import delete
from sqlalchemy.dialects.mysql import insert from sqlalchemy.dialects.mysql import insert
@@ -6,23 +6,12 @@ from sqlalchemy.exc import IntegrityError
from database import models from database import models
from decorators.endpoints import authenticated_user_only from decorators.endpoints import authenticated_user_only
from dependencies.db import get_session from dependencies.db import get_session
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
from .resolvers.base import get_base_resolver, get_list, get_one from .resolvers.base import get_base_resolver, get_list, get_one
from ..dataloaders.multi_models import users_in_organization_dataloader, aircrafts_from_organization_dataloader from graphql_schema.entities.types.types import Organization
if TYPE_CHECKING: if TYPE_CHECKING:
from .user import User pass
from .aircraft import Aircraft
@strawberry_sqlalchemy_type(models.Organization)
class Organization:
users: List[Annotated["User", strawberry.lazy(".user")]] = strawberry.field(
resolver=lambda root: users_in_organization_dataloader.load(root.id)
)
aircrafts: List[Annotated["Aircraft", strawberry.lazy(".aircraft")]] = strawberry.field(
resolver=lambda root: aircrafts_from_organization_dataloader.load(root.id)
)
@strawberry.type @strawberry.type
+8 -37
View File
@@ -1,52 +1,23 @@
import asyncio import asyncio
from typing import List, Optional, Annotated, TYPE_CHECKING from typing import List, Optional
import strawberry import strawberry
from sqlalchemy import select, update from sqlalchemy import update
from strawberry.file_uploads import Upload from strawberry.file_uploads import Upload
from background_jobs.elevation import add_terrain_elevation_to_photo from background_jobs.elevation import add_terrain_elevation_to_photo
from database import models from database import models
from decorators.endpoints import authenticated_user_only from decorators.endpoints import authenticated_user_only
from dependencies.db import get_session from dependencies.db import get_session
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type from graphql_schema.entities.types.types import Photo
from graphql_schema.types import ComboboxInput from paths import get_photo_basepath
from upload_utils import ( from upload_utils import (
get_public_url, handle_file_upload, delete_file, parse_exif_info, generate_thumbnail, file_exists, resize_image, handle_file_upload, delete_file, parse_exif_info, generate_thumbnail, resize_image,
rotate_image rotate_image
) )
from graphql_schema.entities.helpers.combobox import handle_combobox_save from graphql_schema.entities.helpers.combobox import handle_combobox_save
from .resolvers.base import get_base_resolver, get_list from .resolvers.base import get_base_resolver, get_list
from ..dataloaders.single_model import poi_dataloader from graphql_schema.entities.types.mutation_input import ComboboxInput
if TYPE_CHECKING:
from .poi import PointOfInterest
@strawberry_sqlalchemy_type(models.Photo)
class Photo:
def resolve_thumb_url(root):
thumbnail = get_photo_basepath(root.flight_id) + "/thumbs/" + root.filename
if not file_exists(thumbnail):
return get_public_url(f"photos/{root.flight_id}/{root.filename}")
return get_public_url(f"photos/{root.flight_id}/thumbs/{root.filename}")
url: str = strawberry.field(resolver=lambda root: get_public_url(f"photos/{root.flight_id}/{root.filename}"))
thumbnail_url: str = strawberry.field(resolver=resolve_thumb_url)
point_of_interest: Optional[Annotated["PointOfInterest", strawberry.lazy('.poi')]] = strawberry.field(
resolver=lambda root: poi_dataloader.load(root.point_of_interest_id)
)
def get_base_query(user_id: int):
return (
select(models.Photo)
.filter(models.Photo.created_by_id == user_id)
.order_by(models.Photo.id.desc())
)
def get_photo_basepath(flight_id: int) -> str:
return f"/app/uploads/photos/{flight_id}"
@strawberry.type @strawberry.type
@@ -173,9 +144,9 @@ class DeletePhotoMutation:
@strawberry.mutation() @strawberry.mutation()
@authenticated_user_only() @authenticated_user_only()
async def delete_photo(self, info, id: int) -> Photo: async def delete_photo(self, info, id: int) -> Photo:
query = get_base_query(info.context.user_id) query = get_base_resolver(models.Photo, user_id=info.context.user_id, object_id=id)
async with get_session() as db: async with get_session() as db:
photo_model = (await db.scalars(query.filter(models.Photo.id == id))).one() photo_model = (await db.scalars(query)).one()
await db.delete(photo_model) await db.delete(photo_model)
photo = Photo(**photo_model.as_dict()) photo = Photo(**photo_model.as_dict())
+4 -24
View File
@@ -1,33 +1,13 @@
from typing import List, Optional, TYPE_CHECKING, Annotated from typing import List, Optional
import strawberry import strawberry
from sqlalchemy import select, or_
from database import models from database import models
from decorators.endpoints import authenticated_user_only from decorators.endpoints import authenticated_user_only
from dependencies.db import get_session from dependencies.db import get_session
from graphql_schema.entities.helpers.combobox import handle_combobox_save from graphql_schema.entities.helpers.combobox import handle_combobox_save
from graphql_schema.entities.poi_type import PointOfInterestType from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input from graphql_schema.entities.types.types import PointOfInterest
from graphql_schema.types import ComboboxInput
from .resolvers.base import get_base_resolver, get_list, get_one from .resolvers.base import get_base_resolver, get_list, get_one
from ..dataloaders.multi_models import flight_by_poi_dataloader, poi_photos_dataloader from graphql_schema.entities.types.mutation_input import ComboboxInput
from ..dataloaders.single_model import poi_type_dataloader
if TYPE_CHECKING:
from .flight import Flight
from .photo import Photo
@strawberry_sqlalchemy_type(models.PointOfInterest)
class PointOfInterest:
type: Optional[PointOfInterestType] = strawberry.field(
resolver=lambda root: poi_type_dataloader.load(root.type_id)
)
photos: List[Annotated["Photo", strawberry.lazy('.photo')]] = strawberry.field(
resolver=lambda root: poi_photos_dataloader.load(root.id)
)
flights: List[Annotated["Flight", strawberry.lazy('.flight')]] = strawberry.field(
resolver=lambda root: flight_by_poi_dataloader.load(root.id)
)
@strawberry.type @strawberry.type
+1 -6
View File
@@ -3,12 +3,7 @@ import strawberry
from database import models from database import models
from decorators.endpoints import authenticated_user_only from decorators.endpoints import authenticated_user_only
from graphql_schema.entities.resolvers.base import get_base_resolver, get_list, get_one from graphql_schema.entities.resolvers.base import get_base_resolver, get_list, get_one
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type from graphql_schema.entities.types.types import PointOfInterestType
@strawberry_sqlalchemy_type(models.PointOfInterestType)
class PointOfInterestType:
pass
@strawberry.type @strawberry.type
@@ -0,0 +1,225 @@
import asyncio
from datetime import datetime
from typing import List, Optional, Type
from aiocache import cached
from sqlalchemy import delete, insert
from sqlalchemy.ext.asyncio import AsyncSession
from strawberry.file_uploads import Upload
from background_jobs.elevation import add_terrain_elevation_to_flight
from database import models
from database.models import flight_has_copilot
from dependencies.db import get_session
from external.weather import weather_api
from graphql_schema.entities.helpers.combobox import handle_combobox_save
from graphql_schema.entities.resolvers.base import get_base_resolver
from graphql_schema.entities.types.mutation_input import EditFlightInput, TrackItemInput, ComboboxInput
from graphql_schema.entities.types.types import Flight
from upload_utils import delete_file, handle_file_upload
class BaseMutationResolver:
model: Type[models.BaseModel]
graphql_type = Flight
@classmethod
async def delete(cls, user_id: int, id: int):
async with get_session() as db:
model = (
(await db.scalars(
get_base_resolver(cls.model, user_id=user_id, object_id=id)
.filter(cls.model.id == id))
)
.one()
)
if hasattr(cls.model, "deleted"):
model = await cls.model.update(db, obj=model, data=dict(deleted=True))
else:
db.delete(model)
return cls.graphql_type(**model.as_dict())
@classmethod
async def _do_update(cls, db: AsyncSession, obj: models.BaseModel | dict, data: dict):
update_where = {}
if isinstance(obj, models.BaseModel):
update_where['obj'] = obj
else:
update_where['id'] = obj['id']
model = await cls.model.update(db, data=data, **update_where)
return cls.graphql_type(**model.as_dict())
async def handle_upload_gpx(original_gpx_filename: str, gpx_track: Upload):
path = "/app/uploads/tracks"
if original_gpx_filename:
delete_file(path + "/" + original_gpx_filename, silent=True)
return await handle_file_upload(gpx_track, path)
class FlightMutationResolver(BaseMutationResolver):
model = models.Flight
graphql_type = Flight
async def create(self):
pass
@classmethod
async def update(cls, context, id: int, input: EditFlightInput):
user_id = context.user_id
async with get_session() as db:
flight = (await db.scalars(get_base_resolver(models.Flight, user_id=user_id, object_id=id))).one()
flight_data = flight.as_dict()
flight_id = flight.id
data = input.to_dict()
if input.gpx_track is not None:
data['gpx_track_filename'] = await handle_upload_gpx(flight_data['gpx_track_filename'], input.gpx_track)
context.background_tasks.add_task(
add_terrain_elevation_to_flight, flight_id=id, gpx_filename=data['gpx_track_filename']
)
async with get_session() as db:
if input.landing_airport:
landing_airport = await get_airport(db, input.landing_airport, user_id)
landing_datetime = input.landing_datetime or flight_data['landing_datetime']
data['landing_airport_id'] = landing_airport.id
data['landing_datetime'] = landing_datetime
weather_info = await handle_weather_info(db, landing_datetime, landing_airport, flight_data['landing_weather_info_id'])
if weather_info:
data['landing_weather_info_id'] = weather_info.id
if input.takeoff_airport:
takeoff_airport = await get_airport(db, input.takeoff_airport, user_id)
takeoff_datetime = input.takeoff_datetime or flight_data['takeoff_datetime']
data['takeoff_airport_id'] = takeoff_airport.id
data['takeoff_datetime'] = takeoff_datetime
weather_info = await handle_weather_info(db, takeoff_datetime, takeoff_airport, flight_data['takeoff_weather_info_id'])
if weather_info:
data['takeoff_weather_info_id'] = weather_info.id
if input.aircraft is not None:
data['aircraft_id'] = await handle_aircraft_save(db, user_id, input.aircraft)
if input.event is not None:
data['event_id'] = await handle_combobox_save(
db,
model=models.Event,
input=input.event,
extra_data={"description": "", "is_public": False},
user_id=context.user_id
)
if input.track is not None:
await handle_track_edit(db=db, flight_id=flight_id, track=input.track, user_id=user_id)
if input.copilots is not None:
await db.execute(delete(flight_has_copilot).filter_by(flight_id=flight_id))
copilots = await handle_copilots_edit(db, input.copilots or [], user_id)
for copilot_id in copilots:
await db.execute(insert(flight_has_copilot).values(flight_id=flight_id, copilot_id=copilot_id))
return await cls._do_update(db, flight_data, data)
async def handle_weather_info(
db: AsyncSession, date_time: datetime, airport: models.Airport, existing_weather_id: Optional[int] = None
) -> Optional[models.WeatherInfo]:
if not airport.gps_latitude or not airport.gps_longitude:
return None
try:
weather = await weather_api.get_weather_for_hour(
date_time.astimezone(),
gps=(airport.gps_latitude, airport.gps_longitude)
)
except Exception as e:
print(e)
return None
data = {
"datetime": weather['datetime'],
"qnh": weather['pressure_msl'],
"temperature_surface": weather['temperature_2m'],
"dewpoint_surface": weather['dewpoint_2m'],
"rain": weather['rain'],
"cloudcover_total": weather['cloudcover'],
"cloudcover_low": weather['cloudcover_low'],
"wind_speed_surface": weather['windspeed_10m'],
"wind_direction_surface": weather['winddirection_10m'],
}
if existing_weather_id:
model = await models.WeatherInfo.update(db, id=existing_weather_id, data=data)
else:
model = await models.WeatherInfo.create(db, data=data)
return model
async def handle_track_edit(db: AsyncSession, flight_id: int, track: List[TrackItemInput], user_id: int):
await db.execute(delete(models.FlightTrack).filter(models.FlightTrack.flight_id == flight_id))
order = 0
for item in track:
poi_id = None
airport_id = None
if item.point_of_interest:
poi_id = await handle_combobox_save(
db, models.PointOfInterest, item.point_of_interest, user_id, extra_data={"description": ""}
)
if item.airport:
airport_id = await handle_combobox_save(
db, models.Airport, item.airport, user_id,
name_column="icao_code",
extra_data={"name": item.airport.name}
)
await models.FlightTrack.create(
db,
data={
"flight_id": flight_id,
"point_of_interest_id": poi_id,
"airport_id": airport_id,
"order": order,
"landing_duration": item.landing_duration
}
)
order += 1
async def handle_aircraft_save(db: AsyncSession, user_id: int, aircraft: ComboboxInput):
return await handle_combobox_save(
db, models.Aircraft, aircraft, user_id,
name_column="call_sign",
extra_data={
"description": "",
"model": "",
"seats": 2,
"manufacturer": "",
})
@cached()
async def get_airport(db: AsyncSession, input: ComboboxInput, user_id: int):
airport_id = await handle_combobox_save(
db, models.Airport, input, user_id, name_column="icao_code", extra_data={"name": input.name}
)
return (await db.scalars(get_base_resolver(models.Airport, object_id=airport_id))).one()
async def handle_copilots_edit(db: AsyncSession, copilots: List[ComboboxInput], user_id: int) -> tuple:
cors = [handle_combobox_save(db, models.Copilot, copilot, user_id) for copilot in copilots]
return await asyncio.gather(*cors)
@@ -0,0 +1,44 @@
from __future__ import annotations
from typing import Optional, List
import strawberry
from strawberry.file_uploads import Upload
from database import models
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
@strawberry.input()
class ComboboxInput:
id: Optional[int] = None
name: str
@strawberry_sqlalchemy_input(models.Flight, exclude_fields=[
"id", "aircraft_id", "deleted", "landing_airport_id", "takeoff_airport_id",
"takeoff_weather_info_id", "landing_weather_info_id", "gpx_track_filename"
], all_optional=True)
class EditFlightInput:
gpx_track: Optional[Upload] = None # TODO: poresit validaci uploadovaneho souboru!
track: Optional[List[TrackItemInput]] = None
copilots: Optional[List[ComboboxInput]] = None
aircraft: Optional[ComboboxInput] = None
landing_airport: Optional[ComboboxInput] = None
takeoff_airport: Optional[ComboboxInput] = None
event: Optional[ComboboxInput] = None
@strawberry_sqlalchemy_input(models.Flight, exclude_fields=[
"id", "aircraft_id", "landing_airport_id", "takeoff_airport_id", "weather_info_takeoff_id",
"weather_info_landing_id", "with_instructor", "has_terrain_elevation"
])
class CreateFlightInput:
aircraft: ComboboxInput
landing_airport: ComboboxInput
takeoff_airport: ComboboxInput
@strawberry.input()
class TrackItemInput:
# order: int
point_of_interest: Optional[ComboboxInput] = None
airport: Optional[ComboboxInput] = None
landing_duration: Optional[int] = None
+211
View File
@@ -0,0 +1,211 @@
from __future__ import annotations
from datetime import datetime, timedelta
from typing import Optional, Annotated, List
import strawberry
from sqlalchemy import func, select
from config import API_URL
from database import models
from decorators.endpoints import authenticated_user_only
from dependencies.db import get_session
from external.gpx_parser import GPXParser
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
from graphql_schema.dataloaders.single_model import poi_dataloader, poi_type_dataloader, event_dataloader, aircraft_dataloader, airport_dataloader, cover_photo_loader, airport_weather_info_loader, organizations_dataloader
from graphql_schema.entities.airport import Airport
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type
from paths import get_photo_basepath
from upload_utils import file_exists, get_public_url
@strawberry_sqlalchemy_type(models.FlightTrack)
class FlightTrack:
point_of_interest: Optional[PointOfInterest] = strawberry.field(
resolver=lambda root: poi_dataloader.load(root.point_of_interest_id)
)
airport: Optional[Airport] = strawberry.field(
resolver=lambda root: airport_dataloader.load(root.airport_id)
)
@strawberry_sqlalchemy_type(models.PointOfInterestType)
class PointOfInterestType:
pass
@strawberry_sqlalchemy_type(models.WeatherInfo)
class WeatherInfo:
pass
@strawberry.type
class Point:
lat: float
lng: float
@strawberry_sqlalchemy_type(models.PointOfInterest)
class PointOfInterest:
type: Optional[PointOfInterestType] = strawberry.field(
resolver=lambda root: poi_type_dataloader.load(root.type_id)
)
photos: List[Annotated["Photo", strawberry.lazy('.photo')]] = strawberry.field(
resolver=lambda root: poi_photos_dataloader.load(root.id)
)
flights: List[Annotated["Flight", strawberry.lazy('.flight')]] = strawberry.field(
resolver=lambda root: flight_by_poi_dataloader.load(root.id)
)
@strawberry_sqlalchemy_type(models.Photo)
class Photo:
def resolve_thumb_url(root):
thumbnail = get_photo_basepath(root.flight_id) + "/thumbs/" + root.filename
if not file_exists(thumbnail):
return get_public_url(f"photos/{root.flight_id}/{root.filename}")
return get_public_url(f"photos/{root.flight_id}/thumbs/{root.filename}")
url: str = strawberry.field(resolver=lambda root: get_public_url(f"photos/{root.flight_id}/{root.filename}"))
thumbnail_url: str = strawberry.field(resolver=resolve_thumb_url)
point_of_interest: Optional[Annotated["PointOfInterest", strawberry.lazy('.poi')]] = strawberry.field(
resolver=lambda root: poi_dataloader.load(root.point_of_interest_id)
)
@strawberry.type
class GPXTrack:
coordinates: List[Point]
speed: List[float]
altitude: List[float]
magnetic_variation: List[float]
terrain_elevation: List[float]
time: List[datetime]
max_speed: float
avg_speed: float
max_altitude: float
avg_altitude: float
@strawberry_sqlalchemy_type(models.Flight)
class Flight:
async def duration_min_calculated(root):
# TODO: predelat na dataloader, dobu nacitat v DB
diff: timedelta = root.landing_datetime - root.takeoff_datetime
total_time_minutes = diff.seconds / 60
async with get_session() as db:
landing_durations = (await db.scalars(
select(func.sum(models.FlightTrack.landing_duration))
.filter(models.FlightTrack.airport_id.isnot(None))
.filter(models.FlightTrack.flight_id == root.id)
)).one() or 0
print(landing_durations)
return total_time_minutes - float(landing_durations)
async def load_gpx_track(root):
if not root.gpx_track_filename:
return None
try:
gpx_parser = GPXParser(f"/app/uploads/tracks/{root.gpx_track_filename}")
except OSError:
return None
return GPXTrack(
coordinates=[Point(**point) for point in await gpx_parser.get_coordinates()],
speed=await gpx_parser.get_speed(),
altitude=await gpx_parser.get_altitude(),
terrain_elevation=await gpx_parser.get_terrain_elevation(),
time=await gpx_parser.get_times(),
max_speed=await gpx_parser.get_max_speed(),
avg_speed=await gpx_parser.get_avg_speed(),
max_altitude=await gpx_parser.get_max_altitude(),
avg_altitude=await gpx_parser.get_avg_altitude(),
magnetic_variation=await gpx_parser.get_magnetic_variation(),
)
@authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
async def load_copilots(root):
return await flight_copilots_dataloader.load(root.id)
@authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
async def load_event(root):
return await event_dataloader.load(root.event_id)
duration_min_calculated: int = strawberry.field(resolver=duration_min_calculated)
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))
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)
)
landing_weather_info: Optional[WeatherInfo] = strawberry.field(
resolver=lambda root: airport_weather_info_loader.load(root.landing_weather_info_id)
)
photos: List[Photo] = strawberry.field(resolver=lambda root: photos_dataloader.load(root.id))
gpx_track_url: Optional[str] = strawberry.field(
resolver=lambda root: get_public_url(f"/tracks/{root.gpx_track_filename}") if root.gpx_track_filename else None
) # TODO: odstranit
gpx_track: Optional[GPXTrack] = strawberry.field(resolver=load_gpx_track)
@strawberry_sqlalchemy_type(models.Copilot)
class Copilot:
flights: List[Annotated["Flight", strawberry.lazy('.flight')]] = strawberry.field(
resolver=lambda root: flights_by_copilot_dataloader.load(root.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
)
flights: List[Annotated["Flight", strawberry.lazy('.flight')]] = strawberry.field(
resolver=lambda root: flights_by_aircraft_dataloader.load(root.id)
)
organization: Optional[Annotated["Organization", strawberry.lazy(".organization")]] = strawberry.field(
resolver=lambda root: organizations_dataloader.load(root.organization_id)
)
@strawberry_sqlalchemy_type(models.Organization)
class Organization:
users: List[Annotated["User", strawberry.lazy(".user")]] = strawberry.field(
resolver=lambda root: users_in_organization_dataloader.load(root.id)
)
aircrafts: List[Annotated["Aircraft", strawberry.lazy(".aircraft")]] = strawberry.field(
resolver=lambda root: aircrafts_from_organization_dataloader.load(root.id)
)
@strawberry_sqlalchemy_type(models.User, exclude_fields=['password_hashed'])
class User:
async def load_avatar_image_url(root):
if not root.avatar_image_filename:
return None
return get_public_url(f"profile/{root.id}/{root.avatar_image_filename}")
async def load_title_image_url(root):
if not root.title_image_filename:
return f"{API_URL}/static/default-title-image.jpg"
return get_public_url(f"profile/{root.id}/{root.title_image_filename}")
avatar_image_url: Optional[str] = strawberry.field(resolver=load_avatar_image_url)
title_image_url: str = strawberry.field(resolver=load_title_image_url)
organizations: List[Annotated['Organization', strawberry.lazy(".organization")]] = strawberry.field(
resolver=lambda root: user_organizations_dataloader.load(root.id)
)
@strawberry_sqlalchemy_type(models.Event)
class Event:
flights: List[Annotated["Flight", strawberry.lazy('.flight')]] = strawberry.field(
resolver=lambda root: flights_by_event_dataloader.load(root.id)
)
+4 -27
View File
@@ -1,41 +1,18 @@
from typing import Optional, List, Annotated, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
import strawberry import strawberry
from graphql import GraphQLError from graphql import GraphQLError
from passlib.hash import bcrypt from passlib.hash import bcrypt
from sqlalchemy import select from sqlalchemy import select
from strawberry.file_uploads import Upload from strawberry.file_uploads import Upload
from config import API_URL
from database import models from database import models
from decorators.endpoints import authenticated_user_only from decorators.endpoints import authenticated_user_only
from decorators.error_logging import error_logging from decorators.error_logging import error_logging
from dependencies.db import get_session from dependencies.db import get_session
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type from upload_utils import handle_file_upload, delete_file, resize_image
from upload_utils import handle_file_upload, delete_file, get_public_url, resize_image from graphql_schema.entities.types.types import User
from ..dataloaders.multi_models import user_organizations_dataloader
if TYPE_CHECKING: if TYPE_CHECKING:
from .organization import Organization pass
@strawberry_sqlalchemy_type(models.User, exclude_fields=['password_hashed'])
class User:
async def load_avatar_image_url(root):
if not root.avatar_image_filename:
return None
return get_public_url(f"profile/{root.id}/{root.avatar_image_filename}")
async def load_title_image_url(root):
if not root.title_image_filename:
return f"{API_URL}/static/default-title-image.jpg"
return get_public_url(f"profile/{root.id}/{root.title_image_filename}")
avatar_image_url: Optional[str] = strawberry.field(resolver=load_avatar_image_url)
title_image_url: str = strawberry.field(resolver=load_title_image_url)
organizations: List[Annotated['Organization', strawberry.lazy(".organization")]] = strawberry.field(
resolver=lambda root: user_organizations_dataloader.load(root.id)
)
@strawberry.type @strawberry.type
+2 -4
View File
@@ -2,7 +2,7 @@ from strawberry.tools import merge_types
from graphql_schema.entities.aircraft import CreateAircraftMutation, EditAircraftMutation, DeleteAircraftMutation from graphql_schema.entities.aircraft import CreateAircraftMutation, EditAircraftMutation, DeleteAircraftMutation
from graphql_schema.entities.copilot import CreateCopilotMutation, EditCopilotMutation from graphql_schema.entities.copilot import CreateCopilotMutation, EditCopilotMutation
from graphql_schema.entities.event import CreateEventMutation, EditEventMutation from graphql_schema.entities.event import CreateEventMutation, EditEventMutation
from graphql_schema.entities.flight import CreateFlightMutation, EditFlightMutation, DeleteFlightMutation from graphql_schema.entities.flight import FlightMutation
from graphql_schema.entities.organization import ( from graphql_schema.entities.organization import (
CreateOrganizationMutation, EditOrganizationMutation, OrganizationUserMutation CreateOrganizationMutation, EditOrganizationMutation, OrganizationUserMutation
) )
@@ -14,9 +14,7 @@ Mutation = merge_types("Mutation", (
CreateAircraftMutation, CreateAircraftMutation,
EditAircraftMutation, EditAircraftMutation,
DeleteAircraftMutation, DeleteAircraftMutation,
EditFlightMutation, FlightMutation,
DeleteFlightMutation,
CreateFlightMutation,
UploadPhotoMutation, UploadPhotoMutation,
EditPhotoMutation, EditPhotoMutation,
DeletePhotoMutation, DeletePhotoMutation,
-8
View File
@@ -1,8 +0,0 @@
from typing import Optional
import strawberry
@strawberry.input()
class ComboboxInput:
id: Optional[int] = None
name: str
+3
View File
@@ -1,3 +1,6 @@
PHOTO_BASE_PATH = "" PHOTO_BASE_PATH = ""
AIRCRAFT_BASE_PATH = "" AIRCRAFT_BASE_PATH = ""
FLIGHT_BASE_PATH = "" FLIGHT_BASE_PATH = ""
def get_photo_basepath(flight_id: int) -> str:
return f"/app/uploads/photos/{flight_id}"