aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBryan Newbold <bnewbold@robocracy.org>2019-04-04 11:21:41 -0700
committerBryan Newbold <bnewbold@robocracy.org>2019-04-04 11:21:41 -0700
commitc4591cd12298cc03cd96af829a9a007d83d4e537 (patch)
tree1a5e47037bb2294a34841f0a7f7b04da3fccc369
parent005236655dec1cb3f7409724a711a19b52aa9108 (diff)
parentedb9c1b85f367a50957dc0423c3104b900c7e92c (diff)
downloadfatcat-c4591cd12298cc03cd96af829a9a007d83d4e537.tar.gz
fatcat-c4591cd12298cc03cd96af829a9a007d83d4e537.zip
Merge branch 'bnewbold-forms'
-rw-r--r--python/.gitignore2
-rw-r--r--python/Pipfile3
-rw-r--r--python/Pipfile.lock131
-rw-r--r--python/fatcat_web/__init__.py16
-rw-r--r--python/fatcat_web/auth.py13
-rw-r--r--python/fatcat_web/editing_routes.py269
-rw-r--r--python/fatcat_web/forms.py315
-rw-r--r--python/fatcat_web/routes.py262
-rw-r--r--python/fatcat_web/templates/405.html12
-rw-r--r--python/fatcat_web/templates/auth_account.html22
-rw-r--r--python/fatcat_web/templates/base.html8
-rw-r--r--python/fatcat_web/templates/changelog.html25
-rw-r--r--python/fatcat_web/templates/changelog_view.html2
-rw-r--r--python/fatcat_web/templates/container_create.html167
-rw-r--r--python/fatcat_web/templates/container_edit.html50
-rw-r--r--python/fatcat_web/templates/container_view.html3
-rw-r--r--python/fatcat_web/templates/csrf_error.html10
-rw-r--r--python/fatcat_web/templates/edit_macros.html56
-rw-r--r--python/fatcat_web/templates/editgroup_reviewable.html31
-rw-r--r--python/fatcat_web/templates/editgroup_view.html105
-rw-r--r--python/fatcat_web/templates/editor_annotations.html35
-rw-r--r--python/fatcat_web/templates/editor_editgroups.html28
-rw-r--r--python/fatcat_web/templates/editor_view.html3
-rw-r--r--python/fatcat_web/templates/file_create.html20
-rw-r--r--python/fatcat_web/templates/file_edit.html233
-rw-r--r--python/fatcat_web/templates/home.html3
-rw-r--r--python/fatcat_web/templates/release_changelog.html17
-rw-r--r--python/fatcat_web/templates/release_create.html221
-rw-r--r--python/fatcat_web/templates/release_edit.html218
-rw-r--r--python/fatcat_web/templates/release_view.html2
-rw-r--r--python/fatcat_web/templates/stats.html2
-rw-r--r--python/fatcat_web/templates/webcapture_view.html4
-rw-r--r--python/fatcat_web/web_config.py17
-rw-r--r--python/tests/routes.py2
34 files changed, 1702 insertions, 605 deletions
diff --git a/python/.gitignore b/python/.gitignore
index a0bc258b..48b921f1 100644
--- a/python/.gitignore
+++ b/python/.gitignore
@@ -1,9 +1,11 @@
.env
+*.env
codegen-out/
build/
dist/
*.egg-info
*.json.gz
+!example.env
!.coveragerc
!.pylintrc
!.gitignore
diff --git a/python/Pipfile b/python/Pipfile
index e793ad96..321b2d50 100644
--- a/python/Pipfile
+++ b/python/Pipfile
@@ -24,6 +24,9 @@ Flask = ">=1"
flask-uuid = "*"
flask-debugtoolbar = "*"
flask-login = "*"
+flask-wtf = "*"
+Flask-Misaka = "*"
+WTForms = "*"
loginpass = "*"
requests = ">=2"
raven = { extras = ['flask'], version = "*" }
diff --git a/python/Pipfile.lock b/python/Pipfile.lock
index c4271c88..3aaa0c66 100644
--- a/python/Pipfile.lock
+++ b/python/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "9b4f0371d2f2963acb915f4a3be7d46b1fb6923a099b79b2dd5d109c5b976a23"
+ "sha256": "fa764b53404517e2f5e60b61fa8083f0ef50499e9b9b537994720abf85f2aac9"
},
"pipfile-spec": 6,
"requires": {
@@ -172,6 +172,15 @@
"index": "pypi",
"version": "==0.4.1"
},
+ "flask-misaka": {
+ "hashes": [
+ "sha256:bcfdacc0803ccea75d377737e82c83489b2153d922c9d9f9eabc5148d216ed70",
+ "sha256:d0cfb0efd9e5afacda76defd4a605a68390f4fb1bef283c71534fd3ce0d3efb5",
+ "sha256:f423c3beb5502742a57330a272f81d53223f6f99d45cc45b03926e3a3034f589"
+ ],
+ "index": "pypi",
+ "version": "==1.0.0"
+ },
"flask-uuid": {
"hashes": [
"sha256:f9a8196eb896599ba9e74dcf713cfd1aca4669d418c19069e088620ae6294805"
@@ -179,6 +188,14 @@
"index": "pypi",
"version": "==0.2"
},
+ "flask-wtf": {
+ "hashes": [
+ "sha256:5d14d55cfd35f613d99ee7cba0fc3fbbe63ba02f544d349158c14ca15561cc36",
+ "sha256:d9a9e366b32dcbb98ef17228e76be15702cd2600675668bca23f63a7947fd5ac"
+ ],
+ "index": "pypi",
+ "version": "==0.14.2"
+ },
"ftfy": {
"hashes": [
"sha256:84a1614190173bb447ac9d581e50185c6aa35b538754b6bedaba0cc0f83d8e80",
@@ -225,34 +242,34 @@
},
"lxml": {
"hashes": [
- "sha256:0358b9e9642bc7d39aac5cffe9884a99a5ca68e5e2c1b89e570ed60da9139908",
- "sha256:091a359c4dafebbecd3959d9013f1b896b5371859165e4e50b01607a98d9e3e2",
- "sha256:1998e4e60603c64bcc35af61b4331ab3af087457900d3980e18d190e17c3a697",
- "sha256:2000b4088dee9a41f459fddaf6609bba48a435ce6374bb254c5ccdaa8928c5ba",
- "sha256:2afb0064780d8aaf165875be5898c1866766e56175714fa5f9d055433e92d41d",
- "sha256:2d8f1d9334a4e3ff176d096c14ded3100547d73440683567d85b8842a53180bb",
- "sha256:2e38db22f6a3199fd63675e1b4bd795d676d906869047398f29f38ca55cb453a",
- "sha256:3181f84649c1a1ca62b19ddf28436b1b2cb05ae6c7d2628f33872e713994c364",
- "sha256:37462170dfd88af8431d04de6b236e6e9c06cda71e2ca26d88ef2332fd2a5237",
- "sha256:3a9d8521c89bf6f2a929c3d12ad3ad7392c774c327ea809fd08a13be6b3bc05f",
- "sha256:3d0bbd2e1a28b4429f24fd63a122a450ce9edb7a8063d070790092d7343a1aa4",
- "sha256:483d60585ce3ee71929cea70949059f83850fa5e12deb9c094ed1c8c2ec73cbd",
- "sha256:4888be27d5cba55ce94209baef5bcd7bbd7314a3d17021a5fc10000b3a5f737d",
- "sha256:64b0d62e4209170a2a0c404c446ab83b941a0003e96604d2e4f4cb735f8a2254",
- "sha256:68010900898fdf139ac08549c4dba8206c584070a960ffc530aebf0c6f2794ef",
- "sha256:872ecb066de602a0099db98bd9e57f4cfc1d62f6093d94460c787737aa08f39e",
- "sha256:88a32b03f2e4cd0e63f154cac76724709f40b3fc2f30139eb5d6f900521b44ed",
- "sha256:b1dc7683da4e67ab2bebf266afa68098d681ae02ce570f0d1117312273d2b2ac",
- "sha256:b29e27ce9371810250cb1528a771d047a9c7b0f79630dc7dc5815ff828f4273b",
- "sha256:ce197559596370d985f1ce6b7051b52126849d8159040293bf8b98cb2b3e1f78",
- "sha256:d45cf6daaf22584eff2175f48f82c4aa24d8e72a44913c5aff801819bb73d11f",
- "sha256:e2ff9496322b2ce947ba4a7a5eb048158de9d6f3fe9efce29f1e8dd6878561e6",
- "sha256:f7b979518ec1f294a41a707c007d54d0f3b3e1fd15d5b26b7e99b62b10d9a72e",
- "sha256:f9c7268e9d16e34e50f8246c4f24cf7353764affd2bc971f0379514c246e3f6b",
- "sha256:f9c839806089d79de588ee1dde2dae05dc1156d3355dfeb2b51fde84d9c960ad",
- "sha256:ff962953e2389226adc4d355e34a98b0b800984399153c6678f2367b11b4d4b8"
- ],
- "version": "==4.3.2"
+ "sha256:03984196d00670b2ab14ae0ea83d5cc0cfa4f5a42558afa9ab5fa745995328f5",
+ "sha256:0815b0c9f897468de6a386dc15917a0becf48cc92425613aa8bbfc7f0f82951f",
+ "sha256:175f3825f075cf02d15099eb52658457cf0ff103dcf11512b5d2583e1d40f58b",
+ "sha256:30e14c62d88d1e01a26936ecd1c6e784d4afc9aa002bba4321c5897937112616",
+ "sha256:3210da6f36cf4b835ff1be853962b22cc354d506f493b67a4303c88bbb40d57b",
+ "sha256:40f60819fbd5bad6e191ba1329bfafa09ab7f3f174b3d034d413ef5266963294",
+ "sha256:43b26a865a61549919f8a42e094dfdb62847113cf776d84bd6b60e4e3fc20ea3",
+ "sha256:4a03dd682f8e35a10234904e0b9508d705ff98cf962c5851ed052e9340df3d90",
+ "sha256:62f382cddf3d2e52cf266e161aa522d54fd624b8cc567bc18f573d9d50d40e8e",
+ "sha256:7b98f0325be8450da70aa4a796c4f06852949fe031878b4aa1d6c417a412f314",
+ "sha256:846a0739e595871041385d86d12af4b6999f921359b38affb99cdd6b54219a8f",
+ "sha256:a3080470559938a09a5d0ec558c005282e99ac77bf8211fb7b9a5c66390acd8d",
+ "sha256:ad841b78a476623955da270ab8d207c3c694aa5eba71f4792f65926dc46c6ee8",
+ "sha256:afdd75d9735e44c639ffd6258ce04a2de3b208f148072c02478162d0944d9da3",
+ "sha256:b4fbf9b552faff54742bcd0791ab1da5863363fb19047e68f6592be1ac2dab33",
+ "sha256:b90c4e32d6ec089d3fa3518436bdf5ce4d902a0787dbd9bb09f37afe8b994317",
+ "sha256:b91cfe4438c741aeff662d413fd2808ac901cc6229c838236840d11de4586d63",
+ "sha256:bdb0593a42070b0a5f138b79b872289ee73c8e25b3f0bea6564e795b55b6bcdd",
+ "sha256:c4e4bca2bb68ce22320297dfa1a7bf070a5b20bcbaec4ee023f83d2f6e76496f",
+ "sha256:cec4ab14af9eae8501be3266ff50c3c2aecc017ba1e86c160209bb4f0423df6a",
+ "sha256:e83b4b2bf029f5104bc1227dbb7bf5ace6fd8fabaebffcd4f8106fafc69fc45f",
+ "sha256:e995b3734a46d41ae60b6097f7c51ba9958648c6d1e0935b7e0ee446ee4abe22",
+ "sha256:f679d93dec7f7210575c85379a31322df4c46496f184ef650d3aba1484b38a2d",
+ "sha256:fd213bb5166e46974f113c8228daaef1732abc47cb561ce9c4c8eaed4bd3b09b",
+ "sha256:fdcb57b906dbc1f80666e6290e794ab8fb959a2e17aa5aee1758a85d1da4533f",
+ "sha256:ff424b01d090ffe1947ec7432b07f536912e0300458f9a7f48ea217dd8362b86"
+ ],
+ "version": "==4.3.3"
},
"markupsafe": {
"hashes": [
@@ -287,6 +304,12 @@
],
"version": "==1.1.1"
},
+ "misaka": {
+ "hashes": [
+ "sha256:62f35254550095d899fc2ab8b33e156fc5e674176f074959cbca43cf7912ecd7"
+ ],
+ "version": "==2.1.1"
+ },
"pycparser": {
"hashes": [
"sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
@@ -359,11 +382,11 @@
},
"python-snappy": {
"hashes": [
- "sha256:59c79d83350f931ad5cf8f06ccb1c9bd1087a77c3ca7e00806884cda654a6faf",
- "sha256:8a7f803f06083d4106d55387d2daa32c12b5e376c3616b0e2da8b8a87a27d74a"
+ "sha256:9c0ba725755b749ef9b03f6ed7582cefb957c0d9f6f064a7c4314148a9dbdb61",
+ "sha256:d9c26532cfa510f45e8d135cde140e8a5603d3fb254cfec273ebc0ecf9f668e2"
],
"index": "pypi",
- "version": "==0.5.3"
+ "version": "==0.5.4"
},
"raven": {
"hashes": [
@@ -398,10 +421,10 @@
},
"soupsieve": {
"hashes": [
- "sha256:afa56bf14907bb09403e5d15fbed6275caa4174d36b975226e3b67a3bb6e2c4b",
- "sha256:eaed742b48b1f3e2d45ba6f79401b2ed5dc33b2123dfe216adb90d4bfa0ade26"
+ "sha256:3aef141566afd07201b525c17bfaadd07580a8066f82b57f7c9417f26adbd0a3",
+ "sha256:e41a65e99bd125972d84221022beb1e4b5cfc68fa12c170c39834ce32d1b294c"
],
- "version": "==1.8"
+ "version": "==1.9"
},
"tabulate": {
"hashes": [
@@ -425,10 +448,18 @@
},
"werkzeug": {
"hashes": [
- "sha256:590abe38f8be026d78457fe3b5200895b3543e58ac3fc1dd792c6333ea11af64",
- "sha256:ee11b0f0640c56fb491b43b38356c4b588b3202b415a1e03eacf1c5561c961cf"
+ "sha256:0a73e8bb2ff2feecfc5d56e6f458f5b99290ef34f565ffb2665801ff7de6af7a",
+ "sha256:7fad9770a8778f9576693f0cc29c7dcc36964df916b83734f4431c0e612a7fbc"
],
- "version": "==0.15.0"
+ "version": "==0.15.2"
+ },
+ "wtforms": {
+ "hashes": [
+ "sha256:0cdbac3e7f6878086c334aa25dc5a33869a3954e9d1e015130d65a69309b3b61",
+ "sha256:e3ee092c827582c50877cdbd49e9ce6d2c5c1f6561f849b3b068c1b8029626f1"
+ ],
+ "index": "pypi",
+ "version": "==2.2.1"
}
},
"develop": {
@@ -536,11 +567,11 @@
},
"ipython": {
"hashes": [
- "sha256:06de667a9e406924f97781bda22d5d76bfb39762b678762d86a466e63f65dc39",
- "sha256:5d3e020a6b5f29df037555e5c45ab1088d6a7cf3bd84f47e0ba501eeb0c3ec82"
+ "sha256:b038baa489c38f6d853a3cfc4c635b0cda66f2864d136fe8f40c1a6e334e2a6b",
+ "sha256:f5102c1cd67e399ec8ea66bcebe6e3968ea25a8977e53f012963e5affeb1fe38"
],
"index": "pypi",
- "version": "==7.3.0"
+ "version": "==7.4.0"
},
"ipython-genutils": {
"hashes": [
@@ -551,10 +582,10 @@
},
"isort": {
"hashes": [
- "sha256:18c796c2cd35eb1a1d3f012a214a542790a1aed95e29768bdcb9f2197eccbd0b",
- "sha256:96151fca2c6e736503981896495d344781b60d18bfda78dc11b290c6125ebdb6"
+ "sha256:08f8e3f0f0b7249e9fad7e5c41e2113aba44969798a26452ee790c06f155d4ec",
+ "sha256:4e9e9c4bd1acd66cf6c36973f29b031ec752cbfd991c69695e4e259f9a756927"
],
- "version": "==4.3.15"
+ "version": "==4.3.16"
},
"jedi": {
"hashes": [
@@ -606,11 +637,11 @@
},
"more-itertools": {
"hashes": [
- "sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40",
- "sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1"
+ "sha256:2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7",
+ "sha256:c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a"
],
"markers": "python_version > '2.7'",
- "version": "==6.0.0"
+ "version": "==7.0.0"
},
"parso": {
"hashes": [
@@ -730,11 +761,11 @@
},
"pytest": {
"hashes": [
- "sha256:592eaa2c33fae68c7d75aacf042efc9f77b27c08a6224a4f59beab8d9a420523",
- "sha256:ad3ad5c450284819ecde191a654c09b0ec72257a2c711b9633d677c71c9850c4"
+ "sha256:13c5e9fb5ec5179995e9357111ab089af350d788cbc944c628f3cde72285809b",
+ "sha256:f21d2f1fb8200830dcbb5d8ec466a9c9120e20d8b53c7585d180125cce1d297a"
],
"index": "pypi",
- "version": "==4.3.1"
+ "version": "==4.4.0"
},
"pytest-cov": {
"hashes": [
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: &nbsp;<code>{{ container.extra.ISSNe }}</code>
{% endif %}
+ <br>
{% endif %}
{% if container.wikidata_qid != None %}
- <br><b>Wikidata:</b> &nbsp;<a href="https://wikidata.org/wiki/{{ container.wikidata_qid }}"><code>{{ container.wikidata_qid }}</code></a>
+ <b>Wikidata</b> &nbsp;<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> &nbsp;{{ webcapture.timestamp }}
+ <p><b>Capture Time</b> &nbsp;{{ 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
diff --git a/python/tests/routes.py b/python/tests/routes.py
index 5ed6e322..ece91d65 100644
--- a/python/tests/routes.py
+++ b/python/tests/routes.py
@@ -7,7 +7,7 @@ from fixtures import *
def test_static_routes(app):
- for route in ('/health', '/robots.txt', '/', '/about'):
+ for route in ('/health.json', '/robots.txt', '/', '/about'):
rv = app.get(route)
assert rv.status_code == 200