diff --git a/.gitignore b/.gitignore
index 76c4e06b7c77cae22e913df43258e4ce16e6568e..14a22fe146d245a406ef8e77869320e5f66c844c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,3 +39,4 @@ __pycache__
 
 # Virtual environment
 venv
+.idea
diff --git a/app/contributions/forms.py b/app/contributions/forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..d123e6d13990797119baf6f4a829cfae38576891
--- /dev/null
+++ b/app/contributions/forms.py
@@ -0,0 +1,58 @@
+from flask_wtf import FlaskForm
+from flask_wtf.file import FileField, FileRequired
+from wtforms import (
+    BooleanField,
+    StringField,
+    SubmitField,
+    SelectMultipleField,
+    IntegerField
+)
+from wtforms.validators import InputRequired, Length
+from app.services import SERVICES
+
+
+class ContributionForm(FlaskForm):
+    title = StringField(
+        'Title',
+        validators=[InputRequired(), Length(max=64)]
+    )
+    description = StringField(
+        'Description',
+        validators=[InputRequired(), Length(max=255)]
+    )
+    version = StringField(
+        'Version',
+        validators=[InputRequired(), Length(max=16)]
+    )
+    compatible_service_versions = SelectMultipleField(
+        'Compatible service versions'
+    )
+    publisher = StringField(
+        'Publisher',
+        validators=[InputRequired(), Length(max=128)]
+    )
+    publisher_url = StringField(
+        'Publisher URL',
+        validators=[InputRequired(), Length(max=512)]
+    )
+    publishing_url = StringField(
+        'Publishing URL',
+        validators=[InputRequired(), Length(max=512)]
+    )
+    publishing_year = IntegerField(
+        'Publishing year',
+        validators=[InputRequired()]
+    )
+    shared = BooleanField('Shared', validators=[InputRequired()])
+    model_file = FileField('File',validators=[FileRequired()])
+    submit = SubmitField()
+
+
+    def __init__(self, *args, **kwargs):
+        service_manifest = SERVICES['tesseract-ocr-pipeline']
+        super().__init__(*args, **kwargs)
+        self.compatible_service_versions.choices = [('', 'Choose your option')]
+        self.compatible_service_versions.choices += [
+            (x, x) for x in service_manifest['versions'].keys()
+        ]
+        self.compatible_service_versions.default = ''
diff --git a/app/contributions/routes.py b/app/contributions/routes.py
index 80c6a82ded1202f3b9dd14d0173065f47905c9a4..d82bf511f1d255e9c0fced4cb215820eec86e012 100644
--- a/app/contributions/routes.py
+++ b/app/contributions/routes.py
@@ -1,7 +1,10 @@
+from flask import abort, flash, Markup, render_template, url_for
 from flask_login import login_required
+from app import db
 from app.decorators import permission_required
-from app.models import Permission
+from app.models import TesseractOCRModel, Permission
 from . import bp
+from .forms import ContributionForm
 
 
 @bp.before_request
@@ -14,3 +17,38 @@ def before_request():
 @bp.route('')
 def contributions():
     pass
+
+
+@bp.route('/tesseract-ocr-pipeline-models', methods=['GET', 'POST'])
+def tesseract_ocr_pipeline_models():
+    form = ContributionForm(
+        prefix='contribute-tesseract-ocr-pipeline-model-form'
+    )
+    if form.is_submitted():
+        if not form.validate():
+            response = {'errors': form.errors}
+            return response, 400
+        try:
+            tesseract_ocr_model = TesseractOCRModel.create(
+                form.file.data,
+                compatible_service_versions=form.compatible_service_versions.data,
+                description=form.description.data,
+                publisher=form.publisher.data,
+                publisher_url=form.publisher_url.data,
+                publishing_url=form.publishing_url.data,
+                publishing_year=form.publishing_year.data,
+                shared=form.shared.data,
+                title=form.title.data,
+                version=form.version.data
+            )
+        except OSError:
+            abort(500)
+        db.session.commit()
+        message = Markup(f'Model "{tesseract_ocr_model.title}" created')
+        flash(message)
+        return {}, 201, {'Location': url_for('contributions.contributions')}
+    return render_template(
+        'contributions/contribute.html.j2',
+        form=form,
+        title='Contribution'
+    )
diff --git a/app/models.py b/app/models.py
index cc5d60ceaf462537ae1b7a9e98e4f79f15f5326c..c0cedf2149ed65fb8f7461f41982c8d34c9d262d 100644
--- a/app/models.py
+++ b/app/models.py
@@ -620,6 +620,55 @@ class TesseractOCRPipelineModel(FileMixin, HashidMixin, db.Model):
             _json['user'] = self.user.to_json(backrefs=True)
         return _json
 
+    @staticmethod
+    def create(model_file, **kwargs):  # ??? Ist das so richtig? übergeben wir das form? Im anderen Fall ist das nicht so .. aber was soll hier überhaupt rein?
+        filename = kwargs.get('filename', model_file.filename)
+        mimetype = kwargs.get('mimetype', model_file.mimetype)
+        tesseract_ocr_model = TesseractOCRModel(
+            filename=secure_filename(filename),
+            mimetype=mimetype,
+            **kwargs
+        )
+
+        db.session.add(tesseract_ocr_model)
+        db.session.flush(objects=[tesseract_ocr_model])
+        db.session.refresh(tesseract_ocr_model)
+        try:
+            # ??? Woher soll file kommen? Wir haben der methode das ganze form übergeben.
+            # filename = form.file.data.filename
+            model_file.save(tesseract_ocr_model.path)
+        except OSError as e:
+            current_app.logger.error(e)
+            db.session.rollback()
+            raise e
+        return tesseract_ocr_model
+
+
+        # Kann man das so committen?
+        # db.session.add(model)
+        # db.session.flush(objects=[model])
+        # db.session.refresh(model)
+        # model.filename = f'{model.id}.traineddata'
+        # r = requests.get(m['url'], stream=True)
+        # pbar = tqdm(
+        #     desc=f'{model.title} ({model.filename})',
+        #     unit="B",
+        #     unit_scale=True,
+        #     unit_divisor=1024,
+        #     total=int(r.headers['Content-Length'])
+        # )
+        # pbar.clear()
+        # with open(model.path, 'wb') as f:
+        #     for chunk in r.iter_content(chunk_size=1024):
+        #         if chunk:  # filter out keep-alive new chunks
+        #             pbar.update(len(chunk))
+        #             f.write(chunk)
+        #     pbar.close()
+        #
+        # db.session.commit()
+
+
+
 
 class SpaCyNLPPipelineModel(FileMixin, HashidMixin, db.Model):
     __tablename__ = 'spacy_nlp_pipeline_models'
diff --git a/app/templates/contributions/contribute.html.j2 b/app/templates/contributions/contribute.html.j2
new file mode 100644
index 0000000000000000000000000000000000000000..6789e1f85a7c8215c2b4d53ee21f0f5b33d059c2
--- /dev/null
+++ b/app/templates/contributions/contribute.html.j2
@@ -0,0 +1,32 @@
+{% extends "base.html.j2" %}
+{% import "materialize/wtf.html.j2" as wtf %}
+
+
+{% block page_content %}
+<div class="container">
+  <div class="row">
+    <div class="col s12 m8 offset-m2">
+      <h1 id="title">{{ title }}</h1>
+      <p>
+        In order to add a new model, please fill in the form below.
+      </p>
+
+      <form method="POST">
+        <div class="card-panel">
+          {{ form.hidden_tag() }}
+          {{ wtf.render_field(form.title) }}
+          {{ wtf.render_field(form.description) }}
+          {{ wtf.render_field(form.publisher) }}
+          {{ wtf.render_field(form.publisher_url) }}
+          {{ wtf.render_field(form.publishing_url) }}
+          {{ wtf.render_field(form.publishing_year) }}
+          {{ wtf.render_field(form.shared) }}
+          {{ wtf.render_field(form.version) }}
+          {{ wtf.render_field(form.compatible_service_versions) }}
+          {{ wtf.render_field(form.submit, class_='width-100', material_icon='send') }}
+
+        </div>
+      </form>
+  </div>
+</div>
+{% endblock page_content %}
\ No newline at end of file