Commit e710a5b2 by Andy Armstrong

Implement parental controls for the User API

TNL-1739
parent 650a9a9b
......@@ -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,
})
......
# -*- 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")
For all text fields, clients rendering the values should take care
information ("about me").
* 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.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment