From 9d2495aa645ecddbe75f9b6d8bd331c7a1f21077 Mon Sep 17 00:00:00 2001 From: Taka Date: Mon, 15 Mar 2021 16:09:40 +1100 Subject: [PATCH] Implemented the basic webapp functionality. --- .eslintrc.json | 22 + .gitignore | 4 + asl_rulebook2/webapp/__init__.py | 4 +- asl_rulebook2/webapp/content.py | 79 ++ asl_rulebook2/webapp/globvars.py | 30 +- asl_rulebook2/webapp/main.py | 12 + asl_rulebook2/webapp/run_server.py | 21 +- asl_rulebook2/webapp/static/ContentPane.js | 40 + asl_rulebook2/webapp/static/MainApp.js | 70 ++ asl_rulebook2/webapp/static/NavPane.js | 23 + asl_rulebook2/webapp/static/SearchPane.js | 83 ++ asl_rulebook2/webapp/static/SearchResult.js | 23 + asl_rulebook2/webapp/static/TabbedPages.js | 80 ++ .../webapp/static/css/ContentPane.css | 10 + asl_rulebook2/webapp/static/css/MainApp.css | 6 + asl_rulebook2/webapp/static/css/NavPane.css | 3 + .../webapp/static/css/SearchPane.css | 8 + .../webapp/static/css/SearchResult.css | 1 + .../webapp/static/css/TabbedPages.css | 4 + asl_rulebook2/webapp/static/css/global.css | 15 + .../webapp/static/growl/jquery.growl.css | 96 +++ .../webapp/static/growl/jquery.growl.js | 311 +++++++ asl_rulebook2/webapp/static/split/split.js | 769 ++++++++++++++++++ .../webapp/static/split/split.min.js | 3 + asl_rulebook2/webapp/static/src/MainApp.js | 16 - .../webapp/static/tinyemitter/tinyemitter.js | 71 ++ .../static/tinyemitter/tinyemitter.min.js | 1 + asl_rulebook2/webapp/static/utils.js | 17 + asl_rulebook2/webapp/templates/index.html | 30 +- asl_rulebook2/webapp/tests/control_tests.py | 10 + .../webapp/tests/control_tests_servicer.py | 24 + .../webapp/tests/fixtures/simple/simple.docx | Bin 0 -> 16012 bytes .../webapp/tests/fixtures/simple/simple.index | 76 ++ .../webapp/tests/fixtures/simple/simple.pdf | Bin 0 -> 42358 bytes .../tests/fixtures/simple/simple.targets | 15 + .../webapp/tests/proto/control_tests.proto | 8 + .../proto/generated/control_tests_pb2.py | 56 +- .../proto/generated/control_tests_pb2_grpc.py | 34 + asl_rulebook2/webapp/tests/test_basic.py | 12 +- asl_rulebook2/webapp/tests/test_eslint.py | 35 + asl_rulebook2/webapp/tests/utils.py | 37 + asl_rulebook2/webapp/utils.py | 18 + 42 files changed, 2148 insertions(+), 29 deletions(-) create mode 100644 .eslintrc.json create mode 100644 asl_rulebook2/webapp/content.py create mode 100644 asl_rulebook2/webapp/static/ContentPane.js create mode 100644 asl_rulebook2/webapp/static/MainApp.js create mode 100644 asl_rulebook2/webapp/static/NavPane.js create mode 100644 asl_rulebook2/webapp/static/SearchPane.js create mode 100644 asl_rulebook2/webapp/static/SearchResult.js create mode 100644 asl_rulebook2/webapp/static/TabbedPages.js create mode 100644 asl_rulebook2/webapp/static/css/ContentPane.css create mode 100644 asl_rulebook2/webapp/static/css/MainApp.css create mode 100644 asl_rulebook2/webapp/static/css/NavPane.css create mode 100644 asl_rulebook2/webapp/static/css/SearchPane.css create mode 100644 asl_rulebook2/webapp/static/css/SearchResult.css create mode 100644 asl_rulebook2/webapp/static/css/TabbedPages.css create mode 100644 asl_rulebook2/webapp/static/css/global.css create mode 100644 asl_rulebook2/webapp/static/growl/jquery.growl.css create mode 100644 asl_rulebook2/webapp/static/growl/jquery.growl.js create mode 100644 asl_rulebook2/webapp/static/split/split.js create mode 100644 asl_rulebook2/webapp/static/split/split.min.js delete mode 100644 asl_rulebook2/webapp/static/src/MainApp.js create mode 100644 asl_rulebook2/webapp/static/tinyemitter/tinyemitter.js create mode 100644 asl_rulebook2/webapp/static/tinyemitter/tinyemitter.min.js create mode 100644 asl_rulebook2/webapp/static/utils.js create mode 100644 asl_rulebook2/webapp/tests/fixtures/simple/simple.docx create mode 100644 asl_rulebook2/webapp/tests/fixtures/simple/simple.index create mode 100644 asl_rulebook2/webapp/tests/fixtures/simple/simple.pdf create mode 100644 asl_rulebook2/webapp/tests/fixtures/simple/simple.targets create mode 100644 asl_rulebook2/webapp/tests/test_eslint.py diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..331edc0 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,22 @@ +{ + "env": { + "browser": true, + "jquery": true + }, + "extends": [ + "eslint:recommended", + "plugin:vue/essential" + ], + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "plugins": [ + "vue" + ], + "globals": { + "Vue": "readable" + }, + "rules": { + } +} diff --git a/.gitignore b/.gitignore index bfbefd0..82b05e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +package.json +package-lock.json +node_modules/ + _work_/ .vscode diff --git a/asl_rulebook2/webapp/__init__.py b/asl_rulebook2/webapp/__init__.py index e843c4e..1d3c473 100644 --- a/asl_rulebook2/webapp/__init__.py +++ b/asl_rulebook2/webapp/__init__.py @@ -35,7 +35,6 @@ def _on_sigint( signum, stack ): #pylint: disable=unused-argument shutdown_event.set() # call any registered cleanup handlers - from asl_rulebook2.webapp import globvars #pylint: disable=cyclic-import for handler in globvars.cleanup_handlers: handler() @@ -78,6 +77,9 @@ else: # load the application import asl_rulebook2.webapp.main #pylint: disable=wrong-import-position,cyclic-import +import asl_rulebook2.webapp.content #pylint: disable=wrong-import-position,cyclic-import +from asl_rulebook2.webapp import globvars #pylint: disable=wrong-import-position,cyclic-import +app.before_request( globvars.on_request ) # install our signal handler signal.signal( signal.SIGINT, _on_sigint ) diff --git a/asl_rulebook2/webapp/content.py b/asl_rulebook2/webapp/content.py new file mode 100644 index 0000000..1055126 --- /dev/null +++ b/asl_rulebook2/webapp/content.py @@ -0,0 +1,79 @@ +""" Manage the content documents. """ + +import os +import io +import glob + +from flask import jsonify, send_file, url_for, abort + +from asl_rulebook2.webapp import app +from asl_rulebook2.webapp.utils import change_extn, slugify + +content_docs = None + +# --------------------------------------------------------------------- + +def load_content_docs(): + """Load the content documents from the data directory.""" + + # initialize + global content_docs + content_docs = {} + dname = app.config.get( "DATA_DIR" ) + if not dname: + return + if not os.path.dirname( dname ): + raise RuntimeError( "Invalid data directory: {}".format( dname ) ) + + def get_doc( content_doc, key, fname, binary=False ): + fname = os.path.join( dname, fname ) + if not os.path.isfile( fname ): + return + kwargs = {} + kwargs["mode"] = "rb" if binary else "r" + if not binary: + kwargs["encoding"] = "utf-8" + with open( fname, **kwargs ) as fp: + content_doc[ key ] = fp.read() + + # load each content doc + fspec = os.path.join( dname, "*.index" ) + for fname in glob.glob( fspec ): + fname = os.path.basename( fname ) + title = os.path.splitext( fname )[0] + content_doc = { + "doc_id": slugify( title ), + "title": title, + } + get_doc( content_doc, "index", fname ) + get_doc( content_doc, "targets", change_extn(fname,".targets") ) + get_doc( content_doc, "footnotes", change_extn(fname,".footnotes") ) + get_doc( content_doc, "content", change_extn(fname,".pdf"), binary=True ) + content_docs[ content_doc["doc_id"] ] = content_doc + +# --------------------------------------------------------------------- + +@app.route( "/content-docs" ) +def get_content_docs(): + """Return the available content docs.""" + resp = {} + for cdoc in content_docs.values(): + cdoc2 = { + "docId": cdoc["doc_id"], + "title": cdoc["title"], + } + if "content" in cdoc: + cdoc2["url"] = url_for( "get_content", doc_id=cdoc["doc_id"] ) + resp[ cdoc["doc_id"] ] = cdoc2 + return jsonify( resp ) + +# --------------------------------------------------------------------- + +@app.route( "/content/" ) +def get_content( doc_id ): + """Return the content for the specified document.""" + cdoc = content_docs.get( doc_id ) + if not cdoc or "content" not in cdoc: + abort( 404 ) + buf = io.BytesIO( cdoc["content"] ) + return send_file( buf, mimetype="application/pdf" ) diff --git a/asl_rulebook2/webapp/globvars.py b/asl_rulebook2/webapp/globvars.py index 2e7b5d1..ba807b5 100644 --- a/asl_rulebook2/webapp/globvars.py +++ b/asl_rulebook2/webapp/globvars.py @@ -1,4 +1,8 @@ -""" Global variables. """ +""" Global definitions. """ + +import threading + +from flask import request from asl_rulebook2.webapp import app from asl_rulebook2.webapp.config.constants import APP_NAME, APP_VERSION @@ -7,6 +11,30 @@ cleanup_handlers = [] # --------------------------------------------------------------------- +_init_lock = threading.Lock() +_init_done = False + +def on_request(): + """Called before each request.""" + if request.path == "/control-tests": + # NOTE: The test suite calls $/control-tests to find out which port the gRPC test control service + # is running on, which is nice since we don't need to configure both ends with a predefined port. + # However, we don't want this call to trigger initialization, since the tests will often want to + # configure the remote webapp before loading the main page. + return + with _init_lock: + global _init_done + if not _init_done or (request.path == "/" and request.args.get("reload")): + try: + from asl_rulebook2.webapp.main import init_webapp + init_webapp() + finally: + # NOTE: It's important to set this, even if initialization failed, so we don't + # try to initialize again. + _init_done = True + +# --------------------------------------------------------------------- + @app.context_processor def inject_template_params(): """Inject template parameters into Jinja2.""" diff --git a/asl_rulebook2/webapp/main.py b/asl_rulebook2/webapp/main.py index e4cf967..0df4676 100644 --- a/asl_rulebook2/webapp/main.py +++ b/asl_rulebook2/webapp/main.py @@ -8,10 +8,22 @@ import logging from flask import render_template, jsonify, abort from asl_rulebook2.webapp import app, globvars, shutdown_event +from asl_rulebook2.webapp.content import load_content_docs from asl_rulebook2.webapp.utils import parse_int # --------------------------------------------------------------------- +def init_webapp(): + """Initialize the webapp. + + IMPORTANT: This is called on the first Flask request, but can also be called multiple times + after that by the test suite, to reset the webapp before each test. + """ + # initialize the webapp + load_content_docs() + +# --------------------------------------------------------------------- + @app.route( "/" ) def main(): """Return the main page.""" diff --git a/asl_rulebook2/webapp/run_server.py b/asl_rulebook2/webapp/run_server.py index bb38318..0a0f433 100755 --- a/asl_rulebook2/webapp/run_server.py +++ b/asl_rulebook2/webapp/run_server.py @@ -2,6 +2,9 @@ """ Run the webapp server. """ import os +import threading +import urllib.request +import time import glob import click @@ -12,8 +15,10 @@ from asl_rulebook2.webapp import app @click.command() @click.option( "--addr","-a","bind_addr", help="Webapp server address (host:port)." ) +@click.option( "--data","-d","data_dir", help="Data directory." ) +@click.option( "--force-init-delay", default=0, help="Force the webapp to initialize (#seconds delay)." ) @click.option( "--debug","flask_debug", is_flag=True, default=False, help="Run Flask in debug mode." ) -def main( bind_addr, flask_debug ): +def main( bind_addr, data_dir, force_init_delay, flask_debug ): """Run the webapp server.""" # initialize @@ -30,6 +35,12 @@ def main( bind_addr, flask_debug ): if not flask_debug: flask_debug = app.config.get( "FLASK_DEBUG", False ) + # initialize + if data_dir: + if not os.path.isdir( data_dir ): + raise RuntimeError( "Invalid data directory: {}".format( data_dir ) ) + app.config["DATA_DIR"] = data_dir + # validate the configuration if not host: raise RuntimeError( "The server host was not set." ) @@ -51,6 +62,14 @@ def main( bind_addr, flask_debug ): files = glob.glob( fspec ) extra_files.extend( files ) + # check if we should force webapp initialization + if force_init_delay > 0: + def _start_server(): + time.sleep( force_init_delay ) + url = "http://{}:{}/ping".format( host, port ) + _ = urllib.request.urlopen( url ) + threading.Thread( target=_start_server, daemon=True ).start() + # run the server app.run( host=host, port=port, debug=flask_debug, extra_files = extra_files diff --git a/asl_rulebook2/webapp/static/ContentPane.js b/asl_rulebook2/webapp/static/ContentPane.js new file mode 100644 index 0000000..398eb60 --- /dev/null +++ b/asl_rulebook2/webapp/static/ContentPane.js @@ -0,0 +1,40 @@ +import { gMainApp, gEventBus, gUrlParams } from "./MainApp.js" ; + +// -------------------------------------------------------------------- + +gMainApp.component( "content-pane", { + + props: [ "contentDocs" ], + + template: ` + + + + +`, + + mounted() { + gEventBus.on( "show-content-doc", (docId) => { + this.$refs.tabbedPages.activateTab( docId ) ; // nb: tabId == docId + } ) ; + }, + +} ) ; + +// -------------------------------------------------------------------- + +gMainApp.component( "content-doc", { + + props: [ "doc" ], + data() { return { + noContent: gUrlParams.get( "no-content" ), + } ; }, + + template: ` +
+
Content disabled.
+