diff --git a/adhocracy4/comments_async/api.py b/adhocracy4/comments_async/api.py index 788919db1..74d581b01 100644 --- a/adhocracy4/comments_async/api.py +++ b/adhocracy4/comments_async/api.py @@ -1,7 +1,10 @@ from django.apps import apps from django.conf import settings from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import PermissionDenied from django.urls import reverse +from django.urls.exceptions import NoReverseMatch +from django.utils.translation import gettext_lazy as _ from django_filters import rest_framework as filters from rest_framework import mixins from rest_framework import status @@ -113,7 +116,7 @@ def get_serializer_class(self): def _save_terms_agreement(self): 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: if 'agreed_terms_of_use' in self.request.data and \ self.request.data['agreed_terms_of_use']: organisation_model = apps.get_model( @@ -130,6 +133,10 @@ def _save_terms_agreement(self): self.request.data['agreed_terms_of_use'] } ) + else: + raise PermissionDenied( + _("Please agree to the organisation's terms of use.") + ) def perform_create(self, serializer): self._save_terms_agreement() @@ -192,11 +199,14 @@ def list(self, request, *args, **kwargs): user_has_agreed = None use_org_terms_of_use = True organisation = self.content_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: diff --git a/tests/apps/organisations/factories.py b/tests/apps/organisations/factories.py index e1df8dc2d..347c55da6 100644 --- a/tests/apps/organisations/factories.py +++ b/tests/apps/organisations/factories.py @@ -4,6 +4,7 @@ from adhocracy4.test.factories import UserFactory from tests.apps.organisations.models import Member from tests.apps.organisations.models import Organisation +from tests.apps.organisations.models import OrganisationTermsOfUse class OrganisationFactory(factory.django.DjangoModelFactory): @@ -33,3 +34,11 @@ class Meta: member = factory.SubFactory(USER_FACTORY) organisation = factory.SubFactory(ORGANISATION_FACTORY) + + +class OrganisationTermsOfUseFactory(factory.django.DjangoModelFactory): + class Meta: + model = OrganisationTermsOfUse + + user = factory.SubFactory(UserFactory) + organisation = factory.SubFactory(OrganisationFactory) diff --git a/tests/apps/organisations/migrations/0003_organisationtermsofuse.py b/tests/apps/organisations/migrations/0003_organisationtermsofuse.py new file mode 100644 index 000000000..22fdb38ab --- /dev/null +++ b/tests/apps/organisations/migrations/0003_organisationtermsofuse.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.13 on 2022-06-21 14:14 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('a4test_organisations', '0002_member'), + ] + + operations = [ + migrations.CreateModel( + name='OrganisationTermsOfUse', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('has_agreed', models.BooleanField(default=False)), + ('organisation', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.A4_ORGANISATIONS_MODEL)), + ('user', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('user', 'organisation')}, + }, + ), + ] diff --git a/tests/apps/organisations/models.py b/tests/apps/organisations/models.py index d56d713f1..90d4312f6 100644 --- a/tests/apps/organisations/models.py +++ b/tests/apps/organisations/models.py @@ -15,7 +15,7 @@ class Organisation(models.Model): ) groups = models.ManyToManyField( Group, - blank=True + blank=True, ) def __str__(self): @@ -38,10 +38,33 @@ def get_absolute_url(self): class Member(models.Model): - member = models.ForeignKey(User, - on_delete=models.CASCADE) - organisation = models.ForeignKey(settings.A4_ORGANISATIONS_MODEL, - on_delete=models.CASCADE) + member = models.ForeignKey( + User, + on_delete=models.CASCADE, + ) + organisation = models.ForeignKey( + settings.A4_ORGANISATIONS_MODEL, + on_delete=models.CASCADE, + ) class Meta: unique_together = [('member', 'organisation')] + + +class OrganisationTermsOfUse(models.Model): + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + editable=False, + ) + organisation = models.ForeignKey( + settings.A4_ORGANISATIONS_MODEL, + on_delete=models.CASCADE, + editable=False, + ) + has_agreed = models.BooleanField( + default=False, + ) + + class Meta: + unique_together = [('user', 'organisation')] diff --git a/tests/comments/test_async_api.py b/tests/comments/test_async_api.py index eede69cf3..8b04c7fda 100644 --- a/tests/comments/test_async_api.py +++ b/tests/comments/test_async_api.py @@ -361,6 +361,8 @@ def test_fields(user, apiclient, question_ct, question): assert 'would_have_commenting_permission' in response.data assert 'project_is_public' in response.data assert response.data['count'] == 1 + assert 'use_org_terms_of_use' in response.data + assert not response.data['use_org_terms_of_use'] commentDict = response.data['results'][0] assert len(commentDict) == 21 diff --git a/tests/comments/test_async_api_org_terms.py b/tests/comments/test_async_api_org_terms.py new file mode 100644 index 000000000..64f7e0fd9 --- /dev/null +++ b/tests/comments/test_async_api_org_terms.py @@ -0,0 +1,251 @@ +import pytest +from django.contrib.contenttypes.models import ContentType +from django.test import override_settings +from django.urls import reverse +from rest_framework import status + +from tests.apps.organisations.models import OrganisationTermsOfUse +from tests.apps.questions.models import Question +from tests.apps.questions.phases import AskPhase +from tests.helpers import active_phase + + +@pytest.mark.django_db +@pytest.fixture +def question_ct(): + return ContentType.objects.get_for_model(Question) + + +@pytest.mark.django_db +@override_settings( + A4_USE_ORGANISATION_TERMS_OF_USE=True +) +def test_agreement_save_with_create(user, apiclient, question_ct, question): + apiclient.force_authenticate(user=user) + url = reverse( + 'comments_async-list', + kwargs={'content_type': question_ct.pk, + 'object_pk': question.pk}) + data = { + 'comment': 'comment comment', + 'agreed_terms_of_use': True + } + with active_phase(question.module, AskPhase): + apiclient.post(url, data, format='json') + + 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 +) +def test_agreement_save_with_update(comment, apiclient): + apiclient.force_authenticate(user=comment.creator) + url = reverse( + 'comments_async-detail', + kwargs={ + 'pk': comment.pk, + 'content_type': comment.content_type.pk, + 'object_pk': comment.object_pk + }) + data = { + 'comment': 'comment comment comment', + 'agreed_terms_of_use': True + } + + with active_phase(comment.content_object.module, AskPhase): + response = apiclient.patch(url, data, format='json') + assert response.status_code == status.HTTP_200_OK + assert response.data['comment'] == 'comment comment comment' + + terms = OrganisationTermsOfUse.objects.all() + assert len(terms) == 1 + assert terms[0].user == comment.creator + + +@pytest.mark.django_db +@override_settings( + A4_USE_ORGANISATION_TERMS_OF_USE=True +) +def test_agreement_update_with_create( + user, apiclient, question_ct, question, + organisation_terms_of_use_factory): + organisation_terms_of_use = organisation_terms_of_use_factory( + user=question.creator, + organisation=question.module.project.organisation, + has_agreed=False, + ) + assert not organisation_terms_of_use.has_agreed + + apiclient.force_authenticate(user=user) + url = reverse( + 'comments_async-list', + kwargs={'content_type': question_ct.pk, + 'object_pk': question.pk}) + data = { + 'comment': 'comment comment', + 'agreed_terms_of_use': True + } + with active_phase(question.module, AskPhase): + apiclient.post(url, data, format='json') + + 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 +) +def test_agreement_update_with_update( + comment, apiclient, organisation_terms_of_use_factory): + organisation_terms_of_use = organisation_terms_of_use_factory( + user=comment.creator, + organisation=comment.module.project.organisation, + has_agreed=False, + ) + assert not organisation_terms_of_use.has_agreed + apiclient.force_authenticate(user=comment.creator) + url = reverse( + 'comments_async-detail', + kwargs={ + 'pk': comment.pk, + 'content_type': comment.content_type.pk, + 'object_pk': comment.object_pk + }) + data = { + 'comment': 'comment comment comment', + 'agreed_terms_of_use': True + } + + with active_phase(comment.content_object.module, AskPhase): + response = apiclient.patch(url, data, format='json') + assert response.status_code == status.HTTP_200_OK + assert response.data['comment'] == 'comment comment comment' + + 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 +) +def test_admin_agreement_save_with_update(comment, apiclient, admin): + apiclient.force_authenticate(user=admin) + url = reverse( + 'comments_async-detail', + kwargs={ + 'pk': comment.pk, + 'content_type': comment.content_type.pk, + 'object_pk': comment.object_pk + }) + data = { + 'comment': 'comment comment comment', + 'agreed_terms_of_use': True + } + + with active_phase(comment.content_object.module, AskPhase): + response = apiclient.patch(url, data, format='json') + assert response.status_code == status.HTTP_200_OK + assert response.data['comment'] == 'comment comment comment' + + terms = OrganisationTermsOfUse.objects.all() + assert len(terms) == 1 + assert terms[0].user == admin + + +@pytest.mark.django_db +def test_agreement_save_no_settings_no_effect( + comment, apiclient): + apiclient.force_authenticate(user=comment.creator) + url = reverse( + 'comments_async-detail', + kwargs={ + 'pk': comment.pk, + 'content_type': comment.content_type.pk, + 'object_pk': comment.object_pk + }) + data = { + 'comment': 'comment comment comment', + 'agreed_terms_of_use': True + } + + with active_phase(comment.content_object.module, AskPhase): + response = apiclient.patch(url, data, format='json') + assert response.status_code == status.HTTP_200_OK + assert response.data['comment'] == 'comment comment comment' + + terms = OrganisationTermsOfUse.objects.all() + assert len(terms) == 0 + + +@pytest.mark.django_db +@override_settings( + A4_USE_ORGANISATION_TERMS_OF_USE=True +) +def test_comment_save_without_agreement_forbidden( + user, apiclient, question_ct, question): + apiclient.force_authenticate(user=user) + url = reverse( + 'comments_async-list', + kwargs={'content_type': question_ct.pk, + 'object_pk': question.pk}) + data = { + 'comment': 'comment comment', + 'agreed_terms_of_use': False + } + with active_phase(question.module, AskPhase): + response = apiclient.post(url, data, format='json') + + assert response.status_code == status.HTTP_403_FORBIDDEN + terms = OrganisationTermsOfUse.objects.all() + assert len(terms) == 0 + + +@pytest.mark.django_db +@override_settings( + A4_USE_ORGANISATION_TERMS_OF_USE=True +) +def test_comment_save_without_agreement_data_forbidden( + user, apiclient, question_ct, question): + apiclient.force_authenticate(user=user) + url = reverse( + 'comments_async-list', + kwargs={'content_type': question_ct.pk, + 'object_pk': question.pk}) + data = { + 'comment': 'comment comment' + } + with active_phase(question.module, AskPhase): + response = apiclient.post(url, data, format='json') + + assert response.status_code == status.HTTP_403_FORBIDDEN + terms = OrganisationTermsOfUse.objects.all() + assert len(terms) == 0 + + +@pytest.mark.django_db +@override_settings( + A4_USE_ORGANISATION_TERMS_OF_USE=True +) +def test_agreement_checkbox_fields_not_implemented( + user, apiclient, question_ct, question): + apiclient.force_authenticate(user=user) + url = reverse( + 'comments_async-list', + kwargs={'content_type': question_ct.pk, + 'object_pk': question.pk}) + data = {'comment': 'comment comment'} + with active_phase(question.module, AskPhase): + apiclient.post(url, data, format='json') + + url = reverse( + 'comments_async-list', + kwargs={'content_type': question_ct.pk, + 'object_pk': question.pk}) + with pytest.raises(NotImplementedError): + apiclient.get(url) diff --git a/tests/conftest.py b/tests/conftest.py index 496b51daf..b7ec8dfcf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ from tests.apps.locations import factories as location_factories from tests.apps.organisations.factories import MemberFactory from tests.apps.organisations.factories import OrganisationFactory +from tests.apps.organisations.factories import OrganisationTermsOfUseFactory from tests.apps.questions import factories as q_factories from tests.images import factories as img_factories @@ -29,6 +30,7 @@ def image_factory(): register(OrganisationFactory) register(factories.UserFactory) register(MemberFactory) +register(OrganisationTermsOfUseFactory) register(factories.GroupFactory) register(factories.AdminFactory, 'admin') register(factories.UserFactory, 'another_user')