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