parent
5719699379
commit
d0e01039b3
@ -0,0 +1,39 @@ |
||||
"""Added the 'article' table. |
||||
|
||||
Revision ID: 85abe5bcbac0 |
||||
Revises: 6054f3f09626 |
||||
Create Date: 2019-12-08 03:28:43.202699 |
||||
|
||||
""" |
||||
from alembic import op |
||||
import sqlalchemy as sa |
||||
|
||||
|
||||
# revision identifiers, used by Alembic. |
||||
revision = '85abe5bcbac0' |
||||
down_revision = '6054f3f09626' |
||||
branch_labels = None |
||||
depends_on = None |
||||
|
||||
|
||||
def upgrade(): |
||||
# ### commands auto generated by Alembic - please adjust! ### |
||||
op.create_table('article', |
||||
sa.Column('article_id', sa.Integer(), nullable=False), |
||||
sa.Column('article_title', sa.String(length=200), nullable=False), |
||||
sa.Column('article_subtitle', sa.String(length=200), nullable=True), |
||||
sa.Column('article_snippet', sa.String(length=5000), nullable=True), |
||||
sa.Column('article_url', sa.String(length=500), nullable=True), |
||||
sa.Column('pub_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(['pub_id'], ['publication.pub_id'], ondelete='CASCADE'), |
||||
sa.PrimaryKeyConstraint('article_id') |
||||
) |
||||
# ### end Alembic commands ### |
||||
|
||||
|
||||
def downgrade(): |
||||
# ### commands auto generated by Alembic - please adjust! ### |
||||
op.drop_table('article') |
||||
# ### end Alembic commands ### |
@ -0,0 +1,87 @@ |
||||
""" Handle article requests. """ |
||||
|
||||
import datetime |
||||
import logging |
||||
|
||||
from flask import request, jsonify, abort |
||||
|
||||
from asl_articles import app, db |
||||
from asl_articles.models import Article |
||||
from asl_articles.utils import get_request_args, clean_request_args, make_ok_response, apply_attrs |
||||
|
||||
_logger = logging.getLogger( "db" ) |
||||
|
||||
_FIELD_NAMES = [ "article_title", "article_subtitle", "article_snippet", "article_url", "pub_id" ] |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@app.route( "/article/<article_id>" ) |
||||
def get_article( article_id ): |
||||
"""Get an article.""" |
||||
_logger.debug( "Get article: id=%s", article_id ) |
||||
article = Article.query.get( article_id ) |
||||
if not article: |
||||
abort( 404 ) |
||||
_logger.debug( "- %s", article ) |
||||
return jsonify( get_article_vals( article ) ) |
||||
|
||||
def get_article_vals( article ): |
||||
"""Extract public fields from an Article record.""" |
||||
return { |
||||
"article_id": article.article_id, |
||||
"article_title": article.article_title, |
||||
"article_subtitle": article.article_subtitle, |
||||
"article_snippet": article.article_snippet, |
||||
"article_url": article.article_url, |
||||
"pub_id": article.pub_id, |
||||
} |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@app.route( "/article/create", methods=["POST"] ) |
||||
def create_article(): |
||||
"""Create an article.""" |
||||
vals = get_request_args( request.json, _FIELD_NAMES, |
||||
log = ( _logger, "Create article:" ) |
||||
) |
||||
cleaned = clean_request_args( vals, _FIELD_NAMES, _logger ) |
||||
vals[ "time_created" ] = datetime.datetime.now() |
||||
article = Article( **vals ) |
||||
db.session.add( article ) #pylint: disable=no-member |
||||
db.session.commit() #pylint: disable=no-member |
||||
_logger.debug( "- New ID: %d", article.article_id ) |
||||
return make_ok_response( cleaned=cleaned, |
||||
extras = { "article_id": article.article_id } |
||||
) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@app.route( "/article/update", methods=["POST"] ) |
||||
def update_article(): |
||||
"""Update an article.""" |
||||
article_id = request.json[ "article_id" ] |
||||
vals = get_request_args( request.json, _FIELD_NAMES, |
||||
log = ( _logger, "Update article: id={}".format( article_id ) ) |
||||
) |
||||
cleaned = clean_request_args( vals, _FIELD_NAMES, _logger ) |
||||
vals[ "time_updated" ] = datetime.datetime.now() |
||||
article = Article.query.get( article_id ) |
||||
if not article: |
||||
abort( 404 ) |
||||
apply_attrs( article, vals ) |
||||
db.session.commit() #pylint: disable=no-member |
||||
return make_ok_response( cleaned=cleaned ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@app.route( "/article/delete/<article_id>" ) |
||||
def delete_article( article_id ): |
||||
"""Delete an article.""" |
||||
_logger.debug( "Delete article: id=%s", article_id ) |
||||
article = Article.query.get( article_id ) |
||||
if not article: |
||||
abort( 404 ) |
||||
_logger.debug( "- %s", article ) |
||||
db.session.delete( article ) #pylint: disable=no-member |
||||
db.session.commit() #pylint: disable=no-member |
||||
return make_ok_response( extras={} ) |
@ -0,0 +1,12 @@ |
||||
{ |
||||
|
||||
"article": [ |
||||
{ "article_title": "Smoke Gets In Your Eyes", "article_subtitle": "All You Wanted To Know About SMOKE", |
||||
"article_snippet": "SMOKE comes in all shapes, colors, and sizes." |
||||
}, |
||||
{ "article_title": "What To Do If You Have A Tin Can", |
||||
"article_snippet": "My Panthers... My beautiful Panthers!" |
||||
} |
||||
] |
||||
|
||||
} |
@ -0,0 +1,41 @@ |
||||
{ |
||||
|
||||
"publisher": [ |
||||
{ "publ_id": 1, "publ_name": "#pubs=0, #articles=0" }, |
||||
{ "publ_id": 2, "publ_name": "#pubs=1, #articles=0" }, |
||||
{ "publ_id": 3, "publ_name": "#pubs=1, #articles=1" }, |
||||
{ "publ_id": 4, "publ_name": "#pubs=1, #articles=2" }, |
||||
{ "publ_id": 5, "publ_name": "#pubs=2, #articles=0" }, |
||||
{ "publ_id": 6, "publ_name": "#pubs=2, #articles=1" }, |
||||
{ "publ_id": 7, "publ_name": "#pubs=2, #articles=2" }, |
||||
{ "publ_id": 8, "publ_name": "#pubs=2, #articles=4" } |
||||
], |
||||
|
||||
"publication": [ |
||||
{ "pub_id": 20, "pub_name": "publication 2", "publ_id": "2" }, |
||||
{ "pub_id": 30, "pub_name": "publication 3", "publ_id": "3" }, |
||||
{ "pub_id": 40, "pub_name": "publication 4", "publ_id": "4" }, |
||||
{ "pub_id": 50, "pub_name": "publication 5a", "publ_id": "5" }, |
||||
{ "pub_id": 51, "pub_name": "publication 5b", "publ_id": "5" }, |
||||
{ "pub_id": 60, "pub_name": "publication 6a", "publ_id": "6" }, |
||||
{ "pub_id": 61, "pub_name": "publication 6b", "publ_id": "6" }, |
||||
{ "pub_id": 70, "pub_name": "publication 7a", "publ_id": "7" }, |
||||
{ "pub_id": 71, "pub_name": "publication 7b", "publ_id": "7" }, |
||||
{ "pub_id": 80, "pub_name": "publication 8a", "publ_id": "8" }, |
||||
{ "pub_id": 81, "pub_name": "publication 8b", "publ_id": "8" } |
||||
], |
||||
|
||||
"article": [ |
||||
{ "article_title": "article 3", "pub_id": 30 }, |
||||
{ "article_title": "article 4a", "pub_id": 40 }, |
||||
{ "article_title": "article 4b", "pub_id": 40 }, |
||||
{ "article_title": "article 6a", "pub_id": 60 }, |
||||
{ "article_title": "article 7a", "pub_id": 70 }, |
||||
{ "article_title": "article 7b", "pub_id": 71 }, |
||||
{ "article_title": "article 8a.1", "pub_id": 80 }, |
||||
{ "article_title": "article 8a.2", "pub_id": 80 }, |
||||
{ "article_title": "article 8b.1", "pub_id": 81 }, |
||||
{ "article_title": "article 8b.2", "pub_id": 81 } |
||||
] |
||||
|
||||
} |
@ -0,0 +1,15 @@ |
||||
{ |
||||
|
||||
"publication": [ |
||||
{ "pub_id": 1, "pub_name": "Cascading Deletes 1" }, |
||||
{ "pub_id": 2, "pub_name": "Cascading Deletes 2" }, |
||||
{ "pub_id": 3, "pub_name": "Cascading Deletes 3" } |
||||
], |
||||
|
||||
"article": [ |
||||
{ "article_title": "article 2", "pub_id": "2" }, |
||||
{ "article_title": "article 3a", "pub_id": "3" }, |
||||
{ "article_title": "article 3b", "pub_id": "3" } |
||||
] |
||||
|
||||
} |
@ -1,15 +0,0 @@ |
||||
{ |
||||
|
||||
"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,255 @@ |
||||
""" Test article 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_article( webdriver, flask_app, dbconn ): |
||||
"""Test editing articles.""" |
||||
|
||||
# initialize |
||||
init_tests( webdriver, flask_app ) |
||||
init_db( dbconn, "articles.json" ) |
||||
|
||||
# edit "What To Do If You Have A Tin Can" |
||||
results = do_search( "tin can" ) |
||||
assert len(results) == 1 |
||||
result = results[0] |
||||
_edit_article( result, { |
||||
"title": " Updated title ", |
||||
"subtitle": " Updated subtitle ", |
||||
"snippet": " Updated snippet. ", |
||||
"url": " http://updated-article.com ", |
||||
} ) |
||||
|
||||
# check that the search result was updated in the UI |
||||
results = find_children( "#search-results .search-result" ) |
||||
result = results[0] |
||||
_check_result( result, [ "Updated title", "Updated subtitle", "Updated snippet.", "http://updated-article.com/" ] ) |
||||
|
||||
# try to remove all fields from the article (should fail) |
||||
_edit_article( result, |
||||
{ "title": "", "subtitle": "", "snippet": "", "url": "" }, |
||||
expected_error = "Please specify the article's title." |
||||
) |
||||
|
||||
# enter something for the name |
||||
dlg = find_child( "#modal-form" ) |
||||
set_elem_text( find_child( ".title input", dlg ), "Tin Cans Rock!" ) |
||||
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[0] |
||||
assert find_child( ".title a", result ) is None |
||||
assert find_child( ".title", result ).text == "Tin Cans Rock!" |
||||
assert find_child( ".snippet", result ).text == "" |
||||
|
||||
# check that the search result was updated in the database |
||||
results = do_search( "tin can" ) |
||||
_check_result( results[0], [ "Tin Cans Rock!", None, "", None ] ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_create_article( webdriver, flask_app, dbconn ): |
||||
"""Test creating new articles.""" |
||||
|
||||
# initialize |
||||
init_tests( webdriver, flask_app ) |
||||
init_db( dbconn, "basic.json" ) |
||||
|
||||
# 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 |
||||
dlg = find_child( "#modal-form" ) # nb: the form is still on-screen |
||||
set_elem_text( find_child( ".title input", dlg ), "New article" ) |
||||
set_elem_text( find_child( ".subtitle input", dlg ), "New subtitle" ) |
||||
set_elem_text( find_child( ".snippet textarea", dlg ), "New snippet." ) |
||||
set_elem_text( find_child( ".url input", dlg ), "http://new-snippet.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 article appears in the UI |
||||
def check_new_article( result ): |
||||
_check_result( result, [ |
||||
"New article", "New subtitle", "New snippet.", "http://new-snippet.com/" |
||||
] ) |
||||
results = find_children( "#search-results .search-result" ) |
||||
check_new_article( results[0] ) |
||||
|
||||
# check that the new article has been saved in the database |
||||
results = do_search( "new" ) |
||||
assert len( results ) == 1 |
||||
check_new_article( results[0] ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_delete_article( webdriver, flask_app, dbconn ): |
||||
"""Test deleting articles.""" |
||||
|
||||
# initialize |
||||
init_tests( webdriver, flask_app ) |
||||
init_db( dbconn, "articles.json" ) |
||||
|
||||
# start to delete article "Smoke Gets In Your Eyes", but cancel the operation |
||||
results = do_search( "smoke" ) |
||||
assert len(results) == 1 |
||||
result = results[0] |
||||
find_child( ".delete", result ).click() |
||||
check_ask_dialog( ( "Delete this article?", "Smoke Gets In Your Eyes" ), "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( "smoke" ) |
||||
assert results3 == results |
||||
|
||||
# delete the article "Smoke Gets In Your Eyes" |
||||
result = results3[0] |
||||
find_child( ".delete", result ).click() |
||||
set_toast_marker( "info" ) |
||||
check_ask_dialog( ( "Delete this article?", "Smoke Gets In Your Eyes" ), "ok" ) |
||||
wait_for( 2, |
||||
lambda: check_toast( "info", "The article was deleted." ) |
||||
) |
||||
|
||||
# check that search result was removed on-screen |
||||
results = find_children( "#search-results .search-result" ) |
||||
assert get_result_names( results ) == [] |
||||
|
||||
# check that the search result was deleted from the database |
||||
results = do_search( "smoke" ) |
||||
assert get_result_names( results ) == [] |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_unicode( webdriver, flask_app, dbconn ): |
||||
"""Test Unicode content.""" |
||||
|
||||
# initialize |
||||
init_tests( webdriver, flask_app ) |
||||
init_db( dbconn, "basic.json" ) |
||||
|
||||
# create a article with Unicode content |
||||
_create_article( { |
||||
"title": "japan = \u65e5\u672c", |
||||
"subtitle": "s.korea = \ud55c\uad6d", |
||||
"snippet": "greece = \u0395\u03bb\u03bb\u03ac\u03b4\u03b1", |
||||
"url": "http://\ud55c\uad6d.com" |
||||
} ) |
||||
|
||||
# check that the new article is showing the Unicode content correctly |
||||
results = do_search( "japan" ) |
||||
assert len( results ) == 1 |
||||
_check_result( results[0], [ |
||||
"japan = \u65e5\u672c", |
||||
"s.korea = \ud55c\uad6d", |
||||
"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, "basic.json" ) |
||||
|
||||
# create a article with HTML content |
||||
_create_article( { |
||||
"title": "title: <span style='boo!'> <b>bold</b> <xxx>xxx</xxx> <i>italic</i>", |
||||
"subtitle": "<i>italicized subtitle</i>", |
||||
"snippet": "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, [ "title: bold xxx italic", "italicized subtitle", "bad stuff here:", None ] ) |
||||
assert find_child( ".title span" ).get_attribute( "innerHTML" ) \ |
||||
== "title: <span> <b>bold</b> xxx <i>italic</i></span>" |
||||
assert find_child( ".subtitle" ).get_attribute( "innerHTML" ) \ |
||||
== "<i>italicized subtitle</i>" |
||||
assert check_toast( "warning", "Some values had HTML removed.", contains=True ) |
||||
|
||||
# update the article with new HTML content |
||||
_edit_article( result, { |
||||
"title": "<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( ".title span", result ).text == "updated" |
||||
wait_for( 2, check_result ) |
||||
assert check_toast( "warning", "Some values had HTML removed.", contains=True ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def _create_article( vals, toast_type="info" ): |
||||
"""Create a new article.""" |
||||
# initialize |
||||
if toast_type: |
||||
set_toast_marker( toast_type ) |
||||
# create the new article |
||||
find_child( "#menu .new-article" ).click() |
||||
dlg = wait_for_elem( 2, "#modal-form" ) |
||||
for k,v in vals.items(): |
||||
sel = ".{} {}".format( k , "textarea" if k == "snippet" else "input" ) |
||||
set_elem_text( find_child( sel, dlg ), v ) |
||||
find_child( "button.ok", dlg ).click() |
||||
if toast_type: |
||||
# check that the new article was created successfully |
||||
wait_for( 2, |
||||
lambda: check_toast( toast_type, "created OK", contains=True ) |
||||
) |
||||
|
||||
def _edit_article( result, vals, toast_type="info", expected_error=None ): |
||||
"""Edit a article's details.""" |
||||
# update the specified article'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 == "snippet" 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.""" |
||||
assert find_child( ".title span", result ).text == expected[0] |
||||
elem = find_child( ".subtitle", result ) |
||||
if elem: |
||||
assert elem.text == expected[1] |
||||
else: |
||||
assert expected[1] is None |
||||
assert find_child( ".snippet", result ).text == expected[2] |
||||
elem = find_child( ".title a", result ) |
||||
if elem: |
||||
assert elem.get_attribute( "href" ) == expected[3] |
||||
else: |
||||
assert expected[3] is None |
@ -0,0 +1,152 @@ |
||||
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 ArticleSearchResult extends React.Component |
||||
{ |
||||
|
||||
render() { |
||||
// NOTE: The "title" field is also given the CSS class "name" so that the normal CSS will apply to it.
|
||||
// Some tests also look for a generic ".name" class name when checking search results.
|
||||
return ( <div className="search-result article"> |
||||
<div className="title name"> { makeOptionalLink( this.props.data.article_title, this.props.data.article_url ) } |
||||
<img src="/images/edit.png" className="edit" onClick={this.onEditArticle.bind(this)} alt="Edit this article." /> |
||||
<img src="/images/delete.png" className="delete" onClick={this.onDeleteArticle.bind(this)} alt="Delete this article." /> |
||||
{ this.props.data.article_subtitle && <div className="subtitle" dangerouslySetInnerHTML={{ __html: this.props.data.article_subtitle }} /> } |
||||
</div> |
||||
<div className="snippet" dangerouslySetInnerHTML={{__html: this.props.data.article_snippet}} /> |
||||
</div> ) ; |
||||
} |
||||
|
||||
static onNewArticle( notify ) { |
||||
ArticleSearchResult._doEditArticle( {}, (newVals,refs) => { |
||||
axios.post( gAppRef.state.flaskBaseUrl + "/article/create", newVals ) |
||||
.then( resp => { |
||||
// 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.article_id, newVals ) ; |
||||
if ( resp.data.warning ) |
||||
gAppRef.showWarningToast( <div> The new article was created OK. <p> {resp.data.warning} </p> </div> ) ; |
||||
else |
||||
gAppRef.showInfoToast( <div> The new article was created OK. </div> ) ; |
||||
gAppRef.closeModalForm() ; |
||||
} ) |
||||
.catch( err => { |
||||
gAppRef.showErrorMsg( <div> Couldn't create the article: <div className="monospace"> {err.toString()} </div> </div> ) ; |
||||
} ) ; |
||||
} ) ; |
||||
} |
||||
|
||||
onEditArticle() { |
||||
ArticleSearchResult._doEditArticle( this.props.data, (newVals,refs) => { |
||||
// send the updated details to the server
|
||||
newVals.article_id = this.props.data.article_id ; |
||||
axios.post( gAppRef.state.flaskBaseUrl + "/article/update", newVals ) |
||||
.then( resp => { |
||||
// 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 article was updated OK. <p> {resp.data.warning} </p> </div> ) ; |
||||
else |
||||
gAppRef.showInfoToast( <div> The article was updated OK. </div> ) ; |
||||
gAppRef.closeModalForm() ; |
||||
} ) |
||||
.catch( err => { |
||||
gAppRef.showErrorMsg( <div> Couldn't update the article: <div className="monospace"> {err.toString()} </div> </div> ) ; |
||||
} ) ; |
||||
} ); |
||||
} |
||||
|
||||
static _doEditArticle( vals, notify ) { |
||||
let refs = {} ; |
||||
let publications = [ { value: null, label: <i>(none)</i> } ] ; |
||||
let currPub = 0 ; |
||||
for ( let p of Object.entries(gAppRef.caches.publications) ) { |
||||
publications.push( { |
||||
value: p[1].pub_id, |
||||
label: <span dangerouslySetInnerHTML={{__html: p[1].pub_name}} /> |
||||
} ) ; |
||||
if ( p[1].pub_id === vals.pub_id ) |
||||
currPub = publications.length - 1 ; |
||||
} |
||||
publications.sort( (lhs,rhs) => { |
||||
return ReactDOMServer.renderToStaticMarkup( lhs.label ).localeCompare( ReactDOMServer.renderToStaticMarkup( rhs.label ) ) ; |
||||
} ) ; |
||||
const content = <div> |
||||
<div className="row title"> <label> Title: </label> |
||||
<input type="text" defaultValue={vals.article_title} ref={(r) => refs.article_title=r} /> |
||||
</div> |
||||
<div className="row subtitle"> <label> Subtitle: </label> |
||||
<input type="text" defaultValue={vals.article_subtitle} ref={(r) => refs.article_subtitle=r} /> |
||||
</div> |
||||
<div className="row publication"> <label> Publication: </label> |
||||
<Select options={publications} isSearchable={true} |
||||
defaultValue = { publications[ currPub ] } |
||||
ref = { (r) => refs.pub_id=r } |
||||
/> |
||||
</div> |
||||
<div className="row snippet"> <label> Snippet: </label> |
||||
<textarea defaultValue={vals.article_snippet} ref={(r) => refs.article_snippet=r} /> |
||||
</div> |
||||
<div className="row url"> <label> Web: </label> |
||||
<input type="text" defaultValue={vals.article_url} ref={(r) => refs.article_url=r} /> |
||||
</div> |
||||
</div> ; |
||||
const buttons = { |
||||
OK: () => { |
||||
// unload the new values
|
||||
let newVals = {} ; |
||||
for ( let r in refs ) |
||||
newVals[ r ] = (r === "pub_id") ? refs[r].state.value && refs[r].state.value.value : refs[r].value.trim() ; |
||||
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 ) ; |
||||
}, |
||||
Cancel: () => { gAppRef.closeModalForm() ; }, |
||||
} ; |
||||
const isNew = Object.keys( vals ).length === 0 ; |
||||
gAppRef.showModalForm( isNew?"New article":"Edit article", content, buttons ) ; |
||||
} |
||||
|
||||
onDeleteArticle() { |
||||
// confirm the operation
|
||||
const content = ( <div> |
||||
Delete this article? |
||||
<div style={{margin:"0.5em 0 0 2em",fontStyle:"italic"}} dangerouslySetInnerHTML = {{ __html: this.props.data.article_title }} /> |
||||
</div> ) ; |
||||
gAppRef.ask( content, { |
||||
"OK": () => { |
||||
// delete the article on the server
|
||||
axios.get( gAppRef.state.flaskBaseUrl + "/article/delete/" + this.props.data.article_id ) |
||||
.then( resp => { |
||||
// update the UI
|
||||
this.props.onDelete( "article_id", this.props.data.article_id ) ; |
||||
if ( resp.data.warning ) |
||||
gAppRef.showWarningToast( <div> The article was deleted. <p> {resp.data.warning} </p> </div> ) ; |
||||
else |
||||
gAppRef.showInfoToast( <div> The article was deleted. </div> ) ; |
||||
} ) |
||||
.catch( err => { |
||||
gAppRef.showErrorToast( <div> Couldn't delete the article: <div className="monospace"> {err.toString()} </div> </div> ) ; |
||||
} ) ; |
||||
}, |
||||
"Cancel": null, |
||||
} ) ; |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue