mirror of
				https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
				synced 2025-11-03 20:02:47 +00:00 
			
		
		
		
	Add a parallel package for query results.
This commit is contained in:
		@@ -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,16 +1,15 @@
 | 
			
		||||
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;
 | 
			
		||||
    }
 | 
			
		||||
    if (subscriberList) {
 | 
			
		||||
    super(idOrElement, {...RessourceList.options['common'],
 | 
			
		||||
                        ...RessourceList.options[type],
 | 
			
		||||
                        ...options});
 | 
			
		||||
    this.type = type;
 | 
			
		||||
    subscriberList.push(this);
 | 
			
		||||
      super(idOrElement, {...RessourceList.options['common'],
 | 
			
		||||
                          ...RessourceList.options[type],
 | 
			
		||||
                          ...options});
 | 
			
		||||
      this.type = type;
 | 
			
		||||
      subscriberList.push(this);
 | 
			
		||||
    } else {
 | 
			
		||||
      super(idOrElement, {...RessourceList.options['extended'],
 | 
			
		||||
                          ...RessourceList.options[type],
 | 
			
		||||
@@ -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,34 +110,34 @@ 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
 | 
			
		||||
  result: result => ({ query: result.query,
 | 
			
		||||
                       match_count: result.match_count,
 | 
			
		||||
                       corpus_name: result.corpus_name,
 | 
			
		||||
                       corpus_creation_date: result.corpus_creation_date,
 | 
			
		||||
                       corpus_analysis_date: result.corpus_analysis_date,
 | 
			
		||||
                       corpus_type : result.corpus_type,
 | 
			
		||||
                       "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`
 | 
			
		||||
                     }),
 | 
			
		||||
  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,
 | 
			
		||||
                      corpus_creation_date: result.corpus_creation_date,
 | 
			
		||||
                      corpus_analysis_date: result.corpus_analysis_date,
 | 
			
		||||
                      corpus_type : result.corpus_type,
 | 
			
		||||
                      "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`}),
 | 
			
		||||
  // 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,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user