Compare commits

...

7 Commits
v1.0 ... master

  1. 90
      .pylintrc
  2. 2
      Dockerfile
  3. 68
      README.md
  4. 2
      asl_articles/__init__.py
  5. 2
      asl_articles/config/constants.py
  6. 17
      asl_articles/db_report.py
  7. 2
      asl_articles/images.py
  8. 10
      asl_articles/main.py
  9. 11
      asl_articles/tests/test_articles.py
  10. 3
      asl_articles/tests/test_authors.py
  11. 3
      asl_articles/tests/test_import_roar_scenarios.py
  12. 20
      asl_articles/tests/test_publications.py
  13. 8
      asl_articles/tests/test_publishers.py
  14. 3
      asl_articles/tests/test_scenarios.py
  15. 6
      asl_articles/tests/test_tags.py
  16. 20
      asl_articles/tests/utils.py
  17. 23
      conftest.py
  18. 6
      requirements-dev.txt
  19. 18
      requirements.txt
  20. 3
      run_server.py
  21. 14
      setup.py
  22. 3
      tools/import_roar_scenarios.py
  23. 6
      web/Dockerfile
  24. 37257
      web/package-lock.json
  25. 4
      web/package.json
  26. 2
      web/src/App.css
  27. 2
      web/src/ArticleSearchResult2.js
  28. 2
      web/src/DbReport.css
  29. 2
      web/src/SearchForm.css
  30. 2
      web/src/SearchResults.css
  31. 6
      web/src/index.css

@ -60,17 +60,7 @@ confidence=
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=print-statement,
parameter-unpacking,
unpacking-in-except,
old-raise-syntax,
backtick,
long-suffix,
old-ne-operator,
old-octal-literal,
import-star-module-level,
non-ascii-bytes-literal,
raw-checker-failed,
disable=raw-checker-failed,
bad-inline-option,
locally-disabled,
file-ignored,
@ -78,74 +68,15 @@ disable=print-statement,
useless-suppression,
deprecated-pragma,
use-symbolic-message-instead,
apply-builtin,
basestring-builtin,
buffer-builtin,
cmp-builtin,
coerce-builtin,
execfile-builtin,
file-builtin,
long-builtin,
raw_input-builtin,
reduce-builtin,
standarderror-builtin,
unicode-builtin,
xrange-builtin,
coerce-method,
delslice-method,
getslice-method,
setslice-method,
no-absolute-import,
old-division,
dict-iter-method,
dict-view-method,
next-method-called,
metaclass-assignment,
indexing-exception,
raising-string,
reload-builtin,
oct-method,
hex-method,
nonzero-method,
cmp-method,
input-builtin,
round-builtin,
intern-builtin,
unichr-builtin,
map-builtin-not-iterating,
zip-builtin-not-iterating,
range-builtin-not-iterating,
filter-builtin-not-iterating,
using-cmp-argument,
eq-without-hash,
div-method,
idiv-method,
rdiv-method,
exception-message-attribute,
invalid-str-codec,
sys-max-int,
bad-python3-import,
deprecated-string-function,
deprecated-str-translate-call,
deprecated-itertools-function,
deprecated-types-field,
next-method-defined,
dict-items-not-iterating,
dict-keys-not-iterating,
dict-values-not-iterating,
deprecated-operator-function,
deprecated-urllib-function,
xreadlines-attribute,
deprecated-sys-function,
exception-escape,
comprehension-escape,
bad-whitespace,
invalid-name,
wrong-import-position,
global-statement,
bad-continuation,
too-few-public-methods,
no-else-return
no-else-return,
consider-using-f-string,
use-implicit-booleaness-not-comparison,
duplicate-code,
unnecessary-lambda-assignment,
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
@ -262,7 +193,7 @@ ignore-on-opaque-inference=yes
# for classes with dynamically set attributes). This supports the use of
# qualified names.
# NOTE: We disable warnings for SQLAlchemy's Column class members e.g. ilike(), asc()
ignored-classes=optparse.Values,thread._local,_thread._local,Column
ignored-classes=optparse.Values,thread._local,_thread._local,scoped_session,Column
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
@ -307,13 +238,6 @@ max-line-length=120
# Maximum number of lines in a module.
max-module-lines=1000
# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,
dict-separator
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no

@ -1,7 +1,7 @@
# We do a multi-stage build (requires Docker >= 17.05) to install everything, then copy it all
# to the final target image.
FROM centos:8 AS base
FROM rockylinux:8.5 AS base
# update packages and install Python
RUN dnf -y upgrade-minimal && \

@ -1,46 +1,28 @@
This program provides a searchable interface to your ASL magazines and their articles.
<a href="https://github.com/pacman-ghost/asl-articles/raw/master/doc/publishers.png" target="_blank">
<img src="https://github.com/pacman-ghost/asl-articles/raw/master/doc/publishers.png" height="150">
</a>
[<img src="doc/publishers.png" height="150">](doc/publishers.png)
&nbsp;
<a href="https://github.com/pacman-ghost/asl-articles/raw/master/doc/publication.png" target="_blank">
<img src="https://github.com/pacman-ghost/asl-articles/raw/master/doc/publication.png" height="150">
</a>
[<img src="doc/publication.png" height="150">](doc/publication.png)
&nbsp;
<a href="https://github.com/pacman-ghost/asl-articles/raw/master/doc/search.png" target="_blank">
<img src="https://github.com/pacman-ghost/asl-articles/raw/master/doc/search.png" height="150">
</a>
[<img src="doc/search.png" height="150">](doc/search.png)
&nbsp;
<a href="https://github.com/pacman-ghost/asl-articles/raw/master/doc/tag.png" target="_blank">
<img src="https://github.com/pacman-ghost/asl-articles/raw/master/doc/tag.png" height="150">
</a>
[<img src="doc/tag.png" height="150">](doc/tag.png)
*NOTE: This project integrates with my other [asl-rulebook2](https://github.com/pacman-ghost/asl-rulebook2) project. Add a setting to your `site.cfg` e.g.*
*NOTE: This project integrates with my other [asl-rulebook2](https://code.pacman-ghost.com/public/asl-rulebook2) project. Add a setting to your `site.cfg` e.g.*
```
ASLRB_BASE_URL = http://localhost:5020
```
*and references to rules will be converted to clickable links that will open the ASLRB at that rule.*
### To create a new database
*NOTE: This requires the Python environment to have been set up (see the developer notes below).*
Go to the *alembic/* directory and change the database connection string in *alembic.ini* e.g.
```sqlalchemy.url = sqlite:////home/pacman-ghost/asl-articles.db```
Note that there are 3 forward slashes for the protocol, the 4th one is the start of the path to the database.
Run the following command to create the database (you must be in the *alembic/* directory):
```alembic upgrade head```
*and references to rules will be converted to clickable links that will open the ASLRB at that rule.*
### To run the application
Go to the project root directory and run the following command:
Get a copy of the pre-loaded database from the release page.
```./run-containers.sh -d /home/pacman-ghost/asl-articles.db```
Then go to the project root directory and run the following command:
```
./run-containers.sh -d /home/pacman-ghost/asl-articles.db
```
*NOTE: You will need Docker >= 17.05 (for multi-stage builds)*, and `docker-compose`.
@ -55,17 +37,15 @@ It is possible to configure publications and their articles so that clicking the
For security reasons, browsers don't allow *file://* links to PDF's, they must be served by a web server. This program supports this, but some things need to be set up first.
When you run the application, specify the top-level directory that contains your PDF's in the command line e.g.
```
./run-containers.sh \
-d /home/pacman-ghost/asl-articles.db \
-e /home/pacman-ghost/asl-articles-docs/
./run-containers.sh \
-d /home/pacman-ghost/asl-articles.db \
-e /home/pacman-ghost/asl-articles-docs/
```
Then, configure your document paths *relative to that directory*.
For example, say I have my files organized like this:
```
* /home/pacman-ghost/
+-- asl-articles.db
@ -87,14 +67,16 @@ The application is split over 2 Docker containers, one running a React front-end
##### Setting up the Flask (Python) back-end
Create a *virtualenv*, then go to the *asl_articles/* directory and install the requirements:
```pip install -e .[dev]```
```
pip install -e .[dev]
```
Copy *config/site.cfg.example* to *config/site.cfg*, and update it to point to your database.
Then run the server:
```./run-server.py```
```
./run-server.py
```
You can test if things are working by opening a browser and going to http://localhost:5000/ping
@ -103,9 +85,11 @@ You can test if things are working by opening a browser and going to http://loca
##### Setting up the React front-end
Go to the *web/* directory and install the requirements:
```npm install```
```
npm install
```
Then run the server:
```npm start```
```
npm start
```

@ -75,7 +75,7 @@ _load_config( _cfg, _fname, "Debug" )
# initialize logging
_fname = os.path.join( config_dir, "logging.yaml" )
if os.path.isfile( _fname ):
with open( _fname, "r" ) as fp:
with open( _fname, "r", encoding="utf-8" ) as fp:
logging.config.dictConfig( yaml.safe_load( fp ) )
else:
# stop Flask from logging every request :-/

@ -3,7 +3,7 @@
import os
APP_NAME = "ASL Articles"
APP_VERSION = "v1.0" # nb: also update setup.py
APP_VERSION = "v1.1" # nb: also update setup.py
APP_DESCRIPTION = "Searchable index of ASL articles."
BASE_DIR = os.path.abspath( os.path.join( os.path.split(__file__)[0], ".." ) )

@ -65,16 +65,15 @@ def check_db_link():
"""Check if a link appears to be working."""
url = request.args.get( "url" )
try:
resp = urllib.request.urlopen(
urllib.request.Request( url, method="HEAD" )
)
req = urllib.request.Request( url, method="HEAD" )
with urllib.request.urlopen( req ) as resp:
resp_code = resp.code
except urllib.error.URLError as ex:
code = getattr( ex, "code", None )
if code:
abort( code )
abort( 400 )
if resp.code != 200:
abort( resp.code )
resp_code = getattr( ex, "code", None )
if not resp_code:
resp_code = 400
if resp_code != 200:
abort( resp_code )
return "ok"
# ---------------------------------------------------------------------

@ -21,5 +21,5 @@ def get_image( image_type, image_id ):
abort( 404 )
return send_file(
io.BytesIO( img.image_data ),
attachment_filename = img.image_filename # nb: so that Flask can set the MIME type
download_name = img.image_filename # nb: so that Flask can set the MIME type
)

@ -1,7 +1,5 @@
""" Main handlers. """
from flask import request
from asl_articles import app
# ---------------------------------------------------------------------
@ -10,11 +8,3 @@ from asl_articles import app
def ping():
"""Let the caller know we're alive (for testing porpoises)."""
return "pong"
# ---------------------------------------------------------------------
@app.route( "/shutdown" )
def shutdown():
"""Shutdown the server (for testing porpoises)."""
request.environ.get( "werkzeug.server.shutdown" )()
return ""

@ -278,8 +278,9 @@ def test_images( webdriver, flask_app, dbconn ): #pylint: disable=too-many-state
btn = find_child( ".row.image .remove-image", dlg )
assert btn.is_displayed()
# make sure the article's image is correct
resp = urllib.request.urlopen( image_url ).read()
assert resp == open( expected, "rb" ).read()
with urllib.request.urlopen( image_url ) as resp:
with open( expected, "rb" ) as fp:
assert resp.read() == fp.read()
else:
# make sure there is no image
img = find_child( ".row.image img.image", dlg )
@ -290,7 +291,8 @@ def test_images( webdriver, flask_app, dbconn ): #pylint: disable=too-many-state
# make sure the article's image is not available
url = flask_app.url_for( "get_image", image_type="article", image_id=article_id )
try:
resp = urllib.request.urlopen( url )
with urllib.request.urlopen( url ):
pass
assert False, "Should never get here!"
except urllib.error.HTTPError as ex:
assert ex.code == 404
@ -350,7 +352,8 @@ def test_parent_publisher( webdriver, flask_app, dbconn ):
# check that the parent publication was updated in the database
article_id = sr.get_attribute( "testing--article_id" )
url = flask_app.url_for( "get_article", article_id=article_id )
article = json.load( urllib.request.urlopen( url ) )
with urllib.request.urlopen( url ) as resp:
article = json.load( resp )
if expected_parent:
if article["pub_id"] != expected_parent[0]:
return None

@ -92,5 +92,6 @@ def _check_authors( flask_app, all_authors, expected ):
# check the authors in the database
url = flask_app.url_for( "get_authors" )
authors = json.load( urllib.request.urlopen( url ) )
with urllib.request.urlopen( url ) as resp:
authors = json.load( resp )
assert set( a["author_name"] for a in authors.values() ) == all_authors

@ -18,7 +18,8 @@ def test_import_roar_scenarios( dbconn ):
# initialize
session = init_tests( None, None, dbconn )
roar_fname = os.path.join( os.path.split(__file__)[0], "fixtures/roar-scenarios.json" )
roar_data = json.load( open( roar_fname, "r" ) )
with open( roar_fname, "r", encoding="utf-8" ) as fp:
roar_data = json.load( fp )
# do the first import
_do_import( dbconn, session, roar_fname,

@ -246,8 +246,9 @@ def test_images( webdriver, flask_app, dbconn ): #pylint: disable=too-many-state
btn = find_child( ".row.image .remove-image", dlg )
assert btn.is_displayed()
# make sure the publication's image is correct
resp = urllib.request.urlopen( image_url ).read()
assert resp == open( expected, "rb" ).read()
with urllib.request.urlopen( image_url ) as resp:
with open( expected, "rb" ) as fp:
assert resp.read() == fp.read()
else:
# make sure there is no image
img = find_child( ".row.image img.image", dlg )
@ -258,7 +259,8 @@ def test_images( webdriver, flask_app, dbconn ): #pylint: disable=too-many-state
# make sure the publication's image is not available
url = flask_app.url_for( "get_image", image_type="publication", image_id=pub_id )
try:
resp = urllib.request.urlopen( url )
with urllib.request.urlopen( url ):
pass
assert False, "Should never get here!"
except urllib.error.HTTPError as ex:
assert ex.code == 404
@ -318,7 +320,8 @@ def test_parent_publisher( webdriver, flask_app, dbconn ):
# check that the parent publisher was updated in the database
pub_id = sr.get_attribute( "testing--pub_id" )
url = flask_app.url_for( "get_publication", pub_id=pub_id )
pub = json.load( urllib.request.urlopen( url ) )
with urllib.request.urlopen( url ) as resp:
pub = json.load( resp )
if expected_parent:
if pub["publ_id"] != expected_parent[0]:
return None
@ -672,8 +675,11 @@ def test_default_image( webdriver, flask_app, dbconn ):
f: os.path.join( os.path.split(__file__)[0], "fixtures/images/"+f )
for f in images
}
def read_image_data( fname ):
with open( fname, "rb" ) as fp:
return fp.read()
image_data = {
f: open( image_fnames[f], "rb" ).read()
f: read_image_data( image_fnames[f] )
for f in images
}
@ -690,8 +696,8 @@ def test_default_image( webdriver, flask_app, dbconn ):
if img:
assert expected
image_url = img.get_attribute( "src" )
resp = urllib.request.urlopen( image_url ).read()
assert resp == image_data[ expected ]
with urllib.request.urlopen( image_url ) as resp:
assert resp.read() == image_data[ expected ]
else:
assert not expected

@ -176,8 +176,9 @@ def test_images( webdriver, flask_app, dbconn ): #pylint: disable=too-many-state
btn = find_child( ".row.image .remove-image", dlg )
assert btn.is_displayed()
# make sure the publisher's image is correct
resp = urllib.request.urlopen( image_url ).read()
assert resp == open(expected,"rb").read()
with urllib.request.urlopen( image_url ) as resp:
with open( expected, "rb" ) as fp:
assert resp.read() == fp.read()
else:
# make sure there is no image
img = find_child( ".row.image img.image", dlg )
@ -188,7 +189,8 @@ def test_images( webdriver, flask_app, dbconn ): #pylint: disable=too-many-state
# make sure the publisher's image is not available
url = flask_app.url_for( "get_image", image_type="publisher", image_id=publ_id )
try:
resp = urllib.request.urlopen( url )
with urllib.request.urlopen( url ):
pass
assert False, "Should never get here!"
except urllib.error.HTTPError as ex:
assert ex.code == 404

@ -104,7 +104,8 @@ def _check_scenarios( flask_app, all_scenarios, expected ):
# check the scenarios in the database
url = flask_app.url_for( "get_scenarios" )
scenarios = json.load( urllib.request.urlopen( url ) )
with urllib.request.urlopen( url ) as resp:
scenarios = json.load( resp )
assert set( _make_scenario_display_name(a) for a in scenarios.values() ) == all_scenarios
def _make_scenario_display_name( scenario ):

@ -145,10 +145,12 @@ def _check_tags( flask_app, expected ): #pylint: disable=too-many-locals
if sr.text.startswith( "publication" ):
pub_id = sr.get_attribute( "testing--pub_id" )
url = flask_app.url_for( "get_publication", pub_id=pub_id )
pub = json.load( urllib.request.urlopen( url ) )
with urllib.request.urlopen( url ) as resp:
pub = json.load( resp )
assert expected[ pub["pub_name"] ] == fixup_tags( pub["pub_tags"] )
elif sr.text.startswith( "article" ):
article_id = sr.get_attribute( "testing--article_id" )
url = flask_app.url_for( "get_article", article_id=article_id )
article = json.load( urllib.request.urlopen( url ) )
with urllib.request.urlopen( url ) as resp:
article = json.load( resp )
assert expected[ article["article_title"] ] == fixup_tags( article["article_tags"] )

@ -49,7 +49,8 @@ def init_tests( webdriver, flask_app, dbconn, **kwargs ):
# re-initialize the search engine
if flask_app:
url = flask_app.url_for( "init_search_for_test" )
_ = urllib.request.urlopen( url ).read()
with urllib.request.urlopen( url ) as resp:
_ = resp.read()
# initialize the documents directory
dname = kwargs.pop( "docs", None )
@ -70,7 +71,10 @@ def init_tests( webdriver, flask_app, dbconn, **kwargs ):
if to_bool( kwargs.pop( "disable_confirm_discard_changes", True ) ):
kwargs[ "disable_confirm_discard_changes" ] = 1
webdriver.get( webdriver.make_url( "", **kwargs ) )
wait_for_elem( 2, "#search-form" )
# FUDGE! Since we switched from running the test Flask server with app.run() to make_server().serve_forever(),
# stopping and starting the server seems to be much quicker, but refreshing the page can be slower when
# running multiple tests :shrug:
wait_for_elem( 10, "#search-form" )
return session
@ -83,7 +87,8 @@ def load_fixtures( session, fname ):
if fname:
dname = os.path.join( os.path.split(__file__)[0], "fixtures/" )
fname = os.path.join( dname, fname )
data = json.load( open( fname, "r" ) )
with open( fname, "r", encoding="utf-8" ) as fp:
data = json.load( fp )
else:
data = {}
@ -318,21 +323,21 @@ def wait_for_not_elem( timeout, sel ):
def find_child( sel, parent=None ):
"""Find a child element."""
try:
return (parent if parent else _webdriver).find_element_by_css_selector( sel )
return (parent if parent else _webdriver).find_element( By.CSS_SELECTOR, sel )
except NoSuchElementException:
return None
def find_children( sel, parent=None ):
"""Find child elements."""
try:
return (parent if parent else _webdriver).find_elements_by_css_selector( sel )
return (parent if parent else _webdriver).find_elements( By.CSS_SELECTOR, sel )
except NoSuchElementException:
return None
def find_parent_by_class( elem, class_name ):
"""Find a parent element with the specified class."""
while True:
elem = elem.find_element_by_xpath( ".." )
elem = elem.find_element( By.XPATH, ".." )
if not elem:
return None
classes = set( elem.get_attribute( "class" ).split() )
@ -497,7 +502,8 @@ def call_with_retry( func, expected_exceptions, max_retries=10, delay=0.1 ):
def change_image( dlg, fname ):
"""Click on an image to change it."""
# NOTE: This is a bit tricky since we started overlaying the image with the "remove image" icon :-/
data = base64.b64encode( open( fname, "rb" ).read() )
with open( fname, "rb" ) as fp:
data = base64.b64encode( fp.read() )
data = "{}|{}".format( os.path.split(fname)[1], data.decode("ascii") )
elem = find_child( ".row.image img.image", dlg )
_webdriver.execute_script( "arguments[0].scrollTo( 0, 0 )", find_child( ".MuiDialogContent-root", dlg ) )

@ -9,6 +9,7 @@ from urllib.error import URLError
import pytest
import flask
import werkzeug
import sqlalchemy
from flask_sqlalchemy import SQLAlchemy
import alembic
@ -99,19 +100,22 @@ def flask_app( request ):
# the *configured* database connection string (since it will fail to start if there's a problem).
asl_articles._disable_db_startup = True #pylint: disable=protected-access
# yup - make it so
server = werkzeug.serving.make_server(
_FLASK_SERVER_URL[0], _FLASK_SERVER_URL[1],
app, threaded=True
)
thread = threading.Thread(
target = lambda: app.run(
host=_FLASK_SERVER_URL[0], port=_FLASK_SERVER_URL[1],
use_reloader=False
)
target = server.serve_forever,
daemon=True
)
thread.start()
# wait for the server to start up
def is_ready():
"""Try to connect to the Flask server."""
try:
resp = urllib.request.urlopen( app.url_for( "ping" ) ).read()
assert resp == b"pong"
url = app.url_for( "ping" )
with urllib.request.urlopen( url ) as resp:
assert resp.read() == b"pong"
return True
except URLError:
return False
@ -125,7 +129,7 @@ def flask_app( request ):
finally:
# shutdown the local Flask server
if not flask_url:
urllib.request.urlopen( app.url_for("shutdown") ).read()
server.shutdown()
thread.join()
# ---------------------------------------------------------------------
@ -142,10 +146,7 @@ def webdriver( request ):
options = wb.FirefoxOptions()
if headless:
options.add_argument( "--headless" ) #pylint: disable=no-member
driver = wb.Firefox(
options = options,
service_log_path = os.path.join( tempfile.gettempdir(), "geckodriver.log" )
)
driver = wb.Firefox( options=options )
elif driver == "chrome":
options = wb.ChromeOptions()
if headless:

@ -1,5 +1,5 @@
pytest==6.2.1
selenium==3.141.0
pylint==2.6.0
pytest==7.1.2
selenium==4.2.0
pylint==2.14.1
pylint-flask-sqlalchemy==0.2.0
pytest-pylint==0.18.0

@ -1,13 +1,9 @@
# python 3.8.7
# python 3.10.4
flask==1.1.2
# NOTE: Newer versions of SQLAlchemy contain a change that breaks Flask-SQLALchemy :-/
# https://stackoverflow.com/a/66652728
# This wasn't a problem on vm-linux-dev, but manifested itself on the rPi4 (probably because
# the virtualenv on vm-linux-dev was built before this became a problem).
flask==2.1.2
flask-sqlalchemy==2.5.1
psycopg2-binary==2.8.6
alembic==1.4.3
pyyaml==5.3.1
lxml==4.6.2
waitress==2.0.0
psycopg2-binary==2.9.3
alembic==1.8.0
pyyaml==6.0
lxml==4.9.0
waitress==2.1.2

@ -48,7 +48,8 @@ def _force_init():
host = host[:-1]
url = "{}:{}{}".format( host, flask_port, url )
# make the request
_ = urllib.request.urlopen( url ).read()
with urllib.request.urlopen( url ) as resp:
_ = resp.read()
except Exception as ex: #pylint: disable=broad-except
print( "WARNING: Startup ping failed: {}".format( ex ) )
threading.Thread( target=_force_init ).start()

@ -16,20 +16,22 @@ def parse_requirements( fname ):
"""Parse a requirements file."""
lines = []
fname = os.path.join( os.path.split(__file__)[0], fname )
for line in open(fname,"r"):
line = line.strip()
if line == "" or line.startswith("#"):
continue
lines.append( line )
with open( fname, "r", encoding="utf-8" ) as fp:
for line in fp:
line = line.strip()
if line == "" or line.startswith("#"):
continue
lines.append( line )
return lines
# ---------------------------------------------------------------------
setup(
name = "asl-articles",
version = "1.0", # nb: also update constants.py
version = "1.1", # nb: also update constants.py
description = "Searchable index of ASL articles.",
license = "AGPLv3",
url = "https://code.pacman-ghost.com/public/asl-articles",
packages = find_packages(),
install_requires = parse_requirements( "requirements.txt" ),
extras_require = {

@ -62,7 +62,8 @@ def import_roar_scenarios( dbconn, roar_data, progress=None ):
# load the ROAR scenarios
if isinstance( roar_data, str ):
log_progress( "Loading scenarios: {}", roar_data )
roar_data = json.load( open( roar_data, "r" ) )
with open( roar_data, "r", encoding="utf-8" ) as fp:
roar_data = json.load( fp )
else:
assert isinstance( roar_data, dict )
log_progress( "- Last updated: {}".format( roar_data.get("_lastUpdated_","(unknown)") ) )

@ -1,10 +1,10 @@
# NOTE: Multi-stage builds require Docker v17.05 or later.
# create the build environment
FROM node:8.16.2-alpine AS build
FROM node:18-alpine3.15 AS build
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
RUN npm install react-scripts@3.2.0 -g
RUN npm install react-scripts@5.0.1 --location=global
COPY package.json /app/package.json
RUN npm install
COPY . /app/
@ -14,7 +14,7 @@ RUN if [ -n "$ENABLE_TESTS" ]; then echo -e "\nREACT_APP_TEST_MODE=1" >>/app/.en
RUN npm run build
# create the final target image
FROM nginx:1.17.5-alpine
FROM nginx:1.21.6-alpine
COPY docker/nginx-default.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80

37257
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -6,17 +6,19 @@
"@material-ui/core": "^4.7.0",
"@reach/menu-button": "^0.7.2",
"axios": "^0.19.0",
"babel-runtime": "^6.26.0",
"http-proxy-middleware": "^0.20.0",
"jquery": "^3.4.1",
"lodash.clone": "^4.5.0",
"lodash.clonedeep": "^4.5.0",
"lodash.isequal": "^4.5.0",
"query-string": "^7.1.1",
"react": "^16.11.0",
"react-dom": "^16.11.0",
"react-drag-listview": "^0.1.6",
"react-draggable": "^4.1.0",
"react-router-dom": "^5.1.2",
"react-scripts": "3.2.0",
"react-scripts": "5.0.1",
"react-select": "^3.0.8",
"react-tabs": "^3.2.3",
"react-toastify": "^5.4.1"

@ -8,7 +8,7 @@
#menu-button--app { position: absolute ; top: 10px ; right: 10px ;
width: 30px ; height: 30px ;
background: url("/images/main-menu.png") transparent no-repeat ; background-size: 100% ; border: none ;
background: url("/public/images/main-menu.png") transparent no-repeat ; background-size: 100% ; border: none ;
cursor: pointer ;
}
[data-reach-menu] { z-index: 999 ; }

@ -296,7 +296,7 @@ export class ArticleSearchResult2
const optional = [
[ () => parentMode === "publication" && newVals.pub_id === null, "No publication was specified.", refs.pub_id ],
[ () => parentMode === "publisher" && newVals.publ_id === null, "No publisher was specified.", refs.pub_id ],
[ () => newVals.article_pageno === "" && newVals.pub_id !== null, "No page number was specified.", refs.article_pageno ],
[ () => parentMode === "publication" && newVals.article_pageno === "" && newVals.pub_id !== null, "No page number was specified.", refs.article_pageno ],
[ () => newVals.article_pageno !== "" && newVals.pub_id === null, "A page number was specified but no publication.", refs.pub_id ],
[ () => newVals.article_pageno !== "" && !isNumeric(newVals.article_pageno), "The page number is not numeric.", refs.article_pageno ],
[ () => newVals.publ_id && newVals.article_date === "", "The article date was not specified.", refs.article_date ],

@ -13,7 +13,7 @@
#db-report .db-links .check-links-frame { display: inline-block ; position: absolute ; right: 1em ; text-align: center ; }
#db-report .db-links button.check-links { margin-bottom: 0.2em ; padding: 0.25em 0.5em ; }
#db-report .db-links .check-links-frame .status-msg { font-size: 60% ; font-style: italic ; }
#db-report .db-links .link-errors { font-size: 80% ; list-style-image: url("/images/link-error-bullet.png") ; }
#db-report .db-links .link-errors { font-size: 80% ; list-style-image: url("/public/images/link-error-bullet.png") ; }
#db-report .db-links .link-errors .status { font-family: monospace ; font-style: italic ; }
#db-report .db-images .dupe-analysis .collapsible { margin-bottom: 0.5em ; }

@ -2,5 +2,5 @@
#search-form .caption { line-height: 22px ; }
#search-form .query { flex: 1 ; min-width: 5em ; max-width: 30em ; margin: 0 0.25em 0 0.5em ; }
#search-form button[type="submit"] { width: 28px ; height: 28px ;
background: url("/images/search.png") transparent no-repeat 2px 2px ; background-size: 20px ;
background: url("/public/images/search.png") transparent no-repeat 2px 2px ; background-size: 20px ;
}

@ -6,7 +6,7 @@
.search-result button.sr-menu {
width: 1em ; height: 1em ; float: right ; margin-right: -3px ;
background: url("/images/menu.png") transparent no-repeat ; background-size: 100% ; border: none ;
background: url("/public/images/menu.png") transparent no-repeat ; background-size: 100% ; border: none ;
cursor: pointer ;
}

@ -9,8 +9,8 @@ body {
h1:not(:first-child), h2:not(:first-child), h3:not(:first-child), h4:not(:first-child), h5:not(:first-child), h6:not(:first-child) { margin-top: 0.25em ; }
ul, ol { margin: 0 0 0 1.25em ; }
ul { list-style-image: url("/images/bullet.png") }
ul ul, ol ul { list-style-image: url("/images/bullet2.png") }
ul { list-style-image: url("/public/images/bullet.png") }
ul ul, ol ul { list-style-image: url("/public/images/bullet2.png") }
input[type="text"] { height: 22px ; border: 1px solid #c5c5c5 ; padding: 2px 5px ; }
label { height: 1.25em ; margin-top: -3px ; }
textarea { padding: 2px 5px ; resize: vertical ; }
@ -18,7 +18,7 @@ pre { font-size: 90% ; }
blockquote {
margin: .5em 1em .75em 1em ; padding: 5px 5px 5px 15px ;
border: 1px solid #ddd ; background: #fffff0 ;
background-image: url( "/images/blockquote.png" ) ; background-position: 2px 5px ; background-repeat: no-repeat ;
background-image: url( "/public/images/blockquote.png" ) ; background-position: 2px 5px ; background-repeat: no-repeat ;
font-style: italic ;
}

Loading…
Cancel
Save