diff options
-rw-r--r-- | python/fatcat_web/editing_routes.py | 229 | ||||
-rw-r--r-- | python/fatcat_web/templates/entity_delete.html | 49 | ||||
-rw-r--r-- | python/tests/web_editing.py | 57 |
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 |