Refaktoring photo endpointu

This commit is contained in:
Michal Kváček
2023-11-02 09:03:35 +01:00
parent 0de73a5ec2
commit 52ddf6502a
7 changed files with 175 additions and 159 deletions
+1 -1
View File
@@ -16,4 +16,4 @@ async def resize_photo(path: str, filename: str, photo_id: int):
async def generate_thumbnail(path: str, filename: str): async def generate_thumbnail(path: str, filename: str):
editor = PhotoEditor(path, filename) editor = PhotoEditor(path, filename)
editor.resize(new_width=300) editor.resize(new_width=300)
editor.write_to_file(quality=85, dest_path=f"{path}/thumbs/") editor.write_to_file(quality=85, dest_path=f"{path}/thumbs/")
+1 -4
View File
@@ -19,7 +19,7 @@ class PhotoEditorEndpoint(AuthEndpoint):
basepath = get_photo_basepath(photo.flight_id) basepath = get_photo_basepath(photo.flight_id)
filename = photo.filename filename = photo.filename
original_filename = '_original_'+filename original_filename = '_original_' + filename
if os.path.exists(f"{basepath}/{original_filename}"): if os.path.exists(f"{basepath}/{original_filename}"):
filename = original_filename filename = original_filename
@@ -47,7 +47,4 @@ class PhotoEditorEndpoint(AuthEndpoint):
if adjustments: if adjustments:
editor.adjust(**adjustments) editor.adjust(**adjustments)
return StreamingResponse(content=editor.get_as_stream(), media_type="image/jpeg") return StreamingResponse(content=editor.get_as_stream(), media_type="image/jpeg")
+3 -3
View File
@@ -1,4 +1,4 @@
from typing import List from typing import List, Optional
import strawberry import strawberry
from database import models from database import models
from decorators.endpoints import authenticated_user_only from decorators.endpoints import authenticated_user_only
@@ -15,8 +15,8 @@ class CopilotQueries:
return await BaseQueryResolver(Copilot, models.Copilot).get_list(info.context.user_id) return await BaseQueryResolver(Copilot, models.Copilot).get_list(info.context.user_id)
@strawberry.field() @strawberry.field()
@authenticated_user_only() async def copilot(root, info, id: int, pilot_username: Optional[str] = None) -> Copilot:
async def copilot(root, info, id: int) -> Copilot: params = {}
return await BaseQueryResolver(Copilot, models.Copilot).get_one(id, user_id=info.context.user_id) return await BaseQueryResolver(Copilot, models.Copilot).get_one(id, user_id=info.context.user_id)
+12 -144
View File
@@ -1,24 +1,10 @@
import asyncio from typing import List
import os
import shutil
from typing import List, Optional
import strawberry import strawberry
from PIL import Image
from sqlalchemy import delete, select
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 import models
from decorators.endpoints import authenticated_user_only from decorators.endpoints import authenticated_user_only
from database.transaction import get_session from graphql_schema.entities.resolvers.base import BaseQueryResolver
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
from graphql_schema.entities.resolvers.photo import PhotoMutationResolver from graphql_schema.entities.resolvers.photo import PhotoMutationResolver
from graphql_schema.entities.types.types import Photo, PhotoAdjustment from graphql_schema.entities.types.types import Photo
from logger import log
from paths import get_photo_basepath
from utils.file import delete_file
from utils.image import parse_exif_info, rotate_image_no_crop, adjust_image, resize_image, PhotoEditor
from utils.upload import handle_file_upload
from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput
@@ -38,36 +24,7 @@ class PhotoMutation:
@strawberry.mutation @strawberry.mutation
@authenticated_user_only() @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) return await PhotoMutationResolver().upload(info, input)
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
@strawberry.mutation() @strawberry.mutation()
@authenticated_user_only() @authenticated_user_only()
@@ -77,108 +34,19 @@ class PhotoMutation:
@strawberry.mutation() @strawberry.mutation()
@authenticated_user_only() @authenticated_user_only()
async def change_orientation(self, info, id: int, direction: str) -> Photo: async def change_orientation(self, info, id: int, direction: str) -> Photo:
async with get_session() as db: return await PhotoMutationResolver().change_orientation(
photo = (await db.scalars( id=id,
BaseQueryResolver(Photo, models.Photo).get_query(user_id=info.context.user_id, object_id=id) user_id=info.context.user_id,
)).one() direction=direction,
flight_id = photo.flight_id info=info
photo_id = photo.id )
photo_filename = photo.filename
photo_path = get_photo_basepath(flight_id)
original_filename = "_original_" + photo_filename
if not os.path.isfile(photo_path + "/" + original_filename):
shutil.copyfile(photo_path + "/" + photo_filename, photo_path + "/" + original_filename)
degrees_map = {
"clockwise": -90,
"counterClockwise": 90
}
# rotate original
editor = PhotoEditor(photo_path, 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:
photo = await models.Photo.update(db, {
"width": editor.img.width,
"height": editor.img.height
}, id=photo_id)
return Photo(**photo.as_dict())
@strawberry.mutation() @strawberry.mutation()
@authenticated_user_only() @authenticated_user_only()
async def adjust_photo(self, info, id: int, adjustment: AdjustmentInput) -> Photo: async def adjust_photo(self, info, id: int, adjustment: AdjustmentInput) -> Photo:
async with get_session() as db: return await PhotoMutationResolver().adjust(id, info=info, user_id=info.context.user_id, adjustment=adjustment)
photo = (await db.scalars(
BaseQueryResolver(Photo, models.Photo).get_query(user_id=info.context.user_id, object_id=id)
)).one()
flight_id = photo.flight_id
photo_filename = photo.filename
photo_as_dict = photo.as_dict()
photo_path = get_photo_basepath(flight_id)
# TOOD: presunout do samostatne metody
original_filename = "_original_" + photo_filename
if not os.path.isfile(photo_path + "/" + original_filename):
shutil.copyfile(photo_path + "/" + photo_filename, photo_path + "/" + original_filename)
editor = (
PhotoEditor(photo_path, 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
})
width, height = editor.img_size
await models.Photo.update(db, {"width": width, "height": height}, id=id)
return Photo(**photo_as_dict)
@strawberry.mutation() @strawberry.mutation()
@authenticated_user_only() @authenticated_user_only()
async def delete_photo(self, info, id: int) -> Photo: async def delete_photo(self, info, id: int) -> Photo:
photo = await PhotoMutationResolver().delete(user_id=info.context.user_id, id=id) return await PhotoMutationResolver().delete(user_id=info.context.user_id, id=id)
base_path = get_photo_basepath(photo.flight_id)
delete_file(f"{base_path}/{photo.filename}", silent=True)
delete_file(f"{base_path}/thumbs/{photo.filename}", silent=True)
return photo
+157 -5
View File
@@ -1,24 +1,97 @@
from sqlalchemy import update import os
import shutil
from PIL import Image
from pydantic import BaseModel
from sqlalchemy import update, delete
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
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 import models
from database.transaction import get_session from database.transaction import get_session
from graphql_schema.entities.helpers.combobox import handle_combobox_save from graphql_schema.entities.helpers.combobox import handle_combobox_save
from graphql_schema.entities.resolvers.base import BaseMutationResolver from graphql_schema.entities.resolvers.base import BaseMutationResolver
from graphql_schema.entities.types.mutation_input import EditPhotoInput from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput
from graphql_schema.entities.types.types import Photo 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 PhotoDetailInfo(BaseModel):
filename: str
original_filename: str
path: str
flight_id: int
class PhotoMutationResolver(BaseMutationResolver): class PhotoMutationResolver(BaseMutationResolver):
def __init__(self): def __init__(self):
super().__init__(Photo, models.Photo) super().__init__(Photo, models.Photo)
async def reset_flight_cover(self, db: AsyncSession, flight_id: int, ignored_photo_id: int): @staticmethod
async def _reset_flight_cover(db: AsyncSession, flight_id: int, ignored_photo_id: int):
(await db.execute( (await db.execute(
update(models.Photo) update(models.Photo)
.filter(models.Photo.flight_id == flight_id) .filter(models.Photo.flight_id == flight_id)
.filter(models.Photo.id != ignored_photo_id).values(is_flight_cover=False)) .filter(models.Photo.id != ignored_photo_id)
.values(is_flight_cover=False))
) )
@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: async def update(self, id: int, input: EditPhotoInput, user_id: int) -> Photo:
data = input.to_dict() data = input.to_dict()
@@ -35,6 +108,85 @@ class PhotoMutationResolver(BaseMutationResolver):
if input.is_flight_cover: if input.is_flight_cover:
# reset other covers # reset other covers
await self.reset_flight_cover(db, photo.flight_id, id) await self._reset_flight_cover(db, photo.flight_id, id)
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):
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
+1 -1
View File
@@ -10,7 +10,7 @@ class BaseGraphqlInputType:
dict_data = {} dict_data = {}
for key in self.__annotations__.keys(): for key in self.__annotations__.keys():
value = getattr(self, key) value = getattr(self, key)
if value is None or key in self.ignored_fields + self.base_ignored_fields: if value is None or key in (self.ignored_fields + self.base_ignored_fields):
continue continue
if isinstance(value, datetime): if isinstance(value, datetime):
-1
View File
@@ -136,7 +136,6 @@ class PhotoEditor:
return dest return dest
# ----------- # -----------
# odsud niz to bude vse asi na smazani, vse by mela umet trida PhotoEditor # odsud niz to bude vse asi na smazani, vse by mela umet trida PhotoEditor