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
21b47de5
Commit
21b47de5
authored
May 28, 2015
by
Jonathan Piacenti
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add --all option to social stats export.
parent
425677ea
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
90 additions
and
21 deletions
+90
-21
lms/djangoapps/django_comment_client/management/commands/export_discussion_participation.py
+48
-3
lms/djangoapps/django_comment_client/tests/management/test_export_discussion_participation.py
+42
-18
No files found.
lms/djangoapps/django_comment_client/management/commands/export_discussion_participation.py
View file @
21b47de5
...
@@ -5,6 +5,8 @@ from datetime import datetime
...
@@ -5,6 +5,8 @@ from datetime import datetime
from
optparse
import
make_option
from
optparse
import
make_option
from
django.core.management.base
import
BaseCommand
,
CommandError
from
django.core.management.base
import
BaseCommand
,
CommandError
import
os
from
path
import
path
from
opaque_keys
import
InvalidKeyError
from
opaque_keys
import
InvalidKeyError
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
...
@@ -15,6 +17,7 @@ from student.models import CourseEnrollment
...
@@ -15,6 +17,7 @@ from student.models import CourseEnrollment
from
lms.lib.comment_client.user
import
User
from
lms.lib.comment_client.user
import
User
import
django_comment_client.utils
as
utils
import
django_comment_client.utils
as
utils
from
xmodule.modulestore.django
import
modulestore
class
DiscussionExportFields
(
object
):
class
DiscussionExportFields
(
object
):
...
@@ -39,15 +42,18 @@ class Command(BaseCommand):
...
@@ -39,15 +42,18 @@ class Command(BaseCommand):
Usage:
Usage:
./manage.py lms export_discussion_participation course_key [dest_file] [OPTIONS]
./manage.py lms export_discussion_participation course_key [dest_file] [OPTIONS]
./manage.py lms export_discussion_participation [dest_directory] --all [OPTIONS]
* course_key - target course key (e.g. edX/DemoX/T1)
* course_key - target course key (e.g. edX/DemoX/T1)
* dest_file - location of destination file (created if missing, overwritten if exists)
* dest_file - location of destination file (created if missing, overwritten if exists)
* dest_directory - location to store all dumped files to. Will dump into the current directory otherwise.
OPTIONS:
OPTIONS:
* 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
* all - Dump all social stats at once into a particular directory.
Examples:
Examples:
...
@@ -65,6 +71,7 @@ class Command(BaseCommand):
...
@@ -65,6 +71,7 @@ class Command(BaseCommand):
"""
"""
THREAD_TYPE_PARAMETER
=
'thread_type'
THREAD_TYPE_PARAMETER
=
'thread_type'
END_DATE_PARAMETER
=
'end_date'
END_DATE_PARAMETER
=
'end_date'
ALL_PARAMETER
=
'all'
args
=
"<course_id> <output_file_location>"
args
=
"<course_id> <output_file_location>"
...
@@ -86,6 +93,12 @@ class Command(BaseCommand):
...
@@ -86,6 +93,12 @@ class Command(BaseCommand):
default
=
None
,
default
=
None
,
help
=
'Include threads, comments and replies created before the supplied date (iso8601 format)'
help
=
'Include threads, comments and replies created before the supplied date (iso8601 format)'
),
),
make_option
(
'--all'
,
action
=
'store_true'
,
dest
=
ALL_PARAMETER
,
default
=
False
,
)
)
)
def
_get_filter_string_representation
(
self
,
options
):
def
_get_filter_string_representation
(
self
,
options
):
...
@@ -103,8 +116,34 @@ class Command(BaseCommand):
...
@@ -103,8 +116,34 @@ class Command(BaseCommand):
"social_stats_{course}_{date:
%
Y_
%
m_
%
d_
%
H_
%
M_
%
S}.csv"
.
format
(
course
=
course_key
,
date
=
datetime
.
utcnow
())
"social_stats_{course}_{date:
%
Y_
%
m_
%
d_
%
H_
%
M_
%
S}.csv"
.
format
(
course
=
course_key
,
date
=
datetime
.
utcnow
())
)
)
def
handle
(
self
,
*
args
,
**
options
):
@staticmethod
""" Executes command """
def
get_all_courses
():
"""
Gets all courses. Made into a separate function because patch isn't cooperating.
"""
return
modulestore
()
.
get_courses
()
def
dump_all
(
self
,
*
args
,
**
options
):
if
len
(
args
)
>
1
:
raise
CommandError
(
"May not specify course and destination root directory with the --all option."
)
args
=
list
(
args
)
try
:
dir_name
=
path
(
args
.
pop
())
except
IndexError
:
dir_name
=
path
(
'social_stats'
)
if
not
os
.
path
.
exists
(
dir_name
):
os
.
makedirs
(
dir_name
)
for
course
in
self
.
get_all_courses
():
raw_course_key
=
unicode
(
course
.
location
.
course_key
)
args
=
[
raw_course_key
,
dir_name
/
self
.
get_default_file_location
(
raw_course_key
)
]
self
.
dump_one
(
*
args
,
**
options
)
def
dump_one
(
self
,
*
args
,
**
options
):
if
not
args
:
if
not
args
:
raise
CommandError
(
"Course id not specified"
)
raise
CommandError
(
"Course id not specified"
)
if
len
(
args
)
>
2
:
if
len
(
args
)
>
2
:
...
@@ -139,8 +178,14 @@ class Command(BaseCommand):
...
@@ -139,8 +178,14 @@ class Command(BaseCommand):
with
open
(
output_file_location
,
'wb'
)
as
output_stream
:
with
open
(
output_file_location
,
'wb'
)
as
output_stream
:
Exporter
(
output_stream
)
.
export
(
data
)
Exporter
(
output_stream
)
.
export
(
data
)
self
.
stdout
.
write
(
"Success!
\n
"
)
def
handle
(
self
,
*
args
,
**
options
):
""" Executes command """
if
options
.
get
(
self
.
ALL_PARAMETER
,
False
):
self
.
dump_all
(
*
args
,
**
options
)
else
:
self
.
dump_one
(
*
args
,
**
options
)
self
.
stdout
.
write
(
"Success!
\n
"
)
class
Extractor
(
object
):
class
Extractor
(
object
):
""" Extracts discussion participation data from db and cs_comments_service """
""" Extracts discussion participation data from db and cs_comments_service """
...
...
lms/djangoapps/django_comment_client/tests/management/test_export_discussion_participation.py
View file @
21b47de5
...
@@ -42,38 +42,62 @@ class CommandTest(TestCase):
...
@@ -42,38 +42,62 @@ class CommandTest(TestCase):
self
.
command
.
stdout
=
mock
.
Mock
()
self
.
command
.
stdout
=
mock
.
Mock
()
self
.
command
.
stderr
=
mock
.
Mock
()
self
.
command
.
stderr
=
mock
.
Mock
()
def
set_up_default_mocks
(
self
,
patched_get_course
s
):
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
s
.
return_value
=
CourseLocator
(
"edX"
,
"demoX"
,
"now"
)
patched_get_course
.
return_value
=
CourseLocator
(
"edX"
,
"demoX"
,
"now"
)
# pylint:disable=unused-argument
# pylint:disable=unused-argument
def
test_handle_given_no_arguments_raises_command_error
(
self
,
patched_get_course
s
):
def
test_handle_given_no_arguments_raises_command_error
(
self
,
patched_get_course
):
""" Tests that raises error if invoked with no arguments """
""" Tests that raises error if invoked with no arguments """
with
self
.
assertRaises
(
CommandError
):
with
self
.
assertRaises
(
CommandError
):
self
.
command
.
handle
()
self
.
command
.
handle
()
# pylint:disable=unused-argument
# pylint:disable=unused-argument
def
test_handle_given_more_than_two_args_raises_command_error
(
self
,
patched_get_course
s
):
def
test_handle_given_more_than_two_args_raises_command_error
(
self
,
patched_get_course
):
""" Tests that raises error if invoked with too many arguments """
""" Tests that raises error if invoked with too many arguments """
with
self
.
assertRaises
(
CommandError
):
with
self
.
assertRaises
(
CommandError
):
self
.
command
.
handle
(
1
,
2
,
3
)
self
.
command
.
handle
(
1
,
2
,
3
)
def
test_handle_given_invalid_course_key_raises_invalid_key_error
(
self
,
patched_get_course
s
):
def
test_handle_given_invalid_course_key_raises_invalid_key_error
(
self
,
patched_get_course
):
""" Tests that invalid key errors are propagated """
""" Tests that invalid key errors are propagated """
patched_get_course
s
.
return_value
=
None
patched_get_course
.
return_value
=
None
with
self
.
assertRaises
(
InvalidKeyError
):
with
self
.
assertRaises
(
InvalidKeyError
):
self
.
command
.
handle
(
"I'm invalid key"
)
self
.
command
.
handle
(
"I'm invalid key"
)
def
test_handle_given_missing_course_raises_command_error
(
self
,
patched_get_course
s
):
def
test_handle_given_missing_course_raises_command_error
(
self
,
patched_get_course
):
""" Tests that raises command error if missing course key was provided """
""" Tests that raises command error if missing course key was provided """
patched_get_course
s
.
return_value
=
None
patched_get_course
.
return_value
=
None
with
self
.
assertRaises
(
CommandError
):
with
self
.
assertRaises
(
CommandError
):
self
.
command
.
handle
(
"edX/demoX/now"
)
self
.
command
.
handle
(
"edX/demoX/now"
)
# pylint: disable=unused-argument
def
test_all_option
(
self
,
patched_get_course
):
""" Tests that the 'all' option does run the dump command for all courses """
self
.
command
.
dump_one
=
mock
.
Mock
()
self
.
command
.
get_all_courses
=
mock
.
Mock
()
course_list
=
[
mock
.
Mock
()
for
__
in
range
(
0
,
3
)]
locator_list
=
[
CourseLocator
(
org
=
"edX"
,
course
=
"demoX"
,
run
=
"now"
),
CourseLocator
(
org
=
"Sandbox"
,
course
=
"Sandbox"
,
run
=
"Sandbox"
),
CourseLocator
(
org
=
"Test"
,
course
=
"Testy"
,
run
=
"Testify"
),
]
for
index
,
course
in
enumerate
(
course_list
):
course
.
location
.
course_key
=
locator_list
[
index
]
self
.
command
.
get_all_courses
.
return_value
=
course_list
self
.
command
.
handle
(
"test_dir"
,
all
=
True
,
dummy
=
'test'
)
calls
=
self
.
command
.
dump_one
.
call_args_list
self
.
assertEqual
(
len
(
calls
),
3
)
self
.
assertEqual
(
calls
[
0
][
0
][
0
],
'course-v1:edX+demoX+now'
)
self
.
assertEqual
(
calls
[
1
][
0
][
0
],
'course-v1:Sandbox+Sandbox+Sandbox'
)
self
.
assertEqual
(
calls
[
2
][
0
][
0
],
'course-v1:Test+Testy+Testify'
)
self
.
assertIn
(
'test_dir/social_stats_course-v1edXdemoXnow'
,
calls
[
0
][
0
][
1
])
self
.
assertIn
(
'test_dir/social_stats_course-v1SandboxSandboxSandbox'
,
calls
[
1
][
0
][
1
])
self
.
assertIn
(
'test_dir/social_stats_course-v1TestTestyTestify'
,
calls
[
2
][
0
][
1
])
@ddt.data
(
"edX/demoX/now"
,
"otherX/CourseX/later"
)
@ddt.data
(
"edX/demoX/now"
,
"otherX/CourseX/later"
)
def
test_handle_writes_to_correct_location_when_output_file_not_specified
(
self
,
course_key
,
patched_get_course
s
):
def
test_handle_writes_to_correct_location_when_output_file_not_specified
(
self
,
course_key
,
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
s
)
self
.
set_up_default_mocks
(
patched_get_course
)
expected_filename
=
utils
.
format_filename
(
expected_filename
=
utils
.
format_filename
(
"social_stats_{course}_{date:
%
Y_
%
m_
%
d_
%
H_
%
M_
%
S}.csv"
.
format
(
course
=
course_key
,
date
=
datetime
.
utcnow
())
"social_stats_{course}_{date:
%
Y_
%
m_
%
d_
%
H_
%
M_
%
S}.csv"
.
format
(
course
=
course_key
,
date
=
datetime
.
utcnow
())
)
)
...
@@ -85,9 +109,9 @@ class CommandTest(TestCase):
...
@@ -85,9 +109,9 @@ class CommandTest(TestCase):
patched_open
.
assert_called_with
(
expected_filename
,
'wb'
)
patched_open
.
assert_called_with
(
expected_filename
,
'wb'
)
@ddt.data
(
"test.csv"
,
"other_file.csv"
)
@ddt.data
(
"test.csv"
,
"other_file.csv"
)
def
test_handle_writes_to_correct_location_when_output_file_is_specified
(
self
,
location
,
patched_get_course
s
):
def
test_handle_writes_to_correct_location_when_output_file_is_specified
(
self
,
location
,
patched_get_course
):
""" Tests that when explicit filename is given data is exported to chosen location """
""" Tests that when explicit filename is given data is exported to chosen location """
self
.
set_up_default_mocks
(
patched_get_course
s
)
self
.
set_up_default_mocks
(
patched_get_course
)
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
:
...
@@ -95,9 +119,9 @@ class CommandTest(TestCase):
...
@@ -95,9 +119,9 @@ class CommandTest(TestCase):
self
.
command
.
handle
(
"irrelevant/course/key"
,
location
)
self
.
command
.
handle
(
"irrelevant/course/key"
,
location
)
patched_open
.
assert_called_with
(
location
,
'wb'
)
patched_open
.
assert_called_with
(
location
,
'wb'
)
def
test_handle_creates_correct_exporter
(
self
,
patched_get_course
s
):
def
test_handle_creates_correct_exporter
(
self
,
patched_get_course
):
""" Tests that creates correct exporter """
""" Tests that creates correct exporter """
self
.
set_up_default_mocks
(
patched_get_course
s
)
self
.
set_up_default_mocks
(
patched_get_course
)
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
,
\
...
@@ -112,9 +136,9 @@ class CommandTest(TestCase):
...
@@ -112,9 +136,9 @@ class CommandTest(TestCase):
{
"1"
:
{
"num_threads"
:
12
}},
{
"1"
:
{
"num_threads"
:
12
}},
{
"1"
:
{
"num_threads"
:
14
,
"num_comments"
:
7
}}
{
"1"
:
{
"num_threads"
:
14
,
"num_comments"
:
7
}}
)
)
def
test_handle_exports_correct_data
(
self
,
extracted
,
patched_get_course
s
):
def
test_handle_exports_correct_data
(
self
,
extracted
,
patched_get_course
):
""" Tests that invokes export with correct data """
""" Tests that invokes export with correct data """
self
.
set_up_default_mocks
(
patched_get_course
s
)
self
.
set_up_default_mocks
(
patched_get_course
)
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
,
\
...
@@ -126,10 +150,10 @@ class CommandTest(TestCase):
...
@@ -126,10 +150,10 @@ 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
s
self
,
course_key
,
end_date
,
thread_type
,
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
s
)
self
.
set_up_default_mocks
(
patched_get_course
)
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
:
...
...
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