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