diff --git a/app/contributions/forms.py b/app/contributions/forms.py index 44279a1d9c199bd6a0df815e7aad76be7bb55bd3..8577ee979b6583c53a9a1b0115430ba93ebf144a 100644 --- a/app/contributions/forms.py +++ b/app/contributions/forms.py @@ -1,3 +1,4 @@ +from xml.dom import ValidationErr from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileRequired from wtforms import ( @@ -5,13 +6,13 @@ from wtforms import ( StringField, SubmitField, SelectMultipleField, - IntegerField + IntegerField, + ValidationError ) from wtforms.validators import InputRequired, Length from app.services import SERVICES - -class TesseractOCRModelContributionForm(FlaskForm): +class CreateContributionBaseForm(FlaskForm): title = StringField( 'Title', validators=[InputRequired(), Length(max=64)] @@ -24,9 +25,6 @@ class TesseractOCRModelContributionForm(FlaskForm): 'Version', validators=[InputRequired(), Length(max=16)] ) - compatible_service_versions = SelectMultipleField( - 'Compatible service versions' - ) publisher = StringField( 'Publisher', validators=[InputRequired(), Length(max=128)] @@ -43,10 +41,22 @@ class TesseractOCRModelContributionForm(FlaskForm): 'Publishing year', validators=[InputRequired()] ) - shared = BooleanField('Shared', validators=[InputRequired()]) - model_file = FileField('File',validators=[FileRequired()]) + shared = BooleanField( + 'Shared' + ) submit = SubmitField() +class TesseractOCRModelContributionForm(CreateContributionBaseForm): + tesseract_model_file = FileField( + 'File', + validators=[FileRequired()] + ) + compatible_service_versions = SelectMultipleField( + 'Compatible service versions' + ) + def validate_traineddata(self, field): + if field.data.mimetype != '.traineddata': + raise ValidationError('traineddata files only!') def __init__(self, *args, **kwargs): service_manifest = SERVICES['tesseract-ocr-pipeline'] @@ -56,3 +66,17 @@ class TesseractOCRModelContributionForm(FlaskForm): (x, x) for x in service_manifest['versions'].keys() ] self.compatible_service_versions.default = '' + +class TesseractOCRModelEditForm(CreateContributionBaseForm): + def prefill(self, model_file): + ''' Pre-fill the form with data of an exististing corpus file ''' + self.title.data = model_file.title + self.description.data = model_file.description + self.publisher.data = model_file.publisher + self.publishing_year.data = model_file.publishing_year + self.publisher_url.data = model_file.publisher_url + self.publishing_url.data = model_file.publishing_url + self.version.data = model_file.version + self.shared.data = model_file.shared + + diff --git a/app/contributions/routes.py b/app/contributions/routes.py index 287eda18908c4f0b33f74d1bef0aaa9a5963d4cf..385e1eec028c6a70f32ffae4baefca4835d22b0d 100644 --- a/app/contributions/routes.py +++ b/app/contributions/routes.py @@ -1,10 +1,11 @@ -from flask import abort, flash, Markup, render_template, url_for -from flask_login import login_required +from flask import abort, current_app, flash, Markup, redirect, render_template, url_for +from flask_login import login_required, current_user +from threading import Thread from app import db -from app.decorators import permission_required +from app.decorators import admin_required, permission_required from app.models import TesseractOCRPipelineModel, Permission from . import bp -from .forms import TesseractOCRModelContributionForm +from .forms import TesseractOCRModelContributionForm, TesseractOCRModelEditForm @bp.before_request @@ -14,13 +15,77 @@ def before_request(): pass -@bp.route('') +@bp.route('/') +@login_required +@admin_required def contributions(): - pass + tesseract_ocr_user_models = [ + x for x in current_user.tesseract_ocr_pipeline_models + ] + return render_template( + 'contributions/contribution_overview.html.j2', + tesseractOCRUserModels=tesseract_ocr_user_models, + userId = current_user.hashid, + title='Contribution Overview' + ) + +@bp.route('/<hashid:tesseract_ocr_pipeline_model_id>', methods=['GET', 'POST']) +@login_required +def tesseract_ocr_pipeline_model(tesseract_ocr_pipeline_model_id): + tesseract_ocr_pipeline_model = TesseractOCRPipelineModel.query.get_or_404( + tesseract_ocr_pipeline_model_id + ) + form = TesseractOCRModelEditForm(prefix='tesseract-ocr-model-edit-form') + if form.validate_on_submit(): + if tesseract_ocr_pipeline_model.title != form.title.data: + tesseract_ocr_pipeline_model.title = form.title.data + if tesseract_ocr_pipeline_model.description != form.description.data: + tesseract_ocr_pipeline_model.description = form.description.data + if tesseract_ocr_pipeline_model.publisher != form.publisher.data: + tesseract_ocr_pipeline_model.publisher = form.publisher.data + if tesseract_ocr_pipeline_model.publishing_year != form.publishing_year.data: + tesseract_ocr_pipeline_model.publishing_year = form.publishing_year.data + if tesseract_ocr_pipeline_model.publisher_url != form.publisher_url.data: + tesseract_ocr_pipeline_model.publisher_url = form.publisher_url.data + if tesseract_ocr_pipeline_model.publishing_url != form.publishing_url.data: + tesseract_ocr_pipeline_model.publishing_url = form.publishing_url.data + if tesseract_ocr_pipeline_model.version != form.version.data: + tesseract_ocr_pipeline_model.version = form.version.data + if tesseract_ocr_pipeline_model.shared != form.shared.data: + tesseract_ocr_pipeline_model.shared = form.shared.data + db.session.commit() + message = Markup(f'Model "<a href="contribute/{tesseract_ocr_pipeline_model.hashid}">{tesseract_ocr_pipeline_model.title}</a>" updated') + flash(message, category='corpus') + return {}, 201, {'Location': url_for('contributions.contributions')} + form.prefill(tesseract_ocr_pipeline_model) + return render_template( + 'contributions/tesseract_ocr_pipeline_model.html.j2', + tesseract_ocr_pipeline_model=tesseract_ocr_pipeline_model, + form=form, + title='Edit your Tesseract OCR model' + ) +@bp.route('/<hashid:tesseract_ocr_pipeline_model_id>', methods=['DELETE']) +@login_required +def delete_tesseract_model(tesseract_ocr_pipeline_model_id): + def _delete_tesseract_model(app, tesseract_ocr_pipeline_model_id): + with app.app_context(): + model = TesseractOCRPipelineModel.query.get(tesseract_ocr_pipeline_model_id) + model.delete() + db.session.commit() + + model = TesseractOCRPipelineModel.query.get_or_404(tesseract_ocr_pipeline_model_id) + if not (model.user == current_user or current_user.is_administrator()): + abort(403) + thread = Thread( + target=_delete_tesseract_model, + args=(current_app._get_current_object(), tesseract_ocr_pipeline_model_id) + ) + thread.start() + return {}, 202 -@bp.route('/tesseract-ocr-pipeline-models', methods=['GET', 'POST']) -def tesseract_ocr_pipeline_models(): +@bp.route('/add-tesseract-ocr-pipeline-model', methods=['GET', 'POST']) +def add_tesseract_ocr_pipeline_model(): form = TesseractOCRModelContributionForm( prefix='contribute-tesseract-ocr-pipeline-model-form' ) @@ -30,7 +95,7 @@ def tesseract_ocr_pipeline_models(): return response, 400 try: tesseract_ocr_model = TesseractOCRPipelineModel.create( - form.file.data, + form.tesseract_model_file.data, compatible_service_versions=form.compatible_service_versions.data, description=form.description.data, publisher=form.publisher.data, @@ -39,7 +104,8 @@ def tesseract_ocr_pipeline_models(): publishing_year=form.publishing_year.data, shared=form.shared.data, title=form.title.data, - version=form.version.data + version=form.version.data, + user=current_user ) except OSError: abort(500) @@ -47,8 +113,13 @@ def tesseract_ocr_pipeline_models(): message = Markup(f'Model "{tesseract_ocr_model.title}" created') flash(message) return {}, 201, {'Location': url_for('contributions.contributions')} + tesseract_ocr_pipeline_models = [ + x for x in TesseractOCRPipelineModel.query.all() + ] + return render_template( - 'contributions/contribute.html.j2', + 'contributions/contribute_tesseract_ocr_models.html.j2', form=form, - title='Contribution' + tesseract_ocr_pipeline_models=tesseract_ocr_pipeline_models, + title='Tesseract OCR Model Contribution' ) diff --git a/app/models.py b/app/models.py index cc5d60ceaf462537ae1b7a9e98e4f79f15f5326c..e1acf6de0b3d74175c67031f8a94e55d77b8158d 100644 --- a/app/models.py +++ b/app/models.py @@ -603,6 +603,13 @@ class TesseractOCRPipelineModel(FileMixin, HashidMixin, db.Model): pbar.close() db.session.commit() + def delete(self): + try: + os.remove(self.path) + except OSError as e: + current_app.logger.error(e) + db.session.delete(self) + def to_json(self, backrefs=False, relationships=False): _json = { 'id': self.hashid, @@ -1023,11 +1030,8 @@ class CorpusFile(FileMixin, HashidMixin, db.Model): def delete(self): try: os.remove(self.path) - except OSError: - current_app.logger.error( - f'Removing {self.path} led to an OSError!' - ) - pass + except OSError as e: + current_app.logger.error(e) db.session.delete(self) self.corpus.status = CorpusStatus.UNPREPARED diff --git a/app/static/js/Forms/CreateContributionForm.js b/app/static/js/Forms/CreateContributionForm.js new file mode 100644 index 0000000000000000000000000000000000000000..e7651ab0d1ce2a4762d318d8374d805994d9296d --- /dev/null +++ b/app/static/js/Forms/CreateContributionForm.js @@ -0,0 +1,18 @@ +class CreateContributionForm extends Form { + static autoInit() { + let createContributionFormElements = document.querySelectorAll('.create-contribution-form'); + for (let createContributionFormElement of createContributionFormElements) { + new CreateContributionForm(createContributionFormElement); + } + } + + constructor(formElement) { + super(formElement); + + this.addEventListener('requestLoad', (event) => { + if (event.target.status === 201) { + window.location.href = event.target.getResponseHeader('Location'); + } + }); + } +} diff --git a/app/static/js/Forms/Form.js b/app/static/js/Forms/Form.js index 9a21e98661a2e2ba8c64a7d9c846af4af25fd5a3..d93f3e2c7b3ebe69d99585d3c3c5583f1fd421b5 100644 --- a/app/static/js/Forms/Form.js +++ b/app/static/js/Forms/Form.js @@ -1,5 +1,6 @@ class Form { static autoInit() { + CreateContributionForm.autoInit(); CreateCorpusFileForm.autoInit(); CreateJobForm.autoInit(); } diff --git a/app/static/js/RessourceLists/TesseractOCRModelList.js b/app/static/js/RessourceLists/TesseractOCRModelList.js new file mode 100644 index 0000000000000000000000000000000000000000..9080447e361a688ec7dd06595744c2a5f6a33d06 --- /dev/null +++ b/app/static/js/RessourceLists/TesseractOCRModelList.js @@ -0,0 +1,77 @@ +class TesseractOCRModelList { + constructor () { + + this.elements = { + tesseractOCRModelList: document.querySelector('#tesseract-ocr-model-list'), + deleteButtons: document.querySelectorAll('.delete-button'), + editButtons: document.querySelectorAll('.edit-button'), + + } + } + + init () { + let userId = this.elements.tesseractOCRModelList.dataset.userId; + + for (let deleteButton of this.elements.deleteButtons) { + deleteButton.addEventListener('click', () => {this.deleteModel(deleteButton, userId);}); + } + + for (let editButton of this.elements.editButtons) { + editButton.addEventListener('click', () => {this.editModel(editButton);}); + } + } + + deleteModel(deleteButton, userId) { + return new Promise((resolve, reject) => { + let modelId = deleteButton.dataset.modelId; + let model = app.data.users[userId].tesseract_ocr_pipeline_models[modelId]; + + let modalElement = Utils.elementFromString( + ` + <div class="modal"> + <div class="modal-content"> + <h4>Confirm job deletion</h4> + <p>Do you really want to delete? All files will be permanently deleted!</p> + </div> + <div class="modal-footer"> + <a class="action-button btn modal-close waves-effect waves-light" data-action="cancel">Cancel</a> + <a class="action-button btn modal-close red waves-effect waves-light" data-action="confirm">Delete</a> + </div> + </div> + ` + ); + document.querySelector('#modals').appendChild(modalElement); + let modal = M.Modal.init( + modalElement, + { + dismissible: false, + onCloseEnd: () => { + modal.destroy(); + modalElement.remove(); + } + } + ); + let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]'); + confirmElement.addEventListener('click', (event) => { + let modelTitle = model.title; + fetch(`/contributions/${modelId}`, {method: 'DELETE'}) + .then( + (response) => { + app.flash(`Model "${modelTitle}" marked for deletion`, 'corpus'); + resolve(response); + }, + (response) => { + if (response.status === 403) {app.flash('Forbidden', 'error');} + if (response.status === 404) {app.flash('Not Found', 'error');} + reject(response); + } + ); + }); + modal.open(); + }); + } + + editModel(editButton) { + window.location.href = `/contributions/${editButton.dataset.modelId}`; + } +} diff --git a/app/templates/_scripts.html.j2 b/app/templates/_scripts.html.j2 index ccc32a05fc285a91808013a3d66781b2a63f526e..3b93ef668553148b6a0ccc24f67d1b11af3d2fac 100644 --- a/app/templates/_scripts.html.j2 +++ b/app/templates/_scripts.html.j2 @@ -9,6 +9,7 @@ 'js/Forms/Form.js', 'js/Forms/CreateCorpusFileForm.js', 'js/Forms/CreateJobForm.js', + 'js/Forms/CreateContributionForm.js', 'js/CorpusAnalysis/CQiClient.js', 'js/CorpusAnalysis/CorpusAnalysisApp.js', 'js/CorpusAnalysis/CorpusAnalysisConcordance.js', @@ -24,6 +25,7 @@ 'js/RessourceLists/JobInputList.js', 'js/RessourceLists/JobResultList.js', 'js/RessourceLists/QueryResultList.js', + 'js/RessourceLists/TesseractOCRModelList.js', 'js/RessourceLists/UserList.js' %} <script src="{{ ASSET_URL }}"></script> diff --git a/app/templates/contributions/_breadcrumbs.html.j2 b/app/templates/contributions/_breadcrumbs.html.j2 new file mode 100644 index 0000000000000000000000000000000000000000..9d49da6868d6292d7caac41dee7d80262e3f25c3 --- /dev/null +++ b/app/templates/contributions/_breadcrumbs.html.j2 @@ -0,0 +1,18 @@ +{% set breadcrumbs %} +<li class="tab disabled"><i class="material-icons">navigate_next</i></li> +{% if request.path == url_for('.contributions') %} +<li class="tab"><a class="active" href="{{ url_for('.contributions') }}" target="_self">Contributions Overview</a></li> +{% elif request.path == url_for('.tesseract_ocr_pipeline_model', tesseract_ocr_pipeline_model_id=tesseract_ocr_pipeline_model.id) %} +<li class="tab"><a href="{{ url_for('.contributions') }}" target="_self">Contributions Overview</a></li> +<li class="tab disabled"><i class="material-icons">navigate_next</i></li> +<li class="tab"> + <a class="active" href="{{ url_for('.tesseract_ocr_pipeline_model', tesseract_ocr_pipeline_model_id=tesseract_ocr_pipeline_model.hashid) }}" target="_self"> + Edit {{ tesseract_ocr_pipeline_model.title }} + </a> +</li> +{% elif request.path == url_for('.add_tesseract_ocr_pipeline_model, tesseract_ocr_pipeline_model=nn') %} +<li class="tab"><a href="{{ url_for('.contributions', tesseract_ocr_pipeline_model_id=nn) }}" target="_self">Contributions Overview</a></li> +<li class="tab disabled"><i class="material-icons">navigate_next</i></li> +<li class="tab"><a class="active" href="{{ url_for('.add_tesseract_ocr_pipeline_model') }}" target="_self">{{ title }}</a></li> +{% endif %} +{% endset %} diff --git a/app/templates/contributions/contribute_tesseract_ocr_models.html.j2 b/app/templates/contributions/contribute_tesseract_ocr_models.html.j2 new file mode 100644 index 0000000000000000000000000000000000000000..d7c8bd4126190f73a49cb4d37f7ca92c8b70b1bd --- /dev/null +++ b/app/templates/contributions/contribute_tesseract_ocr_models.html.j2 @@ -0,0 +1,124 @@ +{% extends "base.html.j2" %} +{% import "materialize/wtf.html.j2" as wtf %} +{# {% from "contributions/_breadcrumbs.html.j2" import breadcrumbs with context %} #} + +{% block main_attribs %} class="service-scheme" data-service="tesseract-ocr-pipeline"{% endblock main_attribs %} + +{% block page_content %} +<div class="container"> + <div class="row"> + <div class="col s12"> + <h1 id="title">{{ title }}</h1> + </div> + + <div class="col s12 m3 push-m9"> + <div class="center-align"> + <p class="hide-on-small-only"> </p> + <p class="hide-on-small-only"> </p> + <a class="btn-floating btn-large btn-scale-x2 waves-effect waves-light"> + <i class="nopaque-icons service-color darken service-icon" data-service="tesseract-ocr-pipeline"></i> + </a> + </div> + </div> + + <div class="col s12 m9 pull-m3"> + <div class="card service-color-border border-darken" data-service="tesseract-ocr-pipeline" style="border-top: 10px solid;"> + <div class="card-content"> + <div class="row"> + <div class="col s12"> + <div class="card-panel z-depth-0"> + <span class="card-title"><i class="left material-icons">layers</i>Tesseract OCR Models</span> + <p>You can add more Tesseract OCR models using the form below. They will automatically appear in the list of usable models.</p> + <p><a class="modal-trigger" href="#models-modal">Information about the already existing models.</a></p> + <p><a href="">Edit already uploaded models</a></p> + </div> + </div> + </div> + </div> + </div> + </div> + + <div class="col s12"> + <h2>Add a model</h2> + <div class="card"> + <form class="create-contribution-form" enctype="multipart/form-data" method="POST"> + <div class="card-content"> + {{ form.hidden_tag() }} + <div class="row"> + <div class="col s12 l5"> + {{ wtf.render_field(form.tesseract_model_file, accept='.traineddata', placeholder='Choose a .traineddata file') }} + </div> + <div class="col s12 l7"> + {{ wtf.render_field(form.title, material_icon='title') }} + </div> + <div class="col s12"> + {{ wtf.render_field(form.description, material_icon='description') }} + </div> + <div class="col s12 l6"> + {{ wtf.render_field(form.publisher, material_icon='account_balance') }} + </div> + <div class="col s12 l6"> + {{ wtf.render_field(form.publishing_year, material_icon='calendar_month') }} + </div> + <div class="col s12"> + {{ wtf.render_field(form.publisher_url, material_icon='link') }} + </div> + <div class="col s12"> + {{ wtf.render_field(form.publishing_url, material_icon='link') }} + </div> + <div class="col s12 l10"> + {{ wtf.render_field(form.version, material_icon='apps') }} + </div> + <div class="col s12 l6"> + {{ wtf.render_field(form.compatible_service_versions) }} + </div> + <div class="col s12 l6 right-align" style="padding-right:20px;"> + <p></p> + <br> + {{ wtf.render_field(form.shared) }} + </div> + </div> + </div> + <div class="card-action right-align"> + {{ wtf.render_field(form.submit, material_icon='send') }} + </div> + </form> + </div> + </div> + </div> +</div> +{% endblock page_content %} + +{% block modals %} +{{ super() }} +<div id="models-modal" class="modal"> + <div class="modal-content"> + <h4>Tesseract OCR Pipeline models</h4> + <table> + <thead> + <tr> + <th>Title</th> + <th>Description</th> + <th>Biblio</th> + </tr> + </thead> + <tbody> + {% for m in tesseract_ocr_pipeline_models %} + <tr id="tesseract-ocr-pipeline-model-{{ m.hashid }}"> + <td>{{ m.title }}</td> + {% if m.description == '' %} + <td>Description is not available.</td> + {% else %} + <td>{{ m.description }}</td> + {% endif %} + <td><a href="{{ m.publisher_url }}">{{ m.publisher }}</a> ({{ m.publishing_year }}), {{ m.title }} {{ m.version}}, <a href="{{ m.publishing_url }}">{{ m.publishing_url }}</a></td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + <div class="modal-footer"> + <a href="#!" class="modal-close waves-effect waves-light btn">Close</a> + </div> +</div> +{% endblock modals %} diff --git a/app/templates/contributions/contribution_overview.html.j2 b/app/templates/contributions/contribution_overview.html.j2 new file mode 100644 index 0000000000000000000000000000000000000000..6a1ebb1ed3e5d18046480aecb276a2a1dd8873cb --- /dev/null +++ b/app/templates/contributions/contribution_overview.html.j2 @@ -0,0 +1,75 @@ +{% extends "base.html.j2" %} +{% import "materialize/wtf.html.j2" as wtf %} +{% from "contributions/_breadcrumbs.html.j2" import breadcrumbs with context %} + +{% block page_content %} +<div class="container"> + <div class="row"> + <div class="col s12"> + <h1 id="title">{{ title }}</h1> + + {# Tesseract OCR Models #} + <div> + <h3>My Tesseract OCR Pipeline Models</h3> + <p>Here you can see and edit the models that you have created. You can also create new models.</p> + + <div class="row"> + <div class="col s12"> + <div class="card"> + <div class="card-content"> + <div id="tesseract-ocr-model-list" data-user-id="{{ userId }}"> + <table> + <thead> + <tr> + <th>Title</th> + <th>Description</th> + <th>Biblio</th> + <th></th> + </tr> + </thead> + <tbody> + {% if tesseractOCRUserModels|length > 0 %} + {% for m in tesseractOCRUserModels %} + <tr id="tesseract-ocr-pipeline-model-{{ m.hashid }}"> + <td>{{ m.title }}</td> + {% if m.description == '' %} + <td>Description is not available.</td> + {% else %} + <td>{{ m.description }}</td> + {% endif %} + <td><a href="{{ m.publisher_url }}">{{ m.publisher }}</a> ({{ m.publishing_year }}), {{ m.title }} {{ m.version}}, <a href="{{ m.publishing_url }}">{{ m.publishing_url }}</a></td> + <td class="right-align"> + <a class="delete-button btn-floating red waves-effect waves-light" data-model-id="{{ m.hashid }}"><i class="material-icons">delete</i></a> + <a class="edit-button btn-floating service-color darken waves-effect waves-light" data-model-id="{{ m.hashid }}"><i class="material-icons">edit</i></a> + </td> + </tr> + {% endfor %} + {% else %} + <tr> + <td colspan="4">No models available.</td> + </tr> + {% endif %} + </tbody> + </table> + </div> + </div> + <div class="card-action right-align"> + <a href="{{ url_for('contributions.add_tesseract_ocr_pipeline_model') }}" class="btn waves-effect waves-light"><i class="material-icons left">add</i>Add model file</a> + </div> + </div> + </div> + </div> + </div> + + </div> + </div> +</div> +{% endblock page_content %} + +{% block scripts %} +{{ super() }} +<script> +const tesseractOCRModelList = new TesseractOCRModelList(); +tesseractOCRModelList.init(); +</script> +{% endblock scripts %} diff --git a/app/templates/contributions/tesseract_ocr_pipeline_model.html.j2 b/app/templates/contributions/tesseract_ocr_pipeline_model.html.j2 new file mode 100644 index 0000000000000000000000000000000000000000..4db82349d9099e9dfc2110788df3d0573dc47d1e --- /dev/null +++ b/app/templates/contributions/tesseract_ocr_pipeline_model.html.j2 @@ -0,0 +1,56 @@ +{% extends "base.html.j2" %} +{% import "materialize/wtf.html.j2" as wtf %} +{% from "contributions/_breadcrumbs.html.j2" import breadcrumbs with context %} + +{% block main_attribs %} class="service-scheme" data-service="tesseract-ocr-pipeline"{% endblock main_attribs %} + +{% block page_content %} +<div class="container"> + <div class="row"> + <div class="col s12"> + <h1 id="title">{{ title }}</h1> + </div> + + <div class="col s12"> + <div class="card"> + <form class="create-contribution-form" enctype="multipart/form-data" method="POST"> + <div class="card-content"> + {{ form.hidden_tag() }} + <div class="row"> + <div class="col s12 l7"> + {{ wtf.render_field(form.title, material_icon='title') }} + </div> + <div class="col s12"> + {{ wtf.render_field(form.description, material_icon='description') }} + </div> + <div class="col s12 l6"> + {{ wtf.render_field(form.publisher, material_icon='account_balance') }} + </div> + <div class="col s12 l6"> + {{ wtf.render_field(form.publishing_year, material_icon='calendar_month') }} + </div> + <div class="col s12"> + {{ wtf.render_field(form.publisher_url, material_icon='link') }} + </div> + <div class="col s12"> + {{ wtf.render_field(form.publishing_url, material_icon='link') }} + </div> + <div class="col s12 l10"> + {{ wtf.render_field(form.version, material_icon='apps') }} + </div> + <div class="col s12 l6 right-align" style="padding-right:20px;"> + <p></p> + <br> + {{ wtf.render_field(form.shared) }} + </div> + </div> + </div> + <div class="card-action right-align"> + {{ wtf.render_field(form.submit, material_icon='send') }} + </div> + </form> + </div> + </div> + </div> +</div> +{% endblock page_content %}