Skip to content

Commit

Permalink
Adds the search page component and an API route for searching authors…
Browse files Browse the repository at this point in the history
… and articles in the same query — issue #55
  • Loading branch information
mickaobrien committed Jul 27, 2019
1 parent da26112 commit c1000ba
Show file tree
Hide file tree
Showing 14 changed files with 685 additions and 159 deletions.
21 changes: 21 additions & 0 deletions curate/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@
import re


class AuthorSearchResultSerializer(serializers.ModelSerializer):
number_of_articles = serializers.IntegerField(source='articles.count')
search_result_type = serializers.ReadOnlyField(default='AUTHOR')

class Meta:
model = Author
fields = (
'id',
'name',
'affiliations',
'number_of_articles',
'slug',
'search_result_type',
)


class AuthorSerializer(serializers.ModelSerializer):
account = serializers.SlugRelatedField(
slug_field='username',
Expand Down Expand Up @@ -82,6 +98,11 @@ class Meta:
},
}


class ArticleSearchResultSerializer(ArticleListSerializer):
search_result_type = serializers.ReadOnlyField(default='ARTICLE')


class ArticleSerializerNested(WritableNestedModelSerializer):
key_figures = KeyFigureSerializer(many=True, required=False, allow_null=True)
commentaries = CommentarySerializer(many=True)
Expand Down
89 changes: 79 additions & 10 deletions curate/views_api.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from itertools import chain

from django.shortcuts import get_object_or_404
from rest_framework.pagination import PageNumberPagination
from django.contrib.auth.mixins import LoginRequiredMixin
Expand All @@ -24,8 +26,10 @@
KeyFigure,
)
from curate.serializers import (
AuthorSearchResultSerializer,
AuthorSerializer,
AuthorArticleSerializer,
ArticleSearchResultSerializer,
ArticleSerializerNested,
ArticleListSerializer,
CommentarySerializer,
Expand Down Expand Up @@ -154,19 +158,19 @@ def delete_author(request, slug):
author.delete()
return Response(status=status.HTTP_200_OK)

# Article views
@api_view(('GET', ))
def list_articles(request):
'''
Return a list of all existing articles.
'''

def filter_and_sort_articles(request):
""" Parses transparency filters, content filters and ordering query parameters
from the request and return the appropriate Article queryset.
"""
# Sort the results by created or impact
ordering = request.query_params.get('ordering')
if ordering not in ['created', 'impact']:
ordering = 'created'
ordering = None

queryset = Article.objects.all()
if ordering == 'created':
queryset = Article.objects.order_by('-created')
queryset = queryset.order_by('-created')
elif ordering == 'impact':
# Caculate the impact value by adding all view, citations and downloads
impact_fields = [
Expand All @@ -178,9 +182,12 @@ def list_articles(request):
'preprint_views',
]
impact = sum(map(F, impact_fields))
queryset = Article.objects.annotate(impact=impact).order_by('-impact')
queryset = queryset.annotate(impact=impact).order_by('-impact')

# Transparency ilters
# Transparency filters
# There are two types of transparency filters
# - Preregistration
# - Openness
transparency_filters = request.query_params.getlist('transparency')

# Preregistration options
Expand All @@ -193,6 +200,8 @@ def list_articles(request):
]

if prereg_values:
# The preregistration values are combined with an OR so any articles
# matching at least one of the filters are returned
queryset = queryset.filter(prereg_protocol_type__in=prereg_values)

# A dict where the key is the expected query parameter and the value is a
Expand Down Expand Up @@ -220,6 +229,17 @@ def list_articles(request):

queryset = queryset.prefetch_related('commentaries', 'authors')

return queryset

# Article views
@api_view(('GET', ))
def list_articles(request):
'''
Return a list of all existing articles.
'''
queryset = filter_and_sort_articles(request)
queryset = queryset.prefetch_related('commentaries', 'authors')

serializer = ArticleListSerializer(instance=queryset, many=True)

paginator = PageNumberPagination()
Expand Down Expand Up @@ -433,6 +453,55 @@ def search_articles(request):
serializer=ArticleListSerializer(instance=result_page, many=True)
return Response(serializer.data)

@api_view(('GET', ))
def search_articles_and_authors(request):
q = request.GET.get('q', '')
page_size = int(request.GET.get('page_size', 10))
if q:
logger.warning('Query: %s' % q)

article_queryset = filter_and_sort_articles(request)
article_queryset = (
article_queryset
.prefetch_related('authors', 'commentaries')
.filter(
Q(authors__name__icontains=q) |
Q(title__icontains=q) |
Q(author_list__icontains=q)
)
.distinct()
)

author_queryset = (
Author
.objects
.prefetch_related('articles')
.filter(
Q(name__icontains=q) |
Q(affiliations__icontains=q)
)
)
else:
author_queryset = Author.objects.order_by('name')
article_queryset = Article.objects.order_by('updated')

results = list(chain(author_queryset, article_queryset))
paginator = PageNumberPagination()
paginator.page_size = page_size
paginated_results = paginator.paginate_queryset(results, request)

data = []
# Loop through results and use the appropriate serializer
authors = [result for result in paginated_results if type(result) is Author]
articles = [result for result in paginated_results if type(result) is Article]

data = {
'authors': AuthorSearchResultSerializer(authors, many=True).data,
'articles': ArticleSearchResultSerializer(articles, many=True).data,
}
return Response(data)


class ImageUploadView(APIView):
permission_classes = (IsAuthenticated,)
parser_classes = (MultiPartParser,)
Expand Down
1 change: 1 addition & 0 deletions curate_science/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,5 @@
path('api/commentaries/<int:pk>/', api.view_commentary, name='api-view-commentary'),
path('api/commentaries/<int:pk>/update/', api.update_commentary, name='api-update-commentary'),
path('api/commentaries/<int:pk>/delete/', api.delete_commentary, name='api-delete-commentary'),
path('api/search/', api.search_articles_and_authors, name='api-search-articles-and-authors'),
]
74 changes: 67 additions & 7 deletions dist/js/bundle.js

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
import { BrowserRouter as Router, Route, Switch, Link } from "react-router-dom";

import TopBar from './components/TopBar.jsx';
import Footer from './components/Footer.jsx';

import Splash from './pages/Splash.jsx';
import About from './pages/About.jsx';
Expand All @@ -19,7 +20,7 @@ import AuthorPage from './pages/AuthorPage.jsx';
import AuthorPageCreator from './pages/AuthorPageCreator.jsx';
import AdminManage from './pages/AdminManage.jsx';
import AdminInvite from './pages/AdminInvite.jsx';
import Footer from './components/Footer.jsx';
import SearchResults from './pages/SearchResults.jsx';

// UI components
import { fade } from '@material-ui/core/styles/colorManipulator';
Expand Down Expand Up @@ -113,6 +114,7 @@ class App extends React.Component {
<Route path="/author/:slug(.+)" component={() => <AuthorPage user_session={user_session} />} />
<Route path="/article/:id" component={() => <ArticlePage user_session={user_session} />} />
<Route path="/create_author" component={() => <AuthorPageCreator user_session={user_session} />} />
<Route path="/search" component={() => <SearchResults/>} />
<Route path="/admin/manage" component={AdminManage} />
<Route path="/admin/invite" component={AdminInvite} />
</Switch>
Expand Down
21 changes: 20 additions & 1 deletion src/components/ArticleContent.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';

Expand Down Expand Up @@ -32,6 +33,11 @@ const styles = {
marginTop: 0,
marginBottom: 3
},
titleLink: {
'&:hover': {
textDecoration: 'underline'
}
},
authors: {
color: "#009933",
marginTop: 3,
Expand Down Expand Up @@ -193,12 +199,25 @@ class ArticleContent extends React.PureComponent {
])

const created_at = this.created_at()
const title = (
<Typography className={classes.title} variant="h2" color="textPrimary">
{article.title}
</Typography>
)

return (
<div>
<ArticleFullTextLinks {...content_links} />

<Typography className={classes.title} variant="h2" color="textPrimary">{article.title}</Typography>
{
is_article_page ?
<span>{title}</span>
:
<Link to={`/article/${article.id}`} className={classes.titleLink}>
{title}
</Link>
}

<Typography className={classes.authors} color="textSecondary" gutterBottom>
<AuthorList author_list={article.author_list} year={article.year} in_press={article.in_press} />
</Typography>
Expand Down
1 change: 0 additions & 1 deletion src/components/ArticleList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import { withStyles } from '@material-ui/core/styles';

const styles = theme => ({
root: {
paddingTop: 10,
flexGrow: 1
},
articleList: {
Expand Down
67 changes: 67 additions & 0 deletions src/components/AuthorCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';

import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import Grid from '@material-ui/core/Grid';
import Icon from '@material-ui/core/Icon';


const styles = theme => ({
authorDetails: {
'& p': { marginBottom: theme.spacing.unit, marginTop: 0 },
color: theme.typography.body1.color
},
card: {
minWidth: 275,
marginBottom: '9px'
},
cardContent: {
padding: 12
},
numberOfArticles: {
color: '#999999'
},
});

class AuthorCard extends React.PureComponent {
constructor(props) {
super(props);
}

render() {
const { author, classes } = this.props;

const CC_ST = {paddingBottom: 12} // Fix for .MuiCardContent-root-325:last-child adding 24px padding-bottom

return (
<div className="ArticleCard">
<Link to={`/author/${author.slug}`}>
<Card className={classes.card} raised>
<CardContent className={classes.cardContent} style={CC_ST}>
<Grid container>
<Icon style={{fontSize: '3rem', marginRight: '1rem', color: '#999999'}}>person</Icon>

<div className={classes.authorDetails}>
<p style={{fontWeight: 'bold', letterSpacing: '0.025em'}}>{author.name}</p>
<p><em>{author.affiliations}</em></p>
<p className={classes.numberOfArticles}>
{author.number_of_articles} {author.number_of_articles === 1 ? 'article' : 'articles'}
</p>
</div>
</Grid>
</CardContent>
</Card>
</Link>
</div>
)
}
}

AuthorCard.defaultProps = {
author: {}
};

export default withStyles(styles)(AuthorCard);
12 changes: 9 additions & 3 deletions src/components/HomePageFilter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ const styles = theme => ({
menu: {
position: 'absolute',
top: '4rem',
left: 0,
zIndex: 10,
},
menuTitle: {
Expand Down Expand Up @@ -142,7 +141,14 @@ class HomePageFilter extends React.PureComponent {

render() {
let { menu_open } = this.state
let { classes, transparency_filters } = this.props
let { align_right, classes, transparency_filters } = this.props

let menu_styles
if (align_right) {
menu_styles = {right: 0}
} else {
menu_styles = {left: 0}
}

return (
<Grid className={classes.menuRoot}>
Expand All @@ -156,7 +162,7 @@ class HomePageFilter extends React.PureComponent {
Filter
</Button>
{ menu_open ? (
<Paper className={classes.menu}>
<Paper className={classes.menu} style={menu_styles}>
<div className={classes.transparencyGroup}>
<Grid container wrap="nowrap">
<Grid item xs={6}>
Expand Down

2 comments on commit c1000ba

@mickaobrien
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eplebel This commit has most of the work on the search results page. The search logic is pretty simple for the moment but it works. You can see a demo of it here: https://curate-science-staging-2.appspot.com/app/search?q=lebel
There's no search box yet but you can see results for different queries by changing the lebel query parameter.

@eplebel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok great, thanks for the sneak peak, and just let me know when it's ready for testing, thanks!

Please sign in to comment.