Commit bfe54744 by E. Kolpakov

Option to export only cohorted discussions

parent 21b47de5
......@@ -53,6 +53,9 @@ class Command(BaseCommand):
* 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
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.
Examples:
......@@ -68,10 +71,13 @@ class Command(BaseCommand):
* `./manage.py lms export_discussion_participation <course_key> --end-date=<iso8601 datetime>
--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
* `./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'
END_DATE_PARAMETER = 'end_date'
ALL_PARAMETER = 'all'
COHORTED_ONLY_PARAMETER = 'cohorted_only'
args = "<course_id> <output_file_location>"
......@@ -98,6 +104,12 @@ class Command(BaseCommand):
action='store_true',
dest=ALL_PARAMETER,
default=False,
),
make_option(
'--cohorted_only',
action='store_true',
dest=COHORTED_ONLY_PARAMETER,
default=False,
)
)
......@@ -164,12 +176,22 @@ class Command(BaseCommand):
if not course:
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)
end_date = dateutil.parser.parse(raw_end_date) if raw_end_date else None
data = Extractor().extract(
course_key,
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)
......@@ -210,11 +232,13 @@ class Extractor(object):
users = CourseEnrollment.users_enrolled_in(course_key)
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 """
return {
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):
......@@ -232,13 +256,14 @@ class Extractor(object):
result.append(utils.merge_dict(user_record, stats))
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 """
users = self._get_users(course_key)
social_stats = self._get_social_stats(
course_key,
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)
......
......@@ -19,11 +19,13 @@ from datetime import datetime
_target_module = "django_comment_client.management.commands.export_discussion_participation"
_std_parameters_list = (
(CourseLocator(org="edX", course="demoX", run="now"), None, None),
(CourseLocator(org="otherX", course="courseX", run="later"), datetime(2015, 2, 12), None),
(CourseLocator(org="NotAX", course="NotADemo", run="anyyear"), None, 'discussion'),
(CourseLocator(org="YeaAX", course="YesADemo", run="anyday"), None, 'question'),
(CourseLocator(org="WhatX", course="WhatADemo", run="last_year"), datetime(2014, 3, 17), 'question')
(CourseLocator(org="edX", course="demoX", run="now"), None, None, None),
(CourseLocator(org="otherX", course="courseX", run="later"), datetime(2015, 2, 12), None, None),
(CourseLocator(org="NotAX", course="NotADemo", run="anyyear"), None, 'discussion', None),
(CourseLocator(org="YeaAX", course="YesADemo", run="anyday"), None, 'question', None),
(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
......@@ -44,7 +46,7 @@ class CommandTest(TestCase):
def set_up_default_mocks(self, patched_get_course):
""" 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
def test_handle_given_no_arguments_raises_command_error(self, patched_get_course):
......@@ -150,10 +152,14 @@ class CommandTest(TestCase):
@ddt.unpack
@ddt.data(*_std_parameters_list)
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 """
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()
with mock.patch("{}.open".format(_target_module), patched_open, create=True), \
mock.patch(_target_module + ".Extractor.extract") as patched_extractor:
......@@ -161,9 +167,12 @@ class CommandTest(TestCase):
self.command.handle(
str(course_key),
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=""):
......@@ -221,15 +230,17 @@ class ExtractorTest(TestCase):
@ddt.unpack
@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 """
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:
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.patched_all_social_stats = {}
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.data(
......
......@@ -119,9 +119,9 @@ class User(models.Model):
return get_user_social_stats(self.id, self.course_id, end_date=end_date)
@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 """
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):
url = self.url(action='get', params=self.attributes)
......@@ -156,7 +156,7 @@ class User(models.Model):
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 """
if not course_id:
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):
params.update({'end_date': end_date.isoformat()})
if thread_type:
params.update({'thread_type': thread_type})
if thread_ids:
params.update({'thread_ids': ",".join(thread_ids)})
response = perform_request(
'get',
'post',
url,
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