From ece164c82b173f1d685fad8fe8a60549e41467d0 Mon Sep 17 00:00:00 2001 From: fuzzylogic2000 Date: Mon, 27 Jun 2022 17:32:43 +0200 Subject: [PATCH] apps/polls: make sure poll can only be saved with agreement and add tests --- adhocracy4/polls/api.py | 57 ++- tests/polls/test_vote_api.py | 10 +- tests/polls/test_vote_api_org_terms.py | 542 +++++++++++++++++++++++++ 3 files changed, 588 insertions(+), 21 deletions(-) create mode 100644 tests/polls/test_vote_api_org_terms.py diff --git a/adhocracy4/polls/api.py b/adhocracy4/polls/api.py index a5644b045..4c4cd8fa9 100644 --- a/adhocracy4/polls/api.py +++ b/adhocracy4/polls/api.py @@ -1,9 +1,11 @@ from django.apps import apps from django.conf import settings +from django.core.exceptions import PermissionDenied from django.db import transaction from django.http import Http404 from django.shortcuts import get_object_or_404 from django.urls import reverse +from django.urls.exceptions import NoReverseMatch from django.utils.translation import gettext as _ from rest_framework import mixins from rest_framework import status @@ -42,23 +44,46 @@ def rules_method_map(self): POST='a4polls.add_vote', ) + def _get_org_terms_model(self): + """Make sure, only used with A4_USE_ORGANISATION_TERMS_OF_USE.""" + organisation_model = apps.get_model( + settings.A4_ORGANISATIONS_MODEL) + OrganisationTermsOfUse = apps.get_model( + organisation_model._meta.app_label, + 'OrganisationTermsOfUse' + ) + return OrganisationTermsOfUse + + def _user_has_agreed(self, user): + OrganisationTermsOfUse = self._get_org_terms_model() + organisation = self.get_object().project.organisation + user_has_agreed = \ + OrganisationTermsOfUse.objects.filter( + user=user, + organisation=organisation, + has_agreed=True + ).exists() + return user_has_agreed + def add_terms_of_use_info(self, request, data): use_org_terms_of_use = False if hasattr(settings, 'A4_USE_ORGANISATION_TERMS_OF_USE') \ - and settings.A4_USE_ORGANISATION_TERMS_OF_USE: + and settings.A4_USE_ORGANISATION_TERMS_OF_USE: user_has_agreed = None use_org_terms_of_use = True organisation = self.get_object().project.organisation - org_terms_url = reverse( - 'organisation-terms-of-use', kwargs={ - 'organisation_slug': organisation.slug - } - ) + try: + org_terms_url = reverse( + 'organisation-terms-of-use', kwargs={ + 'organisation_slug': organisation.slug + } + ) + except NoReverseMatch: + raise NotImplementedError('Add org terms of use view.') if hasattr(request, 'user'): user = request.user if user.is_authenticated: - user_has_agreed = \ - user.has_agreed_on_org_terms(organisation) + user_has_agreed = self._user_has_agreed(user) data['user_has_agreed'] = user_has_agreed data['org_terms_url'] = org_terms_url data['use_org_terms_of_use'] = use_org_terms_of_use @@ -76,15 +101,11 @@ def retrieve(self, request, *args, **kwargs): permission_classes=[ViewSetRulesPermission]) def vote(self, request, pk): if hasattr(settings, 'A4_USE_ORGANISATION_TERMS_OF_USE') \ - and settings.A4_USE_ORGANISATION_TERMS_OF_USE: + and settings.A4_USE_ORGANISATION_TERMS_OF_USE \ + and not self._user_has_agreed(self.request.user): if 'agreed_terms_of_use' in self.request.data and \ - self.request.data['agreed_terms_of_use']: - organisation_model = apps.get_model( - settings.A4_ORGANISATIONS_MODEL) - OrganisationTermsOfUse = apps.get_model( - organisation_model._meta.app_label, - 'OrganisationTermsOfUse' - ) + self.request.data['agreed_terms_of_use']: + OrganisationTermsOfUse = self._get_org_terms_model() OrganisationTermsOfUse.objects.update_or_create( user=self.request.user, organisation=self.get_object().project.organisation, @@ -93,6 +114,10 @@ def vote(self, request, pk): self.request.data['agreed_terms_of_use'] } ) + else: + raise PermissionDenied( + _("Please agree to the organisation's terms of use.") + ) for question_id in request.data['votes']: question = self.get_question(question_id) diff --git a/tests/polls/test_vote_api.py b/tests/polls/test_vote_api.py index 7d6f0f2dc..b5c146bbf 100644 --- a/tests/polls/test_vote_api.py +++ b/tests/polls/test_vote_api.py @@ -575,11 +575,11 @@ def test_validate_choices(apiclient, user, question_factory, choice_factory): @pytest.mark.django_db -def test_validate_question_belongs_to_poo(apiclient, - user, - poll_factory, - question_factory, - choice_factory): +def test_validate_question_belongs_to_poll(apiclient, + user, + poll_factory, + question_factory, + choice_factory): poll_1 = poll_factory() poll_2 = poll_factory() diff --git a/tests/polls/test_vote_api_org_terms.py b/tests/polls/test_vote_api_org_terms.py new file mode 100644 index 000000000..111acf1f2 --- /dev/null +++ b/tests/polls/test_vote_api_org_terms.py @@ -0,0 +1,542 @@ +from unittest.mock import patch + +import pytest +from django.test import override_settings +from django.urls import reverse +from rest_framework import status + +from adhocracy4.polls.models import Answer +from adhocracy4.polls.models import OtherVote +from adhocracy4.polls.models import Vote +from adhocracy4.polls.phases import VotingPhase +from tests.apps.organisations.models import OrganisationTermsOfUse +from tests.helpers import active_phase + + +@pytest.mark.django_db +@override_settings( + A4_USE_ORGANISATION_TERMS_OF_USE=True +) +@patch('adhocracy4.polls.api.reverse', return_value='/') +def test_admin_can_vote_with_agreement( + mock_provider, admin, apiclient, poll_factory, + question_factory, choice_factory): + poll = poll_factory() + question = question_factory(poll=poll) + choice1 = choice_factory(question=question) + choice_factory(question=question) + + assert Vote.objects.count() == 0 + + apiclient.force_authenticate(user=admin) + + url = reverse('polls-vote', kwargs={'pk': poll.pk}) + + data = { + 'votes': { + question.pk: { + 'choices': [choice1.pk], + 'other_choice_answer': '', + 'open_answer': '' + } + }, + 'agreed_terms_of_use': True + } + + response = apiclient.post(url, data, format='json') + assert response.status_code == status.HTTP_201_CREATED + assert Vote.objects.count() == 1 + terms = OrganisationTermsOfUse.objects.all() + assert len(terms) == 1 + assert terms[0].user == admin + + +@pytest.mark.django_db +@override_settings( + A4_USE_ORGANISATION_TERMS_OF_USE=True +) +@patch('adhocracy4.polls.api.reverse', return_value='/') +def test_user_can_vote_with_agreement( + mock_provider, user, apiclient, poll_factory, + question_factory, choice_factory): + + poll = poll_factory() + question = question_factory(poll=poll) + choice1 = choice_factory(question=question) + choice_factory(question=question) + open_question = question_factory(poll=poll, is_open=True) + + assert Vote.objects.count() == 0 + + apiclient.force_authenticate(user=user) + + url = reverse('polls-vote', kwargs={'pk': poll.pk}) + + data = { + 'votes': { + question.pk: { + 'choices': [choice1.pk], + 'other_choice_answer': '', + 'open_answer': '' + }, + open_question.pk: { + 'choices': [], + 'other_choice_answer': '', + 'open_answer': 'an open answer' + } + }, + 'agreed_terms_of_use': True + } + + with active_phase(poll.module, VotingPhase): + response = apiclient.post(url, data, format='json') + assert response.status_code == status.HTTP_201_CREATED + + assert Vote.objects.count() == 1 + assert Answer.objects.count() == 1 + terms = OrganisationTermsOfUse.objects.all() + assert len(terms) == 1 + assert terms[0].user == user + + +@pytest.mark.django_db +@override_settings( + A4_USE_ORGANISATION_TERMS_OF_USE=True +) +@patch('adhocracy4.polls.api.reverse', return_value='/') +def test_agreement_update_with_vote( + mock_provider, user, apiclient, poll_factory, + question_factory, choice_factory, + organisation_terms_of_use_factory): + + poll = poll_factory() + question = question_factory(poll=poll) + choice1 = choice_factory(question=question) + choice_factory(question=question) + + organisation_terms_of_use = organisation_terms_of_use_factory( + user=user, + organisation=poll.module.project.organisation, + has_agreed=False, + ) + assert not organisation_terms_of_use.has_agreed + assert Vote.objects.count() == 0 + + apiclient.force_authenticate(user=user) + + url = reverse('polls-vote', kwargs={'pk': poll.pk}) + + data = { + 'votes': { + question.pk: { + 'choices': [choice1.pk], + 'other_choice_answer': '', + 'open_answer': '' + }, + }, + 'agreed_terms_of_use': True + } + + with active_phase(poll.module, VotingPhase): + response = apiclient.post(url, data, format='json') + assert response.status_code == status.HTTP_201_CREATED + + assert Vote.objects.count() == 1 + + organisation_terms_of_use.refresh_from_db() + assert organisation_terms_of_use.has_agreed + + +@pytest.mark.django_db +@override_settings( + A4_USE_ORGANISATION_TERMS_OF_USE=True +) +@patch('adhocracy4.polls.api.reverse', return_value='/') +def test_user_cannot_vote_without_agreement( + mock_provider, user, apiclient, poll_factory, + question_factory, choice_factory): + + poll = poll_factory() + question = question_factory(poll=poll) + choice1 = choice_factory(question=question) + choice_factory(question=question) + + apiclient.force_authenticate(user=user) + + url = reverse('polls-vote', kwargs={'pk': poll.pk}) + + data = { + 'votes': { + question.pk: { + 'choices': [choice1.pk], + 'other_choice_answer': '', + 'open_answer': '' + }, + }, + 'agreed_terms_of_use': False + } + + with active_phase(poll.module, VotingPhase): + response = apiclient.post(url, data, format='json') + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +@override_settings( + A4_USE_ORGANISATION_TERMS_OF_USE=True +) +@patch('adhocracy4.polls.api.reverse', return_value='/') +def test_user_cannot_vote_without_agreement_data( + mock_provider, user, apiclient, poll_factory, + question_factory, choice_factory): + + poll = poll_factory() + question = question_factory(poll=poll) + choice1 = choice_factory(question=question) + choice_factory(question=question) + + apiclient.force_authenticate(user=user) + + url = reverse('polls-vote', kwargs={'pk': poll.pk}) + + data = { + 'votes': { + question.pk: { + 'choices': [choice1.pk], + 'other_choice_answer': '', + 'open_answer': '' + }, + } + } + + with active_phase(poll.module, VotingPhase): + response = apiclient.post(url, data, format='json') + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +@override_settings( + A4_USE_ORGANISATION_TERMS_OF_USE=True +) +@patch('adhocracy4.polls.api.reverse', return_value='/') +def test_user_can_vote_with_given_agreement( + mock_provider, user, apiclient, poll_factory, + question_factory, choice_factory, + organisation_terms_of_use_factory): + + poll = poll_factory() + question = question_factory(poll=poll) + choice1 = choice_factory(question=question) + choice_factory(question=question) + open_question = question_factory(poll=poll, is_open=True) + organisation_terms_of_use_factory( + user=user, + organisation=poll.project.organisation, + has_agreed=True, + ) + + assert Vote.objects.count() == 0 + + apiclient.force_authenticate(user=user) + + url = reverse('polls-vote', kwargs={'pk': poll.pk}) + + data = { + 'votes': { + question.pk: { + 'choices': [choice1.pk], + 'other_choice_answer': '', + 'open_answer': '' + }, + open_question.pk: { + 'choices': [], + 'other_choice_answer': '', + 'open_answer': 'an open answer' + } + }, + } + + with active_phase(poll.module, VotingPhase): + response = apiclient.post(url, data, format='json') + assert response.status_code == status.HTTP_201_CREATED + + assert Vote.objects.count() == 1 + assert Answer.objects.count() == 1 + + +@pytest.mark.django_db +@override_settings( + A4_USE_ORGANISATION_TERMS_OF_USE=True +) +@patch('adhocracy4.polls.api.reverse', return_value='/') +def test_other_choice_vote_updated_with_agreement( + mock_provider, user, question, apiclient, + choice_factory): + + choice_factory(question=question) + choice_other = choice_factory(question=question, is_other_choice=True) + + assert Vote.objects.count() == 0 + assert OtherVote.objects.count() == 0 + + apiclient.force_authenticate(user=user) + + url = reverse('polls-vote', kwargs={'pk': question.poll.pk}) + + data = { + 'votes': { + question.pk: { + 'choices': [choice_other.pk], + 'other_choice_answer': 'other choice answer', + 'open_answer': '' + } + }, + 'agreed_terms_of_use': True + } + + with active_phase(question.poll.module, VotingPhase): + response = apiclient.post(url, data, format='json') + assert response.status_code == status.HTTP_201_CREATED + + assert Vote.objects.count() == 1 + assert OtherVote.objects.count() == 1 + assert OtherVote.objects.first().vote == Vote.objects.first() + assert OtherVote.objects.first().answer == 'other choice answer' + + terms = OrganisationTermsOfUse.objects.all() + assert len(terms) == 1 + assert terms[0].user == user + assert terms[0].has_agreed + terms[0].has_agreed = False + terms[0].save() + + data = { + 'votes': { + question.pk: { + 'choices': [choice_other.pk], + 'other_choice_answer': 'other choice answer updated', + 'open_answer': '' + } + }, + 'agreed_terms_of_use': True + } + + with active_phase(question.poll.module, VotingPhase): + response = apiclient.post(url, data, format='json') + assert response.status_code == status.HTTP_201_CREATED + + assert Vote.objects.count() == 1 + assert OtherVote.objects.count() == 1 + assert OtherVote.objects.first().vote == Vote.objects.first() + assert OtherVote.objects.first().answer == 'other choice answer updated' + terms = OrganisationTermsOfUse.objects.all() + assert len(terms) == 1 + assert terms[0].user == user + assert terms[0].has_agreed + + +@pytest.mark.django_db +@override_settings( + A4_USE_ORGANISATION_TERMS_OF_USE=True +) +@patch('adhocracy4.polls.api.reverse', return_value='/') +def test_answer_created( + mock_provider, user, open_question, apiclient): + + assert Answer.objects.count() == 0 + + apiclient.force_authenticate(user=user) + + url = reverse('polls-vote', kwargs={'pk': open_question.poll.pk}) + + data = { + 'votes': { + open_question.pk: { + 'choices': [], + 'other_choice_answer': '', + 'open_answer': 'answer to open question' + } + }, + 'agreed_terms_of_use': True + } + + with active_phase(open_question.poll.module, VotingPhase): + response = apiclient.post(url, data, format='json') + assert response.status_code == status.HTTP_201_CREATED + + assert Answer.objects.count() == 1 + assert Answer.objects.first().answer == 'answer to open question' + terms = OrganisationTermsOfUse.objects.all() + assert len(terms) == 1 + assert terms[0].user == user + assert terms[0].has_agreed + + +@pytest.mark.django_db +@override_settings( + A4_USE_ORGANISATION_TERMS_OF_USE=True +) +@patch('adhocracy4.polls.api.reverse', return_value='/') +def test_answer_updated( + mock_provider, user, open_question, apiclient): + + assert Answer.objects.count() == 0 + + apiclient.force_authenticate(user=user) + + url = reverse('polls-vote', kwargs={'pk': open_question.poll.pk}) + + data = { + 'votes': { + open_question.pk: { + 'choices': [], + 'other_choice_answer': '', + 'open_answer': 'answer to open question' + } + }, + 'agreed_terms_of_use': True + } + + with active_phase(open_question.poll.module, VotingPhase): + response = apiclient.post(url, data, format='json') + assert response.status_code == status.HTTP_201_CREATED + + assert Answer.objects.count() == 1 + assert Answer.objects.first().answer == 'answer to open question' + + data = { + 'votes': { + open_question.pk: { + 'choices': [], + 'other_choice_answer': '', + 'open_answer': 'answer to open question updated' + } + }, + 'agreed_terms_of_use': True + } + + with active_phase(open_question.poll.module, VotingPhase): + response = apiclient.post(url, data, format='json') + assert response.status_code == status.HTTP_201_CREATED + + assert Answer.objects.count() == 1 + assert Answer.objects.first().answer == 'answer to open question updated' + terms = OrganisationTermsOfUse.objects.all() + assert len(terms) == 1 + assert terms[0].user == user + assert terms[0].has_agreed + + +@pytest.mark.django_db +@patch('adhocracy4.polls.api.reverse', return_value='/') +def test_agreement_save_no_settings_no_effect( + mock_provider, user, apiclient, poll_factory, + question_factory, choice_factory): + + poll = poll_factory() + question = question_factory(poll=poll) + choice1 = choice_factory(question=question) + choice_factory(question=question) + open_question = question_factory(poll=poll, is_open=True) + + assert Vote.objects.count() == 0 + + apiclient.force_authenticate(user=user) + + url = reverse('polls-vote', kwargs={'pk': poll.pk}) + + data = { + 'votes': { + question.pk: { + 'choices': [choice1.pk], + 'other_choice_answer': '', + 'open_answer': '' + }, + open_question.pk: { + 'choices': [], + 'other_choice_answer': '', + 'open_answer': 'an open answer' + } + }, + 'agreed_terms_of_use': True + } + + with active_phase(poll.module, VotingPhase): + response = apiclient.post(url, data, format='json') + assert response.status_code == status.HTTP_201_CREATED + + assert Vote.objects.count() == 1 + assert Answer.objects.count() == 1 + terms = OrganisationTermsOfUse.objects.all() + assert len(terms) == 0 + + +@pytest.mark.django_db +@override_settings( + A4_USE_ORGANISATION_TERMS_OF_USE=True +) +def test_agreement_fields_not_implemented( + admin, apiclient, poll_factory, + question_factory, choice_factory): + poll = poll_factory() + question = question_factory(poll=poll) + choice1 = choice_factory(question=question) + choice_factory(question=question) + + assert Vote.objects.count() == 0 + + apiclient.force_authenticate(user=admin) + + url = reverse('polls-vote', kwargs={'pk': poll.pk}) + + data = { + 'votes': { + question.pk: { + 'choices': [choice1.pk], + 'other_choice_answer': '', + 'open_answer': '' + } + }, + 'agreed_terms_of_use': True + } + + with pytest.raises(NotImplementedError): + apiclient.post(url, data, format='json') + + +@pytest.mark.django_db +@override_settings( + A4_USE_ORGANISATION_TERMS_OF_USE=True +) +@patch('adhocracy4.polls.api.reverse', return_value='/') +def test_agreement_info( + mock_provider, admin, apiclient, poll_factory, + question_factory, choice_factory): + poll = poll_factory() + question = question_factory(poll=poll) + choice1 = choice_factory(question=question) + choice_factory(question=question) + + assert Vote.objects.count() == 0 + + apiclient.force_authenticate(user=admin) + + url = reverse('polls-vote', kwargs={'pk': poll.pk}) + + data = { + 'votes': { + question.pk: { + 'choices': [choice1.pk], + 'other_choice_answer': '', + 'open_answer': '' + } + }, + 'agreed_terms_of_use': True + } + + response = apiclient.post(url, data, format='json') + assert response.status_code == status.HTTP_201_CREATED + assert Vote.objects.count() == 1 + terms = OrganisationTermsOfUse.objects.all() + assert len(terms) == 1 + assert terms[0].user == admin