From 9d4572d3d88fd5ab49b38aaf8472f7a8bfdf04b9 Mon Sep 17 00:00:00 2001 From: Taka Date: Thu, 19 Mar 2020 05:07:49 +0000 Subject: [PATCH] Allow articles to be rated. --- .../21ec84874208_added_article_ratings.py | 28 ++++++++ asl_articles/articles.py | 23 +++++++ asl_articles/models.py | 1 + asl_articles/search.py | 17 +++-- asl_articles/tests/test_articles.py | 55 +++++++++++++++ web/public/images/rating-star-disabled.png | Bin 0 -> 6179 bytes web/public/images/rating-star.png | Bin 0 -> 6136 bytes web/src/ArticleSearchResult.js | 15 +++++ web/src/RatingStars.css | 2 + web/src/RatingStars.js | 63 ++++++++++++++++++ 10 files changed, 197 insertions(+), 7 deletions(-) create mode 100644 alembic/versions/21ec84874208_added_article_ratings.py create mode 100644 web/public/images/rating-star-disabled.png create mode 100644 web/public/images/rating-star.png create mode 100644 web/src/RatingStars.css create mode 100644 web/src/RatingStars.js diff --git a/alembic/versions/21ec84874208_added_article_ratings.py b/alembic/versions/21ec84874208_added_article_ratings.py new file mode 100644 index 0000000..8d3b013 --- /dev/null +++ b/alembic/versions/21ec84874208_added_article_ratings.py @@ -0,0 +1,28 @@ +"""Added article ratings. + +Revision ID: 21ec84874208 +Revises: 3d58e8ebf8c6 +Create Date: 2020-03-19 01:10:12.194485 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '21ec84874208' +down_revision = '3d58e8ebf8c6' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('article', sa.Column('article_rating', sa.Integer(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('article', 'article_rating') + # ### end Alembic commands ### diff --git a/asl_articles/articles.py b/asl_articles/articles.py index 3e6cbf1..c30aee3 100644 --- a/asl_articles/articles.py +++ b/asl_articles/articles.py @@ -55,6 +55,7 @@ def get_article_vals( article, add_type=False ): "article_url": article.article_url, "article_scenarios": [ s.scenario_id for s in scenarios ], "article_tags": decode_tags( article.article_tags ), + "article_rating": article.article_rating, "pub_id": article.pub_id, } if add_type: @@ -268,6 +269,28 @@ def update_article(): extras[ "_publications" ] = pubs return make_ok_response( updated=updated, extras=extras, warnings=warnings ) +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +@app.route( "/article/update-rating", methods=["POST"] ) +def update_article_rating(): + """Update an article's rating.""" + + # parse the input + article_id = request.json[ "article_id" ] + new_rating = int( request.json[ "rating" ] ) + if new_rating < 0 or new_rating > 3: + raise ValueError( "Invalid rating." ) + + # update the article's rating + article = Article.query.get( article_id ) + if not article: + abort( 404 ) + article.article_rating = new_rating + db.session.commit() + search.add_or_update_article( None, article ) + + return "OK" + # --------------------------------------------------------------------- @app.route( "/article/delete/" ) diff --git a/asl_articles/models.py b/asl_articles/models.py index fc4dc40..d4ef159 100644 --- a/asl_articles/models.py +++ b/asl_articles/models.py @@ -67,6 +67,7 @@ class Article( db.Model ): article_pageno = db.Column( db.String(20) ) article_url = db.Column( db.String(500) ) article_tags = db.Column( db.String(1000) ) + article_rating = db.Column( db.Integer ) pub_id = db.Column( db.Integer, db.ForeignKey( Publication.__table__.c.pub_id, ondelete="CASCADE" ) ) diff --git a/asl_articles/search.py b/asl_articles/search.py index de1fe05..4fce274 100644 --- a/asl_articles/search.py +++ b/asl_articles/search.py @@ -99,7 +99,8 @@ _FIELD_MAPPINGS = { }, "article": { "name": "article_title", "name2": "article_subtitle", "description": "article_snippet", "authors": _get_authors, "scenarios": _get_scenarios, - "tags": lambda article: _get_tags( article.article_tags ) + "tags": lambda article: _get_tags( article.article_tags ), + "rating": "article_rating" } } @@ -257,9 +258,9 @@ def _do_fts_search( fts_query_string, col_names, results=None ): #pylint: disabl return "highlight( searchable, {}, '{}', '{}' )".format( n, hilites[0], hilites[1] ) - sql = "SELECT owner, rank, {}, {}, {}, {}, {}, {} FROM searchable" \ + sql = "SELECT owner, rank, {}, {}, {}, {}, {}, {}, rating FROM searchable" \ " WHERE searchable MATCH ?" \ - " ORDER BY rank".format( + " ORDER BY rating DESC, rank".format( highlight(1), highlight(2), highlight(3), highlight(4), highlight(5), highlight(6) ) match = "{{ {} }}: {}".format( @@ -280,6 +281,7 @@ def _do_fts_search( fts_query_string, col_names, results=None ): #pylint: disabl # prepare the result for the front-end result = globals()[ "_get_{}_vals".format( owner_type ) ]( obj ) result[ "type" ] = owner_type + result[ "rank" ] = row[1] # return highlighted versions of the content to the caller fields = _FIELD_MAPPINGS[ owner_type ] @@ -387,7 +389,7 @@ def init_search( session, logger ): # (nor UNIQUE constraints), so we have to manage this manually :-( dbconn.conn.execute( "CREATE VIRTUAL TABLE searchable USING fts5" - " ( owner, {}, tokenize='porter unicode61' )".format( + " ( owner, {}, rating, tokenize='porter unicode61' )".format( ", ".join( _SEARCHABLE_COL_NAMES ) ) ) @@ -481,14 +483,15 @@ def _do_add_or_update_searchable( dbconn, owner_type, owner, obj ): def do_add_or_update( dbconn ): sql = "INSERT INTO searchable" \ - " ( owner, {} )" \ - " VALUES (?,?,?,?,?,?,?)".format( + " ( owner, {}, rating )" \ + " VALUES (?,?,?,?,?,?,?,?)".format( ",".join( _SEARCHABLE_COL_NAMES ) ) dbconn.conn.execute( sql, ( owner, vals.get("name"), vals.get("name2"), vals.get("description"), - vals.get("authors"), vals.get("scenarios"), vals.get("tags") + vals.get("authors"), vals.get("scenarios"), vals.get("tags"), + vals.get("rating") ) ) # update the database diff --git a/asl_articles/tests/test_articles.py b/asl_articles/tests/test_articles.py index fbfdc99..7cb7a62 100644 --- a/asl_articles/tests/test_articles.py +++ b/asl_articles/tests/test_articles.py @@ -484,6 +484,61 @@ def test_timestamps( webdriver, flask_app, dbconn ): # --------------------------------------------------------------------- +def test_article_ratings( webdriver, flask_app, dbconn ): + """Test article ratings.""" + + # initialize + init_tests( webdriver, flask_app, dbconn, fixtures="articles.json" ) + + def do_test( article_sr, star_no, expected ): + + # click the specified article star + stars = find_children( ".rating-stars img", article_sr ) + stars[ star_no ].click() + for sr_no,sr in enumerate(results): + assert get_rating(sr) == expected[sr_no] + + # compare the ratings on-screen with what's in the database + for sr in results: + article_id = sr.get_attribute( "testing--article_id" ) + ui_rating = get_rating( sr ) + db_rating = dbconn.execute( + "SELECT article_rating FROM article WHERE article_id={}".format( article_id ) + ).scalar() + if db_rating is None: + assert ui_rating == 0 + else: + assert ui_rating == db_rating + + def get_rating( article_sr ): + stars = [ + "disabled" not in star.get_attribute("src") + for star in find_children( ".rating-stars img", article_sr ) + ] + rating = 0 + for star in stars: + if not star: + assert all( not s for s in stars[rating+1:] ) + break + rating += 1 + return rating + + # get the test articles + results = do_search( SEARCH_ALL_ARTICLES ) + + # do the tests + do_test( results[0], 2, [3,0] ) + do_test( results[1], 1, [3,2] ) + + # do the tests + do_test( results[0], 2, [2,2] ) + do_test( results[0], 2, [3,2] ) + do_test( results[0], 0, [1,2] ) + do_test( results[0], 0, [0,2] ) + do_test( results[0], 0, [1,2] ) + +# --------------------------------------------------------------------- + def create_article( vals, toast_type="info", expected_error=None, expected_constraints=None, dlg=None ): """Create a new article.""" diff --git a/web/public/images/rating-star-disabled.png b/web/public/images/rating-star-disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..1a18c8b622a0c11bff10486cf8791be6fb830245 GIT binary patch literal 6179 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 literal 0 HcmV?d00001 diff --git a/web/public/images/rating-star.png b/web/public/images/rating-star.png new file mode 100644 index 0000000000000000000000000000000000000000..c6c58c2644e062d03414e7a406ddda61497c391e GIT binary patch literal 6136 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 KMNUMnLSTaFaF_l7 literal 0 HcmV?d00001 diff --git a/web/src/ArticleSearchResult.js b/web/src/ArticleSearchResult.js index 34ef83c..5073057 100644 --- a/web/src/ArticleSearchResult.js +++ b/web/src/ArticleSearchResult.js @@ -4,6 +4,7 @@ import { Menu, MenuList, MenuButton, MenuItem } from "@reach/menu-button" ; import { ArticleSearchResult2 } from "./ArticleSearchResult2.js" ; import "./ArticleSearchResult.css" ; import { PublicationSearchResult } from "./PublicationSearchResult.js" ; +import { RatingStars } from "./RatingStars.js" ; import { gAppRef } from "./App.js" ; import { makeScenarioDisplayName, applyUpdatedVals, removeSpecialFields, makeCommaList, isLink } from "./utils.js" ; @@ -125,6 +126,9 @@ export class ArticleSearchResult extends React.Component dangerouslySetInnerHTML = {{ __html: pub_display_name }} /> } + { article_url && @@ -145,6 +149,17 @@ export class ArticleSearchResult extends React.Component ) ; } + onRatingChange( newRating, onFailed ) { + axios.post( gAppRef.makeFlaskUrl( "/article/update-rating", null ), { + article_id: this.props.data.article_id, + rating: newRating, + } ).catch( err => { + gAppRef.showErrorMsg(
Couldn't update the rating:
{err.toString()}
) ; + if ( onFailed ) + onFailed() ; + } ) ; + } + static onNewArticle( notify ) { ArticleSearchResult2._doEditArticle( {}, (newVals,refs) => { axios.post( gAppRef.makeFlaskUrl( "/article/create", {list:1} ), newVals ) diff --git a/web/src/RatingStars.css b/web/src/RatingStars.css new file mode 100644 index 0000000..8f621ca --- /dev/null +++ b/web/src/RatingStars.css @@ -0,0 +1,2 @@ +.rating-stars { float: right ; margin-top: -0.1em ; margin-right: 0.25em ; } +.rating-stars img { height: 0.75em ; margin-right: 0.1em ; cursor: pointer ; } diff --git a/web/src/RatingStars.js b/web/src/RatingStars.js new file mode 100644 index 0000000..24b6a0c --- /dev/null +++ b/web/src/RatingStars.js @@ -0,0 +1,63 @@ +import React from "react" ; +import "./RatingStars.css" ; + +// -------------------------------------------------------------------- + +export class RatingStars extends React.Component +{ + + constructor( props ) { + // initialize + super( props ) ; + this.state = { + rating: props.rating, + } ; + } + + render() { + + let changeRating = ( starIndex ) => { + // update the rating + let newRating ; + if ( starRefs[starIndex].src.indexOf( "rating-star-disabled.png" ) !== -1 ) + newRating = starIndex + 1 ; + else { + // NOTE: We get here if the clicked-on star is enabled. If this is the highest enabled star, + // we disable it (i.e. set the rating to N-1), otherwise we make it the highest star (i.e. set + // the rating to N). This has the down-side that if the user wants to set a rating of 0, + // they have to set the rating to 1 first, then set it to 0, but this is unlikely to be an issue + // i.e. going from 1 to 0 will be far more common than 3 to 0. + if ( starIndex+1 === this.state.rating ) + newRating = starIndex ; + else + newRating = starIndex + 1 ; + } + const prevRating = this.state.rating ; + this.setState( { rating: newRating } ) ; + // notify the parent + if ( this.props.onChange ) { + this.props.onChange( newRating, () => { + this.setState( { rating: prevRating } ) ; // nb: the update failed, rollback + } ) ; + } + } + + // prepare the rating stars + let stars=[], starRefs={} ; + for ( let i=0 ; i < 3 ; ++i ) { + const fname = this.state.rating > i ? "rating-star.png" : "rating-star-disabled.png" ; + stars.push( + Rating star. starRefs[i] = r } + onClick={ () => changeRating(i) } + /> + ) ; + } + + // render the component + return ( + {stars} + ) ; + } + +}