diff --git a/SciPost_v1/settings.py b/SciPost_v1/settings.py index 831913e4b0035d43dc8eb46bd94cd18057460187..4e6a5fc96a19b7b16c793cfa9f3ea3948db188d6 100644 --- a/SciPost_v1/settings.py +++ b/SciPost_v1/settings.py @@ -12,12 +12,14 @@ https://docs.djangoproject.com/en/1.8/ref/settings/ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os - import json +from django.utils.translation import ugettext_lazy as _ + + BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -host_settings_path = os.path.join(os.path.dirname(BASE_DIR),"scipost-host-settings.json") +host_settings_path = os.path.join(os.path.dirname(BASE_DIR), "scipost-host-settings.json") host_settings = json.load(open(host_settings_path)) # Quick-start development settings - unsuitable for production @@ -109,6 +111,7 @@ MATHJAX_CONFIG_DATA = { MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -129,6 +132,7 @@ TEMPLATES = [ 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', + 'django.template.context_processors.i18n', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', 'scipost.context_processors.searchform', @@ -159,7 +163,12 @@ DATABASES = { # https://docs.djangoproject.com/en/1.8/topics/i18n/ LANGUAGE_CODE = 'en-us' - +LANGUAGES = ( + ('en', _('English')), +) +LOCALE_PATHS = ( + os.path.join(BASE_DIR, 'locale'), +) TIME_ZONE = 'CET' USE_I18N = True diff --git a/comments/migrations/0005_merge_20161219_2126.py b/comments/migrations/0005_merge_20161219_2126.py new file mode 100644 index 0000000000000000000000000000000000000000..f604a8605fd90bc48a48ababc797a758ed29c2a3 --- /dev/null +++ b/comments/migrations/0005_merge_20161219_2126.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2016-12-19 20:26 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('comments', '0004_auto_20161212_1931'), + ('comments', '0004_auto_20161213_1208'), + ] + + operations = [ + ] diff --git a/common/helpers/factories.py b/common/helpers/factories.py new file mode 100644 index 0000000000000000000000000000000000000000..b033a898be81b691d884566550a661480c663611 --- /dev/null +++ b/common/helpers/factories.py @@ -0,0 +1,10 @@ +import factory + + +class FormFactory(factory.Factory): + class Meta: + strategy = factory.BUILD_STRATEGY + + @classmethod + def _build(cls, model_class, *args, **kwargs): + return model_class(kwargs) diff --git a/scipost/templates/scipost/base.html b/scipost/templates/scipost/base.html index f69fa7db79b8b713e79d763ca35302a632eb0cad..2bedef9de0857fbfb823f74ed24c21783a37cd06 100644 --- a/scipost/templates/scipost/base.html +++ b/scipost/templates/scipost/base.html @@ -26,6 +26,7 @@ <body> {% include 'scipost/header.html' %} {% include 'scipost/navbar.html' %} + {% include 'scipost/messages.html' %} {% block bodysup %} {% endblock bodysup %} diff --git a/scipost/templates/scipost/messages.html b/scipost/templates/scipost/messages.html new file mode 100644 index 0000000000000000000000000000000000000000..ac8c6dff8c12a6bea16bcdaca31738d52cd55336 --- /dev/null +++ b/scipost/templates/scipost/messages.html @@ -0,0 +1,8 @@ +{% for message in messages %} + <div class="alert {{ message.tags }} alert-dismissible" role="alert"> + <button type="button" class="close" data-dismiss="alert" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + {{ message }} + </div> +{% endfor %} diff --git a/strings/__init__.py b/strings/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..82d957c9dde2ab9c1606aa3568f778e408e13a84 --- /dev/null +++ b/strings/__init__.py @@ -0,0 +1 @@ +acknowledge_request_thesis_link = "Thank you for your request for a Thesis Link. Your request will soon be handled by an editor" diff --git a/theses/factories.py b/theses/factories.py index 1a12cd026941ad06ef7e4ad82c798b6947813a93..bc57863b350e82579b8725bd27884bdeb1382c24 100644 --- a/theses/factories.py +++ b/theses/factories.py @@ -1,7 +1,11 @@ import factory -from .models import ThesisLink + +from common.helpers.factories import FormFactory from scipost.factories import ContributorFactory +from .models import ThesisLink +from .forms import VetThesisLinkForm + class ThesisLinkFactory(factory.django.DjangoModelFactory): class Meta: @@ -9,7 +13,7 @@ class ThesisLinkFactory(factory.django.DjangoModelFactory): requested_by = factory.SubFactory(ContributorFactory) type = ThesisLink.MASTER_THESIS - title = factory.Sequence(lambda n: "thesis {0}".format(n)) + title = factory.Faker('bs') pub_link = factory.Faker('uri') author = factory.Faker('name') supervisor = factory.Faker('name') @@ -17,3 +21,11 @@ class ThesisLinkFactory(factory.django.DjangoModelFactory): defense_date = factory.Faker('date_time_this_century') abstract = factory.Faker('text') domain = 'ET' + + +class VetThesisLinkFormFactory(FormFactory): + class Meta: + model = VetThesisLinkForm + + action_option = VetThesisLinkForm.ACCEPT + # justification = factory.Faker('lorem') diff --git a/theses/forms.py b/theses/forms.py index 13b4a1793a47b46f2e9c993e8c55c2d86ebf0834..05963cb3221ca5cadcc553f653090d316395470b 100644 --- a/theses/forms.py +++ b/theses/forms.py @@ -3,18 +3,6 @@ from django import forms from .models import * from .helpers import past_years -THESIS_ACTION_CHOICES = ( - (0, 'modify'), - (1, 'accept'), - (2, 'refuse (give reason below)'), - ) - -THESIS_REFUSAL_CHOICES = ( - (0, '-'), - (-1, 'a link to this thesis already exists'), - (-2, 'the external link to this thesis does not work'), - ) - class RequestThesisLinkForm(forms.ModelForm): class Meta: @@ -29,13 +17,35 @@ class RequestThesisLinkForm(forms.ModelForm): class VetThesisLinkForm(forms.Form): - action_option = forms.ChoiceField(widget=forms.RadioSelect, - choices=THESIS_ACTION_CHOICES, - required=True, label='Action') + MODIFY = 0 + ACCEPT = 1 + REFUSE = 2 + THESIS_ACTION_CHOICES = ( + (MODIFY, 'modify'), + (ACCEPT, 'accept'), + (REFUSE, 'refuse (give reason below)'), + ) + + EMPTY_CHOICE = 0 + ALREADY_EXISTS = 1 + LINK_DOES_NOT_WORK = 2 + THESIS_REFUSAL_CHOICES = ( + (EMPTY_CHOICE, '---'), + (ALREADY_EXISTS, 'a link to this thesis already exists'), + (LINK_DOES_NOT_WORK, 'the external link to this thesis does not work'), + ) + + action_option = forms.ChoiceField( + widget=forms.RadioSelect, choices=THESIS_ACTION_CHOICES, required=True, label='Action') refusal_reason = forms.ChoiceField(choices=THESIS_REFUSAL_CHOICES, required=False) - email_response_field = forms.CharField(widget=forms.Textarea( + justification = forms.CharField(widget=forms.Textarea( attrs={'rows': 5, 'cols': 40}), label='Justification (optional)', required=False) + def vet_request(self, thesis_link): + print(self.cleaned_data) + if self.cleaned_data['action_option'] == VetThesisLinkForm.ACCEPT: + print('hoi') + class ThesisLinkSearchForm(forms.Form): author = forms.CharField(max_length=100, required=False, label="Author") diff --git a/theses/templates/theses/vet_thesislink_requests.html b/theses/templates/theses/vet_thesislink_requests.html index 4fd2a7d9abc646c28443f658ae4cc156e24e6cb1..c49008776f23329e746b62c1e0d84c9da1f3a178 100644 --- a/theses/templates/theses/vet_thesislink_requests.html +++ b/theses/templates/theses/vet_thesislink_requests.html @@ -22,7 +22,7 @@ <p>{{ thesislink_to_vet.abstract }}</p> </div> <div class="col-4"> - <form action="{% url 'theses:vet_thesislink_request_ack' thesislink_id=thesislink_to_vet.id %}" method="post"> + <form method="post"> {% csrf_token %} {{ form.as_ul }} <input type="submit" value="Submit" /> diff --git a/theses/test_forms.py b/theses/test_forms.py index ce07aa6214b9811d4b0ca54e09c7b5b304b93901..1456184593dd64e0f63f12574fb5c59b87576e6a 100644 --- a/theses/test_forms.py +++ b/theses/test_forms.py @@ -2,8 +2,8 @@ import factory from django.test import TestCase -from .factories import ThesisLinkFactory -from .forms import RequestThesisLinkForm +from .factories import ThesisLinkFactory, VetThesisLinkFormFactory +from .forms import RequestThesisLinkForm, VetThesisLinkForm from common.helpers import model_form_data @@ -24,3 +24,21 @@ class TestRequestThesisLink(TestCase): form = RequestThesisLinkForm(form_data) form.is_valid() self.assertEqual(form.errors['domain'], ['This field is required.']) + + +class TestVetThesisLinkRequests(TestCase): + fixtures = ['permissions', 'groups'] + + def test_thesislink_gets_vetted_when_accepted(self): + thesis_link = ThesisLinkFactory() + form = VetThesisLinkFormFactory() + form.is_valid() + form.vet_request(thesis_link) + self.assertTrue(thesis_link.vetted) + + def test_thesislink_is_not_vetted_when_refused(self): + thesis_link = ThesisLinkFactory() + form = VetThesisLinkFormFactory(action_option=VetThesisLinkForm.REFUSE) + form.is_valid() + form.vet_request(thesis_link) + self.assertFalse(thesis_link.vetted) diff --git a/theses/test_views.py b/theses/test_views.py index 7e70a6dd41170d221dfd8a4d586a4a8b0ff0c38c..faa7f14d5d621096441faa542af7984e8e629573 100644 --- a/theses/test_views.py +++ b/theses/test_views.py @@ -1,11 +1,14 @@ +import re + +from django.core.exceptions import PermissionDenied from django.test import TestCase, RequestFactory from django.test.client import Client -from django.contrib.auth.models import AnonymousUser +from django.contrib.auth.models import Group from django.urls import reverse -from .views import RequestThesisLink -from scipost.factories import UserFactory -from .factories import ThesisLinkFactory +from .views import RequestThesisLink, VetThesisLinkRequests +from scipost.factories import UserFactory, ContributorFactory +from .factories import ThesisLinkFactory, VetThesisLinkFormFactory from .models import ThesisLink @@ -25,18 +28,63 @@ class TestRequestThesisLink(TestCase): def setUp(self): self.client = Client() + self.target = reverse('theses:request_thesislink') def test_response_when_not_logged_in(self): '''A visitor that is not logged in cannot view this page.''' - response = self.client.get(reverse('theses:request_thesislink')) + response = self.client.get(self.target) self.assertEqual(response.status_code, 403) def test_response_when_logged_in(self): - request = RequestFactory().get(reverse('theses:request_thesislink')) + request = RequestFactory().get(self.target) request.user = UserFactory() response = RequestThesisLink.as_view()(request) self.assertEqual(response.status_code, 200) - def test_redirects_to_acknowledgement_page(self): - response = self.client.post(reverse('theses:request_thesislink'), {}, follow=True) - self.assertRedirects(response, reverse('scipost:acknowledgement')) + +class TestVetThesisLinkRequests(TestCase): + fixtures = ['groups', 'permissions'] + + def setUp(self): + self.client = Client() + self.target = reverse('theses:vet_thesislink_requests') + + def test_response_when_not_logged_in(self): + response = self.client.get(self.target) + self.assertEqual(response.status_code, 403) + + def test_response_regular_contributor(self): + ''' + A Contributor needs to be in the Vetting Editors group to be able to + vet submitted thesis links. + ''' + # Create ThesisLink to vet. + ThesisLinkFactory() + request = RequestFactory().get(self.target) + user = UserFactory() + request.user = user + self.assertRaises( + PermissionDenied, VetThesisLinkRequests.as_view(), request) + + def test_response_vetting_editor(self): + # Create ThesisLink to vet. + ThesisLinkFactory() + request = RequestFactory().get(self.target) + user = UserFactory() + user.groups.add(Group.objects.get(name="Vetting Editors")) + request.user = user + response = VetThesisLinkRequests.as_view()(request) + self.assertEqual(response.status_code, 200) + + def test_thesislink_is_vetted_by_correct_contributor(self): + # TODO: how to make sure we are vetting the right thesis link? + contributor = ContributorFactory() + contributor.user.groups.add(Group.objects.get(name="Vetting Editors")) + post_data = VetThesisLinkFormFactory().data + + request = RequestFactory().post(self.target, post_data) + request.user = contributor.user + + response = VetThesisLinkRequests.as_view()(request) + + self.assertTrue(False) diff --git a/theses/urls.py b/theses/urls.py index 05839aa7550bedf319b58d82bb3e7aa59703cacf..3d3c530e497a0ca6794259b9626530d6a8e8fbc9 100644 --- a/theses/urls.py +++ b/theses/urls.py @@ -9,7 +9,7 @@ urlpatterns = [ url(r'^browse/(?P<discipline>[a-z]+)/(?P<nrweeksback>[0-9]+)/$', views.browse, name='browse'), url(r'^(?P<thesislink_id>[0-9]+)/$', views.thesis_detail, name='thesis'), url(r'^request_thesislink$', views.RequestThesisLink.as_view(), name='request_thesislink'), - url(r'^vet_thesislink_requests$', views.vet_thesislink_requests, + url(r'^vet_thesislink_requests$', views.VetThesisLinkRequests.as_view(), name='vet_thesislink_requests'), url(r'^vet_thesislink_request_ack/(?P<thesislink_id>[0-9]+)$', views.vet_thesislink_request_ack, name='vet_thesislink_request_ack'), diff --git a/theses/views.py b/theses/views.py index 00ede98acfc6398837ed08ef52493f877341aa73..7e782a4b88b20052e4c52217c4544106c246ced6 100644 --- a/theses/views.py +++ b/theses/views.py @@ -5,12 +5,13 @@ from django.shortcuts import get_object_or_404, render from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.models import User +from django.contrib import messages from django.core.mail import EmailMessage -from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse, reverse_lazy from django.http import HttpResponse, HttpResponseRedirect from django.views.decorators.csrf import csrf_protect from django.db.models import Avg -from django.views.generic.edit import CreateView +from django.views.generic.edit import CreateView, FormView from django.utils.decorators import method_decorator from .models import * @@ -33,25 +34,43 @@ title_dict = dict(TITLE_CHOICES) # Convert titles for use in emails class RequestThesisLink(CreateView): form_class = RequestThesisLinkForm template_name = 'theses/request_thesislink.html' - success_url = '' + success_url = reverse_lazy('scipost:personal_page') def form_valid(self, form): - context = {'ack_header': 'Thank you for your request for a Thesis Link', - 'ack_message': 'Your request will soon be handled by an Editor. ', - 'followup_message': 'Return to your ', - 'followup_link': reverse('scipost:personal_page'), - 'followup_link_label': 'personal page'} - return render(self.request, 'scipost/acknowledgement.html', context) + messages.add_message(self.request, messages.SUCCESS, + strings.acknowledge_request_thesis_link) + return super(RequestThesisLink, self).form_valid(form) -@permission_required('scipost.can_vet_thesislink_requests', raise_exception=True) -def vet_thesislink_requests(request): - contributor = Contributor.objects.get(user=request.user) - thesislink_to_vet = ThesisLink.objects.filter( - vetted=False).first() # only handle one at a time - form = VetThesisLinkForm() - context = {'contributor': contributor, 'thesislink_to_vet': thesislink_to_vet, 'form': form} - return render(request, 'theses/vet_thesislink_requests.html', context) +@method_decorator(permission_required( + 'scipost.can_vet_thesislink_requests', raise_exception=True), name='dispatch') +class VetThesisLinkRequests(FormView): + form_class = VetThesisLinkForm + template_name = 'theses/vet_thesislink_requests.html' + # TODO: not right yet + success_url = reverse_lazy('theses:vet_thesislink_requests') + + def get_context_data(self, **kwargs): + context = super(VetThesisLinkRequests, self).get_context_data(**kwargs) + context['thesislink_to_vet'] = self.thesislink_to_vet() + return context + + def thesislink_to_vet(self): + return ThesisLink.objects.filter(vetted=False).first() + + def form_valid(self, form): + form.vet_request(self.thesislink_to_vet()) + return super(VetThesisLinkRequests, self).form_valid(form) + + +# @permission_required('scipost.can_vet_thesislink_requests', raise_exception=True) +# def vet_thesislink_requests(request): +# contributor = Contributor.objects.get(user=request.user) +# thesislink_to_vet = ThesisLink.objects.filter( +# vetted=False).first() # only handle one at a time +# form = VetThesisLinkForm() +# context = {'contributor': contributor, 'thesislink_to_vet': thesislink_to_vet, 'form': form} +# return render(request, 'theses/vet_thesislink_requests.html', context) @permission_required('scipost.can_vet_thesislink_requests', raise_exception=True) @@ -111,9 +130,9 @@ def vet_thesislink_request_ack(request, thesislink_id): + ', has not been activated for the following reason: ' + form.cleaned_data['refusal_reason'] + '.\n\nThank you for your interest, \nThe SciPost Team.') - if form.cleaned_data['email_response_field']: + if form.cleaned_data['justification']: email_text += '\n\nFurther explanations: ' + \ - form.cleaned_data['email_response_field'] + form.cleaned_data['justification'] emailmessage = EmailMessage('SciPost Thesis Link', email_text, 'SciPost Theses <theses@scipost.org>', [thesislink.requested_by.user.email], @@ -208,17 +227,16 @@ def thesis_detail(request, thesislink_id): new_comment.save() author.nr_comments = Comment.objects.filter(author=author).count() author.save() - #request.session['thesislink_id'] = thesislink_id - #return HttpResponseRedirect(reverse('comments:comment_submission_ack')) - context = {'ack_header': 'Thank you for contributing a Comment.', - 'ack_message': 'It will soon be vetted by an Editor.', - 'followup_message': 'Back to the ', - 'followup_link': reverse( - 'theses:thesis', - kwargs={'thesislink_id': newcomment.thesislink.id} - ), - 'followup_link_label': ' Thesis Link page you came from' - } + context = { + 'ack_header': 'Thank you for contributing a Comment.', + 'ack_message': 'It will soon be vetted by an Editor.', + 'followup_message': 'Back to the ', + 'followup_link': reverse( + 'theses:thesis', + kwargs={'thesislink_id': new_comment.thesislink.id} + ), + 'followup_link_label': ' Thesis Link page you came from' + } return render(request, 'scipost/acknowledgement.html', context) else: form = CommentForm()