Slugy, webp, hromada zmen
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
"""pridani slugu, odstraneni deprecated sloupcu, pridani defaultu ke jmenu a popisumm
|
||||
|
||||
Revision ID: 6648fb80dd0e
|
||||
Revises: 022bf6fbcd47
|
||||
Create Date: 2024-01-03 06:17:13.457060
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '6648fb80dd0e'
|
||||
down_revision = '022bf6fbcd47'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('aircraft', 'photo_filename')
|
||||
op.add_column('airport', sa.Column('airport_type', sa.Enum('airport', 'ull', 'heliport'), server_default='airport', nullable=False))
|
||||
op.add_column('airport', sa.Column('use_in_gpx_guess', sa.Boolean(), server_default='1', nullable=False))
|
||||
op.add_column('copilot', sa.Column('url_slug', sa.String(length=128), server_default='', nullable=False))
|
||||
op.add_column('copilot', sa.Column('is_public', sa.Boolean(), server_default='0', nullable=False))
|
||||
op.add_column('event', sa.Column('url_slug', sa.String(length=128), server_default='', nullable=False))
|
||||
op.add_column('flight', sa.Column('url_slug', sa.String(length=128), server_default='', nullable=False))
|
||||
op.alter_column('flight', 'takeoff_airport_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True)
|
||||
op.alter_column('flight', 'landing_airport_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True)
|
||||
op.add_column('photo', sa.Column('filename_extension', sa.String(length=4), nullable=False))
|
||||
op.add_column('photo', sa.Column('cache_key', sa.String(length=128), nullable=True))
|
||||
op.drop_column('photo', 'is_flight_cover')
|
||||
op.add_column('point_of_interest', sa.Column('url_slug', sa.String(length=128), server_default='', nullable=False))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('point_of_interest', 'url_slug')
|
||||
op.add_column('photo', sa.Column('is_flight_cover', mysql.TINYINT(display_width=1), server_default=sa.text('0'), autoincrement=False, nullable=False))
|
||||
op.drop_column('photo', 'cache_key')
|
||||
op.drop_column('photo', 'filename_extension')
|
||||
op.alter_column('flight', 'landing_airport_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False)
|
||||
op.alter_column('flight', 'takeoff_airport_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False)
|
||||
op.drop_column('flight', 'url_slug')
|
||||
op.drop_column('event', 'url_slug')
|
||||
op.drop_column('copilot', 'is_public')
|
||||
op.drop_column('copilot', 'url_slug')
|
||||
op.drop_column('airport', 'use_in_gpx_guess')
|
||||
op.drop_column('airport', 'airport_type')
|
||||
op.add_column('aircraft', sa.Column('photo_filename', mysql.VARCHAR(length=128), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,3 +1,5 @@
|
||||
import os.path
|
||||
from time import time
|
||||
from database import models
|
||||
from database.transaction import get_session
|
||||
from utils.image import PhotoEditor
|
||||
@@ -6,14 +8,34 @@ from utils.image import PhotoEditor
|
||||
async def resize_photo(path: str, filename: str, photo_id: int):
|
||||
editor = PhotoEditor(path, filename)
|
||||
editor.resize(new_width=2500)
|
||||
editor.write_to_file(quality=85)
|
||||
|
||||
name, _ = os.path.splitext(filename)
|
||||
editor.write_to_file(quality=85, format_="webp", dest_filename=f"{name}.webp")
|
||||
editor.write_to_file(quality=85) # toto je potreba pro prvni nacteni nahledu ihned po nahrani, pripadne pro
|
||||
# vygenerovani nahledu (async, muze se delat pred/behem zmensovani fotky -> v tu dobu jeste neexistuje webp)
|
||||
width, height = editor.img_size
|
||||
|
||||
# TODO: doresit uklid JPGu -> jsou zbytecne
|
||||
|
||||
async with get_session() as db:
|
||||
await models.Photo.update(db, {"width": width, "height": height}, id=photo_id)
|
||||
await models.Photo.update(
|
||||
db,
|
||||
id=photo_id,
|
||||
data={
|
||||
"width": width,
|
||||
"height": height,
|
||||
"filename_extension": "webp",
|
||||
"cache_key": int(time())
|
||||
})
|
||||
|
||||
|
||||
async def generate_thumbnail(path: str, filename: str):
|
||||
editor = PhotoEditor(path, filename)
|
||||
editor.resize(new_width=300)
|
||||
editor.write_to_file(quality=85, dest_path=f"{path}/thumbs")
|
||||
|
||||
name, _ = os.path.splitext(filename)
|
||||
editor.write_to_file(
|
||||
quality=85,
|
||||
dest_path=f"{path}/thumbs",
|
||||
dest_filename=f"{name}.webp",
|
||||
format_="webp")
|
||||
|
||||
@@ -13,6 +13,7 @@ async def download_weather(date_time: datetime, flight_id: int, airport_id: int,
|
||||
|
||||
try:
|
||||
weather = await weather_api.get_weather_for_hour(date_time.astimezone(), gps=gps)
|
||||
log.warning(weather)
|
||||
except Exception as e:
|
||||
log.error(f"Error in downloading weather: {e}")
|
||||
return None
|
||||
|
||||
+19
-13
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
import datetime
|
||||
from typing import Set, List
|
||||
from sqlalchemy import String, DateTime, ForeignKey, Text, Integer, func, Table, Column, Boolean, select, Float
|
||||
from sqlalchemy import String, DateTime, ForeignKey, Text, Integer, func, Table, Column, Boolean, select, Float, Enum
|
||||
from sqlalchemy.orm import Mapped, relationship, as_declarative, mapped_column
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -74,6 +74,8 @@ class Airport(BaseModel):
|
||||
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
elevation: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||
airport_type: Mapped[str] = mapped_column(Enum("airport", "ull", "heliport"), nullable=False, server_default='airport') # noqa
|
||||
use_in_gpx_guess: Mapped[bool] = mapped_column(Boolean, server_default='1')
|
||||
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=True) # automaticky import nebude mit ID # noqa
|
||||
@@ -103,7 +105,8 @@ class PointOfInterest(BaseModel):
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
description: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
||||
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
|
||||
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
@@ -122,10 +125,11 @@ class Photo(BaseModel):
|
||||
__tablename__ = "photo"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
||||
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||
filename: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
is_flight_cover: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="0") # TODO: odstranit
|
||||
description: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
filename_extension: Mapped[str] = mapped_column(String(4), nullable=False)
|
||||
cache_key: Mapped[str] = mapped_column(String(128), nullable=True)
|
||||
width: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
height: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
exposed_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||
@@ -172,7 +176,6 @@ class Aircraft(BaseModel):
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
call_sign: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
photo_filename: Mapped[str] = mapped_column(String(128), nullable=True) # TODO: odstranit
|
||||
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
|
||||
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||
manufacturer: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||
@@ -253,7 +256,8 @@ class Event(BaseModel):
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
description: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
||||
date_from: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||
date_to: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey('organization.id'), nullable=True)
|
||||
@@ -270,13 +274,13 @@ class Flight(BaseModel):
|
||||
__tablename__ = "flight"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
description: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
||||
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
||||
event_id: Mapped[int] = mapped_column(Integer, ForeignKey("event.id"), nullable=True)
|
||||
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
|
||||
|
||||
takeoff_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=False)
|
||||
landing_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=False)
|
||||
takeoff_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
|
||||
landing_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
|
||||
takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||
landing_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||
duration_total: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||
@@ -310,7 +314,9 @@ class Copilot(BaseModel):
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
||||
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
|
||||
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||
@@ -354,7 +360,7 @@ class User(BaseModel):
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
email: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
|
||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
description: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||
public_username: Mapped[str] = mapped_column(String(128), nullable=True, unique=True)
|
||||
avatar_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
|
||||
title_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
|
||||
|
||||
@@ -25,6 +25,8 @@ class QueryBuilder:
|
||||
|
||||
if only_public and hasattr(self.model, "is_public"):
|
||||
query = query.filter(self.model.is_public.is_(True))
|
||||
if hasattr(self.model, "url_slug"):
|
||||
query = query.filter(self.model.url_slug != '')
|
||||
elif hasattr(self.model, "created_by_id") and created_by_id:
|
||||
query = query.filter(or_(
|
||||
self.model.created_by_id.is_(None),
|
||||
@@ -33,5 +35,7 @@ class QueryBuilder:
|
||||
|
||||
if order_by:
|
||||
query = query.order_by(*order_by)
|
||||
elif hasattr(self.model, "name"):
|
||||
query = query.order_by(self.model.name)
|
||||
|
||||
return query
|
||||
|
||||
@@ -29,12 +29,25 @@ class AircraftQueries:
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@allow_public
|
||||
async def aircraft(root, info, id: int, public: Optional[bool] = False) -> Aircraft:
|
||||
async def aircraft(
|
||||
root, info,
|
||||
id: Optional[int] = None,
|
||||
call_sign: Optional[str] = None,
|
||||
public: Optional[bool] = False
|
||||
) -> Aircraft:
|
||||
filter_params = {}
|
||||
|
||||
if id:
|
||||
filter_params['object_id'] = id
|
||||
|
||||
if call_sign:
|
||||
filter_params['call_sign'] = call_sign
|
||||
|
||||
return await AircraftQueryResolver().get_one(
|
||||
id,
|
||||
user_id=info.context.user_id,
|
||||
organization_ids=info.context.organization_ids,
|
||||
public=public
|
||||
only_public=public,
|
||||
**filter_params
|
||||
)
|
||||
|
||||
|
||||
@@ -44,13 +57,13 @@ class AircraftMutation:
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
async def create_aircraft(root, info, input: CreateAircraftInput) -> Aircraft:
|
||||
return await AircraftMutationResolver().create_new(input, info.context.user_id)
|
||||
return await AircraftMutationResolver().create(info.context, input)
|
||||
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
async def edit_aircraft(root, info, id: int, input: EditAircraftInput) -> Aircraft:
|
||||
return await AircraftMutationResolver().edit(id, user_id=info.context.user_id, data=input)
|
||||
return await AircraftMutationResolver().update(id, user_id=info.context.user_id, data=input)
|
||||
|
||||
@strawberry.mutation
|
||||
@authenticated_user_only()
|
||||
|
||||
@@ -19,4 +19,7 @@ class AirportQueries:
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
async def airport(root, info, id: int) -> Airport:
|
||||
return await BaseQueryResolver(Airport, models.Airport).get_one(id, user_id=info.context.user_id)
|
||||
return await BaseQueryResolver(Airport, models.Airport).get_one(
|
||||
object_id=id,
|
||||
user_id=info.context.user_id
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ from strawberry.types import Info
|
||||
from database import models
|
||||
from decorators.error_logging import error_logging
|
||||
from decorators.endpoints import authenticated_user_only, allow_public
|
||||
from graphql_schema.entities.helpers.detail import get_detail_filters
|
||||
from graphql_schema.entities.resolvers.base import BaseMutationResolver
|
||||
from graphql_schema.entities.resolvers.copilot import CopilotQueryResolver
|
||||
from graphql_schema.entities.types.mutation_input import CreateCopilotInput, EditCopilotInput
|
||||
@@ -21,11 +22,22 @@ class CopilotQueries:
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@allow_public
|
||||
async def copilot(root, info: Info, id: int, pilot_username: Optional[str] = None) -> Copilot:
|
||||
async def copilot(
|
||||
root, info: Info,
|
||||
id: Optional[int] = None,
|
||||
url_slug: Optional[str] = None,
|
||||
pilot_username: Optional[str] = None,
|
||||
public: Optional[bool] = False
|
||||
) -> Copilot:
|
||||
|
||||
filter_params = get_detail_filters(id, url_slug)
|
||||
if pilot_username:
|
||||
filter_params['pilot_username'] = pilot_username
|
||||
|
||||
return await CopilotQueryResolver().get_one(
|
||||
id,
|
||||
user_id=info.context.user_id,
|
||||
pilot_username=pilot_username
|
||||
only_public=public,
|
||||
**filter_params
|
||||
)
|
||||
|
||||
|
||||
@@ -35,10 +47,7 @@ class CopilotMutation:
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
async def create_copilot(root, info, input: CreateCopilotInput) -> Copilot:
|
||||
return await BaseMutationResolver(Copilot, models.Copilot).create(
|
||||
data=input.to_dict(),
|
||||
user_id=info.context.user_id
|
||||
)
|
||||
return await BaseMutationResolver(Copilot, models.Copilot).create(info.context, data=input)
|
||||
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
|
||||
@@ -3,6 +3,7 @@ import strawberry
|
||||
from database import models
|
||||
from decorators.endpoints import authenticated_user_only, allow_public
|
||||
from decorators.error_logging import error_logging
|
||||
from graphql_schema.entities.helpers.detail import get_detail_filters
|
||||
from graphql_schema.entities.helpers.pagination import PaginationWindow, get_pagination_window
|
||||
from graphql_schema.entities.resolvers.base import BaseMutationResolver
|
||||
from graphql_schema.entities.resolvers.event import EventQueryResolver
|
||||
@@ -40,12 +41,21 @@ class EventQueries:
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@allow_public
|
||||
async def event(root, info, id: int, username: Optional[str] = None, public: Optional[bool] = False) -> Event:
|
||||
async def event(
|
||||
root, info,
|
||||
id: Optional[int] = None,
|
||||
url_slug: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
public: Optional[bool] = False
|
||||
) -> Event:
|
||||
filter_params = get_detail_filters(id, url_slug)
|
||||
if username:
|
||||
filter_params['username'] = username
|
||||
|
||||
return await EventQueryResolver().get_one(
|
||||
id,
|
||||
username=username,
|
||||
public=public,
|
||||
user_id=info.context.user_id
|
||||
user_id=info.context.user_id ,
|
||||
** filter_params
|
||||
)
|
||||
|
||||
|
||||
@@ -55,7 +65,7 @@ class EventMutation:
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
async def create_event(root, info, input: CreateEventInput) -> Event:
|
||||
return await BaseMutationResolver(Event, models.Event).create(input.to_dict(), info.context.user_id)
|
||||
return await BaseMutationResolver(Event, models.Event).create(info.context, input)
|
||||
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
|
||||
@@ -10,6 +10,7 @@ from graphql_schema.entities.resolvers.flight import handle_aircraft_save, Fligh
|
||||
from graphql_schema.entities.types.mutation_input import EditFlightInput, CreateFlightInput
|
||||
from graphql_schema.entities.types.types import Flight
|
||||
from .helpers.combobox import handle_combobox_save
|
||||
from .helpers.detail import get_detail_filters
|
||||
from .helpers.pagination import PaginationWindow, get_pagination_window
|
||||
|
||||
|
||||
@@ -50,12 +51,21 @@ class FlightQueries:
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@allow_public
|
||||
async def flight(root, info, id: int, username: Optional[str] = None, public: Optional[bool] = False) -> Flight:
|
||||
async def flight(
|
||||
root, info,
|
||||
id: Optional[int] = None,
|
||||
url_slug: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
public: Optional[bool] = False
|
||||
) -> Flight:
|
||||
filter_params = get_detail_filters(id, url_slug)
|
||||
if username:
|
||||
filter_params['username'] = username
|
||||
|
||||
return await FlightQueryResolver().get_one(
|
||||
id,
|
||||
user_id=info.context.user_id,
|
||||
username=username,
|
||||
public=public
|
||||
only_public=public,
|
||||
**filter_params
|
||||
)
|
||||
|
||||
|
||||
@@ -65,43 +75,7 @@ class FlightMutation:
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
async def create_flight(self, info, input: CreateFlightInput) -> Flight:
|
||||
data = input.to_dict()
|
||||
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)
|
||||
|
||||
# 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,
|
||||
"has_terrain_elevation": False,
|
||||
"name": "",
|
||||
"description": ""
|
||||
})
|
||||
flight = await 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
|
||||
return await FlightMutationResolver().create(info.context, input)
|
||||
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
|
||||
@@ -15,10 +15,11 @@ async def handle_combobox_save(
|
||||
if input.id:
|
||||
return input.id
|
||||
else:
|
||||
|
||||
if not extra_data:
|
||||
extra_data = {}
|
||||
|
||||
# TODO: vyhledat podle `name_column` existujici zaznam, pokud nebude, az pak vytvorit
|
||||
|
||||
data = {name_column: input.name, **extra_data}
|
||||
if hasattr(model, "created_by_id"):
|
||||
data["created_by_id"] = user_id
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
from typing import Optional
|
||||
from graphql import GraphQLError
|
||||
|
||||
|
||||
def get_detail_filters(id: Optional[int], url_slug: Optional[str]) -> dict:
|
||||
filter_params = {}
|
||||
if id:
|
||||
filter_params['object_id'] = id
|
||||
if url_slug is not None:
|
||||
filter_params['url_slug'] = url_slug
|
||||
|
||||
if not filter_params:
|
||||
raise GraphQLError("You must specifiy either urlSlug or id!")
|
||||
|
||||
return filter_params
|
||||
@@ -19,14 +19,18 @@ class OrganizationQueries:
|
||||
@authenticated_user_only()
|
||||
async def organizations(root, info) -> List[Organization]:
|
||||
return await BaseQueryResolver(Organization, models.Organization).get_list(
|
||||
info.context.user_id, order_by=[models.Organization.name]
|
||||
info.context.user_id,
|
||||
order_by=[models.Organization.name]
|
||||
)
|
||||
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
async def organization(root, info, id: int) -> Organization:
|
||||
return await BaseQueryResolver(Organization, models.Organization).get_one(id, info.context.user_id)
|
||||
return await BaseQueryResolver(Organization, models.Organization).get_one(
|
||||
object_id=id,
|
||||
user_id=info.context.user_id
|
||||
)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@@ -36,24 +40,17 @@ class OrganizationMutation:
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
async def create_organization(root, info, input: CreateOrganizationInput) -> Organization:
|
||||
return await BaseMutationResolver(Organization, models.Organization).create(
|
||||
data=input.to_dict(),
|
||||
user_id=info.context.user_id
|
||||
)
|
||||
return await BaseMutationResolver(Organization, models.Organization).create(info.context, data=input)
|
||||
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@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())
|
||||
return await BaseMutationResolver(Organization, models.Organization).update(
|
||||
id,
|
||||
data=input,
|
||||
user_id=info.context.user_id
|
||||
)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
|
||||
@@ -36,7 +36,11 @@ class PhotoQueries:
|
||||
@error_logging
|
||||
@allow_public
|
||||
async def photo(root, info, id: int, public: Optional[bool] = False,) -> Photo:
|
||||
return await BaseQueryResolver(Photo, models.Photo).get_one(id, user_id=info.context.user_id, public=public)
|
||||
return await BaseQueryResolver(Photo, models.Photo).get_one(
|
||||
object_id=id,
|
||||
user_id=info.context.user_id,
|
||||
only_public=public
|
||||
)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
import strawberry
|
||||
from graphql import GraphQLError
|
||||
|
||||
from database import models
|
||||
from decorators.endpoints import authenticated_user_only, allow_public
|
||||
from database.transaction import get_session
|
||||
from decorators.error_logging import error_logging
|
||||
from graphql_schema.entities.helpers.combobox import handle_combobox_save
|
||||
from graphql_schema.entities.helpers.detail import get_detail_filters
|
||||
from graphql_schema.entities.helpers.pagination import get_pagination_window, PaginationWindow
|
||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
|
||||
from graphql_schema.entities.types.types import PointOfInterest
|
||||
from graphql_schema.entities.types.mutation_input import CreatePointOfInterestInput, EditPointOfInterestInput
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class PointOfInterestQueries:
|
||||
@strawberry.field()
|
||||
@@ -33,10 +35,18 @@ class PointOfInterestQueries:
|
||||
|
||||
@strawberry.field()
|
||||
@allow_public
|
||||
async def point_of_interest(root, info, id: int, public: bool = False) -> PointOfInterest:
|
||||
async def point_of_interest(
|
||||
root, info,
|
||||
url_slug: Optional[str] = None,
|
||||
id: Optional[int] = None,
|
||||
public: bool = False
|
||||
) -> PointOfInterest:
|
||||
filter_params = get_detail_filters(id, url_slug)
|
||||
|
||||
return await BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_one(
|
||||
id, info.context.user_id,
|
||||
only_public=public
|
||||
user_id=info.context.user_id,
|
||||
only_public=public,
|
||||
**filter_params
|
||||
)
|
||||
|
||||
|
||||
@@ -54,9 +64,10 @@ class PointOfInterestMutation:
|
||||
db, models.PointOfInterestType, input.type, info.context.user_id
|
||||
)
|
||||
|
||||
return await BaseMutationResolver(PointOfInterest, models.PointOfInterest).create(
|
||||
input_data, info.context.user_id
|
||||
)
|
||||
input_data['created_by_id'] = info.context.user_id
|
||||
return await BaseMutationResolver(PointOfInterest, models.PointOfInterest)._do_create(
|
||||
db, input_data
|
||||
)
|
||||
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
|
||||
@@ -21,7 +21,8 @@ class PointOfInterestTypeQueries:
|
||||
@authenticated_user_only()
|
||||
async def point_of_interest_type(root, info, id: int) -> PointOfInterestType:
|
||||
return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_one(
|
||||
id, info.context.user_id
|
||||
object_id=id,
|
||||
user_id=info.context.user_id
|
||||
)
|
||||
|
||||
#
|
||||
|
||||
@@ -21,10 +21,17 @@ class AircraftQueryResolver(BaseQueryResolver):
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
call_sign = kwargs.get("call_sign")
|
||||
filters = {}
|
||||
if object_id:
|
||||
filters['object_id'] = object_id
|
||||
if call_sign:
|
||||
filters['call_sign'] = call_sign
|
||||
|
||||
query = super().get_query(
|
||||
order_by=[models.Aircraft.id.desc()],
|
||||
object_id=object_id,
|
||||
user_id=user_id if not organization_ids else None
|
||||
user_id=user_id if not organization_ids else None,
|
||||
**filters
|
||||
)
|
||||
if organization_ids:
|
||||
query = (
|
||||
@@ -43,20 +50,22 @@ class AircraftMutationResolver(BaseMutationResolver):
|
||||
def __init__(self):
|
||||
super().__init__(graphql_type=Aircraft, model=models.Aircraft)
|
||||
|
||||
async def create_new(self, data: CreateAircraftInput, user_id: int) -> Aircraft:
|
||||
async def create(self, context, data: CreateAircraftInput) -> Aircraft:
|
||||
input_data = data.to_dict()
|
||||
if data.organization:
|
||||
async with get_session() as db:
|
||||
|
||||
async with get_session() as db:
|
||||
input_data['created_by_id'] = context.user_id
|
||||
if data.organization:
|
||||
input_data['organization_id'] = await handle_combobox_save(
|
||||
db,
|
||||
models.Organization,
|
||||
input=data.organization,
|
||||
user_id=user_id,
|
||||
user_id=context.user_id,
|
||||
)
|
||||
|
||||
return await self.create(data=input_data, user_id=user_id)
|
||||
return await self._do_create(db, data=input_data)
|
||||
|
||||
async def edit(self, id: int, user_id: int, data: EditAircraftInput) -> Aircraft:
|
||||
async def update(self, id: int, user_id: int, data: EditAircraftInput) -> Aircraft:
|
||||
update_data = data.to_dict()
|
||||
async with get_session() as db:
|
||||
if data.organization:
|
||||
@@ -66,5 +75,5 @@ class AircraftMutationResolver(BaseMutationResolver):
|
||||
input=data.organization,
|
||||
user_id=user_id,
|
||||
)
|
||||
aircraft = await models.Aircraft.update(db, id=id, data=update_data)
|
||||
return Aircraft(**aircraft.as_dict())
|
||||
|
||||
return await self._do_update(db, id, update_data)
|
||||
|
||||
@@ -33,7 +33,6 @@ class BaseQueryResolver(BaseResolver):
|
||||
object_id: Optional[int] = None,
|
||||
order_by: Optional[list] = None,
|
||||
only_public: Optional[bool] = False,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
query = self.query_builder.get_simple_query(created_by_id=user_id, order_by=order_by, only_public=only_public)
|
||||
@@ -52,8 +51,8 @@ class BaseQueryResolver(BaseResolver):
|
||||
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) -> GQL_TYPE:
|
||||
query = self.get_query(user_id, object_id=id, **kwargs)
|
||||
async def get_one(self, user_id: Optional[int] = None, **kwargs) -> GQL_TYPE:
|
||||
query = self.get_query(user_id, **kwargs)
|
||||
return await self._get_one(query)
|
||||
|
||||
|
||||
@@ -62,21 +61,31 @@ class BaseMutationResolver(BaseResolver):
|
||||
query = self.query_builder.get_simple_query(created_by_id=created_by_id).filter(self.model.id == id)
|
||||
return (await db.scalars(query)).one()
|
||||
|
||||
async def _do_create(self, data) -> GQL_TYPE:
|
||||
async with get_session() as db:
|
||||
model = await self.model.create(db, data=data)
|
||||
return self.graphql_type(**model.as_dict())
|
||||
async def _do_create(self, db: AsyncSession, data: dict) -> GQL_TYPE:
|
||||
model = await self.model.create(db, data=data)
|
||||
return self.graphql_type(**model.as_dict())
|
||||
|
||||
async def _do_update(self, db: AsyncSession, obj: models.BaseModel | dict, data: dict) -> GQL_TYPE:
|
||||
async def _do_update(self, db: AsyncSession, obj: models.BaseModel | dict | int, data: dict) -> GQL_TYPE:
|
||||
update_where = {}
|
||||
if isinstance(obj, models.BaseModel):
|
||||
update_where['obj'] = obj
|
||||
else:
|
||||
elif isinstance(obj, dict):
|
||||
update_where['id'] = obj['id']
|
||||
else:
|
||||
update_where['id'] = obj
|
||||
|
||||
model = await self.model.update(db, data=data, **update_where)
|
||||
return self.graphql_type(**model.as_dict())
|
||||
|
||||
async def create(self, context, data: BaseGraphqlInputType) -> GQL_TYPE:
|
||||
input_data = data.to_dict()
|
||||
|
||||
if hasattr(self.model, "created_by_id"):
|
||||
input_data['created_by_id'] = context.user_id
|
||||
|
||||
async with get_session() as db:
|
||||
return await self._do_create(db, input_data)
|
||||
|
||||
async def update(self, id: int, data: BaseGraphqlInputType, user_id: int) -> GQL_TYPE:
|
||||
async with get_session() as db:
|
||||
item = await self._get_one(db, id, user_id)
|
||||
@@ -93,9 +102,3 @@ class BaseMutationResolver(BaseResolver):
|
||||
|
||||
return self.graphql_type(**model.as_dict())
|
||||
|
||||
async def create(self, data: dict, user_id: Optional[int] = None) -> GQL_TYPE:
|
||||
input_data = {**data}
|
||||
if hasattr(self.model, "created_by_id"):
|
||||
input_data['created_by_id'] = user_id
|
||||
|
||||
return await self._do_create(input_data)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
from typing import List, Optional
|
||||
from sqlalchemy import delete, insert
|
||||
from sqlalchemy import delete, insert, select, func, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from strawberry.file_uploads import Upload
|
||||
from background_jobs.elevation import add_terrain_elevation_to_flight
|
||||
@@ -8,9 +8,10 @@ from background_jobs.weather import download_weather
|
||||
from database import models
|
||||
from database.models import flight_has_copilot
|
||||
from database.transaction import get_session
|
||||
from external.gpx_parser import GPXParser
|
||||
from graphql_schema.entities.helpers.combobox import handle_combobox_save
|
||||
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
|
||||
from graphql_schema.entities.types.mutation_input import EditFlightInput, TrackItemInput, ComboboxInput
|
||||
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver, GQL_TYPE
|
||||
from graphql_schema.entities.types.mutation_input import EditFlightInput, TrackItemInput, ComboboxInput, CreateFlightInput
|
||||
from graphql_schema.entities.types.types import Flight
|
||||
from paths import FLIGHT_GPX_TRACK_PATH
|
||||
from utils.file import delete_file
|
||||
@@ -25,12 +26,20 @@ class FlightQueryResolver(BaseQueryResolver):
|
||||
self,
|
||||
user_id: Optional[int] = None,
|
||||
object_id: Optional[int] = None,
|
||||
url_slug: Optional[str] = None,
|
||||
only_public: Optional[bool] = False,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
filters = {}
|
||||
if object_id:
|
||||
filters['object_id'] = object_id
|
||||
if url_slug:
|
||||
filters['url_slug'] = url_slug
|
||||
|
||||
query = super().get_query(
|
||||
user_id, object_id,
|
||||
user_id,
|
||||
**filters,
|
||||
order_by=[models.Flight.takeoff_datetime.desc()],
|
||||
only_public=only_public
|
||||
)
|
||||
@@ -66,6 +75,90 @@ class FlightMutationResolver(BaseMutationResolver):
|
||||
def __init__(self):
|
||||
super().__init__(Flight, models.Flight)
|
||||
|
||||
async def get_airport_id_by_gps(self, gps_lat: float, gps_lng: float) -> Optional[int]:
|
||||
async with get_session() as db:
|
||||
query = select(
|
||||
models.Airport,
|
||||
func.coalesce((6371 * func.acos(
|
||||
func.cos(func.radians(gps_lat)) *
|
||||
func.cos(func.radians(models.Airport.gps_latitude)) *
|
||||
func.cos(func.radians(models.Airport.gps_longitude) - func.radians(gps_lng)) +
|
||||
func.sin(func.radians(gps_lat)) *
|
||||
func.sin(func.radians(models.Airport.gps_latitude))
|
||||
)), 9999).label("distance")
|
||||
).filter(models.Airport.use_in_gpx_guess.is_(True)).order_by("distance").having(text("distance < 1")).limit(1)
|
||||
|
||||
data = (await db.execute(query)).one_or_none()
|
||||
|
||||
if data:
|
||||
airport, distance = data
|
||||
return airport.id
|
||||
|
||||
return None
|
||||
|
||||
async def extract_data_from_gpx(self, gpx_filename: str) -> dict:
|
||||
data = GPXParser(f"{FLIGHT_GPX_TRACK_PATH}/{gpx_filename}")
|
||||
|
||||
times, coordinates = await asyncio.gather(
|
||||
data.get_times(),
|
||||
data.get_coordinates()
|
||||
)
|
||||
takeoff_airport_id, landing_airport_id = await asyncio.gather(
|
||||
self.get_airport_id_by_gps(coordinates[0]['lat'], coordinates[0]['lng']),
|
||||
self.get_airport_id_by_gps(coordinates[-1]['lat'], coordinates[-1]['lng']),
|
||||
)
|
||||
|
||||
return {
|
||||
"takeoff_airport_id": takeoff_airport_id,
|
||||
"landing_airport_id": landing_airport_id,
|
||||
"takeoff_datetime": times[0],
|
||||
"landing_datetime": times[-1],
|
||||
}
|
||||
|
||||
async def create(self, context, input: CreateFlightInput) -> Flight:
|
||||
data = input.to_dict()
|
||||
user_id = context.user_id
|
||||
|
||||
if input.gpx_track_file:
|
||||
data['gpx_track_filename'] = await handle_upload_gpx(gpx_track=input.gpx_track_file, context=context)
|
||||
data_from_gpx = await self.extract_data_from_gpx(data['gpx_track_filename'])
|
||||
data.update(data_from_gpx)
|
||||
else:
|
||||
async with get_session() as db:
|
||||
data.update({
|
||||
"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}
|
||||
),
|
||||
"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}
|
||||
),
|
||||
})
|
||||
|
||||
async with get_session() as db:
|
||||
aircraft_id = await handle_aircraft_save(db, context.user_id, input.aircraft)
|
||||
|
||||
data.update({
|
||||
"aircraft_id": aircraft_id,
|
||||
"has_terrain_elevation": False,
|
||||
"name": "",
|
||||
"description": "",
|
||||
"created_by_id": context.user_id
|
||||
})
|
||||
flight = await self._do_create(db, data)
|
||||
|
||||
context.background_tasks.add_task(
|
||||
download_weather,
|
||||
flight_id=flight.id, airport_id=flight.takeoff_airport_id, date_time=flight.takeoff_datetime, type_="takeoff"
|
||||
)
|
||||
context.background_tasks.add_task(
|
||||
download_weather,
|
||||
flight_id=flight.id, airport_id=flight.landing_airport_id, date_time=flight.landing_datetime, type_="landing"
|
||||
)
|
||||
|
||||
return flight
|
||||
|
||||
async def update(self, context, id: int, input: EditFlightInput) -> Flight:
|
||||
user_id = context.user_id
|
||||
async with get_session() as db:
|
||||
@@ -75,10 +168,11 @@ class FlightMutationResolver(BaseMutationResolver):
|
||||
|
||||
data = input.to_dict()
|
||||
|
||||
if input.gpx_track is not None:
|
||||
data['gpx_track_filename'] = await handle_upload_gpx(flight_data['gpx_track_filename'], input.gpx_track)
|
||||
context.background_tasks.add_task(
|
||||
add_terrain_elevation_to_flight, flight_id=id, gpx_filename=data['gpx_track_filename']
|
||||
if input.gpx_track_file is not None:
|
||||
data['gpx_track_filename'] = await handle_upload_gpx(
|
||||
gpx_track=input.gpx_track_file,
|
||||
context=context,
|
||||
original_gpx_filename=flight_data['gpx_track_filename']
|
||||
)
|
||||
|
||||
async with get_session() as db:
|
||||
@@ -136,11 +230,14 @@ class FlightMutationResolver(BaseMutationResolver):
|
||||
return await self._do_update(db, flight_data, data)
|
||||
|
||||
|
||||
async def handle_upload_gpx(original_gpx_filename: str, gpx_track: Upload):
|
||||
async def handle_upload_gpx(gpx_track: Upload, context, original_gpx_filename: Optional[str] = None):
|
||||
if original_gpx_filename:
|
||||
delete_file(FLIGHT_GPX_TRACK_PATH + "/" + original_gpx_filename, silent=True)
|
||||
|
||||
return await handle_file_upload(gpx_track, FLIGHT_GPX_TRACK_PATH)
|
||||
filename = await handle_file_upload(gpx_track, FLIGHT_GPX_TRACK_PATH)
|
||||
context.background_tasks.add_task(add_terrain_elevation_to_flight, flight_id=id, gpx_filename=filename)
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
async def handle_track_edit(db: AsyncSession, flight_id: int, track: List[TrackItemInput], user_id: int):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import shutil
|
||||
from time import time
|
||||
from typing import Optional
|
||||
from PIL import Image
|
||||
from pydantic import BaseModel
|
||||
@@ -91,29 +92,35 @@ class PhotoMutationResolver(BaseMutationResolver):
|
||||
|
||||
async def upload(self, info, input: UploadPhotoInput) -> Photo:
|
||||
path = get_photo_basepath(input.flight_id)
|
||||
filename = await handle_file_upload(input.photo, path)
|
||||
exif_info = await parse_exif_info(path, filename)
|
||||
img_name = await handle_file_upload(input.photo, path, uid_prefix=False, overwrite=False)
|
||||
|
||||
img = Image.open(f"{path}/{filename}")
|
||||
photo = await PhotoMutationResolver().create(
|
||||
user_id=info.context.user_id,
|
||||
data={
|
||||
"flight_id": input.flight_id,
|
||||
"name": input.name,
|
||||
"filename": filename,
|
||||
"width": img.width,
|
||||
"height": img.height,
|
||||
"description": input.description,
|
||||
"exposed_at": exif_info.get("datetime_original"),
|
||||
"gps_latitude": exif_info.get("gps_latitude"),
|
||||
"gps_longitude": exif_info.get("gps_longitude"),
|
||||
"gps_altitude": exif_info.get("gps_altitude"),
|
||||
"is_flight_cover": False,
|
||||
},
|
||||
)
|
||||
exif_info = await parse_exif_info(path, img_name)
|
||||
|
||||
info.context.background_tasks.add_task(resize_photo, path=path, filename=filename, photo_id=photo.id)
|
||||
info.context.background_tasks.add_task(generate_thumbnail, path=path, filename=filename)
|
||||
img = Image.open(f"{path}/{img_name}")
|
||||
filename, filename_ext = os.path.splitext(img_name)
|
||||
|
||||
async with get_session() as db:
|
||||
photo = await PhotoMutationResolver()._do_create(
|
||||
db,
|
||||
data={
|
||||
"flight_id": input.flight_id,
|
||||
"name": input.name,
|
||||
"filename": filename,
|
||||
"filename_extension": filename_ext[1:], # nechci ukladat tecku na zacatku
|
||||
"cache_key": int(time()),
|
||||
"width": img.width,
|
||||
"height": img.height,
|
||||
"description": input.description,
|
||||
"exposed_at": exif_info.get("datetime_original"),
|
||||
"gps_latitude": exif_info.get("gps_latitude"),
|
||||
"gps_longitude": exif_info.get("gps_longitude"),
|
||||
"gps_altitude": exif_info.get("gps_altitude"),
|
||||
"created_by_id": info.context.user_id,
|
||||
},
|
||||
)
|
||||
|
||||
info.context.background_tasks.add_task(resize_photo, path=path, filename=img_name, photo_id=photo.id)
|
||||
info.context.background_tasks.add_task(generate_thumbnail, path=path, filename=img_name)
|
||||
|
||||
if exif_info.get("gps_latitude") and exif_info.get("gps_longitude"):
|
||||
info.context.background_tasks.add_task(add_terrain_elevation_to_photo, photo=photo)
|
||||
@@ -164,7 +171,8 @@ class PhotoMutationResolver(BaseMutationResolver):
|
||||
async with get_session() as db:
|
||||
return await self._do_update(db, obj={"id": id}, data={
|
||||
"width": editor.img.width,
|
||||
"height": editor.img.height
|
||||
"height": editor.img.height,
|
||||
"cache_key": int(time())
|
||||
})
|
||||
|
||||
async def adjust(self, id: int, user_id: int, adjustment: AdjustmentInput, info):
|
||||
@@ -209,15 +217,25 @@ class PhotoMutationResolver(BaseMutationResolver):
|
||||
|
||||
return await self._do_update(db, obj={"id": id}, data={
|
||||
"width": editor.img.width,
|
||||
"height": editor.img.height
|
||||
"height": editor.img.height,
|
||||
"cache_key": int(time())
|
||||
})
|
||||
|
||||
async def delete(self, user_id: int, id: int) -> Photo:
|
||||
photo = await super().delete(user_id, id)
|
||||
|
||||
base_path = get_photo_basepath(photo.flight_id)
|
||||
delete_file(f"{base_path}/{photo.filename}", silent=True)
|
||||
delete_file(f"{base_path}/_original_{photo.filename}", silent=True)
|
||||
delete_file(f"{base_path}/thumbs/{photo.filename}", silent=True)
|
||||
|
||||
files_to_delete = [
|
||||
photo.filename,
|
||||
f"_original_{photo.filename}",
|
||||
f"{photo.filename}.{photo.filename_extension}",
|
||||
f"_original_{photo.filename}.{photo.filename_extension}",
|
||||
f"thumbs/{photo.filename}",
|
||||
f"thumbs/{photo.filename}.{photo.filename_extension}",
|
||||
f"thumbs/{photo.filename}.webp",
|
||||
]
|
||||
for filename in files_to_delete:
|
||||
delete_file(f"{base_path}/{filename}", silent=True)
|
||||
|
||||
return photo
|
||||
|
||||
@@ -102,7 +102,7 @@ class AdjustmentInput:
|
||||
"takeoff_weather_info_id", "landing_weather_info_id", "gpx_track_filename", "event_id"
|
||||
], all_optional=True)
|
||||
class EditFlightInput(BaseGraphqlInputType):
|
||||
gpx_track: Optional[Upload] = None # TODO: poresit validaci uploadovaneho souboru!
|
||||
gpx_track_file: Optional[Upload] = None # TODO: poresit validaci uploadovaneho souboru!
|
||||
track: Optional[List[TrackItemInput]] = None
|
||||
copilots: Optional[List[ComboboxInput]] = None
|
||||
aircraft: Optional[ComboboxInput] = None
|
||||
@@ -114,10 +114,11 @@ class EditFlightInput(BaseGraphqlInputType):
|
||||
@strawberry.input()
|
||||
class CreateFlightInput(BaseGraphqlInputType):
|
||||
aircraft: ComboboxInput
|
||||
landing_airport: ComboboxInput
|
||||
takeoff_airport: ComboboxInput
|
||||
takeoff_datetime: datetime
|
||||
landing_datetime: datetime
|
||||
gpx_track_file: Optional[Upload] = None # TODO: poresit validaci uploadovaneho souboru!
|
||||
landing_airport: Optional[ComboboxInput] = None
|
||||
takeoff_airport: Optional[ComboboxInput] = None
|
||||
takeoff_datetime: Optional[datetime] = None
|
||||
landing_datetime: Optional[datetime] = None
|
||||
|
||||
|
||||
@strawberry.input()
|
||||
|
||||
@@ -128,8 +128,12 @@ class Flight:
|
||||
copilots: Optional[List[Copilot]] = strawberry.field(resolver=load_copilots)
|
||||
event: Optional[Event] = strawberry.field(resolver=load_event)
|
||||
aircraft: Aircraft = strawberry.field(resolver=lambda root: aircraft_dataloader.load(root.aircraft_id))
|
||||
takeoff_airport: Airport = strawberry.field(resolver=lambda root: airport_dataloader.load(root.takeoff_airport_id))
|
||||
landing_airport: Airport = strawberry.field(resolver=lambda root: airport_dataloader.load(root.landing_airport_id))
|
||||
takeoff_airport: Optional[Airport] = strawberry.field(
|
||||
resolver=lambda root: airport_dataloader.load(root.takeoff_airport_id)
|
||||
)
|
||||
landing_airport: Optional[Airport] = strawberry.field(
|
||||
resolver=lambda root: airport_dataloader.load(root.landing_airport_id)
|
||||
)
|
||||
title_photo: Optional[Photo] = strawberry.field(resolver=lambda root: photo_dataloader.load(root.title_photo_id))
|
||||
track: List[FlightTrack] = strawberry.field(resolver=lambda root: flight_track_dataloader.load(root.id))
|
||||
takeoff_weather_info: Optional[WeatherInfo] = strawberry.field(
|
||||
|
||||
+18
-6
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
from typing import Optional
|
||||
from config import API_URL
|
||||
from logger import log
|
||||
|
||||
PHOTO_BASE_PATH = ""
|
||||
AIRCRAFT_BASE_PATH = ""
|
||||
@@ -18,16 +19,27 @@ def get_public_url(filename: Optional[str]) -> str:
|
||||
|
||||
|
||||
def get_photo_url(root) -> str:
|
||||
return get_public_url(f"photos/{root.flight_id}/{root.filename}")
|
||||
filename = root.filename if not root.filename_extension else f"{root.filename}.{root.filename_extension}"
|
||||
return get_public_url(f"photos/{root.flight_id}/{filename}?cache={root.cache_key}")
|
||||
|
||||
|
||||
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}")
|
||||
filename = root.filename if not root.filename_extension else f"{root.filename}.{root.filename_extension}"
|
||||
thumbnail_names = [
|
||||
f"thumbs/{root.filename}.webp",
|
||||
f"thumbs/{filename}",
|
||||
filename,
|
||||
]
|
||||
|
||||
return get_public_url(f"photos/{root.flight_id}/thumbs/{root.filename}")
|
||||
for thumbnail in thumbnail_names:
|
||||
if os.path.isfile(f"{get_photo_basepath(root.flight_id)}/{thumbnail}"):
|
||||
# TODO: logovani
|
||||
return get_public_url(f"photos/{root.flight_id}/{thumbnail}?cache={root.cache_key}")
|
||||
else:
|
||||
log.warning(f"Missing thumbnail {thumbnail} in flight ID={root.flight_id}")
|
||||
|
||||
# TODO: doplnit chybejici nahled, tohle by se ale nikdy nemelo stat! Vzdy musi existovat alespon originalni fotka
|
||||
return get_public_url(f"photos/missing-thumbnail.webp")
|
||||
|
||||
|
||||
def get_avatar_url(user) -> str:
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import asyncio
|
||||
import os.path
|
||||
import time
|
||||
|
||||
import sys
|
||||
from PIL import Image
|
||||
from PIL.Image import DecompressionBombWarning
|
||||
from sqlalchemy import select
|
||||
|
||||
sys.path.insert(0, "/app/src")
|
||||
from paths import get_photo_basepath # noqa
|
||||
from database import async_session, models # noqa
|
||||
|
||||
|
||||
async def migrate_photos_to_webp():
|
||||
async with async_session() as session:
|
||||
photos = (await session.scalars(
|
||||
select(models.Photo).filter(models.Photo.filename_extension == "")
|
||||
)).all()
|
||||
|
||||
for photo in photos:
|
||||
path = get_photo_basepath(photo.flight_id)
|
||||
|
||||
try:
|
||||
img = Image.open(f"{path}/{photo.filename}")
|
||||
|
||||
filename, ext = os.path.splitext(photo.filename)
|
||||
new_filename = filename[37:] # vyhodim uuid z filename
|
||||
|
||||
img.save(f"{path}/{new_filename}.webp", format="webp")
|
||||
|
||||
thumb = Image.open(f"{path}/thumbs/{photo.filename}")
|
||||
thumb.save(f"{path}/thumbs/{new_filename}.webp", format="webp")
|
||||
|
||||
await models.Photo.update(session, {
|
||||
"filename": new_filename,
|
||||
"cache_key": int(time.time()),
|
||||
"filename_ext": "webp",
|
||||
}, obj=photo)
|
||||
print(path, photo.filename, "=>", new_filename, "OK")
|
||||
except (Exception, DecompressionBombWarning) as e:
|
||||
print(path, photo.filename, e)
|
||||
|
||||
await session.flush()
|
||||
await session.commit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(migrate_photos_to_webp())
|
||||
+3
-2
@@ -127,11 +127,12 @@ class PhotoEditor:
|
||||
return img_io
|
||||
|
||||
def write_to_file(
|
||||
self, quality: int = 90, dest_path: Optional[str] = None, dest_filename: Optional[str] = None
|
||||
self, quality: int = 90, dest_path: Optional[str] = None, dest_filename: Optional[str] = None,
|
||||
format_: Optional[str] = "JPEG"
|
||||
) -> str:
|
||||
check_directories(dest_path or self.path)
|
||||
|
||||
dest = f"{dest_path or self.path}/{dest_filename or self.filename}"
|
||||
self.img.save(dest, 'JPEG', quality=quality)
|
||||
self.img.save(dest, format_, quality=quality)
|
||||
|
||||
return dest
|
||||
|
||||
+14
-3
@@ -1,16 +1,27 @@
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from strawberry.file_uploads import Upload
|
||||
|
||||
from utils.file import check_directories
|
||||
|
||||
|
||||
async def handle_file_upload(file: Upload, path: str) -> str:
|
||||
async def handle_file_upload(file: Upload, path: str, filename_maxlength: int = 64, uid_prefix: bool = True, overwrite: bool = True) -> str:
|
||||
check_directories(path)
|
||||
|
||||
filename = f"{uuid.uuid4()}-{file.filename}"
|
||||
prefix = f"{uuid.uuid4()}-" if uid_prefix else ""
|
||||
filename = f"{prefix}{file.filename}"[-1 * filename_maxlength:]
|
||||
|
||||
# sanitize filename
|
||||
filename = re.sub('[^\w_. -]', '', filename).replace(" ", "-")
|
||||
|
||||
target_path = f"{path}/{filename}"
|
||||
|
||||
if os.path.exists(target_path) and not overwrite:
|
||||
raise FileExistsError(f"File {filename} already exists")
|
||||
|
||||
content = await file.read()
|
||||
image = open(path + "/" + filename, "wb")
|
||||
image = open(target_path, "wb")
|
||||
image.write(content)
|
||||
image.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user