diff --git a/api/desecapi/migrations/0022_user_outreach_preference.py b/api/desecapi/migrations/0022_user_outreach_preference.py new file mode 100644 index 000000000..a5390c64f --- /dev/null +++ b/api/desecapi/migrations/0022_user_outreach_preference.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.1 on 2022-01-11 19:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('desecapi', '0021_authenticatednoopuseraction'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='outreach_preference', + field=models.BooleanField(default=True), + ), + ] diff --git a/api/desecapi/models.py b/api/desecapi/models.py index 905766f39..ee0345be3 100644 --- a/api/desecapi/models.py +++ b/api/desecapi/models.py @@ -104,6 +104,7 @@ def _limit_domains_default(): created = models.DateTimeField(auto_now_add=True) limit_domains = models.PositiveIntegerField(default=_limit_domains_default.__func__, null=True, blank=True) needs_captcha = models.BooleanField(default=True) + outreach_preference = models.BooleanField(default=True) objects = MyUserManager() diff --git a/api/desecapi/serializers.py b/api/desecapi/serializers.py index d7587f04c..52882ac34 100644 --- a/api/desecapi/serializers.py +++ b/api/desecapi/serializers.py @@ -639,7 +639,7 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = models.User - fields = ('created', 'email', 'id', 'limit_domains',) + fields = ('created', 'email', 'id', 'limit_domains', 'outreach_preference',) read_only_fields = ('created', 'email', 'id', 'limit_domains',) def validate_password(self, value): @@ -657,7 +657,7 @@ class RegisterAccountSerializer(UserSerializer): class Meta: model = UserSerializer.Meta.model - fields = ('email', 'password', 'domain', 'captcha',) + fields = ('email', 'password', 'domain', 'captcha', 'outreach_preference',) extra_kwargs = { 'password': { 'write_only': True, # Do not expose password field diff --git a/api/desecapi/tests/test_user_management.py b/api/desecapi/tests/test_user_management.py index 857612ea6..a8f333135 100644 --- a/api/desecapi/tests/test_user_management.py +++ b/api/desecapi/tests/test_user_management.py @@ -417,6 +417,7 @@ def _test_registration(self, email=None, password=None, late_captcha=True, **kwa self.assertFalse(User.objects.get(email=email).is_active) self.assertIsNone(User.objects.get(email=email).is_active) self.assertEqual(User.objects.get(email=email).needs_captcha, late_captcha) + self.assertEqual(User.objects.get(email=email).outreach_preference, kwargs.get('outreach_preference', True)) self.assertPassword(email, password) confirmation_link = self.assertRegistrationEmail(email) self.assertConfirmationLinkRedirect(confirmation_link) @@ -539,7 +540,9 @@ def test_authenticated_action_redirect_with_invalid_code(self): self.assertConfirmationLinkRedirect(confirmation_link) def test_registration(self): - self._test_registration(password=self.random_password()) + for outreach_preference in [None, True, False]: + kwargs = dict(outreach_preference=outreach_preference) if outreach_preference is not None else {} + self._test_registration(password=self.random_password(), **kwargs) def test_registration_trim_email(self): user_email = ' {} '.format(self.random_username()) @@ -685,15 +688,14 @@ def _finish_delete_account(self, confirmation_link): def test_view_account(self): response = self.client.view_account(self.token) self.assertEqual(response.status_code, 200) - self.assertEqual(response.data.keys(), {'created', 'email', 'id', 'limit_domains'}) + self.assertEqual(response.data.keys(), {'created', 'email', 'id', 'limit_domains', 'outreach_preference'}) self.assertEqual(response.data['email'], self.email) self.assertEqual(response.data['id'], str(User.objects.get(email=self.email).pk)) self.assertEqual(response.data['limit_domains'], settings.LIMIT_USER_DOMAIN_COUNT_DEFAULT) + self.assertTrue(response.data['outreach_preference']) - def test_view_account_read_only(self): - # Should this test ever be removed (to allow writeable fields), make sure to - # add new tests for each read-only field individually (such as limit_domains)! - for method in [self.client.patch, self.client.put, self.client.post, self.client.delete]: + def test_view_account_forbidden_methods(self): + for method in [self.client.post, self.client.delete]: response = method( reverse('v1:account'), {'limit_domains': 99}, @@ -701,6 +703,31 @@ def test_view_account_read_only(self): ) self.assertResponse(response, status.HTTP_405_METHOD_NOT_ALLOWED) + def test_view_account_update(self): + user = User.objects.get(email=self.email) + immutable_fields = ('created', 'email', 'id', 'limit_domains', 'password',) + immutable_values = [getattr(user, key) for key in immutable_fields] + outreach_preference = user.outreach_preference + + for method in [self.client.patch, self.client.put]: + outreach_preference = not outreach_preference + response = method( + reverse('v1:account'), + { + 'created': '2019-10-16T18:09:17.715702Z', + 'email': 'youremailaddress@example.com', + 'id': '9ab16e5c-805d-4ab1-9030-af3f5a541d47', + 'limit_domains': 42, + 'password': self.random_password(), + 'outreach_preference': outreach_preference, + }, + HTTP_AUTHORIZATION='Token {}'.format(self.token) + ) + self.assertResponse(response, status.HTTP_200_OK) + user = User.objects.get(email=self.email) + self.assertEqual(outreach_preference, user.outreach_preference) # `outreach_preference` updated + self.assertEqual(immutable_values, [getattr(user, k) for k in immutable_fields]) # read-only fields ignored + def test_reset_password(self): self._test_reset_password(self.email) diff --git a/api/desecapi/views.py b/api/desecapi/views.py index 2e401b207..441834e8b 100644 --- a/api/desecapi/views.py +++ b/api/desecapi/views.py @@ -522,7 +522,7 @@ def create(self, request, *args, **kwargs): return Response(data={'detail': message}, status=status.HTTP_202_ACCEPTED) -class AccountView(generics.RetrieveAPIView): +class AccountView(generics.RetrieveUpdateAPIView): permission_classes = (IsAuthenticated, permissions.TokenNoDomainPolicy,) serializer_class = serializers.UserSerializer throttle_scope = 'account_management_passive'