Moznost nahravat fotky copilotem

This commit is contained in:
Michal Kváček
2024-03-07 06:45:00 +01:00
parent c0ae73440d
commit 400ad8b75a
15 changed files with 242 additions and 44 deletions
@@ -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 ###
+2
View File
@@ -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])
+6 -2
View File
@@ -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)
+50 -19
View File
@@ -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)
+14 -1
View File
@@ -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,
+2
View File
@@ -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
+15 -6
View File
@@ -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)
+56 -12
View File
@@ -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)
+7
View File
@@ -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)])