Progress on list rework

This commit is contained in:
Patrick Jentsch 2020-12-07 16:10:40 +01:00
parent 1883a9bc63
commit 1003c4494d
6 changed files with 659 additions and 453 deletions

View File

@ -283,7 +283,7 @@ class JobInput(db.Model):
@property @property
def download_url(self): def download_url(self):
return url_for('job.download_job_input', job_id=self.job_id, return url_for('jobs.download_job_input', job_id=self.job_id,
job_input_id=self.id) job_input_id=self.id)
@property @property
@ -323,7 +323,7 @@ class JobResult(db.Model):
@property @property
def download_url(self): def download_url(self):
return url_for('job.download_job_result', job_id=self.job_id, return url_for('jobs.download_job_result', job_id=self.job_id,
job_result_id=self.id) job_result_id=self.id)
@property @property
@ -384,8 +384,8 @@ class Job(db.Model):
return os.path.join(self.creator.path, 'jobs', str(self.id)) return os.path.join(self.creator.path, 'jobs', str(self.id))
@property @property
def path(self): def url(self):
return url_for('job.job', job_id=self.id) return url_for('jobs.job', job_id=self.id)
def __repr__(self): def __repr__(self):
''' '''
@ -430,9 +430,9 @@ class Job(db.Model):
'description': self.description, 'description': self.description,
'end_date': (self.end_date.timestamp() if self.end_date else 'end_date': (self.end_date.timestamp() if self.end_date else
None), None),
'service': {'args': self.service_args, 'service': self.service,
'name': self.service, 'service_args': self.service_args,
'version': self.service_version}, 'service_version': self.service_version,
'status': self.status, 'status': self.status,
'title': self.title, 'title': self.title,
'inputs': {input.id: input.to_dict() for input in self.inputs}, 'inputs': {input.id: input.to_dict() for input in self.inputs},
@ -529,6 +529,10 @@ class Corpus(db.Model):
files = db.relationship('CorpusFile', backref='corpus', lazy='dynamic', files = db.relationship('CorpusFile', backref='corpus', lazy='dynamic',
cascade='save-update, merge, delete') cascade='save-update, merge, delete')
@property
def analysis_url(self):
return url_for('corpora.analyse_corpus', corpus_id=self.id)
@property @property
def path(self): def path(self):
return os.path.join(self.creator.path, 'corpora', str(self.id)) return os.path.join(self.creator.path, 'corpora', str(self.id))
@ -538,7 +542,8 @@ class Corpus(db.Model):
return url_for('corpora.corpus', corpus_id=self.id) return url_for('corpora.corpus', corpus_id=self.id)
def to_dict(self): def to_dict(self):
return {'url': self.url, return {'analysis_url': self.analysis_url,
'url': self.url,
'id': self.id, 'id': self.id,
'user_id': self.user_id, 'user_id': self.user_id,
'creation_date': self.creation_date.timestamp(), 'creation_date': self.creation_date.timestamp(),
@ -628,8 +633,10 @@ class QueryResult(db.Model):
'url': self.url, 'url': self.url,
'id': self.id, 'id': self.id,
'user_id': self.user_id, 'user_id': self.user_id,
'corpus_title': self.query_metadata['corpus_name'],
'description': self.description, 'description': self.description,
'filename': self.filename, 'filename': self.filename,
'query': self.query_metadata['query'],
'query_metadata': self.query_metadata, 'query_metadata': self.query_metadata,
'title': self.title} 'title': self.title}

View File

@ -27,13 +27,13 @@ nopaque.socket = io({transports: ['websocket']});
nopaque.socket.on("user_data_stream_init", function(msg) { nopaque.socket.on("user_data_stream_init", function(msg) {
nopaque.user = JSON.parse(msg); nopaque.user = JSON.parse(msg);
for (let subscriber of nopaque.corporaSubscribers) { for (let subscriber of nopaque.corporaSubscribers) {
subscriber._init(nopaque.user.corpora); subscriber.init(nopaque.user.corpora);
} }
for (let subscriber of nopaque.jobsSubscribers) { for (let subscriber of nopaque.jobsSubscribers) {
subscriber._init(nopaque.user.jobs); subscriber.init(nopaque.user.jobs);
} }
for (let subscriber of nopaque.queryResultsSubscribers) { for (let subscriber of nopaque.queryResultsSubscribers) {
subscriber._init(nopaque.user.query_results); subscriber.init(nopaque.user.query_results);
} }
}); });
@ -46,13 +46,13 @@ nopaque.socket.on("user_data_stream_update", function(msg) {
jobs_patch = patch.filter(operation => operation.path.startsWith("/jobs")); jobs_patch = patch.filter(operation => operation.path.startsWith("/jobs"));
query_results_patch = patch.filter(operation => operation.path.startsWith("/query_results")); query_results_patch = patch.filter(operation => operation.path.startsWith("/query_results"));
for (let subscriber of nopaque.corporaSubscribers) { for (let subscriber of nopaque.corporaSubscribers) {
subscriber._update(corpora_patch); subscriber.update(corpora_patch);
} }
for (let subscriber of nopaque.jobsSubscribers) { for (let subscriber of nopaque.jobsSubscribers) {
subscriber._update(jobs_patch); subscriber.update(jobs_patch);
} }
for (let subscriber of nopaque.queryResultsSubscribers) { for (let subscriber of nopaque.queryResultsSubscribers) {
subscriber._update(query_results_patch); subscriber.update(query_results_patch);
} }
if (["all", "end"].includes(nopaque.user.settings.job_status_site_notifications)) { if (["all", "end"].includes(nopaque.user.settings.job_status_site_notifications)) {
for (operation of jobs_patch) { for (operation of jobs_patch) {
@ -69,13 +69,13 @@ nopaque.socket.on("user_data_stream_update", function(msg) {
nopaque.socket.on("foreign_user_data_stream_init", function(msg) { nopaque.socket.on("foreign_user_data_stream_init", function(msg) {
nopaque.foreignUser = JSON.parse(msg); nopaque.foreignUser = JSON.parse(msg);
for (let subscriber of nopaque.foreignCorporaSubscribers) { for (let subscriber of nopaque.foreignCorporaSubscribers) {
subscriber._init(nopaque.foreignUser.corpora); subscriber.init(nopaque.foreignUser.corpora);
} }
for (let subscriber of nopaque.foreignJobsSubscribers) { for (let subscriber of nopaque.foreignJobsSubscribers) {
subscriber._init(nopaque.foreignUser.jobs); subscriber.init(nopaque.foreignUser.jobs);
} }
for (let subscriber of nopaque.foreignQueryResultsSubscribers) { for (let subscriber of nopaque.foreignQueryResultsSubscribers) {
subscriber._init(nopaque.foreignUser.query_results); subscriber.init(nopaque.foreignUser.query_results);
} }
}); });
@ -87,9 +87,9 @@ nopaque.socket.on("foreign_user_data_stream_update", function(msg) {
corpora_patch = patch.filter(operation => operation.path.startsWith("/corpora")); corpora_patch = patch.filter(operation => operation.path.startsWith("/corpora"));
jobs_patch = patch.filter(operation => operation.path.startsWith("/jobs")); jobs_patch = patch.filter(operation => operation.path.startsWith("/jobs"));
query_results_patch = patch.filter(operation => operation.path.startsWith("/query_results")); query_results_patch = patch.filter(operation => operation.path.startsWith("/query_results"));
for (let subscriber of nopaque.foreignCorporaSubscribers) {subscriber._update(corpora_patch);} for (let subscriber of nopaque.foreignCorporaSubscribers) {subscriber.update(corpora_patch);}
for (let subscriber of nopaque.foreignJobsSubscribers) {subscriber._update(jobs_patch);} for (let subscriber of nopaque.foreignJobsSubscribers) {subscriber.update(jobs_patch);}
for (let subscriber of nopaque.foreignQueryResultsSubscribers) {subscriber._update(query_results_patch);} for (let subscriber of nopaque.foreignQueryResultsSubscribers) {subscriber.update(query_results_patch);}
}); });
nopaque.Forms = {}; nopaque.Forms = {};

View File

@ -1,420 +1,140 @@
class RessourceList extends List { class RessourceList {
constructor(idOrElement, subscriberList, type, options) { constructor(idOrElement, options = {}) {
if (!type || !["Corpus", "CorpusFile", "Job", "JobInput", "QueryResult", "User"].includes(type)) { this.list = new List(idOrElement, {...RessourceList.options, ...options});
throw "Unknown Type!"; }
}
super(idOrElement, {...RessourceList.options['common'], init(ressources) {
...RessourceList.options[type], this.list.clear();
...(options ? options : {})}); this.add(Object.values(ressources));
if (subscriberList) {subscriberList.push(this);} this.list.sort('id', {order: 'desc'});
this.type = type;
} }
_init(ressources) { update(patch) {
this.clear();
this._add(Object.values(ressources));
this.sort("id", {order: "desc"});
}
_update(patch) {
let item, pathArray; let item, pathArray;
for (let operation of patch) { for (let operation of patch) {
/* "/{ressourceName}/{ressourceId}/..." -> ["{ressourceId}", "..."] */ /*
pathArray = operation.path.split("/").slice(2); * '/{ressourceName}/{ressourceId}/{valueName}' -> ['{ressourceId}', {valueName}]
* Example: '/jobs/1/status' -> ['1', 'status']
*/
let [id, valueName] = operation.path.split("/").slice(2);
switch(operation.op) { switch(operation.op) {
case "add": case 'add':
if (pathArray.includes("results")) {break;} this.add(operation.value);
this._add([operation.value]);
break; break;
case "remove": case 'remove':
this.remove("id", pathArray[0]); this.remove(id);
break;
case 'replace':
this.replace(id, valueName, operation.value);
break; break;
case "replace":
item = this.get("id", pathArray[0])[0];
switch(pathArray[1]) {
case "status":
item.values({status: operation.value,
"analyse-link": ["analysing", "prepared", "start analysis"].includes(operation.value) ? `/corpora/${pathArray[0]}/analyse` : ""});
break;
default:
break;
}
default: default:
break; break;
} }
} }
} }
_add(values, callback) { add(values) {
this.add(values.map(x => RessourceList.dataMappers[this.type](x)), callback); /* WORKAROUND: Set a callback function ('() => {return;}') to force List.js
// Initialize modal and tooltipped elements in list perform the add method asynchronous.
M.AutoInit(this.listContainer); * https://listjs.com/api/#add
*/
this.list.add(values, () => {return;});
}
remove(id) {
this.list.remove('id', id);
}
replace(id, valueName, newValue) {
if (!this.list.valuesNames.includes(valueName)) {return;}
let item = this.list.get('id', id);
item.values({[valueName]: newValue});
} }
} }
RessourceList.options = {page: 5, pagination: [{innerWindow: 4, outerWindow: 1}]};
RessourceList.dataMappers = { class CorpusList extends RessourceList {
// A data mapper describes entitys rendered per row. One key value pair holds constructor(listElementId, options = {}) {
// the data to be rendered in the list.js table. Key has to correspond let listElement = document.querySelector(`#${listElementId}`);
// with the ValueNames defined below in RessourceList.options ValueNames. super(listElement, {...CorpusList.options, ...options});
// Links are declared with double ticks(") around them. The key for links listElement.addEventListener('click', (event) => {
// have to correspond with the class of an <a> element in the let actionButtonElement = event.target.closest('.action-button');
// RessourceList.options item blueprint. if (actionButtonElement === null) {return;}
let corpusId = event.target.closest('tr').dataset.id;
let action = actionButtonElement.dataset.action;
switch (action) {
case 'analyse':
window.location.href = nopaque.user.corpora[corpusId].analysis_url;
}
});
nopaque.corporaSubscribers.push(this);
}
}
/* ### Corpus mapper ### */
Corpus: corpus => ({ CorpusList.options = {
creation_date: corpus.creation_date, item: `<tr>
description: corpus.description, <td><a class="btn-floating disabled"><i class="material-icons">book</i></a></td>
id: corpus.id, <td><b class="title"></b><br><i class="description"></i></td>
link: `/corpora/${corpus.id}`, <td><span class="badge new status" data-badge-caption=""></span></td>
status: corpus.status, <td class="right-align">
title: corpus.title, <a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
title1: corpus.title, <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="edit" data-position="top" data-tooltip="Edit"><i class="material-icons">edit</i></a>
"analyse-link": ["analysing", "prepared", "start analysis"].includes(corpus.status) ? `/corpora/${corpus.id}/analyse` : "", <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="analyse" data-position="top" data-tooltip="Analyse"><i class="material-icons">search</i></a>
"delete-link": `/corpora/${corpus.id}/delete`, </td>
"delete-modal": `delete-corpus-${corpus.id}-modal`, </tr>`,
"delete-modal-trigger": `delete-corpus-${corpus.id}-modal`, valueNames: [{data: ['id']}, {name: "status", attr: "data-status"}, 'description', 'title']
}),
/* ### CorpusFile mapper ### TODO: replace delete-modal with delete-onclick */
CorpusFile: corpus_file => ({
author: corpus_file.author,
filename: corpus_file.filename,
id: corpus_file.id,
link: `${corpus_file.corpus_id}/files/${corpus_file.id}`,
"publishing-year": corpus_file.publishing_year,
title: corpus_file.title,
title1: corpus_file.title,
"delete-link": `/corpora/${corpus_file.corpus_id}/files/${corpus_file.id}/delete`,
"delete-modal": `delete-corpus-file-${corpus_file.id}-modal`,
"delete-modal-trigger": `delete-corpus-file-${corpus_file.id}-modal`,
"download-link": `${corpus_file.corpus_id}/files/${corpus_file.id}/download`,
}),
/* ### Job mapper ### */
Job: job => ({
creation_date: job.creation_date,
description: job.description,
id: job.id,
link: `/jobs/${job.id}`,
service: job.service.name,
status: job.status,
title: job.title,
title1: job.title,
"delete-link": `/jobs/${job.id}/delete`,
"delete-modal": `delete-job-${job.id}-modal`,
"delete-modal-trigger": `delete-job-${job.id}-modal`,
}),
/* ### JobInput mapper ### */
JobInput: job_input => ({
filename: job_input.filename,
id: job_input.job_id,
"download-link": `${job_input.job_id}/inputs/${job_input.id}/download`
}),
/* ### QueryResult mapper ### */
QueryResult: query_result => ({
corpus_name: query_result.query_metadata.corpus_name,
description: query_result.description,
id: query_result.id,
link: `/corpora/result/${query_result.id}`,
query: query_result.query_metadata.query,
title: query_result.title,
"delete-link": `/corpora/result/${query_result.id}/delete`,
"delete-modal": `delete-query-result-${query_result.id}-modal`,
"delete-modal-trigger": `delete-query-result-${query_result.id}-modal`,
"inspect-link": `/corpora/result/${query_result.id}/inspect`,
}),
/* ### User mapper ### */
User: user => ({
confirmed: user.confirmed,
email: user.email,
id: user.id,
link: `users/${user.id}`,
role: user.role.name,
username: user.username,
username2: user.username,
"delete-link": `/admin/users/${user.id}/delete`,
"delete-modal": `delete-user-${user.id}-modal`,
"delete-modal-trigger": `delete-user-${user.id}-modal`,
}),
}; };
RessourceList.options = { class JobList extends RessourceList {
// common list.js options for 5 rows per page etc. constructor(listElementId, options = {}) {
common: {page: 5, pagination: [{innerWindow: 4, outerWindow: 1}]}, let listElement = document.querySelector(`#${listElementId}`);
// extended list.js options for 10 rows per page etc. super(listElement, {...JobList.options, ...options});
extended: { nopaque.jobsSubscribers.push(this);
page: 10, }
pagination: [ }
{
name: "paginationTop",
paginationClass: "paginationTop", JobList.options = {
innerWindow: 8, item: `<tr>
outerWindow: 1 <td><a class="btn-floating disabled"><i class="material-icons service"></i></a></td>
}, <td><b class="title"></b><br><i class="description"></i></td>
{ <td><span class="badge new status" data-badge-caption=""></span></td>
paginationClass: "paginationBottom", <td class="right-align">
innerWindow: 8, <a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
outerWindow: 1, <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
}, </td>
], </tr>`,
}, valueNames: [{data: ['id']}, {name: 'service', attr: 'data-service'}, {name: "status", attr: "data-status"}, 'description', 'title']
/* Type specific List.js options. Usually only "item" and "valueNames" gets
* defined here but it is possible to define other List.js options.
* item: https://listjs.com/api/#item
* valueNames: https://listjs.com/api/#valueNames
*/
Corpus: {
item: `<tr>
<td>
<a class="btn-floating disabled">
<i class="material-icons service">book</i>
</a>
</td>
<td>
<b class="title"></b><br>
<i class="description"></i>
</td>
<td>
<span class="badge new status" data-badge-caption=""></span>
</td>
<td>
<div class="right-align">
<a class="btn-floating modal-trigger red tooltipped waves-effect waves-light delete-modal-trigger" data-position="top" data-tooltip="Delete">
<i class="material-icons">delete</i>
</a>
<a class="btn-floating tooltipped waves-effect waves-light link" data-position="top" data-tooltip="Edit">
<i class="material-icons">edit</i>
</a>
<a class="btn-floating tooltipped waves-effect waves-light analyse-link" data-position="top" data-tooltip="Analyse">
<i class="material-icons">search</i>
</a>
</div>
<div class="modal delete-modal">
<div class="modal-content">
<h4>Confirm corpus deletion</h4>
<p>Do you really want to delete the corpus <b class="title1"></b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light delete-link"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
</td>
</tr>`,
valueNames: [
"creation_date",
"description",
"title",
"title1",
{data: ["id"]},
{name: "analyse-link", attr: "href"},
{name: "delete-link", attr: "href"},
{name: "delete-modal-trigger", attr: "data-target"},
{name: "delete-modal", attr: "id"},
{name: "link", attr: "href"},
{name: "status", attr: "data-status"},
]
},
CorpusFile: {
item: `<tr>
<td class="filename" style="word-break: break-word;"></td>
<td class="author" style="word-break: break-word;"></td>
<td class="title" style="word-break: break-word;"></td>
<td class="publishing-year" style="word-break: break-word;"></td>
<td>
<div class="right-align">
<a class="btn-floating modal-trigger red tooltipped waves-effect waves-light delete-modal-trigger" data-position="top" data-tooltip="Delete">
<i class="material-icons">delete</i>
</a>
<a class="btn-floating tooltipped waves-effect waves-light download-link" data-position="top" data-tooltip="Download">
<i class="material-icons">file_download</i>
</a>
<a class="btn-floating tooltipped waves-effect waves-light link" data-position="top" data-tooltip="Edit">
<i class="material-icons">edit</i>
</a>
</div>
<div class="modal delete-modal">
<div class="modal-content">
<h4>Confirm corpus file deletion</h4>
<p>Do you really want to delete the corpus file <b class="title1"></b>? It be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light delete-link"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
</td>
</tr>`,
valueNames: [
"author",
"filename",
"publishing-year",
"title",
"title1",
{data: ["id"]},
{name: "delete-link", attr: "href"},
{name: "delete-modal-trigger", attr: "data-target"},
{name: "delete-modal", attr: "id"},
{name: "download-link", attr: "href"},
{name: "link", attr: "href"},
],
},
Job: {
item: `<tr>
<td>
<a class="btn-floating disabled">
<i class="material-icons service"></i>
</a>
</td>
<td>
<b class="title"></b><br>
<i class="description"></i>
</td>
<td>
<span class="badge new status" data-badge-caption=""></span>
</td>
<td>
<div class="right-align">
<a class="btn-floating modal-trigger red tooltipped waves-effect waves-light delete-modal-trigger" data-position="top" data-tooltip="Delete">
<i class="material-icons">delete</i>
</a>
<a class="btn-floating tooltipped waves-effect waves-light link" data-position="top" data-tooltip="Go to job">
<i class="material-icons">send</i>
</a>
</div>
<div class="modal delete-modal">
<div class="modal-content">
<h4>Confirm job deletion</h4>
<p>Do you really want to delete the job <b class="title1"></b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light delete-link"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
</td>
</tr>`,
valueNames: [
"creation_date",
"description",
"title",
"title1",
{data: ["id"]},
{name: "delete-link", attr: "href"},
{name: "delete-modal-trigger", attr: "data-target"},
{name: "delete-modal", attr: "id"},
{name: "link", attr: "href"},
{name: "service", attr: "data-service"},
{name: "status", attr: "data-status"},
],
},
JobInput: {
item : `<tr>
<td class="filename"></td>
<td class="right-align">
<a class="btn-floating tooltipped waves-effect waves-light download-link" data-position="top" data-tooltip="Download">
<i class="material-icons">file_download</i>
</a>
</td>
</tr>`,
valueNames: [
"filename",
"id",
{name: "download-link", attr: "href"},
],
},
QueryResult: {
item: `<tr>
<td>
<b class="title"></b><br>
<i class="description"></i><br>
</td>
<td>
<span class="corpus_name"></span><br>
<span class="query"></span>
</td>
<td>
<div class="right-align">
<a class="btn-floating modal-trigger red tooltipped waves-effect waves-light delete-modal-trigger" data-position="top" data-tooltip="Delete">
<i class="material-icons">delete</i>
</a>
<a class="btn-floating tooltipped waves-effect waves-light link" data-position="top" data-tooltip="Info">
<i class="material-icons">info</i>
</a>
<a class="btn-floating tooltipped waves-effect waves-light inspect-link" data-position="top" data-tooltip="Analyse">
<i class="material-icons">search</i>
</a>
</div>
<div class="modal delete-modal">
<div class="modal-content">
<h4>Confirm query result deletion</h4>
<p>Do you really want to delete the query result <b class="title1"></b>? It will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light delete-link"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
</td>
</tr>`,
valueNames: [
"corpus_name",
"description",
"query",
"title",
"title2",
{data: ["id"]},
{name: "delete-link", attr: "href"},
{name: "delete-modal-trigger", attr: "data-target"},
{name: "delete-modal", attr: "id"},
{name: "inspect-link", attr: "href"},
{name: "link", attr: "href"},
],
},
User: {
item: `<tr>
<td class="id"></td>
<td class="username"></td>
<td class="email"></td>
<td class="role"></td>
<td>
<div class="right-align">
<a class="btn-floating modal-trigger red tooltipped waves-effect waves-light delete-modal-trigger" data-position="top" data-tooltip="Delete">
<i class="material-icons">delete</i>
</a>
<a class="btn-floating tooltipped waves-effect waves-light link" data-position="top" data-tooltip="Go to user">
<i class="material-icons">send</i>
</a>
</div>
<div class="modal delete-modal">
<div class="modal-content">
<h4>Confirm corpus deletion</h4>
<p>Do you really want to delete the job <b class="title1"></b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light delete-link"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
</td>
</tr>`,
valueNames: [
"username",
"username2",
"email",
"role",
"id",
{name: "link", attr: "href"},
{name: "delete-link", attr: "href"},
{name: "delete-modal-trigger", attr: "data-target"},
{name: "delete-modal", attr: "id"},
],
},
}; };
export { RessourceList, };
class QueryResultList extends RessourceList {
constructor(listElementId, options = {}) {
let listElement = document.querySelector(`#${listElementId}`);
super(listElement, {...QueryResultList.options, ...options});
nopaque.queryResultsSubscribers.push(this);
}
}
QueryResultList.options = {
item: `<tr>
<td><b class="title"></b><br><i class="description"></i><br></td>
<td><span class="corpus_title"></span><br><span class="query"></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="analyse" data-position="top" data-tooltip="Analyse"><i class="material-icons">search</i></a>
</td>
</tr>`,
valueNames: [{data: ['id']}, 'corpus_title', 'description', 'query', 'title']
};
export { CorpusList, JobList, QueryResultList };

View File

@ -0,0 +1,420 @@
class RessourceList extends List {
constructor(idOrElement, subscriberList, type, options) {
if (!type || !["Corpus", "CorpusFile", "Job", "JobInput", "QueryResult", "User"].includes(type)) {
throw "Unknown Type!";
}
super(idOrElement, {...RessourceList.options['common'],
...RessourceList.options[type],
...(options ? options : {})});
if (subscriberList) {subscriberList.push(this);}
this.type = type;
}
_init(ressources) {
this.clear();
this._add(Object.values(ressources));
this.sort("id", {order: "desc"});
}
_update(patch) {
let item, pathArray;
for (let operation of patch) {
/* "/{ressourceName}/{ressourceId}/..." -> ["{ressourceId}", "..."] */
pathArray = operation.path.split("/").slice(2);
switch(operation.op) {
case "add":
if (pathArray.includes("results")) {break;}
this._add([operation.value]);
break;
case "remove":
this.remove("id", pathArray[0]);
break;
case "replace":
item = this.get("id", pathArray[0])[0];
switch(pathArray[1]) {
case "status":
item.values({status: operation.value,
"analyse-link": ["analysing", "prepared", "start analysis"].includes(operation.value) ? `/corpora/${pathArray[0]}/analyse` : ""});
break;
default:
break;
}
default:
break;
}
}
}
_add(values, callback) {
this.add(values.map(x => RessourceList.dataMappers[this.type](x)), callback);
// Initialize modal and tooltipped elements in list
M.AutoInit(this.listContainer);
}
}
RessourceList.dataMappers = {
// A data mapper describes entitys rendered per row. One key value pair holds
// the data to be rendered in the list.js table. Key has to correspond
// with the ValueNames defined below in RessourceList.options ValueNames.
// Links are declared with double ticks(") around them. The key for links
// have to correspond with the class of an <a> element in the
// RessourceList.options item blueprint.
/* ### Corpus mapper ### */
Corpus: corpus => ({
creation_date: corpus.creation_date,
description: corpus.description,
id: corpus.id,
link: `/corpora/${corpus.id}`,
status: corpus.status,
title: corpus.title,
title1: corpus.title,
"analyse-link": ["analysing", "prepared", "start analysis"].includes(corpus.status) ? `/corpora/${corpus.id}/analyse` : "",
"delete-link": `/corpora/${corpus.id}/delete`,
"delete-modal": `delete-corpus-${corpus.id}-modal`,
"delete-modal-trigger": `delete-corpus-${corpus.id}-modal`,
}),
/* ### CorpusFile mapper ### TODO: replace delete-modal with delete-onclick */
CorpusFile: corpus_file => ({
author: corpus_file.author,
filename: corpus_file.filename,
id: corpus_file.id,
link: `${corpus_file.corpus_id}/files/${corpus_file.id}`,
"publishing-year": corpus_file.publishing_year,
title: corpus_file.title,
title1: corpus_file.title,
"delete-link": `/corpora/${corpus_file.corpus_id}/files/${corpus_file.id}/delete`,
"delete-modal": `delete-corpus-file-${corpus_file.id}-modal`,
"delete-modal-trigger": `delete-corpus-file-${corpus_file.id}-modal`,
"download-link": `${corpus_file.corpus_id}/files/${corpus_file.id}/download`,
}),
/* ### Job mapper ### */
Job: job => ({
creation_date: job.creation_date,
description: job.description,
id: job.id,
link: `/jobs/${job.id}`,
service: job.service.name,
status: job.status,
title: job.title,
title1: job.title,
"delete-link": `/jobs/${job.id}/delete`,
"delete-modal": `delete-job-${job.id}-modal`,
"delete-modal-trigger": `delete-job-${job.id}-modal`,
}),
/* ### JobInput mapper ### */
JobInput: job_input => ({
filename: job_input.filename,
id: job_input.job_id,
"download-link": `${job_input.job_id}/inputs/${job_input.id}/download`
}),
/* ### QueryResult mapper ### */
QueryResult: query_result => ({
corpus_name: query_result.query_metadata.corpus_name,
description: query_result.description,
id: query_result.id,
link: `/corpora/result/${query_result.id}`,
query: query_result.query_metadata.query,
title: query_result.title,
"delete-link": `/corpora/result/${query_result.id}/delete`,
"delete-modal": `delete-query-result-${query_result.id}-modal`,
"delete-modal-trigger": `delete-query-result-${query_result.id}-modal`,
"inspect-link": `/corpora/result/${query_result.id}/inspect`,
}),
/* ### User mapper ### */
User: user => ({
confirmed: user.confirmed,
email: user.email,
id: user.id,
link: `users/${user.id}`,
role: user.role.name,
username: user.username,
username2: user.username,
"delete-link": `/admin/users/${user.id}/delete`,
"delete-modal": `delete-user-${user.id}-modal`,
"delete-modal-trigger": `delete-user-${user.id}-modal`,
}),
};
RessourceList.options = {
// common list.js options for 5 rows per page etc.
common: {page: 5, pagination: [{innerWindow: 4, outerWindow: 1}]},
// extended list.js options for 10 rows per page etc.
extended: {
page: 10,
pagination: [
{
name: "paginationTop",
paginationClass: "paginationTop",
innerWindow: 8,
outerWindow: 1
},
{
paginationClass: "paginationBottom",
innerWindow: 8,
outerWindow: 1,
},
],
},
/* Type specific List.js options. Usually only "item" and "valueNames" gets
* defined here but it is possible to define other List.js options.
* item: https://listjs.com/api/#item
* valueNames: https://listjs.com/api/#valueNames
*/
Corpus: {
item: `<tr>
<td>
<a class="btn-floating disabled">
<i class="material-icons service">book</i>
</a>
</td>
<td>
<b class="title"></b><br>
<i class="description"></i>
</td>
<td>
<span class="badge new status" data-badge-caption=""></span>
</td>
<td>
<div class="right-align">
<a class="btn-floating modal-trigger red tooltipped waves-effect waves-light delete-modal-trigger" data-position="top" data-tooltip="Delete">
<i class="material-icons">delete</i>
</a>
<a class="btn-floating tooltipped waves-effect waves-light link" data-position="top" data-tooltip="Edit">
<i class="material-icons">edit</i>
</a>
<a class="btn-floating tooltipped waves-effect waves-light analyse-link" data-position="top" data-tooltip="Analyse">
<i class="material-icons">search</i>
</a>
</div>
<div class="modal delete-modal">
<div class="modal-content">
<h4>Confirm corpus deletion</h4>
<p>Do you really want to delete the corpus <b class="title1"></b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light delete-link"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
</td>
</tr>`,
valueNames: [
"creation_date",
"description",
"title",
"title1",
{data: ["id"]},
{name: "analyse-link", attr: "href"},
{name: "delete-link", attr: "href"},
{name: "delete-modal-trigger", attr: "data-target"},
{name: "delete-modal", attr: "id"},
{name: "link", attr: "href"},
{name: "status", attr: "data-status"},
]
},
CorpusFile: {
item: `<tr>
<td class="filename" style="word-break: break-word;"></td>
<td class="author" style="word-break: break-word;"></td>
<td class="title" style="word-break: break-word;"></td>
<td class="publishing-year" style="word-break: break-word;"></td>
<td>
<div class="right-align">
<a class="btn-floating modal-trigger red tooltipped waves-effect waves-light delete-modal-trigger" data-position="top" data-tooltip="Delete">
<i class="material-icons">delete</i>
</a>
<a class="btn-floating tooltipped waves-effect waves-light download-link" data-position="top" data-tooltip="Download">
<i class="material-icons">file_download</i>
</a>
<a class="btn-floating tooltipped waves-effect waves-light link" data-position="top" data-tooltip="Edit">
<i class="material-icons">edit</i>
</a>
</div>
<div class="modal delete-modal">
<div class="modal-content">
<h4>Confirm corpus file deletion</h4>
<p>Do you really want to delete the corpus file <b class="title1"></b>? It be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light delete-link"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
</td>
</tr>`,
valueNames: [
"author",
"filename",
"publishing-year",
"title",
"title1",
{data: ["id"]},
{name: "delete-link", attr: "href"},
{name: "delete-modal-trigger", attr: "data-target"},
{name: "delete-modal", attr: "id"},
{name: "download-link", attr: "href"},
{name: "link", attr: "href"},
],
},
Job: {
item: `<tr>
<td>
<a class="btn-floating disabled">
<i class="material-icons service"></i>
</a>
</td>
<td>
<b class="title"></b><br>
<i class="description"></i>
</td>
<td>
<span class="badge new status" data-badge-caption=""></span>
</td>
<td>
<div class="right-align">
<a class="btn-floating modal-trigger red tooltipped waves-effect waves-light delete-modal-trigger" data-position="top" data-tooltip="Delete">
<i class="material-icons">delete</i>
</a>
<a class="btn-floating tooltipped waves-effect waves-light link" data-position="top" data-tooltip="Go to job">
<i class="material-icons">send</i>
</a>
</div>
<div class="modal delete-modal">
<div class="modal-content">
<h4>Confirm job deletion</h4>
<p>Do you really want to delete the job <b class="title1"></b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light delete-link"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
</td>
</tr>`,
valueNames: [
"creation_date",
"description",
"title",
"title1",
{data: ["id"]},
{name: "delete-link", attr: "href"},
{name: "delete-modal-trigger", attr: "data-target"},
{name: "delete-modal", attr: "id"},
{name: "link", attr: "href"},
{name: "service", attr: "data-service"},
{name: "status", attr: "data-status"},
],
},
JobInput: {
item : `<tr>
<td class="filename"></td>
<td class="right-align">
<a class="btn-floating tooltipped waves-effect waves-light download-link" data-position="top" data-tooltip="Download">
<i class="material-icons">file_download</i>
</a>
</td>
</tr>`,
valueNames: [
"filename",
"id",
{name: "download-link", attr: "href"},
],
},
QueryResult: {
item: `<tr>
<td>
<b class="title"></b><br>
<i class="description"></i><br>
</td>
<td>
<span class="corpus_name"></span><br>
<span class="query"></span>
</td>
<td>
<div class="right-align">
<a class="btn-floating modal-trigger red tooltipped waves-effect waves-light delete-modal-trigger" data-position="top" data-tooltip="Delete">
<i class="material-icons">delete</i>
</a>
<a class="btn-floating tooltipped waves-effect waves-light link" data-position="top" data-tooltip="Info">
<i class="material-icons">info</i>
</a>
<a class="btn-floating tooltipped waves-effect waves-light inspect-link" data-position="top" data-tooltip="Analyse">
<i class="material-icons">search</i>
</a>
</div>
<div class="modal delete-modal">
<div class="modal-content">
<h4>Confirm query result deletion</h4>
<p>Do you really want to delete the query result <b class="title1"></b>? It will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light delete-link"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
</td>
</tr>`,
valueNames: [
"corpus_name",
"description",
"query",
"title",
"title2",
{data: ["id"]},
{name: "delete-link", attr: "href"},
{name: "delete-modal-trigger", attr: "data-target"},
{name: "delete-modal", attr: "id"},
{name: "inspect-link", attr: "href"},
{name: "link", attr: "href"},
],
},
User: {
item: `<tr>
<td class="id"></td>
<td class="username"></td>
<td class="email"></td>
<td class="role"></td>
<td>
<div class="right-align">
<a class="btn-floating modal-trigger red tooltipped waves-effect waves-light delete-modal-trigger" data-position="top" data-tooltip="Delete">
<i class="material-icons">delete</i>
</a>
<a class="btn-floating tooltipped waves-effect waves-light link" data-position="top" data-tooltip="Go to user">
<i class="material-icons">send</i>
</a>
</div>
<div class="modal delete-modal">
<div class="modal-content">
<h4>Confirm corpus deletion</h4>
<p>Do you really want to delete the job <b class="title1"></b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light delete-link"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
</td>
</tr>`,
valueNames: [
"username",
"username2",
"email",
"role",
"id",
{name: "link", attr: "href"},
{name: "delete-link", attr: "href"},
{name: "delete-modal-trigger", attr: "data-target"},
{name: "delete-modal", attr: "id"},
],
},
};
export { RessourceList, };

View File

@ -1,33 +1,33 @@
class RessourceList extends List { class RessourceList {
constructor(idOrElement, options) { constructor(idOrElement, options = {}) {
super(idOrElement, {...RessourceList.options['default'], ...(options ? options : {})}); this.list = new List(idOrElement, {...RessourceList.options, ...options});
} }
_init(ressources) { init(ressources) {
this.clear(); this.list.clear();
this._add(Object.values(ressources)); this.add(Object.values(ressources));
this.sort("id", {order: "desc"}); this.list.sort('id', {order: 'desc'});
} }
_update(patch) { update(patch) {
let item, pathArray; let item, pathArray;
for (let operation of patch) { for (let operation of patch) {
/* /*
* '/{ressourceName}/{ressourceId}/...' -> ['{ressourceId}', ...] * '/{ressourceName}/{ressourceId}/{valueName}' -> ['{ressourceId}', {valueName}]
* Example: '/jobs/1/status' -> ['1', 'status'] * Example: '/jobs/1/status' -> ['1', 'status']
*/ */
pathArray = operation.path.split("/").slice(2); let [id, valueName] = operation.path.split("/").slice(2);
switch(operation.op) { switch(operation.op) {
case "add": case 'add':
this.add_handler([operation.value]); this.add(operation.value);
break; break;
case "remove": case 'remove':
this.remove_handler(pathArray[0]); this.remove(id);
break; break;
case "replace": case 'replace':
this.replace_handler(pathArray[0], pathArray[1], operation.value); this.replace(id, valueName, operation.value);
break; break;
default: default:
break; break;
@ -35,34 +35,93 @@ class RessourceList extends List {
} }
} }
add_handler(values, callback) { add(values) {
if (this.hasOwnProperty('add_')) { /* WORKAROUND: Set a callback function ('() => {return;}') to force List.js
this.add_(values, callback); perform the add method asynchronous.
} else { * https://listjs.com/api/#add
this.add(values, callback); */
} this.list.add(values, () => {return;});
} }
remove_handler(id) { remove(id) {
if (this.hasOwnProperty('remove_')) { this.list.remove('id', id);
this.remove_(id);
} else {
this.remove(id);
}
} }
replace_handler(id, valueName, newValue) { replace(id, valueName, newValue) {
let item = this.get('id', id); if (!this.list.valuesNames.includes(valueName)) {return;}
if (this.hasOwnProperty('add_')) let item = this.list.get('id', id);
item.values({valueName: operation.value}); item.values({[valueName]: newValue});
} }
} }
RessourceList.options = { RessourceList.options = {page: 5, pagination: [{innerWindow: 4, outerWindow: 1}]};
// default RessourceList options
default: {page: 5, pagination: [{innerWindow: 4, outerWindow: 1}]},
class CorpusList extends RessourceList {
constructor(idOrElement, options = {}) {
super(idOrElement, {...CorpusList.options, ...options});
nopaque.corporaSubscribers.push(this);
}
}
CorpusList.options = {
item: `<tr>
<td><a class="btn-floating disabled"><i class="material-icons">book</i></a></td>
<td><b class="title"></b><br><i class="description"></i></td>
<td><span class="badge new status" data-badge-caption=""></span></td>
<td class="right-align">
<a class="btn-floating delete red tooltipped waves-effect waves-light" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="btn-floating edit tooltipped waves-effect waves-light" data-position="top" data-tooltip="Edit"><i class="material-icons">edit</i></a>
<a class="analyse btn-floating tooltipped waves-effect waves-light" data-position="top" data-tooltip="Analyse"><i class="material-icons">search</i></a>
</td>
</tr>`,
valueNames: [{data: ['id']}, 'description', 'status', 'title']
}; };
export { RessourceList, }; class JobList extends RessourceList {
constructor(idOrElement, options = {}) {
super(idOrElement, {...JobList.options, ...options});
nopaque.jobsSubscribers.push(this);
}
}
JobList.options = {
item: `<tr>
<td><a class="btn-floating disabled"><i class="material-icons service"></i></a></td>
<td><b class="title"></b><br><i class="description"></i></td>
<td><span class="badge new status" data-badge-caption=""></span></td>
<td class="right-align">
<a class="btn-floating delete red tooltipped waves-effect waves-light" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="btn-floating tooltipped view waves-effect waves-light" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
</td>
</tr>`,
valueNames: [{data: ['id']}, {name: 'service', attr: 'data-service'}, 'description', 'status', 'title']
};
class QueryResultList extends RessourceList {
constructor(idOrElement, options = {}) {
super(idOrElement, {...QueryResultList.options, ...options});
nopaque.queryResultsSubscribers.push(this);
}
}
QueryResultList.options = {
item: `<tr>
<td><b class="title"></b><br><i class="description"></i><br></td>
<td><span class="corpus_title"></span><br><span class="query"></span></td>
<td class="right-align">
<a class="btn-floating delete red tooltipped waves-effect waves-light" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="btn-floating tooltipped view waves-effect waves-light" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
<a class="analyse btn-floating tooltipped waves-effect waves-light" data-position="top" data-tooltip="Analyse"><i class="material-icons">search</i></a>
</td>
</tr>`,
valueNames: [{data: ['id']}, 'corpus_title', 'description', 'query', 'title']
};
export { CorpusList, JobList, QueryResultList };

View File

@ -176,9 +176,9 @@
{% block scripts %} {% block scripts %}
{{ super() }} {{ super() }}
<script type="module"> <script type="module">
import {RessourceList} from '../../static/js/nopaque.lists.js'; import {CorpusList, JobList, QueryResultList} from '../../static/js/nopaque.lists.js';
let corpusList = new RessourceList("corpora", nopaque.corporaSubscribers, "Corpus"); let corpusList = new CorpusList("corpora");
let jobList = new RessourceList("jobs", nopaque.jobsSubscribers, "Job"); let jobList = new JobList("jobs");
let queryResultList = new RessourceList("query-results", nopaque.queryResultsSubscribers, "QueryResult"); let queryResultList = new QueryResultList("query-results");
</script> </script>
{% endblock scripts %} {% endblock scripts %}