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
|
||||
exclude =
|
||||
venv
|
||||
.venv
|
||||
__pycache__
|
||||
alembic
|
||||
alembic
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ COPY ./src/emails/templates/mjml /app/templates
|
||||
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
|
||||
# RUN deluser xfs
|
||||
@@ -36,4 +36,4 @@ COPY . /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
|
||||
@@ -21,4 +21,4 @@ ssh -t michal@kvacek.cz << EOF
|
||||
docker-compose down --remove-orphans
|
||||
docker-compose up -d
|
||||
docker-compose exec -T api alembic upgrade head
|
||||
EOF
|
||||
EOF
|
||||
|
||||
@@ -3,7 +3,6 @@ version: "3.3"
|
||||
services:
|
||||
api:
|
||||
image: "docker.kvacek.cz/poletani/api:latest"
|
||||
command: "uvicorn src.asgi:app --port=8000 --host=0.0.0.0 --workers 4"
|
||||
network_mode: "host"
|
||||
restart: "always"
|
||||
env_file:
|
||||
|
||||
@@ -5,6 +5,9 @@ services:
|
||||
- 3306:3306
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: "rootroot"
|
||||
|
||||
volumes:
|
||||
# - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
||||
- db-data:/var/lib/mysql:rw
|
||||
|
||||
+19
-13
@@ -1,16 +1,22 @@
|
||||
fastapi==0.110.0
|
||||
fastapi-jwt==0.2.0
|
||||
fastapi-mail==1.4.1
|
||||
strawberry-graphql[fastapi]==0.217.1
|
||||
uvicorn==0.26.0
|
||||
sqlalchemy[asyncio] == 2.0.25
|
||||
fastapi==0.115.7
|
||||
fastapi-jwt[authlib]==0.3.0
|
||||
bcrypt==4.2.1
|
||||
fastapi-mail==1.4.2
|
||||
strawberry-graphql[fastapi]==0.258.0
|
||||
|
||||
gunicorn == 23.0.0
|
||||
uvicorn[standard] == 0.34.0
|
||||
|
||||
sqlalchemy[asyncio] == 2.0.54
|
||||
aiomysql==0.2.0
|
||||
alembic==1.13.1
|
||||
alembic==1.14.1
|
||||
passlib==1.7.4
|
||||
pydantic==2.5.3
|
||||
pydantic==2.10.6
|
||||
sentry-sdk[fastapi]
|
||||
pillow==10.2.0
|
||||
exif==1.6.0
|
||||
aiocache==0.12.2
|
||||
aiohttp==3.9.3
|
||||
lxml==5.1.0
|
||||
pillow==11.1.0
|
||||
exif==1.6.1
|
||||
aiocache==0.12.3
|
||||
aiohttp==3.11.11
|
||||
lxml==5.3.0
|
||||
|
||||
colorlog
|
||||
|
||||
@@ -8,13 +8,13 @@ from external.elevation import elevation_api, update_track_points_elevation
|
||||
@retryable
|
||||
async def add_terrain_elevation_to_flight(flight_id: int):
|
||||
async with get_session() as db:
|
||||
track_points = (
|
||||
await db.scalars(
|
||||
select(models.TrackPoint)
|
||||
.join(models.TrackPoint.track)
|
||||
.join(models.Track.flight)
|
||||
.filter(models.Flight.id == flight_id))
|
||||
).all()
|
||||
track_points = (await db.scalars(
|
||||
select(models.TrackPoint)
|
||||
.join(models.TrackPoint.track)
|
||||
.join(models.Track.flight)
|
||||
.filter(models.Flight.id == flight_id)
|
||||
.filter(models.TrackPoint.terrain_elevation.is_(None))
|
||||
)).all()
|
||||
await update_track_points_elevation(db, track_points)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
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):
|
||||
@@ -56,3 +60,23 @@ async def add_circular_avatar(base_image_path, avatar_image_path, output_image_p
|
||||
|
||||
# Save the result
|
||||
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 typing import Literal
|
||||
from typing import Literal, Any
|
||||
from database import models
|
||||
from database.transaction import get_session
|
||||
from external.weather import weather_api
|
||||
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:
|
||||
weather = await weather_api.get_weather_for_hour(date_time.astimezone(), gps=gps)
|
||||
except Exception as e:
|
||||
@@ -14,7 +14,7 @@ async def get_weather(date_time: datetime, gps: tuple[float, float]):
|
||||
return None
|
||||
|
||||
return {
|
||||
"datetime": weather['datetime'],
|
||||
"date_time": date_time, # nemuzu pouzit weather['datetime'], protoze to mi vraci jen cele hodiny
|
||||
"qnh": weather['pressure_msl'],
|
||||
"temperature_surface": weather['temperature_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})
|
||||
|
||||
|
||||
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)
|
||||
if not data:
|
||||
return
|
||||
|
||||
async with get_session() as db:
|
||||
plan = await models.FlightPlan.get_one(db, id=flight_plan_id)
|
||||
|
||||
if plan.weather_info_id:
|
||||
await models.WeatherInfo.update(db, id=plan.weather_info.id, data=data)
|
||||
if marker_weather_info_id:
|
||||
await models.WeatherInfo.update(db, id=marker_weather_info_id, data=data)
|
||||
else:
|
||||
weather_model = await models.WeatherInfo.create(db, data=data)
|
||||
await models.FlightPlan.update(
|
||||
await models.FlightPlanMarker.update(
|
||||
db,
|
||||
obj=plan,
|
||||
id=marker_id,
|
||||
data={"weather_info_id": weather_model.id}
|
||||
)
|
||||
|
||||
@@ -1,480 +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()
|
||||
# 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 database import models
|
||||
|
||||
|
||||
class QueryBuilder:
|
||||
def __init__(self, model: Type[models.BaseModel]):
|
||||
class QueryBuilder[ModelType: models.BaseModel]:
|
||||
def __init__(self, model: Type[ModelType]):
|
||||
self.model = model
|
||||
|
||||
def get_simple_query(
|
||||
self,
|
||||
extra_select: Optional[list] = None,
|
||||
created_by_id: Optional[int] = None,
|
||||
order_by: Optional[list] = None,
|
||||
only_public: Optional[bool] = False,
|
||||
only_my: Optional[bool] = False,
|
||||
include_others_public: Optional[bool] = False,
|
||||
url_slug: Optional[str] = None,
|
||||
extra_select: list | None = None,
|
||||
created_by_id: int | None = None,
|
||||
order_by: list | None = None,
|
||||
only_public: bool | None = False,
|
||||
only_my: bool | None = False,
|
||||
include_others_public: bool | None = False,
|
||||
url_slug: str | None = None,
|
||||
include_deleted: bool = False
|
||||
):
|
||||
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 font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left">
|
||||
Od: [from]
|
||||
Od: {{ email_from | e }}
|
||||
</mj-text>
|
||||
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5">
|
||||
[content]
|
||||
{{ content | e }}
|
||||
</mj-text>
|
||||
<mj-divider border-color="#469ccc"></mj-divider>
|
||||
</mj-column>
|
||||
|
||||
@@ -13,13 +13,12 @@
|
||||
<p>
|
||||
Ahoj,
|
||||
<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>
|
||||
<strong>Jen admin vyzaduje zadani stareho hesla, takze to nebude tak snadne...</strong>
|
||||
</p>
|
||||
</mj-text>
|
||||
<mj-button background-color="#469ccc" color="white" font-size="18px" align="center" href="">
|
||||
Přihlásit se pomocí magického odkazu [login_token]
|
||||
<mj-button background-color="#469ccc" color="white" font-size="18px" align="center" href="https://poletani.cz/zapomenute-heslo/[login_token]">
|
||||
Obnovit heslo
|
||||
</mj-button>
|
||||
<mj-divider border-color="#469ccc"></mj-divider>
|
||||
</mj-column>
|
||||
|
||||
@@ -8,34 +8,34 @@
|
||||
Vítejte v aplikaci Polétání.cz!
|
||||
</mj-text>
|
||||
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5">
|
||||
Ahoj [Jméno],
|
||||
Ahoj!
|
||||
<br>
|
||||
|
||||
<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>
|
||||
Co můžete očekávat:
|
||||
Co můžeš očekávat:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<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>Upozornění na nové funkce a akce v aplikaci.</li>
|
||||
<li>Naplánovat nový výlet pomocí vestavěného plánovače</li>
|
||||
<li>Objevovat nová místa pro další výlety</li>
|
||||
</ul>
|
||||
|
||||
<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>
|
||||
S pozdravem,
|
||||
<br>
|
||||
Tým Polétání.cz
|
||||
Michal Kváček
|
||||
</p>
|
||||
</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
|
||||
</mj-button>
|
||||
<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
|
||||
from fastapi import HTTPException, BackgroundTasks
|
||||
from sqlalchemy import select
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from background_jobs.send_email import send_email
|
||||
from database.models import User
|
||||
@@ -12,7 +11,7 @@ from emails.emails import WelcomeEmail
|
||||
|
||||
class RegistrationInput(BaseModel):
|
||||
email: str = Field(..., min_length=4)
|
||||
name: Optional[str]
|
||||
name: str | None
|
||||
password: str
|
||||
|
||||
@model_validator(mode="before")
|
||||
|
||||
Vendored
+1
-1
@@ -9,7 +9,7 @@ from utils.list_utils import chunks
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ElevationInfo:
|
||||
id: Optional[int]
|
||||
id: int | None
|
||||
lat: float
|
||||
lng: float
|
||||
elevation: float
|
||||
|
||||
Vendored
+1
-1
@@ -53,7 +53,7 @@ class Airspace:
|
||||
upper_limit: str = None
|
||||
lower_limit: str = None
|
||||
center: Optional[Coordinates] = None
|
||||
radius_nm: Optional[float] = None
|
||||
radius_nm: float | None = None
|
||||
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]):
|
||||
url = self.get_weather_info_url(start_date=date, end_date=date, gps=gps)
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as resp:
|
||||
resp.raise_for_status()
|
||||
return await resp.json()
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as resp:
|
||||
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(
|
||||
self, date_time: datetime.datetime, gps: Tuple[float, float]
|
||||
@@ -56,7 +59,7 @@ class Weather:
|
||||
# TODO: kontrola timezone!
|
||||
idx = date_time.hour
|
||||
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
|
||||
|
||||
|
||||
@@ -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 typing import Type, List, Optional
|
||||
from typing import Type
|
||||
from logger import log
|
||||
from database import models, async_session
|
||||
from database.query_builder import QueryBuilder
|
||||
@@ -9,7 +9,7 @@ class BaseDataloader:
|
||||
def __init__(
|
||||
self,
|
||||
model: Type[models.BaseModel],
|
||||
relationship_column, filters: Optional[list] = None
|
||||
relationship_column, filters: list | None = None
|
||||
):
|
||||
super().__init__()
|
||||
self.model = model
|
||||
@@ -25,11 +25,15 @@ class 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:
|
||||
query = (
|
||||
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)
|
||||
)
|
||||
|
||||
@@ -43,10 +47,10 @@ class MultiModelsDataloader(BaseDataloader):
|
||||
self,
|
||||
model: Type[models.BaseModel],
|
||||
relationship_column=None,
|
||||
filters: Optional[list] = None,
|
||||
extra_select: Optional[list] = None,
|
||||
extra_join: Optional[list] = None,
|
||||
order_by: Optional[list] = None,
|
||||
filters: list | None = None,
|
||||
extra_select: list | None = None,
|
||||
extra_join: list | None = None,
|
||||
order_by: list | None = None,
|
||||
):
|
||||
super().__init__(model, relationship_column, filters)
|
||||
|
||||
@@ -62,13 +66,13 @@ class MultiModelsDataloader(BaseDataloader):
|
||||
order_by = [model.id.desc()] # defaultne radit od nejnovejsich zaznamu
|
||||
self.order_by = order_by
|
||||
|
||||
def get_query(self, ids: list[int]):
|
||||
def get_query(self, ids: set[int]):
|
||||
query = (
|
||||
self.query_builder.get_simple_query(
|
||||
extra_select=[self.relationship_column] + self.extra_select,
|
||||
order_by=self.order_by
|
||||
)
|
||||
.filter(self.relationship_column.in_(set(ids)))
|
||||
.filter(self.relationship_column.in_(ids))
|
||||
.filter(*self.filters)
|
||||
)
|
||||
|
||||
@@ -80,8 +84,12 @@ class MultiModelsDataloader(BaseDataloader):
|
||||
|
||||
return query
|
||||
|
||||
async def load(self, ids: List[int]):
|
||||
query = self.get_query(ids)
|
||||
async def load(self, ids: list[int]) -> list:
|
||||
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:
|
||||
data = (await db.execute(query)).all()
|
||||
|
||||
@@ -4,7 +4,7 @@ from database import models
|
||||
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
|
||||
return DataLoader(load_fn=loader, cache=False)
|
||||
|
||||
@@ -14,7 +14,7 @@ airport_dataloader = create_dataloader(models.Airport)
|
||||
aircraft_dataloader = create_dataloader(models.Aircraft)
|
||||
event_dataloader = create_dataloader(models.Event)
|
||||
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)
|
||||
poi_dataloader = create_dataloader(models.PointOfInterest)
|
||||
poi_type_dataloader = create_dataloader(models.PointOfInterestType)
|
||||
|
||||
@@ -1,70 +1,49 @@
|
||||
from typing import Optional
|
||||
import strawberry
|
||||
from decorators.endpoints import authenticated_user_only, allow_public
|
||||
from decorators.error_logging import error_logging
|
||||
from .helpers.pagination import get_pagination_window, PaginationWindow
|
||||
from strawberry import Info
|
||||
from .helpers.filters import get_filters
|
||||
from .helpers.pagination import PaginationWindow
|
||||
from .resolvers.aircraft import AircraftMutationResolver, AircraftQueryResolver
|
||||
from graphql_schema.entities.types.mutation_input import CreateAircraftInput, EditAircraftInput
|
||||
from graphql_schema.entities.types.types import Aircraft
|
||||
from ..extensions.field.auth import AllowPublicAccess, AuthenticatedOnly
|
||||
from ..extensions.field.pagination import OffsetPagination
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class AircraftQueries:
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
async def aircrafts(root, info, limit: int, offset: int = 0) -> PaginationWindow[Aircraft]:
|
||||
query = AircraftQueryResolver().get_query(
|
||||
@strawberry.field(extensions=[OffsetPagination(item_type=Aircraft), AuthenticatedOnly()])
|
||||
async def aircrafts(root, info: Info, search: str | None = None) -> PaginationWindow[Aircraft]:
|
||||
return AircraftQueryResolver().get_query(
|
||||
info.context.user_id,
|
||||
organization_ids=info.context.organization_ids
|
||||
organization_ids=info.context.organization_ids,
|
||||
search=search
|
||||
)
|
||||
|
||||
return await get_pagination_window(
|
||||
query=query,
|
||||
item_type=Aircraft,
|
||||
limit=limit,
|
||||
offset=offset
|
||||
)
|
||||
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@allow_public
|
||||
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||
async def aircraft(
|
||||
root, info,
|
||||
id: Optional[int] = None,
|
||||
call_sign: Optional[str] = None,
|
||||
public: Optional[bool] = False
|
||||
id: int | None = None,
|
||||
call_sign: str | None = None,
|
||||
public: bool | None = False
|
||||
) -> Aircraft:
|
||||
filter_params = {}
|
||||
if id:
|
||||
filter_params['object_id'] = id
|
||||
|
||||
if call_sign:
|
||||
filter_params['call_sign'] = call_sign
|
||||
|
||||
return await AircraftQueryResolver().get_one(
|
||||
user_id=info.context.user_id,
|
||||
organization_ids=info.context.organization_ids if not public else None,
|
||||
only_public=public,
|
||||
**filter_params
|
||||
**get_filters(object_id=id, call_sign=call_sign)
|
||||
)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class AircraftMutation:
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
async def create_aircraft(root, info, input: CreateAircraftInput) -> Aircraft:
|
||||
return await AircraftMutationResolver().create(info.context, input)
|
||||
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
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
|
||||
@authenticated_user_only()
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
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
|
||||
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.types.types import Airport
|
||||
from graphql_schema.extensions.field.auth import AuthenticatedOnly
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class AirportQueries:
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
# @authenticated_user_only()
|
||||
async def airports(root, info) -> List[Airport]:
|
||||
async def airports(root, info) -> list[Airport]:
|
||||
return await BaseQueryResolver(Airport, models.Airport).get_list(info.context.user_id)
|
||||
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||
async def airport(root, info, id: int) -> Airport:
|
||||
return await BaseQueryResolver(Airport, models.Airport).get_one(
|
||||
object_id=id,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from typing import List, Optional
|
||||
import strawberry
|
||||
from database import models
|
||||
from decorators.error_logging import error_logging
|
||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
||||
from graphql_schema.entities.types.types import Airspace
|
||||
|
||||
@@ -9,10 +7,9 @@ from graphql_schema.entities.types.types import Airspace
|
||||
@strawberry.type
|
||||
class AirspaceQueries:
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
async def airspaces(
|
||||
root, info, country: Optional[str] = None, types: Optional[list[str]] = None
|
||||
) -> List[Airspace]:
|
||||
root, info, country: str | None = None, types: list[str] | None = None
|
||||
) -> list[Airspace]:
|
||||
|
||||
filters = []
|
||||
if country:
|
||||
|
||||
@@ -1,48 +1,35 @@
|
||||
from typing import List, Optional
|
||||
import strawberry
|
||||
from graphql import GraphQLError
|
||||
from strawberry.types import Info
|
||||
from database import models
|
||||
from decorators.error_logging import error_logging
|
||||
from decorators.endpoints import authenticated_user_only, allow_public
|
||||
from graphql_schema.entities.helpers.detail import get_detail_filters
|
||||
from graphql_schema.entities.helpers.filters import get_filters
|
||||
from graphql_schema.entities.resolvers.base import BaseMutationResolver
|
||||
from graphql_schema.entities.resolvers.copilot import CopilotQueryResolver
|
||||
from graphql_schema.entities.types.mutation_input import CreateCopilotInput, EditCopilotInput
|
||||
from graphql_schema.entities.types.types import Copilot
|
||||
from graphql_schema.extensions.field.auth import AllowPublicAccess, AuthenticatedOnly
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class CopilotQueries:
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
async def copilots(root, info: Info) -> List[Copilot]:
|
||||
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||
async def copilots(root, info: Info) -> list[Copilot]:
|
||||
return await CopilotQueryResolver().get_list(info.context.user_id)
|
||||
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@allow_public
|
||||
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||
async def copilot(
|
||||
root, info: Info,
|
||||
id: Optional[int] = None,
|
||||
url_slug: Optional[str] = None,
|
||||
pilot_username: Optional[str] = None,
|
||||
upload_token: Optional[str] = None,
|
||||
upload_flight_slug: Optional[str] = None,
|
||||
public: Optional[bool] = False
|
||||
id: int | None = None,
|
||||
url_slug: str | None = None,
|
||||
pilot_username: str | None = None,
|
||||
upload_token: str | None = None,
|
||||
upload_flight_slug: str | None = None,
|
||||
public: bool | None = False
|
||||
) -> Copilot:
|
||||
filter_params = {}
|
||||
if id:
|
||||
filter_params['object_id'] = id
|
||||
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
|
||||
|
||||
filter_params = get_filters(
|
||||
object_id=id, url_slug=url_slug, pilot_username=pilot_username, upload_token=upload_token,
|
||||
upload_flight_slug=upload_flight_slug,
|
||||
)
|
||||
if not filter_params:
|
||||
raise GraphQLError(f"Invalid identification supplied: {filter_params}")
|
||||
|
||||
@@ -55,14 +42,10 @@ class CopilotQueries:
|
||||
|
||||
@strawberry.type
|
||||
class CopilotMutation:
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
async def create_copilot(root, info, input: CreateCopilotInput) -> Copilot:
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
async def create_copilot(root, info: Info, input: CreateCopilotInput) -> Copilot:
|
||||
return await BaseMutationResolver(Copilot, models.Copilot).create(info.context, data=input)
|
||||
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
async def edit_copilot(root, info, id: int, input: EditCopilotInput) -> Copilot:
|
||||
return await BaseMutationResolver(Copilot, models.Copilot).update(id, input, info.context.user_id)
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
async def edit_copilot(root, info: Info, id: int, input: EditCopilotInput) -> Copilot:
|
||||
return await BaseMutationResolver(Copilot, models.Copilot).update(info.context, id, input)
|
||||
|
||||
@@ -1,52 +1,39 @@
|
||||
from typing import Optional
|
||||
import strawberry
|
||||
from database import models
|
||||
from decorators.endpoints import authenticated_user_only, allow_public
|
||||
from decorators.error_logging import error_logging
|
||||
from graphql_schema.entities.helpers.detail import get_detail_filters
|
||||
from graphql_schema.entities.helpers.pagination import PaginationWindow, get_pagination_window
|
||||
from graphql_schema.entities.helpers.filters import get_detail_filters
|
||||
from graphql_schema.entities.helpers.pagination import PaginationWindow
|
||||
from graphql_schema.entities.resolvers.base import BaseMutationResolver
|
||||
from graphql_schema.entities.resolvers.event import EventQueryResolver
|
||||
from graphql_schema.entities.types.mutation_input import CreateEventInput, EditEventInput
|
||||
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
|
||||
class EventQueries:
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@allow_public
|
||||
@strawberry.field(extensions=[OffsetPagination(item_type=Event), AllowPublicAccess()])
|
||||
async def events(
|
||||
root,
|
||||
info,
|
||||
limit: int,
|
||||
offset: int = 0,
|
||||
username: Optional[str] = None,
|
||||
public: Optional[bool] = False,
|
||||
username: str | None = None,
|
||||
public: bool | None = False,
|
||||
) -> PaginationWindow[Event]:
|
||||
query = EventQueryResolver().get_query(
|
||||
return EventQueryResolver().get_query(
|
||||
user_id=info.context.user_id,
|
||||
username=username,
|
||||
order_by=[models.Event.date_from.desc(), models.Event.name.desc()],
|
||||
only_public=public,
|
||||
)
|
||||
|
||||
return await get_pagination_window(
|
||||
query=query,
|
||||
item_type=Event,
|
||||
limit=limit,
|
||||
offset=offset
|
||||
)
|
||||
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@allow_public
|
||||
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||
async def event(
|
||||
root, info,
|
||||
id: Optional[int] = None,
|
||||
url_slug: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
public: Optional[bool] = False
|
||||
id: int | None = None,
|
||||
url_slug: str | None = None,
|
||||
username: str | None = None,
|
||||
public: bool | None = False
|
||||
) -> Event:
|
||||
filter_params = get_detail_filters(id, url_slug)
|
||||
if username:
|
||||
@@ -61,14 +48,10 @@ class EventQueries:
|
||||
|
||||
@strawberry.type
|
||||
class EventMutation:
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
async def create_event(root, info, input: CreateEventInput) -> Event:
|
||||
return await BaseMutationResolver(Event, models.Event).create(info.context, input)
|
||||
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
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
|
||||
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.types.mutation_input import EditFlightInput, CreateFlightInput
|
||||
from graphql_schema.entities.types.types import Flight
|
||||
from .helpers.detail import get_detail_filters
|
||||
from .helpers.pagination import PaginationWindow, get_pagination_window
|
||||
from .helpers.filters import get_detail_filters
|
||||
from .helpers.pagination import PaginationWindow
|
||||
from ..extensions.field.auth import AllowPublicAccess, AuthenticatedOnly
|
||||
from ..extensions.field.pagination import OffsetPagination
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class FlightQueries:
|
||||
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@allow_public
|
||||
@strawberry.field(extensions=[OffsetPagination(item_type=Flight), AllowPublicAccess()])
|
||||
async def flights(
|
||||
root, info,
|
||||
limit: int,
|
||||
offset: int = 0,
|
||||
username: Optional[str] = None,
|
||||
event_id: Optional[int] = None,
|
||||
public: Optional[bool] = False,
|
||||
copilot_id: Optional[int] = None,
|
||||
point_of_interest_id: Optional[int] = None,
|
||||
aircraft_id: Optional[int] = None,
|
||||
username: str | None = None,
|
||||
search: str | None = None,
|
||||
event_id: int | None = None,
|
||||
public: bool | None = False,
|
||||
copilot_id: int | None = None,
|
||||
point_of_interest_id: int | None = None,
|
||||
aircraft_id: int | None = None,
|
||||
) -> PaginationWindow[Flight]:
|
||||
query = FlightQueryResolver().get_query(
|
||||
return FlightQueryResolver().get_query(
|
||||
user_id=info.context.user_id,
|
||||
username=username,
|
||||
event_id=event_id,
|
||||
only_public=public,
|
||||
copilot_id=copilot_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(
|
||||
query=query,
|
||||
item_type=Flight,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@allow_public
|
||||
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||
async def flight(
|
||||
root, info,
|
||||
id: Optional[int] = None,
|
||||
url_slug: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
public: Optional[bool] = False
|
||||
id: int | None = None,
|
||||
url_slug: str | None = None,
|
||||
username: str | None = None,
|
||||
public: bool | None = False
|
||||
) -> Flight:
|
||||
filter_params = get_detail_filters(id, url_slug)
|
||||
if username:
|
||||
@@ -66,20 +54,14 @@ class FlightQueries:
|
||||
|
||||
@strawberry.type
|
||||
class FlightMutation:
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
async def create_flight(self, info, input: CreateFlightInput) -> Flight:
|
||||
return await FlightMutationResolver().create(info.context, input)
|
||||
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
async def edit_flight(self, info, id: int, input: EditFlightInput) -> Flight:
|
||||
return await FlightMutationResolver().update(info.context, id, input)
|
||||
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
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
|
||||
from strawberry.types import Info
|
||||
from decorators.endpoints import authenticated_user_only, allow_public
|
||||
from decorators.error_logging import error_logging
|
||||
from graphql_schema.entities.helpers.detail import get_detail_filters
|
||||
from graphql_schema.entities.helpers.filters import get_detail_filters
|
||||
from graphql_schema.entities.helpers.pagination import PaginationWindow
|
||||
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.types import FlightPlan
|
||||
from graphql_schema.extensions.field.auth import AuthenticatedOnly, AllowPublicAccess
|
||||
from graphql_schema.extensions.field.pagination import OffsetPagination
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class FlightPlanQueries:
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
async def flight_plans(root, info: Info) -> List[FlightPlan]:
|
||||
return await FlightPlanQueryResolver().get_list(info.context.user_id)
|
||||
@strawberry.field(extensions=[OffsetPagination(item_type=FlightPlan), AuthenticatedOnly()])
|
||||
async def flight_plans(root, info: Info, search: str | None = None) -> PaginationWindow[FlightPlan]:
|
||||
return FlightPlanQueryResolver().get_query(info.context.user_id, search=search)
|
||||
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@allow_public
|
||||
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||
async def flight_plan(
|
||||
root,
|
||||
info: Info,
|
||||
id: int,
|
||||
username: Optional[str] = None,
|
||||
public: Optional[bool] = False
|
||||
username: str | None = None,
|
||||
public: bool | None = False
|
||||
) -> FlightPlan:
|
||||
filter_params = get_detail_filters(id)
|
||||
if username:
|
||||
@@ -40,14 +36,10 @@ class FlightPlanQueries:
|
||||
|
||||
@strawberry.type
|
||||
class FlightPlanMutation:
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
async def create_flight_plan(root, info, input: CreateFlightPlanInput) -> FlightPlan:
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
async def create_flight_plan(root, info: Info, input: CreateFlightPlanInput) -> FlightPlan:
|
||||
return await FlightPlanMutationResolver().create(info.context, data=input)
|
||||
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
async def edit_flight_plan(root, info, id: int, input: EditFlightPlanInput) -> FlightPlan:
|
||||
return await FlightPlanMutationResolver().update(id, input, info.context.user_id)
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
async def edit_flight_plan(root, info: Info, id: int, input: EditFlightPlanInput) -> FlightPlan:
|
||||
return await FlightPlanMutationResolver().update(info.context, id, input)
|
||||
|
||||
@@ -10,7 +10,7 @@ async def handle_combobox_save(
|
||||
input: ComboboxInput,
|
||||
user_id: int,
|
||||
name_column: str = "name",
|
||||
extra_data: Optional[dict] = None
|
||||
extra_data: dict | None = None
|
||||
) -> int:
|
||||
if 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())
|
||||
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()
|
||||
dataset = [item_type(**i.as_dict()) for i in data]
|
||||
|
||||
|
||||
@@ -3,27 +3,23 @@ import strawberry
|
||||
from sqlalchemy import delete
|
||||
from sqlalchemy.dialects.mysql import insert
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from strawberry import Info
|
||||
from database import models
|
||||
from decorators.endpoints import authenticated_user_only
|
||||
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.organization import OrganizationQueryResolver
|
||||
from graphql_schema.entities.types.mutation_input import CreateOrganizationInput, EditOrganizationInput
|
||||
from graphql_schema.entities.types.types import Organization
|
||||
from graphql_schema.extensions.field.auth import AuthenticatedOnly
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class OrganizationQueries:
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||
async def organizations(root, info) -> List[Organization]:
|
||||
return await OrganizationQueryResolver().get_list()
|
||||
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||
async def organization(root, info, id: int) -> Organization:
|
||||
return await OrganizationQueryResolver().get_one(object_id=id)
|
||||
|
||||
@@ -31,17 +27,14 @@ class OrganizationQueries:
|
||||
@strawberry.type
|
||||
class OrganizationMutation:
|
||||
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
async def create_organization(root, info, input: CreateOrganizationInput) -> Organization:
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
async def create_organization(root, info: Info, input: CreateOrganizationInput) -> Organization:
|
||||
return await BaseMutationResolver(Organization, models.Organization).create(info.context, data=input)
|
||||
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
async def edit_organization(root, info, id: int, input: EditOrganizationInput) -> Organization:
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
async def edit_organization(root, info: Info, id: int, input: EditOrganizationInput) -> Organization:
|
||||
return await BaseMutationResolver(Organization, models.Organization).update(
|
||||
info.context,
|
||||
id,
|
||||
data=input,
|
||||
user_id=info.context.user_id
|
||||
@@ -51,9 +44,7 @@ class OrganizationMutation:
|
||||
@strawberry.type
|
||||
class OrganizationUserMutation:
|
||||
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
async def add_to_organization(root, info, organization_id: int) -> Organization:
|
||||
async with get_session() as db:
|
||||
organization = (await db.scalars(
|
||||
@@ -72,9 +63,7 @@ class OrganizationUserMutation:
|
||||
|
||||
return Organization(**organization.as_dict())
|
||||
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
async def remove_from_organization(root, info, organization_id: int) -> Organization:
|
||||
async with get_session() as db:
|
||||
organization = (await db.scalars(
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
from typing import List, Optional
|
||||
import strawberry
|
||||
from fastapi import HTTPException
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||
from strawberry import Info
|
||||
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.photo import PhotoMutationResolver, PhotoQueryResolver
|
||||
from graphql_schema.entities.types.types import Photo
|
||||
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
|
||||
class PhotoQueries:
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@allow_public
|
||||
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||
async def photos(
|
||||
root, info,
|
||||
flight_id: Optional[int] = None,
|
||||
copilot_id: Optional[int] = None,
|
||||
uploaded_by_copilot_id: Optional[int] = None,
|
||||
point_of_interest_id: Optional[int] = None,
|
||||
aircraft_id: Optional[int] = None,
|
||||
public: Optional[bool] = False,
|
||||
) -> List[Photo]:
|
||||
root, info: Info,
|
||||
flight_id: int | None = None,
|
||||
copilot_id: int | None = None,
|
||||
uploaded_by_copilot_id: int | None = None,
|
||||
point_of_interest_id: int | None = None,
|
||||
aircraft_id: int | None = None,
|
||||
public: bool | None = False,
|
||||
) -> list[Photo]:
|
||||
return await PhotoQueryResolver().get_list(
|
||||
public=public,
|
||||
flight_id=flight_id,
|
||||
@@ -34,10 +37,8 @@ class PhotoQueries:
|
||||
order_by=[models.Photo.exposed_at]
|
||||
)
|
||||
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@allow_public
|
||||
async def photo(root, info, id: int, public: Optional[bool] = False, ) -> Photo:
|
||||
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||
async def photo(root, info: Info, id: int, public: bool | None = False) -> Photo:
|
||||
return await BaseQueryResolver(Photo, models.Photo).get_one(
|
||||
object_id=id,
|
||||
user_id=info.context.user_id,
|
||||
@@ -47,23 +48,19 @@ class PhotoQueries:
|
||||
|
||||
@strawberry.type
|
||||
class PhotoMutation:
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@strawberry.mutation()
|
||||
async def upload_photo(self, info, input: UploadPhotoInput) -> Photo:
|
||||
if info.context.user_id is None and not input.copilot_upload_token:
|
||||
raise_unauthorized()
|
||||
|
||||
return await PhotoMutationResolver().upload(info, input)
|
||||
|
||||
@strawberry.mutation()
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
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()
|
||||
@error_logging
|
||||
async def change_orientation(self, info, id: int, direction: str, copilot_upload_token: Optional[str] = None) -> Photo:
|
||||
async def change_orientation(self, info, id: int, direction: str, copilot_upload_token: str | None = None) -> Photo:
|
||||
if info.context.user_id is None and not copilot_upload_token:
|
||||
raise_unauthorized()
|
||||
|
||||
@@ -75,16 +72,13 @@ class PhotoMutation:
|
||||
info=info
|
||||
)
|
||||
|
||||
@strawberry.mutation()
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
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)
|
||||
|
||||
@strawberry.mutation()
|
||||
@error_logging
|
||||
async def delete_photo(self, info, id: int, copilot_upload_token: Optional[str] = None) -> Photo:
|
||||
async def delete_photo(self, info, id: int, copilot_upload_token: str | None = None) -> Photo:
|
||||
if info.context.user_id is None and not copilot_upload_token:
|
||||
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
|
||||
from database import models
|
||||
from decorators.endpoints import authenticated_user_only, allow_public
|
||||
from database.transaction import get_session
|
||||
from decorators.error_logging import error_logging
|
||||
from graphql_schema.entities.helpers.combobox import handle_combobox_save
|
||||
from graphql_schema.entities.helpers.detail import get_detail_filters
|
||||
from graphql_schema.entities.helpers.pagination import get_pagination_window, PaginationWindow
|
||||
from graphql_schema.entities.helpers.filters import get_detail_filters
|
||||
from graphql_schema.entities.helpers.pagination import PaginationWindow
|
||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
|
||||
from graphql_schema.entities.types.types import PointOfInterest
|
||||
from graphql_schema.entities.types.mutation_input import CreatePointOfInterestInput, EditPointOfInterestInput
|
||||
from graphql_schema.extensions.field.auth import AuthenticatedOnly, AllowPublicAccess
|
||||
from graphql_schema.extensions.field.pagination import OffsetPagination
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class PointOfInterestQueries:
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@allow_public
|
||||
@strawberry.field(extensions=[OffsetPagination(item_type=PointOfInterest), AllowPublicAccess()])
|
||||
async def points_of_interest(
|
||||
root, info,
|
||||
limit: int, offset: int = 0,
|
||||
search: Optional[str] = None,
|
||||
public: bool = False
|
||||
search: str | None = None,
|
||||
public: bool = False,
|
||||
) -> PaginationWindow[PointOfInterest]:
|
||||
query = BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_query(
|
||||
return BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_query(
|
||||
info.context.user_id,
|
||||
only_my=bool(info.context.user_id),
|
||||
include_others_public=True,
|
||||
only_public=public,
|
||||
search=search,
|
||||
)
|
||||
return await get_pagination_window(
|
||||
query=query,
|
||||
item_type=PointOfInterest,
|
||||
limit=limit,
|
||||
offset=offset
|
||||
)
|
||||
|
||||
@strawberry.field()
|
||||
@allow_public
|
||||
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||
async def point_of_interest(
|
||||
root, info,
|
||||
url_slug: Optional[str] = None,
|
||||
id: Optional[int] = None,
|
||||
url_slug: str | None = None,
|
||||
id: int | None = None,
|
||||
public: bool = False
|
||||
) -> PointOfInterest:
|
||||
filter_params = get_detail_filters(id, url_slug)
|
||||
@@ -56,9 +45,7 @@ class PointOfInterestQueries:
|
||||
|
||||
@strawberry.type
|
||||
class PointOfInterestMutation:
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
async def create_point_of_interest(root, info, input: CreatePointOfInterestInput) -> PointOfInterest:
|
||||
input_data = input.to_dict()
|
||||
|
||||
@@ -73,9 +60,7 @@ class PointOfInterestMutation:
|
||||
db, input_data
|
||||
)
|
||||
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
async def edit_point_of_interest(root, info, id: int, input: EditPointOfInterestInput) -> PointOfInterest:
|
||||
input_data = input.to_dict()
|
||||
|
||||
@@ -93,8 +78,6 @@ class PointOfInterestMutation:
|
||||
updated_poi = await models.PointOfInterest.update(db, obj=poi, data=input_data)
|
||||
return PointOfInterest(**updated_poi.as_dict())
|
||||
|
||||
@strawberry.mutation
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
async def delete_point_of_interest(self, info, id: int) -> PointOfInterest:
|
||||
return await BaseMutationResolver(PointOfInterest, models.PointOfInterest).delete(info.context.user_id, id=id)
|
||||
|
||||
@@ -1,92 +1,20 @@
|
||||
from typing import List
|
||||
import strawberry
|
||||
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.types.types import PointOfInterestType
|
||||
from graphql_schema.extensions.field.auth import AuthenticatedOnly
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class PointOfInterestTypeQueries:
|
||||
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
async def point_of_interest_types(root, info) -> List[PointOfInterestType]:
|
||||
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||
async def point_of_interest_types(root, info) -> list[PointOfInterestType]:
|
||||
return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_list(info.context.user_id)
|
||||
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
@authenticated_user_only()
|
||||
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||
async def point_of_interest_type(root, info, id: int) -> PointOfInterestType:
|
||||
return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_one(
|
||||
object_id=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 typing import Set, Optional
|
||||
|
||||
from sqlalchemy import and_
|
||||
|
||||
from sqlalchemy import and_, or_
|
||||
from database import models
|
||||
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.resolvers.base import BaseMutationResolver, BaseQueryResolver
|
||||
from graphql_schema.entities.types.mutation_input import EditAircraftInput, CreateAircraftInput
|
||||
@@ -17,29 +14,36 @@ class AircraftQueryResolver(BaseQueryResolver):
|
||||
|
||||
def get_query(
|
||||
self,
|
||||
user_id: Optional[int] = None,
|
||||
object_id: Optional[int] = None,
|
||||
order_by: Optional[list] = None,
|
||||
organization_ids: Optional[Set[int]] = None,
|
||||
user_id: int | None = None,
|
||||
object_id: int | None = None,
|
||||
order_by: list | None = None,
|
||||
organization_ids: set[int] | None = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
filters = []
|
||||
search = {}
|
||||
if kwargs.get("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(
|
||||
object_id=object_id,
|
||||
only_my=False,
|
||||
only_public=kwargs.get("only_public", False),
|
||||
order_by=order_by,
|
||||
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:
|
||||
query = (
|
||||
query.filter(
|
||||
@@ -62,7 +66,7 @@ class AircraftMutationResolver(BaseMutationResolver):
|
||||
def __init__(self):
|
||||
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()
|
||||
|
||||
async with get_session() as db:
|
||||
@@ -77,7 +81,7 @@ class AircraftMutationResolver(BaseMutationResolver):
|
||||
|
||||
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()
|
||||
async with get_session() as db:
|
||||
if data.organization:
|
||||
@@ -85,7 +89,7 @@ class AircraftMutationResolver(BaseMutationResolver):
|
||||
db,
|
||||
models.Organization,
|
||||
input=data.organization,
|
||||
user_id=user_id,
|
||||
user_id=context.user_id,
|
||||
)
|
||||
|
||||
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.ext.asyncio import AsyncSession
|
||||
from database import models
|
||||
from database.query_builder import QueryBuilder
|
||||
from database.transaction import get_session
|
||||
from graphql_schema.context import GraphQLContext
|
||||
from graphql_schema.entities.types.base import BaseGraphqlInputType
|
||||
|
||||
|
||||
GQL_TYPE = TypeVar('GQL_TYPE')
|
||||
|
||||
|
||||
@@ -18,7 +19,7 @@ class BaseResolver(Generic[GQL_TYPE]):
|
||||
|
||||
|
||||
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:
|
||||
items = (await db.scalars(query)).all()
|
||||
|
||||
@@ -31,14 +32,14 @@ class BaseQueryResolver(BaseResolver):
|
||||
|
||||
def get_query(
|
||||
self,
|
||||
user_id: Optional[int] = None,
|
||||
object_id: Optional[int] = None,
|
||||
order_by: Optional[list] = None,
|
||||
only_public: Optional[bool] = False,
|
||||
only_my: Optional[bool] = False,
|
||||
include_others_public: Optional[bool] = False,
|
||||
url_slug: Optional[str] = None,
|
||||
filters: Optional[list] = None,
|
||||
user_id: int | None = None,
|
||||
object_id: int | None = None,
|
||||
order_by: list | None = None,
|
||||
only_public: bool | None = False,
|
||||
only_my: bool | None = False,
|
||||
include_others_public: bool | None = False,
|
||||
url_slug: str | None = None,
|
||||
filters: list | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
query = self.query_builder.get_simple_query(
|
||||
@@ -77,11 +78,11 @@ class BaseQueryResolver(BaseResolver):
|
||||
query = query.filter(or_(*search_clauses))
|
||||
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)
|
||||
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)
|
||||
return await self._get_one(query)
|
||||
|
||||
@@ -107,7 +108,7 @@ class BaseMutationResolver(BaseResolver):
|
||||
model = await self.model.update(db, data=data, **update_where)
|
||||
return self.graphql_type(**model.as_dict())
|
||||
|
||||
async def create(self, context, data: BaseGraphqlInputType) -> GQL_TYPE:
|
||||
async def create(self, context: GraphQLContext, data: BaseGraphqlInputType) -> GQL_TYPE:
|
||||
input_data = data.to_dict()
|
||||
|
||||
if hasattr(self.model, "created_by_id"):
|
||||
@@ -116,14 +117,14 @@ class BaseMutationResolver(BaseResolver):
|
||||
async with get_session() as db:
|
||||
return await self._do_create(db, input_data)
|
||||
|
||||
async def update(self, id: int, data: BaseGraphqlInputType, user_id: int) -> GQL_TYPE:
|
||||
async def update(self, context: GraphQLContext, id: int, data: BaseGraphqlInputType) -> GQL_TYPE:
|
||||
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())
|
||||
|
||||
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:
|
||||
model = await self._get_one(db, id, user_id)
|
||||
model = await self._get_one(db, id, context.user_id)
|
||||
|
||||
if hasattr(self.model, "deleted"):
|
||||
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 database import models
|
||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
||||
from graphql_schema.entities.types.types import Copilot
|
||||
@@ -13,10 +10,10 @@ class CopilotQueryResolver(BaseQueryResolver):
|
||||
|
||||
def get_query(
|
||||
self,
|
||||
user_id: Optional[int] = None,
|
||||
object_id: Optional[int] = None,
|
||||
order_by: Optional[list] = None,
|
||||
only_public: Optional[bool] = False,
|
||||
user_id: int | None = None,
|
||||
object_id: int | None = None,
|
||||
order_by: list | None = None,
|
||||
only_public: bool | None = False,
|
||||
**kwargs
|
||||
):
|
||||
pilot_username = kwargs.pop("pilot_username", None)
|
||||
|
||||
@@ -10,10 +10,10 @@ class EventQueryResolver(BaseQueryResolver):
|
||||
|
||||
def get_query(
|
||||
self,
|
||||
user_id: Optional[int] = None,
|
||||
object_id: Optional[int] = None,
|
||||
order_by: Optional[list] = None,
|
||||
only_public: Optional[bool] = True,
|
||||
user_id: int | None = None,
|
||||
object_id: int | None = None,
|
||||
order_by: list | None = None,
|
||||
only_public: bool | None = True,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import asyncio
|
||||
import random
|
||||
from typing import Optional
|
||||
from sqlalchemy import delete, insert
|
||||
from sqlalchemy import delete, insert, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
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 background_jobs.weather import download_weather_for_flight
|
||||
from database import models
|
||||
@@ -26,8 +28,8 @@ class FlightQueryResolver(BaseQueryResolver):
|
||||
|
||||
def get_query(
|
||||
self,
|
||||
user_id: Optional[int] = None,
|
||||
only_public: Optional[bool] = False,
|
||||
user_id: int | None = None,
|
||||
only_public: bool | None = False,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
@@ -55,8 +57,27 @@ class FlightQueryResolver(BaseQueryResolver):
|
||||
|
||||
if kwargs.get("point_of_interest_id"):
|
||||
query = (
|
||||
query.join(models.Flight.turn_points)
|
||||
.filter(models.FlightTurnPoint.point_of_interest_id == kwargs["point_of_interest_id"])
|
||||
query
|
||||
.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'):
|
||||
@@ -72,7 +93,7 @@ class FlightMutationResolver(BaseMutationResolver):
|
||||
def __init__(self):
|
||||
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()
|
||||
user_id = context.user_id
|
||||
|
||||
@@ -108,14 +129,14 @@ class FlightMutationResolver(BaseMutationResolver):
|
||||
if input.track is not None:
|
||||
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)
|
||||
|
||||
schedule_background_tasks(flight.id, data, context)
|
||||
|
||||
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
|
||||
async with get_session() as db:
|
||||
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:
|
||||
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'],
|
||||
date_time=flight_data['takeoff_datetime'], type_="takeoff"
|
||||
)
|
||||
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("title_photo_id"):
|
||||
context.background_tasks.add_task(
|
||||
generate_flight_title_photo, flight_id=flight_id, photo_id=flight_data['title_photo_id']
|
||||
)
|
||||
|
||||
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):
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.dialects.mysql import insert
|
||||
from database import models
|
||||
from database.models import flight_plan_has_copilot
|
||||
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.resolvers.base import BaseMutationResolver, BaseQueryResolver
|
||||
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.types import FlightPlan
|
||||
from utils.flight_plan_helpers import refresh_markers_weather_info
|
||||
from utils.list_utils import evenly_spaced_elements
|
||||
from utils.str_utils import random_str
|
||||
|
||||
|
||||
@@ -19,9 +21,9 @@ class FlightPlanQueryResolver(BaseQueryResolver):
|
||||
|
||||
def get_query(
|
||||
self,
|
||||
user_id: Optional[int] = None,
|
||||
only_public: Optional[bool] = False,
|
||||
object_id: Optional[int] = None,
|
||||
user_id: int | None = None,
|
||||
only_public: bool | None = False,
|
||||
object_id: int | None = None,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
@@ -40,6 +42,12 @@ class FlightPlanQueryResolver(BaseQueryResolver):
|
||||
.filter(models.User.public_username == kwargs['username'])
|
||||
)
|
||||
|
||||
if kwargs.get("search"):
|
||||
search = kwargs["search"]
|
||||
query = (
|
||||
query.filter(models.FlightPlan.name.contains(search))
|
||||
)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
@@ -47,61 +55,24 @@ class FlightPlanMutationResolver(BaseMutationResolver):
|
||||
def __init__(self):
|
||||
super().__init__(graphql_type=FlightPlan, model=models.FlightPlan)
|
||||
|
||||
@staticmethod
|
||||
async def save_markers(db, flight_plan: models.FlightPlan, markers: list):
|
||||
position = 0
|
||||
|
||||
for marker in markers:
|
||||
|
||||
if marker.type == 'poi':
|
||||
assert bool(marker.point_of_interest_id)
|
||||
|
||||
if marker.type == 'airport':
|
||||
assert bool(marker.airport_id)
|
||||
|
||||
await models.FlightPlanMarker.create(db, data={
|
||||
"position": position,
|
||||
"flight_plan_id": flight_plan.id,
|
||||
"airport_id": marker.airport_id,
|
||||
"point_of_interest_id": marker.point_of_interest_id,
|
||||
"type": marker.type,
|
||||
"name": marker.name,
|
||||
"gps_latitude": marker.gps_latitude,
|
||||
"gps_longitude": marker.gps_longitude
|
||||
})
|
||||
position += 1
|
||||
|
||||
@staticmethod
|
||||
async def reset_plan_markers(db, flight_plan: models.FlightPlan):
|
||||
await db.execute(
|
||||
delete(models.FlightPlanMarker)
|
||||
.filter(models.FlightPlanMarker.flight_plan_id == flight_plan.id)
|
||||
)
|
||||
|
||||
async def create(self, context, data: CreateFlightPlanInput) -> 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)
|
||||
await self.save_markers(db, flight_plan, data.markers)
|
||||
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 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))
|
||||
copilots = await asyncio.gather(*[
|
||||
handle_combobox_save(db, models.Copilot, copilot, user_id) for copilot in copilots
|
||||
])
|
||||
|
||||
for copilot_id in copilots:
|
||||
await db.execute(insert(flight_plan_has_copilot).values(
|
||||
flight_plan_id=flight_plan_id,
|
||||
copilot_id=copilot_id,
|
||||
token=random_str(64)
|
||||
))
|
||||
|
||||
async def update(self, id: int, data: EditFlightPlanInput, user_id: int) -> FlightPlan:
|
||||
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)
|
||||
@@ -109,19 +80,29 @@ class FlightPlanMutationResolver(BaseMutationResolver):
|
||||
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)
|
||||
await self.save_markers(db, flight_plan_model, data.markers)
|
||||
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 data.markers:
|
||||
markers = data.markers
|
||||
else:
|
||||
markers = (await db.scalars(select(models.FlightPlanMarker).filter(models.FlightPlanMarker.flight_plan_id == id))).all()
|
||||
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,
|
||||
@@ -134,8 +115,49 @@ class FlightPlanMutationResolver(BaseMutationResolver):
|
||||
return flight_plan
|
||||
|
||||
|
||||
def evenly_spaced_elements(lst: list, count: int) -> list:
|
||||
if count > len(lst):
|
||||
return lst
|
||||
interval = (len(lst) - 1) / (count - 1) if count > 1 else 0
|
||||
return [lst[int(round(i * interval))] for i in range(count)]
|
||||
@staticmethod
|
||||
async def save_markers(db, flight_plan: models.FlightPlan, markers: list):
|
||||
position = 0
|
||||
|
||||
marker_objects = []
|
||||
for marker in markers:
|
||||
if marker.type == 'poi':
|
||||
assert bool(marker.point_of_interest_id)
|
||||
|
||||
if marker.type == 'airport':
|
||||
assert bool(marker.airport_id)
|
||||
|
||||
marker_obj = await models.FlightPlanMarker.create(db, data={
|
||||
"position": position,
|
||||
"flight_plan_id": flight_plan.id,
|
||||
"airport_id": marker.airport_id,
|
||||
"point_of_interest_id": marker.point_of_interest_id,
|
||||
"type": marker.type,
|
||||
"name": marker.name,
|
||||
"gps_latitude": marker.gps_latitude,
|
||||
"gps_longitude": marker.gps_longitude
|
||||
})
|
||||
marker_objects.append(marker_obj)
|
||||
position += 1
|
||||
|
||||
return marker_objects
|
||||
|
||||
@staticmethod
|
||||
async def reset_plan_markers(db, flight_plan: models.FlightPlan):
|
||||
await db.execute(
|
||||
delete(models.FlightPlanMarker)
|
||||
.filter(models.FlightPlanMarker.flight_plan_id == flight_plan.id)
|
||||
)
|
||||
|
||||
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))
|
||||
copilots = await asyncio.gather(*[
|
||||
handle_combobox_save(db, models.Copilot, copilot, user_id) for copilot in copilots
|
||||
])
|
||||
|
||||
for copilot_id in copilots:
|
||||
await db.execute(insert(flight_plan_has_copilot).values(
|
||||
flight_plan_id=flight_plan_id,
|
||||
copilot_id=copilot_id,
|
||||
token=random_str(64)
|
||||
))
|
||||
|
||||
@@ -12,8 +12,8 @@ class OrganizationQueryResolver(BaseQueryResolver):
|
||||
|
||||
def get_query(
|
||||
self,
|
||||
object_id: Optional[int] = None,
|
||||
order_by: Optional[list] = None,
|
||||
object_id: int | None = None,
|
||||
order_by: list | None = None,
|
||||
**kwargs
|
||||
):
|
||||
query = (
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import os
|
||||
import shutil
|
||||
from time import time
|
||||
from typing import Optional
|
||||
import sqlalchemy
|
||||
from PIL import Image
|
||||
from graphql import GraphQLError
|
||||
from pydantic import BaseModel
|
||||
from pymysql.err import IntegrityError
|
||||
from sqlalchemy import delete, insert, select
|
||||
from background_jobs.elevation import add_terrain_elevation_to_photo
|
||||
from background_jobs.photo import generate_thumbnail, resize_photo
|
||||
from database import models
|
||||
from database.transaction import get_session
|
||||
from graphql_schema.context import GraphQLContext
|
||||
from graphql_schema.entities.helpers.combobox import handle_combobox_save
|
||||
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
|
||||
from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput
|
||||
@@ -26,10 +29,10 @@ class PhotoQueryResolver(BaseQueryResolver):
|
||||
|
||||
def get_query(
|
||||
self,
|
||||
user_id: Optional[int] = None,
|
||||
object_id: Optional[int] = None,
|
||||
order_by: Optional[list] = None,
|
||||
only_public: Optional[bool] = False,
|
||||
user_id: int | None = None,
|
||||
object_id: int | None = None,
|
||||
order_by: list | None = None,
|
||||
only_public: bool | None = False,
|
||||
*args, **kwargs
|
||||
):
|
||||
query = super().get_query(
|
||||
@@ -61,7 +64,7 @@ class PhotoQueryResolver(BaseQueryResolver):
|
||||
|
||||
class PhotoDetailInfo(BaseModel):
|
||||
filename: str
|
||||
original_filename: str
|
||||
original_filename: str | None
|
||||
path: str
|
||||
flight_id: int
|
||||
|
||||
@@ -71,14 +74,14 @@ class PhotoMutationResolver(BaseMutationResolver):
|
||||
super().__init__(Photo, models.Photo)
|
||||
|
||||
@staticmethod
|
||||
def _copy_original(path: str, filename: str):
|
||||
def _copy_original(path: str, filename: str) -> str:
|
||||
original_filename = "_original_" + filename
|
||||
if not os.path.isfile(path + "/" + original_filename):
|
||||
shutil.copyfile(path + "/" + filename, path + "/" + 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:
|
||||
if not copilot_token:
|
||||
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
|
||||
|
||||
path = get_photo_basepath(flight_id)
|
||||
|
||||
original_filename = None
|
||||
if copy_original:
|
||||
original_filename = self._copy_original(path, filename)
|
||||
|
||||
return PhotoDetailInfo(
|
||||
flight_id=flight_id,
|
||||
path=path,
|
||||
filename=filename,
|
||||
original_filename=self._copy_original(path, filename) # TODO: tohle nedelat v getteru!
|
||||
original_filename=original_filename
|
||||
)
|
||||
|
||||
@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:
|
||||
return
|
||||
|
||||
@@ -165,17 +173,17 @@ class PhotoMutationResolver(BaseMutationResolver):
|
||||
|
||||
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()
|
||||
|
||||
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:
|
||||
data['point_of_interest_id'] = await handle_combobox_save(
|
||||
db,
|
||||
models.PointOfInterest,
|
||||
input.point_of_interest,
|
||||
user_id,
|
||||
context.user_id,
|
||||
extra_data={"description": ""}
|
||||
)
|
||||
|
||||
@@ -187,7 +195,7 @@ class PhotoMutationResolver(BaseMutationResolver):
|
||||
return await self._do_update(db, obj=photo, data=data)
|
||||
|
||||
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)
|
||||
|
||||
@@ -261,12 +269,15 @@ class PhotoMutationResolver(BaseMutationResolver):
|
||||
"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")
|
||||
await self._get_photo_details(id, user_id, copilot_upload_token)
|
||||
# ^^ pokud neexistuje, vyhodi vyjimku
|
||||
await self._get_photo_details(id, context.user_id, copilot_upload_token, copy_original=False)
|
||||
|
||||
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)
|
||||
|
||||
files_to_delete = [
|
||||
|
||||
@@ -11,13 +11,13 @@ from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_i
|
||||
|
||||
@strawberry.input()
|
||||
class ComboboxInput:
|
||||
id: Optional[int] = None
|
||||
id: int | None = None
|
||||
name: str
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_input(model=models.FlightPlanMarker, exclude_fields=["id", "flight_plan_id"])
|
||||
class FlightPlanMarkerInput(BaseGraphqlInputType):
|
||||
position: Optional[int] = 0
|
||||
position: int | None = 0
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
|
||||
@@ -81,19 +81,19 @@ class EditEventInput(BaseGraphqlInputType):
|
||||
class UploadPhotoInput:
|
||||
photo: Upload
|
||||
flight_id: int
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
point_of_interest: Optional[ComboboxInput] = None
|
||||
copilot_upload_token: Optional[str] = None
|
||||
copilot_upload_token: str | None = None
|
||||
|
||||
|
||||
@strawberry.input
|
||||
class EditPhotoInput:
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
point_of_interest: Optional[ComboboxInput] = None
|
||||
copilots: Optional[List[ComboboxInput]] = None
|
||||
aircraft_id: Optional[int] = None
|
||||
aircraft_id: int | None = None
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
@@ -112,12 +112,12 @@ class CropInput(BaseGraphqlInputType):
|
||||
|
||||
@strawberry.input
|
||||
class AdjustmentInput:
|
||||
rotate: Optional[float] = 0
|
||||
crop_after_rotate: Optional[bool] = True,
|
||||
brightness: Optional[float] = 1
|
||||
contrast: Optional[float] = 1
|
||||
saturation: Optional[float] = 1
|
||||
sharpness: Optional[float] = 1
|
||||
rotate: float | None = 0
|
||||
crop_after_rotate: bool | None = True,
|
||||
brightness: float | None = 1
|
||||
contrast: float | None = 1
|
||||
saturation: float | None = 1
|
||||
sharpness: float | None = 1
|
||||
crop: Optional[CropInput] = None
|
||||
|
||||
|
||||
@@ -138,8 +138,8 @@ class EditFlightInput(BaseGraphqlInputType):
|
||||
@strawberry.input()
|
||||
class CreateFlightInput(BaseGraphqlInputType):
|
||||
aircraft: ComboboxInput
|
||||
flight_plan_id: Optional[int] = None
|
||||
name: Optional[str] = None
|
||||
flight_plan_id: int | None = None
|
||||
name: str | None = None
|
||||
gpx_track_file: Optional[Upload] = None # TODO: poresit validaci uploadovaneho souboru!
|
||||
landing_airport: Optional[ComboboxInput] = None
|
||||
takeoff_airport: Optional[ComboboxInput] = None
|
||||
@@ -152,9 +152,9 @@ class CreateFlightInput(BaseGraphqlInputType):
|
||||
class TrackItemInput:
|
||||
point_of_interest: Optional[ComboboxInput] = None
|
||||
airport: Optional[ComboboxInput] = None
|
||||
landing_duration: Optional[int] = None
|
||||
gps_latitude: Optional[float] = None
|
||||
gps_longitude: Optional[float] = None
|
||||
landing_duration: int | None = None
|
||||
gps_latitude: float | None = None
|
||||
gps_longitude: float | None = None
|
||||
|
||||
|
||||
@strawberry_sqlalchemy_input(models.Aircraft, exclude_fields=['id', 'photo_filename'])
|
||||
|
||||
@@ -3,7 +3,7 @@ import math
|
||||
from typing import Optional, List
|
||||
import strawberry
|
||||
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 external.gpx_parser import GPXParser
|
||||
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 (
|
||||
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
|
||||
)
|
||||
from graphql_schema.permissions import IsAuthenticated
|
||||
@@ -141,11 +141,11 @@ class Flight:
|
||||
for key, value in kwargs.items():
|
||||
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):
|
||||
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):
|
||||
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))
|
||||
turn_points: List[FlightTurnPoint] = strawberry.field(resolver=lambda root: flight_turn_points_dataloader.load(root.id))
|
||||
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(
|
||||
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))
|
||||
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))
|
||||
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')
|
||||
)
|
||||
debriefing_notes: Optional[str] = strawberry.field(
|
||||
debriefing_notes: str | None = strawberry.field(
|
||||
permission_classes=[IsAuthenticated],
|
||||
resolver=lambda root: root.debriefing_notes
|
||||
)
|
||||
@@ -181,13 +181,15 @@ class Flight:
|
||||
|
||||
@strawberry_sqlalchemy_type(model=models.FlightPlanMarker)
|
||||
class FlightPlanMarker:
|
||||
pass
|
||||
weather_info: Optional[WeatherInfo] = strawberry.field(
|
||||
resolver=lambda root: weather_info_loader.load(root.weather_info_id)
|
||||
)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class FlightPlanTrack:
|
||||
bearing: Optional[int]
|
||||
distance: Optional[float]
|
||||
bearing: int | None
|
||||
distance: float | None
|
||||
from_: FlightPlanMarker = strawberry.field(name="from")
|
||||
to: Optional[FlightPlanMarker]
|
||||
|
||||
@@ -216,7 +218,7 @@ class FlightPlan:
|
||||
)
|
||||
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):
|
||||
return await flight_plan_copilots_dataloader.load(root.id)
|
||||
|
||||
@@ -245,7 +247,7 @@ class Copilot:
|
||||
flights: List[Flight] = strawberry.field(resolver=resolve_flights)
|
||||
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))
|
||||
token: Optional[str] = strawberry.field(
|
||||
token: str | None = strawberry.field(
|
||||
permission_classes=[IsAuthenticated],
|
||||
resolver=resolve_copilot_token
|
||||
)
|
||||
@@ -271,9 +273,9 @@ class Organization:
|
||||
|
||||
@strawberry_sqlalchemy_type(models.User, exclude_fields=['password_hashed'])
|
||||
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))
|
||||
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')
|
||||
)
|
||||
organizations: List[Organization] = strawberry.field(
|
||||
@@ -283,7 +285,7 @@ class User:
|
||||
|
||||
@strawberry_sqlalchemy_type(models.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)
|
||||
if not is_user_logged_in or only_public:
|
||||
dataloader = public_flights_by_event_dataloader
|
||||
|
||||
@@ -3,13 +3,13 @@ import strawberry
|
||||
from graphql import GraphQLError
|
||||
from passlib.hash import bcrypt
|
||||
from sqlalchemy import select
|
||||
from strawberry import Info
|
||||
from strawberry.file_uploads import Upload
|
||||
from background_jobs.photo import resize_photo
|
||||
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 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 handle_file_upload
|
||||
|
||||
@@ -17,8 +17,7 @@ from utils.file import handle_file_upload
|
||||
@strawberry.type
|
||||
class UserQueries:
|
||||
@strawberry.field()
|
||||
@error_logging
|
||||
async def user(root, info, username: str) -> User:
|
||||
async def user(root, info: Info, username: str) -> User:
|
||||
if len(username) == 0:
|
||||
raise GraphQLError("Username not set!")
|
||||
|
||||
@@ -28,10 +27,8 @@ class UserQueries:
|
||||
|
||||
return user
|
||||
|
||||
@strawberry.field()
|
||||
@authenticated_user_only()
|
||||
@error_logging
|
||||
async def logged_user(root, info) -> User:
|
||||
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||
async def logged_user(root, info: Info) -> User:
|
||||
async with get_session() as db:
|
||||
user_model = (await db.scalars(
|
||||
select(models.User).filter_by(id=info.context.user_id)
|
||||
@@ -44,17 +41,16 @@ class UserQueries:
|
||||
class EditUserMutation:
|
||||
@strawberry.input
|
||||
class EditUserInput:
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
public_username: Optional[str] = None
|
||||
old_password: Optional[str] = None
|
||||
new_password: Optional[str] = None
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
public_username: str | None = None
|
||||
old_password: str | None = None
|
||||
new_password: str | None = None
|
||||
avatar_image: Optional[Upload] = None
|
||||
title_image: Optional[Upload] = None
|
||||
|
||||
@strawberry.mutation
|
||||
@authenticated_user_only()
|
||||
async def edit_logged_user(root, info, input: EditUserInput) -> User:
|
||||
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||
async def edit_logged_user(root, info: Info, input: EditUserInput) -> User:
|
||||
async with get_session() as db:
|
||||
user = (await db.scalars(
|
||||
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 typing import Set
|
||||
|
||||
from datetime import datetime
|
||||
import strawberry
|
||||
from fastapi_jwt import JwtAuthorizationCredentials
|
||||
from fastapi_jwt.jwt import JwtAccessBearerCookie
|
||||
from starlette.background import BackgroundTasks
|
||||
from strawberry.extensions import SchemaExtension
|
||||
from strawberry.fastapi import BaseContext
|
||||
from strawberry.extensions import SchemaExtension, ValidationCache, ParserCache
|
||||
from graphql_schema.extensions.schema.error_logging import ErrorLogging
|
||||
from logger import log
|
||||
from .mutation import Mutation
|
||||
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):
|
||||
def on_request_start(self):
|
||||
print("request start")
|
||||
def on_request_end(self):
|
||||
# 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):
|
||||
print("request end")
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class GraphQLContext(BaseContext):
|
||||
user_id: int
|
||||
organization_ids: Set[int]
|
||||
jwt_auth_credentials: JwtAuthorizationCredentials
|
||||
jwt: JwtAccessBearerCookie
|
||||
background_tasks: BackgroundTasks
|
||||
request_time_ms = round((request_end - request_start).microseconds / 1000)
|
||||
op_name = self.execution_context.operation_name
|
||||
log.info(f"Operation {op_name} took {request_time_ms}ms")
|
||||
|
||||
|
||||
schema = strawberry.Schema(
|
||||
query=Query,
|
||||
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_
|
||||
|
||||
|
||||
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:
|
||||
exclude_fields = []
|
||||
|
||||
@@ -47,7 +47,7 @@ def strawberry_sqlalchemy_input(
|
||||
model,
|
||||
exclude_fields: Optional[typing.Union[List, typing.Tuple]] = None,
|
||||
all_optional: bool = False
|
||||
) -> typing.Callable[[...], strawberry.object_type]:
|
||||
) -> typing.Callable[[...], strawberry.type]:
|
||||
if exclude_fields is None:
|
||||
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 sys
|
||||
from colorlog import ColoredFormatter
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(logging.DEBUG)
|
||||
|
||||
# Create handlers for logging to the standard output and a file
|
||||
stdoutHandler = logging.StreamHandler(stream=sys.stdout)
|
||||
|
||||
# Set the log levels on the handlers
|
||||
stdoutHandler.setLevel(logging.DEBUG)
|
||||
|
||||
# Create a log format using Log Record attributes
|
||||
fmt = logging.Formatter(
|
||||
"%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d >>> %(message)s"
|
||||
fmt = ColoredFormatter(
|
||||
"%(log_color)s[%(levelname)s]%(reset)s %(blue)s%(message)s%(reset)s %(light_black)s[%(filename)s:%(lineno)d]]%(reset)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)
|
||||
|
||||
# Add each handler to the Logger object
|
||||
log.addHandler(stdoutHandler)
|
||||
|
||||
+28
-105
@@ -1,22 +1,18 @@
|
||||
import sentry_sdk
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
from fastapi import FastAPI, APIRouter, Security, HTTPException
|
||||
from fastapi_jwt import JwtAuthorizationCredentials, JwtAccessBearerCookie, JwtRefreshBearerCookie
|
||||
from fastapi import FastAPI, HTTPException, APIRouter
|
||||
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 starlette.background import BackgroundTasks
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.responses import Response, JSONResponse
|
||||
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.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 routers import forgotten_password, auth, photo_preview, graphql
|
||||
|
||||
|
||||
class StaticFilesCache(StaticFiles):
|
||||
@@ -31,24 +27,26 @@ class StaticFilesCache(StaticFiles):
|
||||
|
||||
|
||||
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):
|
||||
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(
|
||||
dsn=SENTRY_DSN,
|
||||
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(
|
||||
@@ -100,94 +98,19 @@ class App:
|
||||
)
|
||||
|
||||
def setup_routes(self, app: FastAPI):
|
||||
@self.api_router.post("/registration", status_code=201)
|
||||
async def registration(user: RegistrationInput, background_tasks: BackgroundTasks):
|
||||
return await RegistrationEndpoint().on_post(user, background_tasks)
|
||||
api_router = APIRouter()
|
||||
|
||||
@self.api_router.post("/login")
|
||||
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")
|
||||
@api_router.post("/contact", summary="Send email from contact form")
|
||||
async def contact_form_message(input: ContactInput, background_tasks: BackgroundTasks):
|
||||
return await ContactEndpoint().on_post(input, background_tasks)
|
||||
|
||||
@self.api_router.get("/sitemap.xml")
|
||||
@api_router.get("/sitemap.xml")
|
||||
async def sitemap():
|
||||
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
|
||||
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
|
||||
from typing import Optional
|
||||
from config import API_URL
|
||||
from logger import log
|
||||
|
||||
PHOTO_BASE_PATH = ""
|
||||
AIRCRAFT_BASE_PATH = ""
|
||||
FLIGHT_BASE_PATH = ""
|
||||
FLIGHT_GPX_TRACK_PATH = "/app/uploads/tracks"
|
||||
AIRCRAFT_UPLOAD_DEST_PATH = "/app/uploads/aircrafts/"
|
||||
|
||||
|
||||
def get_photo_basepath(flight_id: int) -> str:
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
from database import models
|
||||
from database.transaction import get_session
|
||||
import asyncio
|
||||
import sys
|
||||
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:
|
||||
@staticmethod
|
||||
async def get_plans() -> list:
|
||||
async with get_session() as db:
|
||||
markers_without_weather = (await db.execute(
|
||||
select(models.FlightPlanMarker).filter(models.FlightPlanMarker.weather_info_id.is_(None))
|
||||
))
|
||||
flight_plan_ids = {m.flight_plan_id for m in markers_without_weather}
|
||||
async def get_plans(db: AsyncSession) -> list[Any] | Result[tuple[FlightPlan, Any]]:
|
||||
markers_without_weather = (await db.scalars(
|
||||
select(models.FlightPlanMarker).filter(models.FlightPlanMarker.weather_info_id.is_(None))
|
||||
))
|
||||
flight_plan_ids = {m.flight_plan_id for m in markers_without_weather}
|
||||
flight_plan_ids.add(11)
|
||||
if not flight_plan_ids:
|
||||
return []
|
||||
|
||||
if not flight_plan_ids:
|
||||
return []
|
||||
return (await db.execute(
|
||||
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())
|
||||
|
||||
@@ -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:
|
||||
query = (
|
||||
select(models.Airport, func.coalesce(6371 * func.acos(
|
||||
|
||||
+7
-7
@@ -38,7 +38,7 @@ class PhotoEditor:
|
||||
self.img = Image.open(f"{path}/{filename}")
|
||||
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:
|
||||
raise ValueError("Set either new_width or new_height")
|
||||
|
||||
@@ -102,10 +102,10 @@ class PhotoEditor:
|
||||
|
||||
def adjust(
|
||||
self,
|
||||
brightness: Optional[float] = None,
|
||||
contrast: Optional[float] = None,
|
||||
saturation: Optional[float] = None,
|
||||
sharpness: Optional[float] = None
|
||||
brightness: float | None = None,
|
||||
contrast: float | None = None,
|
||||
saturation: float | None = None,
|
||||
sharpness: float | None = None
|
||||
):
|
||||
adjustments = [
|
||||
(Brightness, brightness),
|
||||
@@ -127,8 +127,8 @@ class PhotoEditor:
|
||||
return img_io
|
||||
|
||||
def write_to_file(
|
||||
self, quality: int = 90, dest_path: Optional[str] = None, dest_filename: Optional[str] = None,
|
||||
format_: Optional[str] = "JPEG"
|
||||
self, quality: int = 90, dest_path: str | None = None, dest_filename: str | None = None,
|
||||
format_: str | None = "JPEG"
|
||||
) -> str:
|
||||
check_directories(dest_path or self.path)
|
||||
|
||||
|
||||
@@ -12,3 +12,10 @@ def chunks(items: list | dict, n: int) -> typing.Generator:
|
||||
it = iter(items)
|
||||
for i in range(0, len(items), n):
|
||||
yield {k: items[k] for k in islice(it, n)}
|
||||
|
||||
|
||||
def evenly_spaced_elements(lst: list, count: int) -> list:
|
||||
if count > len(lst):
|
||||
return lst
|
||||
interval = (len(lst) - 1) / (count - 1) if count > 1 else 0
|
||||
return [lst[int(round(i * interval))] for i in range(count)]
|
||||
|
||||
Reference in New Issue
Block a user