summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBryan Newbold <bnewbold@robocracy.org>2020-07-30 20:35:32 -0700
committerBryan Newbold <bnewbold@robocracy.org>2020-07-31 12:13:06 -0700
commitb7aed96075187af72b510c55e762d1e13c0a5306 (patch)
treea9a70216fe820278ef0e90d83fa92c079c9d876d
parent29593e492b632c8884c31993230d258d646e3d8c (diff)
downloadfatcat-b7aed96075187af72b510c55e762d1e13c0a5306.tar.gz
fatcat-b7aed96075187af72b510c55e762d1e13c0a5306.zip
implement webface entity deletion
-rw-r--r--python/fatcat_web/editing_routes.py229
-rw-r--r--python/fatcat_web/templates/entity_delete.html49
-rw-r--r--python/tests/web_editing.py57
3 files changed, 308 insertions, 27 deletions
diff --git a/python/fatcat_web/editing_routes.py b/python/fatcat_web/editing_routes.py
index e84b14f7..8e3b03b0 100644
--- a/python/fatcat_web/editing_routes.py
+++ b/python/fatcat_web/editing_routes.py
@@ -1,4 +1,6 @@
+from typing import Optional
+
from flask import render_template, abort, redirect, session, flash
from flask_login import login_required
@@ -63,6 +65,28 @@ def generic_entity_delete_edit(user_api, entity_type: str, editgroup_id: str, ed
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)
@@ -359,6 +383,87 @@ def generic_entity_toml_edit(editgroup_id, entity_type, existing_ident, edit_tem
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
@@ -390,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():
@@ -410,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():
@@ -430,35 +597,43 @@ 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('/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>/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>/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('/work/<ident>/delete', methods=['GET', 'POST'])
+@login_required
+def work_delete_view(ident):
+ return generic_entity_delete(None, 'work', ident)
@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'])
@@ -468,7 +643,7 @@ def container_create_toml_view():
@app.route('/container/<ident>/edit/toml', methods=['GET', 'POST'])
@login_required
-def container_edit_toml(ident):
+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'])
@@ -483,7 +658,7 @@ def creator_create_toml_view():
@app.route('/creator/<ident>/edit/toml', methods=['GET', 'POST'])
@login_required
-def creator_edit_toml(ident):
+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'])
@@ -498,7 +673,7 @@ def file_create_toml_view():
@app.route('/file/<ident>/edit/toml', methods=['GET', 'POST'])
@login_required
-def file_edit_toml(ident):
+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'])
@@ -513,7 +688,7 @@ def fileset_create_toml_view():
@app.route('/fileset/<ident>/edit/toml', methods=['GET', 'POST'])
@login_required
-def fileset_edit_toml(ident):
+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'])
@@ -528,7 +703,7 @@ def webcapture_create_toml_view():
@app.route('/webcapture/<ident>/edit/toml', methods=['GET', 'POST'])
@login_required
-def webcapture_edit_toml(ident):
+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'])
@@ -543,7 +718,7 @@ def release_create_toml_view():
@app.route('/release/<ident>/edit/toml', methods=['GET', 'POST'])
@login_required
-def release_edit_toml(ident):
+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'])
@@ -558,7 +733,7 @@ def work_create_toml_view():
@app.route('/work/<ident>/edit/toml', methods=['GET', 'POST'])
@login_required
-def work_edit_toml(ident):
+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'])
@@ -575,7 +750,7 @@ def creator_create_view():
@app.route('/creator/<ident>/edit', methods=['GET'])
@login_required
-def creator_edit(ident):
+def creator_edit_view(ident):
return redirect(f'/creator/{ident}/edit/toml')
@app.route('/editgroup/<editgroup_id>/creator/<ident>/edit', methods=['GET', 'POST'])
@@ -590,7 +765,7 @@ def fileset_create_view():
@app.route('/fileset/<ident>/edit', methods=['GET'])
@login_required
-def fileset_edit(ident):
+def fileset_edit_view(ident):
return redirect(f'/fileset/{ident}/edit/toml')
@app.route('/editgroup/<editgroup_id>/fileset/<ident>/edit', methods=['GET', 'POST'])
@@ -605,7 +780,7 @@ def webcapture_create_view():
@app.route('/webcapture/<ident>/edit', methods=['GET'])
@login_required
-def webcapture_edit(ident):
+def webcapture_edit_view(ident):
return redirect(f'/webcapture/{ident}/edit/toml')
@app.route('/editgroup/<editgroup_id>/webcapture/<ident>/edit', methods=['GET', 'POST'])
@@ -620,7 +795,7 @@ def work_create_view():
@app.route('/work/<ident>/edit', methods=['GET'])
@login_required
-def work_edit(ident):
+def work_edit_view(ident):
return redirect(f'/work/{ident}/edit/toml')
@app.route('/editgroup/<editgroup_id>/work/<ident>/edit', methods=['GET', 'POST'])
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/tests/web_editing.py b/python/tests/web_editing.py
index ea244388..8386badb 100644
--- a/python/tests/web_editing.py
+++ b/python/tests/web_editing.py
@@ -129,6 +129,57 @@ def test_web_file_create(app_admin, api):
follow_redirects=True)
assert rv.status_code == 200
+def test_web_file_toml_create(app_admin, api):
+
+ eg = quick_eg(api)
+
+ # bogus/bad submit
+ rv = app_admin.post('/file/create/toml',
+ data={
+ 'editgroup_id': eg.editgroup_id,
+ },
+ follow_redirects=True)
+ assert rv.status_code == 400
+
+ # ok/valid submit
+ rv = app_admin.post('/file/create/toml',
+ data={
+ 'editgroup_id': eg.editgroup_id,
+ 'toml': """
+size = 12345
+sha1 = "45be56a396c4d03faaa41e055170c23534dec736"
+ """,
+ },
+ follow_redirects=True)
+ assert rv.status_code == 200
+
+ # upper-case SHA-1
+ rv = app_admin.post('/file/create/toml',
+ data={
+ 'editgroup_id': eg.editgroup_id,
+ 'toml': """
+size = 12345
+sha1 = "45BE56A396C4D03FAAA41E055170C23534DEC736"
+ """,
+ },
+ follow_redirects=True)
+ assert rv.status_code == 400
+
+def test_web_file_delete(app_admin, api):
+
+ eg = quick_eg(api)
+
+ rv = app_admin.get('/file/aaaaaaaaaaaaamztaaaaaaaaam/delete')
+ assert rv.status_code == 200
+
+ rv = app_admin.post('/file/aaaaaaaaaaaaamztaaaaaaaaam/delete',
+ data={
+ 'editgroup_id': eg.editgroup_id,
+ },
+ follow_redirects=True)
+ assert rv.status_code == 200
+ # NOTE: did not *accept* the deletion edit
+
DUMMY_DEMO_ENTITIES = {
'container': 'aaaaaaaaaaaaaeiraaaaaaaaam',
'creator': 'aaaaaaaaaaaaaircaaaaaaaaaq',
@@ -186,3 +237,9 @@ def test_web_create_get(app_admin):
rv = app_admin.get(f'/{entity_type}/create/toml')
assert rv.status_code == 200
+
+def test_web_edit_delete(app_admin):
+
+ for entity_type in DUMMY_DEMO_ENTITIES.keys():
+ rv = app_admin.get(f'/{entity_type}/{DUMMY_DEMO_ENTITIES[entity_type]}/delete')
+ assert rv.status_code == 200