From 76924956dec46ef8619d8f4a61ba513e2b93ccba Mon Sep 17 00:00:00 2001
From: Patrick Jentsch <p.jentsch@uni-bielefeld.de>
Date: Mon, 4 Jul 2022 14:09:17 +0200
Subject: [PATCH] Change the user session SocketIO Logic

---
 app/main/__init__.py                          |  2 +-
 app/main/events.py                            | 41 -----------
 app/models.py                                 | 10 +--
 app/static/js/App.js                          | 46 ++++---------
 app/static/js/JobStatusNotifier.js            |  5 +-
 .../js/RessourceDisplays/CorpusDisplay.js     |  2 +-
 app/static/js/RessourceDisplays/JobDisplay.js |  2 +-
 .../js/RessourceDisplays/RessourceDisplay.js  |  6 +-
 .../js/RessourceLists/CorpusFileList.js       |  2 +-
 app/static/js/RessourceLists/CorpusList.js    |  2 +-
 app/static/js/RessourceLists/JobInputList.js  |  2 +-
 app/static/js/RessourceLists/JobList.js       |  2 +-
 app/static/js/RessourceLists/JobResultList.js |  2 +-
 .../js/RessourceLists/QueryResultList.js      |  2 +-
 app/static/js/RessourceLists/RessourceList.js | 10 +--
 app/templates/_scripts.html.j2                |  7 +-
 app/templates/users/users.html.j2             | 69 +++++++++++++++++++
 app/users/__init__.py                         |  5 ++
 app/users/events.py                           | 32 +++++++++
 app/users/routes.py                           | 50 ++++++++++++++
 20 files changed, 200 insertions(+), 99 deletions(-)
 delete mode 100644 app/main/events.py
 create mode 100644 app/templates/users/users.html.j2
 create mode 100644 app/users/__init__.py
 create mode 100644 app/users/events.py
 create mode 100644 app/users/routes.py

diff --git a/app/main/__init__.py b/app/main/__init__.py
index aa4f232e..65630224 100644
--- a/app/main/__init__.py
+++ b/app/main/__init__.py
@@ -2,4 +2,4 @@ from flask import Blueprint
 
 
 bp = Blueprint('main', __name__)
-from . import events, routes
+from . import routes
diff --git a/app/main/events.py b/app/main/events.py
deleted file mode 100644
index c727d171..00000000
--- a/app/main/events.py
+++ /dev/null
@@ -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)
-    }
diff --git a/app/models.py b/app/models.py
index 94b57453..ce3686c8 100644
--- a/app/models.py
+++ b/app/models.py
@@ -1033,6 +1033,8 @@ def ressource_after_delete(mapper, connection, ressource):
     jsonpatch = [{'op': 'remove', 'path': ressource.jsonpatch_path}]
     room = f'users.{ressource.user_hashid}'
     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')
@@ -1047,8 +1049,8 @@ def ressource_after_insert_handler(mapper, connection, ressource):
     jsonpatch = [
         {'op': 'add', 'path': ressource.jsonpatch_path, 'value': value}
     ]
-    room = f'users.{ressource.user_hashid}'
-    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_update')
@@ -1077,8 +1079,8 @@ def ressource_after_update_handler(mapper, connection, ressource):
             }
         )
     if jsonpatch:
-        room = f'users.{ressource.user_hashid}'
-        socketio.emit('users.patch', jsonpatch, room=room)
+        room = f'/users/{ressource.user_hashid}'
+        socketio.emit('PATCH', jsonpatch, room=room)
 
 
 @db.event.listens_for(Job, 'after_update')
diff --git a/app/static/js/App.js b/app/static/js/App.js
index 3761be8c..349eaedf 100644
--- a/app/static/js/App.js
+++ b/app/static/js/App.js
@@ -1,21 +1,30 @@
 class App {
   constructor() {
     this.data = {users: {}};
-    this.eventListeners = {'users.patch': []};
     this.promises = {users: {}};
     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() {
     return this.data.users;
   }
 
-  addEventListener(type, listener) {
-    if (!(type in this.eventListeners)) {
-      throw `Unknown event type: ${type}`;
+  subscribeUser(userId) {
+    if (userId in this.promises.users) {
+      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) {
@@ -50,29 +59,4 @@ class App {
     toastCloseActionElement = toast.el.querySelector('.toast-action[data-action="close"]');
     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);}
-  }
 }
diff --git a/app/static/js/JobStatusNotifier.js b/app/static/js/JobStatusNotifier.js
index bb0ca44d..57ca0135 100644
--- a/app/static/js/JobStatusNotifier.js
+++ b/app/static/js/JobStatusNotifier.js
@@ -1,16 +1,17 @@
 class JobStatusNotifier {
   constructor(userId) {
     this.userId = userId;
+    app.socket.on('PATCH', (patch) => {this.onPATCH(patch);});
   }
 
-  usersPatchHandler(patch) {
+  onPATCH(patch) {
     let filteredPatch;
     let jobId;
     let match;
     let operation;
     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
       .filter(operation => operation.op === 'replace')
       .filter(operation => re.test(operation.path));
diff --git a/app/static/js/RessourceDisplays/CorpusDisplay.js b/app/static/js/RessourceDisplays/CorpusDisplay.js
index 6920b3bc..39d30e89 100644
--- a/app/static/js/RessourceDisplays/CorpusDisplay.js
+++ b/app/static/js/RessourceDisplays/CorpusDisplay.js
@@ -16,7 +16,7 @@ class CorpusDisplay extends RessourceDisplay {
     this.setNumTokens(corpus.num_tokens);
   }
 
-  usersPatchHandler(patch) {
+  onPATCH(patch) {
     let filteredPatch;
     let operation;
     let re;
diff --git a/app/static/js/RessourceDisplays/JobDisplay.js b/app/static/js/RessourceDisplays/JobDisplay.js
index 1c252837..98c16913 100644
--- a/app/static/js/RessourceDisplays/JobDisplay.js
+++ b/app/static/js/RessourceDisplays/JobDisplay.js
@@ -18,7 +18,7 @@ class JobDisplay extends RessourceDisplay {
     this.setTitle(job.title);
   }
 
-  usersPatchHandler(patch) {
+  onPATCH(patch) {
     let filteredPatch;
     let operation;
     let re;
diff --git a/app/static/js/RessourceDisplays/RessourceDisplay.js b/app/static/js/RessourceDisplays/RessourceDisplay.js
index c8c16be1..d98a9635 100644
--- a/app/static/js/RessourceDisplays/RessourceDisplay.js
+++ b/app/static/js/RessourceDisplays/RessourceDisplay.js
@@ -2,13 +2,13 @@ class RessourceDisplay {
   constructor(displayElement) {
     this.displayElement = displayElement;
     this.userId = this.displayElement.dataset.userId;
-    app.addEventListener('users.patch', patch => this.usersPatchHandler(patch));
-    app.getUserById(this.userId).then(user => this.init(user));
+    app.socket.on('PATCH', (patch) => {this.onPATCH(patch);});
+    app.subscribeUser(this.userId).then((user) => {this.init(user);});
   }
 
   init(user) {throw 'Not implemented';}
 
-  usersPatchHandler(patch) {throw 'Not implemented';}
+  onPATCH(patch) {throw 'Not implemented';}
 
   setElement(element, value) {
     switch (element.tagName) {
diff --git a/app/static/js/RessourceLists/CorpusFileList.js b/app/static/js/RessourceLists/CorpusFileList.js
index a4d76dad..b61d624b 100644
--- a/app/static/js/RessourceLists/CorpusFileList.js
+++ b/app/static/js/RessourceLists/CorpusFileList.js
@@ -96,7 +96,7 @@ class CorpusFileList extends RessourceList {
     }
   }
 
-  usersPatchHandler(patch) {
+  onPATCH(patch) {
     let corpusFileId;
     let filteredPatch;
     let match;
diff --git a/app/static/js/RessourceLists/CorpusList.js b/app/static/js/RessourceLists/CorpusList.js
index 068f446a..d6b75bec 100644
--- a/app/static/js/RessourceLists/CorpusList.js
+++ b/app/static/js/RessourceLists/CorpusList.js
@@ -88,7 +88,7 @@ class CorpusList extends RessourceList {
     }
   }
 
-  usersPatchHandler(patch) {
+  onPATCH(patch) {
     let corpusId;
     let filteredPatch;
     let match;
diff --git a/app/static/js/RessourceLists/JobInputList.js b/app/static/js/RessourceLists/JobInputList.js
index c1f31312..d86ff8ca 100644
--- a/app/static/js/RessourceLists/JobInputList.js
+++ b/app/static/js/RessourceLists/JobInputList.js
@@ -54,5 +54,5 @@ class JobInputList extends RessourceList {
     }
   }
 
-  usersPatchHandler(patch) {return;}
+  onPATCH(patch) {return;}
 }
diff --git a/app/static/js/RessourceLists/JobList.js b/app/static/js/RessourceLists/JobList.js
index a487c557..ef027b0e 100644
--- a/app/static/js/RessourceLists/JobList.js
+++ b/app/static/js/RessourceLists/JobList.js
@@ -94,7 +94,7 @@ class JobList extends RessourceList {
     }
   }
 
-  usersPatchHandler(patch) {
+  onPATCH(patch) {
     let filteredPatch;
     let jobId;
     let match;
diff --git a/app/static/js/RessourceLists/JobResultList.js b/app/static/js/RessourceLists/JobResultList.js
index 5f9da3b5..85c3c865 100644
--- a/app/static/js/RessourceLists/JobResultList.js
+++ b/app/static/js/RessourceLists/JobResultList.js
@@ -57,7 +57,7 @@ class JobResultList extends RessourceList {
     }
   }
 
-  usersPatchHandler(patch) {
+  onPATCH(patch) {
     let filteredPatch;
     let operation;
     let re;
diff --git a/app/static/js/RessourceLists/QueryResultList.js b/app/static/js/RessourceLists/QueryResultList.js
index c78ea3cb..8d0c1329 100644
--- a/app/static/js/RessourceLists/QueryResultList.js
+++ b/app/static/js/RessourceLists/QueryResultList.js
@@ -89,7 +89,7 @@ class QueryResultList extends RessourceList {
     }
   }
 
-  usersPatchHandler(patch) {
+  onPATCH(patch) {
     let filteredPatch;
     let match;
     let operation;
diff --git a/app/static/js/RessourceLists/RessourceList.js b/app/static/js/RessourceLists/RessourceList.js
index ca23d820..b36b4d92 100644
--- a/app/static/js/RessourceLists/RessourceList.js
+++ b/app/static/js/RessourceLists/RessourceList.js
@@ -91,10 +91,10 @@ class RessourceList {
     this.userId = this.listjs.listContainer.dataset.userId;
     this.listjs.list.addEventListener('click', event => this.onclick(event));
     if (this.userId) {
-      app.addEventListener('users.patch', patch => this.usersPatchHandler(patch));
-      app.getUserById(this.userId).then(
-        user => this.init(user),
-        error => {throw JSON.stringify(error);}
+      app.socket.on('PATCH', (patch) => {this.onPATCH(patch);});
+      app.subscribeUser(this.userId).then(
+        (user) => {this.init(user);},
+        (error) => {throw JSON.stringify(error);}
       );
     }
   }
@@ -117,7 +117,7 @@ class RessourceList {
 
   onclick(event) {throw 'Not implemented';}
 
-  usersPatchHandler(patch) {throw 'Not implemented';}
+  onPATCH(patch) {throw 'Not implemented';}
 
   add(ressources) {
     let values = Array.isArray(ressources) ? ressources : [ressources];
diff --git a/app/templates/_scripts.html.j2 b/app/templates/_scripts.html.j2
index 89a672dc..9a98ee9b 100644
--- a/app/templates/_scripts.html.j2
+++ b/app/templates/_scripts.html.j2
@@ -32,11 +32,10 @@
   const jobStatusNotifier = new JobStatusNotifier(currentUserId);
 
   // Initialize components for current user
-  app.addEventListener('users.patch', patch => jobStatusNotifier.usersPatchHandler(patch));
-  app.getUserById(currentUserId)
+  app.subscribeUser(currentUserId)
     .then(
-      user => {return;},
-      error => {throw JSON.stringify(error);}
+      (user) => {return;},
+      (error) => {throw JSON.stringify(error);}
     );
   {%- endif %}
 
diff --git a/app/templates/users/users.html.j2 b/app/templates/users/users.html.j2
new file mode 100644
index 00000000..ff9b27ca
--- /dev/null
+++ b/app/templates/users/users.html.j2
@@ -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 %}
diff --git a/app/users/__init__.py b/app/users/__init__.py
new file mode 100644
index 00000000..878fd913
--- /dev/null
+++ b/app/users/__init__.py
@@ -0,0 +1,5 @@
+from flask import Blueprint
+
+
+bp = Blueprint('users', __name__)
+from . import events, routes  # noqa
diff --git a/app/users/events.py b/app/users/events.py
new file mode 100644
index 00000000..1bda2b7b
--- /dev/null
+++ b/app/users/events.py
@@ -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'}
diff --git a/app/users/routes.py b/app/users/routes.py
new file mode 100644
index 00000000..6813aa96
--- /dev/null
+++ b/app/users/routes.py
@@ -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
+    }
-- 
GitLab