diff --git a/web/app/corpora/events.py b/web/app/corpora/events.py index a0ae932262d4f85fd9048362eb3532c26eb92fff..9eb9c20ffd0b56b6c4c34b8fc6a287ddf358f713 100644 --- a/web/app/corpora/events.py +++ b/web/app/corpora/events.py @@ -8,7 +8,8 @@ from ..events import connected_sessions from ..models import Corpus, User import cqi import math -import logging +import os +import shutil ''' @@ -23,6 +24,29 @@ corpus_analysis_sessions = {} corpus_analysis_clients = {} +@socketio.on('corpus_create_zip') +@socketio_login_required +def corpus_create_zip(corpus_id): + corpus = Corpus.query.get_or_404(corpus_id) + # delete old corpus archive if it exists/has been build before + if corpus.archive_file is not None: + if (os.path.isfile(corpus.archive_file)): + os.remove(corpus.archive_file) + root_dir = os.path.join(current_app.config['DATA_DIR'], + str(current_user.id), + 'corpora') + base_dir = os.path.join(root_dir, str(corpus.id)) + zip_name = corpus.title + zip_path = os.path.join(root_dir, zip_name) + corpus.archive_file = os.path.join(base_dir, zip_name) + '.zip' + db.session.commit() + shutil.make_archive(zip_path, + 'zip', + base_dir) + shutil.move(zip_path + '.zip', corpus.archive_file) + socketio.emit('corpus_zip_created', room=request.sid) + + @socketio.on('corpus_analysis_init') @socketio_login_required def init_corpus_analysis(corpus_id): @@ -125,10 +149,6 @@ def corpus_analysis_query(query): chunk_start = 0 context = 50 progress = 0 - # for attr in corpus.structural_attributes.list(): - # if attr.attrs['name'] == 'text': - # text_attr = attr - # logging.warning(results.fdist_1(15, results.attrs['fields']['match'], text_attr)) client.status = 'running' while chunk_start <= results.attrs['size']: if client.status == 'abort': diff --git a/web/app/corpora/forms.py b/web/app/corpora/forms.py index 6eafbf6462de1e94d3f5ac9b4effcb182c93ac1b..0a2e21705f484fe73337b95787dcc49f1f3cd3c9 100644 --- a/web/app/corpora/forms.py +++ b/web/app/corpora/forms.py @@ -71,6 +71,26 @@ class AddCorpusForm(FlaskForm): title = StringField('Title', validators=[DataRequired(), Length(1, 32)]) +class ImportCorpusForm(FlaskForm): + ''' + Form to import a corpus. + ''' + description = StringField('Description', + validators=[DataRequired(), Length(1, 255)]) + file = FileField('File', validators=[DataRequired()]) + submit = SubmitField() + title = StringField('Title', validators=[DataRequired(), Length(1, 32)]) + + def __init__(self, *args, **kwargs): + super(ImportCorpusForm, self).__init__(*args, **kwargs) + + def validate_file(self, field): + if not field.data.filename.lower().endswith('.zip'): + raise ValidationError('File does not have an approved extension: ' + '.zip') + field.data.filename = secure_filename(field.data.filename) + + class QueryForm(FlaskForm): ''' Form to submit a query to the server which is executed via cqi-py. diff --git a/web/app/corpora/import_corpus.py b/web/app/corpora/import_corpus.py new file mode 100644 index 0000000000000000000000000000000000000000..a78f6f26dd507e6be725784c95506c545d76cedf --- /dev/null +++ b/web/app/corpora/import_corpus.py @@ -0,0 +1,89 @@ +check_zip_contents = ['data/', + 'merged/', + 'registry/', + 'registry/corpus', + 'data/corpus/', + 'data/corpus/text_editor.avs', + 'data/corpus/pos.lexicon', + 'data/corpus/simple_pos.huf', + 'data/corpus/word.huf', + 'data/corpus/text_booktitle.avs', + 'data/corpus/word.lexicon.srt', + 'data/corpus/word.lexicon.idx', + 'data/corpus/simple_pos.crx', + 'data/corpus/text_pages.rng', + 'data/corpus/simple_pos.crc', + 'data/corpus/ner.lexicon', + 'data/corpus/lemma.huf', + 'data/corpus/text_title.rng', + 'data/corpus/text_chapter.avx', + 'data/corpus/lemma.lexicon.srt', + 'data/corpus/lemma.lexicon.idx', + 'data/corpus/text_school.rng', + 'data/corpus/text_journal.avs', + 'data/corpus/simple_pos.lexicon', + 'data/corpus/pos.huf', + 'data/corpus/text_editor.avx', + 'data/corpus/lemma.crc', + 'data/corpus/lemma.lexicon', + 'data/corpus/pos.hcd', + 'data/corpus/text_title.avx', + 'data/corpus/text_institution.avs', + 'data/corpus/text_address.avx', + 'data/corpus/lemma.corpus.cnt', + 'data/corpus/word.crx', + 'data/corpus/simple_pos.hcd', + 'data/corpus/simple_pos.huf.syn', + 'data/corpus/simple_pos.lexicon.srt', + 'data/corpus/text_author.avx', + 'data/corpus/text_publisher.avs', + 'data/corpus/text_chapter.avs', + 'data/corpus/ner.corpus.cnt', + 'data/corpus/pos.huf.syn', + 'data/corpus/text_booktitle.rng', + 'data/corpus/lemma.huf.syn', + 'data/corpus/pos.corpus.cnt', + 'data/corpus/word.lexicon', + 'data/corpus/text_publishing_year.avs', + 'data/corpus/lemma.hcd', + 'data/corpus/text_school.avs', + 'data/corpus/text_journal.rng', + 'data/corpus/word.corpus.cnt', + 'data/corpus/text_school.avx', + 'data/corpus/text_journal.avx', + 'data/corpus/pos.lexicon.srt', + 'data/corpus/text_title.avs', + 'data/corpus/word.hcd', + 'data/corpus/text_chapter.rng', + 'data/corpus/text_address.rng', + 'data/corpus/ner.hcd', + 'data/corpus/text_publisher.avx', + 'data/corpus/text_institution.rng', + 'data/corpus/lemma.crx', + 'data/corpus/pos.crc', + 'data/corpus/text_author.rng', + 'data/corpus/text_address.avs', + 'data/corpus/pos.lexicon.idx', + 'data/corpus/ner.huf', + 'data/corpus/ner.huf.syn', + 'data/corpus/text_pages.avs', + 'data/corpus/text_publishing_year.avx', + 'data/corpus/ner.lexicon.idx', + 'data/corpus/text.rng', + 'data/corpus/word.crc', + 'data/corpus/ner.crc', + 'data/corpus/text_publisher.rng', + 'data/corpus/text_editor.rng', + 'data/corpus/text_author.avs', + 'data/corpus/s.rng', + 'data/corpus/text_publishing_year.rng', + 'data/corpus/simple_pos.corpus.cnt', + 'data/corpus/simple_pos.lexicon.idx', + 'data/corpus/word.huf.syn', + 'data/corpus/ner.lexicon.srt', + 'data/corpus/text_pages.avx', + 'data/corpus/text_booktitle.avx', + 'data/corpus/pos.crx', + 'data/corpus/ner.crx', + 'data/corpus/text_institution.avx', + 'merged/corpus.vrt'] diff --git a/web/app/corpora/views.py b/web/app/corpora/views.py index 0bdc9413e357de7fd50f0836f800596dab3132f3..44ba6a697286cbb85d931147b5463c14312b45b3 100644 --- a/web/app/corpora/views.py +++ b/web/app/corpora/views.py @@ -5,12 +5,18 @@ from . import corpora from . import tasks from .forms import (AddCorpusFileForm, AddCorpusForm, AddQueryResultForm, EditCorpusFileForm, QueryDownloadForm, QueryForm, - DisplayOptionsForm, InspectDisplayOptionsForm) + DisplayOptionsForm, InspectDisplayOptionsForm, + ImportCorpusForm) +from jsonschema import validate from .. import db from ..models import Corpus, CorpusFile, QueryResult import json -from jsonschema import validate import os +import shutil +import glob +import xml.etree.ElementTree as ET +from zipfile import ZipFile +from .import_corpus import check_zip_contents @corpora.route('/add', methods=['GET', 'POST']) @@ -40,6 +46,85 @@ def add_corpus(): title='Add corpus') +@corpora.route('/import', methods=['GET', 'POST']) +@login_required +def import_corpus(): + import_corpus_form = ImportCorpusForm() + if import_corpus_form.is_submitted(): + if not import_corpus_form.validate(): + return make_response(import_corpus_form.errors, 400) + corpus = Corpus(creator=current_user, + description=import_corpus_form.description.data, + status='unprepared', + title=import_corpus_form.title.data) + db.session.add(corpus) + db.session.commit() + dir = os.path.join(current_app.config['DATA_DIR'], + str(corpus.user_id), 'corpora', str(corpus.id)) + try: + os.makedirs(dir) + except OSError: + flash('[ERROR]: Could not import corpus!', 'corpus') + corpus.delete() + else: + # Upload zip + archive_file = os.path.join(current_app.config['DATA_DIR'], dir, + import_corpus_form.file.data.filename) + corpus_dir = os.path.dirname(archive_file) + import_corpus_form.file.data.save(archive_file) + # Some checks to verify it is a valid exported corpus + with ZipFile(archive_file, 'r') as zip: + contents = zip.namelist() + if set(check_zip_contents).issubset(contents): + # Unzip + shutil.unpack_archive(archive_file, corpus_dir) + # Register vrt files to corpus + vrts = glob.glob(corpus_dir + '/*.vrt') + for file in vrts: + element_tree = ET.parse(file) + text_node = element_tree.find('text') + corpus_file = CorpusFile( + address=text_node.get('address', 'NULL'), + author=text_node.get('author', 'NULL'), + booktitle=text_node.get('booktitle', 'NULL'), + chapter=text_node.get('chapter', 'NULL'), + corpus=corpus, + dir=dir, + editor=text_node.get('editor', 'NULL'), + filename=os.path.basename(file), + institution=text_node.get('institution', 'NULL'), + journal=text_node.get('journal', 'NULL'), + pages=text_node.get('pages', 'NULL'), + publisher=text_node.get('publisher', 'NULL'), + publishing_year=text_node.get('publishing_year', ''), + school=text_node.get('school', 'NULL'), + title=text_node.get('title', 'NULL')) + db.session.add(corpus_file) + # finish import and got to imported corpus + url = url_for('corpora.corpus', corpus_id=corpus.id) + corpus.status = 'prepared' + db.session.commit() + os.remove(archive_file) + flash('[<a href="{}">{}</a>] imported'.format(url, + corpus.title), + 'corpus') + return make_response( + {'redirect_url': url_for('corpora.corpus', + corpus_id=corpus.id)}, + 201) + else: + # If imported zip is not valid delete corpus and give feedback + corpus.delete() + db.session.commit() + flash('Imported corpus is not valid.', 'error') + return make_response( + {'redirect_url': url_for('corpora.import_corpus')}, + 201) + return render_template('corpora/import_corpus.html.j2', + import_corpus_form=import_corpus_form, + title='Import Corpus') + + @corpora.route('/<int:corpus_id>') @login_required def corpus(corpus_id): @@ -59,6 +144,20 @@ def corpus(corpus_id): title='Corpus') +@corpora.route('/<int:corpus_id>/export') +@login_required +def export_corpus(corpus_id): + corpus = Corpus.query.get_or_404(corpus_id) + if not (corpus.creator == current_user or current_user.is_administrator()): + abort(403) + dir = os.path.dirname(corpus.archive_file) + filename = os.path.basename(corpus.archive_file) + return send_from_directory(directory=dir, + filename=filename, + mimetype='zip', + as_attachment=True) + + @corpora.route('/<int:corpus_id>/analyse') @login_required def analyse_corpus(corpus_id): diff --git a/web/app/decorators.py b/web/app/decorators.py index 4bd2f73195502e41dce75ae08427555f1ced33aa..de0189ade8741631f539b478a5d69670dccb4805 100644 --- a/web/app/decorators.py +++ b/web/app/decorators.py @@ -26,6 +26,7 @@ def background(f): @wraps(f) def wrapped(*args, **kwargs): kwargs['app'] = current_app._get_current_object() + kwargs['current_user'] = current_user._get_current_object() thread = socketio.start_background_task(f, *args, **kwargs) return thread return wrapped diff --git a/web/app/models.py b/web/app/models.py index e28a5b06dff9c08ac3da9d7f2e892712ac661464..00c83245f30e401735a3f469867df90da51dd80d 100644 --- a/web/app/models.py +++ b/web/app/models.py @@ -555,6 +555,7 @@ class Corpus(db.Model): max_nr_of_tokens = db.Column(db.BigInteger, default=2147483647) status = db.Column(db.String(16)) title = db.Column(db.String(32)) + archive_file = db.Column(db.String(255)) # Relationships files = db.relationship('CorpusFile', backref='corpus', lazy='dynamic', cascade='save-update, merge, delete') diff --git a/web/app/static/css/nopaque.css b/web/app/static/css/nopaque.css index 821f8b034a2ce3e22500f63d8a4789bd7f38afaa..1c65e3717a80c9678b9b1aa7e8d2f5b4bb6254ff 100644 --- a/web/app/static/css/nopaque.css +++ b/web/app/static/css/nopaque.css @@ -22,6 +22,16 @@ height: 19.5px !important; } +/* + * changes preoloader size etc. to fit visually better with the chip status + * indicator of jobs + */ +.status-spinner { + margin-bottom: -10px; + width: 30px !important; + height: 30px !important; +} + /* flat-interaction addition to show background color */ .flat-interaction { diff --git a/web/app/static/js/modules/corpus_analysis/view/ResultsView.js b/web/app/static/js/modules/corpus_analysis/view/ResultsView.js index adab3ee75cc7515c835e80bacada5a1c46c06624..9394208e5ffbb8db576403d5ed86d04335e9a7a3 100644 --- a/web/app/static/js/modules/corpus_analysis/view/ResultsView.js +++ b/web/app/static/js/modules/corpus_analysis/view/ResultsView.js @@ -64,7 +64,6 @@ class ResultsList extends List { * hase been issued by the user. */ resetFields() { - this.addToSubResultsStatus = {}; this.subResultsIndexes = {}; } @@ -216,17 +215,21 @@ class ResultsList extends List { btn.textContent = "add"; } /** - * Either adds or removes a match to the sub-results. For this it checks - * onclick if the current button has been checked or not. For this the - * function checks if its status in addToSubResultsStatus is either flase or - * true. Adds match to sub-results if status is false if status is true it - * removes it. + * This function is invoked when the users adds or removes a match using the + * add-btn (+ button/or green checkmark) to/from sub-results. When the button + * is clicked the function checks if the current dataIndex ID is already + * saved in subResultsIndexes or not. If it is not the dataIndex will be used + * as a key in subResultsIndexes with the value true. If it is already added + * the entry with the key dataIndex will be deleted from subResultsIndexes. + * Visual feedback (green checkmark if a match has been added etc.) is also + * handled on the basis of the information stored in subResultsIndexes. */ addToSubResults(dataIndex, client, tableCall=true) { let toShowArray; dataIndex = parseInt(dataIndex); if (!this.subResultsIndexes[dataIndex] - || this.subResultsIndexes === undefined) { + || this.subResultsIndexes[dataIndex] === undefined) { + // add button is activated because status is false or undefined this.helperActivateAddBtn(event.target); this.subResultsIndexes[dataIndex] = true; toShowArray = Object.keys(this.subResultsIndexes).map(index => parseInt(index)); @@ -273,7 +276,7 @@ class ResultsList extends List { this.getHTMLElements(['#query-results-table']); let container = this.queryResultsTable.querySelector(`[data-index="${dataIndex}"]`); let tableAddBtn = container.querySelector('.add-btn'); // gets the add button from the list view - if (this.addToSubResultsStatus[dataIndex]) { + if (this.subResultsIndexes[dataIndex]) { this.helperActivateAddBtn(tableAddBtn); } else { this.helperDeactivateAddBtn(tableAddBtn); diff --git a/web/app/static/js/modules/corpus_analysis/view/listeners.js b/web/app/static/js/modules/corpus_analysis/view/listeners.js index 812c125b0e94c11a7133ab69a676b9e3de361b64..a7c6c675003e2472164aaa329a9b878fce6441e6 100644 --- a/web/app/static/js/modules/corpus_analysis/view/listeners.js +++ b/web/app/static/js/modules/corpus_analysis/view/listeners.js @@ -281,9 +281,9 @@ function exportFullContextSwitch(resultsList) { function createFullResults(resultsList, results) { resultsList.fullResultsCreate.onclick = (event) => { resultsList.fullResultsCreate.querySelector('i').classList.toggle('hide'); - resultsList.fullResultsCreate.innerText = 'Creating...'; + resultsList.fullResultsCreate.textContent = 'Creating...'; resultsList.fullResultsCreate.insertAdjacentHTML('afterbegin', - loadingSpinnerHTML); + loadingSpinnerHTML); // .keys() is for a zero based array. I think... let dataIndexes = [...Array(results.data.match_count).keys()]; // Empty fullResultsData so that no previous data is used. @@ -302,7 +302,7 @@ function createSubResults(resultsList, results) { dataIndexes.push(id); }); resultsList.subResultsCreate.querySelector('i').classList.toggle('hide'); - resultsList.subResultsCreate.innerText = 'Creating...'; + resultsList.subResultsCreate.textContent = 'Creating...'; resultsList.subResultsCreate.insertAdjacentHTML('afterbegin', loadingSpinnerHTML); // Empty subResultsData so that no previous data is used. diff --git a/web/app/templates/corpora/corpus.html.j2 b/web/app/templates/corpora/corpus.html.j2 index d2e7fd07e3fb8c18d42718c544129f312e0bd48b..a316a8d4c34a43f2a1637aabaeaf51a126d6a9b3 100644 --- a/web/app/templates/corpora/corpus.html.j2 +++ b/web/app/templates/corpora/corpus.html.j2 @@ -15,7 +15,8 @@ </div> <div class="col s12 m4"> - <div class="active preloader-wrapper small hide" id="progress-indicator"> + <span class="chip status white-text hide" id="status"></span> + <div class="active preloader-wrapper small hide status-spinner" id="progress-indicator"> <div class="spinner-layer spinner-blue-only"> <div class="circle-clipper left"> <div class="circle"></div> @@ -28,7 +29,6 @@ </div> </div> </div> - <span class="chip status white-text hide" id="status"></span> </div> <div class="col s12 m8"> @@ -61,6 +61,7 @@ <div class="card-action right-align"> <a href="{{ url_for('corpora.analyse_corpus', corpus_id=corpus.id) }}" class="btn disabled hide waves-effect waves-light" id="analyze"><i class="material-icons left">search</i>Analyze</a> <a href="{{ url_for('corpora.prepare_corpus', corpus_id=corpus.id) }}" class="btn disabled hide waves-effect waves-light" id="build"><i class="material-icons left">build</i>Build</a> + <a class="btn hide waves-effect waves-light download" id="corpus_create_zip"><i class="material-icons left">import_export</i>Export Corpus</a> <a data-target="delete-corpus-modal" class="btn modal-trigger red waves-effect waves-light"><i class="material-icons left">delete</i>Delete</a> </div> </div> @@ -123,94 +124,123 @@ {% block scripts %} {{ super() }} <script type="module"> - import {RessourceList} from '../../static/js/nopaque.lists.js'; - class InformationUpdater { - constructor(corpusId, foreignCorpusFlag) { - this.corpusId = corpusId; - this.foreignCorpusFlag = foreignCorpusFlag; - - if (this.foreignCorpusFlag) { - nopaque.foreignCorporaSubscribers.push(this); - } else { - nopaque.corporaSubscribers.push(this); - } +import { + RessourceList +} from '../../static/js/nopaque.lists.js'; + +class InformationUpdater { + constructor(corpusId, foreignCorpusFlag) { + this.corpusId = corpusId; + this.foreignCorpusFlag = foreignCorpusFlag; + + if (this.foreignCorpusFlag) { + nopaque.foreignCorporaSubscribers.push(this); + } else { + nopaque.corporaSubscribers.push(this); } + } - _init() { - let corpus; + _init() { + let corpus; - corpus = (this.foreignCorpusFlag ? nopaque.foreignUser.corpora[this.corpusId] - : nopaque.user.corpora[this.corpusId]); + corpus = (this.foreignCorpusFlag ? nopaque.foreignUser.corpora[this.corpusId] + : nopaque.user.corpora[this.corpusId]); - // Status - this.setStatus(corpus.status); - } + // Status + this.setStatus(corpus.status); + } - _update(patch) { - let pathArray; - - for (let operation of patch) { - /* "/corpora/{corpusId}/valueName" -> ["{corpusId}", ...] */ - pathArray = operation.path.split("/").slice(2); - if (pathArray[0] != this.corpusId) {continue;} - switch(operation.op) { - case "add": - location.reload(); - break; - case "delete": - location.reload(); - break; - case "replace": - if (pathArray[1] === "status") { - this.setStatus(operation.value); - } - break; - default: - break; - } + _update(patch) { + let pathArray; + + for (let operation of patch) { + /* "/corpora/{corpusId}/valueName" -> ["{corpusId}", ...] */ + pathArray = operation.path.split("/").slice(2); + if (pathArray[0] != this.corpusId) {continue;} + switch(operation.op) { + case "add": + location.reload(); + break; + case "delete": + location.reload(); + break; + case "replace": + if (pathArray[1] === "status") { + this.setStatus(operation.value); + } + break; + default: + break; } } + } - setStatus(status) { - let analyzeElement, buildElement, numFiles, progressIndicatorElement, statusElement; - - numFiles = Object.keys((this.foreignCorpusFlag ? nopaque.foreignUser.corpora[this.corpusId] : nopaque.user.corpora[this.corpusId]).files).length; + setStatus(status) { + let analyzeElement, buildElement, numFiles, progressIndicatorElement, statusElement; - progressIndicatorElement = document.getElementById("progress-indicator"); - if (["queued", "running", "start analysis", "stop analysis"].includes(status)) { - progressIndicatorElement.classList.remove("hide"); - } else { - progressIndicatorElement.classList.add("hide"); - } + numFiles = Object.keys((this.foreignCorpusFlag ? nopaque.foreignUser.corpora[this.corpusId] : nopaque.user.corpora[this.corpusId]).files).length; - statusElement = document.getElementById("status"); - statusElement.dataset.status = status; - statusElement.classList.remove("hide"); + progressIndicatorElement = document.getElementById("progress-indicator"); + if (["queued", "running", "start analysis", "stop analysis"].includes(status)) { + progressIndicatorElement.classList.remove("hide"); + } else { + progressIndicatorElement.classList.add("hide"); + } - analyzeElement = document.getElementById("analyze"); - if (["analysing", "prepared", "start analysis"].includes(status)) { - analyzeElement.classList.remove("disabled", "hide"); - } else { - analyzeElement.classList.add("disabled", "hide"); - } + statusElement = document.getElementById("status"); + statusElement.dataset.status = status; + statusElement.classList.remove("hide"); - buildElement = document.getElementById("build"); - if (status === "unprepared" && numFiles > 0) { - buildElement.classList.remove("disabled", "hide"); - } else { - buildElement.classList.add("disabled", "hide"); - } + analyzeElement = document.getElementById("analyze"); + if (["analysing", "prepared", "start analysis"].includes(status)) { + analyzeElement.classList.remove("disabled", "hide"); + } else { + analyzeElement.classList.add("disabled", "hide"); } - } - {% if corpus.creator == current_user %} - var informationUpdater = new InformationUpdater({{ corpus.id }}, false); - {% else %} - var informationUpdater = new InformationUpdater({{ corpus.id }}, true); - nopaque.socket.emit("foreign_user_data_stream_init", {{ corpus.user_id }}); - {% endif %} + buildElement = document.getElementById("build"); + if (status === "unprepared" && numFiles > 0) { + buildElement.classList.remove("disabled", "hide"); + } else { + buildElement.classList.add("disabled", "hide"); + } - let corpusFilesList = new RessourceList("corpus-files", null, "CorpusFile"); - corpusFilesList._add({{ corpus_files|tojson|safe }}); + let downloadBtn = document.querySelector('#corpus_create_zip'); + if (status === "prepared") { + downloadBtn.classList.toggle('hide', false); + } else { + downloadBtn.classList.toggle('hide', true); + } + } +} + +{% if corpus.creator == current_user %} +var informationUpdater = new InformationUpdater({{ corpus.id }}, false); +{% else %} +var informationUpdater = new InformationUpdater({{ corpus.id }}, true); +nopaque.socket.emit("foreign_user_data_stream_init", {{ corpus.user_id }}); +{% endif %} + +let corpusFilesList = new RessourceList("corpus-files", null, "CorpusFile"); +corpusFilesList._add({{ corpus_files|tojson|safe }}); + +// Events to handle full corpus download +let downloadBtn = document.querySelector('#corpus_create_zip'); +downloadBtn.addEventListener('click', () => { + nopaque.flash('Compressing your corpus', 'corpus') + nopaque.socket.emit('corpus_create_zip', {{ corpus.id }}); + downloadBtn.classList.toggle('disabled', true); +}); +document.addEventListener('DOMContentLoaded', () => { + nopaque.socket.on('corpus_zip_created', () => { + nopaque.flash('Downloading your corpus', 'corpus'); + downloadBtn.classList.toggle('disabled', false); + // Little trick to call the download view after ziping has finished + let fakeBtn = document.createElement('a'); + fakeBtn.href = '{{ url_for('corpora.export_corpus', + corpus_id=corpus.id) }}'; + fakeBtn.click(); + }); +}); </script> {% endblock scripts %} diff --git a/web/app/templates/corpora/import_corpus.html.j2 b/web/app/templates/corpora/import_corpus.html.j2 new file mode 100644 index 0000000000000000000000000000000000000000..dd64265e49e1b2b994a26f62b687adbf1113189f --- /dev/null +++ b/web/app/templates/corpora/import_corpus.html.j2 @@ -0,0 +1,46 @@ +{% extends "nopaque.html.j2" %} + +{% block page_content %} +<div class="col s12 m4"> + <p>Fill out the following form to import a corpus.</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_corpus_form.hidden_tag() }} + <div class="row"> + <div class="col s12 m4"> + {{ M.render_field(import_corpus_form.title, data_length='32', material_icon='title') }} + </div> + <div class="col s12 m8"> + {{ M.render_field(import_corpus_form.description, data_length='255', material_icon='description') }} + </div> + </div> + <div class="row"> + <div class="col s12"> + {{ M.render_field(import_corpus_form.file, accept='.zip', placeholder='Choose your exported .zip file') }} + </div> + </div> + </div> + <div class="card-action right-align"> + {{ M.render_field(import_corpus_form.submit, material_icon='send') }} + </div> + </form> + </div> +</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 %} diff --git a/web/app/templates/corpora/interactions/infos.html.j2 b/web/app/templates/corpora/interactions/infos.html.j2 index 7a9eafe8a17edf7abd6179a5c7500369db12a126..83959864b5378052f0611681504a3f82aebc5f80 100644 --- a/web/app/templates/corpora/interactions/infos.html.j2 +++ b/web/app/templates/corpora/interactions/infos.html.j2 @@ -6,20 +6,18 @@ result.--> <h6 style="margin-top: 0px;">Infos</h6> <div class="divider" style="margin-bottom: 10px;"></div> <div class="row"> - <div class="col s12"> - <button id="loading-matches" - class="waves-effect - waves-light - btn-flat - flat-interaction - disabled black-text" - style="color: #000 !important;" - type="submit"> + <div class="col s12" + style="height: 39px; + margin-top: 0px; + padding-top: 5px; + padding-left: 1.75rem;"> + <span id="loading-matches" + class="black-text"> <i class="material-icons left">dvr</i> <span id="recieved-match-count"></span>/ <span id="total-match-count"></span> matches loaded - </button> + </span> </div> <div class="col s12"> <div class="progress hide" id="query-progress-bar"> diff --git a/web/app/templates/jobs/job.html.j2 b/web/app/templates/jobs/job.html.j2 index 20fda0f30d9fb293c0c5d4a77ae1df0fa5488da4..4deeff389fe0df05e7b17fe7fcc29b39068abb5c 100644 --- a/web/app/templates/jobs/job.html.j2 +++ b/web/app/templates/jobs/job.html.j2 @@ -34,7 +34,7 @@ <div class="col s4 m3 l2 right-align"> <span class="chip status white-text"></span> - <div class="active preloader-wrapper small" id="progress-indicator"> + <div class="active preloader-wrapper small status-spinner" id="progress-indicator"> <div class="spinner-layer spinner-blue-only"> <div class="circle-clipper left"> <div class="circle"></div> diff --git a/web/app/templates/main/dashboard.html.j2 b/web/app/templates/main/dashboard.html.j2 index 71d4f480182ed9d0917904ab1a0e1111aede3435..b014ecba9857bd9c432a10eebf807bceba60ac13 100644 --- a/web/app/templates/main/dashboard.html.j2 +++ b/web/app/templates/main/dashboard.html.j2 @@ -82,6 +82,7 @@ <ul class="pagination paginationBottom"></ul> </div> <div class="card-action right-align"> + <a class="waves-effect waves-light btn" href="{{ url_for('corpora.import_corpus') }}"><i class="material-icons right">import_export</i>Import Corpus</a> <a class="waves-effect waves-light btn" href="{{ url_for('corpora.add_query_result') }}">Add query result<i class="material-icons right">file_upload</i></a> </div> </div> diff --git a/web/app/templates/services/corpus_analysis.html.j2 b/web/app/templates/services/corpus_analysis.html.j2 index 906e25863a715dd40683840c8beeb2c38bf6a7a6..b27501249689486c8cfc1ada4afc8cfc5bec6f71 100644 --- a/web/app/templates/services/corpus_analysis.html.j2 +++ b/web/app/templates/services/corpus_analysis.html.j2 @@ -52,6 +52,7 @@ <ul class="pagination paginationBottom"></ul> </div> <div class="card-action right-align"> + <a class="waves-effect waves-light btn" href="{{ url_for('corpora.import_corpus') }}"><i class="material-icons right">import_export</i>Import Corpus</a> <a class="btn waves-effect waves-light" href="{{ url_for('corpora.add_corpus') }}">New corpus<i class="material-icons right">add</i></a> </div> </div> diff --git a/web/migrations/versions/befe5326787e_.py b/web/migrations/versions/befe5326787e_.py new file mode 100644 index 0000000000000000000000000000000000000000..11839d5c15d85198831e0125e2f300f5cd160605 --- /dev/null +++ b/web/migrations/versions/befe5326787e_.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: befe5326787e +Revises: ecaf75fece7b +Create Date: 2020-10-16 13:32:09.620960 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'befe5326787e' +down_revision = 'ecaf75fece7b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('corpora', sa.Column('archive_file', sa.String(length=255), nullable=True)) + op.drop_column('corpora', 'archive_dir') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('corpora', sa.Column('archive_dir', sa.VARCHAR(length=255), autoincrement=False, nullable=True)) + op.drop_column('corpora', 'archive_file') + # ### end Alembic commands ### diff --git a/web/migrations/versions/ecaf75fece7b_.py b/web/migrations/versions/ecaf75fece7b_.py new file mode 100644 index 0000000000000000000000000000000000000000..5e258a2c68125b3557dd19d6260c69173d496d1f --- /dev/null +++ b/web/migrations/versions/ecaf75fece7b_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: ecaf75fece7b +Revises: c3827cddea6e +Create Date: 2020-10-16 13:31:30.681269 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'ecaf75fece7b' +down_revision = 'c3827cddea6e' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('corpora', sa.Column('archive_dir', sa.String(length=255), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('corpora', 'archive_dir') + # ### end Alembic commands ###