Refaktoring a bugfixing
This commit is contained in:
@@ -1,21 +1,20 @@
|
||||
from aiohttp import ClientResponseError
|
||||
from database import models
|
||||
from dependencies.db import get_session
|
||||
from database.transaction import get_session
|
||||
from external.elevation import elevation_api
|
||||
from external.gpx_parser import GPXParser
|
||||
from paths import FLIGHT_GPX_TRACK_PATH
|
||||
|
||||
|
||||
async def add_terrain_elevation_to_flight(flight_id: int, gpx_filename: str):
|
||||
path = "/app/uploads/tracks" # TODO vytahnout do configu
|
||||
|
||||
gpx_parser = GPXParser(f"{path}/{gpx_filename}")
|
||||
gpx_parser = GPXParser(f"{FLIGHT_GPX_TRACK_PATH}/{gpx_filename}")
|
||||
coordinates = await gpx_parser.get_coordinates()
|
||||
|
||||
try:
|
||||
elevation = await elevation_api.get_elevation_for_points(coordinates)
|
||||
tree_with_elevation = gpx_parser.add_terrain_elevation(elevation)
|
||||
output_name = f"terrain_{gpx_filename}"
|
||||
gpx_parser.write(tree_with_elevation, f"{path}/{output_name}")
|
||||
gpx_parser.write(tree_with_elevation, f"{FLIGHT_GPX_TRACK_PATH}/{output_name}")
|
||||
|
||||
async with get_session() as db:
|
||||
await models.Flight.update(
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
from utils.image import resize_image
|
||||
|
||||
|
||||
async def resize_photo(path: str, filename: str):
|
||||
return await resize_image(path, filename, new_width=2500, quality=85)
|
||||
|
||||
|
||||
async def generate_thumbnail(path: str, filename: str, quality: int = 90):
|
||||
return await resize_image(
|
||||
path, filename,
|
||||
new_width=300,
|
||||
dest_path=f"{path}/thumbs/",
|
||||
dest_filename=filename,
|
||||
quality=quality
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
from database import models
|
||||
from database.transaction import get_session
|
||||
from external.weather import weather_api
|
||||
from logger import log
|
||||
|
||||
|
||||
async def download_weather(date_time: datetime, flight_id: int, airport_id: int, type_: Literal['landing', 'takeoff']):
|
||||
log.error("AAAAAAAAAAAAAAAAAAAAAAAAAAA")
|
||||
async with get_session() as db:
|
||||
airport = await models.Airport.get_one(db, airport_id)
|
||||
gps = (airport.gps_latitude, airport.gps_longitude)
|
||||
|
||||
try:
|
||||
weather = await weather_api.get_weather_for_hour(date_time.astimezone(), gps=gps)
|
||||
except Exception as e:
|
||||
log.error(f"Error in downloading weather: {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'],
|
||||
}
|
||||
|
||||
async with get_session() as db:
|
||||
flight = await models.Flight.get_one(db, flight_id)
|
||||
existing_weather_id = getattr(flight, f'{type_}_weather_info_id')
|
||||
|
||||
if existing_weather_id:
|
||||
await models.WeatherInfo.update(db, id=existing_weather_id, data=data)
|
||||
else:
|
||||
weather_model = await models.WeatherInfo.create(db, data=data)
|
||||
await models.Flight.update(db, obj=flight, data={f"{type_}_weather_info_id": weather_model.id})
|
||||
@@ -10,12 +10,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
class BaseModel:
|
||||
excluded_columns_in_dict = ("deleted",)
|
||||
|
||||
@classmethod
|
||||
def _get_column_names(cls):
|
||||
return [col.name for col in cls.__table__.columns]
|
||||
|
||||
def as_dict(self):
|
||||
return {
|
||||
c.name: getattr(self, c.name)
|
||||
for c in self.__table__.columns
|
||||
if c.name not in self.excluded_columns_in_dict
|
||||
}
|
||||
return {c: getattr(self, c) for c in self._get_column_names() if c not in self.excluded_columns_in_dict}
|
||||
|
||||
@classmethod
|
||||
async def get_one(cls, db_session: AsyncSession, id: int):
|
||||
@@ -23,7 +23,7 @@ class BaseModel:
|
||||
|
||||
@classmethod
|
||||
async def create(cls, db_session: AsyncSession, data: dict):
|
||||
model = cls(**data)
|
||||
model = cls(**{col: data[col] for col in cls._get_column_names() if col in data})
|
||||
db_session.add(model)
|
||||
await db_session.flush()
|
||||
|
||||
@@ -37,7 +37,7 @@ class BaseModel:
|
||||
if not obj:
|
||||
obj = await cls.get_one(db_session, id)
|
||||
for key, value in data.items():
|
||||
if getattr(obj, key) != value:
|
||||
if key in cls._get_column_names() and getattr(obj, key) != value:
|
||||
setattr(obj, key, value)
|
||||
|
||||
return obj
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# from fastapi import Depends, Security
|
||||
# from fastapi_jwt import JwtAuthorizationCredentials
|
||||
#
|
||||
#
|
||||
# async def check_jwt_token():
|
||||
# authorize.jwt_required()
|
||||
#
|
||||
# subject = authorize.get_jwt_subject()
|
||||
#
|
||||
# print("Subject: ", subject)
|
||||
@@ -4,7 +4,7 @@ from passlib.hash import bcrypt
|
||||
from sqlalchemy import select
|
||||
from starlette.responses import Response
|
||||
from database.models import User
|
||||
from dependencies.db import get_session
|
||||
from database.transaction import get_session
|
||||
from endpoints.base import AuthEndpoint
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from sqlalchemy import select
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, root_validator, Field
|
||||
from database.models import User
|
||||
from dependencies.db import get_session
|
||||
from database.transaction import get_session
|
||||
from passlib.hash import bcrypt
|
||||
|
||||
|
||||
|
||||
Vendored
+5
-3
@@ -53,16 +53,19 @@ class GPXParser:
|
||||
|
||||
@cached()
|
||||
async def get_max_speed(self):
|
||||
return max(await self.get_speed())
|
||||
return max(await self.get_speed()) or 0
|
||||
|
||||
@cached()
|
||||
async def get_avg_speed(self):
|
||||
speeds = await self.get_speed()
|
||||
if not speeds:
|
||||
return 0
|
||||
|
||||
return round(sum(speeds) / len(speeds), 2)
|
||||
|
||||
@cached()
|
||||
async def get_max_altitude(self):
|
||||
return max(await self.get_altitude())
|
||||
return max(await self.get_altitude()) or 0
|
||||
|
||||
@cached()
|
||||
async def get_avg_altitude(self):
|
||||
@@ -89,5 +92,4 @@ class GPXParser:
|
||||
return self.gpx
|
||||
|
||||
def write(self, tree: _ElementTree, output: str):
|
||||
print(f"ZAPISUJI DO {output}")
|
||||
tree.write(output)
|
||||
|
||||
@@ -1,56 +1,46 @@
|
||||
from typing import List, Optional, TYPE_CHECKING
|
||||
from typing import List
|
||||
import strawberry
|
||||
from strawberry.file_uploads import Upload
|
||||
from database import models
|
||||
from decorators.endpoints import authenticated_user_only
|
||||
from dependencies.db import get_session
|
||||
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
|
||||
from upload_utils import handle_file_upload, delete_file
|
||||
from database.transaction import get_session
|
||||
from graphql_schema.entities.helpers.combobox import handle_combobox_save
|
||||
from .resolvers.aircraft import get_aircraft_resolver
|
||||
from .resolvers.base import get_list, get_one
|
||||
from graphql_schema.entities.types.mutation_input import ComboboxInput
|
||||
from paths import AIRCRAFT_UPLOAD_DEST_PATH
|
||||
from utils.file import delete_file
|
||||
from utils.upload import handle_file_upload
|
||||
from .resolvers.aircraft import AircraftMutationResolver, AircraftQueryResolver
|
||||
from graphql_schema.entities.types.mutation_input import CreateAircraftInput, EditAircraftInput
|
||||
from graphql_schema.entities.types.types import Aircraft
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
AIRCRAFT_UPLOAD_DEST_PATH = "/app/uploads/aircrafts/"
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class AircraftQueries:
|
||||
@strawberry.field()
|
||||
@authenticated_user_only()
|
||||
async def aircrafts(root, info) -> List[Aircraft]:
|
||||
query = get_aircraft_resolver(info.context.user_id, info.context.organization_ids)
|
||||
return await get_list(models.Aircraft, query)
|
||||
return await AircraftQueryResolver().get_list(
|
||||
info.context.user_id,
|
||||
organization_ids=info.context.organization_ids
|
||||
)
|
||||
|
||||
@strawberry.field()
|
||||
@authenticated_user_only()
|
||||
async def aircraft(root, info, id: int) -> Aircraft:
|
||||
query = get_aircraft_resolver(info.context.user_id, info.context.organization_ids, id)
|
||||
return await get_one(models.Aircraft, query)
|
||||
return await AircraftQueryResolver().get_one(
|
||||
id, user_id=info.context.user_id, organization_ids=info.context.organization_ids
|
||||
)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class CreateAircraftMutation:
|
||||
@strawberry_sqlalchemy_input(models.Aircraft, exclude_fields=['id', 'photo_filename'])
|
||||
class CreateAircraftInput:
|
||||
photo: Optional[Upload]
|
||||
organization: Optional[ComboboxInput] = None
|
||||
|
||||
class AircraftMutation:
|
||||
@strawberry.mutation
|
||||
@authenticated_user_only()
|
||||
async def create_aircraft(root, info, input: CreateAircraftInput) -> Aircraft:
|
||||
# TODO: kontrola organizace
|
||||
|
||||
input_data = input.to_dict()
|
||||
if input.photo:
|
||||
input_data['photo_filename'] = await handle_file_upload(input.photo, AIRCRAFT_UPLOAD_DEST_PATH)
|
||||
|
||||
async with get_session() as db:
|
||||
if input.organization:
|
||||
if input.organization:
|
||||
async with get_session() as db:
|
||||
input_data['organization_id'] = await handle_combobox_save(
|
||||
db,
|
||||
models.Organization,
|
||||
@@ -58,29 +48,11 @@ class CreateAircraftMutation:
|
||||
user_id=info.context.user_id,
|
||||
)
|
||||
|
||||
aircraft = await models.Aircraft.create(
|
||||
db,
|
||||
data=dict(
|
||||
**input_data,
|
||||
created_by_id=info.context.user_id,
|
||||
)
|
||||
)
|
||||
|
||||
return Aircraft(**aircraft.as_dict())
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class EditAircraftMutation:
|
||||
@strawberry_sqlalchemy_input(models.Aircraft, exclude_fields=['photo_filename'])
|
||||
class EditAircraftInput:
|
||||
photo: Optional[Upload]
|
||||
organization: Optional[ComboboxInput] = None
|
||||
return await AircraftMutationResolver().create(data=input_data, user_id=info.context.user_id)
|
||||
|
||||
@strawberry.mutation
|
||||
@authenticated_user_only()
|
||||
async def edit_aircraft(root, info, id: int, input: EditAircraftInput) -> Aircraft:
|
||||
# TODO: kontrola organizace
|
||||
|
||||
update_data = input.to_dict()
|
||||
async with get_session() as db:
|
||||
if input.organization:
|
||||
@@ -91,26 +63,24 @@ class EditAircraftMutation:
|
||||
user_id=info.context.user_id,
|
||||
)
|
||||
|
||||
aircraft = (await db.scalars(get_aircraft_resolver(user_id=info.context.user_id, aircraft_id=id))).one()
|
||||
query = AircraftQueryResolver().get_query(
|
||||
user_id=info.context.user_id,
|
||||
object_id=id,
|
||||
organization_ids=info.context.organization_ids
|
||||
)
|
||||
aircraft = (await db.scalars(query)).one()
|
||||
existing_photo_filename = aircraft.photo_filename
|
||||
|
||||
if input.photo:
|
||||
if aircraft.photo_filename:
|
||||
delete_file(AIRCRAFT_UPLOAD_DEST_PATH + "/" + aircraft.photo_filename, silent=True)
|
||||
update_data['photo_filename'] = await handle_file_upload(input.photo, AIRCRAFT_UPLOAD_DEST_PATH)
|
||||
|
||||
aircraft = await models.Aircraft.update(db, obj=aircraft, data=update_data)
|
||||
if input.photo:
|
||||
if existing_photo_filename:
|
||||
delete_file(AIRCRAFT_UPLOAD_DEST_PATH + "/" + existing_photo_filename, silent=True)
|
||||
update_data['photo_filename'] = await handle_file_upload(input.photo, AIRCRAFT_UPLOAD_DEST_PATH)
|
||||
|
||||
async with get_session() as db:
|
||||
aircraft = await models.Aircraft.update(db, id=id, data=update_data)
|
||||
return Aircraft(**aircraft.as_dict())
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class DeleteAircraftMutation:
|
||||
|
||||
@strawberry.mutation
|
||||
@authenticated_user_only()
|
||||
async def delete_aircraft(self, info, id: int) -> Aircraft:
|
||||
async with get_session() as db:
|
||||
aircraft = (await db.scalars(get_aircraft_resolver(info.context.user_id, aircraft_id=id))).one()
|
||||
|
||||
aircraft = await models.Aircraft.update(db, obj=aircraft, data=dict(deleted=True))
|
||||
return Aircraft(**aircraft.as_dict())
|
||||
return await AircraftMutationResolver().delete(info.context.user_id, id)
|
||||
|
||||
@@ -2,13 +2,8 @@ from typing import List
|
||||
import strawberry
|
||||
from database import models
|
||||
from decorators.endpoints import authenticated_user_only
|
||||
from graphql_schema.entities.resolvers.base import get_list, get_one, get_base_resolver
|
||||
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_type(models.Airport)
|
||||
class Airport:
|
||||
pass
|
||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
||||
from graphql_schema.entities.types.types import Airport
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@@ -16,11 +11,9 @@ class AirportQueries:
|
||||
@strawberry.field()
|
||||
@authenticated_user_only()
|
||||
async def airports(root, info) -> List[Airport]:
|
||||
query = get_base_resolver(models.Airport, user_id=info.context.user_id)
|
||||
return await get_list(models.Airport, query)
|
||||
return await BaseQueryResolver(Airport, models.Airport).get_list(info.context.user_id)
|
||||
|
||||
@strawberry.field()
|
||||
@authenticated_user_only()
|
||||
async def airport(root, info, id: int) -> Airport:
|
||||
query = get_base_resolver(models.Airport, user_id=info.context.user_id, object_id=id)
|
||||
return await get_one(models.Airport, query)
|
||||
return await BaseQueryResolver(Airport, models.Airport).get_one(id, user_id=info.context.user_id)
|
||||
|
||||
@@ -1,65 +1,45 @@
|
||||
from typing import List, TYPE_CHECKING
|
||||
from typing import List
|
||||
import strawberry
|
||||
from database import models
|
||||
from decorators.endpoints import authenticated_user_only
|
||||
from dependencies.db import get_session
|
||||
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
|
||||
from .resolvers.base import get_base_resolver, get_list, get_one
|
||||
from database.transaction import get_session
|
||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
|
||||
from graphql_schema.entities.types.mutation_input import CreateCopilotInput, EditCopilotInput
|
||||
from graphql_schema.entities.types.types import Copilot
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class CopilotQueries:
|
||||
@strawberry.field()
|
||||
@authenticated_user_only()
|
||||
async def copilots(root, info) -> List[Copilot]:
|
||||
query = get_base_resolver(models.Copilot, user_id=info.context.user_id, order_by=[models.Copilot.name])
|
||||
return await get_list(models.Copilot, query)
|
||||
return await BaseQueryResolver(Copilot, models.Copilot).get_list(info.context.user_id)
|
||||
|
||||
@strawberry.field()
|
||||
@authenticated_user_only()
|
||||
async def copilot(root, info, id: int) -> Copilot:
|
||||
query = get_base_resolver(models.Copilot, object_id=id, user_id=info.context.user_id)
|
||||
return await get_one(models.Copilot, query)
|
||||
return await BaseQueryResolver(Copilot, models.Copilot).get_one(id, user_id=info.context.user_id)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class CreateCopilotMutation:
|
||||
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
|
||||
class CreateCopilotInput:
|
||||
pass
|
||||
|
||||
@strawberry.mutation
|
||||
@authenticated_user_only()
|
||||
async def create_copilot(root, info, input: CreateCopilotInput) -> Copilot:
|
||||
input_data = input.to_dict()
|
||||
async with get_session() as db:
|
||||
copilot = await models.Copilot.create(
|
||||
db,
|
||||
data=dict(
|
||||
**input_data,
|
||||
created_by_id=info.context.user_id,
|
||||
)
|
||||
)
|
||||
|
||||
return Copilot(**copilot.as_dict())
|
||||
return await BaseMutationResolver(Copilot, models.Copilot).create(
|
||||
data=input.to_dict(),
|
||||
user_id=info.context.user_id
|
||||
)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class EditCopilotMutation:
|
||||
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
|
||||
class EditCopilotInput:
|
||||
pass
|
||||
|
||||
@strawberry.mutation
|
||||
@authenticated_user_only()
|
||||
async def edit_copilot(root, info, id: int, input: EditCopilotInput) -> Copilot:
|
||||
async with get_session() as db:
|
||||
copilot = (await db.scalars(
|
||||
query=get_base_resolver(models.Copilot, object_id=id, user_id=info.context.user_id)
|
||||
BaseQueryResolver(Copilot, models.Copilot).get_query(object_id=id, user_id=info.context.user_id)
|
||||
)).one()
|
||||
|
||||
updated_copilot = await models.Copilot.update(db, obj=copilot, data=input.to_dict())
|
||||
|
||||
@@ -1,68 +1,42 @@
|
||||
from typing import List, TYPE_CHECKING
|
||||
from typing import List
|
||||
import strawberry
|
||||
from database import models
|
||||
from decorators.endpoints import authenticated_user_only
|
||||
from dependencies.db import get_session
|
||||
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
|
||||
from .resolvers.base import get_base_resolver, get_list, get_one
|
||||
from database.transaction import get_session
|
||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
|
||||
from graphql_schema.entities.types.mutation_input import CreateEventInput, EditEventInput
|
||||
from graphql_schema.entities.types.types import Event
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class EventQueries:
|
||||
@strawberry.field()
|
||||
@authenticated_user_only()
|
||||
async def events(root, info) -> List[Event]:
|
||||
query = get_base_resolver(
|
||||
models.Event, user_id=info.context.user_id,
|
||||
return await BaseQueryResolver(Event, models.Event).get_list(
|
||||
info.context.user_id,
|
||||
order_by=[models.Event.date_from.desc(), models.Event.id.desc()]
|
||||
)
|
||||
return await get_list(models.Event, query)
|
||||
|
||||
@strawberry.field()
|
||||
@authenticated_user_only()
|
||||
async def event(root, info, id: int) -> Event:
|
||||
query = get_base_resolver(models.Event, user_id=info.context.user_id, object_id=id)
|
||||
return await get_one(models.Event, query)
|
||||
return await BaseQueryResolver(Event, models.Event).get_one(id, user_id=info.context.user_id)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class CreateEventMutation:
|
||||
@strawberry_sqlalchemy_input(model=models.Event, exclude_fields=["id"])
|
||||
class CreateEventInput:
|
||||
pass
|
||||
|
||||
class EventMutation:
|
||||
@strawberry.mutation
|
||||
@authenticated_user_only()
|
||||
async def create_event(root, info, input: CreateEventInput) -> Event:
|
||||
input_data = input.to_dict()
|
||||
async with get_session() as db:
|
||||
event = await models.Event.create(
|
||||
db,
|
||||
data=dict(
|
||||
**input_data,
|
||||
created_by_id=info.context.user_id,
|
||||
)
|
||||
)
|
||||
|
||||
return Event(**event.as_dict())
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class EditEventMutation:
|
||||
@strawberry_sqlalchemy_input(model=models.Event, exclude_fields=["id"])
|
||||
class EditEventInput:
|
||||
pass
|
||||
return await BaseMutationResolver(Event, models.Event).create(input.to_dict(), info.context.user_id)
|
||||
|
||||
@strawberry.mutation
|
||||
@authenticated_user_only()
|
||||
async def edit_event(root, info, id: int, input: EditEventInput) -> Event:
|
||||
async with get_session() as db:
|
||||
event = (await db.scalars(
|
||||
get_base_resolver(models.Event, user_id=info.context.user_id, object_id=id)
|
||||
BaseQueryResolver(Event, models.Event).get_query(user_id=info.context.user_id, object_id=id)
|
||||
)).one()
|
||||
|
||||
updated_event = await models.Event.update(db, obj=event, data=input.to_dict())
|
||||
|
||||
@@ -2,43 +2,18 @@ import asyncio
|
||||
from typing import List, Optional
|
||||
import strawberry
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import select
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||
from background_jobs.weather import download_weather
|
||||
from database import models
|
||||
from decorators.endpoints import authenticated_user_only
|
||||
from decorators.error_logging import error_logging
|
||||
from dependencies.db import get_session
|
||||
from graphql_schema.entities.resolvers.flight import (
|
||||
handle_aircraft_save, handle_weather_info, FlightMutationResolver, get_airport,
|
||||
)
|
||||
from database.transaction import get_session
|
||||
from graphql_schema.entities.resolvers.flight import handle_aircraft_save, FlightMutationResolver, FlightQueryResolver
|
||||
from graphql_schema.entities.types.mutation_input import EditFlightInput, CreateFlightInput
|
||||
from .resolvers.base import get_list, get_one
|
||||
from .helpers.combobox import handle_combobox_save
|
||||
from graphql_schema.entities.types.types import Flight
|
||||
|
||||
|
||||
def get_base_query(user_id: Optional[int], username: Optional[str] = None, is_auth: bool = False):
|
||||
query = (
|
||||
select(models.Flight)
|
||||
.filter(models.Flight.deleted.is_(False))
|
||||
.order_by(models.Flight.takeoff_datetime.desc())
|
||||
)
|
||||
|
||||
if user_id:
|
||||
query = query.filter(models.Flight.created_by_id == user_id)
|
||||
|
||||
if username:
|
||||
query = (
|
||||
query
|
||||
.join(models.Flight.created_by)
|
||||
.filter(models.User.public_username == username)
|
||||
)
|
||||
|
||||
if not is_auth:
|
||||
query = query.filter(models.Flight.is_public.is_(True))
|
||||
|
||||
return query
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class FlightQueries:
|
||||
|
||||
@@ -47,11 +22,7 @@ class FlightQueries:
|
||||
if not info.context.user_id and not username:
|
||||
raise HTTPException(HTTP_401_UNAUTHORIZED)
|
||||
|
||||
query = (
|
||||
get_base_query(user_id=info.context.user_id, username=username, is_auth=bool(info.context.user_id))
|
||||
.order_by(models.Flight.id.desc())
|
||||
)
|
||||
return await get_list(models.Flight, query)
|
||||
return await FlightQueryResolver().get_list(user_id=info.context.user_id, username=username)
|
||||
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@@ -59,11 +30,7 @@ class FlightQueries:
|
||||
if not info.context.user_id and not username:
|
||||
raise HTTPException(HTTP_401_UNAUTHORIZED)
|
||||
|
||||
query = (
|
||||
get_base_query(user_id=info.context.user_id, username=username, is_auth=bool(info.context.user_id))
|
||||
.filter(models.Flight.id == id)
|
||||
)
|
||||
return await get_one(models.Flight, query)
|
||||
return await FlightQueryResolver().get_one(id, user_id=info.context.user_id, username=username)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@@ -72,36 +39,49 @@ class FlightMutation:
|
||||
@authenticated_user_only()
|
||||
async def create_flight(self, info, input: CreateFlightInput) -> Flight:
|
||||
data = input.to_dict()
|
||||
|
||||
async with (get_session() as db):
|
||||
takeoff_airport = await get_airport(db, input.takeoff_airport, info.context.user_id)
|
||||
landing_airport = await get_airport(db, input.landing_airport, info.context.user_id)
|
||||
|
||||
user_id = info.context.user_id
|
||||
async with get_session() as db:
|
||||
aircraft_id = await handle_aircraft_save(db, info.context.user_id, input.aircraft)
|
||||
weather_takeoff, weather_landing = await asyncio.gather(
|
||||
handle_weather_info(db, data['takeoff_datetime'], takeoff_airport),
|
||||
handle_weather_info(db, data['landing_datetime'], landing_airport)
|
||||
)
|
||||
await db.flush()
|
||||
|
||||
flight = await models.Flight.create(db, data={
|
||||
**data,
|
||||
"takeoff_weather_info_id": weather_takeoff.id if weather_takeoff else None,
|
||||
"landing_weather_info_id": weather_landing.id if weather_landing else None,
|
||||
"takeoff_airport_id": takeoff_airport.id,
|
||||
"landing_airport_id": landing_airport.id,
|
||||
"has_terrain_elevation": False,
|
||||
# TODO: tohle je blbost, bude to vyrabet dve stejne instance!
|
||||
takeoff_airport_id, landing_airport_id = await asyncio.gather(
|
||||
handle_combobox_save(
|
||||
db, models.Airport, input.takeoff_airport, user_id, name_column="icao_code",
|
||||
extra_data={"name": input.takeoff_airport.name}
|
||||
),
|
||||
handle_combobox_save(
|
||||
db, models.Airport, input.landing_airport, user_id, name_column="icao_code",
|
||||
extra_data={"name": input.landing_airport.name}
|
||||
)
|
||||
)
|
||||
|
||||
data.update({
|
||||
"takeoff_airport_id": takeoff_airport_id,
|
||||
"landing_airport_id": landing_airport_id,
|
||||
"aircraft_id": aircraft_id,
|
||||
"created_by_id": info.context.user_id
|
||||
"has_terrain_elevation": False,
|
||||
"name": "",
|
||||
"description": ""
|
||||
})
|
||||
return Flight(**flight.as_dict())
|
||||
flight = FlightMutationResolver().create(data, info.context.user_id)
|
||||
|
||||
info.context.background_tasks.add_task(
|
||||
download_weather,
|
||||
flight_id=flight.id, airport_id=takeoff_airport_id, date_time=flight.takeoff_datetime, type_="takeoff"
|
||||
)
|
||||
info.context.background_tasks.add_task(
|
||||
download_weather,
|
||||
flight_id=flight.id, airport_id=landing_airport_id, date_time=flight.landing_datetime, type_="landing"
|
||||
)
|
||||
|
||||
return flight
|
||||
|
||||
@strawberry.mutation
|
||||
@authenticated_user_only()
|
||||
async def edit_flight(self, info, id: int, input: EditFlightInput) -> Flight:
|
||||
return await FlightMutationResolver.update(info.context, id, input)
|
||||
return await FlightMutationResolver().update(info.context, id, input)
|
||||
|
||||
@strawberry.mutation
|
||||
@authenticated_user_only()
|
||||
async def delete_flight(self, info, id: int) -> Flight:
|
||||
return await FlightMutationResolver.delete(info.context.user_id, id)
|
||||
return await FlightMutationResolver().delete(info.context.user_id, id)
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
from typing import List, TYPE_CHECKING
|
||||
from typing import List
|
||||
import strawberry
|
||||
from sqlalchemy import delete
|
||||
from sqlalchemy.dialects.mysql import insert
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from database import models
|
||||
from decorators.endpoints import authenticated_user_only
|
||||
from dependencies.db import get_session
|
||||
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
|
||||
from .resolvers.base import get_base_resolver, get_list, get_one
|
||||
from database.transaction import get_session
|
||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
|
||||
from graphql_schema.entities.types.mutation_input import CreateOrganizationInput, EditOrganizationInput
|
||||
from graphql_schema.entities.types.types import Organization
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class OrganizationQueries:
|
||||
@strawberry.field()
|
||||
@authenticated_user_only()
|
||||
async def organizations(root, info) -> List[Organization]:
|
||||
query = get_base_resolver(models.Organization, order_by=[models.Organization.name])
|
||||
return await get_list(models.Organization, query)
|
||||
return await BaseQueryResolver(Organization, models.Organization).get_list(
|
||||
info.context.user_id, order_by=[models.Organization.name]
|
||||
)
|
||||
|
||||
@strawberry.field()
|
||||
@authenticated_user_only()
|
||||
async def organization(root, info, id: int) -> Organization:
|
||||
query = get_base_resolver(models.Organization, object_id=id)
|
||||
return await get_one(models.Organization, query)
|
||||
return await BaseQueryResolver(Organization, models.Organization).get_one(id, info.context.user_id)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class CreateOrganizationMutation:
|
||||
@strawberry_sqlalchemy_input(model=models.Organization, exclude_fields=["id"])
|
||||
class CreateOrganizationInput:
|
||||
pass
|
||||
class OrganizationMutation:
|
||||
|
||||
@strawberry.mutation
|
||||
@authenticated_user_only()
|
||||
async def create_organization(root, info, input: CreateOrganizationInput) -> Organization:
|
||||
input_data = input.to_dict()
|
||||
async with get_session() as db:
|
||||
organization = await models.Organization.create(
|
||||
db,
|
||||
data=dict(
|
||||
**input_data,
|
||||
created_by_id=info.context.user_id,
|
||||
)
|
||||
)
|
||||
return await BaseMutationResolver(Organization, models.Organization).create(
|
||||
data=input.to_dict(),
|
||||
user_id=info.context.user_id
|
||||
)
|
||||
|
||||
return Organization(**organization.as_dict())
|
||||
@strawberry.mutation
|
||||
@authenticated_user_only()
|
||||
async def edit_organization(root, info, id: int, input: EditOrganizationInput) -> Organization:
|
||||
async with get_session() as db:
|
||||
organization = (await db.scalars(
|
||||
BaseQueryResolver(Organization, models.Organization).get_query(
|
||||
user_id=info.context.user_id, object_id=id
|
||||
)
|
||||
)).one()
|
||||
|
||||
updated_organization = await models.Organization.update(db, obj=organization, data=input.to_dict())
|
||||
return Organization(**updated_organization.as_dict())
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@@ -58,7 +58,9 @@ class OrganizationUserMutation:
|
||||
@authenticated_user_only()
|
||||
async def add_to_organization(root, info, organization_id: int) -> Organization:
|
||||
async with get_session() as db:
|
||||
organization = (await db.scalars(get_base_resolver(models.Organization, object_id=organization_id))).one()
|
||||
organization = (await db.scalars(
|
||||
BaseQueryResolver(Organization, models.Organization).get_query(object_id=organization_id)
|
||||
)).one()
|
||||
|
||||
try:
|
||||
await db.execute(
|
||||
@@ -68,7 +70,6 @@ class OrganizationUserMutation:
|
||||
)
|
||||
)
|
||||
except IntegrityError:
|
||||
print("jiz existuje")
|
||||
pass
|
||||
|
||||
return Organization(**organization.as_dict())
|
||||
@@ -77,7 +78,9 @@ class OrganizationUserMutation:
|
||||
@authenticated_user_only()
|
||||
async def remove_from_organization(root, info, organization_id: int) -> Organization:
|
||||
async with get_session() as db:
|
||||
organization = (await db.scalars(get_base_resolver(models.Organization, object_id=organization_id))).one()
|
||||
organization = (await db.scalars(
|
||||
BaseQueryResolver(Organization, models.Organization).get_query(object_id=organization_id)
|
||||
)).one()
|
||||
|
||||
await db.execute(
|
||||
delete(models.user_is_in_organization).filter_by(
|
||||
@@ -87,21 +90,3 @@ class OrganizationUserMutation:
|
||||
)
|
||||
|
||||
return Organization(**organization.as_dict())
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class EditOrganizationMutation:
|
||||
@strawberry_sqlalchemy_input(model=models.Organization, exclude_fields=["id"])
|
||||
class EditOrganizationInput:
|
||||
pass
|
||||
|
||||
@strawberry.mutation
|
||||
@authenticated_user_only()
|
||||
async def edit_organization(root, info, id: int, input: EditOrganizationInput) -> Organization:
|
||||
async with get_session() as db:
|
||||
organization = (await db.scalars(
|
||||
get_base_resolver(models.Organization, user_id=info.context.user_id, object_id=id)
|
||||
)).one()
|
||||
|
||||
updated_organization = await models.Organization.update(db, obj=organization, data=input.to_dict())
|
||||
return Organization(**updated_organization.as_dict())
|
||||
|
||||
@@ -4,17 +4,17 @@ import strawberry
|
||||
from sqlalchemy import update
|
||||
from strawberry.file_uploads import Upload
|
||||
from background_jobs.elevation import add_terrain_elevation_to_photo
|
||||
from background_jobs.photo import generate_thumbnail, resize_photo
|
||||
from database import models
|
||||
from decorators.endpoints import authenticated_user_only
|
||||
from dependencies.db import get_session
|
||||
from database.transaction import get_session
|
||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
|
||||
from graphql_schema.entities.types.types import Photo
|
||||
from paths import get_photo_basepath
|
||||
from upload_utils import (
|
||||
handle_file_upload, delete_file, parse_exif_info, generate_thumbnail, resize_image,
|
||||
rotate_image
|
||||
)
|
||||
from graphql_schema.entities.helpers.combobox import handle_combobox_save
|
||||
from .resolvers.base import get_base_resolver, get_list
|
||||
from utils.file import delete_file
|
||||
from utils.image import parse_exif_info, rotate_image
|
||||
from utils.upload import handle_file_upload
|
||||
from graphql_schema.entities.types.mutation_input import ComboboxInput
|
||||
|
||||
|
||||
@@ -22,8 +22,7 @@ from graphql_schema.entities.types.mutation_input import ComboboxInput
|
||||
class PhotoQueries:
|
||||
@strawberry.field()
|
||||
async def photos(root, info) -> List[Photo]:
|
||||
query = get_base_resolver(models.Photo, user_id=info.context.user_id)
|
||||
return await get_list(models.Photo, query)
|
||||
return await BaseQueryResolver(Photo, models.Photo).get_list(user_id=info.context.user_id)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@@ -58,8 +57,7 @@ class UploadPhotoMutation:
|
||||
}, db_session=db)
|
||||
photo = Photo(**photo_model.as_dict())
|
||||
|
||||
# TODO: udelat primo konkretni bg joby na resize a thumbnaily
|
||||
info.context.background_tasks.add_task(resize_image, path=path, filename=filename, new_width=2500, quality=85)
|
||||
info.context.background_tasks.add_task(resize_photo, path=path, filename=filename)
|
||||
info.context.background_tasks.add_task(generate_thumbnail, path=path, filename=filename)
|
||||
|
||||
if exif_info.get("gps_latitude") and exif_info.get("gps_longitude"):
|
||||
@@ -77,19 +75,20 @@ class EditPhotoMutation:
|
||||
point_of_interest: Optional[ComboboxInput] = None
|
||||
is_flight_cover: Optional[bool] = None
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
key: getattr(self, key) for key in ('name', 'description', 'is_flight_cover')
|
||||
if getattr(self, key) is not None
|
||||
}
|
||||
|
||||
@strawberry.mutation()
|
||||
@authenticated_user_only()
|
||||
async def edit_photo(self, info, id: int, input: EditPhotoInput) -> Photo:
|
||||
|
||||
# TODO: base trida pro inputy s definici to_dict/as_dict?
|
||||
data = {
|
||||
key: getattr(input, key) for key in ('name', 'description', 'is_flight_cover')
|
||||
if getattr(input, key) is not None
|
||||
}
|
||||
|
||||
query = get_base_resolver(models.Photo, user_id=info.context.user_id, object_id=id)
|
||||
data = input.to_dict()
|
||||
async with get_session() as db:
|
||||
photo = (await db.scalars(query)).one()
|
||||
photo = (await db.scalars(
|
||||
BaseQueryResolver(Photo, models.Photo).get_query(user_id=info.context.user_id, object_id=id)
|
||||
)).one()
|
||||
|
||||
if input.point_of_interest:
|
||||
data['point_of_interest_id'] = await handle_combobox_save(
|
||||
@@ -116,25 +115,27 @@ class EditPhotoMutation:
|
||||
@strawberry.mutation()
|
||||
@authenticated_user_only()
|
||||
async def rotate_photo(self, info, id: int, angle: int) -> Photo:
|
||||
|
||||
async with get_session() as db:
|
||||
query = get_base_resolver(models.Photo, user_id=info.context.user_id, object_id=id)
|
||||
photo = (await db.scalars(query)).one()
|
||||
photo = (await db.scalars(
|
||||
BaseQueryResolver(Photo, models.Photo).get_query(user_id=info.context.user_id, object_id=id)
|
||||
)).one()
|
||||
photo_filename = photo.filename
|
||||
photo_as_dict = photo.as_dict()
|
||||
|
||||
await asyncio.gather(
|
||||
rotate_image(
|
||||
path=get_photo_basepath(photo.flight_id),
|
||||
filename=photo.filename,
|
||||
angle=angle,
|
||||
),
|
||||
rotate_image(
|
||||
path=get_photo_basepath(photo.flight_id) + "/thumbs",
|
||||
filename=photo.filename,
|
||||
angle=angle,
|
||||
),
|
||||
)
|
||||
await asyncio.gather(
|
||||
rotate_image(
|
||||
path=get_photo_basepath(photo.flight_id),
|
||||
filename=photo_filename,
|
||||
angle=angle,
|
||||
),
|
||||
rotate_image(
|
||||
path=get_photo_basepath(photo.flight_id) + "/thumbs",
|
||||
filename=photo_filename,
|
||||
angle=angle,
|
||||
),
|
||||
)
|
||||
|
||||
return Photo(**photo.as_dict())
|
||||
return Photo(**photo_as_dict)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@@ -142,14 +143,10 @@ class DeletePhotoMutation:
|
||||
@strawberry.mutation()
|
||||
@authenticated_user_only()
|
||||
async def delete_photo(self, info, id: int) -> Photo:
|
||||
query = get_base_resolver(models.Photo, user_id=info.context.user_id, object_id=id)
|
||||
async with get_session() as db:
|
||||
photo_model = (await db.scalars(query)).one()
|
||||
await db.delete(photo_model)
|
||||
photo = Photo(**photo_model.as_dict())
|
||||
photo = await BaseMutationResolver(Photo, models.Photo).delete(user_id=info.context.user_id, id=id)
|
||||
|
||||
base_path = get_photo_basepath(photo.flight_id)
|
||||
delete_file(f"{base_path}/{photo.filename}", silent=True)
|
||||
delete_file(f"{base_path}/thumbs/{photo.filename}", silent=True)
|
||||
|
||||
return photo_model
|
||||
return photo
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
import strawberry
|
||||
from database import models
|
||||
from decorators.endpoints import authenticated_user_only
|
||||
from dependencies.db import get_session
|
||||
from database.transaction import get_session
|
||||
from graphql_schema.entities.helpers.combobox import handle_combobox_save
|
||||
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
|
||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
|
||||
from graphql_schema.entities.types.types import PointOfInterest
|
||||
from .resolvers.base import get_base_resolver, get_list, get_one
|
||||
from graphql_schema.entities.types.mutation_input import ComboboxInput
|
||||
from graphql_schema.entities.types.mutation_input import CreatePointOfInterestInput, EditPointOfInterestInput
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@@ -15,22 +14,16 @@ class PointOfInterestQueries:
|
||||
@strawberry.field()
|
||||
@authenticated_user_only()
|
||||
async def points_of_interest(root, info) -> List[PointOfInterest]:
|
||||
query = get_base_resolver(models.PointOfInterest, user_id=info.context.user_id)
|
||||
return await get_list(models.PointOfInterest, query)
|
||||
return await BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_list(info.context.user_id)
|
||||
|
||||
@strawberry.field()
|
||||
@authenticated_user_only()
|
||||
async def point_of_interest(root, info, id: int) -> PointOfInterest:
|
||||
query = get_base_resolver(models.PointOfInterest, user_id=info.context.user_id, object_id=id)
|
||||
return await get_one(models.PointOfInterest, query)
|
||||
return await BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_one(id, info.context.user_id)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class CreatePointOfInterestMutation:
|
||||
@strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id'])
|
||||
class CreatePointOfInterestInput:
|
||||
type: Optional[ComboboxInput] = None
|
||||
|
||||
class PointOfInterestMutation:
|
||||
@strawberry.mutation
|
||||
@authenticated_user_only()
|
||||
async def create_point_of_interest(root, info, input: CreatePointOfInterestInput) -> PointOfInterest:
|
||||
@@ -42,23 +35,17 @@ class CreatePointOfInterestMutation:
|
||||
db, models.PointOfInterestType, input.type, info.context.user_id
|
||||
)
|
||||
|
||||
poi = await models.PointOfInterest.create(db, data=dict(**input_data, created_by_id=info.context.user_id))
|
||||
return PointOfInterest(**poi.as_dict())
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class EditPointOfInterestMutation:
|
||||
@strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id'])
|
||||
class EditPointOfInterestInput:
|
||||
type: Optional[ComboboxInput] = None
|
||||
return await BaseMutationResolver(PointOfInterest, models.PointOfInterest).create(
|
||||
input_data, info.context.user_id
|
||||
)
|
||||
|
||||
@strawberry.mutation
|
||||
@authenticated_user_only()
|
||||
async def edit_point_of_interest(root, info, id: int, input: EditPointOfInterestInput) -> PointOfInterest:
|
||||
input_data = input.to_dict()
|
||||
|
||||
query = get_base_resolver(
|
||||
models.PointOfInterest, user_id=info.context.user_id, object_id=id, include_public=False
|
||||
query = BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_query(
|
||||
user_id=info.context.user_id, object_id=id, include_public=False
|
||||
)
|
||||
|
||||
async with get_session() as db:
|
||||
@@ -71,19 +58,7 @@ class EditPointOfInterestMutation:
|
||||
updated_poi = await models.PointOfInterest.update(db, obj=poi, data=input_data)
|
||||
return PointOfInterest(**updated_poi.as_dict())
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class DeletePointOfInterestMutation:
|
||||
|
||||
@strawberry.mutation
|
||||
@authenticated_user_only()
|
||||
async def delete_point_of_interest(self, info, id: int) -> PointOfInterest:
|
||||
query = get_base_resolver(
|
||||
models.PointOfInterest, user_id=info.context.user_id, object_id=id, include_public=False
|
||||
)
|
||||
|
||||
async with get_session() as db:
|
||||
poi = (await db.scalars(query)).one()
|
||||
|
||||
updated_poi = await models.PointOfInterest.update(db, obj=poi, data=dict(deleted=True))
|
||||
return PointOfInterest(**updated_poi.as_dict())
|
||||
return await BaseMutationResolver(PointOfInterest, models.PointOfInterest).delete(info.context.user_id, id=id)
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import List
|
||||
import strawberry
|
||||
from database import models
|
||||
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 BaseQueryResolver
|
||||
from graphql_schema.entities.types.types import PointOfInterestType
|
||||
|
||||
|
||||
@@ -12,14 +12,14 @@ class PointOfInterestTypeQueries:
|
||||
@strawberry.field()
|
||||
@authenticated_user_only()
|
||||
async def point_of_interest_types(root, info) -> List[PointOfInterestType]:
|
||||
query = get_base_resolver(models.PointOfInterestType, user_id=info.context.user_id)
|
||||
return await get_list(models.PointOfInterestType, query)
|
||||
return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_list(info.context.user_id)
|
||||
|
||||
@strawberry.field()
|
||||
@authenticated_user_only()
|
||||
async def point_of_interest_type(root, info, id: int) -> PointOfInterestType:
|
||||
query = get_base_resolver(models.PointOfInterestType, user_id=info.context.user_id, object_id=id)
|
||||
return await get_one(models.PointOfInterestType, query)
|
||||
return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_one(
|
||||
id, info.context.user_id
|
||||
)
|
||||
|
||||
#
|
||||
# @strawberry.type
|
||||
|
||||
@@ -1,24 +1,41 @@
|
||||
from operator import or_
|
||||
from typing import Set, Optional
|
||||
from database import models
|
||||
from graphql_schema.entities.resolvers.base import get_base_resolver
|
||||
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
|
||||
from graphql_schema.entities.types.types import Aircraft
|
||||
|
||||
|
||||
def get_aircraft_resolver(user_id: int, organization_ids: Optional[Set[int]] = None, aircraft_id: Optional[int] = None):
|
||||
query = get_base_resolver(
|
||||
model=models.Aircraft,
|
||||
order_by=[models.Aircraft.id.desc()],
|
||||
object_id=aircraft_id,
|
||||
user_id=user_id if not organization_ids else None
|
||||
)
|
||||
if organization_ids:
|
||||
query = (
|
||||
query.filter(
|
||||
or_(
|
||||
models.Aircraft.created_by_id == user_id,
|
||||
models.Aircraft.organization_id.in_(organization_ids)
|
||||
class AircraftQueryResolver(BaseQueryResolver):
|
||||
def __init__(self):
|
||||
super().__init__(graphql_type=Aircraft, model=models.Aircraft)
|
||||
|
||||
def get_query(
|
||||
self,
|
||||
user_id: Optional[int] = None,
|
||||
object_id: Optional[int] = None,
|
||||
order_by: Optional[list] = None,
|
||||
organization_ids: Optional[Set[int]] = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
query = super().get_query(
|
||||
order_by=[models.Aircraft.id.desc()],
|
||||
object_id=object_id,
|
||||
user_id=user_id if not organization_ids else None
|
||||
)
|
||||
if organization_ids:
|
||||
query = (
|
||||
query.filter(
|
||||
or_(
|
||||
models.Aircraft.created_by_id == user_id,
|
||||
models.Aircraft.organization_id.in_(organization_ids)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return query
|
||||
return query
|
||||
|
||||
|
||||
class AircraftMutationResolver(BaseMutationResolver):
|
||||
def __init__(self):
|
||||
super().__init__(graphql_type=Aircraft, model=models.Aircraft)
|
||||
|
||||
@@ -1,53 +1,108 @@
|
||||
from typing import Optional, Type
|
||||
from typing import Optional, Type, T
|
||||
from sqlalchemy import select, or_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from database import models
|
||||
from dependencies.db import get_session
|
||||
from database.transaction import get_session
|
||||
|
||||
|
||||
def get_base_resolver(
|
||||
model: Type[models.BaseModel],
|
||||
object_id: Optional[int] = None,
|
||||
user_id: Optional[int] = None,
|
||||
order_by: Optional[list] = None,
|
||||
include_public: Optional[bool] = True,
|
||||
):
|
||||
query = select(model)
|
||||
class BaseQueryResolver:
|
||||
|
||||
if object_id:
|
||||
if hasattr(model, "id"):
|
||||
query = query.filter(model.id == object_id)
|
||||
def __init__(self, graphql_type, model):
|
||||
self.graphql_type = graphql_type
|
||||
self.model = model
|
||||
|
||||
def get_query(
|
||||
self,
|
||||
user_id: Optional[int] = None,
|
||||
object_id: Optional[int] = None,
|
||||
order_by: Optional[list] = None,
|
||||
include_public: Optional[bool] = True,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
query = select(self.model)
|
||||
|
||||
if object_id:
|
||||
if hasattr(self.model, "id"):
|
||||
query = query.filter(self.model.id == object_id)
|
||||
else:
|
||||
raise AssertionError(f"Model {self.model} has no ID column! Cannot query by ID!")
|
||||
|
||||
if hasattr(self.model, "deleted"):
|
||||
query = query.filter(self.model.deleted.is_(False))
|
||||
|
||||
ownership_clause = []
|
||||
if hasattr(self.model, "is_public") and include_public:
|
||||
ownership_clause.append(self.model.is_public.is_(True))
|
||||
|
||||
if hasattr(self.model, "created_by_id") and user_id:
|
||||
ownership_clause.append(self.model.created_by_id == user_id)
|
||||
|
||||
if len(ownership_clause) > 1:
|
||||
query = query.filter(or_(*ownership_clause))
|
||||
elif len(ownership_clause) == 1:
|
||||
query = query.filter(*ownership_clause)
|
||||
|
||||
if order_by:
|
||||
query = query.order_by(*order_by)
|
||||
|
||||
return query
|
||||
|
||||
async def _get_list(self, query):
|
||||
async with get_session() as db:
|
||||
items = (await db.scalars(query)).all()
|
||||
|
||||
return [self.model(**m.as_dict()) for m in items]
|
||||
|
||||
async def _get_one(self, query):
|
||||
async with get_session() as db:
|
||||
data = (await db.scalars(query)).one()
|
||||
return self.model(**data.as_dict())
|
||||
|
||||
async def get_list(self, user_id: Optional[int] = None, **kwargs) -> list:
|
||||
query = self.get_query(user_id, **kwargs)
|
||||
return await self._get_list(query)
|
||||
|
||||
async def get_one(self, id: int, user_id: Optional[int] = None, **kwargs):
|
||||
query = self.get_query(user_id, object_id=id, **kwargs)
|
||||
return await self._get_one(query)
|
||||
|
||||
|
||||
class BaseMutationResolver:
|
||||
model: Type[models.BaseModel]
|
||||
graphql_type: Type[T] = None
|
||||
|
||||
def __init__(self, graphql_type: Type[T], model: Type[models.BaseModel]):
|
||||
self.graphql_type = graphql_type
|
||||
self.model = model
|
||||
|
||||
async def delete(self, user_id: int, id: int) -> T:
|
||||
async with get_session() as db:
|
||||
query = BaseQueryResolver(self.graphql_type, self.model).get_query(user_id, object_id=id)
|
||||
model = (await db.scalars(query)).one()
|
||||
|
||||
if hasattr(self.model, "deleted"):
|
||||
model = await self.model.update(db, obj=model, data=dict(deleted=True))
|
||||
else:
|
||||
await db.delete(model)
|
||||
|
||||
return self.graphql_type(**model.as_dict())
|
||||
|
||||
async def create(self, data: dict, user_id: Optional[int] = None) -> T:
|
||||
input_data = {**data}
|
||||
if hasattr(self.model, "created_by_id"):
|
||||
input_data['created_by_id'] = user_id
|
||||
|
||||
async with get_session() as db:
|
||||
model = await self.model.create(db, data=input_data)
|
||||
return self.graphql_type(**model.as_dict())
|
||||
|
||||
async def _do_update(self, db: AsyncSession, obj: models.BaseModel | dict, data: dict) -> T:
|
||||
update_where = {}
|
||||
if isinstance(obj, models.BaseModel):
|
||||
update_where['obj'] = obj
|
||||
else:
|
||||
raise AssertionError(f"Model {model} has no ID column! Cannot query by ID!")
|
||||
update_where['id'] = obj['id']
|
||||
|
||||
if hasattr(model, "deleted"):
|
||||
query = query.filter(model.deleted.is_(False))
|
||||
|
||||
ownership_clause = []
|
||||
if hasattr(model, "is_public") and include_public:
|
||||
ownership_clause.append(model.is_public.is_(True))
|
||||
|
||||
if hasattr(model, "created_by_id") and user_id:
|
||||
ownership_clause.append(model.created_by_id == user_id)
|
||||
|
||||
if len(ownership_clause) > 1:
|
||||
query = query.filter(or_(*ownership_clause))
|
||||
elif len(ownership_clause) == 1:
|
||||
query = query.filter(*ownership_clause)
|
||||
|
||||
if order_by:
|
||||
query = query.order_by(*order_by)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
async def get_list(model, query):
|
||||
async with get_session() as db:
|
||||
items = (await db.scalars(query)).all()
|
||||
|
||||
return [model(**m.as_dict()) for m in items]
|
||||
|
||||
|
||||
async def get_one(model, query):
|
||||
async with get_session() as db:
|
||||
data = (await db.scalars(query)).one()
|
||||
return model(**data.as_dict())
|
||||
model = await self.model.update(db, data=data, **update_where)
|
||||
return self.graphql_type(**model.as_dict())
|
||||
|
||||
@@ -1,77 +1,51 @@
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Type
|
||||
from aiocache import cached
|
||||
from typing import List, Optional
|
||||
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 background_jobs.weather import download_weather
|
||||
from database import models
|
||||
from database.models import flight_has_copilot
|
||||
from dependencies.db import get_session
|
||||
from external.weather import weather_api
|
||||
from database.transaction import get_session
|
||||
from graphql_schema.entities.helpers.combobox import handle_combobox_save
|
||||
from graphql_schema.entities.resolvers.base import get_base_resolver
|
||||
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
|
||||
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
|
||||
from paths import FLIGHT_GPX_TRACK_PATH
|
||||
from utils.file import delete_file
|
||||
from utils.upload import handle_file_upload
|
||||
|
||||
|
||||
class BaseMutationResolver:
|
||||
model: Type[models.BaseModel]
|
||||
graphql_type = Flight
|
||||
class FlightQueryResolver(BaseQueryResolver):
|
||||
def __init__(self):
|
||||
super().__init__(graphql_type=Flight, model=models.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()
|
||||
def get_query(self, user_id: int, object_id: Optional[int] = None, *args, **kwargs):
|
||||
query = super().get_query(
|
||||
user_id, object_id,
|
||||
order_by=[models.Flight.takeoff_datetime.desc()],
|
||||
include_public=bool(user_id)
|
||||
)
|
||||
|
||||
if kwargs.get('username'):
|
||||
query = (
|
||||
query
|
||||
.join(models.Flight.created_by)
|
||||
.filter(models.User.public_username == kwargs['username'])
|
||||
)
|
||||
|
||||
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)
|
||||
return query
|
||||
|
||||
|
||||
class FlightMutationResolver(BaseMutationResolver):
|
||||
model = models.Flight
|
||||
graphql_type = Flight
|
||||
def __init__(self):
|
||||
super().__init__(Flight, models.Flight)
|
||||
|
||||
async def create(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
async def update(cls, context, id: int, input: EditFlightInput):
|
||||
async def update(self, 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 = await FlightQueryResolver().get_one(id, user_id)
|
||||
flight_data = flight.as_dict()
|
||||
flight_id = flight.id
|
||||
|
||||
@@ -84,31 +58,32 @@ class FlightMutationResolver(BaseMutationResolver):
|
||||
)
|
||||
|
||||
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']
|
||||
takeoff_airport_id = await handle_combobox_save(
|
||||
db, models.Airport, input.takeoff_airport, user_id, name_column="icao_code",
|
||||
extra_data={"name": input.takeoff_airport.name}
|
||||
)
|
||||
data['takeoff_airport_id'] = takeoff_airport_id
|
||||
data['takeoff_datetime'] = input.takeoff_datetime or flight_data['takeoff_datetime']
|
||||
|
||||
data['takeoff_airport_id'] = takeoff_airport.id
|
||||
data['takeoff_datetime'] = takeoff_datetime
|
||||
context.background_tasks.add_task(
|
||||
download_weather, flight_id=id, airport_id=takeoff_airport_id, date_time=data['takeoff_datetime'],
|
||||
type_="takeoff"
|
||||
)
|
||||
|
||||
weather_info = await handle_weather_info(
|
||||
db, takeoff_datetime, takeoff_airport, flight_data['takeoff_weather_info_id']
|
||||
if input.landing_airport:
|
||||
landing_airport_id = await handle_combobox_save(
|
||||
db, models.Airport, input.landing_airport, user_id, name_column="icao_code",
|
||||
extra_data={"name": input.landing_airport.name}
|
||||
)
|
||||
|
||||
data['landing_airport_id'] = landing_airport_id
|
||||
data['landing_datetime'] = input.landing_datetime or flight_data['landing_datetime']
|
||||
|
||||
context.background_tasks.add_task(
|
||||
download_weather, flight_id=id, airport_id=landing_airport_id, date_time=data['landing_datetime'],
|
||||
type_="landing"
|
||||
)
|
||||
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)
|
||||
@@ -127,46 +102,21 @@ class FlightMutationResolver(BaseMutationResolver):
|
||||
|
||||
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)
|
||||
copilots = await asyncio.gather(*[
|
||||
handle_combobox_save(db, models.Copilot, copilot, user_id) for copilot in input.copilots
|
||||
])
|
||||
|
||||
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)
|
||||
return await self._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
|
||||
async def handle_upload_gpx(original_gpx_filename: str, gpx_track: Upload):
|
||||
if original_gpx_filename:
|
||||
delete_file(FLIGHT_GPX_TRACK_PATH + "/" + original_gpx_filename, silent=True)
|
||||
|
||||
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
|
||||
return await handle_file_upload(gpx_track, FLIGHT_GPX_TRACK_PATH)
|
||||
|
||||
|
||||
async def handle_track_edit(db: AsyncSession, flight_id: int, track: List[TrackItemInput], user_id: int):
|
||||
@@ -176,6 +126,7 @@ async def handle_track_edit(db: AsyncSession, flight_id: int, track: List[TrackI
|
||||
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": ""}
|
||||
@@ -195,7 +146,7 @@ async def handle_track_edit(db: AsyncSession, flight_id: int, track: List[TrackI
|
||||
"point_of_interest_id": poi_id,
|
||||
"airport_id": airport_id,
|
||||
"order": order,
|
||||
"landing_duration": item.landing_duration
|
||||
"landing_duration": item.landing_duration if airport_id else None
|
||||
}
|
||||
)
|
||||
order += 1
|
||||
@@ -211,17 +162,3 @@ async def handle_aircraft_save(db: AsyncSession, user_id: int, aircraft: Combobo
|
||||
"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,21 @@
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
|
||||
class BaseGraphqlInputType:
|
||||
base_ignored_fields: List[str] = ["created_at", "created_by_id", "updated_by_id", "updated_at", "deleted"]
|
||||
ignored_fields: List[str] = []
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
dict_data = {}
|
||||
for key in self.__annotations__.keys():
|
||||
value = getattr(self, key)
|
||||
if value is None or key in self.ignored_fields + self.base_ignored_fields:
|
||||
continue
|
||||
|
||||
if isinstance(value, datetime):
|
||||
value = value.astimezone()
|
||||
|
||||
dict_data[key] = value
|
||||
|
||||
return dict_data
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
import strawberry
|
||||
from strawberry.file_uploads import Upload
|
||||
from database import models
|
||||
from graphql_schema.entities.types.base import BaseGraphqlInputType
|
||||
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
|
||||
|
||||
|
||||
@@ -12,11 +14,51 @@ class ComboboxInput:
|
||||
name: str
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
|
||||
class CreateCopilotInput(BaseGraphqlInputType):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
|
||||
class EditCopilotInput(BaseGraphqlInputType):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id'])
|
||||
class CreatePointOfInterestInput(BaseGraphqlInputType):
|
||||
type: Optional[ComboboxInput] = None
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id'])
|
||||
class EditPointOfInterestInput(BaseGraphqlInputType):
|
||||
type: Optional[ComboboxInput] = None
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_input(model=models.Organization, exclude_fields=["id"])
|
||||
class CreateOrganizationInput(BaseGraphqlInputType):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_input(model=models.Organization, exclude_fields=["id"])
|
||||
class EditOrganizationInput(BaseGraphqlInputType):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_input(model=models.Event, exclude_fields=["id"])
|
||||
class CreateEventInput(BaseGraphqlInputType):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_input(model=models.Event, exclude_fields=["id"])
|
||||
class EditEventInput(BaseGraphqlInputType):
|
||||
pass
|
||||
|
||||
|
||||
@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"
|
||||
"takeoff_weather_info_id", "landing_weather_info_id", "gpx_track_filename", "event_id"
|
||||
], all_optional=True)
|
||||
class EditFlightInput:
|
||||
class EditFlightInput(BaseGraphqlInputType):
|
||||
gpx_track: Optional[Upload] = None # TODO: poresit validaci uploadovaneho souboru!
|
||||
track: Optional[List[TrackItemInput]] = None
|
||||
copilots: Optional[List[ComboboxInput]] = None
|
||||
@@ -26,19 +68,29 @@ class EditFlightInput:
|
||||
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:
|
||||
@strawberry.input()
|
||||
class CreateFlightInput(BaseGraphqlInputType):
|
||||
aircraft: ComboboxInput
|
||||
landing_airport: ComboboxInput
|
||||
takeoff_airport: ComboboxInput
|
||||
takeoff_datetime: datetime
|
||||
landing_datetime: datetime
|
||||
|
||||
|
||||
@strawberry.input()
|
||||
class TrackItemInput:
|
||||
# order: int
|
||||
point_of_interest: Optional[ComboboxInput] = None
|
||||
airport: Optional[ComboboxInput] = None
|
||||
landing_duration: Optional[int] = None
|
||||
|
||||
|
||||
@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'])
|
||||
class EditAircraftInput(BaseGraphqlInputType):
|
||||
photo: Optional[Upload]
|
||||
organization: Optional[ComboboxInput] = None
|
||||
|
||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from typing import Optional, Annotated, List
|
||||
import strawberry
|
||||
from config import API_URL
|
||||
from database import models
|
||||
from decorators.endpoints import authenticated_user_only
|
||||
from external.gpx_parser import GPXParser
|
||||
@@ -17,30 +16,10 @@ 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
|
||||
from paths import (
|
||||
get_public_url, get_avatar_url, get_title_image_url, get_photo_thumbnail_url, get_photo_url, FLIGHT_GPX_TRACK_PATH
|
||||
)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@@ -49,35 +28,6 @@ class Point:
|
||||
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]
|
||||
@@ -92,6 +42,45 @@ class GPXTrack:
|
||||
avg_altitude: float
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_type(models.Airport)
|
||||
class Airport:
|
||||
pass
|
||||
|
||||
|
||||
@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_sqlalchemy_type(models.PointOfInterest)
|
||||
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))
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_type(models.Photo)
|
||||
class Photo:
|
||||
url: str = strawberry.field(resolver=get_photo_url)
|
||||
thumbnail_url: str = strawberry.field(resolver=get_photo_thumbnail_url)
|
||||
point_of_interest: Optional[PointOfInterest] = strawberry.field(
|
||||
resolver=lambda root: poi_dataloader.load(root.point_of_interest_id)
|
||||
)
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_type(models.Flight)
|
||||
class Flight:
|
||||
async def load_gpx_track(root):
|
||||
@@ -99,7 +88,7 @@ class Flight:
|
||||
return None
|
||||
|
||||
try:
|
||||
gpx_parser = GPXParser(f"/app/uploads/tracks/{root.gpx_track_filename}")
|
||||
gpx_parser = GPXParser(f"{FLIGHT_GPX_TRACK_PATH}/{root.gpx_track_filename}")
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
@@ -146,9 +135,7 @@ class Flight:
|
||||
|
||||
@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)
|
||||
)
|
||||
flights: List[Flight] = strawberry.field(resolver=lambda root: flights_by_copilot_dataloader.load(root.id))
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_type(models.Aircraft)
|
||||
@@ -156,40 +143,24 @@ 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(
|
||||
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)
|
||||
)
|
||||
|
||||
|
||||
@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(
|
||||
users: List[User] = strawberry.field(resolver=lambda root: users_in_organization_dataloader.load(root.id))
|
||||
aircrafts: List[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)
|
||||
avatar_image_url: Optional[str] = strawberry.field(resolver=lambda root: get_avatar_url(root))
|
||||
title_image_url: str = strawberry.field(resolver=lambda root: get_title_image_url(root))
|
||||
organizations: List[Annotated['Organization', strawberry.lazy(".organization")]] = strawberry.field(
|
||||
resolver=lambda root: user_organizations_dataloader.load(root.id)
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from typing import Optional
|
||||
import strawberry
|
||||
from graphql import GraphQLError
|
||||
from passlib.hash import bcrypt
|
||||
@@ -7,12 +7,11 @@ from strawberry.file_uploads import Upload
|
||||
from database import models
|
||||
from decorators.endpoints import authenticated_user_only
|
||||
from decorators.error_logging import error_logging
|
||||
from dependencies.db import get_session
|
||||
from upload_utils import handle_file_upload, delete_file, resize_image
|
||||
from database.transaction import get_session
|
||||
from graphql_schema.entities.types.types import User
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
from utils.file import delete_file
|
||||
from utils.image import resize_image
|
||||
from utils.upload import handle_file_upload
|
||||
|
||||
|
||||
@strawberry.type
|
||||
|
||||
@@ -1,31 +1,24 @@
|
||||
from strawberry.tools import merge_types
|
||||
from graphql_schema.entities.aircraft import CreateAircraftMutation, EditAircraftMutation, DeleteAircraftMutation
|
||||
from graphql_schema.entities.aircraft import AircraftMutation
|
||||
from graphql_schema.entities.copilot import CreateCopilotMutation, EditCopilotMutation
|
||||
from graphql_schema.entities.event import CreateEventMutation, EditEventMutation
|
||||
from graphql_schema.entities.event import EventMutation
|
||||
from graphql_schema.entities.flight import FlightMutation
|
||||
from graphql_schema.entities.organization import (
|
||||
CreateOrganizationMutation, EditOrganizationMutation, OrganizationUserMutation
|
||||
)
|
||||
from graphql_schema.entities.organization import OrganizationUserMutation, OrganizationMutation
|
||||
from graphql_schema.entities.photo import UploadPhotoMutation, DeletePhotoMutation, EditPhotoMutation
|
||||
from graphql_schema.entities.poi import CreatePointOfInterestMutation, EditPointOfInterestMutation
|
||||
from graphql_schema.entities.poi import PointOfInterestMutation
|
||||
from graphql_schema.entities.user import EditUserMutation
|
||||
|
||||
Mutation = merge_types("Mutation", (
|
||||
CreateAircraftMutation,
|
||||
EditAircraftMutation,
|
||||
DeleteAircraftMutation,
|
||||
AircraftMutation,
|
||||
FlightMutation,
|
||||
UploadPhotoMutation,
|
||||
EditPhotoMutation,
|
||||
DeletePhotoMutation,
|
||||
CreatePointOfInterestMutation,
|
||||
EditPointOfInterestMutation,
|
||||
PointOfInterestMutation,
|
||||
CreateCopilotMutation,
|
||||
EditCopilotMutation,
|
||||
EditUserMutation,
|
||||
CreateEventMutation,
|
||||
EditEventMutation,
|
||||
CreateOrganizationMutation,
|
||||
EditOrganizationMutation,
|
||||
EventMutation,
|
||||
OrganizationMutation,
|
||||
OrganizationUserMutation,
|
||||
))
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import typing
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
import strawberry
|
||||
import sqlalchemy
|
||||
from sqlalchemy import Column
|
||||
from database.models import BaseModel
|
||||
from graphql_schema.entities.types.base import BaseGraphqlInputType
|
||||
|
||||
|
||||
def get_columns_from_model(model: BaseModel, exclude_fields: List[str]) -> List[typing.Tuple[str, Column]]:
|
||||
@@ -49,20 +49,7 @@ def strawberry_sqlalchemy_input(
|
||||
if exclude_fields is None:
|
||||
exclude_fields = []
|
||||
|
||||
ignored_fields = exclude_fields + ["created_at", "created_by_id", "updated_by_id", "updated_at", "deleted"]
|
||||
|
||||
def to_dict(self):
|
||||
dict_data = {}
|
||||
for key, _ in get_columns_from_model(model, ignored_fields):
|
||||
value = getattr(self, key)
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
if isinstance(value, datetime):
|
||||
value = value.astimezone()
|
||||
|
||||
dict_data[key] = value
|
||||
return dict_data
|
||||
ignored_fields = exclude_fields + BaseGraphqlInputType.base_ignored_fields
|
||||
|
||||
def wrapper(cls):
|
||||
annotations = get_annotations_for_scalars(
|
||||
@@ -72,7 +59,6 @@ def strawberry_sqlalchemy_input(
|
||||
)
|
||||
|
||||
cls.__annotations__.update(annotations)
|
||||
cls.to_dict = to_dict
|
||||
|
||||
for col, col_type in annotations.items():
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(logging.DEBUG)
|
||||
|
||||
# Create handlers for logging to the standard output and a file
|
||||
stdoutHandler = logging.StreamHandler(stream=sys.stdout)
|
||||
|
||||
# Set the log levels on the handlers
|
||||
stdoutHandler.setLevel(logging.DEBUG)
|
||||
|
||||
# Create a log format using Log Record attributes
|
||||
fmt = logging.Formatter(
|
||||
"%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d >>> %(message)s"
|
||||
)
|
||||
|
||||
# Set the log format on each handler
|
||||
stdoutHandler.setFormatter(fmt)
|
||||
|
||||
# Add each handler to the Logger object
|
||||
log.addHandler(stdoutHandler)
|
||||
@@ -1,7 +1,41 @@
|
||||
import os
|
||||
from typing import Optional
|
||||
from config import API_URL
|
||||
|
||||
PHOTO_BASE_PATH = ""
|
||||
AIRCRAFT_BASE_PATH = ""
|
||||
FLIGHT_BASE_PATH = ""
|
||||
FLIGHT_GPX_TRACK_PATH = "/app/uploads/tracks"
|
||||
AIRCRAFT_UPLOAD_DEST_PATH = "/app/uploads/aircrafts/"
|
||||
|
||||
|
||||
def get_photo_basepath(flight_id: int) -> str:
|
||||
return f"/app/uploads/photos/{flight_id}"
|
||||
|
||||
|
||||
def get_public_url(filename: Optional[str]) -> str:
|
||||
return f"{API_URL}/uploads/{filename}" if filename else None
|
||||
|
||||
|
||||
def get_photo_url(root) -> str:
|
||||
return get_public_url(f"photos/{root.flight_id}/{root.filename}")
|
||||
|
||||
|
||||
def get_photo_thumbnail_url(root) -> str:
|
||||
thumbnail = get_photo_basepath(root.flight_id) + "/thumbs/" + root.filename
|
||||
if not os.path.isfile(thumbnail):
|
||||
# TODO: logovani
|
||||
return get_public_url(f"photos/{root.flight_id}/{root.filename}")
|
||||
|
||||
return get_public_url(f"photos/{root.flight_id}/thumbs/{root.filename}")
|
||||
|
||||
|
||||
def get_avatar_url(user) -> str:
|
||||
return get_public_url(f"profile/{user.id}/{user.avatar_image_filename}") if user.avatar_image_filename else None
|
||||
|
||||
|
||||
def get_title_image_url(user):
|
||||
if not user.title_image_filename:
|
||||
return f"{API_URL}/static/default-title-image.jpg"
|
||||
|
||||
return get_public_url(f"profile/{user.id}/{user.title_image_filename}")
|
||||
|
||||
@@ -2,6 +2,8 @@ import asyncio
|
||||
import sys
|
||||
from sqlalchemy import select
|
||||
|
||||
from paths import FLIGHT_GPX_TRACK_PATH
|
||||
|
||||
sys.path.insert(0, "/app/src")
|
||||
|
||||
from database import async_session, models # noqa
|
||||
@@ -45,7 +47,7 @@ async def add_elevation_to_tracks():
|
||||
return
|
||||
|
||||
for flight in flights:
|
||||
gpx_file = f"/app/uploads/tracks/{flight.gpx_track_filename}"
|
||||
gpx_file = f"{FLIGHT_GPX_TRACK_PATH}/{flight.gpx_track_filename}"
|
||||
gpx = GPXParser(gpx_file)
|
||||
|
||||
coordinates = await gpx.get_coordinates()
|
||||
@@ -53,7 +55,7 @@ async def add_elevation_to_tracks():
|
||||
gpx_with_elevation = gpx.add_terrain_elevation(elevation)
|
||||
|
||||
output_name = f"terrain_{flight.gpx_track_filename[30:]}"
|
||||
gpx.write(gpx_with_elevation, output=f"/app/uploads/tracks/{output_name}")
|
||||
gpx.write(gpx_with_elevation, output=f"{FLIGHT_GPX_TRACK_PATH}/{output_name}")
|
||||
await models.Flight.update(
|
||||
db_session=session, obj=flight, data={
|
||||
"has_terrain_elevation": True,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import os
|
||||
|
||||
|
||||
def delete_file(path: str, silent: bool = False):
|
||||
try:
|
||||
os.remove(path)
|
||||
except Exception:
|
||||
if not silent:
|
||||
raise
|
||||
|
||||
|
||||
def check_directories(path: str):
|
||||
if not os.path.isdir(path):
|
||||
os.makedirs(path)
|
||||
@@ -0,0 +1,6 @@
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
def gps_to_decimal(input: Tuple[float, float, float]) -> float:
|
||||
d, m, s = input
|
||||
return d + (m / 60.0) + (s / 3600.0)
|
||||
@@ -1,42 +1,9 @@
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional, Tuple
|
||||
from typing import Optional
|
||||
import exif
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from strawberry.file_uploads import Upload
|
||||
from config import API_URL
|
||||
|
||||
|
||||
def get_public_url(filename: Optional[str]):
|
||||
return f"{API_URL}/uploads/{filename}" if filename else None
|
||||
|
||||
|
||||
def check_directories(path: str):
|
||||
if not os.path.isdir(path):
|
||||
os.makedirs(path)
|
||||
|
||||
|
||||
def file_exists(path: str):
|
||||
return os.path.isfile(path)
|
||||
|
||||
|
||||
async def handle_file_upload(file: Upload, path: str):
|
||||
check_directories(path)
|
||||
|
||||
filename = f"{uuid.uuid4()}-{file.filename}"
|
||||
|
||||
content = await file.read()
|
||||
image = open(path + "/" + filename, "wb")
|
||||
image.write(content)
|
||||
image.close()
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def gps_to_decimal(input: Tuple[float, float, float]) -> float:
|
||||
d, m, s = input
|
||||
return d + (m / 60.0) + (s / 3600.0)
|
||||
from utils.file import check_directories
|
||||
from utils.gps import gps_to_decimal
|
||||
|
||||
|
||||
async def parse_exif_info(path: str, filename: str) -> dict:
|
||||
@@ -101,21 +68,3 @@ async def rotate_image(
|
||||
img = img.rotate(angle, Image.LANCZOS, expand=True)
|
||||
check_directories(dest_path)
|
||||
img.save(f"{dest_path}/{dest_filename}", 'JPEG', quality=100)
|
||||
|
||||
|
||||
async def generate_thumbnail(path: str, filename: str, quality: int = 90):
|
||||
return await resize_image(
|
||||
path, filename,
|
||||
new_width=300,
|
||||
dest_path=f"{path}/thumbs/",
|
||||
dest_filename=filename,
|
||||
quality=quality
|
||||
)
|
||||
|
||||
|
||||
def delete_file(path: str, silent: bool = False):
|
||||
try:
|
||||
os.remove(path)
|
||||
except Exception:
|
||||
if not silent:
|
||||
raise
|
||||
@@ -0,0 +1,17 @@
|
||||
import uuid
|
||||
from strawberry.file_uploads import Upload
|
||||
|
||||
from utils.file import check_directories
|
||||
|
||||
|
||||
async def handle_file_upload(file: Upload, path: str) -> str:
|
||||
check_directories(path)
|
||||
|
||||
filename = f"{uuid.uuid4()}-{file.filename}"
|
||||
|
||||
content = await file.read()
|
||||
image = open(path + "/" + filename, "wb")
|
||||
image.write(content)
|
||||
image.close()
|
||||
|
||||
return filename
|
||||
Reference in New Issue
Block a user