Merge branch 'master' into kimth/generic-coderesponse
This commit is contained in:
@@ -17,8 +17,8 @@ def try_staticfiles_lookup(path):
|
||||
except Exception as err:
|
||||
log.warning("staticfiles_storage couldn't find path {}: {}".format(
|
||||
path, str(err)))
|
||||
# Just return a dead link--don't kill everything.
|
||||
url = "file_not_found"
|
||||
# Just return the original path; don't kill everything.
|
||||
url = path
|
||||
return url
|
||||
|
||||
|
||||
|
||||
@@ -257,8 +257,11 @@ def add_user_to_default_group(user, group):
|
||||
########################## REPLICATION SIGNALS #################################
|
||||
@receiver(post_save, sender=User)
|
||||
def replicate_user_save(sender, **kwargs):
|
||||
user_obj = kwargs['instance']
|
||||
return replicate_model(User.save, user_obj, user_obj.id)
|
||||
user_obj = kwargs['instance']
|
||||
if not should_replicate(user_obj):
|
||||
return
|
||||
for course_db_name in db_names_to_replicate_to(user_obj.id):
|
||||
replicate_user(user_obj, course_db_name)
|
||||
|
||||
@receiver(post_save, sender=CourseEnrollment)
|
||||
def replicate_enrollment_save(sender, **kwargs):
|
||||
@@ -287,8 +290,8 @@ def replicate_enrollment_save(sender, **kwargs):
|
||||
|
||||
@receiver(post_delete, sender=CourseEnrollment)
|
||||
def replicate_enrollment_delete(sender, **kwargs):
|
||||
enrollment_obj = kwargs['instance']
|
||||
return replicate_model(CourseEnrollment.delete, enrollment_obj, enrollment_obj.user_id)
|
||||
enrollment_obj = kwargs['instance']
|
||||
return replicate_model(CourseEnrollment.delete, enrollment_obj, enrollment_obj.user_id)
|
||||
|
||||
@receiver(post_save, sender=UserProfile)
|
||||
def replicate_userprofile_save(sender, **kwargs):
|
||||
@@ -311,23 +314,20 @@ def replicate_user(portal_user, course_db_name):
|
||||
overridden.
|
||||
"""
|
||||
try:
|
||||
# If the user exists in the Course DB, update the appropriate fields and
|
||||
# save it back out to the Course DB.
|
||||
course_user = User.objects.using(course_db_name).get(id=portal_user.id)
|
||||
for field in USER_FIELDS_TO_COPY:
|
||||
setattr(course_user, field, getattr(portal_user, field))
|
||||
|
||||
mark_handled(course_user)
|
||||
log.debug("User {0} found in Course DB, replicating fields to {1}"
|
||||
.format(course_user, course_db_name))
|
||||
course_user.save(using=course_db_name) # Just being explicit.
|
||||
|
||||
except User.DoesNotExist:
|
||||
# Otherwise, just make a straight copy to the Course DB.
|
||||
mark_handled(portal_user)
|
||||
log.debug("User {0} not found in Course DB, creating copy in {1}"
|
||||
.format(portal_user, course_db_name))
|
||||
portal_user.save(using=course_db_name)
|
||||
course_user = User()
|
||||
|
||||
for field in USER_FIELDS_TO_COPY:
|
||||
setattr(course_user, field, getattr(portal_user, field))
|
||||
|
||||
mark_handled(course_user)
|
||||
course_user.save(using=course_db_name)
|
||||
unmark(course_user)
|
||||
|
||||
def replicate_model(model_method, instance, user_id):
|
||||
"""
|
||||
@@ -337,13 +337,14 @@ def replicate_model(model_method, instance, user_id):
|
||||
if not should_replicate(instance):
|
||||
return
|
||||
|
||||
mark_handled(instance)
|
||||
course_db_names = db_names_to_replicate_to(user_id)
|
||||
log.debug("Replicating {0} for user {1} to DBs: {2}"
|
||||
.format(model_method, user_id, course_db_names))
|
||||
|
||||
mark_handled(instance)
|
||||
for db_name in course_db_names:
|
||||
model_method(instance, using=db_name)
|
||||
unmark(instance)
|
||||
|
||||
######### Replication Helpers #########
|
||||
|
||||
@@ -371,7 +372,7 @@ def db_names_to_replicate_to(user_id):
|
||||
def marked_handled(instance):
|
||||
"""Have we marked this instance as being handled to avoid infinite loops
|
||||
caused by saving models in post_save hooks for the same models?"""
|
||||
return hasattr(instance, '_do_not_copy_to_course_db')
|
||||
return hasattr(instance, '_do_not_copy_to_course_db') and instance._do_not_copy_to_course_db
|
||||
|
||||
def mark_handled(instance):
|
||||
"""You have to mark your instance with this function or else we'll go into
|
||||
@@ -384,6 +385,11 @@ def mark_handled(instance):
|
||||
"""
|
||||
instance._do_not_copy_to_course_db = True
|
||||
|
||||
def unmark(instance):
|
||||
"""If we don't unmark a model after we do replication, then consecutive
|
||||
save() calls won't be properly replicated."""
|
||||
instance._do_not_copy_to_course_db = False
|
||||
|
||||
def should_replicate(instance):
|
||||
"""Should this instance be replicated? We need to be a Portal server and
|
||||
the instance has to not have been marked_handled."""
|
||||
@@ -398,9 +404,3 @@ def should_replicate(instance):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ when you run "manage.py test".
|
||||
|
||||
Replace this with more appropriate tests for your application.
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from django.test import TestCase
|
||||
@@ -13,6 +14,8 @@ from .models import User, UserProfile, CourseEnrollment, replicate_user, USER_FI
|
||||
COURSE_1 = 'edX/toy/2012_Fall'
|
||||
COURSE_2 = 'edx/full/6.002_Spring_2012'
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class ReplicationTest(TestCase):
|
||||
|
||||
multi_db = True
|
||||
@@ -47,23 +50,18 @@ class ReplicationTest(TestCase):
|
||||
field, portal_user, course_user
|
||||
))
|
||||
|
||||
if hasattr(portal_user, 'seen_response_count'):
|
||||
# Since it's the first copy over of User data, we should have all of it
|
||||
self.assertEqual(portal_user.seen_response_count,
|
||||
course_user.seen_response_count)
|
||||
|
||||
# But if we replicate again, the user already exists in the Course DB,
|
||||
# so it shouldn't update the seen_response_count (which is Askbot
|
||||
# controlled).
|
||||
# This hasattr lameness is here because we don't want this test to be
|
||||
# triggered when we're being run by CMS tests (Askbot doesn't exist
|
||||
# there, so the test will fail).
|
||||
#
|
||||
# seen_response_count isn't a field we care about, so it shouldn't have
|
||||
# been copied over.
|
||||
if hasattr(portal_user, 'seen_response_count'):
|
||||
portal_user.seen_response_count = 20
|
||||
replicate_user(portal_user, COURSE_1)
|
||||
course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
|
||||
self.assertEqual(portal_user.seen_response_count, 20)
|
||||
self.assertEqual(course_user.seen_response_count, 10)
|
||||
self.assertEqual(course_user.seen_response_count, 0)
|
||||
|
||||
# Another replication should work for an email change however, since
|
||||
# it's a field we care about.
|
||||
@@ -123,6 +121,25 @@ class ReplicationTest(TestCase):
|
||||
UserProfile.objects.using(COURSE_2).get,
|
||||
id=portal_user_profile.id)
|
||||
|
||||
log.debug("Make sure our seen_response_count is not replicated.")
|
||||
if hasattr(portal_user, 'seen_response_count'):
|
||||
portal_user.seen_response_count = 200
|
||||
course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
|
||||
self.assertEqual(portal_user.seen_response_count, 200)
|
||||
self.assertEqual(course_user.seen_response_count, 0)
|
||||
portal_user.save()
|
||||
|
||||
course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
|
||||
self.assertEqual(portal_user.seen_response_count, 200)
|
||||
self.assertEqual(course_user.seen_response_count, 0)
|
||||
|
||||
portal_user.email = 'jim@edx.org'
|
||||
portal_user.save()
|
||||
course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
|
||||
self.assertEqual(portal_user.email, 'jim@edx.org')
|
||||
self.assertEqual(course_user.email, 'jim@edx.org')
|
||||
|
||||
|
||||
|
||||
def test_enrollment_for_user_info_after_enrollment(self):
|
||||
"""Test the effect of modifying User data after you've enrolled."""
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import datetime
|
||||
import feedparser
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import string
|
||||
import sys
|
||||
import uuid
|
||||
import feedparser
|
||||
import time
|
||||
import urllib
|
||||
import itertools
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import logout, authenticate, login
|
||||
@@ -26,17 +27,19 @@ from bs4 import BeautifulSoup
|
||||
from django.core.cache import cache
|
||||
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from student.models import Registration, UserProfile, PendingNameChange, PendingEmailChange, CourseEnrollment
|
||||
from student.models import (Registration, UserProfile,
|
||||
PendingNameChange, PendingEmailChange,
|
||||
CourseEnrollment)
|
||||
from util.cache import cache_if_anonymous
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from models import Registration, UserProfile, PendingNameChange, PendingEmailChange, CourseEnrollment
|
||||
from datetime import date
|
||||
from collections import namedtuple
|
||||
from courseware.courses import course_staff_group_name, has_staff_access_to_course, get_courses_by_university
|
||||
from courseware.courses import (course_staff_group_name, has_staff_access_to_course,
|
||||
get_courses_by_university)
|
||||
|
||||
log = logging.getLogger("mitx.student")
|
||||
Article = namedtuple('Article', 'title url author image deck publication publish_date')
|
||||
@@ -47,7 +50,8 @@ def csrf_token(context):
|
||||
csrf_token = context.get('csrf_token', '')
|
||||
if csrf_token == 'NOTPROVIDED':
|
||||
return ''
|
||||
return u'<div style="display:none"><input type="hidden" name="csrfmiddlewaretoken" value="%s" /></div>' % (csrf_token)
|
||||
return (u'<div style="display:none"><input type="hidden"'
|
||||
' name="csrfmiddlewaretoken" value="%s" /></div>' % (csrf_token))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -162,6 +166,24 @@ def change_enrollment_view(request):
|
||||
"""Delegate to change_enrollment to actually do the work."""
|
||||
return HttpResponse(json.dumps(change_enrollment(request)))
|
||||
|
||||
def enrollment_allowed(user, course):
|
||||
"""If the course has an enrollment period, check whether we are in it.
|
||||
Also respects the DARK_LAUNCH setting"""
|
||||
now = time.gmtime()
|
||||
start = course.enrollment_start
|
||||
end = course.enrollment_end
|
||||
|
||||
if (start is None or now > start) and (end is None or now < end):
|
||||
# in enrollment period.
|
||||
return True
|
||||
|
||||
if settings.MITX_FEATURES['DARK_LAUNCH']:
|
||||
if has_staff_access_to_course(user, course):
|
||||
# if dark launch, staff can enroll outside enrollment window
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def change_enrollment(request):
|
||||
if request.method != "POST":
|
||||
raise Http404
|
||||
@@ -174,7 +196,8 @@ def change_enrollment(request):
|
||||
|
||||
course_id = request.POST.get("course_id", None)
|
||||
if course_id == None:
|
||||
return HttpResponse(json.dumps({'success': False, 'error': 'There was an error receiving the course id.'}))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'There was an error receiving the course id.'}))
|
||||
|
||||
if action == "enroll":
|
||||
# Make sure the course exists
|
||||
@@ -187,12 +210,20 @@ def change_enrollment(request):
|
||||
return {'success': False, 'error': 'The course requested does not exist.'}
|
||||
|
||||
if settings.MITX_FEATURES.get('ACCESS_REQUIRE_STAFF_FOR_COURSE'):
|
||||
# require that user be in the staff_* group (or be an overall admin) to be able to enroll
|
||||
# eg staff_6.002x or staff_6.00x
|
||||
# require that user be in the staff_* group (or be an
|
||||
# overall admin) to be able to enroll eg staff_6.002x or
|
||||
# staff_6.00x
|
||||
if not has_staff_access_to_course(user, course):
|
||||
staff_group = course_staff_group_name(course)
|
||||
log.debug('user %s denied enrollment to %s ; not in %s' % (user,course.location.url(),staff_group))
|
||||
return {'success': False, 'error' : '%s membership required to access course.' % staff_group}
|
||||
log.debug('user %s denied enrollment to %s ; not in %s' % (
|
||||
user, course.location.url(), staff_group))
|
||||
return {'success': False,
|
||||
'error' : '%s membership required to access course.' % staff_group}
|
||||
|
||||
if not enrollment_allowed(user, course):
|
||||
return {'success': False,
|
||||
'error': 'enrollment in {} not allowed at this time'
|
||||
.format(course.display_name)}
|
||||
|
||||
enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id)
|
||||
return {'success': True}
|
||||
|
||||
@@ -21,18 +21,35 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
try:
|
||||
self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M")
|
||||
except KeyError:
|
||||
self.start = time.gmtime(0) #The epoch
|
||||
msg = "Course loaded without a start date. id = %s" % self.id
|
||||
log.critical(msg)
|
||||
except ValueError as e:
|
||||
self.start = time.gmtime(0) #The epoch
|
||||
msg = "Course loaded with a bad start date. %s '%s'" % (self.id, e)
|
||||
log.critical(msg)
|
||||
|
||||
# Don't call the tracker from the exception handler.
|
||||
if msg is not None:
|
||||
self.start = time.gmtime(0) # The epoch
|
||||
log.critical(msg)
|
||||
system.error_tracker(msg)
|
||||
|
||||
def try_parse_time(key):
|
||||
"""
|
||||
Parse an optional metadata key: if present, must be valid.
|
||||
Return None if not present.
|
||||
"""
|
||||
if key in self.metadata:
|
||||
try:
|
||||
return time.strptime(self.metadata[key], "%Y-%m-%dT%H:%M")
|
||||
except ValueError as e:
|
||||
msg = "Course %s loaded with a bad metadata key %s '%s'" % (
|
||||
self.id, self.metadata[key], e)
|
||||
log.warning(msg)
|
||||
return None
|
||||
|
||||
self.enrollment_start = try_parse_time("enrollment_start")
|
||||
self.enrollment_end = try_parse_time("enrollment_end")
|
||||
|
||||
|
||||
|
||||
|
||||
def has_started(self):
|
||||
return time.gmtime() > self.start
|
||||
@@ -100,7 +117,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
for s in c.get_children():
|
||||
if s.metadata.get('graded', False):
|
||||
xmoduledescriptors = list(yield_descriptor_descendents(s))
|
||||
|
||||
|
||||
# The xmoduledescriptors included here are only the ones that have scores.
|
||||
section_description = { 'section_descriptor' : s, 'xmoduledescriptors' : filter(lambda child: child.has_score, xmoduledescriptors) }
|
||||
|
||||
|
||||
@@ -89,6 +89,19 @@ div {
|
||||
}
|
||||
}
|
||||
|
||||
&.processing {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
background: url('../images/spinner.gif') center center no-repeat;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
input {
|
||||
border-color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
&.incorrect, &.ui-icon-close {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
@@ -146,7 +159,7 @@ div {
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
&.processing, &.ui-icon-check {
|
||||
&.processing, &.ui-icon-processing {
|
||||
@include inline-block();
|
||||
background: url('../images/spinner.gif') center center no-repeat;
|
||||
height: 20px;
|
||||
|
||||
@@ -14,7 +14,7 @@ div.video {
|
||||
|
||||
section.video-player {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
// overflow: hidden;
|
||||
padding-bottom: 56.25%;
|
||||
position: relative;
|
||||
|
||||
@@ -45,12 +45,13 @@ div.video {
|
||||
div.slider {
|
||||
@extend .clearfix;
|
||||
background: #c2c2c2;
|
||||
border: none;
|
||||
border-bottom: 1px solid #000;
|
||||
border: 1px solid #000;
|
||||
@include border-radius(0);
|
||||
border-top: 1px solid #000;
|
||||
@include box-shadow(inset 0 1px 0 #eee, 0 1px 0 #555);
|
||||
height: 7px;
|
||||
margin-left: -1px;
|
||||
margin-right: -1px;
|
||||
@include transition(height 2.0s ease-in-out);
|
||||
|
||||
div.ui-widget-header {
|
||||
@@ -58,43 +59,12 @@ div.video {
|
||||
@include box-shadow(inset 0 1px 0 #999);
|
||||
}
|
||||
|
||||
.ui-tooltip.qtip .ui-tooltip-content {
|
||||
background: $mit-red;
|
||||
border: 1px solid darken($mit-red, 20%);
|
||||
@include border-radius(2px);
|
||||
@include box-shadow(inset 0 1px 0 lighten($mit-red, 10%));
|
||||
color: #fff;
|
||||
font: bold 12px $body-font-family;
|
||||
margin-bottom: 6px;
|
||||
margin-right: 0;
|
||||
overflow: visible;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
text-shadow: 0 -1px 0 darken($mit-red, 10%);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
&::after {
|
||||
background: $mit-red;
|
||||
border-bottom: 1px solid darken($mit-red, 20%);
|
||||
border-right: 1px solid darken($mit-red, 20%);
|
||||
bottom: -5px;
|
||||
content: " ";
|
||||
display: block;
|
||||
height: 7px;
|
||||
left: 50%;
|
||||
margin-left: -3px;
|
||||
position: absolute;
|
||||
@include transform(rotate(45deg));
|
||||
width: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
a.ui-slider-handle {
|
||||
background: $mit-red url(../images/slider-handle.png) center center no-repeat;
|
||||
background: $pink url(../images/slider-handle.png) center center no-repeat;
|
||||
@include background-size(50%);
|
||||
border: 1px solid darken($mit-red, 20%);
|
||||
border: 1px solid darken($pink, 20%);
|
||||
@include border-radius(15px);
|
||||
@include box-shadow(inset 0 1px 0 lighten($mit-red, 10%));
|
||||
@include box-shadow(inset 0 1px 0 lighten($pink, 10%));
|
||||
cursor: pointer;
|
||||
height: 15px;
|
||||
margin-left: -7px;
|
||||
@@ -103,7 +73,7 @@ div.video {
|
||||
width: 15px;
|
||||
|
||||
&:focus, &:hover {
|
||||
background-color: lighten($mit-red, 10%);
|
||||
background-color: lighten($pink, 10%);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ class XModule(HTMLSnippet):
|
||||
def get_display_items(self):
|
||||
'''
|
||||
Returns a list of descendent module instances that will display
|
||||
immediately inside this module
|
||||
immediately inside this module.
|
||||
'''
|
||||
items = []
|
||||
for child in self.get_children():
|
||||
@@ -238,7 +238,7 @@ class XModule(HTMLSnippet):
|
||||
def displayable_items(self):
|
||||
'''
|
||||
Returns list of displayable modules contained by this module. If this
|
||||
module is visible, should return [self]
|
||||
module is visible, should return [self].
|
||||
'''
|
||||
return [self]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user