diff --git a/web/app/__init__.py b/web/app/__init__.py index b25e7cd4..7592345e 100644 --- a/web/app/__init__.py +++ b/web/app/__init__.py @@ -55,4 +55,7 @@ def create_app(config_name): from .services import services as services_blueprint 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 diff --git a/web/app/admin/tables.py b/web/app/admin/tables.py index 40452278..52196784 100644 --- a/web/app/admin/tables.py +++ b/web/app/admin/tables.py @@ -2,9 +2,9 @@ from flask_table import Table, Col, LinkCol class AdminUserTable(Table): - """ + ''' Declares the table describing colum by column. - """ + ''' classes = ['highlight', 'responsive-table'] username = Col('Username', column_html_attrs={'class': 'username'}, th_html_attrs={'class': 'sort', @@ -28,9 +28,9 @@ class AdminUserTable(Table): class AdminUserItem(object): - """ + ''' Describes one item like one row per table. - """ + ''' def __init__(self, username, email, role_id, confirmed, id): self.username = username diff --git a/web/app/models.py b/web/app/models.py index 43f9e90d..5d0b7ff5 100644 --- a/web/app/models.py +++ b/web/app/models.py @@ -129,6 +129,8 @@ class User(UserMixin, db.Model): cascade='save-update, merge, delete') jobs = db.relationship('Job', backref='creator', lazy='dynamic', cascade='save-update, merge, delete') + results = db.relationship('Result', backref='creator', lazy='dynamic', + cascade='save-update, merge, delete') def to_dict(self): return {'id': self.id, @@ -532,6 +534,34 @@ class Corpus(db.Model): return ''.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 application’s custom anonymous user by setting ' its class in the login_manager.anonymous_user attribute. diff --git a/web/app/results/__init__.py b/web/app/results/__init__.py new file mode 100644 index 00000000..2f1f59a7 --- /dev/null +++ b/web/app/results/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + + +results = Blueprint('results', __name__) +from . import views # noqa \ No newline at end of file diff --git a/web/app/results/forms.py b/web/app/results/forms.py new file mode 100644 index 00000000..e69de29b diff --git a/web/app/results/tables.py b/web/app/results/tables.py new file mode 100644 index 00000000..e69de29b diff --git a/web/app/results/views.py b/web/app/results/views.py new file mode 100644 index 00000000..0a8af940 --- /dev/null +++ b/web/app/results/views.py @@ -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('//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('//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') diff --git a/web/app/services/forms.py b/web/app/services/forms.py new file mode 100644 index 00000000..13533ca6 --- /dev/null +++ b/web/app/services/forms.py @@ -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) diff --git a/web/app/services/tables.py b/web/app/services/tables.py new file mode 100644 index 00000000..e9d9389c --- /dev/null +++ b/web/app/services/tables.py @@ -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='info_outline') # 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='search') # 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 diff --git a/web/app/services/views.py b/web/app/services/views.py index 3c8d0b08..2647f3a8 100644 --- a/web/app/services/views.py +++ b/web/app/services/views.py @@ -1,13 +1,17 @@ from flask import (abort, current_app, flash, make_response, render_template, url_for) from flask_login import current_user, login_required +from .forms import ImportResultsForm from werkzeug.utils import secure_filename from . import services from .. import db 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 os +import html +from datetime import datetime SERVICES = {'corpus_analysis': {'name': 'Corpus analysis'}, @@ -81,3 +85,84 @@ def service(service): return render_template('services/{}.html.j2'.format(service), title=SERVICES[service]['name'], 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) diff --git a/web/app/static/js/nopaque.InteractionElement.js b/web/app/static/js/nopaque.InteractionElement.js index b3f8ec60..6e85b005 100644 --- a/web/app/static/js/nopaque.InteractionElement.js +++ b/web/app/static/js/nopaque.InteractionElement.js @@ -29,4 +29,32 @@ class InteractionElement { let boundedCallback = callback["function"].bind(callback.bindThis); return boundedCallback; } -} \ No newline at end of file + + 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 + } + }; + } +} diff --git a/web/app/static/js/nopaque.callbacks.js b/web/app/static/js/nopaque.callbacks.js index 45ee321e..1b3cba22 100644 --- a/web/app/static/js/nopaque.callbacks.js +++ b/web/app/static/js/nopaque.callbacks.js @@ -63,7 +63,7 @@ function querySetup(payload) { // This callback is called on socket.on "query_results" // this handels the incoming result chunks -function queryRenderResults(payload) { +function queryRenderResults(payload, imported=false) { let resultItems; // array of built html result items row element // This is called when results are transmitted and being recieved console.log("Current recieved chunk:", payload.chunk); @@ -102,18 +102,23 @@ function queryRenderResults(payload) { console.log("Results recieved:", results.data); // upate progress status progress = payload.progress; // global declaration - if (progress === 100) { + if (progress === 100 && !imported) { queryResultsProgressElement.classList.add("hide"); queryResultsUserFeedbackElement.classList.add("hide"); queryResultsExportElement.classList.remove("disabled"); addToSubResultsElement.removeAttribute("disabled"); - results.jsList.activateInspect(); // inital expert mode check and sub results activation + results.jsList.activateInspect(); if (addToSubResultsElement.checked) { results.jsList.activateAddToSubResults(); } if (expertModeSwitchElement.checked) { results.jsList.expertModeOn("query-display"); } + } else if (imported) { + results.jsList.activateInspect(); + if (expertModeSwitchElement.checked) { + results.jsList.expertModeOn("query-display"); + } } } \ No newline at end of file diff --git a/web/app/templates/corpora/analyse_corpus.html.j2 b/web/app/templates/corpora/analyse_corpus.html.j2 index cbf8ae77..d417d60b 100644 --- a/web/app/templates/corpora/analyse_corpus.html.j2 +++ b/web/app/templates/corpora/analyse_corpus.html.j2 @@ -200,7 +200,6 @@
    - @@ -567,23 +566,7 @@ // checks if a change for every interactionElement happens and executes // the callbacks accordingly - 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 - } - }; + 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 diff --git a/web/app/templates/corpora/corpus.html.j2 b/web/app/templates/corpora/corpus.html.j2 index 3789af0c..0a0a8c76 100644 --- a/web/app/templates/corpora/corpus.html.j2 +++ b/web/app/templates/corpora/corpus.html.j2 @@ -27,13 +27,13 @@
    - +
    - +
    diff --git a/web/app/templates/jobs/job.html.j2 b/web/app/templates/jobs/job.html.j2 index e26448a0..3978183d 100644 --- a/web/app/templates/jobs/job.html.j2 +++ b/web/app/templates/jobs/job.html.j2 @@ -36,7 +36,7 @@
    - +
    diff --git a/web/app/templates/main/dashboard.html.j2 b/web/app/templates/main/dashboard.html.j2 index 0bd1eafb..45569e6f 100644 --- a/web/app/templates/main/dashboard.html.j2 +++ b/web/app/templates/main/dashboard.html.j2 @@ -28,6 +28,7 @@
      diff --git a/web/app/templates/results/result_details.html.j2 b/web/app/templates/results/result_details.html.j2 new file mode 100644 index 00000000..73fe0f0a --- /dev/null +++ b/web/app/templates/results/result_details.html.j2 @@ -0,0 +1,118 @@ +{% extends "nopaque.html.j2" %} + +{% block page_content %} + + +
      +

      Below the metadata for the results from the Corpus + {{ result.corpus_metadata.corpus_name }} generated with the query + {{ result.corpus_metadata.query }} are shown. +

      +

      {{ texts_metadata }}

      +
      + +
      +
      +
      + + + + + + + + + {% for pair in result.corpus_metadata|dictsort %} + + + {% if pair[0] == 'corpus_all_texts' + or pair[0] == 'text_lookup' %} + + {% else %} + + {% endif %} + + {% endfor %} + +
      Metadata DescriptionValue
      {{ pair[0] }} + + {% for key, value in pair[1].items() %} + + + + {% endfor %} +
      + {{ value['title'] }} written + by {{ value['author'] }} + in {{ value['publishing_year'] }} + More + + info_outline + + +
      +
      {{ pair[1] }}
      +
      + +
      +
      + + + + + + +{% endblock %} diff --git a/web/app/templates/results/result_inspect.html.j2 b/web/app/templates/results/result_inspect.html.j2 new file mode 100644 index 00000000..73910f82 --- /dev/null +++ b/web/app/templates/results/result_inspect.html.j2 @@ -0,0 +1,211 @@ +{% extends "nopaque.html.j2" %} + +{% set headline = ' ' %} + +{% set full_width = True %} + +{% block page_content %} +
      +
      +
      +
      +
      +
      +
      Infos
      +
      +
      +

      + Displaying + + of + + matches. +
      + Matches occured in + + corpus files: +
      + +

      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      Display
      +
      +
      +
      +
      + {{ 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) }} +
      +
      +
      +
      +
      +
      +
      + +
      +
        + + + + + + + + + + + + + +
        Nr.TitleLeft contextMatchActionsRight Context
        +
          +
          +
          +
          +
          + + + + + + +{% endblock %} \ No newline at end of file diff --git a/web/app/templates/services/corpus_analysis.html.j2 b/web/app/templates/services/corpus_analysis.html.j2 index 4512ef6f..2d6344ee 100644 --- a/web/app/templates/services/corpus_analysis.html.j2 +++ b/web/app/templates/services/corpus_analysis.html.j2 @@ -36,6 +36,7 @@
            diff --git a/web/app/templates/services/import_results.html.j2 b/web/app/templates/services/import_results.html.j2 new file mode 100644 index 00000000..f1e1d41f --- /dev/null +++ b/web/app/templates/services/import_results.html.j2 @@ -0,0 +1,39 @@ +{% extends "nopaque.html.j2" %} + +{% block page_content %} +
            +

            Fill out the following form to upload and view Results and Sub Results + exported from the Corpus analsis Tool.

            + arrow_backBack to dashboard +
            + +
            +
            +
            +
            + {{ import_results_form.hidden_tag() }} +
            +
            + {{ M.render_field(import_results_form.file, accept='.json', placeholder='Choose your .json file') }} +
            +
            +
            +
            + {{ M.render_field(import_results_form.submit, material_icon='send') }} +
            +
            +
            +
            + + +{% endblock %} diff --git a/web/app/templates/services/results.html.j2 b/web/app/templates/services/results.html.j2 new file mode 100644 index 00000000..2b01d6fd --- /dev/null +++ b/web/app/templates/services/results.html.j2 @@ -0,0 +1,52 @@ +{% extends "nopaque.html.j2" %} + +{% block page_content %} + +
            +

            This is an overview of all your imported results.

            +
            + +
            +
            +
            +
            + search + + +
            +
              + {{ table }} +
                +
                  +
                  + +
                  +
                  + + +{% endblock %} diff --git a/web/migrations/versions/0d7aed934679_.py b/web/migrations/versions/0d7aed934679_.py new file mode 100644 index 00000000..3c45d90c --- /dev/null +++ b/web/migrations/versions/0d7aed934679_.py @@ -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 ### diff --git a/web/migrations/versions/318074622d14_.py b/web/migrations/versions/318074622d14_.py new file mode 100644 index 00000000..84dba226 --- /dev/null +++ b/web/migrations/versions/318074622d14_.py @@ -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 ### diff --git a/web/migrations/versions/389bcf564726_.py b/web/migrations/versions/389bcf564726_.py new file mode 100644 index 00000000..4244fcc1 --- /dev/null +++ b/web/migrations/versions/389bcf564726_.py @@ -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 ### diff --git a/web/migrations/versions/b15366b25bea_.py b/web/migrations/versions/b15366b25bea_.py new file mode 100644 index 00000000..2e90d9b0 --- /dev/null +++ b/web/migrations/versions/b15366b25bea_.py @@ -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 ### diff --git a/web/migrations/versions/e256f5cac75d_.py b/web/migrations/versions/e256f5cac75d_.py new file mode 100644 index 00000000..3e810f2b --- /dev/null +++ b/web/migrations/versions/e256f5cac75d_.py @@ -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 ### diff --git a/web/nopaque.py b/web/nopaque.py index ee20b8f7..ed8ae04b 100644 --- a/web/nopaque.py +++ b/web/nopaque.py @@ -2,11 +2,11 @@ import eventlet eventlet.monkey_patch() # noqa from app import create_app, db, socketio 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 import os - app = create_app(os.getenv('FLASK_CONFIG') or 'default') migrate = Migrate(app, db) @@ -21,6 +21,8 @@ def make_shell_context(): 'JobResult': JobResult, 'NotificationData': NotificationData, 'NotificationEmailData': NotificationEmailData, + 'Result': Result, + 'ResultFile': ResultFile, 'Role': Role, 'User': User}