# coding=utf-8
from collections import defaultdict
from itertools import chain, groupby
import json
from operator import attrgetter
from bson import ObjectId
from flask import Blueprint, request, render_template, url_for, g
from slideatlas.api import apiv2
from slideatlas import models
from slideatlas import security
from slideatlas.common_utils import jsonify
import pdb
NUMBER_ON_PAGE = 10
mod = Blueprint('session', __name__)
################################################################################
@mod.route('/sessions')
[docs]def sessions_view():
"""
- /sessions With no argument displays list of sessions accessible to current user
- /sessions?sessid=10239094124 searches for the session id
"""
# Support legacy requests for a single session, which use query string args
session_id = request.args.get('sessid')
if session_id:
session = models.Session.objects.get_or_404(id=session_id)
return view_a_session(session)
else:
return view_all_sessions()
################################################################################
[docs]def view_all_sessions():
all_sessions_query = models.Session.objects\
.only('collection', 'label', 'image_store', 'type')\
.order_by('collection', 'label')\
.no_dereference()
# disable dereferencing of of sessions, to prevent running a seperate
# query for every single session's collection
all_collections_query = models.Collection.objects\
.no_dereference()
can_admin_collection_ids = [collection.id for collection in all_collections_query.can_access(g.identity.provides , models.Operation.admin)]
editable_sessions_query = all_sessions_query.can_access(g.identity.provides, models.Operation.edit)
viewable_sessions_query = all_sessions_query.can_access(g.identity.provides, models.Operation.view, strict_operation=True)
# fetch the relevant collections in bulk
collections_by_id = {collection.id: collection for collection in
chain(editable_sessions_query.distinct('collection'),
viewable_sessions_query.distinct('collection')
)}
all_sessions = defaultdict(dict)
for sessions_query, can_admin in [
# viewable must come first, so editable can overwrite
(viewable_sessions_query, False),
(editable_sessions_query, True),
]:
for collection_ref, sessions in groupby(sessions_query, attrgetter('collection')):
collection = collections_by_id[collection_ref.id]
all_sessions[collection].update(dict.fromkeys(sessions, can_admin))
all_sessions = [(collection,
True if collection.id in can_admin_collection_ids else False,
sorted(sessions_dict.iteritems(), key=lambda item: item[0].label)
)
for collection, sessions_dict
in sorted(all_sessions.iteritems(), key=lambda item: item[0].label)]
# Whether to include administrative javascript
is_admin = bool(len(can_admin_collection_ids))
if request.args.get('json'):
ajax_session_list = [
{
'rule': collection.label,
'can_admin': can_admin,
'sessions': [
{
'sessdb': str(session.image_store.id),
'sessid': str(session.id),
'label': session.label
}
for session, can_admin in sessions],
}
for collection, can_admin, sessions in all_sessions]
user_name = security.current_user.full_name if security.current_user.is_authenticated() else 'Guest'
return jsonify(sessions=ajax_session_list, name=user_name, ajax=1)
else:
return render_template('sessionlist.html', all_sessions=all_sessions, is_admin=is_admin)
################################################################################
@mod.route('/sessions/<Session:session>')
@security.ViewSessionRequirement.protected
[docs]def view_a_session(session):
# TODO: the template doesn't seem use 'next'
next_arg = int(request.args.get('next', 0))
session_son = apiv2.SessionItemAPI._get(session)
if request.args.get('json'):
images = []
for view_son in session_son['views']:
database = models.ImageStore.objects.get_or_404(id=view_son['image_store_id'])
imgdb = database.to_pymongo()
imgObj = imgdb["images"].find_one({ "_id" : view_son['image_id']})
# these should not happen but to be safe.
if not "dimensions" in imgObj :
imgObj['dimensions'] = [0,0,0]
if not "levels" in imgObj :
imgObj['levels'] = 0
bounds = [0,imgObj['dimensions'][0], 0, imgObj['dimensions'][1]]
if 'bounds' in imgObj:
bounds = imgObj['bounds']
tileSize = 256
if 'tile_size' in imgObj:
tileSize = imgObj['tile_size']
if 'tileSize' in imgObj:
tileSize = imgObj['tileSize']
if 'TileSize' in imgObj:
tileSize = imgObj['TileSize']
images.append({
'db': view_son['image_store_id'],
'img': view_son['image_id'],
'label': view_son['label'],
'view': view_son['id'],
'bounds': bounds,
'tile_size': tileSize,
'levels': imgObj['levels'],
'dimensions': imgObj['dimensions']
})
ajax_data = {
'success': 1,
'session': session_son,
'images': images,
'attachments': session_son['attachments'],
'imagefiles': session_son["imagefiles"],
'db': session.image_store.id, # TODO: deprecate and remove
'sessid': session.id,
'hide': session_son['hide_annotations'],
'next': url_for('.sessions_view', sessid=str(session.id), ajax=1, next=next_arg + NUMBER_ON_PAGE)
}
return jsonify(ajax_data)
else:
return render_template('session.html',
session=session,
session_son=session_son)
################################################################################
@mod.route('/collections/<Collection:collection>/edit')
[docs]def collection_edit_view(collection):
return render_template('collectionedit.html',
collection=collection)
################################################################################
@mod.route('/sessions/<Session:session>/newstack')
@security.EditSessionRequirement.protected
[docs]def session_new_stack(session):
session_son = apiv2.SessionItemAPI._get(session, with_hidden_label=True)
return render_template('sessionNewStack.html',
collection=session.collection,
session=session,
session_son=session_son)
################################################################################
[docs]def deepcopyview(view_id):
if view_id is None:
return None
admin_db = models.ImageStore._get_db()
view = admin_db['views'].find_one({'_id': ObjectId(view_id)})
if view is None:
return None
# copy children
# Also replace references to children in the html text.
if 'Children' in view:
new_children = []
for child in view['Children']:
new_child = deepcopyview(child)
if new_child is not None:
new_children.append(new_child)
view['Children'] = new_children
# There is probably a better way of getting a
# new id that saving. We have to save twice here.
# We have to replace the oldId string with the new for html text.
oldId = view['_id']
# Lets save the old Id. It might be userful for remeber the original
# view
view['CopySource'] = oldId
# this forces a deep copy
del view['_id']
newId = view['_id'] = admin_db['views'].save(view)
# Replace the old id with the new id in the text.
if 'Text' in view :
view["Text"] = view["Text"].replace(str(oldId),str(newId))
# a second save for the updated html text.
admin_db['views'].save(view)
return newId
################################################################################
@mod.route('/session-save', methods=['GET', 'POST'])
[docs]def session_save_view():
input_str = request.form['input'] # for post
input_obj = json.loads(input_str)
session_id = ObjectId(input_obj['session'])
# I assume the session uses this to get the thumbnail
label = input_obj['label']
view_items = input_obj['views']
# I am using this endpoint for the session editor and the collection editor.
# The collection editor can move views from one session to another.
# In this case, we do not want to delete orphaned views.
delete_views = False
if 'delete_views' in input_obj:
delete_views = input_obj["delete_views"]
admindb = models.ImageStore._get_db()
# Todo: if session is undefined, create a new session (copy views when available).
session = models.Session.objects.with_id(session_id)
security.EditSessionRequirement(session).test()
if 'hideAnnotation' in input_obj:
session.hide_annotations = bool(input_obj['hideAnnotation'])
new_views = list()
for view_item in view_items:
# View or Image
if 'view' in view_item:
# deep or shallow copy of an existing view.
# A bit confusing because no viewdb implies no copy unless 'create_new_session'
if 'copy' in view_item and view_item['copy']:
view_item['view'] = deepcopyview(ObjectId(view_item['view']))
# get the view
view = admindb['views'].find_one({'_id': ObjectId(view_item['view'])})
else:
# Make a new minimal note / view
user = security.current_user.full_name if security.current_user.is_authenticated() else 'Guest'
viewer_records = [{
'Image': ObjectId(view_item['img']),
'Database': ObjectId(view_item['imgdb']),
}]
view = {
'User': user,
'Type': 'Note',
'SessionId': session_id,
'ViewerRecords': viewer_records
}
# The client now knows when hide annotations is on and
# always knows both the label and hidden label.
if 'hiddenLabel' in view_item:
view['HiddenTitle'] = view_item['hiddenLabel']
if 'label' in view_item:
view['Title'] = view_item['label']
# TODO: don't save until the end, to make failure transactional
admindb['views'].save(view, manipulate=True)
new_views.append(ObjectId(view['_id']))
# delete the views that are left over, as views are owned by the session.
# TODO: I would like to add deleted views to a trash session.
# There would be a chance of recovery ...
if delete_views:
removed_view_ids = set(session.views) - set(new_views)
for view_id in removed_view_ids:
apiv2.SessionViewItemAPI._delete(view_id)
# update the session
session.label = label # I am using the label for the annotated title, and name for the hidden title.
session.views = new_views
session.type = 'session'
session.save()
return jsonify(session.to_mongo())
################################################################################
@mod.route('/session-save-stack', methods=['GET', 'POST'])
[docs]def session_save_stack():
input_str = request.form['input'] # for post
input_obj = json.loads(input_str)
session_id = ObjectId(input_obj['sessId'])
label = input_obj['label']
stack_items = input_obj['items']
admindb = models.ImageStore._get_db()
session = models.Session.objects.with_id(session_id)
security.EditSessionRequirement(session).test()
records = list()
for item in stack_items:
camera = {'FocalPoint': [item['x'], item['y'], 0],
'Height': item['height'],
'Roll': item['roll']}
viewer_record = {
'Image': ObjectId(item['img']),
'Database': ObjectId(item['db']),
'Camera' : camera}
if 'widget' in item :
viewer_record['Annotations'] = [item['widget']];
records.append(viewer_record)
for idx in range(1,len(stack_items)) :
item0 = stack_items[idx-1]
item1 = stack_items[idx]
correlationHeight = (item0['height']+item1['height'])*0.5;
records[idx]['Transform'] = {'Correlations':[{'point0': [item0['x'], item0['y']],
'point1': [item1['x'], item1['y']],
'roll': item1['roll']-item0['roll'],
'height': correlationHeight } ]}
# Now make the view
user = security.current_user.id if security.current_user.is_authenticated() else 'Guest'
view = {
'CoordinateSystem': 'Pixel',
'User': user,
'Type': 'Stack',
'ViewerRecords': records,
'Title': label,
'HiddenTitle': label
}
# TODO: don't save until the end, to make failure transactional
admindb['views'].save(view, manipulate=True)
# update the session
session.views.insert(0, view['_id'])
session.save()
return jsonify(view)
################################################################################
@mod.route('/bookmarks')
@security.login_required
[docs]def bookmarks_view():
user_id = security.current_user.id
# Compile the rules
# TODO: this is a hack to get a PyMongo admin DB for now, it should be changed
admin_db = models.ImageStore._get_db()
note_array = []
# TODO: why is a 'views' collection being saved in the admin DB!!!!
for note_obj in admin_db['views'].find({'User': user_id, 'Type': 'UserNote'}):
note_data = {
'noteid': str(note_obj['_id']),
'imgid': note_obj['ViewerRecords'][0]['Image']['_id'],
'imgdb': note_obj['ViewerRecords'][0]['Image']['database'],
'title': note_obj['Title']
}
note_array.append(note_data)
data = {'notes': note_array}
return render_template('notes.html', data=data)