Moznost nahravat fotky copilotem
This commit is contained in:
@@ -0,0 +1,53 @@
|
|||||||
|
"""add flight_has_copilot.token
|
||||||
|
|
||||||
|
Revision ID: 7dd139549851
|
||||||
|
Revises: a410455fb3bb
|
||||||
|
Create Date: 2024-03-02 15:02:26.594830
|
||||||
|
|
||||||
|
"""
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '7dd139549851'
|
||||||
|
down_revision = 'a410455fb3bb'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def rand_str():
|
||||||
|
alphabet = string.ascii_letters + string.digits
|
||||||
|
return ''.join([random.choice(alphabet) for _ in range(64)])
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('flight_has_copilot', sa.Column('token', sa.String(length=64), nullable=True))
|
||||||
|
|
||||||
|
conn = op.get_bind()
|
||||||
|
data = conn.execute(text("SELECT flight_id, copilot_id FROM flight_has_copilot"))
|
||||||
|
for flight_id, copilot_id in data:
|
||||||
|
print(flight_id, copilot_id)
|
||||||
|
conn.execute(text(
|
||||||
|
f"UPDATE flight_has_copilot SET token='{rand_str()}'"
|
||||||
|
f" WHERE flight_id={flight_id} AND copilot_id={copilot_id}"
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
op.create_unique_constraint(None, 'flight_has_copilot', ['token'])
|
||||||
|
op.add_column('photo', sa.Column('created_by_copilot_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'photo', 'copilot', ['created_by_copilot_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint('photo_ibfk_5', 'photo', type_='foreignkey')
|
||||||
|
op.drop_column('photo', 'created_by_copilot_id')
|
||||||
|
op.drop_constraint('token', 'flight_has_copilot', type_='unique')
|
||||||
|
op.drop_column('flight_has_copilot', 'token')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -55,6 +55,7 @@ flight_has_copilot = Table(
|
|||||||
BaseModel.metadata,
|
BaseModel.metadata,
|
||||||
Column("flight_id", ForeignKey("flight.id"), primary_key=True),
|
Column("flight_id", ForeignKey("flight.id"), primary_key=True),
|
||||||
Column("copilot_id", ForeignKey("copilot.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 = Table(
|
||||||
@@ -141,6 +142,7 @@ class Photo(BaseModel):
|
|||||||
point_of_interest_id: Mapped[int] = mapped_column(Integer, ForeignKey("point_of_interest.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)
|
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_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())
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
flight: Mapped['Flight'] = relationship(foreign_keys=[flight_id])
|
flight: Mapped['Flight'] = relationship(foreign_keys=[flight_id])
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ from fastapi import HTTPException
|
|||||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||||
|
|
||||||
|
|
||||||
|
def raise_unauthorized():
|
||||||
|
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
|
||||||
|
|
||||||
|
|
||||||
def allow_public(func):
|
def allow_public(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
async def decorator(*args, **kwargs):
|
async def decorator(*args, **kwargs):
|
||||||
@@ -10,7 +14,7 @@ def allow_public(func):
|
|||||||
user_id = kwargs['info'].context.user_id
|
user_id = kwargs['info'].context.user_id
|
||||||
public = kwargs.get('public')
|
public = kwargs.get('public')
|
||||||
if not user_id and not public:
|
if not user_id and not public:
|
||||||
raise HTTPException(HTTP_401_UNAUTHORIZED)
|
raise_unauthorized()
|
||||||
|
|
||||||
return await func(*args, **kwargs)
|
return await func(*args, **kwargs)
|
||||||
|
|
||||||
@@ -24,7 +28,7 @@ def authenticated_user_only(raise_when_unauthorized: bool = True, return_value_u
|
|||||||
if 'info' in kwargs:
|
if 'info' in kwargs:
|
||||||
if not kwargs['info'].context.user_id:
|
if not kwargs['info'].context.user_id:
|
||||||
if raise_when_unauthorized:
|
if raise_when_unauthorized:
|
||||||
raise HTTPException(HTTP_401_UNAUTHORIZED, "Not authorized")
|
raise_unauthorized()
|
||||||
else:
|
else:
|
||||||
return return_value_unauthorized
|
return return_value_unauthorized
|
||||||
return await func(*args, **kwargs)
|
return await func(*args, **kwargs)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Type, List, Optional
|
from typing import Type, List, Optional
|
||||||
|
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 +10,7 @@ class BaseDataloader:
|
|||||||
self,
|
self,
|
||||||
model: Type[models.BaseModel],
|
model: Type[models.BaseModel],
|
||||||
relationship_column, filters: Optional[list] = None
|
relationship_column, filters: Optional[list] = None
|
||||||
) -> None:
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.model = model
|
self.model = model
|
||||||
self.query_builder = QueryBuilder(self.model)
|
self.query_builder = QueryBuilder(self.model)
|
||||||
@@ -43,6 +44,7 @@ class MultiModelsDataloader(BaseDataloader):
|
|||||||
model: Type[models.BaseModel],
|
model: Type[models.BaseModel],
|
||||||
relationship_column=None,
|
relationship_column=None,
|
||||||
filters: Optional[list] = None,
|
filters: Optional[list] = None,
|
||||||
|
extra_select: Optional[list] = None,
|
||||||
extra_join: Optional[list] = None,
|
extra_join: Optional[list] = None,
|
||||||
order_by: Optional[list] = None,
|
order_by: Optional[list] = None,
|
||||||
):
|
):
|
||||||
@@ -52,31 +54,60 @@ class MultiModelsDataloader(BaseDataloader):
|
|||||||
extra_join = []
|
extra_join = []
|
||||||
self.extra_join = extra_join
|
self.extra_join = extra_join
|
||||||
|
|
||||||
|
if extra_select is None:
|
||||||
|
extra_select = []
|
||||||
|
self.extra_select = extra_select
|
||||||
|
|
||||||
if order_by is None:
|
if order_by is None:
|
||||||
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
|
||||||
|
|
||||||
async def load(self, ids: List[int]):
|
def get_query(self, ids: list[int]):
|
||||||
async with async_session() as db:
|
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],
|
order_by=self.order_by
|
||||||
order_by=self.order_by
|
|
||||||
)
|
|
||||||
.filter(self.relationship_column.in_(set(ids)))
|
|
||||||
.filter(*self.filters)
|
|
||||||
)
|
)
|
||||||
|
.filter(self.relationship_column.in_(set(ids)))
|
||||||
|
.filter(*self.filters)
|
||||||
|
)
|
||||||
|
|
||||||
for joined_table in self.extra_join:
|
for joined_table in self.extra_join:
|
||||||
query = query.join(joined_table)
|
query = query.join(joined_table)
|
||||||
|
|
||||||
if self.filters:
|
if self.filters:
|
||||||
query = query.filter(*self.filters)
|
query = query.filter(*self.filters)
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
async def load(self, ids: List[int]):
|
||||||
|
query = self.get_query(ids)
|
||||||
|
|
||||||
|
async with async_session() as db:
|
||||||
data = (await db.execute(query)).all()
|
data = (await db.execute(query)).all()
|
||||||
|
result_data = self.process_data(data)
|
||||||
result_data = defaultdict(list)
|
|
||||||
for item, rel_id in data:
|
|
||||||
result_data[rel_id].append(item)
|
|
||||||
|
|
||||||
return [result_data[id_] for id_ in ids]
|
return [result_data[id_] for id_ in ids]
|
||||||
|
|
||||||
|
def process_data(self, data):
|
||||||
|
result_data = defaultdict(list)
|
||||||
|
for row in data:
|
||||||
|
item, rel_id = row[0:2]
|
||||||
|
extra = row[2:]
|
||||||
|
if extra:
|
||||||
|
log.warning(f"Override function process_data, extra params={extra} are going to be discarded!")
|
||||||
|
|
||||||
|
result_data[rel_id].append(item)
|
||||||
|
return result_data
|
||||||
|
|
||||||
|
|
||||||
|
class FlightCopilotDataloader(MultiModelsDataloader):
|
||||||
|
def process_data(self, data):
|
||||||
|
result_data = defaultdict(list)
|
||||||
|
for row in data:
|
||||||
|
item, rel_id = row[0:2]
|
||||||
|
token = row[2]
|
||||||
|
|
||||||
|
item.token = token
|
||||||
|
result_data[rel_id].append(item)
|
||||||
|
|
||||||
|
return result_data
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from strawberry.dataloader import DataLoader
|
from strawberry.dataloader import DataLoader
|
||||||
from database import models
|
from database import models
|
||||||
from graphql_schema.dataloaders.base import MultiModelsDataloader
|
from graphql_schema.dataloaders.base import MultiModelsDataloader, FlightCopilotDataloader
|
||||||
|
|
||||||
aircrafts_from_organization_dataloader = DataLoader(
|
aircrafts_from_organization_dataloader = DataLoader(
|
||||||
load_fn=MultiModelsDataloader(
|
load_fn=MultiModelsDataloader(
|
||||||
@@ -12,9 +12,14 @@ aircrafts_from_organization_dataloader = DataLoader(
|
|||||||
)
|
)
|
||||||
|
|
||||||
flight_copilots_dataloader = DataLoader(
|
flight_copilots_dataloader = DataLoader(
|
||||||
load_fn=MultiModelsDataloader(
|
load_fn=FlightCopilotDataloader(
|
||||||
models.Copilot,
|
models.Copilot,
|
||||||
relationship_column=models.Flight.id,
|
relationship_column=models.Flight.id,
|
||||||
|
extra_select=[models.flight_has_copilot.c.token],
|
||||||
|
filters=[
|
||||||
|
models.flight_has_copilot.c.flight_id == models.Flight.id,
|
||||||
|
models.flight_has_copilot.c.copilot_id == models.Copilot.id,
|
||||||
|
],
|
||||||
extra_join=[models.Copilot.flights]).load,
|
extra_join=[models.Copilot.flights]).load,
|
||||||
cache=False)
|
cache=False)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
import strawberry
|
import strawberry
|
||||||
|
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 decorators.error_logging import error_logging
|
||||||
@@ -27,12 +28,24 @@ class CopilotQueries:
|
|||||||
id: Optional[int] = None,
|
id: Optional[int] = None,
|
||||||
url_slug: Optional[str] = None,
|
url_slug: Optional[str] = None,
|
||||||
pilot_username: Optional[str] = None,
|
pilot_username: Optional[str] = None,
|
||||||
|
upload_token: Optional[str] = None,
|
||||||
|
upload_flight_slug: Optional[str] = None,
|
||||||
public: Optional[bool] = False
|
public: Optional[bool] = False
|
||||||
) -> Copilot:
|
) -> Copilot:
|
||||||
filter_params = get_detail_filters(id, url_slug)
|
filter_params = {}
|
||||||
|
if id:
|
||||||
|
filter_params['object_id'] = id
|
||||||
|
if url_slug is not None:
|
||||||
|
filter_params['url_slug'] = url_slug
|
||||||
|
if upload_token and upload_flight_slug:
|
||||||
|
filter_params['upload_token'] = upload_token
|
||||||
|
filter_params['upload_flight_slug'] = upload_flight_slug
|
||||||
if pilot_username:
|
if pilot_username:
|
||||||
filter_params['pilot_username'] = pilot_username
|
filter_params['pilot_username'] = pilot_username
|
||||||
|
|
||||||
|
if not filter_params:
|
||||||
|
raise GraphQLError(f"Invalid identification supplied: {filter_params}")
|
||||||
|
|
||||||
return await CopilotQueryResolver().get_one(
|
return await CopilotQueryResolver().get_one(
|
||||||
user_id=info.context.user_id,
|
user_id=info.context.user_id,
|
||||||
only_public=public,
|
only_public=public,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
import strawberry
|
import strawberry
|
||||||
|
from graphql import GraphQLError
|
||||||
|
|
||||||
from decorators.endpoints import authenticated_user_only, allow_public
|
from decorators.endpoints import authenticated_user_only, allow_public
|
||||||
from decorators.error_logging import error_logging
|
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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
import strawberry
|
import strawberry
|
||||||
from database import models
|
from database import models
|
||||||
from decorators.endpoints import authenticated_user_only, allow_public
|
from decorators.endpoints import authenticated_user_only, allow_public, raise_unauthorized
|
||||||
from decorators.error_logging import error_logging
|
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
|
||||||
@@ -18,6 +18,7 @@ class PhotoQueries:
|
|||||||
root, info,
|
root, info,
|
||||||
flight_id: Optional[int] = None,
|
flight_id: Optional[int] = None,
|
||||||
copilot_id: Optional[int] = None,
|
copilot_id: Optional[int] = None,
|
||||||
|
uploaded_by_copilot_id: Optional[int] = None,
|
||||||
point_of_interest_id: Optional[int] = None,
|
point_of_interest_id: Optional[int] = None,
|
||||||
aircraft_id: Optional[int] = None,
|
aircraft_id: Optional[int] = None,
|
||||||
public: Optional[bool] = False,
|
public: Optional[bool] = False,
|
||||||
@@ -27,6 +28,7 @@ class PhotoQueries:
|
|||||||
flight_id=flight_id,
|
flight_id=flight_id,
|
||||||
user_id=info.context.user_id,
|
user_id=info.context.user_id,
|
||||||
copilot_id=copilot_id,
|
copilot_id=copilot_id,
|
||||||
|
uploaded_by_copilot_id=uploaded_by_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,
|
||||||
order_by=[models.Photo.exposed_at]
|
order_by=[models.Photo.exposed_at]
|
||||||
@@ -47,8 +49,10 @@ class PhotoQueries:
|
|||||||
class PhotoMutation:
|
class PhotoMutation:
|
||||||
@strawberry.mutation
|
@strawberry.mutation
|
||||||
@error_logging
|
@error_logging
|
||||||
@authenticated_user_only()
|
|
||||||
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:
|
||||||
|
raise_unauthorized()
|
||||||
|
|
||||||
return await PhotoMutationResolver().upload(info, input)
|
return await PhotoMutationResolver().upload(info, input)
|
||||||
|
|
||||||
@strawberry.mutation()
|
@strawberry.mutation()
|
||||||
@@ -59,11 +63,14 @@ class PhotoMutation:
|
|||||||
|
|
||||||
@strawberry.mutation()
|
@strawberry.mutation()
|
||||||
@error_logging
|
@error_logging
|
||||||
@authenticated_user_only()
|
async def change_orientation(self, info, id: int, direction: str, copilot_upload_token: Optional[str] = None) -> Photo:
|
||||||
async def change_orientation(self, info, id: int, direction: str) -> Photo:
|
if info.context.user_id is None and not copilot_upload_token:
|
||||||
|
raise_unauthorized()
|
||||||
|
|
||||||
return await PhotoMutationResolver().change_orientation(
|
return await PhotoMutationResolver().change_orientation(
|
||||||
id=id,
|
id=id,
|
||||||
user_id=info.context.user_id,
|
user_id=info.context.user_id,
|
||||||
|
copilot_upload_token=copilot_upload_token,
|
||||||
direction=direction,
|
direction=direction,
|
||||||
info=info
|
info=info
|
||||||
)
|
)
|
||||||
@@ -76,6 +83,8 @@ class PhotoMutation:
|
|||||||
|
|
||||||
@strawberry.mutation()
|
@strawberry.mutation()
|
||||||
@error_logging
|
@error_logging
|
||||||
@authenticated_user_only()
|
async def delete_photo(self, info, id: int, copilot_upload_token: Optional[str] = None) -> Photo:
|
||||||
async def delete_photo(self, info, id: int) -> Photo:
|
if info.context.user_id is None and not copilot_upload_token:
|
||||||
|
raise_unauthorized()
|
||||||
|
|
||||||
return await PhotoMutationResolver().delete(user_id=info.context.user_id, id=id)
|
return await PhotoMutationResolver().delete(user_id=info.context.user_id, id=id)
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ class BaseMutationResolver(BaseResolver):
|
|||||||
item = await self._get_one(db, id, user_id)
|
item = await self._get_one(db, id, 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) -> GQL_TYPE:
|
async def delete(self, user_id: int, 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, user_id)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
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
|
||||||
@@ -17,6 +20,8 @@ class CopilotQueryResolver(BaseQueryResolver):
|
|||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
pilot_username = kwargs.pop("pilot_username", None)
|
pilot_username = kwargs.pop("pilot_username", None)
|
||||||
|
upload_token = kwargs.pop("upload_token", None)
|
||||||
|
upload_flight_slug = kwargs.pop("upload_flight_slug", None)
|
||||||
|
|
||||||
query = super().get_query(
|
query = super().get_query(
|
||||||
user_id=user_id, object_id=object_id, order_by=order_by,
|
user_id=user_id, object_id=object_id, order_by=order_by,
|
||||||
@@ -31,4 +36,14 @@ class CopilotQueryResolver(BaseQueryResolver):
|
|||||||
.filter(models.User.public_username == pilot_username)
|
.filter(models.User.public_username == pilot_username)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if upload_token and upload_flight_slug:
|
||||||
|
query = (
|
||||||
|
query.join(models.Copilot.flights)
|
||||||
|
.filter(and_(
|
||||||
|
models.Flight.url_slug == upload_flight_slug,
|
||||||
|
models.Flight.created_by_id == models.User.id
|
||||||
|
))
|
||||||
|
.filter(models.flight_has_copilot.c.token == upload_token)
|
||||||
|
)
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ 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 delete_file
|
||||||
|
from utils.str_utils import random_str
|
||||||
from utils.upload import handle_file_upload
|
from utils.upload import handle_file_upload
|
||||||
|
|
||||||
|
|
||||||
@@ -232,7 +233,11 @@ class FlightMutationResolver(BaseMutationResolver):
|
|||||||
])
|
])
|
||||||
|
|
||||||
for copilot_id in copilots:
|
for copilot_id in copilots:
|
||||||
await db.execute(insert(flight_has_copilot).values(flight_id=flight_id, copilot_id=copilot_id))
|
await db.execute(insert(flight_has_copilot).values(
|
||||||
|
flight_id=flight_id,
|
||||||
|
copilot_id=copilot_id,
|
||||||
|
token=random_str(64)
|
||||||
|
))
|
||||||
|
|
||||||
return await self._do_update(db, flight_data, data)
|
return await self._do_update(db, flight_data, data)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from time import time
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import delete, insert
|
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
|
||||||
@@ -54,6 +54,9 @@ class PhotoQueryResolver(BaseQueryResolver):
|
|||||||
.filter(models.copilot_has_photo.c.copilot_id == kwargs['copilot_id'])
|
.filter(models.copilot_has_photo.c.copilot_id == kwargs['copilot_id'])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if kwargs.get("uploaded_by_copilot_id"):
|
||||||
|
query = query.filter(models.Photo.created_by_copilot_id == kwargs['uploaded_by_copilot_id'])
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
@@ -76,9 +79,13 @@ class PhotoMutationResolver(BaseMutationResolver):
|
|||||||
|
|
||||||
return original_filename
|
return original_filename
|
||||||
|
|
||||||
async def _get_photo_details(self, id: int, user_id: int):
|
async def _get_photo_details(self, id: int, user_id: int, copilot_token: Optional[str]):
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
photo = await self._get_one(db, id, created_by_id=user_id)
|
if not copilot_token:
|
||||||
|
photo = await self._get_one(db, id, created_by_id=user_id)
|
||||||
|
else:
|
||||||
|
photo = await self._get_photo_info_by_copilot_token(db, photo_id=id, copilot_token=copilot_token)
|
||||||
|
|
||||||
flight_id = photo.flight_id
|
flight_id = photo.flight_id
|
||||||
filename = photo.filename + "." + photo.filename_extension
|
filename = photo.filename + "." + photo.filename_extension
|
||||||
|
|
||||||
@@ -87,17 +94,48 @@ class PhotoMutationResolver(BaseMutationResolver):
|
|||||||
flight_id=flight_id,
|
flight_id=flight_id,
|
||||||
path=path,
|
path=path,
|
||||||
filename=filename,
|
filename=filename,
|
||||||
original_filename=self._copy_original(path, filename)
|
original_filename=self._copy_original(path, filename) # TODO: tohle nedelat v getteru!
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _get_photo_info_by_copilot_token(db, photo_id: int, copilot_token: Optional[str]):
|
||||||
|
if not copilot_token:
|
||||||
|
return
|
||||||
|
|
||||||
|
return (
|
||||||
|
await db.scalars(
|
||||||
|
select(models.Photo)
|
||||||
|
.join(models.Photo.flight)
|
||||||
|
.join(models.flight_has_copilot)
|
||||||
|
.filter(models.flight_has_copilot.c.token == copilot_token)
|
||||||
|
.filter(models.Photo.id == photo_id)
|
||||||
|
|
||||||
|
)
|
||||||
|
).one()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _validate_copilot_token(flight_id: int, token: str) -> tuple[int, int]:
|
||||||
|
async with get_session() as db:
|
||||||
|
query = (
|
||||||
|
select(models.Copilot.created_by_id, models.Copilot.id)
|
||||||
|
.join(models.flight_has_copilot)
|
||||||
|
.filter(models.flight_has_copilot.c.flight_id == flight_id)
|
||||||
|
.filter(models.flight_has_copilot.c.token == token)
|
||||||
|
)
|
||||||
|
return (await db.execute(query)).fetchone()
|
||||||
|
|
||||||
async def upload(self, info, input: UploadPhotoInput) -> Photo:
|
async def upload(self, info, input: UploadPhotoInput) -> Photo:
|
||||||
|
created_by_id = info.context.user_id
|
||||||
|
copilot_id = None
|
||||||
|
if input.copilot_upload_token:
|
||||||
|
created_by_id, copilot_id = await self._validate_copilot_token(input.flight_id, input.copilot_upload_token)
|
||||||
|
|
||||||
path = get_photo_basepath(input.flight_id)
|
path = get_photo_basepath(input.flight_id)
|
||||||
img_name = await handle_file_upload(input.photo, path, uid_prefix=False, overwrite=False)
|
img_name = await handle_file_upload(input.photo, path, uid_prefix=False, overwrite=False)
|
||||||
|
|
||||||
exif_info = await parse_exif_info(path, img_name)
|
|
||||||
|
|
||||||
img = Image.open(f"{path}/{img_name}")
|
img = Image.open(f"{path}/{img_name}")
|
||||||
filename, filename_ext = os.path.splitext(img_name)
|
filename, filename_ext = os.path.splitext(img_name)
|
||||||
|
exif_info = await parse_exif_info(path, img_name)
|
||||||
|
|
||||||
async with get_session() as db:
|
async with get_session() as db:
|
||||||
photo = await PhotoMutationResolver()._do_create(
|
photo = await PhotoMutationResolver()._do_create(
|
||||||
@@ -115,7 +153,8 @@ class PhotoMutationResolver(BaseMutationResolver):
|
|||||||
"gps_latitude": exif_info.get("gps_latitude"),
|
"gps_latitude": exif_info.get("gps_latitude"),
|
||||||
"gps_longitude": exif_info.get("gps_longitude"),
|
"gps_longitude": exif_info.get("gps_longitude"),
|
||||||
"gps_altitude": exif_info.get("gps_altitude"),
|
"gps_altitude": exif_info.get("gps_altitude"),
|
||||||
"created_by_id": info.context.user_id,
|
"created_by_id": created_by_id,
|
||||||
|
"created_by_copilot_id": copilot_id
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -148,8 +187,10 @@ 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(self, id: int, user_id: int, direction: str, info):
|
async def change_orientation(
|
||||||
photo = await self._get_photo_details(id, user_id)
|
self, id: int, user_id: int, direction: str, copilot_upload_token: Optional[str], info
|
||||||
|
):
|
||||||
|
photo = await self._get_photo_details(id, user_id, copilot_upload_token)
|
||||||
|
|
||||||
degrees_map = {
|
degrees_map = {
|
||||||
"clockwise": 90,
|
"clockwise": 90,
|
||||||
@@ -176,7 +217,7 @@ class PhotoMutationResolver(BaseMutationResolver):
|
|||||||
})
|
})
|
||||||
|
|
||||||
async def adjust(self, id: int, user_id: int, adjustment: AdjustmentInput, info):
|
async def adjust(self, id: int, user_id: int, adjustment: AdjustmentInput, info):
|
||||||
photo = await self._get_photo_details(id, user_id)
|
photo = await self._get_photo_details(id, user_id, copilot_token=None)
|
||||||
editor = (
|
editor = (
|
||||||
PhotoEditor(photo.path, photo.original_filename)
|
PhotoEditor(photo.path, photo.original_filename)
|
||||||
.adjust(
|
.adjust(
|
||||||
@@ -221,9 +262,12 @@ class PhotoMutationResolver(BaseMutationResolver):
|
|||||||
"cache_key": int(time())
|
"cache_key": int(time())
|
||||||
})
|
})
|
||||||
|
|
||||||
async def delete(self, user_id: int, id: int) -> Photo:
|
async def delete(self, user_id: int, id: int, **kwargs) -> Photo:
|
||||||
photo = await super().delete(user_id, id)
|
copilot_upload_token = kwargs.get("copilot_upload_token")
|
||||||
|
await self._get_photo_details(id, user_id, copilot_upload_token)
|
||||||
|
# ^^ pokud neexistuje, vyhodi vyjimku
|
||||||
|
|
||||||
|
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 = [
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class UploadPhotoInput:
|
|||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
point_of_interest: Optional[ComboboxInput] = None
|
point_of_interest: Optional[ComboboxInput] = None
|
||||||
|
copilot_upload_token: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@strawberry.input
|
@strawberry.input
|
||||||
|
|||||||
@@ -170,9 +170,16 @@ class Copilot:
|
|||||||
|
|
||||||
return await dataloader.load(root.id)
|
return await dataloader.load(root.id)
|
||||||
|
|
||||||
|
def resolve_copilot_token(root, info):
|
||||||
|
return getattr(root, "token")
|
||||||
|
|
||||||
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(
|
||||||
|
permission_classes=[IsAuthenticated],
|
||||||
|
resolver=resolve_copilot_token
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@strawberry_sqlalchemy_type(models.Aircraft)
|
@strawberry_sqlalchemy_type(models.Aircraft)
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
|
||||||
|
def random_str(length: int) -> str:
|
||||||
|
alphabet = string.ascii_letters + string.digits
|
||||||
|
return ''.join([random.choice(alphabet) for _ in range(length)])
|
||||||
Reference in New Issue
Block a user