mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2024-11-15 01:05:42 +00:00
Add a parallel package for query results.
This commit is contained in:
parent
2b3d08b277
commit
6760061d53
@ -52,6 +52,10 @@ def create_app(config_name):
|
|||||||
from .profile import profile as profile_blueprint
|
from .profile import profile as profile_blueprint
|
||||||
app.register_blueprint(profile_blueprint, url_prefix='/profile')
|
app.register_blueprint(profile_blueprint, url_prefix='/profile')
|
||||||
|
|
||||||
|
from .query_results import query_results as query_results_blueprint
|
||||||
|
app.register_blueprint(query_results_blueprint,
|
||||||
|
url_prefix='/query_results')
|
||||||
|
|
||||||
from .services import services as services_blueprint
|
from .services import services as services_blueprint
|
||||||
app.register_blueprint(services_blueprint, url_prefix='/services')
|
app.register_blueprint(services_blueprint, url_prefix='/services')
|
||||||
|
|
||||||
|
@ -135,6 +135,10 @@ class User(UserMixin, db.Model):
|
|||||||
cascade='save-update, merge, delete')
|
cascade='save-update, merge, delete')
|
||||||
results = db.relationship('Result', backref='creator', lazy='dynamic',
|
results = db.relationship('Result', backref='creator', lazy='dynamic',
|
||||||
cascade='save-update, merge, delete')
|
cascade='save-update, merge, delete')
|
||||||
|
query_results = db.relationship('QueryResult',
|
||||||
|
backref='creator',
|
||||||
|
cascade='save-update, merge, delete',
|
||||||
|
lazy='dynamic')
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {'id': self.id,
|
return {'id': self.id,
|
||||||
@ -151,7 +155,9 @@ class User(UserMixin, db.Model):
|
|||||||
self.setting_job_status_site_notifications},
|
self.setting_job_status_site_notifications},
|
||||||
'corpora': {corpus.id: corpus.to_dict()
|
'corpora': {corpus.id: corpus.to_dict()
|
||||||
for corpus in self.corpora},
|
for corpus in self.corpora},
|
||||||
'jobs': {job.id: job.to_dict() for job in self.jobs}}
|
'jobs': {job.id: job.to_dict() for job in self.jobs},
|
||||||
|
'query_results': {query_result.id: query_result.to_dict()
|
||||||
|
for query_result in self.query_results}}
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
'''
|
'''
|
||||||
@ -616,6 +622,43 @@ class Corpus(db.Model):
|
|||||||
return '<Corpus {corpus_title}>'.format(corpus_title=self.title)
|
return '<Corpus {corpus_title}>'.format(corpus_title=self.title)
|
||||||
|
|
||||||
|
|
||||||
|
class QueryResult(db.Model):
|
||||||
|
'''
|
||||||
|
Class to define a corpus analysis result.
|
||||||
|
'''
|
||||||
|
__tablename__ = 'query_results'
|
||||||
|
# Primary key
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
# Foreign keys
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||||
|
# Fields
|
||||||
|
description = db.Column(db.String(255))
|
||||||
|
filename = db.Column(db.String(255))
|
||||||
|
query_metadata = db.Column(db.JSON())
|
||||||
|
title = db.Column(db.String(32))
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
query_result_dir = os.path.join(current_app.config['NOPAQUE_STORAGE'],
|
||||||
|
str(self.user_id),
|
||||||
|
'query_results',
|
||||||
|
str(self.id))
|
||||||
|
shutil.rmtree(query_result_dir, ignore_errors=True)
|
||||||
|
db.session.delete(self)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {'id': self.id,
|
||||||
|
'user_id': self.user_id,
|
||||||
|
'description': self.description,
|
||||||
|
'filename': self.filename,
|
||||||
|
'title': self.title}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
'''
|
||||||
|
String representation of the CorpusAnalysisResult. For human readability.
|
||||||
|
'''
|
||||||
|
return '<QueryResult {}>'.format(self.title)
|
||||||
|
|
||||||
|
|
||||||
class Result(db.Model):
|
class Result(db.Model):
|
||||||
'''
|
'''
|
||||||
Class to define a result set of one query.
|
Class to define a result set of one query.
|
||||||
|
5
web/app/query_results/__init__.py
Normal file
5
web/app/query_results/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
|
||||||
|
query_results = Blueprint('query_results', __name__)
|
||||||
|
from . import views # noqa
|
21
web/app/query_results/forms.py
Normal file
21
web/app/query_results/forms.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
from wtforms import FileField, StringField, SubmitField, ValidationError
|
||||||
|
from wtforms.validators import DataRequired, Length
|
||||||
|
|
||||||
|
|
||||||
|
class AddQueryResultForm(FlaskForm):
|
||||||
|
'''
|
||||||
|
Form used to import one result json file.
|
||||||
|
'''
|
||||||
|
description = StringField('Description',
|
||||||
|
validators=[DataRequired(), Length(1, 255)])
|
||||||
|
file = FileField('File', validators=[DataRequired()])
|
||||||
|
title = StringField('Title', validators=[DataRequired(), Length(1, 32)])
|
||||||
|
submit = SubmitField()
|
||||||
|
|
||||||
|
def validate_file(self, field):
|
||||||
|
if not field.data.filename.lower().endswith('.json'):
|
||||||
|
raise ValidationError('File does not have an approved extension: '
|
||||||
|
'.json')
|
||||||
|
field.data.filename = secure_filename(field.data.filename)
|
13
web/app/query_results/tasks.py
Normal file
13
web/app/query_results/tasks.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from .. import db
|
||||||
|
from ..decorators import background
|
||||||
|
from ..models import QueryResult
|
||||||
|
|
||||||
|
|
||||||
|
@background
|
||||||
|
def delete_query_result(query_result_id, *args, **kwargs):
|
||||||
|
with kwargs['app'].app_context():
|
||||||
|
query_result = QueryResult.query.get(query_result_id)
|
||||||
|
if query_result is None:
|
||||||
|
raise Exception('QueryResult {} not found'.format(query_result_id))
|
||||||
|
query_result.delete()
|
||||||
|
db.session.commit()
|
144
web/app/query_results/views.py
Normal file
144
web/app/query_results/views.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
from . import query_results
|
||||||
|
from . import tasks
|
||||||
|
from .. import db
|
||||||
|
from ..corpora.forms import DisplayOptionsForm
|
||||||
|
from ..models import QueryResult
|
||||||
|
from .forms import AddQueryResultForm
|
||||||
|
from flask import (abort, current_app, flash, make_response, redirect,
|
||||||
|
render_template, request, send_from_directory, url_for)
|
||||||
|
from flask_login import current_user, login_required
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from jsonschema import validate
|
||||||
|
|
||||||
|
|
||||||
|
@query_results.route('/add', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def add_query_result():
|
||||||
|
'''
|
||||||
|
View to import a result as a json file.
|
||||||
|
'''
|
||||||
|
add_query_result_form = AddQueryResultForm(prefix='add-query-result-form')
|
||||||
|
if add_query_result_form.is_submitted():
|
||||||
|
if not add_query_result_form.validate():
|
||||||
|
return make_response(add_query_result_form.errors, 400)
|
||||||
|
query_result = QueryResult(
|
||||||
|
creator=current_user,
|
||||||
|
description=add_query_result_form.description.data,
|
||||||
|
filename=add_query_result_form.file.data.filename,
|
||||||
|
title=add_query_result_form.title.data
|
||||||
|
)
|
||||||
|
db.session.add(query_result)
|
||||||
|
db.session.commit()
|
||||||
|
# create paths to save the uploaded json file
|
||||||
|
query_result_dir = os.path.join(current_app.config['NOPAQUE_STORAGE'],
|
||||||
|
str(current_user.id),
|
||||||
|
'query_results',
|
||||||
|
str(query_result.id))
|
||||||
|
try:
|
||||||
|
os.makedirs(query_result_dir)
|
||||||
|
except Exception:
|
||||||
|
db.session.delete(query_result)
|
||||||
|
db.session.commit()
|
||||||
|
flash('Internal Server Error', 'error')
|
||||||
|
redirect_url = url_for('query_results.add_query_result')
|
||||||
|
return make_response({'redirect_url': redirect_url}, 500)
|
||||||
|
# save the uploaded file
|
||||||
|
query_result_file_path = os.path.join(query_result_dir,
|
||||||
|
query_result.filename)
|
||||||
|
add_query_result_form.file.data.save(query_result_file_path)
|
||||||
|
# parse json from file
|
||||||
|
with open(query_result_file_path, 'r') as file:
|
||||||
|
query_result_file_content = json.load(file)
|
||||||
|
# parse json schema
|
||||||
|
with open('app/static/json_schema/nopaque_cqi_py_results_schema.json', 'r') as file: # noqa
|
||||||
|
schema = json.load(file)
|
||||||
|
try:
|
||||||
|
# validate imported json file
|
||||||
|
validate(instance=query_result_file_content, schema=schema)
|
||||||
|
except Exception:
|
||||||
|
tasks.delete_query_result(query_result.id)
|
||||||
|
flash('Uploaded file is invalid', 'result')
|
||||||
|
redirect_url = url_for('query_results.add_query_result')
|
||||||
|
return make_response({'redirect_url': redirect_url}, 201)
|
||||||
|
query_result_file_content.pop('matches')
|
||||||
|
query_result_file_content.pop('cpos_lookup')
|
||||||
|
query_result.query_metadata = query_result_file_content
|
||||||
|
db.session.commit()
|
||||||
|
flash('Query result added!', 'result')
|
||||||
|
redirect_url = url_for('query_results.query_result',
|
||||||
|
query_result_id=query_result.id)
|
||||||
|
return make_response({'redirect_url': redirect_url}, 201)
|
||||||
|
return render_template('query_results/add_query_result.html.j2',
|
||||||
|
add_query_result_form=add_query_result_form,
|
||||||
|
title='Add query result')
|
||||||
|
|
||||||
|
|
||||||
|
@query_results.route('/<int:query_result_id>')
|
||||||
|
@login_required
|
||||||
|
def query_result(query_result_id):
|
||||||
|
query_result = QueryResult.query.get_or_404(query_result_id)
|
||||||
|
if not (query_result.creator == current_user
|
||||||
|
or current_user.is_administrator()):
|
||||||
|
abort(403)
|
||||||
|
return render_template('query_results/query_result.html.j2',
|
||||||
|
query_result=query_result,
|
||||||
|
title='Query result')
|
||||||
|
|
||||||
|
|
||||||
|
@query_results.route('/<int:query_result_id>/inspect')
|
||||||
|
@login_required
|
||||||
|
def inspect_query_result(query_result_id):
|
||||||
|
'''
|
||||||
|
View to inspect one importe result file in a corpus analysis like interface
|
||||||
|
'''
|
||||||
|
query_result = QueryResult.query.get_or_404(query_result_id)
|
||||||
|
if not (query_result.creator == current_user
|
||||||
|
or current_user.is_administrator()):
|
||||||
|
abort(403)
|
||||||
|
display_options_form = DisplayOptionsForm(
|
||||||
|
prefix='display-options-form',
|
||||||
|
results_per_page=request.args.get('results_per_page', 30),
|
||||||
|
result_context=request.args.get('context', 20)
|
||||||
|
)
|
||||||
|
query_result_file_path = os.path.join(
|
||||||
|
current_app.config['NOPAQUE_STORAGE'],
|
||||||
|
str(current_user.id),
|
||||||
|
'query_results',
|
||||||
|
str(query_result.id),
|
||||||
|
query_result.filename
|
||||||
|
)
|
||||||
|
with open(query_result_file_path, 'r') as query_result_file:
|
||||||
|
query_result_content = json.load(query_result_file)
|
||||||
|
return render_template('query_results/inspect_query_result.html.j2',
|
||||||
|
display_options_form=display_options_form,
|
||||||
|
query_result_content=query_result_content,
|
||||||
|
title='Inspect query result')
|
||||||
|
|
||||||
|
|
||||||
|
@query_results.route('/<int:query_result_id>/delete')
|
||||||
|
@login_required
|
||||||
|
def delete_query_result(query_result_id):
|
||||||
|
query_result = QueryResult.query.get_or_404(query_result_id)
|
||||||
|
if not (query_result.creator == current_user
|
||||||
|
or current_user.is_administrator()):
|
||||||
|
abort(403)
|
||||||
|
tasks.delete_result(query_result_id)
|
||||||
|
flash('Query result deleted!', 'result')
|
||||||
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
|
|
||||||
|
@query_results.route('/<int:query_result_id>/download')
|
||||||
|
@login_required
|
||||||
|
def download_query_result(query_result_id):
|
||||||
|
query_result = QueryResult.query.get_or_404(query_result_id)
|
||||||
|
if not (query_result.creator == current_user
|
||||||
|
or current_user.is_administrator()):
|
||||||
|
abort(403)
|
||||||
|
query_result_dir = os.path.join(current_app.config['NOPAQUE_STORAGE'],
|
||||||
|
str(current_user.id),
|
||||||
|
'query_results',
|
||||||
|
str(query_result.id))
|
||||||
|
return send_from_directory(as_attachment=True,
|
||||||
|
directory=query_result_dir,
|
||||||
|
filename=query_result.filename)
|
@ -14,6 +14,7 @@ nopaque.user.settings = {};
|
|||||||
nopaque.user.settings.darkMode = undefined;
|
nopaque.user.settings.darkMode = undefined;
|
||||||
nopaque.corporaSubscribers = [];
|
nopaque.corporaSubscribers = [];
|
||||||
nopaque.jobsSubscribers = [];
|
nopaque.jobsSubscribers = [];
|
||||||
|
nopaque.queryResultsSubscribers = [];
|
||||||
|
|
||||||
// Foreign user (user inspected with admin credentials) data
|
// Foreign user (user inspected with admin credentials) data
|
||||||
nopaque.foreignUser = {};
|
nopaque.foreignUser = {};
|
||||||
@ -22,6 +23,7 @@ nopaque.foreignUser.settings = {};
|
|||||||
nopaque.foreignUser.settings.darkMode = undefined;
|
nopaque.foreignUser.settings.darkMode = undefined;
|
||||||
nopaque.foreignCorporaSubscribers = [];
|
nopaque.foreignCorporaSubscribers = [];
|
||||||
nopaque.foreignJobsSubscribers = [];
|
nopaque.foreignJobsSubscribers = [];
|
||||||
|
nopaque.foreignQueryResultsSubscribers = [];
|
||||||
|
|
||||||
nopaque.flashedMessages = undefined;
|
nopaque.flashedMessages = undefined;
|
||||||
|
|
||||||
@ -38,6 +40,9 @@ nopaque.socket.init = function() {
|
|||||||
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) {
|
||||||
|
subscriber._init(nopaque.user.query_results);
|
||||||
|
}
|
||||||
RessourceList.modifyTooltips(false)
|
RessourceList.modifyTooltips(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -48,12 +53,16 @@ nopaque.socket.init = function() {
|
|||||||
nopaque.user = jsonpatch.apply_patch(nopaque.user, patch);
|
nopaque.user = jsonpatch.apply_patch(nopaque.user, patch);
|
||||||
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"));
|
||||||
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) {
|
||||||
|
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) {
|
||||||
/* "/jobs/{jobId}/..." -> ["{jobId}", ...] */
|
/* "/jobs/{jobId}/..." -> ["{jobId}", ...] */
|
||||||
@ -74,6 +83,9 @@ nopaque.socket.init = function() {
|
|||||||
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) {
|
||||||
|
subscriber._init(nopaque.foreignUser.query_results);
|
||||||
|
}
|
||||||
RessourceList.modifyTooltips(false)
|
RessourceList.modifyTooltips(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -84,8 +96,10 @@ nopaque.socket.init = function() {
|
|||||||
nopaque.foreignUser = jsonpatch.apply_patch(nopaque.foreignUser, patch);
|
nopaque.foreignUser = jsonpatch.apply_patch(nopaque.foreignUser, patch);
|
||||||
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"));
|
||||||
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);}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
class RessourceList extends List {
|
class RessourceList extends List {
|
||||||
constructor(idOrElement, subscriberList, type, options={}) {
|
constructor(idOrElement, subscriberList, type, options={}) {
|
||||||
if (!["corpus", "job", "result", "user", "job_input",
|
if (!["corpus", "corpus_file", "job", "job_input", "query_result", "result", "user"].includes(type)) {
|
||||||
"corpus_file"].includes(type)) {
|
|
||||||
console.error("Unknown Type!");
|
console.error("Unknown Type!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -81,8 +80,7 @@ class RessourceList extends List {
|
|||||||
|
|
||||||
|
|
||||||
RessourceList.dataMapper = {
|
RessourceList.dataMapper = {
|
||||||
// ### Mapping Genera Info
|
// A data mapper describes entitys rendered per row. One key value pair holds
|
||||||
//The Mapping describes entitys rendered per row. One key value pair holds
|
|
||||||
// the data to be rendered in the list.js table. Key has to correspond
|
// the data to be rendered in the list.js table. Key has to correspond
|
||||||
// with the ValueNames defined below in RessourceList.options ValueNames.
|
// with the ValueNames defined below in RessourceList.options ValueNames.
|
||||||
// Links are declared with double ticks(") around them. The key for links
|
// Links are declared with double ticks(") around them. The key for links
|
||||||
@ -96,8 +94,7 @@ RessourceList.dataMapper = {
|
|||||||
"analyse-link": ["analysing", "prepared", "start analysis"].includes(corpus.status) ? `/corpora/${corpus.id}/analyse` : "",
|
"analyse-link": ["analysing", "prepared", "start analysis"].includes(corpus.status) ? `/corpora/${corpus.id}/analyse` : "",
|
||||||
"edit-link": `/corpora/${corpus.id}`,
|
"edit-link": `/corpora/${corpus.id}`,
|
||||||
status: corpus.status,
|
status: corpus.status,
|
||||||
title: corpus.title
|
title: corpus.title}),
|
||||||
}),
|
|
||||||
// Mapping for corpus file entities shown in the corpus overview
|
// Mapping for corpus file entities shown in the corpus overview
|
||||||
corpus_file: corpus_file => ({filename: corpus_file.filename,
|
corpus_file: corpus_file => ({filename: corpus_file.filename,
|
||||||
author: corpus_file.author,
|
author: corpus_file.author,
|
||||||
@ -105,8 +102,7 @@ RessourceList.dataMapper = {
|
|||||||
publishing_year: corpus_file.publishing_year,
|
publishing_year: corpus_file.publishing_year,
|
||||||
"edit-link": `${corpus_file.corpus_id}/files/${corpus_file.id}/edit`,
|
"edit-link": `${corpus_file.corpus_id}/files/${corpus_file.id}/edit`,
|
||||||
"download-link": `${corpus_file.corpus_id}/files/${corpus_file.id}/download`,
|
"download-link": `${corpus_file.corpus_id}/files/${corpus_file.id}/download`,
|
||||||
"delete-modal": `delete-corpus-file-${corpus_file.id}-modal`
|
"delete-modal": `delete-corpus-file-${corpus_file.id}-modal`}),
|
||||||
}),
|
|
||||||
// Mapping for job entities shown in the dashboard table.
|
// Mapping for job entities shown in the dashboard table.
|
||||||
job: job => ({creation_date: job.creation_date,
|
job: job => ({creation_date: job.creation_date,
|
||||||
description: job.description,
|
description: job.description,
|
||||||
@ -114,16 +110,18 @@ RessourceList.dataMapper = {
|
|||||||
link: `/jobs/${job.id}`,
|
link: `/jobs/${job.id}`,
|
||||||
service: job.service,
|
service: job.service,
|
||||||
status: job.status,
|
status: job.status,
|
||||||
title: job.title
|
title: job.title}),
|
||||||
}),
|
|
||||||
// Mapping for job input files shown in table on every job page
|
// Mapping for job input files shown in table on every job page
|
||||||
job_input: job_input => ({filename: job_input.filename,
|
job_input: job_input => ({filename: job_input.filename,
|
||||||
id: job_input.job_id,
|
id: job_input.job_id,
|
||||||
"download-link": `${job_input.job_id}/inputs/${job_input.id}/download`
|
"download-link": `${job_input.job_id}/inputs/${job_input.id}/download`}),
|
||||||
}),
|
|
||||||
// Mapping for imported result entities from corpus analysis.
|
// Mapping for imported result entities from corpus analysis.
|
||||||
// Shown in imported results table
|
// Shown in imported results table
|
||||||
result: result => ({ query: result.query,
|
query_result: query_result => ({description: query_result.description,
|
||||||
|
id: query_result.id,
|
||||||
|
link: `/query_results/${query_result.id}`,
|
||||||
|
title: query_result.title}),
|
||||||
|
result: result => ({query: result.query,
|
||||||
match_count: result.match_count,
|
match_count: result.match_count,
|
||||||
corpus_name: result.corpus_name,
|
corpus_name: result.corpus_name,
|
||||||
corpus_creation_date: result.corpus_creation_date,
|
corpus_creation_date: result.corpus_creation_date,
|
||||||
@ -132,16 +130,14 @@ RessourceList.dataMapper = {
|
|||||||
"details-link": `${result.id}/details`,
|
"details-link": `${result.id}/details`,
|
||||||
"inspect-link": `${result.id}/inspect`,
|
"inspect-link": `${result.id}/inspect`,
|
||||||
"download-link": `${result.id}/file/${result.file_id}/download`,
|
"download-link": `${result.id}/file/${result.file_id}/download`,
|
||||||
"delete-modal": `delete-result-${result.id}-modal`
|
"delete-modal": `delete-result-${result.id}-modal`}),
|
||||||
}),
|
|
||||||
// Mapping for user entities shown in admin table
|
// Mapping for user entities shown in admin table
|
||||||
user: user => ({username: user.username,
|
user: user => ({username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
role_id: user.role_id,
|
role_id: user.role_id,
|
||||||
confirmed: user.confirmed,
|
confirmed: user.confirmed,
|
||||||
id: user.id,
|
id: user.id,
|
||||||
"profile-link": `user/${user.id}`
|
"profile-link": `user/${user.id}`})
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -289,6 +285,32 @@ RessourceList.options = {
|
|||||||
"id",
|
"id",
|
||||||
{name: "download-link", attr: "href"}]
|
{name: "download-link", attr: "href"}]
|
||||||
},
|
},
|
||||||
|
query_result: {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 class="actions right-align">
|
||||||
|
<a class="btn-floating tooltipped link waves-effect
|
||||||
|
waves-light"
|
||||||
|
data-position="top"
|
||||||
|
data-tooltip="Go to query result">
|
||||||
|
<i class="material-icons">send</i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>`,
|
||||||
|
// Job Value Names per column. Have to correspond with the keys from the
|
||||||
|
// Mapping step above.
|
||||||
|
valueNames: ["description",
|
||||||
|
"title",
|
||||||
|
{data: ["id"]},
|
||||||
|
{name: "link", attr: "href"}]
|
||||||
|
},
|
||||||
// Result (imported from corpus analysis) entity blueprint setting html
|
// Result (imported from corpus analysis) entity blueprint setting html
|
||||||
// strucuture per entity per row
|
// strucuture per entity per row
|
||||||
// Link classes have to correspond with Links defined in the Mapping process
|
// Link classes have to correspond with Links defined in the Mapping process
|
||||||
|
44
web/app/templates/query_results/add_query_result.html.j2
Normal file
44
web/app/templates/query_results/add_query_result.html.j2
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{% extends "nopaque.html.j2" %}
|
||||||
|
|
||||||
|
{% block page_content %}
|
||||||
|
<div class="col s12 m4">
|
||||||
|
<p>Fill out the following form to upload and view your exported query data from the corpus analsis.</p>
|
||||||
|
<a class="waves-effect waves-light btn" href="{{ url_for('main.dashboard') }}"><i class="material-icons left">arrow_back</i>Back to dashboard</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col s12 m8">
|
||||||
|
<form class="nopaque-submit-form" data-progress-modal="progress-modal">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content">
|
||||||
|
{{ add_query_result_form.hidden_tag() }}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col s12 m4">
|
||||||
|
{{ M.render_field(add_query_result_form.title, data_length='32', material_icon='title') }}
|
||||||
|
</div>
|
||||||
|
<div class="col s12 m8">
|
||||||
|
{{ M.render_field(add_query_result_form.description, data_length='255', material_icon='description') }}
|
||||||
|
</div>
|
||||||
|
<div class="col s12">
|
||||||
|
{{ M.render_field(add_query_result_form.file, accept='.json', placeholder='Choose your .json file') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-action right-align">
|
||||||
|
{{ M.render_field(add_query_result_form.submit, material_icon='send') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="progress-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h4><i class="material-icons prefix">file_upload</i> Uploading file...</h4>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="determinate" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<a href="#!" class="modal-close waves-effect waves-light btn red abort-request">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
211
web/app/templates/query_results/inspect_query_result.html.j2
Normal file
211
web/app/templates/query_results/inspect_query_result.html.j2
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
{% extends "nopaque.html.j2" %}
|
||||||
|
|
||||||
|
{% set headline = ' ' %}
|
||||||
|
|
||||||
|
{% set full_width = True %}
|
||||||
|
|
||||||
|
{% block page_content %}
|
||||||
|
<div class="col s12" id="query-display">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content" id="result-list" style="overflow: hidden;">
|
||||||
|
<div class="row" style="margin-bottom: 0px;">
|
||||||
|
<div class="col s12 m3 l3" id="results-info">
|
||||||
|
<div class="row section">
|
||||||
|
<h6 style="margin-top: 0px;">Infos</h6>
|
||||||
|
<div class="divider" style="margin-bottom: 10px;"></div>
|
||||||
|
<div class="col" id="infos">
|
||||||
|
<p>
|
||||||
|
Displaying
|
||||||
|
<span id="received-match-count">
|
||||||
|
</span> of
|
||||||
|
<span id="match-count"></span>
|
||||||
|
matches.
|
||||||
|
<br>
|
||||||
|
Matches occured in
|
||||||
|
<span id="text-lookup-count"></span>
|
||||||
|
corpus files:
|
||||||
|
<br>
|
||||||
|
<span id=text-titles></span>
|
||||||
|
</p>
|
||||||
|
<div class="progress hide" id="query-results-progress">
|
||||||
|
<div class="determinate" id="query-results-determinate"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col s12 m9 l9" id="actions-and-tools">
|
||||||
|
<div class="row section">
|
||||||
|
<div class="col s12 m3 l3" id="display">
|
||||||
|
<h6 style="margin-top: 0px;">Display</h6>
|
||||||
|
<div class="divider" style="margin-bottom: 10px;"></div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col s12">
|
||||||
|
<form id="display-options-form">
|
||||||
|
{{ M.render_field(display_options_form.results_per_page,
|
||||||
|
material_icon='format_list_numbered') }}
|
||||||
|
{{ M.render_field(display_options_form.result_context,
|
||||||
|
material_icon='short_text') }}
|
||||||
|
{{ M.render_field(display_options_form.expert_mode) }}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Table showing the query results -->
|
||||||
|
<div class="col s12">
|
||||||
|
<ul class="pagination paginationTop"></ul>
|
||||||
|
<table class="responsive-table highlight">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 2%">Nr.</th>
|
||||||
|
<th style="width: 3%">Title</th>
|
||||||
|
<th style="width: 25%">Left context</th>
|
||||||
|
<th style="width: 35%">Match</th>
|
||||||
|
<th style="width: 10%">Actions</th>
|
||||||
|
<th style="width: 25%">Right Context</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="list" id="query-results">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<ul class="pagination paginationBottom"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/nopaque.Results.js') }}">
|
||||||
|
</script>
|
||||||
|
<script src="{{ url_for('static', filename='js/nopaque.callbacks.js') }}">
|
||||||
|
</script>
|
||||||
|
<script src="{{ url_for('static', filename='js/nopaque.InteractionElement.js') }}">
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
// ###### global variables ######
|
||||||
|
var full_result_json;
|
||||||
|
var result_json;
|
||||||
|
var queryResultsDeterminateElement; // The progress bar for recieved results
|
||||||
|
var receivedMatchCountElement; // Nr. of loaded matches will be displayed in this element
|
||||||
|
var textLookupCountElement // Nr of texts the matches occured in will be shown in this element
|
||||||
|
var textTitlesElement; // matched text titles
|
||||||
|
var progress; // global progress value
|
||||||
|
var queryResultsProgressElement; // Div element holding the progress bar
|
||||||
|
var expertModeSwitchElement; // Expert mode switch Element
|
||||||
|
var matchCountElement; // Total nr. of matches will be displayed in this element
|
||||||
|
var interactionElements; // Interaction elements and their parameters
|
||||||
|
|
||||||
|
// ###### Defining local scope variables
|
||||||
|
let displayOptionsFormElement; // Form holding the display informations
|
||||||
|
let resultItems; // array of built html result items row element. This is called when results are transmitted and being recieved
|
||||||
|
let hitsPerPageInputElement;let contextPerItemElement; // Form Element for display option
|
||||||
|
let paginationElements;
|
||||||
|
|
||||||
|
// ###### Initializing variables ######
|
||||||
|
displayOptionsFormElement = document.getElementById("display-options-form");
|
||||||
|
resultItems = [];
|
||||||
|
queryResultsDeterminateElement = document.getElementById("query-results-determinate");
|
||||||
|
receivedMatchCountElement = document.getElementById("received-match-count");
|
||||||
|
textLookupCountElement = document.getElementById("text-lookup-count");
|
||||||
|
textTitlesElement = document.getElementById("text-titles");
|
||||||
|
queryResultsProgressElement = document.getElementById("query-results-progress");
|
||||||
|
expertModeSwitchElement = document.getElementById("display-options-form-expert_mode");
|
||||||
|
matchCountElement = document.getElementById("match-count");
|
||||||
|
hitsPerPageInputElement = document.getElementById("display-options-form-results_per_page");
|
||||||
|
contextPerItemElement = document.getElementById("display-options-form-result_context");
|
||||||
|
paginationElements = document.getElementsByClassName("pagination");
|
||||||
|
|
||||||
|
// js list options
|
||||||
|
displayOptionsData = ResultsList.getDisplayOptions(displayOptionsFormElement);
|
||||||
|
resultsListOptions = {page: displayOptionsData["resultsPerPage"],
|
||||||
|
pagination: [{
|
||||||
|
name: "paginationTop",
|
||||||
|
paginationClass: "paginationTop",
|
||||||
|
innerWindow: 8,
|
||||||
|
outerWindow: 1
|
||||||
|
}, {
|
||||||
|
paginationClass: "paginationBottom",
|
||||||
|
innerWindow: 8,
|
||||||
|
outerWindow: 1
|
||||||
|
}],
|
||||||
|
valueNames: ["titles", "lc", "c", "rc", {data: ["index"]}],
|
||||||
|
item: `<span></span>`
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
// ###### recreating chunk structure to reuse callback queryRenderResults()
|
||||||
|
full_result_json = {{ result_json|tojson|safe }};
|
||||||
|
result_json = {};
|
||||||
|
result_json.chunk = {};
|
||||||
|
result_json.chunk["cpos_lookup"] = full_result_json.cpos_lookup;
|
||||||
|
result_json.chunk["cpos_ranges"] = full_result_json.cpos_ranges;
|
||||||
|
result_json.chunk["matches"] = full_result_json.matches;
|
||||||
|
result_json.chunk["text_lookup"] = full_result_json.text_lookup;
|
||||||
|
|
||||||
|
// Init corpus analysis components
|
||||||
|
data = new Data();
|
||||||
|
resultsList = new ResultsList("result-list", resultsListOptions);
|
||||||
|
resultsMetaData = new MetaData();
|
||||||
|
results = new Results(data, resultsList, resultsMetaData);
|
||||||
|
results.clearAll(); // inits some object keys and values
|
||||||
|
// TODO: save metadate into results.metaData
|
||||||
|
|
||||||
|
// setting some initial values for user feedback
|
||||||
|
matchCountElement.innerText = full_result_json.match_count;
|
||||||
|
|
||||||
|
// Initialization of interactionElemnts
|
||||||
|
// An interactionElement is an object identifing a switch or button via
|
||||||
|
// htmlID. Callbacks are set for these elements which will be triggered on
|
||||||
|
// a pagination interaction by the user or if the status of the element has
|
||||||
|
// been altered. (Like the switche has ben turned on or off).
|
||||||
|
interactionElements = new Array();
|
||||||
|
let expertModeInteraction = new InteractionElement("display-options-form-expert_mode");
|
||||||
|
expertModeInteraction.setCallback("on",
|
||||||
|
results.jsList.expertModeOn,
|
||||||
|
results.jsList,
|
||||||
|
["query-display"])
|
||||||
|
expertModeInteraction.setCallback("off",
|
||||||
|
results.jsList.expertModeOff,
|
||||||
|
results.jsList,
|
||||||
|
["query-display"])
|
||||||
|
|
||||||
|
let activateInspectInteraction = new InteractionElement("inspect",
|
||||||
|
false);
|
||||||
|
activateInspectInteraction.setCallback("noCheck",
|
||||||
|
results.jsList.activateInspect,
|
||||||
|
results.jsList);
|
||||||
|
|
||||||
|
let changeContextInteraction = new InteractionElement("display-options-form-results_per_page",
|
||||||
|
false);
|
||||||
|
changeContextInteraction.setCallback("noCheck",
|
||||||
|
results.jsList.changeContext,
|
||||||
|
results.jsList)
|
||||||
|
interactionElements.push(expertModeInteraction, activateInspectInteraction, changeContextInteraction);
|
||||||
|
|
||||||
|
// checks if a change for every interactionElement happens and executes
|
||||||
|
// the callbacks accordingly
|
||||||
|
InteractionElement.onChangeExecute(interactionElements);
|
||||||
|
|
||||||
|
// eventListener if pagination is used to apply new context size to new page
|
||||||
|
// and also activate inspect match if progress is 100
|
||||||
|
// also adds more interaction buttons like add to sub results
|
||||||
|
for (let element of paginationElements) {
|
||||||
|
element.addEventListener("click", (event) => {
|
||||||
|
results.jsList.pageChangeEventInteractionHandler(interactionElements);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// render results in table imported parameter is true
|
||||||
|
queryRenderResults(result_json, true)
|
||||||
|
|
||||||
|
// live update of hits per page if hits per page value is changed
|
||||||
|
let changeHitsPerPageBind = results.jsList.changeHitsPerPage.bind(results.jsList);
|
||||||
|
hitsPerPageInputElement.onchange = changeHitsPerPageBind;
|
||||||
|
|
||||||
|
// live update of lr context per item if context value is changed
|
||||||
|
contextPerItemElement.onchange = results.jsList.changeContext;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
119
web/app/templates/query_results/query_result.html.j2
Normal file
119
web/app/templates/query_results/query_result.html.j2
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
{% extends "nopaque.html.j2" %}
|
||||||
|
|
||||||
|
{% block page_content %}
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col s12">
|
||||||
|
<p>Below the metadata for the results from the Corpus
|
||||||
|
<i>{{ query_result.query_metadata.corpus_name }}</i> generated with the query
|
||||||
|
<i>{{ query_result.query_metadata.query }}</i> are shown.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col s12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content" id="results">
|
||||||
|
<table class="responsive-table highlight">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Metadata Description</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for pair in query_result.query_metadata|dictsort %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ pair[0] }}</td>
|
||||||
|
{% if pair[0] == 'corpus_all_texts'
|
||||||
|
or pair[0] == 'text_lookup' %}
|
||||||
|
<td>
|
||||||
|
<table>
|
||||||
|
{% for key, value in pair[1].items() %}
|
||||||
|
<tr style="border-bottom: none;">
|
||||||
|
<td>
|
||||||
|
<i>{{ value['title'] }}</i> written
|
||||||
|
by <i>{{ value['author'] }}</i>
|
||||||
|
in <i>{{ value['publishing_year'] }}</i>
|
||||||
|
<a class="waves-effect
|
||||||
|
waves-light
|
||||||
|
btn
|
||||||
|
right
|
||||||
|
more-text-detials"
|
||||||
|
data-metadata-key="{{ pair[0] }}"
|
||||||
|
data-text-key="{{ key }}"
|
||||||
|
href="#modal-text-details">More
|
||||||
|
<i class="material-icons right"
|
||||||
|
data-metadata-key="{{ pair[0] }}"
|
||||||
|
data-text-key="{{ key }}">
|
||||||
|
info_outline
|
||||||
|
</i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
{% else %}
|
||||||
|
<td>{{ pair[1] }}</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="card-action right-align">
|
||||||
|
<a class="waves-effect waves-light btn left-align" href="{{ url_for('services.service', service='corpus_analysis') }}">Back To Overview<i class="material-icons right">arrow_back</i></a>
|
||||||
|
<a class="waves-effect waves-light btn" href="{{ url_for('query_results.inspect_query_result', query_result_id=query_result.id) }}">Inspect Results<i class="material-icons right">search</i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Structure -->
|
||||||
|
<div id="modal-text-details" class="modal modal-fixed-footer">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h4>Bibliographic data</h4>
|
||||||
|
<p id="bibliographic-data"></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<a href="#!" class="modal-close waves-effect waves-green red btn">Close</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var moreTextDetailsButtons;
|
||||||
|
moreTextDetailsButtons = document.getElementsByClassName("more-text-detials");
|
||||||
|
for (var btn of moreTextDetailsButtons) {
|
||||||
|
btn.onclick = () => {
|
||||||
|
let modal = document.getElementById("modal-text-details");
|
||||||
|
modal = M.Modal.init(modal, {"dismissible": true});
|
||||||
|
modal.open();
|
||||||
|
let metadataKey = event.target.dataset.metadataKey;
|
||||||
|
let textKey = event.target.dataset.textKey;
|
||||||
|
let textData = {{ query_result.query_metadata|tojson|safe }}[metadataKey][textKey];
|
||||||
|
console.log(textData);
|
||||||
|
let bibliographicData = document.getElementById("bibliographic-data");
|
||||||
|
bibliographicData.innerHTML = "";
|
||||||
|
let table = document.createElement("table");
|
||||||
|
for (let [key, value] of Object.entries(textData)) {
|
||||||
|
table.insertAdjacentHTML("afterbegin",
|
||||||
|
`
|
||||||
|
<tr>
|
||||||
|
<td>${key}</td>
|
||||||
|
<td>${value}</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
table.insertAdjacentHTML("afterbegin",
|
||||||
|
`
|
||||||
|
<thead>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</thead>
|
||||||
|
`)
|
||||||
|
bibliographicData.appendChild(table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
71
web/app/templates/query_results/results.html.j2
Normal file
71
web/app/templates/query_results/results.html.j2
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
{% extends "nopaque.html.j2" %}
|
||||||
|
|
||||||
|
{% set full_width = True %}
|
||||||
|
|
||||||
|
{% block page_content %}
|
||||||
|
|
||||||
|
<div class="col s12">
|
||||||
|
<p>This is an overview of all your imported results.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col s12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content" id="results">
|
||||||
|
<div class="input-field">
|
||||||
|
<i class="material-icons prefix">search</i>
|
||||||
|
<input id="search-results" class="search" type="search"></input>
|
||||||
|
<label for="search-results">Search results</label>
|
||||||
|
</div>
|
||||||
|
<ul class="pagination paginationTop"></ul>
|
||||||
|
<table class="highlight responsive-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="sort" data-sort="query">Query</th>
|
||||||
|
<th class="sort" data-sort="match_count">Match count</th>
|
||||||
|
<th class="sort" data-sort="corpus_name">Corpus name</th>
|
||||||
|
<th class="sort" data-sort="corpus_creation_date">Corpus creation date</th>
|
||||||
|
<th class="sort" data-sort="corpus_analysis_date">Corpus analysis date</th>
|
||||||
|
<th class="sort" data-sort="corpus_type">Corpus type</th>
|
||||||
|
<th>{# Actions #}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="list">
|
||||||
|
<tr class="show-if-only-child">
|
||||||
|
<td colspan="5">
|
||||||
|
<span class="card-title"><i class="material-icons left">folder</i>Nothing here...</span>
|
||||||
|
<p>No results yet imported.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<ul class="pagination paginationBottom"></ul>
|
||||||
|
</div>
|
||||||
|
<div class="card-action right-align">
|
||||||
|
<a class="waves-effect waves-light btn" href="{{ url_for('results.import_results') }}">Import Results<i class="material-icons right">file_upload</i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Delete modals #}
|
||||||
|
{% for result in results %}
|
||||||
|
<div id="delete-result-{{ result.id }}-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h4>Confirm result file deletion</h4>
|
||||||
|
<p>Do you really want to delete the result file created on <i>{{ result.corpus_analysis_date }}</i>?
|
||||||
|
<p>The file holds results for the query <i>{{ result.query }}</i>.</p>
|
||||||
|
<p>The file 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" href="{{ url_for('results.result_delete', result_id=result.id) }}"><i class="material-icons left">delete</i>Delete</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var ressources = {{ results|tojson|safe }};
|
||||||
|
var importedResultsList = new RessourceList("results", null, "result");
|
||||||
|
importedResultsList.addRessources(ressources);
|
||||||
|
RessourceList.modifyTooltips();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
@ -12,6 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col s12">
|
<div class="col s12">
|
||||||
|
<h3>My Corpora</h3>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-content" id="corpora">
|
<div class="card-content" id="corpora">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
@ -36,15 +37,54 @@
|
|||||||
<ul class="pagination"></ul>
|
<ul class="pagination"></ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action right-align">
|
<div class="card-action right-align">
|
||||||
<a class="waves-effect waves-light btn" href="{{ url_for('results.import_results') }}">Import Results<i class="material-icons right">file_upload</i></a>
|
|
||||||
<a class="waves-effect waves-light btn" href="{{ url_for('results.results_overview') }}">Show Imported Results<i class="material-icons right">folder</i></a>
|
|
||||||
<a class="waves-effect waves-light btn" href="{{ url_for('corpora.add_corpus') }}">New corpus<i class="material-icons right">add</i></a>
|
<a class="waves-effect waves-light btn" href="{{ url_for('corpora.add_corpus') }}">New corpus<i class="material-icons right">add</i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col s12">
|
||||||
|
<h3>My query results</h3>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content" id="query-results">
|
||||||
|
<div class="input-field">
|
||||||
|
<i class="material-icons prefix">search</i>
|
||||||
|
<input id="search-query-results" class="search" type="search"></input>
|
||||||
|
<label for="search-query-results">Search query result</label>
|
||||||
|
</div>
|
||||||
|
<ul class="pagination paginationTop"></ul>
|
||||||
|
<table class="highlight responsive-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{# Service #}</th>
|
||||||
|
<th>
|
||||||
|
<span class="sort" data-sort="title">Title</span>
|
||||||
|
<span class="sort" data-sort="description">Description</span>
|
||||||
|
</th>
|
||||||
|
<th>{# Actions #}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="list">
|
||||||
|
<tr class="show-if-only-child">
|
||||||
|
<td colspan="5">
|
||||||
|
<span class="card-title"><i class="material-icons left">folder</i>Nothing here...</span>
|
||||||
|
<p>No query results yet imported.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<ul class="pagination paginationBottom"></ul>
|
||||||
|
</div>
|
||||||
|
<div class="card-action right-align">
|
||||||
|
<a class="waves-effect waves-light btn" href="{{ url_for('query_results.add_query_result') }}">Add query result<i class="material-icons right">file_upload</i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var corpusList = new RessourceList("corpora", nopaque.corporaSubscribers,
|
var corpusList = new RessourceList("corpora", nopaque.corporaSubscribers,
|
||||||
"corpus", {page: 10});
|
"corpus", {page: 10});
|
||||||
|
var queryResultList = new RessourceList("query-results",
|
||||||
|
nopaque.queryResultsSubscribers,
|
||||||
|
"query_result", {page: 10});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
30
web/migrations/versions/33ec4d09b4ca_.py
Normal file
30
web/migrations/versions/33ec4d09b4ca_.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 33ec4d09b4ca
|
||||||
|
Revises: 4cf5e5606a83
|
||||||
|
Create Date: 2020-07-13 09:07:19.297185
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '33ec4d09b4ca'
|
||||||
|
down_revision = '4cf5e5606a83'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('query_results', sa.Column('description', sa.String(length=255), nullable=True))
|
||||||
|
op.add_column('query_results', sa.Column('title', sa.String(length=32), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('query_results', 'title')
|
||||||
|
op.drop_column('query_results', 'description')
|
||||||
|
# ### end Alembic commands ###
|
35
web/migrations/versions/4cf5e5606a83_.py
Normal file
35
web/migrations/versions/4cf5e5606a83_.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 4cf5e5606a83
|
||||||
|
Revises: e256f5cac75d
|
||||||
|
Create Date: 2020-07-13 08:30:57.369850
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '4cf5e5606a83'
|
||||||
|
down_revision = 'e256f5cac75d'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('query_results',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('filename', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('query_metadata', sa.JSON(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('query_results')
|
||||||
|
# ### end Alembic commands ###
|
@ -2,8 +2,8 @@ import eventlet
|
|||||||
eventlet.monkey_patch() # noqa
|
eventlet.monkey_patch() # noqa
|
||||||
from app import create_app, db, socketio
|
from app import create_app, db, socketio
|
||||||
from app.models import (Corpus, CorpusFile, Job, JobInput, JobResult,
|
from app.models import (Corpus, CorpusFile, Job, JobInput, JobResult,
|
||||||
NotificationData, NotificationEmailData, Result,
|
NotificationData, NotificationEmailData, QueryResult,
|
||||||
ResultFile, Role, User)
|
Result, ResultFile, Role, User)
|
||||||
from flask_migrate import Migrate, upgrade
|
from flask_migrate import Migrate, upgrade
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@ -21,6 +21,7 @@ def make_shell_context():
|
|||||||
'JobResult': JobResult,
|
'JobResult': JobResult,
|
||||||
'NotificationData': NotificationData,
|
'NotificationData': NotificationData,
|
||||||
'NotificationEmailData': NotificationEmailData,
|
'NotificationEmailData': NotificationEmailData,
|
||||||
|
'QueryResult': QueryResult,
|
||||||
'Result': Result,
|
'Result': Result,
|
||||||
'ResultFile': ResultFile,
|
'ResultFile': ResultFile,
|
||||||
'Role': Role,
|
'Role': Role,
|
||||||
|
Loading…
Reference in New Issue
Block a user