Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e8f677b6f | ||
|
|
6553da41f2 | ||
|
|
420774f1b4 | ||
|
|
888351baac | ||
|
|
d794208193 | ||
|
|
71911c0905 | ||
|
|
a7fda95c29 | ||
|
|
2f824e264f | ||
|
|
beee16bf80 | ||
|
|
77c9242944 | ||
|
|
c6316262e5 | ||
|
|
d31f1c0c61 | ||
|
|
ca3f6ef07c | ||
|
|
186dbd0720 | ||
|
|
365a1fc1b0 | ||
|
|
0a6b19f334 | ||
|
|
3081580c21 |
@@ -2,5 +2,6 @@
|
|||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
exclude =
|
exclude =
|
||||||
venv
|
venv
|
||||||
|
.venv
|
||||||
__pycache__
|
__pycache__
|
||||||
alembic
|
alembic
|
||||||
+2
-2
@@ -7,7 +7,7 @@ COPY ./src/emails/templates/mjml /app/templates
|
|||||||
RUN mjml /app/templates/* -o /app/templates/html/
|
RUN mjml /app/templates/* -o /app/templates/html/
|
||||||
|
|
||||||
|
|
||||||
FROM python:3.11-alpine AS app
|
FROM python:3.13-alpine AS app
|
||||||
|
|
||||||
# protoze xfs uzivatel ma id=33
|
# protoze xfs uzivatel ma id=33
|
||||||
# RUN deluser xfs
|
# RUN deluser xfs
|
||||||
@@ -36,4 +36,4 @@ COPY . /app
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
CMD ["uvicorn", "src.asgi:app", "--reload", "--reload-dir=/app", "--port=8000", "--host=0.0.0.0", "--log-level=debug"]
|
CMD [ "gunicorn", "src.asgi:app", "--config=/app/config/gunicorn.py" ]
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""prejmenovani date_time u WeatherInfo
|
||||||
|
|
||||||
|
Revision ID: db036549f318
|
||||||
|
Revises: e3620deb41b6
|
||||||
|
Create Date: 2025-09-16 09:26:04.082396
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'db036549f318'
|
||||||
|
down_revision = 'e3620deb41b6'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_unique_constraint(None, 'airport', ['openaip_id'])
|
||||||
|
op.create_index(op.f('ix_flight_turn_point_order'), 'flight_turn_point', ['order'], unique=False)
|
||||||
|
op.alter_column(
|
||||||
|
"weather_info", "datetime", new_column_name="date_time", existing_type=sa.DateTime, nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.alter_column(
|
||||||
|
"weather_info", "date_time", new_column_name="datetime",
|
||||||
|
existing_type=sa.DateTime, nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
op.drop_index(op.f('ix_flight_turn_point_order'), table_name='flight_turn_point')
|
||||||
|
op.drop_constraint(None, 'airport', type_='unique')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
# https://docs.gunicorn.org/en/stable/settings.html#settings
|
||||||
|
|
||||||
|
workers = 1 if os.environ.get("APP_ENV") == "development" else os.cpu_count()
|
||||||
|
bind = "0.0.0.0:8000"
|
||||||
|
worker_class = "uvicorn.workers.UvicornWorker"
|
||||||
|
worker_connections = 1000 # default
|
||||||
|
threads = 1 # default
|
||||||
|
|
||||||
|
# restart workeru
|
||||||
|
max_requests = 500000
|
||||||
|
max_requests_jitter = 10000
|
||||||
|
|
||||||
|
timeout = 30 # default
|
||||||
|
graceful_timeout = 30 # default
|
||||||
|
|
||||||
|
keepalive = 600 # s klientem udrzuje nginx
|
||||||
@@ -3,7 +3,6 @@ version: "3.3"
|
|||||||
services:
|
services:
|
||||||
api:
|
api:
|
||||||
image: "docker.kvacek.cz/poletani/api:latest"
|
image: "docker.kvacek.cz/poletani/api:latest"
|
||||||
command: "uvicorn src.asgi:app --port=8000 --host=0.0.0.0 --workers 4"
|
|
||||||
network_mode: "host"
|
network_mode: "host"
|
||||||
restart: "always"
|
restart: "always"
|
||||||
env_file:
|
env_file:
|
||||||
|
|||||||
+17
-14
@@ -5,6 +5,9 @@ services:
|
|||||||
- 3306:3306
|
- 3306:3306
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
environment:
|
||||||
|
MARIADB_ROOT_PASSWORD: "rootroot"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
# - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
# - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
||||||
- db-data:/var/lib/mysql:rw
|
- db-data:/var/lib/mysql:rw
|
||||||
@@ -26,20 +29,20 @@ services:
|
|||||||
ALLOW_CORS_ORIGINS: "http://localhost:9000 http://localhost:9001 http://localhost:9101 http://localhost:3000"
|
ALLOW_CORS_ORIGINS: "http://localhost:9000 http://localhost:9001 http://localhost:9101 http://localhost:3000"
|
||||||
SENTRY_DSN: "https://184d1d6bd39f4fbb94804ae84e9afdd5@o472821.ingest.sentry.io/5506983"
|
SENTRY_DSN: "https://184d1d6bd39f4fbb94804ae84e9afdd5@o472821.ingest.sentry.io/5506983"
|
||||||
|
|
||||||
# adminer:
|
adminer:
|
||||||
# image: adminer
|
image: adminer
|
||||||
# restart: always
|
restart: always
|
||||||
# ports:
|
ports:
|
||||||
# - 8080:8080
|
- 8080:8080
|
||||||
#
|
|
||||||
# scrapy:
|
scrapy:
|
||||||
# build:
|
build:
|
||||||
# context: ../crawlers
|
context: ../crawlers
|
||||||
# env_file:
|
env_file:
|
||||||
# - .env
|
- .env
|
||||||
# volumes:
|
volumes:
|
||||||
# - db-data:/var/lib/mysql:rw
|
- db-data:/var/lib/mysql:rw
|
||||||
# - ../crawlers:/app
|
- ../crawlers:/app
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db-data: { }
|
db-data: { }
|
||||||
|
|||||||
+19
-14
@@ -1,17 +1,22 @@
|
|||||||
fastapi==0.110.0
|
fastapi==0.115.7
|
||||||
fastapi-jwt==0.2.0
|
fastapi-jwt[authlib]==0.3.0
|
||||||
fastapi-mail==1.4.1
|
bcrypt==4.2.1
|
||||||
strawberry-graphql[fastapi]==0.217.1
|
fastapi-mail==1.4.2
|
||||||
uvicorn==0.26.0
|
strawberry-graphql[fastapi]==0.258.0
|
||||||
sqlalchemy[asyncio] == 2.0.25
|
|
||||||
|
gunicorn == 23.0.0
|
||||||
|
uvicorn[standard] == 0.34.0
|
||||||
|
|
||||||
|
sqlalchemy[asyncio] == 2.0.54
|
||||||
aiomysql==0.2.0
|
aiomysql==0.2.0
|
||||||
alembic==1.13.1
|
alembic==1.14.1
|
||||||
passlib==1.7.4
|
passlib==1.7.4
|
||||||
pydantic==2.5.3
|
pydantic==2.10.6
|
||||||
sentry-sdk[fastapi]
|
sentry-sdk[fastapi]
|
||||||
pillow==10.2.0
|
pillow==11.1.0
|
||||||
exif==1.6.0
|
exif==1.6.1
|
||||||
aiocache==0.12.2
|
aiocache==0.12.3
|
||||||
aiohttp==3.9.3
|
aiohttp==3.11.11
|
||||||
lxml==5.1.0
|
lxml==5.3.0
|
||||||
geopy==2.4.1
|
|
||||||
|
colorlog
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ from external.elevation import elevation_api, update_track_points_elevation
|
|||||||
@retryable
|
@retryable
|
||||||
async def add_terrain_elevation_to_flight(flight_id: int):
|
async def add_terrain_elevation_to_flight(flight_id: int):
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
track_points = (
|
track_points = (await db.scalars(
|
||||||
await db.scalars(
|
select(models.TrackPoint)
|
||||||
select(models.TrackPoint)
|
.join(models.TrackPoint.track)
|
||||||
.join(models.TrackPoint.track)
|
.join(models.Track.flight)
|
||||||
.join(models.Track.flight)
|
.filter(models.Flight.id == flight_id)
|
||||||
.filter(models.Flight.id == flight_id))
|
.filter(models.TrackPoint.terrain_elevation.is_(None))
|
||||||
).all()
|
)).all()
|
||||||
await update_track_points_elevation(db, track_points)
|
await update_track_points_elevation(db, track_points)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
from datetime import datetime, timedelta
|
|
||||||
from sqlalchemy import select, delete
|
|
||||||
from database import models
|
|
||||||
from database.transaction import get_session
|
|
||||||
from utils.gps import generate_points
|
|
||||||
|
|
||||||
|
|
||||||
async def generate_track_for_flight_plan(flight_plan_id: int):
|
|
||||||
async with (get_session() as db):
|
|
||||||
flight_plan = await models.FlightPlan.get_one(db, id=flight_plan_id)
|
|
||||||
|
|
||||||
turn_points = (
|
|
||||||
await db.scalars(
|
|
||||||
select(models.FlightPlanMarker)
|
|
||||||
.filter(models.FlightPlanMarker.flight_plan_id == flight_plan_id)
|
|
||||||
)).all()
|
|
||||||
|
|
||||||
if len(turn_points) == 1:
|
|
||||||
return
|
|
||||||
|
|
||||||
if flight_plan.track_id:
|
|
||||||
track = await models.Track.get_one(db, id=flight_plan.track_id)
|
|
||||||
await db.execute(
|
|
||||||
delete(models.TrackPoint)
|
|
||||||
.filter(models.TrackPoint.track_id == track.id)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
track = await models.Track.create(db, {"bounds": [], "created_by_id": flight_plan.created_by_id})
|
|
||||||
flight_plan.track_id = track.id
|
|
||||||
|
|
||||||
point_ts = flight_plan.planned_takeoff_datetime or datetime.now()
|
|
||||||
track_points_resolution_meters = 500
|
|
||||||
|
|
||||||
speed_m_s = flight_plan.planned_speed / 3.6
|
|
||||||
delta_t_sec = track_points_resolution_meters / speed_m_s
|
|
||||||
|
|
||||||
for i in range(len(turn_points) - 1):
|
|
||||||
from_ = turn_points[i]
|
|
||||||
to = turn_points[i + 1]
|
|
||||||
|
|
||||||
point_from = (from_.gps_latitude, from_.gps_longitude)
|
|
||||||
point_to = (to.gps_latitude, to.gps_longitude)
|
|
||||||
points_between = generate_points(point_from, point_to)
|
|
||||||
|
|
||||||
for point in points_between:
|
|
||||||
await models.TrackPoint.create(db, {
|
|
||||||
"track_id": track.id,
|
|
||||||
"timestamp": point_ts,
|
|
||||||
"gps_latitude": point.latitude,
|
|
||||||
"gps_longitude": point.longitude,
|
|
||||||
})
|
|
||||||
|
|
||||||
point_ts += timedelta(seconds=delta_t_sec)
|
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
from PIL import Image, ImageDraw, ImageOps
|
from PIL import Image, ImageDraw, ImageOps
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from database import models
|
||||||
|
from database.transaction import get_session
|
||||||
|
|
||||||
|
|
||||||
async def add_circular_avatar(base_image_path, avatar_image_path, output_image_path):
|
async def add_circular_avatar(base_image_path, avatar_image_path, output_image_path):
|
||||||
@@ -56,3 +60,23 @@ async def add_circular_avatar(base_image_path, avatar_image_path, output_image_p
|
|||||||
|
|
||||||
# Save the result
|
# Save the result
|
||||||
base_image.save(output_image_path)
|
base_image.save(output_image_path)
|
||||||
|
|
||||||
|
|
||||||
|
async def generate_flight_title_photo(flight_id: int, photo_id: int):
|
||||||
|
async with get_session() as db:
|
||||||
|
photo = await models.Photo.get_one(db, id=photo_id)
|
||||||
|
pilot = (await db.scalars(
|
||||||
|
select(models.User)
|
||||||
|
.select_from(models.Flight)
|
||||||
|
.join(models.Flight.created_by)
|
||||||
|
.filter(models.Flight.id == flight_id)
|
||||||
|
)).one()
|
||||||
|
|
||||||
|
photo_filename, photo_ext = photo.filename, photo.filename_extension
|
||||||
|
pilot_id, avatar_filename = pilot.id, pilot.avatar_image_filename
|
||||||
|
|
||||||
|
await add_circular_avatar(
|
||||||
|
base_image_path=f'/app/uploads/photos/{flight_id}/{photo_filename}.{photo_ext}',
|
||||||
|
avatar_image_path=f'/app/uploads/profile/{pilot_id}/{avatar_filename}',
|
||||||
|
output_image_path=f'/app/uploads/photos/{flight_id}/title_photo.jpg',
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Literal
|
from typing import Literal, Any
|
||||||
from database import models
|
from database import models
|
||||||
from database.transaction import get_session
|
from database.transaction import get_session
|
||||||
from external.weather import weather_api
|
from external.weather import weather_api
|
||||||
from logger import log
|
from logger import log
|
||||||
|
|
||||||
|
|
||||||
async def get_weather(date_time: datetime, gps: tuple[float, float]):
|
async def get_weather(date_time: datetime | str, gps: tuple[float, float]) -> dict[str, Any] | None:
|
||||||
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)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -14,7 +14,7 @@ async def get_weather(date_time: datetime, gps: tuple[float, float]):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"datetime": weather['datetime'],
|
"date_time": date_time, # nemuzu pouzit weather['datetime'], protoze to mi vraci jen cele hodiny
|
||||||
"qnh": weather['pressure_msl'],
|
"qnh": weather['pressure_msl'],
|
||||||
"temperature_surface": weather['temperature_2m'],
|
"temperature_surface": weather['temperature_2m'],
|
||||||
"dewpoint_surface": weather['dewpoint_2m'],
|
"dewpoint_surface": weather['dewpoint_2m'],
|
||||||
@@ -44,18 +44,27 @@ async def download_weather_for_flight(date_time: datetime, flight_id: int, airpo
|
|||||||
await models.Flight.update(db, obj=flight, data={f"{type_}_weather_info_id": weather_model.id})
|
await models.Flight.update(db, obj=flight, data={f"{type_}_weather_info_id": weather_model.id})
|
||||||
|
|
||||||
|
|
||||||
async def download_weather_for_plan(date_time: datetime, flight_plan_id: int, gps: tuple[float, float]):
|
async def download_weather_for_plan_marker(date_time: datetime, marker_id: int):
|
||||||
|
async with get_session() as db:
|
||||||
|
marker = await models.FlightPlanMarker.get_one(db, id=marker_id)
|
||||||
|
|
||||||
|
if not marker.gps_latitude or not marker.gps_longitude:
|
||||||
|
return
|
||||||
|
|
||||||
|
gps = (marker.gps_latitude, marker.gps_longitude)
|
||||||
|
marker_weather_info_id = marker.weather_info_id
|
||||||
|
|
||||||
data = await get_weather(date_time, gps)
|
data = await get_weather(date_time, gps)
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
plan = await models.FlightPlan.get_one(db, id=flight_plan_id)
|
if marker_weather_info_id:
|
||||||
|
await models.WeatherInfo.update(db, id=marker_weather_info_id, data=data)
|
||||||
if plan.weather_info_id:
|
|
||||||
await models.WeatherInfo.update(db, id=plan.weather_info.id, data=data)
|
|
||||||
else:
|
else:
|
||||||
weather_model = await models.WeatherInfo.create(db, data=data)
|
weather_model = await models.WeatherInfo.create(db, data=data)
|
||||||
await models.FlightPlan.update(
|
await models.FlightPlanMarker.update(
|
||||||
db,
|
db,
|
||||||
obj=plan,
|
id=marker_id,
|
||||||
data={"weather_info_id": weather_model.id}
|
data={"weather_info_id": weather_model.id}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,481 +0,0 @@
|
|||||||
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, Enum
|
|
||||||
from sqlalchemy.dialects.mysql import JSON
|
|
||||||
from sqlalchemy.orm import Mapped, relationship, as_declarative, mapped_column
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
|
|
||||||
@as_declarative()
|
|
||||||
class BaseModel:
|
|
||||||
excluded_columns_in_dict = ("deleted",)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _get_column_names(cls):
|
|
||||||
return [col.name for col in cls.__table__.columns]
|
|
||||||
|
|
||||||
def as_dict(self):
|
|
||||||
return {c: getattr(self, c) for c in self._get_column_names() if c not in self.excluded_columns_in_dict}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def get_one(cls, db_session: AsyncSession, **kwargs):
|
|
||||||
return (await db_session.scalars(select(cls).filter_by(**kwargs))).one()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def create(cls, db_session: AsyncSession, data: dict):
|
|
||||||
model = cls(**{col: data[col] for col in cls._get_column_names() if col in data})
|
|
||||||
db_session.add(model)
|
|
||||||
await db_session.flush()
|
|
||||||
|
|
||||||
return model
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def update(cls, db_session: AsyncSession, data: dict, obj: BaseModel = None, id: int = None):
|
|
||||||
if not obj and not id:
|
|
||||||
raise ValueError("Provide either obj or id!")
|
|
||||||
|
|
||||||
if not obj:
|
|
||||||
obj = await cls.get_one(db_session, id=id)
|
|
||||||
for key, value in data.items():
|
|
||||||
if key in cls._get_column_names() and getattr(obj, key) != value:
|
|
||||||
setattr(obj, key, value)
|
|
||||||
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
user_is_in_organization = Table(
|
|
||||||
"user_is_in_organization",
|
|
||||||
BaseModel.metadata,
|
|
||||||
Column("user_id", Integer, ForeignKey("user.id"), primary_key=True),
|
|
||||||
Column("organization_id", Integer, ForeignKey("organization.id"), primary_key=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
flight_has_copilot = Table(
|
|
||||||
"flight_has_copilot",
|
|
||||||
BaseModel.metadata,
|
|
||||||
Column("flight_id", ForeignKey("flight.id"), primary_key=True),
|
|
||||||
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
|
|
||||||
Column("token", String(64), unique=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
flight_plan_has_copilot = Table(
|
|
||||||
"flight_plan_has_copilot",
|
|
||||||
BaseModel.metadata,
|
|
||||||
Column("flight_plan_id", ForeignKey("flight_plan.id"), primary_key=True),
|
|
||||||
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
|
|
||||||
Column("token", String(64), unique=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
copilot_has_photo = Table(
|
|
||||||
"copilot_has_photo",
|
|
||||||
BaseModel.metadata,
|
|
||||||
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
|
|
||||||
Column("photo_id", ForeignKey("photo.id"), primary_key=True),
|
|
||||||
)
|
|
||||||
|
|
||||||
airport_has_reporting_point = Table(
|
|
||||||
"airport_has_reporting_point",
|
|
||||||
BaseModel.metadata,
|
|
||||||
Column("airport_id", ForeignKey("airport.id"), primary_key=True),
|
|
||||||
Column("reporting_point_id", ForeignKey("reporting_point.id"), primary_key=True),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Airspace(BaseModel):
|
|
||||||
__tablename__ = "airspace"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
country: Mapped[str] = mapped_column(String(8), nullable=False, index=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
|
|
||||||
type: Mapped[str] = mapped_column(String(8), nullable=False, index=True)
|
|
||||||
lower_limit: Mapped[str] = mapped_column(String(32), nullable=False)
|
|
||||||
lower_alt_unit: Mapped[str] = mapped_column(String(8), nullable=False)
|
|
||||||
upper_limit: Mapped[str] = mapped_column(String(32), nullable=False)
|
|
||||||
upper_alt_unit: Mapped[str] = mapped_column(String(8), nullable=False)
|
|
||||||
bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON())
|
|
||||||
|
|
||||||
|
|
||||||
class FlightPlan(BaseModel):
|
|
||||||
__tablename__ = "flight_plan"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(256), nullable=False)
|
|
||||||
is_default_name: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
|
||||||
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
|
||||||
is_editable_by_copilot: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
|
||||||
track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'), nullable=True)
|
|
||||||
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
|
||||||
planned_speed: Mapped[int] = mapped_column(Integer, nullable=False, default=150)
|
|
||||||
planned_takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
|
||||||
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=True)
|
|
||||||
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')
|
|
||||||
|
|
||||||
copilots: Mapped[List['Copilot']] = relationship(secondary=flight_plan_has_copilot)
|
|
||||||
aircraft: Mapped['Aircraft'] = relationship()
|
|
||||||
markers: Mapped[List['FlightPlanMarker']] = relationship()
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
class Track(BaseModel):
|
|
||||||
__tablename__ = "track"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON())
|
|
||||||
min_speed: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
max_speed: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
avg_speed: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
max_altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
avg_altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
total_duration: Mapped[int] = mapped_column(Integer, nullable=True, comment="Total duration in seconds")
|
|
||||||
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
|
|
||||||
flight: Mapped['Flight'] = relationship()
|
|
||||||
flight_plan: Mapped['FlightPlan'] = relationship()
|
|
||||||
# created_by: Mapped['User'] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
class TrackPoint(BaseModel):
|
|
||||||
__tablename__ = "track_point"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
timestamp: Mapped[datetime] = mapped_column(DateTime, nullable=False, index=True)
|
|
||||||
track_id: Mapped[id] = mapped_column(Integer, ForeignKey('track.id'))
|
|
||||||
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
|
|
||||||
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
|
|
||||||
terrain_elevation: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
speed: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
magnetic_variation: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
|
|
||||||
track: Mapped['Track'] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
class FlightPlanMarker(BaseModel):
|
|
||||||
__tablename__ = "flight_plan_marker"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
flight_plan_id: Mapped[int] = mapped_column(Integer, ForeignKey('flight_plan.id'))
|
|
||||||
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
|
|
||||||
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
|
|
||||||
weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
||||||
type: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
||||||
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
||||||
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
|
|
||||||
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
|
|
||||||
|
|
||||||
|
|
||||||
class Airport(BaseModel):
|
|
||||||
__tablename__ = "airport"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
||||||
openaip_id: Mapped[str] = mapped_column(String(32), nullable=True, unique=True)
|
|
||||||
icao_code: Mapped[str] = mapped_column(String(8), nullable=False)
|
|
||||||
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')
|
|
||||||
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
|
|
||||||
|
|
||||||
runways: Mapped[list['Runway']] = relationship('Runway')
|
|
||||||
frequencies: Mapped[list['Frequency']] = relationship()
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
class Runway(BaseModel):
|
|
||||||
__tablename__ = "runway"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'))
|
|
||||||
heading: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
||||||
designator: Mapped[str] = mapped_column(String(4), nullable=False)
|
|
||||||
surface: Mapped[str] = mapped_column(Enum("asphalt", "concrete", "grass", "other"), nullable=False, server_default="other") # noqa
|
|
||||||
length: Mapped[int] = mapped_column(Integer, nullable=True)
|
|
||||||
width: Mapped[int] = mapped_column(Integer, nullable=True)
|
|
||||||
|
|
||||||
|
|
||||||
class Frequency(BaseModel):
|
|
||||||
__tablename__ = "frequency"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'), nullable=True)
|
|
||||||
airspace_id: Mapped[int] = mapped_column(Integer, ForeignKey('airspace.id'), nullable=True)
|
|
||||||
frequency: Mapped[str] = mapped_column(String(7), nullable=False)
|
|
||||||
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
||||||
|
|
||||||
|
|
||||||
class ReportingPoint(BaseModel):
|
|
||||||
__tablename__ = "reporting_point"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
openaip_id: Mapped[str] = mapped_column(String(32), nullable=False)
|
|
||||||
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
||||||
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
|
|
||||||
|
|
||||||
class PointOfInterestType(BaseModel):
|
|
||||||
__tablename__ = "point_of_interest_type"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
||||||
icon: Mapped[str] = mapped_column(String(128), nullable=False, server_default='marker')
|
|
||||||
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')
|
|
||||||
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
points_of_interest: Mapped[List[PointOfInterest]] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
class PointOfInterest(BaseModel):
|
|
||||||
__tablename__ = "point_of_interest"
|
|
||||||
|
|
||||||
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, 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)
|
|
||||||
type_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest_type.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')
|
|
||||||
|
|
||||||
type: Mapped[PointOfInterestType] = relationship()
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
|
|
||||||
|
|
||||||
|
|
||||||
class Photo(BaseModel):
|
|
||||||
__tablename__ = "photo"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
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_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)
|
|
||||||
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
gps_altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
terrain_elevation: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=True)
|
|
||||||
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
|
|
||||||
flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False)
|
|
||||||
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
|
||||||
created_by_copilot_id: Mapped[int] = mapped_column(Integer, ForeignKey('copilot.id'), nullable=True)
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
|
|
||||||
flight: Mapped['Flight'] = relationship(foreign_keys=[flight_id])
|
|
||||||
point_of_interest: Mapped['PointOfInterest'] = relationship(foreign_keys=[point_of_interest_id])
|
|
||||||
adjustment: Mapped['PhotoAdjustment'] = relationship(passive_deletes=True)
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
aircraft: Mapped['Aircraft'] = relationship(foreign_keys=[aircraft_id])
|
|
||||||
copilots: Mapped[List['Copilot']] = relationship(secondary=copilot_has_photo)
|
|
||||||
|
|
||||||
|
|
||||||
class PhotoAdjustment(BaseModel):
|
|
||||||
__tablename__ = "photo_adjustment"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id', ondelete='CASCADE'), nullable=False)
|
|
||||||
rotate: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
contrast: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
brightness: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
saturation: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
sharpness: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
|
|
||||||
crop_left: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
crop_top: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
crop_width: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
crop_height: Mapped[float] = mapped_column(Float, nullable=True)
|
|
||||||
|
|
||||||
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
photo: Mapped['Photo'] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
class Aircraft(BaseModel):
|
|
||||||
__tablename__ = "aircraft"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
call_sign: Mapped[str] = mapped_column(String(16), nullable=False)
|
|
||||||
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="")
|
|
||||||
model: Mapped[str] = mapped_column(String(30), nullable=False, server_default="")
|
|
||||||
seats: Mapped[str] = mapped_column(Integer, nullable=False)
|
|
||||||
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
|
||||||
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey('organization.id'), nullable=True)
|
|
||||||
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=False)
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
|
||||||
|
|
||||||
organization: Mapped['Organization'] = relationship()
|
|
||||||
flights: Mapped[Set['Flight']] = relationship()
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
class Organization(BaseModel):
|
|
||||||
__tablename__ = "organization"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
||||||
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')
|
|
||||||
|
|
||||||
users: Mapped[Set['User']] = relationship(secondary=user_is_in_organization)
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
class FlightTurnPoint(BaseModel):
|
|
||||||
__tablename__ = "flight_turn_point"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False)
|
|
||||||
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
|
|
||||||
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
|
|
||||||
landing_duration: Mapped[int] = mapped_column(Integer, nullable=True)
|
|
||||||
order: Mapped[int] = mapped_column(Integer, index=True)
|
|
||||||
|
|
||||||
flight: Mapped['Flight'] = relationship()
|
|
||||||
point_of_interest: Mapped['PointOfInterest'] = relationship()
|
|
||||||
airport: Mapped['Airport'] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
class WeatherInfo(BaseModel):
|
|
||||||
__tablename__ = "weather_info"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
qnh: Mapped[int] = mapped_column(Integer, nullable=True)
|
|
||||||
temperature_surface: Mapped[Float] = mapped_column(Float, nullable=True)
|
|
||||||
dewpoint_surface: Mapped[Float] = mapped_column(Float, nullable=True)
|
|
||||||
rain: Mapped[Float] = mapped_column(Float, nullable=True)
|
|
||||||
cloudcover_low: Mapped[Float] = mapped_column(Float, nullable=True)
|
|
||||||
cloudcover_total: Mapped[Float] = mapped_column(Float, nullable=True)
|
|
||||||
wind_speed_surface: Mapped[Float] = mapped_column(Float, nullable=True)
|
|
||||||
wind_direction_surface: Mapped[Float] = mapped_column(Float, nullable=True)
|
|
||||||
datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
|
||||||
|
|
||||||
|
|
||||||
class Event(BaseModel):
|
|
||||||
__tablename__ = "event"
|
|
||||||
|
|
||||||
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, 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)
|
|
||||||
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')
|
|
||||||
|
|
||||||
organization: Mapped['Organization'] = relationship()
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
class Flight(BaseModel):
|
|
||||||
__tablename__ = "flight"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
|
||||||
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
|
||||||
debriefing_notes: 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=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)
|
|
||||||
duration_pic: Mapped[int] = mapped_column(Integer, nullable=True)
|
|
||||||
flight_plan_id: Mapped[int] = mapped_column(Integer, ForeignKey('flight_plan.id'), nullable=True)
|
|
||||||
track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'), nullable=True)
|
|
||||||
gpx_track_filename: Mapped[str] = mapped_column(String(128), nullable=True) # deprecated
|
|
||||||
has_terrain_elevation: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) # deprecated
|
|
||||||
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey('aircraft.id'))
|
|
||||||
takeoff_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
|
|
||||||
landing_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
|
|
||||||
landings: Mapped[int] = mapped_column(Integer, default=1)
|
|
||||||
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')
|
|
||||||
|
|
||||||
takeoff_airport: Mapped['Airport'] = relationship(foreign_keys=[takeoff_airport_id])
|
|
||||||
landing_airport: Mapped['Airport'] = relationship(foreign_keys=[landing_airport_id])
|
|
||||||
weather_info_landing: Mapped[WeatherInfo] = relationship(foreign_keys=[landing_weather_info_id])
|
|
||||||
weather_info_takeoff: Mapped[WeatherInfo] = relationship(foreign_keys=[takeoff_weather_info_id])
|
|
||||||
turn_points: Mapped[list['FlightTurnPoint']] = relationship()
|
|
||||||
event: Mapped['Event'] = relationship()
|
|
||||||
copilots: Mapped[List['Copilot']] = relationship(secondary=flight_has_copilot)
|
|
||||||
aircraft: Mapped['Aircraft'] = relationship()
|
|
||||||
photos: Mapped[List['Photo']] = relationship(foreign_keys=[Photo.flight_id])
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
|
|
||||||
track: Mapped['Track'] = relationship()
|
|
||||||
|
|
||||||
class Copilot(BaseModel):
|
|
||||||
__tablename__ = "copilot"
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
flights: Mapped[Set['Flight']] = relationship(secondary=flight_has_copilot)
|
|
||||||
flight_plans: Mapped[Set['FlightPlan']] = relationship(secondary=flight_plan_has_copilot)
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
photos: Mapped[List['Photo']] = relationship(secondary=copilot_has_photo)
|
|
||||||
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
|
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
|
||||||
__tablename__ = "user"
|
|
||||||
excluded_columns_in_dict = ('password_hashed',)
|
|
||||||
|
|
||||||
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, 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)
|
|
||||||
password_hashed: Mapped[str] = mapped_column(String(60), nullable=False)
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
|
|
||||||
flights: Mapped[Set['Flight']] = relationship()
|
|
||||||
organizations: Mapped[Set['Organization']] = relationship(secondary=user_is_in_organization)
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordResetToken(BaseModel):
|
|
||||||
__tablename__ = "password_reset_token"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
|
||||||
token: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
||||||
valid_until: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
|
||||||
|
|
||||||
user: Mapped['User'] = relationship()
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
from database.models.airport import Airport, Runway, ReportingPoint, airport_has_reporting_point, Frequency
|
||||||
|
from database.models.aircraft import Aircraft
|
||||||
|
from database.models.airspace import Airspace
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
from database.models.copilot import Copilot, copilot_has_photo
|
||||||
|
from database.models.event import Event
|
||||||
|
from database.models.flight import Flight, FlightTurnPoint, Track, TrackPoint, flight_has_copilot
|
||||||
|
from database.models.flight_plan import FlightPlan, flight_plan_has_copilot, FlightPlanMarker
|
||||||
|
from database.models.orgnization import Organization
|
||||||
|
from database.models.password_reset_token import PasswordResetToken
|
||||||
|
from database.models.photo import PhotoAdjustment, Photo
|
||||||
|
from database.models.point_of_interest import PointOfInterest, PointOfInterestType
|
||||||
|
from database.models.user import User, user_is_in_organization
|
||||||
|
from database.models.weather_info import WeatherInfo
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Aircraft",
|
||||||
|
"Airport",
|
||||||
|
"Runway",
|
||||||
|
"ReportingPoint",
|
||||||
|
"Airspace",
|
||||||
|
"BaseModel",
|
||||||
|
"Copilot",
|
||||||
|
"Event",
|
||||||
|
"Flight",
|
||||||
|
"FlightTurnPoint",
|
||||||
|
"FlightPlan",
|
||||||
|
"FlightPlanMarker",
|
||||||
|
"Frequency",
|
||||||
|
"Track",
|
||||||
|
"TrackPoint",
|
||||||
|
"Organization",
|
||||||
|
"PasswordResetToken",
|
||||||
|
"Photo",
|
||||||
|
"PhotoAdjustment",
|
||||||
|
"PointOfInterest",
|
||||||
|
"PointOfInterestType",
|
||||||
|
"WeatherInfo",
|
||||||
|
"User",
|
||||||
|
"airport_has_reporting_point",
|
||||||
|
"flight_has_copilot",
|
||||||
|
"flight_plan_has_copilot",
|
||||||
|
"user_is_in_organization",
|
||||||
|
"copilot_has_photo",
|
||||||
|
]
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Aircraft(BaseModel):
|
||||||
|
__tablename__ = "aircraft"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
call_sign: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||||
|
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="")
|
||||||
|
model: Mapped[str] = mapped_column(String(30), nullable=False, server_default="")
|
||||||
|
seats: Mapped[str] = mapped_column(Integer, nullable=False)
|
||||||
|
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey('organization.id'), nullable=True)
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=False)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||||
|
|
||||||
|
organization: Mapped['Organization'] = relationship()
|
||||||
|
flights: Mapped[set['Flight']] = relationship()
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float, Enum
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
airport_has_reporting_point = Table(
|
||||||
|
"airport_has_reporting_point",
|
||||||
|
BaseModel.metadata,
|
||||||
|
Column("airport_id", ForeignKey("airport.id"), primary_key=True),
|
||||||
|
Column("reporting_point_id", ForeignKey("reporting_point.id"), primary_key=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Airport(BaseModel):
|
||||||
|
__tablename__ = "airport"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
openaip_id: Mapped[str] = mapped_column(String(32), nullable=True, unique=True)
|
||||||
|
icao_code: Mapped[str] = mapped_column(String(8), nullable=False)
|
||||||
|
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'
|
||||||
|
)
|
||||||
|
use_in_gpx_guess: Mapped[bool] = mapped_column(Boolean, server_default='1')
|
||||||
|
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
|
||||||
|
|
||||||
|
runways: Mapped[list['Runway']] = relationship('Runway')
|
||||||
|
frequencies: Mapped[list['Frequency']] = relationship()
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
class Runway(BaseModel):
|
||||||
|
__tablename__ = "runway"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'))
|
||||||
|
heading: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
designator: Mapped[str] = mapped_column(String(4), nullable=False)
|
||||||
|
surface: Mapped[str] = mapped_column(Enum("asphalt", "concrete", "grass", "other"), nullable=False, server_default="other") # noqa
|
||||||
|
length: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||||
|
width: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Frequency(BaseModel):
|
||||||
|
__tablename__ = "frequency"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'), nullable=True)
|
||||||
|
airspace_id: Mapped[int] = mapped_column(Integer, ForeignKey('airspace.id'), nullable=True)
|
||||||
|
frequency: Mapped[str] = mapped_column(String(7), nullable=False)
|
||||||
|
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class ReportingPoint(BaseModel):
|
||||||
|
__tablename__ = "reporting_point"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
openaip_id: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||||
|
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from sqlalchemy import String, JSON
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Airspace(BaseModel):
|
||||||
|
__tablename__ = "airspace"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
country: Mapped[str] = mapped_column(String(8), nullable=False, index=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
|
||||||
|
type: Mapped[str] = mapped_column(String(8), nullable=False, index=True)
|
||||||
|
lower_limit: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||||
|
lower_alt_unit: Mapped[str] = mapped_column(String(8), nullable=False)
|
||||||
|
upper_limit: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||||
|
upper_alt_unit: Mapped[str] = mapped_column(String(8), nullable=False)
|
||||||
|
bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON())
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
|
||||||
|
|
||||||
|
class BaseModel(DeclarativeBase):
|
||||||
|
excluded_columns_in_dict = ("deleted",)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_column_names(cls):
|
||||||
|
return [col.name for col in cls.__table__.columns]
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
return {c: getattr(self, c) for c in self._get_column_names() if c not in self.excluded_columns_in_dict}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_one(cls, db_session: AsyncSession, **kwargs):
|
||||||
|
return (await db_session.scalars(select(cls).filter_by(**kwargs))).one()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create(cls, db_session: AsyncSession, data: dict):
|
||||||
|
model = cls(**{col: data[col] for col in cls._get_column_names() if col in data})
|
||||||
|
db_session.add(model)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
return model
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def update(cls, db_session: AsyncSession, data: dict, obj: BaseModel = None, id: int = None):
|
||||||
|
if not obj and not id:
|
||||||
|
raise ValueError("Provide either obj or id!")
|
||||||
|
|
||||||
|
if not obj:
|
||||||
|
obj = await cls.get_one(db_session, id=id)
|
||||||
|
for key, value in data.items():
|
||||||
|
if key in cls._get_column_names() and getattr(obj, key) != value:
|
||||||
|
setattr(obj, key, value)
|
||||||
|
|
||||||
|
return obj
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, DateTime, func
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
from database.models.flight import flight_has_copilot
|
||||||
|
from database.models.flight_plan import flight_plan_has_copilot
|
||||||
|
|
||||||
|
copilot_has_photo = Table(
|
||||||
|
"copilot_has_photo",
|
||||||
|
BaseModel.metadata,
|
||||||
|
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
|
||||||
|
Column("photo_id", ForeignKey("photo.id"), primary_key=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Copilot(BaseModel):
|
||||||
|
__tablename__ = "copilot"
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
flights: Mapped[set['Flight']] = relationship(secondary=flight_has_copilot)
|
||||||
|
flight_plans: Mapped[set['FlightPlan']] = relationship(secondary=flight_plan_has_copilot)
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
photos: Mapped[list['Photo']] = relationship(secondary=copilot_has_photo)
|
||||||
|
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Event(BaseModel):
|
||||||
|
__tablename__ = "event"
|
||||||
|
|
||||||
|
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, 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)
|
||||||
|
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')
|
||||||
|
|
||||||
|
organization: Mapped['Organization'] = relationship()
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float, JSON
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
flight_has_copilot = Table(
|
||||||
|
"flight_has_copilot",
|
||||||
|
BaseModel.metadata,
|
||||||
|
Column("flight_id", ForeignKey("flight.id"), primary_key=True),
|
||||||
|
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
|
||||||
|
Column("token", String(64), unique=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FlightTurnPoint(BaseModel):
|
||||||
|
__tablename__ = "flight_turn_point"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False)
|
||||||
|
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
|
||||||
|
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
|
||||||
|
landing_duration: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||||
|
order: Mapped[int] = mapped_column(Integer, index=True)
|
||||||
|
|
||||||
|
flight: Mapped['Flight'] = relationship()
|
||||||
|
point_of_interest: Mapped['PointOfInterest'] = relationship()
|
||||||
|
airport: Mapped['Airport'] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
class Flight(BaseModel):
|
||||||
|
__tablename__ = "flight"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
||||||
|
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
debriefing_notes: 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=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)
|
||||||
|
duration_pic: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||||
|
flight_plan_id: Mapped[int] = mapped_column(Integer, ForeignKey('flight_plan.id'), nullable=True)
|
||||||
|
track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'), nullable=True)
|
||||||
|
gpx_track_filename: Mapped[str] = mapped_column(String(128), nullable=True) # deprecated
|
||||||
|
has_terrain_elevation: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) # deprecated
|
||||||
|
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey('aircraft.id'))
|
||||||
|
takeoff_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
|
||||||
|
landing_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
|
||||||
|
landings: Mapped[int] = mapped_column(Integer, default=1)
|
||||||
|
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')
|
||||||
|
|
||||||
|
takeoff_airport: Mapped['Airport'] = relationship(foreign_keys=[takeoff_airport_id])
|
||||||
|
landing_airport: Mapped['Airport'] = relationship(foreign_keys=[landing_airport_id])
|
||||||
|
weather_info_landing: Mapped['WeatherInfo'] = relationship(foreign_keys=[landing_weather_info_id])
|
||||||
|
weather_info_takeoff: Mapped['WeatherInfo'] = relationship(foreign_keys=[takeoff_weather_info_id])
|
||||||
|
turn_points: Mapped[list['FlightTurnPoint']] = relationship()
|
||||||
|
event: Mapped['Event'] = relationship()
|
||||||
|
copilots: Mapped[list['Copilot']] = relationship(secondary=flight_has_copilot)
|
||||||
|
aircraft: Mapped['Aircraft'] = relationship()
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
|
||||||
|
track: Mapped['Track'] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
class Track(BaseModel):
|
||||||
|
__tablename__ = "track"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON())
|
||||||
|
min_speed: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
max_speed: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
avg_speed: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
max_altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
avg_altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
total_duration: Mapped[int] = mapped_column(Integer, nullable=True, comment="Total duration in seconds")
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
|
flight: Mapped['Flight'] = relationship()
|
||||||
|
track_points: Mapped[list['TrackPoint']] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
class TrackPoint(BaseModel):
|
||||||
|
__tablename__ = "track_point"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
timestamp: Mapped[datetime] = mapped_column(DateTime, nullable=False, index=True)
|
||||||
|
track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'))
|
||||||
|
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
|
||||||
|
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
|
||||||
|
terrain_elevation: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
speed: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
magnetic_variation: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
|
||||||
|
track: Mapped['Track'] = relationship()
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
flight_plan_has_copilot = Table(
|
||||||
|
"flight_plan_has_copilot",
|
||||||
|
BaseModel.metadata,
|
||||||
|
Column("flight_plan_id", ForeignKey("flight_plan.id"), primary_key=True),
|
||||||
|
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
|
||||||
|
Column("token", String(64), unique=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
class FlightPlan(BaseModel):
|
||||||
|
__tablename__ = "flight_plan"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(256), nullable=False)
|
||||||
|
is_default_name: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
is_editable_by_copilot: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'), nullable=True)
|
||||||
|
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
planned_speed: Mapped[int] = mapped_column(Integer, nullable=False, default=150)
|
||||||
|
planned_takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||||
|
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=True)
|
||||||
|
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')
|
||||||
|
|
||||||
|
copilots: Mapped[list['Copilot']] = relationship(secondary=flight_plan_has_copilot)
|
||||||
|
aircraft: Mapped['Aircraft'] = relationship()
|
||||||
|
markers: Mapped[list['FlightPlanMarker']] = relationship()
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
class FlightPlanMarker(BaseModel):
|
||||||
|
__tablename__ = "flight_plan_marker"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
flight_plan_id: Mapped[int] = mapped_column(Integer, ForeignKey('flight_plan.id'))
|
||||||
|
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
|
||||||
|
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
|
||||||
|
weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
type: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||||
|
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
|
||||||
|
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
from database.models.user import user_is_in_organization
|
||||||
|
|
||||||
|
|
||||||
|
class Organization(BaseModel):
|
||||||
|
__tablename__ = "organization"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
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')
|
||||||
|
|
||||||
|
users: Mapped[set['User']] = relationship(secondary=user_is_in_organization)
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, ForeignKey, Integer, DateTime
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetToken(BaseModel):
|
||||||
|
__tablename__ = "password_reset_token"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
token: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
valid_until: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||||
|
|
||||||
|
user: Mapped['User'] = relationship()
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, ForeignKey, Integer, Text, DateTime, func, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Photo(BaseModel):
|
||||||
|
__tablename__ = "photo"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
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_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)
|
||||||
|
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
gps_altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
terrain_elevation: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=True)
|
||||||
|
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
|
||||||
|
flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False)
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
created_by_copilot_id: Mapped[int] = mapped_column(Integer, ForeignKey('copilot.id'), nullable=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
|
flight: Mapped['Flight'] = relationship(foreign_keys=[flight_id])
|
||||||
|
point_of_interest: Mapped['PointOfInterest'] = relationship(foreign_keys=[point_of_interest_id])
|
||||||
|
adjustment: Mapped['PhotoAdjustment'] = relationship(passive_deletes=True)
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
aircraft: Mapped['Aircraft'] = relationship(foreign_keys=[aircraft_id])
|
||||||
|
# copilots: Mapped[list['Copilot']] = relationship(secondary=copilot_has_photo)
|
||||||
|
|
||||||
|
|
||||||
|
class PhotoAdjustment(BaseModel):
|
||||||
|
__tablename__ = "photo_adjustment"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id', ondelete='CASCADE'), nullable=False)
|
||||||
|
rotate: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
contrast: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
brightness: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
saturation: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
sharpness: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
|
||||||
|
crop_left: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
crop_top: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
crop_width: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
crop_height: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
photo: Mapped['Photo'] = relationship()
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class PointOfInterestType(BaseModel):
|
||||||
|
__tablename__ = "point_of_interest_type"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
icon: Mapped[str] = mapped_column(String(128), nullable=False, server_default='marker')
|
||||||
|
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')
|
||||||
|
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
points_of_interest: Mapped[list['PointOfInterest']] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
class PointOfInterest(BaseModel):
|
||||||
|
__tablename__ = "point_of_interest"
|
||||||
|
|
||||||
|
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, 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)
|
||||||
|
type_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest_type.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')
|
||||||
|
|
||||||
|
type: Mapped[PointOfInterestType] = relationship()
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import datetime
|
||||||
|
from typing import Set
|
||||||
|
from sqlalchemy import String, DateTime, ForeignKey, Text, Integer, func, Table, Column
|
||||||
|
from sqlalchemy.orm import Mapped, relationship, mapped_column
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
user_is_in_organization = Table(
|
||||||
|
"user_is_in_organization",
|
||||||
|
BaseModel.metadata,
|
||||||
|
Column("user_id", Integer, ForeignKey("user.id"), primary_key=True),
|
||||||
|
Column("organization_id", Integer, ForeignKey("organization.id"), primary_key=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class User(BaseModel):
|
||||||
|
__tablename__ = "user"
|
||||||
|
excluded_columns_in_dict = ('password_hashed',)
|
||||||
|
|
||||||
|
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, 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)
|
||||||
|
password_hashed: Mapped[str] = mapped_column(String(60), nullable=False)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
|
flights: Mapped[Set['Flight']] = relationship()
|
||||||
|
organizations: Mapped[Set['Organization']] = relationship(secondary=user_is_in_organization)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import Integer, DateTime, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class WeatherInfo(BaseModel):
|
||||||
|
__tablename__ = "weather_info"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
qnh: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||||
|
temperature_surface: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
dewpoint_surface: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
rain: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
cloudcover_low: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
cloudcover_total: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
wind_speed_surface: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
wind_direction_surface: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
date_time: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
from typing import Optional, Type
|
from typing import Type
|
||||||
from sqlalchemy import select, or_, and_
|
from sqlalchemy import select, or_, and_
|
||||||
from database import models
|
from database import models
|
||||||
|
|
||||||
|
|
||||||
class QueryBuilder:
|
class QueryBuilder[ModelType: models.BaseModel]:
|
||||||
def __init__(self, model: Type[models.BaseModel]):
|
def __init__(self, model: Type[ModelType]):
|
||||||
self.model = model
|
self.model = model
|
||||||
|
|
||||||
def get_simple_query(
|
def get_simple_query(
|
||||||
self,
|
self,
|
||||||
extra_select: Optional[list] = None,
|
extra_select: list | None = None,
|
||||||
created_by_id: Optional[int] = None,
|
created_by_id: int | None = None,
|
||||||
order_by: Optional[list] = None,
|
order_by: list | None = None,
|
||||||
only_public: Optional[bool] = False,
|
only_public: bool | None = False,
|
||||||
only_my: Optional[bool] = False,
|
only_my: bool | None = False,
|
||||||
include_others_public: Optional[bool] = False,
|
include_others_public: bool | None = False,
|
||||||
url_slug: Optional[str] = None,
|
url_slug: str | None = None,
|
||||||
include_deleted: bool = False
|
include_deleted: bool = False
|
||||||
):
|
):
|
||||||
if not extra_select:
|
if not extra_select:
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
from functools import wraps
|
|
||||||
from fastapi import HTTPException
|
|
||||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
|
||||||
|
|
||||||
|
|
||||||
def raise_unauthorized():
|
|
||||||
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
|
|
||||||
|
|
||||||
|
|
||||||
def allow_public(func):
|
|
||||||
@wraps(func)
|
|
||||||
async def decorator(*args, **kwargs):
|
|
||||||
if 'info' in kwargs:
|
|
||||||
user_id = kwargs['info'].context.user_id
|
|
||||||
public = kwargs.get('public')
|
|
||||||
if not user_id and not public:
|
|
||||||
raise_unauthorized()
|
|
||||||
|
|
||||||
return await func(*args, **kwargs)
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
def authenticated_user_only(raise_when_unauthorized: bool = True, return_value_unauthorized=None):
|
|
||||||
def wrapper(func):
|
|
||||||
@wraps(func)
|
|
||||||
async def decorator(*args, **kwargs):
|
|
||||||
if 'info' in kwargs:
|
|
||||||
if not kwargs['info'].context.user_id:
|
|
||||||
if raise_when_unauthorized:
|
|
||||||
raise_unauthorized()
|
|
||||||
else:
|
|
||||||
return return_value_unauthorized
|
|
||||||
return await func(*args, **kwargs)
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
from functools import wraps
|
|
||||||
from fastapi import HTTPException
|
|
||||||
from graphql import GraphQLError
|
|
||||||
from sqlalchemy.exc import NoResultFound
|
|
||||||
|
|
||||||
|
|
||||||
def error_logging(func):
|
|
||||||
@wraps(func)
|
|
||||||
async def decorator(*args, **kwargs):
|
|
||||||
try:
|
|
||||||
return await func(*args, **kwargs)
|
|
||||||
except NoResultFound as e:
|
|
||||||
raise GraphQLError("Not found", original_error=e)
|
|
||||||
except HTTPException as e:
|
|
||||||
if e.status_code == 401:
|
|
||||||
raise GraphQLError("Not authorized", original_error=e)
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
@@ -9,10 +9,10 @@
|
|||||||
</mj-text>
|
</mj-text>
|
||||||
|
|
||||||
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left">
|
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left">
|
||||||
Od: [from]
|
Od: {{ email_from | e }}
|
||||||
</mj-text>
|
</mj-text>
|
||||||
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5">
|
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5">
|
||||||
[content]
|
{{ content | e }}
|
||||||
</mj-text>
|
</mj-text>
|
||||||
<mj-divider border-color="#469ccc"></mj-divider>
|
<mj-divider border-color="#469ccc"></mj-divider>
|
||||||
</mj-column>
|
</mj-column>
|
||||||
|
|||||||
@@ -13,13 +13,12 @@
|
|||||||
<p>
|
<p>
|
||||||
Ahoj,
|
Ahoj,
|
||||||
<br>
|
<br>
|
||||||
Každý občas zapomíná. Heslo je to nejmenší. Klikni na následující tlačítko pro přihlášení do aplikace. A tam si budeš moct změnit heslo.
|
Každý občas zapomíná. Heslo je to nejmenší. Klikni na následující tlačítko pro reset hesla.
|
||||||
<br>
|
<br>
|
||||||
<strong>Jen admin vyzaduje zadani stareho hesla, takze to nebude tak snadne...</strong>
|
|
||||||
</p>
|
</p>
|
||||||
</mj-text>
|
</mj-text>
|
||||||
<mj-button background-color="#469ccc" color="white" font-size="18px" align="center" href="">
|
<mj-button background-color="#469ccc" color="white" font-size="18px" align="center" href="https://poletani.cz/zapomenute-heslo/[login_token]">
|
||||||
Přihlásit se pomocí magického odkazu [login_token]
|
Obnovit heslo
|
||||||
</mj-button>
|
</mj-button>
|
||||||
<mj-divider border-color="#469ccc"></mj-divider>
|
<mj-divider border-color="#469ccc"></mj-divider>
|
||||||
</mj-column>
|
</mj-column>
|
||||||
|
|||||||
@@ -8,34 +8,34 @@
|
|||||||
Vítejte v aplikaci Polétání.cz!
|
Vítejte v aplikaci Polétání.cz!
|
||||||
</mj-text>
|
</mj-text>
|
||||||
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5">
|
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5">
|
||||||
Ahoj [Jméno],
|
Ahoj!
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Vítejte v aplikaci Polétání.cz – místě, kde můžete sdílet své letecké dobrodružství a fotografie s ostatními nadšenci.
|
Vítej v aplikaci Polétání.cz – místě, kde můžete sdílet své letecké dobrodružství a fotografie s ostatními nadšenci.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Co můžete očekávat:
|
Co můžeš očekávat:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>Jednoduché nahrávání a sdílení fotografií z vašich letů.</li>
|
<li>Jednoduché nahrávání a sdílení fotografií z vašich letů.</li>
|
||||||
<li>Možnost připojení k letištním komunitám a sdílení tipů a triků.</li>
|
<li>Naplánovat nový výlet pomocí vestavěného plánovače</li>
|
||||||
<li>Upozornění na nové funkce a akce v aplikaci.</li>
|
<li>Objevovat nová místa pro další výlety</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Začněte nyní a sdílejte své vzdušné zážitky s celým světem!
|
Začni nyní a sdílejte své vzdušné zážitky s celým světem!
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
S pozdravem,
|
S pozdravem,
|
||||||
<br>
|
<br>
|
||||||
Tým Polétání.cz
|
Michal Kváček
|
||||||
</p>
|
</p>
|
||||||
</mj-text>
|
</mj-text>
|
||||||
<mj-button background-color="#469ccc" color="white" font-size="16px" align="center" href="[Odkaz na přihlášení]">
|
<mj-button background-color="#469ccc" color="white" font-size="16px" align="center" href="https://poletani.cz/login">
|
||||||
Přihlásit se do Polétání.cz
|
Přihlásit se do Polétání.cz
|
||||||
</mj-button>
|
</mj-button>
|
||||||
<mj-divider border-color="#469ccc"></mj-divider>
|
<mj-divider border-color="#469ccc"></mj-divider>
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
from datetime import timedelta
|
|
||||||
from fastapi import FastAPI, Security, Depends, BackgroundTasks, APIRouter
|
|
||||||
from fastapi_jwt import JwtAuthorizationCredentials
|
|
||||||
from fastapi_jwt.jwt import JwtAccessBearerCookie
|
|
||||||
from sqlalchemy import select
|
|
||||||
from starlette.responses import RedirectResponse
|
|
||||||
from strawberry.fastapi import GraphQLRouter
|
|
||||||
from config import GRAPHIQL, APP_DEBUG
|
|
||||||
from database import async_session, models
|
|
||||||
from graphql_schema.schema import GraphQLContext, schema
|
|
||||||
|
|
||||||
|
|
||||||
def setup_graphql_endpoint(app: FastAPI, access_security: JwtAccessBearerCookie):
|
|
||||||
if APP_DEBUG:
|
|
||||||
debug_router = APIRouter()
|
|
||||||
|
|
||||||
@debug_router.get("/graphql/autologin")
|
|
||||||
async def autologin():
|
|
||||||
access_token = access_security.create_access_token(subject={"id": 6, "name": "Franta Vomacka"})
|
|
||||||
response = RedirectResponse(url="/graphql")
|
|
||||||
access_security.set_access_cookie(response, access_token, expires_delta=timedelta(days=14))
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
app.include_router(debug_router)
|
|
||||||
|
|
||||||
async def setup_graphql_context(credentials: JwtAuthorizationCredentials = Security(access_security)):
|
|
||||||
user_id = credentials['id'] if credentials else None
|
|
||||||
organization_ids = set()
|
|
||||||
|
|
||||||
if user_id:
|
|
||||||
async with async_session() as db:
|
|
||||||
organization_ids = set((await db.scalars(
|
|
||||||
select(models.user_is_in_organization.c.organization_id)
|
|
||||||
.filter(models.user_is_in_organization.c.user_id == user_id)
|
|
||||||
)).all())
|
|
||||||
|
|
||||||
return GraphQLContext(
|
|
||||||
user_id=user_id,
|
|
||||||
organization_ids=organization_ids,
|
|
||||||
jwt_auth_credentials=credentials,
|
|
||||||
jwt=access_security,
|
|
||||||
background_tasks=Depends(BackgroundTasks)
|
|
||||||
)
|
|
||||||
|
|
||||||
graphql_app = GraphQLRouter(
|
|
||||||
schema,
|
|
||||||
graphiql=GRAPHIQL,
|
|
||||||
debug=APP_DEBUG,
|
|
||||||
context_getter=setup_graphql_context
|
|
||||||
)
|
|
||||||
app.include_router(graphql_app, prefix="/graphql")
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
from fastapi import HTTPException, BackgroundTasks
|
from fastapi import HTTPException, BackgroundTasks
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from typing import Optional
|
|
||||||
from pydantic import BaseModel, Field, model_validator
|
from pydantic import BaseModel, Field, model_validator
|
||||||
from background_jobs.send_email import send_email
|
from background_jobs.send_email import send_email
|
||||||
from database.models import User
|
from database.models import User
|
||||||
@@ -12,7 +11,7 @@ from emails.emails import WelcomeEmail
|
|||||||
|
|
||||||
class RegistrationInput(BaseModel):
|
class RegistrationInput(BaseModel):
|
||||||
email: str = Field(..., min_length=4)
|
email: str = Field(..., min_length=4)
|
||||||
name: Optional[str]
|
name: str | None
|
||||||
password: str
|
password: str
|
||||||
|
|
||||||
@model_validator(mode="before")
|
@model_validator(mode="before")
|
||||||
|
|||||||
Vendored
+1
-1
@@ -9,10 +9,10 @@ from utils.list_utils import chunks
|
|||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class ElevationInfo:
|
class ElevationInfo:
|
||||||
|
id: int | None
|
||||||
lat: float
|
lat: float
|
||||||
lng: float
|
lng: float
|
||||||
elevation: float
|
elevation: float
|
||||||
id: Optional[int] = None
|
|
||||||
|
|
||||||
|
|
||||||
class ElevationAPI:
|
class ElevationAPI:
|
||||||
|
|||||||
Vendored
+1
-1
@@ -53,7 +53,7 @@ class Airspace:
|
|||||||
upper_limit: str = None
|
upper_limit: str = None
|
||||||
lower_limit: str = None
|
lower_limit: str = None
|
||||||
center: Optional[Coordinates] = None
|
center: Optional[Coordinates] = None
|
||||||
radius_nm: Optional[float] = None
|
radius_nm: float | None = None
|
||||||
bounds: list[Coordinates] = dataclasses.field(default_factory=lambda: [])
|
bounds: list[Coordinates] = dataclasses.field(default_factory=lambda: [])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Vendored
+8
-5
@@ -43,10 +43,13 @@ class Weather:
|
|||||||
async def download_weather_for_day(self, date: datetime.date, gps: Tuple[float, float]):
|
async def download_weather_for_day(self, date: datetime.date, gps: Tuple[float, float]):
|
||||||
url = self.get_weather_info_url(start_date=date, end_date=date, gps=gps)
|
url = self.get_weather_info_url(start_date=date, end_date=date, gps=gps)
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
try:
|
||||||
async with session.get(url) as resp:
|
async with aiohttp.ClientSession() as session:
|
||||||
resp.raise_for_status()
|
async with session.get(url) as resp:
|
||||||
return await resp.json()
|
resp.raise_for_status()
|
||||||
|
return await resp.json()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in downloading weather: {e}, {type(e)}")
|
||||||
|
|
||||||
async def get_weather_for_hour(
|
async def get_weather_for_hour(
|
||||||
self, date_time: datetime.datetime, gps: Tuple[float, float]
|
self, date_time: datetime.datetime, gps: Tuple[float, float]
|
||||||
@@ -56,7 +59,7 @@ class Weather:
|
|||||||
# TODO: kontrola timezone!
|
# TODO: kontrola timezone!
|
||||||
idx = date_time.hour
|
idx = date_time.hour
|
||||||
result_data = {metric: data['hourly'][metric][idx] for metric in self.METRICS}
|
result_data = {metric: data['hourly'][metric][idx] for metric in self.METRICS}
|
||||||
result_data['datetime'] = datetime.datetime.strptime(data['hourly']['time'][idx], "%Y-%m-%dT%H:%M")
|
result_data['date_time'] = datetime.datetime.strptime(data['hourly']['time'][idx], "%Y-%m-%dT%H:%M")
|
||||||
|
|
||||||
return result_data
|
return result_data
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import dataclasses
|
||||||
|
from datetime import datetime
|
||||||
|
from fastapi import BackgroundTasks, Depends, Security
|
||||||
|
from fastapi_jwt import JwtAuthorizationCredentials
|
||||||
|
from sqlalchemy import select
|
||||||
|
from strawberry.fastapi import BaseContext
|
||||||
|
from database import async_session, models
|
||||||
|
from jwt import access_security
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class GraphQLContext(BaseContext):
|
||||||
|
request_start_time: datetime
|
||||||
|
user_id: int
|
||||||
|
organization_ids: set[int]
|
||||||
|
jwt_auth_credentials: JwtAuthorizationCredentials
|
||||||
|
background_tasks: BackgroundTasks
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_graphql_context(credentials: JwtAuthorizationCredentials = Security(access_security)):
|
||||||
|
user_id = credentials['id'] if credentials else None
|
||||||
|
organization_ids = set()
|
||||||
|
|
||||||
|
if user_id:
|
||||||
|
async with async_session() as db:
|
||||||
|
organization_ids = set((await db.scalars(
|
||||||
|
select(models.user_is_in_organization.c.organization_id)
|
||||||
|
.filter(models.user_is_in_organization.c.user_id == user_id)
|
||||||
|
)).all())
|
||||||
|
|
||||||
|
return GraphQLContext(
|
||||||
|
user_id=user_id,
|
||||||
|
organization_ids=organization_ids,
|
||||||
|
jwt_auth_credentials=credentials,
|
||||||
|
background_tasks=Depends(BackgroundTasks),
|
||||||
|
request_start_time=datetime.now()
|
||||||
|
)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Type, List, Optional
|
from typing import Type
|
||||||
from logger import log
|
from logger import log
|
||||||
from database import models, async_session
|
from database import models, async_session
|
||||||
from database.query_builder import QueryBuilder
|
from database.query_builder import QueryBuilder
|
||||||
@@ -9,7 +9,7 @@ class BaseDataloader:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
model: Type[models.BaseModel],
|
model: Type[models.BaseModel],
|
||||||
relationship_column, filters: Optional[list] = None
|
relationship_column, filters: list | None = None
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.model = model
|
self.model = model
|
||||||
@@ -25,11 +25,15 @@ class BaseDataloader:
|
|||||||
|
|
||||||
|
|
||||||
class SingleModelByIdDataloader(BaseDataloader):
|
class SingleModelByIdDataloader(BaseDataloader):
|
||||||
async def load(self, ids: List[int]):
|
async def load(self, ids: list[int]):
|
||||||
|
ids_set = {id_ for id_ in set(ids) if id_ is not None}
|
||||||
|
if not ids_set:
|
||||||
|
return [None for _ in ids]
|
||||||
|
|
||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
query = (
|
query = (
|
||||||
self.query_builder.get_simple_query(extra_select=[self.relationship_column], include_deleted=True)
|
self.query_builder.get_simple_query(extra_select=[self.relationship_column], include_deleted=True)
|
||||||
.filter(self.relationship_column.in_(set(ids)))
|
.filter(self.relationship_column.in_(ids_set))
|
||||||
.filter(*self.filters)
|
.filter(*self.filters)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -43,10 +47,10 @@ class MultiModelsDataloader(BaseDataloader):
|
|||||||
self,
|
self,
|
||||||
model: Type[models.BaseModel],
|
model: Type[models.BaseModel],
|
||||||
relationship_column=None,
|
relationship_column=None,
|
||||||
filters: Optional[list] = None,
|
filters: list | None = None,
|
||||||
extra_select: Optional[list] = None,
|
extra_select: list | None = None,
|
||||||
extra_join: Optional[list] = None,
|
extra_join: list | None = None,
|
||||||
order_by: Optional[list] = None,
|
order_by: list | None = None,
|
||||||
):
|
):
|
||||||
super().__init__(model, relationship_column, filters)
|
super().__init__(model, relationship_column, filters)
|
||||||
|
|
||||||
@@ -62,13 +66,13 @@ class MultiModelsDataloader(BaseDataloader):
|
|||||||
order_by = [model.id.desc()] # defaultne radit od nejnovejsich zaznamu
|
order_by = [model.id.desc()] # defaultne radit od nejnovejsich zaznamu
|
||||||
self.order_by = order_by
|
self.order_by = order_by
|
||||||
|
|
||||||
def get_query(self, ids: list[int]):
|
def get_query(self, ids: set[int]):
|
||||||
query = (
|
query = (
|
||||||
self.query_builder.get_simple_query(
|
self.query_builder.get_simple_query(
|
||||||
extra_select=[self.relationship_column] + self.extra_select,
|
extra_select=[self.relationship_column] + self.extra_select,
|
||||||
order_by=self.order_by
|
order_by=self.order_by
|
||||||
)
|
)
|
||||||
.filter(self.relationship_column.in_(set(ids)))
|
.filter(self.relationship_column.in_(ids))
|
||||||
.filter(*self.filters)
|
.filter(*self.filters)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -80,8 +84,12 @@ class MultiModelsDataloader(BaseDataloader):
|
|||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
async def load(self, ids: List[int]):
|
async def load(self, ids: list[int]) -> list:
|
||||||
query = self.get_query(ids)
|
ids_set = {id_ for id_ in set(ids) if id_ is not None}
|
||||||
|
if not ids_set:
|
||||||
|
return [[] for _ in ids]
|
||||||
|
|
||||||
|
query = self.get_query(ids_set)
|
||||||
|
|
||||||
async with async_session() as db:
|
async with async_session() as db:
|
||||||
data = (await db.execute(query)).all()
|
data = (await db.execute(query)).all()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from database import models
|
|||||||
from graphql_schema.dataloaders.base import SingleModelByIdDataloader
|
from graphql_schema.dataloaders.base import SingleModelByIdDataloader
|
||||||
|
|
||||||
|
|
||||||
def create_dataloader(model: Type[models.BaseModel], relationship_column=None, filters: Optional[list] = None):
|
def create_dataloader(model: Type[models.BaseModel], relationship_column=None, filters: list | None = None):
|
||||||
loader = SingleModelByIdDataloader(model, relationship_column, filters).load
|
loader = SingleModelByIdDataloader(model, relationship_column, filters).load
|
||||||
return DataLoader(load_fn=loader, cache=False)
|
return DataLoader(load_fn=loader, cache=False)
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ airport_dataloader = create_dataloader(models.Airport)
|
|||||||
aircraft_dataloader = create_dataloader(models.Aircraft)
|
aircraft_dataloader = create_dataloader(models.Aircraft)
|
||||||
event_dataloader = create_dataloader(models.Event)
|
event_dataloader = create_dataloader(models.Event)
|
||||||
organizations_dataloader = create_dataloader(models.Organization)
|
organizations_dataloader = create_dataloader(models.Organization)
|
||||||
airport_weather_info_loader = create_dataloader(models.WeatherInfo)
|
weather_info_loader = create_dataloader(models.WeatherInfo)
|
||||||
track_dataloader = create_dataloader(models.Track)
|
track_dataloader = create_dataloader(models.Track)
|
||||||
poi_dataloader = create_dataloader(models.PointOfInterest)
|
poi_dataloader = create_dataloader(models.PointOfInterest)
|
||||||
poi_type_dataloader = create_dataloader(models.PointOfInterestType)
|
poi_type_dataloader = create_dataloader(models.PointOfInterestType)
|
||||||
|
|||||||
@@ -1,70 +1,49 @@
|
|||||||
from typing import Optional
|
|
||||||
import strawberry
|
import strawberry
|
||||||
from decorators.endpoints import authenticated_user_only, allow_public
|
from strawberry import Info
|
||||||
from decorators.error_logging import error_logging
|
from .helpers.filters import get_filters
|
||||||
from .helpers.pagination import get_pagination_window, PaginationWindow
|
from .helpers.pagination import PaginationWindow
|
||||||
from .resolvers.aircraft import AircraftMutationResolver, AircraftQueryResolver
|
from .resolvers.aircraft import AircraftMutationResolver, AircraftQueryResolver
|
||||||
from graphql_schema.entities.types.mutation_input import CreateAircraftInput, EditAircraftInput
|
from graphql_schema.entities.types.mutation_input import CreateAircraftInput, EditAircraftInput
|
||||||
from graphql_schema.entities.types.types import Aircraft
|
from graphql_schema.entities.types.types import Aircraft
|
||||||
|
from ..extensions.field.auth import AllowPublicAccess, AuthenticatedOnly
|
||||||
|
from ..extensions.field.pagination import OffsetPagination
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class AircraftQueries:
|
class AircraftQueries:
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[OffsetPagination(item_type=Aircraft), AuthenticatedOnly()])
|
||||||
@error_logging
|
async def aircrafts(root, info: Info, search: str | None = None) -> PaginationWindow[Aircraft]:
|
||||||
@authenticated_user_only()
|
return AircraftQueryResolver().get_query(
|
||||||
async def aircrafts(root, info, limit: int, offset: int = 0) -> PaginationWindow[Aircraft]:
|
|
||||||
query = AircraftQueryResolver().get_query(
|
|
||||||
info.context.user_id,
|
info.context.user_id,
|
||||||
organization_ids=info.context.organization_ids
|
organization_ids=info.context.organization_ids,
|
||||||
|
search=search
|
||||||
)
|
)
|
||||||
|
|
||||||
return await get_pagination_window(
|
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||||
query=query,
|
|
||||||
item_type=Aircraft,
|
|
||||||
limit=limit,
|
|
||||||
offset=offset
|
|
||||||
)
|
|
||||||
|
|
||||||
@strawberry.field()
|
|
||||||
@error_logging
|
|
||||||
@allow_public
|
|
||||||
async def aircraft(
|
async def aircraft(
|
||||||
root, info,
|
root, info,
|
||||||
id: Optional[int] = None,
|
id: int | None = None,
|
||||||
call_sign: Optional[str] = None,
|
call_sign: str | None = None,
|
||||||
public: Optional[bool] = False
|
public: bool | None = False
|
||||||
) -> Aircraft:
|
) -> 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(
|
||||||
user_id=info.context.user_id,
|
user_id=info.context.user_id,
|
||||||
organization_ids=info.context.organization_ids if not public else None,
|
organization_ids=info.context.organization_ids if not public else None,
|
||||||
only_public=public,
|
only_public=public,
|
||||||
**filter_params
|
**get_filters(object_id=id, call_sign=call_sign)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class AircraftMutation:
|
class AircraftMutation:
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@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(info.context, input)
|
return await AircraftMutationResolver().create(info.context, input)
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@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().update(id, user_id=info.context.user_id, data=input)
|
return await AircraftMutationResolver().update(info.context, id=id, data=input)
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@authenticated_user_only()
|
|
||||||
async def delete_aircraft(self, info, id: int) -> Aircraft:
|
async def delete_aircraft(self, info, id: int) -> Aircraft:
|
||||||
return await AircraftMutationResolver().delete(info.context.user_id, id)
|
return await AircraftMutationResolver().delete(info.context, id)
|
||||||
|
|||||||
@@ -1,23 +1,17 @@
|
|||||||
from typing import List
|
|
||||||
import strawberry
|
import strawberry
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.error_logging import error_logging
|
|
||||||
from decorators.endpoints import authenticated_user_only
|
|
||||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
||||||
from graphql_schema.entities.types.types import Airport
|
from graphql_schema.entities.types.types import Airport
|
||||||
|
from graphql_schema.extensions.field.auth import AuthenticatedOnly
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class AirportQueries:
|
class AirportQueries:
|
||||||
@strawberry.field()
|
@strawberry.field()
|
||||||
@error_logging
|
async def airports(root, info) -> list[Airport]:
|
||||||
# @authenticated_user_only()
|
|
||||||
async def airports(root, info) -> List[Airport]:
|
|
||||||
return await BaseQueryResolver(Airport, models.Airport).get_list(info.context.user_id)
|
return await BaseQueryResolver(Airport, models.Airport).get_list(info.context.user_id)
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@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(
|
return await BaseQueryResolver(Airport, models.Airport).get_one(
|
||||||
object_id=id,
|
object_id=id,
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
from typing import List, Optional
|
|
||||||
import strawberry
|
import strawberry
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.error_logging import error_logging
|
|
||||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
||||||
from graphql_schema.entities.types.types import Airspace
|
from graphql_schema.entities.types.types import Airspace
|
||||||
|
|
||||||
@@ -9,10 +7,9 @@ from graphql_schema.entities.types.types import Airspace
|
|||||||
@strawberry.type
|
@strawberry.type
|
||||||
class AirspaceQueries:
|
class AirspaceQueries:
|
||||||
@strawberry.field()
|
@strawberry.field()
|
||||||
@error_logging
|
|
||||||
async def airspaces(
|
async def airspaces(
|
||||||
root, info, country: Optional[str] = None, types: Optional[list[str]] = None
|
root, info, country: str | None = None, types: list[str] | None = None
|
||||||
) -> List[Airspace]:
|
) -> list[Airspace]:
|
||||||
|
|
||||||
filters = []
|
filters = []
|
||||||
if country:
|
if country:
|
||||||
|
|||||||
@@ -1,48 +1,35 @@
|
|||||||
from typing import List, Optional
|
|
||||||
import strawberry
|
import strawberry
|
||||||
from graphql import GraphQLError
|
from graphql import GraphQLError
|
||||||
from strawberry.types import Info
|
from strawberry.types import Info
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.error_logging import error_logging
|
from graphql_schema.entities.helpers.filters import get_filters
|
||||||
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
|
||||||
from graphql_schema.entities.types.types import Copilot
|
from graphql_schema.entities.types.types import Copilot
|
||||||
|
from graphql_schema.extensions.field.auth import AllowPublicAccess, AuthenticatedOnly
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class CopilotQueries:
|
class CopilotQueries:
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
async def copilots(root, info: Info) -> list[Copilot]:
|
||||||
@authenticated_user_only()
|
|
||||||
async def copilots(root, info: Info) -> List[Copilot]:
|
|
||||||
return await CopilotQueryResolver().get_list(info.context.user_id)
|
return await CopilotQueryResolver().get_list(info.context.user_id)
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||||
@error_logging
|
|
||||||
@allow_public
|
|
||||||
async def copilot(
|
async def copilot(
|
||||||
root, info: Info,
|
root, info: Info,
|
||||||
id: Optional[int] = None,
|
id: int | None = None,
|
||||||
url_slug: Optional[str] = None,
|
url_slug: str | None = None,
|
||||||
pilot_username: Optional[str] = None,
|
pilot_username: str | None = None,
|
||||||
upload_token: Optional[str] = None,
|
upload_token: str | None = None,
|
||||||
upload_flight_slug: Optional[str] = None,
|
upload_flight_slug: str | None = None,
|
||||||
public: Optional[bool] = False
|
public: bool | None = False
|
||||||
) -> Copilot:
|
) -> Copilot:
|
||||||
filter_params = {}
|
filter_params = get_filters(
|
||||||
if id:
|
object_id=id, url_slug=url_slug, pilot_username=pilot_username, upload_token=upload_token,
|
||||||
filter_params['object_id'] = id
|
upload_flight_slug=upload_flight_slug,
|
||||||
if url_slug is not None:
|
)
|
||||||
filter_params['url_slug'] = url_slug
|
|
||||||
if upload_token and upload_flight_slug:
|
|
||||||
filter_params['upload_token'] = upload_token
|
|
||||||
filter_params['upload_flight_slug'] = upload_flight_slug
|
|
||||||
if pilot_username:
|
|
||||||
filter_params['pilot_username'] = pilot_username
|
|
||||||
|
|
||||||
if not filter_params:
|
if not filter_params:
|
||||||
raise GraphQLError(f"Invalid identification supplied: {filter_params}")
|
raise GraphQLError(f"Invalid identification supplied: {filter_params}")
|
||||||
|
|
||||||
@@ -55,14 +42,10 @@ class CopilotQueries:
|
|||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class CopilotMutation:
|
class CopilotMutation:
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
async def create_copilot(root, info: Info, input: CreateCopilotInput) -> Copilot:
|
||||||
@authenticated_user_only()
|
|
||||||
async def create_copilot(root, info, input: CreateCopilotInput) -> Copilot:
|
|
||||||
return await BaseMutationResolver(Copilot, models.Copilot).create(info.context, data=input)
|
return await BaseMutationResolver(Copilot, models.Copilot).create(info.context, data=input)
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
async def edit_copilot(root, info: Info, id: int, input: EditCopilotInput) -> Copilot:
|
||||||
@authenticated_user_only()
|
return await BaseMutationResolver(Copilot, models.Copilot).update(info.context, id, input)
|
||||||
async def edit_copilot(root, info, id: int, input: EditCopilotInput) -> Copilot:
|
|
||||||
return await BaseMutationResolver(Copilot, models.Copilot).update(id, input, info.context.user_id)
|
|
||||||
|
|||||||
@@ -1,52 +1,39 @@
|
|||||||
from typing import Optional
|
|
||||||
import strawberry
|
import strawberry
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.endpoints import authenticated_user_only, allow_public
|
from graphql_schema.entities.helpers.filters import get_detail_filters
|
||||||
from decorators.error_logging import error_logging
|
from graphql_schema.entities.helpers.pagination import PaginationWindow
|
||||||
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.base import BaseMutationResolver
|
||||||
from graphql_schema.entities.resolvers.event import EventQueryResolver
|
from graphql_schema.entities.resolvers.event import EventQueryResolver
|
||||||
from graphql_schema.entities.types.mutation_input import CreateEventInput, EditEventInput
|
from graphql_schema.entities.types.mutation_input import CreateEventInput, EditEventInput
|
||||||
from graphql_schema.entities.types.types import Event
|
from graphql_schema.entities.types.types import Event
|
||||||
|
from graphql_schema.extensions.field.auth import AllowPublicAccess, AuthenticatedOnly
|
||||||
|
from graphql_schema.extensions.field.pagination import OffsetPagination
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class EventQueries:
|
class EventQueries:
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[OffsetPagination(item_type=Event), AllowPublicAccess()])
|
||||||
@error_logging
|
|
||||||
@allow_public
|
|
||||||
async def events(
|
async def events(
|
||||||
root,
|
root,
|
||||||
info,
|
info,
|
||||||
limit: int,
|
username: str | None = None,
|
||||||
offset: int = 0,
|
public: bool | None = False,
|
||||||
username: Optional[str] = None,
|
|
||||||
public: Optional[bool] = False,
|
|
||||||
) -> PaginationWindow[Event]:
|
) -> PaginationWindow[Event]:
|
||||||
query = EventQueryResolver().get_query(
|
return EventQueryResolver().get_query(
|
||||||
user_id=info.context.user_id,
|
user_id=info.context.user_id,
|
||||||
username=username,
|
username=username,
|
||||||
order_by=[models.Event.date_from.desc(), models.Event.name.desc()],
|
order_by=[models.Event.date_from.desc(), models.Event.name.desc()],
|
||||||
only_public=public,
|
only_public=public,
|
||||||
)
|
)
|
||||||
|
|
||||||
return await get_pagination_window(
|
|
||||||
query=query,
|
|
||||||
item_type=Event,
|
|
||||||
limit=limit,
|
|
||||||
offset=offset
|
|
||||||
)
|
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||||
@error_logging
|
|
||||||
@allow_public
|
|
||||||
async def event(
|
async def event(
|
||||||
root, info,
|
root, info,
|
||||||
id: Optional[int] = None,
|
id: int | None = None,
|
||||||
url_slug: Optional[str] = None,
|
url_slug: str | None = None,
|
||||||
username: Optional[str] = None,
|
username: str | None = None,
|
||||||
public: Optional[bool] = False
|
public: bool | None = False
|
||||||
) -> Event:
|
) -> Event:
|
||||||
filter_params = get_detail_filters(id, url_slug)
|
filter_params = get_detail_filters(id, url_slug)
|
||||||
if username:
|
if username:
|
||||||
@@ -61,14 +48,10 @@ class EventQueries:
|
|||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class EventMutation:
|
class EventMutation:
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@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(info.context, input)
|
return await BaseMutationResolver(Event, models.Event).create(info.context, input)
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def edit_event(root, info, id: int, input: EditEventInput) -> Event:
|
async def edit_event(root, info, id: int, input: EditEventInput) -> Event:
|
||||||
return await BaseMutationResolver(Event, models.Event).update(id, input, info.context.user_id)
|
return await BaseMutationResolver(Event, models.Event).update(info.context, id, input)
|
||||||
|
|||||||
@@ -1,57 +1,45 @@
|
|||||||
from typing import Optional
|
|
||||||
import strawberry
|
import strawberry
|
||||||
from decorators.endpoints import authenticated_user_only, allow_public
|
|
||||||
from decorators.error_logging import error_logging
|
|
||||||
from graphql_schema.entities.resolvers.flight import FlightMutationResolver, FlightQueryResolver
|
from graphql_schema.entities.resolvers.flight import FlightMutationResolver, FlightQueryResolver
|
||||||
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.detail import get_detail_filters
|
from .helpers.filters import get_detail_filters
|
||||||
from .helpers.pagination import PaginationWindow, get_pagination_window
|
from .helpers.pagination import PaginationWindow
|
||||||
|
from ..extensions.field.auth import AllowPublicAccess, AuthenticatedOnly
|
||||||
|
from ..extensions.field.pagination import OffsetPagination
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class FlightQueries:
|
class FlightQueries:
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[OffsetPagination(item_type=Flight), AllowPublicAccess()])
|
||||||
@error_logging
|
|
||||||
@allow_public
|
|
||||||
async def flights(
|
async def flights(
|
||||||
root, info,
|
root, info,
|
||||||
limit: int,
|
username: str | None = None,
|
||||||
offset: int = 0,
|
search: str | None = None,
|
||||||
username: Optional[str] = None,
|
event_id: int | None = None,
|
||||||
event_id: Optional[int] = None,
|
public: bool | None = False,
|
||||||
public: Optional[bool] = False,
|
copilot_id: int | None = None,
|
||||||
copilot_id: Optional[int] = None,
|
point_of_interest_id: int | None = None,
|
||||||
point_of_interest_id: Optional[int] = None,
|
aircraft_id: int | None = None,
|
||||||
aircraft_id: Optional[int] = None,
|
|
||||||
) -> PaginationWindow[Flight]:
|
) -> PaginationWindow[Flight]:
|
||||||
query = FlightQueryResolver().get_query(
|
return FlightQueryResolver().get_query(
|
||||||
user_id=info.context.user_id,
|
user_id=info.context.user_id,
|
||||||
username=username,
|
username=username,
|
||||||
event_id=event_id,
|
event_id=event_id,
|
||||||
only_public=public,
|
only_public=public,
|
||||||
copilot_id=copilot_id,
|
copilot_id=copilot_id,
|
||||||
aircraft_id=aircraft_id,
|
aircraft_id=aircraft_id,
|
||||||
point_of_interest_id=point_of_interest_id
|
point_of_interest_id=point_of_interest_id,
|
||||||
|
search=search
|
||||||
)
|
)
|
||||||
|
|
||||||
return await get_pagination_window(
|
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||||
query=query,
|
|
||||||
item_type=Flight,
|
|
||||||
limit=limit,
|
|
||||||
offset=offset,
|
|
||||||
)
|
|
||||||
|
|
||||||
@strawberry.field()
|
|
||||||
@error_logging
|
|
||||||
@allow_public
|
|
||||||
async def flight(
|
async def flight(
|
||||||
root, info,
|
root, info,
|
||||||
id: Optional[int] = None,
|
id: int | None = None,
|
||||||
url_slug: Optional[str] = None,
|
url_slug: str | None = None,
|
||||||
username: Optional[str] = None,
|
username: str | None = None,
|
||||||
public: Optional[bool] = False
|
public: bool | None = False
|
||||||
) -> Flight:
|
) -> Flight:
|
||||||
filter_params = get_detail_filters(id, url_slug)
|
filter_params = get_detail_filters(id, url_slug)
|
||||||
if username:
|
if username:
|
||||||
@@ -66,20 +54,14 @@ class FlightQueries:
|
|||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class FlightMutation:
|
class FlightMutation:
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def create_flight(self, info, input: CreateFlightInput) -> Flight:
|
async def create_flight(self, info, input: CreateFlightInput) -> Flight:
|
||||||
return await FlightMutationResolver().create(info.context, input)
|
return await FlightMutationResolver().create(info.context, input)
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def edit_flight(self, info, id: int, input: EditFlightInput) -> Flight:
|
async def edit_flight(self, info, id: int, input: EditFlightInput) -> Flight:
|
||||||
return await FlightMutationResolver().update(info.context, id, input)
|
return await FlightMutationResolver().update(info.context, id, input)
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def delete_flight(self, info, id: int) -> Flight:
|
async def delete_flight(self, info, id: int) -> Flight:
|
||||||
return await FlightMutationResolver().delete(info.context.user_id, id)
|
return await FlightMutationResolver().delete(info.context, id)
|
||||||
|
|||||||
@@ -1,31 +1,27 @@
|
|||||||
from typing import List, Optional
|
|
||||||
import strawberry
|
import strawberry
|
||||||
from strawberry.types import Info
|
from strawberry.types import Info
|
||||||
from decorators.endpoints import authenticated_user_only, allow_public
|
from graphql_schema.entities.helpers.filters import get_detail_filters
|
||||||
from decorators.error_logging import error_logging
|
from graphql_schema.entities.helpers.pagination import PaginationWindow
|
||||||
from graphql_schema.entities.helpers.detail import get_detail_filters
|
|
||||||
from graphql_schema.entities.resolvers.flight_plan import FlightPlanMutationResolver, FlightPlanQueryResolver
|
from graphql_schema.entities.resolvers.flight_plan import FlightPlanMutationResolver, FlightPlanQueryResolver
|
||||||
from graphql_schema.entities.types.mutation_input import CreateFlightPlanInput, EditFlightPlanInput
|
from graphql_schema.entities.types.mutation_input import CreateFlightPlanInput, EditFlightPlanInput
|
||||||
from graphql_schema.entities.types.types import FlightPlan
|
from graphql_schema.entities.types.types import FlightPlan
|
||||||
|
from graphql_schema.extensions.field.auth import AuthenticatedOnly, AllowPublicAccess
|
||||||
|
from graphql_schema.extensions.field.pagination import OffsetPagination
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class FlightPlanQueries:
|
class FlightPlanQueries:
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[OffsetPagination(item_type=FlightPlan), AuthenticatedOnly()])
|
||||||
@error_logging
|
async def flight_plans(root, info: Info, search: str | None = None) -> PaginationWindow[FlightPlan]:
|
||||||
@authenticated_user_only()
|
return FlightPlanQueryResolver().get_query(info.context.user_id, search=search)
|
||||||
async def flight_plans(root, info: Info) -> List[FlightPlan]:
|
|
||||||
return await FlightPlanQueryResolver().get_list(info.context.user_id)
|
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||||
@error_logging
|
|
||||||
@allow_public
|
|
||||||
async def flight_plan(
|
async def flight_plan(
|
||||||
root,
|
root,
|
||||||
info: Info,
|
info: Info,
|
||||||
id: int,
|
id: int,
|
||||||
username: Optional[str] = None,
|
username: str | None = None,
|
||||||
public: Optional[bool] = False
|
public: bool | None = False
|
||||||
) -> FlightPlan:
|
) -> FlightPlan:
|
||||||
filter_params = get_detail_filters(id)
|
filter_params = get_detail_filters(id)
|
||||||
if username:
|
if username:
|
||||||
@@ -40,14 +36,10 @@ class FlightPlanQueries:
|
|||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class FlightPlanMutation:
|
class FlightPlanMutation:
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
async def create_flight_plan(root, info: Info, input: CreateFlightPlanInput) -> FlightPlan:
|
||||||
@authenticated_user_only()
|
|
||||||
async def create_flight_plan(root, info, input: CreateFlightPlanInput) -> FlightPlan:
|
|
||||||
return await FlightPlanMutationResolver().create(info.context, data=input)
|
return await FlightPlanMutationResolver().create(info.context, data=input)
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
async def edit_flight_plan(root, info: Info, id: int, input: EditFlightPlanInput) -> FlightPlan:
|
||||||
@authenticated_user_only()
|
return await FlightPlanMutationResolver().update(info.context, id, input)
|
||||||
async def edit_flight_plan(root, info, id: int, input: EditFlightPlanInput) -> FlightPlan:
|
|
||||||
return await FlightPlanMutationResolver().update(id, input, info.context.user_id)
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ async def handle_combobox_save(
|
|||||||
input: ComboboxInput,
|
input: ComboboxInput,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
name_column: str = "name",
|
name_column: str = "name",
|
||||||
extra_data: Optional[dict] = None
|
extra_data: dict | None = None
|
||||||
) -> int:
|
) -> int:
|
||||||
if input.id:
|
if input.id:
|
||||||
return input.id
|
return input.id
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
from typing import Optional
|
|
||||||
from graphql import GraphQLError
|
|
||||||
|
|
||||||
|
|
||||||
def get_detail_filters(id: Optional[int] = None, url_slug: Optional[str] = None) -> 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
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from graphql import GraphQLError
|
||||||
|
|
||||||
|
|
||||||
|
def get_filters(**kwargs) -> dict[str, Any]:
|
||||||
|
return {k: v for k, v in kwargs.items() if v is not None}
|
||||||
|
|
||||||
|
|
||||||
|
def get_detail_filters(id: int | None = None, url_slug: str | None = None) -> dict:
|
||||||
|
filter_params = get_filters(object_id=id, url_slug=url_slug)
|
||||||
|
|
||||||
|
if not filter_params:
|
||||||
|
raise GraphQLError("You must specifiy either urlSlug or id!")
|
||||||
|
|
||||||
|
return filter_params
|
||||||
@@ -30,10 +30,6 @@ async def get_pagination_window(
|
|||||||
cnt_query = query.with_only_columns(func.count())
|
cnt_query = query.with_only_columns(func.count())
|
||||||
total_items_count = (await db.scalars(cnt_query)).one()
|
total_items_count = (await db.scalars(cnt_query)).one()
|
||||||
|
|
||||||
# if offset != 0 and not 0 <= offset < total_items_count:
|
|
||||||
# raise Exception(f"offset ({offset}) is out of range " f"(0-{total_items_count - 1})")
|
|
||||||
|
|
||||||
async with get_session() as db:
|
|
||||||
data = (await db.scalars(query.limit(limit).offset(offset))).all()
|
data = (await db.scalars(query.limit(limit).offset(offset))).all()
|
||||||
dataset = [item_type(**i.as_dict()) for i in data]
|
dataset = [item_type(**i.as_dict()) for i in data]
|
||||||
|
|
||||||
|
|||||||
@@ -3,27 +3,23 @@ import strawberry
|
|||||||
from sqlalchemy import delete
|
from sqlalchemy import delete
|
||||||
from sqlalchemy.dialects.mysql import insert
|
from sqlalchemy.dialects.mysql import insert
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from strawberry import Info
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.endpoints import authenticated_user_only
|
|
||||||
from database.transaction import get_session
|
from database.transaction import get_session
|
||||||
from decorators.error_logging import error_logging
|
|
||||||
from graphql_schema.entities.resolvers.base import BaseMutationResolver
|
from graphql_schema.entities.resolvers.base import BaseMutationResolver
|
||||||
from graphql_schema.entities.resolvers.organization import OrganizationQueryResolver
|
from graphql_schema.entities.resolvers.organization import OrganizationQueryResolver
|
||||||
from graphql_schema.entities.types.mutation_input import CreateOrganizationInput, EditOrganizationInput
|
from graphql_schema.entities.types.mutation_input import CreateOrganizationInput, EditOrganizationInput
|
||||||
from graphql_schema.entities.types.types import Organization
|
from graphql_schema.entities.types.types import Organization
|
||||||
|
from graphql_schema.extensions.field.auth import AuthenticatedOnly
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class OrganizationQueries:
|
class OrganizationQueries:
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def organizations(root, info) -> List[Organization]:
|
async def organizations(root, info) -> List[Organization]:
|
||||||
return await OrganizationQueryResolver().get_list()
|
return await OrganizationQueryResolver().get_list()
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def organization(root, info, id: int) -> Organization:
|
async def organization(root, info, id: int) -> Organization:
|
||||||
return await OrganizationQueryResolver().get_one(object_id=id)
|
return await OrganizationQueryResolver().get_one(object_id=id)
|
||||||
|
|
||||||
@@ -31,17 +27,14 @@ class OrganizationQueries:
|
|||||||
@strawberry.type
|
@strawberry.type
|
||||||
class OrganizationMutation:
|
class OrganizationMutation:
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
async def create_organization(root, info: Info, input: CreateOrganizationInput) -> Organization:
|
||||||
@authenticated_user_only()
|
|
||||||
async def create_organization(root, info, input: CreateOrganizationInput) -> Organization:
|
|
||||||
return await BaseMutationResolver(Organization, models.Organization).create(info.context, data=input)
|
return await BaseMutationResolver(Organization, models.Organization).create(info.context, data=input)
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
async def edit_organization(root, info: Info, id: int, input: EditOrganizationInput) -> Organization:
|
||||||
@authenticated_user_only()
|
|
||||||
async def edit_organization(root, info, id: int, input: EditOrganizationInput) -> Organization:
|
|
||||||
return await BaseMutationResolver(Organization, models.Organization).update(
|
return await BaseMutationResolver(Organization, models.Organization).update(
|
||||||
|
info.context,
|
||||||
id,
|
id,
|
||||||
data=input,
|
data=input,
|
||||||
user_id=info.context.user_id
|
user_id=info.context.user_id
|
||||||
@@ -51,9 +44,7 @@ class OrganizationMutation:
|
|||||||
@strawberry.type
|
@strawberry.type
|
||||||
class OrganizationUserMutation:
|
class OrganizationUserMutation:
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def add_to_organization(root, info, organization_id: int) -> Organization:
|
async def add_to_organization(root, info, organization_id: int) -> Organization:
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
organization = (await db.scalars(
|
organization = (await db.scalars(
|
||||||
@@ -72,9 +63,7 @@ class OrganizationUserMutation:
|
|||||||
|
|
||||||
return Organization(**organization.as_dict())
|
return Organization(**organization.as_dict())
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def remove_from_organization(root, info, organization_id: int) -> Organization:
|
async def remove_from_organization(root, info, organization_id: int) -> Organization:
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
organization = (await db.scalars(
|
organization = (await db.scalars(
|
||||||
|
|||||||
@@ -1,28 +1,31 @@
|
|||||||
from typing import List, Optional
|
|
||||||
import strawberry
|
import strawberry
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||||
|
from strawberry import Info
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.endpoints import authenticated_user_only, allow_public, raise_unauthorized
|
|
||||||
from decorators.error_logging import error_logging
|
|
||||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
||||||
from graphql_schema.entities.resolvers.photo import PhotoMutationResolver, PhotoQueryResolver
|
from graphql_schema.entities.resolvers.photo import PhotoMutationResolver, PhotoQueryResolver
|
||||||
from graphql_schema.entities.types.types import Photo
|
from graphql_schema.entities.types.types import Photo
|
||||||
from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput
|
from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput
|
||||||
|
from graphql_schema.extensions.field.auth import AuthenticatedOnly, AllowPublicAccess
|
||||||
|
|
||||||
|
|
||||||
|
def raise_unauthorized():
|
||||||
|
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class PhotoQueries:
|
class PhotoQueries:
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||||
@error_logging
|
|
||||||
@allow_public
|
|
||||||
async def photos(
|
async def photos(
|
||||||
root, info,
|
root, info: Info,
|
||||||
flight_id: Optional[int] = None,
|
flight_id: int | None = None,
|
||||||
copilot_id: Optional[int] = None,
|
copilot_id: int | None = None,
|
||||||
uploaded_by_copilot_id: Optional[int] = None,
|
uploaded_by_copilot_id: int | None = None,
|
||||||
point_of_interest_id: Optional[int] = None,
|
point_of_interest_id: int | None = None,
|
||||||
aircraft_id: Optional[int] = None,
|
aircraft_id: int | None = None,
|
||||||
public: Optional[bool] = False,
|
public: bool | None = False,
|
||||||
) -> List[Photo]:
|
) -> list[Photo]:
|
||||||
return await PhotoQueryResolver().get_list(
|
return await PhotoQueryResolver().get_list(
|
||||||
public=public,
|
public=public,
|
||||||
flight_id=flight_id,
|
flight_id=flight_id,
|
||||||
@@ -34,10 +37,8 @@ class PhotoQueries:
|
|||||||
order_by=[models.Photo.exposed_at]
|
order_by=[models.Photo.exposed_at]
|
||||||
)
|
)
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||||
@error_logging
|
async def photo(root, info: Info, id: int, public: bool | None = False) -> Photo:
|
||||||
@allow_public
|
|
||||||
async def photo(root, info, id: int, public: Optional[bool] = False, ) -> Photo:
|
|
||||||
return await BaseQueryResolver(Photo, models.Photo).get_one(
|
return await BaseQueryResolver(Photo, models.Photo).get_one(
|
||||||
object_id=id,
|
object_id=id,
|
||||||
user_id=info.context.user_id,
|
user_id=info.context.user_id,
|
||||||
@@ -47,23 +48,19 @@ class PhotoQueries:
|
|||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class PhotoMutation:
|
class PhotoMutation:
|
||||||
@strawberry.mutation
|
@strawberry.mutation()
|
||||||
@error_logging
|
|
||||||
async def upload_photo(self, info, input: UploadPhotoInput) -> Photo:
|
async def upload_photo(self, info, input: UploadPhotoInput) -> Photo:
|
||||||
if info.context.user_id is None and not input.copilot_upload_token:
|
if info.context.user_id is None and not input.copilot_upload_token:
|
||||||
raise_unauthorized()
|
raise_unauthorized()
|
||||||
|
|
||||||
return await PhotoMutationResolver().upload(info, input)
|
return await PhotoMutationResolver().upload(info, input)
|
||||||
|
|
||||||
@strawberry.mutation()
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def edit_photo(self, info, id: int, input: EditPhotoInput) -> Photo:
|
async def edit_photo(self, info, id: int, input: EditPhotoInput) -> Photo:
|
||||||
return await PhotoMutationResolver().update(id, input, info.context.user_id)
|
return await PhotoMutationResolver().update(info.context, id, input, info.context.user_id)
|
||||||
|
|
||||||
@strawberry.mutation()
|
@strawberry.mutation()
|
||||||
@error_logging
|
async def change_orientation(self, info, id: int, direction: str, copilot_upload_token: str | None = None) -> Photo:
|
||||||
async def change_orientation(self, info, id: int, direction: str, copilot_upload_token: Optional[str] = None) -> Photo:
|
|
||||||
if info.context.user_id is None and not copilot_upload_token:
|
if info.context.user_id is None and not copilot_upload_token:
|
||||||
raise_unauthorized()
|
raise_unauthorized()
|
||||||
|
|
||||||
@@ -75,16 +72,13 @@ class PhotoMutation:
|
|||||||
info=info
|
info=info
|
||||||
)
|
)
|
||||||
|
|
||||||
@strawberry.mutation()
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def adjust_photo(self, info, id: int, adjustment: AdjustmentInput) -> Photo:
|
async def adjust_photo(self, info, id: int, adjustment: AdjustmentInput) -> Photo:
|
||||||
return await PhotoMutationResolver().adjust(id, info=info, user_id=info.context.user_id, adjustment=adjustment)
|
return await PhotoMutationResolver().adjust(id, info=info, user_id=info.context.user_id, adjustment=adjustment)
|
||||||
|
|
||||||
@strawberry.mutation()
|
@strawberry.mutation()
|
||||||
@error_logging
|
async def delete_photo(self, info, id: int, copilot_upload_token: str | None = None) -> Photo:
|
||||||
async def delete_photo(self, info, id: int, copilot_upload_token: Optional[str] = None) -> Photo:
|
|
||||||
if info.context.user_id is None and not copilot_upload_token:
|
if info.context.user_id is None and not copilot_upload_token:
|
||||||
raise_unauthorized()
|
raise_unauthorized()
|
||||||
|
|
||||||
return await PhotoMutationResolver().delete(user_id=info.context.user_id, id=id)
|
return await PhotoMutationResolver().delete(info.context, id=id)
|
||||||
|
|||||||
@@ -1,48 +1,37 @@
|
|||||||
from typing import Optional
|
|
||||||
import strawberry
|
import strawberry
|
||||||
from database import models
|
from database import models
|
||||||
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 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.filters import get_detail_filters
|
||||||
from graphql_schema.entities.helpers.pagination import get_pagination_window, PaginationWindow
|
from graphql_schema.entities.helpers.pagination import 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
|
||||||
|
from graphql_schema.extensions.field.auth import AuthenticatedOnly, AllowPublicAccess
|
||||||
|
from graphql_schema.extensions.field.pagination import OffsetPagination
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class PointOfInterestQueries:
|
class PointOfInterestQueries:
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[OffsetPagination(item_type=PointOfInterest), AllowPublicAccess()])
|
||||||
@error_logging
|
|
||||||
@allow_public
|
|
||||||
async def points_of_interest(
|
async def points_of_interest(
|
||||||
root, info,
|
root, info,
|
||||||
limit: int, offset: int = 0,
|
search: str | None = None,
|
||||||
search: Optional[str] = None,
|
public: bool = False,
|
||||||
public: bool = False
|
|
||||||
) -> PaginationWindow[PointOfInterest]:
|
) -> PaginationWindow[PointOfInterest]:
|
||||||
query = BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_query(
|
return BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_query(
|
||||||
info.context.user_id,
|
info.context.user_id,
|
||||||
only_my=bool(info.context.user_id),
|
only_my=bool(info.context.user_id),
|
||||||
include_others_public=True,
|
include_others_public=True,
|
||||||
only_public=public,
|
only_public=public,
|
||||||
search=search,
|
search=search,
|
||||||
)
|
)
|
||||||
return await get_pagination_window(
|
|
||||||
query=query,
|
|
||||||
item_type=PointOfInterest,
|
|
||||||
limit=limit,
|
|
||||||
offset=offset
|
|
||||||
)
|
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||||
@allow_public
|
|
||||||
async def point_of_interest(
|
async def point_of_interest(
|
||||||
root, info,
|
root, info,
|
||||||
url_slug: Optional[str] = None,
|
url_slug: str | None = None,
|
||||||
id: Optional[int] = None,
|
id: int | None = None,
|
||||||
public: bool = False
|
public: bool = False
|
||||||
) -> PointOfInterest:
|
) -> PointOfInterest:
|
||||||
filter_params = get_detail_filters(id, url_slug)
|
filter_params = get_detail_filters(id, url_slug)
|
||||||
@@ -56,9 +45,7 @@ class PointOfInterestQueries:
|
|||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class PointOfInterestMutation:
|
class PointOfInterestMutation:
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def create_point_of_interest(root, info, input: CreatePointOfInterestInput) -> PointOfInterest:
|
async def create_point_of_interest(root, info, input: CreatePointOfInterestInput) -> PointOfInterest:
|
||||||
input_data = input.to_dict()
|
input_data = input.to_dict()
|
||||||
|
|
||||||
@@ -73,9 +60,7 @@ class PointOfInterestMutation:
|
|||||||
db, input_data
|
db, input_data
|
||||||
)
|
)
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def edit_point_of_interest(root, info, id: int, input: EditPointOfInterestInput) -> PointOfInterest:
|
async def edit_point_of_interest(root, info, id: int, input: EditPointOfInterestInput) -> PointOfInterest:
|
||||||
input_data = input.to_dict()
|
input_data = input.to_dict()
|
||||||
|
|
||||||
@@ -93,8 +78,6 @@ class PointOfInterestMutation:
|
|||||||
updated_poi = await models.PointOfInterest.update(db, obj=poi, data=input_data)
|
updated_poi = await models.PointOfInterest.update(db, obj=poi, data=input_data)
|
||||||
return PointOfInterest(**updated_poi.as_dict())
|
return PointOfInterest(**updated_poi.as_dict())
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def delete_point_of_interest(self, info, id: int) -> PointOfInterest:
|
async def delete_point_of_interest(self, info, id: int) -> PointOfInterest:
|
||||||
return await BaseMutationResolver(PointOfInterest, models.PointOfInterest).delete(info.context.user_id, id=id)
|
return await BaseMutationResolver(PointOfInterest, models.PointOfInterest).delete(info.context.user_id, id=id)
|
||||||
|
|||||||
@@ -1,92 +1,20 @@
|
|||||||
from typing import List
|
|
||||||
import strawberry
|
import strawberry
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.endpoints import authenticated_user_only
|
|
||||||
from decorators.error_logging import error_logging
|
|
||||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
||||||
from graphql_schema.entities.types.types import PointOfInterestType
|
from graphql_schema.entities.types.types import PointOfInterestType
|
||||||
|
from graphql_schema.extensions.field.auth import AuthenticatedOnly
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class PointOfInterestTypeQueries:
|
class PointOfInterestTypeQueries:
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
async def point_of_interest_types(root, info) -> list[PointOfInterestType]:
|
||||||
@authenticated_user_only()
|
|
||||||
async def point_of_interest_types(root, info) -> List[PointOfInterestType]:
|
|
||||||
return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_list(info.context.user_id)
|
return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_list(info.context.user_id)
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@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(
|
||||||
object_id=id,
|
object_id=id,
|
||||||
user_id=info.context.user_id
|
user_id=info.context.user_id
|
||||||
)
|
)
|
||||||
|
|
||||||
#
|
|
||||||
# @strawberry.type
|
|
||||||
# class CreatePointOfInterestMutation:
|
|
||||||
# @strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id'])
|
|
||||||
# class CreatePointOfInterestInput:
|
|
||||||
# type: # Optional[ComboboxInput] = None
|
|
||||||
#
|
|
||||||
# @strawberry.mutation
|
|
||||||
# @authenticated_user_only()
|
|
||||||
# async def create_point_of_interest(root, info, input: CreatePointOfInterestInput) -> PointOfInterest:
|
|
||||||
# input_data = input.to_dict()
|
|
||||||
#
|
|
||||||
# input_data['type_id'] = await handle_combobox_save(
|
|
||||||
# info.context.db,
|
|
||||||
# models.PointOfInterestType,
|
|
||||||
# input.type,
|
|
||||||
# info.context.user_id
|
|
||||||
# )
|
|
||||||
#
|
|
||||||
# return await models.PointOfInterest.create(
|
|
||||||
# info.context.db,
|
|
||||||
# data=dict(
|
|
||||||
# **input_data,
|
|
||||||
# created_by_id=info.context.user_id,
|
|
||||||
# )
|
|
||||||
# )
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# @strawberry.type
|
|
||||||
# class EditPointOfInterestMutation:
|
|
||||||
# @strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id'])
|
|
||||||
# class EditPointOfInterestInput:
|
|
||||||
# type: Optional[ComboboxInput] = None
|
|
||||||
#
|
|
||||||
# @strawberry.mutation
|
|
||||||
# @authenticated_user_only()
|
|
||||||
# async def edit_point_of_interest(root, info, id: int, input: EditPointOfInterestInput) -> PointOfInterest:
|
|
||||||
# # TODO: kontrola organizace
|
|
||||||
# input_data = input.to_dict()
|
|
||||||
#
|
|
||||||
# if 'type' in input:
|
|
||||||
# input_data['type_id'] = await handle_combobox_save(
|
|
||||||
# info.context.db,
|
|
||||||
# models.PointOfInterestType,
|
|
||||||
# input.type,
|
|
||||||
# info.context.user_id
|
|
||||||
# )
|
|
||||||
#
|
|
||||||
# poi = (
|
|
||||||
# await info.context.db.scalars(
|
|
||||||
# get_base_query(info.context.user_id, only_my=True)
|
|
||||||
# .filter(models.PointOfInterest.id == id))
|
|
||||||
# ).one()
|
|
||||||
# return await models.PointOfInterest.update(info.context.db, obj=poi, data=input_data)
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# @strawberry.type
|
|
||||||
# class DeletePointOfInterestMutation:
|
|
||||||
#
|
|
||||||
# @strawberry.mutation
|
|
||||||
# @authenticated_user_only()
|
|
||||||
# async def delete_point_of_interest(self, info, id: int) -> PointOfInterest:
|
|
||||||
# poi = get_base_query(info.context.user_id, only_my=True).filter(models.PointOfInterest.id == id).one()
|
|
||||||
#
|
|
||||||
# return await models.PointOfInterest.update(info.context.db, obj=poi, data=dict(deleted=True))
|
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
from operator import or_
|
from sqlalchemy import and_, or_
|
||||||
from typing import Set, Optional
|
|
||||||
|
|
||||||
from sqlalchemy import and_
|
|
||||||
|
|
||||||
from database import models
|
from database import models
|
||||||
from database.transaction import get_session
|
from database.transaction import get_session
|
||||||
|
from graphql_schema.context import GraphQLContext
|
||||||
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
|
||||||
from graphql_schema.entities.types.mutation_input import EditAircraftInput, CreateAircraftInput
|
from graphql_schema.entities.types.mutation_input import EditAircraftInput, CreateAircraftInput
|
||||||
@@ -17,29 +14,36 @@ class AircraftQueryResolver(BaseQueryResolver):
|
|||||||
|
|
||||||
def get_query(
|
def get_query(
|
||||||
self,
|
self,
|
||||||
user_id: Optional[int] = None,
|
user_id: int | None = None,
|
||||||
object_id: Optional[int] = None,
|
object_id: int | None = None,
|
||||||
order_by: Optional[list] = None,
|
order_by: list | None = None,
|
||||||
organization_ids: Optional[Set[int]] = None,
|
organization_ids: set[int] | None = None,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
filters = []
|
filters = []
|
||||||
search = {}
|
|
||||||
if kwargs.get("call_sign"):
|
if kwargs.get("call_sign"):
|
||||||
filters.append(models.Aircraft.call_sign == kwargs['call_sign'])
|
filters.append(models.Aircraft.call_sign == kwargs['call_sign'])
|
||||||
|
|
||||||
if kwargs.get("search"):
|
|
||||||
search['search'] = kwargs.pop("search", None)
|
|
||||||
|
|
||||||
query = super().get_query(
|
query = super().get_query(
|
||||||
object_id=object_id,
|
object_id=object_id,
|
||||||
only_my=False,
|
only_my=False,
|
||||||
only_public=kwargs.get("only_public", False),
|
only_public=kwargs.get("only_public", False),
|
||||||
order_by=order_by,
|
order_by=order_by,
|
||||||
filters=filters,
|
filters=filters,
|
||||||
**search
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if kwargs.get("search"):
|
||||||
|
search = kwargs["search"]
|
||||||
|
query = query.filter(
|
||||||
|
or_(
|
||||||
|
models.Aircraft.call_sign.contains(search),
|
||||||
|
models.Aircraft.manufacturer.contains(search),
|
||||||
|
models.Aircraft.model.contains(search),
|
||||||
|
)
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
if organization_ids:
|
if organization_ids:
|
||||||
query = (
|
query = (
|
||||||
query.filter(
|
query.filter(
|
||||||
@@ -62,7 +66,7 @@ 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(self, context, data: CreateAircraftInput) -> Aircraft:
|
async def create(self, context: GraphQLContext, data: CreateAircraftInput) -> Aircraft:
|
||||||
input_data = data.to_dict()
|
input_data = data.to_dict()
|
||||||
|
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
@@ -77,7 +81,7 @@ class AircraftMutationResolver(BaseMutationResolver):
|
|||||||
|
|
||||||
return await self._do_create(db, data=input_data)
|
return await self._do_create(db, data=input_data)
|
||||||
|
|
||||||
async def update(self, id: int, user_id: int, data: EditAircraftInput) -> Aircraft:
|
async def update(self, context: GraphQLContext, 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:
|
||||||
@@ -85,7 +89,7 @@ class AircraftMutationResolver(BaseMutationResolver):
|
|||||||
db,
|
db,
|
||||||
models.Organization,
|
models.Organization,
|
||||||
input=data.organization,
|
input=data.organization,
|
||||||
user_id=user_id,
|
user_id=context.user_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
return await self._do_update(db, id, update_data)
|
return await self._do_update(db, id, update_data)
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
from typing import Optional, Type, TypeVar, Generic, List
|
from typing import Type, TypeVar, Generic
|
||||||
|
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from database import models
|
from database import models
|
||||||
from database.query_builder import QueryBuilder
|
from database.query_builder import QueryBuilder
|
||||||
from database.transaction import get_session
|
from database.transaction import get_session
|
||||||
|
from graphql_schema.context import GraphQLContext
|
||||||
from graphql_schema.entities.types.base import BaseGraphqlInputType
|
from graphql_schema.entities.types.base import BaseGraphqlInputType
|
||||||
|
|
||||||
|
|
||||||
GQL_TYPE = TypeVar('GQL_TYPE')
|
GQL_TYPE = TypeVar('GQL_TYPE')
|
||||||
|
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ class BaseResolver(Generic[GQL_TYPE]):
|
|||||||
|
|
||||||
|
|
||||||
class BaseQueryResolver(BaseResolver):
|
class BaseQueryResolver(BaseResolver):
|
||||||
async def _get_list(self, query) -> List[GQL_TYPE]:
|
async def _get_list(self, query) -> list[GQL_TYPE]:
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
items = (await db.scalars(query)).all()
|
items = (await db.scalars(query)).all()
|
||||||
|
|
||||||
@@ -31,14 +32,14 @@ class BaseQueryResolver(BaseResolver):
|
|||||||
|
|
||||||
def get_query(
|
def get_query(
|
||||||
self,
|
self,
|
||||||
user_id: Optional[int] = None,
|
user_id: int | None = None,
|
||||||
object_id: Optional[int] = None,
|
object_id: int | None = None,
|
||||||
order_by: Optional[list] = None,
|
order_by: list | None = None,
|
||||||
only_public: Optional[bool] = False,
|
only_public: bool | None = False,
|
||||||
only_my: Optional[bool] = False,
|
only_my: bool | None = False,
|
||||||
include_others_public: Optional[bool] = False,
|
include_others_public: bool | None = False,
|
||||||
url_slug: Optional[str] = None,
|
url_slug: str | None = None,
|
||||||
filters: Optional[list] = None,
|
filters: list | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
query = self.query_builder.get_simple_query(
|
query = self.query_builder.get_simple_query(
|
||||||
@@ -77,11 +78,11 @@ class BaseQueryResolver(BaseResolver):
|
|||||||
query = query.filter(or_(*search_clauses))
|
query = query.filter(or_(*search_clauses))
|
||||||
return query
|
return query
|
||||||
|
|
||||||
async def get_list(self, user_id: Optional[int] = None, **kwargs) -> List[GQL_TYPE]:
|
async def get_list(self, user_id: int | None = None, **kwargs) -> list[GQL_TYPE]:
|
||||||
query = self.get_query(user_id=user_id, **kwargs)
|
query = self.get_query(user_id=user_id, **kwargs)
|
||||||
return await self._get_list(query)
|
return await self._get_list(query)
|
||||||
|
|
||||||
async def get_one(self, user_id: Optional[int] = None, **kwargs) -> GQL_TYPE:
|
async def get_one(self, user_id: int | None = None, **kwargs) -> GQL_TYPE:
|
||||||
query = self.get_query(user_id=user_id, **kwargs)
|
query = self.get_query(user_id=user_id, **kwargs)
|
||||||
return await self._get_one(query)
|
return await self._get_one(query)
|
||||||
|
|
||||||
@@ -107,7 +108,7 @@ class BaseMutationResolver(BaseResolver):
|
|||||||
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:
|
async def create(self, context: GraphQLContext, data: BaseGraphqlInputType) -> GQL_TYPE:
|
||||||
input_data = data.to_dict()
|
input_data = data.to_dict()
|
||||||
|
|
||||||
if hasattr(self.model, "created_by_id"):
|
if hasattr(self.model, "created_by_id"):
|
||||||
@@ -116,14 +117,14 @@ class BaseMutationResolver(BaseResolver):
|
|||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
return await self._do_create(db, input_data)
|
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, context: GraphQLContext, id: int, data: BaseGraphqlInputType) -> 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, context.user_id)
|
||||||
return await self._do_update(db, item, data.to_dict())
|
return await self._do_update(db, item, data.to_dict())
|
||||||
|
|
||||||
async def delete(self, user_id: int, id: int, **kwargs) -> GQL_TYPE:
|
async def delete(self, context: GraphQLContext, id: int, **kwargs) -> GQL_TYPE:
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
model = await self._get_one(db, id, user_id)
|
model = await self._get_one(db, id, context.user_id)
|
||||||
|
|
||||||
if hasattr(self.model, "deleted"):
|
if hasattr(self.model, "deleted"):
|
||||||
model = await self.model.update(db, obj=model, data=dict(deleted=True))
|
model = await self.model.update(db, obj=model, data=dict(deleted=True))
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
|
|
||||||
from database import models
|
from database import models
|
||||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
||||||
from graphql_schema.entities.types.types import Copilot
|
from graphql_schema.entities.types.types import Copilot
|
||||||
@@ -13,10 +10,10 @@ class CopilotQueryResolver(BaseQueryResolver):
|
|||||||
|
|
||||||
def get_query(
|
def get_query(
|
||||||
self,
|
self,
|
||||||
user_id: Optional[int] = None,
|
user_id: int | None = None,
|
||||||
object_id: Optional[int] = None,
|
object_id: int | None = None,
|
||||||
order_by: Optional[list] = None,
|
order_by: list | None = None,
|
||||||
only_public: Optional[bool] = False,
|
only_public: bool | None = False,
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
pilot_username = kwargs.pop("pilot_username", None)
|
pilot_username = kwargs.pop("pilot_username", None)
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ class EventQueryResolver(BaseQueryResolver):
|
|||||||
|
|
||||||
def get_query(
|
def get_query(
|
||||||
self,
|
self,
|
||||||
user_id: Optional[int] = None,
|
user_id: int | None = None,
|
||||||
object_id: Optional[int] = None,
|
object_id: int | None = None,
|
||||||
order_by: Optional[list] = None,
|
order_by: list | None = None,
|
||||||
only_public: Optional[bool] = True,
|
only_public: bool | None = True,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import random
|
import random
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from sqlalchemy import delete, insert
|
from sqlalchemy import delete, insert, or_, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from background_jobs.elevation import add_terrain_elevation_to_flight
|
from background_jobs.elevation import add_terrain_elevation_to_flight
|
||||||
|
from background_jobs.flight_title_photo import add_circular_avatar, generate_flight_title_photo
|
||||||
|
from graphql_schema.context import GraphQLContext
|
||||||
from utils.flight_track_helpers import handle_upload_gpx, save_track_from_gpx_to_db, extract_basic_flight_info_from_gpx
|
from utils.flight_track_helpers import handle_upload_gpx, save_track_from_gpx_to_db, extract_basic_flight_info_from_gpx
|
||||||
from background_jobs.weather import download_weather_for_flight
|
from background_jobs.weather import download_weather_for_flight
|
||||||
from database import models
|
from database import models
|
||||||
@@ -26,8 +28,8 @@ class FlightQueryResolver(BaseQueryResolver):
|
|||||||
|
|
||||||
def get_query(
|
def get_query(
|
||||||
self,
|
self,
|
||||||
user_id: Optional[int] = None,
|
user_id: int | None = None,
|
||||||
only_public: Optional[bool] = False,
|
only_public: bool | None = False,
|
||||||
*args,
|
*args,
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
@@ -55,8 +57,27 @@ class FlightQueryResolver(BaseQueryResolver):
|
|||||||
|
|
||||||
if kwargs.get("point_of_interest_id"):
|
if kwargs.get("point_of_interest_id"):
|
||||||
query = (
|
query = (
|
||||||
query.join(models.Flight.turn_points)
|
query
|
||||||
.filter(models.FlightTurnPoint.point_of_interest_id == kwargs["point_of_interest_id"])
|
.filter(
|
||||||
|
or_(
|
||||||
|
models.Flight.id.in_(
|
||||||
|
select(models.FlightTurnPoint.flight_id)
|
||||||
|
.filter(models.FlightTurnPoint.point_of_interest_id == kwargs["point_of_interest_id"])
|
||||||
|
),
|
||||||
|
models.Flight.id.in_(
|
||||||
|
select(models.Photo.flight_id)
|
||||||
|
.filter(models.Photo.point_of_interest_id == kwargs["point_of_interest_id"])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if kwargs.get("search"):
|
||||||
|
search = kwargs["search"]
|
||||||
|
query = (
|
||||||
|
query.filter(
|
||||||
|
models.Flight.name.contains(search)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if kwargs.get('username'):
|
if kwargs.get('username'):
|
||||||
@@ -72,7 +93,7 @@ class FlightMutationResolver(BaseMutationResolver):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(Flight, models.Flight)
|
super().__init__(Flight, models.Flight)
|
||||||
|
|
||||||
async def create(self, context, input: CreateFlightInput) -> Flight:
|
async def create(self, context: GraphQLContext, input: CreateFlightInput) -> Flight:
|
||||||
data = input.to_dict()
|
data = input.to_dict()
|
||||||
user_id = context.user_id
|
user_id = context.user_id
|
||||||
|
|
||||||
@@ -108,14 +129,14 @@ class FlightMutationResolver(BaseMutationResolver):
|
|||||||
if input.track is not None:
|
if input.track is not None:
|
||||||
await handle_track_edit(db=db, flight_id=flight.id, track=input.track, user_id=user_id)
|
await handle_track_edit(db=db, flight_id=flight.id, track=input.track, user_id=user_id)
|
||||||
|
|
||||||
if data['gpx_track_filename']:
|
if 'gpx_track_filename' in data:
|
||||||
await save_track_from_gpx_to_db(gpx_filename=data['gpx_track_filename'], flight_id=flight.id)
|
await save_track_from_gpx_to_db(gpx_filename=data['gpx_track_filename'], flight_id=flight.id)
|
||||||
|
|
||||||
schedule_background_tasks(flight.id, data, context)
|
schedule_background_tasks(flight.id, data, context)
|
||||||
|
|
||||||
return flight
|
return flight
|
||||||
|
|
||||||
async def update(self, context, id: int, input: EditFlightInput) -> Flight:
|
async def update(self, context: GraphQLContext, 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:
|
||||||
flight = await self._get_one(db, id, user_id)
|
flight = await self._get_one(db, id, user_id)
|
||||||
@@ -179,14 +200,23 @@ class FlightMutationResolver(BaseMutationResolver):
|
|||||||
|
|
||||||
def schedule_background_tasks(flight_id: int, flight_data: dict, context) -> None:
|
def schedule_background_tasks(flight_id: int, flight_data: dict, context) -> None:
|
||||||
context.background_tasks.add_task(add_terrain_elevation_to_flight, flight_id=flight_id)
|
context.background_tasks.add_task(add_terrain_elevation_to_flight, flight_id=flight_id)
|
||||||
context.background_tasks.add_task(
|
|
||||||
download_weather_for_flight, flight_id=id, airport_id=flight_data['takeoff_airport_id'],
|
if flight_data.get("title_photo_id"):
|
||||||
date_time=flight_data['takeoff_datetime'], type_="takeoff"
|
context.background_tasks.add_task(
|
||||||
)
|
generate_flight_title_photo, flight_id=flight_id, photo_id=flight_data['title_photo_id']
|
||||||
context.background_tasks.add_task(
|
)
|
||||||
download_weather_for_flight, flight_id=id, airport_id=flight_data['landing_airport_id'],
|
|
||||||
date_time=flight_data['landing_datetime'], type_="landing"
|
if flight_data.get("takeoff_airport_id"):
|
||||||
)
|
context.background_tasks.add_task(
|
||||||
|
download_weather_for_flight, flight_id=flight_id, airport_id=flight_data['takeoff_airport_id'],
|
||||||
|
date_time=flight_data['takeoff_datetime'], type_="takeoff"
|
||||||
|
)
|
||||||
|
|
||||||
|
if flight_data.get("landing_airport_id"):
|
||||||
|
context.background_tasks.add_task(
|
||||||
|
download_weather_for_flight, flight_id=flight_id, airport_id=flight_data['landing_airport_id'],
|
||||||
|
date_time=flight_data['landing_datetime'], type_="landing"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import Optional
|
|
||||||
from sqlalchemy import delete, select
|
from sqlalchemy import delete, select
|
||||||
from sqlalchemy.dialects.mysql import insert
|
from sqlalchemy.dialects.mysql import insert
|
||||||
|
|
||||||
from background_jobs.flight_plan_track import generate_track_for_flight_plan
|
|
||||||
from database import models
|
from database import models
|
||||||
from database.models import flight_plan_has_copilot
|
from database.models import flight_plan_has_copilot
|
||||||
from database.transaction import get_session
|
from database.transaction import get_session
|
||||||
|
from graphql_schema.context import GraphQLContext
|
||||||
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
|
||||||
from graphql_schema.entities.resolvers.flight import handle_aircraft_save
|
from graphql_schema.entities.resolvers.flight import handle_aircraft_save
|
||||||
from graphql_schema.entities.types.mutation_input import CreateFlightPlanInput, EditFlightPlanInput
|
from graphql_schema.entities.types.mutation_input import CreateFlightPlanInput, EditFlightPlanInput
|
||||||
from graphql_schema.entities.types.types import FlightPlan
|
from graphql_schema.entities.types.types import FlightPlan
|
||||||
|
from utils.flight_plan_helpers import refresh_markers_weather_info
|
||||||
from utils.list_utils import evenly_spaced_elements
|
from utils.list_utils import evenly_spaced_elements
|
||||||
from utils.str_utils import random_str
|
from utils.str_utils import random_str
|
||||||
|
|
||||||
@@ -22,9 +21,9 @@ class FlightPlanQueryResolver(BaseQueryResolver):
|
|||||||
|
|
||||||
def get_query(
|
def get_query(
|
||||||
self,
|
self,
|
||||||
user_id: Optional[int] = None,
|
user_id: int | None = None,
|
||||||
only_public: Optional[bool] = False,
|
only_public: bool | None = False,
|
||||||
object_id: Optional[int] = None,
|
object_id: int | None = None,
|
||||||
*args,
|
*args,
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
@@ -43,6 +42,12 @@ class FlightPlanQueryResolver(BaseQueryResolver):
|
|||||||
.filter(models.User.public_username == kwargs['username'])
|
.filter(models.User.public_username == kwargs['username'])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if kwargs.get("search"):
|
||||||
|
search = kwargs["search"]
|
||||||
|
query = (
|
||||||
|
query.filter(models.FlightPlan.name.contains(search))
|
||||||
|
)
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
@@ -50,10 +55,71 @@ class FlightPlanMutationResolver(BaseMutationResolver):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(graphql_type=FlightPlan, model=models.FlightPlan)
|
super().__init__(graphql_type=FlightPlan, model=models.FlightPlan)
|
||||||
|
|
||||||
|
async def create(self, context: GraphQLContext, data: CreateFlightPlanInput) -> FlightPlan:
|
||||||
|
input_data = data.to_dict()
|
||||||
|
input_data['created_by_id'] = context.user_id
|
||||||
|
|
||||||
|
async with get_session() as db:
|
||||||
|
flight_plan = await self._do_create(db, data=input_data)
|
||||||
|
markers = await self.save_markers(db, flight_plan, data.markers)
|
||||||
|
await refresh_markers_weather_info(
|
||||||
|
planned_takeoff_datetime=flight_plan.planned_takeoff_datetime,
|
||||||
|
planned_speed=flight_plan.planned_speed,
|
||||||
|
markers=markers,
|
||||||
|
background_tasks=context.background_tasks
|
||||||
|
)
|
||||||
|
return flight_plan
|
||||||
|
|
||||||
|
async def update(self, context: GraphQLContext, id: int, data: EditFlightPlanInput) -> FlightPlan:
|
||||||
|
input_data = data.to_dict()
|
||||||
|
user_id = context.user_id
|
||||||
|
|
||||||
|
async with get_session() as db:
|
||||||
|
flight_plan_model = await self._get_one(db, id=id, created_by_id=user_id)
|
||||||
|
|
||||||
|
if data.aircraft is not None:
|
||||||
|
input_data['aircraft_id'] = await handle_aircraft_save(db, user_id, data.aircraft)
|
||||||
|
|
||||||
|
markers = []
|
||||||
|
if data.markers is not None:
|
||||||
|
await self.reset_plan_markers(db, flight_plan_model)
|
||||||
|
markers = await self.save_markers(db, flight_plan_model, data.markers)
|
||||||
|
await refresh_markers_weather_info(
|
||||||
|
planned_takeoff_datetime=flight_plan_model.planned_takeoff_datetime,
|
||||||
|
planned_speed=flight_plan_model.planned_speed,
|
||||||
|
markers=markers,
|
||||||
|
background_tasks=context.background_tasks
|
||||||
|
)
|
||||||
|
|
||||||
|
if flight_plan_model.is_default_name:
|
||||||
|
if not markers:
|
||||||
|
markers = (await db.scalars(
|
||||||
|
select(models.FlightPlanMarker)
|
||||||
|
.filter(models.FlightPlanMarker.flight_plan_id == id)
|
||||||
|
)).all()
|
||||||
|
|
||||||
|
used_markers = evenly_spaced_elements(markers, 5)
|
||||||
|
input_data['name'] = " - ".join(m.name for m in used_markers)
|
||||||
|
|
||||||
|
# TODO: pokud se zmenila rychlost nebo datum/cas letu, je potreba stahnout nove pocasi!
|
||||||
|
|
||||||
|
if data.copilots is not None:
|
||||||
|
await self.save_copilots(
|
||||||
|
db,
|
||||||
|
flight_plan_id=id,
|
||||||
|
copilots=data.copilots,
|
||||||
|
user_id=user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
flight_plan = await self._do_update(db, obj=flight_plan_model, data=input_data)
|
||||||
|
return flight_plan
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def save_markers(db, flight_plan: models.FlightPlan, markers: list):
|
async def save_markers(db, flight_plan: models.FlightPlan, markers: list):
|
||||||
position = 0
|
position = 0
|
||||||
|
|
||||||
|
marker_objects = []
|
||||||
for marker in markers:
|
for marker in markers:
|
||||||
if marker.type == 'poi':
|
if marker.type == 'poi':
|
||||||
assert bool(marker.point_of_interest_id)
|
assert bool(marker.point_of_interest_id)
|
||||||
@@ -61,7 +127,7 @@ class FlightPlanMutationResolver(BaseMutationResolver):
|
|||||||
if marker.type == 'airport':
|
if marker.type == 'airport':
|
||||||
assert bool(marker.airport_id)
|
assert bool(marker.airport_id)
|
||||||
|
|
||||||
await models.FlightPlanMarker.create(db, data={
|
marker_obj = await models.FlightPlanMarker.create(db, data={
|
||||||
"position": position,
|
"position": position,
|
||||||
"flight_plan_id": flight_plan.id,
|
"flight_plan_id": flight_plan.id,
|
||||||
"airport_id": marker.airport_id,
|
"airport_id": marker.airport_id,
|
||||||
@@ -71,8 +137,11 @@ class FlightPlanMutationResolver(BaseMutationResolver):
|
|||||||
"gps_latitude": marker.gps_latitude,
|
"gps_latitude": marker.gps_latitude,
|
||||||
"gps_longitude": marker.gps_longitude
|
"gps_longitude": marker.gps_longitude
|
||||||
})
|
})
|
||||||
|
marker_objects.append(marker_obj)
|
||||||
position += 1
|
position += 1
|
||||||
|
|
||||||
|
return marker_objects
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def reset_plan_markers(db, flight_plan: models.FlightPlan):
|
async def reset_plan_markers(db, flight_plan: models.FlightPlan):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
@@ -80,15 +149,6 @@ class FlightPlanMutationResolver(BaseMutationResolver):
|
|||||||
.filter(models.FlightPlanMarker.flight_plan_id == flight_plan.id)
|
.filter(models.FlightPlanMarker.flight_plan_id == flight_plan.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def create(self, context, data: CreateFlightPlanInput) -> FlightPlan:
|
|
||||||
input_data = data.to_dict()
|
|
||||||
input_data['created_by_id'] = context.user_id
|
|
||||||
|
|
||||||
async with get_session() as db:
|
|
||||||
flight_plan = await self._do_create(db, data=input_data)
|
|
||||||
await self.save_markers(db, flight_plan, data.markers)
|
|
||||||
return flight_plan
|
|
||||||
|
|
||||||
async def save_copilots(self, db, flight_plan_id: int, copilots: list, user_id: int):
|
async def save_copilots(self, db, flight_plan_id: int, copilots: list, user_id: int):
|
||||||
await db.execute(delete(flight_plan_has_copilot).filter_by(flight_plan_id=flight_plan_id))
|
await db.execute(delete(flight_plan_has_copilot).filter_by(flight_plan_id=flight_plan_id))
|
||||||
copilots = await asyncio.gather(*[
|
copilots = await asyncio.gather(*[
|
||||||
@@ -101,42 +161,3 @@ class FlightPlanMutationResolver(BaseMutationResolver):
|
|||||||
copilot_id=copilot_id,
|
copilot_id=copilot_id,
|
||||||
token=random_str(64)
|
token=random_str(64)
|
||||||
))
|
))
|
||||||
|
|
||||||
async def update(self, id: int, data: EditFlightPlanInput, user_id: int) -> FlightPlan:
|
|
||||||
input_data = data.to_dict()
|
|
||||||
|
|
||||||
async with get_session() as db:
|
|
||||||
flight_plan_model = await self._get_one(db, id=id, created_by_id=user_id)
|
|
||||||
|
|
||||||
if data.aircraft is not None:
|
|
||||||
input_data['aircraft_id'] = await handle_aircraft_save(db, user_id, data.aircraft)
|
|
||||||
|
|
||||||
if data.markers is not None:
|
|
||||||
await self.reset_plan_markers(db, flight_plan_model)
|
|
||||||
await self.save_markers(db, flight_plan_model, data.markers)
|
|
||||||
|
|
||||||
if flight_plan_model.is_default_name:
|
|
||||||
if data.markers:
|
|
||||||
markers = data.markers
|
|
||||||
else:
|
|
||||||
markers = (await db.scalars(
|
|
||||||
select(models.FlightPlanMarker)
|
|
||||||
.filter(models.FlightPlanMarker.flight_plan_id == id)
|
|
||||||
)).all()
|
|
||||||
|
|
||||||
used_markers = evenly_spaced_elements(markers, 5)
|
|
||||||
input_data['name'] = " - ".join(m.name for m in used_markers)
|
|
||||||
|
|
||||||
if data.copilots is not None:
|
|
||||||
await self.save_copilots(
|
|
||||||
db,
|
|
||||||
flight_plan_id=id,
|
|
||||||
copilots=data.copilots,
|
|
||||||
user_id=user_id
|
|
||||||
)
|
|
||||||
|
|
||||||
flight_plan = await self._do_update(db, obj=flight_plan_model, data=input_data)
|
|
||||||
|
|
||||||
await generate_track_for_flight_plan(flight_plan.id)
|
|
||||||
|
|
||||||
return flight_plan
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ class OrganizationQueryResolver(BaseQueryResolver):
|
|||||||
|
|
||||||
def get_query(
|
def get_query(
|
||||||
self,
|
self,
|
||||||
object_id: Optional[int] = None,
|
object_id: int | None = None,
|
||||||
order_by: Optional[list] = None,
|
order_by: list | None = None,
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
query = (
|
query = (
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Optional
|
import sqlalchemy
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
from graphql import GraphQLError
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from pymysql.err import IntegrityError
|
||||||
from sqlalchemy import delete, insert, select
|
from sqlalchemy import delete, insert, select
|
||||||
from background_jobs.elevation import add_terrain_elevation_to_photo
|
from background_jobs.elevation import add_terrain_elevation_to_photo
|
||||||
from background_jobs.photo import generate_thumbnail, resize_photo
|
from background_jobs.photo import generate_thumbnail, resize_photo
|
||||||
from database import models
|
from database import models
|
||||||
from database.transaction import get_session
|
from database.transaction import get_session
|
||||||
|
from graphql_schema.context import GraphQLContext
|
||||||
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
|
||||||
from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput
|
from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput
|
||||||
@@ -26,10 +29,10 @@ class PhotoQueryResolver(BaseQueryResolver):
|
|||||||
|
|
||||||
def get_query(
|
def get_query(
|
||||||
self,
|
self,
|
||||||
user_id: Optional[int] = None,
|
user_id: int | None = None,
|
||||||
object_id: Optional[int] = None,
|
object_id: int | None = None,
|
||||||
order_by: Optional[list] = None,
|
order_by: list | None = None,
|
||||||
only_public: Optional[bool] = False,
|
only_public: bool | None = False,
|
||||||
*args, **kwargs
|
*args, **kwargs
|
||||||
):
|
):
|
||||||
query = super().get_query(
|
query = super().get_query(
|
||||||
@@ -61,7 +64,7 @@ class PhotoQueryResolver(BaseQueryResolver):
|
|||||||
|
|
||||||
class PhotoDetailInfo(BaseModel):
|
class PhotoDetailInfo(BaseModel):
|
||||||
filename: str
|
filename: str
|
||||||
original_filename: str
|
original_filename: str | None
|
||||||
path: str
|
path: str
|
||||||
flight_id: int
|
flight_id: int
|
||||||
|
|
||||||
@@ -71,14 +74,14 @@ class PhotoMutationResolver(BaseMutationResolver):
|
|||||||
super().__init__(Photo, models.Photo)
|
super().__init__(Photo, models.Photo)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _copy_original(path: str, filename: str):
|
def _copy_original(path: str, filename: str) -> str:
|
||||||
original_filename = "_original_" + filename
|
original_filename = "_original_" + filename
|
||||||
if not os.path.isfile(path + "/" + original_filename):
|
if not os.path.isfile(path + "/" + original_filename):
|
||||||
shutil.copyfile(path + "/" + filename, path + "/" + original_filename)
|
shutil.copyfile(path + "/" + filename, path + "/" + original_filename)
|
||||||
|
|
||||||
return original_filename
|
return original_filename
|
||||||
|
|
||||||
async def _get_photo_details(self, id: int, user_id: int, copilot_token: Optional[str]):
|
async def _get_photo_details(self, id: int, user_id: int, copilot_token: str | None, copy_original: bool = True):
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
if not copilot_token:
|
if not copilot_token:
|
||||||
photo = await self._get_one(db, id, created_by_id=user_id)
|
photo = await self._get_one(db, id, created_by_id=user_id)
|
||||||
@@ -89,15 +92,20 @@ class PhotoMutationResolver(BaseMutationResolver):
|
|||||||
filename = photo.filename + "." + photo.filename_extension
|
filename = photo.filename + "." + photo.filename_extension
|
||||||
|
|
||||||
path = get_photo_basepath(flight_id)
|
path = get_photo_basepath(flight_id)
|
||||||
|
|
||||||
|
original_filename = None
|
||||||
|
if copy_original:
|
||||||
|
original_filename = self._copy_original(path, filename)
|
||||||
|
|
||||||
return PhotoDetailInfo(
|
return PhotoDetailInfo(
|
||||||
flight_id=flight_id,
|
flight_id=flight_id,
|
||||||
path=path,
|
path=path,
|
||||||
filename=filename,
|
filename=filename,
|
||||||
original_filename=self._copy_original(path, filename) # TODO: tohle nedelat v getteru!
|
original_filename=original_filename
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _get_photo_info_by_copilot_token(db, photo_id: int, copilot_token: Optional[str]):
|
async def _get_photo_info_by_copilot_token(db, photo_id: int, copilot_token: str | None):
|
||||||
if not copilot_token:
|
if not copilot_token:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -165,17 +173,17 @@ class PhotoMutationResolver(BaseMutationResolver):
|
|||||||
|
|
||||||
return photo
|
return photo
|
||||||
|
|
||||||
async def update(self, id: int, input: EditPhotoInput, user_id: int) -> Photo:
|
async def update(self, context: GraphQLContext, id: int, input: EditPhotoInput) -> Photo:
|
||||||
data = input.to_dict()
|
data = input.to_dict()
|
||||||
|
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
photo = await self._get_one(db, id, created_by_id=user_id)
|
photo = await self._get_one(db, id, created_by_id=context.user_id)
|
||||||
if input.point_of_interest:
|
if input.point_of_interest:
|
||||||
data['point_of_interest_id'] = await handle_combobox_save(
|
data['point_of_interest_id'] = await handle_combobox_save(
|
||||||
db,
|
db,
|
||||||
models.PointOfInterest,
|
models.PointOfInterest,
|
||||||
input.point_of_interest,
|
input.point_of_interest,
|
||||||
user_id,
|
context.user_id,
|
||||||
extra_data={"description": ""}
|
extra_data={"description": ""}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -187,7 +195,7 @@ class PhotoMutationResolver(BaseMutationResolver):
|
|||||||
return await self._do_update(db, obj=photo, data=data)
|
return await self._do_update(db, obj=photo, data=data)
|
||||||
|
|
||||||
async def change_orientation(
|
async def change_orientation(
|
||||||
self, id: int, user_id: int, direction: str, copilot_upload_token: Optional[str], info
|
self, id: int, user_id: int, direction: str, copilot_upload_token: str | None, info
|
||||||
):
|
):
|
||||||
photo = await self._get_photo_details(id, user_id, copilot_upload_token)
|
photo = await self._get_photo_details(id, user_id, copilot_upload_token)
|
||||||
|
|
||||||
@@ -261,12 +269,15 @@ class PhotoMutationResolver(BaseMutationResolver):
|
|||||||
"cache_key": int(time())
|
"cache_key": int(time())
|
||||||
})
|
})
|
||||||
|
|
||||||
async def delete(self, user_id: int, id: int, **kwargs) -> Photo:
|
async def delete(self, context: GraphQLContext, id: int, **kwargs) -> Photo:
|
||||||
copilot_upload_token = kwargs.get("copilot_upload_token")
|
copilot_upload_token = kwargs.get("copilot_upload_token")
|
||||||
await self._get_photo_details(id, user_id, copilot_upload_token)
|
await self._get_photo_details(id, context.user_id, copilot_upload_token, copy_original=False)
|
||||||
# ^^ pokud neexistuje, vyhodi vyjimku
|
|
||||||
|
try:
|
||||||
|
photo = await super().delete(context, id)
|
||||||
|
except (IntegrityError, sqlalchemy.exc.IntegrityError):
|
||||||
|
raise GraphQLError("Nelze smazat titulni fotku")
|
||||||
|
|
||||||
photo = await super().delete(user_id, id)
|
|
||||||
base_path = get_photo_basepath(photo.flight_id)
|
base_path = get_photo_basepath(photo.flight_id)
|
||||||
|
|
||||||
files_to_delete = [
|
files_to_delete = [
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_i
|
|||||||
|
|
||||||
@strawberry.input()
|
@strawberry.input()
|
||||||
class ComboboxInput:
|
class ComboboxInput:
|
||||||
id: Optional[int] = None
|
id: int | None = None
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
|
|
||||||
@strawberry_sqlalchemy_input(model=models.FlightPlanMarker, exclude_fields=["id", "flight_plan_id"])
|
@strawberry_sqlalchemy_input(model=models.FlightPlanMarker, exclude_fields=["id", "flight_plan_id"])
|
||||||
class FlightPlanMarkerInput(BaseGraphqlInputType):
|
class FlightPlanMarkerInput(BaseGraphqlInputType):
|
||||||
position: Optional[int] = 0
|
position: int | None = 0
|
||||||
|
|
||||||
|
|
||||||
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
|
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
|
||||||
@@ -81,19 +81,19 @@ class EditEventInput(BaseGraphqlInputType):
|
|||||||
class UploadPhotoInput:
|
class UploadPhotoInput:
|
||||||
photo: Upload
|
photo: Upload
|
||||||
flight_id: int
|
flight_id: int
|
||||||
name: Optional[str] = None
|
name: str | None = None
|
||||||
description: Optional[str] = None
|
description: str | None = None
|
||||||
point_of_interest: Optional[ComboboxInput] = None
|
point_of_interest: Optional[ComboboxInput] = None
|
||||||
copilot_upload_token: Optional[str] = None
|
copilot_upload_token: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@strawberry.input
|
@strawberry.input
|
||||||
class EditPhotoInput:
|
class EditPhotoInput:
|
||||||
name: Optional[str] = None
|
name: str | None = None
|
||||||
description: Optional[str] = None
|
description: str | None = None
|
||||||
point_of_interest: Optional[ComboboxInput] = None
|
point_of_interest: Optional[ComboboxInput] = None
|
||||||
copilots: Optional[List[ComboboxInput]] = None
|
copilots: Optional[List[ComboboxInput]] = None
|
||||||
aircraft_id: Optional[int] = None
|
aircraft_id: int | None = None
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
@@ -112,12 +112,12 @@ class CropInput(BaseGraphqlInputType):
|
|||||||
|
|
||||||
@strawberry.input
|
@strawberry.input
|
||||||
class AdjustmentInput:
|
class AdjustmentInput:
|
||||||
rotate: Optional[float] = 0
|
rotate: float | None = 0
|
||||||
crop_after_rotate: Optional[bool] = True,
|
crop_after_rotate: bool | None = True,
|
||||||
brightness: Optional[float] = 1
|
brightness: float | None = 1
|
||||||
contrast: Optional[float] = 1
|
contrast: float | None = 1
|
||||||
saturation: Optional[float] = 1
|
saturation: float | None = 1
|
||||||
sharpness: Optional[float] = 1
|
sharpness: float | None = 1
|
||||||
crop: Optional[CropInput] = None
|
crop: Optional[CropInput] = None
|
||||||
|
|
||||||
|
|
||||||
@@ -138,8 +138,8 @@ class EditFlightInput(BaseGraphqlInputType):
|
|||||||
@strawberry.input()
|
@strawberry.input()
|
||||||
class CreateFlightInput(BaseGraphqlInputType):
|
class CreateFlightInput(BaseGraphqlInputType):
|
||||||
aircraft: ComboboxInput
|
aircraft: ComboboxInput
|
||||||
flight_plan_id: Optional[int] = None
|
flight_plan_id: int | None = None
|
||||||
name: Optional[str] = None
|
name: str | None = None
|
||||||
gpx_track_file: Optional[Upload] = None # TODO: poresit validaci uploadovaneho souboru!
|
gpx_track_file: Optional[Upload] = None # TODO: poresit validaci uploadovaneho souboru!
|
||||||
landing_airport: Optional[ComboboxInput] = None
|
landing_airport: Optional[ComboboxInput] = None
|
||||||
takeoff_airport: Optional[ComboboxInput] = None
|
takeoff_airport: Optional[ComboboxInput] = None
|
||||||
@@ -152,9 +152,9 @@ class CreateFlightInput(BaseGraphqlInputType):
|
|||||||
class TrackItemInput:
|
class TrackItemInput:
|
||||||
point_of_interest: Optional[ComboboxInput] = None
|
point_of_interest: Optional[ComboboxInput] = None
|
||||||
airport: Optional[ComboboxInput] = None
|
airport: Optional[ComboboxInput] = None
|
||||||
landing_duration: Optional[int] = None
|
landing_duration: int | None = None
|
||||||
gps_latitude: Optional[float] = None
|
gps_latitude: float | None = None
|
||||||
gps_longitude: Optional[float] = None
|
gps_longitude: float | None = None
|
||||||
|
|
||||||
|
|
||||||
@strawberry_sqlalchemy_input(models.Aircraft, exclude_fields=['id', 'photo_filename'])
|
@strawberry_sqlalchemy_input(models.Aircraft, exclude_fields=['id', 'photo_filename'])
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import math
|
|||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
import strawberry
|
import strawberry
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.endpoints import authenticated_user_only
|
# from decorators.endpoints import authenticated_user_only
|
||||||
from utils.gps import get_bearing, get_distance
|
from utils.gps import get_bearing, get_distance
|
||||||
from external.gpx_parser import GPXParser
|
from external.gpx_parser import GPXParser
|
||||||
from graphql_schema.dataloaders.flight_duration import flight_duration_dataloader
|
from graphql_schema.dataloaders.flight_duration import flight_duration_dataloader
|
||||||
@@ -17,7 +17,7 @@ from graphql_schema.dataloaders.multi_models import (
|
|||||||
)
|
)
|
||||||
from graphql_schema.dataloaders.single_model import (
|
from graphql_schema.dataloaders.single_model import (
|
||||||
poi_dataloader, poi_type_dataloader, event_dataloader, aircraft_dataloader, airport_dataloader,
|
poi_dataloader, poi_type_dataloader, event_dataloader, aircraft_dataloader, airport_dataloader,
|
||||||
airport_weather_info_loader, organizations_dataloader, flight_dataloader, photo_adjustment_dataloader,
|
weather_info_loader, organizations_dataloader, flight_dataloader, photo_adjustment_dataloader,
|
||||||
photo_dataloader, user_dataloader, track_dataloader
|
photo_dataloader, user_dataloader, track_dataloader
|
||||||
)
|
)
|
||||||
from graphql_schema.permissions import IsAuthenticated
|
from graphql_schema.permissions import IsAuthenticated
|
||||||
@@ -141,11 +141,11 @@ class Flight:
|
|||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
@authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
|
# @authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
|
||||||
async def load_copilots(root):
|
async def load_copilots(root):
|
||||||
return await flight_copilots_dataloader.load(root.id)
|
return await flight_copilots_dataloader.load(root.id)
|
||||||
|
|
||||||
@authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
|
# @authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
|
||||||
async def load_event(root):
|
async def load_event(root):
|
||||||
return await event_dataloader.load(root.event_id)
|
return await event_dataloader.load(root.event_id)
|
||||||
|
|
||||||
@@ -162,18 +162,18 @@ class Flight:
|
|||||||
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))
|
||||||
turn_points: List[FlightTurnPoint] = strawberry.field(resolver=lambda root: flight_turn_points_dataloader.load(root.id))
|
turn_points: List[FlightTurnPoint] = strawberry.field(resolver=lambda root: flight_turn_points_dataloader.load(root.id))
|
||||||
takeoff_weather_info: Optional[WeatherInfo] = strawberry.field(
|
takeoff_weather_info: Optional[WeatherInfo] = strawberry.field(
|
||||||
resolver=lambda root: airport_weather_info_loader.load(root.takeoff_weather_info_id)
|
resolver=lambda root: weather_info_loader.load(root.takeoff_weather_info_id)
|
||||||
)
|
)
|
||||||
landing_weather_info: Optional[WeatherInfo] = strawberry.field(
|
landing_weather_info: Optional[WeatherInfo] = strawberry.field(
|
||||||
resolver=lambda root: airport_weather_info_loader.load(root.landing_weather_info_id)
|
resolver=lambda root: weather_info_loader.load(root.landing_weather_info_id)
|
||||||
)
|
)
|
||||||
photos: List[Photo] = strawberry.field(resolver=lambda root: photos_dataloader.load(root.id))
|
photos: List[Photo] = strawberry.field(resolver=lambda root: photos_dataloader.load(root.id))
|
||||||
track: Optional[Track] = strawberry.field(resolver=lambda root: track_dataloader.load(root.track_id))
|
track: Optional[Track] = strawberry.field(resolver=lambda root: track_dataloader.load(root.track_id))
|
||||||
duration_min_calculated: int = strawberry.field(resolver=lambda root: flight_duration_dataloader.load(root.id))
|
duration_min_calculated: int = strawberry.field(resolver=lambda root: flight_duration_dataloader.load(root.id))
|
||||||
social_image_url: Optional[str] = strawberry.field(
|
social_image_url: str | None = strawberry.field(
|
||||||
resolver=lambda root: get_public_url(f'photos/{root.id}/title_photo.jpg')
|
resolver=lambda root: get_public_url(f'photos/{root.id}/title_photo.jpg')
|
||||||
)
|
)
|
||||||
debriefing_notes: Optional[str] = strawberry.field(
|
debriefing_notes: str | None = strawberry.field(
|
||||||
permission_classes=[IsAuthenticated],
|
permission_classes=[IsAuthenticated],
|
||||||
resolver=lambda root: root.debriefing_notes
|
resolver=lambda root: root.debriefing_notes
|
||||||
)
|
)
|
||||||
@@ -181,24 +181,26 @@ class Flight:
|
|||||||
|
|
||||||
@strawberry_sqlalchemy_type(model=models.FlightPlanMarker)
|
@strawberry_sqlalchemy_type(model=models.FlightPlanMarker)
|
||||||
class FlightPlanMarker:
|
class FlightPlanMarker:
|
||||||
pass
|
weather_info: Optional[WeatherInfo] = strawberry.field(
|
||||||
|
resolver=lambda root: weather_info_loader.load(root.weather_info_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class FlightPlanNavlog:
|
class FlightPlanTrack:
|
||||||
bearing: Optional[int]
|
bearing: int | None
|
||||||
distance: Optional[float]
|
distance: float | None
|
||||||
from_: FlightPlanMarker = strawberry.field(name="from")
|
from_: FlightPlanMarker = strawberry.field(name="from")
|
||||||
to: Optional[FlightPlanMarker]
|
to: Optional[FlightPlanMarker]
|
||||||
|
|
||||||
|
|
||||||
@strawberry_sqlalchemy_type(model=models.FlightPlan)
|
@strawberry_sqlalchemy_type(model=models.FlightPlan)
|
||||||
class FlightPlan:
|
class FlightPlan:
|
||||||
async def resolve_navlog(self):
|
async def resolve_track(self):
|
||||||
markers = await self.markers()
|
markers = await self.markers()
|
||||||
|
|
||||||
if len(markers) == 1:
|
if len(markers) == 1:
|
||||||
return [FlightPlanNavlog(bearing=None, distance=None, from_=markers[0], to=None)]
|
return [FlightPlanTrack(bearing=None, distance=None, from_=markers[0], to=None)]
|
||||||
|
|
||||||
navigation = []
|
navigation = []
|
||||||
for i in range(len(markers) - 1):
|
for i in range(len(markers) - 1):
|
||||||
@@ -208,7 +210,7 @@ class FlightPlan:
|
|||||||
point_from = (from_.gps_latitude, from_.gps_longitude)
|
point_from = (from_.gps_latitude, from_.gps_longitude)
|
||||||
point_to = (to.gps_latitude, to.gps_longitude)
|
point_to = (to.gps_latitude, to.gps_longitude)
|
||||||
|
|
||||||
navigation.append(FlightPlanNavlog(
|
navigation.append(FlightPlanTrack(
|
||||||
bearing=round(math.degrees(get_bearing(point_from, point_to)) % 360),
|
bearing=round(math.degrees(get_bearing(point_from, point_to)) % 360),
|
||||||
distance=round(get_distance(point_from, point_to), 1),
|
distance=round(get_distance(point_from, point_to), 1),
|
||||||
from_=from_,
|
from_=from_,
|
||||||
@@ -216,15 +218,16 @@ class FlightPlan:
|
|||||||
)
|
)
|
||||||
return navigation
|
return navigation
|
||||||
|
|
||||||
@authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
|
# @authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
|
||||||
async def load_copilots(root):
|
async def load_copilots(root):
|
||||||
return await flight_plan_copilots_dataloader.load(root.id)
|
return await flight_plan_copilots_dataloader.load(root.id)
|
||||||
|
|
||||||
markers: list[FlightPlanMarker] = strawberry.field(
|
markers: list[FlightPlanMarker] = strawberry.field(
|
||||||
resolver=lambda root: flight_plan_markers_dataloader.load(root.id)
|
resolver=lambda root: flight_plan_markers_dataloader.load(root.id)
|
||||||
)
|
)
|
||||||
navlog: Optional[list[FlightPlanNavlog]] = strawberry.field(resolver=resolve_navlog)
|
track: Optional[list[FlightPlanTrack]] = strawberry.field(
|
||||||
track: Optional[Track] = strawberry.field(resolver=lambda root: track_dataloader.load(root.track_id))
|
resolver=resolve_track
|
||||||
|
)
|
||||||
copilots: List[Copilot] = strawberry.field(resolver=load_copilots)
|
copilots: List[Copilot] = strawberry.field(resolver=load_copilots)
|
||||||
aircraft: Optional[Aircraft] = strawberry.field(resolver=lambda root: aircraft_dataloader.load(root.aircraft_id))
|
aircraft: Optional[Aircraft] = strawberry.field(resolver=lambda root: aircraft_dataloader.load(root.aircraft_id))
|
||||||
|
|
||||||
@@ -244,7 +247,7 @@ class Copilot:
|
|||||||
flights: List[Flight] = strawberry.field(resolver=resolve_flights)
|
flights: List[Flight] = strawberry.field(resolver=resolve_flights)
|
||||||
photos: List[Photo] = strawberry.field(resolver=lambda root: photo_copilots_dataloader.load(root.id))
|
photos: List[Photo] = strawberry.field(resolver=lambda root: photo_copilots_dataloader.load(root.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))
|
||||||
token: Optional[str] = strawberry.field(
|
token: str | None = strawberry.field(
|
||||||
permission_classes=[IsAuthenticated],
|
permission_classes=[IsAuthenticated],
|
||||||
resolver=resolve_copilot_token
|
resolver=resolve_copilot_token
|
||||||
)
|
)
|
||||||
@@ -270,9 +273,9 @@ class Organization:
|
|||||||
|
|
||||||
@strawberry_sqlalchemy_type(models.User, exclude_fields=['password_hashed'])
|
@strawberry_sqlalchemy_type(models.User, exclude_fields=['password_hashed'])
|
||||||
class User:
|
class User:
|
||||||
avatar_image_url: Optional[str] = strawberry.field(resolver=lambda root: get_avatar_url(root))
|
avatar_image_url: str | None = strawberry.field(resolver=lambda root: get_avatar_url(root))
|
||||||
title_image_url: str = strawberry.field(resolver=lambda root: get_title_image_url(root))
|
title_image_url: str = strawberry.field(resolver=lambda root: get_title_image_url(root))
|
||||||
social_image_url: Optional[str] = strawberry.field(
|
social_image_url: str | None = strawberry.field(
|
||||||
resolver=lambda root: get_public_url(f'profile/{root.id}/title_photo.jpg')
|
resolver=lambda root: get_public_url(f'profile/{root.id}/title_photo.jpg')
|
||||||
)
|
)
|
||||||
organizations: List[Organization] = strawberry.field(
|
organizations: List[Organization] = strawberry.field(
|
||||||
@@ -282,7 +285,7 @@ class User:
|
|||||||
|
|
||||||
@strawberry_sqlalchemy_type(models.Event)
|
@strawberry_sqlalchemy_type(models.Event)
|
||||||
class Event:
|
class Event:
|
||||||
async def load_flights(root, info, only_public: Optional[bool] = False):
|
async def load_flights(root, info, only_public: bool | None = False):
|
||||||
is_user_logged_in = bool(info.context.user_id)
|
is_user_logged_in = bool(info.context.user_id)
|
||||||
if not is_user_logged_in or only_public:
|
if not is_user_logged_in or only_public:
|
||||||
dataloader = public_flights_by_event_dataloader
|
dataloader = public_flights_by_event_dataloader
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import strawberry
|
|||||||
from graphql import GraphQLError
|
from graphql import GraphQLError
|
||||||
from passlib.hash import bcrypt
|
from passlib.hash import bcrypt
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
from strawberry import Info
|
||||||
from strawberry.file_uploads import Upload
|
from strawberry.file_uploads import Upload
|
||||||
from background_jobs.photo import resize_photo
|
from background_jobs.photo import resize_photo
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.endpoints import authenticated_user_only
|
|
||||||
from decorators.error_logging import error_logging
|
|
||||||
from database.transaction import get_session
|
from database.transaction import get_session
|
||||||
from graphql_schema.entities.types.types import User
|
from graphql_schema.entities.types.types import User
|
||||||
|
from graphql_schema.extensions.field.auth import AuthenticatedOnly
|
||||||
from utils.file import delete_file
|
from utils.file import delete_file
|
||||||
from utils.file import handle_file_upload
|
from utils.file import handle_file_upload
|
||||||
|
|
||||||
@@ -17,8 +17,7 @@ from utils.file import handle_file_upload
|
|||||||
@strawberry.type
|
@strawberry.type
|
||||||
class UserQueries:
|
class UserQueries:
|
||||||
@strawberry.field()
|
@strawberry.field()
|
||||||
@error_logging
|
async def user(root, info: Info, username: str) -> User:
|
||||||
async def user(root, info, username: str) -> User:
|
|
||||||
if len(username) == 0:
|
if len(username) == 0:
|
||||||
raise GraphQLError("Username not set!")
|
raise GraphQLError("Username not set!")
|
||||||
|
|
||||||
@@ -28,10 +27,8 @@ class UserQueries:
|
|||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||||
@authenticated_user_only()
|
async def logged_user(root, info: Info) -> User:
|
||||||
@error_logging
|
|
||||||
async def logged_user(root, info) -> User:
|
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
user_model = (await db.scalars(
|
user_model = (await db.scalars(
|
||||||
select(models.User).filter_by(id=info.context.user_id)
|
select(models.User).filter_by(id=info.context.user_id)
|
||||||
@@ -44,17 +41,16 @@ class UserQueries:
|
|||||||
class EditUserMutation:
|
class EditUserMutation:
|
||||||
@strawberry.input
|
@strawberry.input
|
||||||
class EditUserInput:
|
class EditUserInput:
|
||||||
name: Optional[str] = None
|
name: str | None = None
|
||||||
description: Optional[str] = None
|
description: str | None = None
|
||||||
public_username: Optional[str] = None
|
public_username: str | None = None
|
||||||
old_password: Optional[str] = None
|
old_password: str | None = None
|
||||||
new_password: Optional[str] = None
|
new_password: str | None = None
|
||||||
avatar_image: Optional[Upload] = None
|
avatar_image: Optional[Upload] = None
|
||||||
title_image: Optional[Upload] = None
|
title_image: Optional[Upload] = None
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@authenticated_user_only()
|
async def edit_logged_user(root, info: Info, input: EditUserInput) -> User:
|
||||||
async def edit_logged_user(root, info, input: EditUserInput) -> User:
|
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
user = (await db.scalars(
|
user = (await db.scalars(
|
||||||
select(models.User).filter_by(id=info.context.user_id)
|
select(models.User).filter_by(id=info.context.user_id)
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from typing import Any
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||||
|
from strawberry import Info
|
||||||
|
from strawberry.extensions import FieldExtension
|
||||||
|
from strawberry.extensions.field_extension import AsyncExtensionResolver
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticatedOnly(FieldExtension):
|
||||||
|
async def resolve_async(self, next_: AsyncExtensionResolver, source: Any, info: Info, **kwargs: Any) -> Any:
|
||||||
|
if not info.context.user_id:
|
||||||
|
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
|
||||||
|
|
||||||
|
return await next_(source, info, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class AllowPublicAccess(FieldExtension):
|
||||||
|
async def resolve_async(self, next_: AsyncExtensionResolver, source: Any, info: Info, **kwargs: Any) -> Any:
|
||||||
|
user_id = info.context.user_id
|
||||||
|
public = kwargs.get('public')
|
||||||
|
|
||||||
|
if not user_id and not public:
|
||||||
|
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
|
||||||
|
|
||||||
|
return await next_(source, info, **kwargs)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
from typing import Callable, Any, Type
|
||||||
|
import strawberry
|
||||||
|
from strawberry.annotation import StrawberryAnnotation
|
||||||
|
from strawberry.extensions import FieldExtension
|
||||||
|
from strawberry.types.arguments import StrawberryArgument
|
||||||
|
from strawberry.types.field import StrawberryField
|
||||||
|
from graphql_schema.entities.helpers.pagination import get_pagination_window, PaginationWindow
|
||||||
|
|
||||||
|
class OffsetPagination[Item](FieldExtension):
|
||||||
|
|
||||||
|
def __init__(self, item_type: Type[Item]):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.item_type = item_type
|
||||||
|
|
||||||
|
def apply(self, field: StrawberryField) -> StrawberryField:
|
||||||
|
offset_arg = StrawberryArgument(
|
||||||
|
python_name="offset",
|
||||||
|
graphql_name="offset",
|
||||||
|
type_annotation=StrawberryAnnotation(annotation=int | None),
|
||||||
|
default=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
limit_arg = StrawberryArgument(
|
||||||
|
python_name="limit",
|
||||||
|
graphql_name="limit",
|
||||||
|
type_annotation=StrawberryAnnotation(annotation=int),
|
||||||
|
default=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
field.arguments.append(offset_arg)
|
||||||
|
field.arguments.append(limit_arg)
|
||||||
|
|
||||||
|
return field
|
||||||
|
|
||||||
|
async def resolve_async(
|
||||||
|
self, next_: Callable[..., Any], source: Any, info: strawberry.Info,
|
||||||
|
limit: int, offset: int = 0,
|
||||||
|
**kwargs
|
||||||
|
) -> PaginationWindow[Item]:
|
||||||
|
query = await next_(source, info, **kwargs)
|
||||||
|
|
||||||
|
return await get_pagination_window(
|
||||||
|
query=query,
|
||||||
|
item_type=self.item_type,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from typing import Callable, Any
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from graphql import GraphQLResolveInfo, GraphQLError
|
||||||
|
from sqlalchemy.exc import NoResultFound
|
||||||
|
from strawberry.extensions import SchemaExtension
|
||||||
|
from strawberry.utils.await_maybe import AwaitableOrValue
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorLogging(SchemaExtension):
|
||||||
|
async def resolve_async(self, _next: Callable, root: Any, info: GraphQLResolveInfo, *args: str, **kwargs: Any) -> AwaitableOrValue[object]:
|
||||||
|
try:
|
||||||
|
return await _next(root, info, *args, **kwargs)
|
||||||
|
except NoResultFound as e:
|
||||||
|
raise GraphQLError("Not found", original_error=e)
|
||||||
|
except HTTPException as e:
|
||||||
|
if e.status_code == 401:
|
||||||
|
raise GraphQLError("Not authorized", original_error=e)
|
||||||
|
except Exception as e:
|
||||||
|
raise GraphQLError(f"Unknown error: {e}", original_error=e)
|
||||||
@@ -1,45 +1,30 @@
|
|||||||
import dataclasses
|
from datetime import datetime
|
||||||
from typing import Set
|
|
||||||
|
|
||||||
import strawberry
|
import strawberry
|
||||||
from fastapi_jwt import JwtAuthorizationCredentials
|
from strawberry.extensions import SchemaExtension, ValidationCache, ParserCache
|
||||||
from fastapi_jwt.jwt import JwtAccessBearerCookie
|
from graphql_schema.extensions.schema.error_logging import ErrorLogging
|
||||||
from starlette.background import BackgroundTasks
|
from logger import log
|
||||||
from strawberry.extensions import SchemaExtension
|
|
||||||
from strawberry.fastapi import BaseContext
|
|
||||||
from .mutation import Mutation
|
from .mutation import Mutation
|
||||||
from .query import Query
|
from .query import Query
|
||||||
|
|
||||||
|
|
||||||
# Toto se da kdyztak pouzit jako extension do Schema
|
|
||||||
# class SQLAlchemySession(Extension):
|
|
||||||
# def on_request_start(self):
|
|
||||||
# session = async_session()
|
|
||||||
# print(self.execution_context.context)
|
|
||||||
# self.execution_context.context["db"] = session
|
|
||||||
#
|
|
||||||
# async def on_request_end(self):
|
|
||||||
# await self.execution_context.context["db"].close()
|
|
||||||
|
|
||||||
class LoggingExtension(SchemaExtension):
|
class LoggingExtension(SchemaExtension):
|
||||||
def on_request_start(self):
|
def on_request_end(self):
|
||||||
print("request start")
|
# nastavuje se pri vytvareni GraphQL contextu
|
||||||
|
request_start = self.execution_context.context.request_start_time
|
||||||
|
request_end = datetime.now()
|
||||||
|
|
||||||
async def on_request_end(self):
|
request_time_ms = round((request_end - request_start).microseconds / 1000)
|
||||||
print("request end")
|
op_name = self.execution_context.operation_name
|
||||||
|
log.info(f"Operation {op_name} took {request_time_ms}ms")
|
||||||
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class GraphQLContext(BaseContext):
|
|
||||||
user_id: int
|
|
||||||
organization_ids: Set[int]
|
|
||||||
jwt_auth_credentials: JwtAuthorizationCredentials
|
|
||||||
jwt: JwtAccessBearerCookie
|
|
||||||
background_tasks: BackgroundTasks
|
|
||||||
|
|
||||||
|
|
||||||
schema = strawberry.Schema(
|
schema = strawberry.Schema(
|
||||||
query=Query,
|
query=Query,
|
||||||
mutation=Mutation,
|
mutation=Mutation,
|
||||||
extensions=[LoggingExtension]
|
extensions=[
|
||||||
|
LoggingExtension(),
|
||||||
|
ErrorLogging(),
|
||||||
|
ParserCache(),
|
||||||
|
ValidationCache()
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ def get_annotations_for_scalars(model: BaseModel, exclude_fields=None, force_opt
|
|||||||
return annotations_
|
return annotations_
|
||||||
|
|
||||||
|
|
||||||
def strawberry_sqlalchemy_type(model, exclude_fields: Optional[typing.Union[List, typing.Tuple]] = None):
|
def strawberry_sqlalchemy_type(model: BaseModel, exclude_fields: list | tuple | None = None):
|
||||||
if exclude_fields is None:
|
if exclude_fields is None:
|
||||||
exclude_fields = []
|
exclude_fields = []
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ def strawberry_sqlalchemy_input(
|
|||||||
model,
|
model,
|
||||||
exclude_fields: Optional[typing.Union[List, typing.Tuple]] = None,
|
exclude_fields: Optional[typing.Union[List, typing.Tuple]] = None,
|
||||||
all_optional: bool = False
|
all_optional: bool = False
|
||||||
) -> typing.Callable[[...], strawberry.object_type]:
|
) -> typing.Callable[[...], strawberry.type]:
|
||||||
if exclude_fields is None:
|
if exclude_fields is None:
|
||||||
exclude_fields = []
|
exclude_fields = []
|
||||||
|
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
from fastapi_jwt import JwtAccessBearerCookie, JwtRefreshBearerCookie
|
||||||
|
from config import APP_SECRET_KEY, APP_DEBUG, REFRESH_TOKEN_VALIDITY_DAYS
|
||||||
|
|
||||||
|
access_security = JwtAccessBearerCookie(
|
||||||
|
secret_key=APP_SECRET_KEY,
|
||||||
|
auto_error=False,
|
||||||
|
access_expires_delta=timedelta(days=1) if APP_DEBUG else timedelta(minutes=20)
|
||||||
|
)
|
||||||
|
refresh_security = JwtRefreshBearerCookie(
|
||||||
|
secret_key=APP_SECRET_KEY,
|
||||||
|
auto_error=True,
|
||||||
|
refresh_expires_delta=timedelta(days=REFRESH_TOKEN_VALIDITY_DAYS),
|
||||||
|
)
|
||||||
+14
-10
@@ -1,22 +1,26 @@
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
from colorlog import ColoredFormatter
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(logging.DEBUG)
|
log.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
# Create handlers for logging to the standard output and a file
|
|
||||||
stdoutHandler = logging.StreamHandler(stream=sys.stdout)
|
stdoutHandler = logging.StreamHandler(stream=sys.stdout)
|
||||||
|
|
||||||
# Set the log levels on the handlers
|
|
||||||
stdoutHandler.setLevel(logging.DEBUG)
|
stdoutHandler.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
# Create a log format using Log Record attributes
|
fmt = ColoredFormatter(
|
||||||
fmt = logging.Formatter(
|
"%(log_color)s[%(levelname)s]%(reset)s %(blue)s%(message)s%(reset)s %(light_black)s[%(filename)s:%(lineno)d]]%(reset)s",
|
||||||
"%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d >>> %(message)s"
|
datefmt=None,
|
||||||
|
reset=True,
|
||||||
|
log_colors={
|
||||||
|
'DEBUG': 'cyan',
|
||||||
|
'INFO': 'green',
|
||||||
|
'WARNING': 'yellow',
|
||||||
|
'ERROR': 'red',
|
||||||
|
'CRITICAL': 'red,bg_white',
|
||||||
|
},
|
||||||
|
secondary_log_colors={},
|
||||||
|
style='%'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set the log format on each handler
|
|
||||||
stdoutHandler.setFormatter(fmt)
|
stdoutHandler.setFormatter(fmt)
|
||||||
|
|
||||||
# Add each handler to the Logger object
|
|
||||||
log.addHandler(stdoutHandler)
|
log.addHandler(stdoutHandler)
|
||||||
|
|||||||
+28
-105
@@ -1,22 +1,18 @@
|
|||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from datetime import timedelta
|
from fastapi import FastAPI, HTTPException, APIRouter
|
||||||
from typing import Optional
|
|
||||||
from fastapi import FastAPI, APIRouter, Security, HTTPException
|
|
||||||
from fastapi_jwt import JwtAuthorizationCredentials, JwtAccessBearerCookie, JwtRefreshBearerCookie
|
|
||||||
from graphql import GraphQLError
|
from graphql import GraphQLError
|
||||||
|
from sentry_sdk.integrations.fastapi import FastApiIntegration
|
||||||
|
from sentry_sdk.integrations.starlette import StarletteIntegration
|
||||||
|
from sentry_sdk.integrations.strawberry import StrawberryIntegration
|
||||||
from sqlalchemy.exc import NoResultFound
|
from sqlalchemy.exc import NoResultFound
|
||||||
from starlette.background import BackgroundTasks
|
from starlette.background import BackgroundTasks
|
||||||
from starlette.middleware.cors import CORSMiddleware
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
from starlette.responses import Response, JSONResponse
|
from starlette.responses import Response, JSONResponse
|
||||||
from starlette.staticfiles import StaticFiles
|
from starlette.staticfiles import StaticFiles
|
||||||
from config import APP_SECRET_KEY, ALLOW_CORS_ORIGINS, SENTRY_DSN, REFRESH_TOKEN_VALIDITY_DAYS, APP_DEBUG
|
from config import ALLOW_CORS_ORIGINS, SENTRY_DSN
|
||||||
from endpoints.contact import ContactEndpoint, ContactInput
|
from endpoints.contact import ContactEndpoint, ContactInput
|
||||||
from endpoints.forgotten_password import ForgottenPasswordRequest, ForgottenPasswordEndpoint, ChangeForgottenPassword
|
|
||||||
from endpoints.graphql import setup_graphql_endpoint
|
|
||||||
from endpoints.login import LoginEndpoint, LoginInput, RefreshEndpoint, LogoutEndpoint
|
|
||||||
from endpoints.photo_editor_preview import PhotoEditorEndpoint
|
|
||||||
from endpoints.registration import RegistrationInput, RegistrationEndpoint
|
|
||||||
from endpoints.sitemap import SitemapEndpoint
|
from endpoints.sitemap import SitemapEndpoint
|
||||||
|
from routers import forgotten_password, auth, photo_preview, graphql
|
||||||
|
|
||||||
|
|
||||||
class StaticFilesCache(StaticFiles):
|
class StaticFilesCache(StaticFiles):
|
||||||
@@ -31,24 +27,26 @@ class StaticFilesCache(StaticFiles):
|
|||||||
|
|
||||||
|
|
||||||
class App:
|
class App:
|
||||||
api_router = APIRouter(dependencies=[])
|
|
||||||
access_security = JwtAccessBearerCookie(
|
|
||||||
secret_key=APP_SECRET_KEY,
|
|
||||||
auto_error=False,
|
|
||||||
access_expires_delta=timedelta(days=1) if APP_DEBUG else timedelta(minutes=20)
|
|
||||||
)
|
|
||||||
refresh_security = JwtRefreshBearerCookie(
|
|
||||||
secret_key=APP_SECRET_KEY,
|
|
||||||
auto_error=True,
|
|
||||||
refresh_expires_delta=timedelta(days=REFRESH_TOKEN_VALIDITY_DAYS),
|
|
||||||
)
|
|
||||||
|
|
||||||
def create_app(self):
|
def create_app(self):
|
||||||
if SENTRY_DSN:
|
if SENTRY_DSN:
|
||||||
|
fast_api_params = dict(
|
||||||
|
transaction_style="endpoint",
|
||||||
|
failed_request_status_codes={403, *range(500, 599)},
|
||||||
|
http_methods_to_capture=("GET", "POST"),
|
||||||
|
)
|
||||||
|
|
||||||
sentry_sdk.init(
|
sentry_sdk.init(
|
||||||
dsn=SENTRY_DSN,
|
dsn=SENTRY_DSN,
|
||||||
enable_tracing=True,
|
enable_tracing=True,
|
||||||
ignore_errors=[GraphQLError, HTTPException]
|
traces_sample_rate=0.5,
|
||||||
|
profiles_sample_rate=0.5,
|
||||||
|
ignore_errors=[GraphQLError, HTTPException],
|
||||||
|
integrations=[
|
||||||
|
StrawberryIntegration(async_execution=True),
|
||||||
|
StarletteIntegration(**fast_api_params),
|
||||||
|
FastApiIntegration(**fast_api_params),
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
@@ -100,94 +98,19 @@ class App:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def setup_routes(self, app: FastAPI):
|
def setup_routes(self, app: FastAPI):
|
||||||
@self.api_router.post("/registration", status_code=201)
|
api_router = APIRouter()
|
||||||
async def registration(user: RegistrationInput, background_tasks: BackgroundTasks):
|
|
||||||
return await RegistrationEndpoint().on_post(user, background_tasks)
|
|
||||||
|
|
||||||
@self.api_router.post("/login")
|
@api_router.post("/contact", summary="Send email from contact form")
|
||||||
async def login(resp: Response, user: LoginInput):
|
|
||||||
return await LoginEndpoint(
|
|
||||||
access_token=self.access_security,
|
|
||||||
refresh_token=self.refresh_security
|
|
||||||
).on_post(user, resp)
|
|
||||||
|
|
||||||
@self.api_router.post("/refresh", summary="Refresh access token")
|
|
||||||
async def refresh(
|
|
||||||
resp: Response,
|
|
||||||
credentials: JwtAuthorizationCredentials = Security(self.refresh_security)
|
|
||||||
):
|
|
||||||
return await RefreshEndpoint(
|
|
||||||
access_token=self.access_security,
|
|
||||||
refresh_token=self.refresh_security
|
|
||||||
).on_post(resp, credentials)
|
|
||||||
|
|
||||||
@self.api_router.post("/logout")
|
|
||||||
async def logout(resp: Response):
|
|
||||||
return await LogoutEndpoint(
|
|
||||||
access_token=self.access_security,
|
|
||||||
refresh_token=self.refresh_security
|
|
||||||
).on_post(resp)
|
|
||||||
|
|
||||||
@self.api_router.get(
|
|
||||||
"/forgotten-password/token/{token}",
|
|
||||||
summary="Info about token used for resetting password"
|
|
||||||
)
|
|
||||||
async def token_info(token: str):
|
|
||||||
return await ForgottenPasswordEndpoint().token_info(token)
|
|
||||||
|
|
||||||
@self.api_router.post(
|
|
||||||
"/forgotten-password/request",
|
|
||||||
summary="Request password change, e-mail will be sent to validate your request."
|
|
||||||
)
|
|
||||||
async def request_password_change(input: ForgottenPasswordRequest, background_tasks: BackgroundTasks):
|
|
||||||
return await ForgottenPasswordEndpoint().request(input, background_tasks)
|
|
||||||
|
|
||||||
@self.api_router.post(
|
|
||||||
"/forgotten-password/reset",
|
|
||||||
summary="Set new password after successfull token validation"
|
|
||||||
)
|
|
||||||
async def reset_password(input: ChangeForgottenPassword):
|
|
||||||
return await ForgottenPasswordEndpoint().change_password(input)
|
|
||||||
|
|
||||||
@self.api_router.post("/contact", summary="Send email from contact form")
|
|
||||||
async def contact_form_message(input: ContactInput, background_tasks: BackgroundTasks):
|
async def contact_form_message(input: ContactInput, background_tasks: BackgroundTasks):
|
||||||
return await ContactEndpoint().on_post(input, background_tasks)
|
return await ContactEndpoint().on_post(input, background_tasks)
|
||||||
|
|
||||||
@self.api_router.get("/sitemap.xml")
|
@api_router.get("/sitemap.xml")
|
||||||
async def sitemap():
|
async def sitemap():
|
||||||
return await SitemapEndpoint().on_get()
|
return await SitemapEndpoint().on_get()
|
||||||
|
|
||||||
@self.api_router.get("/photo/editor-preview/{photo_id}", summary="Photo editor preview")
|
|
||||||
async def photo_editor_preview(
|
|
||||||
photo_id: int,
|
|
||||||
brightness: Optional[float] = None,
|
|
||||||
contrast: Optional[float] = None,
|
|
||||||
saturation: Optional[float] = None,
|
|
||||||
sharpness: Optional[float] = None,
|
|
||||||
rotate: Optional[float] = None,
|
|
||||||
crop_left: Optional[float] = None,
|
|
||||||
crop_top: Optional[float] = None,
|
|
||||||
crop_width: Optional[float] = None,
|
|
||||||
crop_height: Optional[float] = None,
|
|
||||||
):
|
|
||||||
return await PhotoEditorEndpoint(
|
|
||||||
access_token=self.access_security,
|
|
||||||
refresh_token=self.refresh_security
|
|
||||||
).show_preview(
|
|
||||||
photo_id=photo_id,
|
|
||||||
logged_user_id=0,
|
|
||||||
saturation=saturation,
|
|
||||||
brightness=brightness,
|
|
||||||
contrast=contrast,
|
|
||||||
sharpness=sharpness,
|
|
||||||
crop_top=crop_top,
|
|
||||||
crop_left=crop_left,
|
|
||||||
crop_height=crop_height,
|
|
||||||
crop_width=crop_width,
|
|
||||||
rotate=rotate,
|
|
||||||
)
|
|
||||||
|
|
||||||
setup_graphql_endpoint(app, self.access_security)
|
|
||||||
|
|
||||||
# musi byt na konci
|
# musi byt na konci
|
||||||
app.include_router(self.api_router)
|
app.include_router(auth.router)
|
||||||
|
app.include_router(forgotten_password.router)
|
||||||
|
app.include_router(photo_preview.router)
|
||||||
|
app.include_router(graphql.router)
|
||||||
|
app.include_router(api_router)
|
||||||
|
|||||||
+1
-6
@@ -1,20 +1,15 @@
|
|||||||
import os
|
import os
|
||||||
from typing import Optional
|
|
||||||
from config import API_URL
|
from config import API_URL
|
||||||
from logger import log
|
from logger import log
|
||||||
|
|
||||||
PHOTO_BASE_PATH = ""
|
|
||||||
AIRCRAFT_BASE_PATH = ""
|
|
||||||
FLIGHT_BASE_PATH = ""
|
|
||||||
FLIGHT_GPX_TRACK_PATH = "/app/uploads/tracks"
|
FLIGHT_GPX_TRACK_PATH = "/app/uploads/tracks"
|
||||||
AIRCRAFT_UPLOAD_DEST_PATH = "/app/uploads/aircrafts/"
|
|
||||||
|
|
||||||
|
|
||||||
def get_photo_basepath(flight_id: int) -> str:
|
def get_photo_basepath(flight_id: int) -> str:
|
||||||
return f"/app/uploads/photos/{flight_id}"
|
return f"/app/uploads/photos/{flight_id}"
|
||||||
|
|
||||||
|
|
||||||
def get_public_url(filename: Optional[str]) -> str:
|
def get_public_url(filename: str | None) -> str:
|
||||||
return f"{API_URL}/uploads/{filename}" if filename else None
|
return f"{API_URL}/uploads/{filename}" if filename else None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
from fastapi import BackgroundTasks, Security, APIRouter
|
||||||
|
from fastapi_jwt import JwtAuthorizationCredentials
|
||||||
|
from starlette.responses import Response
|
||||||
|
from endpoints.login import LoginInput, LoginEndpoint, RefreshEndpoint, LogoutEndpoint
|
||||||
|
from endpoints.registration import RegistrationInput, RegistrationEndpoint
|
||||||
|
from jwt import access_security, refresh_security
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/registration", status_code=201)
|
||||||
|
async def registration(user: RegistrationInput, background_tasks: BackgroundTasks):
|
||||||
|
return await RegistrationEndpoint().on_post(user, background_tasks)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login")
|
||||||
|
async def login(resp: Response, user: LoginInput):
|
||||||
|
return await LoginEndpoint(
|
||||||
|
access_token=access_security,
|
||||||
|
refresh_token=refresh_security
|
||||||
|
).on_post(user, resp)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/refresh", summary="Refresh access token")
|
||||||
|
async def refresh(
|
||||||
|
resp: Response,
|
||||||
|
credentials: JwtAuthorizationCredentials = Security(refresh_security)
|
||||||
|
):
|
||||||
|
return await RefreshEndpoint(
|
||||||
|
access_token=access_security,
|
||||||
|
refresh_token=refresh_security
|
||||||
|
).on_post(resp, credentials)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/logout")
|
||||||
|
async def logout(resp: Response):
|
||||||
|
return await LogoutEndpoint(
|
||||||
|
access_token=access_security,
|
||||||
|
refresh_token=refresh_security
|
||||||
|
).on_post(resp)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from fastapi import BackgroundTasks, APIRouter
|
||||||
|
from endpoints.forgotten_password import ForgottenPasswordEndpoint, ForgottenPasswordRequest, ChangeForgottenPassword
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/forgotten-password/token/{token}",
|
||||||
|
summary="Info about token used for resetting password"
|
||||||
|
)
|
||||||
|
async def token_info(token: str):
|
||||||
|
return await ForgottenPasswordEndpoint().token_info(token)
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/forgotten-password/request",
|
||||||
|
summary="Request password change, e-mail will be sent to validate your request."
|
||||||
|
)
|
||||||
|
async def request_password_change(input: ForgottenPasswordRequest, background_tasks: BackgroundTasks):
|
||||||
|
return await ForgottenPasswordEndpoint().request(input, background_tasks)
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/forgotten-password/reset",
|
||||||
|
summary="Set new password after successfull token validation"
|
||||||
|
)
|
||||||
|
async def reset_password(input: ChangeForgottenPassword):
|
||||||
|
return await ForgottenPasswordEndpoint().change_password(input)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from starlette.responses import RedirectResponse
|
||||||
|
from strawberry.fastapi import GraphQLRouter
|
||||||
|
from config import GRAPHIQL, APP_DEBUG
|
||||||
|
from graphql_schema.context import setup_graphql_context
|
||||||
|
from graphql_schema.schema import schema
|
||||||
|
from jwt import access_security
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
if APP_DEBUG:
|
||||||
|
@router.get("/graphql/autologin")
|
||||||
|
async def autologin():
|
||||||
|
access_token = access_security.create_access_token(subject={"id": 1, "name": "Franta Vomacka"})
|
||||||
|
response = RedirectResponse(url="/graphql")
|
||||||
|
access_security.set_access_cookie(response, access_token, expires_delta=timedelta(days=14))
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
gql_router = GraphQLRouter(
|
||||||
|
schema,
|
||||||
|
graphiql=GRAPHIQL,
|
||||||
|
debug=APP_DEBUG,
|
||||||
|
context_getter=setup_graphql_context,
|
||||||
|
multipart_uploads_enabled=True,
|
||||||
|
prefix="/graphql"
|
||||||
|
)
|
||||||
|
|
||||||
|
router.include_router(gql_router, tags=["login"])
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
from endpoints.photo_editor_preview import PhotoEditorEndpoint
|
||||||
|
from jwt import access_security, refresh_security
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/photo/editor-preview/{photo_id}", summary="Photo editor preview")
|
||||||
|
async def photo_editor_preview(
|
||||||
|
photo_id: int,
|
||||||
|
brightness: float | None = None,
|
||||||
|
contrast: float | None = None,
|
||||||
|
saturation: float | None = None,
|
||||||
|
sharpness: float | None = None,
|
||||||
|
rotate: float | None = None,
|
||||||
|
crop_left: float | None = None,
|
||||||
|
crop_top: float | None = None,
|
||||||
|
crop_width: float | None = None,
|
||||||
|
crop_height: float | None = None,
|
||||||
|
):
|
||||||
|
return await PhotoEditorEndpoint(
|
||||||
|
access_token=access_security,
|
||||||
|
refresh_token=refresh_security
|
||||||
|
).show_preview(
|
||||||
|
photo_id=photo_id,
|
||||||
|
logged_user_id=0,
|
||||||
|
saturation=saturation,
|
||||||
|
brightness=brightness,
|
||||||
|
contrast=contrast,
|
||||||
|
sharpness=sharpness,
|
||||||
|
crop_top=crop_top,
|
||||||
|
crop_left=crop_left,
|
||||||
|
crop_height=crop_height,
|
||||||
|
crop_width=crop_width,
|
||||||
|
rotate=rotate,
|
||||||
|
)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from itertools import groupby
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
sys.path.insert(0, "/app/src")
|
||||||
|
from database import models
|
||||||
|
from database.transaction import get_session
|
||||||
|
|
||||||
|
|
||||||
|
def find_closest(needle: datetime, haystack, _best_difference: float = sys.maxsize):
|
||||||
|
if len(haystack) == 0:
|
||||||
|
return None
|
||||||
|
if len(haystack) == 1:
|
||||||
|
# nalezeno
|
||||||
|
return haystack[0]
|
||||||
|
|
||||||
|
index = len(haystack) / 2
|
||||||
|
diff = haystack[index].timestamp - needle
|
||||||
|
if diff < _best_difference:
|
||||||
|
_best_difference = diff
|
||||||
|
return find_closest(needle, haystack[:index], _best_difference)
|
||||||
|
else:
|
||||||
|
return find_closest(needle, haystack[index + 1:], _best_difference)
|
||||||
|
|
||||||
|
|
||||||
|
async def add_gps_to_photos():
|
||||||
|
async with get_session() as db:
|
||||||
|
photos = (await db.execute(
|
||||||
|
select(models.Photo, models.Photo.flight)
|
||||||
|
.join(models.Photo.flight)
|
||||||
|
.filter(models.Photo.gps_latitude.is_(None))
|
||||||
|
.filter(models.Photo.gps_longitude.is_(None))
|
||||||
|
)).all()
|
||||||
|
|
||||||
|
flight_ids = {photo.flight_id for photo, flight in photos}
|
||||||
|
|
||||||
|
tracks = (await db.execute(
|
||||||
|
select(models.Flight.track_id, models.Flight.id)
|
||||||
|
.select_from(models.Flight)
|
||||||
|
.join(models.Flight.track)
|
||||||
|
.filter(models.Flight.id.in_(flight_ids))
|
||||||
|
)).all()
|
||||||
|
|
||||||
|
track_id_to_flight_id = {track_id: flight_id for track_id, flight_id in tracks}
|
||||||
|
|
||||||
|
track_points_data = (await db.scalars(
|
||||||
|
select(models.TrackPoint)
|
||||||
|
.filter(models.TrackPoint.track_id.in_(track_id_to_flight_id.keys()))
|
||||||
|
.order_by(models.TrackPoint.timestamp)
|
||||||
|
)).all()
|
||||||
|
print(track_points_data)
|
||||||
|
grouped_points_by_track_id = groupby(track_points_data, key=lambda x: x.track_id)
|
||||||
|
|
||||||
|
for photo, flight in photos:
|
||||||
|
best_track_point = find_closest(photo.exposed_at, grouped_points_by_track_id[flight.track_id])
|
||||||
|
print(best_track_point)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
print(grouped_points_by_track_id)
|
||||||
|
# tracks_by_flight_id = {track.flight_id: track_points for track, track_points in tracks}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.run_until_complete(add_gps_to_photos())
|
||||||
@@ -1,22 +1,60 @@
|
|||||||
from sqlalchemy import select
|
import asyncio
|
||||||
from database import models
|
import sys
|
||||||
from database.transaction import get_session
|
from collections import defaultdict
|
||||||
|
from typing import Any
|
||||||
|
from fastapi import BackgroundTasks
|
||||||
|
from sqlalchemy import select, Result
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
sys.path.insert(0, "/app/src")
|
||||||
|
|
||||||
|
from utils.flight_plan_helpers import refresh_markers_weather_info # noqa
|
||||||
|
from database.models import FlightPlan # noqa
|
||||||
|
from database import models # noqa
|
||||||
|
from database.transaction import get_session # noqa
|
||||||
|
|
||||||
|
|
||||||
class FlighPlanWeather:
|
async def get_plans(db: AsyncSession) -> list[Any] | Result[tuple[FlightPlan, Any]]:
|
||||||
@staticmethod
|
markers_without_weather = (await db.scalars(
|
||||||
async def get_plans() -> list:
|
select(models.FlightPlanMarker).filter(models.FlightPlanMarker.weather_info_id.is_(None))
|
||||||
async with get_session() as db:
|
))
|
||||||
markers_without_weather = (await db.execute(
|
flight_plan_ids = {m.flight_plan_id for m in markers_without_weather}
|
||||||
select(models.FlightPlanMarker).filter(models.FlightPlanMarker.weather_info_id.is_(None))
|
flight_plan_ids.add(11)
|
||||||
))
|
if not flight_plan_ids:
|
||||||
flight_plan_ids = {m.flight_plan_id for m in markers_without_weather}
|
return []
|
||||||
|
|
||||||
if not flight_plan_ids:
|
return (await db.execute(
|
||||||
return []
|
select(models.FlightPlan, models.FlightPlanMarker)
|
||||||
|
.join(models.FlightPlan.markers)
|
||||||
|
.filter(models.FlightPlan.planned_takeoff_datetime.is_not(None))
|
||||||
|
.filter(models.FlightPlan.id.in_(flight_plan_ids))
|
||||||
|
))
|
||||||
|
|
||||||
return (await db.execute(
|
|
||||||
select(models.FlightPlan, models.FlightPlan.markers)
|
|
||||||
.filter(models.FlightPlan.id.in_(flight_plan_ids))
|
|
||||||
))
|
|
||||||
|
|
||||||
|
async def download_flight_plan_weather():
|
||||||
|
tasks = BackgroundTasks()
|
||||||
|
|
||||||
|
async with get_session() as db:
|
||||||
|
plans_with_markers_without_weather = await get_plans(db)
|
||||||
|
|
||||||
|
markers_by_plan = defaultdict(list)
|
||||||
|
for plan, marker in plans_with_markers_without_weather:
|
||||||
|
markers_by_plan[plan].append(marker)
|
||||||
|
|
||||||
|
for plan, markers in markers_by_plan.items():
|
||||||
|
await refresh_markers_weather_info(
|
||||||
|
plan.planned_takeoff_datetime,
|
||||||
|
plan.planned_speed, markers,
|
||||||
|
background_tasks=tasks
|
||||||
|
)
|
||||||
|
|
||||||
|
await tasks()
|
||||||
|
|
||||||
|
|
||||||
|
async def run_all():
|
||||||
|
await asyncio.gather(download_flight_plan_weather())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.run_until_complete(run_all())
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.path.insert(0, "/app/src")
|
|
||||||
|
|
||||||
from background_jobs.flight_plan_track import generate_track_for_flight_plan
|
|
||||||
|
|
||||||
|
|
||||||
async def run_all():
|
|
||||||
await generate_track_for_flight_plan(11)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.run_until_complete(run_all())
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
from fastapi import BackgroundTasks
|
||||||
|
from background_jobs.weather import download_weather_for_plan_marker
|
||||||
|
from database import models
|
||||||
|
from utils.gps import get_distance
|
||||||
|
|
||||||
|
|
||||||
|
async def refresh_markers_weather_info(
|
||||||
|
planned_takeoff_datetime: datetime, planned_speed: float,
|
||||||
|
markers: list[models.FlightPlanMarker], background_tasks: BackgroundTasks
|
||||||
|
):
|
||||||
|
if not planned_takeoff_datetime:
|
||||||
|
return
|
||||||
|
|
||||||
|
marker_date_time = planned_takeoff_datetime
|
||||||
|
previous_marker = markers[0] if markers else None
|
||||||
|
for marker in markers:
|
||||||
|
if marker_date_time:
|
||||||
|
# TODO: tohle by se melo volat i v pripade zmeny planovaneho casu vzletu i planovane rychlosti
|
||||||
|
distance_from_prev = get_distance(
|
||||||
|
a=(previous_marker.gps_latitude, previous_marker.gps_longitude),
|
||||||
|
b=(marker.gps_latitude, marker.gps_longitude)
|
||||||
|
)
|
||||||
|
time_from_prev_sec = distance_from_prev / planned_speed * 3600 # pracuji v km/h
|
||||||
|
marker_date_time = marker_date_time + timedelta(seconds=time_from_prev_sec)
|
||||||
|
|
||||||
|
background_tasks.add_task(
|
||||||
|
download_weather_for_plan_marker,
|
||||||
|
marker_id=marker.id, date_time=marker_date_time
|
||||||
|
)
|
||||||
|
previous_marker = marker
|
||||||
@@ -24,7 +24,7 @@ def get_bounds(coordinates: list[dict[str, float]]) -> list[tuple[float, float]]
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
async def get_airport_id_by_gps(gps_lat: float, gps_lng: float) -> Optional[int]:
|
async def get_airport_id_by_gps(gps_lat: float, gps_lng: float) -> int | None:
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
query = (
|
query = (
|
||||||
select(models.Airport, func.coalesce(6371 * func.acos(
|
select(models.Airport, func.coalesce(6371 * func.acos(
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
import math
|
import math
|
||||||
|
|
||||||
from geopy import Point
|
|
||||||
from geopy.distance import geodesic
|
|
||||||
|
|
||||||
EARTH_RADIUS = 6378.1 # Radius of the Earth
|
EARTH_RADIUS = 6378.1 # Radius of the Earth
|
||||||
|
|
||||||
|
|
||||||
@@ -41,23 +38,3 @@ def get_distance(a: tuple[float, float], b: tuple[float, float]):
|
|||||||
def gps_to_decimal(input: Tuple[float, float, float]) -> float:
|
def gps_to_decimal(input: Tuple[float, float, float]) -> float:
|
||||||
d, m, s = input
|
d, m, s = input
|
||||||
return d + (m / 60.0) + (s / 3600.0)
|
return d + (m / 60.0) + (s / 3600.0)
|
||||||
|
|
||||||
|
|
||||||
def generate_points(start: tuple[float, float], end: tuple[float, float], interval_meters: int = 500):
|
|
||||||
start = Point(start[0], start[1])
|
|
||||||
end = Point(end[0], end[1])
|
|
||||||
|
|
||||||
total_distance = geodesic(start, end).meters
|
|
||||||
num_intervals = int(total_distance // interval_meters)
|
|
||||||
|
|
||||||
points = [start]
|
|
||||||
|
|
||||||
for i in range(1, num_intervals + 1):
|
|
||||||
# Calculate intermediate point
|
|
||||||
intermediate_distance = interval_meters * i
|
|
||||||
intermediate_point = geodesic(meters=intermediate_distance).destination(start, get_bearing(start, end))
|
|
||||||
points.append(intermediate_point)
|
|
||||||
|
|
||||||
points.append(end)
|
|
||||||
|
|
||||||
return points
|
|
||||||
|
|||||||
+7
-7
@@ -38,7 +38,7 @@ class PhotoEditor:
|
|||||||
self.img = Image.open(f"{path}/{filename}")
|
self.img = Image.open(f"{path}/{filename}")
|
||||||
self.img_size = self.img.size
|
self.img_size = self.img.size
|
||||||
|
|
||||||
def resize(self, new_width: Optional[int] = None, new_height: Optional[int] = None):
|
def resize(self, new_width: int | None = None, new_height: int | None = None):
|
||||||
if not new_width and not new_height:
|
if not new_width and not new_height:
|
||||||
raise ValueError("Set either new_width or new_height")
|
raise ValueError("Set either new_width or new_height")
|
||||||
|
|
||||||
@@ -102,10 +102,10 @@ class PhotoEditor:
|
|||||||
|
|
||||||
def adjust(
|
def adjust(
|
||||||
self,
|
self,
|
||||||
brightness: Optional[float] = None,
|
brightness: float | None = None,
|
||||||
contrast: Optional[float] = None,
|
contrast: float | None = None,
|
||||||
saturation: Optional[float] = None,
|
saturation: float | None = None,
|
||||||
sharpness: Optional[float] = None
|
sharpness: float | None = None
|
||||||
):
|
):
|
||||||
adjustments = [
|
adjustments = [
|
||||||
(Brightness, brightness),
|
(Brightness, brightness),
|
||||||
@@ -127,8 +127,8 @@ 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: str | None = None, dest_filename: str | None = None,
|
||||||
format_: Optional[str] = "JPEG"
|
format_: str | None = "JPEG"
|
||||||
) -> str:
|
) -> str:
|
||||||
check_directories(dest_path or self.path)
|
check_directories(dest_path or self.path)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user