From 4a08a4ef405e451db0a8251c05a193874b64cddb Mon Sep 17 00:00:00 2001
From: Bryan Newbold <bnewbold@robocracy.org>
Date: Mon, 1 Apr 2019 22:09:48 -0700
Subject: basic release editing

---
 python/fatcat_web/forms.py                      |  37 +++-
 python/fatcat_web/routes.py                     |  50 ++++-
 python/fatcat_web/templates/edit_macros.html    |  36 ++++
 python/fatcat_web/templates/release_create.html | 254 +-----------------------
 python/fatcat_web/templates/release_edit.html   | 231 +++++++++++++++++++++
 5 files changed, 345 insertions(+), 263 deletions(-)
 create mode 100644 python/fatcat_web/templates/edit_macros.html
 create mode 100644 python/fatcat_web/templates/release_edit.html

(limited to 'python')

diff --git a/python/fatcat_web/forms.py b/python/fatcat_web/forms.py
index baff62d7..632e36c6 100644
--- a/python/fatcat_web/forms.py
+++ b/python/fatcat_web/forms.py
@@ -6,7 +6,7 @@ but can't find one that is actually maintained.
 
 from flask_wtf import FlaskForm
 from wtforms import SelectField, DateField, StringField, IntegerField, \
-    FormField, FieldList, validators
+    HiddenField, FormField, FieldList, validators
 
 from fatcat_client import ContainerEntity, CreatorEntity, FileEntity, \
     ReleaseEntity, ReleaseContrib
@@ -51,6 +51,7 @@ class ReleaseContribForm(FlaskForm):
     #given_name
     #creator_id (?)
     #orcid (for match?)
+    prev_index = HiddenField('prev_revision index', default=None)
     raw_name = StringField('Display Name',
         [validators.DataRequired()])
     role = SelectField(
@@ -108,7 +109,14 @@ class ReleaseEntityForm(EntityEditForm):
         """
         ref = ReleaseEntityForm()
         for simple_attr in RELEASE_SIMPLE_ATTRS:
-            setattr(ref, simple_attr, getattr(re, simple_attr))
+            a = getattr(ref, simple_attr)
+            a.data = getattr(re, simple_attr)
+        for i, c in enumerate(re.contribs):
+            rcf = ReleaseContribForm()
+            rcf.prev_index = i
+            rcf.role = c.role
+            rcf.raw_name = c.raw_name
+            ref.contribs.append_entry(rcf)
         return ref
 
     def to_entity(self):
@@ -130,13 +138,26 @@ class ReleaseEntityForm(EntityEditForm):
             if a == '':
                 a = None
             setattr(re, simple_attr, a)
-        # TODO: don't update authors unless necessary!
-        re.contribs = []
+        # bunch of complexity here to preserve old contrib metadata (eg,
+        # affiliation and extra) not included in current forms
+        # TODO: this may be broken; either way needs tests
+        if re.contribs:
+            old_contribs = re.contribs.copy()
+            re.contribs = []
+        else:
+            old_contribs = []
+            re.contribs = []
         for c in self.contribs:
-            re.contribs.append(ReleaseContrib(
-                role=c.role.data or None,
-                raw_name=c.raw_name.data or None,
-            ))
+            if c.prev_index.data not in ('', None):
+                rc = old_contribs[int(c.prev_index.data)]
+                rc.role = c.role.data or None
+                rc.raw_name = c.raw_name.data or None
+            else:
+                rc = ReleaseContrib(
+                    role=c.role.data or None,
+                    raw_name=c.raw_name.data or None,
+                )
+            re.contribs.append(rc)
         if self.edit_description.data:
             re.edit_extra = dict(description=self.edit_description.data)
 
diff --git a/python/fatcat_web/routes.py b/python/fatcat_web/routes.py
index eb62d338..118f402b 100644
--- a/python/fatcat_web/routes.py
+++ b/python/fatcat_web/routes.py
@@ -325,7 +325,7 @@ def release_create():
             user_api = auth_api(session['api_token'])
             if form.editgroup_id.data:
                 # TODO: error handling
-                eg = api.get_editgroup(form.editgroup_id.data)
+                eg = user_api.get_editgroup(form.editgroup_id.data)
             else:
                 # if no editgroup, create one from description
                 eg = user_api.create_editgroup(
@@ -345,8 +345,9 @@ def release_create():
             print("didn't validate...")
     elif len(form.contribs) == 0:
         form.contribs.append_entry()
-    # TODO: check for editgroup in session
-    editgroup_id = session.get('active_editgroup_id', None)
+    if not form.is_submitted():
+        editgroup_id = session.get('active_editgroup_id', None)
+        form.editgroup_id.data = editgroup_id
     return render_template('release_create.html',
         form=form, editgroup_id=editgroup_id)
 
@@ -363,13 +364,50 @@ def release_history(ident):
         entity=entity,
         history=history)
 
-@app.route('/release/<ident>/edit', methods=['GET'])
-def release_edit_view(ident):
+# XXX: figure out CSRF stuff for local dev
+@login_required
+@app.csrf.exempt
+@app.route('/release/<ident>/edit', methods=['GET', 'POST'])
+def release_edit(ident):
+    # TODO: prev_rev interlock
+    # TODO: factor out editgroup active/creation stuff
     try:
         entity = api.get_release(ident)
     except ApiException as ae:
         abort(ae.status)
-    return render_template('entity_edit.html')
+    form = ReleaseEntityForm(csrf_enabled=False) # XXX:
+    if form.is_submitted():
+        if form.validate_on_submit():
+            # API on behalf of user
+            user_api = auth_api(session['api_token'])
+            if form.editgroup_id.data:
+                # TODO: error handling
+                eg = user_api.get_editgroup(form.editgroup_id.data)
+            else:
+                # if no editgroup, create one from description
+                eg = user_api.create_editgroup(
+                    Editgroup(description=form.editgroup_description.data or None))
+                # set this session editgroup_id
+                session['active_editgroup_id'] = eg.editgroup_id
+                print(eg.editgroup_id) # XXX: debug
+                flash('Started new editgroup <a href="/editgroup/{}">{}</a>' \
+                    .format(eg.editgroup_id, eg.editgroup_id))
+            # all the tricky logic is in the update method
+            form.update_entity(entity)
+            edit = user_api.update_release(entity.ident, entity,
+                editgroup_id=eg.editgroup_id)
+            # redirect to release revision
+            # TODO: release_rev_view
+            return redirect('/release/{}'.format(edit.ident))
+        elif form.errors:
+            print("user form errors (didn't validate): {}".format(form.errors))
+    else:
+        form = ReleaseEntityForm.from_entity(entity)
+    if not form.is_submitted():
+        editgroup_id = session.get('active_editgroup_id', None)
+        form.editgroup_id.data = editgroup_id
+    return render_template('release_edit.html',
+        form=form, editgroup_id=editgroup_id, entity=entity)
 
 @app.route('/release/<ident>', methods=['GET'])
 def release_view(ident):
diff --git a/python/fatcat_web/templates/edit_macros.html b/python/fatcat_web/templates/edit_macros.html
new file mode 100644
index 00000000..ad563066
--- /dev/null
+++ b/python/fatcat_web/templates/edit_macros.html
@@ -0,0 +1,36 @@
+
+{% macro form_field_errors(field) -%}
+  {% if field.errors %}
+    <div class="ui pointing red label">
+    {% for err in field.errors %}
+        {{ err }}
+    {% endfor %}
+    </div>
+  {% endif %}
+{%- endmacro %}
+
+{% macro form_field_basic(field, div_classes="") -%}
+<div class="field {{ div_classes }} {% if field.errors %}error{% endif %}">
+  {{ field.label }}
+  {{ field() }}
+  {{ form_field_errors(field) }}
+</div>
+{%- endmacro %}
+
+{% macro form_field_inline(field, div_classes="") -%}
+<div class="ui grid">
+  <div class="three wide column middle aligned right aligned" {# style="padding-right: 0.5rem;" #}>
+    <div class="field inline {{ div_classes }} {% if field.errors %}error{% endif %}">
+      {{ field.label }}
+    </div>
+  </div>
+  <div class="twelve wide column" {# style="padding-left: 0.5rem;" #}>
+    <div class="field {{ div_classes }} {% if field.errors %}error{% endif %}">
+      {{ field() }}
+      {{ form_field_errors(field) }}
+    </div>
+  </div>
+  <div class="one wide column">
+  </div>
+</div>
+{%- endmacro %}
diff --git a/python/fatcat_web/templates/release_create.html b/python/fatcat_web/templates/release_create.html
index 7ede5dfd..5ea3470d 100644
--- a/python/fatcat_web/templates/release_create.html
+++ b/python/fatcat_web/templates/release_create.html
@@ -1,260 +1,16 @@
-{% extends "base.html" %}
+{% extends "release_edit.html" %}
 
-{% macro form_field_errors(field) -%}
-  {% if field.errors %}
-    <div class="ui pointing red label">
-    {% for err in field.errors %}
-        {{ err }}
-    {% endfor %}
-    </div>
-  {% endif %}
-{%- endmacro %}
-
-{% macro form_field_basic(field, div_classes="") -%}
-<div class="field {{ div_classes }} {% if field.errors %}error{% endif %}">
-  {{ field.label }}
-  {{ field() }}
-  {{ form_field_errors(field) }}
-</div>
-{%- endmacro %}
-
-{% macro form_field_inline(field, div_classes="") -%}
-<div class="ui grid">
-  <div class="three wide column middle aligned right aligned" {# style="padding-right: 0.5rem;" #}>
-    <div class="field inline {{ div_classes }} {% if field.errors %}error{% endif %}">
-      {{ field.label }}
-    </div>
-  </div>
-  <div class="twelve wide column" {# style="padding-left: 0.5rem;" #}>
-    <div class="field {{ div_classes }} {% if field.errors %}error{% endif %}">
-      {{ field() }}
-      {{ form_field_errors(field) }}
-    </div>
-  </div>
-  <div class="one wide column">
-  </div>
-</div>
-{%- endmacro %}
-
-{% block body %}
+{% block edit_form_prefix %}
 <div class="ui segment">
 <h1 class="ui header">Create New Release Entity</h1>
 
+<form class="ui form" id="create_release_form" method="POST" action="/release/create">
+{% endblock %}
 
-<form class="ui form" id="add_work_form" method="POST" action="/release/create">
-  {{ form.hidden_tag() }}
-
-  <br>
-  <div class="ui accordion">
-    <div class="{% if not editgroup_id %}active{% endif %} title">
-      <h3><i class="dropdown icon"></i>Editgroup Meta</h3>
-    </div>
-    <div class="{% if not editgroup_id %}active{% endif %} content">
-      {% if editgroup_id %}
-        <p>You have an editgroup in progress, and this edit will be included by
-        default. You can override this below.
-      {% else %}
-        <p>No existing editgroup is in progress (or at least, not is selected).
-        An existing ID can be pasted in, or if you leave that blank but give a
-        description, a new editgroup will be created for this edit.
-      {% endif %}
-      {{ form_field_inline(form.editgroup_id) }}
-      {{ form_field_inline(form.editgroup_description) }}
-    </div>
-  </div>
-
-  <br>
-  <h3 class="ui dividing header">The Basics</h3>
-  <div class="ui grid">
-    <div class="three wide column" style="padding-bottom: 0px;"></div>
-    <div class="twelve wide column" style="padding-bottom: 0px;">
-      <div class="ui equal width fields">
-        {{ form_field_basic(form.release_type, "required") }}
-        {{ form_field_basic(form.release_status) }}
-      </div>
-    </div>
-    <div class="one wide column" style="padding-bottom: 0px;"></div>
-  </div>
-
-  {{ form_field_inline(form.title, "required") }}
-  {{ form_field_inline(form.original_title) }}
-  {{ form_field_inline(form.work_id) }}
-  {{ form_field_inline(form.release_date) }}
-  <div class="ui grid">
-    <div class="three wide column" style="padding-bottom: 0px;"></div>
-    <div class="twelve wide column" style="padding-bottom: 0px;">
-      <div class="ui equal width fields">
-        {{ form_field_basic(form.language) }}
-        {{ form_field_basic(form.license_slug) }}
-      </div>
-    </div>
-    <div class="one wide column" style="padding-bottom: 0px;"></div>
-  </div>
-
-  <br>
-  <h3 class="ui dividing header">Contributors</h3>
-  <div class="list-group" id="contrib_list" name="contrib_list">
-  {% for cform in form.contribs %}
-    <div class="list-group-item ui grid" style="padding-right: 1em;">
-      <div class="one wide column middle aligned center aligned sortable-handle" style="padding-bottom: 0px; padding-right: 0px; padding-left: 0px;">
-        <i class="arrows alternate vertical icon"></i>
-      </div>
-      <div class="three wide column" style="padding-bottom: 0px; padding-left: 0px;">
-        <div class="field {% if cform.role.errors %}error{% endif %}">
-          {{ cform.role }}
-        </div>
-      </div>
-      <div class="eleven wide column" style="padding-bottom: 0px;">
-        <div class="field {% if cform.raw_name.errors %}error{% endif %}">
-          {{ cform.raw_name}}
-        </div>
-      </div>
-      <div class="one wide column right aligned" style="padding-bottom: 0px; padding-left: 0rem;">
-        <button type="button" class="ui icon red button delete-contrib-button"><i class="trash icon"></i></button>
-      </div>
-    </div>
-  {% endfor %}
-  </div>
-  <br>
-  <button type="button" id="add-contrib-button" class="ui right floated icon green button" style="margin-right: 0.3rem;">
-    <i class="plus icon"></i>
-  </button>
-  <br>
-
-  <br>
-  <h3 class="ui dividing header">Identifers</h3>
-  <br>
-  {{ form_field_inline(form.doi) }}
-  {{ form_field_inline(form.wikidata_qid) }}
-  {{ form_field_inline(form.isbn13) }}
-  <div class="ui grid">
-    <div class="three wide column" style="padding-bottom: 0px;"></div>
-    <div class="twelve wide column" style="padding-bottom: 0px;">
-      <div class="ui equal width fields">
-        {{ form_field_basic(form.pmid) }}
-        {{ form_field_basic(form.pmcid) }}
-      </div>
-    </div>
-    <div class="one wide column" style="padding-bottom: 0px;"></div>
-  </div>
-
-  <br>
-  <h3 class="ui dividing header">Container</h3>
-  <br>
-  {{ form_field_inline(form.container_id) }}
-  {{ form_field_inline(form.publisher) }}
-  <br>
-  <div class="ui grid">
-    <div class="three wide column" style="padding-bottom: 0px;"></div>
-    <div class="twelve wide column" style="padding-bottom: 0px;">
-      <div class="ui equal width fields">
-        {{ form_field_basic(form.pages) }}
-        {{ form_field_basic(form.volume) }}
-        {{ form_field_basic(form.issue) }}
-      </div>
-    </div>
-    <div class="one wide column" style="padding-bottom: 0px;"></div>
-  </div>
-
-  <br><br>
-  <h3 class="ui dividing header">Submit</h3>
-  {{ form_field_basic(form.edit_description) }}
-  This description will be attached to this specific action, not to the
-  editgroup as a whole.
+{% block edit_form_suffix %}
   <br><br>
   <input class="ui primary submit button" type="submit" value="Create Release!">
-
 </form>
-
 </div>
 {% endblock %}
 
-{% block postscript %}
-<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
-<script>
-<!-- Form code -->
-$(document).ready(function() {
-
-  // these javascript dropdowns hide the original <input>, which breaks browser
-  // form focusing (eg, for required fields) :(
-  //$('#release_type').dropdown();
-  //$('#release_status').dropdown();
-  $('.ui.accordion').accordion();
-
-  var fixup_contrib_numbering = function(group_item) {
-    items = Array.from(group_item.querySelectorAll(".list-group-item"))
-    for (var i = 0; i < items.length; i++) {
-      var item_el = items[i];
-      input_el = item_el.querySelectorAll("input")[0];
-      select_el = item_el.querySelectorAll("select")[0];
-      //console.log(input_el.id);
-      //console.log(select_el.id);
-      input_el.id = "contribs-" + i + "-raw_name";
-      input_el.name = input_el.id;
-      select_el.id = "contribs-" + i + "-role";
-      select_el.name = select_el.id;
-      //console.log(input_el.id);
-      //console.log(select_el.id);
-    };
-    console.log("re-named contrib rows up to i=" + i);
-  };
-
-  var contrib_list = document.getElementById('contrib_list');
-  var contrib_sortable = Sortable.create(contrib_list, {
-    handle: '.sortable-handle',
-    animation: 150,
-    onSort: function(evt) {
-      fixup_contrib_numbering(contrib_list);
-    },
-  });
-  fixup_contrib_numbering(contrib_list);
-
-  var contrib_delete_handler = function(ev) {
-    row = ev.target.parentNode.parentNode;
-    // I don't understand why this hack is needed; maybe because of the sortable stuff?
-    if(!row.classList.contains("list-group-item")) {
-        row = row.parentNode;
-    }
-    // console.log(row);
-    console.assert(row.classList.contains("list-group-item"));
-    row.parentNode.removeChild(row);
-    fixup_contrib_numbering(contrib_list);
-  };
-
-  var attach_contrib_delete_handler = function(topthing) {
-    Array.from(topthing.querySelectorAll(".delete-contrib-button")).forEach((el) => {
-      el.addEventListener("click", contrib_delete_handler);
-    });
-  };
-  attach_contrib_delete_handler(document);
-
-  // XXX: really need some way to not duplicate this code from above...
-  var contrib_template = `
-  <div class="list-group-item ui grid" style="padding-right: 1em;">
-      <div class="one wide column middle aligned center aligned sortable-handle" style="padding-bottom: 0px; padding-right: 0px; padding-left: 0px;">
-        <i class="arrows alternate vertical icon"></i>
-      </div>
-      <div class="three wide column" style="padding-bottom: 0px; padding-left: 0px;">
-        <select id="contribs-X-role" name="contribs-X-role"><option value="author">Author</option><option value="editor">Editor</option><option value="translator">Translator</option></select>
-      </div>
-      <div class="eleven wide column" style="padding-bottom: 0px;">
-        <input id="contribs-X-raw_name" name="contribs-X-raw_name" type="text" value="">
-      </div>
-      <div class="one wide column right aligned" style="padding-bottom: 0px; padding-left: 0rem;">
-        <button type="button" class="ui icon red button delete-contrib-button"><i class="trash icon"></i></button>
-      </div>
-    </div>
-  `;
-
-  var add_contrib_button = document.getElementById("add-contrib-button");
-  add_contrib_button.addEventListener("click", function(){
-    contrib_list.insertAdjacentHTML('beforeend', contrib_template);
-    attach_contrib_delete_handler(contrib_list.lastElementChild);
-    fixup_contrib_numbering(contrib_list);
-  });
-
-  console.log("Page loaded");
-
-});
-</script>
-{% endblock %}
diff --git a/python/fatcat_web/templates/release_edit.html b/python/fatcat_web/templates/release_edit.html
new file mode 100644
index 00000000..867ae665
--- /dev/null
+++ b/python/fatcat_web/templates/release_edit.html
@@ -0,0 +1,231 @@
+{% import "edit_macros.html" as edit_macros %}
+{% extends "base.html" %}
+
+{% block body %}
+{% block edit_form_prefix %}
+<div class="ui segment">
+<h1 class="ui header">Edit Release Entity</h1>
+
+<form class="ui form" id="edit_release_form" method="POST" action="/release/{{ entity.ident }}/edit">
+{% endblock %}
+  {{ form.hidden_tag() }}
+
+  <br>
+  <div class="ui accordion">
+    <div class="{% if not editgroup_id %}active{% endif %} title">
+      <h3><i class="dropdown icon"></i>Editgroup Meta</h3>
+    </div>
+    <div class="{% if not editgroup_id %}active{% endif %} content">
+      {% if editgroup_id %}
+        <p>You have an editgroup in progress, and this edit will be included by
+        default. You can override this below.
+      {% else %}
+        <p>No existing editgroup is in progress (or at least, not is selected).
+        An existing ID can be pasted in, or if you leave that blank but give a
+        description, a new editgroup will be created for this edit.
+      {% endif %}
+      {{ edit_macros.form_field_inline(form.editgroup_id) }}
+      {{ edit_macros.form_field_inline(form.editgroup_description) }}
+    </div>
+  </div>
+
+  <br>
+  <h3 class="ui dividing header">The Basics</h3>
+  <div class="ui grid">
+    <div class="three wide column" style="padding-bottom: 0px;"></div>
+    <div class="twelve wide column" style="padding-bottom: 0px;">
+      <div class="ui equal width fields">
+        {{ edit_macros.form_field_basic(form.release_type, "required") }}
+        {{ edit_macros.form_field_basic(form.release_status) }}
+      </div>
+    </div>
+    <div class="one wide column" style="padding-bottom: 0px;"></div>
+  </div>
+
+  {{ edit_macros.form_field_inline(form.title, "required") }}
+  {{ edit_macros.form_field_inline(form.original_title) }}
+  {{ edit_macros.form_field_inline(form.work_id) }}
+  {{ edit_macros.form_field_inline(form.release_date) }}
+  <div class="ui grid">
+    <div class="three wide column" style="padding-bottom: 0px;"></div>
+    <div class="twelve wide column" style="padding-bottom: 0px;">
+      <div class="ui equal width fields">
+        {{ edit_macros.form_field_basic(form.language) }}
+        {{ edit_macros.form_field_basic(form.license_slug) }}
+      </div>
+    </div>
+    <div class="one wide column" style="padding-bottom: 0px;"></div>
+  </div>
+
+  <br>
+  <h3 class="ui dividing header">Contributors</h3>
+  <div class="list-group" id="contrib_list" name="contrib_list">
+  {% for cform in form.contribs %}
+    <div class="list-group-item ui grid" style="padding-right: 1em;">
+      {{ cform.hidden_tag() }}
+      <div class="one wide column middle aligned center aligned sortable-handle" style="padding-bottom: 0px; padding-right: 0px; padding-left: 0px;">
+        <i class="arrows alternate vertical icon"></i>
+      </div>
+      <div class="three wide column" style="padding-bottom: 0px; padding-left: 0px;">
+        <div class="field {% if cform.role.errors %}error{% endif %}">
+          {{ cform.role() }}
+        </div>
+      </div>
+      <div class="eleven wide column" style="padding-bottom: 0px;">
+        <div class="field {% if cform.raw_name.errors %}error{% endif %}">
+          {{ cform.raw_name() }}
+        </div>
+      </div>
+      <div class="one wide column right aligned" style="padding-bottom: 0px; padding-left: 0rem;">
+        <button type="button" class="ui icon red button delete-contrib-button"><i class="trash icon"></i></button>
+      </div>
+    </div>
+  {% endfor %}
+  </div>
+  <br>
+  <button type="button" id="add-contrib-button" class="ui right floated icon green button" style="margin-right: 0.3rem;">
+    <i class="plus icon"></i>
+  </button>
+  <br>
+
+  <br>
+  <h3 class="ui dividing header">Identifers</h3>
+  <br>
+  {{ edit_macros.form_field_inline(form.doi) }}
+  {{ edit_macros.form_field_inline(form.wikidata_qid) }}
+  {{ edit_macros.form_field_inline(form.isbn13) }}
+  <div class="ui grid">
+    <div class="three wide column" style="padding-bottom: 0px;"></div>
+    <div class="twelve wide column" style="padding-bottom: 0px;">
+      <div class="ui equal width fields">
+        {{ edit_macros.form_field_basic(form.pmid) }}
+        {{ edit_macros.form_field_basic(form.pmcid) }}
+      </div>
+    </div>
+    <div class="one wide column" style="padding-bottom: 0px;"></div>
+  </div>
+
+  <br>
+  <h3 class="ui dividing header">Container</h3>
+  <br>
+  {{ edit_macros.form_field_inline(form.container_id) }}
+  {{ edit_macros.form_field_inline(form.publisher) }}
+  <br>
+  <div class="ui grid">
+    <div class="three wide column" style="padding-bottom: 0px;"></div>
+    <div class="twelve wide column" style="padding-bottom: 0px;">
+      <div class="ui equal width fields">
+        {{ edit_macros.form_field_basic(form.pages) }}
+        {{ edit_macros.form_field_basic(form.volume) }}
+        {{ edit_macros.form_field_basic(form.issue) }}
+      </div>
+    </div>
+    <div class="one wide column" style="padding-bottom: 0px;"></div>
+  </div>
+
+  <br><br>
+  <h3 class="ui dividing header">Submit</h3>
+  {{ edit_macros.form_field_basic(form.edit_description) }}
+  This description will be attached to this specific action, not to the
+  editgroup as a whole.
+{% block edit_form_suffix %}
+  <br><br>
+  <input class="ui primary submit button" type="submit" value="Update Release!">
+</form>
+</div>
+{% endblock %}
+{% endblock %}
+
+{% block postscript %}
+<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
+<script>
+<!-- Form code -->
+$(document).ready(function() {
+
+  // these javascript dropdowns hide the original <input>, which breaks browser
+  // form focusing (eg, for required fields) :(
+  //$('#release_type').dropdown();
+  //$('#release_status').dropdown();
+  $('.ui.accordion').accordion();
+
+  var fixup_contrib_numbering = function(group_item) {
+    items = Array.from(group_item.querySelectorAll(".list-group-item"))
+    for (var i = 0; i < items.length; i++) {
+      var item_el = items[i];
+      prev_el = item_el.querySelectorAll("input")[0];
+      input_el = item_el.querySelectorAll("input")[1];
+      select_el = item_el.querySelectorAll("select")[0];
+      //console.log(input_el.id);
+      //console.log(select_el.id);
+      input_el.id = "contribs-" + i + "-raw_name";
+      input_el.name = input_el.id;
+      prev_el.id = "contribs-" + i + "-prev_index";
+      prev_el.name = prev_el.id;
+      select_el.id = "contribs-" + i + "-role";
+      select_el.name = select_el.id;
+      //console.log(input_el.id);
+      //console.log(select_el.id);
+    };
+    console.log("re-named contrib rows up to i=" + i);
+  };
+
+  var contrib_list = document.getElementById('contrib_list');
+  var contrib_sortable = Sortable.create(contrib_list, {
+    handle: '.sortable-handle',
+    animation: 150,
+    onSort: function(evt) {
+      fixup_contrib_numbering(contrib_list);
+    },
+  });
+  fixup_contrib_numbering(contrib_list);
+
+  var contrib_delete_handler = function(ev) {
+    row = ev.target.parentNode.parentNode;
+    // I don't understand why this hack is needed; maybe because of the sortable stuff?
+    if(!row.classList.contains("list-group-item")) {
+        row = row.parentNode;
+    }
+    // console.log(row);
+    console.assert(row.classList.contains("list-group-item"));
+    row.parentNode.removeChild(row);
+    fixup_contrib_numbering(contrib_list);
+  };
+
+  var attach_contrib_delete_handler = function(topthing) {
+    Array.from(topthing.querySelectorAll(".delete-contrib-button")).forEach((el) => {
+      el.addEventListener("click", contrib_delete_handler);
+    });
+  };
+  attach_contrib_delete_handler(document);
+
+  // XXX: really need some way to not duplicate this code from above...
+  var contrib_template = `
+  <div class="list-group-item ui grid" style="padding-right: 1em;">
+      <input id="contribs-X-prev_index" name="contribs-X-prev_index" type="hidden" value="">
+      <div class="one wide column middle aligned center aligned sortable-handle" style="padding-bottom: 0px; padding-right: 0px; padding-left: 0px;">
+        <i class="arrows alternate vertical icon"></i>
+      </div>
+      <div class="three wide column" style="padding-bottom: 0px; padding-left: 0px;">
+        <select id="contribs-X-role" name="contribs-X-role"><option value="author">Author</option><option value="editor">Editor</option><option value="translator">Translator</option></select>
+      </div>
+      <div class="eleven wide column" style="padding-bottom: 0px;">
+        <input id="contribs-X-raw_name" name="contribs-X-raw_name" type="text" value="">
+      </div>
+      <div class="one wide column right aligned" style="padding-bottom: 0px; padding-left: 0rem;">
+        <button type="button" class="ui icon red button delete-contrib-button"><i class="trash icon"></i></button>
+      </div>
+    </div>
+  `;
+
+  var add_contrib_button = document.getElementById("add-contrib-button");
+  add_contrib_button.addEventListener("click", function(){
+    contrib_list.insertAdjacentHTML('beforeend', contrib_template);
+    attach_contrib_delete_handler(contrib_list.lastElementChild);
+    fixup_contrib_numbering(contrib_list);
+  });
+
+  console.log("Page loaded");
+
+});
+</script>
+{% endblock %}
-- 
cgit v1.2.3