Refaktoring photo endpointu
This commit is contained in:
@@ -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/")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user