Slugy, webp, hromada zmen

This commit is contained in:
Michal Kváček
2024-01-13 23:29:24 +01:00
parent 3b1fd0351a
commit dabed327dd
26 changed files with 517 additions and 180 deletions
@@ -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 ###
+25 -3
View File
@@ -1,3 +1,5 @@
import os.path
from time import time
from database import models from database import models
from database.transaction import get_session from database.transaction import get_session
from utils.image import PhotoEditor 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): async def resize_photo(path: str, filename: str, photo_id: int):
editor = PhotoEditor(path, filename) editor = PhotoEditor(path, filename)
editor.resize(new_width=2500) 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 width, height = editor.img_size
# TODO: doresit uklid JPGu -> jsou zbytecne
async with get_session() as db: 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): async def generate_thumbnail(path: str, filename: str):
editor = PhotoEditor(path, filename) editor = PhotoEditor(path, filename)
editor.resize(new_width=300) 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")
+1
View File
@@ -13,6 +13,7 @@ async def download_weather(date_time: datetime, flight_id: int, airport_id: int,
try: try:
weather = await weather_api.get_weather_for_hour(date_time.astimezone(), gps=gps) weather = await weather_api.get_weather_for_hour(date_time.astimezone(), gps=gps)
log.warning(weather)
except Exception as e: except Exception as e:
log.error(f"Error in downloading weather: {e}") log.error(f"Error in downloading weather: {e}")
return None return None
+19 -13
View File
@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
import datetime import datetime
from typing import Set, List 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.orm import Mapped, relationship, as_declarative, mapped_column
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -74,6 +74,8 @@ class Airport(BaseModel):
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True) gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True) gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
elevation: Mapped[int] = mapped_column(Integer, 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') is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) 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 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) 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)
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) title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True) gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
gps_longitude: 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" __tablename__ = "photo"
id: Mapped[int] = mapped_column(primary_key=True) 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) filename: Mapped[str] = mapped_column(String(128), nullable=False)
is_flight_cover: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="0") # TODO: odstranit filename_extension: Mapped[str] = mapped_column(String(4), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False) cache_key: Mapped[str] = mapped_column(String(128), nullable=True)
width: Mapped[int] = mapped_column(Integer, nullable=False) width: Mapped[int] = mapped_column(Integer, nullable=False)
height: Mapped[int] = mapped_column(Integer, nullable=False) height: Mapped[int] = mapped_column(Integer, nullable=False)
exposed_at: Mapped[datetime] = mapped_column(DateTime, nullable=True) exposed_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
@@ -172,7 +176,6 @@ class Aircraft(BaseModel):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
call_sign: Mapped[str] = mapped_column(String(16), nullable=False) 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) title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0') is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
manufacturer: Mapped[str] = mapped_column(Text, nullable=False, server_default="") 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) 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)
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_from: Mapped[datetime] = mapped_column(DateTime, nullable=True)
date_to: 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) organization_id: Mapped[int] = mapped_column(Integer, ForeignKey('organization.id'), nullable=True)
@@ -270,13 +274,13 @@ class Flight(BaseModel):
__tablename__ = "flight" __tablename__ = "flight"
id: Mapped[int] = mapped_column(primary_key=True) 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) 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) 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) 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=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=True)
landing_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=False)
takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False) takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
landing_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) duration_total: Mapped[int] = mapped_column(Integer, nullable=True)
@@ -310,7 +314,9 @@ class Copilot(BaseModel):
id: Mapped[int] = mapped_column(primary_key=True) 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)
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) 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_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0') 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) id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(128), nullable=False, unique=True) email: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
name: Mapped[str] = mapped_column(String(128), nullable=False) 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) public_username: Mapped[str] = mapped_column(String(128), nullable=True, unique=True)
avatar_image_filename: Mapped[str] = mapped_column(String(128), nullable=True) avatar_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
title_image_filename: Mapped[str] = mapped_column(String(128), nullable=True) title_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
+4
View File
@@ -25,6 +25,8 @@ class QueryBuilder:
if only_public and hasattr(self.model, "is_public"): if only_public and hasattr(self.model, "is_public"):
query = query.filter(self.model.is_public.is_(True)) 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: elif hasattr(self.model, "created_by_id") and created_by_id:
query = query.filter(or_( query = query.filter(or_(
self.model.created_by_id.is_(None), self.model.created_by_id.is_(None),
@@ -33,5 +35,7 @@ class QueryBuilder:
if order_by: if order_by:
query = query.order_by(*order_by) query = query.order_by(*order_by)
elif hasattr(self.model, "name"):
query = query.order_by(self.model.name)
return query return query
+18 -5
View File
@@ -29,12 +29,25 @@ class AircraftQueries:
@strawberry.field() @strawberry.field()
@error_logging @error_logging
@allow_public @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( return await AircraftQueryResolver().get_one(
id,
user_id=info.context.user_id, user_id=info.context.user_id,
organization_ids=info.context.organization_ids, organization_ids=info.context.organization_ids,
public=public only_public=public,
**filter_params
) )
@@ -44,13 +57,13 @@ class AircraftMutation:
@error_logging @error_logging
@authenticated_user_only() @authenticated_user_only()
async def create_aircraft(root, info, input: CreateAircraftInput) -> Aircraft: 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 @strawberry.mutation
@error_logging @error_logging
@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:
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 @strawberry.mutation
@authenticated_user_only() @authenticated_user_only()
+4 -1
View File
@@ -19,4 +19,7 @@ class AirportQueries:
@error_logging @error_logging
@authenticated_user_only() @authenticated_user_only()
async def airport(root, info, id: int) -> Airport: 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
)
+16 -7
View File
@@ -4,6 +4,7 @@ from strawberry.types import Info
from database import models from database import models
from decorators.error_logging import error_logging from decorators.error_logging import error_logging
from decorators.endpoints import authenticated_user_only, allow_public 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.base import BaseMutationResolver
from graphql_schema.entities.resolvers.copilot import CopilotQueryResolver from graphql_schema.entities.resolvers.copilot import CopilotQueryResolver
from graphql_schema.entities.types.mutation_input import CreateCopilotInput, EditCopilotInput from graphql_schema.entities.types.mutation_input import CreateCopilotInput, EditCopilotInput
@@ -21,11 +22,22 @@ class CopilotQueries:
@strawberry.field() @strawberry.field()
@error_logging @error_logging
@allow_public @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( return await CopilotQueryResolver().get_one(
id,
user_id=info.context.user_id, user_id=info.context.user_id,
pilot_username=pilot_username only_public=public,
**filter_params
) )
@@ -35,10 +47,7 @@ class CopilotMutation:
@error_logging @error_logging
@authenticated_user_only() @authenticated_user_only()
async def create_copilot(root, info, input: CreateCopilotInput) -> Copilot: async def create_copilot(root, info, input: CreateCopilotInput) -> Copilot:
return await BaseMutationResolver(Copilot, models.Copilot).create( return await BaseMutationResolver(Copilot, models.Copilot).create(info.context, data=input)
data=input.to_dict(),
user_id=info.context.user_id
)
@strawberry.mutation @strawberry.mutation
@error_logging @error_logging
+15 -5
View File
@@ -3,6 +3,7 @@ import strawberry
from database import models from database import models
from decorators.endpoints import authenticated_user_only, allow_public from decorators.endpoints import authenticated_user_only, allow_public
from decorators.error_logging import error_logging 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.helpers.pagination import PaginationWindow, get_pagination_window
from graphql_schema.entities.resolvers.base import BaseMutationResolver from graphql_schema.entities.resolvers.base import BaseMutationResolver
from graphql_schema.entities.resolvers.event import EventQueryResolver from graphql_schema.entities.resolvers.event import EventQueryResolver
@@ -40,12 +41,21 @@ class EventQueries:
@strawberry.field() @strawberry.field()
@error_logging @error_logging
@allow_public @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( return await EventQueryResolver().get_one(
id,
username=username,
public=public, public=public,
user_id=info.context.user_id user_id=info.context.user_id ,
** filter_params
) )
@@ -55,7 +65,7 @@ class EventMutation:
@error_logging @error_logging
@authenticated_user_only() @authenticated_user_only()
async def create_event(root, info, input: CreateEventInput) -> Event: 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 @strawberry.mutation
@error_logging @error_logging
+15 -41
View File
@@ -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.mutation_input import EditFlightInput, CreateFlightInput
from graphql_schema.entities.types.types import Flight from graphql_schema.entities.types.types import Flight
from .helpers.combobox import handle_combobox_save from .helpers.combobox import handle_combobox_save
from .helpers.detail import get_detail_filters
from .helpers.pagination import PaginationWindow, get_pagination_window from .helpers.pagination import PaginationWindow, get_pagination_window
@@ -50,12 +51,21 @@ class FlightQueries:
@strawberry.field() @strawberry.field()
@error_logging @error_logging
@allow_public @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( return await FlightQueryResolver().get_one(
id,
user_id=info.context.user_id, user_id=info.context.user_id,
username=username, only_public=public,
public=public **filter_params
) )
@@ -65,43 +75,7 @@ class FlightMutation:
@error_logging @error_logging
@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() return await FlightMutationResolver().create(info.context, input)
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
@strawberry.mutation @strawberry.mutation
@error_logging @error_logging
@@ -15,10 +15,11 @@ async def handle_combobox_save(
if input.id: if input.id:
return input.id return input.id
else: else:
if not extra_data: if not extra_data:
extra_data = {} extra_data = {}
# TODO: vyhledat podle `name_column` existujici zaznam, pokud nebude, az pak vytvorit
data = {name_column: input.name, **extra_data} data = {name_column: input.name, **extra_data}
if hasattr(model, "created_by_id"): if hasattr(model, "created_by_id"):
data["created_by_id"] = user_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
+12 -15
View File
@@ -19,14 +19,18 @@ class OrganizationQueries:
@authenticated_user_only() @authenticated_user_only()
async def organizations(root, info) -> List[Organization]: async def organizations(root, info) -> List[Organization]:
return await BaseQueryResolver(Organization, models.Organization).get_list( 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() @strawberry.field()
@error_logging @error_logging
@authenticated_user_only() @authenticated_user_only()
async def organization(root, info, id: int) -> Organization: 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 @strawberry.type
@@ -36,24 +40,17 @@ class OrganizationMutation:
@error_logging @error_logging
@authenticated_user_only() @authenticated_user_only()
async def create_organization(root, info, input: CreateOrganizationInput) -> Organization: async def create_organization(root, info, input: CreateOrganizationInput) -> Organization:
return await BaseMutationResolver(Organization, models.Organization).create( return await BaseMutationResolver(Organization, models.Organization).create(info.context, data=input)
data=input.to_dict(),
user_id=info.context.user_id
)
@strawberry.mutation @strawberry.mutation
@error_logging @error_logging
@authenticated_user_only() @authenticated_user_only()
async def edit_organization(root, info, id: int, input: EditOrganizationInput) -> Organization: async def edit_organization(root, info, id: int, input: EditOrganizationInput) -> Organization:
async with get_session() as db: return await BaseMutationResolver(Organization, models.Organization).update(
organization = (await db.scalars( id,
BaseQueryResolver(Organization, models.Organization).get_query( data=input,
user_id=info.context.user_id, object_id=id user_id=info.context.user_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
+5 -1
View File
@@ -36,7 +36,11 @@ class PhotoQueries:
@error_logging @error_logging
@allow_public @allow_public
async def photo(root, info, id: int, public: Optional[bool] = False,) -> Photo: 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 @strawberry.type
+19 -8
View File
@@ -1,16 +1,18 @@
from typing import List from typing import List, Optional
import strawberry import strawberry
from graphql import GraphQLError
from database import models from database import models
from decorators.endpoints import authenticated_user_only, allow_public from decorators.endpoints import authenticated_user_only, allow_public
from database.transaction import get_session from database.transaction import get_session
from decorators.error_logging import error_logging from decorators.error_logging import error_logging
from graphql_schema.entities.helpers.combobox import handle_combobox_save 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.helpers.pagination import get_pagination_window, PaginationWindow
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver 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 graphql_schema.entities.types.mutation_input import CreatePointOfInterestInput, EditPointOfInterestInput from graphql_schema.entities.types.mutation_input import CreatePointOfInterestInput, EditPointOfInterestInput
@strawberry.type @strawberry.type
class PointOfInterestQueries: class PointOfInterestQueries:
@strawberry.field() @strawberry.field()
@@ -33,10 +35,18 @@ class PointOfInterestQueries:
@strawberry.field() @strawberry.field()
@allow_public @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( return await BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_one(
id, info.context.user_id, user_id=info.context.user_id,
only_public=public only_public=public,
**filter_params
) )
@@ -54,9 +64,10 @@ class PointOfInterestMutation:
db, models.PointOfInterestType, input.type, info.context.user_id db, models.PointOfInterestType, input.type, info.context.user_id
) )
return await BaseMutationResolver(PointOfInterest, models.PointOfInterest).create( input_data['created_by_id'] = info.context.user_id
input_data, info.context.user_id return await BaseMutationResolver(PointOfInterest, models.PointOfInterest)._do_create(
) db, input_data
)
@strawberry.mutation @strawberry.mutation
@error_logging @error_logging
+2 -1
View File
@@ -21,7 +21,8 @@ class PointOfInterestTypeQueries:
@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:
return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_one( 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, *args,
**kwargs, **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( query = super().get_query(
order_by=[models.Aircraft.id.desc()], 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: if organization_ids:
query = ( query = (
@@ -43,20 +50,22 @@ class AircraftMutationResolver(BaseMutationResolver):
def __init__(self): def __init__(self):
super().__init__(graphql_type=Aircraft, model=models.Aircraft) 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() 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( input_data['organization_id'] = await handle_combobox_save(
db, db,
models.Organization, models.Organization,
input=data.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() update_data = data.to_dict()
async with get_session() as db: async with get_session() as db:
if data.organization: if data.organization:
@@ -66,5 +75,5 @@ class AircraftMutationResolver(BaseMutationResolver):
input=data.organization, input=data.organization,
user_id=user_id, 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)
+18 -15
View File
@@ -33,7 +33,6 @@ class BaseQueryResolver(BaseResolver):
object_id: Optional[int] = None, object_id: Optional[int] = None,
order_by: Optional[list] = None, order_by: Optional[list] = None,
only_public: Optional[bool] = False, only_public: Optional[bool] = False,
*args,
**kwargs, **kwargs,
): ):
query = self.query_builder.get_simple_query(created_by_id=user_id, order_by=order_by, only_public=only_public) 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) query = self.get_query(user_id, **kwargs)
return await self._get_list(query) return await self._get_list(query)
async def get_one(self, id: int, user_id: Optional[int] = None, **kwargs) -> GQL_TYPE: async def get_one(self, user_id: Optional[int] = None, **kwargs) -> GQL_TYPE:
query = self.get_query(user_id, object_id=id, **kwargs) query = self.get_query(user_id, **kwargs)
return await self._get_one(query) 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) query = self.query_builder.get_simple_query(created_by_id=created_by_id).filter(self.model.id == id)
return (await db.scalars(query)).one() return (await db.scalars(query)).one()
async def _do_create(self, data) -> GQL_TYPE: async def _do_create(self, db: AsyncSession, data: dict) -> GQL_TYPE:
async with get_session() as db: model = await self.model.create(db, data=data)
model = await self.model.create(db, data=data) return self.graphql_type(**model.as_dict())
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 = {} update_where = {}
if isinstance(obj, models.BaseModel): if isinstance(obj, models.BaseModel):
update_where['obj'] = obj update_where['obj'] = obj
else: elif isinstance(obj, dict):
update_where['id'] = obj['id'] update_where['id'] = obj['id']
else:
update_where['id'] = obj
model = await self.model.update(db, data=data, **update_where) model = await self.model.update(db, data=data, **update_where)
return self.graphql_type(**model.as_dict()) 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 def update(self, id: int, data: BaseGraphqlInputType, user_id: int) -> GQL_TYPE:
async with get_session() as db: async with get_session() as db:
item = await self._get_one(db, id, user_id) item = await self._get_one(db, id, user_id)
@@ -93,9 +102,3 @@ class BaseMutationResolver(BaseResolver):
return self.graphql_type(**model.as_dict()) 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)
+107 -10
View File
@@ -1,6 +1,6 @@
import asyncio import asyncio
from typing import List, Optional 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 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
@@ -8,9 +8,10 @@ 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 database.transaction import get_session 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.helpers.combobox import handle_combobox_save
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver, GQL_TYPE
from graphql_schema.entities.types.mutation_input import EditFlightInput, TrackItemInput, ComboboxInput from graphql_schema.entities.types.mutation_input import EditFlightInput, TrackItemInput, ComboboxInput, CreateFlightInput
from graphql_schema.entities.types.types import Flight from graphql_schema.entities.types.types import Flight
from paths import FLIGHT_GPX_TRACK_PATH from paths import FLIGHT_GPX_TRACK_PATH
from utils.file import delete_file from utils.file import delete_file
@@ -25,12 +26,20 @@ class FlightQueryResolver(BaseQueryResolver):
self, self,
user_id: Optional[int] = None, user_id: Optional[int] = None,
object_id: Optional[int] = None, object_id: Optional[int] = None,
url_slug: Optional[str] = None,
only_public: Optional[bool] = False, only_public: Optional[bool] = False,
*args, *args,
**kwargs **kwargs
): ):
filters = {}
if object_id:
filters['object_id'] = object_id
if url_slug:
filters['url_slug'] = url_slug
query = super().get_query( query = super().get_query(
user_id, object_id, user_id,
**filters,
order_by=[models.Flight.takeoff_datetime.desc()], order_by=[models.Flight.takeoff_datetime.desc()],
only_public=only_public only_public=only_public
) )
@@ -66,6 +75,90 @@ class FlightMutationResolver(BaseMutationResolver):
def __init__(self): def __init__(self):
super().__init__(Flight, models.Flight) 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: async def update(self, context, id: int, input: EditFlightInput) -> Flight:
user_id = context.user_id user_id = context.user_id
async with get_session() as db: async with get_session() as db:
@@ -75,10 +168,11 @@ class FlightMutationResolver(BaseMutationResolver):
data = input.to_dict() data = input.to_dict()
if input.gpx_track is not None: if input.gpx_track_file is not None:
data['gpx_track_filename'] = await handle_upload_gpx(flight_data['gpx_track_filename'], input.gpx_track) data['gpx_track_filename'] = await handle_upload_gpx(
context.background_tasks.add_task( gpx_track=input.gpx_track_file,
add_terrain_elevation_to_flight, flight_id=id, gpx_filename=data['gpx_track_filename'] context=context,
original_gpx_filename=flight_data['gpx_track_filename']
) )
async with get_session() as db: async with get_session() as db:
@@ -136,11 +230,14 @@ class FlightMutationResolver(BaseMutationResolver):
return await self._do_update(db, flight_data, data) 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: if original_gpx_filename:
delete_file(FLIGHT_GPX_TRACK_PATH + "/" + original_gpx_filename, silent=True) 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): async def handle_track_edit(db: AsyncSession, flight_id: int, track: List[TrackItemInput], user_id: int):
+44 -26
View File
@@ -1,5 +1,6 @@
import os import os
import shutil import shutil
from time import time
from typing import Optional from typing import Optional
from PIL import Image from PIL import Image
from pydantic import BaseModel from pydantic import BaseModel
@@ -91,29 +92,35 @@ class PhotoMutationResolver(BaseMutationResolver):
async def upload(self, info, input: UploadPhotoInput) -> Photo: async def upload(self, info, input: UploadPhotoInput) -> Photo:
path = get_photo_basepath(input.flight_id) path = get_photo_basepath(input.flight_id)
filename = await handle_file_upload(input.photo, path) img_name = await handle_file_upload(input.photo, path, uid_prefix=False, overwrite=False)
exif_info = await parse_exif_info(path, filename)
img = Image.open(f"{path}/{filename}") exif_info = await parse_exif_info(path, img_name)
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,
},
)
info.context.background_tasks.add_task(resize_photo, path=path, filename=filename, photo_id=photo.id) img = Image.open(f"{path}/{img_name}")
info.context.background_tasks.add_task(generate_thumbnail, path=path, filename=filename) 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"): 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) 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: async with get_session() as db:
return await self._do_update(db, obj={"id": id}, data={ return await self._do_update(db, obj={"id": id}, data={
"width": editor.img.width, "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): 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={ return await self._do_update(db, obj={"id": id}, data={
"width": editor.img.width, "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: async def delete(self, user_id: int, id: int) -> Photo:
photo = await super().delete(user_id, id) photo = await super().delete(user_id, id)
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}/_original_{photo.filename}", silent=True) files_to_delete = [
delete_file(f"{base_path}/thumbs/{photo.filename}", silent=True) 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 return photo
@@ -102,7 +102,7 @@ class AdjustmentInput:
"takeoff_weather_info_id", "landing_weather_info_id", "gpx_track_filename", "event_id" "takeoff_weather_info_id", "landing_weather_info_id", "gpx_track_filename", "event_id"
], all_optional=True) ], all_optional=True)
class EditFlightInput(BaseGraphqlInputType): 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 track: Optional[List[TrackItemInput]] = None
copilots: Optional[List[ComboboxInput]] = None copilots: Optional[List[ComboboxInput]] = None
aircraft: Optional[ComboboxInput] = None aircraft: Optional[ComboboxInput] = None
@@ -114,10 +114,11 @@ class EditFlightInput(BaseGraphqlInputType):
@strawberry.input() @strawberry.input()
class CreateFlightInput(BaseGraphqlInputType): class CreateFlightInput(BaseGraphqlInputType):
aircraft: ComboboxInput aircraft: ComboboxInput
landing_airport: ComboboxInput gpx_track_file: Optional[Upload] = None # TODO: poresit validaci uploadovaneho souboru!
takeoff_airport: ComboboxInput landing_airport: Optional[ComboboxInput] = None
takeoff_datetime: datetime takeoff_airport: Optional[ComboboxInput] = None
landing_datetime: datetime takeoff_datetime: Optional[datetime] = None
landing_datetime: Optional[datetime] = None
@strawberry.input() @strawberry.input()
+6 -2
View File
@@ -128,8 +128,12 @@ class Flight:
copilots: Optional[List[Copilot]] = strawberry.field(resolver=load_copilots) copilots: Optional[List[Copilot]] = strawberry.field(resolver=load_copilots)
event: Optional[Event] = strawberry.field(resolver=load_event) event: Optional[Event] = strawberry.field(resolver=load_event)
aircraft: Aircraft = strawberry.field(resolver=lambda root: aircraft_dataloader.load(root.aircraft_id)) 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)) takeoff_airport: Optional[Airport] = strawberry.field(
landing_airport: Airport = strawberry.field(resolver=lambda root: airport_dataloader.load(root.landing_airport_id)) 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)) 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)) track: List[FlightTrack] = strawberry.field(resolver=lambda root: flight_track_dataloader.load(root.id))
takeoff_weather_info: Optional[WeatherInfo] = strawberry.field( takeoff_weather_info: Optional[WeatherInfo] = strawberry.field(
+18 -6
View File
@@ -1,6 +1,7 @@
import os import os
from typing import Optional from typing import Optional
from config import API_URL from config import API_URL
from logger import log
PHOTO_BASE_PATH = "" PHOTO_BASE_PATH = ""
AIRCRAFT_BASE_PATH = "" AIRCRAFT_BASE_PATH = ""
@@ -18,16 +19,27 @@ def get_public_url(filename: Optional[str]) -> str:
def get_photo_url(root) -> 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: def get_photo_thumbnail_url(root) -> str:
thumbnail = get_photo_basepath(root.flight_id) + "/thumbs/" + root.filename filename = root.filename if not root.filename_extension else f"{root.filename}.{root.filename_extension}"
if not os.path.isfile(thumbnail): thumbnail_names = [
# TODO: logovani f"thumbs/{root.filename}.webp",
return get_public_url(f"photos/{root.flight_id}/{root.filename}") 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: def get_avatar_url(user) -> str:
+50
View File
@@ -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
View File
@@ -127,11 +127,12 @@ class PhotoEditor:
return img_io return img_io
def write_to_file( 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: ) -> str:
check_directories(dest_path or self.path) check_directories(dest_path or self.path)
dest = f"{dest_path or self.path}/{dest_filename or self.filename}" 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 return dest
+14 -3
View File
@@ -1,16 +1,27 @@
import os
import re
import uuid import uuid
from strawberry.file_uploads import Upload from strawberry.file_uploads import Upload
from utils.file import check_directories 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) 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() content = await file.read()
image = open(path + "/" + filename, "wb") image = open(target_path, "wb")
image.write(content) image.write(content)
image.close() image.close()