Implement parental controls for the User API
TNL-1739
This commit is contained in:
@@ -19,7 +19,7 @@ from ..helpers import intercept_errors
|
||||
from ..models import UserPreference
|
||||
|
||||
from . import (
|
||||
ACCOUNT_VISIBILITY_PREF_KEY, ALL_USERS_VISIBILITY,
|
||||
ACCOUNT_VISIBILITY_PREF_KEY, ALL_USERS_VISIBILITY, PRIVATE_VISIBILITY,
|
||||
EMAIL_MIN_LENGTH, EMAIL_MAX_LENGTH, PASSWORD_MIN_LENGTH, PASSWORD_MAX_LENGTH,
|
||||
USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH
|
||||
)
|
||||
@@ -76,12 +76,8 @@ def get_account_settings(requesting_user, username=None, configuration=None, vie
|
||||
|
||||
visible_settings = {}
|
||||
|
||||
# Calling UserPreference directly because the requesting user may be different from existing_user
|
||||
# (and does not have to be is_staff).
|
||||
profile_privacy = UserPreference.get_value(existing_user, ACCOUNT_VISIBILITY_PREF_KEY)
|
||||
privacy_setting = profile_privacy if profile_privacy else configuration.get('default_visibility')
|
||||
|
||||
if privacy_setting == ALL_USERS_VISIBILITY:
|
||||
profile_visibility = _get_profile_visibility(existing_user_profile, configuration)
|
||||
if profile_visibility == ALL_USERS_VISIBILITY:
|
||||
field_names = configuration.get('shareable_fields')
|
||||
else:
|
||||
field_names = configuration.get('public_fields')
|
||||
@@ -92,6 +88,17 @@ def get_account_settings(requesting_user, username=None, configuration=None, vie
|
||||
return visible_settings
|
||||
|
||||
|
||||
def _get_profile_visibility(user_profile, configuration):
|
||||
"""Returns the visibility level for the specified user profile."""
|
||||
if user_profile.requires_parental_consent():
|
||||
return PRIVATE_VISIBILITY
|
||||
|
||||
# Calling UserPreference directly because the requesting user may be different from existing_user
|
||||
# (and does not have to be is_staff).
|
||||
profile_privacy = UserPreference.get_value(user_profile.user, ACCOUNT_VISIBILITY_PREF_KEY)
|
||||
return profile_privacy if profile_privacy else configuration.get('default_visibility')
|
||||
|
||||
|
||||
@intercept_errors(UserAPIInternalError, ignore_errors=[UserAPIRequestError])
|
||||
def update_account_settings(requesting_user, update, username=None):
|
||||
"""Update user account information.
|
||||
|
||||
@@ -25,16 +25,17 @@ class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer, Rea
|
||||
Class that serializes the portion of UserProfile model needed for account information.
|
||||
"""
|
||||
profile_image = serializers.SerializerMethodField("get_profile_image")
|
||||
requires_parental_consent = serializers.SerializerMethodField("get_requires_parental_consent")
|
||||
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = (
|
||||
"name", "gender", "goals", "year_of_birth", "level_of_education", "language", "country",
|
||||
"mailing_address", "bio", "profile_image"
|
||||
"mailing_address", "bio", "profile_image", "requires_parental_consent",
|
||||
)
|
||||
# Currently no read-only field, but keep this so view code doesn't need to know.
|
||||
read_only_fields = ()
|
||||
explicit_read_only_fields = ("profile_image",)
|
||||
explicit_read_only_fields = ("profile_image", "requires_parental_consent")
|
||||
|
||||
def validate_name(self, attrs, source):
|
||||
""" Enforce minimum length for name. """
|
||||
@@ -48,15 +49,15 @@ class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer, Rea
|
||||
|
||||
return attrs
|
||||
|
||||
def transform_gender(self, obj, value):
|
||||
def transform_gender(self, user_profile, value):
|
||||
""" Converts empty string to None, to indicate not set. Replaced by to_representation in version 3. """
|
||||
return AccountLegacyProfileSerializer.convert_empty_to_None(value)
|
||||
|
||||
def transform_country(self, obj, value):
|
||||
def transform_country(self, user_profile, value):
|
||||
""" Converts empty string to None, to indicate not set. Replaced by to_representation in version 3. """
|
||||
return AccountLegacyProfileSerializer.convert_empty_to_None(value)
|
||||
|
||||
def transform_level_of_education(self, obj, value):
|
||||
def transform_level_of_education(self, user_profile, value):
|
||||
""" Converts empty string to None, to indicate not set. Replaced by to_representation in version 3. """
|
||||
return AccountLegacyProfileSerializer.convert_empty_to_None(value)
|
||||
|
||||
@@ -65,12 +66,16 @@ class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer, Rea
|
||||
""" Helper method to convert empty string to None (other values pass through). """
|
||||
return None if value == "" else value
|
||||
|
||||
def get_profile_image(self, obj):
|
||||
def get_profile_image(self, user_profile):
|
||||
""" Returns metadata about a user's profile image. """
|
||||
data = {'has_image': obj.has_profile_image}
|
||||
data = {'has_image': user_profile.has_profile_image}
|
||||
data.update({
|
||||
'{image_key_prefix}_{size}'.format(image_key_prefix=PROFILE_IMAGE_KEY_PREFIX, size=size_display_name):
|
||||
get_profile_image_url_for_user(obj.user, size_value)
|
||||
get_profile_image_url_for_user(user_profile.user, size_value)
|
||||
for size_display_name, size_value in PROFILE_IMAGE_SIZES_MAP.items()
|
||||
})
|
||||
return data
|
||||
|
||||
def get_requires_parental_consent(self, user_profile):
|
||||
""" Returns a boolean representing whether the user requires parental controls. """
|
||||
return user_profile.requires_parental_consent()
|
||||
|
||||
@@ -220,6 +220,7 @@ class AccountSettingsOnCreationTest(TestCase):
|
||||
'image_url_full': 'http://example-storage.com/profile_images/default_50.jpg',
|
||||
'image_url_small': 'http://example-storage.com/profile_images/default_10.jpg',
|
||||
},
|
||||
'requires_parental_consent': True,
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
import ddt
|
||||
import hashlib
|
||||
import json
|
||||
@@ -84,9 +85,10 @@ class UserAPITestCase(APITestCase):
|
||||
legacy_profile = UserProfile.objects.get(id=user.id)
|
||||
legacy_profile.country = "US"
|
||||
legacy_profile.level_of_education = "m"
|
||||
legacy_profile.year_of_birth = 1900
|
||||
legacy_profile.year_of_birth = 2000
|
||||
legacy_profile.goals = "world peace"
|
||||
legacy_profile.mailing_address = "Park Ave"
|
||||
legacy_profile.gender = "f"
|
||||
legacy_profile.bio = "Tired mother of twins"
|
||||
legacy_profile.has_profile_image = True
|
||||
legacy_profile.save()
|
||||
@@ -139,27 +141,27 @@ class TestAccountAPI(UserAPITestCase):
|
||||
self.assertIsNone(data["languages"])
|
||||
self.assertEqual("Tired mother of twins", data["bio"])
|
||||
|
||||
def _verify_private_account_response(self, response):
|
||||
def _verify_private_account_response(self, response, requires_parental_consent=False):
|
||||
"""
|
||||
Verify that only the public fields are returned if a user does not want to share account fields
|
||||
"""
|
||||
data = response.data
|
||||
self.assertEqual(2, len(data))
|
||||
self.assertEqual(self.user.username, data["username"])
|
||||
self._verify_profile_image_data(data, True)
|
||||
self._verify_profile_image_data(data, not requires_parental_consent)
|
||||
|
||||
def _verify_full_account_response(self, response):
|
||||
def _verify_full_account_response(self, response, requires_parental_consent=False):
|
||||
"""
|
||||
Verify that all account fields are returned (even those that are not shareable).
|
||||
"""
|
||||
data = response.data
|
||||
self.assertEqual(14, len(data))
|
||||
self.assertEqual(15, len(data))
|
||||
self.assertEqual(self.user.username, data["username"])
|
||||
self.assertEqual(self.user.first_name + " " + self.user.last_name, data["name"])
|
||||
self.assertEqual("US", data["country"])
|
||||
self.assertEqual("", data["language"])
|
||||
self.assertEqual("m", data["gender"])
|
||||
self.assertEqual(1900, data["year_of_birth"])
|
||||
self.assertEqual("f", data["gender"])
|
||||
self.assertEqual(2000, data["year_of_birth"])
|
||||
self.assertEqual("m", data["level_of_education"])
|
||||
self.assertEqual("world peace", data["goals"])
|
||||
self.assertEqual("Park Ave", data['mailing_address'])
|
||||
@@ -167,7 +169,8 @@ class TestAccountAPI(UserAPITestCase):
|
||||
self.assertTrue(data["is_active"])
|
||||
self.assertIsNotNone(data["date_joined"])
|
||||
self.assertEqual("Tired mother of twins", data["bio"])
|
||||
self._verify_profile_image_data(data, True)
|
||||
self._verify_profile_image_data(data, not requires_parental_consent)
|
||||
self.assertEquals(requires_parental_consent, data["requires_parental_consent"])
|
||||
|
||||
def test_anonymous_access(self):
|
||||
"""
|
||||
@@ -269,7 +272,7 @@ class TestAccountAPI(UserAPITestCase):
|
||||
def verify_get_own_information():
|
||||
response = self.send_get(self.client)
|
||||
data = response.data
|
||||
self.assertEqual(14, len(data))
|
||||
self.assertEqual(15, len(data))
|
||||
self.assertEqual(self.user.username, data["username"])
|
||||
self.assertEqual(self.user.first_name + " " + self.user.last_name, data["name"])
|
||||
for empty_field in ("year_of_birth", "level_of_education", "mailing_address", "bio"):
|
||||
@@ -283,6 +286,7 @@ class TestAccountAPI(UserAPITestCase):
|
||||
self.assertIsNotNone(data["date_joined"])
|
||||
self.assertEqual(self.user.is_active, data["is_active"])
|
||||
self._verify_profile_image_data(data, False)
|
||||
self.assertTrue(data["requires_parental_consent"])
|
||||
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
verify_get_own_information()
|
||||
@@ -406,8 +410,8 @@ class TestAccountAPI(UserAPITestCase):
|
||||
"Field '{0}' cannot be edited.".format(field_name), data["field_errors"][field_name]["user_message"]
|
||||
)
|
||||
|
||||
for field_name in ["username", "date_joined", "is_active"]:
|
||||
response = self.send_patch(client, {field_name: "will_error", "gender": "f"}, expected_status=400)
|
||||
for field_name in ["username", "date_joined", "is_active", "profile_image", "requires_parental_consent"]:
|
||||
response = self.send_patch(client, {field_name: "will_error", "gender": "o"}, expected_status=400)
|
||||
verify_error_response(field_name, response.data)
|
||||
|
||||
# Make sure that gender did not change.
|
||||
|
||||
@@ -80,9 +80,24 @@ class AccountView(APIView):
|
||||
* goals: The textual representation of the user's goals, or null.
|
||||
|
||||
* bio: null or textural representation of user biographical
|
||||
information ("about me")
|
||||
information ("about me").
|
||||
|
||||
For all text fields, clients rendering the values should take care
|
||||
* profile_image: a dict with the following keys describing
|
||||
the user's profile image:
|
||||
* "has_image": true if the user has a profile image
|
||||
* "image_url_full": an absolute URL to the user's full
|
||||
profile image
|
||||
* "image_url_large": an absolute URL to a large thumbnail
|
||||
of the profile image
|
||||
* "image_url_medium": an absolute URL to a medium thumbnail
|
||||
of the profile image
|
||||
* "image_url_small": an absolute URL to a small thumbnail
|
||||
of the profile image
|
||||
|
||||
* requires_parental_consent: true if the user is a minor
|
||||
requiring parental consent.
|
||||
|
||||
> For all text fields, clients rendering the values should take care
|
||||
to HTML escape them to avoid script injections, as the data is
|
||||
stored exactly as specified. The intention is that plain text is
|
||||
supported, not HTML.
|
||||
|
||||
Reference in New Issue
Block a user