Commit 44b09dd8 by Fred Smith

Merge pull request #454 from edx-solutions/rc/2015-06-15

Rc/2015 06 15
parents b651789c 4ae4f7c9
...@@ -418,6 +418,40 @@ class UsersApiTests(ModuleStoreTestCase): ...@@ -418,6 +418,40 @@ class UsersApiTests(ModuleStoreTestCase):
self.assertEqual(response.data['is_active'], False) self.assertEqual(response.data['is_active'], False)
self.assertIsNotNone(response.data['created']) self.assertIsNotNone(response.data['created'])
def test_user_detail_invalid_email(self):
test_uri = '{}/{}'.format(self.users_base_uri, self.user.id)
data = {
'email': 'fail'
}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 400)
self.assertIn('Invalid email address', response.content)
def test_user_detail_duplicate_email(self):
user2 = UserFactory()
test_uri = '{}/{}'.format(self.users_base_uri, self.user.id)
test_uri2 = '{}/{}'.format(self.users_base_uri, user2.id)
data = {
'email': self.test_email
}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 200)
response = self.do_post(test_uri2, data)
self.assertEqual(response.status_code, 400)
self.assertIn('A user with that email address already exists.', response.content)
def test_user_detail_email_updated(self):
test_uri = '{}/{}'.format(self.users_base_uri, self.user.id)
new_email = 'test@example.com'
data = {
'email': new_email
}
self.assertNotEqual(self.user.email, new_email)
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 200)
self.user = User.objects.get(id=self.user.id)
self.assertEqual(self.user.email, new_email)
def test_user_detail_post_duplicate_username(self): def test_user_detail_post_duplicate_username(self):
""" """
Create two users, then pass the same first username in request in order to update username of second user. Create two users, then pass the same first username in request in order to update username of second user.
...@@ -1435,6 +1469,8 @@ class UsersApiTests(ModuleStoreTestCase): ...@@ -1435,6 +1469,8 @@ class UsersApiTests(ModuleStoreTestCase):
response.data['level_of_education'], data["level_of_education"]) response.data['level_of_education'], data["level_of_education"])
self.assertEqual( self.assertEqual(
str(response.data['year_of_birth']), data["year_of_birth"]) str(response.data['year_of_birth']), data["year_of_birth"])
# This one's technically on the user model itself, but can be updated.
self.assertEqual(response.data['email'], data['email'])
def test_user_organizations_list(self): def test_user_organizations_list(self):
user_id = self.user.id user_id = self.user.id
......
...@@ -469,6 +469,25 @@ class UsersDetail(SecureAPIView): ...@@ -469,6 +469,25 @@ class UsersDetail(SecureAPIView):
if is_staff is not None: if is_staff is not None:
existing_user.is_staff = is_staff existing_user.is_staff = is_staff
response_data['is_staff'] = existing_user.is_staff response_data['is_staff'] = existing_user.is_staff
email = request.DATA.get('email')
if email is not None:
email_fail = False
try:
validate_email(email)
except ValidationError:
email_fail = True
response_data['message'] = _('Invalid email address {}.').format(repr(email))
if email != existing_user.email:
try:
# Email addresses need to be unique in the LMS, though Django doesn't enforce it directly.
User.objects.get(email=email)
email_fail = True
response_data['message'] = _('A user with that email address already exists.')
except ObjectDoesNotExist:
pass
if email_fail:
return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
existing_user.email = email
existing_user.save() existing_user.save()
username = request.DATA.get('username', None) username = request.DATA.get('username', None)
......
...@@ -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,9 +42,11 @@ class Command(BaseCommand): ...@@ -39,9 +42,11 @@ 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:
...@@ -49,6 +54,10 @@ class Command(BaseCommand): ...@@ -49,6 +54,10 @@ class Command(BaseCommand):
* 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.
Examples: Examples:
* `./manage.py lms export_discussion_participation <course_key>` - exports entire discussion participation stats for * `./manage.py lms export_discussion_participation <course_key>` - exports entire discussion participation stats for
...@@ -62,9 +71,13 @@ class Command(BaseCommand): ...@@ -62,9 +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'
COHORTED_ONLY_PARAMETER = 'cohorted_only'
args = "<course_id> <output_file_location>" args = "<course_id> <output_file_location>"
...@@ -86,6 +99,18 @@ class Command(BaseCommand): ...@@ -86,6 +99,18 @@ 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,
),
make_option(
'--cohorted_only',
action='store_true',
dest=COHORTED_ONLY_PARAMETER,
default=False,
)
) )
def _get_filter_string_representation(self, options): def _get_filter_string_representation(self, options):
...@@ -103,8 +128,34 @@ class Command(BaseCommand): ...@@ -103,8 +128,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:
...@@ -125,12 +176,22 @@ class Command(BaseCommand): ...@@ -125,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)
...@@ -139,8 +200,14 @@ class Command(BaseCommand): ...@@ -139,8 +200,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 """
...@@ -165,11 +232,13 @@ class Extractor(object): ...@@ -165,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):
...@@ -187,13 +256,14 @@ class Extractor(object): ...@@ -187,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
...@@ -42,38 +44,62 @@ class CommandTest(TestCase): ...@@ -42,38 +44,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_courses): 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_courses.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_courses): 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_courses): 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_courses): 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_courses.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_courses): 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_courses.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_courses): 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_courses) 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 +111,9 @@ class CommandTest(TestCase): ...@@ -85,9 +111,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_courses): 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_courses) 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 +121,9 @@ class CommandTest(TestCase): ...@@ -95,9 +121,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_courses): 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_courses) 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 +138,9 @@ class CommandTest(TestCase): ...@@ -112,9 +138,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_courses): 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_courses) 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 +152,14 @@ class CommandTest(TestCase): ...@@ -126,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_courses 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_courses) 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:
...@@ -137,9 +167,12 @@ class CommandTest(TestCase): ...@@ -137,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=""):
...@@ -197,15 +230,17 @@ class ExtractorTest(TestCase): ...@@ -197,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(
......
...@@ -62,8 +62,8 @@ def render_press_release(request, slug): ...@@ -62,8 +62,8 @@ def render_press_release(request, slug):
def render_404(request): def render_404(request):
return HttpResponseNotFound(render_to_string('static_templates/404.html', {})) return HttpResponseNotFound(render_to_string('static_templates/404-plain.html', {}))
def render_500(request): def render_500(request):
return HttpResponseServerError(render_to_string('static_templates/server-error.html', {})) return HttpResponseServerError(render_to_string('static_templates/server-error-plain.html', {}))
...@@ -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
) )
......
<html>
<body>
<h1>Page not found</h1>
</body>
</html>
<html>
<body>
<h1>
There has been a 500 error on the servers
</h1>
</body>
</html>
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
-e git+https://github.com/edx-solutions/xblock-mentoring.git@82a4219b865d12db80ac57bda43fef9e30bec3f1#egg=xblock-mentoring -e git+https://github.com/edx-solutions/xblock-mentoring.git@82a4219b865d12db80ac57bda43fef9e30bec3f1#egg=xblock-mentoring
-e git+https://github.com/edx-solutions/xblock-image-explorer.git@21b9bcc4f2c7917463ab18a596161ac6c58c9c4a#egg=xblock-image-explorer -e git+https://github.com/edx-solutions/xblock-image-explorer.git@21b9bcc4f2c7917463ab18a596161ac6c58c9c4a#egg=xblock-image-explorer
-e git+https://github.com/edx-solutions/xblock-drag-and-drop.git@92ee2055a16899090a073e1df81e35d5293ad767#egg=xblock-drag-and-drop -e git+https://github.com/edx-solutions/xblock-drag-and-drop.git@92ee2055a16899090a073e1df81e35d5293ad767#egg=xblock-drag-and-drop
-e git+https://github.com/edx-solutions/xblock-drag-and-drop-v2.git@ed24dbef753e411f2c9b17339ae21f3a89a8531c#egg=xblock-drag-and-drop-v2 -e git+https://github.com/edx-solutions/xblock-drag-and-drop-v2.git@5736ed8774b92c8b8396b5bd455f8a8fb80295fb#egg=xblock-drag-and-drop-v2
-e git+https://github.com/edx-solutions/xblock-ooyala.git@ac49b30452aff0cc34cace6a34b788e100490f24#egg=xblock-ooyala -e git+https://github.com/edx-solutions/xblock-ooyala.git@ac49b30452aff0cc34cace6a34b788e100490f24#egg=xblock-ooyala
-e git+https://github.com/edx-solutions/xblock-group-project.git@dd8eaf16b3bc7b7be3fb392d588328dadef56c00#egg=xblock-group-project -e git+https://github.com/edx-solutions/xblock-group-project.git@dd8eaf16b3bc7b7be3fb392d588328dadef56c00#egg=xblock-group-project
-e git+https://github.com/edx-solutions/xblock-adventure.git@effa22006bb6528bc6d3788787466eb4e74e1161#egg=xblock-adventure -e git+https://github.com/edx-solutions/xblock-adventure.git@effa22006bb6528bc6d3788787466eb4e74e1161#egg=xblock-adventure
......
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