Added data validation.

master
Pacman Ghost 4 years ago
parent 7ac1119191
commit 34b23bc867
  1. 171
      asl_articles/tests/test_articles.py
  2. 121
      asl_articles/tests/test_publications.py
  3. 76
      asl_articles/tests/test_publishers.py
  4. 11
      asl_articles/tests/utils.py
  5. 1
      web/src/App.css
  6. 14
      web/src/App.js
  7. 26
      web/src/ArticleSearchResult2.js
  8. 56
      web/src/PublicationSearchResult2.js
  9. 27
      web/src/PublisherSearchResult2.js
  10. 70
      web/src/utils.js

@ -10,8 +10,8 @@ from asl_articles.search import SEARCH_ALL_ARTICLES
from asl_articles.tests.utils import init_tests, select_main_menu_option, select_sr_menu_option, \
do_search, get_search_results, find_search_result, get_search_result_names, check_search_result, \
wait_for, wait_for_elem, wait_for_not_elem, find_child, find_children, \
set_elem_text, set_toast_marker, check_toast, send_upload_data, check_ask_dialog, check_error_msg, \
change_image, get_article_row
set_elem_text, set_toast_marker, check_toast, send_upload_data, change_image, get_article_row, \
check_ask_dialog, check_error_msg, check_constraint_warnings
from asl_articles.tests.react_select import ReactSelect
# ---------------------------------------------------------------------
@ -29,29 +29,33 @@ def test_edit_article( webdriver, flask_app, dbconn ):
"title": " Updated title ",
"subtitle": " Updated subtitle ",
"snippet": " Updated snippet. ",
"pageno": " p123 ",
"pageno": " 123 ",
"authors": [ "+Fred Nerk", "+Joe Blow" ],
"tags": [ "+abc", "+xyz" ],
"url": " http://updated-article.com ",
} )
# check that the search result was updated in the UI
sr = check_search_result( "Updated title", _check_sr, [
"Updated title", "Updated subtitle", "Updated snippet.", "p123", ["abc","xyz"], "http://updated-article.com/"
"Updated title", "Updated subtitle", "Updated snippet.", "123",
[ "Fred Nerk", "Joe Blow" ],
[ "abc", "xyz" ],
"http://updated-article.com/"
] )
# try to remove all fields from the article (should fail)
edit_article( sr,
{ "title": "", "subtitle": "", "snippet": "", "tags": ["-abc","-xyz"], "url": "" },
expected_error = "Please specify the article's title."
)
# enter something for the name
dlg = find_child( "#article-form" ) # nb: the form is still on-screen
set_elem_text( find_child( ".row.title input", dlg ), "Tin Cans Rock!" )
find_child( "button.ok", dlg ).click()
# remove all fields from the article
edit_article( sr, {
"title": "Tin Cans Rock!",
"subtitle": "",
"snippet": "",
"pageno": "",
"authors": [ "-Fred Nerk", "-Joe Blow" ],
"tags": [ "-abc", "-xyz" ],
"url": "",
} )
# check that the search result was updated in the UI
expected = [ "Tin Cans Rock!", None, "", "", [], None ]
expected = [ "Tin Cans Rock!", None, "", "", [], [], None ]
check_search_result( expected[0], _check_sr, expected )
# check that the article was updated in the database
@ -67,22 +71,24 @@ def test_create_article( webdriver, flask_app, dbconn ):
init_tests( webdriver, flask_app, dbconn, fixtures="articles.json" )
do_search( SEARCH_ALL_ARTICLES )
# try creating a article with no name (should fail)
create_article( {}, toast_type=None )
check_error_msg( "Please specify the article's title." )
# enter a name and other details
edit_article( None, { # nb: the form is still on-screen
# create a new article
create_article( {
"title": "New article",
"subtitle": "New subtitle",
"snippet": "New snippet.",
"pageno": "99",
"authors": [ "+Me" ],
"tags": [ "+111", "+222", "+333" ],
"url": "http://new-snippet.com"
} )
# check that the new article appears in the UI
expected = [ "New article", "New subtitle", "New snippet.", "99", ["111","222","333"], "http://new-snippet.com/" ]
expected = [
"New article", "New subtitle", "New snippet.", "99",
[ "Me" ],
[ "111", "222", "333" ],
"http://new-snippet.com/"
]
check_search_result( expected[0], _check_sr, expected )
# check that the new article has been saved in the database
@ -91,6 +97,81 @@ def test_create_article( webdriver, flask_app, dbconn ):
# ---------------------------------------------------------------------
def test_constraints( webdriver, flask_app, dbconn ):
"""Test constraint validation."""
# initialize
init_tests( webdriver, flask_app, dbconn, enable_constraints=1, fixtures="publications.json" )
# try to create an article with no title
dlg = create_article( {}, expected_error="Please give it a title." )
def do_create_test( vals, expected ):
return create_article( vals, dlg=dlg, expected_constraints=expected )
def do_edit_test( sr, vals, expected ):
return edit_article( sr, vals, expected_constraints=expected )
# set the article's title
do_create_test( { "title": "New article" }, [
"No publication was specified.",
"No snippet was provided.",
"No authors were specified."
] )
# set the article's page number
do_create_test( { "pageno": 99 }, [
"No publication was specified.",
"A page number was specified but no publication.",
"No snippet was provided.",
"No authors were specified."
] )
# assign the article to a publisher
do_create_test( { "publication": "MMP News", "pageno": "" }, [
"No page number was specified.",
"No snippet was provided.",
"No authors were specified."
] )
# set a non-numeric page number
do_create_test( { "pageno": "foo!" }, [
"The page number is not numeric.",
"No snippet was provided.",
"No authors were specified."
] )
# set the article's page number and provide a snippet
do_create_test( { "pageno": 123, "snippet": "Article snippet." }, [
"No authors were specified."
] )
# accept the constraint warnings
find_child( "button.ok", dlg ).click()
find_child( "#ask button.ok" ).click()
results = wait_for( 2, get_search_results )
article_sr = results[0]
# check that the search result was updated in the UI
check_search_result( article_sr, _check_sr, [
"New article", "", "Article snippet.", "123", [], [], None
] )
# try editing the article
dlg = do_edit_test( article_sr, {}, [
"No authors were specified."
] )
find_child( "button.cancel", dlg ).click()
# set the article's author
do_edit_test( article_sr, { "authors": ["+Joe Blow"] }, None )
# check that the search result was updated in the UI
check_search_result( article_sr, _check_sr, [
"New article", "", "Article snippet.", "123", ["Joe Blow"], [], None
] )
# ---------------------------------------------------------------------
def test_delete_article( webdriver, flask_app, dbconn ):
"""Test deleting articles."""
@ -293,6 +374,7 @@ def test_unicode( webdriver, flask_app, dbconn ):
"s.korea = \ud55c\uad6d",
"greece = \u0395\u03bb\u03bb\u03ac\u03b4\u03b1",
"",
[],
[ "\u0e51", "\u0e52", "\u0e53" ],
"http://xn--3e0b707e.com/"
] )
@ -321,7 +403,7 @@ def test_clean_html( webdriver, flask_app, dbconn ):
"title: bold xxx italic {}".format( replace[1] ),
"italicized subtitle {}".format( replace[1] ),
"bad stuff here: {}".format( replace[1] ),
"", [], None
"", [], [], None
] )
assert find_child( ".title", sr ).get_attribute( "innerHTML" ) \
== "title: <span> <b>bold</b> xxx <i>italic</i> {}</span>".format( replace[1] )
@ -366,29 +448,39 @@ def test_timestamps( webdriver, flask_app, dbconn ):
# ---------------------------------------------------------------------
def create_article( vals, toast_type="info" ):
def create_article( vals, toast_type="info", expected_error=None, expected_constraints=None, dlg=None ):
"""Create a new article."""
# initialize
if toast_type:
set_toast_marker( toast_type )
set_toast_marker( toast_type )
# create the new article
select_main_menu_option( "new-article" )
dlg = wait_for_elem( 2, "#article-form" )
if not dlg:
select_main_menu_option( "new-article" )
dlg = wait_for_elem( 2, "#article-form" )
_update_values( dlg, vals )
find_child( "button.ok", dlg ).click()
if toast_type:
# check that the new article was created successfully
# check what happened
if expected_error:
# we were expecting an error, confirm the error message
check_error_msg( expected_error )
return dlg # nb: the dialog is left on-screen
elif expected_constraints:
# we were expecting constraint warnings, confirm them
check_constraint_warnings( "Do you want to create this article?", expected_constraints, "cancel" )
return dlg # nb: the dialog is left on-screen
else:
# we were expecting the create to work, confirm this
wait_for( 2,
lambda: check_toast( toast_type, "created OK", contains=True )
)
wait_for_not_elem( 2, "#article-form" )
return None
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def edit_article( sr, vals, toast_type="info", expected_error=None ): #pylint: disable=too-many-branches
def edit_article( sr, vals, toast_type="info", expected_error=None, expected_constraints=None ): #pylint: disable=too-many-branches
"""Edit a article's details."""
# initialize
@ -407,6 +499,11 @@ def edit_article( sr, vals, toast_type="info", expected_error=None ): #pylint: d
if expected_error:
# we were expecting an error, confirm the error message
check_error_msg( expected_error )
return dlg # nb: the dialog is left on-screen
elif expected_constraints:
# we were expecting constraint warnings, confirm them
check_constraint_warnings( "Do you want to update this article?", expected_constraints, "cancel" )
return dlg # nb: the dialog is left on-screen
else:
# we were expecting the update to work, confirm this
expected = "updated OK" if sr else "created OK"
@ -414,6 +511,7 @@ def edit_article( sr, vals, toast_type="info", expected_error=None ): #pylint: d
lambda: check_toast( toast_type, expected, contains=True )
)
wait_for_not_elem( 2, "#article-form" )
return None
def _update_values( dlg, vals ):
"""Update an article's values in the form."""
@ -461,16 +559,21 @@ def _check_sr( sr, expected ): #pylint: disable=too-many-return-statements
if find_child( ".snippet", sr ).text != expected[2]:
return False
# check the authors
authors = [ t.text for t in find_children( ".author", sr ) ]
if authors != expected[4]:
return False
# check the tags
tags = [ t.text for t in find_children( ".tag", sr ) ]
if tags != expected[4]:
if tags != expected[5]:
return False
# check the article's link
elem = find_child( "a.open-link", sr )
if expected[5]:
if expected[6]:
assert elem
if elem.get_attribute( "href" ) != expected[5]:
if elem.get_attribute( "href" ) != expected[6]:
return False
else:
assert elem is None

@ -15,7 +15,7 @@ from asl_articles.tests.test_articles import create_article, edit_article
from asl_articles.tests.utils import init_tests, load_fixtures, select_main_menu_option, select_sr_menu_option, \
do_search, get_search_results, get_search_result_names, check_search_result, \
wait_for, wait_for_elem, wait_for_not_elem, find_child, find_children, find_search_result, set_elem_text, \
set_toast_marker, check_toast, send_upload_data, check_ask_dialog, check_error_msg, \
set_toast_marker, check_toast, send_upload_data, check_ask_dialog, check_error_msg, check_constraint_warnings, \
change_image, get_publication_row
from asl_articles.tests.react_select import ReactSelect
@ -40,19 +40,29 @@ def test_edit_publication( webdriver, flask_app, dbconn ):
} )
# check that the search result was updated in the UI
expected = [ "ASL Journal (updated)", "2a", "Jan 2020",
"Updated ASLJ description.", ["abc","xyz"], "http://aslj-updated.com/"
]
sr = find_search_result( "ASL Journal (updated) (2a)" )
check_search_result( sr, _check_sr, expected )
check_search_result( sr, _check_sr, [
"ASL Journal (updated)", "2a", "Jan 2020",
"Updated ASLJ description.", ["abc","xyz"], "http://aslj-updated.com/"
] )
# NOTE: We used to try to remove all fields from "ASL Journal #2" (which should fail, since we can't
# have an empty name), but we can no longer remove an existing publication name (which is OK, since we
# don't want to allow that, only change it).
# remove all fields from the publication
edit_publication( sr, {
"name": "ASLJ",
"edition": "",
"pub_date": "",
"description": "",
"tags": [ "-abc", "-xyz" ],
"url": "",
} )
# check that the search result was updated in the UI
expected = [ "ASLJ", "", "", "", [], "" ]
check_search_result( sr, _check_sr, expected )
# check that the publication was updated in the database
results = do_search( SEARCH_ALL_PUBLICATIONS )
sr = find_search_result( "ASL Journal (updated) (2a)", results )
sr = find_search_result( "ASLJ", results )
check_search_result( sr, _check_sr, expected )
# ---------------------------------------------------------------------
@ -64,12 +74,8 @@ def test_create_publication( webdriver, flask_app, dbconn ):
init_tests( webdriver, flask_app, dbconn, fixtures="publications.json" )
do_search( SEARCH_ALL_PUBLICATIONS )
# try creating a publication with no name (should fail)
create_publication( {}, toast_type=None )
check_error_msg( "Please specify the publication's name." )
# enter a name and other details
edit_publication( None, { # nb: the form is still on-screen
# create a new publication
create_publication( {
"name": "New publication",
"edition": "#1",
"pub_date": "1st January, 1900",
@ -91,6 +97,59 @@ def test_create_publication( webdriver, flask_app, dbconn ):
# ---------------------------------------------------------------------
def test_constraints( webdriver, flask_app, dbconn ):
"""Test constraint validation."""
# initialize
init_tests( webdriver, flask_app, dbconn, enable_constraints=1, fixtures="publications.json" )
# try to create a publication with no name
dlg = create_publication( {}, expected_error="Please give it a name." )
def do_create_test( vals, expected ):
return create_publication( vals, dlg=dlg, expected_constraints=expected )
def do_edit_test( sr, vals, expected ):
return edit_publication( sr, vals, expected_constraints=expected )
# set the publication's name
do_create_test( { "name": "ASL Journal" }, [
"The publication's edition was not specified.",
"The publication date was not specified.",
"A publisher was not specified.",
] )
# try to create a duplicate publication
create_publication( { "edition": 1 }, dlg=dlg,
expected_error = "There is already a publication with this name/edition."
)
# set the publication's edition and date
do_create_test( { "edition": 3, "pub_date": "yesterday" }, [
"A publisher was not specified.",
] )
# accept the constraint warnings
find_child( "button.ok", dlg ).click()
find_child( "#ask button.ok" ).click()
results = wait_for( 2, get_search_results )
pub_sr = results[0]
# check that the search result was updated in the UI
check_search_result( pub_sr, _check_sr, [
"ASL Journal", "3", "yesterday", "", [], ""
] )
# try editing the publication
dlg = do_edit_test( pub_sr, {}, [
"A publisher was not specified.",
] )
find_child( "button.cancel", dlg ).click()
# set the publisher
do_edit_test( pub_sr, { "publisher": "Avalon Hill" }, None )
# ---------------------------------------------------------------------
def test_delete_publication( webdriver, flask_app, dbconn ):
"""Test deleting publications."""
@ -505,27 +564,37 @@ def test_article_order( webdriver, flask_app, dbconn ):
# ---------------------------------------------------------------------
def create_publication( vals, toast_type="info" ):
def create_publication( vals, toast_type="info", expected_error=None, expected_constraints=None, dlg=None ):
"""Create a new publication."""
# initialize
if toast_type:
set_toast_marker( toast_type )
set_toast_marker( toast_type )
# create the new publication
select_main_menu_option( "new-publication" )
dlg = wait_for_elem( 2, "#publication-form" )
if not dlg:
select_main_menu_option( "new-publication" )
dlg = wait_for_elem( 2, "#publication-form" )
_update_values( dlg, vals )
find_child( "button.ok", dlg ).click()
if toast_type:
# check that the new publication was created successfully
# check what happened
if expected_error:
# we were expecting an error, confirm the error message
check_error_msg( expected_error )
return dlg # nb: the dialog is left on-screen
elif expected_constraints:
# we were expecting constraint warnings, confirm them
check_constraint_warnings( "Do you want to create this publication?", expected_constraints, "cancel" )
return dlg # nb: the dialog is left on-screen
else:
# we were expecting the create to work, confirm this
wait_for( 2,
lambda: check_toast( toast_type, "created OK", contains=True )
)
wait_for_not_elem( 2, "#publication-form" )
return None
def edit_publication( sr, vals, toast_type="info", expected_error=None ):
def edit_publication( sr, vals, toast_type="info", expected_error=None, expected_constraints=None ):
"""Edit a publication's details."""
# initialize
@ -544,6 +613,11 @@ def edit_publication( sr, vals, toast_type="info", expected_error=None ):
if expected_error:
# we were expecting an error, confirm the error message
check_error_msg( expected_error )
return dlg # nb: the dialog is left on-screen
elif expected_constraints:
# we were expecting constraint warnings, confirm them
check_constraint_warnings( "Do you want to update this publication?", expected_constraints, "cancel" )
return dlg # nb: the dialog is left on-screen
else:
# we were expecting the update to work, confirm this
expected = "updated OK" if sr else "created OK"
@ -551,6 +625,7 @@ def edit_publication( sr, vals, toast_type="info", expected_error=None ):
lambda: check_toast( toast_type, expected, contains=True )
)
wait_for_not_elem( 2, "#publication-form" )
return None
def _update_values( dlg, vals ):
"""Update a publication's values in the form."""

@ -11,8 +11,8 @@ from asl_articles.search import SEARCH_ALL, SEARCH_ALL_PUBLISHERS
from asl_articles.tests.utils import init_tests, load_fixtures, select_main_menu_option, select_sr_menu_option, \
do_search, get_search_results, get_search_result_names, check_search_result, \
wait_for, wait_for_elem, wait_for_not_elem, find_child, find_search_result, set_elem_text, \
set_toast_marker, check_toast, send_upload_data, check_ask_dialog, check_error_msg, \
change_image, get_publisher_row
set_toast_marker, check_toast, send_upload_data, change_image, get_publisher_row, \
check_ask_dialog, check_error_msg
# ---------------------------------------------------------------------
@ -36,19 +36,15 @@ def test_edit_publisher( webdriver, flask_app, dbconn ):
"Avalon Hill (updated)", "Updated AH description.", "http://ah-updated.com/"
] )
# try to remove all fields from the publisher (should fail)
edit_publisher( sr,
{ "name": "", "description": "", "url": "" },
expected_error = "Please specify the publisher's name."
)
# enter something for the name
dlg = find_child( "#publisher-form" )
set_elem_text( find_child( ".row.name input", dlg ), "Updated Avalon Hill" )
find_child( "button.ok", dlg ).click()
# remove all fields from the publisher
edit_publisher( sr, {
"name": "AH",
"description": "",
"url": ""
} )
# check that the search result was updated in the UI
expected = [ "Updated Avalon Hill", "", None ]
expected = [ "AH", "", None ]
check_search_result( expected[0], _check_sr, expected )
# check that the publisher was updated in the database
@ -64,12 +60,8 @@ def test_create_publisher( webdriver, flask_app, dbconn ):
init_tests( webdriver, flask_app, dbconn, fixtures="publishers.json" )
do_search( SEARCH_ALL_PUBLISHERS )
# try creating a publisher with no name (should fail)
create_publisher( {}, toast_type=None )
check_error_msg( "Please specify the publisher's name." )
# enter a name and other details
edit_publisher( None, { # nb: the form is still on-screen
# create a new publisher
create_publisher( {
"name": "New publisher",
"url": "http://new-publisher.com",
"description": "New publisher description."
@ -85,6 +77,32 @@ def test_create_publisher( webdriver, flask_app, dbconn ):
# ---------------------------------------------------------------------
def test_constraints( webdriver, flask_app, dbconn ):
"""Test constraint validation."""
# initialize
init_tests( webdriver, flask_app, dbconn, enable_constraints=1, fixtures="publishers.json" )
# try to create a publisher with no title
dlg = create_publisher( {}, expected_error="Please give them a name." )
# try to create a duplicate publisher
create_publisher( { "name": "Avalon Hill" }, dlg=dlg,
expected_error = "There is already a publisher with this name."
)
# set the publisher's name
create_publisher( { "name": "Joe Publisher" }, dlg=dlg )
# check that the search result was updated in the UI
expected = [ "Joe Publisher", "", "" ]
sr = check_search_result( expected[0], _check_sr, expected )
# try to remove the publisher's name
edit_publisher( sr, { "name": " " }, expected_error="Please give them a name." )
# ---------------------------------------------------------------------
def test_delete_publisher( webdriver, flask_app, dbconn ):
"""Test deleting publishers."""
@ -384,25 +402,31 @@ def test_timestamps( webdriver, flask_app, dbconn ):
# ---------------------------------------------------------------------
def create_publisher( vals, toast_type="info" ):
def create_publisher( vals, toast_type="info", expected_error=None, dlg=None ):
"""Create a new publisher."""
# initialize
if toast_type:
set_toast_marker( toast_type )
set_toast_marker( toast_type )
# create the new publisher
select_main_menu_option( "new-publisher" )
dlg = wait_for_elem( 2, "#publisher-form" )
if not dlg:
select_main_menu_option( "new-publisher" )
dlg = wait_for_elem( 2, "#publisher-form" )
_update_values( dlg, vals )
find_child( "button.ok", dlg ).click()
if toast_type:
# check that the new publisher was created successfully
# check what happened
if expected_error:
# we were expecting an error, confirm the error message
check_error_msg( expected_error )
return dlg # nb: the dialog is left on-screen
else:
# we were expecting the create to work, confirm this
wait_for( 2,
lambda: check_toast( toast_type, "created OK", contains=True )
)
wait_for_not_elem( 2, "#publisher-form" )
return None
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

@ -17,6 +17,7 @@ from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException, TimeoutException
from asl_articles import search
from asl_articles.utils import to_bool
import asl_articles.models
_webdriver = None
@ -48,6 +49,8 @@ def init_tests( webdriver, flask_app, dbconn, **kwargs ):
# load the home page
if webdriver:
if not to_bool( kwargs.pop( "enable_constraints", False ) ):
kwargs[ "disable_constraints" ] = 1
webdriver.get( webdriver.make_url( "/", **kwargs ) )
wait_for_elem( 2, "#search-form" )
@ -370,6 +373,14 @@ def check_error_msg( expected ):
find_child( "#ask .MuiDialogActions-root button.ok" ).click()
wait_for( 2, lambda: find_child( "#ask" ) is None )
def check_constraint_warnings( expected_caption, expected_constraints, click_on ):
"""Check that a constraints warning dialog is being shown, and its contents."""
dlg = find_child( "#ask" )
assert find_child( ".caption", dlg ).text == expected_caption
constraints = [ c.text for c in find_children( ".constraint", dlg ) ]
assert set( constraints ) == set( expected_constraints )
find_child( ".MuiDialogActions-root button.{}".format( click_on ), dlg ).click()
def check_string( val, expected, contains=False ):
"""Compare a value with its expected value."""
if contains:

@ -16,6 +16,7 @@
.MuiDialogTitle-root { padding: 10px 16px 6px 16px !important ; }
.MuiDialogContent-root>div { margin-bottom: 1em ; }
.MuiDialogContent-root p { margin-top: 0.5em ; }
.MuiDialogActions-root { background: #f0ffff ; }
.MuiDialogActions-root .MuiButton-text { padding: 6px 8px 2px 8px ; }
.MuiDialogActions-root button:hover { background: #40a0c0 ; color: white ; }

@ -12,6 +12,7 @@ import { PublicationSearchResult } from "./PublicationSearchResult" ;
import { ArticleSearchResult } from "./ArticleSearchResult" ;
import ModalForm from "./ModalForm";
import AskDialog from "./AskDialog" ;
import { makeSmartBulletList } from "./utils.js" ;
import "./App.css" ;
const axios = require( "axios" ) ;
@ -37,6 +38,7 @@ export default class App extends React.Component
this.args = queryString.parse( window.location.search ) ;
this._storeMsgs = this.isTestMode() && this.args.store_msgs ;
this._disableSearchResultHighlighting = this.isTestMode() && this.args.no_sr_hilite ;
this._disableConstraints = this.isTestMode() && this.args.disable_constraints ;
this._fakeUploads = this.isTestMode() && this.args.fake_uploads ;
// initialize
@ -242,16 +244,7 @@ export default class App extends React.Component
}
showWarnings( caption, warnings ) {
let content ;
if ( !warnings || warnings.length === 0 )
content = caption ;
else if ( warnings.length === 1 )
content = <div> {caption} <p> {warnings[0]} </p> </div> ;
else {
let bullets = warnings.map( (warning,i) => <li key={i}> {warning} </li> ) ;
content = <div> {caption} <ul> {bullets} </ul> </div> ;
}
this.showWarningToast( content ) ;
this.showWarningToast( makeSmartBulletList( caption, warnings ) ) ;
}
ask( content, iconType, buttons ) {
@ -350,6 +343,7 @@ export default class App extends React.Component
}
isTestMode() { return process.env.REACT_APP_TEST_MODE ; }
isDisableConstraints() { return this._disableConstraints ; }
isFakeUploads() { return this._fakeUploads ; }
setTestAttribute( obj, attrName, attrVal ) {
// set an attribute on an element (for testing porpoises)

@ -5,7 +5,7 @@ import { NEW_ARTICLE_PUB_PRIORITY_CUTOFF } from "./constants.js" ;
import { PublicationSearchResult } from "./PublicationSearchResult.js" ;
import { gAppRef } from "./index.js" ;
import { ImageFileUploader } from "./FileUploader.js" ;
import { makeScenarioDisplayName, parseScenarioDisplayName, sortSelectableOptions, unloadCreatableSelect } from "./utils.js" ;
import { makeScenarioDisplayName, parseScenarioDisplayName, checkConstraints, sortSelectableOptions, unloadCreatableSelect, isNumeric } from "./utils.js" ;
// --------------------------------------------------------------------
@ -190,12 +190,24 @@ export class ArticleSearchResult2
newVals.imageData = imageData ;
newVals.imageFilename = imageFilename ;
}
if ( newVals.article_title === "" ) {
gAppRef.showErrorMsg( <div> Please specify the article's title. </div>) ;
return ;
}
// notify the caller about the new details
notify( newVals, refs ) ;
// check the new values
const required = [
[ () => newVals.article_title === "", "Please give it a title." ],
] ;
const optional = [
[ () => newVals.pub_id === null, "No publication was specified." ],
[ () => newVals.article_pageno === "" && newVals.pub_id !== null, "No page number was specified." ],
[ () => newVals.article_pageno !== "" && newVals.pub_id === null, "A page number was specified but no publication." ],
[ () => newVals.article_pageno !== "" && !isNumeric(newVals.article_pageno), "The page number is not numeric." ],
[ () => newVals.article_snippet === "", "No snippet was provided." ],
[ () => newVals.article_authors.length === 0, "No authors were specified." ],
] ;
const verb = isNew ? "create" : "update" ;
checkConstraints(
required, "Can't " + verb + " this article.",
optional, "Do you want to " + verb + " this article?",
() => notify( newVals, refs )
) ;
},
Cancel: () => { gAppRef.closeModalForm() ; },
} ;

@ -4,7 +4,7 @@ import CreatableSelect from "react-select/creatable" ;
import ReactDragListView from "react-drag-listview/lib/index.js" ;
import { gAppRef } from "./index.js" ;
import { ImageFileUploader } from "./FileUploader.js" ;
import { sortSelectableOptions, unloadCreatableSelect } from "./utils.js" ;
import { checkConstraints, sortSelectableOptions, unloadCreatableSelect, ciCompare, isNumeric } from "./utils.js" ;
// --------------------------------------------------------------------
@ -155,6 +155,38 @@ export class PublicationSearchResult2
return content ;
}
function checkForDupe( vals ) {
// check for an existing publication name/edition
for ( let pub of Object.entries(gAppRef.caches.publications) ) {
if ( ciCompare( pub[1].pub_name, vals.pub_name ) !== 0 )
continue ;
if ( ! pub[1].pub_edition && ! vals.pub_edition )
return true ;
if ( ! pub[1].pub_edition || ! vals.pub_edition )
continue ;
if ( ciCompare( pub[1].pub_edition, vals.pub_edition ) === 0 )
return true ;
}
return false ;
}
function checkArticlePageNumbers( articles ) {
// check the order of the article page numbers
let curr_pageno = null ;
if ( articles === null )
return false ;
for ( let i=0 ; i < articles.length ; ++i ) {
if ( ! isNumeric( articles[i].article_pageno ) )
continue ;
const pageno = parseInt( articles[i].article_pageno, 10 ) ;
if ( curr_pageno !== null ) {
if ( pageno < curr_pageno )
return true ;
}
curr_pageno = pageno ;
}
return false ;
}
// prepare the form buttons
const buttons = {
OK: () => {
@ -176,12 +208,22 @@ export class PublicationSearchResult2
newVals.imageData = imageData ;
newVals.imageFilename = imageFilename ;
}
if ( newVals.pub_name === undefined || newVals.pub_name === "" ) {
gAppRef.showErrorMsg( <div> Please specify the publication's name. </div>) ;
return ;
}
// notify the caller about the new details
notify( newVals, refs ) ;
const required = [
[ () => newVals.pub_name === undefined, "Please give it a name." ],
[ () => isNew && checkForDupe(newVals), "There is already a publication with this name/edition." ],
] ;
const optional = [
[ () => newVals.pub_name !== undefined && newVals.pub_edition === "", "The publication's edition was not specified." ],
[ () => newVals.pub_date === "", "The publication date was not specified." ],
[ () => newVals.publ_id === null, "A publisher was not specified." ],
[ () => checkArticlePageNumbers(articles), "Some article page numbers are out of order." ],
] ;
const verb = isNew ? "create" : "update" ;
checkConstraints(
required, "Can't " + verb + " this publication.",
optional, "Do you want to " + verb + " this publication?",
() => notify( newVals, refs )
) ;
},
Cancel: () => { gAppRef.closeModalForm() ; },
} ;

@ -1,6 +1,7 @@
import React from "react" ;
import { gAppRef } from "./index.js" ;
import { ImageFileUploader } from "./FileUploader.js" ;
import { checkConstraints, ciCompare } from "./utils.js" ;
// --------------------------------------------------------------------
@ -59,6 +60,15 @@ export class PublisherSearchResult2
</div>
</div> ;
function checkForDupe( publName ) {
// check for an existing publisher
for ( let publ of Object.entries(gAppRef.caches.publishers) ) {
if ( ciCompare( publName, publ[1].publ_name ) === 0 )
return true ;
}
return false ;
}
// prepare the form buttons
const buttons = {
OK: () => {
@ -70,12 +80,17 @@ export class PublisherSearchResult2
newVals.imageData = imageData ;
newVals.imageFilename = imageFilename ;
}
if ( newVals.publ_name === "" ) {
gAppRef.showErrorMsg( <div> Please specify the publisher's name. </div>) ;
return ;
}
// notify the caller about the new details
notify( newVals, refs ) ;
// check the new values
const required = [
[ () => newVals.publ_name === "", "Please give them a name." ],
[ () => isNew && checkForDupe(newVals.publ_name), "There is already a publisher with this name." ],
] ;
const verb = isNew ? "create" : "update" ;
checkConstraints(
required, "Can't " + verb + " this publisher.",
null, null,
() => notify( newVals, refs )
) ;
},
Cancel: () => { gAppRef.closeModalForm() ; },
} ;

@ -1,7 +1,52 @@
import React from "react" ;
import ReactDOMServer from "react-dom/server" ;
import { gAppRef } from "./index.js" ;
// --------------------------------------------------------------------
export function checkConstraints( required, requiredCaption, optional, optionalCaption, accept ) {
// check if constraints have been disabled (for testing porpoises only)
if ( gAppRef.isDisableConstraints() ) {
accept() ;
return ;
}
// check the required constraints
let msgs = [] ;
if ( required ) {
for ( let constraint of required ) {
if ( constraint[0]() )
msgs.push( constraint[1] ) ;
}
}
if ( msgs.length > 0 ) {
gAppRef.showErrorMsg( makeSmartBulletList( requiredCaption, msgs, "constraint" ) ) ;
return ;
}
// check the optional constraints
msgs = [] ;
if ( optional ) {
for ( let constraint of optional ) {
if ( constraint[0]() )
msgs.push( constraint[1] ) ;
}
}
if ( msgs.length > 0 ) {
// some constraints failed - ask the user if they want to continue
let content = <div style={{float:"left"}}> { makeSmartBulletList( optionalCaption, msgs, "constraint" ) } </div> ;
gAppRef.ask( content, "ask", {
OK: () => { accept() },
Cancel: null
} ) ;
return ;
}
// everything passed - accept the values
accept() ;
}
export function sortSelectableOptions( options ) {
options.sort( (lhs,rhs) => {
lhs = ReactDOMServer.renderToStaticMarkup( lhs.label ) ;
@ -108,6 +153,18 @@ export function makeCommaList( vals, extract ) {
return result ;
}
export function makeSmartBulletList( caption, vals, className ) {
caption = <div className="caption"> {caption} </div> ;
if ( !vals || vals.length === 0 )
return caption ;
else if ( vals.length === 1 )
return <div> {caption} <p className={className}> {vals[0]} </p> </div> ;
else {
let bullets = vals.map( (v,i) => <li key={i} className={className}> {v} </li> ) ;
return <div> {caption} <ul> {bullets} </ul> </div> ;
}
}
export function bytesDisplayString( nBytes )
{
if ( nBytes === 1 )
@ -130,3 +187,16 @@ export function pluralString( n, str1, str2 ) {
else
return n + " " + (str2 ? str2 : str1+"s") ;
}
export function ciCompare( lhs, rhs ) {
return lhs.localeCompare( rhs, undefined, { sensitivity: "base" } ) ;
}
export function isNumeric( val ) {
if ( val === null || val === undefined )
return false ;
val = val.trim() ;
if ( val === "" )
return false ;
return ! isNaN( val ) ;
}

Loading…
Cancel
Save