Added enrollment hash to login cookie
This will allow the marketing site to determine if locally-stored enrollment data needs to be refreshed. ECOM-4896
This commit is contained in:
@@ -1,17 +1,42 @@
|
|||||||
"""
|
"""
|
||||||
Utility functions for setting "logged in" cookies used by subdomains.
|
Utility functions for setting "logged in" cookies used by subdomains.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import time
|
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
from django.dispatch import Signal
|
import six
|
||||||
|
|
||||||
from django.utils.http import cookie_date
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
from django.core.urlresolvers import reverse, NoReverseMatch
|
||||||
|
from django.dispatch import Signal
|
||||||
|
from django.utils.http import cookie_date
|
||||||
|
|
||||||
CREATE_LOGON_COOKIE = Signal(providing_args=["user", "response"])
|
from student.models import CourseEnrollment
|
||||||
|
|
||||||
|
CREATE_LOGON_COOKIE = Signal(providing_args=['user', 'response'])
|
||||||
|
|
||||||
|
|
||||||
|
def _get_cookie_settings(request):
|
||||||
|
""" Returns the common cookie settings (e.g. expiration time). """
|
||||||
|
|
||||||
|
if request.session.get_expire_at_browser_close():
|
||||||
|
max_age = None
|
||||||
|
expires = None
|
||||||
|
else:
|
||||||
|
max_age = request.session.get_expiry_age()
|
||||||
|
expires_time = time.time() + max_age
|
||||||
|
expires = cookie_date(expires_time)
|
||||||
|
|
||||||
|
cookie_settings = {
|
||||||
|
'max_age': max_age,
|
||||||
|
'expires': expires,
|
||||||
|
'domain': settings.SESSION_COOKIE_DOMAIN,
|
||||||
|
'path': '/',
|
||||||
|
'httponly': None,
|
||||||
|
}
|
||||||
|
|
||||||
|
return cookie_settings
|
||||||
|
|
||||||
|
|
||||||
def set_logged_in_cookies(request, response, user):
|
def set_logged_in_cookies(request, response, user):
|
||||||
@@ -31,7 +56,6 @@ def set_logged_in_cookies(request, response, user):
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"username": "test-user",
|
"username": "test-user",
|
||||||
"email": "test-user@example.com",
|
|
||||||
"header_urls": {
|
"header_urls": {
|
||||||
"account_settings": "https://example.com/account/settings",
|
"account_settings": "https://example.com/account/settings",
|
||||||
"learner_profile": "https://example.com/u/test-user",
|
"learner_profile": "https://example.com/u/test-user",
|
||||||
@@ -49,21 +73,7 @@ def set_logged_in_cookies(request, response, user):
|
|||||||
HttpResponse
|
HttpResponse
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if request.session.get_expire_at_browser_close():
|
cookie_settings = _get_cookie_settings(request)
|
||||||
max_age = None
|
|
||||||
expires = None
|
|
||||||
else:
|
|
||||||
max_age = request.session.get_expiry_age()
|
|
||||||
expires_time = time.time() + max_age
|
|
||||||
expires = cookie_date(expires_time)
|
|
||||||
|
|
||||||
cookie_settings = {
|
|
||||||
'max_age': max_age,
|
|
||||||
'expires': expires,
|
|
||||||
'domain': settings.SESSION_COOKIE_DOMAIN,
|
|
||||||
'path': '/',
|
|
||||||
'httponly': None,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Backwards compatibility: set the cookie indicating that the user
|
# Backwards compatibility: set the cookie indicating that the user
|
||||||
# is logged in. This is just a boolean value, so it's not very useful.
|
# is logged in. This is just a boolean value, so it's not very useful.
|
||||||
@@ -76,6 +86,42 @@ def set_logged_in_cookies(request, response, user):
|
|||||||
**cookie_settings
|
**cookie_settings
|
||||||
)
|
)
|
||||||
|
|
||||||
|
set_user_info_cookie(response, request)
|
||||||
|
|
||||||
|
# give signal receivers a chance to add cookies
|
||||||
|
CREATE_LOGON_COOKIE.send(sender=None, user=user, response=response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def set_user_info_cookie(response, request):
|
||||||
|
""" Sets the user info cookie on the response. """
|
||||||
|
cookie_settings = _get_cookie_settings(request)
|
||||||
|
|
||||||
|
# In production, TLS should be enabled so that this cookie is encrypted
|
||||||
|
# when we send it. We also need to set "secure" to True so that the browser
|
||||||
|
# will transmit it only over secure connections.
|
||||||
|
#
|
||||||
|
# In non-production environments (acceptance tests, devstack, and sandboxes),
|
||||||
|
# we still want to set this cookie. However, we do NOT want to set it to "secure"
|
||||||
|
# because the browser won't send it back to us. This can cause an infinite redirect
|
||||||
|
# loop in the third-party auth flow, which calls `is_logged_in_cookie_set` to determine
|
||||||
|
# whether it needs to set the cookie or continue to the next pipeline stage.
|
||||||
|
user_info_cookie_is_secure = request.is_secure()
|
||||||
|
user_info = get_user_info_cookie_data(request)
|
||||||
|
|
||||||
|
response.set_cookie(
|
||||||
|
settings.EDXMKTG_USER_INFO_COOKIE_NAME.encode('utf-8'),
|
||||||
|
json.dumps(user_info),
|
||||||
|
secure=user_info_cookie_is_secure,
|
||||||
|
**cookie_settings
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_info_cookie_data(request):
|
||||||
|
""" Returns information that wil populate the user info cookie. """
|
||||||
|
user = request.user
|
||||||
|
|
||||||
# Set a cookie with user info. This can be used by external sites
|
# Set a cookie with user info. This can be used by external sites
|
||||||
# to customize content based on user information. Currently,
|
# to customize content based on user information. Currently,
|
||||||
# we include information that's used to customize the "account"
|
# we include information that's used to customize the "account"
|
||||||
@@ -94,38 +140,17 @@ def set_logged_in_cookies(request, response, user):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Convert relative URL paths to absolute URIs
|
# Convert relative URL paths to absolute URIs
|
||||||
for url_name, url_path in header_urls.iteritems():
|
for url_name, url_path in six.iteritems(header_urls):
|
||||||
header_urls[url_name] = request.build_absolute_uri(url_path)
|
header_urls[url_name] = request.build_absolute_uri(url_path)
|
||||||
|
|
||||||
user_info = {
|
user_info = {
|
||||||
'version': settings.EDXMKTG_USER_INFO_COOKIE_VERSION,
|
'version': settings.EDXMKTG_USER_INFO_COOKIE_VERSION,
|
||||||
'username': user.username,
|
'username': user.username,
|
||||||
'email': user.email,
|
|
||||||
'header_urls': header_urls,
|
'header_urls': header_urls,
|
||||||
|
'enrollmentStatusHash': CourseEnrollment.generate_enrollment_status_hash(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
# In production, TLS should be enabled so that this cookie is encrypted
|
return user_info
|
||||||
# when we send it. We also need to set "secure" to True so that the browser
|
|
||||||
# will transmit it only over secure connections.
|
|
||||||
#
|
|
||||||
# In non-production environments (acceptance tests, devstack, and sandboxes),
|
|
||||||
# we still want to set this cookie. However, we do NOT want to set it to "secure"
|
|
||||||
# because the browser won't send it back to us. This can cause an infinite redirect
|
|
||||||
# loop in the third-party auth flow, which calls `is_logged_in_cookie_set` to determine
|
|
||||||
# whether it needs to set the cookie or continue to the next pipeline stage.
|
|
||||||
user_info_cookie_is_secure = request.is_secure()
|
|
||||||
|
|
||||||
response.set_cookie(
|
|
||||||
settings.EDXMKTG_USER_INFO_COOKIE_NAME.encode('utf-8'),
|
|
||||||
json.dumps(user_info),
|
|
||||||
secure=user_info_cookie_is_secure,
|
|
||||||
**cookie_settings
|
|
||||||
)
|
|
||||||
|
|
||||||
# give signal receivers a chance to add cookies
|
|
||||||
CREATE_LOGON_COOKIE.send(sender=None, user=user, response=response)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
def delete_logged_in_cookies(response):
|
def delete_logged_in_cookies(response):
|
||||||
|
|||||||
@@ -10,55 +10,53 @@ file and check it in at the same time as your model changes. To do that,
|
|||||||
2. ./manage.py lms schemamigration student --auto description_of_your_change
|
2. ./manage.py lms schemamigration student --auto description_of_your_change
|
||||||
3. Add the migration file created in edx-platform/common/djangoapps/student/migrations/
|
3. Add the migration file created in edx-platform/common/djangoapps/student/migrations/
|
||||||
"""
|
"""
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
from collections import defaultdict, OrderedDict, namedtuple
|
from collections import defaultdict, OrderedDict, namedtuple
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from functools import total_ordering
|
from functools import total_ordering
|
||||||
import hashlib
|
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from pytz import UTC
|
|
||||||
from urllib import urlencode
|
from urllib import urlencode
|
||||||
import uuid
|
|
||||||
|
|
||||||
import analytics
|
import analytics
|
||||||
|
import dogstats_wrapper as dog_stats_api
|
||||||
from config_models.models import ConfigurationModel
|
from config_models.models import ConfigurationModel
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
|
||||||
from django.db import models, IntegrityError, transaction
|
from django.db import models, IntegrityError, transaction
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.db.models.signals import pre_save, post_save
|
from django.db.models.signals import pre_save, post_save
|
||||||
from django.dispatch import receiver, Signal
|
from django.dispatch import receiver, Signal
|
||||||
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.translation import ugettext_noop
|
from django.utils.translation import ugettext_noop
|
||||||
from django.core.cache import cache
|
|
||||||
from django_countries.fields import CountryField
|
from django_countries.fields import CountryField
|
||||||
import dogstats_wrapper as dog_stats_api
|
|
||||||
from eventtracking import tracker
|
from eventtracking import tracker
|
||||||
from model_utils.models import TimeStampedModel
|
from model_utils.models import TimeStampedModel
|
||||||
from opaque_keys.edx.keys import CourseKey
|
from opaque_keys.edx.keys import CourseKey
|
||||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||||
|
from pytz import UTC
|
||||||
from simple_history.models import HistoricalRecords
|
from simple_history.models import HistoricalRecords
|
||||||
from track import contexts
|
|
||||||
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField, NoneToEmptyManager
|
|
||||||
|
|
||||||
|
import lms.lib.comment_client as cc
|
||||||
|
import request_cache
|
||||||
from certificates.models import GeneratedCertificate
|
from certificates.models import GeneratedCertificate
|
||||||
from course_modes.models import CourseMode
|
from course_modes.models import CourseMode
|
||||||
from enrollment.api import _default_course_mode
|
from enrollment.api import _default_course_mode
|
||||||
import lms.lib.comment_client as cc
|
|
||||||
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client, ECOMMERCE_DATE_FORMAT
|
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client, ECOMMERCE_DATE_FORMAT
|
||||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||||
import request_cache
|
|
||||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||||
|
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField, NoneToEmptyManager
|
||||||
|
from track import contexts
|
||||||
|
from util.milestones_helpers import is_entrance_exams_enabled
|
||||||
from util.model_utils import emit_field_changed_events, get_changed_fields_dict
|
from util.model_utils import emit_field_changed_events, get_changed_fields_dict
|
||||||
from util.query import use_read_replica_if_available
|
from util.query import use_read_replica_if_available
|
||||||
from util.milestones_helpers import is_entrance_exams_enabled
|
|
||||||
|
|
||||||
|
|
||||||
UNENROLL_DONE = Signal(providing_args=["course_enrollment", "skip_refund"])
|
UNENROLL_DONE = Signal(providing_args=["course_enrollment", "skip_refund"])
|
||||||
ENROLL_STATUS_CHANGE = Signal(providing_args=["event", "user", "course_id", "mode", "cost", "currency"])
|
ENROLL_STATUS_CHANGE = Signal(providing_args=["event", "user", "course_id", "mode", "cost", "currency"])
|
||||||
@@ -1030,6 +1028,13 @@ class CourseEnrollment(models.Model):
|
|||||||
"[CourseEnrollment] {}: {} ({}); active: ({})"
|
"[CourseEnrollment] {}: {} ({}); active: ({})"
|
||||||
).format(self.user, self.course_id, self.created, self.is_active)
|
).format(self.user, self.course_id, self.created, self.is_active)
|
||||||
|
|
||||||
|
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||||
|
super(CourseEnrollment, self).save(force_insert=force_insert, force_update=force_update, using=using,
|
||||||
|
update_fields=update_fields)
|
||||||
|
|
||||||
|
# Delete the cached status hash, forcing the value to be recalculated the next time it is needed.
|
||||||
|
cache.delete(self.enrollment_status_hash_cache_key(self.user))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def get_or_create_enrollment(cls, user, course_key):
|
def get_or_create_enrollment(cls, user, course_key):
|
||||||
@@ -1449,6 +1454,47 @@ class CourseEnrollment(models.Model):
|
|||||||
def enrollments_for_user(cls, user):
|
def enrollments_for_user(cls, user):
|
||||||
return cls.objects.filter(user=user, is_active=1)
|
return cls.objects.filter(user=user, is_active=1)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def enrollment_status_hash_cache_key(cls, user):
|
||||||
|
""" Returns the cache key for the cached enrollment status hash.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user (User): User whose cache key should be returned.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Cache key.
|
||||||
|
"""
|
||||||
|
return 'enrollment_status_hash_' + user.username
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_enrollment_status_hash(cls, user):
|
||||||
|
""" Generates a hash encoding the given user's *active* enrollments.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user (User): User whose enrollments should be hashed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Hash of the user's active enrollments. If the user is anonymous, `None` is returned.
|
||||||
|
"""
|
||||||
|
if user.is_anonymous():
|
||||||
|
return None
|
||||||
|
|
||||||
|
cache_key = cls.enrollment_status_hash_cache_key(user)
|
||||||
|
status_hash = cache.get(cache_key)
|
||||||
|
|
||||||
|
if not status_hash:
|
||||||
|
enrollments = cls.enrollments_for_user(user).values_list('course_id', 'mode')
|
||||||
|
enrollments = [(e[0].lower(), e[1].lower()) for e in enrollments]
|
||||||
|
enrollments = sorted(enrollments, key=lambda e: e[0])
|
||||||
|
hash_elements = [user.username]
|
||||||
|
hash_elements += ['{course_id}={mode}'.format(course_id=e[0], mode=e[1]) for e in enrollments]
|
||||||
|
status_hash = hashlib.md5('&'.join(hash_elements).encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
# The hash is cached indefinitely. It will be invalidated when the user enrolls/unenrolls.
|
||||||
|
cache.set(cache_key, status_hash, None)
|
||||||
|
|
||||||
|
return status_hash
|
||||||
|
|
||||||
def is_paid_course(self):
|
def is_paid_course(self):
|
||||||
"""
|
"""
|
||||||
Returns True, if course is paid
|
Returns True, if course is paid
|
||||||
|
|||||||
57
common/djangoapps/student/tests/test_cookies.py
Normal file
57
common/djangoapps/student/tests/test_cookies.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# pylint: disable=missing-docstring
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import six
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.test import RequestFactory
|
||||||
|
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||||
|
from xmodule.modulestore.tests.factories import CourseFactory
|
||||||
|
|
||||||
|
from student.cookies import get_user_info_cookie_data
|
||||||
|
from student.models import CourseEnrollment
|
||||||
|
from student.tests.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
|
class CookieTests(SharedModuleStoreTestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super(CookieTests, cls).setUpClass()
|
||||||
|
cls.course = CourseFactory()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(CookieTests, self).setUp()
|
||||||
|
self.user = UserFactory.create()
|
||||||
|
|
||||||
|
def _get_expected_header_urls(self, request):
|
||||||
|
expected_header_urls = {
|
||||||
|
'logout': reverse('logout'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Studio (CMS) does not have the URLs below
|
||||||
|
if settings.ROOT_URLCONF == 'lms.urls':
|
||||||
|
expected_header_urls.update({
|
||||||
|
'account_settings': reverse('account_settings'),
|
||||||
|
'learner_profile': reverse('learner_profile', kwargs={'username': self.user.username}),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Convert relative URL paths to absolute URIs
|
||||||
|
for url_name, url_path in six.iteritems(expected_header_urls):
|
||||||
|
expected_header_urls[url_name] = request.build_absolute_uri(url_path)
|
||||||
|
|
||||||
|
return expected_header_urls
|
||||||
|
|
||||||
|
def test_get_user_info_cookie_data(self):
|
||||||
|
request = RequestFactory().get('/')
|
||||||
|
request.user = self.user
|
||||||
|
|
||||||
|
actual = get_user_info_cookie_data(request)
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'version': settings.EDXMKTG_USER_INFO_COOKIE_VERSION,
|
||||||
|
'username': self.user.username,
|
||||||
|
'header_urls': self._get_expected_header_urls(request),
|
||||||
|
'enrollmentStatusHash': CourseEnrollment.generate_enrollment_status_hash(self.user)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertDictEqual(actual, expected)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'''
|
"""
|
||||||
Tests for student activation and login
|
Tests for student activation and login
|
||||||
'''
|
"""
|
||||||
import json
|
import json
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
@@ -30,9 +30,9 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
|||||||
|
|
||||||
|
|
||||||
class LoginTest(CacheIsolationTestCase):
|
class LoginTest(CacheIsolationTestCase):
|
||||||
'''
|
"""
|
||||||
Test student.views.login_user() view
|
Test student.views.login_user() view
|
||||||
'''
|
"""
|
||||||
|
|
||||||
ENABLED_CACHES = ['default']
|
ENABLED_CACHES = ['default']
|
||||||
|
|
||||||
@@ -171,12 +171,8 @@ class LoginTest(CacheIsolationTestCase):
|
|||||||
cookie = self.client.cookies[settings.EDXMKTG_USER_INFO_COOKIE_NAME]
|
cookie = self.client.cookies[settings.EDXMKTG_USER_INFO_COOKIE_NAME]
|
||||||
user_info = json.loads(cookie.value)
|
user_info = json.loads(cookie.value)
|
||||||
|
|
||||||
# Check that the version is set
|
|
||||||
self.assertEqual(user_info["version"], settings.EDXMKTG_USER_INFO_COOKIE_VERSION)
|
self.assertEqual(user_info["version"], settings.EDXMKTG_USER_INFO_COOKIE_VERSION)
|
||||||
|
|
||||||
# Check that the username and email are set
|
|
||||||
self.assertEqual(user_info["username"], self.user.username)
|
self.assertEqual(user_info["username"], self.user.username)
|
||||||
self.assertEqual(user_info["email"], self.user.email)
|
|
||||||
|
|
||||||
# Check that the URLs are absolute
|
# Check that the URLs are absolute
|
||||||
for url in user_info["header_urls"].values():
|
for url in user_info["header_urls"].values():
|
||||||
|
|||||||
84
common/djangoapps/student/tests/test_models.py
Normal file
84
common/djangoapps/student/tests/test_models.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# pylint: disable=missing-docstring
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.db.models.functions import Lower
|
||||||
|
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||||
|
from xmodule.modulestore.tests.factories import CourseFactory
|
||||||
|
|
||||||
|
from student.models import CourseEnrollment
|
||||||
|
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||||
|
|
||||||
|
|
||||||
|
class CourseEnrollmentTests(SharedModuleStoreTestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super(CourseEnrollmentTests, cls).setUpClass()
|
||||||
|
cls.course = CourseFactory()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(CourseEnrollmentTests, self).setUp()
|
||||||
|
self.user = UserFactory.create()
|
||||||
|
|
||||||
|
def test_enrollment_status_hash_cache_key(self):
|
||||||
|
username = 'test-user'
|
||||||
|
user = UserFactory(username=username)
|
||||||
|
expected = 'enrollment_status_hash_' + username
|
||||||
|
self.assertEqual(CourseEnrollment.enrollment_status_hash_cache_key(user), expected)
|
||||||
|
|
||||||
|
def assert_enrollment_status_hash_cached(self, user, expected_value):
|
||||||
|
self.assertEqual(cache.get(CourseEnrollment.enrollment_status_hash_cache_key(user)), expected_value)
|
||||||
|
|
||||||
|
def test_generate_enrollment_status_hash(self):
|
||||||
|
""" Verify the method returns a hash of a user's current enrollments. """
|
||||||
|
# Return None for anonymous users
|
||||||
|
self.assertIsNone(CourseEnrollment.generate_enrollment_status_hash(AnonymousUser()))
|
||||||
|
|
||||||
|
# No enrollments
|
||||||
|
expected = hashlib.md5(self.user.username).hexdigest()
|
||||||
|
self.assertEqual(CourseEnrollment.generate_enrollment_status_hash(self.user), expected)
|
||||||
|
self.assert_enrollment_status_hash_cached(self.user, expected)
|
||||||
|
|
||||||
|
# No active enrollments
|
||||||
|
enrollment_mode = 'verified'
|
||||||
|
course_id = self.course.id # pylint: disable=no-member
|
||||||
|
enrollment = CourseEnrollmentFactory.create(user=self.user, course_id=course_id, mode=enrollment_mode,
|
||||||
|
is_active=False)
|
||||||
|
self.assertEqual(CourseEnrollment.generate_enrollment_status_hash(self.user), expected)
|
||||||
|
self.assert_enrollment_status_hash_cached(self.user, expected)
|
||||||
|
|
||||||
|
# One active enrollment
|
||||||
|
enrollment.is_active = True
|
||||||
|
enrollment.save()
|
||||||
|
expected = '{username}&{course_id}={mode}'.format(
|
||||||
|
username=self.user.username, course_id=str(course_id).lower(), mode=enrollment_mode.lower()
|
||||||
|
)
|
||||||
|
expected = hashlib.md5(expected).hexdigest()
|
||||||
|
self.assertEqual(CourseEnrollment.generate_enrollment_status_hash(self.user), expected)
|
||||||
|
self.assert_enrollment_status_hash_cached(self.user, expected)
|
||||||
|
|
||||||
|
# Multiple enrollments
|
||||||
|
CourseEnrollmentFactory.create(user=self.user)
|
||||||
|
enrollments = CourseEnrollment.enrollments_for_user(self.user).order_by(Lower('course_id'))
|
||||||
|
hash_elements = [self.user.username]
|
||||||
|
hash_elements += [
|
||||||
|
'{course_id}={mode}'.format(course_id=str(enrollment.course_id).lower(), mode=enrollment.mode.lower()) for
|
||||||
|
enrollment in enrollments]
|
||||||
|
expected = hashlib.md5('&'.join(hash_elements)).hexdigest()
|
||||||
|
self.assertEqual(CourseEnrollment.generate_enrollment_status_hash(self.user), expected)
|
||||||
|
self.assert_enrollment_status_hash_cached(self.user, expected)
|
||||||
|
|
||||||
|
def test_save_deletes_cached_enrollment_status_hash(self):
|
||||||
|
""" Verify the method deletes the cached enrollment status hash for the user. """
|
||||||
|
# There should be no cached value for a new user with no enrollments.
|
||||||
|
self.assertIsNone(cache.get(CourseEnrollment.enrollment_status_hash_cache_key(self.user)))
|
||||||
|
|
||||||
|
# Generating a status hash should cache the generated value.
|
||||||
|
status_hash = CourseEnrollment.generate_enrollment_status_hash(self.user)
|
||||||
|
self.assert_enrollment_status_hash_cached(self.user, status_hash)
|
||||||
|
|
||||||
|
# Modifying enrollments should delete the cached value.
|
||||||
|
CourseEnrollmentFactory.create(user=self.user)
|
||||||
|
self.assertIsNone(cache.get(CourseEnrollment.enrollment_status_hash_cache_key(self.user)))
|
||||||
@@ -99,6 +99,7 @@ rules==1.1.1
|
|||||||
scipy==0.14.0
|
scipy==0.14.0
|
||||||
Shapely==1.2.16
|
Shapely==1.2.16
|
||||||
singledispatch==3.4.0.2
|
singledispatch==3.4.0.2
|
||||||
|
six>=1.10.0,<2.0.0
|
||||||
sorl-thumbnail==12.3
|
sorl-thumbnail==12.3
|
||||||
sortedcontainers==0.9.2
|
sortedcontainers==0.9.2
|
||||||
stevedore==1.10.0
|
stevedore==1.10.0
|
||||||
|
|||||||
Reference in New Issue
Block a user