فهرست منبع

feat(api): add User.outreach_preference

Peter Thomassen 3 سال پیش
والد
کامیت
8bae2c5a04

+ 18 - 0
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),
+        ),
+    ]

+ 1 - 0
api/desecapi/models.py

@@ -103,6 +103,7 @@ class User(ExportModelOperationsMixin('User'), AbstractBaseUser):
     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()
 

+ 2 - 2
api/desecapi/serializers.py

@@ -721,7 +721,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):
@@ -739,7 +739,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

+ 33 - 6
api/desecapi/tests/test_user_management.py

@@ -417,6 +417,7 @@ class UserManagementTestCase(DesecTestCase, PublicSuffixMockMixin):
         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 @@ class NoUserAccountTestCase(UserLifeCycleTestCase):
         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 @@ class HasUserAccountTestCase(UserManagementTestCase):
     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 @@ class HasUserAccountTestCase(UserManagementTestCase):
             )
             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)
 

+ 1 - 1
api/desecapi/views.py

@@ -521,7 +521,7 @@ class AccountCreateView(generics.CreateAPIView):
         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'