mirror of
				https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
				synced 2025-11-04 04:12:45 +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
 | 
			
		||||
    from .namespaces.cqi_over_sio import CQiOverSocketIONamespace
 | 
			
		||||
    socketio.on_namespace(CQiOverSocketIONamespace('/cqi_over_sio'))
 | 
			
		||||
 | 
			
		||||
    from .namespaces.users import UsersNamespace
 | 
			
		||||
    socketio.on_namespace(UsersNamespace('/users'))
 | 
			
		||||
    # endregion SocketIO Namespaces
 | 
			
		||||
 | 
			
		||||
    # region Database event Listeners
 | 
			
		||||
 
 | 
			
		||||
@@ -15,4 +15,4 @@ def before_request():
 | 
			
		||||
    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: {}
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.sockets = {
 | 
			
		||||
      users: io('/users', {transports: ['websocket'], upgrade: false})
 | 
			
		||||
    };
 | 
			
		||||
    this.socket = io({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) {
 | 
			
		||||
@@ -23,10 +21,8 @@ nopaque.App = class App {
 | 
			
		||||
      return this.#promises.getUser[userId];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let socket = this.sockets.users;
 | 
			
		||||
 | 
			
		||||
    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) {
 | 
			
		||||
          this.data.users[userId] = response.body;
 | 
			
		||||
          resolve(this.data.users[userId]);
 | 
			
		||||
@@ -44,10 +40,8 @@ nopaque.App = class App {
 | 
			
		||||
      return this.#promises.subscribeUser[userId];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let socket = this.sockets.users;
 | 
			
		||||
 | 
			
		||||
    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) {
 | 
			
		||||
          resolve(response);
 | 
			
		||||
        } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ nopaque.resource_displays.ResourceDisplay = class ResourceDisplay {
 | 
			
		||||
    if (this.userId) {
 | 
			
		||||
      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);}
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ nopaque.resource_lists.CorpusFileList = class CorpusFileList extends nopaque.res
 | 
			
		||||
    this.hasPermissionManageFiles =  listContainerElement.dataset?.hasPermissionManageFiles == 'true' || false;
 | 
			
		||||
    if (this.userId === undefined || this.corpusId === undefined) {return;}
 | 
			
		||||
    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);}
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ nopaque.resource_lists.CorpusFollowerList = class CorpusFollowerList extends nop
 | 
			
		||||
    this.corpusId = listContainerElement.dataset.corpusId;
 | 
			
		||||
    if (this.userId === undefined || this.corpusId === undefined) {return;}
 | 
			
		||||
    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);}
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
 | 
			
		||||
    this.userId = listContainerElement.dataset.userId;
 | 
			
		||||
    if (this.userId === undefined) {return;}
 | 
			
		||||
    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);}
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ nopaque.resource_lists.JobList = class JobList extends nopaque.resource_lists.Re
 | 
			
		||||
    this.userId = listContainerElement.dataset.userId;
 | 
			
		||||
    if (this.userId === undefined) {return;}
 | 
			
		||||
    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);}
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ nopaque.resource_lists.JobResultList = class JobResultList extends nopaque.resou
 | 
			
		||||
    this.jobId = listContainerElement.dataset.jobId;
 | 
			
		||||
    if (this.userId === undefined || this.jobId === undefined) {return;}
 | 
			
		||||
    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);}
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ nopaque.resource_lists.SpaCyNLPPipelineModelList = class SpaCyNLPPipelineModelLi
 | 
			
		||||
    this.userId = listContainerElement.dataset.userId;
 | 
			
		||||
    if (this.userId === undefined) {return;}
 | 
			
		||||
    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);}
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ nopaque.resource_lists.TesseractOCRPipelineModelList = class TesseractOCRPipelin
 | 
			
		||||
    this.userId = listContainerElement.dataset.userId;
 | 
			
		||||
    if (this.userId === undefined) {return;}
 | 
			
		||||
    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);}
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@
 | 
			
		||||
{# shown for large devices #}
 | 
			
		||||
<ul class="hide-on-med-and-down" style="margin-left: calc(57px + 1.5rem);">
 | 
			
		||||
  {# dashboard #}
 | 
			
		||||
  <li>
 | 
			
		||||
  <li {% if request.path == url_for('main.dashboard') %}class="active"{% endif %}>
 | 
			
		||||
    <a href="{{ url_for('main.dashboard') }}">
 | 
			
		||||
      <i class="material-icons left">dashboard</i>
 | 
			
		||||
      Dashboard
 | 
			
		||||
@@ -29,7 +29,7 @@
 | 
			
		||||
  </li>
 | 
			
		||||
 | 
			
		||||
  {# contributions #}
 | 
			
		||||
  <li>
 | 
			
		||||
  <li {% if request.path == url_for('contributions.index') %}class="active"{% endif %}>
 | 
			
		||||
    <a href="{{ url_for('contributions.index') }}">
 | 
			
		||||
      <i class="material-icons left">new_label</i>
 | 
			
		||||
      Contributions
 | 
			
		||||
@@ -37,7 +37,7 @@
 | 
			
		||||
  </li>
 | 
			
		||||
 | 
			
		||||
  {# social #}
 | 
			
		||||
  <li>
 | 
			
		||||
  <li {% if request.path == url_for('main.social') %}class="active"{% endif %}>
 | 
			
		||||
    <a href="{{ url_for('main.social') }}">
 | 
			
		||||
      <i class="material-icons left">groups</i>
 | 
			
		||||
      Social
 | 
			
		||||
@@ -62,14 +62,14 @@
 | 
			
		||||
{# large devices #}
 | 
			
		||||
<ul class="right hide-on-med-and-down" style="height: 64px;">
 | 
			
		||||
  {# 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') }}">
 | 
			
		||||
      <i class="material-icons">help_outline</i>
 | 
			
		||||
    </a>
 | 
			
		||||
  </li>
 | 
			
		||||
 | 
			
		||||
  {# 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') }}">
 | 
			
		||||
      <i class="material-icons">email</i>
 | 
			
		||||
    </a>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,21 +5,20 @@
 | 
			
		||||
  <div class="row">
 | 
			
		||||
    <div class="col s12">
 | 
			
		||||
      <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 class="col s12">
 | 
			
		||||
      <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 class="col s12 l4">
 | 
			
		||||
      <h4>Tesseract OCR Pipeline Models</h4>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="col s12">
 | 
			
		||||
    <div class="col s12 l8">
 | 
			
		||||
      <div class="card">
 | 
			
		||||
        <div class="card-content">
 | 
			
		||||
          <div class="tesseract-ocr-pipeline-model-list" data-user-id="{{ current_user.hashid }}"></div>
 | 
			
		||||
@@ -29,6 +28,21 @@
 | 
			
		||||
        </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>
 | 
			
		||||
{% endblock page_content %}
 | 
			
		||||
 
 | 
			
		||||
@@ -74,7 +74,7 @@
 | 
			
		||||
                  {{ form.model.label }}
 | 
			
		||||
                  <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="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>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -56,7 +56,7 @@
 | 
			
		||||
                  {{ form.model.label }}
 | 
			
		||||
                  <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="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>
 | 
			
		||||
                  {% for error in form.model.errors %}
 | 
			
		||||
                  <span class="helper-text error-color-text">{{ error }}</span>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user