Change the user session SocketIO Logic

This commit is contained in:
Patrick Jentsch 2022-07-04 14:09:17 +02:00
parent b8bf004684
commit 76924956de
20 changed files with 200 additions and 99 deletions

View File

@ -2,4 +2,4 @@ from flask import Blueprint
bp = Blueprint('main', __name__) bp = Blueprint('main', __name__)
from . import events, routes from . import routes

View File

@ -1,41 +0,0 @@
from app import hashids, socketio
from app.models import User
from flask_login import current_user
from flask_socketio import join_room
from app.decorators import socketio_login_required
@socketio.on('users.user.get')
@socketio_login_required
def users_user_get(user_hashid):
user_id = hashids.decode(user_hashid)
user = User.query.get(user_id)
if user is None:
return {'code': 404, 'msg': 'Not found'}
if not (user == current_user or current_user.is_administrator):
return {'code': 403, 'msg': 'Forbidden'}
# corpora = [x.to_dict() for x in user.corpora]
# jobs = [x.to_dict() for x in user.jobs]
# transkribus_htr_models = TranskribusHTRModel.query.filter(
# (TranskribusHTRModel.shared == True) | (TranskribusHTRModel.user == user)
# ).all()
# tesseract_ocr_models = TesseractOCRModel.query.filter(
# (TesseractOCRModel.shared == True) | (TesseractOCRModel.user == user)
# ).all()
# response = {
# 'code': 200,
# 'msg': 'OK',
# 'payload': {
# 'user': user.to_dict(),
# 'corpora': corpora,
# 'jobs': jobs,
# 'transkribus_htr_models': transkribus_htr_models,
# 'tesseract_ocr_models': tesseract_ocr_models
# }
# }
join_room(f'users.{user.hashid}')
return {
'code': 200,
'msg': 'OK',
'payload': user.to_dict(backrefs=True, relationships=True)
}

View File

@ -1033,6 +1033,8 @@ def ressource_after_delete(mapper, connection, ressource):
jsonpatch = [{'op': 'remove', 'path': ressource.jsonpatch_path}] jsonpatch = [{'op': 'remove', 'path': ressource.jsonpatch_path}]
room = f'users.{ressource.user_hashid}' room = f'users.{ressource.user_hashid}'
socketio.emit('users.patch', jsonpatch, room=room) socketio.emit('users.patch', jsonpatch, room=room)
room = f'/users/{ressource.user_hashid}'
socketio.emit('PATCH', jsonpatch, room=room)
@db.event.listens_for(Corpus, 'after_insert') @db.event.listens_for(Corpus, 'after_insert')
@ -1047,8 +1049,8 @@ def ressource_after_insert_handler(mapper, connection, ressource):
jsonpatch = [ jsonpatch = [
{'op': 'add', 'path': ressource.jsonpatch_path, 'value': value} {'op': 'add', 'path': ressource.jsonpatch_path, 'value': value}
] ]
room = f'users.{ressource.user_hashid}' room = f'/users/{ressource.user_hashid}'
socketio.emit('users.patch', jsonpatch, room=room) socketio.emit('PATCH', jsonpatch, room=room)
@db.event.listens_for(Corpus, 'after_update') @db.event.listens_for(Corpus, 'after_update')
@ -1077,8 +1079,8 @@ def ressource_after_update_handler(mapper, connection, ressource):
} }
) )
if jsonpatch: if jsonpatch:
room = f'users.{ressource.user_hashid}' room = f'/users/{ressource.user_hashid}'
socketio.emit('users.patch', jsonpatch, room=room) socketio.emit('PATCH', jsonpatch, room=room)
@db.event.listens_for(Job, 'after_update') @db.event.listens_for(Job, 'after_update')

View File

@ -1,21 +1,30 @@
class App { class App {
constructor() { constructor() {
this.data = {users: {}}; this.data = {users: {}};
this.eventListeners = {'users.patch': []};
this.promises = {users: {}}; this.promises = {users: {}};
this.socket = io({transports: ['websocket'], upgrade: false}); this.socket = io({transports: ['websocket'], upgrade: false});
this.socket.on('users.patch', patch => this.usersPatchHandler(patch)); this.socket.on('PATCH', (patch) => {this.data = jsonpatch.applyPatch(this.data, patch).newDocument;});
} }
get users() { get users() {
return this.data.users; return this.data.users;
} }
addEventListener(type, listener) { subscribeUser(userId) {
if (!(type in this.eventListeners)) { if (userId in this.promises.users) {
throw `Unknown event type: ${type}`; return this.promises.users[userId];
} }
this.eventListeners[type].push(listener); this.promises.users[userId] = new Promise((resolve, reject) => {
this.socket.emit('SUBSCRIBE /users/<user_id>', userId, response => {
if (response.code === 200) {
this.data.users[userId] = response.payload;
resolve(this.data.users[userId]);
} else {
reject(response);
}
});
});
return this.promises.users[userId];
} }
flash(message, category) { flash(message, category) {
@ -50,29 +59,4 @@ class App {
toastCloseActionElement = toast.el.querySelector('.toast-action[data-action="close"]'); toastCloseActionElement = toast.el.querySelector('.toast-action[data-action="close"]');
toastCloseActionElement.addEventListener('click', () => {toast.dismiss();}); toastCloseActionElement.addEventListener('click', () => {toast.dismiss();});
} }
getUserById(userId) {
if (userId in this.promises.users) {
return this.promises.users[userId];
}
this.promises.users[userId] = new Promise((resolve, reject) => {
this.socket.emit('users.user.get', userId, response => {
if (response.code === 200) {
this.data.users[userId] = response.payload;
resolve(this.data.users[userId]);
} else {
reject(response);
}
});
});
return this.promises.users[userId];
}
usersPatchHandler(patch) {
let listener;
this.data = jsonpatch.applyPatch(this.data, patch).newDocument;
//this.data = jsonpatch.apply_patch(this.data, patch);
for (listener of this.eventListeners['users.patch']) {listener(patch);}
}
} }

View File

@ -1,16 +1,17 @@
class JobStatusNotifier { class JobStatusNotifier {
constructor(userId) { constructor(userId) {
this.userId = userId; this.userId = userId;
app.socket.on('PATCH', (patch) => {this.onPATCH(patch);});
} }
usersPatchHandler(patch) { onPATCH(patch) {
let filteredPatch; let filteredPatch;
let jobId; let jobId;
let match; let match;
let operation; let operation;
let re; let re;
re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)/status$`) re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)/status$`);
filteredPatch = patch filteredPatch = patch
.filter(operation => operation.op === 'replace') .filter(operation => operation.op === 'replace')
.filter(operation => re.test(operation.path)); .filter(operation => re.test(operation.path));

View File

@ -16,7 +16,7 @@ class CorpusDisplay extends RessourceDisplay {
this.setNumTokens(corpus.num_tokens); this.setNumTokens(corpus.num_tokens);
} }
usersPatchHandler(patch) { onPATCH(patch) {
let filteredPatch; let filteredPatch;
let operation; let operation;
let re; let re;

View File

@ -18,7 +18,7 @@ class JobDisplay extends RessourceDisplay {
this.setTitle(job.title); this.setTitle(job.title);
} }
usersPatchHandler(patch) { onPATCH(patch) {
let filteredPatch; let filteredPatch;
let operation; let operation;
let re; let re;

View File

@ -2,13 +2,13 @@ class RessourceDisplay {
constructor(displayElement) { constructor(displayElement) {
this.displayElement = displayElement; this.displayElement = displayElement;
this.userId = this.displayElement.dataset.userId; this.userId = this.displayElement.dataset.userId;
app.addEventListener('users.patch', patch => this.usersPatchHandler(patch)); app.socket.on('PATCH', (patch) => {this.onPATCH(patch);});
app.getUserById(this.userId).then(user => this.init(user)); app.subscribeUser(this.userId).then((user) => {this.init(user);});
} }
init(user) {throw 'Not implemented';} init(user) {throw 'Not implemented';}
usersPatchHandler(patch) {throw 'Not implemented';} onPATCH(patch) {throw 'Not implemented';}
setElement(element, value) { setElement(element, value) {
switch (element.tagName) { switch (element.tagName) {

View File

@ -96,7 +96,7 @@ class CorpusFileList extends RessourceList {
} }
} }
usersPatchHandler(patch) { onPATCH(patch) {
let corpusFileId; let corpusFileId;
let filteredPatch; let filteredPatch;
let match; let match;

View File

@ -88,7 +88,7 @@ class CorpusList extends RessourceList {
} }
} }
usersPatchHandler(patch) { onPATCH(patch) {
let corpusId; let corpusId;
let filteredPatch; let filteredPatch;
let match; let match;

View File

@ -54,5 +54,5 @@ class JobInputList extends RessourceList {
} }
} }
usersPatchHandler(patch) {return;} onPATCH(patch) {return;}
} }

View File

@ -94,7 +94,7 @@ class JobList extends RessourceList {
} }
} }
usersPatchHandler(patch) { onPATCH(patch) {
let filteredPatch; let filteredPatch;
let jobId; let jobId;
let match; let match;

View File

@ -57,7 +57,7 @@ class JobResultList extends RessourceList {
} }
} }
usersPatchHandler(patch) { onPATCH(patch) {
let filteredPatch; let filteredPatch;
let operation; let operation;
let re; let re;

View File

@ -89,7 +89,7 @@ class QueryResultList extends RessourceList {
} }
} }
usersPatchHandler(patch) { onPATCH(patch) {
let filteredPatch; let filteredPatch;
let match; let match;
let operation; let operation;

View File

@ -91,10 +91,10 @@ class RessourceList {
this.userId = this.listjs.listContainer.dataset.userId; this.userId = this.listjs.listContainer.dataset.userId;
this.listjs.list.addEventListener('click', event => this.onclick(event)); this.listjs.list.addEventListener('click', event => this.onclick(event));
if (this.userId) { if (this.userId) {
app.addEventListener('users.patch', patch => this.usersPatchHandler(patch)); app.socket.on('PATCH', (patch) => {this.onPATCH(patch);});
app.getUserById(this.userId).then( app.subscribeUser(this.userId).then(
user => this.init(user), (user) => {this.init(user);},
error => {throw JSON.stringify(error);} (error) => {throw JSON.stringify(error);}
); );
} }
} }
@ -117,7 +117,7 @@ class RessourceList {
onclick(event) {throw 'Not implemented';} onclick(event) {throw 'Not implemented';}
usersPatchHandler(patch) {throw 'Not implemented';} onPATCH(patch) {throw 'Not implemented';}
add(ressources) { add(ressources) {
let values = Array.isArray(ressources) ? ressources : [ressources]; let values = Array.isArray(ressources) ? ressources : [ressources];

View File

@ -32,11 +32,10 @@
const jobStatusNotifier = new JobStatusNotifier(currentUserId); const jobStatusNotifier = new JobStatusNotifier(currentUserId);
// Initialize components for current user // Initialize components for current user
app.addEventListener('users.patch', patch => jobStatusNotifier.usersPatchHandler(patch)); app.subscribeUser(currentUserId)
app.getUserById(currentUserId)
.then( .then(
user => {return;}, (user) => {return;},
error => {throw JSON.stringify(error);} (error) => {throw JSON.stringify(error);}
); );
{%- endif %} {%- endif %}

View File

@ -0,0 +1,69 @@
{% extends "base.html.j2" %}
{% block page_content %}
<div class="container">
<div class="row">
<div class="col s12">
<h1 id="title">{{ title }}</h1>
</div>
<div class="col s12">
<div class="card">
<div class="card-content">
<table class="" id="users"></table>
</div>
</div>
</div>
</div>
</div>
{% endblock page_content %}
{% block scripts %}
{{ super() }}
<script src="https://unpkg.com/gridjs/dist/gridjs.umd.js"></script>
<script>
const updateUrl = (prev, query) => {
return prev + (prev.indexOf('?') >= 0 ? '&' : '?') + new URLSearchParams(query).toString();
};
new gridjs.Grid({
columns: [
{ id: 'username', name: 'Username' },
{ id: 'email', name: 'Email' },
],
server: {
url: '/users/api_users',
then: results => results.data,
total: results => results.total,
},
search: {
enabled: true,
server: {
url: (prev, search) => {
return updateUrl(prev, {search});
},
},
},
sort: {
enabled: true,
multiColumn: true,
server: {
url: (prev, columns) => {
const columnIds = ['username', 'email'];
const sort = columns.map(col => (col.direction === 1 ? '+' : '-') + columnIds[col.index]);
return updateUrl(prev, {sort});
},
},
},
pagination: {
enabled: true,
server: {
url: (prev, page, limit) => {
return updateUrl(prev, {offset: page * limit, limit: limit});
},
},
}
}).render(document.getElementById('users'));
</script>
{% endblock scripts %}

5
app/users/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('users', __name__)
from . import events, routes # noqa

32
app/users/events.py Normal file
View File

@ -0,0 +1,32 @@
from app import hashids, socketio
from app.decorators import socketio_login_required
from app.models import User
from flask_login import current_user
from flask_socketio import join_room, leave_room
@socketio.on('SUBSCRIBE /users/<user_id>')
@socketio_login_required
def subscribe_user(user_hashid):
user_id = hashids.decode(user_hashid)
user = User.query.get(user_id)
if user is None:
return {'code': 404, 'msg': 'Not found'}
if not (user == current_user or current_user.is_administrator):
return {'code': 403, 'msg': 'Forbidden'}
dict_user = user.to_dict(backrefs=True, relationships=True)
join_room(f'/users/{user.hashid}')
return {'code': 200, 'msg': 'OK', 'payload': dict_user}
@socketio.on('UNSUBSCRIBE /users/<user_id>')
@socketio_login_required
def subscribe_user(user_hashid):
user_id = hashids.decode(user_hashid)
user = User.query.get(user_id)
if user is None:
return {'code': 404, 'msg': 'Not found'}
if not (user == current_user or current_user.is_administrator):
return {'code': 403, 'msg': 'Forbidden'}
leave_room(f'/users/{user.hashid}')
return {'code': 200, 'msg': 'OK'}

50
app/users/routes.py Normal file
View File

@ -0,0 +1,50 @@
from app.models import User
from flask import render_template, request, url_for
from . import bp
@bp.route('/')
def users():
return render_template(
'users/users.html.j2',
title='Users'
)
@bp.route('/api_users')
def api_users():
query = User.query
# search filter
search = request.args.get('search')
if search:
query = query.filter(User.username.like(f'%{search}%') | User.email.like(f'%{search}%'))
total = query.count()
# sorting
sort = request.args.get('sort')
if sort:
order = []
for s in sort.split(','):
direction = s[0]
name = s[1:]
if name not in ['username', 'email']:
name = 'username'
col = getattr(User, name)
if direction == '-':
col = col.desc()
order.append(col)
if order:
query = query.order_by(*order)
# pagination
offset = request.args.get('offset', type=int, default=-1)
limit = request.args.get('limit', type=int, default=-1)
if offset != -1 and limit != -1:
query = query.offset(offset).limit(limit)
# response
return {
'data': [user.to_dict() for user in query],
'total': total
}