From f8245492b958924f71c9a416d8f6b12b1bef9db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kv=C3=A1=C4=8Dek?= Date: Mon, 3 Jul 2023 13:23:59 +0200 Subject: [PATCH] Uprava struktury letu a fotky, uprava GPS pozic v DB --- alembic/env.py | 3 - ...06-205749_first_db_version_de421195d45c.py | 7 +-- ...-073554_flight_cover_photo_7304ac2f580b.py | 30 ++++++++++ ...ps_pridani_poi_id_do_photo_ef51b2cd596a.py | 56 +++++++++++++++++++ .../20230629-154344_fix_typo_7dc6dd7d0f59.py | 34 +++++++++++ ...hoto_u_letu_aircraft_call__f19757a615cc.py | 36 ++++++++++++ src/database/custom_types.py | 13 ----- src/database/models.py | 29 ++++++---- src/dependencies/db.py | 2 +- src/endpoints/flights.py | 17 ------ src/graphql_schema/dataloaders/photos.py | 34 +++++++++++ src/graphql_schema/entities/aircraft.py | 8 +-- src/graphql_schema/entities/flight.py | 18 ++++-- src/graphql_schema/entities/photo.py | 45 ++++++++++++--- src/graphql_schema/mutation.py | 4 ++ .../sqlalchemy_to_strawberry_type.py | 8 ++- src/main.py | 5 -- src/upload_utils.py | 5 ++ 18 files changed, 277 insertions(+), 77 deletions(-) create mode 100644 alembic/versions/20230629-073554_flight_cover_photo_7304ac2f580b.py create mode 100644 alembic/versions/20230629-082124_zmena_v_gps_pridani_poi_id_do_photo_ef51b2cd596a.py create mode 100644 alembic/versions/20230629-154344_fix_typo_7dc6dd7d0f59.py create mode 100644 alembic/versions/20230629-160351_uprava_cover_photo_u_letu_aircraft_call__f19757a615cc.py delete mode 100644 src/database/custom_types.py delete mode 100644 src/endpoints/flights.py create mode 100644 src/graphql_schema/dataloaders/photos.py diff --git a/alembic/env.py b/alembic/env.py index 733ccdd..8c585bd 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -1,12 +1,9 @@ import asyncio from logging.config import fileConfig - from sqlalchemy import pool from sqlalchemy.engine import Connection from sqlalchemy.ext.asyncio import async_engine_from_config - from alembic import context - import os, sys BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) diff --git a/alembic/versions/20230506-205749_first_db_version_de421195d45c.py b/alembic/versions/20230506-205749_first_db_version_de421195d45c.py index 0982ddc..ea7dcd4 100644 --- a/alembic/versions/20230506-205749_first_db_version_de421195d45c.py +++ b/alembic/versions/20230506-205749_first_db_version_de421195d45c.py @@ -7,7 +7,6 @@ Create Date: 2023-05-06 20:57:49.053470 """ from alembic import op import sqlalchemy as sa -import database.custom_types # revision identifiers, used by Alembic. revision = 'de421195d45c' @@ -22,7 +21,7 @@ def upgrade() -> None: sa.Column('id', sa.Integer(), nullable=False), sa.Column('name', sa.String(length=128), nullable=False), sa.Column('icao_code', sa.String(length=4), nullable=False), - sa.Column('gps_position', database.custom_types.Point(), nullable=True), + sa.Column('gps_position', sa.Integer(), nullable=True), sa.Column('elevation', sa.Integer(), nullable=True), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), sa.PrimaryKeyConstraint('id') @@ -94,7 +93,7 @@ def upgrade() -> None: op.create_table('point_of_interest', sa.Column('id', sa.Integer(), nullable=False), sa.Column('name', sa.String(length=128), nullable=False), - sa.Column('gps_position', database.custom_types.Point(), nullable=True), + sa.Column('gps_position', sa.Integer(), nullable=True), sa.Column('type_id', sa.Integer(), nullable=False), sa.Column('is_public', sa.Boolean(), server_default='0', nullable=False), sa.Column('created_by_id', sa.Integer(), nullable=False), @@ -155,7 +154,7 @@ def upgrade() -> None: sa.Column('name', sa.String(length=128), nullable=False), sa.Column('filename', sa.String(length=128), nullable=False), sa.Column('description', sa.Text(), nullable=False), - sa.Column('gps_position', database.custom_types.Point(), nullable=True), + sa.Column('gps_position', sa.Integer(), nullable=True), sa.Column('flight_id', sa.Integer(), nullable=False), sa.Column('created_by_id', sa.Integer(), nullable=False), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), diff --git a/alembic/versions/20230629-073554_flight_cover_photo_7304ac2f580b.py b/alembic/versions/20230629-073554_flight_cover_photo_7304ac2f580b.py new file mode 100644 index 0000000..bb00c19 --- /dev/null +++ b/alembic/versions/20230629-073554_flight_cover_photo_7304ac2f580b.py @@ -0,0 +1,30 @@ +"""flight cover photo + +Revision ID: 7304ac2f580b +Revises: 419cf23cbf83 +Create Date: 2023-06-29 07:35:54.303608 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '7304ac2f580b' +down_revision = '419cf23cbf83' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('flight', sa.Column('cover_photo_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'flight', 'photo', ['cover_photo_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', 'cover_photo_id') + # ### end Alembic commands ### diff --git a/alembic/versions/20230629-082124_zmena_v_gps_pridani_poi_id_do_photo_ef51b2cd596a.py b/alembic/versions/20230629-082124_zmena_v_gps_pridani_poi_id_do_photo_ef51b2cd596a.py new file mode 100644 index 0000000..bcd9c06 --- /dev/null +++ b/alembic/versions/20230629-082124_zmena_v_gps_pridani_poi_id_do_photo_ef51b2cd596a.py @@ -0,0 +1,56 @@ +"""zmena v gps, pridani poi_id do photo + +Revision ID: ef51b2cd596a +Revises: 7304ac2f580b +Create Date: 2023-06-29 08:21:24.755261 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = 'ef51b2cd596a' +down_revision = '7304ac2f580b' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('airport', sa.Column('gps_latitude', sa.Float(), nullable=True)) + op.add_column('airport', sa.Column('gps_longitude', sa.Float(), nullable=True)) + op.drop_column('airport', 'gps_position') + op.add_column('flight_track', sa.Column('point_of_interest_id', sa.Integer(), nullable=False)) + op.drop_constraint('flight_track_ibfk_2', 'flight_track', type_='foreignkey') + op.create_foreign_key(None, 'flight_track', 'point_of_interest', ['point_of_interest_id'], ['id']) + op.drop_column('flight_track', 'poi_id') + op.add_column('photo', sa.Column('gps_latitude', sa.Float(), nullable=True)) + op.add_column('photo', sa.Column('gps_longitude', sa.Float(), nullable=True)) + op.add_column('photo', sa.Column('poin_of_interest_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'photo', 'point_of_interest', ['poin_of_interest_id'], ['id']) + op.drop_column('photo', 'gps_position') + op.add_column('point_of_interest', sa.Column('gps_latitude', sa.Float(), nullable=True)) + op.add_column('point_of_interest', sa.Column('gps_longitude', sa.Float(), nullable=True)) + op.drop_column('point_of_interest', 'gps_position') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('point_of_interest', sa.Column('gps_position', sa.NullType(), nullable=True)) + op.drop_column('point_of_interest', 'gps_longitude') + op.drop_column('point_of_interest', 'gps_latitude') + op.add_column('photo', sa.Column('gps_position', sa.NullType(), nullable=True)) + op.drop_constraint(None, 'photo', type_='foreignkey') + op.drop_column('photo', 'poin_of_interest_id') + op.drop_column('photo', 'gps_longitude') + op.drop_column('photo', 'gps_latitude') + op.add_column('flight_track', sa.Column('poi_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False)) + op.drop_constraint(None, 'flight_track', type_='foreignkey') + op.create_foreign_key('flight_track_ibfk_2', 'flight_track', 'point_of_interest', ['poi_id'], ['id']) + op.drop_column('flight_track', 'point_of_interest_id') + op.add_column('airport', sa.Column('gps_position', sa.NullType(), nullable=True)) + op.drop_column('airport', 'gps_longitude') + op.drop_column('airport', 'gps_latitude') + # ### end Alembic commands ### diff --git a/alembic/versions/20230629-154344_fix_typo_7dc6dd7d0f59.py b/alembic/versions/20230629-154344_fix_typo_7dc6dd7d0f59.py new file mode 100644 index 0000000..c76841a --- /dev/null +++ b/alembic/versions/20230629-154344_fix_typo_7dc6dd7d0f59.py @@ -0,0 +1,34 @@ +"""fix typo + +Revision ID: 7dc6dd7d0f59 +Revises: ef51b2cd596a +Create Date: 2023-06-29 15:43:44.520087 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '7dc6dd7d0f59' +down_revision = 'ef51b2cd596a' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('photo', sa.Column('point_of_interest_id', sa.Integer(), nullable=True)) + op.drop_constraint('photo_ibfk_3', 'photo', type_='foreignkey') + op.create_foreign_key(None, 'photo', 'point_of_interest', ['point_of_interest_id'], ['id']) + op.drop_column('photo', 'poin_of_interest_id') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('photo', sa.Column('poin_of_interest_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'photo', type_='foreignkey') + op.create_foreign_key('photo_ibfk_3', 'photo', 'point_of_interest', ['poin_of_interest_id'], ['id']) + op.drop_column('photo', 'point_of_interest_id') + # ### end Alembic commands ### diff --git a/alembic/versions/20230629-160351_uprava_cover_photo_u_letu_aircraft_call__f19757a615cc.py b/alembic/versions/20230629-160351_uprava_cover_photo_u_letu_aircraft_call__f19757a615cc.py new file mode 100644 index 0000000..83eb4bf --- /dev/null +++ b/alembic/versions/20230629-160351_uprava_cover_photo_u_letu_aircraft_call__f19757a615cc.py @@ -0,0 +1,36 @@ +"""uprava cover photo u letu, aircraft.call_sign + +Revision ID: f19757a615cc +Revises: 7dc6dd7d0f59 +Create Date: 2023-06-29 16:03:51.185228 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = 'f19757a615cc' +down_revision = '7dc6dd7d0f59' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('aircraft', sa.Column('call_sign', sa.String(length=16), nullable=False)) + op.drop_column('aircraft', 'name') + op.drop_constraint('flight_ibfk_6', 'flight', type_='foreignkey') + op.drop_column('flight', 'cover_photo_id') + op.add_column('photo', sa.Column('is_flight_cover', sa.Boolean(), server_default='0', nullable=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('photo', 'is_flight_cover') + op.add_column('flight', sa.Column('cover_photo_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True)) + op.create_foreign_key('flight_ibfk_6', 'flight', 'photo', ['cover_photo_id'], ['id']) + op.add_column('aircraft', sa.Column('name', mysql.VARCHAR(length=128), nullable=False)) + op.drop_column('aircraft', 'call_sign') + # ### end Alembic commands ### diff --git a/src/database/custom_types.py b/src/database/custom_types.py deleted file mode 100644 index e7bd0c6..0000000 --- a/src/database/custom_types.py +++ /dev/null @@ -1,13 +0,0 @@ -from sqlalchemy import func -from sqlalchemy.types import UserDefinedType - - -class Point(UserDefinedType): - def get_col_spec(self): - return 'POINT' - - def bind_expression(self, bindvalue): - return func.ST_GeomFromText(bindvalue, type_=self) - - def column_expression(self, col): - return func.ST_AsText(col, type_=self) diff --git a/src/database/models.py b/src/database/models.py index 811b34c..bea41c0 100644 --- a/src/database/models.py +++ b/src/database/models.py @@ -1,9 +1,8 @@ from __future__ import annotations import datetime from typing import Set, List -from sqlalchemy import String, DateTime, ForeignKey, Text, Integer, func, Table, Column, Boolean, select +from sqlalchemy import String, DateTime, ForeignKey, Text, Integer, func, Table, Column, Boolean, select, Float from sqlalchemy.orm import Mapped, relationship, as_declarative, mapped_column -from database.custom_types import Point from sqlalchemy.ext.asyncio import AsyncSession @@ -65,7 +64,8 @@ class Airport(BaseModel): id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(128), nullable=False) icao_code: Mapped[str] = mapped_column(String(4), nullable=False) - gps_position: Mapped[Point] = mapped_column(Point, nullable=True) + 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) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0') @@ -91,7 +91,8 @@ class PointOfInterest(BaseModel): id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(128), nullable=False) - gps_position: Mapped[Point] = mapped_column(Point, 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")) is_public: Mapped[bool] = mapped_column(Boolean, server_default='0') created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id')) @@ -108,13 +109,17 @@ class Photo(BaseModel): id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(128), nullable=False) filename: Mapped[str] = mapped_column(String(128), nullable=False) + is_flight_cover: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="0") description: Mapped[str] = mapped_column(Text, nullable=False) - gps_position: Mapped[Point] = mapped_column(Point, nullable=True) + gps_latitude: Mapped[float] = mapped_column(Float, nullable=True) + gps_longitude: Mapped[float] = mapped_column(Float, 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_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) - flight: Mapped['Flight'] = relationship() + flight: Mapped['Flight'] = relationship(foreign_keys=[flight_id]) + point_of_interest: Mapped['PointOfInterest'] = relationship() created_by: Mapped['User'] = relationship() @@ -122,13 +127,13 @@ class Aircraft(BaseModel): __tablename__ = "aircraft" id: Mapped[int] = mapped_column(primary_key=True) - name: Mapped[str] = mapped_column(String(128), nullable=False) + call_sign: Mapped[str] = mapped_column(String(16), nullable=False) photo_filename: Mapped[str] = mapped_column(String(128), nullable=True) manufacturer: Mapped[str] = mapped_column(Text, nullable=False, server_default="") - model: Mapped[str] = mapped_column(String(30), nullable=False) - description: Mapped[str] = mapped_column(Text, nullable=False) + model: Mapped[str] = mapped_column(String(30), 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) - created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id')) + 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') @@ -171,7 +176,7 @@ class FlightTrack(BaseModel): id: Mapped[int] = mapped_column(primary_key=True) flight_id: Mapped[int] = mapped_column(Integer, ForeignKey("flight.id"), nullable=False) - poi_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=False) + point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.id"), nullable=False) order: Mapped[int] = mapped_column(Integer) flight: Mapped['Flight'] = relationship() @@ -205,7 +210,7 @@ class Flight(BaseModel): landing_airport: Mapped['Airport'] = relationship(foreign_keys=[landing_airport_id]) copilot: Mapped['Copilot'] = relationship(back_populates="flights") aircraft: Mapped['Aircraft'] = relationship(back_populates="flights") - photos: Mapped[List['Photo']] = relationship() + photos: Mapped[List['Photo']] = relationship(foreign_keys=[Photo.flight_id]) user: Mapped['User'] = relationship(back_populates="flights") created_by: Mapped['User'] = relationship() diff --git a/src/dependencies/db.py b/src/dependencies/db.py index 25144e1..d8383f3 100644 --- a/src/dependencies/db.py +++ b/src/dependencies/db.py @@ -5,4 +5,4 @@ async def db_session(): async with async_session() as session: async with session.begin(): yield session - await session.commit() \ No newline at end of file + await session.commit() diff --git a/src/endpoints/flights.py b/src/endpoints/flights.py deleted file mode 100644 index 46c9afa..0000000 --- a/src/endpoints/flights.py +++ /dev/null @@ -1,17 +0,0 @@ -from sqlalchemy import select, desc - -from database.models import Flight -from endpoints.base import BaseEndpoint - - -class FlightsEndpoint(BaseEndpoint): - - async def resolve(self): - self.db.add(Flight(name="test")) - await self.db.flush() - - data = await self.db.execute(select(Flight).order_by(desc(Flight.id))) - model = data.scalars().first() - return { - "status": model - } diff --git a/src/graphql_schema/dataloaders/photos.py b/src/graphql_schema/dataloaders/photos.py new file mode 100644 index 0000000..0e1b316 --- /dev/null +++ b/src/graphql_schema/dataloaders/photos.py @@ -0,0 +1,34 @@ +from collections import defaultdict +from typing import List +from sqlalchemy import select +from strawberry.dataloader import DataLoader +from database import async_session +from database.models import Photo + + +async def load_collection(ids: List[int]): + async with async_session() as session: + models = (await session.scalars(select(Photo).filter(Photo.flight_id.in_(ids)))).all() + + photos_by_flight_id = defaultdict(list) + for photo in models: + photos_by_flight_id[photo.flight_id].append(photo) + + return [photos_by_flight_id[id_] for id_ in ids] + + +async def load(ids: List[int]): + async with async_session() as session: + models = ( + await session.scalars( + select(Photo) + .filter(Photo.is_flight_cover.is_(True)) + .filter(Photo.flight_id.in_(ids))) + ).all() + photos = {p.id: p for p in models} + return [photos.get(id_) for id_ in ids] + + +photos_dataloader = DataLoader(load_fn=load_collection, cache=False) + +cover_photo_loader = DataLoader(load_fn=load) diff --git a/src/graphql_schema/entities/aircraft.py b/src/graphql_schema/entities/aircraft.py index e755243..b43458d 100644 --- a/src/graphql_schema/entities/aircraft.py +++ b/src/graphql_schema/entities/aircraft.py @@ -1,20 +1,17 @@ -import uuid from typing import List, Optional import strawberry from strawberry.file_uploads import Upload from sqlalchemy import select from database import models 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 AIRCRAFT_UPLOAD_DEST_PATH = "/app/uploads/aircrafts/" @strawberry_sqlalchemy_type(models.Aircraft) class Aircraft: - photo_url: Optional[str] = strawberry.field( - resolver=lambda root: f"http://localhost:8000/uploads/{root.photo_filename}" if root.photo_filename else None - ) + photo_url: Optional[str] = strawberry.field(resolver=lambda root: get_public_url(root.photo_filename)) def get_base_query(user_id: int): @@ -52,7 +49,6 @@ class CreateAircraftMutation: class CreateAircraftInput: photo: Optional[Upload] - @strawberry.mutation async def create_aircraft(root, info, input: CreateAircraftInput) -> Aircraft: # TODO: kontrola organizace diff --git a/src/graphql_schema/entities/flight.py b/src/graphql_schema/entities/flight.py index 31eda7b..7553b2a 100644 --- a/src/graphql_schema/entities/flight.py +++ b/src/graphql_schema/entities/flight.py @@ -2,21 +2,21 @@ from datetime import timedelta from typing import List, Optional import strawberry from sqlalchemy import select -from strawberry.file_uploads import Upload - from database import models from graphql_schema.dataloaders import copilots_dataloader from graphql_schema.dataloaders.aircraft import aircraft_dataloader from graphql_schema.dataloaders.airport import airport_dataloader +from graphql_schema.dataloaders.photos import photos_dataloader, cover_photo_loader from graphql_schema.entities.aircraft import Aircraft from graphql_schema.entities.airport import Airport from graphql_schema.entities.copilot import CopilotType +from graphql_schema.entities.photo import Photo from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input -from upload_utils import handle_file_upload # Bude se hodit: https://strawberry.rocks/docs/types/lazy + @strawberry_sqlalchemy_type(models.Flight) class Flight: async def load_takeoff_airport(root): @@ -31,6 +31,12 @@ class Flight: async def load_copilot(root): return await copilots_dataloader.load(root.copilot_id) + async def load_photos(root): + return await photos_dataloader.load(root.id) + + async def load_cover_photo(root): + return await cover_photo_loader.load(root.id) + def duration_min_calculated(root): if root.duration_total: return root.duration_total @@ -46,6 +52,9 @@ class Flight: aircraft: Aircraft = strawberry.field(resolver=load_aircraft) takeoff_airport: Airport = strawberry.field(resolver=load_takeoff_airport) landing_airport: Airport = strawberry.field(resolver=load_landing_airport) + cover_photo: Optional[Photo] = strawberry.field(resolver=load_cover_photo) + + photos: List[Photo] = strawberry.field(resolver=load_photos) def get_base_query(user_id: int): @@ -113,7 +122,6 @@ class CreateFlightMutation: @strawberry.type class EditFlightMutation: - @strawberry_sqlalchemy_input(models.Flight, exclude_fields=["id"], all_optional=True) class EditFlightInput: pass @@ -122,4 +130,4 @@ class EditFlightMutation: async def edit_flight(self, info, id: int, input: EditFlightInput) -> Flight: input_data = input.to_dict() - return await models.Flight.update(info.context.db, id=id, data=input_data) \ No newline at end of file + return await models.Flight.update(info.context.db, id=id, data=input_data) diff --git a/src/graphql_schema/entities/photo.py b/src/graphql_schema/entities/photo.py index d45523a..6ddeae2 100644 --- a/src/graphql_schema/entities/photo.py +++ b/src/graphql_schema/entities/photo.py @@ -1,13 +1,16 @@ -from typing import List, Optional +from typing import List import strawberry from sqlalchemy import select +from strawberry.file_uploads import Upload from database import models from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input +from upload_utils import get_public_url, handle_file_upload @strawberry_sqlalchemy_type(models.Photo) class Photo: - pass + url: str = strawberry.field(resolver=lambda root: get_public_url(root.filename)) + thumbnail_url: str = strawberry.field(resolver=lambda root: get_public_url(f"thumbs/{root.filename}")) def get_base_query(user_id: int): @@ -27,18 +30,37 @@ class PhotoQueries: @strawberry.type -class DeletePhotoMutation: +class UploadPhotoMutation: + @strawberry_sqlalchemy_input(models.Photo, exclude_fields=["id", "filename"]) + class UploadPhotoInput: + photo: Upload - @strawberry.input - class DeletePhotoInput: - id: int + @strawberry.mutation + async def upload_photo(self, info, input: UploadPhotoInput) -> Photo: + photos_dest = f"/app/uploads/photos/{input.flight_id}/" + filename = await handle_file_upload(input.photo, photos_dest) + # todo: udelat nahled do thumbs slozky + + created_photo = await models.Photo.create(data={ + "flight_id": input.flight_id, + "name": input.name, + "filename": filename, + "description": input.description, + "created_by_id": info.context.user_id, + }, db_session=info.context.db) + + return created_photo + + +@strawberry.type +class EditPhotoMutation: @strawberry_sqlalchemy_input(models.Photo, exclude_fields=[], all_optional=True) - class UpdatePhotoInput: + class EditPhotoInput: pass @strawberry.mutation - async def update_photo(self, info, input: UpdatePhotoInput) -> Photo: + async def update_photo(self, info, input: EditPhotoInput) -> Photo: query = get_base_query(info.context.user_id) photo = info.context.db.scalars(query.filter(models.Photo.id == input.id)) @@ -47,6 +69,13 @@ class DeletePhotoMutation: return photo + +@strawberry.type +class DeletePhotoMutation: + @strawberry.input + class DeletePhotoInput: + id: int + @strawberry.mutation async def delete_photo(self, info, input: DeletePhotoInput) -> Photo: pass diff --git a/src/graphql_schema/mutation.py b/src/graphql_schema/mutation.py index bcf3b2a..d786941 100644 --- a/src/graphql_schema/mutation.py +++ b/src/graphql_schema/mutation.py @@ -1,6 +1,7 @@ from strawberry.tools import merge_types from graphql_schema.entities.aircraft import CreateAircraftMutation, EditAircraftMutation, DeleteAircraftMutation from graphql_schema.entities.flight import CreateFlightMutation, EditFlightMutation +from graphql_schema.entities.photo import UploadPhotoMutation, DeletePhotoMutation, EditPhotoMutation Mutation = merge_types("Mutation", ( CreateAircraftMutation, @@ -8,4 +9,7 @@ Mutation = merge_types("Mutation", ( DeleteAircraftMutation, EditFlightMutation, CreateFlightMutation, + UploadPhotoMutation, + EditPhotoMutation, + DeletePhotoMutation )) diff --git a/src/graphql_schema/sqlalchemy_to_strawberry_type.py b/src/graphql_schema/sqlalchemy_to_strawberry_type.py index cc212d4..9ddfffa 100644 --- a/src/graphql_schema/sqlalchemy_to_strawberry_type.py +++ b/src/graphql_schema/sqlalchemy_to_strawberry_type.py @@ -51,7 +51,11 @@ def strawberry_sqlalchemy_input( ignored_fields = exclude_fields + ["created_at", "created_by_id", "updated_by_id", "updated_at", "deleted"] def to_dict(self): - return {name: getattr(self, name) for name, _ in get_columns_from_model(model, ignored_fields) if getattr(self, name) is not None} + return { + name: getattr(self, name) + for name, _ in get_columns_from_model(model, ignored_fields) + if getattr(self, name) is not None + } def wrapper(cls): annotations = get_annotations_for_scalars( @@ -65,10 +69,8 @@ def strawberry_sqlalchemy_input( for col, col_type in annotations.items(): try: - print("AAAAAAAAAA", col_type) if col_type._name == 'Optional': # noqa setattr(cls, col, None) - print(" nastavuji") except AttributeError: pass diff --git a/src/main.py b/src/main.py index 625d800..8f153b2 100644 --- a/src/main.py +++ b/src/main.py @@ -8,7 +8,6 @@ from strawberry.fastapi import GraphQLRouter from config import APP_SECRET_KEY, GRAPHIQL, APP_DEBUG from dependencies.db import db_session from endpoints.init_data import InitDataEndpoint -from endpoints.flights import FlightsEndpoint from endpoints.login import LoginEndpoint, LoginInput, MeEndpoint from endpoints.registration import RegistrationInput, RegistrationEndpoint from graphql_schema.schema import schema, GraphQLContext @@ -119,9 +118,5 @@ class App: async def init_data(db: AsyncSession = Depends(db_session)): return await InitDataEndpoint(db).on_get() - @self.api_router.get("/flights", status_code=200) - async def flights(db: AsyncSession = Depends(db_session)): - return await FlightsEndpoint(db).resolve() - # musi byt na konci app.include_router(self.api_router) diff --git a/src/upload_utils.py b/src/upload_utils.py index 78c7941..3b1ede9 100644 --- a/src/upload_utils.py +++ b/src/upload_utils.py @@ -1,9 +1,14 @@ import os import uuid +from typing import Optional from strawberry.file_uploads import Upload +def get_public_url(filename: Optional[str]): + return f"http://localhost:8000/uploads/{filename}" if filename else None + + def check_directories(path: str): if not os.path.isdir(path): os.makedirs(path)