Files
api/src/graphql_schema/entities/resolvers/photo.py
T

225 lines
8.4 KiB
Python

import os
import shutil
from typing import Type, Optional, List
from PIL import Image
from pydantic import BaseModel
from sqlalchemy import select, delete, insert
from background_jobs.elevation import add_terrain_elevation_to_photo
from background_jobs.photo import generate_thumbnail, resize_photo
from database import models
from database.transaction import get_session
from graphql_schema.entities.helpers.combobox import handle_combobox_save
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver, GQL_TYPE
from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput
from graphql_schema.entities.types.types import Photo
from paths import get_photo_basepath
from utils.file import delete_file
from utils.image import PhotoEditor, parse_exif_info
from utils.upload import handle_file_upload
class PhotoQueryResolver(BaseQueryResolver):
def __init__(self):
super().__init__(Photo, models.Photo)
def get_query(
self,
user_id: Optional[int] = None,
object_id: Optional[int] = None,
order_by: Optional[list] = None,
only_public: Optional[bool] = False,
*args, **kwargs
):
query = super().get_query(
user_id, object_id, order_by,
aircraft_id=kwargs.get("aircraft_id"),
point_of_interest_id=kwargs.get("point_of_interest_id")
)
if kwargs.get("public"):
query = (
query.join(models.Flight, onclause=models.Photo.flight_id == models.Flight.id)
.filter(models.Flight.is_public.is_(True))
)
if kwargs.get("copilot_id"):
query = (
query.join(models.copilot_has_photo, )
.filter(models.copilot_has_photo.c.copilot_id == kwargs['copilot_id'])
)
return query
class PhotoDetailInfo(BaseModel):
filename: str
original_filename: str
path: str
flight_id: int
class PhotoMutationResolver(BaseMutationResolver):
def __init__(self):
super().__init__(Photo, models.Photo)
@staticmethod
def _copy_original(path: str, filename: str):
original_filename = "_original_" + filename
if not os.path.isfile(path + "/" + original_filename):
shutil.copyfile(path + "/" + filename, path + "/" + original_filename)
return original_filename
async def _get_photo_details(self, id: int, user_id: int):
async with get_session() as db:
photo = await self._get_one(db, id, created_by_id=user_id)
flight_id = photo.flight_id
filename = photo.filename
path = get_photo_basepath(flight_id)
return PhotoDetailInfo(
flight_id=flight_id,
path=path,
filename=filename,
original_filename=self._copy_original(path, filename)
)
async def upload(self, info, input: UploadPhotoInput) -> Photo:
path = get_photo_basepath(input.flight_id)
filename = await handle_file_upload(input.photo, path)
exif_info = await parse_exif_info(path, filename)
img = Image.open(f"{path}/{filename}")
photo = await PhotoMutationResolver().create(
user_id=info.context.user_id,
data={
"flight_id": input.flight_id,
"name": input.name,
"filename": filename,
"width": img.width,
"height": img.height,
"description": input.description,
"exposed_at": exif_info.get("datetime_original"),
"gps_latitude": exif_info.get("gps_latitude"),
"gps_longitude": exif_info.get("gps_longitude"),
"gps_altitude": exif_info.get("gps_altitude"),
"is_flight_cover": False,
},
)
info.context.background_tasks.add_task(resize_photo, path=path, filename=filename, photo_id=photo.id)
info.context.background_tasks.add_task(generate_thumbnail, path=path, filename=filename)
if exif_info.get("gps_latitude") and exif_info.get("gps_longitude"):
info.context.background_tasks.add_task(add_terrain_elevation_to_photo, photo=photo)
return photo
async def update(self, id: int, input: EditPhotoInput, user_id: int) -> Photo:
data = input.to_dict()
async with get_session() as db:
photo = await self._get_one(db, id, created_by_id=user_id)
if input.point_of_interest:
data['point_of_interest_id'] = await handle_combobox_save(
db,
models.PointOfInterest,
input.point_of_interest,
user_id,
extra_data={"description": ""}
)
if input.is_aircraft:
flight = (await db.scalars(select(models.Flight).filter(models.Flight.id == photo.flight_id))).one()
data['aircraft_id'] = flight.aircraft_id
if input.copilots is not None:
await db.execute(delete(models.copilot_has_photo).filter_by(photo_id=id))
for copilot in input.copilots:
await db.execute(insert(models.copilot_has_photo).values(photo_id=id, copilot_id=copilot.id))
return await self._do_update(db, obj=photo, data=data)
async def change_orientation(self, id: int, user_id: int, direction: str, info):
photo = await self._get_photo_details(id, user_id)
degrees_map = {
"clockwise": 90,
"counterClockwise": -90
}
# rotate original
editor = PhotoEditor(photo.path, photo.original_filename)
editor.rotate(degrees=degrees_map[direction], crop_after_rotate=False)
editor.write_to_file(quality=100)
# rotate possibly adjusted image
editor = PhotoEditor(photo.path, photo.filename)
editor.rotate(degrees=degrees_map[direction], crop_after_rotate=False)
editor.write_to_file(quality=100)
info.context.background_tasks.add_task(generate_thumbnail, path=photo.path, filename=photo.filename)
async with get_session() as db:
return await self._do_update(db, obj={"id": id}, data={
"width": editor.img.width,
"height": editor.img.height
})
async def adjust(self, id: int, user_id: int, adjustment: AdjustmentInput, info):
photo = await self._get_photo_details(id, user_id)
editor = (
PhotoEditor(photo.path, photo.original_filename)
.adjust(
brightness=adjustment.brightness,
contrast=adjustment.contrast,
sharpness=adjustment.sharpness,
saturation=adjustment.saturation
)
)
if adjustment.rotate:
rotate_angle = adjustment.rotate
editor.rotate(rotate_angle, adjustment.crop_after_rotate)
if adjustment.crop:
editor.crop(**adjustment.crop.to_dict())
editor.write_to_file(dest_filename=photo.filename)
info.context.background_tasks.add_task(generate_thumbnail, path=photo.path, filename=photo.filename)
async with (get_session() as db):
await db.execute(delete(models.PhotoAdjustment).filter(models.PhotoAdjustment.photo_id == id))
crop_info = {
"crop_" + key: value
for key, value in adjustment.crop.to_dict().items()
} if adjustment.crop else {}
await models.PhotoAdjustment.create(db, {
"photo_id": id,
"contrast": adjustment.contrast,
"saturation": adjustment.saturation,
"brightness": adjustment.brightness,
"rotate": adjustment.rotate,
"sharpness": adjustment.sharpness,
**crop_info
})
return await self._do_update(db, obj={"id": id}, data={
"width": editor.img.width,
"height": editor.img.height
})
async def delete(self, user_id: int, id: int) -> Photo:
photo = await super().delete(user_id, id)
base_path = get_photo_basepath(photo.flight_id)
delete_file(f"{base_path}/{photo.filename}", silent=True)
delete_file(f"{base_path}/_original_{photo.filename}", silent=True)
delete_file(f"{base_path}/thumbs/{photo.filename}", silent=True)
return photo