diff --git a/app/main/events.py b/app/main/events.py index e516a00e7c0b7315b594e9e565d534b014a6b82d..c734799e05a70276559a9cf02ad428952c161a69 100644 --- a/app/main/events.py +++ b/app/main/events.py @@ -1,9 +1,11 @@ from flask import current_app, request from flask_login import current_user, login_required +from ..decorators import admin_required from .. import db, socketio from ..models import User import json import jsonpatch +import logging ''' @@ -28,6 +30,26 @@ def connect(): request.sid) +@socketio.on('connect_admin') +@login_required +@admin_required +def connect_admin(selected_user_id): + ''' + ' The Socket.IO module creates a session id (sid) on each request. The + ' initiating admin is automatically placed in a room with that sid, which + ' will be used for further information exchange generated by a background + ' task associated with the sid. Admin will be placed in that room on emiting + ' "conncect_admin". + ''' + logger = logging.getLogger(__name__) + logger.warning('Admin emitted "connect_admin".') + logger.warning('Selected user id is: {}'.format(selected_user_id)) + socketio.start_background_task(background_task_foreign, + current_app._get_current_object(), + selected_user_id, + request.sid) + + @socketio.on('disconnect') @login_required def disconnect(): @@ -51,9 +73,13 @@ def background_task(app, user_id, session_id): ''' with app.app_context(): user = db.session.query(User).filter_by(id=user_id).first() + logging.getLogger(__name__) + logging.warning('User object is: {}'.format(user)) ''' Get current values from the database. ''' corpora = user.corpora_as_dict() + logging.warning('Corpora are: {}'.format(corpora)) jobs = user.jobs_as_dict() + logging.warning('Jobs are: {}'.format(jobs)) ''' Send initial values. ''' socketio.emit('init-corpora', json.dumps(corpora), @@ -83,3 +109,54 @@ def background_task(app, user_id, session_id): jobs = new_jobs socketio.sleep(3) disconnected.remove(session_id) + + +def background_task_foreign(app, user_id, session_id): + ''' + ' Sends initial corpus and job lists to the client. Afterwards it checks + ' every 3 seconds if changes to the initial values appeared. If changes are + ' detected, a RFC 6902 compliant JSON patch gets send. + ' + ' NOTE: The initial values are send as a init-* events. + ' The JSON patches are send as update-* events. + ' + ' > where '*' is either 'corpora' or 'jobs' + ''' + with app.app_context(): + user = db.session.query(User).filter_by(id=user_id).first() + logging.getLogger(__name__) + logging.warning('User object is: {}'.format(user)) + ''' Get current values from the database. ''' + corpora = user.corpora_as_dict() + logging.warning('Corpora are: {}'.format(corpora)) + jobs = user.jobs_as_dict() + logging.warning('Jobs are: {}'.format(jobs)) + ''' Send initial values. ''' + socketio.emit('init-foreign-corpora', + json.dumps(corpora), + room=session_id) + socketio.emit('init-foreign-jobs', + json.dumps(jobs), + room=session_id) + ''' TODO: Implement maximum runtime for this loop. ''' + while session_id not in disconnected: + ''' Get current values from the database ''' + new_corpora = user.corpora_as_dict() + new_jobs = user.jobs_as_dict() + ''' Compute JSON patches. ''' + corpus_patch = jsonpatch.JsonPatch.from_diff(corpora, new_corpora) + jobs_patch = jsonpatch.JsonPatch.from_diff(jobs, new_jobs) + ''' In case there are patches, send them to the user. ''' + if corpus_patch: + socketio.emit('update-foreign-corpora', + corpus_patch.to_string(), + room=session_id) + if jobs_patch: + socketio.emit('update-foreign-jobs', + jobs_patch.to_string(), + room=session_id) + ''' Set new values as references for the next iteration. ''' + corpora = new_corpora + jobs = new_jobs + socketio.sleep(3) + disconnected.remove(session_id) diff --git a/app/templates/admin/admin_user_page.html.j2 b/app/templates/admin/admin_user_page.html.j2 index 8a2204f7e881089581e6c2802899bba33fbf5fa0..8b13a6d4d5da2442f6a359d04cc03820cd7a8c77 100644 --- a/app/templates/admin/admin_user_page.html.j2 +++ b/app/templates/admin/admin_user_page.html.j2 @@ -21,50 +21,41 @@ </div> </div> </div> +<script type="text/javascript"> + var selected_user_id = {{selected_user.id|tojson|safe}} + socket.emit('connect_admin', selected_user_id); +</script> <div class="col s12 m6"> - <div class="card large"> - <div class="card-content"> - <span class="card-title">User Jobs</span> - <div id="users"> - <div class="input-field"> - <i class="material-icons prefix">search</i> - <input id="search-corpus" class="search" type="text"></input> - <label for="search-corpus">Search jobs</label> - </div> - <div class="collection list"> - {% for job in selected_user.jobs.all() %} - {% if job.service == 'nlp' %} - {% set service_color = 'blue' %} - {% set service_icon = 'format_textdirection_l_to_r' %} - {% elif job.service =='ocr' %} - {% set service_color = 'green' %} - {% set service_icon = 'find_in_page' %} - {% else %} - {% set service_color = 'red' %} - {% set service_icon = 'help' %} - {% endif %} - {% if job.status == 'pending' %} - {% set badge_color = 'amber' %} - {% elif job.status =='running' %} - {% set badge_color = 'indigo' %} - {% elif job.status =='complete' %} - {% set badge_color = 'teal' %} - {% else %} - {% set badge_color = 'red' %} - {% endif %} - <a href="{{ url_for('main.job', job_id=job.id) }}" class="collection-item avatar"> - <i class="material-icons circle {{ service_color }}">{{ service_icon }}</i> - <span class="new badge {{ badge_color }}" data-badge-caption="">{{ job.status }}</span> - <span class="title">{{ job.title }}</span> - <p>{{ job.description }}</p> - </a> - {% endfor %} + <div id="job-list"> + <div class="card"> + <div class="card-content"> + <div class="row"> + <div class="col s12"> + <div class="input-field"> + <i class="material-icons prefix">search</i> + <input id="search-job" class="search" type="text"></input> + <label for="search-job">Search job</label> + </div> + </div> + <div class="col s12"> + <ul class="pagination"></ul> + </div> </div> - <ul class="pagination"></ul> </div> </div> + <div class="collection list"></div> </div> </div> +<script> + var jobList = new JobList("job-list", { + item: '<div><span class="title"></span><span class="description"></span></div>', + page: 4, + pagination: true, + valueNames: ["description", "title", {data: ["id"]}] + }); + jobList.on("filterComplete", List.updatePagination); + jobList.on("searchComplete", List.updatePagination); +</script> <div class="col s12"> <div class="card large"> <div class="card-content"> diff --git a/app/templates/base.html.j2 b/app/templates/base.html.j2 index c6bac81a217fd99fec758413e1fb1554fa1b1e4e..8488c8dfcf03a70abfbf18d472bffcca2cd6741b 100644 --- a/app/templates/base.html.j2 +++ b/app/templates/base.html.j2 @@ -26,6 +26,7 @@ var jobsSubscribers = []; var socket = io(); + socket.on('init-corpora', function(msg) { var subscriber; @@ -64,6 +65,46 @@ console.log(msg); }); </script> + <script> + var foreignCorpora; + var foreignCorporaSubscribers = []; + var foreignJobs; + var foreignJobsSubscribers = []; + + + socket.on('init-foreign-corpora', function(msg) { + var subscriber; + + foreignCorpora = JSON.parse(msg); + for (subscriber of foreignCorporaSubscribers) {subscriber._init();} + }); + + + socket.on('init-foreign-jobs', function(msg) { + var subscriber; + + foreignJobs = JSON.parse(msg); + for (subscriber of foreignJobsSubscribers) {subscriber._init();} + }); + + + socket.on('update-foreign-corpora', function(msg) { + var patch, patchedCorpora, subscriber; + + patch = JSON.parse(msg); + foreignCorpora = jsonpatch.apply_patch(foreignCorpora, patch); + for (subscriber of foreignCorporaSubscribers) {subscriber._update(patch);} + }); + + + socket.on('update-foreign-jobs', function(msg) { + var patch, patchedJobs, subscriber; + + patch = JSON.parse(msg); + foreignJobs = jsonpatch.apply_patch(foreignJobs, patch); + for (subscriber of foreignJobsSubscribers) {subscriber._update(patch);} + }); + </script> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> </head> <body>