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):
self.assertEqual(response.data['is_active'], False)
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):
"""
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):
response.data['level_of_education'], data["level_of_education"])
self.assertEqual(
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):
user_id = self.user.id
......
......@@ -469,6 +469,25 @@ class UsersDetail(SecureAPIView):
if is_staff is not None:
existing_user.is_staff = 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()
username = request.DATA.get('username', None)
......
......@@ -5,6 +5,8 @@ from datetime import datetime
from optparse import make_option
from django.core.management.base import BaseCommand, CommandError
import os
from path import path
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
......@@ -15,6 +17,7 @@ from student.models import CourseEnrollment
from lms.lib.comment_client.user import User
import django_comment_client.utils as utils
from xmodule.modulestore.django import modulestore
class DiscussionExportFields(object):
......@@ -39,9 +42,11 @@ class Command(BaseCommand):
Usage:
./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)
* 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:
......@@ -49,6 +54,10 @@ class Command(BaseCommand):
* 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:
* `./manage.py lms export_discussion_participation <course_key>` - exports entire discussion participation stats for
......@@ -62,9 +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>"
......@@ -86,6 +99,18 @@ class Command(BaseCommand):
default=None,
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):
......@@ -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())
)
def handle(self, *args, **options):
""" Executes command """
@staticmethod
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:
raise CommandError("Course id not specified")
if len(args) > 2:
......@@ -125,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)
......@@ -139,8 +200,14 @@ class Command(BaseCommand):
with open(output_file_location, 'wb') as output_stream:
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):
""" Extracts discussion participation data from db and cs_comments_service """
......@@ -165,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):
......@@ -187,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)
......
......@@ -62,8 +62,8 @@ def render_press_release(request, slug):
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):
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):
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
)
......
<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 @@
-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-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-group-project.git@dd8eaf16b3bc7b7be3fb392d588328dadef56c00#egg=xblock-group-project
-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