Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
97e44ed2
Commit
97e44ed2
authored
Mar 19, 2015
by
Daniel Friedman
Committed by
Andy Armstrong
Apr 17, 2015
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Implement language proficiencies.
TNL-1488
parent
4125bf96
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
168 additions
and
20 deletions
+168
-20
common/djangoapps/student/migrations/0049_auto__add_languageproficiency__add_unique_languageproficiency_code_use.py
+0
-0
common/djangoapps/student/models.py
+16
-0
lms/envs/common.py
+3
-1
openedx/core/djangoapps/user_api/accounts/serializers.py
+34
-3
openedx/core/djangoapps/user_api/accounts/tests/test_api.py
+15
-1
openedx/core/djangoapps/user_api/accounts/tests/test_views.py
+86
-6
openedx/core/djangoapps/user_api/accounts/views.py
+14
-9
No files found.
common/djangoapps/student/migrations/0049_auto__add_languageproficiency__add_unique_languageproficiency_code_use.py
0 → 100644
View file @
97e44ed2
This diff is collapsed.
Click to expand it.
common/djangoapps/student/models.py
View file @
97e44ed2
...
...
@@ -1608,3 +1608,19 @@ class EntranceExamConfiguration(models.Model):
except
EntranceExamConfiguration
.
DoesNotExist
:
can_skip
=
False
return
can_skip
class
LanguageProficiency
(
models
.
Model
):
"""
Represents a user's language proficiency.
"""
class
Meta
:
unique_together
=
((
'code'
,
'user_profile'
),)
user_profile
=
models
.
ForeignKey
(
UserProfile
,
db_index
=
True
,
related_name
=
'language_proficiencies'
)
code
=
models
.
CharField
(
max_length
=
16
,
blank
=
False
,
choices
=
settings
.
ALL_LANGUAGES
,
help_text
=
ugettext_lazy
(
"The ISO 639-1 language code for this language."
)
)
lms/envs/common.py
View file @
97e44ed2
...
...
@@ -1924,6 +1924,8 @@ TIME_ZONE_DISPLAYED_FOR_DEADLINES = 'UTC'
# Source:
# http://loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt according to http://en.wikipedia.org/wiki/ISO_639-1
# Note that this is used as the set of choices to the `code` field of the
# `LanguageProficiency` model.
ALL_LANGUAGES
=
(
[
u"aa"
,
u"Afar"
],
[
u"ab"
,
u"Abkhazian"
],
...
...
@@ -2230,7 +2232,7 @@ ACCOUNT_VISIBILITY_CONFIGURATION = {
'profile_image'
,
'country'
,
'time_zone'
,
'languages'
,
'language
_proficiencie
s'
,
'bio'
,
],
...
...
openedx/core/djangoapps/user_api/accounts/serializers.py
View file @
97e44ed2
...
...
@@ -3,12 +3,34 @@ from django.contrib.auth.models import User
from
openedx.core.djangoapps.user_api.accounts
import
NAME_MIN_LENGTH
from
openedx.core.djangoapps.user_api.serializers
import
ReadOnlyFieldsSerializerMixin
from
student.models
import
UserProfile
from
student.models
import
UserProfile
,
LanguageProficiency
from
.helpers
import
get_profile_image_url_for_user
,
PROFILE_IMAGE_SIZES_MAP
PROFILE_IMAGE_KEY_PREFIX
=
'image_url'
class
LanguageProficiencySerializer
(
serializers
.
ModelSerializer
):
"""
Class that serializes the LanguageProficiency model for account
information.
"""
class
Meta
:
model
=
LanguageProficiency
fields
=
(
"code"
,)
def
get_identity
(
self
,
data
):
"""
This is used in bulk updates to determine the identity of an object.
The default is to use the id of an object, but we want to override that
and consider the language code to be the canonical identity of a
LanguageProficiency model.
"""
try
:
return
data
.
get
(
'code'
,
None
)
except
AttributeError
:
return
None
class
AccountUserSerializer
(
serializers
.
HyperlinkedModelSerializer
,
ReadOnlyFieldsSerializerMixin
):
"""
Class that serializes the portion of User model needed for account information.
...
...
@@ -26,12 +48,13 @@ class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer, Rea
"""
profile_image
=
serializers
.
SerializerMethodField
(
"get_profile_image"
)
requires_parental_consent
=
serializers
.
SerializerMethodField
(
"get_requires_parental_consent"
)
language_proficiencies
=
LanguageProficiencySerializer
(
many
=
True
,
allow_add_remove
=
True
,
required
=
False
)
class
Meta
:
model
=
UserProfile
fields
=
(
"name"
,
"gender"
,
"goals"
,
"year_of_birth"
,
"level_of_education"
,
"
language"
,
"
country"
,
"mailing_address"
,
"bio"
,
"profile_image"
,
"requires_parental_consent"
,
"name"
,
"gender"
,
"goals"
,
"year_of_birth"
,
"level_of_education"
,
"country"
,
"mailing_address"
,
"bio"
,
"profile_image"
,
"requires_parental_consent"
,
"language_proficiencies"
)
# Currently no read-only field, but keep this so view code doesn't need to know.
read_only_fields
=
()
...
...
@@ -49,6 +72,14 @@ class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer, Rea
return
attrs
def
validate_language_proficiencies
(
self
,
attrs
,
source
):
""" Enforce all languages are unique. """
language_proficiencies
=
[
language
for
language
in
attrs
.
get
(
source
,
[])]
unique_language_proficiencies
=
set
(
language
.
code
for
language
in
language_proficiencies
)
if
len
(
language_proficiencies
)
!=
len
(
unique_language_proficiencies
):
raise
serializers
.
ValidationError
(
"The language_proficiencies field must consist of unique languages"
)
return
attrs
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
)
...
...
openedx/core/djangoapps/user_api/accounts/tests/test_api.py
View file @
97e44ed2
...
...
@@ -123,6 +123,20 @@ class TestAccountApi(TestCase):
{
"profile_image"
:
{
"has_image"
:
"not_allowed"
,
"image_url"
:
"not_allowed"
}}
)
# Check the various language_proficiencies validation failures.
# language_proficiencies must be a list of dicts, each containing a
# unique 'code' key representing the language code.
with
self
.
assertRaises
(
AccountValidationError
):
update_account_settings
(
self
.
user
,
{
"language_proficiencies"
:
"not_a_list"
}
)
with
self
.
assertRaises
(
AccountValidationError
):
update_account_settings
(
self
.
user
,
{
"language_proficiencies"
:
[{}]}
)
def
test_update_multiple_validation_errors
(
self
):
"""Test that all validation errors are built up and returned at once"""
# Send a read-only error, serializer error, and email validation error.
...
...
@@ -207,7 +221,6 @@ class AccountSettingsOnCreationTest(TestCase):
'email'
:
self
.
EMAIL
,
'name'
:
u''
,
'gender'
:
None
,
'language'
:
u''
,
'goals'
:
None
,
'is_active'
:
False
,
'level_of_education'
:
None
,
...
...
@@ -221,6 +234,7 @@ class AccountSettingsOnCreationTest(TestCase):
'image_url_small'
:
'http://example-storage.com/profile_images/default_10.jpg'
,
},
'requires_parental_consent'
:
True
,
'language_proficiencies'
:
[],
})
...
...
openedx/core/djangoapps/user_api/accounts/tests/test_views.py
View file @
97e44ed2
...
...
@@ -13,7 +13,7 @@ from django.test.utils import override_settings
from
rest_framework.test
import
APITestCase
,
APIClient
from
student.tests.factories
import
UserFactory
from
student.models
import
UserProfile
,
PendingEmailChange
from
student.models
import
UserProfile
,
LanguageProficiency
,
PendingEmailChange
from
openedx.core.djangoapps.user_api.accounts
import
ACCOUNT_VISIBILITY_PREF_KEY
from
openedx.core.djangoapps.user_api.preferences.api
import
set_user_preference
from
..
import
PRIVATE_VISIBILITY
,
ALL_USERS_VISIBILITY
...
...
@@ -91,6 +91,7 @@ class UserAPITestCase(APITestCase):
legacy_profile
.
gender
=
"f"
legacy_profile
.
bio
=
"Tired mother of twins"
legacy_profile
.
has_profile_image
=
True
legacy_profile
.
language_proficiencies
.
add
(
LanguageProficiency
(
code
=
'en'
))
legacy_profile
.
save
()
...
...
@@ -138,7 +139,7 @@ class TestAccountAPI(UserAPITestCase):
self
.
assertEqual
(
"US"
,
data
[
"country"
])
self
.
_verify_profile_image_data
(
data
,
True
)
self
.
assertIsNone
(
data
[
"time_zone"
])
self
.
assert
IsNone
(
data
[
"languag
es"
])
self
.
assert
Equal
([{
"code"
:
"en"
}],
data
[
"language_proficienci
es"
])
self
.
assertEqual
(
"Tired mother of twins"
,
data
[
"bio"
])
def
_verify_private_account_response
(
self
,
response
,
requires_parental_consent
=
False
):
...
...
@@ -159,7 +160,6 @@ class TestAccountAPI(UserAPITestCase):
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
(
"f"
,
data
[
"gender"
])
self
.
assertEqual
(
2000
,
data
[
"year_of_birth"
])
self
.
assertEqual
(
"m"
,
data
[
"level_of_education"
])
...
...
@@ -171,6 +171,7 @@ class TestAccountAPI(UserAPITestCase):
self
.
assertEqual
(
"Tired mother of twins"
,
data
[
"bio"
])
self
.
_verify_profile_image_data
(
data
,
not
requires_parental_consent
)
self
.
assertEquals
(
requires_parental_consent
,
data
[
"requires_parental_consent"
])
self
.
assertEqual
([{
"code"
:
"en"
}],
data
[
"language_proficiencies"
])
def
test_anonymous_access
(
self
):
"""
...
...
@@ -279,7 +280,6 @@ class TestAccountAPI(UserAPITestCase):
self
.
assertIsNone
(
data
[
empty_field
])
self
.
assertIsNone
(
data
[
"country"
])
# TODO: what should the format of this be?
self
.
assertEqual
(
""
,
data
[
"language"
])
self
.
assertEqual
(
"m"
,
data
[
"gender"
])
self
.
assertEqual
(
"Learn a lot"
,
data
[
"goals"
])
self
.
assertEqual
(
self
.
user
.
email
,
data
[
"email"
])
...
...
@@ -287,6 +287,7 @@ class TestAccountAPI(UserAPITestCase):
self
.
assertEqual
(
self
.
user
.
is_active
,
data
[
"is_active"
])
self
.
_verify_profile_image_data
(
data
,
False
)
self
.
assertTrue
(
data
[
"requires_parental_consent"
])
self
.
assertEqual
([],
data
[
"language_proficiencies"
])
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
verify_get_own_information
()
...
...
@@ -347,7 +348,6 @@ class TestAccountAPI(UserAPITestCase):
(
"year_of_birth"
,
2009
,
"not_an_int"
,
u"Enter a whole number."
),
(
"name"
,
"bob"
,
"z"
*
256
,
u"Ensure this value has at most 255 characters (it has 256)."
),
(
"name"
,
u"ȻħȺɍłɇs"
,
"z "
,
u"The name field must be at least 2 characters long."
),
(
"language"
,
"Creole"
),
(
"goals"
,
"Smell the roses"
),
(
"mailing_address"
,
"Sesame Street"
),
(
"bio"
,
"Lacrosse-playing superhero"
),
...
...
@@ -355,6 +355,7 @@ class TestAccountAPI(UserAPITestCase):
# Note that we store the raw data, so it is up to client to escape the HTML.
(
"bio"
,
"<html>fancy text</html>"
),
# Note that email is tested below, as it is not immediately updated.
# Note that language_proficiencies is tested below as there are multiple error and success conditions.
)
@ddt.unpack
def
test_patch_account
(
self
,
field
,
value
,
fails_validation_value
=
None
,
developer_validation_message
=
None
):
...
...
@@ -535,6 +536,42 @@ class TestAccountAPI(UserAPITestCase):
)
self
.
assertEqual
(
"Valid e-mail address required."
,
field_errors
[
"email"
][
"user_message"
])
def
test_patch_language_proficiencies
(
self
):
"""
Verify that patching the language_proficiencies field of the user
profile completely overwrites the previous value.
"""
client
=
self
.
login_client
(
"client"
,
"user"
)
# Patching language_proficiencies exercises the
# `LanguageProficiencySerializer.get_identity` method, which compares
# identifies language proficiencies based on their language code rather
# than django model id.
for
proficiencies
in
([{
"code"
:
"en"
},
{
"code"
:
"fr"
},
{
"code"
:
"es"
}],
[{
"code"
:
"fr"
}],
[{
"code"
:
"aa"
}],
[]):
self
.
send_patch
(
client
,
{
"language_proficiencies"
:
proficiencies
})
response
=
self
.
send_get
(
client
)
self
.
assertItemsEqual
(
response
.
data
[
"language_proficiencies"
],
proficiencies
)
@ddt.data
(
(
u"not_a_list"
,
[{
u'non_field_errors'
:
[
u'Expected a list of items.'
]}]),
([
u"not_a_JSON_object"
],
[{
u'non_field_errors'
:
[
u'Invalid data'
]}]),
([{}],
[{
"code"
:
[
u"This field is required."
]}]),
([{
u"code"
:
u"invalid_language_code"
}],
[{
'code'
:
[
u'Select a valid choice. invalid_language_code is not one of the available choices.'
]}]),
([{
u"code"
:
u"kw"
},
{
u"code"
:
u"el"
},
{
u"code"
:
u"kw"
}],
[
u'The language_proficiencies field must consist of unique languages'
]),
)
@ddt.unpack
def
test_patch_invalid_language_proficiencies
(
self
,
patch_value
,
expected_error_message
):
"""
Verify we handle error cases when patching the language_proficiencies
field.
"""
client
=
self
.
login_client
(
"client"
,
"user"
)
response
=
self
.
send_patch
(
client
,
{
"language_proficiencies"
:
patch_value
},
expected_status
=
400
)
self
.
assertEqual
(
response
.
data
[
"field_errors"
][
"language_proficiencies"
][
"developer_message"
],
u"Value '{patch_value}' is not valid for field 'language_proficiencies': {error_message}"
.
format
(
patch_value
=
patch_value
,
error_message
=
expected_error_message
)
)
@patch
(
'openedx.core.djangoapps.user_api.accounts.serializers.AccountUserSerializer.save'
)
def
test_patch_serializer_save_fails
(
self
,
serializer_save
):
"""
...
...
@@ -566,9 +603,52 @@ class TestAccountAPI(UserAPITestCase):
"image_url_full"
:
"http://testserver/profile_images/default_50.jpg"
,
"image_url_small"
:
"http://testserver/profile_images/default_10.jpg"
}
)
)
@ddt.data
(
(
"client"
,
"user"
,
True
),
(
"different_client"
,
"different_user"
,
False
),
(
"staff_client"
,
"staff_user"
,
True
),
)
@ddt.unpack
def
test_parental_consent
(
self
,
api_client
,
requesting_username
,
has_full_access
):
"""
Verifies that under thirteens never return a public profile.
"""
client
=
self
.
login_client
(
api_client
,
requesting_username
)
# Set the user to be ten years old with a public profile
legacy_profile
=
UserProfile
.
objects
.
get
(
id
=
self
.
user
.
id
)
current_year
=
datetime
.
datetime
.
now
()
.
year
legacy_profile
.
year_of_birth
=
current_year
-
10
legacy_profile
.
save
()
set_user_preference
(
self
.
user
,
ACCOUNT_VISIBILITY_PREF_KEY
,
ALL_USERS_VISIBILITY
)
# Verify that the default view is still private (except for clients with full access)
response
=
self
.
send_get
(
client
)
if
has_full_access
:
data
=
response
.
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
(
self
.
user
.
email
,
data
[
"email"
])
self
.
assertEqual
(
current_year
-
10
,
data
[
"year_of_birth"
])
for
empty_field
in
(
"country"
,
"level_of_education"
,
"mailing_address"
,
"bio"
):
self
.
assertIsNone
(
data
[
empty_field
])
self
.
assertEqual
(
"m"
,
data
[
"gender"
])
self
.
assertEqual
(
"Learn a lot"
,
data
[
"goals"
])
self
.
assertTrue
(
data
[
"is_active"
])
self
.
assertIsNotNone
(
data
[
"date_joined"
])
self
.
_verify_profile_image_data
(
data
,
False
)
self
.
assertTrue
(
data
[
"requires_parental_consent"
])
else
:
self
.
_verify_private_account_response
(
response
,
requires_parental_consent
=
True
)
# Verify that the shared view is still private
response
=
self
.
send_get
(
client
,
query_parameters
=
'view=shared'
)
self
.
_verify_private_account_response
(
response
,
requires_parental_consent
=
True
)
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
class
TestAccountAPITransactions
(
TransactionTestCase
):
"""
...
...
openedx/core/djangoapps/user_api/accounts/views.py
View file @
97e44ed2
...
...
@@ -47,18 +47,16 @@ class AccountView(APIView):
format provided by datetime.
For example, "2014-08-26T17:52:11Z".
* gender: One of the fullowing values:
* "m"
* "f"
* "o"
* null
* gender: One of the following values:
* "m"
* "f"
* "o"
* null
* year_of_birth: The year the user was born, as an integer, or
null.
null.
* level_of_education: One of the following values:
* "p": PhD or Doctorate
* "m": Master's or professional degree
* "b": Bachelor's degree
...
...
@@ -72,10 +70,13 @@ class AccountView(APIView):
* language: The user's preferred language, or null.
* country: null (not set), or a Country corresponding to one of
the ISO 3166-1 countries.
* country: A ISO 3166 country code or null.
* mailing_address: The textual representation of the user's
mailing address, or null.
mailing address, or null.
* goals: The textual representation of the user's goals, or null.
...
...
@@ -98,6 +99,10 @@ class AccountView(APIView):
* requires_parental_consent: true if the user is a minor
requiring parental consent.
* language_proficiencies: array of language preferences.
Each preference is a JSON object with the following keys:
* "code": string ISO 639-1 language code e.g. "en".
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
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment