Compare commits

...

5 Commits

  1. 26
      README.md
  2. 14
      asl_articles/search.py
  3. 11
      asl_articles/tests/test_search.py
  4. 4
      web/src/App.js
  5. 5
      web/src/PreviewableImage.js
  6. 26
      web/src/PublicationSearchResult.js
  7. 12
      web/src/SearchResults.js

@ -17,30 +17,20 @@ This program provides a searchable interface to your ASL magazines and their art
</a>
*NOTE: This project integrates with my other [asl-rulebook2](https://github.com/pacman-ghost/asl-rulebook2) project. Add a setting to your `site.cfg` e.g.*
```
ASLRB_BASE_URL = http://localhost:5020
```
*and references to rules will be converted to clickable links that will open the ASLRB at that rule.*
### To create a new database
*NOTE: This requires the Python environment to have been set up (see the developer notes below).*
Go to the *alembic/* directory and change the database connection string in *alembic.ini* e.g.
```sqlalchemy.url = sqlite:////home/pacman-ghost/asl-articles.db```
``` ASLRB_BASE_URL = http://localhost:5020 ```
Note that there are 3 forward slashes for the protocol, the 4th one is the start of the path to the database.
Run the following command to create the database (you must be in the *alembic/* directory):
```alembic upgrade head```
*and references to rules will be converted to clickable links that will open the ASLRB at that rule.*
### To run the application
Go to the project root directory and run the following command:
Get a copy of the pre-loaded database from the release page.
```./run-containers.sh -d /home/pacman-ghost/asl-articles.db```
Then go to the project root directory and run the following command:
```
./run-containers.sh -d /home/pacman-ghost/asl-articles.db
```
*NOTE: You will need Docker >= 17.05 (for multi-stage builds)*, and `docker-compose`.

@ -410,20 +410,24 @@ def _make_fts_query_string( query_string, search_aliases ): #pylint: disable=too
if is_raw_query:
return [ val.strip() ]
tokens = []
DQUOTE_MARKER = "<!~!>"
for word in val.split():
# FUDGE! It's difficult to figure out if we have a multi-word quoted phrase when the query string
# contains nested quotes, so we hack around this by temporarily removing the inner quotes.
word = word.replace( '""', DQUOTE_MARKER )
if len(tokens) > 0:
if tokens[-1].startswith( '"' ) and not tokens[-1].endswith( '"' ):
# the previous token is a quoted phrase, continue it
# the previous token is a the start of a quoted phrase - continue it
tokens[-1] += " " + word
continue
if not tokens[-1].startswith( '"' ) and word.endswith( '"' ):
tokens.append( quote( word[:-1] ) )
continue
tokens.append( quote( word ) )
if len(tokens) > 0 and tokens[-1].startswith( '"' ) and not tokens[-1].endswith( '"' ):
# we have an unterminated quoted phrase, terminate it
tokens[-1] += '"'
return [ t for t in tokens if t ]
return [
t.replace( DQUOTE_MARKER, '""' )
for t in tokens if t
]
# split the query string into parts (alias replacement texts, and everything else)
parts, pos = [], 0

@ -588,6 +588,17 @@ def test_make_fts_query_string():
do_test( '"Mr. Jones"', '"Mr. Jones"' )
do_test( 'foo "Mr. Jones" bar', 'foo AND "Mr. Jones" AND bar' )
# test nested quoted phrases
# NOTE: This is important since searching for an author wraps their name in double quotes,
# so we need to be able to handle a quoted phrase (e.g. a nickname) within the name.
do_test( 'Joseph "Joey" Blow', 'Joseph AND "Joey" AND Blow' )
do_test( 'Joseph "Joey Joe" Blow', 'Joseph AND "Joey Joe" AND Blow' )
do_test( 'Joseph ""Joey"" Blow', 'Joseph AND ""Joey"" AND Blow' )
# NOTE: This one doesn't work properly, but no-one is going to be doing this :-/
# do_test( 'Joseph ""Joey Joe"" Blow', 'Joseph AND ""Joey Joe"" AND Blow' )
do_test( '"Joseph ""Joey"" Blow"', '"Joseph ""Joey"" Blow"' )
do_test( '"Joseph ""Joey Joe"" Blow"', '"Joseph ""Joey Joe"" Blow"' )
# test some incorrectly quoted phrases
do_test( '"', '' )
do_test( ' " " " ', '' )

@ -563,6 +563,10 @@ export class App extends React.Component
this.setWindowTitle( null ) ;
}
setWindowTitle( caption ) {
if ( caption ) {
let doc = new DOMParser().parseFromString( caption, "text/html" ) ;
caption = doc.body.textContent ;
}
document.title = caption ? APP_NAME + " - " + caption : APP_NAME ;
}

@ -18,7 +18,10 @@ export class PreviewableImage extends React.Component
static initPreviewableImages() {
// load the imageZoom script
$.getScript( "/jQuery/imageZoom/jquery.imageZoom.js" ) ;
$.getScript( {
url: "/jQuery/imageZoom/jquery.imageZoom.js",
cache: true,
} ) ;
// load the imageZoom CSS
let cssNode = document.createElement( "link" ) ;
cssNode.type = "text/css" ;

@ -58,21 +58,17 @@ export class PublicationSearchResult extends React.Component
if ( this.props.data.articles ) {
for ( let i=0 ; i < this.props.data.articles.length ; ++i ) {
const article = this.props.data.articles[ i ] ;
if ( this.props.onArticleClick ) {
// forward clicks on the article to the parent
articles.push( <div
dangerouslySetInnerHTML = {{__html: article.article_title}}
onClick = { () => this.props.onArticleClick( article.article_id ) }
style = {{ cursor: "pointer" }}
title = "Go to this article."
/> ) ;
} else {
// handle clicks on the article normally
articles.push( <Link title="Show this article."
to = { gAppRef.makeAppUrl( "/article/" + article.article_id ) }
dangerouslySetInnerHTML = {{ __html: article.article_title }}
/> ) ;
}
let onArticleClick = (evt) => {
// NOTE: We let the parent take a look at clicks first, so that they can scroll
// to the article if it's already on-screen.
if ( this.props.onArticleClick && this.props.onArticleClick( article.article_id ) )
evt.preventDefault() ;
} ;
articles.push( <Link title="Show this article."
to = { gAppRef.makeAppUrl( "/article/" + article.article_id ) }
onClick = {onArticleClick}
dangerouslySetInnerHTML = {{ __html: article.article_title }}
/> ) ;
}
}

@ -34,11 +34,17 @@ export class SearchResults extends React.Component
// track articles
let articleRefs = {} ;
function scrollToArticle( article_id ) {
// NOTE: If the user has clicked on an article that has been listed as part of a publication,
// we just scroll to that article (since articles are also returned as part of the search results
// when searching for a publication).
// NOTE: We could do the same thing when clicking on a publication that has been listed as part
// of a publisher, but in this case, it's probably better UX to show the publication's page,
// along with its articles.
const node = ReactDOM.findDOMNode( articleRefs[article_id] ) ;
if ( node )
if ( node ) {
node.scrollIntoView() ;
else
document.location = gAppRef.makeAppUrl( "/article/" + article_id ) ;
return true ;
}
}
// render the search results
results = [] ;

Loading…
Cancel
Save