parent
5272bf9934
commit
5719699379
@ -0,0 +1,39 @@ |
||||
"""Added the 'publication' table. |
||||
|
||||
Revision ID: 6054f3f09626 |
||||
Revises: 39196521adc5 |
||||
Create Date: 2019-12-02 14:42:56.073111 |
||||
|
||||
""" |
||||
from alembic import op |
||||
import sqlalchemy as sa |
||||
|
||||
|
||||
# revision identifiers, used by Alembic. |
||||
revision = '6054f3f09626' |
||||
down_revision = '39196521adc5' |
||||
branch_labels = None |
||||
depends_on = None |
||||
|
||||
|
||||
def upgrade(): |
||||
# ### commands auto generated by Alembic - please adjust! ### |
||||
op.create_table('publication', |
||||
sa.Column('pub_id', sa.Integer(), nullable=False), |
||||
sa.Column('pub_name', sa.String(length=100), nullable=False), |
||||
sa.Column('pub_edition', sa.String(length=100), nullable=True), |
||||
sa.Column('pub_description', sa.String(length=1000), nullable=True), |
||||
sa.Column('pub_url', sa.String(length=500), nullable=True), |
||||
sa.Column('publ_id', sa.Integer(), nullable=True), |
||||
sa.Column('time_created', sa.TIMESTAMP(timezone=True), nullable=True), |
||||
sa.Column('time_updated', sa.TIMESTAMP(timezone=True), nullable=True), |
||||
sa.ForeignKeyConstraint(['publ_id'], ['publisher.publ_id'], ondelete='CASCADE'), |
||||
sa.PrimaryKeyConstraint('pub_id') |
||||
) |
||||
# ### end Alembic commands ### |
||||
|
||||
|
||||
def downgrade(): |
||||
# ### commands auto generated by Alembic - please adjust! ### |
||||
op.drop_table('publication') |
||||
# ### end Alembic commands ### |
@ -0,0 +1,108 @@ |
||||
""" Handle publication requests. """ |
||||
|
||||
import datetime |
||||
import logging |
||||
|
||||
from flask import request, jsonify, abort |
||||
|
||||
from asl_articles import app, db |
||||
from asl_articles.models import Publication |
||||
from asl_articles.utils import get_request_args, clean_request_args, make_ok_response, apply_attrs |
||||
|
||||
_logger = logging.getLogger( "db" ) |
||||
|
||||
_FIELD_NAMES = [ "pub_name", "pub_edition", "pub_description", "pub_url", "publ_id" ] |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@app.route( "/publications" ) |
||||
def get_publications(): |
||||
"""Get all publications.""" |
||||
return jsonify( do_get_publications() ) |
||||
|
||||
def do_get_publications(): |
||||
"""Get all publications.""" |
||||
# NOTE: The front-end maintains a cache of the publications, so as a convenience, |
||||
# we return the current list as part of the response to a create/update/delete operation. |
||||
results = list( Publication.query ) |
||||
return { r.pub_id: get_publication_vals(r) for r in results } |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@app.route( "/publication/<pub_id>" ) |
||||
def get_publication( pub_id ): |
||||
"""Get a publication.""" |
||||
_logger.debug( "Get publication: id=%s", pub_id ) |
||||
pub = Publication.query.get( pub_id ) |
||||
if not pub: |
||||
abort( 404 ) |
||||
_logger.debug( "- %s", pub ) |
||||
return jsonify( get_publication_vals( pub ) ) |
||||
|
||||
def get_publication_vals( pub ): |
||||
"""Extract public fields from a Publication record.""" |
||||
return { |
||||
"pub_id": pub.pub_id, |
||||
"pub_name": pub.pub_name, |
||||
"pub_edition": pub.pub_edition, |
||||
"pub_description": pub.pub_description, |
||||
"pub_url": pub.pub_url, |
||||
"publ_id": pub.publ_id, |
||||
} |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@app.route( "/publication/create", methods=["POST"] ) |
||||
def create_publication(): |
||||
"""Create a publication.""" |
||||
vals = get_request_args( request.json, _FIELD_NAMES, |
||||
log = ( _logger, "Create publication:" ) |
||||
) |
||||
cleaned = clean_request_args( vals, _FIELD_NAMES, _logger ) |
||||
vals[ "time_created" ] = datetime.datetime.now() |
||||
pub = Publication( **vals ) |
||||
db.session.add( pub ) #pylint: disable=no-member |
||||
db.session.commit() #pylint: disable=no-member |
||||
_logger.debug( "- New ID: %d", pub.pub_id ) |
||||
extras = { "pub_id": pub.pub_id } |
||||
if request.args.get( "list" ): |
||||
extras[ "publications" ] = do_get_publications() |
||||
return make_ok_response( cleaned=cleaned, extras=extras ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@app.route( "/publication/update", methods=["POST"] ) |
||||
def update_publication(): |
||||
"""Update a publication.""" |
||||
pub_id = request.json[ "pub_id" ] |
||||
vals = get_request_args( request.json, _FIELD_NAMES, |
||||
log = ( _logger, "Update publication: id={}".format( pub_id ) ) |
||||
) |
||||
cleaned = clean_request_args( vals, _FIELD_NAMES, _logger ) |
||||
vals[ "time_updated" ] = datetime.datetime.now() |
||||
pub = Publication.query.get( pub_id ) |
||||
if not pub: |
||||
abort( 404 ) |
||||
apply_attrs( pub, vals ) |
||||
db.session.commit() #pylint: disable=no-member |
||||
extras = {} |
||||
if request.args.get( "list" ): |
||||
extras[ "publications" ] = do_get_publications() |
||||
return make_ok_response( cleaned=cleaned, extras=extras ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@app.route( "/publication/delete/<pub_id>" ) |
||||
def delete_publication( pub_id ): |
||||
"""Delete a publication.""" |
||||
_logger.debug( "Delete publication: id=%s", pub_id ) |
||||
pub = Publication.query.get( pub_id ) |
||||
if not pub: |
||||
abort( 404 ) |
||||
_logger.debug( "- %s", pub ) |
||||
db.session.delete( pub ) #pylint: disable=no-member |
||||
db.session.commit() #pylint: disable=no-member |
||||
extras = {} |
||||
if request.args.get( "list" ): |
||||
extras[ "publications" ] = do_get_publications() |
||||
return make_ok_response( extras=extras ) |
@ -0,0 +1,15 @@ |
||||
{ |
||||
|
||||
"publisher": [ |
||||
{ "publ_id": 1, "publ_name": "Cascading Deletes 0" }, |
||||
{ "publ_id": 2, "publ_name": "Cascading Deletes 1" }, |
||||
{ "publ_id": 3, "publ_name": "Cascading Deletes 2" } |
||||
], |
||||
|
||||
"publication": [ |
||||
{ "pub_name": "publication 1", "publ_id": "2" }, |
||||
{ "pub_name": "publication 2a", "publ_id": "3" }, |
||||
{ "pub_name": "publication 2b", "publ_id": "3" } |
||||
] |
||||
|
||||
} |
@ -0,0 +1,14 @@ |
||||
{ |
||||
|
||||
"publisher": [ |
||||
{ "publ_id": 1, "publ_name": "Avalon Hill", "publ_description": "AH description", "publ_url": "http://ah.com" }, |
||||
{ "publ_id": 2, "publ_name": "Multiman Publishing", "publ_url": "http://mmp.com" } |
||||
], |
||||
|
||||
"publication": [ |
||||
{ "pub_name": "ASL Journal", "pub_edition": "1", "pub_description": "ASL Journal #1", "pub_url": "http://aslj.com/1" }, |
||||
{ "pub_name": "ASL Journal", "pub_edition": "2", "pub_description": "ASL Journal #2", "pub_url": "http://aslj.com/2" }, |
||||
{ "pub_name": "MMP News" } |
||||
] |
||||
|
||||
} |
@ -0,0 +1,252 @@ |
||||
""" Test publication operations. """ |
||||
|
||||
from asl_articles.tests.utils import init_tests, init_db, do_search, get_result_names, \ |
||||
wait_for, wait_for_elem, find_child, find_children, set_elem_text, \ |
||||
set_toast_marker, check_toast, check_ask_dialog, check_error_msg |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_edit_publication( webdriver, flask_app, dbconn ): |
||||
"""Test editing publications.""" |
||||
|
||||
# initialize |
||||
init_tests( webdriver, flask_app ) |
||||
init_db( dbconn, "publications.json" ) |
||||
|
||||
# edit "ASL Journal #2" |
||||
results = do_search( "asl journal" ) |
||||
assert len(results) == 2 |
||||
result = results[1] |
||||
_edit_publication( result, { |
||||
"name": " ASL Journal (updated) ", |
||||
"edition": " 2a ", |
||||
"description": " Updated ASLJ description. ", |
||||
"url": " http://aslj-updated.com ", |
||||
} ) |
||||
|
||||
# check that the search result was updated in the UI |
||||
results = find_children( "#search-results .search-result" ) |
||||
result = results[1] |
||||
_check_result( result, [ "ASL Journal (updated)", "2a", "Updated ASLJ description.", "http://aslj-updated.com/" ] ) |
||||
|
||||
# try to remove all fields from "ASL Journal #2" (should fail) |
||||
_edit_publication( result, |
||||
{ "name": "", "edition": "", "description": "", "url": "" }, |
||||
expected_error = "Please specify the publication's name." |
||||
) |
||||
|
||||
# enter something for the name |
||||
dlg = find_child( "#modal-form" ) |
||||
set_elem_text( find_child( ".name input", dlg ), "Updated ASL Journal" ) |
||||
find_child( "button.ok", dlg ).click() |
||||
|
||||
# check that the search result was updated in the UI |
||||
results = find_children( "#search-results .search-result" ) |
||||
result = results[1] |
||||
assert find_child( ".name a", result ) is None |
||||
assert find_child( ".name", result ).text == "Updated ASL Journal" |
||||
assert find_child( ".description", result ).text == "" |
||||
|
||||
# check that the search result was updated in the database |
||||
results = do_search( "ASL Journal" ) |
||||
assert get_result_names( results ) == [ "ASL Journal (1)", "Updated ASL Journal" ] |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_create_publication( webdriver, flask_app, dbconn ): |
||||
"""Test creating new publications.""" |
||||
|
||||
# initialize |
||||
init_tests( webdriver, flask_app ) |
||||
init_db( dbconn, "basic.json" ) |
||||
|
||||
# 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 |
||||
dlg = find_child( "#modal-form" ) # nb: the form is still on-screen |
||||
set_elem_text( find_child( ".name input", dlg ), "New publication" ) |
||||
set_elem_text( find_child( ".edition input", dlg ), "#1" ) |
||||
set_elem_text( find_child( ".description textarea", dlg ), "New publication description." ) |
||||
set_elem_text( find_child( ".url input", dlg ), "http://new-publication.com" ) |
||||
set_toast_marker( "info" ) |
||||
find_child( "button.ok", dlg ).click() |
||||
wait_for( 2, |
||||
lambda: check_toast( "info", "created OK", contains=True ) |
||||
) |
||||
|
||||
# check that the new publication appears in the UI |
||||
def check_new_publication( result ): |
||||
_check_result( result, [ |
||||
"New publication", "#1", "New publication description.", "http://new-publication.com/" |
||||
] ) |
||||
results = find_children( "#search-results .search-result" ) |
||||
check_new_publication( results[0] ) |
||||
|
||||
# check that the new publication has been saved in the database |
||||
results = do_search( "new" ) |
||||
assert len( results ) == 1 |
||||
check_new_publication( results[0] ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_delete_publication( webdriver, flask_app, dbconn ): |
||||
"""Test deleting publications.""" |
||||
|
||||
# initialize |
||||
init_tests( webdriver, flask_app ) |
||||
init_db( dbconn, "publications.json" ) |
||||
|
||||
# start to delete publication "ASL Journal #1", but cancel the operation |
||||
results = do_search( "ASL Journal" ) |
||||
assert len(results) == 2 |
||||
result = results[1] |
||||
assert find_child( ".name", result ).text == "ASL Journal (2)" |
||||
find_child( ".delete", result ).click() |
||||
check_ask_dialog( ( "Do you want to delete", "ASL Journal (2)" ), "cancel" ) |
||||
|
||||
# check that search results are unchanged on-screen |
||||
results2 = find_children( "#search-results .search-result" ) |
||||
assert results2 == results |
||||
|
||||
# check that the search results are unchanged in the database |
||||
results3 = do_search( "ASL Journal" ) |
||||
assert results3 == results |
||||
|
||||
# delete the publication "ASL Journal 2" |
||||
result = results3[1] |
||||
assert find_child( ".name", result ).text == "ASL Journal (2)" |
||||
find_child( ".delete", result ).click() |
||||
set_toast_marker( "info" ) |
||||
check_ask_dialog( ( "Do you want to delete", "ASL Journal (2)" ), "ok" ) |
||||
wait_for( 2, |
||||
lambda: check_toast( "info", "The publication was deleted." ) |
||||
) |
||||
|
||||
# check that search result was removed on-screen |
||||
results = find_children( "#search-results .search-result" ) |
||||
assert get_result_names( results ) == [ "ASL Journal (1)" ] |
||||
|
||||
# check that the search result was deleted from the database |
||||
results = do_search( "ASL Journal" ) |
||||
assert get_result_names( results ) == [ "ASL Journal (1)" ] |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_unicode( webdriver, flask_app, dbconn ): |
||||
"""Test Unicode content.""" |
||||
|
||||
# initialize |
||||
init_tests( webdriver, flask_app ) |
||||
init_db( dbconn, "publications.json" ) |
||||
|
||||
# create a publication with Unicode content |
||||
_create_publication( { |
||||
"name": "japan = \u65e5\u672c", |
||||
"edition": "\u263a", |
||||
"url": "http://\ud55c\uad6d.com", |
||||
"description": "greece = \u0395\u03bb\u03bb\u03ac\u03b4\u03b1" |
||||
} ) |
||||
|
||||
# check that the new publication is showing the Unicode content correctly |
||||
results = do_search( "japan" ) |
||||
assert len( results ) == 1 |
||||
_check_result( results[0], [ |
||||
"japan = \u65e5\u672c", "\u263a", |
||||
"greece = \u0395\u03bb\u03bb\u03ac\u03b4\u03b1", |
||||
"http://xn--3e0b707e.com/" |
||||
] ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_clean_html( webdriver, flask_app, dbconn ): |
||||
"""Test cleaning HTML content.""" |
||||
|
||||
# initialize |
||||
init_tests( webdriver, flask_app ) |
||||
init_db( dbconn, "publications.json" ) |
||||
|
||||
# create a publication with HTML content |
||||
_create_publication( { |
||||
"name": "name: <span style='boo!'> <b>bold</b> <xxx>xxx</xxx> <i>italic</i>", |
||||
"edition": "<i>2</i>", |
||||
"description": "bad stuff here: <script>HCF</script>" |
||||
}, toast_type="warning" ) |
||||
|
||||
# check that the HTML was cleaned |
||||
results = wait_for( 2, |
||||
lambda: find_children( "#search-results .search-result" ) |
||||
) |
||||
assert len( results ) == 1 |
||||
result = results[0] |
||||
_check_result( result, [ "name: bold xxx italic", "2", "bad stuff here:", None ] ) |
||||
assert find_child( ".name span" ).get_attribute( "innerHTML" ) \ |
||||
== "name: <span> <b>bold</b> xxx <i>italic</i></span> (<i>2</i>)" |
||||
assert check_toast( "warning", "Some values had HTML removed.", contains=True ) |
||||
|
||||
# update the publication with new HTML content |
||||
_edit_publication( result, { |
||||
"name": "<div style='...'>updated</div>" |
||||
}, toast_type="warning" ) |
||||
def check_result(): |
||||
results = find_children( "#search-results .search-result" ) |
||||
assert len( results ) == 1 |
||||
result = results[0] |
||||
return find_child( ".name", result ).text == "updated (2)" |
||||
wait_for( 2, check_result ) |
||||
assert check_toast( "warning", "Some values had HTML removed.", contains=True ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def _create_publication( vals, toast_type="info" ): |
||||
"""Create a new publication.""" |
||||
# initialize |
||||
if toast_type: |
||||
set_toast_marker( toast_type ) |
||||
# create the new publication |
||||
find_child( "#menu .new-publication" ).click() |
||||
dlg = wait_for_elem( 2, "#modal-form" ) |
||||
for k,v in vals.items(): |
||||
sel = ".{} {}".format( k , "textarea" if k == "description" else "input" ) |
||||
set_elem_text( find_child( sel, dlg ), v ) |
||||
find_child( "button.ok", dlg ).click() |
||||
if toast_type: |
||||
# check that the new publication was created successfully |
||||
wait_for( 2, |
||||
lambda: check_toast( toast_type, "created OK", contains=True ) |
||||
) |
||||
|
||||
def _edit_publication( result, vals, toast_type="info", expected_error=None ): |
||||
"""Edit a publication's details.""" |
||||
# update the specified publication's details |
||||
find_child( ".edit", result ).click() |
||||
dlg = wait_for_elem( 2, "#modal-form" ) |
||||
for k,v in vals.items(): |
||||
sel = ".{} {}".format( k , "textarea" if k == "description" else "input" ) |
||||
set_elem_text( find_child( sel, dlg ), v ) |
||||
set_toast_marker( toast_type ) |
||||
find_child( "button.ok", dlg ).click() |
||||
if expected_error: |
||||
# we were expecting an error, confirm the error message |
||||
check_error_msg( expected_error ) |
||||
else: |
||||
# we were expecting the update to work, confirm this |
||||
wait_for( 2, |
||||
lambda: check_toast( toast_type, "updated OK", contains=True ) |
||||
) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def _check_result( result, expected ): |
||||
"""Check a result.""" |
||||
expected_name = expected[0] |
||||
if expected[1]: |
||||
expected_name += " ({})".format( expected[1] ) |
||||
assert find_child( ".name", result ).text == expected_name |
||||
assert find_child( ".description", result ).text == expected[2] |
||||
elem = find_child( ".name a", result ) |
||||
if elem: |
||||
assert elem.get_attribute( "href" ) == expected[3] |
||||
else: |
||||
assert expected[3] is None |
@ -0,0 +1,162 @@ |
||||
import React from "react" ; |
||||
import ReactDOMServer from "react-dom/server" ; |
||||
import Select from "react-select" ; |
||||
import { gAppRef } from "./index.js" ; |
||||
import { makeOptionalLink } from "./utils.js" ; |
||||
|
||||
const axios = require( "axios" ) ; |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
export class PublicationSearchResult extends React.Component |
||||
{ |
||||
|
||||
render() { |
||||
return ( <div className="search-result publication"> |
||||
<div className="name"> { makeOptionalLink( this._makeDisplayName(), this.props.data.pub_url ) } |
||||
<img src="/images/edit.png" className="edit" onClick={this.onEditPublication.bind(this)} alt="Edit this publication." /> |
||||
<img src="/images/delete.png" className="delete" onClick={this.onDeletePublication.bind(this)} alt="Delete this publication." /> |
||||
</div> |
||||
<div className="description" dangerouslySetInnerHTML={{__html: this.props.data.pub_description}} /> |
||||
</div> ) ; |
||||
} |
||||
|
||||
static onNewPublication( notify ) { |
||||
PublicationSearchResult._doEditPublication( {}, (newVals,refs) => { |
||||
axios.post( gAppRef.state.flaskBaseUrl + "/publication/create?list=1", newVals ) |
||||
.then( resp => { |
||||
// update the cached publications
|
||||
gAppRef.caches.publications = resp.data.publications ; |
||||
// unload any cleaned values
|
||||
for ( let r in refs ) { |
||||
if ( resp.data.cleaned && resp.data.cleaned[r] ) |
||||
newVals[ r ] = resp.data.cleaned[ r ] ; |
||||
} |
||||
// update the UI with the new details
|
||||
notify( resp.data.pub_id, newVals ) ; |
||||
if ( resp.data.warning ) |
||||
gAppRef.showWarningToast( <div> The new publication was created OK. <p> {resp.data.warning} </p> </div> ) ; |
||||
else |
||||
gAppRef.showInfoToast( <div> The new publication was created OK. </div> ) ; |
||||
gAppRef.closeModalForm() ; |
||||
} ) |
||||
.catch( err => { |
||||
gAppRef.showErrorMsg( <div> Couldn't create the publication: <div className="monospace"> {err.toString()} </div> </div> ) ; |
||||
} ) ; |
||||
} ) ; |
||||
} |
||||
|
||||
onEditPublication() { |
||||
PublicationSearchResult._doEditPublication( this.props.data, (newVals,refs) => { |
||||
// send the updated details to the server
|
||||
newVals.pub_id = this.props.data.pub_id ; |
||||
axios.post( gAppRef.state.flaskBaseUrl + "/publication/update?list=1", newVals ) |
||||
.then( resp => { |
||||
// update the cached publications
|
||||
gAppRef.caches.publications = resp.data.publications ; |
||||
// update the UI with the new details
|
||||
for ( let r in refs ) |
||||
this.props.data[ r ] = (resp.data.cleaned && resp.data.cleaned[r]) || newVals[r] ; |
||||
this.forceUpdate() ; |
||||
if ( resp.data.warning ) |
||||
gAppRef.showWarningToast( <div> The publication was updated OK. <p> {resp.data.warning} </p> </div> ) ; |
||||
else |
||||
gAppRef.showInfoToast( <div> The publication was updated OK. </div> ) ; |
||||
gAppRef.closeModalForm() ; |
||||
} ) |
||||
.catch( err => { |
||||
gAppRef.showErrorMsg( <div> Couldn't update the publication: <div className="monospace"> {err.toString()} </div> </div> ) ; |
||||
} ) ; |
||||
} ); |
||||
} |
||||
|
||||
static _doEditPublication( vals, notify ) { |
||||
let refs = {} ; |
||||
let publishers = [ { value: null, label: <i>(none)</i> } ] ; |
||||
let currPubl = 0 ; |
||||
for ( let p of Object.entries(gAppRef.caches.publishers) ) { |
||||
publishers.push( { |
||||
value: p[1].publ_id, |
||||
label: <span dangerouslySetInnerHTML={{__html: p[1].publ_name}} /> |
||||
} ) ; |
||||
if ( p[1].publ_id === vals.publ_id ) |
||||
currPubl = publishers.length - 1 ; |
||||
} |
||||
publishers.sort( (lhs,rhs) => { |
||||
return ReactDOMServer.renderToStaticMarkup( lhs.label ).localeCompare( ReactDOMServer.renderToStaticMarkup( rhs.label ) ) ; |
||||
} ) ; |
||||
const content = <div> |
||||
<div className="row name"> <label> Name: </label> |
||||
<input type="text" defaultValue={vals.pub_name} ref={(r) => refs.pub_name=r} /> |
||||
</div> |
||||
<div className="row edition"> <label> Edition: </label> |
||||
<input type="text" defaultValue={vals.pub_edition} ref={(r) => refs.pub_edition=r} /> |
||||
</div> |
||||
<div className="row publisher"> <label> Publisher: </label> |
||||
<Select options={publishers} isSearchable={true} |
||||
defaultValue = { publishers[ currPubl ] } |
||||
ref = { (r) => refs.publ_id=r } |
||||
/> |
||||
</div> |
||||
<div className="row description"> <label> Description: </label> |
||||
<textarea defaultValue={vals.pub_description} ref={(r) => refs.pub_description=r} /> |
||||
</div> |
||||
<div className="row url"> <label> Web: </label> |
||||
<input type="text" defaultValue={vals.pub_url} ref={(r) => refs.pub_url=r} /> |
||||
</div> |
||||
</div> ; |
||||
const buttons = { |
||||
OK: () => { |
||||
// unload the new values
|
||||
let newVals = {} ; |
||||
for ( let r in refs ) |
||||
newVals[ r ] = (r === "publ_id") ? refs[r].state.value && refs[r].state.value.value : refs[r].value.trim() ; |
||||
if ( newVals.pub_name === "" ) { |
||||
gAppRef.showErrorMsg( <div> Please specify the publication's name. </div>) ; |
||||
return ; |
||||
} |
||||
// notify the caller about the new details
|
||||
notify( newVals, refs ) ; |
||||
}, |
||||
Cancel: () => { gAppRef.closeModalForm() ; }, |
||||
} ; |
||||
const isNew = Object.keys( vals ).length === 0 ; |
||||
gAppRef.showModalForm( isNew?"New publication":"Edit publication", content, buttons ) ; |
||||
} |
||||
|
||||
onDeletePublication() { |
||||
// confirm the operation
|
||||
const content = ( <div> |
||||
Do you want to delete this publication? |
||||
<div style={{margin:"0.5em 0 0 2em",fontStyle:"italic"}} dangerouslySetInnerHTML = {{ __html: this._makeDisplayName() }} /> |
||||
</div> ) ; |
||||
gAppRef.ask( content, { |
||||
"OK": () => { |
||||
// delete the publication on the server
|
||||
axios.get( gAppRef.state.flaskBaseUrl + "/publication/delete/" + this.props.data.pub_id + "?list=1" ) |
||||
.then( resp => { |
||||
// update the cached publications
|
||||
gAppRef.caches.publications = resp.data.publications ; |
||||
// update the UI
|
||||
this.props.onDelete( "pub_id", this.props.data.pub_id ) ; |
||||
if ( resp.data.warning ) |
||||
gAppRef.showWarningToast( <div> The publication was deleted. <p> {resp.data.warning} </p> </div> ) ; |
||||
else |
||||
gAppRef.showInfoToast( <div> The publication was deleted. </div> ) ; |
||||
} ) |
||||
.catch( err => { |
||||
gAppRef.showErrorToast( <div> Couldn't delete the publication: <div className="monospace"> {err.toString()} </div> </div> ) ; |
||||
} ) ; |
||||
}, |
||||
"Cancel": null, |
||||
} ) ; |
||||
} |
||||
|
||||
_makeDisplayName() { |
||||
if ( this.props.data.pub_edition ) |
||||
return this.props.data.pub_name + " (" + this.props.data.pub_edition + ")" ; |
||||
else |
||||
return this.props.data.pub_name ; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,157 @@ |
||||
import React from "react" ; |
||||
import { gAppRef } from "./index.js" ; |
||||
import { makeOptionalLink, pluralString } from "./utils.js" ; |
||||
|
||||
const axios = require( "axios" ) ; |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
export class PublisherSearchResult extends React.Component |
||||
{ |
||||
|
||||
render() { |
||||
return ( <div className="search-result publisher"> |
||||
<div className="name"> { makeOptionalLink( this.props.data.publ_name, this.props.data.publ_url ) } |
||||
<img src="/images/edit.png" className="edit" onClick={this.onEditPublisher.bind(this)} alt="Edit this publisher." /> |
||||
<img src="/images/delete.png" className="delete" onClick={this.onDeletePublisher.bind(this)} alt="Delete this publisher." /> |
||||
</div> |
||||
<div className="description" dangerouslySetInnerHTML={{__html: this.props.data.publ_description}} /> |
||||
</div> ) ; |
||||
} |
||||
|
||||
static onNewPublisher( notify ) { |
||||
PublisherSearchResult._doEditPublisher( {}, (newVals,refs) => { |
||||
axios.post( gAppRef.state.flaskBaseUrl + "/publisher/create?list=1", newVals ) |
||||
.then( resp => { |
||||
// update the cached publishers
|
||||
gAppRef.caches.publishers = resp.data.publishers ; |
||||
// unload any cleaned values
|
||||
for ( let r in refs ) { |
||||
if ( resp.data.cleaned && resp.data.cleaned[r] ) |
||||
newVals[ r ] = resp.data.cleaned[ r ] ; |
||||
} |
||||
// update the UI with the new details
|
||||
notify( resp.data.publ_id, newVals ) ; |
||||
if ( resp.data.warning ) |
||||
gAppRef.showWarningToast( <div> The new publisher was created OK. <p> {resp.data.warning} </p> </div> ) ; |
||||
else |
||||
gAppRef.showInfoToast( <div> The new publisher was created OK. </div> ) ; |
||||
gAppRef.closeModalForm() ; |
||||
} ) |
||||
.catch( err => { |
||||
gAppRef.showErrorMsg( <div> Couldn't create the publisher: <div className="monospace"> {err.toString()} </div> </div> ) ; |
||||
} ) ; |
||||
} ) ; |
||||
} |
||||
|
||||
onEditPublisher() { |
||||
PublisherSearchResult._doEditPublisher( this.props.data, (newVals,refs) => { |
||||
// send the updated details to the server
|
||||
newVals.publ_id = this.props.data.publ_id ; |
||||
axios.post( gAppRef.state.flaskBaseUrl + "/publisher/update?list=1", newVals ) |
||||
.then( resp => { |
||||
// update the cached publishers
|
||||
gAppRef.caches.publishers = resp.data.publishers ; |
||||
// update the UI with the new details
|
||||
for ( let r in refs ) |
||||
this.props.data[ r ] = (resp.data.cleaned && resp.data.cleaned[r]) || newVals[r] ; |
||||
this.forceUpdate() ; |
||||
if ( resp.data.warning ) |
||||
gAppRef.showWarningToast( <div> The publisher was updated OK. <p> {resp.data.warning} </p> </div> ) ; |
||||
else |
||||
gAppRef.showInfoToast( <div> The publisher was updated OK. </div> ) ; |
||||
gAppRef.closeModalForm() ; |
||||
} ) |
||||
.catch( err => { |
||||
gAppRef.showErrorMsg( <div> Couldn't update the publisher: <div className="monospace"> {err.toString()} </div> </div> ) ; |
||||
} ) ; |
||||
} ); |
||||
} |
||||
|
||||
static _doEditPublisher( vals, notify ) { |
||||
let refs = {} ; |
||||
const content = <div> |
||||
<div className="row name"> <label> Name: </label> |
||||
<input type="text" defaultValue={vals.publ_name} ref={(r) => refs.publ_name=r} /> |
||||
</div> |
||||
<div className="row description"> <label> Description: </label> |
||||
<textarea defaultValue={vals.publ_description} ref={(r) => refs.publ_description=r} /> |
||||
</div> |
||||
<div className="row url"> <label> Web: </label> |
||||
<input type="text" defaultValue={vals.publ_url} ref={(r) => refs.publ_url=r} /> |
||||
</div> |
||||
</div> ; |
||||
const buttons = { |
||||
OK: () => { |
||||
// unload the new values
|
||||
let newVals = {} ; |
||||
for ( let r in refs ) |
||||
newVals[ r ] = refs[r].value.trim() ; |
||||
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 ) ; |
||||
}, |
||||
Cancel: () => { gAppRef.closeModalForm() ; }, |
||||
} ; |
||||
const isNew = Object.keys( vals ).length === 0 ; |
||||
gAppRef.showModalForm( isNew?"New publisher":"Edit publisher", content, buttons ) ; |
||||
} |
||||
|
||||
onDeletePublisher() { |
||||
let doDelete = ( nPubs ) => { |
||||
let warning ; |
||||
if ( typeof nPubs === "number" ) { |
||||
if ( nPubs === 0 ) |
||||
warning = <div> No publications will be deleted. </div> ; |
||||
else |
||||
warning = <div> { pluralString(nPubs,"associated publication") + " will also be deleted." } </div> ; |
||||
} else { |
||||
warning = ( <div> <img className="icon" src="/images/error.png" alt="Error." /> |
||||
WARNING: Couldn't check if any associated publications will be deleted: |
||||
<div className="monospace"> {nPubs.toString()} </div> |
||||
</div> ) ; |
||||
} |
||||
let content = ( <div> |
||||
Do you want to delete this publisher? |
||||
<div style={{margin:"0.5em 0 0.5em 2em",fontStyle:"italic"}} dangerouslySetInnerHTML={{__html: this.props.data.publ_name}} /> |
||||
{warning} |
||||
</div> ) ; |
||||
gAppRef.ask( content, { |
||||
"OK": () => { |
||||
// delete the publisher on the server
|
||||
axios.get( gAppRef.state.flaskBaseUrl + "/publisher/delete/" + this.props.data.publ_id + "?list=1" ) |
||||
.then( resp => { |
||||
// update the cached publishers
|
||||
gAppRef.caches.publishers = resp.data.publishers ; |
||||
gAppRef.caches.publications = resp.data.publications ; // nb: because of cascading deletes
|
||||
// update the UI
|
||||
this.props.onDelete( "publ_id", this.props.data.publ_id ) ; |
||||
resp.data.deletedPublications.forEach( pub_id => { |
||||
this.props.onDelete( "pub_id", pub_id ) ; |
||||
} ) ; |
||||
if ( resp.data.warning ) |
||||
gAppRef.showWarningToast( <div> The publisher was deleted. <p> {resp.data.warning} </p> </div> ) ; |
||||
else |
||||
gAppRef.showInfoToast( <div> The publisher was deleted. </div> ) ; |
||||
} ) |
||||
.catch( err => { |
||||
gAppRef.showErrorToast( <div> Couldn't delete the publisher: <div className="monospace"> {err.toString()} </div> </div> ) ; |
||||
} ) ; |
||||
}, |
||||
"Cancel": null, |
||||
} ) ; |
||||
} ; |
||||
// get the publisher details
|
||||
axios.get( gAppRef.state.flaskBaseUrl + "/publisher/" + this.props.data.publ_id ) |
||||
.then( resp => { |
||||
doDelete( resp.data.nPublications ) ; |
||||
} ) |
||||
.catch( err => { |
||||
doDelete( err ) ; |
||||
} ) ; |
||||
} |
||||
|
||||
} |
@ -1,3 +1,21 @@ |
||||
import React from "react" ; |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
export function makeOptionalLink( caption, url ) { |
||||
let link = <span dangerouslySetInnerHTML={{ __html: caption }} /> ; |
||||
if ( url ) |
||||
link = <a href={url} target="_blank" rel="noopener noreferrer"> {link} </a> ; |
||||
return link ; |
||||
} |
||||
|
||||
export function slugify( val ) { |
||||
return val.toLowerCase().replace( " ", "-" ) ; |
||||
} |
||||
|
||||
export function pluralString( n, str1, str2 ) { |
||||
if ( n === 1 ) |
||||
return n + " " + str1 ; |
||||
else |
||||
return n + " " + (str2 ? str2 : str1+"s") ; |
||||
} |
||||
|
Loading…
Reference in new issue