Commit bfe54744 by E. Kolpakov

Option to export only cohorted discussions

parent 21b47de5
...@@ -53,6 +53,9 @@ class Command(BaseCommand): ...@@ -53,6 +53,9 @@ class Command(BaseCommand):
* thread-type - one of {discussion, question}. Filters discussion participation stats by discussion thread type. * thread-type - one of {discussion, question}. Filters discussion participation stats by discussion thread type.
* end-date - date time in iso8601 format (YYYY-MM-DD hh:mm:ss). Filters discussion participation stats * end-date - date time in iso8601 format (YYYY-MM-DD hh:mm:ss). Filters discussion participation stats
by creation date: no threads/comments/replies created *after* this date is included in calculation by creation date: no threads/comments/replies created *after* this date is included in calculation
FLAGS:
* cohorted_only - only dump cohorted inline discussion threads
* all - Dump all social stats at once into a particular directory. * all - Dump all social stats at once into a particular directory.
Examples: Examples:
...@@ -68,10 +71,13 @@ class Command(BaseCommand): ...@@ -68,10 +71,13 @@ class Command(BaseCommand):
* `./manage.py lms export_discussion_participation <course_key> --end-date=<iso8601 datetime> * `./manage.py lms export_discussion_participation <course_key> --end-date=<iso8601 datetime>
--thread-type=[discussion|question]` - exports discussion participation stats for a course for --thread-type=[discussion|question]` - exports discussion participation stats for a course for
threads/comments/replies created before specified date, including only threads of specified type threads/comments/replies created before specified date, including only threads of specified type
* `./manage.py lms export_discussion_participation <course_key> --cohorted_only` - exports only cohorted discussion
participation stats for a course; output is written to default location (same folder, auto-generated file name)
""" """
THREAD_TYPE_PARAMETER = 'thread_type' THREAD_TYPE_PARAMETER = 'thread_type'
END_DATE_PARAMETER = 'end_date' END_DATE_PARAMETER = 'end_date'
ALL_PARAMETER = 'all' ALL_PARAMETER = 'all'
COHORTED_ONLY_PARAMETER = 'cohorted_only'
args = "<course_id> <output_file_location>" args = "<course_id> <output_file_location>"
...@@ -98,6 +104,12 @@ class Command(BaseCommand): ...@@ -98,6 +104,12 @@ class Command(BaseCommand):
action='store_true', action='store_true',
dest=ALL_PARAMETER, dest=ALL_PARAMETER,
default=False, default=False,
),
make_option(
'--cohorted_only',
action='store_true',
dest=COHORTED_ONLY_PARAMETER,
default=False,
) )
) )
...@@ -164,12 +176,22 @@ class Command(BaseCommand): ...@@ -164,12 +176,22 @@ class Command(BaseCommand):
if not course: if not course:
raise CommandError("Invalid course id: {}".format(course_key)) raise CommandError("Invalid course id: {}".format(course_key))
target_discussion_ids = None
if options.get(self.COHORTED_ONLY_PARAMETER, False):
cohorted_discussions = course.cohort_config.get('cohorted_inline_discussions', None)
if not cohorted_discussions:
raise CommandError("Only cohorted discussions are marked for export, "
"but no cohorted discussions found for the course")
else:
target_discussion_ids = cohorted_discussions
raw_end_date = options.get(self.END_DATE_PARAMETER, None) raw_end_date = options.get(self.END_DATE_PARAMETER, None)
end_date = dateutil.parser.parse(raw_end_date) if raw_end_date else None end_date = dateutil.parser.parse(raw_end_date) if raw_end_date else None
data = Extractor().extract( data = Extractor().extract(
course_key, course_key,
end_date=end_date, end_date=end_date,
thread_type=(options.get(self.THREAD_TYPE_PARAMETER, None)) thread_type=(options.get(self.THREAD_TYPE_PARAMETER, None)),
thread_ids=target_discussion_ids,
) )
filter_str = self._get_filter_string_representation(options) filter_str = self._get_filter_string_representation(options)
...@@ -210,11 +232,13 @@ class Extractor(object): ...@@ -210,11 +232,13 @@ class Extractor(object):
users = CourseEnrollment.users_enrolled_in(course_key) users = CourseEnrollment.users_enrolled_in(course_key)
return {user.id: user for user in users} return {user.id: user for user in users}
def _get_social_stats(self, course_key, end_date=None, thread_type=None): def _get_social_stats(self, course_key, end_date=None, thread_type=None, thread_ids=None):
""" Gets social stats for course with specified filter parameters """ """ Gets social stats for course with specified filter parameters """
return { return {
int(user_id): data for user_id, data int(user_id): data for user_id, data
in User.all_social_stats(str(course_key), end_date=end_date, thread_type=thread_type).iteritems() in User.all_social_stats(
str(course_key), end_date=end_date, thread_type=thread_type, thread_ids=thread_ids
).iteritems()
} }
def _merge_user_data_and_social_stats(self, userdata, social_stats): def _merge_user_data_and_social_stats(self, userdata, social_stats):
...@@ -232,13 +256,14 @@ class Extractor(object): ...@@ -232,13 +256,14 @@ class Extractor(object):
result.append(utils.merge_dict(user_record, stats)) result.append(utils.merge_dict(user_record, stats))
return result return result
def extract(self, course_key, end_date=None, thread_type=None): def extract(self, course_key, end_date=None, thread_type=None, thread_ids=None):
""" Extracts and merges data according to course key and filter parameters """ """ Extracts and merges data according to course key and filter parameters """
users = self._get_users(course_key) users = self._get_users(course_key)
social_stats = self._get_social_stats( social_stats = self._get_social_stats(
course_key, course_key,
end_date=end_date, end_date=end_date,
thread_type=thread_type thread_type=thread_type,
thread_ids=thread_ids
) )
return self._merge_user_data_and_social_stats(users, social_stats) return self._merge_user_data_and_social_stats(users, social_stats)
......
...@@ -19,11 +19,13 @@ from datetime import datetime ...@@ -19,11 +19,13 @@ from datetime import datetime
_target_module = "django_comment_client.management.commands.export_discussion_participation" _target_module = "django_comment_client.management.commands.export_discussion_participation"
_std_parameters_list = ( _std_parameters_list = (
(CourseLocator(org="edX", course="demoX", run="now"), None, None), (CourseLocator(org="edX", course="demoX", run="now"), None, None, None),
(CourseLocator(org="otherX", course="courseX", run="later"), datetime(2015, 2, 12), None), (CourseLocator(org="otherX", course="courseX", run="later"), datetime(2015, 2, 12), None, None),
(CourseLocator(org="NotAX", course="NotADemo", run="anyyear"), None, 'discussion'), (CourseLocator(org="NotAX", course="NotADemo", run="anyyear"), None, 'discussion', None),
(CourseLocator(org="YeaAX", course="YesADemo", run="anyday"), None, 'question'), (CourseLocator(org="YeaAX", course="YesADemo", run="anyday"), None, 'question', None),
(CourseLocator(org="WhatX", course="WhatADemo", run="last_year"), datetime(2014, 3, 17), 'question') (CourseLocator(org="WhatX", course="WhatADemo", run="last_year"), datetime(2014, 3, 17), 'question', None),
(CourseLocator(org="WhatX", course="WhatADemo", run="last_year"), datetime(2014, 3, 17), None, ['123']),
(CourseLocator(org="WhatX", course="WhatADemo", run="last_year"), datetime(2014, 3, 17), 'question', ['1', '2']),
) )
# pylint: enable=invalid-name # pylint: enable=invalid-name
...@@ -44,7 +46,7 @@ class CommandTest(TestCase): ...@@ -44,7 +46,7 @@ class CommandTest(TestCase):
def set_up_default_mocks(self, patched_get_course): def set_up_default_mocks(self, patched_get_course):
""" Sets up default mocks passed via class decorator """ """ Sets up default mocks passed via class decorator """
patched_get_course.return_value = CourseLocator("edX", "demoX", "now") patched_get_course.return_value = mock.Mock(spec=CourseLocator)
# pylint:disable=unused-argument # pylint:disable=unused-argument
def test_handle_given_no_arguments_raises_command_error(self, patched_get_course): def test_handle_given_no_arguments_raises_command_error(self, patched_get_course):
...@@ -150,10 +152,14 @@ class CommandTest(TestCase): ...@@ -150,10 +152,14 @@ class CommandTest(TestCase):
@ddt.unpack @ddt.unpack
@ddt.data(*_std_parameters_list) @ddt.data(*_std_parameters_list)
def test_handle_passes_correct_parameters_to_extractor( def test_handle_passes_correct_parameters_to_extractor(
self, course_key, end_date, thread_type, patched_get_course self, course_key, end_date, thread_type, cohorted_thread_ids, patched_get_course
): ):
""" Tests that when no explicit filename is given data is exported to default location """ """ Tests that when no explicit filename is given data is exported to default location """
self.set_up_default_mocks(patched_get_course) self.set_up_default_mocks(patched_get_course)
if cohorted_thread_ids:
type(patched_get_course.return_value).cohort_config = mock.PropertyMock(
return_value={'cohorted_inline_discussions': cohorted_thread_ids}
)
patched_open = mock.mock_open() patched_open = mock.mock_open()
with mock.patch("{}.open".format(_target_module), patched_open, create=True), \ with mock.patch("{}.open".format(_target_module), patched_open, create=True), \
mock.patch(_target_module + ".Extractor.extract") as patched_extractor: mock.patch(_target_module + ".Extractor.extract") as patched_extractor:
...@@ -161,9 +167,12 @@ class CommandTest(TestCase): ...@@ -161,9 +167,12 @@ class CommandTest(TestCase):
self.command.handle( self.command.handle(
str(course_key), str(course_key),
end_date=end_date.isoformat() if end_date else end_date, end_date=end_date.isoformat() if end_date else end_date,
thread_type=thread_type thread_type=thread_type,
cohorted_only=True if cohorted_thread_ids else False
)
patched_extractor.assert_called_with(
course_key, end_date=end_date, thread_type=thread_type, thread_ids=cohorted_thread_ids
) )
patched_extractor.assert_called_with(course_key, end_date=end_date, thread_type=thread_type)
def _make_user_mock(user_id, username="", email="", first_name="", last_name=""): def _make_user_mock(user_id, username="", email="", first_name="", last_name=""):
...@@ -221,15 +230,17 @@ class ExtractorTest(TestCase): ...@@ -221,15 +230,17 @@ class ExtractorTest(TestCase):
@ddt.unpack @ddt.unpack
@ddt.data(*_std_parameters_list) @ddt.data(*_std_parameters_list)
def test_extract_invokes_correct_data_extraction_methods(self, course_key, end_date, thread_type): def test_extract_invokes_correct_data_extraction_methods(self, course_key, end_date, thread_type, thread_ids):
""" Tests that correct underlying extractors are called with proper arguments """ """ Tests that correct underlying extractors are called with proper arguments """
with mock.patch(_target_module + '.CourseEnrollment.users_enrolled_in') as patched_users_enrolled_in, \ with mock.patch(_target_module + '.CourseEnrollment.users_enrolled_in') as patched_users_enrolled_in, \
mock.patch(_target_module + ".User.all_social_stats") as patched_all_social_stats: mock.patch(_target_module + ".User.all_social_stats") as patched_all_social_stats:
self.extractor.extract(course_key, end_date=end_date, thread_type=thread_type) self.extractor.extract(course_key, end_date=end_date, thread_type=thread_type, thread_ids=thread_ids)
patched_users_enrolled_in.return_value = [] patched_users_enrolled_in.return_value = []
patched_users_enrolled_in.patched_all_social_stats = {} patched_users_enrolled_in.patched_all_social_stats = {}
patched_users_enrolled_in.assert_called_with(course_key) patched_users_enrolled_in.assert_called_with(course_key)
patched_all_social_stats.assert_called_with(str(course_key), end_date=end_date, thread_type=thread_type) patched_all_social_stats.assert_called_with(
str(course_key), end_date=end_date, thread_type=thread_type, thread_ids=thread_ids
)
@ddt.unpack @ddt.unpack
@ddt.data( @ddt.data(
......
...@@ -119,9 +119,9 @@ class User(models.Model): ...@@ -119,9 +119,9 @@ class User(models.Model):
return get_user_social_stats(self.id, self.course_id, end_date=end_date) return get_user_social_stats(self.id, self.course_id, end_date=end_date)
@classmethod @classmethod
def all_social_stats(cls, course_id, end_date=None, thread_type=None): def all_social_stats(cls, course_id, end_date=None, thread_type=None, thread_ids=None):
""" Get social stats for all users participating in a course """ """ Get social stats for all users participating in a course """
return get_user_social_stats('*', course_id, end_date=end_date, thread_type=thread_type) return get_user_social_stats('*', course_id, end_date=end_date, thread_type=thread_type, thread_ids=thread_ids)
def _retrieve(self, *args, **kwargs): def _retrieve(self, *args, **kwargs):
url = self.url(action='get', params=self.attributes) url = self.url(action='get', params=self.attributes)
...@@ -156,7 +156,7 @@ class User(models.Model): ...@@ -156,7 +156,7 @@ class User(models.Model):
self._update_from_response(response) self._update_from_response(response)
def get_user_social_stats(user_id, course_id, end_date=None, thread_type=None): def get_user_social_stats(user_id, course_id, end_date=None, thread_type=None, thread_ids=None):
""" Queries cs_comments_service for social_stats """ """ Queries cs_comments_service for social_stats """
if not course_id: if not course_id:
raise CommentClientRequestError("Must provide course_id when retrieving social stats for the user") raise CommentClientRequestError("Must provide course_id when retrieving social stats for the user")
...@@ -167,9 +167,11 @@ def get_user_social_stats(user_id, course_id, end_date=None, thread_type=None): ...@@ -167,9 +167,11 @@ def get_user_social_stats(user_id, course_id, end_date=None, thread_type=None):
params.update({'end_date': end_date.isoformat()}) params.update({'end_date': end_date.isoformat()})
if thread_type: if thread_type:
params.update({'thread_type': thread_type}) params.update({'thread_type': thread_type})
if thread_ids:
params.update({'thread_ids': ",".join(thread_ids)})
response = perform_request( response = perform_request(
'get', 'post',
url, url,
params params
) )
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment