diff --git a/.bootstraprc b/.bootstraprc index c3cf2650279e29d294291b6cb860a8e75d49474a..924360a9818097b167d91384b0effefbc2d47a14 100644 --- a/.bootstraprc +++ b/.bootstraprc @@ -14,6 +14,7 @@ # First tables, due to self dependencies "tables": true, "alert": true, + "badge": true, "button-group": true, "buttons": true, "breadcrumb": true, @@ -40,6 +41,7 @@ "alert": true, "collapse": true, "modal": true, + "scrollspy": true, "tab": true, "tooltip": true, "util": true, diff --git a/SciPost_v1/settings/base.py b/SciPost_v1/settings/base.py index 3abb27c0d8eaba1c8e5ac09122a2a0e7d62aab09..8ca8486fa749b7b1f673b1345520d0c95a3abd33 100644 --- a/SciPost_v1/settings/base.py +++ b/SciPost_v1/settings/base.py @@ -98,13 +98,25 @@ INSTALLED_APPS = ( 'webpack_loader', ) + HAYSTACK_CONNECTIONS = { 'default': { 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine', 'PATH': 'local_files/haystack/', }, + # SimpleEngine doesn't need indexing, and could be useful for debugging, however + # does not fully support all haystack features + # 'base_search': { + # 'ENGINE': 'haystack.backends.simple_backend.SimpleEngine', + # } } +# Brute force automatically re-index Haystack using post_save signals on all models. +# When write-traffic increases, a custom processor is preferred which only connects +# signals to eg. `vet-accepted` signals possibly using cron jobs instead of realtime updates. +HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' + + SPHINXDOC_BASE_TEMPLATE = 'scipost/base.html' SPHINXDOC_PROTECTED_PROJECTS = { 'scipost': ['scipost.can_view_docs_scipost'], diff --git a/commentaries/search_indexes.py b/commentaries/search_indexes.py new file mode 100644 index 0000000000000000000000000000000000000000..e68fc14a8d0060ba1f2d3c622a58453e8b8de6b1 --- /dev/null +++ b/commentaries/search_indexes.py @@ -0,0 +1,19 @@ +# import datetime + +from haystack import indexes + +from .models import Commentary + + +class CommentaryIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.CharField(document=True, model_attr='title', use_template=True) + authors = indexes.CharField(model_attr='author_list') + date = indexes.DateTimeField(model_attr='pub_date', null=True) + abstract = indexes.CharField(model_attr='pub_abstract') + + def get_model(self): + return Commentary + + def index_queryset(self, using=None): + """Used when the entire index for model is updated.""" + return self.get_model().objects.vetted() diff --git a/commentaries/templates/partials/commentaries/search_card.html b/commentaries/templates/partials/commentaries/search_card.html new file mode 100644 index 0000000000000000000000000000000000000000..70d7531590e671d4798380a62c83a5edfb17d86b --- /dev/null +++ b/commentaries/templates/partials/commentaries/search_card.html @@ -0,0 +1 @@ +{% extends 'commentaries/_commentary_card_content.html' %} diff --git a/comments/search_indexes.py b/comments/search_indexes.py new file mode 100644 index 0000000000000000000000000000000000000000..3ccccda8f8bf33b3eb77de14c759c08ef86b33ef --- /dev/null +++ b/comments/search_indexes.py @@ -0,0 +1,17 @@ +# import datetime + +from haystack import indexes + +from .models import Comment + + +class CommentIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.CharField(document=True, model_attr='comment_text', use_template=True) + date = indexes.DateTimeField(model_attr='date_submitted') + + def get_model(self): + return Comment + + def index_queryset(self, using=None): + """Used when the entire index for model is updated.""" + return self.get_model().objects.vetted() diff --git a/comments/templates/partials/comments/search_card.html b/comments/templates/partials/comments/search_card.html new file mode 100644 index 0000000000000000000000000000000000000000..ccda3bf319b2060a8847f407370d0afddcb6351f --- /dev/null +++ b/comments/templates/partials/comments/search_card.html @@ -0,0 +1 @@ +{% extends 'comments/_comment_card_content.html' %} diff --git a/common/forms.py b/common/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..2fa6e945c51ced6edb894a97343086b3f16f65e6 --- /dev/null +++ b/common/forms.py @@ -0,0 +1,114 @@ +import calendar +import datetime +import re + +from django.forms.widgets import Widget, Select +from django.utils.dates import MONTHS +from django.utils.safestring import mark_safe + +__all__ = ('MonthYearWidget',) + +RE_DATE = re.compile(r'(\d{4})-(\d\d?)-(\d\d?)$') + + +class MonthYearWidget(Widget): + """ + A Widget that splits date input into two <select> boxes for month and year, + with 'day' defaulting to the first of the month. + + Based on SelectDateWidget, in + + django/trunk/django/forms/extras/widgets.py + + + """ + none_value = (0, '---') + month_field = '%s_month' + year_field = '%s_year' + + def __init__(self, attrs=None, end=False, required=False): + self.attrs = attrs or {} + self.required = required + self.today = datetime.date.today() + self.round_to_end = end + + # Years + this_year = self.today.year + self.year_choices = [(i, i) for i in range(this_year - 4, this_year + 1)] + if not self.required: + self.year_choices.insert(0, self.none_value) + + # Month + self.month_choices = dict(MONTHS.items()) + if not self.required: + self.month_choices[self.none_value[0]] = self.none_value[1] + self.month_choices = sorted(self.month_choices.items()) + + def sqeeze_form_group(self, html, width=6): + return '<div class="form-group col-md-{width}">{html}</div>'.format(width=width, html=html) + + def render(self, name, value, attrs=None): + print('>>>> render', name, value, attrs) + try: + year_val, month_val = value.year, value.month + except AttributeError: + year_val = month_val = None + if isinstance(value, (str, bytes)): + match = RE_DATE.match(value) + if match: + year_val, month_val, day_val = [int(v) for v in match.groups()] + + output = [] + + if 'id' in self.attrs: + id_ = self.attrs['id'] + else: + id_ = 'id_%s' % name + + if hasattr(self, 'month_choices'): + local_attrs = self.build_attrs(id=self.month_field % id_) + s = Select(choices=self.month_choices, attrs={'class': 'form-control'}) + select_html = s.render(self.month_field % name, month_val, local_attrs) + output.append(self.sqeeze_form_group(select_html)) + + if hasattr(self, 'year_choices'): + local_attrs = self.build_attrs(id=self.year_field % id_) + s = Select(choices=self.year_choices, attrs={'class': 'form-control'}) + select_html = s.render(self.year_field % name, year_val, local_attrs) + output.append(self.sqeeze_form_group(select_html)) + + return mark_safe(u'\n'.join(output)) + + @classmethod + def id_for_label(self, id_): + return '%s_month' % id_ + + def value_from_datadict(self, data, files, name): + y = data.get(self.year_field % name) + m = data.get(self.month_field % name) + + if y == m == "0": + return None + + # Defaults for `month` + if m == "0": + m = "12" if self.round_to_end else "1" + + if y == "0": + if self.round_to_end: + y = self.year_choices[-1][0] + else: + index = 0 + if not self.required: + index += 1 + y = self.year_choices[index][0] + + if y and m: + # Days are used for filtering, but not communicated to the user + if self.round_to_end: + d = calendar.monthrange(int(y), int(m))[1] + else: + d = 1 + + return '%s-%s-%s' % (y, m, d) + return data.get(name, None) diff --git a/journals/search_indexes.py b/journals/search_indexes.py new file mode 100644 index 0000000000000000000000000000000000000000..08c3b2898fcfc8613a8e4eb8d48a398ca914d87f --- /dev/null +++ b/journals/search_indexes.py @@ -0,0 +1,20 @@ +# import datetime + +from haystack import indexes + +from .models import Publication + + +class PublicationIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.CharField(document=True, model_attr='title', use_template=True) + authors = indexes.CharField(model_attr='author_list') + date = indexes.DateTimeField(model_attr='publication_date') + abstract = indexes.CharField(model_attr='abstract') + doi_label = indexes.CharField(model_attr='doi_label') + + def get_model(self): + return Publication + + def index_queryset(self, using=None): + """Used when the entire index for model is updated.""" + return self.get_model().objects.published() diff --git a/journals/templates/partials/journals/search_card.html b/journals/templates/partials/journals/search_card.html new file mode 100644 index 0000000000000000000000000000000000000000..ff17dfb329eb875d2b3ec2b01965688d2305f34d --- /dev/null +++ b/journals/templates/partials/journals/search_card.html @@ -0,0 +1 @@ +{% extends 'journals/_publication_card_content.html' %} diff --git a/scipost/forms.py b/scipost/forms.py index ef56c9cf1c29ff7832d30553c6bb9139206f18a8..0bc51c277730cf537459dee08582cc1c6276f369 100644 --- a/scipost/forms.py +++ b/scipost/forms.py @@ -1,3 +1,5 @@ +import datetime + from django import forms from django.contrib.auth import authenticate from django.contrib.auth.models import User, Group @@ -5,6 +7,7 @@ from django.contrib.auth.password_validation import validate_password from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse_lazy from django.utils import timezone +from django.utils.dates import MONTHS from django.utils.http import is_safe_url from django_countries import countries @@ -13,15 +16,16 @@ from django_countries.fields import LazyTypedChoiceField from captcha.fields import ReCaptchaField from ajax_select.fields import AutoCompleteSelectField +from haystack.forms import ModelSearchForm as HayStackSearchForm from .constants import SCIPOST_DISCIPLINES, TITLE_CHOICES, SCIPOST_FROM_ADDRESSES from .decorators import has_contributor from .models import Contributor, DraftInvitation, RegistrationInvitation,\ UnavailabilityPeriod, PrecookedEmail +from common.forms import MonthYearWidget from partners.decorators import has_contact from journals.models import Publication -# from mailing_lists.models import MailchimpList, MailchimpSubscription REGISTRATION_REFUSAL_CHOICES = ( @@ -421,8 +425,38 @@ class RemarkForm(forms.Form): 'placeholder': 'Enter your remarks here. You can use LaTeX in $...$ or \[ \].'}) -class SearchForm(forms.Form): - q = forms.CharField(max_length=100) +def get_date_filter_choices(): + today = datetime.date.today() + empty = [(0, '---')] + months = empty + list(MONTHS.items()) + years = empty + [(i, i) for i in range(today.year - 4, today.year + 1)] + return months, years + + +class SearchForm(HayStackSearchForm): + # The date filters doesn't function well... + start = forms.DateField(widget=MonthYearWidget(), required=False) # Month + # start_2 = forms.DateField(widget=MonthYearWidget(months=False), required=False) # Year + end = forms.DateField(widget=MonthYearWidget(end=True), required=False) # Month + # end_2 = forms.DateField(widget=MonthYearWidget(months=False), required=False) # Years + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + models = self.fields['models'].choices + models = filter(lambda x: x[0] != 'sphinxdoc.document', models) + self.fields['models'].choices = models + + def search(self): + sqs = super().search()#.using('base_search') + c = self.cleaned_data + + if self.cleaned_data['start']: + sqs = sqs.filter(date__gte=self.cleaned_data['start']) + + if self.cleaned_data['end']: + sqs = sqs.filter(date__lte=self.cleaned_data['end']) + l = list(sqs) + return sqs class EmailGroupMembersForm(forms.Form): diff --git a/scipost/global_methods.py b/scipost/global_methods.py deleted file mode 100644 index 3510aef9ea26803d370bc4035445190599249f0e..0000000000000000000000000000000000000000 --- a/scipost/global_methods.py +++ /dev/null @@ -1,10 +0,0 @@ -from .models import Contributor - - -class Global(object): - ''' Is this thing really being used?''' - - @classmethod - def get_contributor(cls, request): - '''This should be fixed within the user model itself?''' - Contributor.objects.get(user=request.user) diff --git a/scipost/static/scipost/SciPost.css b/scipost/static/scipost/SciPost.css index b4248ef268dc319dae35282531fdc524dc3133d1..59a65ec9ace010ea561c03571b2d1dc1cf80645e 100644 --- a/scipost/static/scipost/SciPost.css +++ b/scipost/static/scipost/SciPost.css @@ -4,39 +4,6 @@ General style sheet for SciPost @import url(//fonts.googleapis.com/css?family=Merriweather+Sans); -* { - box-sizing: border-box; -} - -.row:after { - content: ""; - clear: both; - display: block; -} - -[class*="col-"] { - /*float: left; - padding: 5px;*/ -} -/* For desktop: */ -.col-1 {width: 8.33%;} -.col-2 {width: 16.66%;} -.col-3 {width: 25%;} -.col-4 {width: 33.33%;} -.col-5 {width: 41.66%;} -.col-6 {width: 50%;} -.col-7 {width: 58.33%;} -.col-8 {width: 66.66%;} -.col-9 {width: 75%;} -.col-10 {width: 83.33%;} -.col-11 {width: 91.66%;} -.col-12 {width: 100%;} -@media only screen and (max-width: 768px) { - /* For mobile phones: */ - [class*="col-"] { - width: 100%; - } -} hr.hr6 { height: 6px; @@ -307,16 +274,6 @@ ul.personalTabMenu li a.inactive { padding: 0px 4px; } -body { -/* font-family: Merriweather, sans-serif; */ - font-family: 'Merriweather Sans'; - font-weight: lighter; - font-size: 0.8rem; - margin: 0px; - padding: 0px; - background-color: #ffffff; -} - body section div.sectionbox { width: 300px; background-color: #f4f4f4; @@ -326,16 +283,6 @@ body section div.sectionbox { vertical-align: top; } -a { - color: #eeeeee; -/* text-decoration: underline; */ - target: _parent; -} -a:hover { - color: #ffffff; - text-decoration: underline; -} - .submitButton { background-color: #002B49; color: #ffffff; @@ -372,19 +319,6 @@ header p { margin: 25px; } -nav { - background-color: #f0f0f0; - /*color: #002B49; - margin:0px; - padding:0px; - float:center; - clear:both;*/ - /*font-size: 12px;*/ - /*font-weight: 500;*/ -} - - - ol { list-style-type: none; counter-reset: item; diff --git a/scipost/static/scipost/assets/config/preconfig.scss b/scipost/static/scipost/assets/config/preconfig.scss index a67c0d9ceadf3b07734d0e93cd64ee84064e6654..b0e0d444387739b04fc179363c0dd821e376019c 100644 --- a/scipost/static/scipost/assets/config/preconfig.scss +++ b/scipost/static/scipost/assets/config/preconfig.scss @@ -27,6 +27,7 @@ $container-max-widths: ( // $scipost-light: #C3D7EE; $scipost-lightblue: #6885c3; +$scipost-lightestblue: #d3e3f6; $scipost-darkblue: #002b49; $scipost-orange: #f6a11a; $scipost-white: #f9f9f9; @@ -38,7 +39,7 @@ $body-color: $scipost-darkblue; $white: #fff; $blue: $scipost-lightblue; // Primary $green: #6ebb6e; -$cyan: $scipost-lightblue; +$cyan: $scipost-lightestblue; $yellow: $scipost-orange; $gray-600: #ccc; $text-muted: #636c72; @@ -71,12 +72,16 @@ $breadcrumb-divider-color: $scipost-orange; // Forms // -$input-btn-padding-x: .5rem; -$input-btn-padding-y: .25rem; -$input-btn-padding-y-sm: .15rem; +$input-btn-padding-y-sm: 0.1rem; +$input-btn-padding-x: 0.5rem; +$input-btn-padding-y: 0.25rem; +$input-btn-padding-y-lg: 0.25rem; +$input-btn-padding-x-lg: 0.75rem; +$input-btn-line-height-lg: 1.4; $input-border-radius-sm: $base-border-radius; $input-border-radius: $base-border-radius; $input-border-radius-lg: $base-border-radius; +$enable-rounded: true !important; $btn-transition: none; $input-height: calc(1.5rem + 2px); diff --git a/scipost/static/scipost/assets/css/_buttons.scss b/scipost/static/scipost/assets/css/_buttons.scss index aa1038e1dec480fc51e8b4dc2388f272a6157730..b499a56eed8565f667bb083f58ff642e775aca51 100644 --- a/scipost/static/scipost/assets/css/_buttons.scss +++ b/scipost/static/scipost/assets/css/_buttons.scss @@ -31,6 +31,10 @@ } } +.btn-info { + color: $scipost-darkblue; +} + .category-group, .voting-group { border-radius: $card-border-radius; diff --git a/scipost/static/scipost/assets/css/_form.scss b/scipost/static/scipost/assets/css/_form.scss index 7ed22071c9a78ef80474c04b7e6ef3a21784a5a6..25694c4652bcedaceed0f28c56a540a518ed0b14 100644 --- a/scipost/static/scipost/assets/css/_form.scss +++ b/scipost/static/scipost/assets/css/_form.scss @@ -4,6 +4,7 @@ */ .form-control { font-family: inherit; + border-radius: $base-border-radius; } .has-error .form-control { @@ -53,6 +54,8 @@ input[type="file"] { } .search-nav-form { + min-width: 260px; + input { margin-right: 0 !important; outline: 0; @@ -61,12 +64,14 @@ input[type="file"] { input[name="q"] { border-right: 0; - min-width: 200px; + width: 200px; border-radius: $base-border-radius 0 0 $base-border-radius; } input[type="submit"] { background: #f4f4f4; + width: 60px; + text-align: center; border-radius: 0 $base-border-radius $base-border-radius 0; &:hover { @@ -75,6 +80,24 @@ input[type="file"] { } } +.form-group.checkboxes { + [type="checkbox"] { + display: none; + } + + [type="checkbox"]:checked + .btn-info { + color: $white; + background-color: $scipost-lightblue; + border-color: $scipost-lightblue; + + } + + [type="checkbox"]:checked + .btn-primary { + background-color: $scipost-darkblue; + border-color: $scipost-darkblue; + } +} + // Formset // .delete-form-group { diff --git a/scipost/static/scipost/assets/css/_grid.scss b/scipost/static/scipost/assets/css/_grid.scss index e7ef90aaf393c8ec59f20056ba1c6ec87fa6fe0a..7b5c41d378921df7532013bb2938e9b4ce3d96eb 100644 --- a/scipost/static/scipost/assets/css/_grid.scss +++ b/scipost/static/scipost/assets/css/_grid.scss @@ -22,6 +22,12 @@ img { display: none; } +// footer.footer { +// position: fixed; +// width: 100%; +// bottom: 0; +// } + footer.secondary { color: $scipost-darkblue; background: transparent; diff --git a/scipost/static/scipost/assets/css/_nav.scss b/scipost/static/scipost/assets/css/_nav.scss index 4ff137786c65dd73d06efbb4dea0415f80e395cb..b904d97c8a7021ce05661e2d6b7be56f9ffdd3b9 100644 --- a/scipost/static/scipost/assets/css/_nav.scss +++ b/scipost/static/scipost/assets/css/_nav.scss @@ -1,3 +1,8 @@ +.nav, +nav { + background-color: #f0f0f0; +} + .nav.btn-group .nav-item { padding: 0 !important; } diff --git a/scipost/static/scipost/assets/js/scripts.js b/scipost/static/scipost/assets/js/scripts.js index 6dfe09de3b42b5601b25d5bc9dba53bae26f456f..c17d95694a341b8d5df160393ae149ec8214f5f9 100644 --- a/scipost/static/scipost/assets/js/scripts.js +++ b/scipost/static/scipost/assets/js/scripts.js @@ -39,4 +39,9 @@ $(function(){ var tab_name = e.target.hash.substring(1) window.history.replaceState({}, null, '?tab=' + tab_name); }); + + // Auto-submit hook for general form elements + $("form .auto-submit input").on('change', function(){ + $(this).parents('form').submit() + }) }); diff --git a/scipost/templates/scipost/about.html b/scipost/templates/scipost/about.html index 0b9e87cdcfbc52898db555f0abaa26f980c42149..bedfaadba0a6a0c582874b4131334dd0770ac3ee 100644 --- a/scipost/templates/scipost/about.html +++ b/scipost/templates/scipost/about.html @@ -25,14 +25,10 @@ <h2>SciPost is endorsed by</h2> <div class="row"> - <div class="col-md-6"> + <div class="col-12"> <a target="_blank" href="http://www.nwo.nl/en"><img src="{% static 'scipost/images/NWO_logo_EN.jpg' %}" alt="NWO logo" width='300px' /></a> <p id="NWOOpenAccess" class="mt-3">All articles published in SciPost Journals fulfill the Gold standard Open Access requirements of the NWO, as stipulated on the NWO’s <a href="http://www.nwo.nl/en/policies/open+science">Open Science page</a>.</p> </div> - <div class="col-md-6"> - <a target="_blank" href="http://www.fom.nl/live/english/home.pag"><img src="{% static 'scipost/images/FOMlogo_fc.jpg' %}" alt="FOM logo" width='100px' /></a> - <p style="font-size: 50%">FOM is part of NWO</p> - </div> </div> </div> </div> diff --git a/scipost/templates/scipost/footer.html b/scipost/templates/scipost/footer.html index a69ef2bf8bd2ab871184b3708c9a2ca4e0620da1..f792aebbd718746f0af4c4081b2c4e0cb3a68c1b 100644 --- a/scipost/templates/scipost/footer.html +++ b/scipost/templates/scipost/footer.html @@ -1,26 +1,28 @@ {% load staticfiles %} -<footer class="container-fluid py-2"> - <div class="row mt-2"> +<footer class="footer"> + <div class="container-fluid py-1"> + <div class="row my-3"> - <div class="col-md-4"> - Copyright © <a href="{% url 'scipost:foundation' %}" target="_">SciPost Foundation</a>. - <br/> - Contact the <a href="mailto:admin@scipost.org">administrators</a> or <a href="mailto:techsupport@scipost.org">tech support</a> - <br/> - <a href="{% url 'scipost:terms_and_conditions' %}">Terms and conditions.</a> - </div> - <div class="col-md-3"> - Follow us:<br/> - <table> - <tr> - <td><a href="//www.facebook.com/scipost" target="_blank" title="Facebook"><img src="{% static 'scipost/images/FB-f-Logo__white_29.png' %}" width="20" alt="Facebook"/></a></td> - <td><a href="//twitter.com/scipost_dot_org" target="_blank" title="Twitter"><img src="{% static 'scipost/images/Twitter_Logo_Blue.png' %}" width="32" alt="Twitter"/></a></td> - <td><a style="float: right;" href="{% url 'scipost:feeds' %}"><img src="{% static 'scipost/images/feed-icon-28x28.png' %}" alt="Feed logo" width="20"></a></td> - </tr> - </table> - </div> - <div class="col-md-5"> - <a rel="license" href="//creativecommons.org/licenses/by/4.0/" target="_blank"><img alt="Creative Commons License" style="border-width:0" src="//i.creativecommons.org/l/by/4.0/80x15.png" /></a><br />Except where otherwise noted, all content on SciPost is licensed under a <a rel="license" href="//creativecommons.org/licenses/by/4.0/" target="_blank">Creative Commons Attribution 4.0 International License</a>. - </div> + <div class="col-md-4 mb-3 mb-md-0"> + Copyright © <a href="{% url 'scipost:foundation' %}" target="_">SciPost Foundation</a> + <br/> + Contact the <a href="mailto:admin@scipost.org">administrators</a> or <a href="mailto:techsupport@scipost.org">tech support</a> + <br/> + <a href="{% url 'scipost:terms_and_conditions' %}">Terms and conditions</a> + </div> + <div class="col-md-3 mb-3 mb-md-0"> + Follow us:<br/> + <table> + <tr> + <td><a href="//www.facebook.com/scipost" target="_blank" title="Facebook"><img src="{% static 'scipost/images/FB-f-Logo__white_29.png' %}" width="20" alt="Facebook"/></a></td> + <td><a href="//twitter.com/scipost_dot_org" target="_blank" title="Twitter"><img src="{% static 'scipost/images/Twitter_Logo_Blue.png' %}" width="32" alt="Twitter"/></a></td> + <td><a style="float: right;" href="{% url 'scipost:feeds' %}"><img src="{% static 'scipost/images/feed-icon-28x28.png' %}" alt="Feed logo" width="20"></a></td> + </tr> + </table> + </div> + <div class="col-md-5"> + <a rel="license" href="//creativecommons.org/licenses/by/4.0/" target="_blank"><img alt="Creative Commons License" style="border-width:0" src="//i.creativecommons.org/l/by/4.0/80x15.png" /></a><br />Except where otherwise noted, all content on SciPost is licensed under a <a rel="license" href="//creativecommons.org/licenses/by/4.0/" target="_blank">Creative Commons Attribution 4.0 International License</a>. + </div> + </div> </div> </footer> diff --git a/scipost/templates/scipost/header.html b/scipost/templates/scipost/header.html index 2f9ddf2da8de3355fb651ceb26017797df7015ca..c85dd99638939ffefad2995eac8fb76b81e747a0 100644 --- a/scipost/templates/scipost/header.html +++ b/scipost/templates/scipost/header.html @@ -5,7 +5,7 @@ <div class="flex-logobox"> <a href="{% url 'scipost:index' %}"><img src="{% static 'scipost/images/logo_scipost_RGB_HTML_groot.png' %}" alt="SciPost logo" width="240" /></a> </div> - <div class="flex-blurbbox hidden-sm-down"> + <div class="flex-blurbbox d-none d-md-block"> <p> <i>The complete scientific publication portal</i><br /> <i>Managed by professional scientists</i><br /> diff --git a/scipost/templates/scipost/index.html b/scipost/templates/scipost/index.html index e4123e9a08226077699209ab1b2e29c25aaf9fe2..8e8b59886ec9c92fb95432fd1c59d68372c58d1c 100644 --- a/scipost/templates/scipost/index.html +++ b/scipost/templates/scipost/index.html @@ -112,7 +112,8 @@ <div class="card-footer"><a href="{% url 'news:news' %}">More news</a></div> </div><!-- End news --> - <!-- Partners --><!-- Disabled + {% comment %} + <!-- Partners --> <div class="card card-grey"> <div class="card-body"> <h2 class="card-title"> @@ -132,7 +133,8 @@ </div> </div> - </div>--><!-- End Partners --> + </div><!-- End Partners --> + {% endcomment %} <!-- Summarized --> <div class="card card-grey"> diff --git a/scipost/templates/scipost/navbar.html b/scipost/templates/scipost/navbar.html index f38d7da1d7a3e6c095107237e4274612bbf2e32e..ed33013d14f50075d450eba4b2c9916846f1a638 100644 --- a/scipost/templates/scipost/navbar.html +++ b/scipost/templates/scipost/navbar.html @@ -50,7 +50,7 @@ </ul> <form action="{% url 'scipost:search' %}" method="get" class="form-inline search-nav-form"> - <input class="form-control" id="id_q" maxlength="100" name="q" type="text" required="required" value="{{search_query}}"> + <input class="form-control" id="id_q" maxlength="100" name="q" type="text" required="required" value="{{search_query|default:''}}"> <input class="btn btn-secondary" type="submit" value="Search"> </form> </div> diff --git a/scipost/templates/scipost/search.html b/scipost/templates/scipost/search.html deleted file mode 100644 index dd9d6d630935dfd9ee83abfe95198de6019530f2..0000000000000000000000000000000000000000 --- a/scipost/templates/scipost/search.html +++ /dev/null @@ -1,171 +0,0 @@ -{% extends 'scipost/base.html' %} - -{% block pagetitle %}: list{% endblock pagetitle %} - -{% block content %} - -<div class="row"> - <div class="col-12"> - <h1>Search results{% if search_term %}: <small><i>{{ search_term }}</i></small>{% endif %}</h1> - {% if not publication_search_list and not commentary_search_list and not submission_search_list and not thesislink_search_list and not comment_search_link and not comment_search_list %} - <p>Your search query did not return any result.</p> - {% endif %} - </div> -</div> - -{% if publication_search_list %} -<hr> -<div class="row"> - <div class="col-12"> - <h2 class="highlight">Publications ({{publication_search_list|length}})</h2> - </div> -</div> - -<div class="row"> - <div class="col-12"> - {% for publication in publication_search_list %} - <div class="card card-publication"> - {% include 'journals/_publication_card_content.html' with publication=publication %} - </div> - {% endfor %} - </div> -</div> - -<div class="row"> - <div class="col-12"> - <div class="pagination"> - <span class="step-links"> - {% if publication_search_list.has_previous %} - <a href="?publication_search_list_page={{ publication_search_list.previous_page_number }}">previous</a> - {% endif %} - <span class="current"> - Page {{ publication_search_list.number }} of {{ publication_search_list.paginator.num_pages }}. - </span> - {% if publication_search_list.has_next %} - <a href="?publication_search_list_page={{ publication_search_list.next_page_number }}">next</a> - {% endif %} - </span> - </div> - </div> -</div> -{% endif %} - - -{% if commentary_search_list %} -<hr> -<div class="row"> - <div class="col-12"> - <h2 class="highlight">Commentaries ({{commentary_search_list|length}})</h2> - </div> -</div> -<div class="row"> - <div class="col-12"> - <ul class="list-group list-group-flush"> - {% for commentary in commentary_search_list %} - <li class="list-group-item"> - {% include 'commentaries/_commentary_card_content.html' with commentary=commentary %} - </li> - {% endfor %} - </ul> - </div> -</div> - -<div class="row"> - <div class="col-12"> - <div class="pagination"> - <span class="step-links"> - {% if commentary_search_list.has_previous %} - <a href="?commentary_search_list_page={{ commentary_search_list.previous_page_number }}">previous</a> - {% endif %} - <span class="current"> - Page {{ commentary_search_list.number }} of {{ commentary_search_list.paginator.num_pages }}. - </span> - {% if commentary_search_list.has_next %} - <a href="?commentary_search_list_page={{ commentary_search_list.next_page_number }}">next</a> - {% endif %} - </span> - </div> - </div> -</div> -{% endif %} - - -{% if submission_search_list %} -<hr> -<div class="row"> - <div class="col-12"> - <h2 class="highlight">Submissions ({{submission_search_list|length}})</h2> - </div> -</div> -<div class="row"> - <div class="col-12"> - <ul class="list-group list-group-flush"> - {% for submission in submission_search_list %} - <li class="list-group-item"> - {% include 'submissions/_submission_card_content.html' with submission=submission %} - </li> - {% endfor %} - </ul> - </div> -</div> - -<div class="row"> - <div class="col-12"> - <div class="pagination"> - <span class="step-links"> - {% if submission_search_list.has_previous %} - <a href="?submission_search_list_page={{ submission_search_list.previous_page_number }}">previous</a> - {% endif %} - <span class="current"> - Page {{ submission_search_list.number }} of {{ submission_search_list.paginator.num_pages }} - </span> - {% if submission_search_list.has_next %} - <a href="?submission_search_list_page={{ submission_search_list.next_page_number }}">next</a> - {% endif %} - </span> - </div> - </div> -</div> -{% endif %} - -{% if thesislink_search_list %} -<hr> -<div class="row"> - <div class="col-12"> - <h2 class="highlight">Theses ({{thesislink_search_list|length}})</h2> - </div> -</div> -<div class="row"> - <div class="col-12"> - <ul class="list-group list-group-flush"> - {% for thesislink in thesislink_search_list %} - <li class="list-group-item"> - {% include 'theses/_thesislink_card_content.html' with thesislink=thesislink %} - </li> - {% endfor %} - </ul> - </div> -</div> -{% endif %} - -{% if comment_search_list %} -<hr> -<div class="row"> - <div class="col-12"> - <h2 class="highlight">Comments ({{comment_search_list|length}})</h2> - </div> -</div> -<div class="row"> - <div class="col-12"> - <ul class="list-group list-group-flush"> - {% for comment in comment_search_list %} - <li class="list-group-item"> - {% include 'comments/_comment_card_extended_content.html' with comment=comment %} - </li> - {% endfor %} - </ul> - </div> -</div> -{% endif %} - -{% endblock content %} diff --git a/scipost/urls.py b/scipost/urls.py index 71d914230408fe0c8b94779cb33f014f3b98219a..e67452102c333fe77542e5a64e4fc24cf28e22dd 100644 --- a/scipost/urls.py +++ b/scipost/urls.py @@ -62,7 +62,7 @@ urlpatterns = [ name='pub_feed_spec_atom'), # Search - url(r'^search$', views.search, name='search'), + url(r'^search', views.SearchView.as_view(), name='search'), ################ # Contributors: diff --git a/scipost/views.py b/scipost/views.py index 58aeea2e4f475c25d64d285fa6cac7b412856ac7..282be7167390d69d24fe67cf9669acf14c4f5fb5 100644 --- a/scipost/views.py +++ b/scipost/views.py @@ -9,18 +9,17 @@ from django.contrib.auth.models import Group from django.contrib.auth.views import password_reset, password_reset_confirm from django.core import mail from django.core.mail import EmailMessage, EmailMultiAlternatives -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger +from django.core.paginator import Paginator from django.core.urlresolvers import reverse -from django.db.models import Q +from django.db.models import Prefetch from django.shortcuts import redirect from django.template import Context, Template from django.views.decorators.http import require_POST from django.views.generic.list import ListView -from django.db.models import Prefetch - from guardian.decorators import permission_required from guardian.shortcuts import assign_perm, get_objects_for_user +from haystack.generic_views import SearchView from .constants import SCIPOST_SUBJECT_AREAS, subject_areas_raw_dict, SciPost_from_addresses_dict from .decorators import has_contributor @@ -56,118 +55,19 @@ def is_registered(user): return user.groups.exists() -# Global search -def normalize_query(query_string, - findterms=re.compile(r'"([^"]+)"|(\S+)').findall, - normspace=re.compile(r'\s{2,}').sub): - """ Splits a query string in individual keywords, keeping quoted words together. """ - return [normspace(' ', (t[0] or t[1]).strip()) for t in findterms(query_string)] - - -def get_query(query_string, search_fields): - """ Returns a query, namely a combination of Q objects. """ - query = None - terms = normalize_query(query_string) - for term in terms: - or_query = None - for field_name in search_fields: - q = Q(**{"%s__icontains" % field_name: term}) - if or_query is None: - or_query = q - else: - or_query = or_query | q - if query is None: - query = or_query - else: - query = query & or_query - return query - +class SearchView(SearchView): + template_name = 'search/search.html' + form_class = SearchForm -def documentsSearchResults(query): - """ - Searches through commentaries, submissions and thesislinks. - Returns a Context object which can be further used in templates. - Naive implementation based on exact match of query. - NEEDS UPDATING with e.g. Haystack. - """ - publication_query = get_query(query, ['title', 'author_list', 'abstract', 'doi_label']) - commentary_query = get_query(query, ['title', 'author_list', 'pub_abstract']) - submission_query = get_query(query, ['title', 'author_list', 'abstract']) - thesislink_query = get_query(query, ['title', 'author', 'abstract', 'supervisor']) - comment_query = get_query(query, ['comment_text']) - - publication_search_queryset = (Publication.objects.published() - .filter(publication_query).order_by('-publication_date')) - commentary_search_queryset = (Commentary.objects.vetted() - .filter(commentary_query).order_by('-pub_date')) - submission_search_queryset = (Submission.objects.public_unlisted() - .filter(submission_query).order_by('-submission_date')) - thesislink_search_list = (ThesisLink.objects.vetted() - .filter(thesislink_query).order_by('-defense_date')) - comment_search_list = (Comment.objects.vetted() - .filter(comment_query).order_by('-date_submitted')) - - context = {'publication_search_queryset': publication_search_queryset, - 'commentary_search_queryset': commentary_search_queryset, - 'submission_search_queryset': submission_search_queryset, - 'thesislink_search_list': thesislink_search_list, - 'comment_search_list': comment_search_list} - return context - - -def search(request): - """ For the global search form in navbar """ - form = SearchForm(request.GET or None) - context = {} - if form.is_valid(): - context = documentsSearchResults(form.cleaned_data['q']) - request.session['query'] = form.cleaned_data['q'] - context['search_term'] = form.cleaned_data['q'] - elif 'query' in request.session: - context = documentsSearchResults(request.session['query']) - context['search_term'] = request.session['query'] - - if 'publication_search_queryset' in context: - publication_search_list_paginator = Paginator(context['publication_search_queryset'], 10) - publication_search_list_page = request.GET.get('publication_search_list_page') - try: - publication_search_list = publication_search_list_paginator.page( - publication_search_list_page) - except PageNotAnInteger: - publication_search_list = publication_search_list_paginator.page(1) - except EmptyPage: - publication_search_list = publication_search_list_paginator.page( - publication_search_list_paginator.num_pages) - context['publication_search_list'] = publication_search_list - - if 'commentary_search_queryset' in context: - commentary_search_list_paginator = Paginator(context['commentary_search_queryset'], 10) - commentary_search_list_page = request.GET.get('commentary_search_list_page') - try: - commentary_search_list = commentary_search_list_paginator.page( - commentary_search_list_page) - except PageNotAnInteger: - commentary_search_list = commentary_search_list_paginator.page(1) - except EmptyPage: - commentary_search_list = commentary_search_list_paginator.page( - commentary_search_list_paginator.num_pages) - context['commentary_search_list'] = commentary_search_list - - if 'submission_search_queryset' in context: - submission_search_list_paginator = Paginator(context['submission_search_queryset'], 10) - submission_search_list_page = request.GET.get('submission_search_list_page') - try: - submission_search_list = submission_search_list_paginator.page( - submission_search_list_page) - except PageNotAnInteger: - submission_search_list = submission_search_list_paginator.page(1) - except EmptyPage: - submission_search_list = submission_search_list_paginator.page( - submission_search_list_paginator.num_pages) - context['submission_search_list'] = submission_search_list - - context['search_query'] = request.GET.get('q') - return render(request, 'scipost/search.html', context) + def get_context_data(self, *args, **kwargs): + ctx = super().get_context_data(*args, **kwargs) + ctx['search_query'] = self.request.GET.get('q') + ctx['results_count'] = kwargs['object_list'].count() + + # Methods not supported by Whoosh engine + # ctx['stats_results'] = kwargs['object_list'].stats_results() + # ctx['facet_counts'] = kwargs['object_list'].facet('text').facet_counts() + return ctx ############# diff --git a/submissions/search_indexes.py b/submissions/search_indexes.py new file mode 100644 index 0000000000000000000000000000000000000000..c00fa9848ce54f09e27a5731da94df038fd027e6 --- /dev/null +++ b/submissions/search_indexes.py @@ -0,0 +1,19 @@ +# import datetime + +from haystack import indexes + +from .models import Submission + + +class SubmissionIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.CharField(document=True, model_attr='title', use_template=True) + authors = indexes.CharField(model_attr='author_list') + date = indexes.DateTimeField(model_attr='submission_date') + abstract = indexes.CharField(model_attr='abstract') + + def get_model(self): + return Submission + + def index_queryset(self, using=None): + """Used when the entire index for model is updated.""" + return self.get_model().objects.public() diff --git a/submissions/templates/partials/submissions/search_card.html b/submissions/templates/partials/submissions/search_card.html new file mode 100644 index 0000000000000000000000000000000000000000..537ed070cd91eafa4186ada284e3c42247bf5b91 --- /dev/null +++ b/submissions/templates/partials/submissions/search_card.html @@ -0,0 +1 @@ +{% extends 'submissions/_submission_card_content.html' %} diff --git a/templates/search/indexes/commentaries/commentary_text.txt b/templates/search/indexes/commentaries/commentary_text.txt new file mode 100644 index 0000000000000000000000000000000000000000..c1b85e04afc93c62a9f045e1dbf1fc090764ec30 --- /dev/null +++ b/templates/search/indexes/commentaries/commentary_text.txt @@ -0,0 +1,3 @@ +{{object.title}} +{{object.author_list}} +{{object.pub_abstract}} diff --git a/templates/search/indexes/comments/comment_text.txt b/templates/search/indexes/comments/comment_text.txt new file mode 100644 index 0000000000000000000000000000000000000000..ab1ca5595e0f2abc4146aad5a299b966b8e12665 --- /dev/null +++ b/templates/search/indexes/comments/comment_text.txt @@ -0,0 +1,2 @@ +{{object.comment_text}} +{{object.date_submitted}} diff --git a/templates/search/indexes/journals/publication_text.txt b/templates/search/indexes/journals/publication_text.txt new file mode 100644 index 0000000000000000000000000000000000000000..9301802cec499403a346eca064adb3c2b88650a7 --- /dev/null +++ b/templates/search/indexes/journals/publication_text.txt @@ -0,0 +1,6 @@ +{{object.title}} +{{object.author_list}} +{{object.publication_date}} +{{object.abstract}} +{{object.doi_string}} +{{object.citation}} diff --git a/templates/search/indexes/submissions/submission_text.txt b/templates/search/indexes/submissions/submission_text.txt new file mode 100644 index 0000000000000000000000000000000000000000..b2ce6f8d337a7831bab1472119067bb5d8129c41 --- /dev/null +++ b/templates/search/indexes/submissions/submission_text.txt @@ -0,0 +1,4 @@ +{{object.title}} +{{object.author_list}} +{{object.abstract}} +{{object.arxiv_identifier_w_vn_nr}} diff --git a/templates/search/indexes/theses/thesislink_text.txt b/templates/search/indexes/theses/thesislink_text.txt new file mode 100644 index 0000000000000000000000000000000000000000..1d4004be339036931c68984888c22ecef500e353 --- /dev/null +++ b/templates/search/indexes/theses/thesislink_text.txt @@ -0,0 +1,4 @@ +{{object.title}} +{{object.author}} +{{object.supervisor}} +{{object.abstract}} diff --git a/templates/search/search.html b/templates/search/search.html new file mode 100644 index 0000000000000000000000000000000000000000..ca70b5b79aab9d7322a0cbf847948bad91be43f8 --- /dev/null +++ b/templates/search/search.html @@ -0,0 +1,98 @@ +{% extends 'scipost/base.html' %} + +{% load bootstrap %} +{# {% load highlight %}#} + +{% block pagetitle %}: list{% endblock pagetitle %} + +{% block content %} + + +<div class="row"> + <div class="col-md-4"> + <h1>Filter</h1> + <form method="get"> + <div class="form-group"> + <label for="{{form.q.auto_id}}">{{form.q.label}}</label> + <input type="text" name="{{form.q.name}}" class="form-control form-control-lg" id="{{form.q.auto_id}}" aria-describedby="search_help" placeholder="Search term" value="{{form.q.value|default:''}}" required="required"> + <small id="search_help" class="form-text text-muted">{{form.q.help_text}}</small> + </div> + + <div class="form-group"> + <label>Type</label> + </div> + <div class="form-group checkboxes auto-submit"> + {% for field in form.models %} + <input type="checkbox" name="{{field.name}}" id="{{field.id_for_label}}" value="{{field.choice_value|stringformat:'s'}}" {% if field.choice_value in form.models.value %}checked="checked"{% endif %}> + <label class="btn btn-info" for="{{field.id_for_label}}"> + {{field.choice_label}} + </label> + + {% endfor %} + </div> + + <label>Date from</label> + <div class="form-row"> + {{form.start}} + </div> + + <label>Date until</label> + <div class="form-row"> + {{form.end}} + </div> + + <div class="form-group"> + {# <input type="reset" class="btn btn-danger" value="Reset">#} + <input type="submit" class="btn btn-primary" value="Search"> + </div> + </form> + </div> + <div class="col-md-8"> + <h1 class="my-3 mt-md-0">Search results{% if results_count and query %} ({{results_count}} found){% endif %}</h1> + + {# without this logic, an request without GET parameters would return *all* objects indexed by Haystack #} + {% if query %} + <ul class="list-group list-group-flush"> + {% for result in object_list %} + <li class="list-group-item{% if result.content_type == 'journals.publication' %} border-0{% endif %}"> + {% if result.content_type == 'submissions.submission' %} + {% include 'partials/submissions/search_card.html' with submission=result.object %} + {% elif result.content_type == 'commentaries.commentary' %} + {% include 'partials/commentaries/search_card.html' with commentary=result.object %} + {% elif result.content_type == 'theses.thesislink' %} + {% include 'partials/theses/search_card.html' with thesislink=result.object %} + {% elif result.content_type == 'comments.comment' %} + <div class="py-2"> + {% include 'partials/comments/search_card.html' with comment=result.object %} + </div> + {% elif result.content_type == 'journals.publication' %} + <div class="card card-publication"> + {% include 'partials/journals/search_card.html' with publication=result.object %} + </div> + {% else %} + <a href="{{ result.object.get_absolute_url }}">{{ result.object.title }}</a> + {% endif %} + </li> + {% empty %} + <p>Your search query did not return any result.</p> + {% endfor %} + + {% if is_paginated %} + <p> + {% if page_obj.has_previous %} + <a href="?q={{ query }}&page={{ page_obj.previous_page_number }}">Previous</a> + {% endif %} + Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}. + {% if page_obj.has_next %} + <a href="?q={{ query }}&page={{ page_obj.next_page_number }}">Next</a> + {% endif %} + </p> + {% endif %} + </ul> + {% else %} + <p>Your search query did not return any result.</p> + {% endif %} + </div> +</div> + +{% endblock content %} diff --git a/theses/models.py b/theses/models.py index e15ebe73301fc9b7b5282a5576ba81a7b515ee15..ad143b5a72d9c5cfa1b4fff33fe4cdbfa0d0afca 100644 --- a/theses/models.py +++ b/theses/models.py @@ -44,7 +44,7 @@ class ThesisLink(models.Model): author_false_claims = models.ManyToManyField( 'scipost.Contributor', blank=True, related_name='authors_thesis_false_claims') - supervisor = models.CharField(max_length=1000, default='') + supervisor = models.CharField(max_length=1000) supervisor_as_cont = models.ManyToManyField( 'scipost.Contributor', blank=True, verbose_name='supervisor(s)', diff --git a/theses/search_indexes.py b/theses/search_indexes.py new file mode 100644 index 0000000000000000000000000000000000000000..804c2d206f4a1bb6416f8b2e2fb05fe362322898 --- /dev/null +++ b/theses/search_indexes.py @@ -0,0 +1,20 @@ +# import datetime + +from haystack import indexes + +from .models import ThesisLink + + +class ThesisIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.CharField(document=True, model_attr='title', use_template=True) + authors = indexes.CharField(model_attr='author') + supervisor = indexes.CharField(model_attr='supervisor') + date = indexes.DateTimeField(model_attr='defense_date') + abstract = indexes.CharField(model_attr='abstract') + + def get_model(self): + return ThesisLink + + def index_queryset(self, using=None): + """Used when the entire index for model is updated.""" + return self.get_model().objects.vetted() diff --git a/theses/templates/partials/theses/search_card.html b/theses/templates/partials/theses/search_card.html new file mode 100644 index 0000000000000000000000000000000000000000..dc78efd39c67be7c11c563e13ef3ac586ce0d609 --- /dev/null +++ b/theses/templates/partials/theses/search_card.html @@ -0,0 +1 @@ +{% extends 'theses/_thesislink_card_content.html' %}