mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2024-12-26 11:24:18 +00:00
Compare commits
3 Commits
460257294d
...
54c4295bf7
Author | SHA1 | Date | |
---|---|---|---|
|
54c4295bf7 | ||
|
1e5c26b8e3 | ||
|
9f56647cf7 |
@ -132,9 +132,6 @@ def create_app(config: Config = Config) -> Flask:
|
|||||||
# region SocketIO Namespaces
|
# region SocketIO Namespaces
|
||||||
from .namespaces.cqi_over_sio import CQiOverSocketIONamespace
|
from .namespaces.cqi_over_sio import CQiOverSocketIONamespace
|
||||||
socketio.on_namespace(CQiOverSocketIONamespace('/cqi_over_sio'))
|
socketio.on_namespace(CQiOverSocketIONamespace('/cqi_over_sio'))
|
||||||
|
|
||||||
from .namespaces.users import UsersNamespace
|
|
||||||
socketio.on_namespace(UsersNamespace('/users'))
|
|
||||||
# endregion SocketIO Namespaces
|
# endregion SocketIO Namespaces
|
||||||
|
|
||||||
# region Database event Listeners
|
# region Database event Listeners
|
||||||
|
@ -15,4 +15,4 @@ def before_request():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
from . import cli, json_routes, routes, settings
|
from . import cli, events, json_routes, routes, settings
|
||||||
|
82
app/blueprints/users/events.py
Normal file
82
app/blueprints/users/events.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
from flask_login import current_user
|
||||||
|
from flask_socketio import join_room, leave_room
|
||||||
|
from app import hashids, socketio
|
||||||
|
from app.decorators import socketio_login_required
|
||||||
|
from app.models import User
|
||||||
|
|
||||||
|
|
||||||
|
@socketio.on('users.get_user')
|
||||||
|
@socketio_login_required
|
||||||
|
def get_user(user_hashid: str) -> dict:
|
||||||
|
user_id = hashids.decode(user_hashid)
|
||||||
|
|
||||||
|
if not isinstance(user_id, int):
|
||||||
|
return {'status': 400, 'statusText': 'Bad Request'}
|
||||||
|
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return {'status': 404, 'statusText': 'Not found'}
|
||||||
|
|
||||||
|
if not (
|
||||||
|
user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
return {'status': 403, 'statusText': 'Forbidden'}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'body': user.to_json_serializeable(
|
||||||
|
backrefs=True,
|
||||||
|
relationships=True
|
||||||
|
),
|
||||||
|
'status': 200,
|
||||||
|
'statusText': 'OK'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@socketio.on('users.subscribe_user')
|
||||||
|
@socketio_login_required
|
||||||
|
def subscribe_user(user_hashid: str) -> dict:
|
||||||
|
user_id = hashids.decode(user_hashid)
|
||||||
|
|
||||||
|
if not isinstance(user_id, int):
|
||||||
|
return {'status': 400, 'statusText': 'Bad Request'}
|
||||||
|
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return {'status': 404, 'statusText': 'Not found'}
|
||||||
|
|
||||||
|
if not (
|
||||||
|
user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
return {'status': 403, 'statusText': 'Forbidden'}
|
||||||
|
|
||||||
|
join_room(f'/users/{user.hashid}')
|
||||||
|
|
||||||
|
return {'status': 200, 'statusText': 'OK'}
|
||||||
|
|
||||||
|
|
||||||
|
@socketio.on('users.unsubscribe_user')
|
||||||
|
@socketio_login_required
|
||||||
|
def on_unsubscribe_user(user_hashid: str) -> dict:
|
||||||
|
user_id = hashids.decode(user_hashid)
|
||||||
|
|
||||||
|
if not isinstance(user_id, int):
|
||||||
|
return {'status': 400, 'statusText': 'Bad Request'}
|
||||||
|
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return {'status': 404, 'statusText': 'Not found'}
|
||||||
|
|
||||||
|
if not (
|
||||||
|
user == current_user
|
||||||
|
or current_user.is_administrator
|
||||||
|
):
|
||||||
|
return {'status': 403, 'statusText': 'Forbidden'}
|
||||||
|
|
||||||
|
leave_room(f'/users/{user.hashid}')
|
||||||
|
|
||||||
|
return {'status': 200, 'statusText': 'OK'}
|
@ -1,78 +0,0 @@
|
|||||||
from flask_login import current_user
|
|
||||||
from flask_socketio import join_room, leave_room, Namespace
|
|
||||||
from app import hashids
|
|
||||||
from app.decorators import socketio_login_required
|
|
||||||
from app.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class UsersNamespace(Namespace):
|
|
||||||
@socketio_login_required
|
|
||||||
def on_get_user(self, user_hashid: str) -> dict:
|
|
||||||
user_id = hashids.decode(user_hashid)
|
|
||||||
|
|
||||||
if not isinstance(user_id, int):
|
|
||||||
return {'status': 400, 'statusText': 'Bad Request'}
|
|
||||||
|
|
||||||
user = User.query.get(user_id)
|
|
||||||
|
|
||||||
if user is None:
|
|
||||||
return {'status': 404, 'statusText': 'Not found'}
|
|
||||||
|
|
||||||
if not (
|
|
||||||
user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
return {'status': 403, 'statusText': 'Forbidden'}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'body': user.to_json_serializeable(
|
|
||||||
backrefs=True,
|
|
||||||
relationships=True
|
|
||||||
),
|
|
||||||
'status': 200,
|
|
||||||
'statusText': 'OK'
|
|
||||||
}
|
|
||||||
|
|
||||||
@socketio_login_required
|
|
||||||
def on_subscribe_user(self, user_hashid: str) -> dict:
|
|
||||||
user_id = hashids.decode(user_hashid)
|
|
||||||
|
|
||||||
if not isinstance(user_id, int):
|
|
||||||
return {'status': 400, 'statusText': 'Bad Request'}
|
|
||||||
|
|
||||||
user = User.query.get(user_id)
|
|
||||||
|
|
||||||
if user is None:
|
|
||||||
return {'status': 404, 'statusText': 'Not found'}
|
|
||||||
|
|
||||||
if not (
|
|
||||||
user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
return {'status': 403, 'statusText': 'Forbidden'}
|
|
||||||
|
|
||||||
join_room(f'/users/{user.hashid}')
|
|
||||||
|
|
||||||
return {'status': 200, 'statusText': 'OK'}
|
|
||||||
|
|
||||||
@socketio_login_required
|
|
||||||
def on_unsubscribe_user(self, user_hashid: str) -> dict:
|
|
||||||
user_id = hashids.decode(user_hashid)
|
|
||||||
|
|
||||||
if not isinstance(user_id, int):
|
|
||||||
return {'status': 400, 'statusText': 'Bad Request'}
|
|
||||||
|
|
||||||
user = User.query.get(user_id)
|
|
||||||
|
|
||||||
if user is None:
|
|
||||||
return {'status': 404, 'statusText': 'Not found'}
|
|
||||||
|
|
||||||
if not (
|
|
||||||
user == current_user
|
|
||||||
or current_user.is_administrator
|
|
||||||
):
|
|
||||||
return {'status': 403, 'statusText': 'Forbidden'}
|
|
||||||
|
|
||||||
leave_room(f'/users/{user.hashid}')
|
|
||||||
|
|
||||||
return {'status': 200, 'statusText': 'OK'}
|
|
@ -11,11 +11,9 @@ nopaque.App = class App {
|
|||||||
subscribeUser: {}
|
subscribeUser: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sockets = {
|
this.socket = io({transports: ['websocket'], upgrade: false});
|
||||||
users: io('/users', {transports: ['websocket'], upgrade: false})
|
|
||||||
};
|
|
||||||
|
|
||||||
this.sockets.users.on('patch_user', (patch) => {this.onPatch(patch);});
|
this.socket.on('patch_user', (patch) => {this.onPatch(patch);});
|
||||||
}
|
}
|
||||||
|
|
||||||
getUser(userId) {
|
getUser(userId) {
|
||||||
@ -23,10 +21,8 @@ nopaque.App = class App {
|
|||||||
return this.#promises.getUser[userId];
|
return this.#promises.getUser[userId];
|
||||||
}
|
}
|
||||||
|
|
||||||
let socket = this.sockets.users;
|
|
||||||
|
|
||||||
this.#promises.getUser[userId] = new Promise((resolve, reject) => {
|
this.#promises.getUser[userId] = new Promise((resolve, reject) => {
|
||||||
socket.emit('get_user', userId, (response) => {
|
this.socket.emit('users.get_user', userId, (response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
this.data.users[userId] = response.body;
|
this.data.users[userId] = response.body;
|
||||||
resolve(this.data.users[userId]);
|
resolve(this.data.users[userId]);
|
||||||
@ -44,10 +40,8 @@ nopaque.App = class App {
|
|||||||
return this.#promises.subscribeUser[userId];
|
return this.#promises.subscribeUser[userId];
|
||||||
}
|
}
|
||||||
|
|
||||||
let socket = this.sockets.users;
|
|
||||||
|
|
||||||
this.#promises.subscribeUser[userId] = new Promise((resolve, reject) => {
|
this.#promises.subscribeUser[userId] = new Promise((resolve, reject) => {
|
||||||
socket.emit('subscribe_user', userId, (response) => {
|
this.socket.emit('users.subscribe_user', userId, (response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
resolve(response);
|
resolve(response);
|
||||||
} else {
|
} else {
|
||||||
|
@ -8,7 +8,7 @@ nopaque.resource_displays.ResourceDisplay = class ResourceDisplay {
|
|||||||
if (this.userId) {
|
if (this.userId) {
|
||||||
app.subscribeUser(this.userId)
|
app.subscribeUser(this.userId)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
app.sockets.users.on('patch_user', (patch) => {
|
app.socket.on('patch_user', (patch) => {
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
if (this.isInitialized) {this.onPatch(patch);}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -15,7 +15,7 @@ nopaque.resource_lists.CorpusFileList = class CorpusFileList extends nopaque.res
|
|||||||
this.hasPermissionManageFiles = listContainerElement.dataset?.hasPermissionManageFiles == 'true' || false;
|
this.hasPermissionManageFiles = listContainerElement.dataset?.hasPermissionManageFiles == 'true' || false;
|
||||||
if (this.userId === undefined || this.corpusId === undefined) {return;}
|
if (this.userId === undefined || this.corpusId === undefined) {return;}
|
||||||
app.subscribeUser(this.userId).then((response) => {
|
app.subscribeUser(this.userId).then((response) => {
|
||||||
app.sockets.users.on('patch_user', (patch) => {
|
app.socket.on('patch_user', (patch) => {
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
if (this.isInitialized) {this.onPatch(patch);}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -13,7 +13,7 @@ nopaque.resource_lists.CorpusFollowerList = class CorpusFollowerList extends nop
|
|||||||
this.corpusId = listContainerElement.dataset.corpusId;
|
this.corpusId = listContainerElement.dataset.corpusId;
|
||||||
if (this.userId === undefined || this.corpusId === undefined) {return;}
|
if (this.userId === undefined || this.corpusId === undefined) {return;}
|
||||||
app.subscribeUser(this.userId).then((response) => {
|
app.subscribeUser(this.userId).then((response) => {
|
||||||
app.sockets.users.on('patch_user', (patch) => {
|
app.socket.on('patch_user', (patch) => {
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
if (this.isInitialized) {this.onPatch(patch);}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -12,7 +12,7 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
|
|||||||
this.userId = listContainerElement.dataset.userId;
|
this.userId = listContainerElement.dataset.userId;
|
||||||
if (this.userId === undefined) {return;}
|
if (this.userId === undefined) {return;}
|
||||||
app.subscribeUser(this.userId).then((response) => {
|
app.subscribeUser(this.userId).then((response) => {
|
||||||
app.sockets.users.on('patch_user', (patch) => {
|
app.socket.on('patch_user', (patch) => {
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
if (this.isInitialized) {this.onPatch(patch);}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -13,7 +13,7 @@ nopaque.resource_lists.JobList = class JobList extends nopaque.resource_lists.Re
|
|||||||
this.userId = listContainerElement.dataset.userId;
|
this.userId = listContainerElement.dataset.userId;
|
||||||
if (this.userId === undefined) {return;}
|
if (this.userId === undefined) {return;}
|
||||||
app.subscribeUser(this.userId).then((response) => {
|
app.subscribeUser(this.userId).then((response) => {
|
||||||
app.sockets.users.on('patch_user', (patch) => {
|
app.socket.on('patch_user', (patch) => {
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
if (this.isInitialized) {this.onPatch(patch);}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,7 @@ nopaque.resource_lists.JobResultList = class JobResultList extends nopaque.resou
|
|||||||
this.jobId = listContainerElement.dataset.jobId;
|
this.jobId = listContainerElement.dataset.jobId;
|
||||||
if (this.userId === undefined || this.jobId === undefined) {return;}
|
if (this.userId === undefined || this.jobId === undefined) {return;}
|
||||||
app.subscribeUser(this.userId).then((response) => {
|
app.subscribeUser(this.userId).then((response) => {
|
||||||
app.sockets.users.on('patch_user', (patch) => {
|
app.socket.on('patch_user', (patch) => {
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
if (this.isInitialized) {this.onPatch(patch);}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,7 @@ nopaque.resource_lists.SpaCyNLPPipelineModelList = class SpaCyNLPPipelineModelLi
|
|||||||
this.userId = listContainerElement.dataset.userId;
|
this.userId = listContainerElement.dataset.userId;
|
||||||
if (this.userId === undefined) {return;}
|
if (this.userId === undefined) {return;}
|
||||||
app.subscribeUser(this.userId).then((response) => {
|
app.subscribeUser(this.userId).then((response) => {
|
||||||
app.sockets.users.on('patch_user', (patch) => {
|
app.socket.on('patch_user', (patch) => {
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
if (this.isInitialized) {this.onPatch(patch);}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,7 @@ nopaque.resource_lists.TesseractOCRPipelineModelList = class TesseractOCRPipelin
|
|||||||
this.userId = listContainerElement.dataset.userId;
|
this.userId = listContainerElement.dataset.userId;
|
||||||
if (this.userId === undefined) {return;}
|
if (this.userId === undefined) {return;}
|
||||||
app.subscribeUser(this.userId).then((response) => {
|
app.subscribeUser(this.userId).then((response) => {
|
||||||
app.sockets.users.on('patch_user', (patch) => {
|
app.socket.on('patch_user', (patch) => {
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
if (this.isInitialized) {this.onPatch(patch);}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
{# shown for large devices #}
|
{# shown for large devices #}
|
||||||
<ul class="hide-on-med-and-down" style="margin-left: calc(57px + 1.5rem);">
|
<ul class="hide-on-med-and-down" style="margin-left: calc(57px + 1.5rem);">
|
||||||
{# dashboard #}
|
{# dashboard #}
|
||||||
<li>
|
<li {% if request.path == url_for('main.dashboard') %}class="active"{% endif %}>
|
||||||
<a href="{{ url_for('main.dashboard') }}">
|
<a href="{{ url_for('main.dashboard') }}">
|
||||||
<i class="material-icons left">dashboard</i>
|
<i class="material-icons left">dashboard</i>
|
||||||
Dashboard
|
Dashboard
|
||||||
@ -29,7 +29,7 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
{# contributions #}
|
{# contributions #}
|
||||||
<li>
|
<li {% if request.path == url_for('contributions.index') %}class="active"{% endif %}>
|
||||||
<a href="{{ url_for('contributions.index') }}">
|
<a href="{{ url_for('contributions.index') }}">
|
||||||
<i class="material-icons left">new_label</i>
|
<i class="material-icons left">new_label</i>
|
||||||
Contributions
|
Contributions
|
||||||
@ -37,7 +37,7 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
{# social #}
|
{# social #}
|
||||||
<li>
|
<li {% if request.path == url_for('main.social') %}class="active"{% endif %}>
|
||||||
<a href="{{ url_for('main.social') }}">
|
<a href="{{ url_for('main.social') }}">
|
||||||
<i class="material-icons left">groups</i>
|
<i class="material-icons left">groups</i>
|
||||||
Social
|
Social
|
||||||
@ -62,14 +62,14 @@
|
|||||||
{# large devices #}
|
{# large devices #}
|
||||||
<ul class="right hide-on-med-and-down" style="height: 64px;">
|
<ul class="right hide-on-med-and-down" style="height: 64px;">
|
||||||
{# manual #}
|
{# manual #}
|
||||||
<li class="tooltipped" data-position="bottom" data-tooltip="Manual">
|
<li class="tooltipped {% if request.path == url_for('main.manual') %}active{% endif %}" data-position="bottom" data-tooltip="Manual">
|
||||||
<a href="{{ url_for('main.manual') }}">
|
<a href="{{ url_for('main.manual') }}">
|
||||||
<i class="material-icons">help_outline</i>
|
<i class="material-icons">help_outline</i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{# news #}
|
{# news #}
|
||||||
<li class="tooltipped" data-position="bottom" data-tooltip="News">
|
<li class="tooltipped {% if request.path == url_for('main.news') %}active{% endif %}" data-position="bottom" data-tooltip="News">
|
||||||
<a href="{{ url_for('main.news') }}">
|
<a href="{{ url_for('main.news') }}">
|
||||||
<i class="material-icons">email</i>
|
<i class="material-icons">email</i>
|
||||||
</a>
|
</a>
|
||||||
|
@ -5,21 +5,20 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s12">
|
<div class="col s12">
|
||||||
<h1 id="title">{{ title }}</h1>
|
<h1 id="title">{{ title }}</h1>
|
||||||
<p>Here you can see and edit the models that you have created. You can also create new models.</p>
|
<p>
|
||||||
|
Upload your own language models into nopaque. This is useful for
|
||||||
|
working with different languages that are not available as standard in
|
||||||
|
nopaque or if a you want to work with a language model that you have
|
||||||
|
developed by yourself. Uploaded models can be found in the model list
|
||||||
|
of the corresponding service and can be used immediately.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col s12">
|
<div class="col s12 l4">
|
||||||
<div class="card">
|
<h4>Tesseract OCR Pipeline Models</h4>
|
||||||
<div class="card-content">
|
|
||||||
<div class="spacy-nlp-pipeline-model-list" data-user-id="{{ current_user.hashid }}"></div>
|
|
||||||
</div>
|
|
||||||
<div class="card-action right-align">
|
|
||||||
<a href="{{ url_for('.spacy_nlp_pipeline_models.create') }}" class="btn waves-effect waves-light"><i class="material-icons left">add</i>Create</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col s12">
|
<div class="col s12 l8">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="tesseract-ocr-pipeline-model-list" data-user-id="{{ current_user.hashid }}"></div>
|
<div class="tesseract-ocr-pipeline-model-list" data-user-id="{{ current_user.hashid }}"></div>
|
||||||
@ -29,6 +28,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col s12 l4">
|
||||||
|
<h4>SpaCy NLP Pipeline Models</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col s12 l8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="spacy-nlp-pipeline-model-list" data-user-id="{{ current_user.hashid }}"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card-action right-align">
|
||||||
|
<a href="{{ url_for('.spacy_nlp_pipeline_models.create') }}" class="btn waves-effect waves-light"><i class="material-icons left">add</i>Create</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock page_content %}
|
{% endblock page_content %}
|
||||||
|
@ -74,7 +74,7 @@
|
|||||||
{{ form.model.label }}
|
{{ form.model.label }}
|
||||||
<span class="helper-text">
|
<span class="helper-text">
|
||||||
<a class="modal-trigger tooltipped" href="#models-modal" data-position="bottom" data-tooltip="See more information about models"><i class="material-icons">help_outline</i></a>
|
<a class="modal-trigger tooltipped" href="#models-modal" data-position="bottom" data-tooltip="See more information about models"><i class="material-icons">help_outline</i></a>
|
||||||
<a class="tooltipped" href="{{ url_for('spacy_nlp_pipeline_models.create') }}" data-position="bottom" data-tooltip="Add your own spaCy NLP models"><i class="material-icons">new_label</i></a>
|
<a class="tooltipped" href="{{ url_for('contributions.spacy_nlp_pipeline_models.create') }}" data-position="bottom" data-tooltip="Add your own spaCy NLP models"><i class="material-icons">new_label</i></a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,7 +56,7 @@
|
|||||||
{{ form.model.label }}
|
{{ form.model.label }}
|
||||||
<span class="helper-text">
|
<span class="helper-text">
|
||||||
<a class="modal-trigger tooltipped" href="#models-modal" data-position="bottom" data-tooltip="See more information about models"><i class="material-icons">help_outline</i></a>
|
<a class="modal-trigger tooltipped" href="#models-modal" data-position="bottom" data-tooltip="See more information about models"><i class="material-icons">help_outline</i></a>
|
||||||
<a class="tooltipped" href="{{ url_for('tesseract_ocr_pipeline_models.create') }}" data-position="bottom" data-tooltip="Add your own Tesseract OCR models"><i class="material-icons">new_label</i></a>
|
<a class="tooltipped" href="{{ url_for('contributions.tesseract_ocr_pipeline_models.create') }}" data-position="bottom" data-tooltip="Add your own Tesseract OCR models"><i class="material-icons">new_label</i></a>
|
||||||
</span>
|
</span>
|
||||||
{% for error in form.model.errors %}
|
{% for error in form.model.errors %}
|
||||||
<span class="helper-text error-color-text">{{ error }}</span>
|
<span class="helper-text error-color-text">{{ error }}</span>
|
||||||
|
Loading…
Reference in New Issue
Block a user