parent
14257c7b9a
commit
0dcc040d4b
@ -0,0 +1,30 @@ |
|||||||
|
"""Added columns to store tags. |
||||||
|
|
||||||
|
Revision ID: 4594e1b85c8b |
||||||
|
Revises: 85abe5bcbac0 |
||||||
|
Create Date: 2019-12-09 09:21:42.902996 |
||||||
|
|
||||||
|
""" |
||||||
|
from alembic import op |
||||||
|
import sqlalchemy as sa |
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic. |
||||||
|
revision = '4594e1b85c8b' |
||||||
|
down_revision = '85abe5bcbac0' |
||||||
|
branch_labels = None |
||||||
|
depends_on = None |
||||||
|
|
||||||
|
|
||||||
|
def upgrade(): |
||||||
|
# ### commands auto generated by Alembic - please adjust! ### |
||||||
|
op.add_column('article', sa.Column('article_tags', sa.String(length=1000), nullable=True)) |
||||||
|
op.add_column('publication', sa.Column('pub_tags', sa.String(length=1000), nullable=True)) |
||||||
|
# ### end Alembic commands ### |
||||||
|
|
||||||
|
|
||||||
|
def downgrade(): |
||||||
|
# ### commands auto generated by Alembic - please adjust! ### |
||||||
|
op.drop_column('publication', 'pub_tags') |
||||||
|
op.drop_column('article', 'article_tags') |
||||||
|
# ### end Alembic commands ### |
@ -0,0 +1,38 @@ |
|||||||
|
""" Handle tag requests. """ |
||||||
|
|
||||||
|
from collections import defaultdict |
||||||
|
|
||||||
|
from flask import jsonify |
||||||
|
|
||||||
|
from asl_articles import app, db |
||||||
|
from asl_articles.models import Publication, Article |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
@app.route( "/tags" ) |
||||||
|
def get_tags(): |
||||||
|
"""Get all tags.""" |
||||||
|
return jsonify( do_get_tags() ) |
||||||
|
|
||||||
|
def do_get_tags(): |
||||||
|
"""Get all tags.""" |
||||||
|
|
||||||
|
# get all the tags |
||||||
|
# NOTE: This is pretty inefficient, since an article/publication's tags are munged into one big string |
||||||
|
# and stored in a single column, so we need to manually unpack everything, but we'll see how it goes... |
||||||
|
tags = defaultdict( int ) |
||||||
|
def count_tags( query ): |
||||||
|
for row in query: |
||||||
|
if not row[0]: |
||||||
|
continue |
||||||
|
for tag in row[0].split( ";" ): |
||||||
|
tags[ tag ] = tags[ tag ] + 1 |
||||||
|
count_tags( db.session.query( Publication.pub_tags ) ) #pylint: disable=no-member |
||||||
|
count_tags( db.session.query( Article.article_tags ) ) #pylint: disable=no-member |
||||||
|
|
||||||
|
# sort the results |
||||||
|
tags = sorted( tags.items(), |
||||||
|
key = lambda v: ( -v[1], v[0] ) # sort by # instances, then name |
||||||
|
) |
||||||
|
|
||||||
|
return tags |
@ -0,0 +1,59 @@ |
|||||||
|
""" Control a react-select droplist. """ |
||||||
|
|
||||||
|
from selenium.webdriver.common.keys import Keys |
||||||
|
|
||||||
|
from asl_articles.tests.utils import find_child, find_children |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
class ReactSelect: |
||||||
|
"""Control a react-select droplist.""" |
||||||
|
|
||||||
|
def __init__( self, elem ): |
||||||
|
self.select = elem |
||||||
|
|
||||||
|
def select_by_name( self, val ): |
||||||
|
"""Select an option by name.""" |
||||||
|
find_child( ".react-select__dropdown-indicator", self.select ).click() |
||||||
|
options = [ e for e in find_children( ".react-select__option", self.select ) |
||||||
|
if e.text == val |
||||||
|
] |
||||||
|
assert len( options ) == 1 |
||||||
|
options[0].click() |
||||||
|
|
||||||
|
def get_multiselect_choices( self ): |
||||||
|
"""Get the available multi-select choices.""" |
||||||
|
btn = find_child( ".react-select__dropdown-indicator", self.select ) |
||||||
|
btn.click() # show the dropdown |
||||||
|
choices = [ e.text for e in find_children( ".react-select__option", self.select ) ] |
||||||
|
btn.click() # close the dropdown |
||||||
|
return choices |
||||||
|
|
||||||
|
def get_multiselect_values( self ): |
||||||
|
"""Get the current multi-select values.""" |
||||||
|
return [ e.text for e in find_children( ".react-select__multi-value", self.select ) ] |
||||||
|
|
||||||
|
def update_multiselect_values( self, *vals ): |
||||||
|
"""Add/remove multi-select values.""" |
||||||
|
for v in vals: |
||||||
|
if v.startswith( "+" ): |
||||||
|
self.add_multiselect_value( v[1:] ) |
||||||
|
elif v.startswith( "-" ): |
||||||
|
self.remove_multiselect_value( v[1:] ) |
||||||
|
else: |
||||||
|
assert False, "Multi-select values must start with +/-." |
||||||
|
|
||||||
|
def add_multiselect_value( self, val ): |
||||||
|
"""Add a multi-select value.""" |
||||||
|
elem = find_child( "input", self.select ) |
||||||
|
elem.clear() |
||||||
|
elem.send_keys( val ) |
||||||
|
elem.send_keys( Keys.RETURN ) |
||||||
|
|
||||||
|
def remove_multiselect_value( self, val ): |
||||||
|
"""Remove a multi-select value.""" |
||||||
|
for elem in find_children( ".react-select__multi-value", self.select ): |
||||||
|
if elem.text == val: |
||||||
|
find_child( ".react-select__multi-value__remove", elem ).click() |
||||||
|
return |
||||||
|
assert False, "Can't find multi-select value: {}".format( val ) |
@ -0,0 +1,116 @@ |
|||||||
|
""" Test tag operations. """ |
||||||
|
|
||||||
|
import urllib.request |
||||||
|
import json |
||||||
|
|
||||||
|
from asl_articles.tests.utils import init_tests, wait_for_elem, find_child, find_children, \ |
||||||
|
find_search_result , get_result_names |
||||||
|
from asl_articles.tests.react_select import ReactSelect |
||||||
|
|
||||||
|
from asl_articles.tests.test_publications import _create_publication, _edit_publication |
||||||
|
from asl_articles.tests.test_articles import _create_article, _edit_article |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
def test_tags( webdriver, flask_app, dbconn ): |
||||||
|
"""Test tag operations.""" |
||||||
|
|
||||||
|
# initialize |
||||||
|
init_tests( webdriver, flask_app, dbconn ) |
||||||
|
|
||||||
|
# create a test publication and article |
||||||
|
_create_publication( { "name": "publication 1" } ) |
||||||
|
_create_article( { "title": "article 1" } ) |
||||||
|
_check_tags( flask_app, { |
||||||
|
"publication 1": [], |
||||||
|
"article 1": [] |
||||||
|
} ) |
||||||
|
|
||||||
|
# add some tags to the publication |
||||||
|
_edit_publication( find_search_result( "publication 1" ), { |
||||||
|
"tags": [ "+aaa", "+bbb" ] |
||||||
|
} ) |
||||||
|
_check_tags( flask_app, { |
||||||
|
"publication 1": [ "aaa", "bbb" ], |
||||||
|
"article 1": [] |
||||||
|
} ) |
||||||
|
|
||||||
|
# add some tags to the article |
||||||
|
_edit_article( find_search_result( "article 1" ), { |
||||||
|
"tags": [ "+bbb", "+ccc" ] |
||||||
|
} ) |
||||||
|
_check_tags( flask_app, { |
||||||
|
"publication 1": [ "aaa", "bbb" ], |
||||||
|
"article 1": [ "bbb", "ccc" ] |
||||||
|
} ) |
||||||
|
|
||||||
|
# remove some tags from the publication |
||||||
|
_edit_article( find_search_result( "publication 1" ), { |
||||||
|
"tags": [ "-bbb" ] |
||||||
|
} ) |
||||||
|
_check_tags( flask_app, { |
||||||
|
"publication 1": [ "aaa" ], |
||||||
|
"article 1": [ "bbb", "ccc" ] |
||||||
|
} ) |
||||||
|
|
||||||
|
# remove some tags from the article |
||||||
|
_edit_article( find_search_result( "article 1" ), { |
||||||
|
"tags": [ "-ccc", "-bbb" ] |
||||||
|
} ) |
||||||
|
_check_tags( flask_app, { |
||||||
|
"publication 1": [ "aaa" ], |
||||||
|
"article 1": [] |
||||||
|
} ) |
||||||
|
|
||||||
|
# add duplicate tags to the publication |
||||||
|
_edit_article( find_search_result( "publication 1" ), { |
||||||
|
"tags": [ "+bbb", "+aaa", "+eee" ] |
||||||
|
} ) |
||||||
|
_check_tags( flask_app, { |
||||||
|
"publication 1": [ "aaa","bbb","eee" ], |
||||||
|
"article 1": [] |
||||||
|
} ) |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
def _check_tags( flask_app, expected ): |
||||||
|
"""Check the tags in the UI and database.""" |
||||||
|
|
||||||
|
# get the complete list of expected tags |
||||||
|
expected_available = set() |
||||||
|
for tags in expected.values(): |
||||||
|
expected_available.update( tags ) |
||||||
|
|
||||||
|
# check the tags in the UI |
||||||
|
elems = find_children( "#search-results .search-result" ) |
||||||
|
assert set( get_result_names( elems ) ) == set( expected.keys() ) |
||||||
|
for sr in elems: |
||||||
|
# check the tags in the search result |
||||||
|
name = find_child( ".name span", sr ).text |
||||||
|
tags = [ t.text for t in find_children( ".tag", sr ) ] |
||||||
|
assert tags == expected[ name ] |
||||||
|
# check the tags in the publication/article |
||||||
|
find_child( ".edit", sr ).click() |
||||||
|
dlg = wait_for_elem( 2, "#modal-form" ) |
||||||
|
select = ReactSelect( find_child( ".tags .react-select", dlg ) ) |
||||||
|
assert select.get_multiselect_values() == expected[ name ] |
||||||
|
# check that the list of available tags is correct |
||||||
|
# NOTE: We don't bother checking the tag order here. |
||||||
|
assert set( select.get_multiselect_choices() ) == expected_available.difference( expected[name] ) |
||||||
|
find_child( "button.cancel", dlg ).click() |
||||||
|
|
||||||
|
def fixup_tags( tags ): |
||||||
|
return [] if tags is None else tags |
||||||
|
|
||||||
|
# check the tags in the database |
||||||
|
for sr in elems: |
||||||
|
if sr.text.startswith( "publication" ): |
||||||
|
pub_id = sr.get_attribute( "testing--pub_id" ) |
||||||
|
url = flask_app.url_for( "get_publication", pub_id=pub_id ) |
||||||
|
pub = json.load( urllib.request.urlopen( url ) ) |
||||||
|
assert expected[ pub["pub_name"] ] == fixup_tags( pub["pub_tags"] ) |
||||||
|
elif sr.text.startswith( "article" ): |
||||||
|
article_id = sr.get_attribute( "testing--article_id" ) |
||||||
|
url = flask_app.url_for( "get_article", article_id=article_id ) |
||||||
|
article = json.load( urllib.request.urlopen( url ) ) |
||||||
|
assert expected[ article["article_title"] ] == fixup_tags( article["article_tags"] ) |
Loading…
Reference in new issue