diff options
Diffstat (limited to 'python/fatcat_web')
30 files changed, 1615 insertions, 554 deletions
diff --git a/python/fatcat_web/__init__.py b/python/fatcat_web/__init__.py index e80393c2..3a332e84 100644 --- a/python/fatcat_web/__init__.py +++ b/python/fatcat_web/__init__.py @@ -1,8 +1,11 @@ from flask import Flask +from flask.logging import create_logger from flask_uuid import FlaskUUID from flask_debugtoolbar import DebugToolbarExtension from flask_login import LoginManager +from flask_wtf.csrf import CSRFProtect +from flask_misaka import Misaka from authlib.flask.client import OAuth from loginpass import create_flask_blueprint, Gitlab from raven.contrib.flask import Sentry @@ -10,11 +13,22 @@ import fatcat_client from fatcat_web.web_config import Config + toolbar = DebugToolbarExtension() app = Flask(__name__) app.config.from_object(Config) toolbar = DebugToolbarExtension(app) FlaskUUID(app) +app.csrf = CSRFProtect(app) +app.log = create_logger(app) + +# This is the Markdown processor; setting default here +Misaka(app, + autolink=True, + no_intra_emphasis=True, + strikethrough=True, + escape=True, +) login_manager = LoginManager() login_manager.init_app(app) @@ -46,7 +60,7 @@ else: print("No privileged token found") priv_api = None -from fatcat_web import routes, auth, cors +from fatcat_web import routes, editing_routes, auth, cors, forms gitlab_bp = create_flask_blueprint(Gitlab, oauth, auth.handle_oauth) app.register_blueprint(gitlab_bp, url_prefix='/auth/gitlab') diff --git a/python/fatcat_web/auth.py b/python/fatcat_web/auth.py index 1953151b..7f51b970 100644 --- a/python/fatcat_web/auth.py +++ b/python/fatcat_web/auth.py @@ -5,7 +5,7 @@ import pymacaroons from flask import Flask, render_template, send_from_directory, request, \ url_for, abort, g, redirect, jsonify, session, flash from flask_login import logout_user, login_user, UserMixin -from fatcat_web import login_manager, api, priv_api, Config +from fatcat_web import login_manager, app, api, priv_api, Config import fatcat_client def handle_logout(): @@ -20,6 +20,7 @@ def handle_token_login(token): m = pymacaroons.Macaroon.deserialize(token) except pymacaroons.exceptions.MacaroonDeserializationException: # TODO: what kind of Exceptions? + app.log.warn("auth fail: MacaroonDeserializationException") return abort(400) # extract editor_id editor_id = None @@ -28,6 +29,7 @@ def handle_token_login(token): if caveat.startswith(b"editor_id = "): editor_id = caveat[12:].decode('utf-8') if not editor_id: + app.log.warn("auth fail: editor_id missing in macaroon") abort(400) # fetch editor info editor = api.get_editor(editor_id) @@ -93,12 +95,11 @@ def handle_ia_xauth(email, password): try: flash("Internet Archive email/password didn't match: {}".format(resp.json()['values']['reason'])) except: - print("IA XAuth fail: {}".format(resp.content)) + app.log.warn("IA XAuth fail: {}".format(resp.content)) return render_template('auth_ia_login.html', email=email), resp.status_code elif resp.status_code != 200: flash("Internet Archive login failed (internal error?)") - # TODO: log.warn - print("IA XAuth fail: {}".format(resp.content)) + app.log.warn("IA XAuth fail: {}".format(resp.content)) return render_template('auth_ia_login.html', email=email), resp.status_code # Successful login; now fetch info... @@ -112,8 +113,7 @@ def handle_ia_xauth(email, password): }) if resp.status_code != 200: flash("Internet Archive login failed (internal error?)") - # TODO: log.warn - print("IA XAuth fail: {}".format(resp.content)) + app.log.warn("IA XAuth fail: {}".format(resp.content)) return render_template('auth_ia_login.html', email=email), resp.status_code ia_info = resp.json()['values'] @@ -139,5 +139,6 @@ def load_user(editor_id): user.id = editor_id user.editor_id = editor_id user.username = editor['username'] + user.is_admin = editor['is_admin'] user.token = token return user diff --git a/python/fatcat_web/editing_routes.py b/python/fatcat_web/editing_routes.py new file mode 100644 index 00000000..a9b3d326 --- /dev/null +++ b/python/fatcat_web/editing_routes.py @@ -0,0 +1,269 @@ + +import os +import json +from flask import Flask, render_template, send_from_directory, request, \ + url_for, abort, g, redirect, jsonify, session, flash, Response +from flask_login import login_required + +from fatcat_client import Editgroup +from fatcat_client.rest import ApiException +from fatcat_tools.transforms import * +from fatcat_web import app, api, auth_api, priv_api +from fatcat_web.auth import handle_token_login, handle_logout, load_user, handle_ia_xauth +from fatcat_web.cors import crossdomain +from fatcat_web.search import * +from fatcat_web.forms import * + + +### Helper Methods ########################################################## + +def form_editgroup_get_or_create(api, edit_form): + """ + This function expects a submitted, validated + """ + if edit_form.editgroup_id.data: + try: + eg = api.get_editgroup(edit_form.editgroup_id.data) + except ApiException as ae: + if ae.status == 404: + edit_form.editgroup_id.errors.append("Editgroup does not exist") + return None + app.log.warn(ae) + abort(ae.status) + # TODO: check here that editgroup hasn't been merged already + else: + # if no editgroup, create one from description + try: + eg = api.create_editgroup( + Editgroup(description=edit_form.editgroup_description.data or None)) + except ApiException as ae: + app.log.warn(ae) + abort(ae.status) + # set this session editgroup_id + session['active_editgroup_id'] = eg.editgroup_id + flash('Started new editgroup <a href="/editgroup/{}">{}</a>' \ + .format(eg.editgroup_id, eg.editgroup_id)) + return eg + +### Views ################################################################### + +@app.route('/container/create', methods=['GET', 'POST']) +@login_required +def container_create(): + form = ContainerEntityForm() + if form.is_submitted(): + if form.validate_on_submit(): + # API on behalf of user + user_api = auth_api(session['api_token']) + eg = form_editgroup_get_or_create(user_api, form) + if eg: + # no merge or anything hard to do; just create the entity + entity = form.to_entity() + try: + edit = user_api.create_container(entity, editgroup_id=eg.editgroup_id) + except ApiException as ae: + app.log.warn(ae) + abort(ae.status) + # redirect to new entity + return redirect('/container/{}'.format(edit.ident)) + elif form.errors: + app.log.info("form errors (did not validate): {}".format(form.errors)) + else: + editgroup_id = session.get('active_editgroup_id', None) + form.editgroup_id.data = editgroup_id + return render_template('container_create.html', form=form) + +@login_required +@app.route('/container/<ident>/edit', methods=['GET', 'POST']) +def container_edit(ident): + # TODO: prev_rev interlock + try: + entity = api.get_container(ident) + except ApiException as ae: + abort(ae.status) + form = ContainerEntityForm() + if form.is_submitted(): + if form.validate_on_submit(): + # API on behalf of user + user_api = auth_api(session['api_token']) + eg = form_editgroup_get_or_create(user_api, form) + if eg: + # all the tricky logic is in the update method + form.update_entity(entity) + try: + edit = user_api.update_container(entity.ident, entity, + editgroup_id=eg.editgroup_id) + except ApiException as ae: + app.log.warn(ae) + abort(ae.status) + # redirect to entity revision + # TODO: container_rev_view + return redirect('/container/{}'.format(edit.ident)) + elif form.errors: + app.log.info("form errors (did not validate): {}".format(form.errors)) + else: + form = ContainerEntityForm.from_entity(entity) + if not form.is_submitted(): + editgroup_id = session.get('active_editgroup_id', None) + form.editgroup_id.data = editgroup_id + return render_template('container_edit.html', form=form, entity=entity) + +@app.route('/creator/<ident>/edit', methods=['GET']) +def creator_edit(ident): + try: + entity = api.get_creator(ident) + except ApiException as ae: + abort(ae.status) + return render_template('entity_edit.html') + +@app.route('/file/create', methods=['GET', 'POST']) +@login_required +def file_create(): + form = FileEntityForm() + if form.is_submitted(): + if form.validate_on_submit(): + # API on behalf of user + user_api = auth_api(session['api_token']) + eg = form_editgroup_get_or_create(user_api, form) + if eg: + # no merge or anything hard to do; just create the entity + entity = form.to_entity() + try: + edit = user_api.create_file(entity, editgroup_id=eg.editgroup_id) + except ApiException as ae: + app.log.warn(ae) + abort(ae.status) + # redirect to new entity + return redirect('/file/{}'.format(edit.ident)) + elif form.errors: + app.log.info("form errors (did not validate): {}".format(form.errors)) + else: + editgroup_id = session.get('active_editgroup_id', None) + form.editgroup_id.data = editgroup_id + form.urls.append_entry() + form.release_ids.append_entry() + return render_template('file_create.html', + form=form) + +@login_required +@app.route('/file/<ident>/edit', methods=['GET', 'POST']) +def file_edit(ident): + # TODO: prev_rev interlock + try: + entity = api.get_file(ident) + except ApiException as ae: + abort(ae.status) + form = FileEntityForm() + if form.is_submitted(): + if form.validate_on_submit(): + # API on behalf of user + user_api = auth_api(session['api_token']) + eg = form_editgroup_get_or_create(user_api, form) + if eg: + # all the tricky logic is in the update method + form.update_entity(entity) + try: + edit = user_api.update_file(entity.ident, entity, + editgroup_id=eg.editgroup_id) + except ApiException as ae: + app.log.warn(ae) + abort(ae.status) + # redirect to entity revision + # TODO: file_rev_view + return redirect('/file/{}'.format(edit.ident)) + elif form.errors: + app.log.info("form errors (did not validate): {}".format(form.errors)) + else: # not submitted + form = FileEntityForm.from_entity(entity) + editgroup_id = session.get('active_editgroup_id', None) + form.editgroup_id.data = editgroup_id + return render_template('file_edit.html', form=form, entity=entity) + +@app.route('/fileset/<ident>/edit', methods=['GET']) +def fileset_edit(ident): + try: + entity = api.get_fileset(ident) + except ApiException as ae: + abort(ae.status) + return render_template('entity_edit.html') + +@app.route('/webcapture/<ident>/edit', methods=['GET']) +def webcapture_edit(ident): + try: + entity = api.get_webcapture(ident) + except ApiException as ae: + abort(ae.status) + return render_template('entity_edit.html') + +@app.route('/release/create', methods=['GET', 'POST']) +@login_required +def release_create(): + form = ReleaseEntityForm() + if form.is_submitted(): + if form.validate_on_submit(): + # API on behalf of user + user_api = auth_api(session['api_token']) + eg = form_editgroup_get_or_create(user_api, form) + if eg: + # no merge or anything hard to do; just create the entity + entity = form.to_entity() + try: + edit = user_api.create_release(entity, editgroup_id=eg.editgroup_id) + except ApiException as ae: + app.log.warn(ae) + abort(ae.status) + # redirect to new release + return redirect('/release/{}'.format(edit.ident)) + elif form.errors: + app.log.info("form errors (did not validate): {}".format(form.errors)) + else: # not submitted + form.contribs.append_entry() + editgroup_id = session.get('active_editgroup_id', None) + form.editgroup_id.data = editgroup_id + return render_template('release_create.html', form=form) + +@login_required +@app.route('/release/<ident>/edit', methods=['GET', 'POST']) +def release_edit(ident): + # TODO: prev_rev interlock + try: + entity = api.get_release(ident) + except ApiException as ae: + abort(ae.status) + form = ReleaseEntityForm() + if form.is_submitted(): + if form.validate_on_submit(): + # API on behalf of user + user_api = auth_api(session['api_token']) + eg = form_editgroup_get_or_create(user_api, form) + if eg: + # all the tricky logic is in the update method + form.update_entity(entity) + try: + edit = user_api.update_release(entity.ident, entity, + editgroup_id=eg.editgroup_id) + except ApiException as ae: + app.log.warn(ae) + abort(ae.status) + # redirect to entity revision + # TODO: release_rev_view + return redirect('/release/{}'.format(edit.ident)) + elif form.errors: + app.log.info("form errors (did not validate): {}".format(form.errors)) + else: # not submitted + form = ReleaseEntityForm.from_entity(entity) + editgroup_id = session.get('active_editgroup_id', None) + form.editgroup_id.data = editgroup_id + return render_template('release_edit.html', form=form, entity=entity) + +@app.route('/work/create', methods=['GET']) +def work_create_view(): + return abort(404) + +@app.route('/work/<ident>/edit', methods=['GET']) +def work_edit_view(ident): + try: + entity = api.get_work(ident) + except ApiException as ae: + abort(ae.status) + return render_template('entity_edit.html') diff --git a/python/fatcat_web/forms.py b/python/fatcat_web/forms.py new file mode 100644 index 00000000..776812ae --- /dev/null +++ b/python/fatcat_web/forms.py @@ -0,0 +1,315 @@ + +""" +Note: in thoery could use, eg, https://github.com/christabor/swagger_wtforms, +but can't find one that is actually maintained. +""" + +from flask_wtf import FlaskForm +from wtforms import SelectField, DateField, StringField, IntegerField, \ + HiddenField, FormField, FieldList, validators + +from fatcat_client import ContainerEntity, CreatorEntity, FileEntity, \ + ReleaseEntity, ReleaseContrib, FileEntityUrls + +release_type_options = [ + ('', 'Unknown'), + ('article-journal', 'Journal Article'), + ('paper-conference', 'Conference Proceeding'), + ('article', 'Article (non-journal)'), + ('book', 'Book'), + ('chapter', 'Book Chapter'), + ('dataset', 'Dataset'), + ('stub', 'Invalid/Stub'), +] +release_status_options = [ + ('', 'Unknown'), + ('draft', 'Draft'), + ('submitted', 'Submitted'), + ('accepted', 'Accepted'), + ('published', 'Published'), + ('updated', 'Updated'), +] +role_type_options = [ + ('author', 'Author'), + ('editor', 'Editor'), + ('translator', 'Translator'), +] + +class EntityEditForm(FlaskForm): + editgroup_id = StringField('Editgroup ID', + [validators.Optional(True), + validators.Length(min=26, max=26)]) + editgroup_description = StringField('Editgroup Description', + [validators.Optional(True)]) + edit_description = StringField('Description of Changes', + [validators.Optional(True)]) + +class ReleaseContribForm(FlaskForm): + class Meta: + # this is a sub-form, so disable CSRF + csrf = False + + #surname + #given_name + #creator_id (?) + #orcid (for match?) + prev_index = HiddenField('prev_revision index', default=None) + raw_name = StringField('Display Name', + [validators.DataRequired()]) + role = SelectField( + [validators.DataRequired()], + choices=role_type_options, + default='author') + +RELEASE_SIMPLE_ATTRS = ['title', 'original_title', 'work_id', 'container_id', + 'release_type', 'release_status', 'release_date', 'doi', 'wikidata_qid', + 'isbn13', 'pmid', 'pmcid', 'volume', 'issue', 'pages', 'publisher', + 'language', 'license_slug'] + +class ReleaseEntityForm(EntityEditForm): + """ + TODO: + - field types: fatcat id + - date + """ + title = StringField('Title', + [validators.DataRequired()]) + original_title = StringField('Original Title') + work_id = StringField('Work FCID', + [validators.Optional(True), + validators.Length(min=26, max=26)]) + container_id = StringField('Container FCID', + [validators.Optional(True), + validators.Length(min=26, max=26)]) + release_type = SelectField('Release Type', + [validators.DataRequired()], + choices=release_type_options, + default='') + release_status = SelectField(choices=release_status_options) + release_date = DateField('Release Date', + [validators.Optional(True)]) + #release_year + doi = StringField('DOI', + [validators.Regexp('^10\..*\/.*', message="DOI must be valid"), + validators.Optional(True)]) + wikidata_qid = StringField('Wikidata QID') + isbn13 = StringField('ISBN-13') + pmid = StringField('PubMed Id') + pmcid = StringField('PubMed Central Id') + #core_id + #arxiv_id + #jstor_id + volume = StringField('Volume') + issue = StringField('Issue') + pages = StringField('Pages') + publisher = StringField('Publisher (optional)') + language = StringField('Language (code)') + license_slug = StringField('License (slug)') + contribs = FieldList(FormField(ReleaseContribForm)) + #refs + #abstracts + + @staticmethod + def from_entity(re): + """ + Initializes form with values from an existing release entity. + """ + ref = ReleaseEntityForm() + for simple_attr in RELEASE_SIMPLE_ATTRS: + a = getattr(ref, simple_attr) + a.data = getattr(re, simple_attr) + for i, c in enumerate(re.contribs): + rcf = ReleaseContribForm() + rcf.prev_index = i + rcf.role = c.role + rcf.raw_name = c.raw_name + ref.contribs.append_entry(rcf) + return ref + + def to_entity(self): + assert(self.title.data) + entity = ReleaseEntity(title=self.title.data) + self.update_entity(entity) + return entity + + def update_entity(self, re): + """ + Mutates a release entity in place, updating fields with values from + this form. + + Form must be validated *before* calling this function. + """ + for simple_attr in RELEASE_SIMPLE_ATTRS: + a = getattr(self, simple_attr).data + # special case blank strings + if a == '': + a = None + setattr(re, simple_attr, a) + # bunch of complexity here to preserve old contrib metadata (eg, + # affiliation and extra) not included in current forms + # TODO: this may be broken; either way needs tests + if re.contribs: + old_contribs = re.contribs.copy() + re.contribs = [] + else: + old_contribs = [] + re.contribs = [] + for c in self.contribs: + if c.prev_index.data not in ('', None): + rc = old_contribs[int(c.prev_index.data)] + rc.role = c.role.data or None + rc.raw_name = c.raw_name.data or None + else: + rc = ReleaseContrib( + role=c.role.data or None, + raw_name=c.raw_name.data or None, + ) + re.contribs.append(rc) + if self.edit_description.data: + re.edit_extra = dict(description=self.edit_description.data) + +container_type_options = ( + ('journal', 'Journal'), + ('proceedings', 'Conference Proceedings'), + ('blog', 'Blog'), +) + +CONTAINER_SIMPLE_ATTRS = ['name', 'container_type', 'publisher', 'issnl', + 'wikidata_qid'] + +class ContainerEntityForm(EntityEditForm): + name = StringField('Name/Title', + [validators.DataRequired()]) + container_type = SelectField('Container Type', + [validators.Optional(True)], + choices=container_type_options, + default='') + publisher = StringField("Publisher") + issnl = StringField("ISSN-L") + wikidata_qid = StringField('Wikidata QID') + + @staticmethod + def from_entity(re): + """ + Initializes form with values from an existing container entity. + """ + ref = ContainerEntityForm() + for simple_attr in CONTAINER_SIMPLE_ATTRS: + a = getattr(ref, simple_attr) + a.data = getattr(re, simple_attr) + return ref + + def to_entity(self): + assert(self.name.data) + entity = ContainerEntity(name=self.name.data) + self.update_entity(entity) + return entity + + def update_entity(self, ce): + """ + Mutates a container entity in place, updating fields with values from + this form. + + Form must be validated *before* calling this function. + """ + for simple_attr in CONTAINER_SIMPLE_ATTRS: + a = getattr(self, simple_attr).data + # special case blank strings + if a == '': + a = None + setattr(ce, simple_attr, a) + if self.edit_description.data: + ce.edit_extra = dict(description=self.edit_description.data) + +url_rel_options = [ + ('web', 'Public Web'), + ('webarchive', 'Web Archive'), + ('repository', 'Repository'), + ('social', 'Academic Social Network'), + ('publisher', 'Publisher'), + ('dweb', 'Decentralized Web'), +] + +FILE_SIMPLE_ATTRS = ['size', 'md5', 'sha1', 'sha256', 'mimetype'] + +class FileUrlForm(FlaskForm): + class Meta: + # this is a sub-form, so disable CSRF + csrf = False + + url = StringField('Display Name', + [validators.DataRequired(), + validators.URL(require_tld=False)]) + rel = SelectField( + [validators.DataRequired()], + choices=url_rel_options, + default='web') + +class FileEntityForm(EntityEditForm): + size = IntegerField('Size (bytes)', + [validators.DataRequired()]) + # TODO: positive definite + md5 = StringField("MD5", + [validators.Optional(True), + validators.Length(min=32, max=32)]) + sha1 = StringField("SHA-1", + [validators.DataRequired(), + validators.Length(min=40, max=40)]) + sha256 = StringField("SHA-256", + [validators.Optional(True), + validators.Length(min=64, max=64)]) + urls = FieldList(FormField(FileUrlForm)) + mimetype = StringField("Mimetype") + release_ids = FieldList( + StringField("Release FCID", + [validators.DataRequired(), + validators.Length(min=26, max=26)])) + + @staticmethod + def from_entity(fe): + """ + Initializes form with values from an existing file entity. + """ + ref = FileEntityForm() + for simple_attr in FILE_SIMPLE_ATTRS: + a = getattr(ref, simple_attr) + a.data = getattr(fe, simple_attr) + for i, c in enumerate(fe.urls): + ruf = FileUrlForm() + ruf.rel = c.rel + ruf.url = c.url + ref.urls.append_entry(ruf) + for r in fe.release_ids: + ref.release_ids.append_entry(r) + return ref + + def to_entity(self): + assert(self.sha1.data) + entity = FileEntity() + self.update_entity(entity) + return entity + + def update_entity(self, fe): + """ + Mutates in place, updating fields with values from this form. + + Form must be validated *before* calling this function. + """ + for simple_attr in FILE_SIMPLE_ATTRS: + a = getattr(self, simple_attr).data + # special case blank strings + if a == '': + a = None + setattr(fe, simple_attr, a) + fe.urls = [] + for u in self.urls: + fe.urls.append(FileEntityUrls( + rel=u.rel.data or None, + url=u.url.data or None, + )) + fe.release_ids = [] + for ri in self.release_ids: + fe.release_ids.append(ri.data) + if self.edit_description.data: + fe.edit_extra = dict(description=self.edit_description.data) + diff --git a/python/fatcat_web/routes.py b/python/fatcat_web/routes.py index e2c5fc3a..8e07aff0 100644 --- a/python/fatcat_web/routes.py +++ b/python/fatcat_web/routes.py @@ -4,7 +4,9 @@ import json from flask import Flask, render_template, send_from_directory, request, \ url_for, abort, g, redirect, jsonify, session, flash, Response from flask_login import login_required +from flask_wtf.csrf import CSRFError +from fatcat_client import Editgroup, EditgroupAnnotation from fatcat_client.rest import ApiException from fatcat_tools.transforms import * from fatcat_web import app, api, auth_api, priv_api @@ -21,6 +23,7 @@ def container_history(ident): entity = api.get_container(ident) history = api.get_container_history(ident) except ApiException as ae: + app.log.info(ae) abort(ae.status) #print(history) return render_template('entity_history.html', @@ -29,43 +32,6 @@ def container_history(ident): entity=entity, history=history) -@app.route('/container/<ident>/edit', methods=['GET']) -def container_edit_view(ident): - try: - entity = api.get_container(ident) - except ApiException as ae: - abort(ae.status) - return render_template('entity_edit.html') - -#@app.route('/container/<ident>/edit', methods=['POST']) -#def container_edit(ident): -# raise NotImplemented() -# params = dict() -# for k in request.form: -# if k.startswith('container_'): -# params[k[10:]] = request.form[k] -# edit = api.update_container(params=params) -# return redirect("/container/{}".format(edit.ident)) -# # else: -# #return render_template('container_edit.html') - -@app.route('/container/create', methods=['GET']) -@login_required -def container_create_view(): - return render_template('container_create.html') - -@app.route('/container/create', methods=['POST']) -@login_required -def container_create(): - raise NotImplementedError - params = dict() - for k in request.form: - if k.startswith('container_'): - params[k[10:]] = request.form[k] - container = None - #edit = api.create_container(container, params=params) - #return redirect("/container/{}".format(edit.ident)) - @app.route('/container/lookup', methods=['GET']) def container_lookup(): extid = None @@ -93,7 +59,7 @@ def container_view(ident): stats = get_elastic_container_stats(entity.issnl) except Exception as e: stats = None - print(e) + app.log.error(e) else: stats = None @@ -119,14 +85,6 @@ def creator_history(ident): entity=entity, history=history) -@app.route('/creator/<ident>/edit', methods=['GET']) -def creator_edit_view(ident): - try: - entity = api.get_creator(ident) - except ApiException as ae: - abort(ae.status) - return render_template('entity_edit.html') - @app.route('/creator/lookup', methods=['GET']) def creator_lookup(): for key in ('orcid', 'wikidata_qid'): @@ -167,14 +125,6 @@ def file_history(ident): entity=entity, history=history) -@app.route('/file/<ident>/edit', methods=['GET']) -def file_edit_view(ident): - try: - entity = api.get_file(ident) - except ApiException as ae: - abort(ae.status) - return render_template('entity_edit.html') - @app.route('/file/lookup', methods=['GET']) def file_lookup(): for key in ('md5', 'sha1', 'sha256'): @@ -221,14 +171,6 @@ def fileset_history(ident): entity=entity, history=history) -@app.route('/fileset/<ident>/edit', methods=['GET']) -def fileset_edit_view(ident): - try: - entity = api.get_fileset(ident) - except ApiException as ae: - abort(ae.status) - return render_template('entity_edit.html') - @app.route('/fileset/lookup', methods=['GET']) def fileset_lookup(): raise NotImplementedError @@ -266,14 +208,6 @@ def webcapture_history(ident): entity=entity, history=history) -@app.route('/webcapture/<ident>/edit', methods=['GET']) -def webcapture_edit_view(ident): - try: - entity = api.get_webcapture(ident) - except ApiException as ae: - abort(ae.status) - return render_template('entity_edit.html') - @app.route('/webcapture/lookup', methods=['GET']) def webcapture_lookup(): raise NotImplementedError @@ -311,23 +245,6 @@ def release_lookup(): abort(ae.status) return redirect('/release/{}'.format(resp.ident)) -@app.route('/release/create', methods=['GET']) -@login_required -def release_create_view(): - return render_template('release_create.html') - -@app.route('/release/create', methods=['POST']) -@login_required -def release_create(): - raise NotImplementedError - params = dict() - for k in request.form: - if k.startswith('release_'): - params[k[10:]] = request.form[k] - release = None - #edit = api.create_release(release, params=params) - #return redirect("/release/{}".format(edit.ident)) - @app.route('/release/<ident>/history', methods=['GET']) def release_history(ident): try: @@ -341,14 +258,6 @@ def release_history(ident): entity=entity, history=history) -@app.route('/release/<ident>/edit', methods=['GET']) -def release_edit_view(ident): - try: - entity = api.get_release(ident) - except ApiException as ae: - abort(ae.status) - return render_template('entity_edit.html') - @app.route('/release/<ident>', methods=['GET']) def release_view(ident): try: @@ -373,10 +282,6 @@ def release_view(ident): return render_template('release_view.html', release=entity, authors=authors, container=container) -@app.route('/work/create', methods=['GET']) -def work_create_view(): - return abort(404) - @app.route('/work/<ident>/history', methods=['GET']) def work_history(ident): try: @@ -390,14 +295,6 @@ def work_history(ident): entity=entity, history=history) -@app.route('/work/<ident>/edit', methods=['GET']) -def work_edit_view(ident): - try: - entity = api.get_work(ident) - except ApiException as ae: - abort(ae.status) - return render_template('entity_edit.html') - @app.route('/work/<ident>', methods=['GET']) def work_view(ident): try: @@ -414,11 +311,104 @@ def work_view(ident): @app.route('/editgroup/<ident>', methods=['GET']) def editgroup_view(ident): try: - entity = api.get_editgroup(str(ident)) - entity.editor = api.get_editor(entity.editor_id) + eg = api.get_editgroup(str(ident)) + eg.editor = api.get_editor(eg.editor_id) + eg.annotations = api.get_editgroup_annotations(eg.editgroup_id, expand="editors") + except ApiException as ae: + abort(ae.status) + # TODO: idomatic check for login? + auth_to = dict( + submit=False, + accept=False, + annotate=False, + ) + if session.get('editor'): + user = load_user(session['editor']['editor_id']) + auth_to['annotate'] = True + if user.is_admin or user.editor_id == eg.editor_id: + auth_to['submit'] = True + if user.is_admin: + auth_to['accept'] = True + return render_template('editgroup_view.html', editgroup=eg, + auth_to=auth_to) + +@app.route('/editgroup/<ident>/annotation', methods=['POST']) +@login_required +def editgroup_create_annotation(ident): + app.csrf.protect() + comment_markdown = request.form.get('comment_markdown') + if not comment_markdown: + app.log.info("empty comment field") + abort(400) + # on behalf of user... + user_api = auth_api(session['api_token']) + try: + eg = user_api.get_editgroup(str(ident)) + if eg.changelog_index: + flash("Editgroup already accepted") + abort(400) + ega = EditgroupAnnotation( + comment_markdown=comment_markdown, + extra=None, + ) + user_api.create_editgroup_annotation(eg.editgroup_id, ega) + except ApiException as ae: + app.log.info(ae) + abort(ae.status) + return redirect('/editgroup/{}'.format(ident)) + +@app.route('/editgroup/<ident>/accept', methods=['POST']) +@login_required +def editgroup_accept(ident): + app.csrf.protect() + # on behalf of user... + user_api = auth_api(session['api_token']) + try: + eg = user_api.get_editgroup(str(ident)) + if eg.changelog_index: + flash("Editgroup already accepted") + abort(400) + user_api.accept_editgroup(str(ident)) + except ApiException as ae: + app.log.info(ae) + abort(ae.status) + return redirect('/editgroup/{}'.format(ident)) + +@app.route('/editgroup/<ident>/unsubmit', methods=['POST']) +@login_required +def editgroup_unsubmit(ident): + app.csrf.protect() + # on behalf of user... + user_api = auth_api(session['api_token']) + try: + eg = user_api.get_editgroup(str(ident)) + if eg.changelog_index: + flash("Editgroup already accepted") + abort(400) + user_api.update_editgroup(eg.editgroup_id, eg, submit=False) + except ApiException as ae: + app.log.info(ae) + abort(ae.status) + return redirect('/editgroup/{}'.format(ident)) + +@app.route('/editgroup/<ident>/submit', methods=['POST']) +@login_required +def editgroup_submit(ident): + app.csrf.protect() + # on behalf of user... + print("submitting...") + user_api = auth_api(session['api_token']) + try: + eg = user_api.get_editgroup(str(ident)) + if eg.changelog_index: + flash("Editgroup already accepted") + abort(400) + user_api.update_editgroup(eg.editgroup_id, eg, submit=True) except ApiException as ae: + print(ae) + app.log.info(ae) abort(ae.status) - return render_template('editgroup_view.html', editgroup=entity) + return redirect('/editgroup/{}'.format(ident)) @app.route('/editor/<ident>', methods=['GET']) def editor_view(ident): @@ -433,15 +423,29 @@ def editor_editgroups(ident): try: editor = api.get_editor(ident) editgroups = api.get_editor_editgroups(ident, limit=50) + # cheaper than API-side expand? + for eg in editgroups: + eg.editor = editor except ApiException as ae: abort(ae.status) return render_template('editor_editgroups.html', editor=editor, editgroups=editgroups) +@app.route('/editor/<ident>/annotations', methods=['GET']) +def editor_annotations(ident): + try: + editor = api.get_editor(ident) + annotations = api.get_editor_annotations(ident, limit=50) + except ApiException as ae: + abort(ae.status) + return render_template('editor_annotations.html', editor=editor, + annotations=annotations) + @app.route('/changelog', methods=['GET']) def changelog_view(): try: - entries = api.get_changelog(limit=request.args.get('limit')) + #limit = int(request.args.get('limit', 10)) + entries = api.get_changelog() # TODO: expand="editors" except ApiException as ae: abort(ae.status) return render_template('changelog.html', entries=entries) @@ -451,10 +455,21 @@ def changelog_entry_view(index): try: entry = api.get_changelog_entry(int(index)) entry.editgroup.editor = api.get_editor(entry.editgroup.editor_id) + entry.editgroup.annotations = \ + api.get_editgroup_annotations(entry.editgroup_id, expand="editors") except ApiException as ae: abort(ae.status) return render_template('changelog_view.html', entry=entry, editgroup=entry.editgroup) +@app.route('/reviewable', methods=['GET']) +def reviewable_view(): + try: + #limit = int(request.args.get('limit', 10)) + entries = api.get_editgroups_reviewable(expand="editors") + except ApiException as ae: + abort(ae.status) + return render_template('editgroup_reviewable.html', entries=entries) + ### Search ################################################################## @app.route('/release/search', methods=['GET', 'POST']) @@ -501,7 +516,7 @@ def stats_page(): stats = get_elastic_entity_stats() stats.update(get_changelog_stats()) except Exception as ae: - print(ae) + app.log.error(ae) abort(503) return render_template('stats.html', stats=stats) @@ -514,7 +529,7 @@ def stats_json(): stats = get_elastic_entity_stats() stats.update(get_changelog_stats()) except Exception as ae: - print(ae) + app.log.error(ae) abort(503) return jsonify(stats) @@ -524,7 +539,7 @@ def container_issnl_stats(issnl): try: stats = get_elastic_container_stats(issnl) except Exception as ae: - print(ae) + app.log.error(ae) abort(503) return jsonify(stats) @@ -558,6 +573,11 @@ def release_citeproc(ident): else: return Response(cite, mimetype="text/plain") +@app.route('/health.json', methods=['GET', 'OPTIONS']) +@crossdomain(origin='*',headers=['access-control-allow-origin','Content-Type']) +def health_json(): + return jsonify({'ok': True}) + ### Auth #################################################################### @@ -588,6 +608,7 @@ def token_login(): @app.route('/auth/change_username', methods=['POST']) @login_required def change_username(): + app.csrf.protect() # show the user a list of login options if not 'username' in request.form: abort(400) @@ -627,6 +648,10 @@ def page_not_found(e): def page_not_authorized(e): return render_template('403.html'), 403 +@app.errorhandler(405) +def page_method_not_allowed(e): + return render_template('405.html'), 405 + @app.errorhandler(400) def page_bad_request(e): return render_template('400.html'), 400 @@ -645,6 +670,10 @@ def page_server_error(e): def page_server_down(e): return render_template('503.html'), 503 +@app.errorhandler(CSRFError) +def page_csrf_error(e): + return render_template('csrf_error.html', reason=e.description), 400 + @app.route('/', methods=['GET']) def page_home(): return render_template('home.html') @@ -672,8 +701,3 @@ def fatcat_photo(): return send_from_directory(os.path.join(app.root_path, 'static'), 'fatcat.jpg', mimetype='image/jpeg') - -@app.route('/health', methods=['GET', 'OPTIONS']) -@crossdomain(origin='*',headers=['access-control-allow-origin','Content-Type']) -def health(): - return jsonify({'ok': True}) diff --git a/python/fatcat_web/templates/405.html b/python/fatcat_web/templates/405.html new file mode 100644 index 00000000..97d21d73 --- /dev/null +++ b/python/fatcat_web/templates/405.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block body %} + +<center> +<div style="font-size: 8em;">405</div> +<div style="font-size: 3em;">Method Not Allowed</div> + +<p>Either we have a bug, or you tried something weird (like making up a URL). + +</center> + +{% endblock %} diff --git a/python/fatcat_web/templates/auth_account.html b/python/fatcat_web/templates/auth_account.html index 57155722..4b1562d7 100644 --- a/python/fatcat_web/templates/auth_account.html +++ b/python/fatcat_web/templates/auth_account.html @@ -1,27 +1,33 @@ {% extends "base.html" %} {% block body %} -<h1>Your Account</h1> +<h1 class="ui header"> + <i class="settings icon"></i> + Account Settings +</h1> <p><b>Username:</b> <code>{{ current_user.username }}</code> <p><b>Editor Id:</b> <code><a href="/editor/{{ current_user.editor_id }}">{{ current_user.editor_id }}</a></code> -<div> -<p>Change username: +<br> +<div class="ui segment"> +<h3 class="ui header">Change Username</h3> <form class="" role="change_username" action="/auth/change_username" method="post"> + <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> <div class="ui form"> - <div class="ui action input medium fluid"> + <div class="ui action input medium"> <input type="text" name="username" value="{{ current_user.username }}" aria-label="account username"> - <button class="ui button">Update</button> + <button class="ui red button">Update</button> </div> </div> </form> </div> -<p>In the future, you might be able to... +<br> +<p>In the future, you will be able to... <ul> - <li>Create a bot user - <li>Generate an API token + <li>Create and manage bot accounts + <li>Generate API tokens </ul> {% endblock %} diff --git a/python/fatcat_web/templates/base.html b/python/fatcat_web/templates/base.html index d3353ca4..dda905e0 100644 --- a/python/fatcat_web/templates/base.html +++ b/python/fatcat_web/templates/base.html @@ -59,10 +59,10 @@ <div class="ui simple dropdown item"> {{ current_user.username }} <i class="dropdown icon"></i> <div class="menu"> - <a class="item" href="#"><i class="edit icon"></i>Edits in Progress</a> - <a class="item" href="/editor/{{ current_user.editor_id }}/changelog"><i class="history icon"></i>History</a> + <a class="item" href="/editor/{{ current_user.editor_id }}/editgroups"><i class="history icon"></i>Edit History</a> + <a class="item" href="/editor/{{ current_user.editor_id }}/annotations"><i class="edit icon"></i>Comment History</a> <div class="divider"></div> - <a class="item" href="/auth/account"><i class="user icon"></i>Account</a> + <a class="item" href="/auth/account"><i class="settings icon"></i>Account</a> <a class="item" href="/auth/logout"><i class="sign out icon"></i>Logout</a> </div> </div> @@ -84,7 +84,7 @@ <div class="header">Now Hear This...</div> <ul class="list"> {% for message in messages %} - <li>{{ message }} + <li>{{ message|safe }} {% endfor %} </ul> </div> diff --git a/python/fatcat_web/templates/changelog.html b/python/fatcat_web/templates/changelog.html index f33fe7c8..7d5505bc 100644 --- a/python/fatcat_web/templates/changelog.html +++ b/python/fatcat_web/templates/changelog.html @@ -4,20 +4,31 @@ <h1 class="ui header">Recent Changes <div class="sub header"><code>changelog</code></div></h1> -Limited to the most recent ~50 entries. +Limited to the most recent entries. <table class="ui table"> <thead><tr><th>Changelog<br>Index - <th>Timestamp (UTC) <th>Editgroup - <th>Editor <th>Description <tbody> {% for entry in entries %} - <tr><td><a href="/changelog/{{ entry.index }}">{{ entry.index }}</a> - <td>{{ entry.timestamp }} - <td><a href="/editgroup/{{ entry.editgroup_id }}">{{ entry.editgroup_id }}</a> - <td><a href="/editor/{{ entry.editgroup.editor_id }}">{{ entry.editgroup.editor_id }}</a> + <tr><td><a href="/changelog/{{ entry.index }}">#{{ entry.index }}</a> + <br>{{ entry.timestamp.strftime("%Y-%m-%d %H:%M:%S") }} + <td> + {# + {% if entry.editgroup.editor.is_bot %} + <i class="icon bug"></i> + {% else %} + <i class="icon user"></i> + {% endif %} + #} + Editor: <code><a href="/editor/{{ entry.editgroup.editor_id }}"> + {{ entry.editgroup.editor_id[:8] }}...</a> + </a></code> + <br> + <small><code><a href="/editgroup/{{ entry.editgroup.editgroup_id }}"> + {{ entry.editgroup.editgroup_id }} + </a></code></small> <td>{% if entry.editgroup.description != None %}{{ entry.editgroup.description }}{% endif %} {% endfor %} </table> diff --git a/python/fatcat_web/templates/changelog_view.html b/python/fatcat_web/templates/changelog_view.html index 1a758559..8c4684d5 100644 --- a/python/fatcat_web/templates/changelog_view.html +++ b/python/fatcat_web/templates/changelog_view.html @@ -7,7 +7,7 @@ </div> </h1> -<br><b>Timestamp:</b> {{ entry.timestamp }} +<br><b>Timestamp:</b> {{ entry.timestamp.strftime("%Y-%m-%d %H:%M:%S") }} <br><b><a href="/editgroup/{{editgroup.editgroup_id}}">Editgroup</a></b> <br> {% endblock %} diff --git a/python/fatcat_web/templates/container_create.html b/python/fatcat_web/templates/container_create.html index 15288142..5dde37bf 100644 --- a/python/fatcat_web/templates/container_create.html +++ b/python/fatcat_web/templates/container_create.html @@ -1,7 +1,8 @@ -{% extends "base.html" %} -{% block body %} +{% extends "container_edit.html" %} + +{% block edit_form_prefix %} <div class="ui segment"> -<h1 class="ui header">Adding a New Container</h1> +<h1 class="ui header">Create New Container Entity</h1> <p>A "container" is a anything that groups publications together. For example, a journal (eg, "New England Journal of Medicine"), conference proceedings, a @@ -9,160 +10,18 @@ book series, or a blog. <p>Not all publications are in a container. -<form class="ui form" id="add_container_form" method="post" action="/container/create"> - - <h3 class="ui dividing header">The Basics</h3> - - <div class="ui huge field required"> - <label>Name or Title</label> - <input name="container_name" type="text" placeholder="Title of Container (in English)"> - </div> - - <div class="ui field required"> - <label>Type of Container</label> - <select class="ui dropdown" id="container_type"> - <option value="">Primary Type</option> - <option value="journal">Journal</option> - <option value="book-series">Book Series</option> - <option value="conference">Conference Proceedings</option> - <option value="blog">Blog</option> - <option value="other">Other</option> - </select> - </div> - - <!-- Publisher --> - <div class="ui huge field required"> - <label>Name of Publisher</label> - <input name="container_publisher" type="text" placeholder="Name of Publisher"> - </div> - - <!-- Identifier --> - <div class="ui huge field required"> - <label>ISSN Number</label> - <input name="container_issn" type="text" placeholder="eg, 1234-567X"> - </div> - - <!-- Primary/Original Language --> - <div class="field"> - <label>Primary Language</label> - <select class="ui search select dropdown" id="language-select"> - <option value="">Select if Appropriate</option> - <option value="en">English</option> - <option value="es">Spanish</option> - <option value="">Russian</option> - <option value="">Thai</option> - <option value="">Indonesian</option> - <option value="">Chinese</option> - </select> - </div> - - <!-- Subject / Categorization / Tags --> - <div class="field"> - <label>Subject</label> - <select multiple="" class="ui dropdown" id="subjects"> - <option value="">Select Subject/Tags</option> - <option value="AF">Natural Sciences</option> - <option value="AX">Humanities</option> - <option value="AL">Arts</option> - <option value="AL">Engineering</option> - <option value="AL">Other</option> - </select> - </div> - - <!-- Date --> - <!-- Container / Part-Of --> - <!-- Region --> - <!-- Anything Else? --> - <h3 class="ui dividing header">Anything Else?</h3> - -<div class="ui submit button">Create container</div> - -<p><i>Entity will be created as part of the current edit group, which needs to be -submited and approved before the entity will formally be included in the -catalog.</i> +<form class="ui form" id="create_container_form" method="POST" action="/container/create"> +{% 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 + submited and approved before the entity will formally be included in the + catalog.</i> </form> - </div> {% endblock %} -{% block postscript %} -<script> -<!-- Form validation code --> -$(document).ready(function() { - - $('#add_container_form') - .form({ - fields: { - name: { - identifier: 'name', - rules: [ - { - type : 'empty', - prompt : 'Please enter your name' - } - ] - }, - skills: { - identifier: 'skills', - rules: [ - { - type : 'minCount[2]', - prompt : 'Please select at least two skills' - } - ] - }, - gender: { - identifier: 'gender', - rules: [ - { - type : 'empty', - prompt : 'Please select a gender' - } - ] - }, - username: { - identifier: 'username', - rules: [ - { - type : 'empty', - prompt : 'Please enter a username' - } - ] - }, - password: { - identifier: 'password', - rules: [ - { - type : 'empty', - prompt : 'Please enter a password' - }, - { - type : 'minLength[6]', - prompt : 'Your password must be at least {ruleValue} characters' - } - ] - }, - terms: { - identifier: 'terms', - rules: [ - { - type : 'checked', - prompt : 'You must agree to the terms and conditions' - } - ] - } - } - }) - ; - - $('#container_type').dropdown(); - $('#subjects').dropdown(); - $('#language-select').dropdown(); - - console.log("Page loaded"); - -}); -</script> -{% endblock %} diff --git a/python/fatcat_web/templates/container_edit.html b/python/fatcat_web/templates/container_edit.html new file mode 100644 index 00000000..2a3f6f5f --- /dev/null +++ b/python/fatcat_web/templates/container_edit.html @@ -0,0 +1,50 @@ +{% 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 Container Entity</h1> + +<form class="ui form" id="edit_container_form" method="POST" action="/container/{{ entity.ident }}/edit"> +{% endblock %} + {{ form.hidden_tag() }} + + <br> + {{ edit_macros.editgroup_dropdown(form) }} + + <h3 class="ui dividing header">The Basics</h3> + <br> + {{ edit_macros.form_field_inline(form.container_type, "required") }} + {{ edit_macros.form_field_inline(form.name, "required") }} + {{ edit_macros.form_field_inline(form.publisher) }} + {{ edit_macros.form_field_inline(form.issnl) }} + {{ edit_macros.form_field_inline(form.wikidata_qid) }} + + <br> + <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 Container!"> + <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> +<!-- Form code --> +$(document).ready(function() { + + $('.ui.accordion').accordion(); + +}); +</script> +{% endblock %} + diff --git a/python/fatcat_web/templates/container_view.html b/python/fatcat_web/templates/container_view.html index 1e9a524b..3d0627ca 100644 --- a/python/fatcat_web/templates/container_view.html +++ b/python/fatcat_web/templates/container_view.html @@ -68,9 +68,10 @@ {% if container.extra != None and container.extra.ISSNe != None and (container.extra.ISSNe|length > 0) %} <br><i class="icon plug"></i>Electronic: <code>{{ container.extra.ISSNe }}</code> {% endif %} + <br> {% endif %} {% if container.wikidata_qid != None %} - <br><b>Wikidata:</b> <a href="https://wikidata.org/wiki/{{ container.wikidata_qid }}"><code>{{ container.wikidata_qid }}</code></a> + <b>Wikidata</b> <a href="https://wikidata.org/wiki/{{ container.wikidata_qid }}"><code>{{ container.wikidata_qid }}</code></a> {% endif %} </div><div class="ui segment attached"> {% endif %} diff --git a/python/fatcat_web/templates/csrf_error.html b/python/fatcat_web/templates/csrf_error.html new file mode 100644 index 00000000..357f9047 --- /dev/null +++ b/python/fatcat_web/templates/csrf_error.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} +{% block body %} + +<center> +<div style="font-size: 8em;">400</div> +<div style="font-size: 3em;">Cross-Site Scripting Error</div> +{{ reason }} +</center> + +{% endblock %} diff --git a/python/fatcat_web/templates/edit_macros.html b/python/fatcat_web/templates/edit_macros.html new file mode 100644 index 00000000..9bd14596 --- /dev/null +++ b/python/fatcat_web/templates/edit_macros.html @@ -0,0 +1,56 @@ + +{% macro form_field_errors(field) -%} + {% if field.errors %} + <div class="ui pointing red label"> + {% for err in field.errors %} + {{ err }} + {% endfor %} + </div> + {% endif %} +{%- endmacro %} + +{% macro form_field_basic(field, div_classes="") -%} +<div class="field {{ div_classes }} {% if field.errors %}error{% endif %}"> + {{ field.label }} + {{ field() }} + {{ form_field_errors(field) }} +</div> +{%- endmacro %} + +{% macro form_field_inline(field, div_classes="") -%} +<div class="ui grid"> + <div class="three wide column middle aligned right aligned" {# style="padding-right: 0.5rem;" #}> + <div class="field inline {{ div_classes }} {% if field.errors %}error{% endif %}"> + {{ field.label }} + </div> + </div> + <div class="twelve wide column" {# style="padding-left: 0.5rem;" #}> + <div class="field {{ div_classes }} {% if field.errors %}error{% endif %}"> + {{ field() }} + {{ form_field_errors(field) }} + </div> + </div> + <div class="one wide column"> + </div> +</div> +{%- endmacro %} + +{% macro editgroup_dropdown(form) -%} + <div class="ui accordion"> + <div class="{% if not editgroup_id %}active{% endif %} title"> + <h3><i class="dropdown icon"></i>Editgroup Meta</h3> + </div> + <div class="{% if not editgroup_id %}active{% endif %} content"> + {% if editgroup_id %} + <p>You have an editgroup in progress, and this edit will be included by + default. You can override this below. + {% else %} + <p>No existing editgroup is in progress (or at least, not is selected). + An existing ID can be pasted in, or if you leave that blank but give a + description, a new editgroup will be created for this edit. + {% endif %} + {{ form_field_inline(form.editgroup_id) }} + {{ form_field_inline(form.editgroup_description) }} + </div> + </div> +{%- endmacro %} diff --git a/python/fatcat_web/templates/editgroup_reviewable.html b/python/fatcat_web/templates/editgroup_reviewable.html new file mode 100644 index 00000000..b1ece6af --- /dev/null +++ b/python/fatcat_web/templates/editgroup_reviewable.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} +{% block body %} + +<h1 class="ui header">Reviewable Editgroups +</h1> + +Limited to the most recent entries. + +<table class="ui table"> + <thead><tr><th>Editgroup + <th>Description + <tbody> + {% for editgroup in entries %} + <tr><td> + {% if editgroup.editor.is_bot %} + <i class="icon bug"></i> + {% else %} + <i class="icon user"></i> + {% endif %} + <a href="/editor/{{ editgroup.editor_id }}">{{ editgroup.editor.username }}</a> + <br> + Submitted: {{ editgroup.submitted.strftime("%Y-%m-%d %H:%M:%S") }} + <br> + <small><code><a href="/editgroup/{{ editgroup.editgroup_id }}"> + {{ editgroup.editgroup_id }} + </a></code></small> + <td>{% if editgroup.description != None %}{{ editgroup.description }}{% endif %} + {% endfor %} +</table> + +{% endblock %} diff --git a/python/fatcat_web/templates/editgroup_view.html b/python/fatcat_web/templates/editgroup_view.html index 2341f06a..f7f3ad45 100644 --- a/python/fatcat_web/templates/editgroup_view.html +++ b/python/fatcat_web/templates/editgroup_view.html @@ -1,4 +1,6 @@ {% extends "base.html" %} +{% import "entity_macros.html" as entity_macros %} + {% block body %} {% macro edit_list(edits, entity_type, entity_name) -%} @@ -35,13 +37,60 @@ {# extended by changelog_entry #} {% block editgroupheader %} -<h1 class="ui header">Edit Group +{% if not editgroup.changelog_index %} + <div class="ui right floated center aligned segment"> + {% if auth_to.accept %} + <form id="submit_editgroup_form" method="POST" action="/editgroup/{{ editgroup.editgroup_id }}/accept"> + <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> + <button class="ui orange button">Accept Edits</button> + </form><br> + {% endif %} + {% if auth_to.submit %} + {% if editgroup.submitted %} + <form id="submit_editgroup_form" method="POST" action="/editgroup/{{ editgroup.editgroup_id }}/unsubmit"> + <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> + <button class="ui button">Un-Submit</button> + </form><br> + <form id="submit_editgroup_form" method="POST" action="/editgroup/{{ editgroup.editgroup_id }}/submit"> + <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> + <button class="ui button">Re-Submit</button> + </form> + {% else %} + <form id="submit_editgroup_form" method="POST" action="/editgroup/{{ editgroup.editgroup_id }}/submit"> + <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> + <button class="ui primary button">Submit</button> + </form> + {% endif %} + {% endif %} + </div> +{% endif %} + +<h1 class="ui header">Editgroup <div class="sub header"><code>editgroup {{ editgroup.editgroup_id }}</code></div></h1> {% endblock %} -<b>Editor:</b> <a href="/editor/{{editgroup.editor_id}}">{{ editgroup.editor.username }}</a> -<br><b>Description:</b> {{ editgroup.description }} -<br><br> +<p><b>What is an editgroup?</b> +An editgroup is a set of entity edits, bundled together into a coherent, +reviewable bundle. +<br> + +<br><b>Status:</b> +{% if editgroup.changelog_index %} + Merged (<a href="/changelog/{{ editgroup.changelog_index }}">Changelog #{{ editgroup.changelog_index }}</a>) +{% elif editgroup.submitted %} + Submitted ({{ editgroup.submitted.strftime("%Y-%m-%d %H:%M:%S") }}) +{% else %} + Not Submitted +{% endif %} + +<br><b>Editor:</b> <a href="/editor/{{editgroup.editor_id}}">{{ editgroup.editor.username }}</a> +<br><b>Description:</b> +{% if editgroup.description %} + {{ editgroup.description }} +{% else %} + <i>none</i> +{% endif %} +<br><br clear="all"> <div class="ui styled fluid accordion"> {{ edit_list(editgroup.edits.works, "work", "Work") }} @@ -53,11 +102,51 @@ {{ edit_list(editgroup.edits.webcaptures, "webcapture", "Web Capture") }} </div> - <br> -<p><b>What is an editgroup?</b> -An editgroup is a set of entity edits, bundled together into a coherent, -reviewable bundle. +<h2 class="ui header">Comments and Annotations</h2> +{% for annotation in editgroup.annotations|reverse %} + <div class="ui segments"> + <div class="ui top attached secondary segment"> + {% if annotation.editor.is_bot %} + <i class="icon bug"></i> + {% else %} + <i class="icon user"></i> + {% endif %} + <b><a href="/editor/{{ annotation.editor_id }}">{{ annotation.editor.username}}</a></b> + {% if annotation.editor.is_admin %} + <span class="ui tiny olive label">Admin</span> + {% endif %} + at {{ annotation.created.strftime("%Y-%m-%d %H:%M:%S") }} + </div> + {% if annotation.extra %} + <div class="ui attached segment"> + {{ entity_macros.extra_metadata(annotation.extra) }} + </div> + {% endif %} + <div class="ui bottom attached segment"> + {{ annotation.comment_markdown|markdown(escape=True) }} + </div> + </div> +{% else %} + <i>None!</i> +{% endfor %} + +{% if not editgroup.changelog_index and auth_to.annotate %} + <div class="ui segment"> + <h3 class="ui header">Add Comment</h3> + <form class="ui form" id="submit_editgroup_annotation_form" method="POST" action="/editgroup/{{ editgroup.editgroup_id }}/annotation"> + <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> + <div class="field"> + <textarea rows="2" name="comment_markdown" required type="text" value=""></textarea> + </div> + <i>Markdown is allowed</i> + <button class="ui right floated primary button"> + <i class="icon edit"></i> Submit + </button> + <br> + </form><br> + </div> +{% endif %} {% endblock %} diff --git a/python/fatcat_web/templates/editor_annotations.html b/python/fatcat_web/templates/editor_annotations.html new file mode 100644 index 00000000..c46039f5 --- /dev/null +++ b/python/fatcat_web/templates/editor_annotations.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% block body %} + +<h1 class="ui header">Comments and Annotations +<div class="sub header"> + <code>editor + <a href="/editor/{{editor.editor_id}}">{{ editor.username }}</a> + </code> + </a> +</div> +</h1> + +<br> +{% for annotation in annotations %} + <div class="ui segments"> + <div class="ui top attached secondary segment"> + On <b><small><code><a href="/editgroup/{{ annotation.editgroup_id }}"> + {{ annotation.editgroup_id }} + </a></code></small></b> + at {{ annotation.created.strftime("%Y-%m-%d %H:%M:%S") }} + </div> + {% if annotation.extra %} + <div class="ui attached segment"> + {{ entity_macros.extra_metadata(annotation.extra) }} + </div> + {% endif %} + <div class="ui bottom attached segment"> + {{ annotation.comment_markdown|markdown(escape=True) }} + </div> + </div> +{% else %} + <i>None!</i> +{% endfor %} + +{% endblock %} diff --git a/python/fatcat_web/templates/editor_editgroups.html b/python/fatcat_web/templates/editor_editgroups.html index cb9b2f56..3c3dd20d 100644 --- a/python/fatcat_web/templates/editor_editgroups.html +++ b/python/fatcat_web/templates/editor_editgroups.html @@ -1,25 +1,35 @@ {% extends "base.html" %} {% block body %} -<h1 class="ui header">Editor Changelog: {{ editor.username }} +<h1 class="ui header">Edit History <div class="sub header"> - <a href="/editor/{{editor.editor_id}}"> - <code>editor {{ editor.editor_id }}</code> + <code>editor + <a href="/editor/{{editor.editor_id}}">{{ editor.username }}</a> + </code> </a> </div> </h1> -<p>Changes accepted (aka, merged editgroups): <table class="ui table"> - <thead><tr><th>Created (UTC) + <thead><tr>{# <th>Created (UTC) #} + <th>Status <th>Editgroup - <th>Editor <th>Description <tbody> {% for editgroup in editgroups %} - <tr><td>{{ editgroup.created }} - <td><code><a href="/editgroup/{{ editgroup.editgroup_id }}">{{ editgroup.editgroup_id }}</a></code> - <td><code><a href="/editor/{{ editgroup.editor_id }}">{{ editgroup.editor_id }}</a></code> + <tr>{# <td>{{ editgroup.created.strftime("%Y-%m-%d %H:%M:%S") }} #} + <td>{% if editgroup.changelog_index %} + Merged + <br><a href="/changelog/{{ editgroup.changelog_index }}">#{{ editgroup.changelog_index }}</a> + {% elif editgroup.submitted %} + Submitted + <br>{{ editgroup.submitted.strftime("%Y-%m-%d %H:%M:%S") }} + {% else %} + Work in Progress + {% endif %} + <td><small><code><a href="/editgroup/{{ editgroup.editgroup_id }}"> + {{ editgroup.editgroup_id }} + </a></code></small> <td>{% if editgroup.description != None %}{{ editgroup.description }}{% endif %} {% endfor %} </table> diff --git a/python/fatcat_web/templates/editor_view.html b/python/fatcat_web/templates/editor_view.html index eef4f040..c62f8d93 100644 --- a/python/fatcat_web/templates/editor_view.html +++ b/python/fatcat_web/templates/editor_view.html @@ -7,6 +7,7 @@ </div> </h1> -<p><b><a href="/editor/{{ editor.editor_id }}/changelog">View editor's changelog</a></b> +<p><b><a href="/editor/{{ editor.editor_id }}/editgroups">Edit History</a></b> +<p><b><a href="/editor/{{ editor.editor_id }}/annotations">Comments and Annotation History</a></b> {% endblock %} diff --git a/python/fatcat_web/templates/file_create.html b/python/fatcat_web/templates/file_create.html new file mode 100644 index 00000000..a7c99b96 --- /dev/null +++ b/python/fatcat_web/templates/file_create.html @@ -0,0 +1,20 @@ +{% extends "file_edit.html" %} + +{% block edit_form_prefix %} +<div class="ui segment"> +<h1 class="ui header">Create New File Entity</h1> + +<form class="ui form" id="create_file_form" method="POST" action="/file/create"> +{% 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 + submited and approved before the entity will formally be included in the + catalog.</i> +</form> +</div> +{% endblock %} + diff --git a/python/fatcat_web/templates/file_edit.html b/python/fatcat_web/templates/file_edit.html new file mode 100644 index 00000000..279acca9 --- /dev/null +++ b/python/fatcat_web/templates/file_edit.html @@ -0,0 +1,233 @@ +{% 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 File Entity</h1> + +<form class="ui form" id="edit_file_form" method="POST" action="/file/{{ entity.ident }}/edit"> +{% endblock %} + {{ form.hidden_tag() }} + + <br> + {{ edit_macros.editgroup_dropdown(form) }} + + <br> + <h3 class="ui dividing header">File Metadata</h3> + + {{ edit_macros.form_field_inline(form.size, "required") }} + {{ edit_macros.form_field_inline(form.mimetype) }} + {{ edit_macros.form_field_inline(form.md5) }} + {{ edit_macros.form_field_inline(form.sha1, "required") }} + {{ edit_macros.form_field_inline(form.sha256) }} + + <br> + <h3 class="ui dividing header">Locations (URLs)</h3> + <i>Public web (no login/paywall) locations of this exact file (should match + by hashes).</i> + <br><br> + <div class="list-group" id="url_list" name="url_list"> + {% for cform in form.urls %} + <div class="list-group-item ui grid" style="padding-right: 1em;"> + {{ cform.hidden_tag() }} + <div class="one wide column middle aligned center aligned" style="padding-bottom: 0px; padding-right: 0px; padding-left: 0px;"> + <i class="arrows alternate vertical icon"></i> + </div> + <div class="three wide column" style="padding-bottom: 0px; padding-left: 0px;"> + <div class="field {% if cform.rel.errors %}error{% endif %}"> + {{ cform.rel() }} + </div> + </div> + <div class="eleven wide column" style="padding-bottom: 0px;"> + <div class="field {% if cform.url.errors %}error{% endif %}"> + {{ cform.url() }} + </div> + </div> + <div class="one wide column right aligned" style="padding-bottom: 0px; padding-left: 0rem;"> + <button type="button" class="ui icon red button delete-url-button"><i class="trash icon"></i></button> + </div> + </div> + {% endfor %} + </div> + <br> + <button type="button" id="add-url-button" class="ui right floated icon green button" style="margin-right: 0.3rem;"> + <i class="plus icon"></i> + </button> + + <br> + <h3 class="ui dividing header">Releases</h3> + <i>Usually one, but sometimes multiple Release entities (by FCID) that this + file is a fulltext copy of.</i> + <br><br> + <div class="list-group" id="release_id_list" name="release_id_list"> + {% for rfield in form.release_ids %} + <div class="list-group-item ui grid" style="padding-right: 1em;"> + <div class="one wide column middle aligned center aligned" style="padding-bottom: 0px; padding-right: 0px; padding-left: 0px;"> + </div> + <div class="fourteen wide column" style="padding-bottom: 0px;"> + <div class="field {% if rfield.errors %}error{% endif %}"> + {{ rfield() }} + </div> + </div> + <div class="one wide column right aligned" style="padding-bottom: 0px; padding-left: 0rem;"> + <button type="button" class="ui icon red button delete-release_id-button"><i class="trash icon"></i></button> + </div> + </div> + {% endfor %} + </div> + <br> + <button type="button" id="add-release_id-button" class="ui right floated icon green button" style="margin-right: 0.3rem;"> + <i class="plus icon"></i> + </button> + + <br> + <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 File!"> + <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> +<!-- Form code --> +$(document).ready(function() { + + $('.ui.accordion').accordion(); + + var fixup_url_numbering = function(group_item) { + items = Array.from(group_item.querySelectorAll(".list-group-item")) + for (var i = 0; i < items.length; i++) { + var item_el = items[i]; + input_el = item_el.querySelectorAll("input")[0]; + select_el = item_el.querySelectorAll("select")[0]; + //console.log(input_el.id); + //console.log(select_el.id); + input_el.id = "urls-" + i + "-url"; + input_el.name = input_el.id; + select_el.id = "urls-" + i + "-rel"; + select_el.name = select_el.id; + //console.log(input_el.id); + //console.log(select_el.id); + }; + console.log("re-named url rows up to i=" + i); + }; + + var url_list = document.getElementById('url_list'); + fixup_url_numbering(url_list); + + var url_delete_handler = function(ev) { + row = ev.target.parentNode.parentNode; + // I don't understand why this hack is needed; maybe because of the sortable stuff? + if(!row.classList.contains("list-group-item")) { + row = row.parentNode; + } + // console.log(row); + console.assert(row.classList.contains("list-group-item")); + row.parentNode.removeChild(row); + fixup_url_numbering(url_list); + }; + + var attach_url_delete_handler = function(topthing) { + Array.from(topthing.querySelectorAll(".delete-url-button")).forEach((el) => { + el.addEventListener("click", url_delete_handler); + }); + }; + attach_url_delete_handler(document); + + // XXX: really need some way to not duplicate this code from above... + var url_template = ` + <div class="list-group-item ui grid" style="padding-right: 1em;"> + <div class="one wide column middle aligned center aligned" style="padding-bottom: 0px; padding-right: 0px; padding-left: 0px;"> + </div> + <div class="three wide column" style="padding-bottom: 0px; padding-left: 0px;"> + <select id="urls-X-rel" name="urls-X-rel"><option selected value="web">Public Web</option><option value="webarchive">Web Archive</option><option value="repository">Repository</option><option value="social">Academic Social Network</option><option value="publisher">Publisher</option><option value="dweb">Decentralized Web</option></select> + </div> + <div class="eleven wide column" style="padding-bottom: 0px;"> + <input id="urls-X-url" name="urls-X-url" type="text" value=""> + </div> + <div class="one wide column right aligned" style="padding-bottom: 0px; padding-left: 0rem;"> + <button type="button" class="ui icon red button delete-url-button"><i class="trash icon"></i></button> + </div> + </div> + `; + + var add_url_button = document.getElementById("add-url-button"); + add_url_button.addEventListener("click", function(){ + url_list.insertAdjacentHTML('beforeend', url_template); + attach_url_delete_handler(url_list.lastElementChild); + fixup_url_numbering(url_list); + }); + + var fixup_release_id_numbering = function(group_item) { + items = Array.from(group_item.querySelectorAll(".list-group-item")) + for (var i = 0; i < items.length; i++) { + var item_el = items[i]; + input_el = item_el.querySelectorAll("input")[0]; + //console.log(input_el.id); + input_el.id = "release_ids-" + i; + input_el.name = input_el.id; + //console.log(input_el.id); + }; + console.log("re-named release_id rows up to i=" + i); + }; + + var release_id_list = document.getElementById('release_id_list'); + fixup_release_id_numbering(release_id_list); + + var release_id_delete_handler = function(ev) { + row = ev.target.parentNode.parentNode; + // I don't understand why this hack is needed; maybe because of the sortable stuff? + if(!row.classList.contains("list-group-item")) { + row = row.parentNode; + } + // console.log(row); + console.assert(row.classList.contains("list-group-item")); + row.parentNode.removeChild(row); + fixup_release_id_numbering(release_id_list); + }; + + var attach_release_id_delete_handler = function(topthing) { + Array.from(topthing.querySelectorAll(".delete-release_id-button")).forEach((el) => { + el.addEventListener("click", release_id_delete_handler); + }); + }; + attach_release_id_delete_handler(document); + + // XXX: really need some way to not duplicate this code from above... + var release_id_template = ` + <div class="list-group-item ui grid" style="padding-right: 1em;"> + <div class="one wide column middle aligned center aligned" style="padding-bottom: 0px; padding-right: 0px; padding-left: 0px;"> + </div> + <div class="fourteen wide column" style="padding-bottom: 0px;"> + <div class="field "> + <input id="release_ids-X" name="release_ids-X" type="text" value="" required> + </div> + </div> + <div class="one wide column right aligned" style="padding-bottom: 0px; padding-left: 0rem;"> + <button type="button" class="ui icon red button delete-release_id-button"><i class="trash icon"></i></button> + </div> + </div> + `; + + var add_release_id_button = document.getElementById("add-release_id-button"); + add_release_id_button.addEventListener("click", function(){ + release_id_list.insertAdjacentHTML('beforeend', release_id_template); + attach_release_id_delete_handler(release_id_list.lastElementChild); + fixup_release_id_numbering(release_id_list); + }); + + console.log("Page loaded"); + +}); +</script> +{% endblock %} diff --git a/python/fatcat_web/templates/home.html b/python/fatcat_web/templates/home.html index d55cc96b..f4d9d8f6 100644 --- a/python/fatcat_web/templates/home.html +++ b/python/fatcat_web/templates/home.html @@ -55,6 +55,7 @@ indexing (aka, linking together of pre-prints and final copies). <br>published version of a Work {% if config.FATCAT_DOMAIN == 'fatcat.wiki' %} <td><a href="/release/search">Search</a> + <a href="/release/create">Create</a> <td><a href="/release/tb24ghkawzgaho5bkim3cbmbnu">Paper</a> {% else %} <td><a href="/release/search">Search</a> @@ -111,7 +112,7 @@ indexing (aka, linking together of pre-prints and final copies). <td> <td><a href="/file/wklqsb5apzfhbbxxc7rgu2yw6m">PDF</a> {% else %} - <td> + <td><a href="/file/create">Create</a> <td><a href="/file/wklqsb5apzfhbbxxc7rgu2yw6m">PDF</a> (prod) <br><a href="/file/aaaaaaaaaaaaamztaaaaaaaaai">Dummy</a> <br><a href="/file/aaaaaaaaaaaaamztaaaaaaaaam">Realistic</a> diff --git a/python/fatcat_web/templates/release_changelog.html b/python/fatcat_web/templates/release_changelog.html deleted file mode 100644 index 706a5642..00000000 --- a/python/fatcat_web/templates/release_changelog.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends "base.html" %} -{% block body %} - -<h1>Release Changelog: {{ release.id }}</h1> - -<p>release: <a href="/release/{{ release.id }}">{{ release.id }}</a> - -<p>Changelog: -<ul> -{% for entry in changelog_entries %} - <li><a href="/editgroup/{{ entry.editgroup }}">Edit Group #{{ entry.editgroup }}</a> (on {{ entry.timestamp }}) -{% else %} -NONE -{% endfor %} -</ul> - -{% endblock %} diff --git a/python/fatcat_web/templates/release_create.html b/python/fatcat_web/templates/release_create.html index ac8a8169..5ec2efe5 100644 --- a/python/fatcat_web/templates/release_create.html +++ b/python/fatcat_web/templates/release_create.html @@ -1,215 +1,20 @@ -{% extends "base.html" %} -{% block body %} -<div class="ui segment"> -<h1 class="ui header">Adding a New Thing</h1> - -<form class="ui form" id="add_work_form"> - - <h3 class="ui dividing header">The Basics</h3> - - <div class="ui huge field required"> - <label>Title</label> - <input name="work_title" type="text" placeholder="Title of Work (in English)"> - </div> - - <div class="ui field required"> - <label>Type of Work</label> - <select class="ui dropdown" id="work_type"> - <option value="">Primary Type</option> - <option value="journal-article">Journal Article</option> - <option value="book">Book</option> - <option value="book-chapter">Book Chapter</option> - <option value="dataset">Dataset</option> - <option value="dissertation">Thesis or Dissertation</option> - <option value="monograph">Monograph</option> - <option value="proceedings-article">Conference Proceeding</option> - <option value="report">Report</option> - <option value="other">Other</option> - </select> - </div> - - <!-- Primary Creators/Authors --> - <div class="ui field search" id="work_creators"> - <label>Primary Creator(s)</label> - <div class="ui icon input"> - <input class="prompt" type="text" placeholder="Search..."> - <i class="search icon"></i> - </div> - <div class="results"></div> - </div> - - <!-- Description (not an abstract) --> - <div class="ui field"> - <label>Description</label> - <div class="field"> - <label>Not an abstract...</label> - <textarea rows="2"></textarea> - </div> - </div> - - <!-- Primary/Original Language --> - <div class="field"> - <label>Primary Language</label> - <select class="ui search select dropdown" id="language-select"> - <option value="">Select if Appropriate</option> - <option value="en">English</option> - <option value="es">Spanish</option> - </select> - </div> - - <!-- Subject / Categorization / Tags --> - <div class="field"> - <label>Subject</label> - <select multiple="" class="ui dropdown" id="subjects"> - <option value="">Select Subject/Tags</option> - <option value="AF">Afghanistan</option> - <option value="AX">Ă…land Islands</option> - <option value="AL">Albania</option> - <option value="DZ">Algeria</option> - <option value="AS">American Samoa</option> - <option value="AD">Andorra</option> - <option value="AO">Angola</option> - </select> - </div> - - - <h3 class="ui dividing header">Primary Release / Edition</h3> - - <!-- Contributors (and how) --> - <div class="ui field search" id="release_creators"> - <label>Primary Creator(s)</label> - <div class="ui icon input"> - <input class="prompt" type="text" placeholder="Search..."> - <i class="search icon"></i> - </div> - <div class="results"></div> - </div> +{% extends "release_edit.html" %} - <!-- Date --> - <!-- Container / Part-Of --> - <!-- Publisher --> - <!-- Identifier --> - <!-- Language --> - <!-- Type / Media --> - <!-- Issue / Volume / Pages / Chapter --> - - <!-- Anything Else? --> - <h3 class="ui dividing header">Anything Else?</h3> +{% block edit_form_prefix %} +<div class="ui segment"> +<h1 class="ui header">Create New Release Entity</h1> - <!-- File / Copy / URL --> - <!-- Citations --> +<form class="ui form" id="create_release_form" method="POST" action="/release/create"> +{% endblock %} -<div class="ui submit button">Create Work</div> +{% 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 + submited and approved before the entity will formally be included in the + catalog.</i> </form> - </div> {% endblock %} -{% block postscript %} -<script> -<!-- Form validation code --> -$(document).ready(function() { - - $('#add_work_form') - .form({ - fields: { - name: { - identifier: 'name', - rules: [ - { - type : 'empty', - prompt : 'Please enter your name' - } - ] - }, - skills: { - identifier: 'skills', - rules: [ - { - type : 'minCount[2]', - prompt : 'Please select at least two skills' - } - ] - }, - gender: { - identifier: 'gender', - rules: [ - { - type : 'empty', - prompt : 'Please select a gender' - } - ] - }, - username: { - identifier: 'username', - rules: [ - { - type : 'empty', - prompt : 'Please enter a username' - } - ] - }, - password: { - identifier: 'password', - rules: [ - { - type : 'empty', - prompt : 'Please enter a password' - }, - { - type : 'minLength[6]', - prompt : 'Your password must be at least {ruleValue} characters' - } - ] - }, - terms: { - identifier: 'terms', - rules: [ - { - type : 'checked', - prompt : 'You must agree to the terms and conditions' - } - ] - } - } - }) - ; - - var example_authors = [ - { title: 'Andorra' }, - { title: 'United Arab Emirates' }, - { title: 'Afghanistan' }, - { title: 'Antigua' }, - { title: 'Anguilla' }, - { title: 'Albania' }, - { title: 'Armenia' }, - { title: 'Netherlands Antilles' }, - { title: 'Angola' }, - { title: 'Argentina' }, - { title: 'American Samoa' }, - { title: 'Austria' }, - { title: 'Australia' }, - { title: 'Aruba' }, - ]; - - $('#work_creators') - .search({ - source: example_authors - }) - ; - - $('#release_creators') - .search({ - source: example_authors - }) - ; - - $('#work_type').dropdown(); - $('#subjects').dropdown(); - $('#language-select').dropdown(); - - console.log("Page loaded"); - -}); -</script> -{% endblock %} diff --git a/python/fatcat_web/templates/release_edit.html b/python/fatcat_web/templates/release_edit.html new file mode 100644 index 00000000..16c189ab --- /dev/null +++ b/python/fatcat_web/templates/release_edit.html @@ -0,0 +1,218 @@ +{% import "edit_macros.html" as edit_macros %} +{% extends "base.html" %} + +{% block body %} +{% block edit_form_prefix %} +<div class="ui segment"> +<h1 class="ui header">Edit Release Entity</h1> + +<form class="ui form" id="edit_release_form" method="POST" action="/release/{{ entity.ident }}/edit"> +{% endblock %} + {{ form.hidden_tag() }} + + <br> + {{ edit_macros.editgroup_dropdown(form) }} + + <br> + <h3 class="ui dividing header">The Basics</h3> + <div class="ui grid"> + <div class="three wide column" style="padding-bottom: 0px;"></div> + <div class="twelve wide column" style="padding-bottom: 0px;"> + <div class="ui equal width fields"> + {{ edit_macros.form_field_basic(form.release_type, "required") }} + {{ edit_macros.form_field_basic(form.release_status) }} + </div> + </div> + <div class="one wide column" style="padding-bottom: 0px;"></div> + </div> + + {{ edit_macros.form_field_inline(form.title, "required") }} + {{ edit_macros.form_field_inline(form.original_title) }} + {{ edit_macros.form_field_inline(form.work_id) }} + {{ edit_macros.form_field_inline(form.release_date) }} + <div class="ui grid"> + <div class="three wide column" style="padding-bottom: 0px;"></div> + <div class="twelve wide column" style="padding-bottom: 0px;"> + <div class="ui equal width fields"> + {{ edit_macros.form_field_basic(form.language) }} + {{ edit_macros.form_field_basic(form.license_slug) }} + </div> + </div> + <div class="one wide column" style="padding-bottom: 0px;"></div> + </div> + + <br> + <h3 class="ui dividing header">Contributors</h3> + <div class="list-group" id="contrib_list" name="contrib_list"> + {% for cform in form.contribs %} + <div class="list-group-item ui grid" style="padding-right: 1em;"> + {{ cform.hidden_tag() }} + <div class="one wide column middle aligned center aligned sortable-handle" style="padding-bottom: 0px; padding-right: 0px; padding-left: 0px;"> + <i class="arrows alternate vertical icon"></i> + </div> + <div class="three wide column" style="padding-bottom: 0px; padding-left: 0px;"> + <div class="field {% if cform.role.errors %}error{% endif %}"> + {{ cform.role() }} + </div> + </div> + <div class="eleven wide column" style="padding-bottom: 0px;"> + <div class="field {% if cform.raw_name.errors %}error{% endif %}"> + {{ cform.raw_name() }} + </div> + </div> + <div class="one wide column right aligned" style="padding-bottom: 0px; padding-left: 0rem;"> + <button type="button" class="ui icon red button delete-contrib-button"><i class="trash icon"></i></button> + </div> + </div> + {% endfor %} + </div> + <br> + <button type="button" id="add-contrib-button" class="ui right floated icon green button" style="margin-right: 0.3rem;"> + <i class="plus icon"></i> + </button> + <br> + + <br> + <h3 class="ui dividing header">Identifers</h3> + <br> + {{ edit_macros.form_field_inline(form.doi) }} + {{ edit_macros.form_field_inline(form.wikidata_qid) }} + {{ edit_macros.form_field_inline(form.isbn13) }} + <div class="ui grid"> + <div class="three wide column" style="padding-bottom: 0px;"></div> + <div class="twelve wide column" style="padding-bottom: 0px;"> + <div class="ui equal width fields"> + {{ edit_macros.form_field_basic(form.pmid) }} + {{ edit_macros.form_field_basic(form.pmcid) }} + </div> + </div> + <div class="one wide column" style="padding-bottom: 0px;"></div> + </div> + + <br> + <h3 class="ui dividing header">Container</h3> + <br> + {{ edit_macros.form_field_inline(form.container_id) }} + {{ edit_macros.form_field_inline(form.publisher) }} + <br> + <div class="ui grid"> + <div class="three wide column" style="padding-bottom: 0px;"></div> + <div class="twelve wide column" style="padding-bottom: 0px;"> + <div class="ui equal width fields"> + {{ edit_macros.form_field_basic(form.pages) }} + {{ edit_macros.form_field_basic(form.volume) }} + {{ edit_macros.form_field_basic(form.issue) }} + </div> + </div> + <div class="one wide column" style="padding-bottom: 0px;"></div> + </div> + + <br><br> + <h3 class="ui dividing header">Submit</h3> + {{ edit_macros.form_field_basic(form.edit_description) }} + This description will be attached to 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) :( + //$('#release_type').dropdown(); + //$('#release_status').dropdown(); + $('.ui.accordion').accordion(); + + var fixup_contrib_numbering = function(group_item) { + items = Array.from(group_item.querySelectorAll(".list-group-item")) + for (var i = 0; i < items.length; i++) { + var item_el = items[i]; + prev_el = item_el.querySelectorAll("input")[0]; + input_el = item_el.querySelectorAll("input")[1]; + select_el = item_el.querySelectorAll("select")[0]; + //console.log(input_el.id); + //console.log(select_el.id); + input_el.id = "contribs-" + i + "-raw_name"; + input_el.name = input_el.id; + prev_el.id = "contribs-" + i + "-prev_index"; + prev_el.name = prev_el.id; + select_el.id = "contribs-" + i + "-role"; + select_el.name = select_el.id; + //console.log(input_el.id); + //console.log(select_el.id); + }; + console.log("re-named contrib rows up to i=" + i); + }; + + var contrib_list = document.getElementById('contrib_list'); + var contrib_sortable = Sortable.create(contrib_list, { + handle: '.sortable-handle', + animation: 150, + onSort: function(evt) { + fixup_contrib_numbering(contrib_list); + }, + }); + fixup_contrib_numbering(contrib_list); + + var contrib_delete_handler = function(ev) { + row = ev.target.parentNode.parentNode; + // I don't understand why this hack is needed; maybe because of the sortable stuff? + if(!row.classList.contains("list-group-item")) { + row = row.parentNode; + } + // console.log(row); + console.assert(row.classList.contains("list-group-item")); + row.parentNode.removeChild(row); + fixup_contrib_numbering(contrib_list); + }; + + var attach_contrib_delete_handler = function(topthing) { + Array.from(topthing.querySelectorAll(".delete-contrib-button")).forEach((el) => { + el.addEventListener("click", contrib_delete_handler); + }); + }; + attach_contrib_delete_handler(document); + + // XXX: really need some way to not duplicate this code from above... + var contrib_template = ` + <div class="list-group-item ui grid" style="padding-right: 1em;"> + <input id="contribs-X-prev_index" name="contribs-X-prev_index" type="hidden" value=""> + <div class="one wide column middle aligned center aligned sortable-handle" style="padding-bottom: 0px; padding-right: 0px; padding-left: 0px;"> + <i class="arrows alternate vertical icon"></i> + </div> + <div class="three wide column" style="padding-bottom: 0px; padding-left: 0px;"> + <select id="contribs-X-role" name="contribs-X-role"><option value="author">Author</option><option value="editor">Editor</option><option value="translator">Translator</option></select> + </div> + <div class="eleven wide column" style="padding-bottom: 0px;"> + <input id="contribs-X-raw_name" name="contribs-X-raw_name" type="text" value=""> + </div> + <div class="one wide column right aligned" style="padding-bottom: 0px; padding-left: 0rem;"> + <button type="button" class="ui icon red button delete-contrib-button"><i class="trash icon"></i></button> + </div> + </div> + `; + + var add_contrib_button = document.getElementById("add-contrib-button"); + add_contrib_button.addEventListener("click", function(){ + contrib_list.insertAdjacentHTML('beforeend', contrib_template); + attach_contrib_delete_handler(contrib_list.lastElementChild); + fixup_contrib_numbering(contrib_list); + }); + + console.log("Page loaded"); + +}); +</script> +{% endblock %} diff --git a/python/fatcat_web/templates/release_view.html b/python/fatcat_web/templates/release_view.html index d2078d13..fa157193 100644 --- a/python/fatcat_web/templates/release_view.html +++ b/python/fatcat_web/templates/release_view.html @@ -262,7 +262,7 @@ <tbody> {% for webcapture in entity.webcaptures %} <tr><td><b><a href="{{ webcapture.original_url }}">{{ webcapture.original_url }}</a></b> - <br>{{ webcapture.timestamp }} | {{ webcapture.cdx|count }} resources + <br>{{ webcapture.timestamp.strftime("%Y-%m-%d %H:%M:%S") }} | {{ webcapture.cdx|count }} resources <br><small><code><a href="/webcapture/{{ webcapture.ident }}">webcapture:{{ webcapture.ident }}</a></code></small> <td class="single line"> {% for url in webcapture.archive_urls[:5] %} diff --git a/python/fatcat_web/templates/stats.html b/python/fatcat_web/templates/stats.html index f11ca820..d117e1a4 100644 --- a/python/fatcat_web/templates/stats.html +++ b/python/fatcat_web/templates/stats.html @@ -7,7 +7,7 @@ You can also fetch these numbers <a href="./stats.json">as JSON</a>. <h3>Changelog</h3> -<p>Latest changelog index is {{ stats.changelog.latest.index }} ({{ stats.changelog.latest.timestamp[:10] }}). +<p>Latest changelog index is {{ stats.changelog.latest.index }} ({{ stats.changelog.latest.timestamp.strftime("%Y-%m-%d %H:%M:%S") }}). <h3>Entities</h3> diff --git a/python/fatcat_web/templates/webcapture_view.html b/python/fatcat_web/templates/webcapture_view.html index a6c628d9..921d5d48 100644 --- a/python/fatcat_web/templates/webcapture_view.html +++ b/python/fatcat_web/templates/webcapture_view.html @@ -43,7 +43,7 @@ <code><a href="{{ row.url }}">{{ row.url}}</a></code> </div> <div style="margin-left: 1em;"> - {{ row.timestamp }} + {{ row.timestamp.strftime("%Y-%m-%d %H:%M:%S") }} {% if row.mimetype %}| {{ row.mimetype }}{% endif %} <br> <code><small style="color: #666;"> @@ -72,7 +72,7 @@ No known public archive for this webcapture. {% if webcapture.timestamp != None %} <div class="ui segment attached"> - <p><b>Capture Time</b> {{ webcapture.timestamp }} + <p><b>Capture Time</b> {{ webcapture.timestamp.strftime("%Y-%m-%d %H:%M:%S") }} </div> {% endif %} diff --git a/python/fatcat_web/web_config.py b/python/fatcat_web/web_config.py index 8fe50049..b12cb114 100644 --- a/python/fatcat_web/web_config.py +++ b/python/fatcat_web/web_config.py @@ -39,8 +39,18 @@ class Config(object): IA_XAUTH_CLIENT_ID = os.environ.get("IA_XAUTH_CLIENT_ID", default=None) IA_XAUTH_CLIENT_SECRET = os.environ.get("IA_XAUTH_CLIENT_SECRET", default=None) - # protect cookies (which include API tokens) - if FATCAT_DOMAIN != "dev.fatcat.wiki": + # CSRF on by default, but only for WTF forms (not, eg, search, lookups, GET + # forms) + WTF_CSRF_CHECK_DEFAULT = False + WTF_CSRF_TIME_LIMIT = None + + if FATCAT_DOMAIN == "dev.fatcat.wiki": + # "Even more verbose" debug options + #SQLALCHEMY_ECHO = True + #DEBUG = True + pass + else: + # protect cookies (which include API tokens) SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SECURE = True SESSION_COOKIE_SAMESITE = 'Lax' @@ -61,6 +71,3 @@ class Config(object): }, } - # "Even more verbose" debug options - #SQLALCHEMY_ECHO = True - #DEBUG = True |