Skip to content

Commit 43fd179

Browse files
authored
fix: Change search order for users (#516)
* fix: Change search order for users In some cases the context user and the data user are different, such as when an instructor enrolls a learner through the instructor dashboard. This change ensures that the data user is checked first, since that should be the "actor" for our purposes. It also changes search order to look for user id before username for performance reasons.
1 parent 4cbc5f5 commit 43fd179

File tree

5 files changed

+90
-72
lines changed

5 files changed

+90
-72
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ Change Log
1111

1212
.. There should always be an "Unreleased" section for changes pending release.
1313
14+
[9.3.6]
15+
16+
* Fixes issues where the context user is not the same as the data user, such as enrolling uses via the Instructor Dashboard
17+
1418
[9.3.5]
1519

1620
* Adding django52 support.

event_routing_backends/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
Various backends for receiving edX LMS events..
33
"""
44

5-
__version__ = '9.3.5'
5+
__version__ = "9.3.6"

event_routing_backends/models.py

Lines changed: 44 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
21
"""
32
Database models for event_routing_backends.
43
"""
4+
55
import logging
66
import re
77

@@ -39,7 +39,7 @@ def get_value_from_dotted_path(dict_obj, dotted_key):
3939
ANY : Returns the value found in the dict or `None` if
4040
no value exists for provided dotted path.
4141
"""
42-
nested_keys = dotted_key.split('.')
42+
nested_keys = dotted_key.split(".")
4343
result = dict_obj
4444
try:
4545
for key in nested_keys:
@@ -64,12 +64,18 @@ def get_routers(self, backend_name):
6464
if not backend_name:
6565
return []
6666

67-
cache_key = get_cache_key(namespace="event_routing_backends", resource=backend_name)
67+
cache_key = get_cache_key(
68+
namespace="event_routing_backends", resource=backend_name
69+
)
6870
cached_response = TieredCache.get_cached_response(cache_key)
6971
if cached_response.is_found:
7072
return cached_response.value
7173

72-
current = self.current_set().filter(backend_name=backend_name, enabled=True).order_by('-change_date')
74+
current = (
75+
self.current_set()
76+
.filter(backend_name=backend_name, enabled=True)
77+
.order_by("-change_date")
78+
)
7379
TieredCache.set_all_tiers(cache_key, current, backend_cache_ttl())
7480
return current
7581

@@ -114,13 +120,19 @@ class RouterConfiguration(ConfigurationModel):
114120
115121
"""
116122

117-
AUTH_BASIC = 'Basic'
118-
AUTH_BEARER = 'Bearer'
119-
AUTH_CHOICES = ((AUTH_BASIC, 'Basic'), (AUTH_BEARER, 'Bearer'),)
120-
CALIPER_BACKEND = 'Caliper'
121-
XAPI_BACKEND = 'xAPI'
122-
BACKEND_CHOICES = ((CALIPER_BACKEND, 'Caliper'), (XAPI_BACKEND, 'xAPI'),)
123-
KEY_FIELDS = ('route_url',)
123+
AUTH_BASIC = "Basic"
124+
AUTH_BEARER = "Bearer"
125+
AUTH_CHOICES = (
126+
(AUTH_BASIC, "Basic"),
127+
(AUTH_BEARER, "Bearer"),
128+
)
129+
CALIPER_BACKEND = "Caliper"
130+
XAPI_BACKEND = "xAPI"
131+
BACKEND_CHOICES = (
132+
(CALIPER_BACKEND, "Caliper"),
133+
(XAPI_BACKEND, "xAPI"),
134+
)
135+
KEY_FIELDS = ("route_url",)
124136
backend_name = models.CharField(
125137
choices=BACKEND_CHOICES,
126138
max_length=50,
@@ -129,49 +141,40 @@ class RouterConfiguration(ConfigurationModel):
129141
db_index=True,
130142
default=XAPI_BACKEND,
131143
help_text=(
132-
'Name of the tracking backend on which this router should be applied.'
133-
'<br/>'
134-
'Please note that this field is <b>case sensitive.</b>'
135-
)
144+
"Name of the tracking backend on which this router should be applied."
145+
"<br/>"
146+
"Please note that this field is <b>case sensitive.</b>"
147+
),
136148
)
137149

138150
route_url = models.CharField(
139151
max_length=255,
140-
verbose_name='Route url',
152+
verbose_name="Route url",
141153
null=False,
142154
blank=False,
143155
help_text=(
144-
'Route Url of the tracking backend on which this router should be applied.'
145-
'<br/>'
146-
'Please note that this field is <b>case sensitive.</b>'
147-
)
156+
"Route Url of the tracking backend on which this router should be applied."
157+
"<br/>"
158+
"Please note that this field is <b>case sensitive.</b>"
159+
),
148160
)
149161

150162
auth_scheme = models.CharField(
151163
choices=AUTH_CHOICES,
152-
verbose_name='Auth Scheme',
164+
verbose_name="Auth Scheme",
153165
max_length=6,
154166
default=None,
155167
blank=True,
156-
null=True
168+
null=True,
157169
)
158170
auth_key = EncryptedCharField(
159-
verbose_name='Auth Key',
160-
max_length=256,
161-
blank=True,
162-
null=True
171+
verbose_name="Auth Key", max_length=256, blank=True, null=True
163172
)
164173
username = EncryptedCharField(
165-
verbose_name='Username',
166-
max_length=256,
167-
blank=True,
168-
null=True
174+
verbose_name="Username", max_length=256, blank=True, null=True
169175
)
170176
password = EncryptedCharField(
171-
verbose_name='Password',
172-
max_length=256,
173-
blank=True,
174-
null=True
177+
verbose_name="Password", max_length=256, blank=True, null=True
175178
)
176179
configurations = EncryptedJSONField(blank=True, default=None)
177180
objects = RouterConfigurationManager()
@@ -181,17 +184,17 @@ class Meta:
181184
Addition of class names.
182185
"""
183186

184-
verbose_name = 'Router Configuration'
185-
verbose_name_plural = 'Router Configurations'
187+
verbose_name = "Router Configuration"
188+
verbose_name_plural = "Router Configurations"
186189

187190
def __str__(self):
188191
"""
189192
Return string representation for class instance.
190193
"""
191-
return '{id} - {backend} - {enabled}'.format(
194+
return "{id} - {backend} - {enabled}".format(
192195
id=self.pk,
193196
backend=self.backend_name,
194-
enabled='Enabled' if self.enabled else 'Disabled'
197+
enabled="Enabled" if self.enabled else "Disabled",
195198
)
196199

197200
@classmethod
@@ -255,7 +258,7 @@ def get_allowed_host(self, original_event):
255258
dict
256259
"""
257260
if not self.configurations:
258-
return {'host_configurations': {}}
261+
return {"host_configurations": {}}
259262

260263
is_allowed = self._match_event_for_host(original_event, self.configurations)
261264

@@ -275,7 +278,7 @@ def _match_event_for_host(self, original_event, host_config):
275278
Returns:
276279
bool
277280
"""
278-
for key, value in host_config.get('match_params', {}).items():
281+
for key, value in host_config.get("match_params", {}).items():
279282
original_event_value = get_value_from_dotted_path(original_event, key)
280283
if isinstance(value, list):
281284
matched = False
@@ -302,7 +305,5 @@ def _is_match(self, regex_exp, value_str):
302305
try:
303306
return bool(re.compile(str(regex_exp))) and re.search(regex_exp, value_str)
304307
except TypeError as err:
305-
logger.info(
306-
'Invalid regex %s with error: %s', regex_exp, err
307-
)
308+
logger.info("Invalid regex %s with error: %s", regex_exp, err)
308309
return False

event_routing_backends/processors/mixins/base_transformer.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Base Transformer Mixin to add or transform common data values.
33
"""
4+
45
import logging
56

67
from django.conf import settings
@@ -43,6 +44,7 @@ def find_nested(source_dict, key):
4344
Returns:
4445
ANY
4546
"""
47+
4648
def _find_nested(event_dict):
4749
"""
4850
Inner recursive method to find the key in dict.
@@ -84,7 +86,7 @@ def transformer_version(self):
8486
version of transformer package used to transform events
8587
"""
8688

87-
if getattr(settings, 'RUNNING_WITH_TEST_SETTINGS', False):
89+
if getattr(settings, "RUNNING_WITH_TEST_SETTINGS", False):
8890
return "{}@{}".format("event-routing-backends", "1.1.1")
8991
else:
9092
return "{}@{}".format("event-routing-backends", __version__)
@@ -103,13 +105,13 @@ def transform(self):
103105
if hasattr(self, key):
104106
value = getattr(self, key)
105107
transformed_event[key] = value
106-
elif hasattr(self, f'get_{key}'):
107-
value = getattr(self, f'get_{key}')()
108+
elif hasattr(self, f"get_{key}"):
109+
value = getattr(self, f"get_{key}")()
108110
transformed_event[key] = value
109111
else:
110112
raise ValueError(
111113
'Cannot find value for "{}" in transformer {} for the edx event "{}"'.format(
112-
key, self.__class__.__name__, self.get_data('name', True)
114+
key, self.__class__.__name__, self.get_data("name", True)
113115
)
114116
)
115117

@@ -125,12 +127,17 @@ def extract_username_or_userid(self):
125127
Returns:
126128
str
127129
"""
128-
username_or_id = self.get_data('username') or self.get_data('user_id')
129-
if not username_or_id:
130-
username_or_id = self.get_data('data.username') or self.get_data('data.user_id')
131-
if not username_or_id:
132-
username_or_id = self.get_data('context.username') or self.get_data('context.user_id')
133-
return username_or_id
130+
# Prefer the user data in "data", since in some cases the user performing an action
131+
# (the one in "context") is not the user being acted upon. This happens when an instructor
132+
# creates an enrollment via the Instructor Dashboard, for instance.
133+
#
134+
# Prefer user id to username, since usernames can change (via retirement) and usernames can
135+
# require more database lookups (again, for finding retired users).
136+
id_or_username = self.get_data("data.user_id") or self.get_data("data.username")
137+
if not id_or_username:
138+
id_or_username = self.get_data("user_id") or self.get_data("username")
139+
140+
return id_or_username
134141

135142
def extract_sessionid(self):
136143
"""
@@ -139,7 +146,7 @@ def extract_sessionid(self):
139146
Returns:
140147
str
141148
"""
142-
return self.get_data('session') or self.get_data('context.session') or self.get_data('data.session')
149+
return self.get_data("session")
143150

144151
def get_data(self, key, required=False):
145152
"""
@@ -166,7 +173,7 @@ def get_data(self, key, required=False):
166173
}
167174
}
168175
"""
169-
if '.' in key:
176+
if "." in key:
170177
result = get_value_from_dotted_path(self.event, key)
171178
else:
172179
result = BaseTransformerMixin.find_nested(self.event, key)
@@ -177,7 +184,9 @@ def get_data(self, key, required=False):
177184
if result is None:
178185
if required:
179186
raise ValueError(
180-
'Could not get value for {} in event "{}"'.format(key, self.event.get('name', None))
187+
'Could not get value for {} in event "{}"'.format(
188+
key, self.event.get("name", None)
189+
)
181190
)
182191

183192
return result
@@ -210,10 +219,8 @@ def get_object_iri(self, object_type, object_id):
210219
if object_id is None or object_type is None:
211220
return None
212221

213-
return '{root_url}/{object_type}/{object_id}'.format(
214-
root_url=settings.LMS_ROOT_URL,
215-
object_type=object_type,
216-
object_id=object_id
222+
return "{root_url}/{object_type}/{object_id}".format(
223+
root_url=settings.LMS_ROOT_URL, object_type=object_type, object_id=object_id
217224
)
218225

219226
def get_object(self):

test_utils/__init__.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
99
So this package is the place to put them.
1010
"""
11+
1112
import sys
1213
from unittest import mock
1314

@@ -18,36 +19,41 @@ def _mock_third_party_modules():
1819
"""
1920
# mock external_user_ids module
2021
external_id = mock.MagicMock()
21-
external_id.external_user_id = '32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb'
22+
external_id.external_user_id = "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb"
2223
external_user_ids_module = mock.MagicMock()
23-
external_user_ids_module.ExternalId.add_new_user_id.return_value = (external_id, True)
24-
external_user_ids_module.ExternalIdType.XAPI = 'xapi'
25-
sys.modules['openedx.core.djangoapps.external_user_ids.models'] = external_user_ids_module
24+
external_user_ids_module.ExternalId.add_new_user_id.return_value = (
25+
external_id,
26+
True,
27+
)
28+
external_user_ids_module.ExternalIdType.XAPI = "xapi"
29+
sys.modules["openedx.core.djangoapps.external_user_ids.models"] = (
30+
external_user_ids_module
31+
)
2632

2733
# mock course
2834
mocked_course = {
29-
'display_name': 'Demonstration Course',
35+
"display_name": "Demonstration Course",
3036
}
3137

3238
mocked_courses = mock.MagicMock()
3339
mocked_courses.get_course_overviews.return_value = [mocked_course]
34-
sys.modules['openedx.core.djangoapps.content.course_overviews.api'] = mocked_courses
40+
sys.modules["openedx.core.djangoapps.content.course_overviews.api"] = mocked_courses
3541

3642
# mock opaque keys module
3743
mocked_keys = mock.MagicMock()
38-
sys.modules['opaque_keys.edx.keys'] = mocked_keys
44+
sys.modules["opaque_keys.edx.keys"] = mocked_keys
3945

4046
# mock retired user
4147
mocked_user = mock.MagicMock()
42-
mocked_user.username = 'edx_retired'
43-
mocked_user.email = 'edx_retired@example.com'
48+
mocked_user.username = "edx"
49+
mocked_user.email = "edx@example.com"
4450
mocked_models = mock.MagicMock()
4551
mocked_models.get_potentially_retired_user_by_username.return_value = mocked_user
46-
sys.modules['common.djangoapps.student.models'] = mocked_models
52+
sys.modules["common.djangoapps.student.models"] = mocked_models
4753

4854

4955
def mocked_course_reverse(_, kwargs):
5056
"""
5157
Return the reverse method to return course root URL.
5258
"""
53-
return '/courses/{}/'.format(kwargs.get('course_id'))
59+
return "/courses/{}/".format(kwargs.get("course_id"))

0 commit comments

Comments
 (0)