aboutsummaryrefslogtreecommitdiffstats
path: root/python/fatcat_web
diff options
context:
space:
mode:
Diffstat (limited to 'python/fatcat_web')
-rw-r--r--python/fatcat_web/editing_routes.py571
-rw-r--r--python/fatcat_web/entity_helpers.py27
-rw-r--r--python/fatcat_web/forms.py85
-rw-r--r--python/fatcat_web/routes.py2
-rw-r--r--python/fatcat_web/templates/base.html4
-rw-r--r--python/fatcat_web/templates/container_create.html4
-rw-r--r--python/fatcat_web/templates/container_edit.html8
-rw-r--r--python/fatcat_web/templates/deleted_entity.html2
-rw-r--r--python/fatcat_web/templates/edit_macros.html19
-rw-r--r--python/fatcat_web/templates/editgroup_view.html2
-rw-r--r--python/fatcat_web/templates/entity_create_toml.html20
-rw-r--r--python/fatcat_web/templates/entity_delete.html49
-rw-r--r--python/fatcat_web/templates/entity_edit.html8
-rw-r--r--python/fatcat_web/templates/entity_edit_toml.html55
-rw-r--r--python/fatcat_web/templates/entity_macros.html12
-rw-r--r--python/fatcat_web/templates/file_create.html4
-rw-r--r--python/fatcat_web/templates/file_edit.html8
-rw-r--r--python/fatcat_web/templates/home.html12
-rw-r--r--python/fatcat_web/templates/release_create.html4
-rw-r--r--python/fatcat_web/templates/release_edit.html35
20 files changed, 825 insertions, 106 deletions
diff --git a/python/fatcat_web/editing_routes.py b/python/fatcat_web/editing_routes.py
index ffce35f3..8e3b03b0 100644
--- a/python/fatcat_web/editing_routes.py
+++ b/python/fatcat_web/editing_routes.py
@@ -1,8 +1,10 @@
+from typing import Optional
+
from flask import render_template, abort, redirect, session, flash
from flask_login import login_required
-from fatcat_openapi_client import Editgroup
+from fatcat_openapi_client import *
from fatcat_openapi_client.rest import ApiException
from fatcat_tools.transforms import *
from fatcat_web import app, api, auth_api
@@ -13,6 +15,104 @@ from fatcat_web.entity_helpers import *
### Helper Methods ##########################################################
+def generic_entity_create_from_toml(user_api, entity_type: str, editgroup_id: str, toml_str: str) -> EntityEdit:
+ if entity_type == 'container':
+ entity = entity_from_toml(toml_str, ContainerEntity)
+ edit = user_api.create_container(editgroup_id, entity)
+ elif entity_type == 'creator':
+ entity = entity_from_toml(toml_str, CreatorEntity)
+ edit = user_api.create_creator(editgroup_id, entity)
+ elif entity_type == 'file':
+ entity = entity_from_toml(toml_str, FileEntity)
+ edit = user_api.create_file(editgroup_id, entity)
+ elif entity_type == 'fileset':
+ entity = entity_from_toml(toml_str, FilesetEntity)
+ edit = user_api.create_fileset(editgroup_id, entity)
+ elif entity_type == 'webcapture':
+ entity = entity_from_toml(toml_str, WebcaptureEntity)
+ edit = user_api.create_webcapture(editgroup_id, entity)
+ elif entity_type == 'release':
+ entity = entity_from_toml(toml_str, ReleaseEntity)
+ edit = user_api.create_release(editgroup_id, entity)
+ elif entity_type == 'work':
+ entity = entity_from_toml(toml_str, WorkEntity)
+ edit = user_api.create_work(editgroup_id, entity)
+ else:
+ raise NotImplementedError
+ return edit
+
+def generic_entity_delete_edit(user_api, entity_type: str, editgroup_id: str, edit_id: str) -> None:
+ try:
+ if entity_type == 'container':
+ user_api.delete_container_edit(editgroup_id, edit_id)
+ elif entity_type == 'creator':
+ user_api.delete_creator_edit(editgroup_id, edit_id)
+ elif entity_type == 'file':
+ user_api.delete_file_edit(editgroup_id, edit_id)
+ elif entity_type == 'fileset':
+ user_api.delete_fileset_edit(editgroup_id, edit_id)
+ elif entity_type == 'webcapture':
+ user_api.delete_webcapture_edit(editgroup_id, edit_id)
+ elif entity_type == 'release':
+ user_api.delete_release_edit(editgroup_id, edit_id)
+ elif entity_type == 'work':
+ user_api.delete_work_edit(editgroup_id, edit_id)
+ else:
+ raise NotImplementedError
+ except ApiException as ae:
+ if ae.status == 404:
+ pass
+ else:
+ raise ae
+
+def generic_entity_delete_entity(user_api, entity_type: str, editgroup_id: str, entity_ident: str) -> None:
+ try:
+ if entity_type == 'container':
+ edit = user_api.delete_container(editgroup_id, entity_ident)
+ elif entity_type == 'creator':
+ edit = user_api.delete_creator(editgroup_id, entity_ident)
+ elif entity_type == 'file':
+ edit = user_api.delete_file(editgroup_id, entity_ident)
+ elif entity_type == 'fileset':
+ edit = user_api.delete_fileset(editgroup_id, entity_ident)
+ elif entity_type == 'webcapture':
+ edit = user_api.delete_webcapture(editgroup_id, entity_ident)
+ elif entity_type == 'release':
+ edit = user_api.delete_release(editgroup_id, entity_ident)
+ elif entity_type == 'work':
+ edit = user_api.delete_work(editgroup_id, entity_ident)
+ else:
+ raise NotImplementedError
+ except ApiException as ae:
+ raise ae
+ return edit
+
+def generic_entity_update_from_toml(user_api, entity_type: str, editgroup_id: str, existing_ident, toml_str: str) -> EntityEdit:
+ if entity_type == 'container':
+ entity = entity_from_toml(toml_str, ContainerEntity)
+ edit = user_api.update_container(editgroup_id, existing_ident, entity)
+ elif entity_type == 'creator':
+ entity = entity_from_toml(toml_str, CreatorEntity)
+ edit = user_api.update_creator(editgroup_id, existing_ident, entity)
+ elif entity_type == 'file':
+ entity = entity_from_toml(toml_str, FileEntity)
+ edit = user_api.update_file(editgroup_id, existing_ident, entity)
+ elif entity_type == 'fileset':
+ entity = entity_from_toml(toml_str, FilesetEntity)
+ edit = user_api.update_fileset(editgroup_id, existing_ident, entity)
+ elif entity_type == 'webcapture':
+ entity = entity_from_toml(toml_str, WebcaptureEntity)
+ edit = user_api.update_webcapture(editgroup_id, existing_ident, entity)
+ elif entity_type == 'release':
+ entity = entity_from_toml(toml_str, ReleaseEntity)
+ edit = user_api.update_release(editgroup_id, existing_ident, entity)
+ elif entity_type == 'work':
+ entity = entity_from_toml(toml_str, WorkEntity)
+ edit = user_api.update_work(editgroup_id, existing_ident, entity)
+ else:
+ raise NotImplementedError
+ return edit
+
def form_editgroup_get_or_create(api, edit_form):
"""
This function expects a submitted, validated edit form
@@ -75,7 +175,7 @@ def generic_entity_edit(editgroup_id, entity_type, existing_ident, edit_template
try:
editgroup = api.get_editgroup(editgroup_id)
except ApiException as ae:
- abort(ae.status)
+ raise ae
# check that editgroup is edit-able
if editgroup.changelog_index != None:
@@ -138,14 +238,7 @@ def generic_entity_edit(editgroup_id, entity_type, existing_ident, edit_template
# a "update pointer" edit
existing.revision = None
try:
- if entity_type == 'container':
- user_api.delete_container_edit(editgroup.editgroup_id, existing_edit.edit_id)
- elif entity_type == 'file':
- user_api.delete_file_edit(editgroup.editgroup_id, existing_edit.edit_id)
- elif entity_type == 'release':
- user_api.delete_release_edit(editgroup.editgroup_id, existing_edit.edit_id)
- else:
- raise NotImplementedError
+ generic_entity_delete_edit(user_api, entity_type, editgroup.editgroup_id, existing_edit.edit_id)
except ApiException as ae:
if ae.status == 404:
pass
@@ -194,7 +287,13 @@ def generic_entity_edit(editgroup_id, entity_type, existing_ident, edit_template
existing_ident=existing_ident, editgroup=editgroup,
potential_editgroups=potential_editgroups), status
-def generic_edit_delete(editgroup_id, entity_type, edit_id):
+def generic_entity_toml_edit(editgroup_id, entity_type, existing_ident, edit_template):
+ """
+ Similar to generic_entity_edit(), but for TOML editing mode.
+
+ Handles both creation and update/edit paths.
+ """
+
# fetch editgroup (if set) or 404
editgroup = None
if editgroup_id:
@@ -205,23 +304,185 @@ def generic_edit_delete(editgroup_id, entity_type, edit_id):
# check that editgroup is edit-able
if editgroup.changelog_index != None:
- abort(400, "Editgroup already merged")
+ flash("Editgroup already merged")
+ abort(400)
+
+ # fetch entity (if set) or 404
+ existing = None
+ existing_edit = None
+ if editgroup and existing_ident:
+ existing, existing_edit = generic_get_editgroup_entity(editgroup, entity_type, existing_ident)
+ elif existing_ident:
+ existing = generic_get_entity(entity_type, existing_ident)
+
+ # parse form (if submitted)
+ status = 200
+ form = EntityTomlForm()
+
+ if form.is_submitted():
+ if form.validate_on_submit():
+ # API on behalf of user
+ user_api = auth_api(session['api_token'])
+ if not editgroup:
+ editgroup = form_editgroup_get_or_create(user_api, form)
+
+ if editgroup:
+
+ if not existing_ident: # it's a create
+ try:
+ edit = generic_entity_create_from_toml(user_api, entity_type, editgroup.editgroup_id, form.toml.data)
+ except ValueError as ve:
+ form.toml.errors = [ve]
+ status = 400
+ except ApiException as ae:
+ app.log.warning(ae)
+ raise ae
+ if status == 200:
+ return redirect('/editgroup/{}/{}/{}'.format(editgroup.editgroup_id, entity_type, edit.ident))
+ else: # it's an update
+ # TODO: some danger of wiping database state here is
+ # "updated edit" causes, eg, a 4xx error. Better to allow
+ # this in the API itself. For now, form validation *should*
+ # catch most errors, and if not editor can hit back and try
+ # again. This means, need to allow failure of deletion.
+ if existing_edit:
+ # need to clear revision on object or this becomes just
+ # a "update pointer" edit
+ existing.revision = None
+ generic_entity_delete_edit(user_api, entity_type, editgroup.editgroup_id, existing_edit.edit_id)
+ try:
+ edit = generic_entity_update_from_toml(user_api, entity_type, editgroup.editgroup_id, existing.ident, form.toml.data)
+ except ValueError as ve:
+ form.toml.errors = [ve]
+ status = 400
+ except ApiException as ae:
+ app.log.warning(ae)
+ raise ae
+ if status == 200:
+ return redirect('/editgroup/{}/{}/{}'.format(editgroup.editgroup_id, entity_type, edit.ident))
+ else:
+ status = 400
+ elif form.errors:
+ status = 400
+ app.log.info("form errors (did not validate): {}".format(form.errors))
+
+ else: # form is not submitted
+ if existing:
+ form = EntityTomlForm.from_entity(existing)
+
+ editor_editgroups = api.get_editor_editgroups(session['editor']['editor_id'], limit=20)
+ potential_editgroups = [e for e in editor_editgroups if e.changelog_index == None and e.submitted == None]
+
+ if not form.is_submitted():
+ # default to most recent not submitted, fallback to "create new"
+ form.editgroup_id.data = ""
+ if potential_editgroups:
+ form.editgroup_id.data = potential_editgroups[0].editgroup_id
+
+ return render_template(edit_template, form=form, entity_type=entity_type,
+ existing_ident=existing_ident, editgroup=editgroup,
+ potential_editgroups=potential_editgroups), status
+
+def generic_entity_delete(editgroup_id: Optional[str], entity_type: str, existing_ident: str):
+ """
+ Similar to generic_entity_edit(), but for deleting entities. This is a bit
+ simpler!
+
+ Handles both creation and update/edit paths.
+ """
+
+ # fetch editgroup (if set) or 404
+ editgroup = None
+ if editgroup_id:
+ try:
+ editgroup = api.get_editgroup(editgroup_id)
+ except ApiException as ae:
+ raise ae
+
+ # check that editgroup is edit-able
+ if editgroup.changelog_index != None:
+ flash("Editgroup already merged")
+ abort(400)
+
+ # fetch entity (if set) or 404
+ existing = None
+ existing_edit = None
+ if editgroup and existing_ident:
+ existing, existing_edit = generic_get_editgroup_entity(editgroup, entity_type, existing_ident)
+ elif existing_ident:
+ existing = generic_get_entity(entity_type, existing_ident)
+
+ # parse form (if submitted)
+ status = 200
+ form = EntityEditForm()
+
+ if form.is_submitted():
+ if form.validate_on_submit():
+ # API on behalf of user
+ user_api = auth_api(session['api_token'])
+ if not editgroup:
+ editgroup = form_editgroup_get_or_create(user_api, form)
+
+ if editgroup:
+ # TODO: some danger of wiping database state here is
+ # "updated edit" causes, eg, a 4xx error. Better to allow
+ # this in the API itself. For now, form validation *should*
+ # catch most errors, and if not editor can hit back and try
+ # again. This means, need to allow failure of deletion.
+ if existing_edit:
+ # need to clear revision on object or this becomes just
+ # a "update pointer" edit
+ existing.revision = None
+ generic_entity_delete_edit(user_api, entity_type, editgroup.editgroup_id, existing_edit.edit_id)
+ try:
+ edit = generic_entity_delete_entity(user_api, entity_type, editgroup.editgroup_id, existing.ident)
+ except ApiException as ae:
+ app.log.warning(ae)
+ raise ae
+ if status == 200:
+ return redirect('/editgroup/{}/{}/{}'.format(editgroup.editgroup_id, entity_type, edit.ident))
+ else:
+ status = 400
+ elif form.errors:
+ status = 400
+ app.log.info("form errors (did not validate): {}".format(form.errors))
+
+ else: # form is not submitted
+ if existing:
+ form = EntityTomlForm.from_entity(existing)
+
+ editor_editgroups = api.get_editor_editgroups(session['editor']['editor_id'], limit=20)
+ potential_editgroups = [e for e in editor_editgroups if e.changelog_index == None and e.submitted == None]
+
+ if not form.is_submitted():
+ # default to most recent not submitted, fallback to "create new"
+ form.editgroup_id.data = ""
+ if potential_editgroups:
+ form.editgroup_id.data = potential_editgroups[0].editgroup_id
+
+ return render_template("entity_delete.html", form=form, entity_type=entity_type,
+ existing_ident=existing_ident, editgroup=editgroup,
+ potential_editgroups=potential_editgroups), status
+
+def generic_edit_delete(editgroup_id, entity_type, edit_id):
+ # fetch editgroup (if set) or 404
+ editgroup = None
+ if editgroup_id:
+ try:
+ editgroup = api.get_editgroup(editgroup_id)
+ except ApiException as ae:
+ abort(ae.status)
+
+ # check that editgroup is edit-able
+ if editgroup.changelog_index != None:
+ flash("Editgroup already merged")
+ abort(400)
# API on behalf of user
user_api = auth_api(session['api_token'])
# do the deletion
- try:
- if entity_type == 'container':
- user_api.delete_container_edit(editgroup.editgroup_id, edit_id)
- elif entity_type == 'file':
- user_api.delete_file_edit(editgroup.editgroup_id, edit_id)
- elif entity_type == 'release':
- user_api.delete_release_edit(editgroup.editgroup_id, edit_id)
- else:
- raise NotImplementedError
- except ApiException as ae:
- raise ae
+ generic_entity_delete_edit(user_api, entity_type, editgroup.editgroup_id, edit_id)
return redirect("/editgroup/{}".format(editgroup_id))
@@ -234,19 +495,43 @@ def container_create_view():
@app.route('/container/<ident>/edit', methods=['GET', 'POST'])
@login_required
-def container_edit(ident):
+def container_edit_view(ident):
return generic_entity_edit(None, 'container', ident, 'container_edit.html')
+@app.route('/container/<ident>/delete', methods=['GET', 'POST'])
+@login_required
+def container_delete_view(ident):
+ return generic_entity_delete(None, 'container', ident)
+
@app.route('/editgroup/<editgroup_id>/container/<ident>/edit', methods=['GET', 'POST'])
@login_required
-def container_editgroup_edit(editgroup_id, ident):
+def container_editgroup_edit_view(editgroup_id, ident):
return generic_entity_edit(editgroup_id, 'container', ident, 'container_edit.html')
+@app.route('/editgroup/<editgroup_id>/container/<ident>/delete', methods=['GET', 'POST'])
+@login_required
+def container_editgroup_delete_view(editgroup_id, ident):
+ return generic_entity_delete(editgroup_id, 'container', ident)
+
@app.route('/editgroup/<editgroup_id>/container/edit/<edit_id>/delete', methods=['POST'])
@login_required
def container_edit_delete(editgroup_id, edit_id):
return generic_edit_delete(editgroup_id, 'container', edit_id)
+@app.route('/creator/<ident>/delete', methods=['GET', 'POST'])
+@login_required
+def creator_delete_view(ident):
+ return generic_entity_delete(None, 'creator', ident)
+
+@app.route('/editgroup/<editgroup_id>/creator/edit/<edit_id>/delete', methods=['POST'])
+def creator_edit_delete(editgroup_id, edit_id):
+ return generic_edit_delete(editgroup_id, 'creator', edit_id)
+
+@app.route('/editgroup/<editgroup_id>/creator/<ident>/delete', methods=['GET', 'POST'])
+@login_required
+def creator_editgroup_delete(editgroup_id, ident):
+ return generic_entity_delete(editgroup_id, 'creator', ident)
+
@app.route('/file/create', methods=['GET', 'POST'])
@login_required
def file_create_view():
@@ -254,19 +539,57 @@ def file_create_view():
@app.route('/file/<ident>/edit', methods=['GET', 'POST'])
@login_required
-def file_edit(ident):
+def file_edit_view(ident):
return generic_entity_edit(None, 'file', ident, 'file_edit.html')
+@app.route('/file/<ident>/delete', methods=['GET', 'POST'])
+@login_required
+def file_delete_view(ident):
+ return generic_entity_delete(None, 'file', ident)
+
@app.route('/editgroup/<editgroup_id>/file/<ident>/edit', methods=['GET', 'POST'])
@login_required
-def file_editgroup_edit(editgroup_id, ident):
+def file_editgroup_edit_view(editgroup_id, ident):
return generic_entity_edit(editgroup_id, 'file', ident, 'file_edit.html')
+@app.route('/editgroup/<editgroup_id>/file/<ident>/delete', methods=['GET', 'POST'])
+@login_required
+def file_editgroup_delete_view(editgroup_id, ident):
+ return generic_entity_delete(editgroup_id, 'file', ident)
+
@app.route('/editgroup/<editgroup_id>/file/edit/<edit_id>/delete', methods=['POST'])
@login_required
def file_edit_delete(editgroup_id, edit_id):
return generic_edit_delete(editgroup_id, 'file', edit_id)
+@app.route('/fileset/<ident>/delete', methods=['GET', 'POST'])
+@login_required
+def fileset_delete_view(ident):
+ return generic_entity_delete(None, 'fileset', ident)
+
+@app.route('/editgroup/<editgroup_id>/fileset/edit/<edit_id>/delete', methods=['POST'])
+def fileset_edit_delete(editgroup_id, edit_id):
+ return generic_edit_delete(editgroup_id, 'fileset', edit_id)
+
+@app.route('/editgroup/<editgroup_id>/fileset/<ident>/delete', methods=['GET', 'POST'])
+@login_required
+def fileset_editgroup_delete(editgroup_id, ident):
+ return generic_entity_delete(editgroup_id, 'fileset', ident)
+
+@app.route('/webcapture/<ident>/delete', methods=['GET', 'POST'])
+@login_required
+def webcapture_delete_view(ident):
+ return generic_entity_delete(None, 'webcapture', ident)
+
+@app.route('/editgroup/<editgroup_id>/webcapture/edit/<edit_id>/delete', methods=['POST'])
+def webcapture_edit_delete(editgroup_id, edit_id):
+ return generic_edit_delete(editgroup_id, 'webcapture', edit_id)
+
+@app.route('/editgroup/<editgroup_id>/webcapture/<ident>/delete', methods=['GET', 'POST'])
+@login_required
+def webcapture_editgroup_delete(editgroup_id, ident):
+ return generic_entity_delete(editgroup_id, 'webcapture', ident)
+
@app.route('/release/create', methods=['GET', 'POST'])
@login_required
def release_create_view():
@@ -274,82 +597,208 @@ def release_create_view():
@app.route('/release/<ident>/edit', methods=['GET', 'POST'])
@login_required
-def release_edit(ident):
+def release_edit_view(ident):
return generic_entity_edit(None, 'release', ident, 'release_edit.html')
+@app.route('/release/<ident>/delete', methods=['GET', 'POST'])
+@login_required
+def release_delete_view(ident):
+ return generic_entity_delete(None, 'release', ident)
+
@app.route('/editgroup/<editgroup_id>/release/<ident>/edit', methods=['GET', 'POST'])
@login_required
def release_editgroup_edit(editgroup_id, ident):
return generic_entity_edit(editgroup_id, 'release', ident, 'release_edit.html')
+@app.route('/editgroup/<editgroup_id>/release/<ident>/delete', methods=['GET', 'POST'])
+@login_required
+def release_editgroup_delete(editgroup_id, ident):
+ return generic_entity_delete(editgroup_id, 'release', ident)
+
@app.route('/editgroup/<editgroup_id>/release/edit/<edit_id>/delete', methods=['POST'])
@login_required
def release_edit_delete(editgroup_id, edit_id):
return generic_edit_delete(editgroup_id, 'release', edit_id)
+@app.route('/work/<ident>/delete', methods=['GET', 'POST'])
+@login_required
+def work_delete_view(ident):
+ return generic_entity_delete(None, 'work', ident)
-### Not-Implemented Views ###################################################
+@app.route('/editgroup/<editgroup_id>/work/edit/<edit_id>/delete', methods=['POST'])
+def work_edit_delete(editgroup_id, edit_id):
+ return generic_edit_delete(editgroup_id, 'work', edit_id)
+
+@app.route('/editgroup/<editgroup_id>/work/<ident>/delete', methods=['GET', 'POST'])
+@login_required
+def work_editgroup_delete(editgroup_id, ident):
+ return generic_entity_delete(editgroup_id, 'work', ident)
+
+### TOML Views ##############################################################
+
+@app.route('/container/create/toml', methods=['GET', 'POST'])
+@login_required
+def container_create_toml_view():
+ return generic_entity_toml_edit(None, 'container', None, 'entity_create_toml.html')
+
+@app.route('/container/<ident>/edit/toml', methods=['GET', 'POST'])
+@login_required
+def container_edit_toml_view(ident):
+ return generic_entity_toml_edit(None, 'container', ident, 'entity_edit_toml.html')
+
+@app.route('/editgroup/<editgroup_id>/container/<ident>/edit/toml', methods=['GET', 'POST'])
+@login_required
+def container_editgroup_edit_toml(editgroup_id, ident):
+ return generic_entity_toml_edit(editgroup_id, 'container', ident, 'entity_edit_toml.html')
+
+@app.route('/creator/create/toml', methods=['GET', 'POST'])
+@login_required
+def creator_create_toml_view():
+ return generic_entity_toml_edit(None, 'creator', None, 'entity_create_toml.html')
+
+@app.route('/creator/<ident>/edit/toml', methods=['GET', 'POST'])
+@login_required
+def creator_edit_toml_view(ident):
+ return generic_entity_toml_edit(None, 'creator', ident, 'entity_edit_toml.html')
+
+@app.route('/editgroup/<editgroup_id>/creator/<ident>/edit/toml', methods=['GET', 'POST'])
+@login_required
+def creator_editgroup_edit_toml(editgroup_id, ident):
+ return generic_entity_toml_edit(editgroup_id, 'creator', ident, 'entity_edit_toml.html')
+
+@app.route('/file/create/toml', methods=['GET', 'POST'])
+@login_required
+def file_create_toml_view():
+ return generic_entity_toml_edit(None, 'file', None, 'entity_create_toml.html')
+
+@app.route('/file/<ident>/edit/toml', methods=['GET', 'POST'])
+@login_required
+def file_edit_toml_view(ident):
+ return generic_entity_toml_edit(None, 'file', ident, 'entity_edit_toml.html')
+
+@app.route('/editgroup/<editgroup_id>/file/<ident>/edit/toml', methods=['GET', 'POST'])
+@login_required
+def file_editgroup_edit_toml(editgroup_id, ident):
+ return generic_entity_toml_edit(editgroup_id, 'file', ident, 'entity_edit_toml.html')
+
+@app.route('/fileset/create/toml', methods=['GET', 'POST'])
+@login_required
+def fileset_create_toml_view():
+ return generic_entity_toml_edit(None, 'fileset', None, 'entity_create_toml.html')
+
+@app.route('/fileset/<ident>/edit/toml', methods=['GET', 'POST'])
+@login_required
+def fileset_edit_toml_view(ident):
+ return generic_entity_toml_edit(None, 'fileset', ident, 'entity_edit_toml.html')
+
+@app.route('/editgroup/<editgroup_id>/fileset/<ident>/edit/toml', methods=['GET', 'POST'])
+@login_required
+def fileset_editgroup_edit_toml(editgroup_id, ident):
+ return generic_entity_toml_edit(editgroup_id, 'fileset', ident, 'entity_edit_toml.html')
+
+@app.route('/webcapture/create/toml', methods=['GET', 'POST'])
+@login_required
+def webcapture_create_toml_view():
+ return generic_entity_toml_edit(None, 'webcapture', None, 'entity_create_toml.html')
+
+@app.route('/webcapture/<ident>/edit/toml', methods=['GET', 'POST'])
+@login_required
+def webcapture_edit_toml_view(ident):
+ return generic_entity_toml_edit(None, 'webcapture', ident, 'entity_edit_toml.html')
+
+@app.route('/editgroup/<editgroup_id>/webcapture/<ident>/edit/toml', methods=['GET', 'POST'])
+@login_required
+def webcapture_editgroup_edit_toml(editgroup_id, ident):
+ return generic_entity_toml_edit(editgroup_id, 'webcapture', ident, 'entity_edit_toml.html')
+
+@app.route('/release/create/toml', methods=['GET', 'POST'])
+@login_required
+def release_create_toml_view():
+ return generic_entity_toml_edit(None, 'release', None, 'entity_create_toml.html')
+
+@app.route('/release/<ident>/edit/toml', methods=['GET', 'POST'])
+@login_required
+def release_edit_toml_view(ident):
+ return generic_entity_toml_edit(None, 'release', ident, 'entity_edit_toml.html')
+
+@app.route('/editgroup/<editgroup_id>/release/<ident>/edit/toml', methods=['GET', 'POST'])
+@login_required
+def release_editgroup_edit_toml(editgroup_id, ident):
+ return generic_entity_toml_edit(editgroup_id, 'release', ident, 'entity_edit_toml.html')
+
+@app.route('/work/create/toml', methods=['GET', 'POST'])
+@login_required
+def work_create_toml_view():
+ return generic_entity_toml_edit(None, 'work', None, 'entity_create_toml.html')
+
+@app.route('/work/<ident>/edit/toml', methods=['GET', 'POST'])
+@login_required
+def work_edit_toml_view(ident):
+ return generic_entity_toml_edit(None, 'work', ident, 'entity_edit_toml.html')
+
+@app.route('/editgroup/<editgroup_id>/work/<ident>/edit/toml', methods=['GET', 'POST'])
+@login_required
+def work_editgroup_edit_toml(editgroup_id, ident):
+ return generic_entity_toml_edit(editgroup_id, 'work', ident, 'entity_edit_toml.html')
+
+### TOML-Only Editing Redirects ################################################
@app.route('/creator/create', methods=['GET'])
+@login_required
def creator_create_view():
- return abort(404)
+ return redirect('/creator/create/toml')
@app.route('/creator/<ident>/edit', methods=['GET'])
-def creator_edit(ident):
- return render_template('entity_edit.html'), 404
+@login_required
+def creator_edit_view(ident):
+ return redirect(f'/creator/{ident}/edit/toml')
@app.route('/editgroup/<editgroup_id>/creator/<ident>/edit', methods=['GET', 'POST'])
+@login_required
def creator_editgroup_edit(editgroup_id, ident):
- return abort(404)
-
-@app.route('/editgroup/<editgroup_id>/creator/edit/<edit_id>/delete', methods=['POST'])
-def creator_edit_delete(editgroup_id, edit_id):
- return abort(404)
+ return redirect(f'/editgroup/{editgroup_id}/creator/{ident}/edit/toml')
@app.route('/fileset/create', methods=['GET'])
+@login_required
def fileset_create_view():
- return abort(404)
+ return redirect('/fileset/create/toml')
@app.route('/fileset/<ident>/edit', methods=['GET'])
-def fileset_edit(ident):
- return render_template('entity_edit.html'), 404
+@login_required
+def fileset_edit_view(ident):
+ return redirect(f'/fileset/{ident}/edit/toml')
@app.route('/editgroup/<editgroup_id>/fileset/<ident>/edit', methods=['GET', 'POST'])
+@login_required
def fileset_editgroup_edit(editgroup_id, ident):
- return abort(404)
-
-@app.route('/editgroup/<editgroup_id>/fileset/edit/<edit_id>/delete', methods=['POST'])
-def fileset_edit_delete(editgroup_id, edit_id):
- return abort(404)
+ return redirect(f'/editgroup/{editgroup_id}/fileset/{ident}/edit/toml')
@app.route('/webcapture/create', methods=['GET'])
+@login_required
def webcapture_create_view():
- return abort(404)
+ return redirect('/webcapture/create/toml')
@app.route('/webcapture/<ident>/edit', methods=['GET'])
-def webcapture_edit(ident):
- return render_template('entity_edit.html'), 404
+@login_required
+def webcapture_edit_view(ident):
+ return redirect(f'/webcapture/{ident}/edit/toml')
@app.route('/editgroup/<editgroup_id>/webcapture/<ident>/edit', methods=['GET', 'POST'])
+@login_required
def webcapture_editgroup_edit(editgroup_id, ident):
- return abort(404)
-
-@app.route('/editgroup/<editgroup_id>/webcapture/edit/<edit_id>/delete', methods=['POST'])
-def webcapture_edit_delete(editgroup_id, edit_id):
- return abort(404)
+ return redirect(f'/editgroup/{editgroup_id}/webcapture/{ident}/edit/toml')
@app.route('/work/create', methods=['GET'])
+@login_required
def work_create_view():
- return abort(404)
+ return redirect('/work/create/toml')
@app.route('/work/<ident>/edit', methods=['GET'])
-def work_edit(ident):
- return render_template('entity_edit.html'), 404
+@login_required
+def work_edit_view(ident):
+ return redirect(f'/work/{ident}/edit/toml')
@app.route('/editgroup/<editgroup_id>/work/<ident>/edit', methods=['GET', 'POST'])
+@login_required
def work_editgroup_edit(editgroup_id, ident):
- return abort(404)
-
-@app.route('/editgroup/<editgroup_id>/work/edit/<edit_id>/delete', methods=['POST'])
-def work_edit_delete(editgroup_id, edit_id):
- return abort(404)
+ return redirect(f'/editgroup/{editgroup_id}/work/{ident}/edit/toml')
diff --git a/python/fatcat_web/entity_helpers.py b/python/fatcat_web/entity_helpers.py
index c9a57290..7156b9be 100644
--- a/python/fatcat_web/entity_helpers.py
+++ b/python/fatcat_web/entity_helpers.py
@@ -1,5 +1,6 @@
from flask import abort
+from fatcat_openapi_client import *
from fatcat_openapi_client.rest import ApiException, ApiValueError
from fatcat_tools.transforms import *
from fatcat_web import api
@@ -148,6 +149,26 @@ def generic_get_entity_revision(entity_type, revision_id):
except ApiValueError:
abort(400)
+def generic_deleted_entity(entity_type, ident):
+ if entity_type == 'container':
+ entity = ContainerEntity()
+ elif entity_type == 'creator':
+ entity = CreatorEntity()
+ elif entity_type == 'file':
+ entity = FileEntity()
+ elif entity_type == 'fileset':
+ entity = FilesetEntity()
+ elif entity_type == 'webcapture':
+ entity = WebcaptureEntity()
+ elif entity_type == 'release':
+ entity = ReleaseEntity()
+ elif entity_type == 'work':
+ entity = WorkEntity()
+ else:
+ raise NotImplementedError
+ entity.ident = ident
+ return entity
+
def generic_get_editgroup_entity(editgroup, entity_type, ident):
if entity_type == 'container':
edits = editgroup.edits.containers
@@ -166,14 +187,18 @@ def generic_get_editgroup_entity(editgroup, entity_type, ident):
else:
raise NotImplementedError
revision_id = None
+ edit = None
for e in edits:
if e.ident == ident:
revision_id = e.revision
edit = e
break
- if not revision_id:
+ if not edit:
# couldn't find relevant edit in this editgroup
abort(404)
+ if not revision_id:
+ # deletion, presumably
+ return generic_deleted_entity(entity_type, ident), edit
try:
entity = generic_get_entity_revision(entity_type, revision_id)
diff --git a/python/fatcat_web/forms.py b/python/fatcat_web/forms.py
index 15585bf6..ff885fcb 100644
--- a/python/fatcat_web/forms.py
+++ b/python/fatcat_web/forms.py
@@ -4,10 +4,14 @@ Note: in thoery could use, eg, https://github.com/christabor/swagger_wtforms,
but can't find one that is actually maintained.
"""
+import datetime
+
+import toml
from flask_wtf import FlaskForm
from wtforms import SelectField, DateField, StringField, IntegerField, \
- HiddenField, FormField, FieldList, validators
+ HiddenField, FormField, FieldList, validators, ValidationError, TextAreaField
+from fatcat_tools import entity_to_toml
from fatcat_openapi_client import ContainerEntity, FileEntity, \
ReleaseEntity, ReleaseContrib, FileUrl, ReleaseExtIds
@@ -29,6 +33,16 @@ release_stage_options = [
('published', 'Published'),
('updated', 'Updated'),
]
+withdrawn_status_options = [
+ ('', 'Not Withdrawn (blank)'),
+ ('retracted', 'Retracted'),
+ ('withdrawn', 'Withdrawn'),
+ ('concern', 'Concern Noted'),
+ ('spam', 'Spam'),
+ ('legal', 'Legal Taketown'),
+ ('safety', 'Public Safety'),
+ ('national-security', 'National Security'),
+]
role_type_options = [
('author', 'Author'),
('editor', 'Editor'),
@@ -62,11 +76,25 @@ class ReleaseContribForm(FlaskForm):
default='author')
RELEASE_SIMPLE_ATTRS = ['title', 'original_title', 'work_id', 'container_id',
- 'release_type', 'release_stage', 'release_date', 'volume', 'issue',
- 'pages', 'publisher', 'language', 'license_slug']
+ 'release_type', 'release_stage', 'withdrawn_status', 'release_date',
+ 'release_year', 'volume', 'issue', 'pages', 'publisher', 'language',
+ 'license_slug']
RELEASE_EXTID_ATTRS = ['doi', 'wikidata_qid', 'isbn13', 'pmid', 'pmcid']
+def valid_year(form, field):
+ if field.data > datetime.date.today().year + 5:
+ raise ValidationError(
+ f"Year is too far in the future: {field.data}")
+ if field.data < 10:
+ raise ValidationError(
+ f"Year is too far in the past: {field.data}")
+
+def valid_2char_ascii(form, field):
+ if len(field.data) != 2 or len(field.data.encode('utf-8')) != 2 or not field.data.isalpha() or field.data != field.data.lower():
+ raise ValidationError(
+ f"Must be 2-character ISO format, lower case: {field.data}")
+
class ReleaseEntityForm(EntityEditForm):
"""
TODO:
@@ -75,7 +103,7 @@ class ReleaseEntityForm(EntityEditForm):
"""
title = StringField('Title',
[validators.DataRequired()])
- original_title = StringField('Original Title')
+ original_title = StringField('Title in Original Language (if different)')
work_id = StringField('Work FCID',
[validators.Optional(True),
validators.Length(min=26, max=26)])
@@ -87,9 +115,14 @@ class ReleaseEntityForm(EntityEditForm):
choices=release_type_options,
default='')
release_stage = SelectField(choices=release_stage_options)
+ withdrawn_status = SelectField("Withdrawn Status",
+ [validators.Optional(True)],
+ choices=withdrawn_status_options,
+ default='')
release_date = DateField('Release Date',
[validators.Optional(True)])
- #release_year
+ release_year = IntegerField('Release Year',
+ [validators.Optional(True), valid_year])
doi = StringField('DOI',
[validators.Regexp(r'^10\..*\/.*', message="DOI must be valid"),
validators.Optional(True)])
@@ -104,7 +137,8 @@ class ReleaseEntityForm(EntityEditForm):
issue = StringField('Issue')
pages = StringField('Pages')
publisher = StringField('Publisher (optional)')
- language = StringField('Language (code)')
+ language = StringField('Language (code)',
+ [validators.Optional(True), valid_2char_ascii])
license_slug = StringField('License (slug)')
contribs = FieldList(FormField(ReleaseContribForm))
#refs
@@ -150,11 +184,13 @@ class ReleaseEntityForm(EntityEditForm):
a = None
setattr(re, simple_attr, a)
for extid_attr in RELEASE_EXTID_ATTRS:
- a = getattr(self, simple_attr).data
+ a = getattr(self, extid_attr).data
# special case blank strings
if a == '':
a = None
- setattr(re.ext_ids, simple_attr, a)
+ setattr(re.ext_ids, extid_attr, a)
+ if self.release_date.data:
+ re.release_year = self.release_date.data.year
# 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
@@ -204,8 +240,9 @@ class ContainerEntityForm(EntityEditForm):
issnl = StringField("ISSN-L (linking)")
issne = StringField("ISSN (electronic)")
issnp = StringField("ISSN (print)")
- original_name = StringField("Original Name (native language)")
- country = StringField("Country of Publication (ISO code)")
+ original_name = StringField("Name in Original Language (if different)")
+ country = StringField("Country of Publication (ISO code)",
+ [validators.Optional(True), valid_2char_ascii])
wikidata_qid = StringField('Wikidata QID')
urls = FieldList(
StringField("Container URLs",
@@ -413,3 +450,31 @@ class SavePaperNowForm(FlaskForm):
ingest_request['link_source'] = 'arxiv'
ingest_request['link_source_id'] = release.ext_ids.arxiv
return ingest_request
+
+def valid_toml(form, field):
+ try:
+ toml.loads(field.data)
+ except toml.TomlDecodeError as tpe:
+ raise ValidationError(tpe)
+
+class EntityTomlForm(EntityEditForm):
+
+ toml = TextAreaField(
+ "TOML",
+ [validators.DataRequired(),
+ valid_toml,
+ ],
+ )
+
+ @staticmethod
+ def from_entity(entity):
+ """
+ Initializes form with TOML version of existing entity
+ """
+ etf = EntityTomlForm()
+ if entity.state == 'active':
+ pop_fields = ['ident', 'state', 'revision', 'redirect']
+ else:
+ pop_fields = ['ident', 'state']
+ etf.toml.data = entity_to_toml(entity, pop_fields=pop_fields)
+ return etf
diff --git a/python/fatcat_web/routes.py b/python/fatcat_web/routes.py
index 74805c69..da2bb6cf 100644
--- a/python/fatcat_web/routes.py
+++ b/python/fatcat_web/routes.py
@@ -227,7 +227,7 @@ def generic_editgroup_entity_view(editgroup_id, entity_type, ident, view_templat
entity, edit = generic_get_editgroup_entity(editgroup, entity_type, ident)
- if entity.state == "deleted":
+ if entity.revision is None or entity.state == "deleted":
return render_template('deleted_entity.html', entity=entity,
entity_type=entity_type, editgroup=editgroup)
diff --git a/python/fatcat_web/templates/base.html b/python/fatcat_web/templates/base.html
index 18c66077..2c18ec44 100644
--- a/python/fatcat_web/templates/base.html
+++ b/python/fatcat_web/templates/base.html
@@ -25,6 +25,10 @@
@media only screen and (max-width: 479px) {
.mobile-hide{ display: none !important; }
}
+
+ .field textarea#toml {
+ font-family: monospace;
+ }
</style>
{% block extra_head %}{% endblock %}
</head>
diff --git a/python/fatcat_web/templates/container_create.html b/python/fatcat_web/templates/container_create.html
index 5786d05d..be8c5671 100644
--- a/python/fatcat_web/templates/container_create.html
+++ b/python/fatcat_web/templates/container_create.html
@@ -9,13 +9,15 @@ a journal (eg, "New England Journal of Medicine"), conference proceedings, a
book series, or a blog. Not all publications are in a container.
<form class="ui form" id="create_container_form" method="POST" action="/container/create">
+ <p>Experienced users can also use the <a href="/container/create/toml">TOML
+ creation form</a> to access all metadata fields in a raw format.
{% endblock %}
{% block edit_form_suffix %}
<br><br>
<input class="ui primary submit button" type="submit" value="Create Container!">
<p>
- <i>New entity will be part of the current editgroup, which needs to be
+ <i>New container entity will be part of the current editgroup, which needs to be
submited and approved before the entity will formally be included in the
catalog.</i>
</form>
diff --git a/python/fatcat_web/templates/container_edit.html b/python/fatcat_web/templates/container_edit.html
index 5188ce0d..99f77d53 100644
--- a/python/fatcat_web/templates/container_edit.html
+++ b/python/fatcat_web/templates/container_edit.html
@@ -7,6 +7,14 @@
<h1 class="ui header">Edit Container Entity</h1>
<form class="ui form" id="edit_container_form" method="POST" action="{% if editgroup %}/editgroup/{{ editgroup.editgroup_id }}{% endif %}/container/{{ existing_ident }}/edit">
+
+ <p>Experienced users can also use the <a href="{% if editgroup
+ %}/editgroup/{{ editgroup.editgroup_id }}{% endif %}/container/{{
+ existing_ident }}/edit/toml">TOML editing form</a> to access all metadata
+ fields in a raw format.
+ {% if not editgroup %}
+ You can also <a href="/container/{{ existing_ident }}/delete">delete this entity</a>.
+ {% endif %}
{% endblock %}
<p>See <a href="https://guide.fatcat.wiki/entity_container.html">the catalog
diff --git a/python/fatcat_web/templates/deleted_entity.html b/python/fatcat_web/templates/deleted_entity.html
index eefc87cf..4c6b14b6 100644
--- a/python/fatcat_web/templates/deleted_entity.html
+++ b/python/fatcat_web/templates/deleted_entity.html
@@ -30,7 +30,7 @@
<b>Entity Type:</b> <code>{{ entity_type }}</code>
</div>
-{{ entity_macros.fatcat_bits(entity, entity_type, "") }}
+{{ entity_macros.fatcat_bits(entity, entity_type, "", editgroup=editgroup) }}
</div>
</div>
diff --git a/python/fatcat_web/templates/edit_macros.html b/python/fatcat_web/templates/edit_macros.html
index a207e51e..d4839373 100644
--- a/python/fatcat_web/templates/edit_macros.html
+++ b/python/fatcat_web/templates/edit_macros.html
@@ -17,6 +17,23 @@
</div>
{%- endmacro %}
+{% macro form_toml_field(field, div_classes="") -%}
+<h3 class="ui dividing header">Raw Metadata (TOML)</h3>
+<div class="field {{ div_classes }} {% if field.errors %}error{% endif %}">
+ <p><a href="https://toml.io/en/">TOML</a> is a markup language, similar to
+ YAML or Wikitext. The schema here is the same as the Fatcat API JSON schema, but
+ with a syntax that is easier to read and edit by hand. All nested metadata
+ fields are available here; refer to the fatcat guide for metadata
+ documentation and style guide, or TOML documentation for syntax notes (eg,
+ what those double brackets mean, and how to represent lists of authors or
+ references).
+ <br>
+ <br>
+ {{ field(rows=24) }}
+ {{ 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;" #}>
@@ -38,7 +55,7 @@
{% macro editgroup_dropdown(form, editgroup=None, potential_editgroups=None) -%}
{% if editgroup %}
<p>You are updating an existing un-merged editgroup: <a href="/editgroup/{{ editgroup.editgroup_id}}">{{ editgroup.editgroup_id }}</a>.
- <p><b>Description:</b> {{ editgroup.description }}
+ <p><b>Description:</b> {{ editgroup.description or "" }}
{% else %}
{% if not potential_editgroups %}
<p>You have no un-submitted editgroups in progress; a new one will be
diff --git a/python/fatcat_web/templates/editgroup_view.html b/python/fatcat_web/templates/editgroup_view.html
index e8146d19..a36dc3e5 100644
--- a/python/fatcat_web/templates/editgroup_view.html
+++ b/python/fatcat_web/templates/editgroup_view.html
@@ -25,7 +25,7 @@
updated
{% endif %}
<a href="/editgroup/{{ editgroup.editgroup_id }}/{{ entity_type }}/{{ edit.ident }}">[view edit]</a>
- {% if auth_to.edit and not editgroup.changelog_index and not editgroup.submitted and entity_type in ('release', 'file', 'container') %}
+ {% if auth_to.edit and not editgroup.changelog_index and not editgroup.submitted %}
<a href="/editgroup/{{ editgroup.editgroup_id }}/{{ entity_type }}/{{ edit.ident }}/edit" style="color: green;">[re-edit]</a>
<form id="submit_edit_delete" method="POST" action="/editgroup/{{ editgroup.editgroup_id }}/{{ entity_type }}/edit/{{ edit.edit_id }}/delete" style="display:inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
diff --git a/python/fatcat_web/templates/entity_create_toml.html b/python/fatcat_web/templates/entity_create_toml.html
new file mode 100644
index 00000000..ec5bc4a2
--- /dev/null
+++ b/python/fatcat_web/templates/entity_create_toml.html
@@ -0,0 +1,20 @@
+{% extends "entity_edit_toml.html" %}
+
+{% block edit_form_prefix %}
+<div class="ui segment">
+<h1 class="ui header">Create New {{ entity_type }} Entity (TOML mode)</h1>
+
+<form class="ui form" id="create_entity_form" method="POST" action="/{{ entity_type }}/create/toml">
+{% endblock %}
+
+{% block edit_form_suffix %}
+ <br><br>
+ <input class="ui primary submit button" type="submit" value="Create {{ entity_type }}!">
+ <p>
+ <i>New {{ entity_type }} entity will be part of the current editgroup, which
+ needs to be submited and approved before the entity will formally be included
+ in the catalog.</i>
+</form>
+</div>
+{% endblock %}
+
diff --git a/python/fatcat_web/templates/entity_delete.html b/python/fatcat_web/templates/entity_delete.html
new file mode 100644
index 00000000..b2e13af4
--- /dev/null
+++ b/python/fatcat_web/templates/entity_delete.html
@@ -0,0 +1,49 @@
+{% import "edit_macros.html" as edit_macros %}
+{% extends "base.html" %}
+
+{% block body %}
+{% block edit_form_prefix %}
+ <div class="ui segment">
+ <h1 class="ui header">Delete Entity</h1>
+
+ <form class="ui form" id="delete_form" method="POST" action="{% if editgroup and editgroup.editgroup_id %}/editgroup/{{ editgroup.editgroup_id }}{% endif %}/{{ entity_type }}/{{ existing_ident }}/delete">
+{% endblock %}
+
+ <p>See <a href="https://guide.fatcat.wiki/entity_release.html">the catalog
+ style guide</a> for schema notes, and <a
+ href="https://guide.fatcat.wiki/editing_quickstart.html">the editing
+ tutorial</a> if this is your first time making an edit.
+
+ {{ form.hidden_tag() }}
+
+ <h3 class="ui dividing header">Editgroup Metadata</h3>
+ {{ edit_macros.editgroup_dropdown(form, editgroup, potential_editgroups) }}
+
+ <h3 class="ui dividing header">Submit</h3>
+ {{ edit_macros.form_field_basic(form.edit_description) }}
+ This description will be attached to the individual edit, not to the
+ editgroup as a whole.
+
+{% block edit_form_suffix %}
+ <br><br>
+ <input class="ui primary submit button" type="submit" value="Update Release!">
+ <p>
+ <i>Deletion will be part of the current editgroup, which needs to be submited and
+ approved before the change is included in the catalog.</i>
+</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) :(
+ $('.ui.dropdown') .dropdown();
+});
+</script>
+{% endblock %}
diff --git a/python/fatcat_web/templates/entity_edit.html b/python/fatcat_web/templates/entity_edit.html
deleted file mode 100644
index 97f7bf46..00000000
--- a/python/fatcat_web/templates/entity_edit.html
+++ /dev/null
@@ -1,8 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
-
-<h1>Not Implemented</h1>
-
-<p>Entity editing via the web interface isn't implemented yet. Sorry!
-
-{% endblock %}
diff --git a/python/fatcat_web/templates/entity_edit_toml.html b/python/fatcat_web/templates/entity_edit_toml.html
new file mode 100644
index 00000000..64768d6e
--- /dev/null
+++ b/python/fatcat_web/templates/entity_edit_toml.html
@@ -0,0 +1,55 @@
+{% 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 Entity (TOML mode)</h1>
+
+ <form class="ui form" id="edit_toml_form" method="POST" action="{% if editgroup and editgroup.editgroup_id %}/editgroup/{{ editgroup.editgroup_id }}{% endif %}/{{ entity_type }}/{{ existing_ident }}/edit/toml">
+
+ {% if not editgroup %}
+ <p>You can also <a href="/{{ entity_type }}/{{ existing_ident }}/delete">delete this entity</a>.
+ {% endif %}
+{% endblock %}
+
+ <p>See <a href="https://guide.fatcat.wiki/entity_release.html">the catalog
+ style guide</a> for schema notes, and <a
+ href="https://guide.fatcat.wiki/editing_quickstart.html">the editing
+ tutorial</a> if this is your first time making an edit.
+
+ {{ form.hidden_tag() }}
+
+ <h3 class="ui dividing header">Editgroup Metadata</h3>
+ {{ edit_macros.editgroup_dropdown(form, editgroup, potential_editgroups) }}
+
+ {{ edit_macros.form_toml_field(form.toml, "required") }}
+
+ <h3 class="ui dividing header">Submit</h3>
+ {{ edit_macros.form_field_basic(form.edit_description) }}
+ This description will be attached to the individual edit, not to the
+ editgroup as a whole.
+
+{% block edit_form_suffix %}
+ <br><br>
+ <input class="ui primary submit button" type="submit" value="Update Release!">
+ <p>
+ <i>Edit will be part of the current editgroup, which needs to be submited and
+ approved before the change is included in the catalog.</i>
+</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) :(
+ $('.ui.dropdown') .dropdown();
+});
+</script>
+{% endblock %}
diff --git a/python/fatcat_web/templates/entity_macros.html b/python/fatcat_web/templates/entity_macros.html
index 718c071c..0ce646bf 100644
--- a/python/fatcat_web/templates/entity_macros.html
+++ b/python/fatcat_web/templates/entity_macros.html
@@ -33,7 +33,7 @@
{% if entity.state %}
State is "{{ entity.state }}".
{% endif %}
- {% if entity.state != "deleted" %}
+ {% if entity.revision %}
Revision:
<br><small><code><a href="/{{ entity_type }}/rev/{{ entity.revision }}">{{ entity.revision }}</a></code></small>
{% endif %}
@@ -43,11 +43,13 @@
{%- else -%}
https://api.{{ config.FATCAT_DOMAIN }}
{%- endif -%}
- /v0/{{ entity_type }}
- {%- if entity.ident -%}
- /{{ entity.ident }}
+ /v0
+ {%- if editgroup and entity.ident -%}
+ /editgroup/{{ editgroup.editgroup_id }}{# /{{ entity_type }}/{{ entity.ident }} #}
+ {%- elif entity.ident -%}
+ /{{ entity_type }}/{{ entity.ident }}
{%- elif entity.revision -%}
- /rev/{{ entity.revision }}
+ /{{ entity_type }}/rev/{{ entity.revision }}
{% endif %}
{% if expand %}?expand={{ expand}}{% endif %}">
As JSON object via API
diff --git a/python/fatcat_web/templates/file_create.html b/python/fatcat_web/templates/file_create.html
index a7c99b96..affcfb6e 100644
--- a/python/fatcat_web/templates/file_create.html
+++ b/python/fatcat_web/templates/file_create.html
@@ -5,13 +5,15 @@
<h1 class="ui header">Create New File Entity</h1>
<form class="ui form" id="create_file_form" method="POST" action="/file/create">
+ <p>Experienced users can also use the <a href="/file/create/toml">TOML
+ creation form</a> to access all metadata fields in a raw format.
{% endblock %}
{% block edit_form_suffix %}
<br><br>
<input class="ui primary submit button" type="submit" value="Create File!">
<p>
- <i>New entity will be part of the current editgroup, which needs to be
+ <i>New file entity will be part of the current editgroup, which needs to be
submited and approved before the entity will formally be included in the
catalog.</i>
</form>
diff --git a/python/fatcat_web/templates/file_edit.html b/python/fatcat_web/templates/file_edit.html
index e8a421b3..745b0c41 100644
--- a/python/fatcat_web/templates/file_edit.html
+++ b/python/fatcat_web/templates/file_edit.html
@@ -7,6 +7,14 @@
<h1 class="ui header">Edit File Entity</h1>
<form class="ui form" id="edit_file_form" method="POST" action="{% if editgroup %}/editgroup/{{ editgroup.editgroup_id }}{% endif %}/file/{{ existing_ident }}/edit">
+
+ <p>Experienced users can also use the <a href="{% if editgroup
+ %}/editgroup/{{ editgroup.editgroup_id }}{% endif %}/file/{{
+ existing_ident }}/edit/toml">TOML editing form</a> to access all metadata
+ fields in a raw format.
+ {% if not editgroup %}
+ You can also <a href="/file/{{ existing_ident }}/delete">delete this entity</a>.
+ {% endif %}
{% endblock %}
<p>See <a href="https://guide.fatcat.wiki/entity_file.html">the catalog
diff --git a/python/fatcat_web/templates/home.html b/python/fatcat_web/templates/home.html
index 4557e212..de32d6a4 100644
--- a/python/fatcat_web/templates/home.html
+++ b/python/fatcat_web/templates/home.html
@@ -171,11 +171,10 @@
<tr><td><b>Creator</b>
<br>authors, editors, translators
+ <td><a href="/creator/create">Create</a>
{% if config.FATCAT_DOMAIN == 'fatcat.wiki' %}
- <td>
<td><a href="/creator/iimvc523xbhqlav6j3sbthuehu">Author</a>
{% else %}
- <td><!-- <a href="/creator/create">Create</a> -->
<td><a href="/creator/iimvc523xbhqlav6j3sbthuehu">Author</a> (prod)
<br><a href="/creator/aaaaaaaaaaaaaircaaaaaaaaai">Dummy</a>
<br><a href="/creator/aaaaaaaaaaaaaircaaaaaaaaam">Realistic</a>
@@ -206,11 +205,10 @@
</form>
<tr><td><b>File Set</b>
<br>datasets, suplementary materials
+ <td><a href="/fileset/create">Create</a>
{% if config.FATCAT_DOMAIN == 'fatcat.wiki' %}
- <td>
<td><a href="/fileset/ho376wmdanckpp66iwfs7g22ne">Dataset</a>
{% else %}
- <td>
<td><a href="/fileset/ho376wmdanckpp66iwfs7g22ne">Dataset</a> (prod)
<br><a href="/fileset/aaaaaaaaaaaaaztgaaaaaaaaai">Dummy</a>
<br><a href="/fileset/aaaaaaaaaaaaaztgaaaaaaaaam">Realistic</a>
@@ -218,12 +216,11 @@
<td>
<tr><td><b>Web Capture</b>
<br>HTML and interactive articles, blog posts
+ <td><a href="/webcapture/create">Create</a>
{% if config.FATCAT_DOMAIN == 'fatcat.wiki' %}
- <td>
<td><a href="/webcapture/z7uaeatyvfgwdpuxtrdu4okqii">D-Lib</a>
<br><a href="/webcapture/5l2pubtfefbmdnqws2izccqlpm">Blog Post</a>
{% else %}
- <td>
<td><a href="/webcapture/z7uaeatyvfgwdpuxtrdu4okqii">D-Lib</a> (prod)
<br><a href="/webcapture/aaaaaaaaaaaaa53xaaaaaaaaai">Dummy</a>
<br><a href="/webcapture/aaaaaaaaaaaaa53xaaaaaaaaam">Realistic</a>
@@ -231,11 +228,10 @@
<td>
<tr><td><b>Work</b>
<br>for grouping Releases
+ <td><a href="/work/create">Create</a>
{% if config.FATCAT_DOMAIN == 'fatcat.wiki' %}
- <td>
<td><a href="/work/ftl6xv267vb6xfech3khri3nwa">Paper</a>
{% else %}
- <td>
<td><a href="/work/ftl6xv267vb6xfech3khri3nwa">Paper</a> (prod)
<br><a href="/work/aaaaaaaaaaaaavkvaaaaaaaaai">Dummy</a>
<br><a href="/work/aaaaaaaaaaaaavkvaaaaaaaaam">Realistic</a>
diff --git a/python/fatcat_web/templates/release_create.html b/python/fatcat_web/templates/release_create.html
index 5ec2efe5..4f5dabd7 100644
--- a/python/fatcat_web/templates/release_create.html
+++ b/python/fatcat_web/templates/release_create.html
@@ -5,13 +5,15 @@
<h1 class="ui header">Create New Release Entity</h1>
<form class="ui form" id="create_release_form" method="POST" action="/release/create">
+ <p>Experienced users can also use the <a href="/release/create/toml">TOML
+ creation form</a> to access all metadata fields in a raw format.
{% endblock %}
{% block edit_form_suffix %}
<br><br>
<input class="ui primary submit button" type="submit" value="Create Release!">
<p>
- <i>New entity will be part of the current editgroup, which needs to be
+ <i>New release entity will be part of the current editgroup, which needs to be
submited and approved before the entity will formally be included in the
catalog.</i>
</form>
diff --git a/python/fatcat_web/templates/release_edit.html b/python/fatcat_web/templates/release_edit.html
index a4a7e56f..c26c9850 100644
--- a/python/fatcat_web/templates/release_edit.html
+++ b/python/fatcat_web/templates/release_edit.html
@@ -7,6 +7,14 @@
<h1 class="ui header">Edit Release Entity</h1>
<form class="ui form" id="edit_release_form" method="POST" action="{% if editgroup %}/editgroup/{{ editgroup.editgroup_id }}{% endif %}/release/{{ existing_ident }}/edit">
+
+ <p>Experienced users can also use the <a href="{% if editgroup
+ %}/editgroup/{{ editgroup.editgroup_id }}{% endif %}/release/{{
+ existing_ident }}/edit/toml">TOML editing form</a> to access all metadata
+ fields in a raw format.
+ {% if not editgroup %}
+ You can also <a href="/release/{{ existing_ident }}/delete">delete this entity</a>.
+ {% endif %}
{% endblock %}
<p>See <a href="https://guide.fatcat.wiki/entity_release.html">the catalog
@@ -14,6 +22,7 @@
href="https://guide.fatcat.wiki/editing_quickstart.html">the editing
tutorial</a> if this is your first time making an edit.
+
{{ form.hidden_tag() }}
<h3 class="ui dividing header">Editgroup Metadata</h3>
@@ -21,21 +30,22 @@
<br>
<h3 class="ui dividing header">The Basics</h3>
+
+ {{ edit_macros.form_field_inline(form.release_type, "required") }}
+ {{ edit_macros.form_field_inline(form.title, "required") }}
+ {{ edit_macros.form_field_inline(form.original_title) }}
+
<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_stage) }}
+ {{ edit_macros.form_field_basic(form.release_year) }}
+ {{ edit_macros.form_field_basic(form.release_date) }}
</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;">
@@ -47,6 +57,19 @@
<div class="one wide column" style="padding-bottom: 0px;"></div>
</div>
+ <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_stage) }}
+ {{ edit_macros.form_field_basic(form.withdrawn_status) }}
+ </div>
+ </div>
+ <div class="one wide column" style="padding-bottom: 0px;"></div>
+ </div>
+
+ {{ edit_macros.form_field_inline(form.work_id) }}
+
<br>
<h3 class="ui dividing header">Contributors</h3>
<div class="list-group" id="contrib_list" name="contrib_list">