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
fade4a10
Commit
fade4a10
authored
Jun 02, 2017
by
Kyle McCormick
Committed by
Kyle McCormick
Jul 11, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Implement event for forum thread views
EDUCATOR-341
parent
4d157db6
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
985 additions
and
211 deletions
+985
-211
common/djangoapps/track/views/segmentio.py
+44
-4
common/djangoapps/track/views/tests/base.py
+112
-0
common/djangoapps/track/views/tests/test_segmentio.py
+30
-119
common/djangoapps/util/testing.py
+17
-0
lms/djangoapps/discussion/tests/test_views.py
+137
-22
lms/djangoapps/discussion/views.py
+60
-15
lms/djangoapps/django_comment_client/base/__init__.py
+2
-0
lms/djangoapps/django_comment_client/base/event_transformers.py
+158
-0
lms/djangoapps/django_comment_client/base/tests.py
+346
-2
lms/djangoapps/django_comment_client/base/views.py
+16
-3
lms/djangoapps/django_comment_client/utils.py
+31
-6
lms/envs/common.py
+1
-1
lms/lib/comment_client/thread.py
+13
-21
lms/lib/comment_client/user.py
+18
-18
No files found.
common/djangoapps/track/views/segmentio.py
View file @
fade4a10
...
...
@@ -28,6 +28,10 @@ ERROR_MISSING_TIMESTAMP = 'Required timestamp field not found'
ERROR_MISSING_RECEIVED_AT
=
'Required receivedAt field not found'
FORUM_THREAD_VIEWED_EVENT_LABEL
=
'Forum: View Thread'
BI_SCREEN_VIEWED_EVENT_NAME
=
u'edx.bi.app.navigation.screen'
@require_POST
@expect_json
@csrf_exempt
...
...
@@ -141,11 +145,8 @@ def track_segmentio_event(request): # pylint: disable=too-many-statements
if
not
segment_event_type
or
(
segment_event_type
.
lower
()
not
in
allowed_types
):
return
if
'name'
not
in
segment_properties
:
raise
EventValidationError
(
ERROR_MISSING_NAME
)
# Ignore event names that are unsupported
segment_event_name
=
segment_properties
[
'name'
]
segment_event_name
=
_get_segmentio_event_name
(
segment_properties
)
disallowed_substring_names
=
[
a
.
lower
()
for
a
in
getattr
(
settings
,
'TRACKING_SEGMENTIO_DISALLOWED_SUBSTRING_NAMES'
,
[])
]
...
...
@@ -220,10 +221,49 @@ def track_segmentio_event(request): # pylint: disable=too-many-statements
context
[
'ip'
]
=
segment_properties
.
get
(
'context'
,
{})
.
get
(
'ip'
,
''
)
# For Business Intelligence events: Add label to context
if
'label'
in
segment_properties
:
context
[
'label'
]
=
segment_properties
[
'label'
]
# For Android-sourced Business Intelligence events: add course ID to context
if
'course_id'
in
segment_properties
:
context
[
'course_id'
]
=
segment_properties
[
'course_id'
]
with
tracker
.
get_tracker
()
.
context
(
'edx.segmentio'
,
context
):
tracker
.
emit
(
segment_event_name
,
segment_properties
.
get
(
'data'
,
{}))
def
_get_segmentio_event_name
(
event_properties
):
"""
Get the name of a SegmentIO event.
Args:
event_properties: dict
The properties of the event, which should contain the event's
name or, in the case of an old Android screen event, its screen
label.
Returns: str
The name (or effective name) of the event.
Note:
In older versions of the Android app, screen-view tracking events
did not have a name. So, in order to capture forum-thread-viewed events
from those old-versioned apps, we have to accept the event based on
its screen label. We return an event name that matches screen-view
events in the iOS app and newer versions of the Android app.
Raises:
EventValidationError if name is missing
"""
if
'name'
in
event_properties
:
return
event_properties
[
'name'
]
elif
event_properties
.
get
(
'label'
)
==
FORUM_THREAD_VIEWED_EVENT_LABEL
:
return
BI_SCREEN_VIEWED_EVENT_NAME
else
:
raise
EventValidationError
(
ERROR_MISSING_NAME
)
def
parse_iso8601_timestamp
(
timestamp
):
"""Parse a particular type of ISO8601 formatted timestamp"""
return
datetime
.
datetime
.
strptime
(
timestamp
,
"
%
Y-
%
m-
%
dT
%
H:
%
M:
%
S.
%
fZ"
)
common/djangoapps/track/views/tests/base.py
0 → 100644
View file @
fade4a10
"""
Base class for tests related to emitted events to one of the tracking 'views'
(e.g. SegmentIO).
"""
import
json
from
mock
import
sentinel
from
django.test.client
import
RequestFactory
from
django.test.utils
import
override_settings
from
track.views
import
segmentio
from
track.tests
import
EventTrackingTestCase
SEGMENTIO_TEST_SECRET
=
'anything'
SEGMENTIO_TEST_ENDPOINT
=
'/segmentio/test/event'
SEGMENTIO_TEST_USER_ID
=
10
_MOBILE_SHIM_PROCESSOR
=
[
{
'ENGINE'
:
'track.shim.LegacyFieldMappingProcessor'
},
{
'ENGINE'
:
'track.shim.PrefixedEventProcessor'
},
]
@override_settings
(
TRACKING_SEGMENTIO_WEBHOOK_SECRET
=
SEGMENTIO_TEST_SECRET
,
TRACKING_IGNORE_URL_PATTERNS
=
[
SEGMENTIO_TEST_ENDPOINT
],
TRACKING_SEGMENTIO_ALLOWED_TYPES
=
[
'track'
],
TRACKING_SEGMENTIO_DISALLOWED_SUBSTRING_NAMES
=
[],
TRACKING_SEGMENTIO_SOURCE_MAP
=
{
'test-app'
:
'mobile'
},
EVENT_TRACKING_PROCESSORS
=
_MOBILE_SHIM_PROCESSOR
,
)
class
SegmentIOTrackingTestCaseBase
(
EventTrackingTestCase
):
"""
Base class for tests that test the processing of Segment events.
"""
def
setUp
(
self
):
super
(
SegmentIOTrackingTestCaseBase
,
self
)
.
setUp
()
self
.
maxDiff
=
None
# pylint: disable=invalid-name
self
.
request_factory
=
RequestFactory
()
def
create_request
(
self
,
key
=
None
,
**
kwargs
):
"""Create a fake request that emulates a request from the Segment servers to ours"""
if
key
is
None
:
key
=
SEGMENTIO_TEST_SECRET
request
=
self
.
request_factory
.
post
(
SEGMENTIO_TEST_ENDPOINT
+
"?key="
+
key
,
**
kwargs
)
if
'data'
in
kwargs
:
request
.
json
=
json
.
loads
(
kwargs
[
'data'
])
return
request
def
post_segmentio_event
(
self
,
**
kwargs
):
"""Post a fake Segment event to the view that processes it"""
request
=
self
.
create_request
(
data
=
self
.
create_segmentio_event_json
(
**
kwargs
),
content_type
=
'application/json'
)
segmentio
.
track_segmentio_event
(
request
)
def
create_segmentio_event
(
self
,
**
kwargs
):
"""Populate a fake Segment event with data of interest"""
action
=
kwargs
.
get
(
'action'
,
'Track'
)
sample_event
=
{
"userId"
:
kwargs
.
get
(
'user_id'
,
SEGMENTIO_TEST_USER_ID
),
"event"
:
"Did something"
,
"properties"
:
{
'name'
:
kwargs
.
get
(
'name'
,
str
(
sentinel
.
name
)),
'data'
:
kwargs
.
get
(
'data'
,
{}),
'context'
:
{
'course_id'
:
kwargs
.
get
(
'course_id'
)
or
''
,
'app_name'
:
'edx.mobile.android'
,
}
},
"channel"
:
'server'
,
"context"
:
{
"library"
:
{
"name"
:
kwargs
.
get
(
'library_name'
,
'test-app'
),
"version"
:
"unknown"
},
"app"
:
{
"version"
:
"1.0.1"
,
},
'userAgent'
:
str
(
sentinel
.
user_agent
),
},
"receivedAt"
:
"2014-08-27T16:33:39.100Z"
,
"timestamp"
:
"2014-08-27T16:33:39.215Z"
,
"type"
:
action
.
lower
(),
"projectId"
:
"u0j33yjkr8"
,
"messageId"
:
"qy52hwp4"
,
"version"
:
2
,
"integrations"
:
{},
"options"
:
{
"library"
:
"unknown"
,
"providers"
:
{}
},
"action"
:
action
}
if
'context'
in
kwargs
:
sample_event
[
'properties'
][
'context'
]
.
update
(
kwargs
[
'context'
])
if
'label'
in
kwargs
:
sample_event
[
'properties'
][
'label'
]
=
kwargs
[
'label'
]
if
kwargs
.
get
(
'exclude_name'
)
is
True
:
del
sample_event
[
'properties'
][
'name'
]
return
sample_event
def
create_segmentio_event_json
(
self
,
**
kwargs
):
"""Return a json string containing a fake Segment event"""
return
json
.
dumps
(
self
.
create_segmentio_event
(
**
kwargs
))
common/djangoapps/track/views/tests/test_segmentio.py
View file @
fade4a10
...
...
@@ -8,23 +8,17 @@ from mock import sentinel
from
nose.plugins.attrib
import
attr
from
django.contrib.auth.models
import
User
from
django.test.client
import
RequestFactory
from
django.test.utils
import
override_settings
from
openedx.core.lib.tests.assertions.events
import
assert_event_matches
from
track.middleware
import
TrackMiddleware
from
track.tests
import
EventTrackingTestCase
from
track.views
import
segmentio
SECRET
=
'anything'
ENDPOINT
=
'/segmentio/test/event'
USER_ID
=
10
MOBILE_SHIM_PROCESSOR
=
[
{
'ENGINE'
:
'track.shim.LegacyFieldMappingProcessor'
},
{
'ENGINE'
:
'track.shim.PrefixedEventProcessor'
},
]
from
track.views.tests.base
import
(
SegmentIOTrackingTestCaseBase
,
SEGMENTIO_TEST_SECRET
,
SEGMENTIO_TEST_ENDPOINT
,
SEGMENTIO_TEST_USER_ID
)
def
expect_failure_with_message
(
message
):
...
...
@@ -39,24 +33,13 @@ def expect_failure_with_message(message):
@attr
(
shard
=
3
)
@ddt
@override_settings
(
TRACKING_SEGMENTIO_WEBHOOK_SECRET
=
SECRET
,
TRACKING_IGNORE_URL_PATTERNS
=
[
ENDPOINT
],
TRACKING_SEGMENTIO_ALLOWED_TYPES
=
[
'track'
],
TRACKING_SEGMENTIO_DISALLOWED_SUBSTRING_NAMES
=
[
'.bi.'
],
TRACKING_SEGMENTIO_SOURCE_MAP
=
{
'test-app'
:
'mobile'
},
EVENT_TRACKING_PROCESSORS
=
MOBILE_SHIM_PROCESSOR
,
)
class
SegmentIOTrackingTestCase
(
EventTrackingTestCase
):
"""Test processing of Segment events"""
def
setUp
(
self
):
super
(
SegmentIOTrackingTestCase
,
self
)
.
setUp
()
self
.
maxDiff
=
None
# pylint: disable=invalid-name
self
.
request_factory
=
RequestFactory
()
class
SegmentIOTrackingTestCase
(
SegmentIOTrackingTestCaseBase
):
"""
Test processing of Segment events.
"""
def
test_get_request
(
self
):
request
=
self
.
request_factory
.
get
(
ENDPOINT
)
request
=
self
.
request_factory
.
get
(
SEGMENTIO_TEST_
ENDPOINT
)
response
=
segmentio
.
segmentio_event
(
request
)
self
.
assertEquals
(
response
.
status_code
,
405
)
self
.
assert_no_events_emitted
()
...
...
@@ -65,13 +48,13 @@ class SegmentIOTrackingTestCase(EventTrackingTestCase):
TRACKING_SEGMENTIO_WEBHOOK_SECRET
=
None
)
def
test_no_secret_config
(
self
):
request
=
self
.
request_factory
.
post
(
ENDPOINT
)
request
=
self
.
request_factory
.
post
(
SEGMENTIO_TEST_
ENDPOINT
)
response
=
segmentio
.
segmentio_event
(
request
)
self
.
assertEquals
(
response
.
status_code
,
401
)
self
.
assert_no_events_emitted
()
def
test_no_secret_provided
(
self
):
request
=
self
.
request_factory
.
post
(
ENDPOINT
)
request
=
self
.
request_factory
.
post
(
SEGMENTIO_TEST_
ENDPOINT
)
response
=
segmentio
.
segmentio_event
(
request
)
self
.
assertEquals
(
response
.
status_code
,
401
)
self
.
assert_no_events_emitted
()
...
...
@@ -82,83 +65,11 @@ class SegmentIOTrackingTestCase(EventTrackingTestCase):
self
.
assertEquals
(
response
.
status_code
,
401
)
self
.
assert_no_events_emitted
()
def
create_request
(
self
,
key
=
None
,
**
kwargs
):
"""Create a fake request that emulates a request from the Segment servers to ours"""
if
key
is
None
:
key
=
SECRET
request
=
self
.
request_factory
.
post
(
ENDPOINT
+
"?key="
+
key
,
**
kwargs
)
if
'data'
in
kwargs
:
request
.
json
=
json
.
loads
(
kwargs
[
'data'
])
return
request
@data
(
'identify'
,
'Group'
,
'Alias'
,
'Page'
,
'identify'
,
'screen'
)
def
test_segmentio_ignore_actions
(
self
,
action
):
self
.
post_segmentio_event
(
action
=
action
)
self
.
assert_no_events_emitted
()
@data
(
'edx.bi.some_name'
,
'EDX.BI.CAPITAL_NAME'
)
def
test_segmentio_ignore_names
(
self
,
name
):
self
.
post_segmentio_event
(
name
=
name
)
self
.
assert_no_events_emitted
()
def
post_segmentio_event
(
self
,
**
kwargs
):
"""Post a fake Segment event to the view that processes it"""
request
=
self
.
create_request
(
data
=
self
.
create_segmentio_event_json
(
**
kwargs
),
content_type
=
'application/json'
)
segmentio
.
track_segmentio_event
(
request
)
def
create_segmentio_event
(
self
,
**
kwargs
):
"""Populate a fake Segment event with data of interest"""
action
=
kwargs
.
get
(
'action'
,
'Track'
)
sample_event
=
{
"userId"
:
kwargs
.
get
(
'user_id'
,
USER_ID
),
"event"
:
"Did something"
,
"properties"
:
{
'name'
:
kwargs
.
get
(
'name'
,
str
(
sentinel
.
name
)),
'data'
:
kwargs
.
get
(
'data'
,
{}),
'context'
:
{
'course_id'
:
kwargs
.
get
(
'course_id'
)
or
''
,
'app_name'
:
'edx.mobile.android'
,
}
},
"channel"
:
'server'
,
"context"
:
{
"library"
:
{
"name"
:
kwargs
.
get
(
'library_name'
,
'test-app'
),
"version"
:
"unknown"
},
"app"
:
{
"version"
:
"1.0.1"
,
},
'userAgent'
:
str
(
sentinel
.
user_agent
),
},
"receivedAt"
:
"2014-08-27T16:33:39.100Z"
,
"timestamp"
:
"2014-08-27T16:33:39.215Z"
,
"type"
:
action
.
lower
(),
"projectId"
:
"u0j33yjkr8"
,
"messageId"
:
"qy52hwp4"
,
"version"
:
2
,
"integrations"
:
{},
"options"
:
{
"library"
:
"unknown"
,
"providers"
:
{}
},
"action"
:
action
}
if
'context'
in
kwargs
:
sample_event
[
'properties'
][
'context'
]
.
update
(
kwargs
[
'context'
])
return
sample_event
def
create_segmentio_event_json
(
self
,
**
kwargs
):
"""Return a json string containing a fake Segment event"""
return
json
.
dumps
(
self
.
create_segmentio_event
(
**
kwargs
))
def
test_segmentio_ignore_unknown_libraries
(
self
):
self
.
post_segmentio_event
(
library_name
=
'foo'
)
self
.
assert_no_events_emitted
()
...
...
@@ -179,7 +90,7 @@ class SegmentIOTrackingTestCase(EventTrackingTestCase):
data
=
self
.
create_segmentio_event_json
(
data
=
{
'foo'
:
'bar'
},
course_id
=
course_id
),
content_type
=
'application/json'
)
User
.
objects
.
create
(
pk
=
USER_ID
,
username
=
str
(
sentinel
.
username
))
User
.
objects
.
create
(
pk
=
SEGMENTIO_TEST_
USER_ID
,
username
=
str
(
sentinel
.
username
))
middleware
.
process_request
(
request
)
# The middleware normally emits an event, make sure it doesn't in this case.
...
...
@@ -207,10 +118,10 @@ class SegmentIOTrackingTestCase(EventTrackingTestCase):
'name'
:
'edx.mobile.android'
,
'version'
:
'1.0.1'
,
},
'user_id'
:
USER_ID
,
'user_id'
:
SEGMENTIO_TEST_
USER_ID
,
'course_id'
:
course_id
,
'org_id'
:
u'foo'
,
'path'
:
ENDPOINT
,
'path'
:
SEGMENTIO_TEST_
ENDPOINT
,
'client'
:
{
'library'
:
{
'name'
:
'test-app'
,
...
...
@@ -233,7 +144,7 @@ class SegmentIOTrackingTestCase(EventTrackingTestCase):
data
=
self
.
create_segmentio_event_json
(
course_id
=
'invalid'
),
content_type
=
'application/json'
)
User
.
objects
.
create
(
pk
=
USER_ID
,
username
=
str
(
sentinel
.
username
))
User
.
objects
.
create
(
pk
=
SEGMENTIO_TEST_
USER_ID
,
username
=
str
(
sentinel
.
username
))
segmentio
.
track_segmentio_event
(
request
)
self
.
assert_events_emitted
()
...
...
@@ -245,7 +156,7 @@ class SegmentIOTrackingTestCase(EventTrackingTestCase):
data
=
json
.
dumps
(
sample_event_raw
),
content_type
=
'application/json'
)
User
.
objects
.
create
(
pk
=
USER_ID
,
username
=
str
(
sentinel
.
username
))
User
.
objects
.
create
(
pk
=
SEGMENTIO_TEST_
USER_ID
,
username
=
str
(
sentinel
.
username
))
segmentio
.
track_segmentio_event
(
request
)
...
...
@@ -257,7 +168,7 @@ class SegmentIOTrackingTestCase(EventTrackingTestCase):
data
=
json
.
dumps
(
sample_event_raw
),
content_type
=
'application/json'
)
User
.
objects
.
create
(
pk
=
USER_ID
,
username
=
str
(
sentinel
.
username
))
User
.
objects
.
create
(
pk
=
SEGMENTIO_TEST_
USER_ID
,
username
=
str
(
sentinel
.
username
))
segmentio
.
track_segmentio_event
(
request
)
...
...
@@ -268,7 +179,7 @@ class SegmentIOTrackingTestCase(EventTrackingTestCase):
data
=
json
.
dumps
(
sample_event_raw
),
content_type
=
'application/json'
)
User
.
objects
.
create
(
pk
=
USER_ID
,
username
=
str
(
sentinel
.
username
))
User
.
objects
.
create
(
pk
=
SEGMENTIO_TEST_
USER_ID
,
username
=
str
(
sentinel
.
username
))
segmentio
.
track_segmentio_event
(
request
)
...
...
@@ -279,7 +190,7 @@ class SegmentIOTrackingTestCase(EventTrackingTestCase):
data
=
json
.
dumps
(
sample_event_raw
),
content_type
=
'application/json'
)
User
.
objects
.
create
(
pk
=
USER_ID
,
username
=
str
(
sentinel
.
username
))
User
.
objects
.
create
(
pk
=
SEGMENTIO_TEST_
USER_ID
,
username
=
str
(
sentinel
.
username
))
segmentio
.
track_segmentio_event
(
request
)
...
...
@@ -294,8 +205,8 @@ class SegmentIOTrackingTestCase(EventTrackingTestCase):
return
event
def
test_string_user_id
(
self
):
User
.
objects
.
create
(
pk
=
USER_ID
,
username
=
str
(
sentinel
.
username
))
self
.
post_segmentio_event
(
user_id
=
str
(
USER_ID
))
User
.
objects
.
create
(
pk
=
SEGMENTIO_TEST_
USER_ID
,
username
=
str
(
sentinel
.
username
))
self
.
post_segmentio_event
(
user_id
=
str
(
SEGMENTIO_TEST_
USER_ID
))
self
.
assert_events_emitted
()
def
test_hiding_failure
(
self
):
...
...
@@ -304,7 +215,7 @@ class SegmentIOTrackingTestCase(EventTrackingTestCase):
data
=
json
.
dumps
(
sample_event_raw
),
content_type
=
'application/json'
)
User
.
objects
.
create
(
pk
=
USER_ID
,
username
=
str
(
sentinel
.
username
))
User
.
objects
.
create
(
pk
=
SEGMENTIO_TEST_
USER_ID
,
username
=
str
(
sentinel
.
username
))
response
=
segmentio
.
segmentio_event
(
request
)
self
.
assertEquals
(
response
.
status_code
,
200
)
...
...
@@ -350,7 +261,7 @@ class SegmentIOTrackingTestCase(EventTrackingTestCase):
}),
content_type
=
'application/json'
)
User
.
objects
.
create
(
pk
=
USER_ID
,
username
=
str
(
sentinel
.
username
))
User
.
objects
.
create
(
pk
=
SEGMENTIO_TEST_
USER_ID
,
username
=
str
(
sentinel
.
username
))
middleware
.
process_request
(
request
)
try
:
...
...
@@ -371,10 +282,10 @@ class SegmentIOTrackingTestCase(EventTrackingTestCase):
'time'
:
datetime
.
strptime
(
"2014-08-27T16:33:39.215Z"
,
"
%
Y-
%
m-
%
dT
%
H:
%
M:
%
S.
%
fZ"
),
'host'
:
'testserver'
,
'context'
:
{
'user_id'
:
USER_ID
,
'user_id'
:
SEGMENTIO_TEST_
USER_ID
,
'course_id'
:
course_id
,
'org_id'
:
'foo'
,
'path'
:
ENDPOINT
,
'path'
:
SEGMENTIO_TEST_
ENDPOINT
,
'client'
:
{
'library'
:
{
'name'
:
'test-app'
,
...
...
@@ -484,7 +395,7 @@ class SegmentIOTrackingTestCase(EventTrackingTestCase):
),
content_type
=
'application/json'
)
User
.
objects
.
create
(
pk
=
USER_ID
,
username
=
str
(
sentinel
.
username
))
User
.
objects
.
create
(
pk
=
SEGMENTIO_TEST_
USER_ID
,
username
=
str
(
sentinel
.
username
))
middleware
.
process_request
(
request
)
try
:
...
...
@@ -505,10 +416,10 @@ class SegmentIOTrackingTestCase(EventTrackingTestCase):
'time'
:
datetime
.
strptime
(
"2014-08-27T16:33:39.215Z"
,
"
%
Y-
%
m-
%
dT
%
H:
%
M:
%
S.
%
fZ"
),
'host'
:
'testserver'
,
'context'
:
{
'user_id'
:
USER_ID
,
'user_id'
:
SEGMENTIO_TEST_
USER_ID
,
'course_id'
:
course_id
,
'org_id'
:
'foo'
,
'path'
:
ENDPOINT
,
'path'
:
SEGMENTIO_TEST_
ENDPOINT
,
'client'
:
{
'library'
:
{
'name'
:
'test-app'
,
...
...
common/djangoapps/util/testing.py
View file @
fade4a10
...
...
@@ -96,12 +96,29 @@ class EventTestMixin(object):
kwargs
)
def
assert_event_emission_count
(
self
,
event_name
,
expected_count
):
"""
Verify that the event with the given name was emitted
a specific number of times.
"""
actual_count
=
0
for
call_args
in
self
.
mock_tracker
.
emit
.
call_args_list
:
if
call_args
[
0
][
0
]
==
event_name
:
actual_count
+=
1
self
.
assertEqual
(
actual_count
,
expected_count
)
def
reset_tracker
(
self
):
"""
Reset the mock tracker in order to forget about old events.
"""
self
.
mock_tracker
.
reset_mock
()
def
get_latest_call_args
(
self
):
"""
Return the arguments of the latest call to emit.
"""
return
self
.
mock_tracker
.
emit
.
call_args
[
0
]
class
PatchMediaTypeMixin
(
object
):
"""
...
...
lms/djangoapps/discussion/tests/test_views.py
View file @
fade4a10
...
...
@@ -4,15 +4,19 @@ from datetime import datetime
import
ddt
from
django.core.urlresolvers
import
reverse
from
django.http
import
Http404
from
django.http
import
Http
Response
,
Http
404
from
django.test.client
import
Client
,
RequestFactory
from
django.test.utils
import
override_settings
from
django.utils
import
translation
from
mock
import
ANY
,
Mock
,
call
,
patch
from
nose.tools
import
assert_true
from
rest_framework.test
import
APIRequestFactory
from
common.test.utils
import
MockSignalHandlerMixin
,
disable_signal
from
course_modes.models
import
CourseMode
from
course_modes.tests.factories
import
CourseModeFactory
from
discussion_api
import
api
from
discussion_api.tests.utils
import
CommentsServiceMockMixin
,
make_minimal_cs_thread
from
django_comment_client.constants
import
TYPE_ENTRY
,
TYPE_SUBCATEGORY
from
django_comment_client.permissions
import
get_team
from
django_comment_client.tests.group_id
import
(
...
...
@@ -20,6 +24,7 @@ from django_comment_client.tests.group_id import (
GroupIdAssertionMixin
,
NonCohortedTopicGroupIdTestMixin
)
from
django_comment_client.base.views
import
create_thread
from
django_comment_client.tests.unicode
import
UnicodeTestMixin
from
django_comment_client.tests.utils
import
(
CohortedTestCase
,
...
...
@@ -28,21 +33,27 @@ from django_comment_client.tests.utils import (
topic_name_to_id
)
from
django_comment_client.utils
import
strip_none
from
django_comment_common.models
import
CourseDiscussionSettings
,
ForumsConfig
from
django_comment_common.utils
import
ThreadContext
from
django_comment_common.models
import
(
CourseDiscussionSettings
,
ForumsConfig
,
FORUM_ROLE_STUDENT
,
Role
)
from
django_comment_common.utils
import
ThreadContext
,
seed_permissions_roles
from
lms.djangoapps.courseware.exceptions
import
CourseAccessRedirect
from
lms.djangoapps.discussion
import
views
from
lms.djangoapps.discussion.views
import
_get_discussion_default_topic_id
from
lms.djangoapps.discussion.views
import
course_discussions_settings_handler
from
lms.djangoapps.teams.tests.factories
import
CourseTeamFactory
from
lms.djangoapps.teams.tests.factories
import
CourseTeamFactory
,
CourseTeamMembershipFactory
from
lms.lib.comment_client.utils
import
CommentClientPaginatedResult
from
openedx.core.djangoapps.course_groups.models
import
CourseUserGroup
from
openedx.core.djangoapps.course_groups.tests.helpers
import
config_course_cohorts
from
openedx.core.djangoapps.course_groups.tests.test_views
import
CohortViewsTestCase
from
openedx.core.djangoapps.util.testing
import
ContentGroupTestCase
from
openedx.features.enterprise_support.tests.mixins.enterprise
import
EnterpriseTestConsentRequired
from
student.tests.factories
import
CourseEnrollmentFactory
,
UserFactory
from
util.testing
import
UrlResetMixin
from
student.roles
import
CourseStaffRole
,
UserBasedRole
from
student.tests.factories
import
CourseAccessRoleFactory
,
CourseEnrollmentFactory
,
UserFactory
from
util.testing
import
EventTestMixin
,
UrlResetMixin
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.tests.django_utils
import
(
...
...
@@ -167,7 +178,7 @@ def make_mock_thread_data(
return
thread_data
def
make_mock_request_impl
(
def
make_mock_
perform_
request_impl
(
course
,
text
,
thread_id
=
"dummy_thread_id"
,
...
...
@@ -175,11 +186,10 @@ def make_mock_request_impl(
commentable_id
=
None
,
num_thread_responses
=
1
,
):
def
mock_request_impl
(
*
args
,
**
kwargs
):
def
mock_
perform_
request_impl
(
*
args
,
**
kwargs
):
url
=
args
[
1
]
data
=
None
if
url
.
endswith
(
"threads"
)
or
url
.
endswith
(
"user_profile"
):
data
=
{
return
{
"collection"
:
[
make_mock_thread_data
(
course
=
course
,
...
...
@@ -192,7 +202,7 @@ def make_mock_request_impl(
]
}
elif
thread_id
and
url
.
endswith
(
thread_id
):
data
=
make_mock_thread_data
(
return
make_mock_thread_data
(
course
=
course
,
text
=
text
,
thread_id
=
thread_id
,
...
...
@@ -201,7 +211,7 @@ def make_mock_request_impl(
commentable_id
=
commentable_id
)
elif
"/users/"
in
url
:
data
=
{
res
=
{
"default_sort_key"
:
"date"
,
"upvoted_ids"
:
[],
"downvoted_ids"
:
[],
...
...
@@ -209,13 +219,39 @@ def make_mock_request_impl(
}
# comments service adds these attributes when course_id param is present
if
kwargs
.
get
(
'params'
,
{})
.
get
(
'course_id'
):
data
.
update
({
res
.
update
({
"threads_count"
:
1
,
"comments_count"
:
2
})
return
res
else
:
return
None
return
mock_perform_request_impl
def
make_mock_request_impl
(
course
,
text
,
thread_id
=
"dummy_thread_id"
,
group_id
=
None
,
commentable_id
=
None
,
num_thread_responses
=
1
,
):
impl
=
make_mock_perform_request_impl
(
course
,
text
,
thread_id
=
thread_id
,
group_id
=
group_id
,
commentable_id
=
commentable_id
,
num_thread_responses
=
num_thread_responses
)
def
mock_request_impl
(
*
args
,
**
kwargs
):
data
=
impl
(
*
args
,
**
kwargs
)
if
data
:
return
Mock
(
status_code
=
200
,
text
=
json
.
dumps
(
data
),
json
=
Mock
(
return_value
=
data
))
return
Mock
(
status_code
=
404
)
else
:
return
Mock
(
status_code
=
404
)
return
mock_request_impl
...
...
@@ -370,18 +406,18 @@ class SingleThreadQueryCountTestCase(ForumsEnableMixin, ModuleStoreTestCase):
# course is outside the context manager that is verifying the number of queries,
# and with split mongo, that method ends up querying disabled_xblocks (which is then
# cached and hence not queried as part of call_single_thread).
(
ModuleStoreEnum
.
Type
.
mongo
,
False
,
1
,
5
,
3
,
14
,
1
),
(
ModuleStoreEnum
.
Type
.
mongo
,
False
,
50
,
5
,
3
,
14
,
1
),
(
ModuleStoreEnum
.
Type
.
mongo
,
False
,
1
,
6
,
4
,
17
,
4
),
(
ModuleStoreEnum
.
Type
.
mongo
,
False
,
50
,
6
,
4
,
17
,
4
),
# split mongo: 3 queries, regardless of thread response size.
(
ModuleStoreEnum
.
Type
.
split
,
False
,
1
,
3
,
3
,
1
3
,
1
),
(
ModuleStoreEnum
.
Type
.
split
,
False
,
50
,
3
,
3
,
1
3
,
1
),
(
ModuleStoreEnum
.
Type
.
split
,
False
,
1
,
3
,
3
,
1
6
,
4
),
(
ModuleStoreEnum
.
Type
.
split
,
False
,
50
,
3
,
3
,
1
6
,
4
),
# Enabling Enterprise integration should have no effect on the number of mongo queries made.
(
ModuleStoreEnum
.
Type
.
mongo
,
True
,
1
,
5
,
3
,
14
,
1
),
(
ModuleStoreEnum
.
Type
.
mongo
,
True
,
50
,
5
,
3
,
14
,
1
),
(
ModuleStoreEnum
.
Type
.
mongo
,
True
,
1
,
6
,
4
,
17
,
4
),
(
ModuleStoreEnum
.
Type
.
mongo
,
True
,
50
,
6
,
4
,
17
,
4
),
# split mongo: 3 queries, regardless of thread response size.
(
ModuleStoreEnum
.
Type
.
split
,
True
,
1
,
3
,
3
,
1
3
,
1
),
(
ModuleStoreEnum
.
Type
.
split
,
True
,
50
,
3
,
3
,
1
3
,
1
),
(
ModuleStoreEnum
.
Type
.
split
,
True
,
1
,
3
,
3
,
1
6
,
4
),
(
ModuleStoreEnum
.
Type
.
split
,
True
,
50
,
3
,
3
,
1
6
,
4
),
)
@ddt.unpack
def
test_number_of_mongo_queries
(
...
...
@@ -1917,3 +1953,82 @@ class DefaultTopicIdGetterTestCase(ModuleStoreTestCase):
expected_id
=
'another_discussion_id'
result
=
_get_discussion_default_topic_id
(
course
)
self
.
assertEqual
(
expected_id
,
result
)
class
ThreadViewedEventTestCase
(
EventTestMixin
,
ForumsEnableMixin
,
UrlResetMixin
,
ModuleStoreTestCase
):
"""
Forum thread views are expected to launch analytics events. Test these here.
"""
CATEGORY_ID
=
'i4x-edx-discussion-id'
CATEGORY_NAME
=
'Discussion 1'
PARENT_CATEGORY_NAME
=
'Chapter 1'
DUMMY_THREAD_ID
=
'dummythreadids'
DUMMY_TITLE
=
'Dummy title'
DUMMY_URL
=
'https://example.com/dummy/url/'
@patch.dict
(
"django.conf.settings.FEATURES"
,
{
"ENABLE_DISCUSSION_SERVICE"
:
True
})
def
setUp
(
self
):
super
(
ThreadViewedEventTestCase
,
self
)
.
setUp
(
'eventtracking.tracker'
)
self
.
course
=
CourseFactory
.
create
()
seed_permissions_roles
(
self
.
course
.
id
)
PASSWORD
=
'test'
self
.
student
=
UserFactory
.
create
(
password
=
PASSWORD
)
CourseEnrollmentFactory
(
user
=
self
.
student
,
course_id
=
self
.
course
.
id
)
self
.
staff
=
UserFactory
.
create
(
is_staff
=
True
)
UserBasedRole
(
user
=
self
.
staff
,
role
=
CourseStaffRole
.
ROLE
)
.
add_course
(
self
.
course
.
id
)
self
.
category
=
ItemFactory
.
create
(
parent_location
=
self
.
course
.
location
,
category
=
'discussion'
,
discussion_id
=
self
.
CATEGORY_ID
,
discussion_category
=
self
.
PARENT_CATEGORY_NAME
,
discussion_target
=
self
.
CATEGORY_NAME
,
)
self
.
team
=
CourseTeamFactory
.
create
(
name
=
'Team 1'
,
course_id
=
self
.
course
.
id
,
topic_id
=
'arbitrary-topic-id'
,
discussion_topic_id
=
self
.
category
.
discussion_id
,
)
CourseTeamMembershipFactory
.
create
(
team
=
self
.
team
,
user
=
self
.
student
)
self
.
client
.
login
(
username
=
self
.
student
.
username
,
password
=
PASSWORD
)
@patch.dict
(
"django.conf.settings.FEATURES"
,
{
"ENABLE_DISCUSSION_SERVICE"
:
True
})
@patch
(
'lms.lib.comment_client.utils.perform_request'
)
def
test_thread_viewed_event
(
self
,
mock_perform_request
):
mock_perform_request
.
side_effect
=
make_mock_perform_request_impl
(
course
=
self
.
course
,
text
=
self
.
DUMMY_TITLE
,
thread_id
=
self
.
DUMMY_THREAD_ID
,
commentable_id
=
self
.
category
.
discussion_id
,
)
url
=
'/courses/{0}/discussion/forum/{1}/threads/{2}'
.
format
(
unicode
(
self
.
course
.
id
),
self
.
category
.
discussion_id
,
self
.
DUMMY_THREAD_ID
)
self
.
client
.
get
(
url
,
HTTP_X_REQUESTED_WITH
=
'XMLHttpRequest'
)
expected_event
=
{
'id'
:
self
.
DUMMY_THREAD_ID
,
'title'
:
self
.
DUMMY_TITLE
,
'commentable_id'
:
self
.
category
.
discussion_id
,
'category_id'
:
self
.
category
.
discussion_id
,
'category_name'
:
self
.
category
.
discussion_target
,
'user_forums_roles'
:
[
FORUM_ROLE_STUDENT
],
'user_course_roles'
:
[],
'target_username'
:
self
.
student
.
username
,
'team_id'
:
self
.
team
.
id
,
'url'
:
self
.
DUMMY_URL
,
}
expected_event_items
=
expected_event
.
items
()
self
.
assert_event_emission_count
(
'edx.forum.thread.viewed'
,
1
)
_
,
event
=
self
.
get_latest_call_args
()
event_items
=
event
.
items
()
self
.
assertTrue
(
kv_pair
in
event_items
for
kv_pair
in
expected_event_items
)
lms/djangoapps/discussion/views.py
View file @
fade4a10
...
...
@@ -28,6 +28,7 @@ import lms.lib.comment_client as cc
from
courseware.access
import
has_access
from
courseware.courses
import
get_course_with_access
from
courseware.views.views
import
CourseTabView
from
django_comment_client.base.views
import
track_thread_viewed_event
from
django_comment_client.constants
import
TYPE_ENTRY
from
django_comment_client.permissions
import
get_team
,
has_permission
from
django_comment_client.utils
import
(
...
...
@@ -291,10 +292,13 @@ def single_thread(request, course_key, discussion_id, thread_id):
cc_user
=
cc
.
User
.
from_django_user
(
request
.
user
)
user_info
=
cc_user
.
to_dict
()
is_staff
=
has_permission
(
request
.
user
,
'openclose_thread'
,
course
.
id
)
thread
=
_find_thread
(
request
,
course
,
discussion_id
=
discussion_id
,
thread_id
=
thread_id
)
if
not
thread
:
raise
Http404
thread
=
_load_thread_for_viewing
(
request
,
course
,
discussion_id
=
discussion_id
,
thread_id
=
thread_id
,
raise_event
=
True
,
)
with
newrelic_function_trace
(
"get_annotated_content_infos"
):
annotated_content_info
=
utils
.
get_annotated_content_infos
(
...
...
@@ -358,6 +362,34 @@ def _find_thread(request, course, discussion_id, thread_id):
return
thread
def
_load_thread_for_viewing
(
request
,
course
,
discussion_id
,
thread_id
,
raise_event
):
"""
Loads the discussion thread with the specified ID and fires an
edx.forum.thread.viewed event.
Args:
request: The Django request.
course_id: The ID of the owning course.
discussion_id: The ID of the owning discussion.
thread_id: The ID of the thread.
raise_event: Whether an edx.forum.thread.viewed tracking event should
be raised
Returns:
The thread in question if the user can see it.
Raises:
Http404 if the thread does not exist or the user cannot
see it.
"""
thread
=
_find_thread
(
request
,
course
,
discussion_id
=
discussion_id
,
thread_id
=
thread_id
)
if
not
thread
:
raise
Http404
if
raise_event
:
track_thread_viewed_event
(
request
,
course
,
thread
)
return
thread
def
_create_base_discussion_view_context
(
request
,
course_key
):
"""
Returns the default template context for rendering any discussion view.
...
...
@@ -393,20 +425,20 @@ def _get_discussion_default_topic_id(course):
return
entry
[
'id'
]
def
_create_discussion_board_context
(
request
,
course_key
,
discussion_id
=
None
,
thread_i
d
=
None
):
def
_create_discussion_board_context
(
request
,
base_context
,
threa
d
=
None
):
"""
Returns the template context for rendering the discussion board.
"""
context
=
_create_base_discussion_view_context
(
request
,
course_key
)
context
=
base_context
.
copy
(
)
course
=
context
[
'course'
]
course_key
=
course
.
id
thread_id
=
thread
.
id
if
thread
else
None
discussion_id
=
thread
.
commentable_id
if
thread
else
None
course_settings
=
context
[
'course_settings'
]
user
=
context
[
'user'
]
cc_user
=
cc
.
User
.
from_django_user
(
user
)
user_info
=
context
[
'user_info'
]
if
thread_id
:
thread
=
_find_thread
(
request
,
course
,
discussion_id
=
discussion_id
,
thread_id
=
thread_id
)
if
not
thread
:
raise
Http404
if
thread
:
# Since we're in page render mode, and the discussions UI will request the thread list itself,
# we need only return the thread information for this one.
...
...
@@ -637,12 +669,25 @@ class DiscussionBoardFragmentView(EdxFragmentView):
"""
course_key
=
CourseKey
.
from_string
(
course_id
)
try
:
context
=
_create_discussion_board_context
(
request
,
course_key
,
discussion_id
=
discussion_id
,
thread_id
=
thread_id
,
base_context
=
_create_base_discussion_view_context
(
request
,
course_key
)
# Note:
# After the thread is rendered in this fragment, an AJAX
# request is made and the thread is completely loaded again
# (yes, this is something to fix). Because of this, we pass in
# raise_event=False to _load_thread_for_viewing avoid duplicate
# tracking events.
thread
=
(
_load_thread_for_viewing
(
request
,
base_context
[
'course'
],
discussion_id
=
discussion_id
,
thread_id
=
thread_id
,
raise_event
=
False
,
)
if
thread_id
else
None
)
context
=
_create_discussion_board_context
(
request
,
base_context
,
thread
=
thread
)
html
=
render_to_string
(
'discussion/discussion_board_fragment.html'
,
context
)
inline_js
=
render_to_string
(
'discussion/discussion_board_js.template'
,
context
)
...
...
lms/djangoapps/django_comment_client/base/__init__.py
View file @
fade4a10
# This import registers the ForumThreadViewedEventTransformer
import
event_transformers
# pylint: disable=unused-import
lms/djangoapps/django_comment_client/base/event_transformers.py
0 → 100644
View file @
fade4a10
"""
Transformers for Discussion-related events.
"""
from
django.contrib.auth.models
import
User
from
django.core.urlresolvers
import
reverse
,
NoReverseMatch
from
eventtracking.processors.exceptions
import
EventEmissionExit
from
opaque_keys
import
InvalidKeyError
from
opaque_keys.edx.locator
import
CourseLocator
from
django_comment_client.base.views
import
add_truncated_title_to_event_data
from
django_comment_client.permissions
import
get_team
from
django_comment_client.utils
import
get_cached_discussion_id_map_by_course_id
from
track.transformers
import
EventTransformer
,
EventTransformerRegistry
from
track.views.segmentio
import
(
BI_SCREEN_VIEWED_EVENT_NAME
,
FORUM_THREAD_VIEWED_EVENT_LABEL
)
def
_get_string
(
dictionary
,
key
,
del_if_bad
=
True
):
"""
Get a string from a dictionary by key.
If the key is not in the dictionary or does not refer to a string:
- Return None
- Optionally delete the key (del_if_bad)
"""
if
key
in
dictionary
:
value
=
dictionary
[
key
]
if
isinstance
(
value
,
basestring
):
return
value
else
:
if
del_if_bad
:
del
dictionary
[
key
]
return
None
else
:
return
None
@EventTransformerRegistry.register
class
ForumThreadViewedEventTransformer
(
EventTransformer
):
"""
Transformer for forum-thread-viewed mobile navigation events.
"""
match_key
=
BI_SCREEN_VIEWED_EVENT_NAME
def
process_event
(
self
):
"""
Process incoming mobile navigation events.
For forum-thread-viewed events, change their names to
edx.forum.thread.viewed and manipulate their data to conform with
edx.forum.thread.viewed event design.
Throw out other events.
"""
# Get event context dict
# Throw out event if context nonexistent or wrong type
context
=
self
.
get
(
'context'
)
if
not
isinstance
(
context
,
dict
):
raise
EventEmissionExit
()
# Throw out event if it's not a forum thread view
if
_get_string
(
context
,
'label'
,
del_if_bad
=
False
)
!=
FORUM_THREAD_VIEWED_EVENT_LABEL
:
raise
EventEmissionExit
()
# Change name and event type
self
[
'name'
]
=
'edx.forum.thread.viewed'
self
[
'event_type'
]
=
self
[
'name'
]
# If no event data, set it to an empty dict
if
'event'
not
in
self
:
self
[
'event'
]
=
{}
self
.
event
=
{}
# Throw out the context dict within the event data
# (different from the context dict extracted above)
if
'context'
in
self
.
event
:
del
self
.
event
[
'context'
]
# Parse out course key
course_id_string
=
_get_string
(
context
,
'course_id'
)
if
context
else
None
course_id
=
None
if
course_id_string
:
try
:
course_id
=
CourseLocator
.
from_string
(
course_id_string
)
except
InvalidKeyError
:
pass
# Change 'thread_id' field to 'id'
thread_id
=
_get_string
(
self
.
event
,
'thread_id'
)
if
thread_id
:
del
self
.
event
[
'thread_id'
]
self
.
event
[
'id'
]
=
thread_id
# Change 'topic_id' to 'commentable_id'
commentable_id
=
_get_string
(
self
.
event
,
'topic_id'
)
if
commentable_id
:
del
self
.
event
[
'topic_id'
]
self
.
event
[
'commentable_id'
]
=
commentable_id
# Change 'action' to 'title' and truncate
title
=
_get_string
(
self
.
event
,
'action'
)
if
title
is
not
None
:
del
self
.
event
[
'action'
]
add_truncated_title_to_event_data
(
self
.
event
,
title
)
# Change 'author' to 'target_username'
author
=
_get_string
(
self
.
event
,
'author'
)
if
author
is
not
None
:
del
self
.
event
[
'author'
]
self
.
event
[
'target_username'
]
=
author
# Load user
username
=
_get_string
(
self
,
'username'
)
user
=
None
if
username
:
try
:
user
=
User
.
objects
.
get
(
username
=
username
)
except
User
.
DoesNotExist
:
pass
# If in a category, add category name and ID
if
course_id
and
commentable_id
and
user
:
id_map
=
get_cached_discussion_id_map_by_course_id
(
course_id
,
[
commentable_id
],
user
)
if
commentable_id
in
id_map
:
self
.
event
[
'category_name'
]
=
id_map
[
commentable_id
][
'title'
]
self
.
event
[
'category_id'
]
=
commentable_id
# Add thread URL
if
course_id
and
commentable_id
and
thread_id
:
url_kwargs
=
{
'course_id'
:
course_id_string
,
'discussion_id'
:
commentable_id
,
'thread_id'
:
thread_id
}
try
:
self
.
event
[
'url'
]
=
reverse
(
'single_thread'
,
kwargs
=
url_kwargs
)
except
NoReverseMatch
:
pass
# Add user's forum and course roles
if
course_id
and
user
:
self
.
event
[
'user_forums_roles'
]
=
[
role
.
name
for
role
in
user
.
roles
.
filter
(
course_id
=
course_id
)
]
self
.
event
[
'user_course_roles'
]
=
[
role
.
role
for
role
in
user
.
courseaccessrole_set
.
filter
(
course_id
=
course_id
)
]
# Add team ID
if
commentable_id
:
team
=
get_team
(
commentable_id
)
if
team
:
self
.
event
[
'team_id'
]
=
team
.
team_id
lms/djangoapps/django_comment_client/base/tests.py
View file @
fade4a10
...
...
@@ -2,6 +2,7 @@
"""Tests for django comment client views."""
import
json
import
logging
import
mock
from
contextlib
import
contextmanager
import
ddt
...
...
@@ -9,6 +10,7 @@ from django.contrib.auth.models import User
from
django.core.management
import
call_command
from
django.core.urlresolvers
import
reverse
from
django.test.client
import
RequestFactory
from
eventtracking.processors.exceptions
import
EventEmissionExit
from
mock
import
ANY
,
Mock
,
patch
from
nose.plugins.attrib
import
attr
from
nose.tools
import
assert_equal
,
assert_true
...
...
@@ -25,18 +27,34 @@ from django_comment_client.tests.group_id import (
)
from
django_comment_client.tests.unicode
import
UnicodeTestMixin
from
django_comment_client.tests.utils
import
CohortedTestCase
,
ForumsEnableMixin
from
django_comment_common.models
import
CourseDiscussionSettings
,
Role
,
assign_role
from
django_comment_common.models
import
(
assign_role
,
CourseDiscussionSettings
,
FORUM_ROLE_ADMINISTRATOR
,
FORUM_ROLE_STUDENT
,
Role
)
from
django_comment_common.utils
import
ThreadContext
,
seed_permissions_roles
,
set_course_discussion_settings
from
lms.djangoapps.teams.tests.factories
import
CourseTeamFactory
,
CourseTeamMembershipFactory
from
lms.lib.comment_client
import
Thread
from
openedx.core.djangoapps.course_groups.cohorts
import
set_course_cohorted
from
openedx.core.djangoapps.course_groups.tests.helpers
import
CohortFactory
from
student.roles
import
CourseStaffRole
,
UserBasedRole
from
student.tests.factories
import
CourseAccessRoleFactory
,
CourseEnrollmentFactory
,
UserFactory
from
util.testing
import
UrlResetMixin
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
,
SharedModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
,
check_mongo_calls
from
track.middleware
import
TrackMiddleware
from
track.views
import
segmentio
from
track.views.tests.base
import
(
SegmentIOTrackingTestCaseBase
,
SEGMENTIO_TEST_USER_ID
)
from
event_transformers
import
ForumThreadViewedEventTransformer
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -1734,7 +1752,7 @@ class ForumEventTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockReque
@patch
(
'eventtracking.tracker.emit'
)
@patch
(
'lms.lib.comment_client.utils.requests.request'
,
autospec
=
True
)
def
test_thread_event
(
self
,
__
,
mock_emit
):
def
test_thread_
created_
event
(
self
,
__
,
mock_emit
):
request
=
RequestFactory
()
.
post
(
"dummy_url"
,
{
"thread_type"
:
"discussion"
,
...
...
@@ -1983,3 +2001,329 @@ class UsersEndpointTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockRe
response
=
self
.
make_request
(
username
=
"other"
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
json
.
loads
(
response
.
content
)[
"users"
],
[])
@ddt.ddt
class
SegmentIOForumThreadViewedEventTestCase
(
SegmentIOTrackingTestCaseBase
):
def
_raise_navigation_event
(
self
,
label
,
include_name
):
middleware
=
TrackMiddleware
()
kwargs
=
{
'label'
:
label
}
if
include_name
:
kwargs
[
'name'
]
=
'edx.bi.app.navigation.screen'
else
:
kwargs
[
'exclude_name'
]
=
True
request
=
self
.
create_request
(
data
=
self
.
create_segmentio_event_json
(
**
kwargs
),
content_type
=
'application/json'
,
)
User
.
objects
.
create
(
pk
=
SEGMENTIO_TEST_USER_ID
,
username
=
str
(
mock
.
sentinel
.
username
))
middleware
.
process_request
(
request
)
try
:
response
=
segmentio
.
segmentio_event
(
request
)
self
.
assertEquals
(
response
.
status_code
,
200
)
finally
:
middleware
.
process_response
(
request
,
None
)
@ddt.data
(
True
,
False
)
def
test_thread_viewed
(
self
,
include_name
):
"""
Tests that a SegmentIO thread viewed event is accepted and transformed.
Only tests that the transformation happens at all; does not
comprehensively test that it happens correctly.
ForumThreadViewedEventTransformerTestCase tests for correctness.
"""
self
.
_raise_navigation_event
(
'Forum: View Thread'
,
include_name
)
event
=
self
.
get_event
()
self
.
assertEqual
(
event
[
'name'
],
'edx.forum.thread.viewed'
)
self
.
assertEqual
(
event
[
'event_type'
],
event
[
'name'
])
@ddt.data
(
True
,
False
)
def
test_non_thread_viewed
(
self
,
include_name
):
"""
Tests that other BI events are thrown out.
"""
self
.
_raise_navigation_event
(
'Forum: Create Thread'
,
include_name
)
self
.
assert_no_events_emitted
()
def
_get_transformed_event
(
input_event
):
transformer
=
ForumThreadViewedEventTransformer
(
**
input_event
)
transformer
.
transform
()
return
transformer
def
_create_event
(
label
=
'Forum: View Thread'
,
include_context
=
True
,
inner_context
=
None
,
username
=
None
,
course_id
=
None
,
**
event_data
):
result
=
{
'name'
:
'edx.bi.app.navigation.screen'
}
if
include_context
:
result
[
'context'
]
=
{
'label'
:
label
}
if
course_id
:
result
[
'context'
][
'course_id'
]
=
str
(
course_id
)
if
username
:
result
[
'username'
]
=
username
if
event_data
:
result
[
'event'
]
=
event_data
if
inner_context
:
if
not
event_data
:
result
[
'event'
]
=
{}
result
[
'event'
][
'context'
]
=
inner_context
return
result
def
_create_and_transform_event
(
**
kwargs
):
event
=
_create_event
(
**
kwargs
)
return
event
,
_get_transformed_event
(
event
)
@ddt.ddt
class
ForumThreadViewedEventTransformerTestCase
(
ForumsEnableMixin
,
UrlResetMixin
,
ModuleStoreTestCase
):
"""
Test that the ForumThreadViewedEventTransformer transforms events correctly
and without raising exceptions.
Because the events passed through the transformer can come from external
sources (e.g., a mobile app), we carefully test a myriad of cases, including
those with incomplete and malformed events.
"""
CATEGORY_ID
=
'i4x-edx-discussion-id'
CATEGORY_NAME
=
'Discussion 1'
PARENT_CATEGORY_NAME
=
'Chapter 1'
TEAM_CATEGORY_ID
=
'i4x-edx-team-discussion-id'
TEAM_CATEGORY_NAME
=
'Team Chat'
TEAM_PARENT_CATEGORY_NAME
=
PARENT_CATEGORY_NAME
DUMMY_CATEGORY_ID
=
'i4x-edx-dummy-commentable-id'
DUMMY_THREAD_ID
=
'dummy_thread_id'
@mock.patch.dict
(
"student.models.settings.FEATURES"
,
{
"ENABLE_DISCUSSION_SERVICE"
:
True
})
def
setUp
(
self
):
super
(
ForumThreadViewedEventTransformerTestCase
,
self
)
.
setUp
()
self
.
courses_by_store
=
{
ModuleStoreEnum
.
Type
.
mongo
:
CourseFactory
.
create
(
org
=
'TestX'
,
course
=
'TR-101'
,
run
=
'Event_Transform_Test'
,
default_store
=
ModuleStoreEnum
.
Type
.
mongo
,
),
ModuleStoreEnum
.
Type
.
split
:
CourseFactory
.
create
(
org
=
'TestX'
,
course
=
'TR-101S'
,
run
=
'Event_Transform_Test_Split'
,
default_store
=
ModuleStoreEnum
.
Type
.
split
,
),
}
self
.
course
=
self
.
courses_by_store
[
'mongo'
]
self
.
student
=
UserFactory
.
create
()
self
.
staff
=
UserFactory
.
create
(
is_staff
=
True
)
UserBasedRole
(
user
=
self
.
staff
,
role
=
CourseStaffRole
.
ROLE
)
.
add_course
(
self
.
course
.
id
)
CourseEnrollmentFactory
.
create
(
user
=
self
.
student
,
course_id
=
self
.
course
.
id
)
self
.
category
=
ItemFactory
.
create
(
parent_location
=
self
.
course
.
location
,
category
=
'discussion'
,
discussion_id
=
self
.
CATEGORY_ID
,
discussion_category
=
self
.
PARENT_CATEGORY_NAME
,
discussion_target
=
self
.
CATEGORY_NAME
,
)
self
.
team_category
=
ItemFactory
.
create
(
parent_location
=
self
.
course
.
location
,
category
=
'discussion'
,
discussion_id
=
self
.
TEAM_CATEGORY_ID
,
discussion_category
=
self
.
TEAM_PARENT_CATEGORY_NAME
,
discussion_target
=
self
.
TEAM_CATEGORY_NAME
,
)
self
.
team
=
CourseTeamFactory
.
create
(
name
=
'Team 1'
,
course_id
=
self
.
course
.
id
,
topic_id
=
'arbitrary-topic-id'
,
discussion_topic_id
=
self
.
team_category
.
discussion_id
,
)
def
test_missing_context
(
self
):
event
=
_create_event
(
include_context
=
False
)
with
self
.
assertRaises
(
EventEmissionExit
):
_get_transformed_event
(
event
)
def
test_no_data
(
self
):
event
,
event_trans
=
_create_and_transform_event
()
event
[
'name'
]
=
'edx.forum.thread.viewed'
event
[
'event_type'
]
=
event
[
'name'
]
event
[
'event'
]
=
{}
self
.
assertDictEqual
(
event_trans
,
event
)
def
test_inner_context
(
self
):
_
,
event_trans
=
_create_and_transform_event
(
inner_context
=
{})
self
.
assertNotIn
(
'context'
,
event_trans
[
'event'
])
def
test_non_thread_view
(
self
):
event
=
_create_event
(
label
=
'Forum: Create Thread'
,
course_id
=
self
.
course
.
id
,
topic_id
=
self
.
DUMMY_CATEGORY_ID
,
thread_id
=
self
.
DUMMY_THREAD_ID
,
)
with
self
.
assertRaises
(
EventEmissionExit
):
_get_transformed_event
(
event
)
def
test_bad_field_types
(
self
):
event
,
event_trans
=
_create_and_transform_event
(
course_id
=
{},
topic_id
=
3
,
thread_id
=
object
(),
action
=
3.14
,
)
event
[
'name'
]
=
'edx.forum.thread.viewed'
event
[
'event_type'
]
=
event
[
'name'
]
self
.
assertDictEqual
(
event_trans
,
event
)
def
test_bad_course_id
(
self
):
event
,
event_trans
=
_create_and_transform_event
(
course_id
=
'non-existent-course-id'
)
event_data
=
event_trans
[
'event'
]
self
.
assertNotIn
(
'category_id'
,
event_data
)
self
.
assertNotIn
(
'category_name'
,
event_data
)
self
.
assertNotIn
(
'url'
,
event_data
)
self
.
assertNotIn
(
'user_forums_roles'
,
event_data
)
self
.
assertNotIn
(
'user_course_roles'
,
event_data
)
def
test_bad_username
(
self
):
event
,
event_trans
=
_create_and_transform_event
(
username
=
'non-existent-username'
)
event_data
=
event_trans
[
'event'
]
self
.
assertNotIn
(
'category_id'
,
event_data
)
self
.
assertNotIn
(
'category_name'
,
event_data
)
self
.
assertNotIn
(
'user_forums_roles'
,
event_data
)
self
.
assertNotIn
(
'user_course_roles'
,
event_data
)
def
test_bad_url
(
self
):
event
,
event_trans
=
_create_and_transform_event
(
course_id
=
self
.
course
.
id
,
topic_id
=
'malformed/commentable/id'
,
thread_id
=
'malformed/thread/id'
,
)
self
.
assertNotIn
(
'url'
,
event_trans
[
'event'
])
def
test_renamed_fields
(
self
):
AUTHOR
=
'joe-the-plumber'
event
,
event_trans
=
_create_and_transform_event
(
course_id
=
self
.
course
.
id
,
topic_id
=
self
.
DUMMY_CATEGORY_ID
,
thread_id
=
self
.
DUMMY_THREAD_ID
,
author
=
AUTHOR
,
)
self
.
assertEqual
(
event_trans
[
'event'
][
'commentable_id'
],
self
.
DUMMY_CATEGORY_ID
)
self
.
assertEqual
(
event_trans
[
'event'
][
'id'
],
self
.
DUMMY_THREAD_ID
)
self
.
assertEqual
(
event_trans
[
'event'
][
'target_username'
],
AUTHOR
)
def
test_titles
(
self
):
# No title
_
,
event_1_trans
=
_create_and_transform_event
()
self
.
assertNotIn
(
'title'
,
event_1_trans
[
'event'
])
self
.
assertNotIn
(
'title_truncated'
,
event_1_trans
[
'event'
])
# Short title
_
,
event_2_trans
=
_create_and_transform_event
(
action
=
'!'
,
)
self
.
assertIn
(
'title'
,
event_2_trans
[
'event'
])
self
.
assertIn
(
'title_truncated'
,
event_2_trans
[
'event'
])
self
.
assertFalse
(
event_2_trans
[
'event'
][
'title_truncated'
])
# Long title
_
,
event_3_trans
=
_create_and_transform_event
(
action
=
(
'covfefe'
*
200
),
)
self
.
assertIn
(
'title'
,
event_3_trans
[
'event'
])
self
.
assertIn
(
'title_truncated'
,
event_3_trans
[
'event'
])
self
.
assertTrue
(
event_3_trans
[
'event'
][
'title_truncated'
])
@ddt.data
(
ModuleStoreEnum
.
Type
.
mongo
,
ModuleStoreEnum
.
Type
.
split
)
def
test_urls
(
self
,
store
):
course
=
self
.
courses_by_store
[
store
]
commentable_id
=
self
.
DUMMY_CATEGORY_ID
thread_id
=
self
.
DUMMY_THREAD_ID
_
,
event_trans
=
_create_and_transform_event
(
course_id
=
course
.
id
,
topic_id
=
commentable_id
,
thread_id
=
thread_id
,
)
expected_path
=
'/courses/{0}/discussion/forum/{1}/threads/{2}'
.
format
(
course
.
id
,
commentable_id
,
thread_id
)
self
.
assertTrue
(
event_trans
[
'event'
]
.
get
(
'url'
)
.
endswith
(
expected_path
))
def
test_categories
(
self
):
# Bad category
_
,
event_trans_1
=
_create_and_transform_event
(
username
=
self
.
student
.
username
,
course_id
=
self
.
course
.
id
,
topic_id
=
'non-existent-category-id'
,
)
self
.
assertNotIn
(
'category_id'
,
event_trans_1
[
'event'
])
self
.
assertNotIn
(
'category_name'
,
event_trans_1
[
'event'
])
# Good category
_
,
event_trans_2
=
_create_and_transform_event
(
username
=
self
.
student
.
username
,
course_id
=
self
.
course
.
id
,
topic_id
=
self
.
category
.
discussion_id
,
)
self
.
assertEqual
(
event_trans_2
[
'event'
]
.
get
(
'category_id'
),
self
.
category
.
discussion_id
)
full_category_name
=
'{0} / {1}'
.
format
(
self
.
category
.
discussion_category
,
self
.
category
.
discussion_target
)
self
.
assertEqual
(
event_trans_2
[
'event'
]
.
get
(
'category_name'
),
full_category_name
)
def
test_roles
(
self
):
# No user
_
,
event_trans_1
=
_create_and_transform_event
(
course_id
=
self
.
course
.
id
,
)
self
.
assertNotIn
(
'user_forums_roles'
,
event_trans_1
[
'event'
])
self
.
assertNotIn
(
'user_course_roles'
,
event_trans_1
[
'event'
])
# Student user
_
,
event_trans_2
=
_create_and_transform_event
(
course_id
=
self
.
course
.
id
,
username
=
self
.
student
.
username
,
)
self
.
assertEqual
(
event_trans_2
[
'event'
]
.
get
(
'user_forums_roles'
),
[
FORUM_ROLE_STUDENT
])
self
.
assertEqual
(
event_trans_2
[
'event'
]
.
get
(
'user_course_roles'
),
[])
# Course staff user
_
,
event_trans_3
=
_create_and_transform_event
(
course_id
=
self
.
course
.
id
,
username
=
self
.
staff
.
username
,
)
self
.
assertEqual
(
event_trans_3
[
'event'
]
.
get
(
'user_forums_roles'
),
[])
self
.
assertEqual
(
event_trans_3
[
'event'
]
.
get
(
'user_course_roles'
),
[
CourseStaffRole
.
ROLE
])
def
test_teams
(
self
):
# No category
_
,
event_trans_1
=
_create_and_transform_event
(
course_id
=
self
.
course
.
id
,
)
self
.
assertNotIn
(
'team_id'
,
event_trans_1
)
# Non-team category
_
,
event_trans_2
=
_create_and_transform_event
(
course_id
=
self
.
course
.
id
,
topic_id
=
self
.
CATEGORY_ID
,
)
self
.
assertNotIn
(
'team_id'
,
event_trans_2
)
# Team category
_
,
event_trans_3
=
_create_and_transform_event
(
course_id
=
self
.
course
.
id
,
topic_id
=
self
.
TEAM_CATEGORY_ID
,
)
self
.
assertEqual
(
event_trans_3
[
'event'
]
.
get
(
'team_id'
),
self
.
team
.
team_id
)
lms/djangoapps/django_comment_client/base/views.py
View file @
fade4a10
...
...
@@ -45,7 +45,7 @@ from django_comment_common.signals import (
thread_voted
)
from
django_comment_common.utils
import
ThreadContext
from
eventtracking
import
tracker
import
eventtracking
from
lms.djangoapps.courseware.exceptions
import
CourseAccessRedirect
from
util.file
import
store_uploaded_file
...
...
@@ -82,7 +82,7 @@ def track_forum_event(request, event_name, course, obj, data, id_map=None):
role
.
role
for
role
in
user
.
courseaccessrole_set
.
filter
(
course_id
=
course
.
id
)
]
tracker
.
emit
(
event_name
,
data
)
eventtracking
.
tracker
.
emit
(
event_name
,
data
)
def
track_created_event
(
request
,
event_name
,
course
,
obj
,
data
):
...
...
@@ -97,7 +97,7 @@ def track_created_event(request, event_name, course, obj, data):
track_forum_event
(
request
,
event_name
,
course
,
obj
,
data
)
def
add_truncated_title_to_event_data
(
event_data
,
full_title
):
def
add_truncated_title_to_event_data
(
event_data
,
full_title
):
# pylint: disable=invalid-name
event_data
[
'title_truncated'
]
=
(
len
(
full_title
)
>
TRACKING_MAX_FORUM_TITLE
)
event_data
[
'title'
]
=
full_title
[:
TRACKING_MAX_FORUM_TITLE
]
...
...
@@ -158,6 +158,19 @@ def track_voted_event(request, course, obj, vote_value, undo_vote=False):
track_forum_event
(
request
,
event_name
,
course
,
obj
,
event_data
)
def
track_thread_viewed_event
(
request
,
course
,
thread
):
"""
Send analytics event for a viewed thread.
"""
event_name
=
_EVENT_NAME_TEMPLATE
.
format
(
obj_type
=
'thread'
,
action_name
=
'viewed'
)
event_data
=
{}
event_data
[
'commentable_id'
]
=
thread
.
commentable_id
if
hasattr
(
thread
,
'username'
):
event_data
[
'target_username'
]
=
thread
.
username
add_truncated_title_to_event_data
(
event_data
,
thread
.
title
)
track_forum_event
(
request
,
event_name
,
course
,
thread
,
event_data
)
def
permitted
(
func
):
"""
View decorator to verify the user is authorized to access this endpoint.
...
...
lms/djangoapps/django_comment_client/utils.py
View file @
fade4a10
...
...
@@ -130,11 +130,19 @@ def get_accessible_discussion_xblocks(course, user, include_all=False): # pylin
Return a list of all valid discussion xblocks in this course that
are accessible to the given user.
"""
all_xblocks
=
modulestore
()
.
get_items
(
course
.
id
,
qualifiers
=
{
'category'
:
'discussion'
},
include_orphans
=
False
)
return
get_accessible_discussion_xblocks_by_course_id
(
course
.
id
,
user
,
include_all
=
include_all
)
def
get_accessible_discussion_xblocks_by_course_id
(
course_id
,
user
,
include_all
=
False
):
# pylint: disable=invalid-name
"""
Return a list of all valid discussion xblocks in this course that
are accessible to the given user.
"""
all_xblocks
=
modulestore
()
.
get_items
(
course_id
,
qualifiers
=
{
'category'
:
'discussion'
},
include_orphans
=
False
)
return
[
xblock
for
xblock
in
all_xblocks
if
has_required_keys
(
xblock
)
and
(
include_all
or
has_access
(
user
,
'load'
,
xblock
,
course
.
id
))
if
has_required_keys
(
xblock
)
and
(
include_all
or
has_access
(
user
,
'load'
,
xblock
,
course
_
id
))
]
...
...
@@ -178,19 +186,27 @@ def get_cached_discussion_id_map(course, discussion_ids, user):
Returns a dict mapping discussion_ids to respective discussion xblock metadata if it is cached and visible to the
user. If not, returns the result of get_discussion_id_map
"""
return
get_cached_discussion_id_map_by_course_id
(
course
.
id
,
discussion_ids
,
user
)
def
get_cached_discussion_id_map_by_course_id
(
course_id
,
discussion_ids
,
user
):
# pylint: disable=invalid-name
"""
Returns a dict mapping discussion_ids to respective discussion xblock metadata if it is cached and visible to the
user. If not, returns the result of get_discussion_id_map
"""
try
:
entries
=
[]
for
discussion_id
in
discussion_ids
:
key
=
get_cached_discussion_key
(
course
.
id
,
discussion_id
)
key
=
get_cached_discussion_key
(
course
_
id
,
discussion_id
)
if
not
key
:
continue
xblock
=
modulestore
()
.
get_item
(
key
)
if
not
(
has_required_keys
(
xblock
)
and
has_access
(
user
,
'load'
,
xblock
,
course
.
id
)):
if
not
(
has_required_keys
(
xblock
)
and
has_access
(
user
,
'load'
,
xblock
,
course
_
id
)):
continue
entries
.
append
(
get_discussion_id_map_entry
(
xblock
))
return
dict
(
entries
)
except
DiscussionIdMapIsNotCached
:
return
get_discussion_id_map
(
course
,
user
)
return
get_discussion_id_map
_by_course_id
(
course_id
,
user
)
def
get_discussion_id_map
(
course
,
user
):
...
...
@@ -198,7 +214,16 @@ def get_discussion_id_map(course, user):
Transform the list of this course's discussion xblocks (visible to a given user) into a dictionary of metadata keyed
by discussion_id.
"""
return
dict
(
map
(
get_discussion_id_map_entry
,
get_accessible_discussion_xblocks
(
course
,
user
)))
return
get_discussion_id_map_by_course_id
(
course
.
id
,
user
)
def
get_discussion_id_map_by_course_id
(
course_id
,
user
):
# pylint: disable=invalid-name
"""
Transform the list of this course's discussion xblocks (visible to a given user) into a dictionary of metadata keyed
by discussion_id.
"""
xblocks
=
get_accessible_discussion_xblocks_by_course_id
(
course_id
,
user
)
return
dict
(
map
(
get_discussion_id_map_entry
,
xblocks
))
def
_filter_unstarted_categories
(
category_map
,
course
):
...
...
lms/envs/common.py
View file @
fade4a10
...
...
@@ -739,7 +739,7 @@ if FEATURES.get('ENABLE_SQL_TRACKING_LOGS'):
TRACKING_SEGMENTIO_WEBHOOK_SECRET
=
None
TRACKING_SEGMENTIO_ALLOWED_TYPES
=
[
'track'
]
TRACKING_SEGMENTIO_DISALLOWED_SUBSTRING_NAMES
=
[
'.bi.'
]
TRACKING_SEGMENTIO_DISALLOWED_SUBSTRING_NAMES
=
[]
TRACKING_SEGMENTIO_SOURCE_MAP
=
{
'analytics-android'
:
'mobile'
,
'analytics-ios'
:
'mobile'
,
...
...
lms/lib/comment_client/thread.py
View file @
fade4a10
...
...
@@ -5,15 +5,7 @@ import settings
import
models
from
eventtracking
import
tracker
from
.utils
import
(
CommentClientPaginatedResult
,
CommentClientRequestError
,
extract
,
merge_dict
,
perform_request
,
strip_blank
,
strip_none
)
import
utils
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -61,15 +53,15 @@ class Thread(models.Model):
default_params
=
{
'page'
:
1
,
'per_page'
:
20
,
'course_id'
:
query_params
[
'course_id'
]}
params
=
merge_dict
(
default_params
,
strip_blank
(
strip_none
(
query_params
)))
params
=
utils
.
merge_dict
(
default_params
,
utils
.
strip_blank
(
utils
.
strip_none
(
query_params
)))
if
query_params
.
get
(
'text'
):
url
=
cls
.
url
(
action
=
'search'
)
else
:
url
=
cls
.
url
(
action
=
'get_all'
,
params
=
extract
(
params
,
'commentable_id'
))
url
=
cls
.
url
(
action
=
'get_all'
,
params
=
utils
.
extract
(
params
,
'commentable_id'
))
if
params
.
get
(
'commentable_id'
):
del
params
[
'commentable_id'
]
response
=
perform_request
(
response
=
utils
.
perform_request
(
'get'
,
url
,
params
,
...
...
@@ -107,7 +99,7 @@ class Thread(models.Model):
)
)
return
CommentClientPaginatedResult
(
return
utils
.
CommentClientPaginatedResult
(
collection
=
response
.
get
(
'collection'
,
[]),
page
=
response
.
get
(
'page'
,
1
),
num_pages
=
response
.
get
(
'num_pages'
,
1
),
...
...
@@ -149,9 +141,9 @@ class Thread(models.Model):
'resp_skip'
:
kwargs
.
get
(
'response_skip'
),
'resp_limit'
:
kwargs
.
get
(
'response_limit'
),
}
request_params
=
strip_none
(
request_params
)
request_params
=
utils
.
strip_none
(
request_params
)
response
=
perform_request
(
response
=
utils
.
perform_request
(
'get'
,
url
,
request_params
,
...
...
@@ -166,9 +158,9 @@ class Thread(models.Model):
elif
voteable
.
type
==
'comment'
:
url
=
_url_for_flag_comment
(
voteable
.
id
)
else
:
raise
CommentClientRequestError
(
"Can only flag/unflag threads or comments"
)
raise
utils
.
CommentClientRequestError
(
"Can only flag/unflag threads or comments"
)
params
=
{
'user_id'
:
user
.
id
}
response
=
perform_request
(
response
=
utils
.
perform_request
(
'put'
,
url
,
params
,
...
...
@@ -183,13 +175,13 @@ class Thread(models.Model):
elif
voteable
.
type
==
'comment'
:
url
=
_url_for_unflag_comment
(
voteable
.
id
)
else
:
raise
CommentClientRequestError
(
"Can only flag/unflag for threads or comments"
)
raise
utils
.
CommentClientRequestError
(
"Can only flag/unflag for threads or comments"
)
params
=
{
'user_id'
:
user
.
id
}
#if you're an admin, when you unflag, remove ALL flags
if
removeAll
:
params
[
'all'
]
=
True
response
=
perform_request
(
response
=
utils
.
perform_request
(
'put'
,
url
,
params
,
...
...
@@ -201,7 +193,7 @@ class Thread(models.Model):
def
pin
(
self
,
user
,
thread_id
):
url
=
_url_for_pin_thread
(
thread_id
)
params
=
{
'user_id'
:
user
.
id
}
response
=
perform_request
(
response
=
utils
.
perform_request
(
'put'
,
url
,
params
,
...
...
@@ -213,7 +205,7 @@ class Thread(models.Model):
def
un_pin
(
self
,
user
,
thread_id
):
url
=
_url_for_un_pin_thread
(
thread_id
)
params
=
{
'user_id'
:
user
.
id
}
response
=
perform_request
(
response
=
utils
.
perform_request
(
'put'
,
url
,
params
,
...
...
lms/lib/comment_client/user.py
View file @
fade4a10
...
...
@@ -3,7 +3,7 @@ import settings
import
models
from
.utils
import
CommentClientPaginatedResult
,
CommentClientRequestError
,
merge_dict
,
perform_request
import
utils
class
User
(
models
.
Model
):
...
...
@@ -36,7 +36,7 @@ class User(models.Model):
Calls cs_comments_service to mark thread as read for the user
"""
params
=
{
'source_type'
:
source
.
type
,
'source_id'
:
source
.
id
}
perform_request
(
utils
.
perform_request
(
'post'
,
_url_for_read
(
self
.
id
),
params
,
...
...
@@ -46,7 +46,7 @@ class User(models.Model):
def
follow
(
self
,
source
):
params
=
{
'source_type'
:
source
.
type
,
'source_id'
:
source
.
id
}
response
=
perform_request
(
response
=
utils
.
perform_request
(
'post'
,
_url_for_subscription
(
self
.
id
),
params
,
...
...
@@ -56,7 +56,7 @@ class User(models.Model):
def
unfollow
(
self
,
source
):
params
=
{
'source_type'
:
source
.
type
,
'source_id'
:
source
.
id
}
response
=
perform_request
(
response
=
utils
.
perform_request
(
'delete'
,
_url_for_subscription
(
self
.
id
),
params
,
...
...
@@ -70,9 +70,9 @@ class User(models.Model):
elif
voteable
.
type
==
'comment'
:
url
=
_url_for_vote_comment
(
voteable
.
id
)
else
:
raise
CommentClientRequestError
(
"Can only vote / unvote for threads or comments"
)
raise
utils
.
CommentClientRequestError
(
"Can only vote / unvote for threads or comments"
)
params
=
{
'user_id'
:
self
.
id
,
'value'
:
value
}
response
=
perform_request
(
response
=
utils
.
perform_request
(
'put'
,
url
,
params
,
...
...
@@ -87,9 +87,9 @@ class User(models.Model):
elif
voteable
.
type
==
'comment'
:
url
=
_url_for_vote_comment
(
voteable
.
id
)
else
:
raise
CommentClientRequestError
(
"Can only vote / unvote for threads or comments"
)
raise
utils
.
CommentClientRequestError
(
"Can only vote / unvote for threads or comments"
)
params
=
{
'user_id'
:
self
.
id
}
response
=
perform_request
(
response
=
utils
.
perform_request
(
'delete'
,
url
,
params
,
...
...
@@ -100,11 +100,11 @@ class User(models.Model):
def
active_threads
(
self
,
query_params
=
{}):
if
not
self
.
course_id
:
raise
CommentClientRequestError
(
"Must provide course_id when retrieving active threads for the user"
)
raise
utils
.
CommentClientRequestError
(
"Must provide course_id when retrieving active threads for the user"
)
url
=
_url_for_user_active_threads
(
self
.
id
)
params
=
{
'course_id'
:
self
.
course_id
.
to_deprecated_string
()}
params
=
merge_dict
(
params
,
query_params
)
response
=
perform_request
(
params
=
utils
.
merge_dict
(
params
,
query_params
)
response
=
utils
.
perform_request
(
'get'
,
url
,
params
,
...
...
@@ -116,11 +116,11 @@ class User(models.Model):
def
subscribed_threads
(
self
,
query_params
=
{}):
if
not
self
.
course_id
:
raise
CommentClientRequestError
(
"Must provide course_id when retrieving subscribed threads for the user"
)
raise
utils
.
CommentClientRequestError
(
"Must provide course_id when retrieving subscribed threads for the user"
)
url
=
_url_for_user_subscribed_threads
(
self
.
id
)
params
=
{
'course_id'
:
self
.
course_id
.
to_deprecated_string
()}
params
=
merge_dict
(
params
,
query_params
)
response
=
perform_request
(
params
=
utils
.
merge_dict
(
params
,
query_params
)
response
=
utils
.
perform_request
(
'get'
,
url
,
params
,
...
...
@@ -128,7 +128,7 @@ class User(models.Model):
metric_tags
=
self
.
_metric_tags
,
paged_results
=
True
)
return
CommentClientPaginatedResult
(
return
utils
.
CommentClientPaginatedResult
(
collection
=
response
.
get
(
'collection'
,
[]),
page
=
response
.
get
(
'page'
,
1
),
num_pages
=
response
.
get
(
'num_pages'
,
1
),
...
...
@@ -144,19 +144,19 @@ class User(models.Model):
if
self
.
attributes
.
get
(
'group_id'
):
retrieve_params
[
'group_id'
]
=
self
.
group_id
try
:
response
=
perform_request
(
response
=
utils
.
perform_request
(
'get'
,
url
,
retrieve_params
,
metric_action
=
'model.retrieve'
,
metric_tags
=
self
.
_metric_tags
,
)
except
CommentClientRequestError
as
e
:
except
utils
.
CommentClientRequestError
as
e
:
if
e
.
status_code
==
404
:
# attempt to gracefully recover from a previous failure
# to sync this user to the comments service.
self
.
save
()
response
=
perform_request
(
response
=
utils
.
perform_request
(
'get'
,
url
,
retrieve_params
,
...
...
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