Commit 3a4a1e94 by Jason Bau Committed by Giulio Gratta

initial commit of sneak peek support for courses

* some more tests

* make preview work with shib classes

* Styled unauth access button and banner.
  - renamed "preview" button to "Explore Course" and styled.
  - moved banner from main navigation to course navigation.
  - moved span tag inside link to make clickable area larger

Conflicts:
	common/djangoapps/student/tests/factories.py
	common/djangoapps/student/views.py
	lms/djangoapps/courseware/tests/test_access.py
parent ddb9e9db
...@@ -259,6 +259,8 @@ class ShibSPTest(ModuleStoreTestCase): ...@@ -259,6 +259,8 @@ class ShibSPTest(ModuleStoreTestCase):
'honor_code': 'true'} 'honor_code': 'true'}
# use RequestFactory instead of TestClient here because we want access to request.user # use RequestFactory instead of TestClient here because we want access to request.user
request2 = self.request_factory.post('/create_account', data=postvars) request2 = self.request_factory.post('/create_account', data=postvars)
# saving session because create_account deletes existing sessions (due to logout then login)
saved_eamap = client.session['ExternalAuthMap']
request2.session = client.session request2.session = client.session
request2.user = AnonymousUser() request2.user = AnonymousUser()
with patch('student.views.AUDIT_LOG') as mock_audit_log: with patch('student.views.AUDIT_LOG') as mock_audit_log:
...@@ -307,14 +309,13 @@ class ShibSPTest(ModuleStoreTestCase): ...@@ -307,14 +309,13 @@ class ShibSPTest(ModuleStoreTestCase):
if sn_empty and given_name_empty: if sn_empty and given_name_empty:
self.assertEqual(profile.name, postvars['name']) self.assertEqual(profile.name, postvars['name'])
else: else:
self.assertEqual(profile.name, request2.session['ExternalAuthMap'].external_name) self.assertEqual(profile.name, saved_eamap.external_name)
self.assertNotIn(u';', profile.name) self.assertNotIn(u';', profile.name)
else: else:
self.assertEqual(profile.name, request2.session['ExternalAuthMap'].external_name) self.assertEqual(profile.name, saved_eamap.external_name)
self.assertEqual(profile.name, identity.get('displayName')) self.assertEqual(profile.name, identity.get('displayName'))
# clean up for next loop # clean up for next loop
request2.session['ExternalAuthMap'].delete()
UserProfile.objects.filter(user=user).delete() UserProfile.objects.filter(user=user).delete()
Registration.objects.filter(user=user).delete() Registration.objects.filter(user=user).delete()
user.delete() user.delete()
......
...@@ -528,7 +528,9 @@ def course_specific_login(request, course_id): ...@@ -528,7 +528,9 @@ def course_specific_login(request, course_id):
return _redirect_with_get_querydict('signin_user', request.GET) return _redirect_with_get_querydict('signin_user', request.GET)
# now the dispatching conditionals. Only shib for now # now the dispatching conditionals. Only shib for now
if settings.MITX_FEATURES.get('AUTH_USE_SHIB') and course.enrollment_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX): if (settings.MITX_FEATURES.get('AUTH_USE_SHIB') and
course.enrollment_domain and
course.enrollment_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX)):
return _redirect_with_get_querydict('shib-login', request.GET) return _redirect_with_get_querydict('shib-login', request.GET)
# Default fallthrough to normal signin page # Default fallthrough to normal signin page
...@@ -547,7 +549,9 @@ def course_specific_register(request, course_id): ...@@ -547,7 +549,9 @@ def course_specific_register(request, course_id):
return _redirect_with_get_querydict('register_user', request.GET) return _redirect_with_get_querydict('register_user', request.GET)
# now the dispatching conditionals. Only shib for now # now the dispatching conditionals. Only shib for now
if settings.MITX_FEATURES.get('AUTH_USE_SHIB') and course.enrollment_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX): if (settings.MITX_FEATURES.get('AUTH_USE_SHIB') and
course.enrollment_domain and
course.enrollment_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX)):
# shib-login takes care of both registration and login flows # shib-login takes care of both registration and login flows
return _redirect_with_get_querydict('shib-login', request.GET) return _redirect_with_get_querydict('shib-login', request.GET)
......
...@@ -20,8 +20,9 @@ import uuid ...@@ -20,8 +20,9 @@ import uuid
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.crypto import get_random_string
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.db import models from django.db import models, transaction
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.forms import ModelForm, forms from django.forms import ModelForm, forms
...@@ -123,6 +124,9 @@ class UserProfile(models.Model): ...@@ -123,6 +124,9 @@ class UserProfile(models.Model):
goals = models.TextField(blank=True, null=True) goals = models.TextField(blank=True, null=True)
allow_certificate = models.BooleanField(default=1) allow_certificate = models.BooleanField(default=1)
# "nonregistered" users are auto-created and have no meaningful profile info
nonregistered = models.BooleanField(default=False)
def get_meta(self): def get_meta(self):
js_str = self.meta js_str = self.meta
if not js_str: if not js_str:
...@@ -135,6 +139,38 @@ class UserProfile(models.Model): ...@@ -135,6 +139,38 @@ class UserProfile(models.Model):
def set_meta(self, js): def set_meta(self, js):
self.meta = json.dumps(js) self.meta = json.dumps(js)
@classmethod
def get_random_anon_username(cls):
candidate = "anon__{}".format(get_random_string(24)) # django 1.4 has 30 char usernames
while User.objects.filter(username=candidate).exists():
candidate = "anon__{}".format(get_random_string(24)) # get_random_string output is alphanumeric
return candidate
@classmethod
@transaction.commit_on_success
def create_nonregistered_user(cls):
anon_username = cls.get_random_anon_username()
email_split = settings.ANONYMOUS_USER_EMAIL.split('@')
anon_email = "{}+{}@{}".format(email_split[0],
anon_username,
email_split[-1])
anon_user = User(username=anon_username, email=anon_email, is_active=False)
anon_user.save()
profile = UserProfile(user=anon_user, nonregistered=True)
profile.save()
return anon_user
@classmethod
def has_registered(cls, user):
"""
Handles django anonymous users. SHOULD use this to test whether request.user has registered,
i.e. has a profile that says not nonregistered,
instead of directly accessing user.profile.nonregistered,
because if the user is AnonymousUser it won't have a profile.
"""
return hasattr(user, 'profile') and not user.profile.nonregistered
TEST_CENTER_STATUS_ACCEPTED = "Accepted" TEST_CENTER_STATUS_ACCEPTED = "Accepted"
TEST_CENTER_STATUS_ERROR = "Error" TEST_CENTER_STATUS_ERROR = "Error"
......
...@@ -79,6 +79,30 @@ class UserFactory(DjangoModelFactory): ...@@ -79,6 +79,30 @@ class UserFactory(DjangoModelFactory):
else: else:
return None return None
@post_generation
def groups(self, create, extracted, **kwargs):
if extracted is None:
return
if isinstance(extracted, basestring):
extracted = [extracted]
for group_name in extracted:
self.groups.add(GroupFactory.simple_generate(create, name=group_name))
class NonRegisteredUserFactory(UserFactory):
# only difference from UserFactory is the profile has nonregistered bit set
@classmethod
def _after_postgeneration(cls, obj, create, results=None):
if create:
obj.profile.nonregistered = True
obj.profile.save()
class AnonymousUserFactory(Factory):
FACTORY_FOR = AnonymousUser
class AdminFactory(UserFactory): class AdminFactory(UserFactory):
is_staff = True is_staff = True
......
...@@ -410,7 +410,7 @@ class PaidRegistrationTest(ModuleStoreTestCase): ...@@ -410,7 +410,7 @@ class PaidRegistrationTest(ModuleStoreTestCase):
self.req_factory = RequestFactory() self.req_factory = RequestFactory()
self.course = CourseFactory.create(org=self.COURSE_ORG, display_name=self.COURSE_NAME, number=self.COURSE_SLUG) self.course = CourseFactory.create(org=self.COURSE_ORG, display_name=self.COURSE_NAME, number=self.COURSE_SLUG)
self.assertIsNotNone(self.course) self.assertIsNotNone(self.course)
self.user = User.objects.create(username="jack", email="jack@fake.edx.org") self.user = UserFactory(username="jack", email="jack@fake.edx.org")
@unittest.skipUnless(settings.MITX_FEATURES.get('ENABLE_SHOPPING_CART'), "Shopping Cart not enabled in settings") @unittest.skipUnless(settings.MITX_FEATURES.get('ENABLE_SHOPPING_CART'), "Shopping Cart not enabled in settings")
def test_change_enrollment_add_to_cart(self): def test_change_enrollment_add_to_cart(self):
......
...@@ -58,6 +58,7 @@ from collections import namedtuple ...@@ -58,6 +58,7 @@ from collections import namedtuple
from courseware.courses import get_courses, sort_by_announcement from courseware.courses import get_courses, sort_by_announcement
from courseware.access import has_access from courseware.access import has_access
from courseware.models import CoursePreference
from external_auth.models import ExternalAuthMap from external_auth.models import ExternalAuthMap
import external_auth.views import external_auth.views
...@@ -252,7 +253,7 @@ def signin_user(request): ...@@ -252,7 +253,7 @@ def signin_user(request):
""" """
This view will display the non-modal login form This view will display the non-modal login form
""" """
if request.user.is_authenticated(): if UserProfile.has_registered(request.user):
return redirect(reverse('dashboard')) return redirect(reverse('dashboard'))
context = { context = {
...@@ -270,7 +271,7 @@ def register_user(request, extra_context=None): ...@@ -270,7 +271,7 @@ def register_user(request, extra_context=None):
if settings.MITX_FEATURES.get('USE_CME_REGISTRATION'): if settings.MITX_FEATURES.get('USE_CME_REGISTRATION'):
return cme_register_user(request, extra_context=extra_context) return cme_register_user(request, extra_context=extra_context)
if request.user.is_authenticated(): if UserProfile.has_registered(request.user):
return redirect(reverse('dashboard')) return redirect(reverse('dashboard'))
context = { context = {
...@@ -313,6 +314,9 @@ def complete_course_mode_info(course_id, enrollment): ...@@ -313,6 +314,9 @@ def complete_course_mode_info(course_id, enrollment):
def dashboard(request): def dashboard(request):
user = request.user user = request.user
if not UserProfile.has_registered(user):
logout(request)
return redirect(reverse('dashboard'))
# Build our courses list for the user, but ignore any courses that no longer # Build our courses list for the user, but ignore any courses that no longer
# exist (because the course IDs have changed). Still, we don't delete those # exist (because the course IDs have changed). Still, we don't delete those
# enrollments, because it could have been a data push snafu. # enrollments, because it could have been a data push snafu.
...@@ -374,6 +378,36 @@ def dashboard(request): ...@@ -374,6 +378,36 @@ def dashboard(request):
return render_to_response('dashboard.html', context) return render_to_response('dashboard.html', context)
def _create_and_login_nonregistered_user(request):
new_student = UserProfile.create_nonregistered_user()
new_student.backend = settings.AUTHENTICATION_BACKENDS[0]
login(request, new_student)
request.session.set_expiry(604800) # set session to very long to reduce number of nonreg users created
@require_POST
def setup_sneakpeek(request, course_id):
if not CoursePreference.course_allows_nonregistered_access(course_id):
return HttpResponseForbidden("Cannot access the course")
if not request.user.is_authenticated():
# if there's no user, create a nonregistered user
_create_and_login_nonregistered_user(request)
elif UserProfile.has_registered(request.user):
# registered users can't sneakpeek, so log them out and create a new nonregistered user
logout(request)
_create_and_login_nonregistered_user(request)
can_enroll, error_msg = _check_can_enroll_in_course(request.user,
course_id,
access_type='within_enrollment_period')
if not can_enroll:
log.error(error_msg)
return HttpResponseBadRequest(error_msg)
CourseEnrollment.enroll(request.user, course_id)
return HttpResponse("OK. Allowed sneakpeek")
def try_change_enrollment(request): def try_change_enrollment(request):
""" """
This method calls change_enrollment if the necessary POST This method calls change_enrollment if the necessary POST
...@@ -424,21 +458,16 @@ def change_enrollment(request): ...@@ -424,21 +458,16 @@ def change_enrollment(request):
if course_id is None: if course_id is None:
return HttpResponseBadRequest(_("Course id not specified")) return HttpResponseBadRequest(_("Course id not specified"))
if not user.is_authenticated(): if not UserProfile.has_registered(user):
return HttpResponseForbidden() return HttpResponseForbidden()
if action == "enroll": if action == "enroll":
# Make sure the course exists # Make sure the course exists
# We don't do this check on unenroll, or a bad course id can't be unenrolled from # We don't do this check on unenroll, or a bad course id can't be unenrolled from
try: can_enroll, error_msg = _check_can_enroll_in_course(user, course_id)
course = course_from_id(course_id)
except ItemNotFoundError:
log.warning("User {0} tried to enroll in non-existent course {1}"
.format(user.username, course_id))
return HttpResponseBadRequest(_("Course id is invalid"))
if not has_access(user, course, 'enroll'): if not can_enroll:
return HttpResponseBadRequest(_("Enrollment is closed")) return HttpResponseBadRequest(error_msg)
# If this course is available in multiple modes, redirect them to a page # If this course is available in multiple modes, redirect them to a page
# where they can choose which mode they want. # where they can choose which mode they want.
...@@ -458,7 +487,7 @@ def change_enrollment(request): ...@@ -458,7 +487,7 @@ def change_enrollment(request):
"run:{0}".format(run)] "run:{0}".format(run)]
) )
CourseEnrollment.enroll(user, course.id, mode=current_mode.slug) CourseEnrollment.enroll(user, course_id, mode=current_mode.slug)
return HttpResponse() return HttpResponse()
...@@ -494,6 +523,24 @@ def change_enrollment(request): ...@@ -494,6 +523,24 @@ def change_enrollment(request):
return HttpResponseBadRequest(_("Enrollment action is invalid")) return HttpResponseBadRequest(_("Enrollment action is invalid"))
def _check_can_enroll_in_course(user, course_id, access_type="enroll"):
"""
Refactored check for user being able to enroll in course
Returns (bool, error_message), where error message is only applicable if bool == False
"""
try:
course = course_from_id(course_id)
except ItemNotFoundError:
log.warning("User {0} tried to enroll in non-existent course {1}"
.format(user.username, course_id))
return False, _("Course id is invalid")
if not has_access(user, course, access_type):
return False, _("Enrollment is closed")
return True, ""
def _parse_course_id_from_string(input_str): def _parse_course_id_from_string(input_str):
""" """
Helper function to determine if input_str (typically the queryparam 'next') contains a course_id. Helper function to determine if input_str (typically the queryparam 'next') contains a course_id.
...@@ -587,6 +634,7 @@ def login_user(request, error=""): ...@@ -587,6 +634,7 @@ def login_user(request, error=""):
try: try:
# We do not log here, because we have a handler registered # We do not log here, because we have a handler registered
# to perform logging on successful logins. # to perform logging on successful logins.
logout(request)
login(request, user) login(request, user)
if request.POST.get('remember') == 'true': if request.POST.get('remember') == 'true':
request.session.set_expiry(604800) request.session.set_expiry(604800)
...@@ -926,6 +974,7 @@ def create_account(request, post_override=None): ...@@ -926,6 +974,7 @@ def create_account(request, post_override=None):
# logged in until they close the browser. They can't log in again until they click # logged in until they close the browser. They can't log in again until they click
# the activation link from the email. # the activation link from the email.
login_user = authenticate(username=post_vars['username'], password=post_vars['password']) login_user = authenticate(username=post_vars['username'], password=post_vars['password'])
logout(request)
login(request, login_user) login(request, login_user)
request.session.set_expiry(0) request.session.set_expiry(0)
...@@ -1529,4 +1578,3 @@ def change_email_settings(request): ...@@ -1529,4 +1578,3 @@ def change_email_settings(request):
track.views.server_track(request, "change-email-settings", {"receive_emails": "no", "course": course_id}, page='dashboard') track.views.server_track(request, "change-email-settings", {"receive_emails": "no", "course": course_id}, page='dashboard')
return HttpResponse(json.dumps({'success': True})) return HttpResponse(json.dumps({'success': True}))
...@@ -8,6 +8,7 @@ from mitxmako.shortcuts import render_to_response ...@@ -8,6 +8,7 @@ from mitxmako.shortcuts import render_to_response
import student.views import student.views
import branding import branding
import courseware.views import courseware.views
from student.models import UserProfile
from mitxmako.shortcuts import marketing_link from mitxmako.shortcuts import marketing_link
from util.cache import cache_if_anonymous from util.cache import cache_if_anonymous
...@@ -19,7 +20,7 @@ def index(request): ...@@ -19,7 +20,7 @@ def index(request):
Redirects to main page -- info page if user authenticated, or marketing if not Redirects to main page -- info page if user authenticated, or marketing if not
''' '''
if settings.COURSEWARE_ENABLED and request.user.is_authenticated(): if settings.COURSEWARE_ENABLED and UserProfile.has_registered(request.user):
return redirect(reverse('dashboard')) return redirect(reverse('dashboard'))
if settings.MITX_FEATURES.get('AUTH_USE_MIT_CERTIFICATES'): if settings.MITX_FEATURES.get('AUTH_USE_MIT_CERTIFICATES'):
......
...@@ -13,7 +13,7 @@ from xmodule.error_module import ErrorDescriptor ...@@ -13,7 +13,7 @@ from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.x_module import XModule, XModuleDescriptor from xmodule.x_module import XModule, XModuleDescriptor
from student.models import CourseEnrollmentAllowed from student.models import CourseEnrollmentAllowed, UserProfile
from external_auth.models import ExternalAuthMap from external_auth.models import ExternalAuthMap
from courseware.masquerade import is_masquerading_as_student from courseware.masquerade import is_masquerading_as_student
from django.utils.timezone import UTC from django.utils.timezone import UTC
...@@ -134,10 +134,22 @@ def _has_access_course_desc(user, course, action): ...@@ -134,10 +134,22 @@ def _has_access_course_desc(user, course, action):
""" """
Can this user access the forums in this course? Can this user access the forums in this course?
""" """
return (can_load() and \ return (
(CourseEnrollment.is_enrolled(user, course.id) or \ can_load() and
_has_staff_access_to_descriptor(user, course) UserProfile.has_registered(user) and
)) (CourseEnrollment.is_enrolled(user, course.id) or
_has_staff_access_to_descriptor(user, course))
)
def within_enrollment_period():
"""
Just a time boundary check, handles if start or stop were set to None
"""
now = datetime.now(UTC())
start = course.enrollment_start
end = course.enrollment_end
return (start is None or now > start) and (end is None or now < end)
def can_enroll(): def can_enroll():
""" """
...@@ -163,11 +175,7 @@ def _has_access_course_desc(user, course, action): ...@@ -163,11 +175,7 @@ def _has_access_course_desc(user, course, action):
else: else:
reg_method_ok = True #if not using this access check, it's always OK. reg_method_ok = True #if not using this access check, it's always OK.
now = datetime.now(UTC()) if reg_method_ok and within_enrollment_period():
start = course.enrollment_start
end = course.enrollment_end
if reg_method_ok and (start is None or now > start) and (end is None or now < end):
# in enrollment period, so any user is allowed to enroll. # in enrollment period, so any user is allowed to enroll.
debug("Allow: in enrollment period") debug("Allow: in enrollment period")
return True return True
...@@ -207,6 +215,7 @@ def _has_access_course_desc(user, course, action): ...@@ -207,6 +215,7 @@ def _has_access_course_desc(user, course, action):
'load_forum': can_load_forum, 'load_forum': can_load_forum,
'enroll': can_enroll, 'enroll': can_enroll,
'see_exists': see_exists, 'see_exists': see_exists,
'within_enrollment_period': within_enrollment_period,
'staff': lambda: _has_staff_access_to_descriptor(user, course), 'staff': lambda: _has_staff_access_to_descriptor(user, course),
'instructor': lambda: _has_instructor_access_to_descriptor(user, course), 'instructor': lambda: _has_instructor_access_to_descriptor(user, course),
} }
...@@ -247,6 +256,47 @@ def _has_access_error_desc(user, descriptor, action, course_context): ...@@ -247,6 +256,47 @@ def _has_access_error_desc(user, descriptor, action, course_context):
return _dispatch(checkers, action, user, descriptor) return _dispatch(checkers, action, user, descriptor)
NONREGISTERED_CATEGORY_WHITELIST = [
"about",
"chapter",
"course",
"course_info",
"problem",
"sequential",
"vertical",
"videoalpha",
# "combinedopenended",
# "discussion",
"html",
# "peergrading",
"static_tab",
"video",
# "annotatable",
"book",
"conditional",
# "crowdsource_hinter",
"custom_tag_template",
# "discuss",
# "error",
"hidden",
"image",
"problemset",
"randomize",
"raw",
"section",
"slides",
"timelimit",
"videodev",
"videosequence",
"word_cloud",
"wrapper",
]
def _can_load_descriptor_nonregistered(descriptor):
return descriptor.category in NONREGISTERED_CATEGORY_WHITELIST
def _has_access_descriptor(user, descriptor, action, course_context=None): def _has_access_descriptor(user, descriptor, action, course_context=None):
""" """
Check if user has access to this descriptor. Check if user has access to this descriptor.
...@@ -266,6 +316,10 @@ def _has_access_descriptor(user, descriptor, action, course_context=None): ...@@ -266,6 +316,10 @@ def _has_access_descriptor(user, descriptor, action, course_context=None):
students to see modules. If not, views should check the course, so we students to see modules. If not, views should check the course, so we
don't have to hit the enrollments table on every module load. don't have to hit the enrollments table on every module load.
""" """
# nonregistered users shouldn't be able to access certain descriptor types
if not UserProfile.has_registered(user):
return _can_load_descriptor_nonregistered(descriptor)
# If start dates are off, can always load # If start dates are off, can always load
if settings.MITX_FEATURES['DISABLE_START_DATES'] and not is_masquerading_as_student(user): if settings.MITX_FEATURES['DISABLE_START_DATES'] and not is_masquerading_as_student(user):
debug("Allow: DISABLE_START_DATES") debug("Allow: DISABLE_START_DATES")
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
django admin pages for courseware model django admin pages for courseware model
''' '''
from courseware.models import StudentModule, OfflineComputedGrade, OfflineComputedGradeLog from courseware.models import StudentModule, OfflineComputedGrade, OfflineComputedGradeLog, CoursePreference
from ratelimitbackend import admin from ratelimitbackend import admin
from django.contrib.auth.models import User from django.contrib.auth.models import User
...@@ -11,3 +11,5 @@ admin.site.register(StudentModule) ...@@ -11,3 +11,5 @@ admin.site.register(StudentModule)
admin.site.register(OfflineComputedGrade) admin.site.register(OfflineComputedGrade)
admin.site.register(OfflineComputedGradeLog) admin.site.register(OfflineComputedGradeLog)
admin.site.register(CoursePreference)
\ No newline at end of file
...@@ -232,3 +232,31 @@ class OfflineComputedGradeLog(models.Model): ...@@ -232,3 +232,31 @@ class OfflineComputedGradeLog(models.Model):
def __unicode__(self): def __unicode__(self):
return "[OCGLog] %s: %s" % (self.course_id, self.created) return "[OCGLog] %s: %s" % (self.course_id, self.created)
class CoursePreference(models.Model):
"""
This is a place to keep course preferences that are not inherent to the course. Those should be attributes
of the course xmodule (advanced settings).
A good example is whether this course allows nonregistered users to access it.
"""
course_id = models.CharField(max_length=255, db_index=True)
pref_key = models.CharField(max_length=255)
pref_value = models.CharField(max_length=255, null=True)
class Meta:
unique_together = (('course_id', 'pref_key'))
@classmethod
def get_pref_value(cls, course_id, pref_key):
try:
return cls.objects.get(course_id=course_id, pref_key=pref_key).pref_value
except cls.DoesNotExist:
return None
@classmethod
def course_allows_nonregistered_access(cls, course_id):
return bool(cls.get_pref_value(course_id, 'allow_nonregistered_access'))
def __unicode__(self):
return u"{} : {} : {}".format(self.course_id, self.pref_key, self.pref_value)
\ No newline at end of file
...@@ -22,7 +22,7 @@ from courseware.access import has_access ...@@ -22,7 +22,7 @@ from courseware.access import has_access
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from courseware.model_data import FieldDataCache from courseware.model_data import FieldDataCache
from student.models import UserProfile
from open_ended_grading import open_ended_notifications from open_ended_grading import open_ended_notifications
import waffle import waffle
...@@ -262,6 +262,7 @@ VALID_TAB_TYPES = { ...@@ -262,6 +262,7 @@ VALID_TAB_TYPES = {
'syllabus': TabImpl(null_validator, _syllabus) 'syllabus': TabImpl(null_validator, _syllabus)
} }
NONREGISTERED_TAB_TYPES=['courseware', 'course_info', 'static_tab', 'syllabus']
### External interface below this. ### External interface below this.
...@@ -321,6 +322,10 @@ def get_course_tabs(user, course, active_page, request): ...@@ -321,6 +322,10 @@ def get_course_tabs(user, course, active_page, request):
else: else:
course_tabs = course.tabs course_tabs = course.tabs
# handle nonregistered (and anonymous) users
if not UserProfile.has_registered(user):
course_tabs = [tab for tab in course.tabs if tab['type'] in NONREGISTERED_TAB_TYPES]
for tab in course_tabs: for tab in course_tabs:
# expect handlers to return lists--handles things that are turned off # expect handlers to return lists--handles things that are turned off
# via feature flags, and things like 'textbook' which might generate # via feature flags, and things like 'textbook' which might generate
......
...@@ -71,7 +71,8 @@ class AccessTestCase(TestCase): ...@@ -71,7 +71,8 @@ class AccessTestCase(TestCase):
# TODO: override DISABLE_START_DATES and test the start date branch of the method # TODO: override DISABLE_START_DATES and test the start date branch of the method
u = Mock() u = Mock()
d = Mock() d = Mock()
d.start = datetime.datetime.now(UTC()) - datetime.timedelta(days=1) # make sure the start time is in the past d.category = 'course'
d.start = datetime.datetime.now(pytz.utc) - datetime.timedelta(days=1) # make sure the start time is in the past
# Always returns true because DISABLE_START_DATES is set in test.py # Always returns true because DISABLE_START_DATES is set in test.py
self.assertTrue(access._has_access_descriptor(u, d, 'load')) self.assertTrue(access._has_access_descriptor(u, d, 'load'))
......
...@@ -16,7 +16,7 @@ from django.conf import settings ...@@ -16,7 +16,7 @@ from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from student.models import CourseEnrollment from student.models import CourseEnrollment
from student.tests.factories import AdminFactory from student.tests.factories import AdminFactory, NonRegisteredUserFactory
from mitxmako.middleware import MakoMiddleware from mitxmako.middleware import MakoMiddleware
from xmodule.modulestore import Location from xmodule.modulestore import Location
...@@ -367,3 +367,22 @@ class TestAccordionDueDate(BaseDueDateTests): ...@@ -367,3 +367,22 @@ class TestAccordionDueDate(BaseDueDateTests):
return views.render_accordion( return views.render_accordion(
self.request, course, course.get_children()[0].id, None, None self.request, course, course.get_children()[0].id, None, None
) )
class TestNonRegisteredUser(TestCase):
"""
Tests nonregistered (auto-created) users
"""
def setUp(self):
self.request_factory = RequestFactory()
self.user = NonRegisteredUserFactory()
self.course_id = "course/id/doesnt_matter"
def test_nonregistered_user_factory(self):
self.assertTrue(self.user.profile.nonregistered)
def test_nonregistered_progress_404(self):
with self.assertRaises(Http404):
req = self.request_factory.get(reverse('progress', args=[self.course_id]))
req.user = self.user
views.progress(req, self.course_id)
\ No newline at end of file
...@@ -9,7 +9,7 @@ from django.core.exceptions import PermissionDenied ...@@ -9,7 +9,7 @@ from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import Http404, HttpResponse, HttpResponseRedirect from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseForbidden
from django.shortcuts import redirect from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
...@@ -22,12 +22,13 @@ from courseware.courses import (get_courses, get_course_with_access, ...@@ -22,12 +22,13 @@ from courseware.courses import (get_courses, get_course_with_access,
get_courses_by_university, sort_by_announcement) get_courses_by_university, sort_by_announcement)
import courseware.tabs as tabs import courseware.tabs as tabs
from courseware.masquerade import setup_masquerade from courseware.masquerade import setup_masquerade
from courseware.models import CoursePreference
from courseware.model_data import FieldDataCache from courseware.model_data import FieldDataCache
from .module_render import toc_for_course, get_module_for_descriptor, get_module from .module_render import toc_for_course, get_module_for_descriptor, get_module
from courseware.models import StudentModule, StudentModuleHistory from courseware.models import StudentModule, StudentModuleHistory
from course_modes.models import CourseMode from course_modes.models import CourseMode
from student.models import UserTestGroup, CourseEnrollment from student.models import UserTestGroup, CourseEnrollment, UserProfile
from util.cache import cache, cache_if_anonymous from util.cache import cache, cache_if_anonymous
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xmodule.modulestore import Location from xmodule.modulestore import Location
...@@ -596,7 +597,8 @@ def course_about(request, course_id): ...@@ -596,7 +597,8 @@ def course_about(request, course_id):
raise Http404 raise Http404
course = get_course_with_access(request.user, course_id, 'see_exists') course = get_course_with_access(request.user, course_id, 'see_exists')
registered = registered_for_course(course, request.user) regularly_registered = (registered_for_course(course, request.user) and
UserProfile.has_registered(request.user))
if has_access(request.user, course, 'load'): if has_access(request.user, course, 'load'):
course_target = reverse('info', args=[course.id]) course_target = reverse('info', args=[course.id])
...@@ -621,9 +623,20 @@ def course_about(request, course_id): ...@@ -621,9 +623,20 @@ def course_about(request, course_id):
reg_then_add_to_cart_link = "{reg_url}?course_id={course_id}&enrollment_action=add_to_cart".format( reg_then_add_to_cart_link = "{reg_url}?course_id={course_id}&enrollment_action=add_to_cart".format(
reg_url=reverse('register_user'), course_id=course.id) reg_url=reverse('register_user'), course_id=course.id)
# only allow course sneak peek if
# 1) within enrollment period
# 2) course specifies it's okay
# 3) request.user is not a registered user.
sneakpeek_allowed = (has_access(request.user, course, 'within_enrollment_period') and
CoursePreference.course_allows_nonregistered_access(course_id) and
not UserProfile.has_registered(request.user))
print(sneakpeek_allowed)
return render_to_response('courseware/course_about.html', return render_to_response('courseware/course_about.html',
{'course': course, {'course': course,
'registered': registered, 'regularly_registered': regularly_registered,
'sneakpeek_allowed': sneakpeek_allowed,
'course_target': course_target, 'course_target': course_target,
'registration_price': registration_price, 'registration_price': registration_price,
'in_cart': in_cart, 'in_cart': in_cart,
...@@ -669,7 +682,6 @@ def mktg_course_about(request, course_id): ...@@ -669,7 +682,6 @@ def mktg_course_about(request, course_id):
'course_modes': course_modes, 'course_modes': course_modes,
}) })
@login_required @login_required
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def progress(request, course_id, student_id=None): def progress(request, course_id, student_id=None):
...@@ -677,6 +689,9 @@ def progress(request, course_id, student_id=None): ...@@ -677,6 +689,9 @@ def progress(request, course_id, student_id=None):
Course staff are allowed to see the progress of students in their class. Course staff are allowed to see the progress of students in their class.
""" """
if not UserProfile.has_registered(request.user):
raise Http404
course = get_course_with_access(request.user, course_id, 'load', depth=None) course = get_course_with_access(request.user, course_id, 'load', depth=None)
staff_access = has_access(request.user, course, 'staff') staff_access = has_access(request.user, course, 'staff')
......
...@@ -215,6 +215,9 @@ GENERATE_PROFILE_SCORES = False ...@@ -215,6 +215,9 @@ GENERATE_PROFILE_SCORES = False
# Used with XQueue # Used with XQueue
XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds
# Email to give anonymous users. Should be a black-hole email address, but not cause errors when email is sent there
# This is actually just a base email. We'll make it 'noreply+<username>@example.com' to ensure uniqueness
ANONYMOUS_USER_EMAIL = 'noreply@example.com'
############################# SET PATH INFORMATION ############################# ############################# SET PATH INFORMATION #############################
PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /edx-platform/lms PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /edx-platform/lms
......
p.unauth-warning {
padding: 10px;
text-align: center;
font-weight: bold;
background-color: $black;
color: $white;
}
p.unauth-warning a {
font-weight: bold;
padding: 5px;
background-color: $white;
border-radius: 5px;
}
nav.course-material { nav.course-material {
@include clearfix; @include clearfix;
@include box-sizing(border-box); @include box-sizing(border-box);
......
...@@ -103,6 +103,8 @@ ...@@ -103,6 +103,8 @@
@include box-sizing(border-box); @include box-sizing(border-box);
border-radius: 3px; border-radius: 3px;
display: block; display: block;
float: left;
margin-right: flex-gutter(12);
font: normal 1.2rem/1.6rem $sans-serif; font: normal 1.2rem/1.6rem $sans-serif;
letter-spacing: 1px; letter-spacing: 1px;
padding: 10px 0px; padding: 10px 0px;
...@@ -139,7 +141,7 @@ ...@@ -139,7 +141,7 @@
} }
} }
span.register, span.add-to-cart { span.register, span.add-to-cart, span.sneakpeek {
background: $button-archive-color; background: $button-archive-color;
border: 1px solid darken($button-archive-color, 50%); border: 1px solid darken($button-archive-color, 50%);
@include box-sizing(border-box); @include box-sizing(border-box);
...@@ -150,9 +152,9 @@ ...@@ -150,9 +152,9 @@
text-transform: uppercase; text-transform: uppercase;
text-align: center; text-align: center;
float: left; float: left;
margin: 1px flex-gutter(8) 0 0; margin: 1px 0 0 0;
@include transition(none); @include transition(none);
width: flex-grid(5, 8); width: flex-grid(6);
} }
#register_error { #register_error {
......
...@@ -4,11 +4,14 @@ ...@@ -4,11 +4,14 @@
from courseware.courses import course_image_url, get_course_about_section from courseware.courses import course_image_url, get_course_about_section
from courseware.access import has_access from courseware.access import has_access
from django.conf import settings from django.conf import settings
from student.models import UserProfile
if settings.MITX_FEATURES.get('ENABLE_SHOPPING_CART'): if settings.MITX_FEATURES.get('ENABLE_SHOPPING_CART'):
cart_link = reverse('shoppingcart.views.show_cart') cart_link = reverse('shoppingcart.views.show_cart')
else: else:
cart_link = "" cart_link = ""
%> %>
<%namespace name='static' file='../static_content.html'/> <%namespace name='static' file='../static_content.html'/>
...@@ -31,6 +34,26 @@ ...@@ -31,6 +34,26 @@
event.preventDefault(); event.preventDefault();
}); });
sneakpeek_handler = function(jqXHR) {
if (jqXHR.status == 200) {
location.href = "${course_target}";
}
else {
$("#register_error")
.html("${_('An error occurred. Please try again later.')}")
.css("display", "block");
}
};
$(".course_sneakpeek").click(function(event) {
$.ajax({
url: "${reverse('course_sneakpeek', args=[course.id])}",
type: "POST",
complete: sneakpeek_handler
});
event.preventDefault();
});
% if settings.MITX_FEATURES.get('ENABLE_SHOPPING_CART') and settings.MITX_FEATURES.get('ENABLE_PAID_COURSE_REGISTRATION'): % if settings.MITX_FEATURES.get('ENABLE_SHOPPING_CART') and settings.MITX_FEATURES.get('ENABLE_PAID_COURSE_REGISTRATION'):
add_course_complete_handler = function(jqXHR, textStatus) { add_course_complete_handler = function(jqXHR, textStatus) {
if (jqXHR.status == 200) { if (jqXHR.status == 200) {
...@@ -146,7 +169,7 @@ ...@@ -146,7 +169,7 @@
</hgroup> </hgroup>
<div class="main-cta"> <div class="main-cta">
%if user.is_authenticated() and registered: %if regularly_registered:
%if show_courseware_link: %if show_courseware_link:
<a href="${course_target}"> <a href="${course_target}">
%endif %endif
...@@ -182,6 +205,11 @@ ...@@ -182,6 +205,11 @@
<a href="#" class="register"> <a href="#" class="register">
${_("Register for {course.display_number_with_default}").format(course=course) | h} ${_("Register for {course.display_number_with_default}").format(course=course) | h}
</a> </a>
% if sneakpeek_allowed and not regularly_registered:
<a href="#" class="course_sneakpeek"</a>
<span class="sneakpeek">${_("Explore Course")}</span>
</a>
% endif
<div id="register_error"></div> <div id="register_error"></div>
%endif %endif
</div> </div>
...@@ -308,7 +336,7 @@ ...@@ -308,7 +336,7 @@
</section> </section>
</section> </section>
%if not registered: %if not regularly_registered:
<div style="display: none;"> <div style="display: none;">
<form id="class_enroll_form" method="post" data-remote="true" action="${reverse('change_enrollment')}"> <form id="class_enroll_form" method="post" data-remote="true" action="${reverse('change_enrollment')}">
<fieldset class="enroll_fieldset"> <fieldset class="enroll_fieldset">
......
...@@ -12,9 +12,28 @@ def url_class(is_active): ...@@ -12,9 +12,28 @@ def url_class(is_active):
return "" return ""
%> %>
<%! from courseware.tabs import get_course_tabs %> <%! from courseware.tabs import get_course_tabs %>
<%! from django.utils.translation import ugettext as _ %> <%!
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from student.models import UserProfile
%>
<% import waffle %> <% import waffle %>
% if course and user.is_authenticated() and not UserProfile.has_registered(user):
<p class="unauth-warning">
<%
if settings.MITX_FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD'):
reg_url = reverse('course-specific-register', args=[course.id])
else:
reg_url = reverse('register_user')
%>
${_("Non-registered mode. {tag_start}Register{tag_end} to save your course progress.")\
.format(platform_name=settings.PLATFORM_NAME,
tag_start="<a href='{}'>".format(reg_url),
tag_end="</a>")}
</p>
% endif
<nav class="${active_page} course-material"> <nav class="${active_page} course-material">
<div class="inner-wrapper"> <div class="inner-wrapper">
<ol class="course-tabs"> <ol class="course-tabs">
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
<%! <%!
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from student.models import UserProfile
# App that handles subdomain specific branding # App that handles subdomain specific branding
import branding import branding
...@@ -53,7 +54,7 @@ site_status_msg = get_site_status_msg(course_id) ...@@ -53,7 +54,7 @@ site_status_msg = get_site_status_msg(course_id)
<h2><span class="provider">${course.display_org_with_default | h}:</span> ${course.display_number_with_default | h} ${course.display_name_with_default}</h2> <h2><span class="provider">${course.display_org_with_default | h}:</span> ${course.display_number_with_default | h} ${course.display_name_with_default}</h2>
% endif % endif
% if user.is_authenticated(): % if UserProfile.has_registered(user):
<ol class="left nav-global authenticated"> <ol class="left nav-global authenticated">
<%block name="navigation_global_links_authenticated"> <%block name="navigation_global_links_authenticated">
...@@ -120,16 +121,19 @@ site_status_msg = get_site_status_msg(course_id) ...@@ -120,16 +121,19 @@ site_status_msg = get_site_status_msg(course_id)
</%block> </%block>
</ol> </ol>
<ol class="right nav-courseware"> <ol class="right nav-courseware">
<li class="nav-courseware-01">
% if not settings.MITX_FEATURES['DISABLE_LOGIN_BUTTON']: <li class="nav-courseware-01">
% if course and settings.MITX_FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain: % if not settings.MITX_FEATURES['DISABLE_LOGIN_BUTTON']:
<a class="cta cta-login" href="${reverse('course-specific-login', args=[course.id])}${login_query()}">${_("Log in")}</a> % if course and settings.MITX_FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
% else: <a class="cta cta-login" href="${reverse('course-specific-login', args=[course.id])}${login_query()}">${_("Log in")}</a>
<a class="cta cta-login" href="/login${login_query()}">${_("Log in")}</a> % else:
% endif <a class="cta cta-login" href="/login${login_query()}">${_("Log in")}</a>
% endif % endif
</li> % endif
</li>
</ol> </ol>
% endif % endif
</nav> </nav>
......
...@@ -179,7 +179,6 @@ if settings.COURSEWARE_ENABLED: ...@@ -179,7 +179,6 @@ if settings.COURSEWARE_ENABLED:
'courseware.module_render.modx_dispatch', 'courseware.module_render.modx_dispatch',
name='modx_dispatch'), name='modx_dispatch'),
# Software Licenses # Software Licenses
# TODO: for now, this is the endpoint of an ajax replay # TODO: for now, this is the endpoint of an ajax replay
...@@ -205,6 +204,9 @@ if settings.COURSEWARE_ENABLED: ...@@ -205,6 +204,9 @@ if settings.COURSEWARE_ENABLED:
'student.views.change_enrollment', name="change_enrollment"), 'student.views.change_enrollment', name="change_enrollment"),
url(r'^change_email_settings$', 'student.views.change_email_settings', name="change_email_settings"), url(r'^change_email_settings$', 'student.views.change_email_settings', name="change_email_settings"),
url(r'^course_sneakpeek/(?P<course_id>[^/]+/[^/]+/[^/]+)/$',
'student.views.setup_sneakpeek', name="course_sneakpeek"),
#About the course #About the course
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/about$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/about$',
'courseware.views.course_about', name="about_course"), 'courseware.views.course_about', name="about_course"),
......
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