Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e8f677b6f | ||
|
|
6553da41f2 | ||
|
|
420774f1b4 | ||
|
|
888351baac | ||
|
|
d794208193 | ||
|
|
71911c0905 | ||
|
|
a7fda95c29 | ||
|
|
2f824e264f | ||
|
|
beee16bf80 | ||
|
|
77c9242944 | ||
|
|
c6316262e5 | ||
|
|
d31f1c0c61 | ||
|
|
ca3f6ef07c | ||
|
|
186dbd0720 | ||
|
|
365a1fc1b0 | ||
|
|
0a6b19f334 | ||
|
|
3081580c21 | ||
|
|
94639da820 | ||
|
|
9e6b0b574c | ||
|
|
10bc0847b6 | ||
|
|
e541508f75 | ||
|
|
ac3b496459 | ||
|
|
2bf737b9d6 | ||
|
|
ba5e73108f | ||
|
|
94613a5b1b |
@@ -2,5 +2,6 @@
|
|||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
exclude =
|
exclude =
|
||||||
venv
|
venv
|
||||||
|
.venv
|
||||||
__pycache__
|
__pycache__
|
||||||
alembic
|
alembic
|
||||||
@@ -10,3 +10,5 @@ uploads/*
|
|||||||
!uploads/.gitkeep
|
!uploads/.gitkeep
|
||||||
src/emails/templates/html/*
|
src/emails/templates/html/*
|
||||||
.env
|
.env
|
||||||
|
/static/airports/
|
||||||
|
/static/airports/.gitkeep
|
||||||
|
|||||||
+9
-7
@@ -7,17 +7,19 @@ COPY ./src/emails/templates/mjml /app/templates
|
|||||||
RUN mjml /app/templates/* -o /app/templates/html/
|
RUN mjml /app/templates/* -o /app/templates/html/
|
||||||
|
|
||||||
|
|
||||||
FROM python:3.11-alpine AS app
|
FROM python:3.13-alpine AS app
|
||||||
|
|
||||||
|
# protoze xfs uzivatel ma id=33
|
||||||
|
# RUN deluser xfs
|
||||||
|
|
||||||
|
# uzivatel www-data v alpine neexistuje
|
||||||
|
RUN delgroup www-data && addgroup -g 33 -S www-data && adduser -u 33 -D -S -G www-data www-data
|
||||||
|
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add --no-cache tzdata mariadb-client mariadb-dev && \
|
apk add --no-cache tzdata mariadb-client mariadb-dev && \
|
||||||
pip install --upgrade pip
|
pip install --upgrade pip
|
||||||
|
|
||||||
# protoze xfs uzivatel ma id=33
|
|
||||||
RUN deluser xfs
|
|
||||||
|
|
||||||
# uzivatel www-data v alpine neexistuje
|
|
||||||
RUN delgroup www-data && addgroup -g 33 -S www-data && adduser -u 33 -D -S -G www-data www-data
|
|
||||||
|
|
||||||
COPY requirements.txt /app/requirements.txt
|
COPY requirements.txt /app/requirements.txt
|
||||||
COPY --from=email_builder /app/templates/html/ /app/src/email/templates/html/
|
COPY --from=email_builder /app/templates/html/ /app/src/email/templates/html/
|
||||||
@@ -34,4 +36,4 @@ COPY . /app
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
CMD ["uvicorn", "src.asgi:app", "--reload", "--reload-dir=/app", "--port=8000", "--host=0.0.0.0", "--log-level=debug"]
|
CMD [ "gunicorn", "src.asgi:app", "--config=/app/config/gunicorn.py" ]
|
||||||
|
|||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
"""add weather info to flight plan markers
|
||||||
|
|
||||||
|
Revision ID: 8a67af3909aa
|
||||||
|
Revises: 790ba00fc021
|
||||||
|
Create Date: 2024-05-16 06:19:13.112911
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '8a67af3909aa'
|
||||||
|
down_revision = '790ba00fc021'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('flight_plan_marker', sa.Column('weather_info_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'flight_plan_marker', 'weather_info', ['weather_info_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'flight_plan_marker', type_='foreignkey')
|
||||||
|
op.drop_column('flight_plan_marker', 'weather_info_id')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""add flight_plan.description
|
||||||
|
|
||||||
|
Revision ID: a3e1f1a37899
|
||||||
|
Revises: 8a67af3909aa
|
||||||
|
Create Date: 2024-06-11 16:46:29.786830
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'a3e1f1a37899'
|
||||||
|
down_revision = '8a67af3909aa'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('flight_plan', sa.Column('description', sa.Text(), server_default='', nullable=False))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('flight_plan', 'description')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""add track to flight
|
||||||
|
|
||||||
|
Revision ID: f9e1e8a01683
|
||||||
|
Revises: a3e1f1a37899
|
||||||
|
Create Date: 2024-06-24 16:08:01.084570
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'f9e1e8a01683'
|
||||||
|
down_revision = 'a3e1f1a37899'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('track',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('bounds', mysql.JSON(), nullable=False),
|
||||||
|
sa.Column('created_by_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['created_by_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('track_point',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('timestamp', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('track_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('gps_latitude', sa.Float(), nullable=False),
|
||||||
|
sa.Column('gps_longitude', sa.Float(), nullable=False),
|
||||||
|
sa.Column('elevation', sa.Float(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['track_id'], ['track.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.add_column('flight', sa.Column('track_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'flight', 'track', ['track_id'], ['id'])
|
||||||
|
op.add_column('flight_plan', sa.Column('track_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'flight_plan', 'track', ['track_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'flight_plan', type_='foreignkey')
|
||||||
|
op.drop_column('flight_plan', 'track_id')
|
||||||
|
op.drop_constraint(None, 'flight', type_='foreignkey')
|
||||||
|
op.drop_column('flight', 'track_id')
|
||||||
|
op.drop_table('track_point')
|
||||||
|
op.drop_table('track')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""add flight.flight_plan_id fk
|
||||||
|
|
||||||
|
Revision ID: 989152b7184a
|
||||||
|
Revises: f9e1e8a01683
|
||||||
|
Create Date: 2024-06-27 17:09:19.933094
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '989152b7184a'
|
||||||
|
down_revision = 'f9e1e8a01683'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('flight', sa.Column('flight_plan_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'flight', 'flight_plan', ['flight_plan_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'flight', type_='foreignkey')
|
||||||
|
op.drop_column('flight', 'flight_plan_id')
|
||||||
|
# ### end Alembic commands ###
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
"""Add poi and airport id to flight plan track marker
|
||||||
|
|
||||||
|
Revision ID: 8cc01c03e980
|
||||||
|
Revises: 989152b7184a
|
||||||
|
Create Date: 2024-06-30 00:27:08.976891
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '8cc01c03e980'
|
||||||
|
down_revision = '989152b7184a'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('airport', 'openaip_id',
|
||||||
|
existing_type=mysql.VARCHAR(length=32),
|
||||||
|
nullable=True)
|
||||||
|
op.add_column('flight_plan_marker', sa.Column('airport_id', sa.Integer(), nullable=True))
|
||||||
|
op.add_column('flight_plan_marker', sa.Column('point_of_interest_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'flight_plan_marker', 'point_of_interest', ['point_of_interest_id'], ['id'])
|
||||||
|
op.create_foreign_key(None, 'flight_plan_marker', 'airport', ['airport_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'flight_plan_marker', type_='foreignkey')
|
||||||
|
op.drop_constraint(None, 'flight_plan_marker', type_='foreignkey')
|
||||||
|
op.drop_column('flight_plan_marker', 'point_of_interest_id')
|
||||||
|
op.drop_column('flight_plan_marker', 'airport_id')
|
||||||
|
op.alter_column('airport', 'openaip_id',
|
||||||
|
existing_type=mysql.VARCHAR(length=32),
|
||||||
|
nullable=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
"""add speed, elevation, altitude to track
|
||||||
|
|
||||||
|
Revision ID: 03de3f7f8fdd
|
||||||
|
Revises: 8cc01c03e980
|
||||||
|
Create Date: 2024-07-11 07:45:59.376941
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '03de3f7f8fdd'
|
||||||
|
down_revision = '8cc01c03e980'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('track', sa.Column('min_speed', sa.Float(), nullable=True))
|
||||||
|
op.add_column('track', sa.Column('max_speed', sa.Float(), nullable=True))
|
||||||
|
op.add_column('track', sa.Column('avg_speed', sa.Float(), nullable=True))
|
||||||
|
op.add_column('track', sa.Column('max_altitude', sa.Float(), nullable=True))
|
||||||
|
op.add_column('track', sa.Column('avg_altitude', sa.Float(), nullable=True))
|
||||||
|
op.add_column('track', sa.Column('total_duration', sa.Integer(), nullable=True, comment='Total duration in seconds'))
|
||||||
|
op.add_column('track_point', sa.Column('terrain_elevation', sa.Float(), nullable=True))
|
||||||
|
op.add_column('track_point', sa.Column('speed', sa.Float(), nullable=True))
|
||||||
|
op.add_column('track_point', sa.Column('altitude', sa.Float(), nullable=True))
|
||||||
|
op.add_column('track_point', sa.Column('magnetic_variation', sa.Float(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('track_point', 'magnetic_variation')
|
||||||
|
op.drop_column('track_point', 'altitude')
|
||||||
|
op.drop_column('track_point', 'speed')
|
||||||
|
op.drop_column('track_point', 'terrain_elevation')
|
||||||
|
op.drop_column('track', 'total_duration')
|
||||||
|
op.drop_column('track', 'avg_altitude')
|
||||||
|
op.drop_column('track', 'max_altitude')
|
||||||
|
op.drop_column('track', 'avg_speed')
|
||||||
|
op.drop_column('track', 'max_speed')
|
||||||
|
op.drop_column('track', 'min_speed')
|
||||||
|
# ### end Alembic commands ###
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
"""delete duplicated column with altitude
|
||||||
|
|
||||||
|
Revision ID: e3620deb41b6
|
||||||
|
Revises: 03de3f7f8fdd
|
||||||
|
Create Date: 2024-07-15 06:35:13.536320
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'e3620deb41b6'
|
||||||
|
down_revision = '03de3f7f8fdd'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
op.rename_table("flight_track", "flight_turn_point")
|
||||||
|
op.create_index(op.f('ix_track_point_timestamp'), 'track_point', ['timestamp'], unique=False)
|
||||||
|
op.drop_column('track_point', 'elevation')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('track_point', sa.Column('elevation', mysql.FLOAT(), nullable=True))
|
||||||
|
op.drop_index(op.f('ix_track_point_timestamp'), table_name='track_point')
|
||||||
|
op.rename_table("flight_turn_point", "flight_track")
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""prejmenovani date_time u WeatherInfo
|
||||||
|
|
||||||
|
Revision ID: db036549f318
|
||||||
|
Revises: e3620deb41b6
|
||||||
|
Create Date: 2025-09-16 09:26:04.082396
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'db036549f318'
|
||||||
|
down_revision = 'e3620deb41b6'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_unique_constraint(None, 'airport', ['openaip_id'])
|
||||||
|
op.create_index(op.f('ix_flight_turn_point_order'), 'flight_turn_point', ['order'], unique=False)
|
||||||
|
op.alter_column(
|
||||||
|
"weather_info", "datetime", new_column_name="date_time", existing_type=sa.DateTime, nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.alter_column(
|
||||||
|
"weather_info", "date_time", new_column_name="datetime",
|
||||||
|
existing_type=sa.DateTime, nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
op.drop_index(op.f('ix_flight_turn_point_order'), table_name='flight_turn_point')
|
||||||
|
op.drop_constraint(None, 'airport', type_='unique')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
# https://docs.gunicorn.org/en/stable/settings.html#settings
|
||||||
|
|
||||||
|
workers = 1 if os.environ.get("APP_ENV") == "development" else os.cpu_count()
|
||||||
|
bind = "0.0.0.0:8000"
|
||||||
|
worker_class = "uvicorn.workers.UvicornWorker"
|
||||||
|
worker_connections = 1000 # default
|
||||||
|
threads = 1 # default
|
||||||
|
|
||||||
|
# restart workeru
|
||||||
|
max_requests = 500000
|
||||||
|
max_requests_jitter = 10000
|
||||||
|
|
||||||
|
timeout = 30 # default
|
||||||
|
graceful_timeout = 30 # default
|
||||||
|
|
||||||
|
keepalive = 600 # s klientem udrzuje nginx
|
||||||
@@ -3,7 +3,6 @@ version: "3.3"
|
|||||||
services:
|
services:
|
||||||
api:
|
api:
|
||||||
image: "docker.kvacek.cz/poletani/api:latest"
|
image: "docker.kvacek.cz/poletani/api:latest"
|
||||||
command: "uvicorn src.asgi:app --port=8000 --host=0.0.0.0 --workers 4"
|
|
||||||
network_mode: "host"
|
network_mode: "host"
|
||||||
restart: "always"
|
restart: "always"
|
||||||
env_file:
|
env_file:
|
||||||
@@ -12,3 +11,4 @@ services:
|
|||||||
SENTRY_DSN: "https://184d1d6bd39f4fbb94804ae84e9afdd5@o472821.ingest.sentry.io/5506983"
|
SENTRY_DSN: "https://184d1d6bd39f4fbb94804ae84e9afdd5@o472821.ingest.sentry.io/5506983"
|
||||||
volumes:
|
volumes:
|
||||||
- ./uploads:/app/uploads
|
- ./uploads:/app/uploads
|
||||||
|
- ./static:/app/static
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ services:
|
|||||||
- 3306:3306
|
- 3306:3306
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
environment:
|
||||||
|
MARIADB_ROOT_PASSWORD: "rootroot"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
# - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
# - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
||||||
- db-data:/var/lib/mysql:rw
|
- db-data:/var/lib/mysql:rw
|
||||||
|
|||||||
+19
-13
@@ -1,16 +1,22 @@
|
|||||||
fastapi==0.110.0
|
fastapi==0.115.7
|
||||||
fastapi-jwt==0.2.0
|
fastapi-jwt[authlib]==0.3.0
|
||||||
fastapi-mail==1.4.1
|
bcrypt==4.2.1
|
||||||
strawberry-graphql[fastapi]==0.217.1
|
fastapi-mail==1.4.2
|
||||||
uvicorn==0.26.0
|
strawberry-graphql[fastapi]==0.258.0
|
||||||
sqlalchemy[asyncio] == 2.0.25
|
|
||||||
|
gunicorn == 23.0.0
|
||||||
|
uvicorn[standard] == 0.34.0
|
||||||
|
|
||||||
|
sqlalchemy[asyncio] == 2.0.54
|
||||||
aiomysql==0.2.0
|
aiomysql==0.2.0
|
||||||
alembic==1.13.1
|
alembic==1.14.1
|
||||||
passlib==1.7.4
|
passlib==1.7.4
|
||||||
pydantic==2.5.3
|
pydantic==2.10.6
|
||||||
sentry-sdk[fastapi]
|
sentry-sdk[fastapi]
|
||||||
pillow==10.2.0
|
pillow==11.1.0
|
||||||
exif==1.6.0
|
exif==1.6.1
|
||||||
aiocache==0.12.2
|
aiocache==0.12.3
|
||||||
aiohttp==3.9.3
|
aiohttp==3.11.11
|
||||||
lxml==5.1.0
|
lxml==5.3.0
|
||||||
|
|
||||||
|
colorlog
|
||||||
|
|||||||
@@ -1,40 +1,34 @@
|
|||||||
from aiohttp import ClientResponseError
|
from sqlalchemy import select
|
||||||
from database import models
|
from database import models
|
||||||
from database.transaction import get_session
|
from database.transaction import get_session
|
||||||
from external.elevation import elevation_api
|
from decorators.retryable import retryable
|
||||||
from external.gpx_parser import GPXParser
|
from external.elevation import elevation_api, update_track_points_elevation
|
||||||
from paths import FLIGHT_GPX_TRACK_PATH
|
|
||||||
|
|
||||||
|
|
||||||
async def add_terrain_elevation_to_flight(flight_id: int, gpx_filename: str):
|
@retryable
|
||||||
gpx_parser = GPXParser(f"{FLIGHT_GPX_TRACK_PATH}/{gpx_filename}")
|
async def add_terrain_elevation_to_flight(flight_id: int):
|
||||||
coordinates = await gpx_parser.get_coordinates_all()
|
async with get_session() as db:
|
||||||
|
track_points = (await db.scalars(
|
||||||
try:
|
select(models.TrackPoint)
|
||||||
elevation = await elevation_api.get_elevation_for_points(coordinates)
|
.join(models.TrackPoint.track)
|
||||||
tree_with_elevation = gpx_parser.add_terrain_elevation(elevation)
|
.join(models.Track.flight)
|
||||||
output_name = f"terrain_{gpx_filename}"
|
.filter(models.Flight.id == flight_id)
|
||||||
gpx_parser.write(tree_with_elevation, f"{FLIGHT_GPX_TRACK_PATH}/{output_name}")
|
.filter(models.TrackPoint.terrain_elevation.is_(None))
|
||||||
|
)).all()
|
||||||
async with get_session() as db:
|
await update_track_points_elevation(db, track_points)
|
||||||
await models.Flight.update(
|
|
||||||
db, {"gpx_track_filename": output_name, "has_terrain_elevation": True},
|
|
||||||
id=flight_id
|
|
||||||
)
|
|
||||||
except ClientResponseError as e:
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
|
|
||||||
|
@retryable
|
||||||
async def add_terrain_elevation_to_photo(photo):
|
async def add_terrain_elevation_to_photo(photo):
|
||||||
try:
|
try:
|
||||||
elevation = await elevation_api.get_elevation_for_points([
|
elevation = await elevation_api.get_elevation_for_points(
|
||||||
{"lat": photo.gps_latitude, "lng": photo.gps_longitude}
|
[{"lat": photo.gps_latitude, "lng": photo.gps_longitude}]
|
||||||
])
|
)
|
||||||
if not elevation:
|
if not elevation:
|
||||||
print("Cannot get elevation")
|
print("Cannot get elevation")
|
||||||
return
|
return
|
||||||
|
|
||||||
terrain_elevation = elevation[0]['elevation']
|
terrain_elevation = elevation[0].elevation
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
await models.Photo.update(db_session=db, obj=photo, data={"terrain_elevation": terrain_elevation})
|
await models.Photo.update(db_session=db, obj=photo, data={"terrain_elevation": terrain_elevation})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
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):
|
||||||
|
# Load the base image
|
||||||
|
base_image = Image.open(base_image_path)
|
||||||
|
base_width, base_height = base_image.size
|
||||||
|
|
||||||
|
# Calculate the new dimensions for cropping to a 1.9:1 aspect ratio
|
||||||
|
new_width = base_width
|
||||||
|
new_height = int(new_width / 1.9)
|
||||||
|
if new_height > base_height:
|
||||||
|
new_height = base_height
|
||||||
|
new_width = int(new_height * 1.9)
|
||||||
|
|
||||||
|
# Center crop the base image to the new dimensions
|
||||||
|
left = (base_width - new_width) // 2
|
||||||
|
top = (base_height - new_height) // 2
|
||||||
|
right = (base_width + new_width) // 2
|
||||||
|
bottom = (base_height + new_height) // 2
|
||||||
|
base_image = base_image.crop((left, top, right, bottom))
|
||||||
|
base_width, base_height = base_image.size
|
||||||
|
|
||||||
|
# Load the avatar image
|
||||||
|
avatar_image = Image.open(avatar_image_path)
|
||||||
|
avatar_size = int(base_height * 0.3) # Set the size of the avatar
|
||||||
|
avatar_image = avatar_image.resize((avatar_size, avatar_size), Image.LANCZOS)
|
||||||
|
|
||||||
|
# Create a mask to make the avatar circular
|
||||||
|
mask = Image.new('L', (avatar_size, avatar_size), 0)
|
||||||
|
draw = ImageDraw.Draw(mask)
|
||||||
|
draw.ellipse((0, 0, avatar_size, avatar_size), fill=255)
|
||||||
|
|
||||||
|
# Apply the mask to the avatar image
|
||||||
|
avatar_image = ImageOps.fit(avatar_image, mask.size, centering=(0.5, 0.5))
|
||||||
|
avatar_image.putalpha(mask)
|
||||||
|
|
||||||
|
# Create a border around the avatar
|
||||||
|
border_size = int(avatar_size * 0.03)
|
||||||
|
border_image = Image.new('RGBA', (avatar_size + 2 * border_size, avatar_size + 2 * border_size), (255, 255, 255, 0))
|
||||||
|
border_mask = Image.new('L', (avatar_size + 2 * border_size, avatar_size + 2 * border_size), 0)
|
||||||
|
border_draw = ImageDraw.Draw(border_mask)
|
||||||
|
border_draw.ellipse((0, 0, avatar_size + 2 * border_size, avatar_size + 2 * border_size), fill=255)
|
||||||
|
border_draw.ellipse((border_size, border_size, avatar_size + border_size, avatar_size + border_size), fill=0)
|
||||||
|
|
||||||
|
avatar_margin = int(avatar_size * 0.1)
|
||||||
|
|
||||||
|
border_image.putalpha(border_mask)
|
||||||
|
border_image.paste(avatar_image, (border_size, border_size), avatar_image)
|
||||||
|
|
||||||
|
# Calculate the position to paste the avatar (bottom right corner)
|
||||||
|
position = (base_width - avatar_size - 2 * border_size - avatar_margin, base_height - avatar_size - 2 * border_size - avatar_margin)
|
||||||
|
|
||||||
|
# Paste the avatar with border onto the base image
|
||||||
|
base_image.paste(border_image, position, border_image)
|
||||||
|
|
||||||
|
# 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,25 +1,20 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Literal
|
from typing import Literal, Any
|
||||||
from database import models
|
from database import models
|
||||||
from database.transaction import get_session
|
from database.transaction import get_session
|
||||||
from external.weather import weather_api
|
from external.weather import weather_api
|
||||||
from logger import log
|
from logger import log
|
||||||
|
|
||||||
|
|
||||||
async def download_weather(date_time: datetime, flight_id: int, airport_id: int, type_: Literal['landing', 'takeoff']):
|
async def get_weather(date_time: datetime | str, gps: tuple[float, float]) -> dict[str, Any] | None:
|
||||||
async with get_session() as db:
|
|
||||||
airport = await models.Airport.get_one(db, id=airport_id)
|
|
||||||
gps = (airport.gps_latitude, airport.gps_longitude)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
weather = await weather_api.get_weather_for_hour(date_time.astimezone(), gps=gps)
|
weather = await weather_api.get_weather_for_hour(date_time.astimezone(), gps=gps)
|
||||||
log.warning(weather)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Error in downloading weather: {e}")
|
log.error(f"Error in downloading weather: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
data = {
|
return {
|
||||||
"datetime": weather['datetime'],
|
"date_time": date_time, # nemuzu pouzit weather['datetime'], protoze to mi vraci jen cele hodiny
|
||||||
"qnh": weather['pressure_msl'],
|
"qnh": weather['pressure_msl'],
|
||||||
"temperature_surface": weather['temperature_2m'],
|
"temperature_surface": weather['temperature_2m'],
|
||||||
"dewpoint_surface": weather['dewpoint_2m'],
|
"dewpoint_surface": weather['dewpoint_2m'],
|
||||||
@@ -30,6 +25,14 @@ async def download_weather(date_time: datetime, flight_id: int, airport_id: int,
|
|||||||
"wind_direction_surface": weather['winddirection_10m'],
|
"wind_direction_surface": weather['winddirection_10m'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def download_weather_for_flight(date_time: datetime, flight_id: int, airport_id: int, type_: Literal['landing', 'takeoff']):
|
||||||
|
async with get_session() as db:
|
||||||
|
airport = await models.Airport.get_one(db, id=airport_id)
|
||||||
|
gps = (airport.gps_latitude, airport.gps_longitude)
|
||||||
|
|
||||||
|
data = await get_weather(date_time, gps)
|
||||||
|
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
flight = await models.Flight.get_one(db, id=flight_id)
|
flight = await models.Flight.get_one(db, id=flight_id)
|
||||||
existing_weather_id = getattr(flight, f'{type_}_weather_info_id')
|
existing_weather_id = getattr(flight, f'{type_}_weather_info_id')
|
||||||
@@ -39,3 +42,29 @@ async def download_weather(date_time: datetime, flight_id: int, airport_id: int,
|
|||||||
else:
|
else:
|
||||||
weather_model = await models.WeatherInfo.create(db, data=data)
|
weather_model = await models.WeatherInfo.create(db, data=data)
|
||||||
await models.Flight.update(db, obj=flight, data={f"{type_}_weather_info_id": weather_model.id})
|
await models.Flight.update(db, obj=flight, data={f"{type_}_weather_info_id": weather_model.id})
|
||||||
|
|
||||||
|
|
||||||
|
async def download_weather_for_plan_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:
|
||||||
|
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.FlightPlanMarker.update(
|
||||||
|
db,
|
||||||
|
id=marker_id,
|
||||||
|
data={"weather_info_id": weather_model.id}
|
||||||
|
)
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
APP_DEBUG = True
|
APP_DEBUG = os.environ.get("APP_ENV", 'production') == 'development'
|
||||||
GRAPHIQL = True
|
GRAPHIQL = True
|
||||||
|
|
||||||
REFRESH_TOKEN_VALIDITY_DAYS = 30
|
REFRESH_TOKEN_VALIDITY_DAYS = 30
|
||||||
|
|||||||
@@ -1,454 +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')
|
|
||||||
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['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'))
|
|
||||||
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=False, 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
|
|
||||||
|
|
||||||
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()
|
|
||||||
# notes: Mapped['AircraftNotes'] = relationship()
|
|
||||||
|
|
||||||
|
|
||||||
# class AircraftNotes(BaseModel):
|
|
||||||
# __tablename__ = "aircraft_notes"
|
|
||||||
#
|
|
||||||
# id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
# aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=False)
|
|
||||||
# name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
||||||
# description: Mapped[str] = mapped_column(Text, nullable=False)
|
|
||||||
# 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())
|
|
||||||
#
|
|
||||||
# created_by: Mapped['User'] = relationship()
|
|
||||||
# aircraft: Mapped['Aircraft'] = 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 FlightTrack(BaseModel):
|
|
||||||
__tablename__ = "flight_track"
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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)
|
|
||||||
gpx_track_filename: Mapped[str] = mapped_column(String(128), nullable=True)
|
|
||||||
has_terrain_elevation: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
||||||
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])
|
|
||||||
track: Mapped['FlightTrack'] = 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])
|
|
||||||
|
|
||||||
|
|
||||||
class Copilot(BaseModel):
|
|
||||||
__tablename__ = "copilot"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
||||||
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
|
||||||
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
|
|
||||||
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
|
||||||
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
|
||||||
|
|
||||||
flights: Mapped[Set['Flight']] = relationship(secondary=flight_has_copilot)
|
|
||||||
flight_plans: Mapped[Set['FlightPlan']] = relationship(secondary=flight_plan_has_copilot)
|
|
||||||
created_by: Mapped['User'] = relationship()
|
|
||||||
photos: Mapped[List['Photo']] = relationship(secondary=copilot_has_photo)
|
|
||||||
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
|
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
|
||||||
__tablename__ = "user"
|
|
||||||
excluded_columns_in_dict = ('password_hashed',)
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
email: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
||||||
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
|
||||||
public_username: Mapped[str] = mapped_column(String(128), nullable=True, unique=True)
|
|
||||||
avatar_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
|
|
||||||
title_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
|
|
||||||
password_hashed: Mapped[str] = mapped_column(String(60), nullable=False)
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
|
|
||||||
flights: Mapped[Set['Flight']] = relationship()
|
|
||||||
organizations: Mapped[Set['Organization']] = relationship(secondary=user_is_in_organization)
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordResetToken(BaseModel):
|
|
||||||
__tablename__ = "password_reset_token"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
|
||||||
token: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
||||||
valid_until: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
|
||||||
|
|
||||||
user: Mapped['User'] = relationship()
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
from database.models.airport import Airport, Runway, ReportingPoint, airport_has_reporting_point, Frequency
|
||||||
|
from database.models.aircraft import Aircraft
|
||||||
|
from database.models.airspace import Airspace
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
from database.models.copilot import Copilot, copilot_has_photo
|
||||||
|
from database.models.event import Event
|
||||||
|
from database.models.flight import Flight, FlightTurnPoint, Track, TrackPoint, flight_has_copilot
|
||||||
|
from database.models.flight_plan import FlightPlan, flight_plan_has_copilot, FlightPlanMarker
|
||||||
|
from database.models.orgnization import Organization
|
||||||
|
from database.models.password_reset_token import PasswordResetToken
|
||||||
|
from database.models.photo import PhotoAdjustment, Photo
|
||||||
|
from database.models.point_of_interest import PointOfInterest, PointOfInterestType
|
||||||
|
from database.models.user import User, user_is_in_organization
|
||||||
|
from database.models.weather_info import WeatherInfo
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Aircraft",
|
||||||
|
"Airport",
|
||||||
|
"Runway",
|
||||||
|
"ReportingPoint",
|
||||||
|
"Airspace",
|
||||||
|
"BaseModel",
|
||||||
|
"Copilot",
|
||||||
|
"Event",
|
||||||
|
"Flight",
|
||||||
|
"FlightTurnPoint",
|
||||||
|
"FlightPlan",
|
||||||
|
"FlightPlanMarker",
|
||||||
|
"Frequency",
|
||||||
|
"Track",
|
||||||
|
"TrackPoint",
|
||||||
|
"Organization",
|
||||||
|
"PasswordResetToken",
|
||||||
|
"Photo",
|
||||||
|
"PhotoAdjustment",
|
||||||
|
"PointOfInterest",
|
||||||
|
"PointOfInterestType",
|
||||||
|
"WeatherInfo",
|
||||||
|
"User",
|
||||||
|
"airport_has_reporting_point",
|
||||||
|
"flight_has_copilot",
|
||||||
|
"flight_plan_has_copilot",
|
||||||
|
"user_is_in_organization",
|
||||||
|
"copilot_has_photo",
|
||||||
|
]
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Aircraft(BaseModel):
|
||||||
|
__tablename__ = "aircraft"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
call_sign: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||||
|
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
|
||||||
|
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
manufacturer: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
model: Mapped[str] = mapped_column(String(30), nullable=False, server_default="")
|
||||||
|
seats: Mapped[str] = mapped_column(Integer, nullable=False)
|
||||||
|
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey('organization.id'), nullable=True)
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=False)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||||
|
|
||||||
|
organization: Mapped['Organization'] = relationship()
|
||||||
|
flights: Mapped[set['Flight']] = relationship()
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float, Enum
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
airport_has_reporting_point = Table(
|
||||||
|
"airport_has_reporting_point",
|
||||||
|
BaseModel.metadata,
|
||||||
|
Column("airport_id", ForeignKey("airport.id"), primary_key=True),
|
||||||
|
Column("reporting_point_id", ForeignKey("reporting_point.id"), primary_key=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Airport(BaseModel):
|
||||||
|
__tablename__ = "airport"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
openaip_id: Mapped[str] = mapped_column(String(32), nullable=True, unique=True)
|
||||||
|
icao_code: Mapped[str] = mapped_column(String(8), nullable=False)
|
||||||
|
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
elevation: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||||
|
airport_type: Mapped[str] = mapped_column(
|
||||||
|
Enum("airport", "ull", "heliport"),
|
||||||
|
nullable=False,
|
||||||
|
server_default='airport'
|
||||||
|
)
|
||||||
|
use_in_gpx_guess: Mapped[bool] = mapped_column(Boolean, server_default='1')
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=True) # automaticky import nebude mit ID # noqa
|
||||||
|
|
||||||
|
runways: Mapped[list['Runway']] = relationship('Runway')
|
||||||
|
frequencies: Mapped[list['Frequency']] = relationship()
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
class Runway(BaseModel):
|
||||||
|
__tablename__ = "runway"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'))
|
||||||
|
heading: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
designator: Mapped[str] = mapped_column(String(4), nullable=False)
|
||||||
|
surface: Mapped[str] = mapped_column(Enum("asphalt", "concrete", "grass", "other"), nullable=False, server_default="other") # noqa
|
||||||
|
length: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||||
|
width: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Frequency(BaseModel):
|
||||||
|
__tablename__ = "frequency"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey('airport.id'), nullable=True)
|
||||||
|
airspace_id: Mapped[int] = mapped_column(Integer, ForeignKey('airspace.id'), nullable=True)
|
||||||
|
frequency: Mapped[str] = mapped_column(String(7), nullable=False)
|
||||||
|
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class ReportingPoint(BaseModel):
|
||||||
|
__tablename__ = "reporting_point"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
openaip_id: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||||
|
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from sqlalchemy import String, JSON
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Airspace(BaseModel):
|
||||||
|
__tablename__ = "airspace"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
country: Mapped[str] = mapped_column(String(8), nullable=False, index=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
|
||||||
|
type: Mapped[str] = mapped_column(String(8), nullable=False, index=True)
|
||||||
|
lower_limit: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||||
|
lower_alt_unit: Mapped[str] = mapped_column(String(8), nullable=False)
|
||||||
|
upper_limit: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||||
|
upper_alt_unit: Mapped[str] = mapped_column(String(8), nullable=False)
|
||||||
|
bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON())
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
|
||||||
|
|
||||||
|
class BaseModel(DeclarativeBase):
|
||||||
|
excluded_columns_in_dict = ("deleted",)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_column_names(cls):
|
||||||
|
return [col.name for col in cls.__table__.columns]
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
return {c: getattr(self, c) for c in self._get_column_names() if c not in self.excluded_columns_in_dict}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_one(cls, db_session: AsyncSession, **kwargs):
|
||||||
|
return (await db_session.scalars(select(cls).filter_by(**kwargs))).one()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create(cls, db_session: AsyncSession, data: dict):
|
||||||
|
model = cls(**{col: data[col] for col in cls._get_column_names() if col in data})
|
||||||
|
db_session.add(model)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
return model
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def update(cls, db_session: AsyncSession, data: dict, obj: BaseModel = None, id: int = None):
|
||||||
|
if not obj and not id:
|
||||||
|
raise ValueError("Provide either obj or id!")
|
||||||
|
|
||||||
|
if not obj:
|
||||||
|
obj = await cls.get_one(db_session, id=id)
|
||||||
|
for key, value in data.items():
|
||||||
|
if key in cls._get_column_names() and getattr(obj, key) != value:
|
||||||
|
setattr(obj, key, value)
|
||||||
|
|
||||||
|
return obj
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, DateTime, func
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
from database.models.flight import flight_has_copilot
|
||||||
|
from database.models.flight_plan import flight_plan_has_copilot
|
||||||
|
|
||||||
|
copilot_has_photo = Table(
|
||||||
|
"copilot_has_photo",
|
||||||
|
BaseModel.metadata,
|
||||||
|
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
|
||||||
|
Column("photo_id", ForeignKey("photo.id"), primary_key=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Copilot(BaseModel):
|
||||||
|
__tablename__ = "copilot"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
||||||
|
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
|
||||||
|
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||||
|
|
||||||
|
flights: Mapped[set['Flight']] = relationship(secondary=flight_has_copilot)
|
||||||
|
flight_plans: Mapped[set['FlightPlan']] = relationship(secondary=flight_plan_has_copilot)
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
photos: Mapped[list['Photo']] = relationship(secondary=copilot_has_photo)
|
||||||
|
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Event(BaseModel):
|
||||||
|
__tablename__ = "event"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
||||||
|
date_from: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||||
|
date_to: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||||
|
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey('organization.id'), nullable=True)
|
||||||
|
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||||
|
|
||||||
|
organization: Mapped['Organization'] = relationship()
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float, JSON
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
flight_has_copilot = Table(
|
||||||
|
"flight_has_copilot",
|
||||||
|
BaseModel.metadata,
|
||||||
|
Column("flight_id", ForeignKey("flight.id"), primary_key=True),
|
||||||
|
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
|
||||||
|
Column("token", String(64), unique=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FlightTurnPoint(BaseModel):
|
||||||
|
__tablename__ = "flight_turn_point"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False)
|
||||||
|
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
|
||||||
|
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
|
||||||
|
landing_duration: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||||
|
order: Mapped[int] = mapped_column(Integer, index=True)
|
||||||
|
|
||||||
|
flight: Mapped['Flight'] = relationship()
|
||||||
|
point_of_interest: Mapped['PointOfInterest'] = relationship()
|
||||||
|
airport: Mapped['Airport'] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
class Flight(BaseModel):
|
||||||
|
__tablename__ = "flight"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
||||||
|
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
debriefing_notes: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
||||||
|
event_id: Mapped[int] = mapped_column(Integer, ForeignKey("event.id"), nullable=True)
|
||||||
|
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
|
||||||
|
takeoff_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
|
||||||
|
landing_airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
|
||||||
|
takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||||
|
landing_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||||
|
duration_total: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||||
|
duration_pic: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||||
|
flight_plan_id: Mapped[int] = mapped_column(Integer, ForeignKey('flight_plan.id'), nullable=True)
|
||||||
|
track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'), nullable=True)
|
||||||
|
gpx_track_filename: Mapped[str] = mapped_column(String(128), nullable=True) # deprecated
|
||||||
|
has_terrain_elevation: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) # deprecated
|
||||||
|
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey('aircraft.id'))
|
||||||
|
takeoff_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
|
||||||
|
landing_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
|
||||||
|
landings: Mapped[int] = mapped_column(Integer, default=1)
|
||||||
|
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||||
|
|
||||||
|
takeoff_airport: Mapped['Airport'] = relationship(foreign_keys=[takeoff_airport_id])
|
||||||
|
landing_airport: Mapped['Airport'] = relationship(foreign_keys=[landing_airport_id])
|
||||||
|
weather_info_landing: Mapped['WeatherInfo'] = relationship(foreign_keys=[landing_weather_info_id])
|
||||||
|
weather_info_takeoff: Mapped['WeatherInfo'] = relationship(foreign_keys=[takeoff_weather_info_id])
|
||||||
|
turn_points: Mapped[list['FlightTurnPoint']] = relationship()
|
||||||
|
event: Mapped['Event'] = relationship()
|
||||||
|
copilots: Mapped[list['Copilot']] = relationship(secondary=flight_has_copilot)
|
||||||
|
aircraft: Mapped['Aircraft'] = relationship()
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
|
||||||
|
track: Mapped['Track'] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
class Track(BaseModel):
|
||||||
|
__tablename__ = "track"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
bounds: Mapped[list[tuple[float, float]]] = mapped_column(JSON())
|
||||||
|
min_speed: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
max_speed: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
avg_speed: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
max_altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
avg_altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
total_duration: Mapped[int] = mapped_column(Integer, nullable=True, comment="Total duration in seconds")
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
|
flight: Mapped['Flight'] = relationship()
|
||||||
|
track_points: Mapped[list['TrackPoint']] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
class TrackPoint(BaseModel):
|
||||||
|
__tablename__ = "track_point"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
timestamp: Mapped[datetime] = mapped_column(DateTime, nullable=False, index=True)
|
||||||
|
track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'))
|
||||||
|
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
|
||||||
|
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
|
||||||
|
terrain_elevation: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
speed: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
magnetic_variation: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
|
||||||
|
track: Mapped['Track'] = relationship()
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
flight_plan_has_copilot = Table(
|
||||||
|
"flight_plan_has_copilot",
|
||||||
|
BaseModel.metadata,
|
||||||
|
Column("flight_plan_id", ForeignKey("flight_plan.id"), primary_key=True),
|
||||||
|
Column("copilot_id", ForeignKey("copilot.id"), primary_key=True),
|
||||||
|
Column("token", String(64), unique=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
class FlightPlan(BaseModel):
|
||||||
|
__tablename__ = "flight_plan"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(256), nullable=False)
|
||||||
|
is_default_name: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
is_editable_by_copilot: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
track_id: Mapped[int] = mapped_column(Integer, ForeignKey('track.id'), nullable=True)
|
||||||
|
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
planned_speed: Mapped[int] = mapped_column(Integer, nullable=False, default=150)
|
||||||
|
planned_takeoff_datetime: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||||
|
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=True)
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||||
|
|
||||||
|
copilots: Mapped[list['Copilot']] = relationship(secondary=flight_plan_has_copilot)
|
||||||
|
aircraft: Mapped['Aircraft'] = relationship()
|
||||||
|
markers: Mapped[list['FlightPlanMarker']] = relationship()
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
class FlightPlanMarker(BaseModel):
|
||||||
|
__tablename__ = "flight_plan_marker"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
flight_plan_id: Mapped[int] = mapped_column(Integer, ForeignKey('flight_plan.id'))
|
||||||
|
airport_id: Mapped[int] = mapped_column(Integer, ForeignKey("airport.id"), nullable=True)
|
||||||
|
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
|
||||||
|
weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
type: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||||
|
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
|
||||||
|
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
from database.models.user import user_is_in_organization
|
||||||
|
|
||||||
|
|
||||||
|
class Organization(BaseModel):
|
||||||
|
__tablename__ = "organization"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||||
|
|
||||||
|
users: Mapped[set['User']] = relationship(secondary=user_is_in_organization)
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, ForeignKey, Integer, DateTime
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetToken(BaseModel):
|
||||||
|
__tablename__ = "password_reset_token"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
token: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
valid_until: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||||
|
|
||||||
|
user: Mapped['User'] = relationship()
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, ForeignKey, Integer, Text, DateTime, func, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Photo(BaseModel):
|
||||||
|
__tablename__ = "photo"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
||||||
|
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
filename: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
filename_extension: Mapped[str] = mapped_column(String(4), nullable=False)
|
||||||
|
cache_key: Mapped[str] = mapped_column(String(128), nullable=True)
|
||||||
|
width: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
height: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
exposed_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||||
|
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
gps_altitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
terrain_elevation: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey("aircraft.id"), nullable=True)
|
||||||
|
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=True)
|
||||||
|
flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False)
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
created_by_copilot_id: Mapped[int] = mapped_column(Integer, ForeignKey('copilot.id'), nullable=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
|
flight: Mapped['Flight'] = relationship(foreign_keys=[flight_id])
|
||||||
|
point_of_interest: Mapped['PointOfInterest'] = relationship(foreign_keys=[point_of_interest_id])
|
||||||
|
adjustment: Mapped['PhotoAdjustment'] = relationship(passive_deletes=True)
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
aircraft: Mapped['Aircraft'] = relationship(foreign_keys=[aircraft_id])
|
||||||
|
# copilots: Mapped[list['Copilot']] = relationship(secondary=copilot_has_photo)
|
||||||
|
|
||||||
|
|
||||||
|
class PhotoAdjustment(BaseModel):
|
||||||
|
__tablename__ = "photo_adjustment"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id', ondelete='CASCADE'), nullable=False)
|
||||||
|
rotate: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
contrast: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
brightness: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
saturation: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
sharpness: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
|
||||||
|
crop_left: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
crop_top: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
crop_width: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
crop_height: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
photo: Mapped['Photo'] = relationship()
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import String, Boolean, Table, ForeignKey, Column, Integer, Text, DateTime, func, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped, relationship
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class PointOfInterestType(BaseModel):
|
||||||
|
__tablename__ = "point_of_interest_type"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
icon: Mapped[str] = mapped_column(String(128), nullable=False, server_default='marker')
|
||||||
|
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||||
|
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
points_of_interest: Mapped[list['PointOfInterest']] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
class PointOfInterest(BaseModel):
|
||||||
|
__tablename__ = "point_of_interest"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
url_slug: Mapped[str] = mapped_column(String(128), nullable=False, server_default="")
|
||||||
|
title_photo_id: Mapped[int] = mapped_column(Integer, ForeignKey('photo.id'), nullable=True)
|
||||||
|
gps_latitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
gps_longitude: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
type_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest_type.id"), nullable=True)
|
||||||
|
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
|
||||||
|
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
|
||||||
|
|
||||||
|
type: Mapped[PointOfInterestType] = relationship()
|
||||||
|
created_by: Mapped['User'] = relationship()
|
||||||
|
title_photo: Mapped['Photo'] = relationship(foreign_keys=[title_photo_id])
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import datetime
|
||||||
|
from typing import Set
|
||||||
|
from sqlalchemy import String, DateTime, ForeignKey, Text, Integer, func, Table, Column
|
||||||
|
from sqlalchemy.orm import Mapped, relationship, mapped_column
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
user_is_in_organization = Table(
|
||||||
|
"user_is_in_organization",
|
||||||
|
BaseModel.metadata,
|
||||||
|
Column("user_id", Integer, ForeignKey("user.id"), primary_key=True),
|
||||||
|
Column("organization_id", Integer, ForeignKey("organization.id"), primary_key=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class User(BaseModel):
|
||||||
|
__tablename__ = "user"
|
||||||
|
excluded_columns_in_dict = ('password_hashed',)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
email: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
|
||||||
|
public_username: Mapped[str] = mapped_column(String(128), nullable=True, unique=True)
|
||||||
|
avatar_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
|
||||||
|
title_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
|
||||||
|
password_hashed: Mapped[str] = mapped_column(String(60), nullable=False)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
|
flights: Mapped[Set['Flight']] = relationship()
|
||||||
|
organizations: Mapped[Set['Organization']] = relationship(secondary=user_is_in_organization)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import Integer, DateTime, Float
|
||||||
|
from sqlalchemy.orm import mapped_column, Mapped
|
||||||
|
from database.models.base_model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class WeatherInfo(BaseModel):
|
||||||
|
__tablename__ = "weather_info"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
qnh: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||||
|
temperature_surface: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
dewpoint_surface: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
rain: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
cloudcover_low: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
cloudcover_total: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
wind_speed_surface: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
wind_direction_surface: Mapped[float] = mapped_column(Float, nullable=True)
|
||||||
|
date_time: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
from typing import Optional, Type
|
from typing import Type
|
||||||
from sqlalchemy import select, or_, and_
|
from sqlalchemy import select, or_, and_
|
||||||
from database import models
|
from database import models
|
||||||
|
|
||||||
|
|
||||||
class QueryBuilder:
|
class QueryBuilder[ModelType: models.BaseModel]:
|
||||||
def __init__(self, model: Type[models.BaseModel]):
|
def __init__(self, model: Type[ModelType]):
|
||||||
self.model = model
|
self.model = model
|
||||||
|
|
||||||
def get_simple_query(
|
def get_simple_query(
|
||||||
self,
|
self,
|
||||||
extra_select: Optional[list] = None,
|
extra_select: list | None = None,
|
||||||
created_by_id: Optional[int] = None,
|
created_by_id: int | None = None,
|
||||||
order_by: Optional[list] = None,
|
order_by: list | None = None,
|
||||||
only_public: Optional[bool] = False,
|
only_public: bool | None = False,
|
||||||
only_my: Optional[bool] = False,
|
only_my: bool | None = False,
|
||||||
include_others_public: Optional[bool] = False,
|
include_others_public: bool | None = False,
|
||||||
url_slug: Optional[str] = None,
|
url_slug: str | None = None,
|
||||||
include_deleted: bool = False
|
include_deleted: bool = False
|
||||||
):
|
):
|
||||||
if not extra_select:
|
if not extra_select:
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
from functools import wraps
|
|
||||||
from fastapi import HTTPException
|
|
||||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
|
||||||
|
|
||||||
|
|
||||||
def raise_unauthorized():
|
|
||||||
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
|
|
||||||
|
|
||||||
|
|
||||||
def allow_public(func):
|
|
||||||
@wraps(func)
|
|
||||||
async def decorator(*args, **kwargs):
|
|
||||||
if 'info' in kwargs:
|
|
||||||
user_id = kwargs['info'].context.user_id
|
|
||||||
public = kwargs.get('public')
|
|
||||||
if not user_id and not public:
|
|
||||||
raise_unauthorized()
|
|
||||||
|
|
||||||
return await func(*args, **kwargs)
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
def authenticated_user_only(raise_when_unauthorized: bool = True, return_value_unauthorized=None):
|
|
||||||
def wrapper(func):
|
|
||||||
@wraps(func)
|
|
||||||
async def decorator(*args, **kwargs):
|
|
||||||
if 'info' in kwargs:
|
|
||||||
if not kwargs['info'].context.user_id:
|
|
||||||
if raise_when_unauthorized:
|
|
||||||
raise_unauthorized()
|
|
||||||
else:
|
|
||||||
return return_value_unauthorized
|
|
||||||
return await func(*args, **kwargs)
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
from functools import wraps
|
|
||||||
from fastapi import HTTPException
|
|
||||||
from graphql import GraphQLError
|
|
||||||
from sqlalchemy.exc import NoResultFound
|
|
||||||
|
|
||||||
|
|
||||||
def error_logging(func):
|
|
||||||
@wraps(func)
|
|
||||||
async def decorator(*args, **kwargs):
|
|
||||||
try:
|
|
||||||
return await func(*args, **kwargs)
|
|
||||||
except NoResultFound as e:
|
|
||||||
raise GraphQLError("Not found", original_error=e)
|
|
||||||
except HTTPException as e:
|
|
||||||
if e.status_code == 401:
|
|
||||||
raise GraphQLError("Not authorized", original_error=e)
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
|
||||||
|
def retryable(func):
|
||||||
|
max_retries = 5
|
||||||
|
|
||||||
|
async def func_wrapper(*args, **kwargs):
|
||||||
|
for i in range(1, max_retries):
|
||||||
|
try:
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
except Exception as ex:
|
||||||
|
if i > max_retries:
|
||||||
|
raise ex
|
||||||
|
sleep_time = i * 2
|
||||||
|
print(
|
||||||
|
"Error while getting the data from source:"
|
||||||
|
" decorator is going to sleep for %s seconds"
|
||||||
|
" and retry it again (%s/%s)" % (
|
||||||
|
sleep_time, i, max_retries
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await asyncio.sleep(sleep_time)
|
||||||
|
|
||||||
|
return func_wrapper
|
||||||
@@ -9,10 +9,10 @@
|
|||||||
</mj-text>
|
</mj-text>
|
||||||
|
|
||||||
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left">
|
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left">
|
||||||
Od: [from]
|
Od: {{ email_from | e }}
|
||||||
</mj-text>
|
</mj-text>
|
||||||
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5">
|
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5">
|
||||||
[content]
|
{{ content | e }}
|
||||||
</mj-text>
|
</mj-text>
|
||||||
<mj-divider border-color="#469ccc"></mj-divider>
|
<mj-divider border-color="#469ccc"></mj-divider>
|
||||||
</mj-column>
|
</mj-column>
|
||||||
|
|||||||
@@ -13,13 +13,12 @@
|
|||||||
<p>
|
<p>
|
||||||
Ahoj,
|
Ahoj,
|
||||||
<br>
|
<br>
|
||||||
Každý občas zapomíná. Heslo je to nejmenší. Klikni na následující tlačítko pro přihlášení do aplikace. A tam si budeš moct změnit heslo.
|
Každý občas zapomíná. Heslo je to nejmenší. Klikni na následující tlačítko pro reset hesla.
|
||||||
<br>
|
<br>
|
||||||
<strong>Jen admin vyzaduje zadani stareho hesla, takze to nebude tak snadne...</strong>
|
|
||||||
</p>
|
</p>
|
||||||
</mj-text>
|
</mj-text>
|
||||||
<mj-button background-color="#469ccc" color="white" font-size="18px" align="center" href="">
|
<mj-button background-color="#469ccc" color="white" font-size="18px" align="center" href="https://poletani.cz/zapomenute-heslo/[login_token]">
|
||||||
Přihlásit se pomocí magického odkazu [login_token]
|
Obnovit heslo
|
||||||
</mj-button>
|
</mj-button>
|
||||||
<mj-divider border-color="#469ccc"></mj-divider>
|
<mj-divider border-color="#469ccc"></mj-divider>
|
||||||
</mj-column>
|
</mj-column>
|
||||||
|
|||||||
@@ -8,34 +8,34 @@
|
|||||||
Vítejte v aplikaci Polétání.cz!
|
Vítejte v aplikaci Polétání.cz!
|
||||||
</mj-text>
|
</mj-text>
|
||||||
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5">
|
<mj-text font-size="16px" color="#333333" font-family="Arial, sans-serif" align="left" line-height="1.5">
|
||||||
Ahoj [Jméno],
|
Ahoj!
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Vítejte v aplikaci Polétání.cz – místě, kde můžete sdílet své letecké dobrodružství a fotografie s ostatními nadšenci.
|
Vítej v aplikaci Polétání.cz – místě, kde můžete sdílet své letecké dobrodružství a fotografie s ostatními nadšenci.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Co můžete očekávat:
|
Co můžeš očekávat:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>Jednoduché nahrávání a sdílení fotografií z vašich letů.</li>
|
<li>Jednoduché nahrávání a sdílení fotografií z vašich letů.</li>
|
||||||
<li>Možnost připojení k letištním komunitám a sdílení tipů a triků.</li>
|
<li>Naplánovat nový výlet pomocí vestavěného plánovače</li>
|
||||||
<li>Upozornění na nové funkce a akce v aplikaci.</li>
|
<li>Objevovat nová místa pro další výlety</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Začněte nyní a sdílejte své vzdušné zážitky s celým světem!
|
Začni nyní a sdílejte své vzdušné zážitky s celým světem!
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
S pozdravem,
|
S pozdravem,
|
||||||
<br>
|
<br>
|
||||||
Tým Polétání.cz
|
Michal Kváček
|
||||||
</p>
|
</p>
|
||||||
</mj-text>
|
</mj-text>
|
||||||
<mj-button background-color="#469ccc" color="white" font-size="16px" align="center" href="[Odkaz na přihlášení]">
|
<mj-button background-color="#469ccc" color="white" font-size="16px" align="center" href="https://poletani.cz/login">
|
||||||
Přihlásit se do Polétání.cz
|
Přihlásit se do Polétání.cz
|
||||||
</mj-button>
|
</mj-button>
|
||||||
<mj-divider border-color="#469ccc"></mj-divider>
|
<mj-divider border-color="#469ccc"></mj-divider>
|
||||||
|
|||||||
@@ -1,53 +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 not APP_DEBUG:
|
|
||||||
return
|
|
||||||
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")
|
|
||||||
@@ -33,6 +33,8 @@ class LoginEndpoint(AuthEndpoint):
|
|||||||
access_token = self.access_security.create_access_token(subject=subject)
|
access_token = self.access_security.create_access_token(subject=subject)
|
||||||
refresh_token = self.refresh_security.create_refresh_token(subject=subject)
|
refresh_token = self.refresh_security.create_refresh_token(subject=subject)
|
||||||
|
|
||||||
|
# TODO: SameSite na refresh_token_cookie!
|
||||||
|
# https://medium.com/swlh/how-the-new-chrome-80-cookie-rule-samesite-none-secure-affects-web-development-c06380220ced
|
||||||
# self.access_security.set_access_cookie(resp, access_token)
|
# self.access_security.set_access_cookie(resp, access_token)
|
||||||
self.refresh_security.set_refresh_cookie(
|
self.refresh_security.set_refresh_cookie(
|
||||||
resp, refresh_token,
|
resp, refresh_token,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
from fastapi import HTTPException, BackgroundTasks
|
from fastapi import HTTPException, BackgroundTasks
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from typing import Optional
|
|
||||||
from pydantic import BaseModel, Field, model_validator
|
from pydantic import BaseModel, Field, model_validator
|
||||||
from background_jobs.send_email import send_email
|
from background_jobs.send_email import send_email
|
||||||
from database.models import User
|
from database.models import User
|
||||||
@@ -12,7 +11,7 @@ from emails.emails import WelcomeEmail
|
|||||||
|
|
||||||
class RegistrationInput(BaseModel):
|
class RegistrationInput(BaseModel):
|
||||||
email: str = Field(..., min_length=4)
|
email: str = Field(..., min_length=4)
|
||||||
name: Optional[str]
|
name: str | None
|
||||||
password: str
|
password: str
|
||||||
|
|
||||||
@model_validator(mode="before")
|
@model_validator(mode="before")
|
||||||
|
|||||||
Vendored
+73
-13
@@ -1,27 +1,87 @@
|
|||||||
from typing import List, Dict
|
import dataclasses
|
||||||
|
from typing import Optional
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
|
from database import models
|
||||||
|
from decorators.retryable import retryable
|
||||||
|
from utils.list_utils import chunks
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class ElevationInfo:
|
||||||
|
id: int | None
|
||||||
|
lat: float
|
||||||
|
lng: float
|
||||||
|
elevation: float
|
||||||
|
|
||||||
|
|
||||||
class ElevationAPI:
|
class ElevationAPI:
|
||||||
ELEVATION_ENDPOINT = "https://api.open-elevation.com/api/v1/lookup"
|
ELEVATION_ENDPOINT: str
|
||||||
|
|
||||||
def get_request(self, points: List[Dict[str, float]]):
|
def get_request(self, session: aiohttp.ClientSession, points: list[dict[str, float]]):
|
||||||
return {"locations": [{"latitude": point['lat'], "longitude": point['lng']} for point in points]}
|
...
|
||||||
|
|
||||||
async def call_api(self, points: List[Dict[str, float]]):
|
@retryable
|
||||||
|
async def call_api(self, points: list[dict[str, float]]):
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.post(self.ELEVATION_ENDPOINT, json=self.get_request(points)) as resp:
|
async with self.get_request(session, points) as resp:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return await resp.json()
|
return await resp.json()
|
||||||
|
|
||||||
async def get_elevation_for_points(self, points: List[Dict[str, float]]) -> List[dict]:
|
|
||||||
|
class OpenElevationAPI(ElevationAPI):
|
||||||
|
ELEVATION_ENDPOINT = "https://api.open-elevation.com/api/v1/lookup"
|
||||||
|
|
||||||
|
def get_request(self, session: aiohttp.ClientSession, points: list[dict[str, float]]):
|
||||||
|
data = {"locations": [{"latitude": point['lat'], "longitude": point['lng']} for point in points]}
|
||||||
|
return session.post(self.ELEVATION_ENDPOINT, json=data)
|
||||||
|
|
||||||
|
async def get_elevation_for_points(self, points: list[dict[str, float]]) -> list[ElevationInfo]:
|
||||||
response = await self.call_api(points)
|
response = await self.call_api(points)
|
||||||
|
|
||||||
return [{
|
elevation = []
|
||||||
"lat": loc['latitude'],
|
for i, point in enumerate(points):
|
||||||
"lng": loc['longitude'],
|
response_point = response['results'][i]
|
||||||
"elevation": loc['elevation']
|
elevation.append(ElevationInfo(**point, elevation=response_point['elevation']))
|
||||||
} for loc in response['results']]
|
return elevation
|
||||||
|
|
||||||
|
|
||||||
elevation_api = ElevationAPI()
|
class OpenMeteoElevationAPI(ElevationAPI):
|
||||||
|
ELEVATION_ENDPOINT = "https://api.open-meteo.com/v1/elevation"
|
||||||
|
|
||||||
|
def get_request(self, session: aiohttp.ClientSession, points: list[dict[str, float]]):
|
||||||
|
latitudes = ",".join(str(point['lat']) for point in points)
|
||||||
|
longitudes = ",".join(str(point['lng']) for point in points)
|
||||||
|
url = f"{self.ELEVATION_ENDPOINT}?latitude={latitudes}&longitude={longitudes}"
|
||||||
|
|
||||||
|
return session.get(url)
|
||||||
|
|
||||||
|
async def get_elevation_for_points(self, points: list[dict[str, float]]) -> list[ElevationInfo]:
|
||||||
|
elevation = []
|
||||||
|
for chunk in chunks(points, n=100):
|
||||||
|
response = await self.call_api(chunk)
|
||||||
|
chunk_elevation = []
|
||||||
|
for i, point in enumerate(chunk):
|
||||||
|
chunk_elevation.append(ElevationInfo(**point, elevation=response['elevation'][i]))
|
||||||
|
|
||||||
|
elevation += chunk_elevation
|
||||||
|
return elevation
|
||||||
|
|
||||||
|
|
||||||
|
class CombinedElevationAPI:
|
||||||
|
async def get_elevation_for_points(self, points: list[dict[str, float]]) -> list[ElevationInfo]:
|
||||||
|
for api in (OpenMeteoElevationAPI(), OpenElevationAPI()):
|
||||||
|
elevation = await api.get_elevation_for_points(points)
|
||||||
|
|
||||||
|
if elevation:
|
||||||
|
return elevation
|
||||||
|
|
||||||
|
|
||||||
|
async def update_track_points_elevation(db, track_points: list[models.TrackPoint]):
|
||||||
|
points = [{"id": p.id, "lat": p.gps_latitude, "lng": p.gps_longitude} for p in track_points]
|
||||||
|
elevation = await elevation_api.get_elevation_for_points(points)
|
||||||
|
for point in elevation:
|
||||||
|
await models.TrackPoint.update(db, {"terrain_elevation": point.elevation}, id=point.id)
|
||||||
|
|
||||||
|
|
||||||
|
elevation_api = CombinedElevationAPI()
|
||||||
|
|||||||
Vendored
-34
@@ -1,34 +0,0 @@
|
|||||||
import math
|
|
||||||
|
|
||||||
EARTH_RADIUS = 6378.1 # Radius of the Earth
|
|
||||||
|
|
||||||
|
|
||||||
def get_bearing(center_point: tuple[float, float], end_point: tuple[float, float]):
|
|
||||||
lat3 = math.radians(end_point[0])
|
|
||||||
long3 = math.radians(end_point[1])
|
|
||||||
lat1 = math.radians(center_point[0])
|
|
||||||
long1 = math.radians(center_point[1])
|
|
||||||
|
|
||||||
dLon = long3 - long1
|
|
||||||
|
|
||||||
X = math.cos(lat3) * math.sin(dLon)
|
|
||||||
Y = math.cos(lat1) * math.sin(lat3) - math.sin(lat1) * math.cos(lat3) * math.cos(dLon)
|
|
||||||
|
|
||||||
end_brng = math.atan2(X, Y)
|
|
||||||
|
|
||||||
return end_brng
|
|
||||||
|
|
||||||
|
|
||||||
def get_distance(a: tuple[float, float], b: tuple[float, float]):
|
|
||||||
lat1 = math.radians(a[0])
|
|
||||||
lon1 = math.radians(a[1])
|
|
||||||
lat2 = math.radians(b[0])
|
|
||||||
lon2 = math.radians(b[1])
|
|
||||||
|
|
||||||
dlon = lon2 - lon1
|
|
||||||
dlat = lat2 - lat1
|
|
||||||
|
|
||||||
a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
|
|
||||||
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
|
||||||
|
|
||||||
return EARTH_RADIUS * c
|
|
||||||
Vendored
+8
-42
@@ -1,9 +1,7 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
from aiocache import cached
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from lxml.etree import _ElementTree
|
|
||||||
|
|
||||||
|
|
||||||
class GPXParser:
|
class GPXParser:
|
||||||
@@ -68,50 +66,47 @@ class GPXParser:
|
|||||||
def run_xpath(self, path: str):
|
def run_xpath(self, path: str):
|
||||||
return self.gpx.xpath(path, namespaces=self.namespace)
|
return self.gpx.xpath(path, namespaces=self.namespace)
|
||||||
|
|
||||||
@cached()
|
|
||||||
async def get_times_all(self):
|
async def get_times_all(self):
|
||||||
nodes = self.run_xpath("//gpx:trkpt/gpx:time")
|
nodes = self.run_xpath("//gpx:trkpt/gpx:time")
|
||||||
return [datetime.fromisoformat(node.text).astimezone() for node in nodes]
|
return [datetime.fromisoformat(node.text).astimezone() for node in nodes]
|
||||||
|
|
||||||
@cached()
|
|
||||||
async def get_times(self):
|
async def get_times(self):
|
||||||
times = await self.get_times_all()
|
times = await self.get_times_all()
|
||||||
return await self.sample_times(times)
|
return await self.sample_times(times)
|
||||||
|
|
||||||
@cached()
|
async def get_total_duration(self) -> timedelta:
|
||||||
|
times = await self.get_times_all()
|
||||||
|
return times[-1] - times[0]
|
||||||
|
|
||||||
async def get_coordinates(self) -> List[Dict[str, float]]:
|
async def get_coordinates(self) -> List[Dict[str, float]]:
|
||||||
return await self.average_coordinates(await self.get_coordinates_all())
|
return await self.average_coordinates(await self.get_coordinates_all())
|
||||||
|
|
||||||
@cached()
|
|
||||||
async def get_coordinates_all(self) -> List[Dict[str, float]]:
|
async def get_coordinates_all(self) -> List[Dict[str, float]]:
|
||||||
nodes = self.run_xpath("//gpx:trkpt")
|
nodes = self.run_xpath("//gpx:trkpt")
|
||||||
return [{"lat": float(node.attrib["lat"]), "lng": float(node.attrib['lon'])} for node in nodes]
|
return [{"lat": float(node.attrib["lat"]), "lng": float(node.attrib['lon'])} for node in nodes]
|
||||||
|
|
||||||
@cached()
|
|
||||||
async def get_speed(self) -> List[float]:
|
async def get_speed(self) -> List[float]:
|
||||||
nodes = self.run_xpath("//gpx:speed")
|
nodes = self.run_xpath("//gpx:speed")
|
||||||
return await self.average_sample_numbers([float(node.text) for node in nodes])
|
return await self.average_sample_numbers([float(node.text) for node in nodes])
|
||||||
|
|
||||||
@cached()
|
|
||||||
async def get_magnetic_variation(self) -> List[float]:
|
async def get_magnetic_variation(self) -> List[float]:
|
||||||
nodes = self.run_xpath("//gpx:magvar")
|
nodes = self.run_xpath("//gpx:magvar")
|
||||||
return await self.average_sample_numbers([int(node.text) for node in nodes])
|
return await self.average_sample_numbers([int(node.text) for node in nodes])
|
||||||
|
|
||||||
@cached()
|
|
||||||
async def get_altitude(self) -> List[float]:
|
async def get_altitude(self) -> List[float]:
|
||||||
nodes = self.run_xpath("//gpx:ele")
|
nodes = self.run_xpath("//gpx:ele")
|
||||||
return await self.average_sample_numbers([float(node.text) for node in nodes])
|
return await self.average_sample_numbers([float(node.text) for node in nodes])
|
||||||
|
|
||||||
@cached()
|
|
||||||
async def get_terrain_elevation(self) -> List[float]:
|
async def get_terrain_elevation(self) -> List[float]:
|
||||||
nodes = self.run_xpath("//gpx:terrain_elevation")
|
nodes = self.run_xpath("//gpx:terrain_elevation")
|
||||||
return await self.average_sample_numbers([float(node.text) for node in nodes])
|
return await self.average_sample_numbers([float(node.text) for node in nodes])
|
||||||
|
|
||||||
@cached()
|
|
||||||
async def get_max_speed(self):
|
async def get_max_speed(self):
|
||||||
return max(await self.get_speed()) or 0
|
return max(await self.get_speed()) or 0
|
||||||
|
|
||||||
@cached()
|
async def get_min_speed(self):
|
||||||
|
return min(await self.get_speed()) or 0
|
||||||
|
|
||||||
async def get_avg_speed(self):
|
async def get_avg_speed(self):
|
||||||
speeds = await self.get_speed()
|
speeds = await self.get_speed()
|
||||||
if not speeds:
|
if not speeds:
|
||||||
@@ -119,38 +114,9 @@ class GPXParser:
|
|||||||
|
|
||||||
return round(sum(speeds) / len(speeds), 2)
|
return round(sum(speeds) / len(speeds), 2)
|
||||||
|
|
||||||
@cached()
|
|
||||||
async def get_max_altitude(self):
|
async def get_max_altitude(self):
|
||||||
return max(await self.get_altitude()) or 0
|
return max(await self.get_altitude()) or 0
|
||||||
|
|
||||||
@cached()
|
|
||||||
async def get_avg_altitude(self):
|
async def get_avg_altitude(self):
|
||||||
altitudes = await self.get_altitude()
|
altitudes = await self.get_altitude()
|
||||||
return round(sum(altitudes) / len(altitudes), 2)
|
return round(sum(altitudes) / len(altitudes), 2)
|
||||||
|
|
||||||
def add_terrain_elevation(self, points_with_elevation: List[Dict[str, float]]):
|
|
||||||
track_points = self.run_xpath("//gpx:trkpt")
|
|
||||||
# TODO: open elevation API umi jen presnost na 6 desetinnych mist!
|
|
||||||
track_points_index = {(float(n.attrib['lat']), float(n.attrib['lon'])): n for n in track_points}
|
|
||||||
|
|
||||||
for point_with_elevation in points_with_elevation:
|
|
||||||
lat = point_with_elevation['lat']
|
|
||||||
lng = point_with_elevation['lng']
|
|
||||||
elevation = point_with_elevation['elevation']
|
|
||||||
target_node = track_points_index.get((lat, lng))
|
|
||||||
|
|
||||||
if target_node is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
extensions = target_node.find("./extensions", self.gpx.getroot().nsmap)
|
|
||||||
if extensions is not None:
|
|
||||||
extensions.append(etree.XML(f"<terrain_elevation>{elevation}</terrain_elevation>"))
|
|
||||||
else:
|
|
||||||
target_node.append(
|
|
||||||
etree.XML(f"<extensions><terrain_elevation>{elevation}</terrain_elevation></extensions>")
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.gpx
|
|
||||||
|
|
||||||
def write(self, tree: _ElementTree, output: str):
|
|
||||||
tree.write(output)
|
|
||||||
|
|||||||
Vendored
+2
-2
@@ -8,7 +8,7 @@ import aiohttp
|
|||||||
from sqlalchemy import delete
|
from sqlalchemy import delete
|
||||||
from database import models
|
from database import models
|
||||||
from database.transaction import get_session
|
from database.transaction import get_session
|
||||||
from external.gps_utils import get_distance, get_bearing, EARTH_RADIUS
|
from utils.gps import get_distance, get_bearing, EARTH_RADIUS
|
||||||
|
|
||||||
|
|
||||||
# https://pyopenair.readthedocs.io/en/latest/openair.html
|
# https://pyopenair.readthedocs.io/en/latest/openair.html
|
||||||
@@ -53,7 +53,7 @@ class Airspace:
|
|||||||
upper_limit: str = None
|
upper_limit: str = None
|
||||||
lower_limit: str = None
|
lower_limit: str = None
|
||||||
center: Optional[Coordinates] = None
|
center: Optional[Coordinates] = None
|
||||||
radius_nm: Optional[float] = None
|
radius_nm: float | None = None
|
||||||
bounds: list[Coordinates] = dataclasses.field(default_factory=lambda: [])
|
bounds: list[Coordinates] = dataclasses.field(default_factory=lambda: [])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Vendored
+10
-6
@@ -3,6 +3,7 @@ import urllib.parse
|
|||||||
from typing import Tuple, Dict
|
from typing import Tuple, Dict
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiocache import cached
|
from aiocache import cached
|
||||||
|
from decorators.retryable import retryable
|
||||||
|
|
||||||
|
|
||||||
class Weather:
|
class Weather:
|
||||||
@@ -38,13 +39,17 @@ class Weather:
|
|||||||
return f"{url}{query_string}"
|
return f"{url}{query_string}"
|
||||||
|
|
||||||
@cached(ttl=6 * 3600)
|
@cached(ttl=6 * 3600)
|
||||||
|
@retryable
|
||||||
async def download_weather_for_day(self, date: datetime.date, gps: Tuple[float, float]):
|
async def download_weather_for_day(self, date: datetime.date, gps: Tuple[float, float]):
|
||||||
url = self.get_weather_info_url(start_date=date, end_date=date, gps=gps)
|
url = self.get_weather_info_url(start_date=date, end_date=date, gps=gps)
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
try:
|
||||||
async with session.get(url) as resp:
|
async with aiohttp.ClientSession() as session:
|
||||||
resp.raise_for_status()
|
async with session.get(url) as resp:
|
||||||
return await resp.json()
|
resp.raise_for_status()
|
||||||
|
return await resp.json()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in downloading weather: {e}, {type(e)}")
|
||||||
|
|
||||||
async def get_weather_for_hour(
|
async def get_weather_for_hour(
|
||||||
self, date_time: datetime.datetime, gps: Tuple[float, float]
|
self, date_time: datetime.datetime, gps: Tuple[float, float]
|
||||||
@@ -52,10 +57,9 @@ class Weather:
|
|||||||
data = await self.download_weather_for_day(date_time.date(), gps)
|
data = await self.download_weather_for_day(date_time.date(), gps)
|
||||||
|
|
||||||
# TODO: kontrola timezone!
|
# TODO: kontrola timezone!
|
||||||
# TODO: interpolace - udelat vazenyprumer z dvou po sobe jdoucich hodin
|
|
||||||
idx = date_time.hour
|
idx = date_time.hour
|
||||||
result_data = {metric: data['hourly'][metric][idx] for metric in self.METRICS}
|
result_data = {metric: data['hourly'][metric][idx] for metric in self.METRICS}
|
||||||
result_data['datetime'] = datetime.datetime.strptime(data['hourly']['time'][idx], "%Y-%m-%dT%H:%M")
|
result_data['date_time'] = datetime.datetime.strptime(data['hourly']['time'][idx], "%Y-%m-%dT%H:%M")
|
||||||
|
|
||||||
return result_data
|
return result_data
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import dataclasses
|
||||||
|
from datetime import datetime
|
||||||
|
from fastapi import BackgroundTasks, Depends, Security
|
||||||
|
from fastapi_jwt import JwtAuthorizationCredentials
|
||||||
|
from sqlalchemy import select
|
||||||
|
from strawberry.fastapi import BaseContext
|
||||||
|
from database import async_session, models
|
||||||
|
from jwt import access_security
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class GraphQLContext(BaseContext):
|
||||||
|
request_start_time: datetime
|
||||||
|
user_id: int
|
||||||
|
organization_ids: set[int]
|
||||||
|
jwt_auth_credentials: JwtAuthorizationCredentials
|
||||||
|
background_tasks: BackgroundTasks
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_graphql_context(credentials: JwtAuthorizationCredentials = Security(access_security)):
|
||||||
|
user_id = credentials['id'] if credentials else None
|
||||||
|
organization_ids = set()
|
||||||
|
|
||||||
|
if user_id:
|
||||||
|
async with async_session() as db:
|
||||||
|
organization_ids = set((await db.scalars(
|
||||||
|
select(models.user_is_in_organization.c.organization_id)
|
||||||
|
.filter(models.user_is_in_organization.c.user_id == user_id)
|
||||||
|
)).all())
|
||||||
|
|
||||||
|
return GraphQLContext(
|
||||||
|
user_id=user_id,
|
||||||
|
organization_ids=organization_ids,
|
||||||
|
jwt_auth_credentials=credentials,
|
||||||
|
background_tasks=Depends(BackgroundTasks),
|
||||||
|
request_start_time=datetime.now()
|
||||||
|
)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Type, List, Optional
|
from typing import Type
|
||||||
from logger import log
|
from logger import log
|
||||||
from database import models, async_session
|
from database import models, async_session
|
||||||
from database.query_builder import QueryBuilder
|
from database.query_builder import QueryBuilder
|
||||||
@@ -9,7 +9,7 @@ class BaseDataloader:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
model: Type[models.BaseModel],
|
model: Type[models.BaseModel],
|
||||||
relationship_column, filters: Optional[list] = None
|
relationship_column, filters: list | None = None
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.model = model
|
self.model = model
|
||||||
@@ -25,11 +25,15 @@ class BaseDataloader:
|
|||||||
|
|
||||||
|
|
||||||
class SingleModelByIdDataloader(BaseDataloader):
|
class SingleModelByIdDataloader(BaseDataloader):
|
||||||
async def load(self, ids: List[int]):
|
async def load(self, ids: list[int]):
|
||||||
|
ids_set = {id_ for id_ in set(ids) if id_ is not None}
|
||||||
|
if not ids_set:
|
||||||
|
return [None for _ in ids]
|
||||||
|
|
||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
query = (
|
query = (
|
||||||
self.query_builder.get_simple_query(extra_select=[self.relationship_column], include_deleted=True)
|
self.query_builder.get_simple_query(extra_select=[self.relationship_column], include_deleted=True)
|
||||||
.filter(self.relationship_column.in_(set(ids)))
|
.filter(self.relationship_column.in_(ids_set))
|
||||||
.filter(*self.filters)
|
.filter(*self.filters)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -43,10 +47,10 @@ class MultiModelsDataloader(BaseDataloader):
|
|||||||
self,
|
self,
|
||||||
model: Type[models.BaseModel],
|
model: Type[models.BaseModel],
|
||||||
relationship_column=None,
|
relationship_column=None,
|
||||||
filters: Optional[list] = None,
|
filters: list | None = None,
|
||||||
extra_select: Optional[list] = None,
|
extra_select: list | None = None,
|
||||||
extra_join: Optional[list] = None,
|
extra_join: list | None = None,
|
||||||
order_by: Optional[list] = None,
|
order_by: list | None = None,
|
||||||
):
|
):
|
||||||
super().__init__(model, relationship_column, filters)
|
super().__init__(model, relationship_column, filters)
|
||||||
|
|
||||||
@@ -62,13 +66,13 @@ class MultiModelsDataloader(BaseDataloader):
|
|||||||
order_by = [model.id.desc()] # defaultne radit od nejnovejsich zaznamu
|
order_by = [model.id.desc()] # defaultne radit od nejnovejsich zaznamu
|
||||||
self.order_by = order_by
|
self.order_by = order_by
|
||||||
|
|
||||||
def get_query(self, ids: list[int]):
|
def get_query(self, ids: set[int]):
|
||||||
query = (
|
query = (
|
||||||
self.query_builder.get_simple_query(
|
self.query_builder.get_simple_query(
|
||||||
extra_select=[self.relationship_column] + self.extra_select,
|
extra_select=[self.relationship_column] + self.extra_select,
|
||||||
order_by=self.order_by
|
order_by=self.order_by
|
||||||
)
|
)
|
||||||
.filter(self.relationship_column.in_(set(ids)))
|
.filter(self.relationship_column.in_(ids))
|
||||||
.filter(*self.filters)
|
.filter(*self.filters)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -80,8 +84,12 @@ class MultiModelsDataloader(BaseDataloader):
|
|||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
async def load(self, ids: List[int]):
|
async def load(self, ids: list[int]) -> list:
|
||||||
query = self.get_query(ids)
|
ids_set = {id_ for id_ in set(ids) if id_ is not None}
|
||||||
|
if not ids_set:
|
||||||
|
return [[] for _ in ids]
|
||||||
|
|
||||||
|
query = self.get_query(ids_set)
|
||||||
|
|
||||||
async with async_session() as db:
|
async with async_session() as db:
|
||||||
data = (await db.execute(query)).all()
|
data = (await db.execute(query)).all()
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ async def load_flight_durations(ids: List[int]):
|
|||||||
select(
|
select(
|
||||||
models.Flight.id,
|
models.Flight.id,
|
||||||
func.timediff(models.Flight.landing_datetime, models.Flight.takeoff_datetime).label("diff"),
|
func.timediff(models.Flight.landing_datetime, models.Flight.takeoff_datetime).label("diff"),
|
||||||
func.coalesce(func.sum(models.FlightTrack.landing_duration), 0).label("landing_duration")
|
func.coalesce(func.sum(models.FlightTurnPoint.landing_duration), 0).label("landing_duration")
|
||||||
).join(models.Flight.track, isouter=True)
|
).join(models.Flight.turn_points, isouter=True)
|
||||||
.group_by(models.Flight.id)
|
.group_by(models.Flight.id)
|
||||||
.filter(models.Flight.id.in_(ids))
|
.filter(models.Flight.id.in_(ids))
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,24 @@ reporting_points_dataloader = DataLoader(
|
|||||||
).load, cache=False
|
).load, cache=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
runways_dataloader = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.Runway,
|
||||||
|
relationship_column=models.Airport.id,
|
||||||
|
extra_join=[models.Airport.runways],
|
||||||
|
order_by=[models.Runway.designator]
|
||||||
|
).load, cache=False
|
||||||
|
)
|
||||||
|
|
||||||
|
frequencies_dataloader = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.Frequency,
|
||||||
|
relationship_column=models.Airport.id,
|
||||||
|
extra_join=[models.Airport.frequencies],
|
||||||
|
order_by=[models.Frequency.frequency]
|
||||||
|
).load, cache=False
|
||||||
|
)
|
||||||
|
|
||||||
flights_by_copilot_dataloader = DataLoader(
|
flights_by_copilot_dataloader = DataLoader(
|
||||||
load_fn=MultiModelsDataloader(
|
load_fn=MultiModelsDataloader(
|
||||||
models.Flight,
|
models.Flight,
|
||||||
@@ -80,7 +98,7 @@ flight_by_poi_dataloader = DataLoader(
|
|||||||
models.Flight,
|
models.Flight,
|
||||||
relationship_column=models.PointOfInterest.id,
|
relationship_column=models.PointOfInterest.id,
|
||||||
order_by=[models.Flight.takeoff_datetime.desc()],
|
order_by=[models.Flight.takeoff_datetime.desc()],
|
||||||
extra_join=[models.Flight.track, models.PointOfInterest]
|
extra_join=[models.Flight.turn_points, models.PointOfInterest]
|
||||||
).load,
|
).load,
|
||||||
cache=False
|
cache=False
|
||||||
)
|
)
|
||||||
@@ -137,11 +155,20 @@ poi_photos_dataloader = DataLoader(
|
|||||||
).load,
|
).load,
|
||||||
cache=False
|
cache=False
|
||||||
)
|
)
|
||||||
flight_track_dataloader = DataLoader(
|
flight_turn_points_dataloader = DataLoader(
|
||||||
load_fn=MultiModelsDataloader(
|
load_fn=MultiModelsDataloader(
|
||||||
models.FlightTrack,
|
models.FlightTurnPoint,
|
||||||
relationship_column=models.FlightTrack.flight_id,
|
relationship_column=models.FlightTurnPoint.flight_id,
|
||||||
order_by=[models.FlightTrack.order]
|
order_by=[models.FlightTurnPoint.order]
|
||||||
|
).load,
|
||||||
|
cache=False
|
||||||
|
)
|
||||||
|
|
||||||
|
track_points_dataloder = DataLoader(
|
||||||
|
load_fn=MultiModelsDataloader(
|
||||||
|
models.TrackPoint,
|
||||||
|
relationship_column=models.TrackPoint.track_id,
|
||||||
|
order_by=[models.TrackPoint.timestamp, models.TrackPoint.id]
|
||||||
).load,
|
).load,
|
||||||
cache=False
|
cache=False
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from database import models
|
|||||||
from graphql_schema.dataloaders.base import SingleModelByIdDataloader
|
from graphql_schema.dataloaders.base import SingleModelByIdDataloader
|
||||||
|
|
||||||
|
|
||||||
def create_dataloader(model: Type[models.BaseModel], relationship_column=None, filters: Optional[list] = None):
|
def create_dataloader(model: Type[models.BaseModel], relationship_column=None, filters: list | None = None):
|
||||||
loader = SingleModelByIdDataloader(model, relationship_column, filters).load
|
loader = SingleModelByIdDataloader(model, relationship_column, filters).load
|
||||||
return DataLoader(load_fn=loader, cache=False)
|
return DataLoader(load_fn=loader, cache=False)
|
||||||
|
|
||||||
@@ -14,7 +14,8 @@ airport_dataloader = create_dataloader(models.Airport)
|
|||||||
aircraft_dataloader = create_dataloader(models.Aircraft)
|
aircraft_dataloader = create_dataloader(models.Aircraft)
|
||||||
event_dataloader = create_dataloader(models.Event)
|
event_dataloader = create_dataloader(models.Event)
|
||||||
organizations_dataloader = create_dataloader(models.Organization)
|
organizations_dataloader = create_dataloader(models.Organization)
|
||||||
airport_weather_info_loader = create_dataloader(models.WeatherInfo)
|
weather_info_loader = create_dataloader(models.WeatherInfo)
|
||||||
|
track_dataloader = create_dataloader(models.Track)
|
||||||
poi_dataloader = create_dataloader(models.PointOfInterest)
|
poi_dataloader = create_dataloader(models.PointOfInterest)
|
||||||
poi_type_dataloader = create_dataloader(models.PointOfInterestType)
|
poi_type_dataloader = create_dataloader(models.PointOfInterestType)
|
||||||
flight_dataloader = create_dataloader(models.Flight)
|
flight_dataloader = create_dataloader(models.Flight)
|
||||||
|
|||||||
@@ -1,70 +1,49 @@
|
|||||||
from typing import Optional
|
|
||||||
import strawberry
|
import strawberry
|
||||||
from decorators.endpoints import authenticated_user_only, allow_public
|
from strawberry import Info
|
||||||
from decorators.error_logging import error_logging
|
from .helpers.filters import get_filters
|
||||||
from .helpers.pagination import get_pagination_window, PaginationWindow
|
from .helpers.pagination import PaginationWindow
|
||||||
from .resolvers.aircraft import AircraftMutationResolver, AircraftQueryResolver
|
from .resolvers.aircraft import AircraftMutationResolver, AircraftQueryResolver
|
||||||
from graphql_schema.entities.types.mutation_input import CreateAircraftInput, EditAircraftInput
|
from graphql_schema.entities.types.mutation_input import CreateAircraftInput, EditAircraftInput
|
||||||
from graphql_schema.entities.types.types import Aircraft
|
from graphql_schema.entities.types.types import Aircraft
|
||||||
|
from ..extensions.field.auth import AllowPublicAccess, AuthenticatedOnly
|
||||||
|
from ..extensions.field.pagination import OffsetPagination
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class AircraftQueries:
|
class AircraftQueries:
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[OffsetPagination(item_type=Aircraft), AuthenticatedOnly()])
|
||||||
@error_logging
|
async def aircrafts(root, info: Info, search: str | None = None) -> PaginationWindow[Aircraft]:
|
||||||
@authenticated_user_only()
|
return AircraftQueryResolver().get_query(
|
||||||
async def aircrafts(root, info, limit: int, offset: int = 0) -> PaginationWindow[Aircraft]:
|
|
||||||
query = AircraftQueryResolver().get_query(
|
|
||||||
info.context.user_id,
|
info.context.user_id,
|
||||||
organization_ids=info.context.organization_ids
|
organization_ids=info.context.organization_ids,
|
||||||
|
search=search
|
||||||
)
|
)
|
||||||
|
|
||||||
return await get_pagination_window(
|
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||||
query=query,
|
|
||||||
item_type=Aircraft,
|
|
||||||
limit=limit,
|
|
||||||
offset=offset
|
|
||||||
)
|
|
||||||
|
|
||||||
@strawberry.field()
|
|
||||||
@error_logging
|
|
||||||
@allow_public
|
|
||||||
async def aircraft(
|
async def aircraft(
|
||||||
root, info,
|
root, info,
|
||||||
id: Optional[int] = None,
|
id: int | None = None,
|
||||||
call_sign: Optional[str] = None,
|
call_sign: str | None = None,
|
||||||
public: Optional[bool] = False
|
public: bool | None = False
|
||||||
) -> Aircraft:
|
) -> Aircraft:
|
||||||
filter_params = {}
|
|
||||||
if id:
|
|
||||||
filter_params['object_id'] = id
|
|
||||||
|
|
||||||
if call_sign:
|
|
||||||
filter_params['call_sign'] = call_sign
|
|
||||||
|
|
||||||
return await AircraftQueryResolver().get_one(
|
return await AircraftQueryResolver().get_one(
|
||||||
user_id=info.context.user_id,
|
user_id=info.context.user_id,
|
||||||
organization_ids=info.context.organization_ids if not public else None,
|
organization_ids=info.context.organization_ids if not public else None,
|
||||||
only_public=public,
|
only_public=public,
|
||||||
**filter_params
|
**get_filters(object_id=id, call_sign=call_sign)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class AircraftMutation:
|
class AircraftMutation:
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def create_aircraft(root, info, input: CreateAircraftInput) -> Aircraft:
|
async def create_aircraft(root, info, input: CreateAircraftInput) -> Aircraft:
|
||||||
return await AircraftMutationResolver().create(info.context, input)
|
return await AircraftMutationResolver().create(info.context, input)
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def edit_aircraft(root, info, id: int, input: EditAircraftInput) -> Aircraft:
|
async def edit_aircraft(root, info, id: int, input: EditAircraftInput) -> Aircraft:
|
||||||
return await AircraftMutationResolver().update(id, user_id=info.context.user_id, data=input)
|
return await AircraftMutationResolver().update(info.context, id=id, data=input)
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@authenticated_user_only()
|
|
||||||
async def delete_aircraft(self, info, id: int) -> Aircraft:
|
async def delete_aircraft(self, info, id: int) -> Aircraft:
|
||||||
return await AircraftMutationResolver().delete(info.context.user_id, id)
|
return await AircraftMutationResolver().delete(info.context, id)
|
||||||
|
|||||||
@@ -1,23 +1,17 @@
|
|||||||
from typing import List
|
|
||||||
import strawberry
|
import strawberry
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.error_logging import error_logging
|
|
||||||
from decorators.endpoints import authenticated_user_only
|
|
||||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
||||||
from graphql_schema.entities.types.types import Airport
|
from graphql_schema.entities.types.types import Airport
|
||||||
|
from graphql_schema.extensions.field.auth import AuthenticatedOnly
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class AirportQueries:
|
class AirportQueries:
|
||||||
@strawberry.field()
|
@strawberry.field()
|
||||||
@error_logging
|
async def airports(root, info) -> list[Airport]:
|
||||||
@authenticated_user_only()
|
|
||||||
async def airports(root, info) -> List[Airport]:
|
|
||||||
return await BaseQueryResolver(Airport, models.Airport).get_list(info.context.user_id)
|
return await BaseQueryResolver(Airport, models.Airport).get_list(info.context.user_id)
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def airport(root, info, id: int) -> Airport:
|
async def airport(root, info, id: int) -> Airport:
|
||||||
return await BaseQueryResolver(Airport, models.Airport).get_one(
|
return await BaseQueryResolver(Airport, models.Airport).get_one(
|
||||||
object_id=id,
|
object_id=id,
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
from typing import List, Optional
|
|
||||||
import strawberry
|
import strawberry
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.error_logging import error_logging
|
|
||||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
||||||
from graphql_schema.entities.types.types import Airspace
|
from graphql_schema.entities.types.types import Airspace
|
||||||
|
|
||||||
@@ -9,10 +7,9 @@ from graphql_schema.entities.types.types import Airspace
|
|||||||
@strawberry.type
|
@strawberry.type
|
||||||
class AirspaceQueries:
|
class AirspaceQueries:
|
||||||
@strawberry.field()
|
@strawberry.field()
|
||||||
@error_logging
|
|
||||||
async def airspaces(
|
async def airspaces(
|
||||||
root, info, country: Optional[str] = None, types: Optional[list[str]] = None
|
root, info, country: str | None = None, types: list[str] | None = None
|
||||||
) -> List[Airspace]:
|
) -> list[Airspace]:
|
||||||
|
|
||||||
filters = []
|
filters = []
|
||||||
if country:
|
if country:
|
||||||
|
|||||||
@@ -1,48 +1,35 @@
|
|||||||
from typing import List, Optional
|
|
||||||
import strawberry
|
import strawberry
|
||||||
from graphql import GraphQLError
|
from graphql import GraphQLError
|
||||||
from strawberry.types import Info
|
from strawberry.types import Info
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.error_logging import error_logging
|
from graphql_schema.entities.helpers.filters import get_filters
|
||||||
from decorators.endpoints import authenticated_user_only, allow_public
|
|
||||||
from graphql_schema.entities.helpers.detail import get_detail_filters
|
|
||||||
from graphql_schema.entities.resolvers.base import BaseMutationResolver
|
from graphql_schema.entities.resolvers.base import BaseMutationResolver
|
||||||
from graphql_schema.entities.resolvers.copilot import CopilotQueryResolver
|
from graphql_schema.entities.resolvers.copilot import CopilotQueryResolver
|
||||||
from graphql_schema.entities.types.mutation_input import CreateCopilotInput, EditCopilotInput
|
from graphql_schema.entities.types.mutation_input import CreateCopilotInput, EditCopilotInput
|
||||||
from graphql_schema.entities.types.types import Copilot
|
from graphql_schema.entities.types.types import Copilot
|
||||||
|
from graphql_schema.extensions.field.auth import AllowPublicAccess, AuthenticatedOnly
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class CopilotQueries:
|
class CopilotQueries:
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
async def copilots(root, info: Info) -> list[Copilot]:
|
||||||
@authenticated_user_only()
|
|
||||||
async def copilots(root, info: Info) -> List[Copilot]:
|
|
||||||
return await CopilotQueryResolver().get_list(info.context.user_id)
|
return await CopilotQueryResolver().get_list(info.context.user_id)
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||||
@error_logging
|
|
||||||
@allow_public
|
|
||||||
async def copilot(
|
async def copilot(
|
||||||
root, info: Info,
|
root, info: Info,
|
||||||
id: Optional[int] = None,
|
id: int | None = None,
|
||||||
url_slug: Optional[str] = None,
|
url_slug: str | None = None,
|
||||||
pilot_username: Optional[str] = None,
|
pilot_username: str | None = None,
|
||||||
upload_token: Optional[str] = None,
|
upload_token: str | None = None,
|
||||||
upload_flight_slug: Optional[str] = None,
|
upload_flight_slug: str | None = None,
|
||||||
public: Optional[bool] = False
|
public: bool | None = False
|
||||||
) -> Copilot:
|
) -> Copilot:
|
||||||
filter_params = {}
|
filter_params = get_filters(
|
||||||
if id:
|
object_id=id, url_slug=url_slug, pilot_username=pilot_username, upload_token=upload_token,
|
||||||
filter_params['object_id'] = id
|
upload_flight_slug=upload_flight_slug,
|
||||||
if url_slug is not None:
|
)
|
||||||
filter_params['url_slug'] = url_slug
|
|
||||||
if upload_token and upload_flight_slug:
|
|
||||||
filter_params['upload_token'] = upload_token
|
|
||||||
filter_params['upload_flight_slug'] = upload_flight_slug
|
|
||||||
if pilot_username:
|
|
||||||
filter_params['pilot_username'] = pilot_username
|
|
||||||
|
|
||||||
if not filter_params:
|
if not filter_params:
|
||||||
raise GraphQLError(f"Invalid identification supplied: {filter_params}")
|
raise GraphQLError(f"Invalid identification supplied: {filter_params}")
|
||||||
|
|
||||||
@@ -55,14 +42,10 @@ class CopilotQueries:
|
|||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class CopilotMutation:
|
class CopilotMutation:
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
async def create_copilot(root, info: Info, input: CreateCopilotInput) -> Copilot:
|
||||||
@authenticated_user_only()
|
|
||||||
async def create_copilot(root, info, input: CreateCopilotInput) -> Copilot:
|
|
||||||
return await BaseMutationResolver(Copilot, models.Copilot).create(info.context, data=input)
|
return await BaseMutationResolver(Copilot, models.Copilot).create(info.context, data=input)
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
async def edit_copilot(root, info: Info, id: int, input: EditCopilotInput) -> Copilot:
|
||||||
@authenticated_user_only()
|
return await BaseMutationResolver(Copilot, models.Copilot).update(info.context, id, input)
|
||||||
async def edit_copilot(root, info, id: int, input: EditCopilotInput) -> Copilot:
|
|
||||||
return await BaseMutationResolver(Copilot, models.Copilot).update(id, input, info.context.user_id)
|
|
||||||
|
|||||||
@@ -1,52 +1,39 @@
|
|||||||
from typing import Optional
|
|
||||||
import strawberry
|
import strawberry
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.endpoints import authenticated_user_only, allow_public
|
from graphql_schema.entities.helpers.filters import get_detail_filters
|
||||||
from decorators.error_logging import error_logging
|
from graphql_schema.entities.helpers.pagination import PaginationWindow
|
||||||
from graphql_schema.entities.helpers.detail import get_detail_filters
|
|
||||||
from graphql_schema.entities.helpers.pagination import PaginationWindow, get_pagination_window
|
|
||||||
from graphql_schema.entities.resolvers.base import BaseMutationResolver
|
from graphql_schema.entities.resolvers.base import BaseMutationResolver
|
||||||
from graphql_schema.entities.resolvers.event import EventQueryResolver
|
from graphql_schema.entities.resolvers.event import EventQueryResolver
|
||||||
from graphql_schema.entities.types.mutation_input import CreateEventInput, EditEventInput
|
from graphql_schema.entities.types.mutation_input import CreateEventInput, EditEventInput
|
||||||
from graphql_schema.entities.types.types import Event
|
from graphql_schema.entities.types.types import Event
|
||||||
|
from graphql_schema.extensions.field.auth import AllowPublicAccess, AuthenticatedOnly
|
||||||
|
from graphql_schema.extensions.field.pagination import OffsetPagination
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class EventQueries:
|
class EventQueries:
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[OffsetPagination(item_type=Event), AllowPublicAccess()])
|
||||||
@error_logging
|
|
||||||
@allow_public
|
|
||||||
async def events(
|
async def events(
|
||||||
root,
|
root,
|
||||||
info,
|
info,
|
||||||
limit: int,
|
username: str | None = None,
|
||||||
offset: int = 0,
|
public: bool | None = False,
|
||||||
username: Optional[str] = None,
|
|
||||||
public: Optional[bool] = False,
|
|
||||||
) -> PaginationWindow[Event]:
|
) -> PaginationWindow[Event]:
|
||||||
query = EventQueryResolver().get_query(
|
return EventQueryResolver().get_query(
|
||||||
user_id=info.context.user_id,
|
user_id=info.context.user_id,
|
||||||
username=username,
|
username=username,
|
||||||
order_by=[models.Event.date_from.desc(), models.Event.name.desc()],
|
order_by=[models.Event.date_from.desc(), models.Event.name.desc()],
|
||||||
only_public=public,
|
only_public=public,
|
||||||
)
|
)
|
||||||
|
|
||||||
return await get_pagination_window(
|
|
||||||
query=query,
|
|
||||||
item_type=Event,
|
|
||||||
limit=limit,
|
|
||||||
offset=offset
|
|
||||||
)
|
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||||
@error_logging
|
|
||||||
@allow_public
|
|
||||||
async def event(
|
async def event(
|
||||||
root, info,
|
root, info,
|
||||||
id: Optional[int] = None,
|
id: int | None = None,
|
||||||
url_slug: Optional[str] = None,
|
url_slug: str | None = None,
|
||||||
username: Optional[str] = None,
|
username: str | None = None,
|
||||||
public: Optional[bool] = False
|
public: bool | None = False
|
||||||
) -> Event:
|
) -> Event:
|
||||||
filter_params = get_detail_filters(id, url_slug)
|
filter_params = get_detail_filters(id, url_slug)
|
||||||
if username:
|
if username:
|
||||||
@@ -61,14 +48,10 @@ class EventQueries:
|
|||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class EventMutation:
|
class EventMutation:
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def create_event(root, info, input: CreateEventInput) -> Event:
|
async def create_event(root, info, input: CreateEventInput) -> Event:
|
||||||
return await BaseMutationResolver(Event, models.Event).create(info.context, input)
|
return await BaseMutationResolver(Event, models.Event).create(info.context, input)
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def edit_event(root, info, id: int, input: EditEventInput) -> Event:
|
async def edit_event(root, info, id: int, input: EditEventInput) -> Event:
|
||||||
return await BaseMutationResolver(Event, models.Event).update(id, input, info.context.user_id)
|
return await BaseMutationResolver(Event, models.Event).update(info.context, id, input)
|
||||||
|
|||||||
@@ -1,59 +1,45 @@
|
|||||||
from typing import Optional
|
|
||||||
import strawberry
|
import strawberry
|
||||||
from graphql import GraphQLError
|
|
||||||
|
|
||||||
from decorators.endpoints import authenticated_user_only, allow_public
|
|
||||||
from decorators.error_logging import error_logging
|
|
||||||
from graphql_schema.entities.resolvers.flight import FlightMutationResolver, FlightQueryResolver
|
from graphql_schema.entities.resolvers.flight import FlightMutationResolver, FlightQueryResolver
|
||||||
from graphql_schema.entities.types.mutation_input import EditFlightInput, CreateFlightInput
|
from graphql_schema.entities.types.mutation_input import EditFlightInput, CreateFlightInput
|
||||||
from graphql_schema.entities.types.types import Flight
|
from graphql_schema.entities.types.types import Flight
|
||||||
from .helpers.detail import get_detail_filters
|
from .helpers.filters import get_detail_filters
|
||||||
from .helpers.pagination import PaginationWindow, get_pagination_window
|
from .helpers.pagination import PaginationWindow
|
||||||
|
from ..extensions.field.auth import AllowPublicAccess, AuthenticatedOnly
|
||||||
|
from ..extensions.field.pagination import OffsetPagination
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class FlightQueries:
|
class FlightQueries:
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[OffsetPagination(item_type=Flight), AllowPublicAccess()])
|
||||||
@error_logging
|
|
||||||
@allow_public
|
|
||||||
async def flights(
|
async def flights(
|
||||||
root, info,
|
root, info,
|
||||||
limit: int,
|
username: str | None = None,
|
||||||
offset: int = 0,
|
search: str | None = None,
|
||||||
username: Optional[str] = None,
|
event_id: int | None = None,
|
||||||
event_id: Optional[int] = None,
|
public: bool | None = False,
|
||||||
public: Optional[bool] = False,
|
copilot_id: int | None = None,
|
||||||
copilot_id: Optional[int] = None,
|
point_of_interest_id: int | None = None,
|
||||||
point_of_interest_id: Optional[int] = None,
|
aircraft_id: int | None = None,
|
||||||
aircraft_id: Optional[int] = None,
|
|
||||||
) -> PaginationWindow[Flight]:
|
) -> PaginationWindow[Flight]:
|
||||||
query = FlightQueryResolver().get_query(
|
return FlightQueryResolver().get_query(
|
||||||
user_id=info.context.user_id,
|
user_id=info.context.user_id,
|
||||||
username=username,
|
username=username,
|
||||||
event_id=event_id,
|
event_id=event_id,
|
||||||
only_public=public,
|
only_public=public,
|
||||||
copilot_id=copilot_id,
|
copilot_id=copilot_id,
|
||||||
aircraft_id=aircraft_id,
|
aircraft_id=aircraft_id,
|
||||||
point_of_interest_id=point_of_interest_id
|
point_of_interest_id=point_of_interest_id,
|
||||||
|
search=search
|
||||||
)
|
)
|
||||||
|
|
||||||
return await get_pagination_window(
|
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||||
query=query,
|
|
||||||
item_type=Flight,
|
|
||||||
limit=limit,
|
|
||||||
offset=offset,
|
|
||||||
)
|
|
||||||
|
|
||||||
@strawberry.field()
|
|
||||||
@error_logging
|
|
||||||
@allow_public
|
|
||||||
async def flight(
|
async def flight(
|
||||||
root, info,
|
root, info,
|
||||||
id: Optional[int] = None,
|
id: int | None = None,
|
||||||
url_slug: Optional[str] = None,
|
url_slug: str | None = None,
|
||||||
username: Optional[str] = None,
|
username: str | None = None,
|
||||||
public: Optional[bool] = False
|
public: bool | None = False
|
||||||
) -> Flight:
|
) -> Flight:
|
||||||
filter_params = get_detail_filters(id, url_slug)
|
filter_params = get_detail_filters(id, url_slug)
|
||||||
if username:
|
if username:
|
||||||
@@ -68,20 +54,14 @@ class FlightQueries:
|
|||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class FlightMutation:
|
class FlightMutation:
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def create_flight(self, info, input: CreateFlightInput) -> Flight:
|
async def create_flight(self, info, input: CreateFlightInput) -> Flight:
|
||||||
return await FlightMutationResolver().create(info.context, input)
|
return await FlightMutationResolver().create(info.context, input)
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def edit_flight(self, info, id: int, input: EditFlightInput) -> Flight:
|
async def edit_flight(self, info, id: int, input: EditFlightInput) -> Flight:
|
||||||
return await FlightMutationResolver().update(info.context, id, input)
|
return await FlightMutationResolver().update(info.context, id, input)
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def delete_flight(self, info, id: int) -> Flight:
|
async def delete_flight(self, info, id: int) -> Flight:
|
||||||
return await FlightMutationResolver().delete(info.context.user_id, id)
|
return await FlightMutationResolver().delete(info.context, id)
|
||||||
|
|||||||
@@ -1,43 +1,45 @@
|
|||||||
from typing import List
|
|
||||||
import strawberry
|
import strawberry
|
||||||
from strawberry.types import Info
|
from strawberry.types import Info
|
||||||
from database import models
|
from graphql_schema.entities.helpers.filters import get_detail_filters
|
||||||
from decorators.error_logging import error_logging
|
from graphql_schema.entities.helpers.pagination import PaginationWindow
|
||||||
from decorators.endpoints import authenticated_user_only, allow_public
|
from graphql_schema.entities.resolvers.flight_plan import FlightPlanMutationResolver, FlightPlanQueryResolver
|
||||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
|
||||||
from graphql_schema.entities.resolvers.flight_plan import FlightPlanMutationResolver
|
|
||||||
from graphql_schema.entities.types.mutation_input import CreateFlightPlanInput, EditFlightPlanInput
|
from graphql_schema.entities.types.mutation_input import CreateFlightPlanInput, EditFlightPlanInput
|
||||||
from graphql_schema.entities.types.types import FlightPlan
|
from graphql_schema.entities.types.types import FlightPlan
|
||||||
|
from graphql_schema.extensions.field.auth import AuthenticatedOnly, AllowPublicAccess
|
||||||
|
from graphql_schema.extensions.field.pagination import OffsetPagination
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class FlightPlanQueries:
|
class FlightPlanQueries:
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[OffsetPagination(item_type=FlightPlan), AuthenticatedOnly()])
|
||||||
@error_logging
|
async def flight_plans(root, info: Info, search: str | None = None) -> PaginationWindow[FlightPlan]:
|
||||||
@authenticated_user_only()
|
return FlightPlanQueryResolver().get_query(info.context.user_id, search=search)
|
||||||
async def flight_plans(root, info: Info) -> List[FlightPlan]:
|
|
||||||
return await BaseQueryResolver(FlightPlan, models.FlightPlan).get_list(info.context.user_id)
|
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||||
@error_logging
|
async def flight_plan(
|
||||||
@allow_public
|
root,
|
||||||
async def flight_plan(root, info: Info, id: int) -> FlightPlan:
|
info: Info,
|
||||||
return await BaseQueryResolver(FlightPlan, models.FlightPlan).get_one(
|
id: int,
|
||||||
|
username: str | None = None,
|
||||||
|
public: bool | None = False
|
||||||
|
) -> FlightPlan:
|
||||||
|
filter_params = get_detail_filters(id)
|
||||||
|
if username:
|
||||||
|
filter_params['username'] = username
|
||||||
|
|
||||||
|
return await FlightPlanQueryResolver().get_one(
|
||||||
user_id=info.context.user_id,
|
user_id=info.context.user_id,
|
||||||
object_id=id
|
only_public=public,
|
||||||
|
**filter_params
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class FlightPlanMutation:
|
class FlightPlanMutation:
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
async def create_flight_plan(root, info: Info, input: CreateFlightPlanInput) -> FlightPlan:
|
||||||
@authenticated_user_only()
|
|
||||||
async def create_flight_plan(root, info, input: CreateFlightPlanInput) -> FlightPlan:
|
|
||||||
return await FlightPlanMutationResolver().create(info.context, data=input)
|
return await FlightPlanMutationResolver().create(info.context, data=input)
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
async def edit_flight_plan(root, info: Info, id: int, input: EditFlightPlanInput) -> FlightPlan:
|
||||||
@authenticated_user_only()
|
return await FlightPlanMutationResolver().update(info.context, id, input)
|
||||||
async def edit_flight_plan(root, info, id: int, input: EditFlightPlanInput) -> FlightPlan:
|
|
||||||
return await FlightPlanMutationResolver().update(id, input, info.context.user_id)
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ async def handle_combobox_save(
|
|||||||
input: ComboboxInput,
|
input: ComboboxInput,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
name_column: str = "name",
|
name_column: str = "name",
|
||||||
extra_data: Optional[dict] = None
|
extra_data: dict | None = None
|
||||||
) -> int:
|
) -> int:
|
||||||
if input.id:
|
if input.id:
|
||||||
return input.id
|
return input.id
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
from typing import Optional
|
|
||||||
from graphql import GraphQLError
|
|
||||||
|
|
||||||
|
|
||||||
def get_detail_filters(id: Optional[int], url_slug: Optional[str]) -> dict:
|
|
||||||
filter_params = {}
|
|
||||||
if id:
|
|
||||||
filter_params['object_id'] = id
|
|
||||||
if url_slug is not None:
|
|
||||||
filter_params['url_slug'] = url_slug
|
|
||||||
|
|
||||||
if not filter_params:
|
|
||||||
raise GraphQLError("You must specifiy either urlSlug or id!")
|
|
||||||
|
|
||||||
return filter_params
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from graphql import GraphQLError
|
||||||
|
|
||||||
|
|
||||||
|
def get_filters(**kwargs) -> dict[str, Any]:
|
||||||
|
return {k: v for k, v in kwargs.items() if v is not None}
|
||||||
|
|
||||||
|
|
||||||
|
def get_detail_filters(id: int | None = None, url_slug: str | None = None) -> dict:
|
||||||
|
filter_params = get_filters(object_id=id, url_slug=url_slug)
|
||||||
|
|
||||||
|
if not filter_params:
|
||||||
|
raise GraphQLError("You must specifiy either urlSlug or id!")
|
||||||
|
|
||||||
|
return filter_params
|
||||||
@@ -30,10 +30,6 @@ async def get_pagination_window(
|
|||||||
cnt_query = query.with_only_columns(func.count())
|
cnt_query = query.with_only_columns(func.count())
|
||||||
total_items_count = (await db.scalars(cnt_query)).one()
|
total_items_count = (await db.scalars(cnt_query)).one()
|
||||||
|
|
||||||
# if offset != 0 and not 0 <= offset < total_items_count:
|
|
||||||
# raise Exception(f"offset ({offset}) is out of range " f"(0-{total_items_count - 1})")
|
|
||||||
|
|
||||||
async with get_session() as db:
|
|
||||||
data = (await db.scalars(query.limit(limit).offset(offset))).all()
|
data = (await db.scalars(query.limit(limit).offset(offset))).all()
|
||||||
dataset = [item_type(**i.as_dict()) for i in data]
|
dataset = [item_type(**i.as_dict()) for i in data]
|
||||||
|
|
||||||
|
|||||||
@@ -3,27 +3,23 @@ import strawberry
|
|||||||
from sqlalchemy import delete
|
from sqlalchemy import delete
|
||||||
from sqlalchemy.dialects.mysql import insert
|
from sqlalchemy.dialects.mysql import insert
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from strawberry import Info
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.endpoints import authenticated_user_only
|
|
||||||
from database.transaction import get_session
|
from database.transaction import get_session
|
||||||
from decorators.error_logging import error_logging
|
|
||||||
from graphql_schema.entities.resolvers.base import BaseMutationResolver
|
from graphql_schema.entities.resolvers.base import BaseMutationResolver
|
||||||
from graphql_schema.entities.resolvers.organization import OrganizationQueryResolver
|
from graphql_schema.entities.resolvers.organization import OrganizationQueryResolver
|
||||||
from graphql_schema.entities.types.mutation_input import CreateOrganizationInput, EditOrganizationInput
|
from graphql_schema.entities.types.mutation_input import CreateOrganizationInput, EditOrganizationInput
|
||||||
from graphql_schema.entities.types.types import Organization
|
from graphql_schema.entities.types.types import Organization
|
||||||
|
from graphql_schema.extensions.field.auth import AuthenticatedOnly
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class OrganizationQueries:
|
class OrganizationQueries:
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def organizations(root, info) -> List[Organization]:
|
async def organizations(root, info) -> List[Organization]:
|
||||||
return await OrganizationQueryResolver().get_list()
|
return await OrganizationQueryResolver().get_list()
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def organization(root, info, id: int) -> Organization:
|
async def organization(root, info, id: int) -> Organization:
|
||||||
return await OrganizationQueryResolver().get_one(object_id=id)
|
return await OrganizationQueryResolver().get_one(object_id=id)
|
||||||
|
|
||||||
@@ -31,17 +27,14 @@ class OrganizationQueries:
|
|||||||
@strawberry.type
|
@strawberry.type
|
||||||
class OrganizationMutation:
|
class OrganizationMutation:
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
async def create_organization(root, info: Info, input: CreateOrganizationInput) -> Organization:
|
||||||
@authenticated_user_only()
|
|
||||||
async def create_organization(root, info, input: CreateOrganizationInput) -> Organization:
|
|
||||||
return await BaseMutationResolver(Organization, models.Organization).create(info.context, data=input)
|
return await BaseMutationResolver(Organization, models.Organization).create(info.context, data=input)
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
async def edit_organization(root, info: Info, id: int, input: EditOrganizationInput) -> Organization:
|
||||||
@authenticated_user_only()
|
|
||||||
async def edit_organization(root, info, id: int, input: EditOrganizationInput) -> Organization:
|
|
||||||
return await BaseMutationResolver(Organization, models.Organization).update(
|
return await BaseMutationResolver(Organization, models.Organization).update(
|
||||||
|
info.context,
|
||||||
id,
|
id,
|
||||||
data=input,
|
data=input,
|
||||||
user_id=info.context.user_id
|
user_id=info.context.user_id
|
||||||
@@ -51,9 +44,7 @@ class OrganizationMutation:
|
|||||||
@strawberry.type
|
@strawberry.type
|
||||||
class OrganizationUserMutation:
|
class OrganizationUserMutation:
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def add_to_organization(root, info, organization_id: int) -> Organization:
|
async def add_to_organization(root, info, organization_id: int) -> Organization:
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
organization = (await db.scalars(
|
organization = (await db.scalars(
|
||||||
@@ -72,9 +63,7 @@ class OrganizationUserMutation:
|
|||||||
|
|
||||||
return Organization(**organization.as_dict())
|
return Organization(**organization.as_dict())
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def remove_from_organization(root, info, organization_id: int) -> Organization:
|
async def remove_from_organization(root, info, organization_id: int) -> Organization:
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
organization = (await db.scalars(
|
organization = (await db.scalars(
|
||||||
|
|||||||
@@ -1,28 +1,31 @@
|
|||||||
from typing import List, Optional
|
|
||||||
import strawberry
|
import strawberry
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||||
|
from strawberry import Info
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.endpoints import authenticated_user_only, allow_public, raise_unauthorized
|
|
||||||
from decorators.error_logging import error_logging
|
|
||||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
||||||
from graphql_schema.entities.resolvers.photo import PhotoMutationResolver, PhotoQueryResolver
|
from graphql_schema.entities.resolvers.photo import PhotoMutationResolver, PhotoQueryResolver
|
||||||
from graphql_schema.entities.types.types import Photo
|
from graphql_schema.entities.types.types import Photo
|
||||||
from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput
|
from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput
|
||||||
|
from graphql_schema.extensions.field.auth import AuthenticatedOnly, AllowPublicAccess
|
||||||
|
|
||||||
|
|
||||||
|
def raise_unauthorized():
|
||||||
|
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class PhotoQueries:
|
class PhotoQueries:
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||||
@error_logging
|
|
||||||
@allow_public
|
|
||||||
async def photos(
|
async def photos(
|
||||||
root, info,
|
root, info: Info,
|
||||||
flight_id: Optional[int] = None,
|
flight_id: int | None = None,
|
||||||
copilot_id: Optional[int] = None,
|
copilot_id: int | None = None,
|
||||||
uploaded_by_copilot_id: Optional[int] = None,
|
uploaded_by_copilot_id: int | None = None,
|
||||||
point_of_interest_id: Optional[int] = None,
|
point_of_interest_id: int | None = None,
|
||||||
aircraft_id: Optional[int] = None,
|
aircraft_id: int | None = None,
|
||||||
public: Optional[bool] = False,
|
public: bool | None = False,
|
||||||
) -> List[Photo]:
|
) -> list[Photo]:
|
||||||
return await PhotoQueryResolver().get_list(
|
return await PhotoQueryResolver().get_list(
|
||||||
public=public,
|
public=public,
|
||||||
flight_id=flight_id,
|
flight_id=flight_id,
|
||||||
@@ -34,10 +37,8 @@ class PhotoQueries:
|
|||||||
order_by=[models.Photo.exposed_at]
|
order_by=[models.Photo.exposed_at]
|
||||||
)
|
)
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||||
@error_logging
|
async def photo(root, info: Info, id: int, public: bool | None = False) -> Photo:
|
||||||
@allow_public
|
|
||||||
async def photo(root, info, id: int, public: Optional[bool] = False, ) -> Photo:
|
|
||||||
return await BaseQueryResolver(Photo, models.Photo).get_one(
|
return await BaseQueryResolver(Photo, models.Photo).get_one(
|
||||||
object_id=id,
|
object_id=id,
|
||||||
user_id=info.context.user_id,
|
user_id=info.context.user_id,
|
||||||
@@ -47,23 +48,19 @@ class PhotoQueries:
|
|||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class PhotoMutation:
|
class PhotoMutation:
|
||||||
@strawberry.mutation
|
@strawberry.mutation()
|
||||||
@error_logging
|
|
||||||
async def upload_photo(self, info, input: UploadPhotoInput) -> Photo:
|
async def upload_photo(self, info, input: UploadPhotoInput) -> Photo:
|
||||||
if info.context.user_id is None and not input.copilot_upload_token:
|
if info.context.user_id is None and not input.copilot_upload_token:
|
||||||
raise_unauthorized()
|
raise_unauthorized()
|
||||||
|
|
||||||
return await PhotoMutationResolver().upload(info, input)
|
return await PhotoMutationResolver().upload(info, input)
|
||||||
|
|
||||||
@strawberry.mutation()
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def edit_photo(self, info, id: int, input: EditPhotoInput) -> Photo:
|
async def edit_photo(self, info, id: int, input: EditPhotoInput) -> Photo:
|
||||||
return await PhotoMutationResolver().update(id, input, info.context.user_id)
|
return await PhotoMutationResolver().update(info.context, id, input, info.context.user_id)
|
||||||
|
|
||||||
@strawberry.mutation()
|
@strawberry.mutation()
|
||||||
@error_logging
|
async def change_orientation(self, info, id: int, direction: str, copilot_upload_token: str | None = None) -> Photo:
|
||||||
async def change_orientation(self, info, id: int, direction: str, copilot_upload_token: Optional[str] = None) -> Photo:
|
|
||||||
if info.context.user_id is None and not copilot_upload_token:
|
if info.context.user_id is None and not copilot_upload_token:
|
||||||
raise_unauthorized()
|
raise_unauthorized()
|
||||||
|
|
||||||
@@ -75,16 +72,13 @@ class PhotoMutation:
|
|||||||
info=info
|
info=info
|
||||||
)
|
)
|
||||||
|
|
||||||
@strawberry.mutation()
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def adjust_photo(self, info, id: int, adjustment: AdjustmentInput) -> Photo:
|
async def adjust_photo(self, info, id: int, adjustment: AdjustmentInput) -> Photo:
|
||||||
return await PhotoMutationResolver().adjust(id, info=info, user_id=info.context.user_id, adjustment=adjustment)
|
return await PhotoMutationResolver().adjust(id, info=info, user_id=info.context.user_id, adjustment=adjustment)
|
||||||
|
|
||||||
@strawberry.mutation()
|
@strawberry.mutation()
|
||||||
@error_logging
|
async def delete_photo(self, info, id: int, copilot_upload_token: str | None = None) -> Photo:
|
||||||
async def delete_photo(self, info, id: int, copilot_upload_token: Optional[str] = None) -> Photo:
|
|
||||||
if info.context.user_id is None and not copilot_upload_token:
|
if info.context.user_id is None and not copilot_upload_token:
|
||||||
raise_unauthorized()
|
raise_unauthorized()
|
||||||
|
|
||||||
return await PhotoMutationResolver().delete(user_id=info.context.user_id, id=id)
|
return await PhotoMutationResolver().delete(info.context, id=id)
|
||||||
|
|||||||
@@ -1,48 +1,37 @@
|
|||||||
from typing import Optional
|
|
||||||
import strawberry
|
import strawberry
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.endpoints import authenticated_user_only, allow_public
|
|
||||||
from database.transaction import get_session
|
from database.transaction import get_session
|
||||||
from decorators.error_logging import error_logging
|
|
||||||
from graphql_schema.entities.helpers.combobox import handle_combobox_save
|
from graphql_schema.entities.helpers.combobox import handle_combobox_save
|
||||||
from graphql_schema.entities.helpers.detail import get_detail_filters
|
from graphql_schema.entities.helpers.filters import get_detail_filters
|
||||||
from graphql_schema.entities.helpers.pagination import get_pagination_window, PaginationWindow
|
from graphql_schema.entities.helpers.pagination import PaginationWindow
|
||||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
|
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
|
||||||
from graphql_schema.entities.types.types import PointOfInterest
|
from graphql_schema.entities.types.types import PointOfInterest
|
||||||
from graphql_schema.entities.types.mutation_input import CreatePointOfInterestInput, EditPointOfInterestInput
|
from graphql_schema.entities.types.mutation_input import CreatePointOfInterestInput, EditPointOfInterestInput
|
||||||
|
from graphql_schema.extensions.field.auth import AuthenticatedOnly, AllowPublicAccess
|
||||||
|
from graphql_schema.extensions.field.pagination import OffsetPagination
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class PointOfInterestQueries:
|
class PointOfInterestQueries:
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[OffsetPagination(item_type=PointOfInterest), AllowPublicAccess()])
|
||||||
@error_logging
|
|
||||||
@allow_public
|
|
||||||
async def points_of_interest(
|
async def points_of_interest(
|
||||||
root, info,
|
root, info,
|
||||||
limit: int, offset: int = 0,
|
search: str | None = None,
|
||||||
search: Optional[str] = None,
|
public: bool = False,
|
||||||
public: bool = False
|
|
||||||
) -> PaginationWindow[PointOfInterest]:
|
) -> PaginationWindow[PointOfInterest]:
|
||||||
query = BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_query(
|
return BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_query(
|
||||||
info.context.user_id,
|
info.context.user_id,
|
||||||
only_my=bool(info.context.user_id),
|
only_my=bool(info.context.user_id),
|
||||||
include_others_public=True,
|
include_others_public=True,
|
||||||
only_public=public,
|
only_public=public,
|
||||||
search=search,
|
search=search,
|
||||||
)
|
)
|
||||||
return await get_pagination_window(
|
|
||||||
query=query,
|
|
||||||
item_type=PointOfInterest,
|
|
||||||
limit=limit,
|
|
||||||
offset=offset
|
|
||||||
)
|
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AllowPublicAccess()])
|
||||||
@allow_public
|
|
||||||
async def point_of_interest(
|
async def point_of_interest(
|
||||||
root, info,
|
root, info,
|
||||||
url_slug: Optional[str] = None,
|
url_slug: str | None = None,
|
||||||
id: Optional[int] = None,
|
id: int | None = None,
|
||||||
public: bool = False
|
public: bool = False
|
||||||
) -> PointOfInterest:
|
) -> PointOfInterest:
|
||||||
filter_params = get_detail_filters(id, url_slug)
|
filter_params = get_detail_filters(id, url_slug)
|
||||||
@@ -56,9 +45,7 @@ class PointOfInterestQueries:
|
|||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class PointOfInterestMutation:
|
class PointOfInterestMutation:
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def create_point_of_interest(root, info, input: CreatePointOfInterestInput) -> PointOfInterest:
|
async def create_point_of_interest(root, info, input: CreatePointOfInterestInput) -> PointOfInterest:
|
||||||
input_data = input.to_dict()
|
input_data = input.to_dict()
|
||||||
|
|
||||||
@@ -73,9 +60,7 @@ class PointOfInterestMutation:
|
|||||||
db, input_data
|
db, input_data
|
||||||
)
|
)
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def edit_point_of_interest(root, info, id: int, input: EditPointOfInterestInput) -> PointOfInterest:
|
async def edit_point_of_interest(root, info, id: int, input: EditPointOfInterestInput) -> PointOfInterest:
|
||||||
input_data = input.to_dict()
|
input_data = input.to_dict()
|
||||||
|
|
||||||
@@ -93,8 +78,6 @@ class PointOfInterestMutation:
|
|||||||
updated_poi = await models.PointOfInterest.update(db, obj=poi, data=input_data)
|
updated_poi = await models.PointOfInterest.update(db, obj=poi, data=input_data)
|
||||||
return PointOfInterest(**updated_poi.as_dict())
|
return PointOfInterest(**updated_poi.as_dict())
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def delete_point_of_interest(self, info, id: int) -> PointOfInterest:
|
async def delete_point_of_interest(self, info, id: int) -> PointOfInterest:
|
||||||
return await BaseMutationResolver(PointOfInterest, models.PointOfInterest).delete(info.context.user_id, id=id)
|
return await BaseMutationResolver(PointOfInterest, models.PointOfInterest).delete(info.context.user_id, id=id)
|
||||||
|
|||||||
@@ -1,92 +1,20 @@
|
|||||||
from typing import List
|
|
||||||
import strawberry
|
import strawberry
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.endpoints import authenticated_user_only
|
|
||||||
from decorators.error_logging import error_logging
|
|
||||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
||||||
from graphql_schema.entities.types.types import PointOfInterestType
|
from graphql_schema.entities.types.types import PointOfInterestType
|
||||||
|
from graphql_schema.extensions.field.auth import AuthenticatedOnly
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class PointOfInterestTypeQueries:
|
class PointOfInterestTypeQueries:
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
async def point_of_interest_types(root, info) -> list[PointOfInterestType]:
|
||||||
@authenticated_user_only()
|
|
||||||
async def point_of_interest_types(root, info) -> List[PointOfInterestType]:
|
|
||||||
return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_list(info.context.user_id)
|
return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_list(info.context.user_id)
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||||
@error_logging
|
|
||||||
@authenticated_user_only()
|
|
||||||
async def point_of_interest_type(root, info, id: int) -> PointOfInterestType:
|
async def point_of_interest_type(root, info, id: int) -> PointOfInterestType:
|
||||||
return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_one(
|
return await BaseQueryResolver(PointOfInterestType, models.PointOfInterestType).get_one(
|
||||||
object_id=id,
|
object_id=id,
|
||||||
user_id=info.context.user_id
|
user_id=info.context.user_id
|
||||||
)
|
)
|
||||||
|
|
||||||
#
|
|
||||||
# @strawberry.type
|
|
||||||
# class CreatePointOfInterestMutation:
|
|
||||||
# @strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id'])
|
|
||||||
# class CreatePointOfInterestInput:
|
|
||||||
# type: # Optional[ComboboxInput] = None
|
|
||||||
#
|
|
||||||
# @strawberry.mutation
|
|
||||||
# @authenticated_user_only()
|
|
||||||
# async def create_point_of_interest(root, info, input: CreatePointOfInterestInput) -> PointOfInterest:
|
|
||||||
# input_data = input.to_dict()
|
|
||||||
#
|
|
||||||
# input_data['type_id'] = await handle_combobox_save(
|
|
||||||
# info.context.db,
|
|
||||||
# models.PointOfInterestType,
|
|
||||||
# input.type,
|
|
||||||
# info.context.user_id
|
|
||||||
# )
|
|
||||||
#
|
|
||||||
# return await models.PointOfInterest.create(
|
|
||||||
# info.context.db,
|
|
||||||
# data=dict(
|
|
||||||
# **input_data,
|
|
||||||
# created_by_id=info.context.user_id,
|
|
||||||
# )
|
|
||||||
# )
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# @strawberry.type
|
|
||||||
# class EditPointOfInterestMutation:
|
|
||||||
# @strawberry_sqlalchemy_input(models.PointOfInterest, exclude_fields=['id', 'type_id'])
|
|
||||||
# class EditPointOfInterestInput:
|
|
||||||
# type: Optional[ComboboxInput] = None
|
|
||||||
#
|
|
||||||
# @strawberry.mutation
|
|
||||||
# @authenticated_user_only()
|
|
||||||
# async def edit_point_of_interest(root, info, id: int, input: EditPointOfInterestInput) -> PointOfInterest:
|
|
||||||
# # TODO: kontrola organizace
|
|
||||||
# input_data = input.to_dict()
|
|
||||||
#
|
|
||||||
# if 'type' in input:
|
|
||||||
# input_data['type_id'] = await handle_combobox_save(
|
|
||||||
# info.context.db,
|
|
||||||
# models.PointOfInterestType,
|
|
||||||
# input.type,
|
|
||||||
# info.context.user_id
|
|
||||||
# )
|
|
||||||
#
|
|
||||||
# poi = (
|
|
||||||
# await info.context.db.scalars(
|
|
||||||
# get_base_query(info.context.user_id, only_my=True)
|
|
||||||
# .filter(models.PointOfInterest.id == id))
|
|
||||||
# ).one()
|
|
||||||
# return await models.PointOfInterest.update(info.context.db, obj=poi, data=input_data)
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# @strawberry.type
|
|
||||||
# class DeletePointOfInterestMutation:
|
|
||||||
#
|
|
||||||
# @strawberry.mutation
|
|
||||||
# @authenticated_user_only()
|
|
||||||
# async def delete_point_of_interest(self, info, id: int) -> PointOfInterest:
|
|
||||||
# poi = get_base_query(info.context.user_id, only_my=True).filter(models.PointOfInterest.id == id).one()
|
|
||||||
#
|
|
||||||
# return await models.PointOfInterest.update(info.context.db, obj=poi, data=dict(deleted=True))
|
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
from operator import or_
|
from sqlalchemy import and_, or_
|
||||||
from typing import Set, Optional
|
|
||||||
|
|
||||||
from sqlalchemy import and_
|
|
||||||
|
|
||||||
from database import models
|
from database import models
|
||||||
from database.transaction import get_session
|
from database.transaction import get_session
|
||||||
|
from graphql_schema.context import GraphQLContext
|
||||||
from graphql_schema.entities.helpers.combobox import handle_combobox_save
|
from graphql_schema.entities.helpers.combobox import handle_combobox_save
|
||||||
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
|
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
|
||||||
from graphql_schema.entities.types.mutation_input import EditAircraftInput, CreateAircraftInput
|
from graphql_schema.entities.types.mutation_input import EditAircraftInput, CreateAircraftInput
|
||||||
@@ -17,29 +14,36 @@ class AircraftQueryResolver(BaseQueryResolver):
|
|||||||
|
|
||||||
def get_query(
|
def get_query(
|
||||||
self,
|
self,
|
||||||
user_id: Optional[int] = None,
|
user_id: int | None = None,
|
||||||
object_id: Optional[int] = None,
|
object_id: int | None = None,
|
||||||
order_by: Optional[list] = None,
|
order_by: list | None = None,
|
||||||
organization_ids: Optional[Set[int]] = None,
|
organization_ids: set[int] | None = None,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
filters = []
|
filters = []
|
||||||
search = {}
|
|
||||||
if kwargs.get("call_sign"):
|
if kwargs.get("call_sign"):
|
||||||
filters.append(models.Aircraft.call_sign == kwargs['call_sign'])
|
filters.append(models.Aircraft.call_sign == kwargs['call_sign'])
|
||||||
|
|
||||||
if kwargs.get("search"):
|
|
||||||
search['search'] = kwargs.pop("search", None)
|
|
||||||
|
|
||||||
query = super().get_query(
|
query = super().get_query(
|
||||||
object_id=object_id,
|
object_id=object_id,
|
||||||
only_my=False,
|
only_my=False,
|
||||||
only_public=kwargs.get("only_public", False),
|
only_public=kwargs.get("only_public", False),
|
||||||
order_by=order_by,
|
order_by=order_by,
|
||||||
filters=filters,
|
filters=filters,
|
||||||
**search
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if kwargs.get("search"):
|
||||||
|
search = kwargs["search"]
|
||||||
|
query = query.filter(
|
||||||
|
or_(
|
||||||
|
models.Aircraft.call_sign.contains(search),
|
||||||
|
models.Aircraft.manufacturer.contains(search),
|
||||||
|
models.Aircraft.model.contains(search),
|
||||||
|
)
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
if organization_ids:
|
if organization_ids:
|
||||||
query = (
|
query = (
|
||||||
query.filter(
|
query.filter(
|
||||||
@@ -62,7 +66,7 @@ class AircraftMutationResolver(BaseMutationResolver):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(graphql_type=Aircraft, model=models.Aircraft)
|
super().__init__(graphql_type=Aircraft, model=models.Aircraft)
|
||||||
|
|
||||||
async def create(self, context, data: CreateAircraftInput) -> Aircraft:
|
async def create(self, context: GraphQLContext, data: CreateAircraftInput) -> Aircraft:
|
||||||
input_data = data.to_dict()
|
input_data = data.to_dict()
|
||||||
|
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
@@ -77,7 +81,7 @@ class AircraftMutationResolver(BaseMutationResolver):
|
|||||||
|
|
||||||
return await self._do_create(db, data=input_data)
|
return await self._do_create(db, data=input_data)
|
||||||
|
|
||||||
async def update(self, id: int, user_id: int, data: EditAircraftInput) -> Aircraft:
|
async def update(self, context: GraphQLContext, id: int, data: EditAircraftInput) -> Aircraft:
|
||||||
update_data = data.to_dict()
|
update_data = data.to_dict()
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
if data.organization:
|
if data.organization:
|
||||||
@@ -85,7 +89,7 @@ class AircraftMutationResolver(BaseMutationResolver):
|
|||||||
db,
|
db,
|
||||||
models.Organization,
|
models.Organization,
|
||||||
input=data.organization,
|
input=data.organization,
|
||||||
user_id=user_id,
|
user_id=context.user_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
return await self._do_update(db, id, update_data)
|
return await self._do_update(db, id, update_data)
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
from typing import Optional, Type, TypeVar, Generic, List
|
from typing import Type, TypeVar, Generic
|
||||||
|
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from database import models
|
from database import models
|
||||||
from database.query_builder import QueryBuilder
|
from database.query_builder import QueryBuilder
|
||||||
from database.transaction import get_session
|
from database.transaction import get_session
|
||||||
|
from graphql_schema.context import GraphQLContext
|
||||||
from graphql_schema.entities.types.base import BaseGraphqlInputType
|
from graphql_schema.entities.types.base import BaseGraphqlInputType
|
||||||
|
|
||||||
|
|
||||||
GQL_TYPE = TypeVar('GQL_TYPE')
|
GQL_TYPE = TypeVar('GQL_TYPE')
|
||||||
|
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ class BaseResolver(Generic[GQL_TYPE]):
|
|||||||
|
|
||||||
|
|
||||||
class BaseQueryResolver(BaseResolver):
|
class BaseQueryResolver(BaseResolver):
|
||||||
async def _get_list(self, query) -> List[GQL_TYPE]:
|
async def _get_list(self, query) -> list[GQL_TYPE]:
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
items = (await db.scalars(query)).all()
|
items = (await db.scalars(query)).all()
|
||||||
|
|
||||||
@@ -31,14 +32,14 @@ class BaseQueryResolver(BaseResolver):
|
|||||||
|
|
||||||
def get_query(
|
def get_query(
|
||||||
self,
|
self,
|
||||||
user_id: Optional[int] = None,
|
user_id: int | None = None,
|
||||||
object_id: Optional[int] = None,
|
object_id: int | None = None,
|
||||||
order_by: Optional[list] = None,
|
order_by: list | None = None,
|
||||||
only_public: Optional[bool] = False,
|
only_public: bool | None = False,
|
||||||
only_my: Optional[bool] = False,
|
only_my: bool | None = False,
|
||||||
include_others_public: Optional[bool] = False,
|
include_others_public: bool | None = False,
|
||||||
url_slug: Optional[str] = None,
|
url_slug: str | None = None,
|
||||||
filters: Optional[list] = None,
|
filters: list | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
query = self.query_builder.get_simple_query(
|
query = self.query_builder.get_simple_query(
|
||||||
@@ -77,11 +78,11 @@ class BaseQueryResolver(BaseResolver):
|
|||||||
query = query.filter(or_(*search_clauses))
|
query = query.filter(or_(*search_clauses))
|
||||||
return query
|
return query
|
||||||
|
|
||||||
async def get_list(self, user_id: Optional[int] = None, **kwargs) -> List[GQL_TYPE]:
|
async def get_list(self, user_id: int | None = None, **kwargs) -> list[GQL_TYPE]:
|
||||||
query = self.get_query(user_id=user_id, **kwargs)
|
query = self.get_query(user_id=user_id, **kwargs)
|
||||||
return await self._get_list(query)
|
return await self._get_list(query)
|
||||||
|
|
||||||
async def get_one(self, user_id: Optional[int] = None, **kwargs) -> GQL_TYPE:
|
async def get_one(self, user_id: int | None = None, **kwargs) -> GQL_TYPE:
|
||||||
query = self.get_query(user_id=user_id, **kwargs)
|
query = self.get_query(user_id=user_id, **kwargs)
|
||||||
return await self._get_one(query)
|
return await self._get_one(query)
|
||||||
|
|
||||||
@@ -107,7 +108,7 @@ class BaseMutationResolver(BaseResolver):
|
|||||||
model = await self.model.update(db, data=data, **update_where)
|
model = await self.model.update(db, data=data, **update_where)
|
||||||
return self.graphql_type(**model.as_dict())
|
return self.graphql_type(**model.as_dict())
|
||||||
|
|
||||||
async def create(self, context, data: BaseGraphqlInputType) -> GQL_TYPE:
|
async def create(self, context: GraphQLContext, data: BaseGraphqlInputType) -> GQL_TYPE:
|
||||||
input_data = data.to_dict()
|
input_data = data.to_dict()
|
||||||
|
|
||||||
if hasattr(self.model, "created_by_id"):
|
if hasattr(self.model, "created_by_id"):
|
||||||
@@ -116,14 +117,14 @@ class BaseMutationResolver(BaseResolver):
|
|||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
return await self._do_create(db, input_data)
|
return await self._do_create(db, input_data)
|
||||||
|
|
||||||
async def update(self, id: int, data: BaseGraphqlInputType, user_id: int) -> GQL_TYPE:
|
async def update(self, context: GraphQLContext, id: int, data: BaseGraphqlInputType) -> GQL_TYPE:
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
item = await self._get_one(db, id, user_id)
|
item = await self._get_one(db, id, context.user_id)
|
||||||
return await self._do_update(db, item, data.to_dict())
|
return await self._do_update(db, item, data.to_dict())
|
||||||
|
|
||||||
async def delete(self, user_id: int, id: int, **kwargs) -> GQL_TYPE:
|
async def delete(self, context: GraphQLContext, id: int, **kwargs) -> GQL_TYPE:
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
model = await self._get_one(db, id, user_id)
|
model = await self._get_one(db, id, context.user_id)
|
||||||
|
|
||||||
if hasattr(self.model, "deleted"):
|
if hasattr(self.model, "deleted"):
|
||||||
model = await self.model.update(db, obj=model, data=dict(deleted=True))
|
model = await self.model.update(db, obj=model, data=dict(deleted=True))
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
|
|
||||||
from database import models
|
from database import models
|
||||||
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
from graphql_schema.entities.resolvers.base import BaseQueryResolver
|
||||||
from graphql_schema.entities.types.types import Copilot
|
from graphql_schema.entities.types.types import Copilot
|
||||||
@@ -13,10 +10,10 @@ class CopilotQueryResolver(BaseQueryResolver):
|
|||||||
|
|
||||||
def get_query(
|
def get_query(
|
||||||
self,
|
self,
|
||||||
user_id: Optional[int] = None,
|
user_id: int | None = None,
|
||||||
object_id: Optional[int] = None,
|
object_id: int | None = None,
|
||||||
order_by: Optional[list] = None,
|
order_by: list | None = None,
|
||||||
only_public: Optional[bool] = False,
|
only_public: bool | None = False,
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
pilot_username = kwargs.pop("pilot_username", None)
|
pilot_username = kwargs.pop("pilot_username", None)
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ class EventQueryResolver(BaseQueryResolver):
|
|||||||
|
|
||||||
def get_query(
|
def get_query(
|
||||||
self,
|
self,
|
||||||
user_id: Optional[int] = None,
|
user_id: int | None = None,
|
||||||
object_id: Optional[int] = None,
|
object_id: int | None = None,
|
||||||
order_by: Optional[list] = None,
|
order_by: list | None = None,
|
||||||
only_public: Optional[bool] = True,
|
only_public: bool | None = True,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import List, Optional
|
import random
|
||||||
from sqlalchemy import delete, insert, select, func, text
|
from typing import Optional
|
||||||
|
from sqlalchemy import delete, insert, or_, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from strawberry.file_uploads import Upload
|
|
||||||
from background_jobs.elevation import add_terrain_elevation_to_flight
|
from background_jobs.elevation import add_terrain_elevation_to_flight
|
||||||
from background_jobs.weather import download_weather
|
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
|
from database import models
|
||||||
from database.models import flight_has_copilot
|
from database.models import flight_has_copilot
|
||||||
from database.transaction import get_session
|
from database.transaction import get_session
|
||||||
from external.gpx_parser import GPXParser
|
|
||||||
from graphql_schema.entities.helpers.combobox import handle_combobox_save
|
from graphql_schema.entities.helpers.combobox import handle_combobox_save
|
||||||
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
|
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
|
||||||
from graphql_schema.entities.types.mutation_input import (
|
from graphql_schema.entities.types.mutation_input import (
|
||||||
@@ -16,9 +18,8 @@ from graphql_schema.entities.types.mutation_input import (
|
|||||||
)
|
)
|
||||||
from graphql_schema.entities.types.types import Flight
|
from graphql_schema.entities.types.types import Flight
|
||||||
from paths import FLIGHT_GPX_TRACK_PATH
|
from paths import FLIGHT_GPX_TRACK_PATH
|
||||||
from utils.file import delete_file
|
from utils.file import handle_file_upload
|
||||||
from utils.str_utils import random_str
|
from utils.str_utils import random_str
|
||||||
from utils.upload import handle_file_upload
|
|
||||||
|
|
||||||
|
|
||||||
class FlightQueryResolver(BaseQueryResolver):
|
class FlightQueryResolver(BaseQueryResolver):
|
||||||
@@ -27,18 +28,12 @@ class FlightQueryResolver(BaseQueryResolver):
|
|||||||
|
|
||||||
def get_query(
|
def get_query(
|
||||||
self,
|
self,
|
||||||
user_id: Optional[int] = None,
|
user_id: int | None = None,
|
||||||
object_id: Optional[int] = None,
|
only_public: bool | None = False,
|
||||||
url_slug: Optional[str] = None,
|
|
||||||
only_public: Optional[bool] = False,
|
|
||||||
*args,
|
*args,
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
filters = {}
|
filters = {key: kwargs[key] for key in ("object_id", "url_slug") if key in kwargs}
|
||||||
if object_id:
|
|
||||||
filters['object_id'] = object_id
|
|
||||||
if url_slug:
|
|
||||||
filters['url_slug'] = url_slug
|
|
||||||
|
|
||||||
query = super().get_query(
|
query = super().get_query(
|
||||||
user_id,
|
user_id,
|
||||||
@@ -62,8 +57,27 @@ class FlightQueryResolver(BaseQueryResolver):
|
|||||||
|
|
||||||
if kwargs.get("point_of_interest_id"):
|
if kwargs.get("point_of_interest_id"):
|
||||||
query = (
|
query = (
|
||||||
query.join(models.Flight.track)
|
query
|
||||||
.filter(models.FlightTrack.point_of_interest_id == kwargs["point_of_interest_id"])
|
.filter(
|
||||||
|
or_(
|
||||||
|
models.Flight.id.in_(
|
||||||
|
select(models.FlightTurnPoint.flight_id)
|
||||||
|
.filter(models.FlightTurnPoint.point_of_interest_id == kwargs["point_of_interest_id"])
|
||||||
|
),
|
||||||
|
models.Flight.id.in_(
|
||||||
|
select(models.Photo.flight_id)
|
||||||
|
.filter(models.Photo.point_of_interest_id == kwargs["point_of_interest_id"])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if kwargs.get("search"):
|
||||||
|
search = kwargs["search"]
|
||||||
|
query = (
|
||||||
|
query.filter(
|
||||||
|
models.Flight.name.contains(search)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if kwargs.get('username'):
|
if kwargs.get('username'):
|
||||||
@@ -79,55 +93,13 @@ class FlightMutationResolver(BaseMutationResolver):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(Flight, models.Flight)
|
super().__init__(Flight, models.Flight)
|
||||||
|
|
||||||
async def get_airport_id_by_gps(self, gps_lat: float, gps_lng: float) -> Optional[int]:
|
async def create(self, context: GraphQLContext, input: CreateFlightInput) -> Flight:
|
||||||
async with (get_session() as db):
|
|
||||||
query = (
|
|
||||||
select(models.Airport, func.coalesce(6371 * func.acos(
|
|
||||||
func.cos(func.radians(gps_lat)) *
|
|
||||||
func.cos(func.radians(models.Airport.gps_latitude)) *
|
|
||||||
func.cos(func.radians(models.Airport.gps_longitude) - func.radians(gps_lng)) +
|
|
||||||
func.sin(func.radians(gps_lat)) *
|
|
||||||
func.sin(func.radians(models.Airport.gps_latitude))
|
|
||||||
), 9999).label("distance"))
|
|
||||||
.filter(models.Airport.use_in_gpx_guess.is_(True))
|
|
||||||
.order_by("distance")
|
|
||||||
.having(text("distance < 1"))
|
|
||||||
.limit(1)
|
|
||||||
)
|
|
||||||
|
|
||||||
data = (await db.execute(query)).one_or_none()
|
|
||||||
if data:
|
|
||||||
airport, distance = data
|
|
||||||
return airport.id
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def extract_data_from_gpx(self, gpx_filename: str) -> dict:
|
|
||||||
data = GPXParser(f"{FLIGHT_GPX_TRACK_PATH}/{gpx_filename}")
|
|
||||||
|
|
||||||
times, coordinates = await asyncio.gather(
|
|
||||||
data.get_times(),
|
|
||||||
data.get_coordinates()
|
|
||||||
)
|
|
||||||
takeoff_airport_id, landing_airport_id = await asyncio.gather(
|
|
||||||
self.get_airport_id_by_gps(coordinates[0]['lat'], coordinates[0]['lng']),
|
|
||||||
self.get_airport_id_by_gps(coordinates[-1]['lat'], coordinates[-1]['lng']),
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"takeoff_airport_id": takeoff_airport_id,
|
|
||||||
"landing_airport_id": landing_airport_id,
|
|
||||||
"takeoff_datetime": times[0],
|
|
||||||
"landing_datetime": times[-1],
|
|
||||||
}
|
|
||||||
|
|
||||||
async def create(self, context, input: CreateFlightInput) -> Flight:
|
|
||||||
data = input.to_dict()
|
data = input.to_dict()
|
||||||
user_id = context.user_id
|
user_id = context.user_id
|
||||||
|
|
||||||
if input.gpx_track_file:
|
if input.gpx_track_file:
|
||||||
data['gpx_track_filename'] = await handle_upload_gpx(gpx_track=input.gpx_track_file, context=context)
|
data['gpx_track_filename'] = await handle_file_upload(input.gpx_track_file, FLIGHT_GPX_TRACK_PATH)
|
||||||
data_from_gpx = await self.extract_data_from_gpx(data['gpx_track_filename'])
|
data_from_gpx = await extract_basic_flight_info_from_gpx(data['gpx_track_filename'])
|
||||||
data.update(data_from_gpx)
|
data.update(data_from_gpx)
|
||||||
else:
|
else:
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
@@ -148,26 +120,23 @@ class FlightMutationResolver(BaseMutationResolver):
|
|||||||
data.update({
|
data.update({
|
||||||
"aircraft_id": aircraft_id,
|
"aircraft_id": aircraft_id,
|
||||||
"has_terrain_elevation": False,
|
"has_terrain_elevation": False,
|
||||||
"name": "",
|
"name": input.name or '',
|
||||||
"description": "",
|
"description": "",
|
||||||
"created_by_id": context.user_id
|
"created_by_id": context.user_id
|
||||||
})
|
})
|
||||||
flight = await self._do_create(db, data)
|
flight = await self._do_create(db, data)
|
||||||
|
|
||||||
context.background_tasks.add_task(
|
if input.track is not None:
|
||||||
download_weather,
|
await handle_track_edit(db=db, flight_id=flight.id, track=input.track, user_id=user_id)
|
||||||
flight_id=flight.id, airport_id=flight.takeoff_airport_id, date_time=flight.takeoff_datetime,
|
|
||||||
type_="takeoff"
|
if 'gpx_track_filename' in data:
|
||||||
)
|
await save_track_from_gpx_to_db(gpx_filename=data['gpx_track_filename'], flight_id=flight.id)
|
||||||
context.background_tasks.add_task(
|
|
||||||
download_weather,
|
schedule_background_tasks(flight.id, data, context)
|
||||||
flight_id=flight.id, airport_id=flight.landing_airport_id, date_time=flight.landing_datetime,
|
|
||||||
type_="landing"
|
|
||||||
)
|
|
||||||
|
|
||||||
return flight
|
return flight
|
||||||
|
|
||||||
async def update(self, context, id: int, input: EditFlightInput) -> Flight:
|
async def update(self, context: GraphQLContext, id: int, input: EditFlightInput) -> Flight:
|
||||||
user_id = context.user_id
|
user_id = context.user_id
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
flight = await self._get_one(db, id, user_id)
|
flight = await self._get_one(db, id, user_id)
|
||||||
@@ -177,40 +146,23 @@ class FlightMutationResolver(BaseMutationResolver):
|
|||||||
data = input.to_dict()
|
data = input.to_dict()
|
||||||
|
|
||||||
if input.gpx_track_file is not None:
|
if input.gpx_track_file is not None:
|
||||||
data['gpx_track_filename'] = await handle_upload_gpx(
|
data['gpx_track_filename'] = await handle_upload_gpx(gpx_track=input.gpx_track_file, flight_id=flight_id)
|
||||||
gpx_track=input.gpx_track_file,
|
|
||||||
context=context,
|
|
||||||
original_gpx_filename=flight_data['gpx_track_filename']
|
|
||||||
)
|
|
||||||
|
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
if input.takeoff_airport:
|
if input.takeoff_airport:
|
||||||
takeoff_airport_id = await handle_combobox_save(
|
data['takeoff_airport_id'] = await handle_combobox_save(
|
||||||
db, models.Airport, input.takeoff_airport, user_id, name_column="icao_code",
|
db, models.Airport, input.takeoff_airport, user_id, name_column="icao_code",
|
||||||
extra_data={"name": input.takeoff_airport.name}
|
extra_data={"name": input.takeoff_airport.name}
|
||||||
)
|
)
|
||||||
data['takeoff_airport_id'] = takeoff_airport_id
|
|
||||||
data['takeoff_datetime'] = input.takeoff_datetime or flight_data['takeoff_datetime']
|
data['takeoff_datetime'] = input.takeoff_datetime or flight_data['takeoff_datetime']
|
||||||
|
|
||||||
context.background_tasks.add_task(
|
|
||||||
download_weather, flight_id=id, airport_id=takeoff_airport_id, date_time=data['takeoff_datetime'],
|
|
||||||
type_="takeoff"
|
|
||||||
)
|
|
||||||
|
|
||||||
if input.landing_airport:
|
if input.landing_airport:
|
||||||
landing_airport_id = await handle_combobox_save(
|
data['landing_airport_id'] = await handle_combobox_save(
|
||||||
db, models.Airport, input.landing_airport, user_id, name_column="icao_code",
|
db, models.Airport, input.landing_airport, user_id, name_column="icao_code",
|
||||||
extra_data={"name": input.landing_airport.name}
|
extra_data={"name": input.landing_airport.name}
|
||||||
)
|
)
|
||||||
|
|
||||||
data['landing_airport_id'] = landing_airport_id
|
|
||||||
data['landing_datetime'] = input.landing_datetime or flight_data['landing_datetime']
|
data['landing_datetime'] = input.landing_datetime or flight_data['landing_datetime']
|
||||||
|
|
||||||
context.background_tasks.add_task(
|
|
||||||
download_weather, flight_id=id, airport_id=landing_airport_id, date_time=data['landing_datetime'],
|
|
||||||
type_="landing"
|
|
||||||
)
|
|
||||||
|
|
||||||
if input.aircraft is not None:
|
if input.aircraft is not None:
|
||||||
data['aircraft_id'] = await handle_aircraft_save(db, user_id, input.aircraft)
|
data['aircraft_id'] = await handle_aircraft_save(db, user_id, input.aircraft)
|
||||||
|
|
||||||
@@ -239,21 +191,36 @@ class FlightMutationResolver(BaseMutationResolver):
|
|||||||
token=random_str(64)
|
token=random_str(64)
|
||||||
))
|
))
|
||||||
|
|
||||||
return await self._do_update(db, flight_data, data)
|
flight_model = await self._do_update(db, flight_data, data)
|
||||||
|
|
||||||
|
schedule_background_tasks(flight_id, data, context)
|
||||||
|
|
||||||
|
return flight_model
|
||||||
|
|
||||||
|
|
||||||
async def handle_upload_gpx(gpx_track: Upload, context, original_gpx_filename: Optional[str] = None):
|
def schedule_background_tasks(flight_id: int, flight_data: dict, context) -> None:
|
||||||
if original_gpx_filename:
|
context.background_tasks.add_task(add_terrain_elevation_to_flight, flight_id=flight_id)
|
||||||
delete_file(FLIGHT_GPX_TRACK_PATH + "/" + original_gpx_filename, silent=True)
|
|
||||||
|
|
||||||
filename = await handle_file_upload(gpx_track, FLIGHT_GPX_TRACK_PATH)
|
if flight_data.get("title_photo_id"):
|
||||||
context.background_tasks.add_task(add_terrain_elevation_to_flight, flight_id=id, gpx_filename=filename)
|
context.background_tasks.add_task(
|
||||||
|
generate_flight_title_photo, flight_id=flight_id, photo_id=flight_data['title_photo_id']
|
||||||
|
)
|
||||||
|
|
||||||
return filename
|
if flight_data.get("takeoff_airport_id"):
|
||||||
|
context.background_tasks.add_task(
|
||||||
|
download_weather_for_flight, flight_id=flight_id, airport_id=flight_data['takeoff_airport_id'],
|
||||||
|
date_time=flight_data['takeoff_datetime'], type_="takeoff"
|
||||||
|
)
|
||||||
|
|
||||||
|
if flight_data.get("landing_airport_id"):
|
||||||
|
context.background_tasks.add_task(
|
||||||
|
download_weather_for_flight, flight_id=flight_id, airport_id=flight_data['landing_airport_id'],
|
||||||
|
date_time=flight_data['landing_datetime'], type_="landing"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def handle_track_edit(db: AsyncSession, flight_id: int, track: List[TrackItemInput], user_id: int):
|
async def handle_track_edit(db: AsyncSession, flight_id: int, track: list[TrackItemInput], user_id: int):
|
||||||
await db.execute(delete(models.FlightTrack).filter(models.FlightTrack.flight_id == flight_id))
|
await db.execute(delete(models.FlightTurnPoint).filter(models.FlightTurnPoint.flight_id == flight_id))
|
||||||
|
|
||||||
order = 0
|
order = 0
|
||||||
for item in track:
|
for item in track:
|
||||||
@@ -262,17 +229,26 @@ async def handle_track_edit(db: AsyncSession, flight_id: int, track: List[TrackI
|
|||||||
|
|
||||||
if item.point_of_interest:
|
if item.point_of_interest:
|
||||||
poi_id = await handle_combobox_save(
|
poi_id = await handle_combobox_save(
|
||||||
db, models.PointOfInterest, item.point_of_interest, user_id, extra_data={"description": ""}
|
db, models.PointOfInterest, item.point_of_interest, user_id, extra_data={
|
||||||
|
"description": "",
|
||||||
|
'gps_latitude': item.gps_latitude,
|
||||||
|
'gps_longitude': item.gps_longitude
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if item.airport:
|
if item.airport:
|
||||||
airport_id = await handle_combobox_save(
|
airport_id = await handle_combobox_save(
|
||||||
db, models.Airport, item.airport, user_id,
|
db, models.Airport, item.airport, user_id,
|
||||||
name_column="icao_code",
|
name_column="icao_code",
|
||||||
extra_data={"name": item.airport.name}
|
extra_data={
|
||||||
|
"name": item.airport.name,
|
||||||
|
'gps_latitude': item.gps_latitude,
|
||||||
|
'gps_longitude': item.gps_longitude,
|
||||||
|
'openaip_id': f'custom-{random.randint(0, 999999)}'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
await models.FlightTrack.create(
|
await models.FlightTurnPoint.create(
|
||||||
db,
|
db,
|
||||||
data={
|
data={
|
||||||
"flight_id": flight_id,
|
"flight_id": flight_id,
|
||||||
|
|||||||
@@ -1,69 +1,78 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from sqlalchemy import delete, select
|
from sqlalchemy import delete, select
|
||||||
from sqlalchemy.dialects.mysql import insert
|
from sqlalchemy.dialects.mysql import insert
|
||||||
|
|
||||||
from database import models
|
from database import models
|
||||||
from database.models import flight_plan_has_copilot
|
from database.models import flight_plan_has_copilot
|
||||||
from database.transaction import get_session
|
from database.transaction import get_session
|
||||||
|
from graphql_schema.context import GraphQLContext
|
||||||
from graphql_schema.entities.helpers.combobox import handle_combobox_save
|
from graphql_schema.entities.helpers.combobox import handle_combobox_save
|
||||||
from graphql_schema.entities.resolvers.base import BaseMutationResolver
|
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
|
||||||
from graphql_schema.entities.resolvers.flight import handle_aircraft_save
|
from graphql_schema.entities.resolvers.flight import handle_aircraft_save
|
||||||
from graphql_schema.entities.types.mutation_input import CreateFlightPlanInput, EditFlightPlanInput
|
from graphql_schema.entities.types.mutation_input import CreateFlightPlanInput, EditFlightPlanInput
|
||||||
from graphql_schema.entities.types.types import FlightPlan
|
from graphql_schema.entities.types.types import FlightPlan
|
||||||
|
from utils.flight_plan_helpers import refresh_markers_weather_info
|
||||||
|
from utils.list_utils import evenly_spaced_elements
|
||||||
from utils.str_utils import random_str
|
from utils.str_utils import random_str
|
||||||
|
|
||||||
|
|
||||||
|
class FlightPlanQueryResolver(BaseQueryResolver):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(graphql_type=FlightPlan, model=models.FlightPlan)
|
||||||
|
|
||||||
|
def get_query(
|
||||||
|
self,
|
||||||
|
user_id: int | None = None,
|
||||||
|
only_public: bool | None = False,
|
||||||
|
object_id: int | None = None,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
filters = {"object_id": object_id} if object_id else {}
|
||||||
|
|
||||||
|
query = super().get_query(
|
||||||
|
user_id,
|
||||||
|
**filters,
|
||||||
|
order_by=[models.FlightPlan.planned_takeoff_datetime.desc(), models.FlightPlan.id.desc()],
|
||||||
|
only_public=only_public,
|
||||||
|
only_my=not only_public
|
||||||
|
)
|
||||||
|
if kwargs.get('username'):
|
||||||
|
query = (
|
||||||
|
query.join(models.FlightPlan.created_by)
|
||||||
|
.filter(models.User.public_username == kwargs['username'])
|
||||||
|
)
|
||||||
|
|
||||||
|
if kwargs.get("search"):
|
||||||
|
search = kwargs["search"]
|
||||||
|
query = (
|
||||||
|
query.filter(models.FlightPlan.name.contains(search))
|
||||||
|
)
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
class FlightPlanMutationResolver(BaseMutationResolver):
|
class FlightPlanMutationResolver(BaseMutationResolver):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(graphql_type=FlightPlan, model=models.FlightPlan)
|
super().__init__(graphql_type=FlightPlan, model=models.FlightPlan)
|
||||||
|
|
||||||
@staticmethod
|
async def create(self, context: GraphQLContext, data: CreateFlightPlanInput) -> FlightPlan:
|
||||||
async def save_markers(db, flight_plan: models.FlightPlan, markers: list):
|
|
||||||
position = 0
|
|
||||||
|
|
||||||
for marker in markers:
|
|
||||||
await models.FlightPlanMarker.create(db, data={
|
|
||||||
"position": position,
|
|
||||||
"flight_plan_id": flight_plan.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:
|
|
||||||
input_data = data.to_dict()
|
input_data = data.to_dict()
|
||||||
input_data['created_by_id'] = context.user_id
|
input_data['created_by_id'] = context.user_id
|
||||||
|
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
flight_plan = await self._do_create(db, data=input_data)
|
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
|
return flight_plan
|
||||||
|
|
||||||
async def save_copilots(self, db, flight_plan_id: int, copilots: list, user_id: int):
|
async def update(self, context: GraphQLContext, id: int, data: EditFlightPlanInput) -> FlightPlan:
|
||||||
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:
|
|
||||||
input_data = data.to_dict()
|
input_data = data.to_dict()
|
||||||
|
user_id = context.user_id
|
||||||
|
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
flight_plan_model = await self._get_one(db, id=id, created_by_id=user_id)
|
flight_plan_model = await self._get_one(db, id=id, created_by_id=user_id)
|
||||||
@@ -71,19 +80,29 @@ class FlightPlanMutationResolver(BaseMutationResolver):
|
|||||||
if data.aircraft is not None:
|
if data.aircraft is not None:
|
||||||
input_data['aircraft_id'] = await handle_aircraft_save(db, user_id, data.aircraft)
|
input_data['aircraft_id'] = await handle_aircraft_save(db, user_id, data.aircraft)
|
||||||
|
|
||||||
|
markers = []
|
||||||
if data.markers is not None:
|
if data.markers is not None:
|
||||||
await self.reset_plan_markers(db, flight_plan_model)
|
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 flight_plan_model.is_default_name:
|
||||||
if data.markers:
|
if not markers:
|
||||||
markers = data.markers
|
markers = (await db.scalars(
|
||||||
else:
|
select(models.FlightPlanMarker)
|
||||||
markers = (await db.scalars(select(models.FlightPlanMarker).filter(models.FlightPlanMarker.flight_plan_id == id))).all()
|
.filter(models.FlightPlanMarker.flight_plan_id == id)
|
||||||
|
)).all()
|
||||||
|
|
||||||
used_markers = evenly_spaced_elements(markers, 5)
|
used_markers = evenly_spaced_elements(markers, 5)
|
||||||
input_data['name'] = " - ".join(m.name for m in used_markers)
|
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:
|
if data.copilots is not None:
|
||||||
await self.save_copilots(
|
await self.save_copilots(
|
||||||
db,
|
db,
|
||||||
@@ -96,8 +115,49 @@ class FlightPlanMutationResolver(BaseMutationResolver):
|
|||||||
return flight_plan
|
return flight_plan
|
||||||
|
|
||||||
|
|
||||||
def evenly_spaced_elements(lst: list, count: int) -> list:
|
@staticmethod
|
||||||
if count > len(lst):
|
async def save_markers(db, flight_plan: models.FlightPlan, markers: list):
|
||||||
return lst
|
position = 0
|
||||||
interval = (len(lst) - 1) / (count - 1) if count > 1 else 0
|
|
||||||
return [lst[int(round(i * interval))] for i in range(count)]
|
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(
|
def get_query(
|
||||||
self,
|
self,
|
||||||
object_id: Optional[int] = None,
|
object_id: int | None = None,
|
||||||
order_by: Optional[list] = None,
|
order_by: list | None = None,
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
query = (
|
query = (
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Optional
|
import sqlalchemy
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
from graphql import GraphQLError
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from pymysql.err import IntegrityError
|
||||||
from sqlalchemy import delete, insert, select
|
from sqlalchemy import delete, insert, select
|
||||||
from background_jobs.elevation import add_terrain_elevation_to_photo
|
from background_jobs.elevation import add_terrain_elevation_to_photo
|
||||||
from background_jobs.photo import generate_thumbnail, resize_photo
|
from background_jobs.photo import generate_thumbnail, resize_photo
|
||||||
from database import models
|
from database import models
|
||||||
from database.transaction import get_session
|
from database.transaction import get_session
|
||||||
|
from graphql_schema.context import GraphQLContext
|
||||||
from graphql_schema.entities.helpers.combobox import handle_combobox_save
|
from graphql_schema.entities.helpers.combobox import handle_combobox_save
|
||||||
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
|
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
|
||||||
from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput
|
from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput
|
||||||
@@ -16,7 +19,7 @@ from graphql_schema.entities.types.types import Photo
|
|||||||
from paths import get_photo_basepath
|
from paths import get_photo_basepath
|
||||||
from utils.file import delete_file
|
from utils.file import delete_file
|
||||||
from utils.image import PhotoEditor, parse_exif_info
|
from utils.image import PhotoEditor, parse_exif_info
|
||||||
from utils.upload import handle_file_upload
|
from utils.file import handle_file_upload
|
||||||
|
|
||||||
|
|
||||||
class PhotoQueryResolver(BaseQueryResolver):
|
class PhotoQueryResolver(BaseQueryResolver):
|
||||||
@@ -26,10 +29,10 @@ class PhotoQueryResolver(BaseQueryResolver):
|
|||||||
|
|
||||||
def get_query(
|
def get_query(
|
||||||
self,
|
self,
|
||||||
user_id: Optional[int] = None,
|
user_id: int | None = None,
|
||||||
object_id: Optional[int] = None,
|
object_id: int | None = None,
|
||||||
order_by: Optional[list] = None,
|
order_by: list | None = None,
|
||||||
only_public: Optional[bool] = False,
|
only_public: bool | None = False,
|
||||||
*args, **kwargs
|
*args, **kwargs
|
||||||
):
|
):
|
||||||
query = super().get_query(
|
query = super().get_query(
|
||||||
@@ -61,7 +64,7 @@ class PhotoQueryResolver(BaseQueryResolver):
|
|||||||
|
|
||||||
class PhotoDetailInfo(BaseModel):
|
class PhotoDetailInfo(BaseModel):
|
||||||
filename: str
|
filename: str
|
||||||
original_filename: str
|
original_filename: str | None
|
||||||
path: str
|
path: str
|
||||||
flight_id: int
|
flight_id: int
|
||||||
|
|
||||||
@@ -71,14 +74,14 @@ class PhotoMutationResolver(BaseMutationResolver):
|
|||||||
super().__init__(Photo, models.Photo)
|
super().__init__(Photo, models.Photo)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _copy_original(path: str, filename: str):
|
def _copy_original(path: str, filename: str) -> str:
|
||||||
original_filename = "_original_" + filename
|
original_filename = "_original_" + filename
|
||||||
if not os.path.isfile(path + "/" + original_filename):
|
if not os.path.isfile(path + "/" + original_filename):
|
||||||
shutil.copyfile(path + "/" + filename, path + "/" + original_filename)
|
shutil.copyfile(path + "/" + filename, path + "/" + original_filename)
|
||||||
|
|
||||||
return original_filename
|
return original_filename
|
||||||
|
|
||||||
async def _get_photo_details(self, id: int, user_id: int, copilot_token: Optional[str]):
|
async def _get_photo_details(self, id: int, user_id: int, copilot_token: str | None, copy_original: bool = True):
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
if not copilot_token:
|
if not copilot_token:
|
||||||
photo = await self._get_one(db, id, created_by_id=user_id)
|
photo = await self._get_one(db, id, created_by_id=user_id)
|
||||||
@@ -89,15 +92,20 @@ class PhotoMutationResolver(BaseMutationResolver):
|
|||||||
filename = photo.filename + "." + photo.filename_extension
|
filename = photo.filename + "." + photo.filename_extension
|
||||||
|
|
||||||
path = get_photo_basepath(flight_id)
|
path = get_photo_basepath(flight_id)
|
||||||
|
|
||||||
|
original_filename = None
|
||||||
|
if copy_original:
|
||||||
|
original_filename = self._copy_original(path, filename)
|
||||||
|
|
||||||
return PhotoDetailInfo(
|
return PhotoDetailInfo(
|
||||||
flight_id=flight_id,
|
flight_id=flight_id,
|
||||||
path=path,
|
path=path,
|
||||||
filename=filename,
|
filename=filename,
|
||||||
original_filename=self._copy_original(path, filename) # TODO: tohle nedelat v getteru!
|
original_filename=original_filename
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _get_photo_info_by_copilot_token(db, photo_id: int, copilot_token: Optional[str]):
|
async def _get_photo_info_by_copilot_token(db, photo_id: int, copilot_token: str | None):
|
||||||
if not copilot_token:
|
if not copilot_token:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -165,17 +173,17 @@ class PhotoMutationResolver(BaseMutationResolver):
|
|||||||
|
|
||||||
return photo
|
return photo
|
||||||
|
|
||||||
async def update(self, id: int, input: EditPhotoInput, user_id: int) -> Photo:
|
async def update(self, context: GraphQLContext, id: int, input: EditPhotoInput) -> Photo:
|
||||||
data = input.to_dict()
|
data = input.to_dict()
|
||||||
|
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
photo = await self._get_one(db, id, created_by_id=user_id)
|
photo = await self._get_one(db, id, created_by_id=context.user_id)
|
||||||
if input.point_of_interest:
|
if input.point_of_interest:
|
||||||
data['point_of_interest_id'] = await handle_combobox_save(
|
data['point_of_interest_id'] = await handle_combobox_save(
|
||||||
db,
|
db,
|
||||||
models.PointOfInterest,
|
models.PointOfInterest,
|
||||||
input.point_of_interest,
|
input.point_of_interest,
|
||||||
user_id,
|
context.user_id,
|
||||||
extra_data={"description": ""}
|
extra_data={"description": ""}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -187,7 +195,7 @@ class PhotoMutationResolver(BaseMutationResolver):
|
|||||||
return await self._do_update(db, obj=photo, data=data)
|
return await self._do_update(db, obj=photo, data=data)
|
||||||
|
|
||||||
async def change_orientation(
|
async def change_orientation(
|
||||||
self, id: int, user_id: int, direction: str, copilot_upload_token: Optional[str], info
|
self, id: int, user_id: int, direction: str, copilot_upload_token: str | None, info
|
||||||
):
|
):
|
||||||
photo = await self._get_photo_details(id, user_id, copilot_upload_token)
|
photo = await self._get_photo_details(id, user_id, copilot_upload_token)
|
||||||
|
|
||||||
@@ -261,12 +269,15 @@ class PhotoMutationResolver(BaseMutationResolver):
|
|||||||
"cache_key": int(time())
|
"cache_key": int(time())
|
||||||
})
|
})
|
||||||
|
|
||||||
async def delete(self, user_id: int, id: int, **kwargs) -> Photo:
|
async def delete(self, context: GraphQLContext, id: int, **kwargs) -> Photo:
|
||||||
copilot_upload_token = kwargs.get("copilot_upload_token")
|
copilot_upload_token = kwargs.get("copilot_upload_token")
|
||||||
await self._get_photo_details(id, user_id, copilot_upload_token)
|
await self._get_photo_details(id, context.user_id, copilot_upload_token, copy_original=False)
|
||||||
# ^^ pokud neexistuje, vyhodi vyjimku
|
|
||||||
|
try:
|
||||||
|
photo = await super().delete(context, id)
|
||||||
|
except (IntegrityError, sqlalchemy.exc.IntegrityError):
|
||||||
|
raise GraphQLError("Nelze smazat titulni fotku")
|
||||||
|
|
||||||
photo = await super().delete(user_id, id)
|
|
||||||
base_path = get_photo_basepath(photo.flight_id)
|
base_path = get_photo_basepath(photo.flight_id)
|
||||||
|
|
||||||
files_to_delete = [
|
files_to_delete = [
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_i
|
|||||||
|
|
||||||
@strawberry.input()
|
@strawberry.input()
|
||||||
class ComboboxInput:
|
class ComboboxInput:
|
||||||
id: Optional[int] = None
|
id: int | None = None
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
|
|
||||||
@strawberry_sqlalchemy_input(model=models.FlightPlanMarker, exclude_fields=["id", "flight_plan_id"])
|
@strawberry_sqlalchemy_input(model=models.FlightPlanMarker, exclude_fields=["id", "flight_plan_id"])
|
||||||
class FlightPlanMarkerInput(BaseGraphqlInputType):
|
class FlightPlanMarkerInput(BaseGraphqlInputType):
|
||||||
position: Optional[int] = 0
|
position: int | None = 0
|
||||||
|
|
||||||
|
|
||||||
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
|
@strawberry_sqlalchemy_input(model=models.Copilot, exclude_fields=["id"])
|
||||||
@@ -81,19 +81,19 @@ class EditEventInput(BaseGraphqlInputType):
|
|||||||
class UploadPhotoInput:
|
class UploadPhotoInput:
|
||||||
photo: Upload
|
photo: Upload
|
||||||
flight_id: int
|
flight_id: int
|
||||||
name: Optional[str] = None
|
name: str | None = None
|
||||||
description: Optional[str] = None
|
description: str | None = None
|
||||||
point_of_interest: Optional[ComboboxInput] = None
|
point_of_interest: Optional[ComboboxInput] = None
|
||||||
copilot_upload_token: Optional[str] = None
|
copilot_upload_token: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@strawberry.input
|
@strawberry.input
|
||||||
class EditPhotoInput:
|
class EditPhotoInput:
|
||||||
name: Optional[str] = None
|
name: str | None = None
|
||||||
description: Optional[str] = None
|
description: str | None = None
|
||||||
point_of_interest: Optional[ComboboxInput] = None
|
point_of_interest: Optional[ComboboxInput] = None
|
||||||
copilots: Optional[List[ComboboxInput]] = None
|
copilots: Optional[List[ComboboxInput]] = None
|
||||||
aircraft_id: Optional[int] = None
|
aircraft_id: int | None = None
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
@@ -112,12 +112,12 @@ class CropInput(BaseGraphqlInputType):
|
|||||||
|
|
||||||
@strawberry.input
|
@strawberry.input
|
||||||
class AdjustmentInput:
|
class AdjustmentInput:
|
||||||
rotate: Optional[float] = 0
|
rotate: float | None = 0
|
||||||
crop_after_rotate: Optional[bool] = True,
|
crop_after_rotate: bool | None = True,
|
||||||
brightness: Optional[float] = 1
|
brightness: float | None = 1
|
||||||
contrast: Optional[float] = 1
|
contrast: float | None = 1
|
||||||
saturation: Optional[float] = 1
|
saturation: float | None = 1
|
||||||
sharpness: Optional[float] = 1
|
sharpness: float | None = 1
|
||||||
crop: Optional[CropInput] = None
|
crop: Optional[CropInput] = None
|
||||||
|
|
||||||
|
|
||||||
@@ -138,18 +138,23 @@ class EditFlightInput(BaseGraphqlInputType):
|
|||||||
@strawberry.input()
|
@strawberry.input()
|
||||||
class CreateFlightInput(BaseGraphqlInputType):
|
class CreateFlightInput(BaseGraphqlInputType):
|
||||||
aircraft: ComboboxInput
|
aircraft: ComboboxInput
|
||||||
|
flight_plan_id: int | None = None
|
||||||
|
name: str | None = None
|
||||||
gpx_track_file: Optional[Upload] = None # TODO: poresit validaci uploadovaneho souboru!
|
gpx_track_file: Optional[Upload] = None # TODO: poresit validaci uploadovaneho souboru!
|
||||||
landing_airport: Optional[ComboboxInput] = None
|
landing_airport: Optional[ComboboxInput] = None
|
||||||
takeoff_airport: Optional[ComboboxInput] = None
|
takeoff_airport: Optional[ComboboxInput] = None
|
||||||
takeoff_datetime: Optional[datetime] = None
|
takeoff_datetime: Optional[datetime] = None
|
||||||
landing_datetime: Optional[datetime] = None
|
landing_datetime: Optional[datetime] = None
|
||||||
|
track: Optional[List[TrackItemInput]] = None
|
||||||
|
|
||||||
|
|
||||||
@strawberry.input()
|
@strawberry.input()
|
||||||
class TrackItemInput:
|
class TrackItemInput:
|
||||||
point_of_interest: Optional[ComboboxInput] = None
|
point_of_interest: Optional[ComboboxInput] = None
|
||||||
airport: Optional[ComboboxInput] = None
|
airport: Optional[ComboboxInput] = None
|
||||||
landing_duration: Optional[int] = None
|
landing_duration: int | None = None
|
||||||
|
gps_latitude: float | None = None
|
||||||
|
gps_longitude: float | None = None
|
||||||
|
|
||||||
|
|
||||||
@strawberry_sqlalchemy_input(models.Aircraft, exclude_fields=['id', 'photo_filename'])
|
@strawberry_sqlalchemy_input(models.Aircraft, exclude_fields=['id', 'photo_filename'])
|
||||||
|
|||||||
@@ -1,30 +1,28 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import math
|
import math
|
||||||
from datetime import datetime
|
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
import strawberry
|
import strawberry
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.endpoints import authenticated_user_only
|
# from decorators.endpoints import authenticated_user_only
|
||||||
from external.gps_utils import get_bearing, get_distance
|
from utils.gps import get_bearing, get_distance
|
||||||
from external.gpx_parser import GPXParser
|
from external.gpx_parser import GPXParser
|
||||||
from graphql_schema.dataloaders.flight_duration import flight_duration_dataloader
|
from graphql_schema.dataloaders.flight_duration import flight_duration_dataloader
|
||||||
from graphql_schema.dataloaders.multi_models import (
|
from graphql_schema.dataloaders.multi_models import (
|
||||||
poi_photos_dataloader, flight_by_poi_dataloader, flight_copilots_dataloader, flight_track_dataloader,
|
poi_photos_dataloader, flight_by_poi_dataloader, flight_copilots_dataloader, flight_turn_points_dataloader,
|
||||||
photos_dataloader, flights_by_aircraft_dataloader, users_in_organization_dataloader,
|
photos_dataloader, flights_by_aircraft_dataloader, users_in_organization_dataloader,
|
||||||
aircrafts_from_organization_dataloader, user_organizations_dataloader, flights_by_event_dataloader,
|
aircrafts_from_organization_dataloader, user_organizations_dataloader, flights_by_event_dataloader,
|
||||||
flights_by_copilot_dataloader, public_flights_by_event_dataloader, public_flights_by_copilot_dataloader,
|
flights_by_copilot_dataloader, public_flights_by_event_dataloader, public_flights_by_copilot_dataloader,
|
||||||
photo_copilots_dataloader, photos_aircraft_dataloader, copilots_in_photo_dataloader, flight_plan_markers_dataloader,
|
photo_copilots_dataloader, photos_aircraft_dataloader, copilots_in_photo_dataloader, flight_plan_markers_dataloader,
|
||||||
reporting_points_dataloader, flight_plan_copilots_dataloader
|
reporting_points_dataloader, flight_plan_copilots_dataloader, runways_dataloader, frequencies_dataloader, track_points_dataloder
|
||||||
)
|
)
|
||||||
from graphql_schema.dataloaders.single_model import (
|
from graphql_schema.dataloaders.single_model import (
|
||||||
poi_dataloader, poi_type_dataloader, event_dataloader, aircraft_dataloader, airport_dataloader,
|
poi_dataloader, poi_type_dataloader, event_dataloader, aircraft_dataloader, airport_dataloader,
|
||||||
airport_weather_info_loader, organizations_dataloader, flight_dataloader, photo_adjustment_dataloader,
|
weather_info_loader, organizations_dataloader, flight_dataloader, photo_adjustment_dataloader,
|
||||||
photo_dataloader, user_dataloader
|
photo_dataloader, user_dataloader, track_dataloader
|
||||||
)
|
)
|
||||||
from graphql_schema.permissions import IsAuthenticated
|
from graphql_schema.permissions import IsAuthenticated
|
||||||
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type
|
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type
|
||||||
from paths import get_avatar_url, get_title_image_url, get_photo_thumbnail_url, get_photo_url, FLIGHT_GPX_TRACK_PATH
|
from paths import get_avatar_url, get_title_image_url, get_photo_thumbnail_url, get_photo_url, FLIGHT_GPX_TRACK_PATH, airport_icon_url, get_public_url
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
@@ -33,18 +31,6 @@ class Point:
|
|||||||
lng: float
|
lng: float
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
|
||||||
class GPXTrack:
|
|
||||||
coordinates: List[Point]
|
|
||||||
speed: List[float]
|
|
||||||
altitude: List[float]
|
|
||||||
magnetic_variation: List[float]
|
|
||||||
terrain_elevation: List[float]
|
|
||||||
time: List[datetime]
|
|
||||||
max_speed: float
|
|
||||||
avg_speed: float
|
|
||||||
max_altitude: float
|
|
||||||
avg_altitude: float
|
|
||||||
|
|
||||||
|
|
||||||
@strawberry_sqlalchemy_type(models.ReportingPoint)
|
@strawberry_sqlalchemy_type(models.ReportingPoint)
|
||||||
@@ -52,14 +38,24 @@ class ReportingPoint:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_sqlalchemy_type(models.Runway)
|
||||||
|
class Runway:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_sqlalchemy_type(models.Frequency)
|
||||||
|
class Frequency:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@strawberry_sqlalchemy_type(models.Airport)
|
@strawberry_sqlalchemy_type(models.Airport)
|
||||||
class Airport:
|
class Airport:
|
||||||
reporting_points: List[ReportingPoint] = strawberry.field(
|
reporting_points: List[ReportingPoint] = strawberry.field(
|
||||||
resolver=lambda root: reporting_points_dataloader.load(root.id)
|
resolver=lambda root: reporting_points_dataloader.load(root.id)
|
||||||
)
|
)
|
||||||
|
runways: List[Runway] = strawberry.field(resolver=lambda root: runways_dataloader.load(root.id))
|
||||||
# TODO: loader na ikonu
|
frequencies: List[Frequency] = strawberry.field(resolver=lambda root: frequencies_dataloader.load(root.id))
|
||||||
# TODO: loader na RWY a frekvence
|
icon_url: str = strawberry.field(resolver=lambda root: airport_icon_url(root.icao_code))
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
@@ -78,8 +74,8 @@ class Airspace:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@strawberry_sqlalchemy_type(models.FlightTrack)
|
@strawberry_sqlalchemy_type(models.FlightTurnPoint)
|
||||||
class FlightTrack:
|
class FlightTurnPoint:
|
||||||
point_of_interest: Optional[PointOfInterest] = strawberry.field(
|
point_of_interest: Optional[PointOfInterest] = strawberry.field(
|
||||||
resolver=lambda root: poi_dataloader.load(root.point_of_interest_id)
|
resolver=lambda root: poi_dataloader.load(root.point_of_interest_id)
|
||||||
)
|
)
|
||||||
@@ -123,6 +119,20 @@ class Photo:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_sqlalchemy_type(models.TrackPoint)
|
||||||
|
class TrackPoint:
|
||||||
|
coordinates: Point = strawberry.field(resolver=lambda root: Point(lat=root.gps_latitude, lng=root.gps_longitude))
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_sqlalchemy_type(models.Track, exclude_fields=['bounds'])
|
||||||
|
class Track:
|
||||||
|
bounds: list[tuple[float, float]]
|
||||||
|
map_bounds: list[Point] = strawberry.field(
|
||||||
|
resolver=lambda root: [Point(lat=point[1], lng=point[0]) for point in root.bounds]
|
||||||
|
)
|
||||||
|
points: list[TrackPoint] = strawberry.field(resolver=lambda root: track_points_dataloder.load(root.id))
|
||||||
|
|
||||||
|
|
||||||
@strawberry_sqlalchemy_type(models.Flight)
|
@strawberry_sqlalchemy_type(models.Flight)
|
||||||
class Flight:
|
class Flight:
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
@@ -131,33 +141,11 @@ class Flight:
|
|||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
async def load_gpx_track(root):
|
# @authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
|
||||||
if not root.gpx_track_filename:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
gpx_parser = GPXParser(f"{FLIGHT_GPX_TRACK_PATH}/{root.gpx_track_filename}")
|
|
||||||
except OSError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return GPXTrack(
|
|
||||||
coordinates=[Point(**point) for point in await gpx_parser.get_coordinates()],
|
|
||||||
speed=await gpx_parser.get_speed(),
|
|
||||||
altitude=await gpx_parser.get_altitude(),
|
|
||||||
terrain_elevation=await gpx_parser.get_terrain_elevation(),
|
|
||||||
time=await gpx_parser.get_times(),
|
|
||||||
max_speed=await gpx_parser.get_max_speed(),
|
|
||||||
avg_speed=await gpx_parser.get_avg_speed(),
|
|
||||||
max_altitude=await gpx_parser.get_max_altitude(),
|
|
||||||
avg_altitude=await gpx_parser.get_avg_altitude(),
|
|
||||||
magnetic_variation=await gpx_parser.get_magnetic_variation(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
|
|
||||||
async def load_copilots(root):
|
async def load_copilots(root):
|
||||||
return await flight_copilots_dataloader.load(root.id)
|
return await flight_copilots_dataloader.load(root.id)
|
||||||
|
|
||||||
@authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
|
# @authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
|
||||||
async def load_event(root):
|
async def load_event(root):
|
||||||
return await event_dataloader.load(root.event_id)
|
return await event_dataloader.load(root.event_id)
|
||||||
|
|
||||||
@@ -172,20 +160,20 @@ class Flight:
|
|||||||
resolver=lambda root: airport_dataloader.load(root.landing_airport_id)
|
resolver=lambda root: airport_dataloader.load(root.landing_airport_id)
|
||||||
)
|
)
|
||||||
title_photo: Optional[Photo] = strawberry.field(resolver=lambda root: photo_dataloader.load(root.title_photo_id))
|
title_photo: Optional[Photo] = strawberry.field(resolver=lambda root: photo_dataloader.load(root.title_photo_id))
|
||||||
track: List[FlightTrack] = strawberry.field(resolver=lambda root: flight_track_dataloader.load(root.id))
|
turn_points: List[FlightTurnPoint] = strawberry.field(resolver=lambda root: flight_turn_points_dataloader.load(root.id))
|
||||||
takeoff_weather_info: Optional[WeatherInfo] = strawberry.field(
|
takeoff_weather_info: Optional[WeatherInfo] = strawberry.field(
|
||||||
resolver=lambda root: airport_weather_info_loader.load(root.takeoff_weather_info_id)
|
resolver=lambda root: weather_info_loader.load(root.takeoff_weather_info_id)
|
||||||
)
|
)
|
||||||
landing_weather_info: Optional[WeatherInfo] = strawberry.field(
|
landing_weather_info: Optional[WeatherInfo] = strawberry.field(
|
||||||
resolver=lambda root: airport_weather_info_loader.load(root.landing_weather_info_id)
|
resolver=lambda root: weather_info_loader.load(root.landing_weather_info_id)
|
||||||
)
|
)
|
||||||
photos: List[Photo] = strawberry.field(resolver=lambda root: photos_dataloader.load(root.id))
|
photos: List[Photo] = strawberry.field(resolver=lambda root: photos_dataloader.load(root.id))
|
||||||
gpx_track: Optional[GPXTrack] = strawberry.field(resolver=load_gpx_track)
|
track: Optional[Track] = strawberry.field(resolver=lambda root: track_dataloader.load(root.track_id))
|
||||||
duration_min_calculated: int = strawberry.field(
|
duration_min_calculated: int = strawberry.field(resolver=lambda root: flight_duration_dataloader.load(root.id))
|
||||||
resolver=lambda root: flight_duration_dataloader.load(root.id)
|
social_image_url: str | None = strawberry.field(
|
||||||
|
resolver=lambda root: get_public_url(f'photos/{root.id}/title_photo.jpg')
|
||||||
)
|
)
|
||||||
|
debriefing_notes: str | None = strawberry.field(
|
||||||
debriefing_notes: Optional[str] = strawberry.field(
|
|
||||||
permission_classes=[IsAuthenticated],
|
permission_classes=[IsAuthenticated],
|
||||||
resolver=lambda root: root.debriefing_notes
|
resolver=lambda root: root.debriefing_notes
|
||||||
)
|
)
|
||||||
@@ -193,13 +181,15 @@ class Flight:
|
|||||||
|
|
||||||
@strawberry_sqlalchemy_type(model=models.FlightPlanMarker)
|
@strawberry_sqlalchemy_type(model=models.FlightPlanMarker)
|
||||||
class FlightPlanMarker:
|
class FlightPlanMarker:
|
||||||
pass
|
weather_info: Optional[WeatherInfo] = strawberry.field(
|
||||||
|
resolver=lambda root: weather_info_loader.load(root.weather_info_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class FlightPlanTrack:
|
class FlightPlanTrack:
|
||||||
bearing: Optional[int]
|
bearing: int | None
|
||||||
distance: Optional[float]
|
distance: float | None
|
||||||
from_: FlightPlanMarker = strawberry.field(name="from")
|
from_: FlightPlanMarker = strawberry.field(name="from")
|
||||||
to: Optional[FlightPlanMarker]
|
to: Optional[FlightPlanMarker]
|
||||||
|
|
||||||
@@ -228,7 +218,7 @@ class FlightPlan:
|
|||||||
)
|
)
|
||||||
return navigation
|
return navigation
|
||||||
|
|
||||||
@authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
|
# @authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
|
||||||
async def load_copilots(root):
|
async def load_copilots(root):
|
||||||
return await flight_plan_copilots_dataloader.load(root.id)
|
return await flight_plan_copilots_dataloader.load(root.id)
|
||||||
|
|
||||||
@@ -257,7 +247,7 @@ class Copilot:
|
|||||||
flights: List[Flight] = strawberry.field(resolver=resolve_flights)
|
flights: List[Flight] = strawberry.field(resolver=resolve_flights)
|
||||||
photos: List[Photo] = strawberry.field(resolver=lambda root: photo_copilots_dataloader.load(root.id))
|
photos: List[Photo] = strawberry.field(resolver=lambda root: photo_copilots_dataloader.load(root.id))
|
||||||
title_photo: Optional[Photo] = strawberry.field(resolver=lambda root: photo_dataloader.load(root.title_photo_id))
|
title_photo: Optional[Photo] = strawberry.field(resolver=lambda root: photo_dataloader.load(root.title_photo_id))
|
||||||
token: Optional[str] = strawberry.field(
|
token: str | None = strawberry.field(
|
||||||
permission_classes=[IsAuthenticated],
|
permission_classes=[IsAuthenticated],
|
||||||
resolver=resolve_copilot_token
|
resolver=resolve_copilot_token
|
||||||
)
|
)
|
||||||
@@ -283,8 +273,11 @@ class Organization:
|
|||||||
|
|
||||||
@strawberry_sqlalchemy_type(models.User, exclude_fields=['password_hashed'])
|
@strawberry_sqlalchemy_type(models.User, exclude_fields=['password_hashed'])
|
||||||
class User:
|
class User:
|
||||||
avatar_image_url: Optional[str] = strawberry.field(resolver=lambda root: get_avatar_url(root))
|
avatar_image_url: str | None = strawberry.field(resolver=lambda root: get_avatar_url(root))
|
||||||
title_image_url: str = strawberry.field(resolver=lambda root: get_title_image_url(root))
|
title_image_url: str = strawberry.field(resolver=lambda root: get_title_image_url(root))
|
||||||
|
social_image_url: str | None = strawberry.field(
|
||||||
|
resolver=lambda root: get_public_url(f'profile/{root.id}/title_photo.jpg')
|
||||||
|
)
|
||||||
organizations: List[Organization] = strawberry.field(
|
organizations: List[Organization] = strawberry.field(
|
||||||
resolver=lambda root: user_organizations_dataloader.load(root.id)
|
resolver=lambda root: user_organizations_dataloader.load(root.id)
|
||||||
)
|
)
|
||||||
@@ -292,7 +285,7 @@ class User:
|
|||||||
|
|
||||||
@strawberry_sqlalchemy_type(models.Event)
|
@strawberry_sqlalchemy_type(models.Event)
|
||||||
class Event:
|
class Event:
|
||||||
async def load_flights(root, info, only_public: Optional[bool] = False):
|
async def load_flights(root, info, only_public: bool | None = False):
|
||||||
is_user_logged_in = bool(info.context.user_id)
|
is_user_logged_in = bool(info.context.user_id)
|
||||||
if not is_user_logged_in or only_public:
|
if not is_user_logged_in or only_public:
|
||||||
dataloader = public_flights_by_event_dataloader
|
dataloader = public_flights_by_event_dataloader
|
||||||
|
|||||||
@@ -3,22 +3,21 @@ import strawberry
|
|||||||
from graphql import GraphQLError
|
from graphql import GraphQLError
|
||||||
from passlib.hash import bcrypt
|
from passlib.hash import bcrypt
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
from strawberry import Info
|
||||||
from strawberry.file_uploads import Upload
|
from strawberry.file_uploads import Upload
|
||||||
from background_jobs.photo import resize_photo
|
from background_jobs.photo import resize_photo
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.endpoints import authenticated_user_only
|
|
||||||
from decorators.error_logging import error_logging
|
|
||||||
from database.transaction import get_session
|
from database.transaction import get_session
|
||||||
from graphql_schema.entities.types.types import User
|
from graphql_schema.entities.types.types import User
|
||||||
|
from graphql_schema.extensions.field.auth import AuthenticatedOnly
|
||||||
from utils.file import delete_file
|
from utils.file import delete_file
|
||||||
from utils.upload import handle_file_upload
|
from utils.file import handle_file_upload
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class UserQueries:
|
class UserQueries:
|
||||||
@strawberry.field()
|
@strawberry.field()
|
||||||
@error_logging
|
async def user(root, info: Info, username: str) -> User:
|
||||||
async def user(root, info, username: str) -> User:
|
|
||||||
if len(username) == 0:
|
if len(username) == 0:
|
||||||
raise GraphQLError("Username not set!")
|
raise GraphQLError("Username not set!")
|
||||||
|
|
||||||
@@ -28,10 +27,8 @@ class UserQueries:
|
|||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field(extensions=[AuthenticatedOnly()])
|
||||||
@authenticated_user_only()
|
async def logged_user(root, info: Info) -> User:
|
||||||
@error_logging
|
|
||||||
async def logged_user(root, info) -> User:
|
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
user_model = (await db.scalars(
|
user_model = (await db.scalars(
|
||||||
select(models.User).filter_by(id=info.context.user_id)
|
select(models.User).filter_by(id=info.context.user_id)
|
||||||
@@ -44,17 +41,16 @@ class UserQueries:
|
|||||||
class EditUserMutation:
|
class EditUserMutation:
|
||||||
@strawberry.input
|
@strawberry.input
|
||||||
class EditUserInput:
|
class EditUserInput:
|
||||||
name: Optional[str] = None
|
name: str | None = None
|
||||||
description: Optional[str] = None
|
description: str | None = None
|
||||||
public_username: Optional[str] = None
|
public_username: str | None = None
|
||||||
old_password: Optional[str] = None
|
old_password: str | None = None
|
||||||
new_password: Optional[str] = None
|
new_password: str | None = None
|
||||||
avatar_image: Optional[Upload] = None
|
avatar_image: Optional[Upload] = None
|
||||||
title_image: Optional[Upload] = None
|
title_image: Optional[Upload] = None
|
||||||
|
|
||||||
@strawberry.mutation
|
@strawberry.mutation(extensions=[AuthenticatedOnly()])
|
||||||
@authenticated_user_only()
|
async def edit_logged_user(root, info: Info, input: EditUserInput) -> User:
|
||||||
async def edit_logged_user(root, info, input: EditUserInput) -> User:
|
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
user = (await db.scalars(
|
user = (await db.scalars(
|
||||||
select(models.User).filter_by(id=info.context.user_id)
|
select(models.User).filter_by(id=info.context.user_id)
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from typing import Any
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||||
|
from strawberry import Info
|
||||||
|
from strawberry.extensions import FieldExtension
|
||||||
|
from strawberry.extensions.field_extension import AsyncExtensionResolver
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticatedOnly(FieldExtension):
|
||||||
|
async def resolve_async(self, next_: AsyncExtensionResolver, source: Any, info: Info, **kwargs: Any) -> Any:
|
||||||
|
if not info.context.user_id:
|
||||||
|
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
|
||||||
|
|
||||||
|
return await next_(source, info, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class AllowPublicAccess(FieldExtension):
|
||||||
|
async def resolve_async(self, next_: AsyncExtensionResolver, source: Any, info: Info, **kwargs: Any) -> Any:
|
||||||
|
user_id = info.context.user_id
|
||||||
|
public = kwargs.get('public')
|
||||||
|
|
||||||
|
if not user_id and not public:
|
||||||
|
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
|
||||||
|
|
||||||
|
return await next_(source, info, **kwargs)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
from typing import Callable, Any, Type
|
||||||
|
import strawberry
|
||||||
|
from strawberry.annotation import StrawberryAnnotation
|
||||||
|
from strawberry.extensions import FieldExtension
|
||||||
|
from strawberry.types.arguments import StrawberryArgument
|
||||||
|
from strawberry.types.field import StrawberryField
|
||||||
|
from graphql_schema.entities.helpers.pagination import get_pagination_window, PaginationWindow
|
||||||
|
|
||||||
|
class OffsetPagination[Item](FieldExtension):
|
||||||
|
|
||||||
|
def __init__(self, item_type: Type[Item]):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.item_type = item_type
|
||||||
|
|
||||||
|
def apply(self, field: StrawberryField) -> StrawberryField:
|
||||||
|
offset_arg = StrawberryArgument(
|
||||||
|
python_name="offset",
|
||||||
|
graphql_name="offset",
|
||||||
|
type_annotation=StrawberryAnnotation(annotation=int | None),
|
||||||
|
default=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
limit_arg = StrawberryArgument(
|
||||||
|
python_name="limit",
|
||||||
|
graphql_name="limit",
|
||||||
|
type_annotation=StrawberryAnnotation(annotation=int),
|
||||||
|
default=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
field.arguments.append(offset_arg)
|
||||||
|
field.arguments.append(limit_arg)
|
||||||
|
|
||||||
|
return field
|
||||||
|
|
||||||
|
async def resolve_async(
|
||||||
|
self, next_: Callable[..., Any], source: Any, info: strawberry.Info,
|
||||||
|
limit: int, offset: int = 0,
|
||||||
|
**kwargs
|
||||||
|
) -> PaginationWindow[Item]:
|
||||||
|
query = await next_(source, info, **kwargs)
|
||||||
|
|
||||||
|
return await get_pagination_window(
|
||||||
|
query=query,
|
||||||
|
item_type=self.item_type,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from typing import Callable, Any
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from graphql import GraphQLResolveInfo, GraphQLError
|
||||||
|
from sqlalchemy.exc import NoResultFound
|
||||||
|
from strawberry.extensions import SchemaExtension
|
||||||
|
from strawberry.utils.await_maybe import AwaitableOrValue
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorLogging(SchemaExtension):
|
||||||
|
async def resolve_async(self, _next: Callable, root: Any, info: GraphQLResolveInfo, *args: str, **kwargs: Any) -> AwaitableOrValue[object]:
|
||||||
|
try:
|
||||||
|
return await _next(root, info, *args, **kwargs)
|
||||||
|
except NoResultFound as e:
|
||||||
|
raise GraphQLError("Not found", original_error=e)
|
||||||
|
except HTTPException as e:
|
||||||
|
if e.status_code == 401:
|
||||||
|
raise GraphQLError("Not authorized", original_error=e)
|
||||||
|
except Exception as e:
|
||||||
|
raise GraphQLError(f"Unknown error: {e}", original_error=e)
|
||||||
@@ -1,45 +1,30 @@
|
|||||||
import dataclasses
|
from datetime import datetime
|
||||||
from typing import Set
|
|
||||||
|
|
||||||
import strawberry
|
import strawberry
|
||||||
from fastapi_jwt import JwtAuthorizationCredentials
|
from strawberry.extensions import SchemaExtension, ValidationCache, ParserCache
|
||||||
from fastapi_jwt.jwt import JwtAccessBearerCookie
|
from graphql_schema.extensions.schema.error_logging import ErrorLogging
|
||||||
from starlette.background import BackgroundTasks
|
from logger import log
|
||||||
from strawberry.extensions import SchemaExtension
|
|
||||||
from strawberry.fastapi import BaseContext
|
|
||||||
from .mutation import Mutation
|
from .mutation import Mutation
|
||||||
from .query import Query
|
from .query import Query
|
||||||
|
|
||||||
|
|
||||||
# Toto se da kdyztak pouzit jako extension do Schema
|
|
||||||
# class SQLAlchemySession(Extension):
|
|
||||||
# def on_request_start(self):
|
|
||||||
# session = async_session()
|
|
||||||
# print(self.execution_context.context)
|
|
||||||
# self.execution_context.context["db"] = session
|
|
||||||
#
|
|
||||||
# async def on_request_end(self):
|
|
||||||
# await self.execution_context.context["db"].close()
|
|
||||||
|
|
||||||
class LoggingExtension(SchemaExtension):
|
class LoggingExtension(SchemaExtension):
|
||||||
def on_request_start(self):
|
def on_request_end(self):
|
||||||
print("request start")
|
# nastavuje se pri vytvareni GraphQL contextu
|
||||||
|
request_start = self.execution_context.context.request_start_time
|
||||||
|
request_end = datetime.now()
|
||||||
|
|
||||||
async def on_request_end(self):
|
request_time_ms = round((request_end - request_start).microseconds / 1000)
|
||||||
print("request end")
|
op_name = self.execution_context.operation_name
|
||||||
|
log.info(f"Operation {op_name} took {request_time_ms}ms")
|
||||||
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class GraphQLContext(BaseContext):
|
|
||||||
user_id: int
|
|
||||||
organization_ids: Set[int]
|
|
||||||
jwt_auth_credentials: JwtAuthorizationCredentials
|
|
||||||
jwt: JwtAccessBearerCookie
|
|
||||||
background_tasks: BackgroundTasks
|
|
||||||
|
|
||||||
|
|
||||||
schema = strawberry.Schema(
|
schema = strawberry.Schema(
|
||||||
query=Query,
|
query=Query,
|
||||||
mutation=Mutation,
|
mutation=Mutation,
|
||||||
extensions=[LoggingExtension]
|
extensions=[
|
||||||
|
LoggingExtension(),
|
||||||
|
ErrorLogging(),
|
||||||
|
ParserCache(),
|
||||||
|
ValidationCache()
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ def get_annotations_for_scalars(model: BaseModel, exclude_fields=None, force_opt
|
|||||||
return annotations_
|
return annotations_
|
||||||
|
|
||||||
|
|
||||||
def strawberry_sqlalchemy_type(model, exclude_fields: Optional[typing.Union[List, typing.Tuple]] = None):
|
def strawberry_sqlalchemy_type(model: BaseModel, exclude_fields: list | tuple | None = None):
|
||||||
if exclude_fields is None:
|
if exclude_fields is None:
|
||||||
exclude_fields = []
|
exclude_fields = []
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ def strawberry_sqlalchemy_input(
|
|||||||
model,
|
model,
|
||||||
exclude_fields: Optional[typing.Union[List, typing.Tuple]] = None,
|
exclude_fields: Optional[typing.Union[List, typing.Tuple]] = None,
|
||||||
all_optional: bool = False
|
all_optional: bool = False
|
||||||
) -> typing.Callable[[...], strawberry.object_type]:
|
) -> typing.Callable[[...], strawberry.type]:
|
||||||
if exclude_fields is None:
|
if exclude_fields is None:
|
||||||
exclude_fields = []
|
exclude_fields = []
|
||||||
|
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
from fastapi_jwt import JwtAccessBearerCookie, JwtRefreshBearerCookie
|
||||||
|
from config import APP_SECRET_KEY, APP_DEBUG, REFRESH_TOKEN_VALIDITY_DAYS
|
||||||
|
|
||||||
|
access_security = JwtAccessBearerCookie(
|
||||||
|
secret_key=APP_SECRET_KEY,
|
||||||
|
auto_error=False,
|
||||||
|
access_expires_delta=timedelta(days=1) if APP_DEBUG else timedelta(minutes=20)
|
||||||
|
)
|
||||||
|
refresh_security = JwtRefreshBearerCookie(
|
||||||
|
secret_key=APP_SECRET_KEY,
|
||||||
|
auto_error=True,
|
||||||
|
refresh_expires_delta=timedelta(days=REFRESH_TOKEN_VALIDITY_DAYS),
|
||||||
|
)
|
||||||
+14
-10
@@ -1,22 +1,26 @@
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
from colorlog import ColoredFormatter
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(logging.DEBUG)
|
log.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
# Create handlers for logging to the standard output and a file
|
|
||||||
stdoutHandler = logging.StreamHandler(stream=sys.stdout)
|
stdoutHandler = logging.StreamHandler(stream=sys.stdout)
|
||||||
|
|
||||||
# Set the log levels on the handlers
|
|
||||||
stdoutHandler.setLevel(logging.DEBUG)
|
stdoutHandler.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
# Create a log format using Log Record attributes
|
fmt = ColoredFormatter(
|
||||||
fmt = logging.Formatter(
|
"%(log_color)s[%(levelname)s]%(reset)s %(blue)s%(message)s%(reset)s %(light_black)s[%(filename)s:%(lineno)d]]%(reset)s",
|
||||||
"%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d >>> %(message)s"
|
datefmt=None,
|
||||||
|
reset=True,
|
||||||
|
log_colors={
|
||||||
|
'DEBUG': 'cyan',
|
||||||
|
'INFO': 'green',
|
||||||
|
'WARNING': 'yellow',
|
||||||
|
'ERROR': 'red',
|
||||||
|
'CRITICAL': 'red,bg_white',
|
||||||
|
},
|
||||||
|
secondary_log_colors={},
|
||||||
|
style='%'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set the log format on each handler
|
|
||||||
stdoutHandler.setFormatter(fmt)
|
stdoutHandler.setFormatter(fmt)
|
||||||
|
|
||||||
# Add each handler to the Logger object
|
|
||||||
log.addHandler(stdoutHandler)
|
log.addHandler(stdoutHandler)
|
||||||
|
|||||||
+28
-104
@@ -1,22 +1,18 @@
|
|||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from datetime import timedelta
|
from fastapi import FastAPI, HTTPException, APIRouter
|
||||||
from typing import Optional
|
|
||||||
from fastapi import FastAPI, APIRouter, Security, HTTPException
|
|
||||||
from fastapi_jwt import JwtAuthorizationCredentials, JwtAccessBearerCookie, JwtRefreshBearerCookie
|
|
||||||
from graphql import GraphQLError
|
from graphql import GraphQLError
|
||||||
|
from sentry_sdk.integrations.fastapi import FastApiIntegration
|
||||||
|
from sentry_sdk.integrations.starlette import StarletteIntegration
|
||||||
|
from sentry_sdk.integrations.strawberry import StrawberryIntegration
|
||||||
from sqlalchemy.exc import NoResultFound
|
from sqlalchemy.exc import NoResultFound
|
||||||
from starlette.background import BackgroundTasks
|
from starlette.background import BackgroundTasks
|
||||||
from starlette.middleware.cors import CORSMiddleware
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
from starlette.responses import Response, JSONResponse
|
from starlette.responses import Response, JSONResponse
|
||||||
from starlette.staticfiles import StaticFiles
|
from starlette.staticfiles import StaticFiles
|
||||||
from config import APP_SECRET_KEY, ALLOW_CORS_ORIGINS, SENTRY_DSN, REFRESH_TOKEN_VALIDITY_DAYS
|
from config import ALLOW_CORS_ORIGINS, SENTRY_DSN
|
||||||
from endpoints.contact import ContactEndpoint, ContactInput
|
from endpoints.contact import ContactEndpoint, ContactInput
|
||||||
from endpoints.forgotten_password import ForgottenPasswordRequest, ForgottenPasswordEndpoint, ChangeForgottenPassword
|
|
||||||
from endpoints.graphql import setup_graphql_endpoint
|
|
||||||
from endpoints.login import LoginEndpoint, LoginInput, RefreshEndpoint, LogoutEndpoint
|
|
||||||
from endpoints.photo_editor_preview import PhotoEditorEndpoint
|
|
||||||
from endpoints.registration import RegistrationInput, RegistrationEndpoint
|
|
||||||
from endpoints.sitemap import SitemapEndpoint
|
from endpoints.sitemap import SitemapEndpoint
|
||||||
|
from routers import forgotten_password, auth, photo_preview, graphql
|
||||||
|
|
||||||
|
|
||||||
class StaticFilesCache(StaticFiles):
|
class StaticFilesCache(StaticFiles):
|
||||||
@@ -31,23 +27,26 @@ class StaticFilesCache(StaticFiles):
|
|||||||
|
|
||||||
|
|
||||||
class App:
|
class App:
|
||||||
api_router = APIRouter(dependencies=[])
|
|
||||||
access_security = JwtAccessBearerCookie(
|
|
||||||
secret_key=APP_SECRET_KEY,
|
|
||||||
auto_error=False,
|
|
||||||
)
|
|
||||||
refresh_security = JwtRefreshBearerCookie(
|
|
||||||
secret_key=APP_SECRET_KEY,
|
|
||||||
auto_error=True,
|
|
||||||
refresh_expires_delta=timedelta(days=REFRESH_TOKEN_VALIDITY_DAYS),
|
|
||||||
)
|
|
||||||
|
|
||||||
def create_app(self):
|
def create_app(self):
|
||||||
if SENTRY_DSN:
|
if SENTRY_DSN:
|
||||||
|
fast_api_params = dict(
|
||||||
|
transaction_style="endpoint",
|
||||||
|
failed_request_status_codes={403, *range(500, 599)},
|
||||||
|
http_methods_to_capture=("GET", "POST"),
|
||||||
|
)
|
||||||
|
|
||||||
sentry_sdk.init(
|
sentry_sdk.init(
|
||||||
dsn=SENTRY_DSN,
|
dsn=SENTRY_DSN,
|
||||||
enable_tracing=True,
|
enable_tracing=True,
|
||||||
ignore_errors=[GraphQLError, HTTPException]
|
traces_sample_rate=0.5,
|
||||||
|
profiles_sample_rate=0.5,
|
||||||
|
ignore_errors=[GraphQLError, HTTPException],
|
||||||
|
integrations=[
|
||||||
|
StrawberryIntegration(async_execution=True),
|
||||||
|
StarletteIntegration(**fast_api_params),
|
||||||
|
FastApiIntegration(**fast_api_params),
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
@@ -99,94 +98,19 @@ class App:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def setup_routes(self, app: FastAPI):
|
def setup_routes(self, app: FastAPI):
|
||||||
@self.api_router.post("/registration", status_code=201)
|
api_router = APIRouter()
|
||||||
async def registration(user: RegistrationInput, background_tasks: BackgroundTasks):
|
|
||||||
return await RegistrationEndpoint().on_post(user, background_tasks)
|
|
||||||
|
|
||||||
@self.api_router.post("/login")
|
@api_router.post("/contact", summary="Send email from contact form")
|
||||||
async def login(resp: Response, user: LoginInput):
|
|
||||||
return await LoginEndpoint(
|
|
||||||
access_token=self.access_security,
|
|
||||||
refresh_token=self.refresh_security
|
|
||||||
).on_post(user, resp)
|
|
||||||
|
|
||||||
@self.api_router.post("/refresh", summary="Refresh access token")
|
|
||||||
async def refresh(
|
|
||||||
resp: Response,
|
|
||||||
credentials: JwtAuthorizationCredentials = Security(self.refresh_security)
|
|
||||||
):
|
|
||||||
return await RefreshEndpoint(
|
|
||||||
access_token=self.access_security,
|
|
||||||
refresh_token=self.refresh_security
|
|
||||||
).on_post(resp, credentials)
|
|
||||||
|
|
||||||
@self.api_router.post("/logout")
|
|
||||||
async def logout(resp: Response):
|
|
||||||
return await LogoutEndpoint(
|
|
||||||
access_token=self.access_security,
|
|
||||||
refresh_token=self.refresh_security
|
|
||||||
).on_post(resp)
|
|
||||||
|
|
||||||
@self.api_router.get(
|
|
||||||
"/forgotten-password/token/{token}",
|
|
||||||
summary="Info about token used for resetting password"
|
|
||||||
)
|
|
||||||
async def token_info(token: str):
|
|
||||||
return await ForgottenPasswordEndpoint().token_info(token)
|
|
||||||
|
|
||||||
@self.api_router.post(
|
|
||||||
"/forgotten-password/request",
|
|
||||||
summary="Request password change, e-mail will be sent to validate your request."
|
|
||||||
)
|
|
||||||
async def request_password_change(input: ForgottenPasswordRequest, background_tasks: BackgroundTasks):
|
|
||||||
return await ForgottenPasswordEndpoint().request(input, background_tasks)
|
|
||||||
|
|
||||||
@self.api_router.post(
|
|
||||||
"/forgotten-password/reset",
|
|
||||||
summary="Set new password after successfull token validation"
|
|
||||||
)
|
|
||||||
async def reset_password(input: ChangeForgottenPassword):
|
|
||||||
return await ForgottenPasswordEndpoint().change_password(input)
|
|
||||||
|
|
||||||
@self.api_router.post("/contact", summary="Send email from contact form")
|
|
||||||
async def contact_form_message(input: ContactInput, background_tasks: BackgroundTasks):
|
async def contact_form_message(input: ContactInput, background_tasks: BackgroundTasks):
|
||||||
return await ContactEndpoint().on_post(input, background_tasks)
|
return await ContactEndpoint().on_post(input, background_tasks)
|
||||||
|
|
||||||
@self.api_router.get("/sitemap.xml")
|
@api_router.get("/sitemap.xml")
|
||||||
async def sitemap():
|
async def sitemap():
|
||||||
return await SitemapEndpoint().on_get()
|
return await SitemapEndpoint().on_get()
|
||||||
|
|
||||||
@self.api_router.get("/photo/editor-preview/{photo_id}", summary="Photo editor preview")
|
|
||||||
async def photo_editor_preview(
|
|
||||||
photo_id: int,
|
|
||||||
brightness: Optional[float] = None,
|
|
||||||
contrast: Optional[float] = None,
|
|
||||||
saturation: Optional[float] = None,
|
|
||||||
sharpness: Optional[float] = None,
|
|
||||||
rotate: Optional[float] = None,
|
|
||||||
crop_left: Optional[float] = None,
|
|
||||||
crop_top: Optional[float] = None,
|
|
||||||
crop_width: Optional[float] = None,
|
|
||||||
crop_height: Optional[float] = None,
|
|
||||||
):
|
|
||||||
return await PhotoEditorEndpoint(
|
|
||||||
access_token=self.access_security,
|
|
||||||
refresh_token=self.refresh_security
|
|
||||||
).show_preview(
|
|
||||||
photo_id=photo_id,
|
|
||||||
logged_user_id=0,
|
|
||||||
saturation=saturation,
|
|
||||||
brightness=brightness,
|
|
||||||
contrast=contrast,
|
|
||||||
sharpness=sharpness,
|
|
||||||
crop_top=crop_top,
|
|
||||||
crop_left=crop_left,
|
|
||||||
crop_height=crop_height,
|
|
||||||
crop_width=crop_width,
|
|
||||||
rotate=rotate,
|
|
||||||
)
|
|
||||||
|
|
||||||
setup_graphql_endpoint(app, self.access_security)
|
|
||||||
|
|
||||||
# musi byt na konci
|
# musi byt na konci
|
||||||
app.include_router(self.api_router)
|
app.include_router(auth.router)
|
||||||
|
app.include_router(forgotten_password.router)
|
||||||
|
app.include_router(photo_preview.router)
|
||||||
|
app.include_router(graphql.router)
|
||||||
|
app.include_router(api_router)
|
||||||
|
|||||||
+5
-6
@@ -1,23 +1,22 @@
|
|||||||
import os
|
import os
|
||||||
from typing import Optional
|
|
||||||
from config import API_URL
|
from config import API_URL
|
||||||
from logger import log
|
from logger import log
|
||||||
|
|
||||||
PHOTO_BASE_PATH = ""
|
|
||||||
AIRCRAFT_BASE_PATH = ""
|
|
||||||
FLIGHT_BASE_PATH = ""
|
|
||||||
FLIGHT_GPX_TRACK_PATH = "/app/uploads/tracks"
|
FLIGHT_GPX_TRACK_PATH = "/app/uploads/tracks"
|
||||||
AIRCRAFT_UPLOAD_DEST_PATH = "/app/uploads/aircrafts/"
|
|
||||||
|
|
||||||
|
|
||||||
def get_photo_basepath(flight_id: int) -> str:
|
def get_photo_basepath(flight_id: int) -> str:
|
||||||
return f"/app/uploads/photos/{flight_id}"
|
return f"/app/uploads/photos/{flight_id}"
|
||||||
|
|
||||||
|
|
||||||
def get_public_url(filename: Optional[str]) -> str:
|
def get_public_url(filename: str | None) -> str:
|
||||||
return f"{API_URL}/uploads/{filename}" if filename else None
|
return f"{API_URL}/uploads/{filename}" if filename else None
|
||||||
|
|
||||||
|
|
||||||
|
def airport_icon_url(icao_code: str) -> str:
|
||||||
|
return f"{API_URL}/static/airports/{icao_code}.png"
|
||||||
|
|
||||||
|
|
||||||
def get_photo_url(root) -> str:
|
def get_photo_url(root) -> str:
|
||||||
filename = root.filename if not root.filename_extension else f"{root.filename}.{root.filename_extension}"
|
filename = root.filename if not root.filename_extension else f"{root.filename}.{root.filename_extension}"
|
||||||
return get_public_url(f"photos/{root.flight_id}/{filename}?cache={root.cache_key}")
|
return get_public_url(f"photos/{root.flight_id}/{filename}?cache={root.cache_key}")
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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 []
|
||||||
|
|
||||||
|
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))
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
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())
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user