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.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")
+1
View File
@@ -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
View File
@@ -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)
+4
View File
@@ -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
+18 -5
View File
@@ -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()
+4 -1
View File
@@ -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
)
+16 -7
View File
@@ -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
+15 -5
View File
@@ -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
+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.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
+12 -15
View File
@@ -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
+5 -1
View File
@@ -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
+19 -8
View File
@@ -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
+2 -1
View File
@@ -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)
+18 -15
View File
@@ -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)
+107 -10
View File
@@ -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):
+44 -26
View File
@@ -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()
+6 -2
View File
@@ -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
View File
@@ -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:
+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
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
View File
@@ -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()