From cf72709ddf4dae1194ecf8fb480207338caf2dd0 Mon Sep 17 00:00:00 2001 From: tanshu Date: Sun, 28 Jun 2015 18:21:27 +0530 Subject: [PATCH] Updated picture_raw view to show different sizes. Albums shifted to semantic url names. Uploads are now thumbnailed when first viewed. --- soter/__init__.py | 2 +- soter/models/master.py | 8 ++- .../static/app/picture/picture.controller.js | 7 +- soter/static/app/picture/view.html | 4 +- soter/static/index.html | 1 + soter/views/album.py | 30 ++++++-- soter/views/picture.py | 71 ++++++++++++++----- 7 files changed, 91 insertions(+), 32 deletions(-) diff --git a/soter/__init__.py b/soter/__init__.py index 00863a2..26c3d98 100644 --- a/soter/__init__.py +++ b/soter/__init__.py @@ -39,7 +39,7 @@ def main(global_config, **settings): config.add_route('logout', '/logout') add_route(config, 'picture', '/picture') - config.add_route('picture_raw', '/p/{id}') + config.add_route('picture_raw', '/p/{size}/{id}') config.add_route('upload', '/upload') config.add_route('api_upload', '/v1/upload') diff --git a/soter/models/master.py b/soter/models/master.py index 7354e65..49a275a 100644 --- a/soter/models/master.py +++ b/soter/models/master.py @@ -78,7 +78,7 @@ class Album(Base): @name.setter def name(self, name): - self.name = name + self._name = name self.semantic = semantic_name(name) def __init__(self, name, description, user_id, is_public, id=None, is_fixture=False): @@ -105,6 +105,12 @@ class Album(Base): return None return DBSession.query(cls).filter(cls.name.ilike(name)).first() + @classmethod + def by_semantic(cls, semantic): + if not semantic: + return None + return DBSession.query(cls).filter(cls.semantic.ilike(semantic)).first() + @classmethod def menu_item(cls): return uuid.UUID('dad46805-f577-4e5b-8073-9b788e0173fc') diff --git a/soter/static/app/picture/picture.controller.js b/soter/static/app/picture/picture.controller.js index cff0413..5a67c6c 100644 --- a/soter/static/app/picture/picture.controller.js +++ b/soter/static/app/picture/picture.controller.js @@ -33,9 +33,10 @@ var UploadCtrlResolve = {}; var PictureCtrlResolve = { - album: ['$route', 'Picture', function ($route, Picture) { - var id = $route.current.params.id; - return Picture.get({id: id}).$promise; + picture: ['$route', 'Picture', function ($route, Picture) { + var id = $route.current.params.id, + size = $route.current.params.s; + return Picture.get({id: id, s: size}).$promise; }] }; diff --git a/soter/static/app/picture/view.html b/soter/static/app/picture/view.html index dfff7d4..e5145e6 100644 --- a/soter/static/app/picture/view.html +++ b/soter/static/app/picture/view.html @@ -1,5 +1,5 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/soter/static/index.html b/soter/static/index.html index acba536..8debf1a 100644 --- a/soter/static/index.html +++ b/soter/static/index.html @@ -51,6 +51,7 @@ + diff --git a/soter/views/album.py b/soter/views/album.py index 5a2b637..c2c723c 100644 --- a/soter/views/album.py +++ b/soter/views/album.py @@ -2,8 +2,11 @@ import uuid import re import pkg_resources + from pyramid.response import FileResponse, Response + from pyramid.security import authenticated_userid + from pyramid.view import view_config import transaction @@ -35,7 +38,13 @@ def save(request): @view_config(request_method='POST', route_name='api_album_id', renderer='json', permission='Albums') @TryCatchFunction def update(request): - item = Album.by_id(uuid.UUID(request.matchdict['id'])) + p = re.compile('^[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}$') + id = request.matchdict['id'] + if p.match(id): + item = Album.by_id(uuid.UUID(id)) + else: + item = Album.by_semantic(id) + if item.is_fixture: raise ValidationError("{0} is a fixture and cannot be edited or deleted.".format(item.name)) item.name = request.json_body['name'] @@ -61,7 +70,13 @@ def delete(request): @view_config(request_method='GET', route_name='api_album_id', renderer='json', permission='Albums') def show_id(request): - return album_info(request.matchdict['id']) + p = re.compile('^[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}$') + id = request.matchdict['id'] + if p.match(id): + item = Album.by_id(uuid.UUID(id)) + else: + item = Album.by_semantic(id) + return album_info(item) @view_config(request_method='GET', route_name='api_album', renderer='json', permission='Albums') @@ -77,8 +92,8 @@ def show_list(request): for item in list: albums.append({'name': item.name, 'description': item.description, 'isPublic': item.is_public, 'user': item.user.name, 'isFixture': item.is_fixture, 'pictures': [], - 'viewUrl': request.route_url('album_id_view', id=item.id, ), - 'editUrl': request.route_url('album_id', id=item.id)}) + 'viewUrl': request.route_url('album_id_view', id=item.semantic, ), + 'editUrl': request.route_url('album_id', id=item.semantic)}) return albums @@ -90,11 +105,12 @@ def show_pics(request): if p.match(id): album = Album.by_id(uuid.UUID(id)) else: - album = Album.by_name(id) + album = Album.by_semantic(id) info = album_info(album) for item in album.pictures: - info['pictures'].append({'name': item.name, 'url': request.route_url('picture_id', id=item.id), - 'imageUrl': request.route_url('picture_raw', id=item.id)}) + info['pictures'].append({'name': item.name, + 'url': request.route_url('picture_id', id=item.id, _query={'s': '1600x1200'}), + 'imageUrl': request.route_url('picture_raw', size='242x200', id=item.id)}) return info diff --git a/soter/views/picture.py b/soter/views/picture.py index 03705e6..17393ac 100644 --- a/soter/views/picture.py +++ b/soter/views/picture.py @@ -4,10 +4,12 @@ import shutil import uuid from PIL import Image, ExifTags +from PIL.Image import LANCZOS import pkg_resources -from pyramid.response import FileResponse +from pyramid.response import FileResponse, Response from pyramid.security import authenticated_userid from pyramid.view import view_config +from sqlalchemy.exc import IntegrityError import transaction import numpy @@ -16,6 +18,7 @@ from soter.models.master import Picture, Album @view_config(request_method='GET', route_name='upload', permission='Albums') +@view_config(request_method='GET', route_name='picture_id', permission='Albums') def html(request): package, resource = 'soter:static/index.html'.split(':', 1) file = pkg_resources.resource_filename(package, resource) @@ -26,23 +29,34 @@ def html(request): def upload(request): input_file = request.POST['file'].file file_id = uuid.uuid4() - file_path = pkg_resources.resource_filename('soter', 'upload/' + str(file_id) + '.jpg') + file_path = pkg_resources.resource_filename('soter', 'upload/o/' + str(file_id) + '.jpg') album = request.POST['album'] temp_file_path = file_path + '~' input_file.seek(0) with open(temp_file_path, 'wb') as output_file: shutil.copyfileobj(input_file, output_file) - os.rename(temp_file_path, file_path) - # Process picture here to get the hash and the exif tags to be added to the database - pic = process_picture(file_path, str(file_id), uuid.UUID(authenticated_userid(request)), file_id, uuid.UUID(album)) + pic = process_picture(temp_file_path, str(file_id), uuid.UUID(authenticated_userid(request)), file_id, + uuid.UUID(album)) DBSession.add(pic) - transaction.commit() + try: + transaction.commit() + except IntegrityError as ex: + transaction.abort() + os.remove(temp_file_path) + response = Response("Duplicate file") + response.status_int = 500 + return response + else: + os.rename(temp_file_path, file_path) return {'location': file_path} def process_picture(path, name, user_id, file_id, album_id): image = get_image(path) - hash = hashlib.sha512(numpy.array(image)).digest() + img = numpy.array(image) + img = img[:, :, ::-1].copy() + + hash = hashlib.sha512(numpy.array(img)).digest() picture = Picture(name, hash, user_id, True, file_id) picture.albums.append(Album.by_id(album_id)) return picture @@ -51,37 +65,58 @@ def process_picture(path, name, user_id, file_id, album_id): def get_image(path): image = Image.open(path) orientation = [tag for tag in ExifTags.TAGS.keys() if ExifTags.TAGS[tag] == 'Orientation'][0] - exif = dict(image._getexif().items()) + exif = getattr(image, "_getexif", None) + if exif is None: + return image + exif = image._getexif() + if exif is None: + return image + exif = dict(exif.items()) if exif[orientation] == 3: image = image.rotate(180, expand=True) elif exif[orientation] == 6: image = image.rotate(270, expand=True) elif exif[orientation] == 8: image = image.rotate(90, expand=True) - img = numpy.array(image) - # CONVERT RGB TO BGR - return img[:, :, ::-1].copy() + return image @view_config(request_method='GET', route_name='picture_raw', permission='Albums') def show_raw(request): - package, resource = ('soter:upload/' + request.matchdict['id'] + '.jpg').split(':', 1) + size = request.matchdict['size'] + id = request.matchdict['id'] + package, resource = ('soter:upload/' + size + '/' + id + '.jpg').split(':', 1) file = pkg_resources.resource_filename(package, resource) + if not os.path.isfile(file): + create_thumbnail(id, size) return FileResponse(file, request=request) -@view_config(request_method='GET', route_name='api_album_id', renderer='json', request_param='pictures', - permission='Albums') +def create_thumbnail(id, size): + file_path = pkg_resources.resource_filename('soter', 'upload/' + size + '/' + id + '.jpg') + size = tuple([int(i) for i in size.split('x')]) + file = pkg_resources.resource_filename('soter', 'upload/o/' + id + '.jpg') + image = get_image(file) + image.thumbnail(size, resample=LANCZOS) + image.save(file_path) + + +@view_config(request_method='GET', route_name='api_picture_id', renderer='json', request_param='s', permission='Albums') def show_id(request): - return picture_info(request.matchdict['id'], request) + return picture_info(request.matchdict['id'], request.GET['s'], request) -def picture_info(id, request): +@view_config(request_method='GET', route_name='api_picture_id', renderer='json', permission='Albums') +def show_id_org(request): + return picture_info(request.matchdict['id'], 'o', request) + + +def picture_info(id, size, request): if not isinstance(id, Picture): picture = Picture.by_id(id) else: picture = id - album = {'id': picture.id, 'name': picture.name, 'imageUrl': request.route_url('picture_raw', id=picture.id), - + album = {'id': picture.id, 'name': picture.name, + 'imageUrl': request.route_url('picture_raw', id=picture.id, size=size), 'user': {'id': picture.user_id, 'name': picture.user.name}} return album