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
|
||||
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
|
||||
app.register_blueprint(services_blueprint, url_prefix='/services')
|
||||
|
||||
|
@ -135,6 +135,10 @@ class User(UserMixin, db.Model):
|
||||
cascade='save-update, merge, delete')
|
||||
results = db.relationship('Result', backref='creator', lazy='dynamic',
|
||||
cascade='save-update, merge, delete')
|
||||
query_results = db.relationship('QueryResult',
|
||||
backref='creator',
|
||||
cascade='save-update, merge, delete',
|
||||
lazy='dynamic')
|
||||
|
||||
def to_dict(self):
|
||||
return {'id': self.id,
|
||||
@ -151,7 +155,9 @@ class User(UserMixin, db.Model):
|
||||
self.setting_job_status_site_notifications},
|
||||
'corpora': {corpus.id: corpus.to_dict()
|
||||
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):
|
||||
'''
|
||||
@ -616,6 +622,43 @@ class Corpus(db.Model):
|
||||
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 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.corporaSubscribers = [];
|
||||
nopaque.jobsSubscribers = [];
|
||||
nopaque.queryResultsSubscribers = [];
|
||||
|
||||
// Foreign user (user inspected with admin credentials) data
|
||||
nopaque.foreignUser = {};
|
||||
@ -22,6 +23,7 @@ nopaque.foreignUser.settings = {};
|
||||
nopaque.foreignUser.settings.darkMode = undefined;
|
||||
nopaque.foreignCorporaSubscribers = [];
|
||||
nopaque.foreignJobsSubscribers = [];
|
||||
nopaque.foreignQueryResultsSubscribers = [];
|
||||
|
||||
nopaque.flashedMessages = undefined;
|
||||
|
||||
@ -38,6 +40,9 @@ nopaque.socket.init = function() {
|
||||
for (let subscriber of nopaque.jobsSubscribers) {
|
||||
subscriber._init(nopaque.user.jobs);
|
||||
}
|
||||
for (let subscriber of nopaque.queryResultsSubscribers) {
|
||||
subscriber._init(nopaque.user.query_results);
|
||||
}
|
||||
RessourceList.modifyTooltips(false)
|
||||
});
|
||||
|
||||
@ -48,12 +53,16 @@ nopaque.socket.init = function() {
|
||||
nopaque.user = jsonpatch.apply_patch(nopaque.user, patch);
|
||||
corpora_patch = patch.filter(operation => operation.path.startsWith("/corpora"));
|
||||
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) {
|
||||
subscriber._update(corpora_patch);
|
||||
}
|
||||
for (let subscriber of nopaque.jobsSubscribers) {
|
||||
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)) {
|
||||
for (operation of jobs_patch) {
|
||||
/* "/jobs/{jobId}/..." -> ["{jobId}", ...] */
|
||||
@ -74,6 +83,9 @@ nopaque.socket.init = function() {
|
||||
for (let subscriber of nopaque.foreignJobsSubscribers) {
|
||||
subscriber._init(nopaque.foreignUser.jobs);
|
||||
}
|
||||
for (let subscriber of nopaque.foreignQueryResultsSubscribers) {
|
||||
subscriber._init(nopaque.foreignUser.query_results);
|
||||
}
|
||||
RessourceList.modifyTooltips(false)
|
||||
});
|
||||
|
||||
@ -84,8 +96,10 @@ nopaque.socket.init = function() {
|
||||
nopaque.foreignUser = jsonpatch.apply_patch(nopaque.foreignUser, patch);
|
||||
corpora_patch = patch.filter(operation => operation.path.startsWith("/corpora"));
|
||||
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.foreignJobsSubscribers) {subscriber._update(jobs_patch);}
|
||||
for (let subscriber of nopaque.foreignQueryResultsSubscribers) {subscriber._update(query_results_patch);}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
class RessourceList extends List {
|
||||
constructor(idOrElement, subscriberList, type, options={}) {
|
||||
if (!["corpus", "job", "result", "user", "job_input",
|
||||
"corpus_file"].includes(type)) {
|
||||
if (!["corpus", "corpus_file", "job", "job_input", "query_result", "result", "user"].includes(type)) {
|
||||
console.error("Unknown Type!");
|
||||
return;
|
||||
}
|
||||
@ -81,8 +80,7 @@ class RessourceList extends List {
|
||||
|
||||
|
||||
RessourceList.dataMapper = {
|
||||
// ### Mapping Genera Info
|
||||
//The Mapping describes entitys rendered per row. One key value pair holds
|
||||
// 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
|
||||
@ -96,8 +94,7 @@ RessourceList.dataMapper = {
|
||||
"analyse-link": ["analysing", "prepared", "start analysis"].includes(corpus.status) ? `/corpora/${corpus.id}/analyse` : "",
|
||||
"edit-link": `/corpora/${corpus.id}`,
|
||||
status: corpus.status,
|
||||
title: corpus.title
|
||||
}),
|
||||
title: corpus.title}),
|
||||
// Mapping for corpus file entities shown in the corpus overview
|
||||
corpus_file: corpus_file => ({filename: corpus_file.filename,
|
||||
author: corpus_file.author,
|
||||
@ -105,8 +102,7 @@ RessourceList.dataMapper = {
|
||||
publishing_year: corpus_file.publishing_year,
|
||||
"edit-link": `${corpus_file.corpus_id}/files/${corpus_file.id}/edit`,
|
||||
"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.
|
||||
job: job => ({creation_date: job.creation_date,
|
||||
description: job.description,
|
||||
@ -114,15 +110,17 @@ RessourceList.dataMapper = {
|
||||
link: `/jobs/${job.id}`,
|
||||
service: job.service,
|
||||
status: job.status,
|
||||
title: job.title
|
||||
}),
|
||||
title: job.title}),
|
||||
// Mapping for job input files shown in table on every job page
|
||||
job_input: job_input => ({filename: job_input.filename,
|
||||
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.
|
||||
// Shown in imported results table
|
||||
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,
|
||||
corpus_name: result.corpus_name,
|
||||
@ -132,16 +130,14 @@ RessourceList.dataMapper = {
|
||||
"details-link": `${result.id}/details`,
|
||||
"inspect-link": `${result.id}/inspect`,
|
||||
"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
|
||||
user: user => ({username: user.username,
|
||||
email: user.email,
|
||||
role_id: user.role_id,
|
||||
confirmed: user.confirmed,
|
||||
id: user.id,
|
||||
"profile-link": `user/${user.id}`
|
||||
})
|
||||
"profile-link": `user/${user.id}`})
|
||||
};
|
||||
|
||||
|
||||
@ -289,6 +285,32 @@ RessourceList.options = {
|
||||
"id",
|
||||
{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
|
||||
// strucuture per entity per row
|
||||
// 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 class="col s12">
|
||||
<h3>My Corpora</h3>
|
||||
<div class="card">
|
||||
<div class="card-content" id="corpora">
|
||||
<div class="input-field">
|
||||
@ -36,15 +37,54 @@
|
||||
<ul class="pagination"></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>
|
||||
<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>
|
||||
</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>
|
||||
var corpusList = new RessourceList("corpora", nopaque.corporaSubscribers,
|
||||
"corpus", {page: 10});
|
||||
var queryResultList = new RessourceList("query-results",
|
||||
nopaque.queryResultsSubscribers,
|
||||
"query_result", {page: 10});
|
||||
</script>
|
||||
{% 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
|
||||
from app import create_app, db, socketio
|
||||
from app.models import (Corpus, CorpusFile, Job, JobInput, JobResult,
|
||||
NotificationData, NotificationEmailData, Result,
|
||||
ResultFile, Role, User)
|
||||
NotificationData, NotificationEmailData, QueryResult,
|
||||
Result, ResultFile, Role, User)
|
||||
from flask_migrate import Migrate, upgrade
|
||||
import os
|
||||
|
||||
@ -21,6 +21,7 @@ def make_shell_context():
|
||||
'JobResult': JobResult,
|
||||
'NotificationData': NotificationData,
|
||||
'NotificationEmailData': NotificationEmailData,
|
||||
'QueryResult': QueryResult,
|
||||
'Result': Result,
|
||||
'ResultFile': ResultFile,
|
||||
'Role': Role,
|
||||
|
Loading…
Reference in New Issue
Block a user