Refaktoring a bugfixing

This commit is contained in:
Michal Kváček
2023-10-13 23:24:53 +02:00
parent ce2c023eaf
commit 7f7f79dad6
36 changed files with 698 additions and 722 deletions
+4 -5
View File
@@ -1,21 +1,20 @@
from aiohttp import ClientResponseError from aiohttp import ClientResponseError
from database import models from database import models
from dependencies.db import get_session from database.transaction import get_session
from external.elevation import elevation_api from external.elevation import elevation_api
from external.gpx_parser import GPXParser 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): 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"{FLIGHT_GPX_TRACK_PATH}/{gpx_filename}")
gpx_parser = GPXParser(f"{path}/{gpx_filename}")
coordinates = await gpx_parser.get_coordinates() coordinates = await gpx_parser.get_coordinates()
try: try:
elevation = await elevation_api.get_elevation_for_points(coordinates) elevation = await elevation_api.get_elevation_for_points(coordinates)
tree_with_elevation = gpx_parser.add_terrain_elevation(elevation) tree_with_elevation = gpx_parser.add_terrain_elevation(elevation)
output_name = f"terrain_{gpx_filename}" 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: async with get_session() as db:
await models.Flight.update( await models.Flight.update(
+15
View File
@@ -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
)
+41
View File
@@ -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})
+7 -7
View File
@@ -10,12 +10,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
class BaseModel: class BaseModel:
excluded_columns_in_dict = ("deleted",) excluded_columns_in_dict = ("deleted",)
@classmethod
def _get_column_names(cls):
return [col.name for col in cls.__table__.columns]
def as_dict(self): def as_dict(self):
return { return {c: getattr(self, c) for c in self._get_column_names() if c not in self.excluded_columns_in_dict}
c.name: getattr(self, c.name)
for c in self.__table__.columns
if c.name not in self.excluded_columns_in_dict
}
@classmethod @classmethod
async def get_one(cls, db_session: AsyncSession, id: int): async def get_one(cls, db_session: AsyncSession, id: int):
@@ -23,7 +23,7 @@ class BaseModel:
@classmethod @classmethod
async def create(cls, db_session: AsyncSession, data: dict): 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) db_session.add(model)
await db_session.flush() await db_session.flush()
@@ -37,7 +37,7 @@ class BaseModel:
if not obj: if not obj:
obj = await cls.get_one(db_session, id) obj = await cls.get_one(db_session, id)
for key, value in data.items(): 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) setattr(obj, key, value)
return obj return obj
View File
-10
View File
@@ -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)
+1 -1
View File
@@ -4,7 +4,7 @@ from passlib.hash import bcrypt
from sqlalchemy import select from sqlalchemy import select
from starlette.responses import Response from starlette.responses import Response
from database.models import User from database.models import User
from dependencies.db import get_session from database.transaction import get_session
from endpoints.base import AuthEndpoint from endpoints.base import AuthEndpoint
from pydantic import BaseModel from pydantic import BaseModel
+1 -1
View File
@@ -4,7 +4,7 @@ from sqlalchemy import select
from typing import Optional from typing import Optional
from pydantic import BaseModel, root_validator, Field from pydantic import BaseModel, root_validator, Field
from database.models import User from database.models import User
from dependencies.db import get_session from database.transaction import get_session
from passlib.hash import bcrypt from passlib.hash import bcrypt
+5 -3
View File
@@ -53,16 +53,19 @@ class GPXParser:
@cached() @cached()
async def get_max_speed(self): async def get_max_speed(self):
return max(await self.get_speed()) return max(await self.get_speed()) or 0
@cached() @cached()
async def get_avg_speed(self): async def get_avg_speed(self):
speeds = await self.get_speed() speeds = await self.get_speed()
if not speeds:
return 0
return round(sum(speeds) / len(speeds), 2) return round(sum(speeds) / len(speeds), 2)
@cached() @cached()
async def get_max_altitude(self): async def get_max_altitude(self):
return max(await self.get_altitude()) return max(await self.get_altitude()) or 0
@cached() @cached()
async def get_avg_altitude(self): async def get_avg_altitude(self):
@@ -89,5 +92,4 @@ class GPXParser:
return self.gpx return self.gpx
def write(self, tree: _ElementTree, output: str): def write(self, tree: _ElementTree, output: str):
print(f"ZAPISUJI DO {output}")
tree.write(output) tree.write(output)
+32 -62
View File
@@ -1,56 +1,46 @@
from typing import List, Optional, TYPE_CHECKING from typing import List
import strawberry import strawberry
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 database.transaction import get_session
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
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 paths import AIRCRAFT_UPLOAD_DEST_PATH
from .resolvers.base import get_list, get_one from utils.file import delete_file
from graphql_schema.entities.types.mutation_input import ComboboxInput 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 from graphql_schema.entities.types.types import Aircraft
if TYPE_CHECKING:
pass
AIRCRAFT_UPLOAD_DEST_PATH = "/app/uploads/aircrafts/"
@strawberry.type @strawberry.type
class AircraftQueries: class AircraftQueries:
@strawberry.field() @strawberry.field()
@authenticated_user_only() @authenticated_user_only()
async def aircrafts(root, info) -> List[Aircraft]: async def aircrafts(root, info) -> List[Aircraft]:
query = get_aircraft_resolver(info.context.user_id, info.context.organization_ids) return await AircraftQueryResolver().get_list(
return await get_list(models.Aircraft, query) info.context.user_id,
organization_ids=info.context.organization_ids
)
@strawberry.field() @strawberry.field()
@authenticated_user_only() @authenticated_user_only()
async def aircraft(root, info, id: int) -> Aircraft: async def aircraft(root, info, id: int) -> Aircraft:
query = get_aircraft_resolver(info.context.user_id, info.context.organization_ids, id) return await AircraftQueryResolver().get_one(
return await get_one(models.Aircraft, query) id, user_id=info.context.user_id, organization_ids=info.context.organization_ids
)
@strawberry.type @strawberry.type
class CreateAircraftMutation: class AircraftMutation:
@strawberry_sqlalchemy_input(models.Aircraft, exclude_fields=['id', 'photo_filename'])
class CreateAircraftInput:
photo: Optional[Upload]
organization: Optional[ComboboxInput] = None
@strawberry.mutation @strawberry.mutation
@authenticated_user_only() @authenticated_user_only()
async def create_aircraft(root, info, input: CreateAircraftInput) -> Aircraft: async def create_aircraft(root, info, input: CreateAircraftInput) -> Aircraft:
# TODO: kontrola organizace
input_data = input.to_dict() input_data = input.to_dict()
if input.photo: if input.photo:
input_data['photo_filename'] = await handle_file_upload(input.photo, AIRCRAFT_UPLOAD_DEST_PATH) 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( input_data['organization_id'] = await handle_combobox_save(
db, db,
models.Organization, models.Organization,
@@ -58,29 +48,11 @@ class CreateAircraftMutation:
user_id=info.context.user_id, user_id=info.context.user_id,
) )
aircraft = await models.Aircraft.create( return await AircraftMutationResolver().create(data=input_data, user_id=info.context.user_id)
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
@strawberry.mutation @strawberry.mutation
@authenticated_user_only() @authenticated_user_only()
async def edit_aircraft(root, info, id: int, input: EditAircraftInput) -> Aircraft: async def edit_aircraft(root, info, id: int, input: EditAircraftInput) -> Aircraft:
# TODO: kontrola organizace
update_data = input.to_dict() update_data = input.to_dict()
async with get_session() as db: async with get_session() as db:
if input.organization: if input.organization:
@@ -91,26 +63,24 @@ class EditAircraftMutation:
user_id=info.context.user_id, 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 input.photo:
if aircraft.photo_filename: if existing_photo_filename:
delete_file(AIRCRAFT_UPLOAD_DEST_PATH + "/" + aircraft.photo_filename, silent=True) 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) 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)
async with get_session() as db:
aircraft = await models.Aircraft.update(db, id=id, data=update_data)
return Aircraft(**aircraft.as_dict()) return Aircraft(**aircraft.as_dict())
@strawberry.type
class DeleteAircraftMutation:
@strawberry.mutation @strawberry.mutation
@authenticated_user_only() @authenticated_user_only()
async def delete_aircraft(self, info, id: int) -> Aircraft: async def delete_aircraft(self, info, id: int) -> Aircraft:
async with get_session() as db: return await AircraftMutationResolver().delete(info.context.user_id, id)
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())
+4 -11
View File
@@ -2,13 +2,8 @@ from typing import List
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 graphql_schema.entities.resolvers.base import get_list, get_one, get_base_resolver from graphql_schema.entities.resolvers.base import BaseQueryResolver
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type from graphql_schema.entities.types.types import Airport
@strawberry_sqlalchemy_type(models.Airport)
class Airport:
pass
@strawberry.type @strawberry.type
@@ -16,11 +11,9 @@ class AirportQueries:
@strawberry.field() @strawberry.field()
@authenticated_user_only() @authenticated_user_only()
async def airports(root, info) -> List[Airport]: async def airports(root, info) -> List[Airport]:
query = get_base_resolver(models.Airport, user_id=info.context.user_id) return await BaseQueryResolver(Airport, models.Airport).get_list(info.context.user_id)
return await get_list(models.Airport, query)
@strawberry.field() @strawberry.field()
@authenticated_user_only() @authenticated_user_only()
async def airport(root, info, id: int) -> Airport: 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 BaseQueryResolver(Airport, models.Airport).get_one(id, user_id=info.context.user_id)
return await get_one(models.Airport, query)
+11 -31
View File
@@ -1,65 +1,45 @@
from typing import List, TYPE_CHECKING from typing import List
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 database.transaction import get_session
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
from .resolvers.base import get_base_resolver, get_list, get_one from graphql_schema.entities.types.mutation_input import CreateCopilotInput, EditCopilotInput
from graphql_schema.entities.types.types import Copilot from graphql_schema.entities.types.types import Copilot
if TYPE_CHECKING:
pass
@strawberry.type @strawberry.type
class CopilotQueries: class CopilotQueries:
@strawberry.field() @strawberry.field()
@authenticated_user_only() @authenticated_user_only()
async def copilots(root, info) -> List[Copilot]: 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 BaseQueryResolver(Copilot, models.Copilot).get_list(info.context.user_id)
return await get_list(models.Copilot, query)
@strawberry.field() @strawberry.field()
@authenticated_user_only() @authenticated_user_only()
async def copilot(root, info, id: int) -> Copilot: 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 BaseQueryResolver(Copilot, models.Copilot).get_one(id, user_id=info.context.user_id)
return await get_one(models.Copilot, query)
@strawberry.type @strawberry.type
class CreateCopilotMutation: class CreateCopilotMutation:
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
class CreateCopilotInput:
pass
@strawberry.mutation @strawberry.mutation
@authenticated_user_only() @authenticated_user_only()
async def create_copilot(root, info, input: CreateCopilotInput) -> Copilot: async def create_copilot(root, info, input: CreateCopilotInput) -> Copilot:
input_data = input.to_dict() return await BaseMutationResolver(Copilot, models.Copilot).create(
async with get_session() as db: data=input.to_dict(),
copilot = await models.Copilot.create( user_id=info.context.user_id
db, )
data=dict(
**input_data,
created_by_id=info.context.user_id,
)
)
return Copilot(**copilot.as_dict())
@strawberry.type @strawberry.type
class EditCopilotMutation: class EditCopilotMutation:
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
class EditCopilotInput:
pass
@strawberry.mutation @strawberry.mutation
@authenticated_user_only() @authenticated_user_only()
async def edit_copilot(root, info, id: int, input: EditCopilotInput) -> Copilot: async def edit_copilot(root, info, id: int, input: EditCopilotInput) -> Copilot:
async with get_session() as db: async with get_session() as db:
copilot = (await db.scalars( 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() )).one()
updated_copilot = await models.Copilot.update(db, obj=copilot, data=input.to_dict()) updated_copilot = await models.Copilot.update(db, obj=copilot, data=input.to_dict())
+10 -36
View File
@@ -1,68 +1,42 @@
from typing import List, TYPE_CHECKING from typing import List
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 database.transaction import get_session
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
from .resolvers.base import get_base_resolver, get_list, get_one from graphql_schema.entities.types.mutation_input import CreateEventInput, EditEventInput
from graphql_schema.entities.types.types import Event from graphql_schema.entities.types.types import Event
if TYPE_CHECKING:
pass
@strawberry.type @strawberry.type
class EventQueries: class EventQueries:
@strawberry.field() @strawberry.field()
@authenticated_user_only() @authenticated_user_only()
async def events(root, info) -> List[Event]: async def events(root, info) -> List[Event]:
query = get_base_resolver( return await BaseQueryResolver(Event, models.Event).get_list(
models.Event, user_id=info.context.user_id, info.context.user_id,
order_by=[models.Event.date_from.desc(), models.Event.id.desc()] order_by=[models.Event.date_from.desc(), models.Event.id.desc()]
) )
return await get_list(models.Event, query)
@strawberry.field() @strawberry.field()
@authenticated_user_only() @authenticated_user_only()
async def event(root, info, id: int) -> Event: 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 BaseQueryResolver(Event, models.Event).get_one(id, user_id=info.context.user_id)
return await get_one(models.Event, query)
@strawberry.type @strawberry.type
class CreateEventMutation: class EventMutation:
@strawberry_sqlalchemy_input(model=models.Event, exclude_fields=["id"])
class CreateEventInput:
pass
@strawberry.mutation @strawberry.mutation
@authenticated_user_only() @authenticated_user_only()
async def create_event(root, info, input: CreateEventInput) -> Event: async def create_event(root, info, input: CreateEventInput) -> Event:
input_data = input.to_dict() return await BaseMutationResolver(Event, models.Event).create(input.to_dict(), info.context.user_id)
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
@strawberry.mutation @strawberry.mutation
@authenticated_user_only() @authenticated_user_only()
async def edit_event(root, info, id: int, input: EditEventInput) -> Event: async def edit_event(root, info, id: int, input: EditEventInput) -> Event:
async with get_session() as db: async with get_session() as db:
event = (await db.scalars( 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() )).one()
updated_event = await models.Event.update(db, obj=event, data=input.to_dict()) updated_event = await models.Event.update(db, obj=event, data=input.to_dict())
+40 -60
View File
@@ -2,43 +2,18 @@ import asyncio
from typing import List, Optional from typing import List, Optional
import strawberry import strawberry
from fastapi import HTTPException from fastapi import HTTPException
from sqlalchemy import select
from starlette.status import HTTP_401_UNAUTHORIZED from starlette.status import HTTP_401_UNAUTHORIZED
from background_jobs.weather import download_weather
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 database.transaction import get_session
from graphql_schema.entities.resolvers.flight import ( from graphql_schema.entities.resolvers.flight import handle_aircraft_save, FlightMutationResolver, FlightQueryResolver
handle_aircraft_save, handle_weather_info, FlightMutationResolver, get_airport,
)
from graphql_schema.entities.types.mutation_input import EditFlightInput, CreateFlightInput 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 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 @strawberry.type
class FlightQueries: class FlightQueries:
@@ -47,11 +22,7 @@ class FlightQueries:
if not info.context.user_id and not username: if not info.context.user_id and not username:
raise HTTPException(HTTP_401_UNAUTHORIZED) raise HTTPException(HTTP_401_UNAUTHORIZED)
query = ( return await FlightQueryResolver().get_list(user_id=info.context.user_id, username=username)
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)
@strawberry.field() @strawberry.field()
@error_logging @error_logging
@@ -59,11 +30,7 @@ class FlightQueries:
if not info.context.user_id and not username: if not info.context.user_id and not username:
raise HTTPException(HTTP_401_UNAUTHORIZED) raise HTTPException(HTTP_401_UNAUTHORIZED)
query = ( return await FlightQueryResolver().get_one(id, user_id=info.context.user_id, username=username)
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)
@strawberry.type @strawberry.type
@@ -72,36 +39,49 @@ class FlightMutation:
@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()
user_id = info.context.user_id
async with (get_session() as db): 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)
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(
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={ # TODO: tohle je blbost, bude to vyrabet dve stejne instance!
**data, takeoff_airport_id, landing_airport_id = await asyncio.gather(
"takeoff_weather_info_id": weather_takeoff.id if weather_takeoff else None, handle_combobox_save(
"landing_weather_info_id": weather_landing.id if weather_landing else None, db, models.Airport, input.takeoff_airport, user_id, name_column="icao_code",
"takeoff_airport_id": takeoff_airport.id, extra_data={"name": input.takeoff_airport.name}
"landing_airport_id": landing_airport.id, ),
"has_terrain_elevation": False, 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, "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 @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:
return await FlightMutationResolver.update(info.context, id, input) return await FlightMutationResolver().update(info.context, id, input)
@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:
return await FlightMutationResolver.delete(info.context.user_id, id) return await FlightMutationResolver().delete(info.context.user_id, id)
+31 -46
View File
@@ -1,54 +1,54 @@
from typing import List, TYPE_CHECKING from typing import List
import strawberry import strawberry
from sqlalchemy import delete from sqlalchemy import delete
from sqlalchemy.dialects.mysql import insert from sqlalchemy.dialects.mysql import insert
from sqlalchemy.exc import IntegrityError 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 database.transaction import get_session
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
from .resolvers.base import get_base_resolver, get_list, get_one from graphql_schema.entities.types.mutation_input import CreateOrganizationInput, EditOrganizationInput
from graphql_schema.entities.types.types import Organization from graphql_schema.entities.types.types import Organization
if TYPE_CHECKING:
pass
@strawberry.type @strawberry.type
class OrganizationQueries: class OrganizationQueries:
@strawberry.field() @strawberry.field()
@authenticated_user_only() @authenticated_user_only()
async def organizations(root, info) -> List[Organization]: async def organizations(root, info) -> List[Organization]:
query = get_base_resolver(models.Organization, order_by=[models.Organization.name]) return await BaseQueryResolver(Organization, models.Organization).get_list(
return await get_list(models.Organization, query) info.context.user_id, order_by=[models.Organization.name]
)
@strawberry.field() @strawberry.field()
@authenticated_user_only() @authenticated_user_only()
async def organization(root, info, id: int) -> Organization: async def organization(root, info, id: int) -> Organization:
query = get_base_resolver(models.Organization, object_id=id) return await BaseQueryResolver(Organization, models.Organization).get_one(id, info.context.user_id)
return await get_one(models.Organization, query)
@strawberry.type @strawberry.type
class CreateOrganizationMutation: class OrganizationMutation:
@strawberry_sqlalchemy_input(model=models.Organization, exclude_fields=["id"])
class CreateOrganizationInput:
pass
@strawberry.mutation @strawberry.mutation
@authenticated_user_only() @authenticated_user_only()
async def create_organization(root, info, input: CreateOrganizationInput) -> Organization: async def create_organization(root, info, input: CreateOrganizationInput) -> Organization:
input_data = input.to_dict() return await BaseMutationResolver(Organization, models.Organization).create(
async with get_session() as db: data=input.to_dict(),
organization = await models.Organization.create( user_id=info.context.user_id
db, )
data=dict(
**input_data,
created_by_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 @strawberry.type
@@ -58,7 +58,9 @@ class OrganizationUserMutation:
@authenticated_user_only() @authenticated_user_only()
async def add_to_organization(root, info, organization_id: int) -> Organization: async def add_to_organization(root, info, organization_id: int) -> Organization:
async with get_session() as db: 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: try:
await db.execute( await db.execute(
@@ -68,7 +70,6 @@ class OrganizationUserMutation:
) )
) )
except IntegrityError: except IntegrityError:
print("jiz existuje")
pass pass
return Organization(**organization.as_dict()) return Organization(**organization.as_dict())
@@ -77,7 +78,9 @@ class OrganizationUserMutation:
@authenticated_user_only() @authenticated_user_only()
async def remove_from_organization(root, info, organization_id: int) -> Organization: async def remove_from_organization(root, info, organization_id: int) -> Organization:
async with get_session() as db: 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( await db.execute(
delete(models.user_is_in_organization).filter_by( delete(models.user_is_in_organization).filter_by(
@@ -87,21 +90,3 @@ class OrganizationUserMutation:
) )
return Organization(**organization.as_dict()) 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())
+38 -41
View File
@@ -4,17 +4,17 @@ import strawberry
from sqlalchemy import 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 background_jobs.photo import generate_thumbnail, resize_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 database.transaction import get_session
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
from graphql_schema.entities.types.types import Photo from graphql_schema.entities.types.types import Photo
from paths import get_photo_basepath 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 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 from graphql_schema.entities.types.mutation_input import ComboboxInput
@@ -22,8 +22,7 @@ from graphql_schema.entities.types.mutation_input import ComboboxInput
class PhotoQueries: class PhotoQueries:
@strawberry.field() @strawberry.field()
async def photos(root, info) -> List[Photo]: async def photos(root, info) -> List[Photo]:
query = get_base_resolver(models.Photo, user_id=info.context.user_id) return await BaseQueryResolver(Photo, models.Photo).get_list(user_id=info.context.user_id)
return await get_list(models.Photo, query)
@strawberry.type @strawberry.type
@@ -58,8 +57,7 @@ class UploadPhotoMutation:
}, db_session=db) }, db_session=db)
photo = Photo(**photo_model.as_dict()) photo = Photo(**photo_model.as_dict())
# TODO: udelat primo konkretni bg joby na resize a thumbnaily info.context.background_tasks.add_task(resize_photo, path=path, filename=filename)
info.context.background_tasks.add_task(resize_image, path=path, filename=filename, new_width=2500, quality=85)
info.context.background_tasks.add_task(generate_thumbnail, 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"): if exif_info.get("gps_latitude") and exif_info.get("gps_longitude"):
@@ -77,19 +75,20 @@ class EditPhotoMutation:
point_of_interest: Optional[ComboboxInput] = None point_of_interest: Optional[ComboboxInput] = None
is_flight_cover: Optional[bool] = 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() @strawberry.mutation()
@authenticated_user_only() @authenticated_user_only()
async def edit_photo(self, info, id: int, input: EditPhotoInput) -> Photo: async def edit_photo(self, info, id: int, input: EditPhotoInput) -> Photo:
data = input.to_dict()
# 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)
async with get_session() as db: 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: if input.point_of_interest:
data['point_of_interest_id'] = await handle_combobox_save( data['point_of_interest_id'] = await handle_combobox_save(
@@ -116,25 +115,27 @@ class EditPhotoMutation:
@strawberry.mutation() @strawberry.mutation()
@authenticated_user_only() @authenticated_user_only()
async def rotate_photo(self, info, id: int, angle: int) -> Photo: async def rotate_photo(self, info, id: int, angle: int) -> Photo:
async with get_session() as db: 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(
photo = (await db.scalars(query)).one() 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( await asyncio.gather(
rotate_image( rotate_image(
path=get_photo_basepath(photo.flight_id), path=get_photo_basepath(photo.flight_id),
filename=photo.filename, filename=photo_filename,
angle=angle, angle=angle,
), ),
rotate_image( rotate_image(
path=get_photo_basepath(photo.flight_id) + "/thumbs", path=get_photo_basepath(photo.flight_id) + "/thumbs",
filename=photo.filename, filename=photo_filename,
angle=angle, angle=angle,
), ),
) )
return Photo(**photo.as_dict()) return Photo(**photo_as_dict)
@strawberry.type @strawberry.type
@@ -142,14 +143,10 @@ 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_resolver(models.Photo, user_id=info.context.user_id, object_id=id) photo = await BaseMutationResolver(Photo, models.Photo).delete(user_id=info.context.user_id, 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())
base_path = get_photo_basepath(photo.flight_id) base_path = get_photo_basepath(photo.flight_id)
delete_file(f"{base_path}/{photo.filename}", silent=True) delete_file(f"{base_path}/{photo.filename}", silent=True)
delete_file(f"{base_path}/thumbs/{photo.filename}", silent=True) delete_file(f"{base_path}/thumbs/{photo.filename}", silent=True)
return photo_model return photo
+13 -38
View File
@@ -1,13 +1,12 @@
from typing import List, Optional from typing import List
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 database.transaction 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.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 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 CreatePointOfInterestInput, EditPointOfInterestInput
from graphql_schema.entities.types.mutation_input import ComboboxInput
@strawberry.type @strawberry.type
@@ -15,22 +14,16 @@ class PointOfInterestQueries:
@strawberry.field() @strawberry.field()
@authenticated_user_only() @authenticated_user_only()
async def points_of_interest(root, info) -> List[PointOfInterest]: async def points_of_interest(root, info) -> List[PointOfInterest]:
query = get_base_resolver(models.PointOfInterest, user_id=info.context.user_id) return await BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_list(info.context.user_id)
return await get_list(models.PointOfInterest, query)
@strawberry.field() @strawberry.field()
@authenticated_user_only() @authenticated_user_only()
async def point_of_interest(root, info, id: int) -> PointOfInterest: 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 BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_one(id, info.context.user_id)
return await get_one(models.PointOfInterest, query)
@strawberry.type @strawberry.type
class CreatePointOfInterestMutation: class PointOfInterestMutation:
@strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id'])
class CreatePointOfInterestInput:
type: Optional[ComboboxInput] = None
@strawberry.mutation @strawberry.mutation
@authenticated_user_only() @authenticated_user_only()
async def create_point_of_interest(root, info, input: CreatePointOfInterestInput) -> PointOfInterest: 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 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 await BaseMutationResolver(PointOfInterest, models.PointOfInterest).create(
return PointOfInterest(**poi.as_dict()) input_data, info.context.user_id
)
@strawberry.type
class EditPointOfInterestMutation:
@strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id'])
class EditPointOfInterestInput:
type: Optional[ComboboxInput] = None
@strawberry.mutation @strawberry.mutation
@authenticated_user_only() @authenticated_user_only()
async def edit_point_of_interest(root, info, id: int, input: EditPointOfInterestInput) -> PointOfInterest: async def edit_point_of_interest(root, info, id: int, input: EditPointOfInterestInput) -> PointOfInterest:
input_data = input.to_dict() input_data = input.to_dict()
query = get_base_resolver( query = BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_query(
models.PointOfInterest, user_id=info.context.user_id, object_id=id, include_public=False user_id=info.context.user_id, object_id=id, include_public=False
) )
async with get_session() as db: async with get_session() as db:
@@ -71,19 +58,7 @@ class EditPointOfInterestMutation:
updated_poi = await models.PointOfInterest.update(db, obj=poi, data=input_data) updated_poi = await models.PointOfInterest.update(db, obj=poi, data=input_data)
return PointOfInterest(**updated_poi.as_dict()) return PointOfInterest(**updated_poi.as_dict())
@strawberry.type
class DeletePointOfInterestMutation:
@strawberry.mutation @strawberry.mutation
@authenticated_user_only() @authenticated_user_only()
async def delete_point_of_interest(self, info, id: int) -> PointOfInterest: async def delete_point_of_interest(self, info, id: int) -> PointOfInterest:
query = get_base_resolver( return await BaseMutationResolver(PointOfInterest, models.PointOfInterest).delete(info.context.user_id, id=id)
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())
+5 -5
View File
@@ -2,7 +2,7 @@ from typing import List
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 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 from graphql_schema.entities.types.types import PointOfInterestType
@@ -12,14 +12,14 @@ class PointOfInterestTypeQueries:
@strawberry.field() @strawberry.field()
@authenticated_user_only() @authenticated_user_only()
async def point_of_interest_types(root, info) -> List[PointOfInterestType]: async def point_of_interest_types(root, info) -> List[PointOfInterestType]:
query = get_base_resolver(models.PointOfInterestType, user_id=info.context.user_id) return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_list(info.context.user_id)
return await get_list(models.PointOfInterestType, query)
@strawberry.field() @strawberry.field()
@authenticated_user_only() @authenticated_user_only()
async def point_of_interest_type(root, info, id: int) -> PointOfInterestType: 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 BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_one(
return await get_one(models.PointOfInterestType, query) id, info.context.user_id
)
# #
# @strawberry.type # @strawberry.type
@@ -1,24 +1,41 @@
from operator import or_ from operator import or_
from typing import Set, Optional from typing import Set, Optional
from database import models 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): class AircraftQueryResolver(BaseQueryResolver):
query = get_base_resolver( def __init__(self):
model=models.Aircraft, super().__init__(graphql_type=Aircraft, model=models.Aircraft)
order_by=[models.Aircraft.id.desc()],
object_id=aircraft_id, def get_query(
user_id=user_id if not organization_ids else None self,
) user_id: Optional[int] = None,
if organization_ids: object_id: Optional[int] = None,
query = ( order_by: Optional[list] = None,
query.filter( organization_ids: Optional[Set[int]] = None,
or_( *args,
models.Aircraft.created_by_id == user_id, **kwargs,
models.Aircraft.organization_id.in_(organization_ids) ):
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)
+101 -46
View File
@@ -1,53 +1,108 @@
from typing import Optional, Type from typing import Optional, Type, T
from sqlalchemy import select, or_ from sqlalchemy import select, or_
from sqlalchemy.ext.asyncio import AsyncSession
from database import models from database import models
from dependencies.db import get_session from database.transaction import get_session
def get_base_resolver( class BaseQueryResolver:
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)
if object_id: def __init__(self, graphql_type, model):
if hasattr(model, "id"): self.graphql_type = graphql_type
query = query.filter(model.id == object_id) 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: else:
raise AssertionError(f"Model {model} has no ID column! Cannot query by ID!") update_where['id'] = obj['id']
if hasattr(model, "deleted"): model = await self.model.update(db, data=data, **update_where)
query = query.filter(model.deleted.is_(False)) return self.graphql_type(**model.as_dict())
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())
+60 -123
View File
@@ -1,77 +1,51 @@
import asyncio import asyncio
from datetime import datetime from typing import List, Optional
from typing import List, Optional, Type
from aiocache import cached
from sqlalchemy import delete, insert from sqlalchemy import delete, insert
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from strawberry.file_uploads import Upload from strawberry.file_uploads import Upload
from background_jobs.elevation import add_terrain_elevation_to_flight from background_jobs.elevation import add_terrain_elevation_to_flight
from background_jobs.weather import download_weather
from database import models from database import models
from database.models import flight_has_copilot from database.models import flight_has_copilot
from dependencies.db import get_session from database.transaction import get_session
from external.weather import weather_api
from graphql_schema.entities.helpers.combobox import handle_combobox_save 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.mutation_input import EditFlightInput, TrackItemInput, ComboboxInput
from graphql_schema.entities.types.types import Flight 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: class FlightQueryResolver(BaseQueryResolver):
model: Type[models.BaseModel] def __init__(self):
graphql_type = Flight super().__init__(graphql_type=Flight, model=models.Flight)
@classmethod def get_query(self, user_id: int, object_id: Optional[int] = None, *args, **kwargs):
async def delete(cls, user_id: int, id: int): query = super().get_query(
async with get_session() as db: user_id, object_id,
model = ( order_by=[models.Flight.takeoff_datetime.desc()],
(await db.scalars( include_public=bool(user_id)
get_base_resolver(cls.model, user_id=user_id, object_id=id) )
.filter(cls.model.id == id))
) if kwargs.get('username'):
.one() query = (
query
.join(models.Flight.created_by)
.filter(models.User.public_username == kwargs['username'])
) )
if hasattr(cls.model, "deleted"): return query
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): class FlightMutationResolver(BaseMutationResolver):
model = models.Flight def __init__(self):
graphql_type = Flight super().__init__(Flight, models.Flight)
async def create(self): async def update(self, context, id: int, input: EditFlightInput):
pass
@classmethod
async def update(cls, context, id: int, input: EditFlightInput):
user_id = context.user_id user_id = context.user_id
async with get_session() as db: 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_data = flight.as_dict()
flight_id = flight.id flight_id = flight.id
@@ -84,31 +58,32 @@ class FlightMutationResolver(BaseMutationResolver):
) )
async with get_session() as db: 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: if input.takeoff_airport:
takeoff_airport = await get_airport(db, input.takeoff_airport, user_id) takeoff_airport_id = await handle_combobox_save(
takeoff_datetime = input.takeoff_datetime or flight_data['takeoff_datetime'] 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 context.background_tasks.add_task(
data['takeoff_datetime'] = takeoff_datetime download_weather, flight_id=id, airport_id=takeoff_airport_id, date_time=data['takeoff_datetime'],
type_="takeoff"
)
weather_info = await handle_weather_info( if input.landing_airport:
db, takeoff_datetime, takeoff_airport, flight_data['takeoff_weather_info_id'] 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: if input.aircraft is not None:
data['aircraft_id'] = await handle_aircraft_save(db, user_id, input.aircraft) 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: if input.copilots is not None:
await db.execute(delete(flight_has_copilot).filter_by(flight_id=flight_id)) 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: for copilot_id in copilots:
await db.execute(insert(flight_has_copilot).values(flight_id=flight_id, copilot_id=copilot_id)) 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( async def handle_upload_gpx(original_gpx_filename: str, gpx_track: Upload):
db: AsyncSession, date_time: datetime, airport: models.Airport, existing_weather_id: Optional[int] = None if original_gpx_filename:
) -> Optional[models.WeatherInfo]: delete_file(FLIGHT_GPX_TRACK_PATH + "/" + original_gpx_filename, silent=True)
if not airport.gps_latitude or not airport.gps_longitude:
return None
try: return await handle_file_upload(gpx_track, FLIGHT_GPX_TRACK_PATH)
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): 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: for item in track:
poi_id = None poi_id = None
airport_id = None airport_id = None
if item.point_of_interest: if item.point_of_interest:
poi_id = await handle_combobox_save( poi_id = await handle_combobox_save(
db, models.PointOfInterest, item.point_of_interest, user_id, extra_data={"description": ""} 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, "point_of_interest_id": poi_id,
"airport_id": airport_id, "airport_id": airport_id,
"order": order, "order": order,
"landing_duration": item.landing_duration "landing_duration": item.landing_duration if airport_id else None
} }
) )
order += 1 order += 1
@@ -211,17 +162,3 @@ async def handle_aircraft_save(db: AsyncSession, user_id: int, aircraft: Combobo
"seats": 2, "seats": 2,
"manufacturer": "", "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)
+21
View File
@@ -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 __future__ import annotations
from datetime import datetime
from typing import Optional, List from typing import Optional, List
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 graphql_schema.entities.types.base import BaseGraphqlInputType
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_input
@@ -12,11 +14,51 @@ class ComboboxInput:
name: str 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=[ @strawberry_sqlalchemy_input(models.Flight, exclude_fields=[
"id", "aircraft_id", "deleted", "landing_airport_id", "takeoff_airport_id", "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) ], all_optional=True)
class EditFlightInput: class EditFlightInput(BaseGraphqlInputType):
gpx_track: Optional[Upload] = None # TODO: poresit validaci uploadovaneho souboru! gpx_track: Optional[Upload] = None # TODO: poresit validaci uploadovaneho souboru!
track: Optional[List[TrackItemInput]] = None track: Optional[List[TrackItemInput]] = None
copilots: Optional[List[ComboboxInput]] = None copilots: Optional[List[ComboboxInput]] = None
@@ -26,19 +68,29 @@ class EditFlightInput:
event: Optional[ComboboxInput] = None event: Optional[ComboboxInput] = None
@strawberry_sqlalchemy_input(models.Flight, exclude_fields=[ @strawberry.input()
"id", "aircraft_id", "landing_airport_id", "takeoff_airport_id", "weather_info_takeoff_id", class CreateFlightInput(BaseGraphqlInputType):
"weather_info_landing_id", "with_instructor", "has_terrain_elevation"
])
class CreateFlightInput:
aircraft: ComboboxInput aircraft: ComboboxInput
landing_airport: ComboboxInput landing_airport: ComboboxInput
takeoff_airport: ComboboxInput takeoff_airport: ComboboxInput
takeoff_datetime: datetime
landing_datetime: datetime
@strawberry.input() @strawberry.input()
class TrackItemInput: class TrackItemInput:
# order: int
point_of_interest: Optional[ComboboxInput] = None point_of_interest: Optional[ComboboxInput] = None
airport: Optional[ComboboxInput] = None airport: Optional[ComboboxInput] = None
landing_duration: Optional[int] = 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
+50 -79
View File
@@ -2,7 +2,6 @@ from __future__ import annotations
from datetime import datetime from datetime import datetime
from typing import Optional, Annotated, List from typing import Optional, Annotated, List
import strawberry import strawberry
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 external.gpx_parser import GPXParser 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, poi_dataloader, poi_type_dataloader, event_dataloader, aircraft_dataloader, airport_dataloader, cover_photo_loader,
airport_weather_info_loader, organizations_dataloader 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 graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type
from paths import get_photo_basepath from paths import (
from upload_utils import file_exists, get_public_url get_public_url, get_avatar_url, get_title_image_url, get_photo_thumbnail_url, get_photo_url, FLIGHT_GPX_TRACK_PATH
)
@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 @strawberry.type
@@ -49,35 +28,6 @@ class Point:
lng: 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 @strawberry.type
class GPXTrack: class GPXTrack:
coordinates: List[Point] coordinates: List[Point]
@@ -92,6 +42,45 @@ class GPXTrack:
avg_altitude: float 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) @strawberry_sqlalchemy_type(models.Flight)
class Flight: class Flight:
async def load_gpx_track(root): async def load_gpx_track(root):
@@ -99,7 +88,7 @@ class Flight:
return None return None
try: 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: except OSError:
return None return None
@@ -146,9 +135,7 @@ class Flight:
@strawberry_sqlalchemy_type(models.Copilot) @strawberry_sqlalchemy_type(models.Copilot)
class Copilot: class Copilot:
flights: List[Annotated["Flight", strawberry.lazy('.flight')]] = strawberry.field( flights: List[Flight] = strawberry.field(resolver=lambda root: flights_by_copilot_dataloader.load(root.id))
resolver=lambda root: flights_by_copilot_dataloader.load(root.id)
)
@strawberry_sqlalchemy_type(models.Aircraft) @strawberry_sqlalchemy_type(models.Aircraft)
@@ -156,40 +143,24 @@ class Aircraft:
photo_url: Optional[str] = strawberry.field( photo_url: Optional[str] = strawberry.field(
resolver=lambda root: get_public_url(f"aircrafts/{root.photo_filename}") if root.photo_filename else None 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( flights: List[Flight] = strawberry.field(resolver=lambda root: flights_by_aircraft_dataloader.load(root.id))
resolver=lambda root: flights_by_aircraft_dataloader.load(root.id) organization: Optional[Organization] = strawberry.field(
)
organization: Optional[Annotated["Organization", strawberry.lazy(".organization")]] = strawberry.field(
resolver=lambda root: organizations_dataloader.load(root.organization_id) resolver=lambda root: organizations_dataloader.load(root.organization_id)
) )
@strawberry_sqlalchemy_type(models.Organization) @strawberry_sqlalchemy_type(models.Organization)
class Organization: class Organization:
users: List[Annotated["User", strawberry.lazy(".user")]] = strawberry.field( users: List[User] = strawberry.field(resolver=lambda root: users_in_organization_dataloader.load(root.id))
resolver=lambda root: users_in_organization_dataloader.load(root.id) aircrafts: List[Aircraft] = strawberry.field(
)
aircrafts: List[Annotated["Aircraft", strawberry.lazy(".aircraft")]] = strawberry.field(
resolver=lambda root: aircrafts_from_organization_dataloader.load(root.id) resolver=lambda root: aircrafts_from_organization_dataloader.load(root.id)
) )
@strawberry_sqlalchemy_type(models.User, exclude_fields=['password_hashed']) @strawberry_sqlalchemy_type(models.User, exclude_fields=['password_hashed'])
class User: class User:
async def load_avatar_image_url(root): avatar_image_url: Optional[str] = strawberry.field(resolver=lambda root: get_avatar_url(root))
if not root.avatar_image_filename: title_image_url: str = strawberry.field(resolver=lambda root: get_title_image_url(root))
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( organizations: List[Annotated['Organization', strawberry.lazy(".organization")]] = strawberry.field(
resolver=lambda root: user_organizations_dataloader.load(root.id) resolver=lambda root: user_organizations_dataloader.load(root.id)
) )
+5 -6
View File
@@ -1,4 +1,4 @@
from typing import Optional, TYPE_CHECKING from typing import Optional
import strawberry import strawberry
from graphql import GraphQLError from graphql import GraphQLError
from passlib.hash import bcrypt from passlib.hash import bcrypt
@@ -7,12 +7,11 @@ 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 decorators.error_logging import error_logging from decorators.error_logging import error_logging
from dependencies.db import get_session from database.transaction import get_session
from upload_utils import handle_file_upload, delete_file, resize_image
from graphql_schema.entities.types.types import User from graphql_schema.entities.types.types import User
from utils.file import delete_file
if TYPE_CHECKING: from utils.image import resize_image
pass from utils.upload import handle_file_upload
@strawberry.type @strawberry.type
+8 -15
View File
@@ -1,31 +1,24 @@
from strawberry.tools import merge_types 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.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.flight import FlightMutation
from graphql_schema.entities.organization import ( from graphql_schema.entities.organization import OrganizationUserMutation, OrganizationMutation
CreateOrganizationMutation, EditOrganizationMutation, OrganizationUserMutation
)
from graphql_schema.entities.photo import UploadPhotoMutation, DeletePhotoMutation, EditPhotoMutation 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 from graphql_schema.entities.user import EditUserMutation
Mutation = merge_types("Mutation", ( Mutation = merge_types("Mutation", (
CreateAircraftMutation, AircraftMutation,
EditAircraftMutation,
DeleteAircraftMutation,
FlightMutation, FlightMutation,
UploadPhotoMutation, UploadPhotoMutation,
EditPhotoMutation, EditPhotoMutation,
DeletePhotoMutation, DeletePhotoMutation,
CreatePointOfInterestMutation, PointOfInterestMutation,
EditPointOfInterestMutation,
CreateCopilotMutation, CreateCopilotMutation,
EditCopilotMutation, EditCopilotMutation,
EditUserMutation, EditUserMutation,
CreateEventMutation, EventMutation,
EditEventMutation, OrganizationMutation,
CreateOrganizationMutation,
EditOrganizationMutation,
OrganizationUserMutation, OrganizationUserMutation,
)) ))
@@ -1,10 +1,10 @@
import typing import typing
from datetime import datetime
from typing import List, Optional from typing import List, Optional
import strawberry import strawberry
import sqlalchemy import sqlalchemy
from sqlalchemy import Column from sqlalchemy import Column
from database.models import BaseModel 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]]: 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: if exclude_fields is None:
exclude_fields = [] exclude_fields = []
ignored_fields = exclude_fields + ["created_at", "created_by_id", "updated_by_id", "updated_at", "deleted"] ignored_fields = exclude_fields + BaseGraphqlInputType.base_ignored_fields
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
def wrapper(cls): def wrapper(cls):
annotations = get_annotations_for_scalars( annotations = get_annotations_for_scalars(
@@ -72,7 +59,6 @@ def strawberry_sqlalchemy_input(
) )
cls.__annotations__.update(annotations) cls.__annotations__.update(annotations)
cls.to_dict = to_dict
for col, col_type in annotations.items(): for col, col_type in annotations.items():
try: try:
+22
View File
@@ -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)
+34
View File
@@ -1,7 +1,41 @@
import os
from typing import Optional
from config import API_URL
PHOTO_BASE_PATH = "" PHOTO_BASE_PATH = ""
AIRCRAFT_BASE_PATH = "" AIRCRAFT_BASE_PATH = ""
FLIGHT_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: def get_photo_basepath(flight_id: int) -> str:
return f"/app/uploads/photos/{flight_id}" 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}")
+4 -2
View File
@@ -2,6 +2,8 @@ import asyncio
import sys import sys
from sqlalchemy import select from sqlalchemy import select
from paths import FLIGHT_GPX_TRACK_PATH
sys.path.insert(0, "/app/src") sys.path.insert(0, "/app/src")
from database import async_session, models # noqa from database import async_session, models # noqa
@@ -45,7 +47,7 @@ async def add_elevation_to_tracks():
return return
for flight in flights: 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) gpx = GPXParser(gpx_file)
coordinates = await gpx.get_coordinates() coordinates = await gpx.get_coordinates()
@@ -53,7 +55,7 @@ async def add_elevation_to_tracks():
gpx_with_elevation = gpx.add_terrain_elevation(elevation) gpx_with_elevation = gpx.add_terrain_elevation(elevation)
output_name = f"terrain_{flight.gpx_track_filename[30:]}" 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( await models.Flight.update(
db_session=session, obj=flight, data={ db_session=session, obj=flight, data={
"has_terrain_elevation": True, "has_terrain_elevation": True,
+14
View File
@@ -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)
+6
View File
@@ -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)
+3 -54
View File
@@ -1,42 +1,9 @@
import os
import uuid
from datetime import datetime from datetime import datetime
from typing import Optional, Tuple from typing import Optional
import exif import exif
from PIL import Image, UnidentifiedImageError from PIL import Image, UnidentifiedImageError
from strawberry.file_uploads import Upload from utils.file import check_directories
from config import API_URL from utils.gps import gps_to_decimal
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)
async def parse_exif_info(path: str, filename: str) -> dict: 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) img = img.rotate(angle, Image.LANCZOS, expand=True)
check_directories(dest_path) check_directories(dest_path)
img.save(f"{dest_path}/{dest_filename}", 'JPEG', quality=100) 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
+17
View File
@@ -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