diff --git a/.gitignore b/.gitignore index 0eeb06ba57fd256d5cd7eab1ba083b5be4f0cc83..63c279548e44fe2f5fc42adada3d549cd9f72055 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ __pycache__ *~ +.DS_STORE + # Package managers /venv* /node_modules/ diff --git a/README.md b/README.md index da09e066c26eea7bc0ad388464ef197014d84910..0a4608961069118a9a1404f4966efe48fdb0e225 100644 --- a/README.md +++ b/README.md @@ -158,8 +158,16 @@ To build the documentation, run: After this, generated documentation should be available in `docs/_build/html`. ## Writing tests -It is recommended, when writing tests, to use the `ContributorFactory` located in `scipost.factories`. This will automatically generate a related user with Registered Contributor membership. You may probably need to use the fixture list `["permissions", "groups"]` in your tests make sure the permissions groups are working properly. -It is recommended, when writing tests for new models, to make use of ModelFactories instead of fixtures to prevent issues with altering fields in the model later on. +It is recommended, when writing tests, to use the `ContributorFactory` located in `scipost.factories`. This will +automatically generate a related user with Registered Contributor membership. Using the `Contributor` model in tests +requires loading the permissions and groups. Previously, this was done by including `fixtures = ["permissions", +"groups"]` at the top of the `TestCase`, but since these fixtures behave unpredictable and are a nuisance to keep up to +date with the actual groups and permissions, it is much better to call `add_groups_and_permissions`, located in +`common.helpers.test`, in a function named `setUp`, which runs before each test. `add_groups_and_permissions` wraps the +management command of the same name. + +It is recommended, when writing tests for new models, to make use of `ModelFactory` instead of fixtures +for the same reason. A basic example of a test might look like: ```shell @@ -167,12 +175,12 @@ from django.contrib.auth.models import Group from django.test import TestCase from scipost.factories import ContributorFactory +from common.helpers.test import add_groups_and_permissions class VetCommentaryRequestsTest(TestCase): - fixtures = ['groups', 'permissions'] - def setUp(self): + add_groups_and_permissions() self.contributor = ContributorFactory(user__password='test123') # The default password is `adm1n` def test_example_test(self): diff --git a/commentaries/test_forms.py b/commentaries/test_forms.py index cec31d3071f2944cc61a8c0cebb785adb186f27f..8733064ae2267d07111ed7c9fe0c1662cc85fb3f 100644 --- a/commentaries/test_forms.py +++ b/commentaries/test_forms.py @@ -6,12 +6,12 @@ from scipost.factories import UserFactory from .factories import VettedCommentaryFactory, UnvettedCommentaryFactory from .forms import RequestCommentaryForm, VetCommentaryForm from .models import Commentary +from common.helpers.test import add_groups_and_permissions class TestVetCommentaryForm(TestCase): - fixtures = ['permissions', 'groups'] - def setUp(self): + add_groups_and_permissions() self.commentary = UnvettedCommentaryFactory.create() self.user = UserFactory() self.form_data = { @@ -71,9 +71,8 @@ class TestVetCommentaryForm(TestCase): class TestRequestCommentaryForm(TestCase): - fixtures = ['permissions', 'groups'] - def setUp(self): + add_groups_and_permissions() factory_instance = VettedCommentaryFactory.build() self.user = UserFactory() self.valid_form_data = model_form_data(factory_instance, RequestCommentaryForm) diff --git a/commentaries/test_views.py b/commentaries/test_views.py index 50d6850fd46c2b7166370a078492a227e04dcda5..17ea43b4c1d38d48a40b41d0e9edddd86930d61a 100644 --- a/commentaries/test_views.py +++ b/commentaries/test_views.py @@ -1,46 +1,45 @@ from django.core.urlresolvers import reverse from django.contrib.auth.models import Group -from django.test import TestCase, Client +from django.test import TestCase, Client, RequestFactory -from scipost.factories import ContributorFactory +from scipost.factories import ContributorFactory, UserFactory from .factories import UnvettedCommentaryFactory, VettedCommentaryFactory, UnpublishedVettedCommentaryFactory from .forms import CommentarySearchForm from .models import Commentary +from .views import RequestCommentary +from common.helpers.test import add_groups_and_permissions class RequestCommentaryTest(TestCase): """Test cases for `request_commentary` view method""" - fixtures = ['permissions', 'groups', 'contributors'] - def setUp(self): + add_groups_and_permissions() self.view_url = reverse('commentaries:request_commentary') self.login_url = reverse('scipost:login') self.redirected_login_url = '%s?next=%s' % (self.login_url, self.view_url) - def test_get_requests(self): - """Test different GET requests on view""" - # Anoymous user should redirect to login page + def test_redirects_if_not_logged_in(self): request = self.client.get(self.view_url) self.assertRedirects(request, self.redirected_login_url) - # Registered Contributor should get 200 - self.client.login(username="Test", password="testpw") - request = self.client.get(self.view_url) - self.assertEquals(request.status_code, 200) + def test_valid_response_if_logged_in(self): + """Test different GET requests on view""" + request = RequestFactory().get(self.view_url) + request.user = UserFactory() + response = RequestCommentary.as_view()(request) + self.assertEqual(response.status_code, 200) def test_post_invalid_forms(self): """Test different kind of invalid RequestCommentaryForm submits""" - self.client.login(username="Test", password="testpw") - request = self.client.post(self.view_url) - self.assertEquals(request.status_code, 200) + raise NotImplementedError class VetCommentaryRequestsTest(TestCase): """Test cases for `vet_commentary_requests` view method""" - fixtures = ['groups', 'permissions'] def setUp(self): + add_groups_and_permissions() self.view_url = reverse('commentaries:vet_commentary_requests') self.login_url = reverse('scipost:login') self.password = 'test123' @@ -90,9 +89,9 @@ class VetCommentaryRequestsTest(TestCase): class BrowseCommentariesTest(TestCase): """Test cases for `browse` view.""" - fixtures = ['groups', 'permissions'] def setUp(self): + add_groups_and_permissions() VettedCommentaryFactory(discipline='physics') self.view_url = reverse('commentaries:browse', kwargs={ 'discipline': 'physics', @@ -111,9 +110,8 @@ class BrowseCommentariesTest(TestCase): class CommentaryDetailTest(TestCase): - fixtures = ['permissions', 'groups'] - def setUp(self): + add_groups_and_permissions() self.client = Client() self.commentary = UnpublishedVettedCommentaryFactory() self.target = reverse( diff --git a/comments/test_views.py b/comments/test_views.py index e1824139a5c2dd7def2f41ae2f6c6fd742914e41..d80fea2bd5e779336dc0d3ce31a6e77f9ae23a91 100644 --- a/comments/test_views.py +++ b/comments/test_views.py @@ -13,12 +13,13 @@ from .factories import CommentFactory from .forms import CommentForm from .models import Comment from .views import new_comment - from common.helpers import model_form_data +from common.helpers.test import add_groups_and_permissions class TestNewComment(TestCase): - fixtures = ['groups', 'permissions'] + def setUp(self): + add_groups_and_permissions() def install_messages_middleware(self, request): # I don't know what the following three lines do, but they help make a RequestFactory diff --git a/common/helpers/__init__.py b/common/helpers/__init__.py index 5bab1621fd150b0eaa7904b18cbf5e41cc82f628..78f99d9b8e50de1c0ce2fc15ae82ddf0eb23ef85 100644 --- a/common/helpers/__init__.py +++ b/common/helpers/__init__.py @@ -29,7 +29,6 @@ def model_form_data(model, form_class, form_kwargs={}): form_fields = list(form_class(**form_kwargs).fields.keys()) return filter_keys(model_data, form_fields) - def random_arxiv_identifier_with_version_number(): return random_arxiv_identifier_without_version_number() + "v0" diff --git a/common/helpers/test.py b/common/helpers/test.py new file mode 100644 index 0000000000000000000000000000000000000000..2c0d7c96d9724179fc73d98a7a14709ba2d35fbb --- /dev/null +++ b/common/helpers/test.py @@ -0,0 +1,4 @@ +import scipost.management.commands.add_groups_and_permissions + +def add_groups_and_permissions(): + scipost.management.commands.add_groups_and_permissions.Command().handle(verbose=False) diff --git a/scipost/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py index 8ca05f6dffe5bc6f5a6c728945ee903f6d019731..b661bab5b80ec3002a089da3bdd0639e4484c571 100644 --- a/scipost/management/commands/add_groups_and_permissions.py +++ b/scipost/management/commands/add_groups_and_permissions.py @@ -9,7 +9,7 @@ from scipost.models import Contributor class Command(BaseCommand): help = 'Defines groups and permissions' - def handle(self, *args, **options): + def handle(self, *args, verbose=True, **options): """Append all user Groups and setup a Contributor roles to user.""" # Create Groups @@ -229,4 +229,6 @@ class Command(BaseCommand): can_view_docs_scipost, ) - self.stdout.write(self.style.SUCCESS('Successfully created groups and permissions.')) + + if verbose: + self.stdout.write(self.style.SUCCESS('Successfully created groups and permissions.')) diff --git a/scipost/services.py b/scipost/services.py index 957bb7603e66f5980f8d1568fcd29508427b5b8c..395e61e7ea5859a147a2170994c84e43ab6c16c3 100644 --- a/scipost/services.py +++ b/scipost/services.py @@ -166,7 +166,7 @@ class DOICaller(BaseCaller): class ArxivCaller(BaseCaller): """ Performs an Arxiv article lookup for given identifier """ - # # State of the caller + # State of the caller resubmission = False previous_submissions = [] errormessages = arxiv_caller_errormessages diff --git a/scipost/test_views.py b/scipost/test_views.py index 5b764927f0baa855389a30b22dafd751cfaa6a2a..22291f792c19630af0cc6b5192667fb3b4c4d8b4 100644 --- a/scipost/test_views.py +++ b/scipost/test_views.py @@ -8,8 +8,8 @@ from commentaries.forms import CommentarySearchForm from commentaries.models import Commentary from .factories import ContributorFactory,\ - EditorialCollegeFellowFactory, EditorialCollegeFactory -from .models import EditorialCollege, EditorialCollegeFellow + EditorialCollegeFellowshipFactory, EditorialCollegeFactory +from .models import EditorialCollege, EditorialCollegeFellowship class RequestCommentaryTest(TestCase): @@ -137,7 +137,7 @@ class AboutViewTest(TestCase): # Create College with 10 members self.college = EditorialCollegeFactory() - EditorialCollegeFellowFactory.create_batch(10) + EditorialCollegeFellowshipFactory.create_batch(10) def test_status_code_200_including_members(self): response = self.client.get(self.target) @@ -151,4 +151,4 @@ class AboutViewTest(TestCase): # Members exist in college self.assertTrue(college.member.count() >= 10) last_member = college.member.last() - self.assertTrue(isinstance(last_member, EditorialCollegeFellow)) + self.assertTrue(isinstance(last_member, EditorialCollegeFellowship)) diff --git a/scipost/utils.py b/scipost/utils.py index 07da56a2a7a0f5c5eb0f312a2a4156112bade871..4e8236490f9f1dad43085a23b5d069b389b000f6 100644 --- a/scipost/utils.py +++ b/scipost/utils.py @@ -217,7 +217,7 @@ class Utils(BaseMailUtil): email_text_html += ',<br/>' if len(cls.invitation.personal_message) > 3: email_text += cls.invitation.personal_message + '\n\n' - email_text_html += '\n<i>{{ personal_message|linebreaks }}</i><br/>\n' + email_text_html += '\n{{ personal_message|linebreaks }}<br/>\n' email_context['personal_message'] = cls.invitation.personal_message # This text to be put in C, ci invitations @@ -304,6 +304,14 @@ class Utils(BaseMailUtil): 'https://scipost.org/invitation/' + cls.invitation.invitation_key + '\n\n' 'after which your registration will be activated, allowing you to contribute, ' 'in particular by providing referee reports.\n\n' + 'To ensure timely processing of the submission (out of respect for the authors), ' + 'we would appreciate a quick accept/decline ' + 'response from you, ideally within the next 2 days.\n\n' + 'If you are not able to provide a Report, you can let us know by simply ' + 'navigating to \n\nhttps://scipost.org/submissions/decline_ref_invitation/' + + cls.invitation.invitation_key + '\n\n' + 'If you are able to provide a Report, you can confirm this after registering ' + 'and logging in (you will automatically be prompted for a confirmation).\n\n' 'We very much hope that we can count on your expertise,\n\n' 'Many thanks in advance,\n\nThe SciPost Team') email_text_html += ( @@ -315,6 +323,15 @@ class Utils(BaseMailUtil): '<a href="https://scipost.org/invitation/{{ invitation_key }}">registration form</a> ' 'for you. After activation of your registration, you will be allowed to contribute, ' 'in particular by providing referee reports.</p>' + '<p>To ensure timely processing of the submission (out of respect for the authors), ' + 'we would appreciate a quick accept/decline ' + 'response from you, ideally within the next 2 days.</p>' + '<p>If you are <strong>not</strong> able to provide a Report, ' + 'you can let us know by simply ' + '<a href="https://scipost.org/submissions/decline_ref_invitation/{{ invitation_key }}>' + 'clicking here</a>.</p>' + '<p>If you are able to provide a Report, you can confirm this after registering ' + 'and logging in (you will automatically be prompted for a confirmation).</p>' '<p>We very much hope that we can count on your expertise,</p>' '<p>Many thanks in advance,</p>' '<p>The SciPost Team</p>') diff --git a/submissions/templates/submissions/decline_ref_invitation.html b/submissions/templates/submissions/decline_ref_invitation.html new file mode 100644 index 0000000000000000000000000000000000000000..9efedb177f5058eb3fbe73a51397de573d09391d --- /dev/null +++ b/submissions/templates/submissions/decline_ref_invitation.html @@ -0,0 +1,41 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: decline refereeing invitation{% endblock pagetitle %} + +{% load bootstrap %} + +{% block content %} + +<script> +$(document).ready(function(){ + + $('[name="accept"]').on('change', function() { + if($('[name="accept"]:checked').val() == 'False') { + $('#id_refusal_reason').parents('.form-group').show(); + } + else { + $('#id_refusal_reason').parents('.form-group').hide(); + } + }).trigger('change'); + }); +</script> + + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">SciPost Submission which you have been asked to Referee:</h1> + {% include 'submissions/_submission_summary.html' with submission=invitation.submission %} + </div> +</div> +<div class="row"> + <div class="col-12"> + <h3 class="highlight">You are choosing to decline this Refereeing Invitation</h3> + <form action="{% url 'submissions:decline_ref_invitation' invitation_key=invitation.invitation_key %}" method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input type="submit" class="btn btn-secondary" value="Submit" /> + </form> + </div> +</div> + +{% endblock content %} diff --git a/submissions/templates/submissions/refereeing_overview.html b/submissions/templates/submissions/refereeing_overview.html index bda7f5c3502d94e203179d0e8399c05cc5e93019..a7e09c540820a90a020a229f3f8bad204736186a 100644 --- a/submissions/templates/submissions/refereeing_overview.html +++ b/submissions/templates/submissions/refereeing_overview.html @@ -35,7 +35,7 @@ <h4>Refereeing status summary:</h4> {% include 'submissions/_submission_refereeing_status.html' with submission=submission %} <h3 class="mb-2">Detail of refereeing invitations:</h3> - {% include 'submissions/_submission_refereeing_invitations.html' with submission=submission invitations=ref_invitations %} + {% include 'submissions/_submission_refereeing_invitations.html' with submission=submission invitations=submission.referee_invitations.all %} <a href="{% url 'submissions:communication' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr comtype='StoE' %}">Send a communication to the Editor-in-charge</a> </div> </div> diff --git a/submissions/urls.py b/submissions/urls.py index 48e208052b98fc264fd01f9c991d80325bd5a886..8465d4f9c7b572bed6e6e1437a7f904c1d377132 100644 --- a/submissions/urls.py +++ b/submissions/urls.py @@ -54,6 +54,8 @@ urlpatterns = [ views.accept_or_decline_ref_invitations, name='accept_or_decline_ref_invitations'), url(r'^accept_or_decline_ref_invitation/(?P<invitation_id>[0-9]+)$', views.accept_or_decline_ref_invitation_ack, name='accept_or_decline_ref_invitation_ack'), + url(r'^decline_ref_invitation/(?P<invitation_key>.+)$', + views.decline_ref_invitation, name='decline_ref_invitation'), url(r'^ref_invitation_reminder/(?P<arxiv_identifier_w_vn_nr>[0-9]{4,}.[0-9]{5,}v[0-9]{1,2})/(?P<invitation_id>[0-9]+)$', views.ref_invitation_reminder, name='ref_invitation_reminder'), url(r'^cancel_ref_invitation/(?P<arxiv_identifier_w_vn_nr>[0-9]{4,}.[0-9]{5,}v[0-9]{1,2})/(?P<invitation_id>[0-9]+)$', views.cancel_ref_invitation, name='cancel_ref_invitation'), diff --git a/submissions/utils.py b/submissions/utils.py index fb2039cc6df596f19c5dbce7b37985c794f9ee11..df19145f93f6a1e2db97d190fdb2e5a59d658fd0 100644 --- a/submissions/utils.py +++ b/submissions/utils.py @@ -616,6 +616,7 @@ class SubmissionUtils(BaseMailUtil): emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) + @classmethod def send_refereeing_invitation_email(cls): """ @@ -701,66 +702,162 @@ class SubmissionUtils(BaseMailUtil): emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) + @classmethod + def send_unreg_ref_reminder_email(cls): + """ + This method is used to remind a referee who has not yet responded. + It is used for unregistered referees only. + It is called from the ref_invitation_reminder method in submissions/views.py. + """ + email_text = ( + 'Dear ' + cls.invitation.title + ' ' + + cls.invitation.last_name + ',\n\n' + 'On behalf of the Editor-in-charge ' + + cls.invitation.submission.editor_in_charge.get_title_display() + ' ' + + cls.invitation.submission.editor_in_charge.user.last_name + + ', we would like to cordially remind you of our recent request to referee\n\n' + + cls.invitation.submission.title + ' by ' + + cls.invitation.submission.author_list + '.') + email_text_html = ( + '<p>Dear {{ title }} {{ last_name }},</p>' + '<p>On behalf of the Editor-in-charge {{ EIC_title }} {{ EIC_last_name }}, ' + 'we would like to cordially remind you of our recent request to referee</p>' + '<p>{{ sub_title }}</p>' + '\n<p>by {{ author_list }}.</p>') + email_text += ( + '\n\nWe would also like to renew ' + 'our invitation to become a Contributor on SciPost ' + '(our records show that you are not yet registered); ' + 'your partially pre-filled registration form is still available at\n\n' + 'https://scipost.org/invitation/' + cls.invitation.invitation_key + '\n\n' + 'after which your registration will be activated, giving you full access to ' + 'the portal\'s facilities (in particular allowing you to ' + 'provide referee reports).\n\n' + 'To ensure timely processing of the submission (out of respect for the authors), ' + 'we would appreciate a quick accept/decline response from you, ' + 'ideally within the next 2 days.\n\n' + 'If you are not able to provide a Report, you can quickly let us know by simply ' + 'navigating to \n\nhttps://scipost.org/decline_ref_invitation/' + + cls.invitation.invitation_key + '\n\n' + 'If you are able to provide a Report, you can confirm this after registering ' + 'and logging in (you will automatically be prompted for a confirmation). ' + 'Your report can thereafter be submitted by simply clicking on ' + 'the "Contribute a Report" link at ' + 'https://scipost.org/submission/' + + cls.invitation.submission.arxiv_identifier_w_vn_nr + + ' before the reporting deadline (currently set at ' + + datetime.datetime.strftime(cls.invitation.submission.reporting_deadline, "%Y-%m-%d") + + '; your report will be automatically recognized as an invited report). ' + 'You might want to make sure you are familiar with our refereeing code of conduct ' + 'https://scipost.org/journals/journals_terms_and_conditions and with the ' + 'refereeing procedure https://scipost.org/submissions/sub_and_ref_procedure.' + '\n\nWe very much hope we can count on your expertise,' + '\n\nMany thanks in advance,\n\nThe SciPost Team' + ) + email_text_html += ( + '\n<p>We would also like to renew ' + 'our invitation to become a Contributor on SciPost ' + '(our records show that you are not yet registered); ' + 'your partially pre-filled ' + '<a href="https://scipost.org/invitation/{{ invitation_key }}">' + 'registration form</a> is still available, ' + 'after which your registration will be activated, giving you full access to ' + 'the portal\'s facilities (in particular allowing you to provide referee reports).</p>' + '<p>To ensure timely processing of the submission (out of respect for the authors), ' + 'we would appreciate a quick accept/decline response from you, ' + 'ideally within the next 2 days.</p>' + '<p>If you are <strong>not</strong> able to provide a Report, ' + 'you can quickly let us know by simply ' + '<a href="https://scipost.org/decline_ref_invitation/{{ invitation_key }}>' + 'clicking here</a>.</p>' + '<p>If you are able to provide a Report, you can confirm this after registering ' + 'and logging in (you will automatically be prompted for a confirmation). ' + 'Your report can thereafter be submitted by simply clicking on ' + 'the "Contribute a Report" link at ' + 'the <a href="https://scipost.org/submission/{{ arxiv_identifier_w_vn_nr }}">' + 'Submission\'s page</a> before the reporting deadline (currently set at ' + '{{ deadline }}; your report will be automatically recognized as an invited report).</p>' + '\n<p>You might want to make sure you are familiar with our ' + '<a href="https://scipost.org/journals/journals_terms_and_conditions">' + 'refereeing code of conduct</a> and with the ' + '<a href="https://scipost.org/submissions/sub_and_ref_procedure">' + 'refereeing procedure</a>.</p>' + '<p>We very much hope we can count on your expertise,</p>' + '<p>Many thanks in advance,</p>' + '<p>The SciPost Team</p>') + email_context = Context({ + 'title': cls.invitation.title, + 'last_name': cls.invitation.last_name, + 'EIC_title': cls.invitation.submission.editor_in_charge.get_title_display(), + 'EIC_last_name': cls.invitation.submission.editor_in_charge.user.last_name, + 'sub_title': cls.invitation.submission.title, + 'author_list': cls.invitation.submission.author_list, + 'arxiv_identifier_w_vn_nr': cls.invitation.submission.arxiv_identifier_w_vn_nr, + 'deadline': datetime.datetime.strftime(cls.invitation.submission.reporting_deadline, + "%Y-%m-%d"), + 'invitation_key': cls.invitation.invitation_key, + }) + email_text_html += '<br/>' + EMAIL_FOOTER + html_template = Template(email_text_html) + html_version = html_template.render(email_context) + emailmessage = EmailMultiAlternatives( + 'SciPost: reminder (refereeing request and registration invitation)', email_text, + 'SciPost Submissions <submissions@scipost.org>', + [cls.invitation.email_address], + bcc=[cls.invitation.submission.editor_in_charge.user.email, + 'submissions@scipost.org'], + reply_to=['submissions@scipost.org']) + emailmessage.attach_alternative(html_version, 'text/html') + emailmessage.send(fail_silently=False) + + @classmethod def send_ref_reminder_email(cls): """ - This method is used to remind a referee who is not registered as a Contributor. + This method is used to remind a referee who has not yet responded. + It is used for registered Contributors only. It is called from the ref_invitation_reminder method in submissions/views.py. """ - email_text = ('Dear ' + cls.invitation.get_title_display() + ' ' - + cls.invitation.last_name + ',\n\n' - 'On behalf of the Editor-in-charge ' - + cls.invitation.submission.editor_in_charge.get_title_display() + ' ' - + cls.invitation.submission.editor_in_charge.user.last_name - + ', we would like to cordially remind you of our recent request to referee\n\n' - + cls.invitation.submission.title + ' by ' - + cls.invitation.submission.author_list + '.') + email_text = ( + 'Dear ' + cls.invitation.get_title_display() + ' ' + + cls.invitation.last_name + ',\n\n' + 'On behalf of the Editor-in-charge ' + + cls.invitation.submission.editor_in_charge.get_title_display() + ' ' + + cls.invitation.submission.editor_in_charge.user.last_name + + ', we would like to cordially remind you of our recent request to referee\n\n' + + cls.invitation.submission.title + ' by ' + + cls.invitation.submission.author_list + '.') email_text_html = ( '<p>Dear {{ title }} {{ last_name }},</p>' '<p>On behalf of the Editor-in-charge {{ EIC_title }} {{ EIC_last_name }}, ' 'we would like to cordially remind you of our recent request to referee</p>' '<p>{{ sub_title }}</p>' '\n<p>by {{ author_list }}.</p>') - if cls.invitation.referee is None: - email_text += ('\n\nWe would also like to renew ' - 'our invitation to become a Contributor on SciPost ' - '(our records show that you are not yet registered); ' - 'your partially pre-filled registration form is still available at\n\n' - 'https://scipost.org/invitation/' + cls.invitation.invitation_key + '\n\n' - 'after which your registration will be activated, giving you full access to ' - 'the portal\'s facilities (in particular allowing you to provide referee reports).') - email_text_html += ( - '\n<p>We would also like to renew ' - 'our invitation to become a Contributor on SciPost ' - '(our records show that you are not yet registered); ' - 'your partially pre-filled ' - '<a href="https://scipost.org/invitation/{{ invitation_key }}">' - 'registration form</a> is still available, ' - 'after which your registration will be activated, giving you full access to ' - 'the portal\'s facilities (in particular allowing you to provide referee reports).</p>') if cls.invitation.accepted is None: - email_text += ('\n\nPlease visit ' - 'https://scipost.org/submissions/accept_or_decline_ref_invitations ' - '(login required) as soon as possible (ideally within the next 2 days) ' - 'in order to accept or decline this invitation.') + email_text += ( + '\n\nPlease visit ' + 'https://scipost.org/submissions/accept_or_decline_ref_invitations ' + '(login required) as soon as possible (ideally within the next 2 days) ' + 'in order to accept or decline this invitation.') email_text_html += ( '\n<p>Please ' '<a href="https://scipost.org/submissions/accept_or_decline_ref_invitations">' 'accept or decline the invitation</a> ' '(login required) as soon as possible (ideally within the next 2 days) ' 'in order to ensure rapid processing of the submission.') - email_text += ('\n\nYour report can be submitted by simply clicking on ' - 'the "Contribute a Report" link at ' - 'https://scipost.org/submission/' - + cls.invitation.submission.arxiv_identifier_w_vn_nr - + ' before the reporting deadline (currently set at ' - + datetime.datetime.strftime(cls.invitation.submission.reporting_deadline, "%Y-%m-%d") - + '; your report will be automatically recognized as an invited report). ' - 'You might want to make sure you are familiar with our refereeing code of conduct ' - 'https://scipost.org/journals/journals_terms_and_conditions and with the ' - 'refereeing procedure https://scipost.org/submissions/sub_and_ref_procedure.' - '\n\nWe very much hope we can count on your expertise,' - '\n\nMany thanks in advance,\n\nThe SciPost Team') + email_text += ( + '\n\nYour report can be submitted by simply clicking on ' + 'the "Contribute a Report" link at ' + 'https://scipost.org/submission/' + + cls.invitation.submission.arxiv_identifier_w_vn_nr + + ' before the reporting deadline (currently set at ' + + datetime.datetime.strftime(cls.invitation.submission.reporting_deadline, "%Y-%m-%d") + + '; your report will be automatically recognized as an invited report). ' + 'You might want to make sure you are familiar with our refereeing code of conduct ' + 'https://scipost.org/journals/journals_terms_and_conditions and with the ' + 'refereeing procedure https://scipost.org/submissions/sub_and_ref_procedure.' + '\n\nWe very much hope we can count on your expertise,' + '\n\nMany thanks in advance,\n\nThe SciPost Team') email_text_html += ( '\n<p>Your report can be submitted by simply clicking on ' 'the "Contribute a Report" link at ' @@ -800,6 +897,7 @@ class SubmissionUtils(BaseMailUtil): emailmessage.attach_alternative(html_version, 'text/html') emailmessage.send(fail_silently=False) + @classmethod def send_ref_cancellation_email(cls): """ diff --git a/submissions/views.py b/submissions/views.py index 81375ff8b1198c191c9a0000b1e3d64687bcdc62..387ac0debb92a8bfbbf80734ef0d87d1e22dd4e3 100644 --- a/submissions/views.py +++ b/submissions/views.py @@ -800,7 +800,10 @@ def ref_invitation_reminder(request, arxiv_identifier_w_vn_nr, invitation_id): invitation.date_last_reminded = timezone.now() invitation.save() SubmissionUtils.load({'invitation': invitation}) - SubmissionUtils.send_ref_reminder_email() + if invitation.referee is not None: + SubmissionUtils.send_ref_reminder_email() + else: + SubmissionUtils.send_unreg_ref_reminder_email() return redirect(reverse('submissions:editorial_page', kwargs={'arxiv_identifier_w_vn_nr': arxiv_identifier_w_vn_nr})) @@ -842,6 +845,24 @@ def accept_or_decline_ref_invitation_ack(request, invitation_id): return render(request, 'submissions/accept_or_decline_ref_invitation_ack.html', context) +def decline_ref_invitation(request, invitation_key): + invitation = get_object_or_404(RefereeInvitation, invitation_key=invitation_key) + if request.method == 'POST': + form = ConsiderRefereeInvitationForm(request.POST) + if form.is_valid(): + invitation.accepted = False + invitation.refusal_reason = form.cleaned_data['refusal_reason'] + invitation.save() + SubmissionUtils.load({'invitation': invitation}, request) + SubmissionUtils.email_referee_response_to_EIC() + messages.success(request, 'Thank you for informing us that you will not provide a Report.') + return redirect(reverse('scipost:index')) + else: + form = ConsiderRefereeInvitationForm(initial={'accept': False}) + context = {'invitation': invitation, 'form': form} + return render(request, 'submissions/decline_ref_invitation.html', context) + + @login_required @permission_required_or_403('can_take_editorial_actions', (Submission, 'arxiv_identifier_w_vn_nr', 'arxiv_identifier_w_vn_nr')) diff --git a/templates/email/referee_in_response_to_decision.html b/templates/email/referee_in_response_to_decision.html index 563f0956c294831980e22356293ffa4d6432d5f2..4e3b8502223fa7402894ea9aa1f935c1923eeae8 100644 --- a/templates/email/referee_in_response_to_decision.html +++ b/templates/email/referee_in_response_to_decision.html @@ -5,6 +5,7 @@ We hereby confirm your choice to {% if invitation.accepted %}accept{% else %}dec {{invitation.submission.title}} by {{invitation.submission.author_list}}\n\n {% if invitation.accepted %} +We will look forward to receiving your Report by the reporting deadline {{ invitation.submission.reporting_deadline|date:'Y-m-d' }}.\n\n Many thanks for your collaboration,\n {% else %} Nonetheless, we thank you very much for considering this refereeing invitation,\n diff --git a/templates/email/referee_in_response_to_decision_html.html b/templates/email/referee_in_response_to_decision_html.html index cb418b3609f26d5462ca664a661f3ad19d012a1e..a512ce69174076cdf2bbcebbdb6cce239af332f8 100644 --- a/templates/email/referee_in_response_to_decision_html.html +++ b/templates/email/referee_in_response_to_decision_html.html @@ -9,6 +9,7 @@ <p> {% if invitation.accepted %} + We will look forward to receiving your Report by the reporting deadline {{ invitation.submission.reporting_deadline|date:'Y-m-d' }}. Many thanks for your collaboration, {% else %} Nonetheless, we thank you very much for considering this refereeing invitation, diff --git a/templates/email/referee_response_to_EIC.html b/templates/email/referee_response_to_EIC.html index c3e7b18856e90284714941a74d3a529b134c5c5b..e73f226ccf6c68346f1d879ba008e59d5d828fcf 100644 --- a/templates/email/referee_response_to_EIC.html +++ b/templates/email/referee_response_to_EIC.html @@ -1,6 +1,6 @@ Dear {{invitation.submission.editor_in_charge.get_title_display}} {{invitation.submission.editor_in_charge.user.last_name}},\n\n -Referee {{invitation.referee.get_title_display}} {{invitation.referee.user.last_name}} has {% if invitation.accepted %}accepted{% else %}declined (due to reason: {{invitation.get_refusal_reason_display}}){% endif %} to referee Submission\n\n +Referee {% if invitation.referee %}{{invitation.referee.get_title_display}} {{invitation.referee.user.last_name}}{% else %}{{ invitation.title }} {{ invitation.first_name }} {{ invitation.last_name }}{% endif %} has {% if invitation.accepted %}accepted{% else %}declined (due to reason: {{invitation.get_refusal_reason_display}}){% endif %} to referee Submission\n\n {{invitation.submission.title}} by {{invitation.submission.author_list}}\n\n diff --git a/templates/email/referee_response_to_EIC_html.html b/templates/email/referee_response_to_EIC_html.html index 0cdce8b6dc46e3f57acd13a47a2877e95d43b3cb..529b34c5f923afb7cecefaa688e8c96c4ec31ae2 100644 --- a/templates/email/referee_response_to_EIC_html.html +++ b/templates/email/referee_response_to_EIC_html.html @@ -1,7 +1,7 @@ <p>Dear {{invitation.submission.editor_in_charge.get_title_display}} {{invitation.submission.editor_in_charge.user.last_name}},</p> <p> - Referee {{invitation.referee.get_title_display}} {{invitation.referee.user.last_name}} has {% if invitation.accepted %}accepted{% else %}declined (due to reason: {{invitation.get_refusal_reason_display}}){% endif %} to referee Submission + Referee {% if invitation.referee %}{{invitation.referee.get_title_display}} {{invitation.referee.user.last_name}}{% else %}{{ invitation.title }} {{ invitation.first_name }} {{ invitation.last_name }}{% endif %} has {% if invitation.accepted %}accepted{% else %}declined (due to reason: {{invitation.get_refusal_reason_display}}){% endif %} to referee Submission </p> <p> "{{invitation.submission.title}} by {{invitation.submission.author_list}}". diff --git a/theses/factories.py b/theses/factories.py index bf4a32440a8b5ddbbe1932c0bb2a27feaca7a464..fe9091788560fdc69f34f7b65519f0d0772157b9 100644 --- a/theses/factories.py +++ b/theses/factories.py @@ -5,6 +5,7 @@ from scipost.factories import ContributorFactory from .models import ThesisLink from .forms import VetThesisLinkForm +from .constants import MASTER_THESIS class ThesisLinkFactory(factory.django.DjangoModelFactory): @@ -12,7 +13,7 @@ class ThesisLinkFactory(factory.django.DjangoModelFactory): model = ThesisLink requested_by = factory.SubFactory(ContributorFactory) - type = ThesisLink.MASTER_THESIS + type = MASTER_THESIS title = factory.Faker('bs') pub_link = factory.Faker('uri') author = factory.Faker('name') diff --git a/theses/test_forms.py b/theses/test_forms.py index dd649cedb0fecb2ead03e333ea6e280e3f658df9..e43421af0883d236ceaf90809572d91d06a1e792 100644 --- a/theses/test_forms.py +++ b/theses/test_forms.py @@ -6,12 +6,12 @@ from scipost.factories import ContributorFactory from .factories import ThesisLinkFactory, VetThesisLinkFormFactory from .forms import RequestThesisLinkForm, VetThesisLinkForm from common.helpers import model_form_data +from common.helpers.test import add_groups_and_permissions class TestRequestThesisLink(TestCase): - fixtures = ['permissions', 'groups'] - def setUp(self): + add_groups_and_permissions() self.contributor = ContributorFactory() self.user = self.contributor.user self.request = RequestFactory() diff --git a/theses/test_models.py b/theses/test_models.py index 9cda6172906f7feeb4a8789f014fb4a09f5f7bf9..c878df0efa98a652ee2799dc19b40a82511e354e 100644 --- a/theses/test_models.py +++ b/theses/test_models.py @@ -5,10 +5,12 @@ from django.core.exceptions import ValidationError from .models import ThesisLink from .factories import ThesisLinkFactory +from common.helpers.test import add_groups_and_permissions class ThesisLinkTestCase(TestCase): - fixtures = ['permissions', 'groups'] + def setUp(self): + add_groups_and_permissions() def test_domain_cannot_be_blank(self): thesis_link = ThesisLinkFactory() diff --git a/theses/test_views.py b/theses/test_views.py index 6db7757f05dd44f226adc606d42ca37d227641f5..91e37fc3c6b157d72e479765f8dc7d3c5ba6344e 100644 --- a/theses/test_views.py +++ b/theses/test_views.py @@ -17,10 +17,11 @@ from .factories import ThesisLinkFactory, VettedThesisLinkFactory, VetThesisLink from .models import ThesisLink from .forms import VetThesisLinkForm from common.helpers import model_form_data - +from common.helpers.test import add_groups_and_permissions class TestThesisDetail(TestCase): - fixtures = ['groups', 'permissions'] + def setUp(self): + add_groups_and_permissions() def test_visits_valid_thesis_detail(self): """ A visitor does not have to be logged in to view a thesis link. """ @@ -32,9 +33,8 @@ class TestThesisDetail(TestCase): class TestRequestThesisLink(TestCase): - fixtures = ['groups', 'permissions'] - def setUp(self): + add_groups_and_permissions() self.client = Client() self.target = reverse('theses:request_thesislink') @@ -51,9 +51,8 @@ class TestRequestThesisLink(TestCase): class TestVetThesisLinkRequests(TestCase): - fixtures = ['groups', 'permissions'] - def setUp(self): + add_groups_and_permissions() self.client = Client() self.thesislink = ThesisLinkFactory() self.target = reverse('theses:vet_thesislink', kwargs={'pk': self.thesislink.id}) @@ -158,24 +157,23 @@ class TestVetThesisLinkRequests(TestCase): class TestTheses(TestCase): - fixtures = ['groups', 'permissions'] - def setUp(self): + add_groups_and_permissions() self.client = Client() self.target = reverse('theses:theses') def test_empty_search_query(self): thesislink = VettedThesisLinkFactory() response = self.client.get(self.target) - search_results = response.context["search_results"] - recent_theses = response.context["recent_theses"] - self.assertEqual(search_results, []) - self.assertEqual(recent_theses.exists(), True) + search_results = response.context["object_list"] + self.assertTrue(thesislink in search_results) def test_search_query_on_author(self): thesislink = VettedThesisLinkFactory() + other_thesislink = VettedThesisLinkFactory() form_data = {'author': thesislink.author} response = self.client.get(self.target, form_data) - search_results = response.context['search_results'] + search_results = response.context['object_list'] self.assertTrue(thesislink in search_results) + self.assertTrue(other_thesislink not in search_results) self.assertEqual(len(search_results), 1) diff --git a/theses/views.py b/theses/views.py index d299924c227b8a73722d50867017b1a80711b07c..72856c885224b4f0c2534714e737e208ed9b8fbb 100644 --- a/theses/views.py +++ b/theses/views.py @@ -80,7 +80,6 @@ class VetThesisLink(UpdateView): class ThesisListView(ListView): model = ThesisLink form = ThesisLinkSearchForm - thesis_search_list = [] paginate_by = 10 def get_queryset(self):