first change with new MVCish pattern

This commit is contained in:
Stephan Porada 2020-08-21 16:33:47 +02:00
parent dabeb818d1
commit 0507ae4e34
8 changed files with 308 additions and 154 deletions

View File

@ -30,7 +30,7 @@ def init_corpus_analysis(corpus_id):
corpus_id, current_user.id, request.sid) corpus_id, current_user.id, request.sid)
@socketio.on('corpus_analysis_get_meta_data') @socketio.on('corpus_analysis_meta_data')
@socketio_login_required @socketio_login_required
def corpus_analysis_get_meta_data(corpus_id): def corpus_analysis_get_meta_data(corpus_id):
# get meta data from db # get meta data from db
@ -69,7 +69,7 @@ def corpus_analysis_get_meta_data(corpus_id):
payload = metadata payload = metadata
response = {'code': 200, 'desc': 'Corpus meta data', 'msg': 'OK', response = {'code': 200, 'desc': 'Corpus meta data', 'msg': 'OK',
'payload': payload} 'payload': payload}
socketio.emit('corpus_analysis_send_meta_data', response, room=request.sid) socketio.emit('corpus_analysis_meta_data', response, room=request.sid)
@socketio.on('corpus_analysis_query') @socketio.on('corpus_analysis_query')

View File

@ -1,10 +1,10 @@
/** /**
* This class is used to create a Client object. * This class is used to create a Client object.
* The client handels the client server communication. * The client handels the client server communication.
* It requests data (e.g. the analysis session or query results) from the * It communicates with the server (e.g. connection or query)
* the server and recieves them, if it dynamicMode is true. * and recieves data from it, if dynamicMode is true.
* If dynamicMode is false, the client can also handle data that is already * If dynamicMode is false, the client can also handle data that is already
* loaded and not coming in in chunks. * loaded and is not coming in in chunks.
*/ */
class Client { class Client {
constructor({corpusId = null, constructor({corpusId = null,
@ -12,16 +12,18 @@ class Client {
logging = true, logging = true,
dynamicMode = true} = {}) { dynamicMode = true} = {}) {
this.corpusId = corpusId; this.corpusId = corpusId;
this.displays = {};
this.dynamicMode = dynamicMode; this.dynamicMode = dynamicMode;
this.logging = logging; this.logging = logging;
this.requestQueryProgress = 0; this.requestQueryProgress = 0;
this.results = undefined; // holds the results object later on
this.socket = socket; this.socket = socket;
this.socketEventListeners = {}; this.socketEventListeners = {};
this.recivedMetaData = false;
/** /**
* Disable all console logging. * Disable all console logging.
* This is global. So every other log message in every other Class or
* function used in conjunction with the client either logs or does not
* log depending on the logging flag.
* Credits to https://gist.github.com/kmonsoor/0244fdb4ad79a4826371e58a1a5fa984 * Credits to https://gist.github.com/kmonsoor/0244fdb4ad79a4826371e58a1a5fa984
*/ */
if (!logging) { if (!logging) {
@ -37,6 +39,7 @@ class Client {
}); });
})(); })();
} }
console.info("Client initialized:", this);
} }
// Registers one or more SocketEventListeners to the Client. // Registers one or more SocketEventListeners to the Client.
@ -56,10 +59,10 @@ class Client {
} }
} }
/**
// TODO: get rid of this disply stuff and send commands to the viewer * This functions sends events to the View to trigger specific functions that
// on what to do/show/hide etc * are handleing the represnetation of data stored in the model.
*/
notifyView(SendWhatToDo) { notifyView(SendWhatToDo) {
} }
@ -108,6 +111,12 @@ class Client {
this.socket.emit('corpus_analysis_init', this.corpusId); this.socket.emit('corpus_analysis_init', this.corpusId);
} }
getMetaData() {
console.info('corpus_analysis_meta_data: Client getting meta data via',
'socket.emit.');
this.socket.emit('corpus_analysis_meta_data', this.corpusId);
}
/** /**
* Emits query to the server via socket.io. Server will send the results * Emits query to the server via socket.io. Server will send the results
* back. * back.
@ -119,80 +128,6 @@ class Client {
} }
} }
/**
* This class is used to create an CorpusAnalysisDisplay object.
* Input is one HTMLElement that can then be hidden or shown depending on
* its CSS classes.
*/
class CorpusAnalysisDisplay {
constructor(element) {
// with this function initalized modals can also be handeld
this.element = (() => {if (element instanceof HTMLElement) {
return element;
} else {
element = element['$el'][0];
return element;
}
})();
this.errorContainer = element.querySelector('.error-container');
this.showOnError = element.querySelectorAll('.show-on-error');
this.showOnSuccess = element.querySelectorAll('.show-on-success');
this.showWhileWaiting = element.querySelectorAll('.show-while-waiting');
this.hideOnComplete = element.querySelectorAll('.hide-on-complete')
}
// Changes the visibility of its own
setVisibilityByStatus(status) {
switch (status) {
case 'error':
for (let element of this.showOnError) {
element.classList.remove('hide');
}
for (let element of this.showOnSuccess) {
element.classList.add('hide');
}
for (let element of this.showWhileWaiting) {
element.classList.add('hide');
}
break;
case 'success':
for (let element of this.showOnError) {
element.classList.add('hide');
}
for (let element of this.showOnSuccess) {
element.classList.remove('hide');
}
for (let element of this.showWhileWaiting) {
element.classList.add('hide');
}
break;
case 'waiting':
for (let element of this.showOnError) {
element.classList.add('hide');
}
for (let element of this.showOnSuccess) {
element.classList.add('hide');
}
for (let element of this.showWhileWaiting) {
element.classList.remove('hide');
}
break;
default:
// Hide all
for (let element of this.showOnError) {
element.classList.add('hide');
}
for (let element of this.showOnSuccess) {
element.classList.add('hide');
}
for (let element of this.showWhileWaiting) {
element.classList.add('hide');
}
}
}
}
/** /**
* This class is used to create an SocketEventListener. * This class is used to create an SocketEventListener.
* Input are an identifying type string, the listener function and callbacks * Input are an identifying type string, the listener function and callbacks
@ -200,7 +135,7 @@ class CorpusAnalysisDisplay {
* type string is also used as the socket event event identifier. * type string is also used as the socket event event identifier.
*/ */
class SocketEventListener { class SocketEventListener {
constructor(type, listenerFunction) { constructor(type, listenerFunction, args=null) {
this.listenerCallbacks = {}; this.listenerCallbacks = {};
this.listenerFunction = listenerFunction; this.listenerFunction = listenerFunction;
this.type = type; this.type = type;
@ -222,9 +157,10 @@ class SocketEventListener {
* yield the keys in order of insertion. * yield the keys in order of insertion.
* So all modern Browsers. * So all modern Browsers.
*/ */
executeCallbacks(args) { executeCallbacks(payload) {
for (let [type, listenerCallback] of Object.entries(this.listenerCallbacks)) { for (let [type, listenerCallback] of Object.entries(this.listenerCallbacks)) {
listenerCallback.callbackFunction(...args); listenerCallback.args.unshift(payload);
listenerCallback.callbackFunction(...listenerCallback.args);
} }
} }
} }
@ -235,7 +171,8 @@ class SocketEventListener {
* callback functions. * callback functions.
*/ */
class ListenerCallback { class ListenerCallback {
constructor(type, callbackFunction) { constructor(type, callbackFunction, argsList) {
this.args = argsList;
this.callbackFunction = callbackFunction; this.callbackFunction = callbackFunction;
this.type = type; this.type = type;
} }
@ -245,6 +182,5 @@ class ListenerCallback {
export { export {
Client, Client,
SocketEventListener, SocketEventListener,
CorpusAnalysisDisplay,
ListenerCallback, ListenerCallback,
}; };

View File

@ -1,9 +1,35 @@
/**
* This callback is called on a socket.on "corpus_analysis_send_meta_data".
* Handels incoming corpus metadata
*/
function saveMetaData() {
let [payload, client, results, rest] = arguments;
results.metaData.init(payload)
client.recivedMetaData = true;
console.info('Metada saved:', results.metaData);
}
/** /**
* This callback should be registered to the SocketEventListener 'recieveQueryStatus'. * This callback should be registered to the SocketEventListener 'recieveQueryStatus'.
* It just gets the incoming status data of the issued query * It just gets the incoming status data of the issued query
* and does some preperation work like hiding or showing elements and deleting * and does some preperation work like hiding or showing elements and deleting
* the data from the last query. * the data from the last query.
*/ */
function prepareQueryData() {
// deletes old data from query issued before this new query
let [payload, client, results, rest] = arguments;
results.init();
client.requestQueryProgress = 0;
}
function saveQueryData(args) {
let [payload, client, results, rest] = arguments;
results.data.addData(payload);
client.requestQueryProgress = payload.progress;
console.info('Query data chunk saved', results.data);
}
function querySetup(payload, client) { function querySetup(payload, client) {
// deletes old data from query issued before this new query // deletes old data from query issued before this new query
client.results.clearAll(); client.results.clearAll();
@ -12,7 +38,6 @@ function querySetup(payload, client) {
'#recieved-match-count', '#total-match-count', '#recieved-match-count', '#total-match-count',
'#text-lookup-count', '#text-lookup-titles', '#text-lookup-count', '#text-lookup-titles',
'#query-results-create', '#add-to-sub-results']); '#query-results-create', '#add-to-sub-results']);
client.requestQueryProgress = 0;
client.recievedMatchCount.textContent = 0; client.recievedMatchCount.textContent = 0;
client.totalMatchCount.textContent = `${payload.match_count}`; client.totalMatchCount.textContent = `${payload.match_count}`;
client.queryResultsUserFeedback.classList.toggle('hide'); client.queryResultsUserFeedback.classList.toggle('hide');
@ -117,4 +142,4 @@ function helperQueryRenderResults (payload, client) {
} }
// export callbacks // export callbacks
export { querySetup, queryRenderResults }; export { prepareQueryData, saveMetaData, saveQueryData };

View File

@ -1,40 +1,57 @@
import { querySetup, queryRenderResults } from './nopaque.listenerCallbacks.js'
/** /**
* Recieves a corpus analysis session via socket.io. * Recieves a corpus analysis connected signal via socket.io.
* Closes the loading modal that has been opend with requestSession at the
* start of the request.
*/ */
function recieveSession(type, client) { function recieveConnected(type, client) {
client.socket.on(type, (response) => { client.socket.on(type, (response) => {
/** /**
* Check if request for session was OK. * Check if request for session was OK.
* If OK execute callbacks and hide/show displays. * If OK execute registered callbacks and notify View.
*/ */
console.group('recieve session')
if (response.code === 200) { if (response.code === 200) {
console.info('corpus_analysis_init: Client recieving session/or error', console.group('Connected!')
console.info('corpus_analysis_init: Client recieving connected codes',
'codes via socket.on'); 'codes via socket.on');
console.info(`corpus_analysis_init: ${response.code} - ${response.msg}`); console.info(`corpus_analysis_init: ${response.code} - ${response.msg}`);
console.info('corpus_analysis_init: Initialization succeeded'); console.info('corpus_analysis_init: Initialization succeeded');
console.info(response); console.info(response);
// Handling hide/show of displays console.groupEnd();
if (client.displays.init != undefined) { // get meta data immediately
client.displays.init.element.M_Modal.close(); client.getMetaData();
client.displays.init.setVisibilityByStatus('success');
}
} else { } else {
let errorText = `Error ${response.code} - ${response.msg}`; let errorText = `Error ${response.code} - ${response.msg}`;
if (client.displays.init.errorContainer != undefined) { console.group('Connection failed!')
client.displays.init.errorContainer.innerHTML = `<p class="red-text">`
+ `<i class="material-icons tiny">error</i>${errorText}</p>`;
}
if (client.displays.init != undefined) {
client.displays.init.setVisibilityByStatus('error');
}
console.error(`corpus_analysis_init: ${errorText}`); console.error(`corpus_analysis_init: ${errorText}`);
}
console.groupEnd(); console.groupEnd();
}
});
}
/**
* Recieves meta data from the server via socket.io.
*/
function recieveMetaData(type, client) {
client.socket.on(type, (response) => {
/**
* Check if request for session was OK.
* If OK execute registered callbacks and notify View.
*/
if (response.code === 200) {
console.group('Client recieving meta data')
console.info('corpus_analysis_meta_data: Client recieving meta data',
'via socket.on');
console.info(`corpus_analysis_meta_data: ${response.code} - ${response.msg}`);
console.info(response);
// executing the registered callbacks
client.socketEventListeners[type].executeCallbacks(response.payload);
console.groupEnd();
} else {
console.group('Failed to recieve meta data.');
console.error('corpus_analysis_meta_data: Client failed to recieve',
'meta data via socket.on');
let errorText = `Error ${response.payload.code} - ${response.payload.msg}`;
console.error(`corpus_analysis_meta_data: ${errorText}`);
console.groupEnd();
}
}); });
} }
@ -47,36 +64,27 @@ function recieveSession(type, client) {
function recieveQueryStatus(type, client) { function recieveQueryStatus(type, client) {
client.socket.on(type, (response) => { client.socket.on(type, (response) => {
/** /**
* Check if issued query was OK. * Check if request for session was OK.
* If OK execute registered callbacks and hide/show displays. * If OK execute registered callbacks and notify View.
*/ */
if (response.code === 200) {
console.group('corpus_analysis_query: Client recieving query process', console.group('corpus_analysis_query: Client recieving query process',
'status via socket.on'); 'status via socket.on');
if (response.code === 200) {
console.info(`corpus_analysis_query: ${response.code} - ${response.msg}`); console.info(`corpus_analysis_query: ${response.code} - ${response.msg}`);
console.info(response); console.info(response);
// Handling hide/show of displays
if (client.displays.query != undefined) {
client.displays.query.setVisibilityByStatus("success");
}
// executing the registered callbacks // executing the registered callbacks
client.socketEventListeners[type].executeCallbacks([response.payload, client]); client.socketEventListeners[type].executeCallbacks(response.payload);
console.groupEnd();
} else { } else {
console.group('corpus_analysis_query: Client failed recieving',
'query process status via socket.on');
let errorText = `Error ${response.payload.code} - ${response.payload.msg}`; let errorText = `Error ${response.payload.code} - ${response.payload.msg}`;
if (response.payload.code == 1281) { if (response.payload.code == 1281) {
errorText += ' - Invalid Query'; errorText += ' - Invalid Query';
} }
nopaque.flash(errorText, "error");
if (client.displays.query.errorContainer != undefined) {
client.displays.query.errorContainer.innerHTML = `<p class="red-text">`+
`<i class="material-icons tiny">error</i> ${errorText}</p>`;
}
if (client.displays.query != undefined) {
client.displays.query.setVisibilityByStatus("error");
}
console.error(`corpus_analysis_query: ${errorText}`); console.error(`corpus_analysis_query: ${errorText}`);
}
console.groupEnd(); console.groupEnd();
}
}); });
} }
@ -84,24 +92,43 @@ function recieveQueryStatus(type, client) {
* Recieves the query data from the request and handles it. * Recieves the query data from the request and handles it.
*/ */
function recieveQueryData(type, client) { function recieveQueryData(type, client) {
console.group('corpus_analysis_query_results: Client recieving or loading', /**
'query data.'); * Check if request for session was OK.
* If OK execute registered callbacks and notify View.
*/
if (client.dynamicMode) { if (client.dynamicMode) {
console.info('Client recieving query data via socket.on');
client.socket.on(type, (response) => { client.socket.on(type, (response) => {
if (response.code === 200) {
console.group('corpus_analysis_query_results: Recieveing query data')
console.info('Client recieving query data via socket.on');
console.info('Recieved chunk', response); console.info('Recieved chunk', response);
// executing the registered callbacks /**
client.socketEventListeners[type].executeCallbacks([response.payload, client]); * Execute registered callbacks and notify View.
console.info('Added chunk data to results.data and rendered it with', */
'results.jsList'); client.socketEventListeners[type].executeCallbacks(response.payload);
console.info('Added chunk data to results.data.');
console.groupEnd();
} else {
console.group('corpus_analysis_query_results: Client failed recieving',
'the results via socket.on');
let errorText = `Error ${response.payload.code} - ${response.payload.msg}`;
console.error(`corpus_analysis_query: ${errorText}`);
console.groupEnd();
}
}); });
} else { } else {
console.group('corpus_analysis_query_results: Loading query data.')
console.info('Client loading imported query data from database.'); console.info('Client loading imported query data from database.');
// executing the registered callbacks // executing the registered callbacks
client.socketEventListeners[type].executeCallbacks([client.results.data, client]); client.socketEventListeners[type].executeCallbacks();
}
console.groupEnd(); console.groupEnd();
} }
}
// export listeners from this module // export listeners from this module
export { recieveSession, recieveQueryStatus, recieveQueryData }; export {
recieveConnected,
recieveMetaData,
recieveQueryStatus,
recieveQueryData
};

View File

@ -5,17 +5,15 @@
*/ */
class Results { class Results {
constructor(data, jsList , metaData) { constructor() {
this.data = data; this.data = new Data();
this.jsList = jsList; this.metaData = new MetaData();
this.metaData = metaData
this.resultsData = new Data(); this.resultsData = new Data();
this.subResultsData = new Data(); this.subResultsData = new Data();
console.info('Initialized the Results object.');
} }
clearAll() { init() {
this.jsList.clear();
this.jsList.update();
this.data.init(); this.data.init();
this.metaData.init(); this.metaData.init();
this.resultsData.init() this.resultsData.init()

View File

@ -0,0 +1,73 @@
/**
* This class is used to create an Display object.
* Input is one HTMLElement that can then be hidden or shown depending on
* its CSS classes.
*/
class Display {
constructor(element) {
// with this function initalized modals can also be handeld
this.element = (() => {if (element instanceof HTMLElement) {
return element;
} else {
element = element['$el'][0];
return element;
}
})();
this.errorContainer = element.querySelector('.error-container');
this.showOnError = element.querySelectorAll('.show-on-error');
this.showOnSuccess = element.querySelectorAll('.show-on-success');
this.showWhileWaiting = element.querySelectorAll('.show-while-waiting');
this.hideOnComplete = element.querySelectorAll('.hide-on-complete')
}
// Changes the visibility of its own
setVisibilityByStatus(status) {
switch (status) {
case 'error':
for (let element of this.showOnError) {
element.classList.remove('hide');
}
for (let element of this.showOnSuccess) {
element.classList.add('hide');
}
for (let element of this.showWhileWaiting) {
element.classList.add('hide');
}
break;
case 'success':
for (let element of this.showOnError) {
element.classList.add('hide');
}
for (let element of this.showOnSuccess) {
element.classList.remove('hide');
}
for (let element of this.showWhileWaiting) {
element.classList.add('hide');
}
break;
case 'waiting':
for (let element of this.showOnError) {
element.classList.add('hide');
}
for (let element of this.showOnSuccess) {
element.classList.add('hide');
}
for (let element of this.showWhileWaiting) {
element.classList.remove('hide');
}
break;
default:
// Hide all
for (let element of this.showOnError) {
element.classList.add('hide');
}
for (let element of this.showOnSuccess) {
element.classList.add('hide');
}
for (let element of this.showWhileWaiting) {
element.classList.add('hide');
}
}
}
}

View File

@ -65,7 +65,102 @@
{% include 'modals/export_query_results.html.j2' %} {% include 'modals/export_query_results.html.j2' %}
{% include 'modals/context_modal.html.j2' %} {% include 'modals/context_modal.html.j2' %}
<script type="text/javascript" <!-- import modules -->
src="web/app/static/js/modules/corpus_analysis/main.js"> <script type="module">
/**
* First Phase:
* Document content is loaded and scripts are being imported and executed.
*/
import {
Client,
SocketEventListener,
ListenerCallback,
} from '../../static/js/modules/corpus_analysis/client/Client.js';
import {
recieveConnected,
recieveMetaData,
recieveQueryStatus,
recieveQueryData,
} from '../../static/js/modules/corpus_analysis/client/listeners.js';
import {
Results,
} from '../../static/js/modules/corpus_analysis/model/Results.js';
import {
prepareQueryData,
saveQueryData,
saveMetaData,
} from '../../static/js/modules/corpus_analysis/client/callbacks.js';
/**
* Second Phase:
* Asynchronus and event driven code
*/
document.addEventListener("DOMContentLoaded", () => {
// Initialize the client for server client communication in dynamic mode
let corpusId = {{ corpus_id }}
const client = new Client({'corpusId': corpusId,
'socket': nopaque.socket,
'logging': true,
'dynamicMode': true});
/**
* Initializing the results object as a model holding all the data of a query.
* Also holds the metadata of one query.
*/
let results = new Results();
/**
* Register listeners listening to socket.io events and their callbacks
* Afterwards load them.
*/
const listenForConnected = new SocketEventListener('corpus_analysis_init',
recieveConnected);
const listenForMetaData = new SocketEventListener('corpus_analysis_meta_data',
recieveMetaData);
const metaDataCallback = new ListenerCallback('corpus_analysis_meta_data',
saveMetaData,
[client, results]);
listenForMetaData.setCallbacks([metaDataCallback]);
const listenForQueryStatus = new SocketEventListener('corpus_analysis_query',
recieveQueryStatus);
const queryStatusCallback = new ListenerCallback('corpus_analysis_query',
prepareQueryData,
[client, results]);
listenForQueryStatus.setCallbacks([queryStatusCallback]);
const listenForQueryData = new SocketEventListener('corpus_analysis_query_results',
recieveQueryData);
const queryDataCallback = new ListenerCallback('corpus_analysis_query_results',
saveQueryData,
[client, results]);
listenForQueryData.setCallbacks([queryDataCallback]);
client.setSocketEventListeners([listenForConnected,
listenForQueryStatus,
listenForQueryData,
listenForMetaData]);
client.loadSocketEventListeners();
// Connect client to server
client.connect();
// Send a query and recieve its answer data
let queryFormElement = document.getElementById('query-form');
queryFormElement.addEventListener('submit', (event) => {
try {
/**
* Selects first page of result list if pagination is already available
* from an query submitted before.
* This avoids confusion for the user e.g.: The user was on page 24
* reviewing the results and issues a new query. He would not see any
* results until the new results reach page 24 or he clicks on another
* valid result page element from the new pagination.
*/
let firstPageElement = document.querySelector('a.page');
firstPageElement.click();
} catch (e) {
// No page element is present if first query is submitted.
}
// Prevent page from reloading on submit
event.preventDefault();
// Get query string and send query to server
results.data.getQueryStr(queryFormElement);
client.query(results.data.query);
});
});
</script> </script>
{% endblock %} {% endblock %}