Vetsi sada zmen

This commit is contained in:
Michal Kváček
2023-08-21 22:45:33 +02:00
parent 33e0a35f4b
commit 9314d31186
22 changed files with 352 additions and 201 deletions
+1 -2
View File
@@ -9,7 +9,6 @@ RUN deluser xfs
# uzivatel www-data v alpine neexistuje # 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 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
# required packages for pip (becasue of yarl lib) # required packages for pip (becasue of yarl lib)
@@ -24,4 +23,4 @@ COPY . /app
WORKDIR /app WORKDIR /app
CMD ["uvicorn", "src.asgi:app", "--reload", "--port=8000", "--host=0.0.0.0", "--log-level=debug"] CMD ["uvicorn", "src.asgi:app", "--reload", "--reload-dir=/app", "--port=8000", "--host=0.0.0.0", "--log-level=debug"]
@@ -0,0 +1,28 @@
"""add seats to aircraft
Revision ID: 71d100e65f04
Revises: dd48d8dab743
Create Date: 2023-08-16 06:46:02.618843
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '71d100e65f04'
down_revision = 'dd48d8dab743'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('aircraft', sa.Column('seats', sa.Integer(), nullable=False))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('aircraft', 'seats')
# ### end Alembic commands ###
@@ -0,0 +1,38 @@
"""make flight-copilot M:N relationship
Revision ID: 434066bcb25d
Revises: 71d100e65f04
Create Date: 2023-08-16 07:03:50.599364
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '434066bcb25d'
down_revision = '71d100e65f04'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('flight_has_copilot',
sa.Column('flight_id', sa.Integer(), nullable=False),
sa.Column('copilot_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['copilot_id'], ['copilot.id'], ),
sa.ForeignKeyConstraint(['flight_id'], ['flight.id'], ),
sa.PrimaryKeyConstraint('flight_id', 'copilot_id')
)
op.drop_constraint('flight_ibfk_2', 'flight', type_='foreignkey')
op.drop_column('flight', 'copilot_id')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('flight', sa.Column('copilot_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True))
op.create_foreign_key('flight_ibfk_2', 'flight', 'copilot', ['copilot_id'], ['id'])
op.drop_table('flight_has_copilot')
# ### end Alembic commands ###
@@ -0,0 +1,42 @@
"""rename columns with weather
Revision ID: 5c43394aa8e0
Revises: 434066bcb25d
Create Date: 2023-08-16 16:31:36.204608
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '5c43394aa8e0'
down_revision = '434066bcb25d'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('flight', sa.Column('takeoff_weather_info_id', sa.Integer(), nullable=True))
op.add_column('flight', sa.Column('landing_weather_info_id', sa.Integer(), nullable=True))
op.drop_constraint('flight_ibfk_6', 'flight', type_='foreignkey')
op.drop_constraint('flight_ibfk_7', 'flight', type_='foreignkey')
op.create_foreign_key(None, 'flight', 'weather_info', ['takeoff_weather_info_id'], ['id'])
op.create_foreign_key(None, 'flight', 'weather_info', ['landing_weather_info_id'], ['id'])
op.drop_column('flight', 'weather_info_landing_id')
op.drop_column('flight', 'weather_info_takeoff_id')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('flight', sa.Column('weather_info_takeoff_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True))
op.add_column('flight', sa.Column('weather_info_landing_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True))
op.drop_constraint(None, 'flight', type_='foreignkey')
op.drop_constraint(None, 'flight', type_='foreignkey')
op.create_foreign_key('flight_ibfk_7', 'flight', 'weather_info', ['weather_info_takeoff_id'], ['id'])
op.create_foreign_key('flight_ibfk_6', 'flight', 'weather_info', ['weather_info_landing_id'], ['id'])
op.drop_column('flight', 'landing_weather_info_id')
op.drop_column('flight', 'takeoff_weather_info_id')
# ### end Alembic commands ###
+2 -9
View File
@@ -19,6 +19,8 @@ services:
- .:/app - .:/app
environment: environment:
TZ: "Europe/Prague" TZ: "Europe/Prague"
API_URL: "http://localhost:8000"
ALLOW_CORS_ORIGINS: "http://localhost:9000 http://localhost:9001"
scrapy: scrapy:
build: build:
@@ -32,14 +34,5 @@ services:
- db-data:/var/lib/mysql:rw - db-data:/var/lib/mysql:rw
- ../crawlers:/app - ../crawlers:/app
nginx:
image: nginx:alpine
restart: always
volumes:
- ./config/nginx.dev.yml:/etc/nginx/conf.d/app-dev.conf
- ./uploads:/app/uploads
ports:
- "8999:8999"
volumes: volumes:
db-data: { } db-data: { }
+8 -1
View File
@@ -1,3 +1,10 @@
import os
APP_DEBUG = True APP_DEBUG = True
GRAPHIQL = True GRAPHIQL = True
APP_SECRET_KEY = "test" API_URL = os.environ.get("API_URL") or "http://localhost:8000"
APP_SECRET_KEY = os.environ.get("APP_SECRET_KEY") or "test"
ALLOW_CORS_ORIGINS = os.environ.get("ALLOW_CORS_ORIGINS", "").split()
if not APP_SECRET_KEY:
raise ValueError("Missing APP_SECRET_KEY!")
+18 -14
View File
@@ -8,8 +8,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
@as_declarative() @as_declarative()
class BaseModel: class BaseModel:
# __mapper_args__ = {"eager_defaults": True}
excluded_columns_in_dict = tuple() excluded_columns_in_dict = tuple()
def as_dict(self): def as_dict(self):
@@ -52,13 +50,20 @@ user_is_in_organization = Table(
Column("organization_id", Integer, ForeignKey("organization.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),
)
class Airport(BaseModel): class Airport(BaseModel):
__tablename__ = "airport" __tablename__ = "airport"
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False) name: Mapped[str] = mapped_column(String(128), nullable=False)
icao_code: Mapped[str] = mapped_column(String(4), nullable=False) icao_code: Mapped[str] = mapped_column(String(8), nullable=False)
gps_latitude: Mapped[float] = mapped_column(Float, nullable=False) gps_latitude: Mapped[float] = mapped_column(Float, nullable=False)
gps_longitude: Mapped[float] = mapped_column(Float, nullable=False) gps_longitude: Mapped[float] = mapped_column(Float, nullable=False)
elevation: Mapped[int] = mapped_column(Integer, nullable=True) elevation: Mapped[int] = mapped_column(Integer, nullable=True)
@@ -132,6 +137,7 @@ class Aircraft(BaseModel):
photo_filename: Mapped[str] = mapped_column(String(128), nullable=True) photo_filename: Mapped[str] = mapped_column(String(128), nullable=True)
manufacturer: Mapped[str] = mapped_column(Text, nullable=False, server_default="") manufacturer: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
model: Mapped[str] = mapped_column(String(30), 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="") description: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey('organization.id'), nullable=True) 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_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=False)
@@ -213,9 +219,8 @@ class Flight(BaseModel):
duration_pic: 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) gpx_track_filename: Mapped[str] = mapped_column(String(128), nullable=True)
aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey('aircraft.id')) aircraft_id: Mapped[int] = mapped_column(Integer, ForeignKey('aircraft.id'))
copilot_id: Mapped[int] = mapped_column(Integer, ForeignKey('copilot.id'), nullable=True) takeoff_weather_info_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
weather_info_takeoff_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)
weather_info_landing_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
landings: Mapped[int] = mapped_column(Integer, default=1) landings: Mapped[int] = mapped_column(Integer, default=1)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0') is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id')) created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
@@ -224,13 +229,12 @@ class Flight(BaseModel):
takeoff_airport: Mapped['Airport'] = relationship(foreign_keys=[takeoff_airport_id]) takeoff_airport: Mapped['Airport'] = relationship(foreign_keys=[takeoff_airport_id])
landing_airport: Mapped['Airport'] = relationship(foreign_keys=[landing_airport_id]) landing_airport: Mapped['Airport'] = relationship(foreign_keys=[landing_airport_id])
weather_info_landing: Mapped[WeatherInfo] = relationship(foreign_keys=[weather_info_landing_id]) weather_info_landing: Mapped[WeatherInfo] = relationship(foreign_keys=[landing_weather_info_id])
weather_info_takeoff: Mapped[WeatherInfo] = relationship(foreign_keys=[weather_info_takeoff_id]) weather_info_takeoff: Mapped[WeatherInfo] = relationship(foreign_keys=[takeoff_weather_info_id])
track: Mapped['FlightTrack'] = relationship() track: Mapped['FlightTrack'] = relationship()
copilot: Mapped['Copilot'] = relationship(back_populates="flights") copilots: Mapped[List['Copilot']] = relationship(secondary=flight_has_copilot)
aircraft: Mapped['Aircraft'] = relationship(back_populates="flights") aircraft: Mapped['Aircraft'] = relationship(back_populates="flights")
photos: Mapped[List['Photo']] = relationship(foreign_keys=[Photo.flight_id]) photos: Mapped[List['Photo']] = relationship(foreign_keys=[Photo.flight_id])
user: Mapped['User'] = relationship(back_populates="flights")
created_by: Mapped['User'] = relationship() created_by: Mapped['User'] = relationship()
@@ -243,7 +247,7 @@ class Copilot(BaseModel):
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0') deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
flights: Mapped[Set['Flight']] = relationship(back_populates="copilot") flights: Mapped[Set['Flight']] = relationship(secondary=flight_has_copilot)
created_by: Mapped['User'] = relationship() created_by: Mapped['User'] = relationship()
@@ -287,6 +291,6 @@ class User(BaseModel):
password_hashed: Mapped[str] = mapped_column(String(60), nullable=False) password_hashed: Mapped[str] = mapped_column(String(60), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
licences: Mapped[Set['License']] = relationship(back_populates="user") licences: Mapped[Set['License']] = relationship()
flights: Mapped[Set['Flight']] = relationship(back_populates="user") flights: Mapped[Set['Flight']] = relationship()
organizations: Mapped[Set['Organization']] = relationship(back_populates="users", secondary=user_is_in_organization) organizations: Mapped[Set['Organization']] = relationship(secondary=user_is_in_organization)
+14 -8
View File
@@ -7,13 +7,19 @@ def public_endpoint(func):
pass pass
def authenticated_user_only(func): def authenticated_user_only(raise_when_unauthorized: bool = True, return_value_unauthorized=None):
@wraps(func) def wrapper(func):
async def decorator(*args, **kwargs): @wraps(func)
if 'info' in kwargs: async def decorator(*args, **kwargs):
if not kwargs['info'].context.user_id: if 'info' in kwargs:
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized") if not kwargs['info'].context.user_id:
if raise_when_unauthorized:
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
else:
return return_value_unauthorized
return await func(*args, **kwargs) return await func(*args, **kwargs)
return decorator return decorator
return wrapper
+2 -1
View File
@@ -1,9 +1,10 @@
from fastapi_jwt.jwt import JwtAccess, JwtRefresh from fastapi_jwt.jwt import JwtAccess, JwtRefresh
class BaseEndpoint: class BaseEndpoint(object):
def __init__(self, db): def __init__(self, db):
self.db = db self.db = db
super().__init__()
class AuthEndpoint: class AuthEndpoint:
@@ -1,5 +0,0 @@
__all__ = [
'copilots_dataloader'
]
from graphql_schema.dataloaders.copilots import copilots_dataloader
+13 -5
View File
@@ -1,16 +1,24 @@
from collections import defaultdict
from typing import List from typing import List
from sqlalchemy import select from sqlalchemy import select
from strawberry.dataloader import DataLoader from strawberry.dataloader import DataLoader
from database import async_session from database import async_session
from database.models import Copilot from database.models import Copilot, Flight
async def load(ids: List[int]): async def load(ids: List[int]):
async with async_session() as session: async with async_session() as session:
models = (await session.scalars(select(Copilot).filter(Copilot.id.in_(ids)))).all() models = (await session.execute(
select(Copilot, Flight.id)
.join(Copilot.flights)
.filter(Flight.id.in_(ids))
)).all()
models_by_id = {model.id: model for model in models} copilots_by_flight_id = defaultdict(list)
return [models_by_id.get(id_) for id_ in ids] for copilot, flight_id in models:
copilots_by_flight_id[flight_id].append(copilot)
return [copilots_by_flight_id[id_] for id_ in ids]
copilots_dataloader = DataLoader(load_fn=load, cache=False) flight_copilots_dataloader = DataLoader(load_fn=load, cache=False)
+19 -10
View File
@@ -1,29 +1,38 @@
from collections import defaultdict from collections import defaultdict
from typing import List from typing import List, Optional
from sqlalchemy import select from sqlalchemy import select
from strawberry.dataloader import DataLoader from strawberry.dataloader import DataLoader
from database import async_session from database import async_session
from database.models import Flight from database.models import Flight, Copilot
class FlightsLoader: class FlightsLoader:
def __init__(self, relationship_column: str): def __init__(self, relationship_column, extra_join: Optional[list] = None):
if extra_join is None:
extra_join = []
self.relationship_column = relationship_column self.relationship_column = relationship_column
self.extra_join = extra_join
async def load(self, ids: List[int]): async def load(self, ids: List[int]):
async with async_session() as session: async with async_session() as session:
rel_column = self.relationship_column
query = ( query = (
select(Flight) select(Flight, rel_column)
.filter(getattr(Flight, self.relationship_column).in_(ids)) .filter(rel_column.in_(ids))
) )
data = (await session.scalars(query)).all()
for table in self.extra_join:
query = query.join(table)
data = (await session.execute(query)).all()
result_data = defaultdict(list) result_data = defaultdict(list)
for poi in data: for item, rel_id in data:
result_data[getattr(poi, self.relationship_column)].append(poi) result_data[rel_id].append(item)
return [result_data[id_] for id_ in ids] return [result_data[id_] for id_ in ids]
flights_by_copilot_dataloader = DataLoader(load_fn=FlightsLoader("copilot_id").load, cache=False) flights_by_copilot_dataloader = DataLoader(load_fn=FlightsLoader(Copilot.id, extra_join=[Flight.copilots]).load, cache=False)
flights_by_aircraft_dataloader = DataLoader(load_fn=FlightsLoader("aircraft_id").load, cache=False) flights_by_aircraft_dataloader = DataLoader(load_fn=FlightsLoader(Flight.aircraft_id).load, cache=False)
+7 -7
View File
@@ -37,8 +37,8 @@ def get_base_query(user_id: int):
@strawberry.type @strawberry.type
class AircraftQueries: class AircraftQueries:
@strawberry.field @strawberry.field()
@authenticated_user_only @authenticated_user_only()
async def aircrafts(root, info) -> List[Aircraft]: async def aircrafts(root, info) -> List[Aircraft]:
query = ( query = (
get_base_query(info.context.user_id) get_base_query(info.context.user_id)
@@ -47,8 +47,8 @@ class AircraftQueries:
return (await info.context.db.scalars(query)).all() return (await info.context.db.scalars(query)).all()
@strawberry.field @strawberry.field()
@authenticated_user_only @authenticated_user_only()
async def aircraft(root, info, id: int) -> Aircraft: async def aircraft(root, info, id: int) -> Aircraft:
query = ( query = (
get_base_query(info.context.user_id) get_base_query(info.context.user_id)
@@ -64,7 +64,7 @@ class CreateAircraftMutation:
photo: Optional[Upload] photo: Optional[Upload]
@strawberry.mutation @strawberry.mutation
@authenticated_user_only @authenticated_user_only()
async def create_aircraft(root, info, input: CreateAircraftInput) -> Aircraft: async def create_aircraft(root, info, input: CreateAircraftInput) -> Aircraft:
# TODO: kontrola organizace # TODO: kontrola organizace
@@ -88,7 +88,7 @@ class EditAircraftMutation:
photo: Optional[Upload] photo: Optional[Upload]
@strawberry.mutation @strawberry.mutation
@authenticated_user_only @authenticated_user_only()
async def edit_aircraft(root, info, id: int, input: EditAircraftInput) -> Aircraft: async def edit_aircraft(root, info, id: int, input: EditAircraftInput) -> Aircraft:
# TODO: kontrola organizace # TODO: kontrola organizace
# TODO: kontrola opravneni na akci # TODO: kontrola opravneni na akci
@@ -108,7 +108,7 @@ class EditAircraftMutation:
class DeleteAircraftMutation: class DeleteAircraftMutation:
@strawberry.mutation @strawberry.mutation
@authenticated_user_only @authenticated_user_only()
async def delete_aircraft(self, info, id: int) -> Aircraft: async def delete_aircraft(self, info, id: int) -> Aircraft:
# TODO: kontrola opravneni na akci # TODO: kontrola opravneni na akci
+7 -2
View File
@@ -2,6 +2,7 @@ from typing import List, Annotated, TYPE_CHECKING
import strawberry import strawberry
from sqlalchemy import select from sqlalchemy import select
from database import models from database import models
from decorators.endpoints import authenticated_user_only
from graphql_schema.dataloaders.flight import flights_by_copilot_dataloader from graphql_schema.dataloaders.flight import flights_by_copilot_dataloader
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input
@@ -28,13 +29,15 @@ def get_base_query(user_id: int):
@strawberry.type @strawberry.type
class CopilotQueries: class CopilotQueries:
@strawberry.field @strawberry.field()
@authenticated_user_only()
async def copilots(root, info) -> List[Copilot]: async def copilots(root, info) -> List[Copilot]:
return (await info.context.db.scalars( return (await info.context.db.scalars(
get_base_query(info.context.user_id) get_base_query(info.context.user_id)
)).all() )).all()
@strawberry.field @strawberry.field()
@authenticated_user_only()
async def copilot(root, info, id: int) -> Copilot: async def copilot(root, info, id: int) -> Copilot:
return (await info.context.db.scalars( return (await info.context.db.scalars(
get_base_query(info.context.user_id) get_base_query(info.context.user_id)
@@ -50,6 +53,7 @@ class CreateCopilotMutation:
pass pass
@strawberry.mutation @strawberry.mutation
@authenticated_user_only()
async def create_copilot(root, info, input: CreateCopilotInput) -> Copilot: async def create_copilot(root, info, input: CreateCopilotInput) -> Copilot:
input_data = input.to_dict() input_data = input.to_dict()
return await models.Copilot.create( return await models.Copilot.create(
@@ -69,6 +73,7 @@ class EditCopilotMutation:
pass pass
@strawberry.mutation @strawberry.mutation
@authenticated_user_only()
async def edit_copilot(root, info, id: int, input: EditCopilotInput) -> Copilot: async def edit_copilot(root, info, id: int, input: EditCopilotInput) -> Copilot:
copilot = (await info.context.db.scalars( copilot = (await info.context.db.scalars(
get_base_query(info.context.user_id).filter(models.Copilot.id == id) get_base_query(info.context.user_id).filter(models.Copilot.id == id)
+55 -102
View File
@@ -1,27 +1,27 @@
from datetime import timedelta from datetime import timedelta
from functools import wraps
from typing import List, Optional, Annotated, TYPE_CHECKING, Tuple from typing import List, Optional, Annotated, TYPE_CHECKING, Tuple
import strawberry import strawberry
from fastapi import HTTPException from fastapi import HTTPException
from sqlalchemy import select from sqlalchemy import select, insert, delete
from starlette.status import HTTP_401_UNAUTHORIZED from starlette.status import HTTP_401_UNAUTHORIZED
from strawberry.file_uploads import Upload from strawberry.file_uploads import Upload
from database import models from database import models
from database.models import flight_has_copilot
from decorators.endpoints import authenticated_user_only from decorators.endpoints import authenticated_user_only
from decorators.error_logging import error_logging from decorators.error_logging import error_logging
from graphql_schema.dataloaders import copilots_dataloader
from graphql_schema.dataloaders.aircraft import aircraft_dataloader from graphql_schema.dataloaders.aircraft import aircraft_dataloader
from graphql_schema.dataloaders.airport import airport_dataloader from graphql_schema.dataloaders.airport import airport_dataloader
from graphql_schema.dataloaders.copilots import flight_copilots_dataloader
from graphql_schema.dataloaders.photos import photos_dataloader, cover_photo_loader from graphql_schema.dataloaders.photos import photos_dataloader, cover_photo_loader
from graphql_schema.dataloaders.poi import flight_track_dataloader, poi_dataloader from graphql_schema.dataloaders.poi import flight_track_dataloader, poi_dataloader
from graphql_schema.dataloaders.weather import airport_weather_info_loader
from graphql_schema.entities.aircraft import Aircraft from graphql_schema.entities.aircraft import Aircraft
from graphql_schema.entities.airport import Airport from graphql_schema.entities.airport import Airport
from graphql_schema.entities.photo import Photo from graphql_schema.entities.photo import Photo
from graphql_schema.entities.poi import PointOfInterest from graphql_schema.entities.poi import PointOfInterest
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input
from upload_utils import handle_file_upload, file_exists, delete_file from upload_utils import get_public_url
from .helpers.flight import handle_aircraft_save, handle_track_edit, handle_copilot_edit, handle_weather_info from .helpers.flight import handle_aircraft_save, handle_track_edit, handle_copilots_edit, handle_weather_info, get_airports, handle_upload_gpx, handle_airport_changed
from ..dataloaders.weather import airport_weather_info_loader
from ..types import ComboboxInput from ..types import ComboboxInput
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -55,9 +55,6 @@ class Flight:
async def load_aircraft(root): async def load_aircraft(root):
return await aircraft_dataloader.load(root.aircraft_id) return await aircraft_dataloader.load(root.aircraft_id)
async def load_copilot(root):
return await copilots_dataloader.load(root.copilot_id)
async def load_photos(root): async def load_photos(root):
return await photos_dataloader.load(root.id) return await photos_dataloader.load(root.id)
@@ -65,10 +62,10 @@ class Flight:
return await cover_photo_loader.load(root.id) return await cover_photo_loader.load(root.id)
async def load_takeoff_weather_info(root): async def load_takeoff_weather_info(root):
return await airport_weather_info_loader.load(root.weather_info_takeoff_id) return await airport_weather_info_loader.load(root.takeoff_weather_info_id)
async def load_landing_weather_info(root): async def load_landing_weather_info(root):
return await airport_weather_info_loader.load(root.weather_info_landing_id) return await airport_weather_info_loader.load(root.landing_weather_info_id)
def duration_min_calculated(root): def duration_min_calculated(root):
if root.duration_total: if root.duration_total:
@@ -84,10 +81,14 @@ class Flight:
if not root.gpx_track_filename: if not root.gpx_track_filename:
return None return None
return f"http://localhost:8000/uploads/tracks/{root.gpx_track_filename}" return get_public_url(f"/tracks/{root.gpx_track_filename}")
@authenticated_user_only(raise_when_unauthorized=False, return_value_unauthorized=[])
async def load_copilots(root):
return await flight_copilots_dataloader.load(root.id)
duration_min_calculated: int = strawberry.field(resolver=duration_min_calculated) duration_min_calculated: int = strawberry.field(resolver=duration_min_calculated)
copilot: Optional[Annotated["Copilot", strawberry.lazy(".copilot")]] = strawberry.field(resolver=load_copilot) copilots: Optional[List[Annotated["Copilot", strawberry.lazy(".copilot")]]] = strawberry.field(resolver=load_copilots)
aircraft: Aircraft = strawberry.field(resolver=load_aircraft) aircraft: Aircraft = strawberry.field(resolver=load_aircraft)
takeoff_airport: Airport = strawberry.field(resolver=load_takeoff_airport) takeoff_airport: Airport = strawberry.field(resolver=load_takeoff_airport)
landing_airport: Airport = strawberry.field(resolver=load_landing_airport) landing_airport: Airport = strawberry.field(resolver=load_landing_airport)
@@ -110,7 +111,7 @@ def get_base_query(user_id: Optional[int], username: Optional[str] = None, is_au
query = query.filter(models.Flight.created_by_id == user_id) query = query.filter(models.Flight.created_by_id == user_id)
if username: if username:
query = query.filter(models.Flight.created_by.public_username == username) query = query.join(models.Flight.created_by).filter(models.User.public_username == username)
if not is_auth: if not is_auth:
query = query.filter(models.Flight.is_public.is_(True)) query = query.filter(models.Flight.is_public.is_(True))
@@ -121,10 +122,10 @@ def get_base_query(user_id: Optional[int], username: Optional[str] = None, is_au
@strawberry.type @strawberry.type
class FlightQueries: class FlightQueries:
@strawberry.field @strawberry.field()
async def flights(root, info, username: Optional[str] = None) -> List[Flight]: async def flights(root, info, username: Optional[str] = None) -> List[Flight]:
if not info.context.user_id and not username: if not info.context.user_id and not username:
raise HTTPException(HTTP_401_UNAUTHORIZED) raise HTTPException(HTTP_401_UNAUTHORIZED, f"user_id={info.context.user_id}, {username=}")
query = ( query = (
get_base_query(user_id=info.context.user_id, username=username, is_auth=bool(info.context.user_id)) get_base_query(user_id=info.context.user_id, username=username, is_auth=bool(info.context.user_id))
@@ -132,7 +133,7 @@ class FlightQueries:
) )
return (await info.context.db.scalars(query)).all() return (await info.context.db.scalars(query)).all()
@strawberry.field @strawberry.field()
@error_logging @error_logging
async def flight(root, info, id: int, username: Optional[str] = None) -> Flight: async def flight(root, info, id: int, username: Optional[str] = None) -> Flight:
if not info.context.user_id and not username: if not info.context.user_id and not username:
@@ -141,26 +142,10 @@ class FlightQueries:
query = ( query = (
get_base_query(user_id=info.context.user_id, username=username, is_auth=bool(info.context.user_id)) get_base_query(user_id=info.context.user_id, username=username, is_auth=bool(info.context.user_id))
.filter(models.Flight.id == id) .filter(models.Flight.id == id)
.filter(models.Flight.deleted.is_(False))
) )
return (await info.context.db.scalars(query)).one() return (await info.context.db.scalars(query)).one()
async def get_airports(db, takeoff_airport_id: int, landing_airport_id: int) -> Tuple[models.Airport, models.Airport]:
takeoff_airport = (await db.scalars(
select(models.Airport).filter(models.Airport.id == takeoff_airport_id)
)).one()
if takeoff_airport_id == landing_airport_id:
landing_airport = takeoff_airport
else:
landing_airport = (await db.scalars(
select(models.Airport).filter(models.Airport.id == landing_airport_id)
)).one()
return takeoff_airport, landing_airport
@strawberry.type @strawberry.type
class CreateFlightMutation: class CreateFlightMutation:
@strawberry_sqlalchemy_input(models.Flight, exclude_fields=[ @strawberry_sqlalchemy_input(models.Flight, exclude_fields=[
@@ -173,24 +158,20 @@ class CreateFlightMutation:
takeoff_airport: ComboboxInput takeoff_airport: ComboboxInput
@strawberry.mutation @strawberry.mutation
@authenticated_user_only @authenticated_user_only()
async def create_flight(self, info, input: CreateFlightInput) -> Flight: async def create_flight(self, info, input: CreateFlightInput) -> Flight:
db = info.context.db db = info.context.db
aircraft_id = await handle_aircraft_save(db, info.context.user_id, input.aircraft) aircraft_id = await handle_aircraft_save(db, info.context.user_id, input.aircraft)
data = input.to_dict() data = input.to_dict()
data['takeoff_datetime'] = data['takeoff_datetime'].astimezone()
data['landing_datetime'] = data['landing_datetime'].astimezone()
takeoff_airport, landing_airport = await get_airports(db, input.takeoff_airport.id, input.landing_airport.id) takeoff_airport, landing_airport = await get_airports(db, input.takeoff_airport.id, input.landing_airport.id)
weather_takeoff = await handle_weather_info(db, data['takeoff_datetime'], takeoff_airport)
weather_takeoff = await handle_weather_info(db, input.takeoff_datetime, takeoff_airport) weather_landing = await handle_weather_info(db, data['landing_datetime'], landing_airport)
weather_landing = await handle_weather_info(db, input.landing_datetime, landing_airport)
return await models.Flight.create(db, data={ return await models.Flight.create(db, data={
**data, **data,
"weather_info_takeoff_id": weather_takeoff.id, "takeoff_weather_info_id": weather_takeoff.id,
"weather_info_landing_id": weather_landing.id, "landing_weather_info_id": weather_landing.id,
"takeoff_airport_id": takeoff_airport.id, "takeoff_airport_id": takeoff_airport.id,
"landing_airport_id": landing_airport.id, "landing_airport_id": landing_airport.id,
"aircraft_id": aircraft_id, "aircraft_id": aircraft_id,
@@ -198,106 +179,78 @@ class CreateFlightMutation:
}) })
@strawberry.type @strawberry.type
class EditFlightMutation: class EditFlightMutation:
@strawberry_sqlalchemy_input(models.Flight, exclude_fields=[ @strawberry_sqlalchemy_input(models.Flight, exclude_fields=[
"id", "aircraft_id", "copilot_id", "deleted", "landing_airport_id", "takeoff_airport_id", "id", "aircraft_id", "deleted", "landing_airport_id", "takeoff_airport_id"
"weather_info_takeoff_id", "weather_info_landing_id", "gpx_track_filename" "takeoff_weather_info_id", "landing_weather_info_id", "gpx_track_filename"
], all_optional=True) ], all_optional=True)
class EditFlightInput: class EditFlightInput:
gpx_track: Optional[Upload] = None gpx_track: Optional[Upload] = None # TODO: poresit validaci uploadovaneho souboru!
track: Optional[List[ComboboxInput]] = None track: Optional[List[ComboboxInput]] = None
copilot: Optional[ComboboxInput] = None copilots: Optional[List[ComboboxInput]] = None
aircraft: Optional[ComboboxInput] = None aircraft: Optional[ComboboxInput] = None
landing_airport: Optional[ComboboxInput] = None landing_airport: Optional[ComboboxInput] = None
takeoff_airport: Optional[ComboboxInput] = None takeoff_airport: Optional[ComboboxInput] = None
@strawberry.mutation @strawberry.mutation
@authenticated_user_only @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:
db = info.context.db db = info.context.db
user_id = info.context.user_id user_id = info.context.user_id
flight = (await db.scalars( flight = (await db.scalars(
get_base_query(user_id=user_id, is_auth=bool(user_id)) get_base_query(user_id=user_id, is_auth=bool(user_id)).filter(models.Flight.id == id)
.filter(models.Flight.id == id)
)).one() )).one()
data = input.to_dict()
if input.takeoff_datetime:
data['takeoff_datetime'] = data['takeoff_datetime'].astimezone()
if input.landing_datetime:
data['landing_datetime'] = data['landing_datetime'].astimezone()
if input.gpx_track is not None:
# TODO: poresit validaci uploadovaneho souboru!
path = "/app/uploads/tracks"
if flight.gpx_track_filename and file_exists(path + "/" + flight.gpx_track_filename):
delete_file(path + "/" + flight.gpx_track_filename)
data['gpx_track_filename'] = await handle_file_upload(input.gpx_track, path)
update_takeoff_weather = False
update_landing_weather = False
if input.takeoff_airport is not None and input.takeoff_airport.id != flight.takeoff_airport_id:
data['takeoff_airport_id'] = input.takeoff_airport.id
update_takeoff_weather = True
if input.landing_airport is not None and input.landing_airport.id != flight.landing_airport_id:
data['landing_airport_id'] = input.landing_airport.id
update_landing_weather = True
takeoff_airport, landing_airport = await get_airports( takeoff_airport, landing_airport = await get_airports(
db, db,
takeoff_airport_id=input.takeoff_airport.id if input.takeoff_airport else flight.takeoff_airport_id, takeoff_airport_id=input.takeoff_airport.id if input.takeoff_airport else flight.takeoff_airport_id,
landing_airport_id=input.landing_airport.id if input.landing_airport else flight.landing_airport_id, landing_airport_id=input.landing_airport.id if input.landing_airport else flight.landing_airport_id,
) )
if input.takeoff_datetime is not None and input.takeoff_datetime != flight.takeoff_datetime: data = input.to_dict()
update_takeoff_weather = True
if input.landing_datetime is not None and input.landing_datetime != flight.landing_datetime: if input.gpx_track is not None:
update_landing_weather = True data['gpx_track_filename'] = await handle_upload_gpx(flight, input.gpx_track)
if update_takeoff_weather: if input.takeoff_airport and input.takeoff_airport.id != flight.takeoff_airport_id:
weather = await handle_weather_info(db, input.takeoff_datetime, takeoff_airport) await handle_airport_changed(
db,
flight,
takeoff_airport,
type_="takeoff",
input_datetime=data.get('takeoff_datetime')
)
# if flight.weather_info_takeoff_id: if input.landing_airport and input.landing_airport.id != flight.landing_airport_id:
# db.delete(flight.weather_info_takeoff) await handle_airport_changed(
db,
data['weather_info_takeoff_id'] = weather.id flight,
landing_airport,
if update_landing_weather: type_="landing",
weather = await handle_weather_info(db, input.landing_datetime, landing_airport) input_datetime=data.get('landing_datetime')
)
# if flight.weather_info_landing_id:
# db.delete(flight.weather_info_landing)
data['weather_info_landing_id'] = weather.id
# TODO: ^^ to pocasi smrdi zbytecne duplicitnim kodem, neslo by to nejak sjednotit?
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)
data['copilot_id'] = await handle_copilot_edit(db, input.copilot, user_id) if input.copilot else None
flight = await models.Flight.update(db, id=id, data=data)
if input.track is not None: if input.track is not None:
await handle_track_edit(db=db, flight=flight, track=input.track, user_id=user_id) await handle_track_edit(db=db, flight=flight, track=input.track, user_id=user_id)
return flight copilots = await handle_copilots_edit(db, input.copilots or [], user_id)
await db.execute(delete(flight_has_copilot).filter_by(flight_id=flight.id))
for copilot_id in copilots:
await db.execute(insert(flight_has_copilot).values(flight_id=flight.id, copilot_id=copilot_id))
return await models.Flight.update(db, obj=flight, data=data)
@strawberry.type @strawberry.type
class DeleteFlightMutation: class DeleteFlightMutation:
@strawberry.mutation @strawberry.mutation
@authenticated_user_only()
async def delete_flight(self, info, id: int) -> Flight: async def delete_flight(self, info, id: int) -> Flight:
flight = ( flight = (
(await info.context.db.scalars( (await info.context.db.scalars(
+52 -4
View File
@@ -1,10 +1,14 @@
import asyncio
from datetime import datetime from datetime import datetime
from typing import List, Type from typing import List, Type, Literal, Optional, Tuple
from sqlalchemy import select, delete from sqlalchemy import select, delete
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from strawberry.file_uploads import Upload
from database import models from database import models
from external.weather import Weather from external.weather import Weather
from graphql_schema.types import ComboboxInput from graphql_schema.types import ComboboxInput
from upload_utils import delete_file, file_exists, handle_file_upload
weather_api = Weather() weather_api = Weather()
@@ -74,8 +78,52 @@ async def handle_aircraft_save(db: AsyncSession, user_id: int, aircraft: Combobo
return obj.id return obj.id
async def handle_copilot_edit(db: AsyncSession, copilot: ComboboxInput, user_id: int) -> int: async def get_airports(db, takeoff_airport_id: int, landing_airport_id: int) -> Tuple[models.Airport, models.Airport]:
return await handle_combobox_save(db, models.Copilot, copilot, user_id) takeoff_airport = (await db.scalars(
select(models.Airport).filter(models.Airport.id == takeoff_airport_id)
)).one()
if takeoff_airport_id == landing_airport_id:
landing_airport = takeoff_airport
else:
landing_airport = (await db.scalars(
select(models.Airport).filter(models.Airport.id == landing_airport_id)
)).one()
return takeoff_airport, landing_airport
async def handle_airport_changed(
db, flight: models.Flight, airport: models.Airport, type_: Literal['takeoff', 'landing'],
input_datetime: Optional[datetime]
):
flight_datetime = getattr(flight, f"{type_}_datetime")
if input_datetime and input_datetime != flight_datetime:
weather = await handle_weather_info(db, input_datetime, airport)
existing_weather_id = getattr(flight, f"{type_}_weather_info_id")
if existing_weather_id:
# db.delete(delete())
pass
setattr(flight, f"{type_}_weather_info_id", weather.id)
setattr(flight, f"{type_}_airport_id", airport.id)
setattr(flight, f"{type_}_datetime", input_datetime)
async def handle_upload_gpx(flight: models.Flight, gpx_track: Upload):
path = "/app/uploads/tracks"
if flight.gpx_track_filename and file_exists(path + "/" + flight.gpx_track_filename):
delete_file(path + "/" + flight.gpx_track_filename)
return await handle_file_upload(gpx_track, path)
async def handle_copilots_edit(db: AsyncSession, copilots: List[ComboboxInput], user_id: int) -> List[int]:
cors = [handle_combobox_save(db, models.Copilot, copilot, user_id) for copilot in copilots]
return await asyncio.gather(*cors)
async def handle_combobox_save( async def handle_combobox_save(
@@ -83,7 +131,7 @@ async def handle_combobox_save(
input: ComboboxInput, input: ComboboxInput,
user_id: int, user_id: int,
name_column: str = "name" name_column: str = "name"
): ) -> int:
if input.id: if input.id:
return input.id return input.id
else: else:
+8 -4
View File
@@ -3,6 +3,7 @@ import strawberry
from sqlalchemy import select, update from sqlalchemy import select, update
from strawberry.file_uploads import Upload from strawberry.file_uploads import Upload
from database import models from database import models
from decorators.endpoints import authenticated_user_only
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input
from upload_utils import get_public_url, handle_file_upload, delete_file, parse_exif_info, generate_thumbnail, file_exists from upload_utils import get_public_url, handle_file_upload, delete_file, parse_exif_info, generate_thumbnail, file_exists
@@ -37,7 +38,7 @@ def get_photo_basepath(flight_id: int) -> str:
@strawberry.type @strawberry.type
class PhotoQueries: class PhotoQueries:
@strawberry.field @strawberry.field()
async def photos(root, info) -> List[Photo]: async def photos(root, info) -> List[Photo]:
query = get_base_query(info.context.user_id) query = get_base_query(info.context.user_id)
return (await info.context.db.scalars(query)).all() return (await info.context.db.scalars(query)).all()
@@ -52,6 +53,7 @@ class UploadPhotoMutation:
photo: Upload photo: Upload
@strawberry.mutation @strawberry.mutation
@authenticated_user_only()
async def upload_photo(self, info, input: UploadPhotoInput) -> Photo: async def upload_photo(self, info, input: UploadPhotoInput) -> Photo:
path = get_photo_basepath(input.flight_id) path = get_photo_basepath(input.flight_id)
filename = await handle_file_upload(input.photo, path) filename = await handle_file_upload(input.photo, path)
@@ -81,7 +83,8 @@ class EditPhotoMutation:
class EditPhotoInput: class EditPhotoInput:
pass pass
@strawberry.mutation @strawberry.mutation()
@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:
query = get_base_query(info.context.user_id) query = get_base_query(info.context.user_id)
photo = (await info.context.db.scalars(query.filter(models.Photo.id == id))).one() photo = (await info.context.db.scalars(query.filter(models.Photo.id == id))).one()
@@ -102,7 +105,8 @@ class EditPhotoMutation:
@strawberry.type @strawberry.type
class DeletePhotoMutation: class DeletePhotoMutation:
@strawberry.mutation @strawberry.mutation()
@authenticated_user_only()
async def delete_photo(self, info, id: int) -> Photo: async def delete_photo(self, info, id: int) -> Photo:
query = get_base_query(info.context.user_id) query = get_base_query(info.context.user_id)
photo = (await info.context.db.scalars(query.filter(models.Photo.id == id))).one() photo = (await info.context.db.scalars(query.filter(models.Photo.id == id))).one()
@@ -110,7 +114,7 @@ class DeletePhotoMutation:
base_path = get_photo_basepath(photo.flight_id) base_path = get_photo_basepath(photo.flight_id)
try: try:
delete_file(f"{base_path}/{photo.filename}") delete_file(f"{base_path}/{photo.filename}")
# TODO: odstranit nahledy delete_file(f"{base_path}/thumbs/{photo.filename}")
except Exception as e: except Exception as e:
print("ERROR", e) print("ERROR", e)
+7 -1
View File
@@ -2,6 +2,7 @@ from typing import List
import strawberry import strawberry
from sqlalchemy import select, or_ from sqlalchemy import select, or_
from database import models from database import models
from decorators.endpoints import authenticated_user_only
from graphql_schema.dataloaders.photos import poi_photos_dataloader from graphql_schema.dataloaders.photos import poi_photos_dataloader
from graphql_schema.entities.photo import Photo from graphql_schema.entities.photo import Photo
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input
@@ -36,6 +37,7 @@ def get_base_query(user_id: int, only_my: bool = False):
class PointOfInterestQueries: class PointOfInterestQueries:
@strawberry.field @strawberry.field
@authenticated_user_only()
async def points_of_interest(root, info) -> List[PointOfInterest]: async def points_of_interest(root, info) -> List[PointOfInterest]:
query = ( query = (
get_base_query(info.context.user_id) get_base_query(info.context.user_id)
@@ -45,6 +47,7 @@ class PointOfInterestQueries:
return (await info.context.db.scalars(query)).all() return (await info.context.db.scalars(query)).all()
@strawberry.field @strawberry.field
@authenticated_user_only()
async def point_of_interest(root, info, id: int) -> PointOfInterest: async def point_of_interest(root, info, id: int) -> PointOfInterest:
query = ( query = (
get_base_query(info.context.user_id) get_base_query(info.context.user_id)
@@ -60,6 +63,7 @@ class CreatePointOfInterestMutation:
pass pass
@strawberry.mutation @strawberry.mutation
@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()
return await models.PointOfInterest.create( return await models.PointOfInterest.create(
@@ -78,6 +82,7 @@ class EditPointOfInterestMutation:
pass pass
@strawberry.mutation @strawberry.mutation
@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:
# TODO: kontrola organizace # TODO: kontrola organizace
# TODO: kontrola opravneni na akci # TODO: kontrola opravneni na akci
@@ -94,7 +99,8 @@ class EditPointOfInterestMutation:
class DeletePointOfInterestMutation: class DeletePointOfInterestMutation:
@strawberry.mutation @strawberry.mutation
@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:
poi = (await get_base_query(info.context.user_id, only_my=True).filter(models.PointOfInterest.id == id)).one() 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)) return await models.PointOfInterest.update(info.context.db, obj=poi, data=dict(deleted=True))
+11 -14
View File
@@ -4,30 +4,29 @@ 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 sqlalchemy.exc import NoResultFound
from strawberry.file_uploads import Upload from strawberry.file_uploads import Upload
from config import API_URL
from database import models from database import models
from database.models import User from database.models import User
from decorators.endpoints import authenticated_user_only from decorators.endpoints import authenticated_user_only
from decorators.error_logging import error_logging from decorators.error_logging import error_logging
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input
from upload_utils import handle_file_upload, delete_file from upload_utils import handle_file_upload, delete_file, get_public_url
@strawberry_sqlalchemy_type(User, exclude_fields=['password_hashed']) @strawberry_sqlalchemy_type(User, exclude_fields=['password_hashed'])
class User: class User:
async def load_avatar_image_url(root): async def load_avatar_image_url(root):
if not root.avatar_image_filename: if not root.avatar_image_filename:
return None return None
return f"http://localhost:8000/uploads/profile/{root.id}/{root.avatar_image_filename}" return get_public_url(f"profile/{root.id}/{root.avatar_image_filename}")
async def load_title_image_url(root): async def load_title_image_url(root):
if not root.title_image_filename: if not root.title_image_filename:
return "http://localhost:8000/static/default-title-image.jpg" return f"{API_URL}/static/default-title-image.jpg"
return f"http://localhost:8000/uploads/profile/{root.id}/{root.title_image_filename}" return get_public_url(f"profile/{root.id}/{root.title_image_filename}")
avatar_image_url: Optional[str] = strawberry.field(resolver=load_avatar_image_url) avatar_image_url: Optional[str] = strawberry.field(resolver=load_avatar_image_url)
title_image_url: str = strawberry.field(resolver=load_title_image_url) title_image_url: str = strawberry.field(resolver=load_title_image_url)
@@ -35,19 +34,17 @@ class User:
@strawberry.type @strawberry.type
class UserQueries: class UserQueries:
@strawberry.field @strawberry.field()
@error_logging @error_logging
async def user(root, 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!")
return (await info.context.db.scalars( return (await info.context.db.scalars(select(models.User).filter_by(public_username=username))).one()
select(models.User).filter_by(public_username=username)
)).one()
@strawberry.field @strawberry.field()
@authenticated_user_only @authenticated_user_only()
# @error_logging @error_logging
async def logged_user(root, info) -> User: async def logged_user(root, info) -> User:
user = (await info.context.db.scalars( user = (await info.context.db.scalars(
select(models.User).filter_by(id=info.context.user_id) select(models.User).filter_by(id=info.context.user_id)
@@ -71,7 +68,7 @@ class EditUserMutation:
title_image: Optional[Upload] = None title_image: Optional[Upload] = None
@strawberry.mutation @strawberry.mutation
@authenticated_user_only @authenticated_user_only()
async def edit_logged_user(root, info, input: EditUserInput) -> User: async def edit_logged_user(root, info, input: EditUserInput) -> User:
user = (await info.context.db.scalars( user = (await info.context.db.scalars(
select(models.User).filter_by(id=info.context.user_id) select(models.User).filter_by(id=info.context.user_id)
@@ -1,4 +1,5 @@
import typing import typing
from datetime import datetime
from typing import List, Optional from typing import List, Optional
import strawberry import strawberry
import sqlalchemy import sqlalchemy
@@ -44,18 +45,24 @@ def strawberry_sqlalchemy_type(model, exclude_fields: Optional[typing.Union[List
def strawberry_sqlalchemy_input( 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]:
if exclude_fields is None: if exclude_fields is None:
exclude_fields = [] exclude_fields = []
ignored_fields = exclude_fields + ["created_at", "created_by_id", "updated_by_id", "updated_at", "deleted"] ignored_fields = exclude_fields + ["created_at", "created_by_id", "updated_by_id", "updated_at", "deleted"]
def to_dict(self): def to_dict(self):
return { dict_data = {}
name: getattr(self, name) for key, _ in get_columns_from_model(model, ignored_fields):
for name, _ in get_columns_from_model(model, ignored_fields) value = getattr(self, key)
if getattr(self, name) is not None if value is None:
} continue
if isinstance(value, datetime):
value = value.astimezone()
dict_data[key] = value
return dict_data
def wrapper(cls): def wrapper(cls):
annotations = get_annotations_for_scalars( annotations = get_annotations_for_scalars(
+4 -4
View File
@@ -7,7 +7,7 @@ from starlette.middleware.cors import CORSMiddleware
from starlette.responses import RedirectResponse, Response from starlette.responses import RedirectResponse, Response
from starlette.staticfiles import StaticFiles from starlette.staticfiles import StaticFiles
from strawberry.fastapi import GraphQLRouter from strawberry.fastapi import GraphQLRouter
from config import APP_SECRET_KEY, GRAPHIQL, APP_DEBUG from config import APP_SECRET_KEY, GRAPHIQL, APP_DEBUG, ALLOW_CORS_ORIGINS
from dependencies.db import db_session from dependencies.db import db_session
from endpoints.login import LoginEndpoint, LoginInput, RefreshEndpoint, LogoutEndpoint from endpoints.login import LoginEndpoint, LoginInput, RefreshEndpoint, LogoutEndpoint
from endpoints.registration import RegistrationInput, RegistrationEndpoint from endpoints.registration import RegistrationInput, RegistrationEndpoint
@@ -19,7 +19,7 @@ class App:
access_security = JwtAccessBearerCookie( access_security = JwtAccessBearerCookie(
secret_key=APP_SECRET_KEY, secret_key=APP_SECRET_KEY,
auto_error=False, auto_error=False,
access_expires_delta=timedelta(seconds=20), access_expires_delta=timedelta(minutes=20),
) )
refresh_security = JwtRefreshBearerCookie( refresh_security = JwtRefreshBearerCookie(
secret_key=APP_SECRET_KEY, secret_key=APP_SECRET_KEY,
@@ -45,7 +45,7 @@ class App:
def setup_middleware(app: FastAPI): def setup_middleware(app: FastAPI):
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["http://localhost:9001"], # TODO: pridat pres ENV URL produkce allow_origins=ALLOW_CORS_ORIGINS,
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
@@ -83,7 +83,7 @@ class App:
if APP_DEBUG: if APP_DEBUG:
@self.api_router.get("/graphql/autologin") @self.api_router.get("/graphql/autologin")
async def autologin(): async def autologin():
access_token = self.access_security.create_access_token(subject={"id": 18, "name": "Franta Vomacka"}) access_token = self.access_security.create_access_token(subject={"id": 1, "name": "Franta Vomacka"})
response = RedirectResponse(url="/graphql") response = RedirectResponse(url="/graphql")
self.access_security.set_access_cookie(response, access_token, expires_delta=timedelta(days=14)) self.access_security.set_access_cookie(response, access_token, expires_delta=timedelta(days=14))
+3 -2
View File
@@ -6,10 +6,11 @@ import exif
from PIL import Image, UnidentifiedImageError from PIL import Image, UnidentifiedImageError
from strawberry.file_uploads import Upload from strawberry.file_uploads import Upload
from config import API_URL
def get_public_url(filename: Optional[str]): def get_public_url(filename: Optional[str]):
# TODO: pouzit staticfiles z /uploads - port na API, nginx nebude potreba (pro dev) return f"{API_URL}/uploads/{filename}" if filename else None
return f"http://localhost:8999/{filename}" if filename else None
def check_directories(path: str): def check_directories(path: str):