Add function to import exported results and view them after the import

This commit is contained in:
Stephan Porada 2020-07-03 14:41:57 +02:00
parent a997fbe0ee
commit 1811623583
27 changed files with 880 additions and 32 deletions

View File

@ -55,4 +55,7 @@ def create_app(config_name):
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')
from .results import results as results_blueprint
app.register_blueprint(results_blueprint, url_prefix='/results')
return app return app

View File

@ -2,9 +2,9 @@ from flask_table import Table, Col, LinkCol
class AdminUserTable(Table): class AdminUserTable(Table):
""" '''
Declares the table describing colum by column. Declares the table describing colum by column.
""" '''
classes = ['highlight', 'responsive-table'] classes = ['highlight', 'responsive-table']
username = Col('Username', column_html_attrs={'class': 'username'}, username = Col('Username', column_html_attrs={'class': 'username'},
th_html_attrs={'class': 'sort', th_html_attrs={'class': 'sort',
@ -28,9 +28,9 @@ class AdminUserTable(Table):
class AdminUserItem(object): class AdminUserItem(object):
""" '''
Describes one item like one row per table. Describes one item like one row per table.
""" '''
def __init__(self, username, email, role_id, confirmed, id): def __init__(self, username, email, role_id, confirmed, id):
self.username = username self.username = username

View File

@ -129,6 +129,8 @@ class User(UserMixin, db.Model):
cascade='save-update, merge, delete') cascade='save-update, merge, delete')
jobs = db.relationship('Job', backref='creator', lazy='dynamic', jobs = db.relationship('Job', backref='creator', lazy='dynamic',
cascade='save-update, merge, delete') cascade='save-update, merge, delete')
results = db.relationship('Result', backref='creator', lazy='dynamic',
cascade='save-update, merge, delete')
def to_dict(self): def to_dict(self):
return {'id': self.id, return {'id': self.id,
@ -532,6 +534,34 @@ class Corpus(db.Model):
return '<Corpus {corpus_title}>'.format(corpus_title=self.title) return '<Corpus {corpus_title}>'.format(corpus_title=self.title)
class Result (db.Model):
'''
Class to define a result set of one query.
'''
__tablename__ = 'results'
id = db.Column(db.Integer, primary_key=True)
# Foreign keys
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
# Relationships'
corpus_metadata = db.Column(db.JSON())
file = db.relationship('ResultFile', backref='result', lazy='dynamic',
cascade='save-update, merge, delete')
class ResultFile(db.Model):
'''
Class to define a ResultFile
'''
__tablename__ = 'result_files'
# Primary key
id = db.Column(db.Integer, primary_key=True)
# Foreign keys
result_id = db.Column(db.Integer, db.ForeignKey('results.id'))
# Fields
filename = db.Column(db.String(255))
dir = db.Column(db.String(255))
''' '''
' Flask-Login is told to use the applications custom anonymous user by setting ' Flask-Login is told to use the applications custom anonymous user by setting
' its class in the login_manager.anonymous_user attribute. ' its class in the login_manager.anonymous_user attribute.

View File

@ -0,0 +1,5 @@
from flask import Blueprint
results = Blueprint('results', __name__)
from . import views # noqa

0
web/app/results/forms.py Normal file
View File

View File

46
web/app/results/views.py Normal file
View File

@ -0,0 +1,46 @@
from . import results
from ..models import Result
from flask import abort, render_template, current_app, request
from flask_login import current_user, login_required
from ..corpora.forms import DisplayOptionsForm
import json
import os
@results.route('/<int:result_id>/details')
@login_required
def result_details(result_id):
'''
View to show metadate and details about on imported result file.
'''
result = Result.query.get_or_404(result_id)
if not (result.creator == current_user or current_user.is_administrator()):
abort(403)
return render_template('results/result_details.html.j2',
result=result,
title='Result Details')
@results.route('/<int:result_id>/inspect')
@login_required
def result_inspect(result_id):
'''
View to inspect one importe result file in a corpus analysis like interface
'''
display_options_form = DisplayOptionsForm(
prefix='display-options-form',
result_context=request.args.get('context', 20),
results_per_page=request.args.get('results_per_page', 30))
result = Result.query.get_or_404(result_id)
result_file_path = os.path.join(current_app.config['NOPAQUE_STORAGE'],
result.file[0].dir,
result.file[0].filename)
with open(result_file_path, 'r') as result_json:
result_json = json.load(result_json)
if not (result.creator == current_user or current_user.is_administrator()):
abort(403)
return render_template('results/result_inspect.html.j2',
display_options_form=display_options_form,
result=result,
result_json=result_json,
title='Result Insepct')

18
web/app/services/forms.py Normal file
View File

@ -0,0 +1,18 @@
from flask_wtf import FlaskForm
from werkzeug.utils import secure_filename
from wtforms import FileField, SubmitField, ValidationError
from wtforms.validators import DataRequired
class ImportResultsForm(FlaskForm):
'''
Form used to import one result json file.
'''
file = FileField('File', validators=[DataRequired()])
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)

View File

@ -0,0 +1,65 @@
from flask_table import Table, Col, DatetimeCol, LinkCol
class ResultTable(Table):
'''
Declares the Table showing results. Declaration is column by column.
'''
classes = ['highlight', 'responsive-table']
query = Col('Query', column_html_attrs={'class': 'query'},
th_html_attrs={'class': 'sort',
'data-sort': 'query'})
match_count = Col('Match count', column_html_attrs={'class':
'match-count'},
th_html_attrs={'class': 'sort',
'data-sort': 'match-count'})
corpus_name = Col('Corpus name', column_html_attrs={'class':
'corpus-name'},
th_html_attrs={'class': 'sort',
'data-sort': 'corpus-name'})
corpus_creation_date = DatetimeCol('Corpus creation date',
column_html_attrs={'class':
'corpus-creation- date'}, # noqa
th_html_attrs={'class': 'sort',
'data-sort':
'corpus-creation-date'},
datetime_format='dd/MM/yyyy, HH:mm:ss a') # noqa
corpus_analysis_date = DatetimeCol('Date of result creation',
column_html_attrs={'class':
'corpus-analysis-data'}, # noqa
th_html_attrs={'class': 'sort',
'data-sort':
'corpus-analysis-data'},
datetime_format='dd/MM/yyyy, HH:mm:ss a') # noqa
corpus_type = Col('Result Type',
column_html_attrs={'class':
'corpus-type'},
th_html_attrs={'class': 'sort',
'data-sort':
'corpus-type'})
details = LinkCol('Details', 'results.result_details',
url_kwargs=dict(result_id='id'),
anchor_attrs={'class': 'waves-effect waves-light btn-floating'}, # noqa
text_fallback='<i class="material-icons">info_outline</i>') # noqa
inspect = LinkCol('Inspect', 'results.result_inspect',
url_kwargs=dict(result_id='id'),
anchor_attrs={'class': 'waves-effect waves-light btn-floating'}, # noqa
text_fallback='<i class="material-icons">search</i>') # noqa
# TODO: Maybe somehow fix taht there are two columns fpr two action buttons
# Or maybe just get rid of flask tables?
class ResultItem(object):
'''
Describes one result item row.
'''
def __init__(self, query, match_count, corpus_name, corpus_creation_date,
corpus_analysis_date, corpus_type, id):
self.query = query
self.match_count = match_count
self.corpus_name = corpus_name
self.corpus_creation_date = corpus_creation_date
self.corpus_analysis_date = corpus_analysis_date
self.corpus_type = corpus_type
self.id = id

View File

@ -1,13 +1,17 @@
from flask import (abort, current_app, flash, make_response, render_template, from flask import (abort, current_app, flash, make_response, render_template,
url_for) url_for)
from flask_login import current_user, login_required from flask_login import current_user, login_required
from .forms import ImportResultsForm
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from . import services from . import services
from .. import db from .. import db
from ..jobs.forms import AddFileSetupJobForm, AddNLPJobForm, AddOCRJobForm from ..jobs.forms import AddFileSetupJobForm, AddNLPJobForm, AddOCRJobForm
from ..models import Job, JobInput from ..models import Job, JobInput, Result, ResultFile, User
from .tables import ResultTable, ResultItem
import json import json
import os import os
import html
from datetime import datetime
SERVICES = {'corpus_analysis': {'name': 'Corpus analysis'}, SERVICES = {'corpus_analysis': {'name': 'Corpus analysis'},
@ -81,3 +85,84 @@ def service(service):
return render_template('services/{}.html.j2'.format(service), return render_template('services/{}.html.j2'.format(service),
title=SERVICES[service]['name'], title=SERVICES[service]['name'],
add_job_form=add_job_form) add_job_form=add_job_form)
@services.route('/import_results', methods=['GET', 'POST'])
@login_required
def import_results():
'''
View to import one json result file. Uses the ImportReultFileForm.
'''
# TODO: Build in a check if uploaded json is actually a result file and
# not something different
# Add the possibility to add several result files at once.
import_results_form = ImportResultsForm(prefix='add-result-file-form')
if import_results_form.is_submitted():
if not import_results_form.validate():
return make_response(import_results_form.errors, 400)
# Save the file
# result creation only happens on file save to avoid creating a result
# object in the db everytime by just visiting the import_results page
result = Result(user_id=current_user.id)
db.session.add(result)
db.session.commit()
if not (result.creator == current_user
or current_user.is_administrator()):
abort(403)
dir = os.path.join(str(result.user_id),
'results',
'corpus_analysis_results',
str(result.id))
abs_dir = os.path.join(current_app.config['NOPAQUE_STORAGE'], dir)
abs_file_path = os.path.join(abs_dir,
import_results_form.file.data.filename)
os.makedirs(abs_dir)
import_results_form.file.data.save(abs_file_path)
# Saves all needed metadata entries in one json field
with open(abs_file_path, 'r') as f:
corpus_metadata = json.load(f)
del corpus_metadata['matches']
del corpus_metadata['cpos_lookup']
result_file = ResultFile(
result_id=result.id,
dir=dir,
filename=import_results_form.file.data.filename)
result.corpus_metadata = corpus_metadata
db.session.add(result_file)
db.session.commit()
flash('Result file added!', 'result')
return make_response(
{'redirect_url': url_for('services.results')},
201)
return render_template('services/import_results.html.j2',
import_results_form=import_results_form,
title='Add corpus file')
@services.route('/results')
@login_required
def results():
'''
Shows an overview of imported results.
'''
# get all results of current user
results = User.query.get(current_user.id).results
# create table row for every result#
def __p_time(time_str):
return datetime.strptime(time_str, '%Y-%m-%dT%H:%M:%S.%f')
items = [ResultItem(r.corpus_metadata['query'],
r.corpus_metadata['match_count'],
r.corpus_metadata['corpus_name'],
__p_time(r.corpus_metadata['corpus_creation_date']),
__p_time(r.corpus_metadata['corpus_analysis_date']),
r.corpus_metadata['corpus_type'],
r.id) for r in results]
# create table with items and save it as html
table = html.unescape(ResultTable(items).__html__())
# add class=list to table body with string replacement
table = table.replace('tbody', 'tbody class=list', 1)
return render_template('services/results.html.j2',
title='Imported Results',
table=table)

View File

@ -29,4 +29,32 @@ class InteractionElement {
let boundedCallback = callback["function"].bind(callback.bindThis); let boundedCallback = callback["function"].bind(callback.bindThis);
return boundedCallback; return boundedCallback;
} }
}
static onChangeExecute(interactionElements) {
// checks if a change for every interactionElement happens and executes
// the callbacks accordingly
// TODO: This function scould be a static function of the Class InteractionElements
// This class does not exist yet. The Class InteractionElements should hold
// a list of InteractionElement objects. onChangeExecute loops over InteractionElements
// and executes the callbacks as mentioned accordingly. An additional
// InteractionElements Class is logically right but also makes things a little more
// complex. It is not yet decided.
for (let interaction of interactionElements) {
if (interaction.checkStatus) {
interaction.element.addEventListener("change", (event) => {
if (event.target.checked) {
let f_on = interaction.bindThisToCallback("on");
let args_on = interaction.callbacks.on.args;
f_on(...args_on);
} else if (!event.target.checked){
let f_off = interaction.bindThisToCallback("off");
let args_off = interaction.callbacks.off.args;
f_off(...args_off);
}
});
} else {
continue
}
};
}
}

View File

@ -63,7 +63,7 @@ function querySetup(payload) {
// This callback is called on socket.on "query_results" // This callback is called on socket.on "query_results"
// this handels the incoming result chunks // this handels the incoming result chunks
function queryRenderResults(payload) { function queryRenderResults(payload, imported=false) {
let resultItems; // array of built html result items row element let resultItems; // array of built html result items row element
// This is called when results are transmitted and being recieved // This is called when results are transmitted and being recieved
console.log("Current recieved chunk:", payload.chunk); console.log("Current recieved chunk:", payload.chunk);
@ -102,18 +102,23 @@ function queryRenderResults(payload) {
console.log("Results recieved:", results.data); console.log("Results recieved:", results.data);
// upate progress status // upate progress status
progress = payload.progress; // global declaration progress = payload.progress; // global declaration
if (progress === 100) { if (progress === 100 && !imported) {
queryResultsProgressElement.classList.add("hide"); queryResultsProgressElement.classList.add("hide");
queryResultsUserFeedbackElement.classList.add("hide"); queryResultsUserFeedbackElement.classList.add("hide");
queryResultsExportElement.classList.remove("disabled"); queryResultsExportElement.classList.remove("disabled");
addToSubResultsElement.removeAttribute("disabled"); addToSubResultsElement.removeAttribute("disabled");
results.jsList.activateInspect();
// inital expert mode check and sub results activation // inital expert mode check and sub results activation
results.jsList.activateInspect();
if (addToSubResultsElement.checked) { if (addToSubResultsElement.checked) {
results.jsList.activateAddToSubResults(); results.jsList.activateAddToSubResults();
} }
if (expertModeSwitchElement.checked) { if (expertModeSwitchElement.checked) {
results.jsList.expertModeOn("query-display"); results.jsList.expertModeOn("query-display");
} }
} else if (imported) {
results.jsList.activateInspect();
if (expertModeSwitchElement.checked) {
results.jsList.expertModeOn("query-display");
}
} }
} }

View File

@ -200,7 +200,6 @@
</tbody> </tbody>
</table> </table>
<ul class="pagination paginationBottom"></ul> <ul class="pagination paginationBottom"></ul>
</div> </div>
</div> </div>
</div> </div>
@ -567,23 +566,7 @@
// checks if a change for every interactionElement happens and executes // checks if a change for every interactionElement happens and executes
// the callbacks accordingly // the callbacks accordingly
for (let interaction of interactionElements) { InteractionElement.onChangeExecute(interactionElements);
if (interaction.checkStatus) {
interaction.element.addEventListener("change", (event) => {
if (event.target.checked) {
let f_on = interaction.bindThisToCallback("on");
let args_on = interaction.callbacks.on.args;
f_on(...args_on);
} else if (!event.target.checked){
let f_off = interaction.bindThisToCallback("off");
let args_off = interaction.callbacks.off.args;
f_off(...args_off);
}
});
} else {
continue
}
};
// eventListener if pagination is used to apply new context size to new page // eventListener if pagination is used to apply new context size to new page
// and also activate inspect match if progress is 100 // and also activate inspect match if progress is 100

View File

@ -27,13 +27,13 @@
<div class="row"> <div class="row">
<div class="col s12 m6"> <div class="col s12 m6">
<div class="input-field"> <div class="input-field">
<input disabled value="{{ corpus.creation_date.strftime('%m/%d/%Y, %H:%M:%S %p') }}" id="creation-date" type="text" class="validate"> <input disabled value="{{ corpus.creation_date.strftime('%d/%m/%Y, %H:%M:%S %p') }}" id="creation-date" type="text" class="validate">
<label for="creation-date">Creation date</label> <label for="creation-date">Creation date</label>
</div> </div>
</div> </div>
<div class="col s12 m6"> <div class="col s12 m6">
<div class="input-field"> <div class="input-field">
<input disabled value="{{ corpus.last_edited_date.strftime('%m/%d/%Y, %H:%M:%S %p') }}" id="last_edited_date" type="text" class="validate"> <input disabled value="{{ corpus.last_edited_date.strftime('%d/%m/%Y, %H:%M:%S %p') }}" id="last_edited_date" type="text" class="validate">
<label for="creation-date">Last edited</label> <label for="creation-date">Last edited</label>
</div> </div>
</div> </div>

View File

@ -36,7 +36,7 @@
<div class="col s12 m6"> <div class="col s12 m6">
<div class="input-field"> <div class="input-field">
<input disabled id="creation-date" type="text" value="{{ job.creation_date.strftime('%m/%d/%Y, %H:%M:%S %p') }}"> <input disabled id="creation-date" type="text" value="{{ job.creation_date.strftime('%d/%m/%Y, %H:%M:%S %p') }}">
<label for="creation-date">Creation date</label> <label for="creation-date">Creation date</label>
</div> </div>
</div> </div>

View File

@ -28,6 +28,7 @@
<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('services.results') }}">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>

View File

@ -0,0 +1,118 @@
{% extends "nopaque.html.j2" %}
{% block page_content %}
<div class="col s12">
<p>Below the metadata for the results from the Corpus
<i>{{ result.corpus_metadata.corpus_name }}</i> generated with the query
<i>{{ result.corpus_metadata.query }}</i> are shown.
</p>
<p>{{ texts_metadata }}</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 result.corpus_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" href="{{ url_for('services.import_results') }}">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 = {{ result.corpus_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 %}

View 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 %}

View File

@ -36,6 +36,7 @@
<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('services.results') }}">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>

View File

@ -0,0 +1,39 @@
{% extends "nopaque.html.j2" %}
{% block page_content %}
<div class="col s12 m4">
<p>Fill out the following form to upload and view Results and Sub Results
exported from the Corpus analsis Tool.</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">
{{ import_results_form.hidden_tag() }}
<div class="row">
<div class="col s12">
{{ M.render_field(import_results_form.file, accept='.json', placeholder='Choose your .json file') }}
</div>
</div>
</div>
<div class="card-action right-align">
{{ M.render_field(import_results_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 %}

View File

@ -0,0 +1,52 @@
{% extends "nopaque.html.j2" %}
{% 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 }}
<ul class="pagination paginationBottom"></ul>
<ul class="pagination"></ul>
</div>
<div class="card-action right-align">
<a class="waves-effect waves-light btn" href="{{ url_for('services.import_results') }}">Import Results<i class="material-icons right">file_upload</i></a>
</div>
</div>
</div>
<script>
var options = {page: 10,
pagination: [
{
name: "paginationTop",
paginationClass: "paginationTop",
innerWindow: 8,
outerWindow: 1
},
{
paginationClass: "paginationBottom",
innerWindow: 8,
outerWindow: 1
}
],
valueNames: ['query',
'match-count',
'corpus-name',
'corpus-creation-date',
'corpus-analysis-date',
'corpus-type']
};
var resultsList = new List('results', options);
</script>
{% endblock %}

View File

@ -0,0 +1,28 @@
"""empty message
Revision ID: 0d7aed934679
Revises: b15366b25bea
Create Date: 2020-06-30 13:57:48.782173
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0d7aed934679'
down_revision = 'b15366b25bea'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('result_files', sa.Column('corpus_metadata', sa.JSON(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('result_files', 'corpus_metadata')
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""empty message
Revision ID: 318074622d14
Revises: 0d7aed934679
Create Date: 2020-06-30 14:00:18.968769
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '318074622d14'
down_revision = '0d7aed934679'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('result_files', 'corpus_metadata')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('result_files', sa.Column('corpus_metadata', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True))
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""empty message
Revision ID: 389bcf564726
Revises: 318074622d14
Create Date: 2020-06-30 14:03:33.384379
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '389bcf564726'
down_revision = '318074622d14'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('result_files', sa.Column('corpus_metadata', sa.JSON(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('result_files', 'corpus_metadata')
# ### end Alembic commands ###

View File

@ -0,0 +1,42 @@
"""empty message
Revision ID: b15366b25bea
Revises: 4886241e0f5d
Create Date: 2020-06-29 13:41:14.394680
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b15366b25bea'
down_revision = '4886241e0f5d'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('results',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('result_files',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('result_id', sa.Integer(), nullable=True),
sa.Column('filename', sa.String(length=255), nullable=True),
sa.Column('dir', sa.String(length=255), nullable=True),
sa.ForeignKeyConstraint(['result_id'], ['results.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('result_files')
op.drop_table('results')
# ### end Alembic commands ###

View File

@ -0,0 +1,30 @@
"""empty message
Revision ID: e256f5cac75d
Revises: 389bcf564726
Create Date: 2020-07-01 07:45:24.637861
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'e256f5cac75d'
down_revision = '389bcf564726'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('result_files', 'corpus_metadata')
op.add_column('results', sa.Column('corpus_metadata', sa.JSON(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('results', 'corpus_metadata')
op.add_column('result_files', sa.Column('corpus_metadata', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True))
# ### end Alembic commands ###

View File

@ -2,11 +2,11 @@ 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, Role, User) NotificationData, NotificationEmailData, Result,
ResultFile, Role, User)
from flask_migrate import Migrate, upgrade from flask_migrate import Migrate, upgrade
import os import os
app = create_app(os.getenv('FLASK_CONFIG') or 'default') app = create_app(os.getenv('FLASK_CONFIG') or 'default')
migrate = Migrate(app, db) migrate = Migrate(app, db)
@ -21,6 +21,8 @@ def make_shell_context():
'JobResult': JobResult, 'JobResult': JobResult,
'NotificationData': NotificationData, 'NotificationData': NotificationData,
'NotificationEmailData': NotificationEmailData, 'NotificationEmailData': NotificationEmailData,
'Result': Result,
'ResultFile': ResultFile,
'Role': Role, 'Role': Role,
'User': User} 'User': User}