Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
06c77f0
Add grade functionality for LTI (BLD-384)
Oct 31, 2013
94d1800
Add lookup table to students module.
auraz Nov 8, 2013
0fc3eeb
Update test lti server and add script for manual run
olmar Nov 12, 2013
3960a0a
Investigations.
auraz Nov 12, 2013
78306ce
Add processing of XML request to LTI.
auraz Nov 12, 2013
a4a2abe
Add course_id to lookup table.
auraz Nov 14, 2013
f947690
Fix post dict and add click on submit button.
olmar Nov 14, 2013
809cc09
Updates on lookup table and functions.
auraz Nov 15, 2013
35393bc
Add custom handler.
auraz Nov 15, 2013
271cf51
Add oauth sign.
olmar Nov 18, 2013
1b6279d
Updated mock_lti_server and removed odd unicode decalarion.
auraz Nov 18, 2013
08d7e9a
Fix oauth body hash.
olmar Nov 18, 2013
0c2da2f
Add get_handler_url and update mocked server.
auraz Nov 18, 2013
44874a7
Avoid csrf for OAuth signed request from LTI.
auraz Nov 19, 2013
e02cb26
OAuth for lti. Working.
olmar Nov 19, 2013
694a8d3
Add body hash compare. Still working on.
olmar Nov 20, 2013
1a2cb55
Fix.
auraz Nov 20, 2013
2f09f21
Fix.
olmar Nov 20, 2013
1fdb1f7
Implement oauth signing.
auraz Nov 20, 2013
cae7017
Make resource_link_id mandatory.
auraz Nov 21, 2013
b8a42da
Add test.
olmar Nov 21, 2013
59200ba
Add tests.
olmar Nov 22, 2013
117b3c6
Various fixes to LTI XML grade processing.
auraz Nov 23, 2013
a7ecfc8
Cherry-picked 9131eb825dddb40c1669dc7d04072724ac11d8e0.
Nov 24, 2013
e99c080
Small fixes and rename of unique_id_for_useг.
auraz Nov 24, 2013
cdbba28
Changes self.user to key.user_id.
auraz Nov 24, 2013
be1a2c9
Simplify LTI code.
auraz Nov 24, 2013
1ece163
Removed get_handler_url from x_module.
auraz Nov 24, 2013
4028167
Fix comments and clean-up.
auraz Nov 24, 2013
b6285cb
Change hash of xblock repo.
auraz Nov 25, 2013
edec45a
Add url.quote.
polesye Nov 25, 2013
81de246
cherry-picked 5bfd2976961ac0dfd7220ff994157a68fe44b2b4.
Nov 24, 2013
d9c9eee
URL resource for CodeMajor/Severity Interpretation Matrix error/statu…
Nov 25, 2013
3dc4df0
Minor corrections to code comments.
Nov 25, 2013
d7f14c2
Fixes user in model_data and unquote url from webob request.
auraz Nov 25, 2013
2bf8726
Merge branch 'valera/lti_graded' of github.com:edx/edx-platform into …
auraz Nov 25, 2013
8fdbcdb
Mock LTI server return TC response body.
auraz Nov 25, 2013
0b8d42f
Fine tuning responses.
Nov 25, 2013
45370a9
Adding initial documentation for LTI module.
Nov 25, 2013
6b104ea
Added an entry to CHANGELOG.rst file.
Nov 25, 2013
c1d5b85
Included Jira issue in changelog.
Nov 25, 2013
c1bcc2d
Add integration test.
polesye Nov 25, 2013
a2d7d1e
Merge branch 'valera/lti_graded' of https://github.com/edx/edx-platfo…
polesye Nov 25, 2013
975e863
Add unique together to anonymous lookup table.
auraz Nov 25, 2013
5996af4
Merge branch 'valera/lti_graded' of github.com:edx/edx-platform into …
auraz Nov 25, 2013
03daa64
Use default in get_or_create in lookup table.
auraz Nov 25, 2013
0b66713
Remove obsolete mocking.
auraz Nov 25, 2013
d2bfec3
Make anonymous_id_for_user use simple_anonymous_id_for_user.
auraz Nov 25, 2013
0aa0406
Make export have simple_anonymous_id for historical reasons.
auraz Nov 25, 2013
709f2d7
Update docstrings.
auraz Nov 25, 2013
53cb969
Update header in csv.
auraz Nov 25, 2013
089d2bc
Add escaping for imsx_messageIdentifier.
auraz Nov 25, 2013
c6bfa98
LTI grade XML namespace is hardcoded.
auraz Nov 25, 2013
69e8645
Add messages.
olmar Nov 25, 2013
10efe0c
Clean.
olmar Nov 25, 2013
9885b52
Updated docs.
Nov 25, 2013
e4f091c
Updated LTI docs.
Nov 25, 2013
a36f4c2
Add tests for anonymous lookup table.
auraz Nov 25, 2013
68a7eaf
Adding docs for LTI.
Nov 25, 2013
39e461c
Fix messages.
olmar Nov 25, 2013
da51a52
Add unittests.
olmar Nov 25, 2013
c72107f
Fix return value in get_clien_key_secret
auraz Nov 25, 2013
b173e48
Refactor LTI unittests.
auraz Nov 25, 2013
406d811
Cleanup in LTI tests.
auraz Nov 25, 2013
8568eb2
Fix lti tests.
polesye Nov 25, 2013
38b30f3
Tighten the user id assertion, and fix tests.
Nov 25, 2013
1e576f2
Cleanup the AnonymousUserId model and associated functions
cpennington Nov 25, 2013
38c44e1
Use the per-student anonymized id for the subset of modules known to …
cpennington Nov 25, 2013
1b1a40f
Fix tests
cpennington Nov 25, 2013
ac7597b
Adapt to new way of doing thirdparty handlers.
Nov 25, 2013
1d19cb9
Fix acceptance test.
polesye Nov 26, 2013
871c00c
Fix test.
polesye Nov 26, 2013
7592ca6
Fix typo.
auraz Nov 26, 2013
8dcfd90
Fix default path in docs.
auraz Nov 26, 2013
0424f76
Fix tests.
polesye Nov 26, 2013
7beefd5
Fix unittest.
auraz Nov 26, 2013
657f9d6
Rename unittests.
polesye Nov 26, 2013
03e0da8
Moved LTI docs to course_authors section.
Nov 26, 2013
2f12ead
Fix.
olmar Nov 26, 2013
f1fcc8a
Updated Python doc strings for LTI.
Nov 26, 2013
6d5dee5
Tidy up LTI docs in PY.
Nov 26, 2013
42bef78
Work in progress.
polesye Nov 26, 2013
f024c2e
Merge branch 'valera/lti_graded' of https://github.com/edx/edx-platfo…
polesye Nov 26, 2013
22e579f
Add two tests for LTI module functions.
auraz Nov 26, 2013
ca6c76f
Fix typo
cpennington Nov 26, 2013
2fcbd20
Remove unused import
cpennington Nov 26, 2013
78ba085
Rename the LTIModuleDescriptor to LTIDescriptor to follow convention
cpennington Nov 26, 2013
2836cbf
Make per-course anonymized ids specific to LTIModule, with the intent…
cpennington Nov 26, 2013
563500e
Fix LTIModule imports
cpennington Nov 26, 2013
1d3aec7
Don't use a list for issubclass
cpennington Nov 26, 2013
52e01d2
Don't use a list for issubclass, again
cpennington Nov 26, 2013
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.

Blades: Added grading support for LTI module. LTI providers can now grade
student's work and send edX scores. OAuth1 based authentication
implemented. BLD-384.

Blades: Put 2nd "Hide output" button at top of test box & increase text size for
code response questions. BLD-126.

Expand Down
2 changes: 1 addition & 1 deletion cms/djangoapps/contentstore/views/preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method
"""
An XModule ModuleSystem for use in Studio previews
"""
def handler_url(self, block, handler_name, suffix='', query=''):
def handler_url(self, block, handler_name, suffix='', query='', thirdparty=False):
return handler_prefix(block, handler_name, suffix) + '?' + query


Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
# -*- coding: utf-8 -*-
"""Dump username,unique_id_for_user pairs as CSV.
"""Dump username, per-student anonymous id, and per-course anonymous id triples as CSV.

Give instructors easy access to the mapping from anonymized IDs to user IDs
with a simple Django management command to generate a CSV mapping. To run, use
the following:

rake django-admin[anonymized_id_mapping,x,y,z]

[Naturally, substitute the appropriate values for x, y, and z. (I.e.,
lms, dev, and MITx/6.002x/Circuits)]"""
./manage.py lms anonymized_id_mapping COURSE_ID
"""

import csv

from django.contrib.auth.models import User
from django.core.management.base import BaseCommand, CommandError

from student.models import unique_id_for_user
from student.models import anonymous_id_for_user


class Command(BaseCommand):
Expand Down Expand Up @@ -52,9 +50,17 @@ def handle(self, *args, **options):
try:
with open(output_filename, 'wb') as output_file:
csv_writer = csv.writer(output_file)
csv_writer.writerow(("User ID", "Anonymized user ID"))
csv_writer.writerow((
"User ID",
"Per-Student anonymized user ID",
"Per-course anonymized user id"
))
for student in students:
csv_writer.writerow((student.id, unique_id_for_user(student)))
csv_writer.writerow((
student.id,
anonymous_id_for_user(student, ''),
anonymous_id_for_user(student, course_id)
))
except IOError:
raise CommandError("Error writing to file: %s" % output_filename)

Large diffs are not rendered by default.

73 changes: 59 additions & 14 deletions common/djangoapps/student/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from django.dispatch import receiver
import django.dispatch
from django.forms import ModelForm, forms
from django.core.exceptions import ObjectDoesNotExist

from course_modes.models import CourseMode
import lms.lib.comment_client as cc
Expand All @@ -42,6 +43,63 @@
AUDIT_LOG = logging.getLogger("audit")


class AnonymousUserId(models.Model):
"""
This table contains user, course_Id and anonymous_user_id

Purpose of this table is to provide user by anonymous_user_id.

We are generating anonymous_user_id using md5 algorithm, so resulting length will always be 16 bytes.
http://docs.python.org/2/library/md5.html#md5.digest_size
"""
user = models.ForeignKey(User, db_index=True)
anonymous_user_id = models.CharField(unique=True, max_length=16)
course_id = models.CharField(db_index=True, max_length=255)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a key over user/course_id, so that lookups are fast (since this is going to be a large, frequently used table.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, this should be unique across user/course_id.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking closer, I see that you have an index on each of user and course_id individually, but given that the most common query is going to use both, I think a joint index is more important.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cpennington Do you mean add unique_together = (user, course_id) constraint which will result in creating joint index in MySQL? I'm not sure, still, if this index will be created in sqlite.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: Added unique together to anonymous lookup table

unique_together = (user, course_id)


def anonymous_id_for_user(user, course_id):
"""
Return a unique id for a (user, course) pair, suitable for inserting
into e.g. personalized survey links.

If user is an `AnonymousUser`, returns `None`
"""
# This part is for ability to get xblock instance in xblock_noauth handlers, where user is unauthenticated.
if user.is_anonymous():
return None

# include the secret key as a salt, and to make the ids unique across different LMS installs.
hasher = hashlib.md5()
hasher.update(settings.SECRET_KEY)
hasher.update(str(user.id))
hasher.update(course_id)

return AnonymousUserId.objects.get_or_create(
defaults={'anonymous_user_id': hasher.hexdigest()},
user=user,
course_id=course_id
)[0].anonymous_user_id


def user_by_anonymous_id(id):
"""
Return user by anonymous_user_id using AnonymousUserId lookup table.

Do not raise `django.ObjectDoesNotExist` exception,
if there is no user for anonymous_student_id,
because this function will be used inside xmodule w/o django access.
"""

if id is None:
return None

try:
return User.objects.get(anonymoususerid__anonymous_user_id=id)
except ObjectDoesNotExist:
return None


class UserStanding(models.Model):
"""
This table contains a student's account's status.
Expand Down Expand Up @@ -619,19 +677,6 @@ def get_testcenter_registration(user, course_id, exam_series_code):
get_testcenter_registration.__test__ = False


def unique_id_for_user(user):
"""
Return a unique id for a user, suitable for inserting into
e.g. personalized survey links.
"""
# include the secret key as a salt, and to make the ids unique across
# different LMS installs.
h = hashlib.md5()
h.update(settings.SECRET_KEY)
h.update(str(user.id))
return h.hexdigest()


# TODO: Should be renamed to generic UserGroup, and possibly
# Given an optional field for type of group
class UserTestGroup(models.Model):
Expand Down Expand Up @@ -769,7 +814,7 @@ def update_enrollment(self, mode=None, is_active=None):
activation_changed = True

mode_changed = False
# if mode is None, the call to update_enrollment didn't specify a new
# if mode is None, the call to update_enrollment didn't specify a new
# mode, so leave as-is
if self.mode != mode and mode is not None:
self.mode = mode
Expand Down
64 changes: 53 additions & 11 deletions common/djangoapps/student/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from django.test import TestCase
from django.test.utils import override_settings
from django.test.client import RequestFactory
from django.contrib.auth.models import User
from django.contrib.auth.models import User, AnonymousUser
from django.contrib.auth.hashers import UNUSABLE_PASSWORD
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import int_to_base36
Expand All @@ -28,7 +28,7 @@
from mock import Mock, patch, sentinel
from textwrap import dedent

from student.models import unique_id_for_user, CourseEnrollment
from student.models import anonymous_id_for_user, user_by_anonymous_id, CourseEnrollment
from student.views import (process_survey_link, _cert_info, password_reset, password_reset_confirm_wrapper,
change_enrollment, complete_course_mode_info)
from student.tests.factories import UserFactory, CourseModeFactory
Expand Down Expand Up @@ -138,20 +138,28 @@ class CourseEndingTest(TestCase):
"""Test things related to course endings: certificates, surveys, etc"""

def test_process_survey_link(self):
username = "fred"
user = Mock(username=username)
id = unique_id_for_user(user)
course_id = "test_id"
user = UserFactory()

link1 = "http://www.mysurvey.com"
self.assertEqual(process_survey_link(link1, user), link1)
self.assertEqual(process_survey_link(link1, user, course_id), link1)

link2 = "http://www.mysurvey.com?unique={UNIQUE_ID}"
link2_expected = "http://www.mysurvey.com?unique={UNIQUE_ID}".format(UNIQUE_ID=id)
self.assertEqual(process_survey_link(link2, user), link2_expected)

expected_id = anonymous_id_for_user(user, course_id)
link2_expected = "http://www.mysurvey.com?unique={UNIQUE_ID}".format(UNIQUE_ID=expected_id)

self.assertEqual(process_survey_link(link2, user, course_id), link2_expected)

def test_cert_info(self):
user = Mock(username="fred")
"""
_cert_info() calls process_survey_link(), process_survey_link() calls anonymous_id_for_user()
anonymous_id_for_user() returns 'Anonymous' because user is not authenticated.
"""
user = Mock(username="fred", id="test_user_id")
survey_url = "http://a_survey.com"
course = Mock(end_of_course_survey_url=survey_url)

course = Mock(end_of_course_survey_url=survey_url, id="test_id")

self.assertEqual(_cert_info(user, course, None),
{'status': 'processing',
Expand Down Expand Up @@ -211,7 +219,7 @@ def test_cert_info(self):
})

# Test a course that doesn't have a survey specified
course2 = Mock(end_of_course_survey_url=None)
course2 = Mock(end_of_course_survey_url=None, id="test_course_id2")
cert_status = {'status': 'notpassing', 'grade': '67',
'download_url': download_url}
self.assertEqual(_cert_info(user, course2, cert_status),
Expand Down Expand Up @@ -506,3 +514,37 @@ def test_change_enrollment_add_to_cart(self):
self.assertEqual(response.content, reverse('shoppingcart.views.show_cart'))
self.assertTrue(shoppingcart.models.PaidCourseRegistration.contained_in_order(
shoppingcart.models.Order.get_cart_for_user(self.user), self.course.id))


@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class AnonymousLookupTable(TestCase):
"""
Tests for anonymous_id_functions
"""
# arbitrary constant
COURSE_SLUG = "100"
COURSE_NAME = "test_course"
COURSE_ORG = "EDX"

def setUp(self):
self.course = CourseFactory.create(org=self.COURSE_ORG, display_name=self.COURSE_NAME, number=self.COURSE_SLUG)
self.assertIsNotNone(self.course)
self.user = UserFactory()
CourseModeFactory.create(
course_id=self.course.id,
mode_slug='honor',
mode_display_name='Honor Code',
)
patcher = patch('student.models.server_track')
self.mock_server_track = patcher.start()
self.addCleanup(patcher.stop)

def test_for_unregistered_user(self): # same path as for logged out user
self.assertEqual(None, anonymous_id_for_user(AnonymousUser(), self.course.id))
self.assertIsNone(user_by_anonymous_id(None))

def test_roundtrip_for_logged_user(self):
enrollment = CourseEnrollment.enroll(self.user, self.course.id)
anonymous_id = anonymous_id_for_user(self.user, self.course.id)
real_user = user_by_anonymous_id(anonymous_id)
self.assertEqual(self.user, real_user)
12 changes: 6 additions & 6 deletions common/djangoapps/student/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from student.models import (
Registration, UserProfile, TestCenterUser, TestCenterUserForm,
TestCenterRegistration, TestCenterRegistrationForm, PendingNameChange,
PendingEmailChange, CourseEnrollment, unique_id_for_user,
PendingEmailChange, CourseEnrollment, anonymous_id_for_user,
get_testcenter_registration, CourseEnrollmentAllowed, UserStanding,
)
from student.forms import PasswordResetFormNoActive
Expand Down Expand Up @@ -149,12 +149,12 @@ def press(request):
return render_to_response('static_templates/press.html', {'articles': articles})


def process_survey_link(survey_link, user):
def process_survey_link(survey_link, user, course_id):
"""
If {UNIQUE_ID} appears in the link, replace it with a unique id for the user.
Currently, this is sha1(user.username). Otherwise, return survey_link.
Currently, this is sha1() updated with secret_key, user.id and course.id. Otherwise, return survey_link.
"""
return survey_link.format(UNIQUE_ID=unique_id_for_user(user))
return survey_link.format(UNIQUE_ID=anonymous_id_for_user(user, course_id))


def cert_info(user, course):
Expand Down Expand Up @@ -209,7 +209,7 @@ def _cert_info(user, course, cert_status):
course.end_of_course_survey_url is not None):
d.update({
'show_survey_button': True,
'survey_url': process_survey_link(course.end_of_course_survey_url, user)})
'survey_url': process_survey_link(course.end_of_course_survey_url, user, course.id)})
else:
d['show_survey_button'] = False

Expand Down Expand Up @@ -296,7 +296,7 @@ def complete_course_mode_info(course_id, enrollment):
def dashboard(request):
user = request.user

# Build our (course, enorllment) list for the user, but ignore any courses that no
# Build our (course, enorllment) list for the user, but ignore any courses that no
# longer exist (because the course IDs have changed). Still, we don't delete those
# enrollments, because it could have been a data push snafu.
course_enrollment_pairs = []
Expand Down
2 changes: 1 addition & 1 deletion common/lib/xmodule/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"hidden = xmodule.hidden_module:HiddenDescriptor",
"raw = xmodule.raw_module:RawDescriptor",
"crowdsource_hinter = xmodule.crowdsource_hinter:CrowdsourceHinterDescriptor",
"lti = xmodule.lti_module:LTIModuleDescriptor",
"lti = xmodule.lti_module:LTIDescriptor",
]

setup(
Expand Down
Loading