Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -9,6 +9,10 @@ LMS: Add feature for providing background grade report generation via Celery
instructor task, with reports uploaded to S3. Feature is visible on the beta
instructor dashboard. LMS-58

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.

LMS: Beta-tester status is now set on a per-course-run basis, rather than being valid
across all runs with the same course name. Old group membership will still work
across runs, but new beta-testers will only be added to a single course run.
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 @@ -115,7 +115,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.

67 changes: 61 additions & 6 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)
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 @@ -624,12 +682,9 @@ 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()
# Setting course_id to '' makes it not affect the generated hash,
# and thus produce the old per-student anonymous id
return anonymous_id_for_user(user, '')


# TODO: Should be renamed to generic UserGroup, and possibly
Expand Down
38 changes: 36 additions & 2 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, unique_id_for_user
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 @@ -501,3 +501,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)
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