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 31, 2019
1 parent 6c4e834 commit b4a744e
Show file tree
Hide file tree
Showing 14 changed files with 683 additions and 160 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
88 changes: 77 additions & 11 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,20 +158,16 @@ 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.
'''
# Only show live articles
queryset = Article.objects.filter(is_live=True)

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 = queryset.order_by('-created')
elif ordering == 'impact':
Expand All @@ -183,7 +183,10 @@ def list_articles(request):
impact = sum(map(F, impact_fields))
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 @@ -196,6 +199,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 @@ -223,6 +228,18 @@ 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.filter(is_live=True)
queryset = queryset.prefetch_related('commentaries', 'authors')

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

paginator = PageNumberPagination()
Expand Down Expand Up @@ -436,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

0 comments on commit b4a744e

Please sign in to comment.