mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2025-07-06 20:53:18 +00:00
Compare commits
3 Commits
access-pip
...
cdf6f9fcfd
Author | SHA1 | Date | |
---|---|---|---|
cdf6f9fcfd | |||
268da220d2 | |||
84e1755a57 |
@ -5,9 +5,8 @@
|
||||
!app
|
||||
!migrations
|
||||
!tests
|
||||
!.flaskenv
|
||||
!boot.sh
|
||||
!config.py
|
||||
!docker-nopaque-entrypoint.sh
|
||||
!nopaque.py
|
||||
!requirements.txt
|
||||
!wsgi.py
|
||||
|
@ -46,7 +46,7 @@ COPY docker-nopaque-entrypoint.sh /usr/local/bin/
|
||||
COPY --chown=nopaque:nopaque app app
|
||||
COPY --chown=nopaque:nopaque migrations migrations
|
||||
COPY --chown=nopaque:nopaque tests tests
|
||||
COPY --chown=nopaque:nopaque .flaskenv boot.sh config.py nopaque.py requirements.txt ./
|
||||
COPY --chown=nopaque:nopaque boot.sh config.py wsgi.py requirements.txt ./
|
||||
|
||||
RUN mkdir logs
|
||||
|
||||
|
@ -33,6 +33,9 @@ scheduler = APScheduler()
|
||||
socketio = SocketIO()
|
||||
|
||||
|
||||
# TODO: Create export for lemmatized corpora
|
||||
|
||||
|
||||
def create_app(config: Config = Config) -> Flask:
|
||||
''' Creates an initialized Flask (WSGI Application) object. '''
|
||||
app = Flask(__name__)
|
||||
|
@ -1,6 +1,6 @@
|
||||
from flask import abort, request
|
||||
from app import db
|
||||
from app.decorators import content_negotiation
|
||||
from app import db
|
||||
from app.models import User
|
||||
from . import bp
|
||||
|
||||
|
@ -9,12 +9,12 @@ from app.users.settings.forms import (
|
||||
UpdateAccountInformationForm,
|
||||
UpdateProfileInformationForm
|
||||
)
|
||||
from . import bp
|
||||
from .forms import UpdateUserForm
|
||||
from app.users.utils import (
|
||||
user_endpoint_arguments_constructor as user_eac,
|
||||
user_dynamic_list_constructor as user_dlc
|
||||
)
|
||||
from . import bp
|
||||
from .forms import UpdateUserForm
|
||||
|
||||
|
||||
@bp.route('')
|
||||
|
@ -5,8 +5,8 @@ from flask import abort, Blueprint
|
||||
from werkzeug.exceptions import InternalServerError
|
||||
from app import db, hashids
|
||||
from app.models import Job, JobInput, JobStatus, TesseractOCRPipelineModel
|
||||
from .schemas import EmptySchema, JobSchema, SpaCyNLPPipelineJobSchema, TesseractOCRPipelineJobSchema, TesseractOCRPipelineModelSchema
|
||||
from .auth import auth_error_responses, token_auth
|
||||
from .schemas import EmptySchema, JobSchema, SpaCyNLPPipelineJobSchema, TesseractOCRPipelineJobSchema, TesseractOCRPipelineModelSchema
|
||||
|
||||
|
||||
bp = Blueprint('jobs', __name__)
|
||||
|
@ -3,11 +3,11 @@ from apifairy import authenticate, body, response
|
||||
from apifairy.decorators import other_responses
|
||||
from flask import abort, Blueprint
|
||||
from werkzeug.exceptions import InternalServerError
|
||||
from app import db
|
||||
from app.email import create_message, send
|
||||
from app import db
|
||||
from app.models import User
|
||||
from .schemas import EmptySchema, UserSchema
|
||||
from .auth import auth_error_responses, token_auth
|
||||
from .schemas import EmptySchema, UserSchema
|
||||
|
||||
|
||||
bp = Blueprint('users', __name__)
|
||||
|
@ -4,7 +4,7 @@ from threading import Thread
|
||||
from app import db
|
||||
from app.decorators import content_negotiation, permission_required
|
||||
from app.models import SpaCyNLPPipelineModel
|
||||
from .. import bp
|
||||
from . import bp
|
||||
|
||||
|
||||
@bp.route('/spacy-nlp-pipeline-models/<hashid:spacy_nlp_pipeline_model_id>', methods=['DELETE'])
|
||||
|
@ -8,9 +8,7 @@ from .forms import (
|
||||
CreateSpaCyNLPPipelineModelForm,
|
||||
UpdateSpaCyNLPPipelineModelForm
|
||||
)
|
||||
from .utils import (
|
||||
spacy_nlp_pipeline_model_dlc as spacy_nlp_pipeline_model_dlc
|
||||
)
|
||||
from .utils import spacy_nlp_pipeline_model_dlc
|
||||
|
||||
|
||||
@bp.route('/spacy-nlp-pipeline-models')
|
||||
|
@ -8,9 +8,7 @@ from .forms import (
|
||||
CreateTesseractOCRPipelineModelForm,
|
||||
UpdateTesseractOCRPipelineModelForm
|
||||
)
|
||||
from .utils import (
|
||||
tesseract_ocr_pipeline_model_dlc as tesseract_ocr_pipeline_model_dlc
|
||||
)
|
||||
from .utils import tesseract_ocr_pipeline_model_dlc
|
||||
|
||||
|
||||
@bp.route('/tesseract-ocr-pipeline-models')
|
||||
|
@ -1,11 +1,11 @@
|
||||
from flask import current_app
|
||||
from app import db
|
||||
from app.models import User, Corpus, CorpusFile
|
||||
from datetime import datetime
|
||||
from flask import current_app
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
import json
|
||||
import shutil
|
||||
from app import db
|
||||
from app.models import User, Corpus, CorpusFile
|
||||
|
||||
|
||||
class SandpaperConverter:
|
||||
|
@ -1,7 +1,7 @@
|
||||
from flask import abort, current_app
|
||||
from flask import current_app
|
||||
from threading import Thread
|
||||
from app import db
|
||||
from app.decorators import content_negotiation
|
||||
from app import db
|
||||
from app.models import CorpusFile
|
||||
from ..decorators import corpus_follower_permission_required
|
||||
from . import bp
|
||||
|
@ -1,6 +1,5 @@
|
||||
from flask import request, url_for
|
||||
from app.models import CorpusFile
|
||||
from ..utils import corpus_endpoint_arguments_constructor as corpus_eac
|
||||
|
||||
|
||||
def corpus_file_dynamic_list_constructor():
|
||||
|
@ -1,2 +0,0 @@
|
||||
from .container_column import ContainerColumn
|
||||
from .int_enum_column import IntEnumColumn
|
@ -1,21 +0,0 @@
|
||||
import json
|
||||
from app import db
|
||||
|
||||
|
||||
class ContainerColumn(db.TypeDecorator):
|
||||
impl = db.String
|
||||
|
||||
def __init__(self, container_type, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.container_type = container_type
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
if isinstance(value, self.container_type):
|
||||
return json.dumps(value)
|
||||
elif isinstance(value, str) and isinstance(json.loads(value), self.container_type):
|
||||
return value
|
||||
else:
|
||||
return TypeError()
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
return json.loads(value)
|
1
app/extensions/__init__.py
Normal file
1
app/extensions/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
2
app/extensions/sqlalchemy/__init__.py
Normal file
2
app/extensions/sqlalchemy/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .types import ContainerColumn
|
||||
from .types import IntEnumColumn
|
@ -1,6 +1,26 @@
|
||||
import json
|
||||
from app import db
|
||||
|
||||
|
||||
class ContainerColumn(db.TypeDecorator):
|
||||
impl = db.String
|
||||
|
||||
def __init__(self, container_type, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.container_type = container_type
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
if isinstance(value, self.container_type):
|
||||
return json.dumps(value)
|
||||
elif isinstance(value, str) and isinstance(json.loads(value), self.container_type):
|
||||
return value
|
||||
else:
|
||||
return TypeError()
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
return json.loads(value)
|
||||
|
||||
|
||||
class IntEnumColumn(db.TypeDecorator):
|
||||
impl = db.Integer
|
||||
|
@ -9,7 +9,7 @@ import shutil
|
||||
import xml.etree.ElementTree as ET
|
||||
from app import db
|
||||
from app.converters.vrt import normalize_vrt_file
|
||||
from app.ext.flask_sqlalchemy import IntEnumColumn
|
||||
from app.extensions.sqlalchemy import IntEnumColumn
|
||||
from .corpus_follower_association import CorpusFollowerAssociation
|
||||
|
||||
|
||||
|
@ -7,7 +7,7 @@ from typing import Union
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from app import db
|
||||
from app.ext.flask_sqlalchemy import ContainerColumn, IntEnumColumn
|
||||
from app.extensions.sqlalchemy import ContainerColumn, IntEnumColumn
|
||||
|
||||
|
||||
class JobStatus(IntEnum):
|
||||
|
@ -5,7 +5,7 @@ from pathlib import Path
|
||||
import requests
|
||||
import yaml
|
||||
from app import db
|
||||
from app.ext.flask_sqlalchemy import ContainerColumn
|
||||
from app.extensions.sqlalchemy import ContainerColumn
|
||||
from .file_mixin import FileMixin
|
||||
from .user import User
|
||||
|
||||
|
@ -5,7 +5,7 @@ from pathlib import Path
|
||||
import requests
|
||||
import yaml
|
||||
from app import db
|
||||
from app.ext.flask_sqlalchemy import ContainerColumn
|
||||
from app.extensions.sqlalchemy import ContainerColumn
|
||||
from .file_mixin import FileMixin
|
||||
from .user import User
|
||||
|
||||
|
@ -12,7 +12,7 @@ import re
|
||||
import secrets
|
||||
import shutil
|
||||
from app import db, hashids
|
||||
from app.ext.flask_sqlalchemy import IntEnumColumn
|
||||
from app.extensions.sqlalchemy import IntEnumColumn
|
||||
from .corpus import Corpus
|
||||
from .corpus_follower_association import CorpusFollowerAssociation
|
||||
from .corpus_follower_role import CorpusFollowerRole
|
||||
|
@ -6,7 +6,6 @@ from app import db, hashids
|
||||
from app.models import (
|
||||
Job,
|
||||
JobInput,
|
||||
JobResult,
|
||||
JobStatus,
|
||||
TesseractOCRPipelineModel,
|
||||
SpaCyNLPPipelineModel
|
||||
@ -75,8 +74,6 @@ def tesseract_ocr_pipeline():
|
||||
version = request.args.get('version', service_manifest['latest_version'])
|
||||
if version not in service_manifest['versions']:
|
||||
abort(404)
|
||||
job_results = JobResult.query.all()
|
||||
choosable_job_ids = [job_result.job.hashid for job_result in job_results if job_result.job.service == "file-setup-pipeline" and job_result.filename.endswith('.pdf')]
|
||||
form = CreateTesseractOCRPipelineJobForm(prefix='create-job-form', version=version)
|
||||
if form.is_submitted():
|
||||
if not form.validate():
|
||||
@ -114,7 +111,6 @@ def tesseract_ocr_pipeline():
|
||||
return render_template(
|
||||
'services/tesseract_ocr_pipeline.html.j2',
|
||||
title=service_manifest['name'],
|
||||
choosable_job_ids=choosable_job_ids,
|
||||
form=form,
|
||||
tesseract_ocr_pipeline_models=tesseract_ocr_pipeline_models,
|
||||
user_tesseract_ocr_pipeline_models_count=user_tesseract_ocr_pipeline_models_count
|
||||
|
57
app/static/js/forms/base-form-new.js
Normal file
57
app/static/js/forms/base-form-new.js
Normal file
@ -0,0 +1,57 @@
|
||||
export class BaseForm {
|
||||
constructor(formElement) {
|
||||
this.element = formElement;
|
||||
|
||||
this.element.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
this.submit();
|
||||
});
|
||||
console.log("UsernamePostFormInitialized");
|
||||
}
|
||||
|
||||
submit() {
|
||||
let errorTextElements = this.element
|
||||
.querySelectorAll('.supporting-text[data-supporting-text-type="error"]');
|
||||
for (let errorTextElement of errorTextElements) {errorTextElement.remove();}
|
||||
|
||||
const body = new FormData(this.element);
|
||||
const headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
const method = this.element.method;
|
||||
|
||||
const fetchPromise = new Promise((resolve, reject) => {
|
||||
fetch(this.element.action, {body: body, headers: headers, method: method})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
console.log("reject", response);
|
||||
reject(response);
|
||||
return;
|
||||
}
|
||||
console.log("resolve", response);
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
|
||||
fetchPromise
|
||||
.then(
|
||||
(response) => {console.log("Hello from resolve handler");return response.json();},
|
||||
(response) => {
|
||||
console.log("Hello from reject handler 1/2");
|
||||
response.json()
|
||||
.then((errors) => {
|
||||
console.log("Hello from reject handler 2/2");
|
||||
for (let [name, messages] of Object.entries(errors)) {
|
||||
console.log(name, messages);
|
||||
const inputFieldElement = this.element[name].closest('.input-field');
|
||||
for (let message of messages) {
|
||||
const messageHTML = `<span class="supporting-text" data-supporting-text-type="error">${message}</span>`;
|
||||
inputFieldElement.insertAdjacentHTML('beforeend', messageHTML);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
@ -1,137 +0,0 @@
|
||||
nopaque.resource_lists.JobOutputList = class JobOutputList extends nopaque.resource_lists.ResourceList {
|
||||
static htmlClass = 'job-output-list';
|
||||
|
||||
constructor(listContainerElement, options = {}) {
|
||||
super(listContainerElement, options);
|
||||
this.listjs.list.addEventListener('click', (event) => {this.onClick(event)});
|
||||
this.isInitialized = false;
|
||||
this.userId = listContainerElement.dataset.userId;
|
||||
this.jobOutput = listContainerElement.dataset.jobOutput;
|
||||
this.jobIds = listContainerElement.dataset.jobIds;
|
||||
if (this.userId === undefined) {return;}
|
||||
app.subscribeUser(this.userId).then((response) => {
|
||||
app.socket.on('PATCH', (patch) => {
|
||||
if (this.isInitialized) {this.onPatch(patch);}
|
||||
});
|
||||
});
|
||||
app.getUser(this.userId).then((user) => {
|
||||
let jobIds = JSON.parse(this.jobIds.replace(/'/g, '"'));
|
||||
let job_results = {};
|
||||
for (let jobId of jobIds) {
|
||||
for (let jobResult of Object.values(user.jobs[jobId].results)) {
|
||||
if (jobResult.mimetype === 'application/pdf') {
|
||||
job_results[jobResult.id] = jobResult;
|
||||
job_results[jobResult.id].description = user.jobs[jobId].description;
|
||||
job_results[jobResult.id].title = user.jobs[jobId].title;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.add(Object.values(job_results));
|
||||
this.isInitialized = true;
|
||||
});
|
||||
}
|
||||
|
||||
get item() {
|
||||
return `
|
||||
<tr class="list-item clickable hoverable">
|
||||
<td><span class="title"></span></td>
|
||||
<td><span class="description"></span></td>
|
||||
<td><span class="filename"></span></td>
|
||||
<td class="right-align">
|
||||
<a class="list-action-trigger btn-flat waves-effect waves-light" data-list-action="add"><i class="material-icons">add</i></a>
|
||||
</td>
|
||||
</tr>
|
||||
`.trim();
|
||||
}
|
||||
|
||||
get valueNames() {
|
||||
return [
|
||||
{data: ['id']},
|
||||
{data: ['creation-date']},
|
||||
'title',
|
||||
'description',
|
||||
'filename'
|
||||
];
|
||||
}
|
||||
|
||||
initListContainerElement() {
|
||||
if (!this.listContainerElement.hasAttribute('id')) {
|
||||
this.listContainerElement.id = nopaque.Utils.generateElementId('job-output-list-');
|
||||
}
|
||||
let listSearchElementId = nopaque.Utils.generateElementId(`${this.listContainerElement.id}-search-`);
|
||||
this.listContainerElement.innerHTML = `
|
||||
<div class="input-field">
|
||||
<i class="material-icons prefix">search</i>
|
||||
<input id="${listSearchElementId}" class="search" type="text"></input>
|
||||
<label for="${listSearchElementId}">Search job output</label>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Filename</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="list"></tbody>
|
||||
</table>
|
||||
<ul class="pagination"></ul>
|
||||
`;
|
||||
}
|
||||
|
||||
mapResourceToValue(jobOutput) {
|
||||
console.log(jobOutput);
|
||||
return {
|
||||
'id': jobOutput.id,
|
||||
'creation-date': jobOutput.creationDate,
|
||||
'title': jobOutput.title,
|
||||
'description': jobOutput.description,
|
||||
'filename': jobOutput.filename
|
||||
};
|
||||
}
|
||||
|
||||
sort() {
|
||||
this.listjs.sort('title', {order: 'asc'});
|
||||
}
|
||||
|
||||
onClick(event) {
|
||||
let listItemElement = event.target.closest('.list-item[data-id]');
|
||||
if (listItemElement === null) {return;}
|
||||
let itemId = listItemElement.dataset.id;
|
||||
let listActionElement = event.target.closest('.list-action-trigger[data-list-action]');
|
||||
let listAction = listActionElement === null ? 'add' : listActionElement.dataset.listAction;
|
||||
switch (listAction) {
|
||||
case 'add': {
|
||||
listActionElement.querySelector('i').textContent = 'done';
|
||||
listActionElement.dataset.listAction = 'remove';
|
||||
break;
|
||||
}
|
||||
case 'remove': {
|
||||
listActionElement.querySelector('i').textContent = 'add';
|
||||
listActionElement.dataset.listAction = 'add';
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// onPatch(patch) {
|
||||
// let re = new RegExp(`^/users/${this.userId}/jobs/${this.jobId}/results/([A-Za-z0-9]*)`);
|
||||
// let filteredPatch = patch.filter(operation => re.test(operation.path));
|
||||
// for (let operation of filteredPatch) {
|
||||
// switch(operation.op) {
|
||||
// case 'add': {
|
||||
// let re = new RegExp(`^/users/${this.userId}/jobs/${this.jobId}/results/([A-Za-z0-9]*)$`);
|
||||
// if (re.test(operation.path)) {this.add(operation.value);}
|
||||
// break;
|
||||
// }
|
||||
// default: {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
};
|
@ -52,7 +52,6 @@
|
||||
'js/resource-lists/job-input-list.js',
|
||||
'js/resource-lists/job-list.js',
|
||||
'js/resource-lists/job-result-list.js',
|
||||
'js/resource-lists/job-output-list.js',
|
||||
'js/resource-lists/public-corpus-list.js',
|
||||
'js/resource-lists/public-user-list.js',
|
||||
'js/resource-lists/spacy-nlp-pipeline-model-list.js',
|
||||
|
@ -37,15 +37,6 @@
|
||||
|
||||
<div class="col s12">
|
||||
<h2>Submit a job</h2>
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<p>Add an existing file from your workflow or add a new one below.</p>
|
||||
<div class="job-output-list" data-user-id="{{ current_user.hashid}}" data-job-ids="{{ choosable_job_ids }}"></div>
|
||||
</div>
|
||||
<div class="card-action right-align">
|
||||
<a class="waves-effect waves-light btn"><i class="material-icons right">send</i>Submit</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<form class="create-job-form" enctype="multipart/form-data" method="POST">
|
||||
<div class="card-content">
|
||||
@ -60,8 +51,6 @@
|
||||
<div class="col s12 l5">
|
||||
{{ wtf.render_field(form.pdf, accept='application/pdf', placeholder='Choose a PDF file') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col s12 l4">
|
||||
<div class="input-field">
|
||||
<i class="material-icons prefix">language</i>
|
||||
|
@ -1,4 +1,3 @@
|
||||
from flask_login import current_user
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileField, FileRequired
|
||||
from wtforms import (
|
||||
@ -17,7 +16,7 @@ from wtforms.validators import (
|
||||
Regexp
|
||||
)
|
||||
from app.models import User, UserSettingJobStatusMailNotificationLevel
|
||||
from app.wtforms.validators import FileSize
|
||||
from app.extensions.wtforms.validators import FileSize
|
||||
|
||||
|
||||
class UpdateAccountInformationForm(FlaskForm):
|
||||
|
2
boot.sh
2
boot.sh
@ -24,7 +24,7 @@ if [[ "${#}" == "0" ]]; then
|
||||
sleep 5
|
||||
done
|
||||
fi
|
||||
python3 nopaque.py
|
||||
python3 wsgi.py
|
||||
elif [[ "${1}" == "flask" ]]; then
|
||||
flask ${@:2}
|
||||
elif [[ "${1}" == "--help" || "${1}" == "-h" ]]; then
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: "3.5"
|
||||
|
||||
# The docker-compose.yml file is not meant to be modified itself.
|
||||
# Instead use the following files for configurations:
|
||||
# - .env: Environment variables for the docker-compose.yml file.
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: "3.5"
|
||||
|
||||
services:
|
||||
nopaque:
|
||||
environment:
|
||||
@ -13,6 +11,6 @@ services:
|
||||
- "./config.py:/home/nopaque/config.py"
|
||||
- "./docker-entrypoint.sh:/usr/local/bin/docker-entrypoint.sh"
|
||||
- "./migrations:/home/nopaque/migrations"
|
||||
- "./nopaque.py:/home/nopaque/nopaque.py"
|
||||
- "./requirements.txt:/home/nopaque/requirements.txt"
|
||||
- "./tests:/home/nopaque/tests"
|
||||
- "./wsgi.py:/home/nopaque/wsgi.py"
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: "3.5"
|
||||
|
||||
services:
|
||||
nopaque:
|
||||
environment:
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: "3.5"
|
||||
|
||||
networks:
|
||||
traefik:
|
||||
external: true
|
||||
|
@ -15,7 +15,7 @@ Flask-Marshmallow==0.14.0
|
||||
Flask-Menu==0.7.2
|
||||
Flask-Migrate
|
||||
Flask-Paranoid
|
||||
Flask-SocketIO
|
||||
Flask-SocketIO==5.3.6
|
||||
Flask-SQLAlchemy==2.5.1
|
||||
Flask-WTF
|
||||
hiredis
|
||||
|
Reference in New Issue
Block a user