diff options
-rw-r--r-- | python/fatcat_web/editing_routes.py | 183 | ||||
-rw-r--r-- | python/fatcat_web/forms.py | 28 |
2 files changed, 201 insertions, 10 deletions
diff --git a/python/fatcat_web/editing_routes.py b/python/fatcat_web/editing_routes.py index ffce35f3..9598a754 100644 --- a/python/fatcat_web/editing_routes.py +++ b/python/fatcat_web/editing_routes.py @@ -2,7 +2,7 @@ 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 +13,82 @@ 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_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 @@ -138,14 +214,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,6 +263,102 @@ 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_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: + 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 = 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_edit_delete(editgroup_id, entity_type, edit_id): # fetch editgroup (if set) or 404 editgroup = None diff --git a/python/fatcat_web/forms.py b/python/fatcat_web/forms.py index 15585bf6..b06788b8 100644 --- a/python/fatcat_web/forms.py +++ b/python/fatcat_web/forms.py @@ -4,10 +4,12 @@ Note: in thoery could use, eg, https://github.com/christabor/swagger_wtforms, but can't find one that is actually maintained. """ +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 @@ -413,3 +415,27 @@ 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() + etf.toml.data = entity_to_toml(entity) + return etf |