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]
|
||||
|
||||
|
||||
@@ -145,15 +145,11 @@ def progress_summary(student, course, grader, student_module_cache):
|
||||
instance_modules for the student
|
||||
"""
|
||||
chapters = []
|
||||
for c in course.get_children():
|
||||
# Don't include chapters that aren't displayable (e.g. due to error)
|
||||
if c not in c.displayable_items():
|
||||
continue
|
||||
# Don't include chapters that aren't displayable (e.g. due to error)
|
||||
for c in course.get_display_items():
|
||||
sections = []
|
||||
for s in c.get_children():
|
||||
for s in c.get_display_items():
|
||||
# Same for sections
|
||||
if s not in s.displayable_items():
|
||||
continue
|
||||
graded = s.metadata.get('graded', False)
|
||||
scores = []
|
||||
for module in yield_module_descendents(s):
|
||||
|
||||
@@ -58,8 +58,22 @@ def mongo_store_config(data_dir):
|
||||
}
|
||||
}
|
||||
|
||||
def xml_store_config(data_dir):
|
||||
return {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
|
||||
'OPTIONS': {
|
||||
'data_dir': data_dir,
|
||||
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
|
||||
'eager': True,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
|
||||
TEST_DATA_MODULESTORE = mongo_store_config(TEST_DATA_DIR)
|
||||
TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR)
|
||||
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
|
||||
|
||||
REAL_DATA_DIR = settings.GITHUB_REPO_ROOT
|
||||
REAL_DATA_MODULESTORE = mongo_store_config(REAL_DATA_DIR)
|
||||
@@ -149,8 +163,27 @@ class ActivateLoginTestCase(TestCase):
|
||||
class PageLoader(ActivateLoginTestCase):
|
||||
''' Base class that adds a function to load all pages in a modulestore '''
|
||||
|
||||
def _enroll(self, course):
|
||||
"""Post to the enrollment view, and return the parsed json response"""
|
||||
resp = self.client.post('/change_enrollment', {
|
||||
'enrollment_action': 'enroll',
|
||||
'course_id': course.id,
|
||||
})
|
||||
return parse_json(resp)
|
||||
|
||||
def try_enroll(self, course):
|
||||
"""Try to enroll. Return bool success instead of asserting it."""
|
||||
data = self._enroll(course)
|
||||
print 'Enrollment in {} result: {}'.format(course.location.url(), data)
|
||||
return data['success']
|
||||
|
||||
def enroll(self, course):
|
||||
"""Enroll the currently logged-in user, and check that it worked."""
|
||||
data = self._enroll(course)
|
||||
self.assertTrue(data['success'])
|
||||
|
||||
def unenroll(self, course):
|
||||
"""Unenroll the currently logged-in user, and check that it worked."""
|
||||
resp = self.client.post('/change_enrollment', {
|
||||
'enrollment_action': 'enroll',
|
||||
'course_id': course.id,
|
||||
@@ -159,6 +192,7 @@ class PageLoader(ActivateLoginTestCase):
|
||||
self.assertTrue(data['success'])
|
||||
|
||||
def check_pages_load(self, course_name, data_dir, modstore):
|
||||
"""Make all locations in course load"""
|
||||
print "Checking course {0} in {1}".format(course_name, data_dir)
|
||||
import_from_xml(modstore, data_dir, [course_name])
|
||||
|
||||
@@ -191,7 +225,7 @@ class PageLoader(ActivateLoginTestCase):
|
||||
self.assertTrue(all_ok)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
class TestCoursesLoadTestCase(PageLoader):
|
||||
'''Check that all pages in test courses load properly'''
|
||||
|
||||
@@ -207,7 +241,7 @@ class TestCoursesLoadTestCase(PageLoader):
|
||||
self.check_pages_load('full', TEST_DATA_DIR, modulestore())
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
class TestViewAuth(PageLoader):
|
||||
"""Check that view authentication works properly"""
|
||||
|
||||
@@ -215,15 +249,15 @@ class TestViewAuth(PageLoader):
|
||||
# can't do imports there without manually hacking settings.
|
||||
|
||||
def setUp(self):
|
||||
print "sys.path: {}".format(sys.path)
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
modulestore().collection.drop()
|
||||
import_from_xml(modulestore(), TEST_DATA_DIR, ['toy'])
|
||||
import_from_xml(modulestore(), TEST_DATA_DIR, ['full'])
|
||||
courses = modulestore().get_courses()
|
||||
# get the two courses sorted out
|
||||
courses.sort(key=lambda c: c.location.course)
|
||||
[self.full, self.toy] = courses
|
||||
|
||||
def find_course(name):
|
||||
"""Assumes the course is present"""
|
||||
return [c for c in courses if c.location.course==name][0]
|
||||
|
||||
self.full = find_course("full")
|
||||
self.toy = find_course("toy")
|
||||
|
||||
# Create two accounts
|
||||
self.student = 'view@test.com'
|
||||
@@ -304,26 +338,35 @@ class TestViewAuth(PageLoader):
|
||||
self.check_for_get_code(200, url)
|
||||
|
||||
|
||||
def test_dark_launch(self):
|
||||
"""Make sure that when dark launch is on, students can't access course
|
||||
pages, but instructors can"""
|
||||
|
||||
# test.py turns off start dates, enable them and set them correctly.
|
||||
# Because settings is global, be careful not to mess it up for other tests
|
||||
# (Can't use override_settings because we're only changing part of the
|
||||
# MITX_FEATURES dict)
|
||||
def run_wrapped(self, test):
|
||||
"""
|
||||
test.py turns off start dates. Enable them and DARK_LAUNCH.
|
||||
Because settings is global, be careful not to mess it up for other tests
|
||||
(Can't use override_settings because we're only changing part of the
|
||||
MITX_FEATURES dict)
|
||||
"""
|
||||
oldDSD = settings.MITX_FEATURES['DISABLE_START_DATES']
|
||||
oldDL = settings.MITX_FEATURES['DARK_LAUNCH']
|
||||
|
||||
try:
|
||||
settings.MITX_FEATURES['DISABLE_START_DATES'] = False
|
||||
settings.MITX_FEATURES['DARK_LAUNCH'] = True
|
||||
self._do_test_dark_launch()
|
||||
test()
|
||||
finally:
|
||||
settings.MITX_FEATURES['DISABLE_START_DATES'] = oldDSD
|
||||
settings.MITX_FEATURES['DARK_LAUNCH'] = oldDL
|
||||
|
||||
|
||||
def test_dark_launch(self):
|
||||
"""Make sure that when dark launch is on, students can't access course
|
||||
pages, but instructors can"""
|
||||
self.run_wrapped(self._do_test_dark_launch)
|
||||
|
||||
def test_enrollment_period(self):
|
||||
"""Check that enrollment periods work"""
|
||||
self.run_wrapped(self._do_test_enrollment_period)
|
||||
|
||||
|
||||
def _do_test_dark_launch(self):
|
||||
"""Actually do the test, relying on settings to be right."""
|
||||
|
||||
@@ -338,6 +381,7 @@ class TestViewAuth(PageLoader):
|
||||
self.assertTrue(settings.MITX_FEATURES['DARK_LAUNCH'])
|
||||
|
||||
def reverse_urls(names, course):
|
||||
"""Reverse a list of course urls"""
|
||||
return [reverse(name, kwargs={'course_id': course.id}) for name in names]
|
||||
|
||||
def dark_student_urls(course):
|
||||
@@ -424,6 +468,53 @@ class TestViewAuth(PageLoader):
|
||||
check_staff(self.toy)
|
||||
check_staff(self.full)
|
||||
|
||||
def _do_test_enrollment_period(self):
|
||||
"""Actually do the test, relying on settings to be right."""
|
||||
|
||||
# Make courses start in the future
|
||||
tomorrow = time.time() + 24 * 3600
|
||||
nextday = tomorrow + 24 * 3600
|
||||
yesterday = time.time() - 24 * 3600
|
||||
|
||||
print "changing"
|
||||
# toy course's enrollment period hasn't started
|
||||
self.toy.enrollment_start = time.gmtime(tomorrow)
|
||||
self.toy.enrollment_end = time.gmtime(nextday)
|
||||
|
||||
# full course's has
|
||||
self.full.enrollment_start = time.gmtime(yesterday)
|
||||
self.full.enrollment_end = time.gmtime(tomorrow)
|
||||
|
||||
print "login"
|
||||
# First, try with an enrolled student
|
||||
print '=== Testing student access....'
|
||||
self.login(self.student, self.password)
|
||||
self.assertFalse(self.try_enroll(self.toy))
|
||||
self.assertTrue(self.try_enroll(self.full))
|
||||
|
||||
print '=== Testing course instructor access....'
|
||||
# Make the instructor staff in the toy course
|
||||
group_name = course_staff_group_name(self.toy)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(user(self.instructor))
|
||||
|
||||
print "logout/login"
|
||||
self.logout()
|
||||
self.login(self.instructor, self.password)
|
||||
print "Instructor should be able to enroll in toy course"
|
||||
self.assertTrue(self.try_enroll(self.toy))
|
||||
|
||||
print '=== Testing staff access....'
|
||||
# now make the instructor global staff, but not in the instructor group
|
||||
g.user_set.remove(user(self.instructor))
|
||||
u = user(self.instructor)
|
||||
u.is_staff = True
|
||||
u.save()
|
||||
|
||||
# unenroll and try again
|
||||
self.unenroll(self.toy)
|
||||
self.assertTrue(self.try_enroll(self.toy))
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=REAL_DATA_MODULESTORE)
|
||||
class RealCoursesLoadTestCase(PageLoader):
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
@import 'base/base';
|
||||
@import 'base/extends';
|
||||
@import 'base/animations';
|
||||
@import 'shared/tooltips';
|
||||
|
||||
// Course base / layout styles
|
||||
@import 'course/layout/courseware_subnav';
|
||||
|
||||
@@ -15,15 +15,15 @@ div.info-wrapper {
|
||||
|
||||
> ol {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin-bottom: lh();
|
||||
padding-left: 0;
|
||||
|
||||
> li {
|
||||
@extend .clearfix;
|
||||
border-bottom: 1px solid lighten($border-color, 10%);
|
||||
list-style-type: disk;
|
||||
margin-bottom: lh();
|
||||
padding-bottom: lh(.5);
|
||||
list-style-type: disk;
|
||||
|
||||
&:first-child {
|
||||
margin: 0 (-(lh(.5))) lh();
|
||||
@@ -41,10 +41,10 @@ div.info-wrapper {
|
||||
|
||||
h2 {
|
||||
float: left;
|
||||
margin: 0 flex-gutter() 0 0;
|
||||
width: flex-grid(2, 9);
|
||||
font-size: $body-font-size;
|
||||
font-weight: bold;
|
||||
margin: 0 flex-gutter() 0 0;
|
||||
width: flex-grid(2, 9);
|
||||
}
|
||||
|
||||
section.update-description {
|
||||
@@ -68,15 +68,15 @@ div.info-wrapper {
|
||||
|
||||
section.handouts {
|
||||
@extend .sidebar;
|
||||
border-left: 1px solid #d3d3d3;
|
||||
border-left: 1px solid $border-color;
|
||||
@include border-radius(0 4px 4px 0);
|
||||
@include box-shadow(none);
|
||||
border-right: 0;
|
||||
@include box-shadow(none);
|
||||
|
||||
h1 {
|
||||
@extend .bottom-border;
|
||||
padding: lh(.5) lh(.5);
|
||||
margin-bottom: 0;
|
||||
padding: lh(.5) lh(.5);
|
||||
}
|
||||
|
||||
ol {
|
||||
@@ -90,8 +90,9 @@ div.info-wrapper {
|
||||
&.expandable,
|
||||
&.collapsable {
|
||||
h4 {
|
||||
font-weight: normal;
|
||||
color: $blue;
|
||||
font-size: 1em;
|
||||
font-weight: normal;
|
||||
padding: lh(.25) 0 lh(.25) lh(1.5);
|
||||
}
|
||||
}
|
||||
@@ -145,7 +146,8 @@ div.info-wrapper {
|
||||
filter: alpha(opacity=60);
|
||||
|
||||
+ h4 {
|
||||
background-color: #e3e3e3;
|
||||
@extend a:hover;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ body {
|
||||
}
|
||||
|
||||
body, h1, h2, h3, h4, h5, h6, p, p a:link, p a:visited, a {
|
||||
text-align: left;
|
||||
font-family: $sans-serif;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,24 +25,12 @@ h1.top-header {
|
||||
}
|
||||
}
|
||||
|
||||
.action-link {
|
||||
a {
|
||||
color: $mit-red;
|
||||
|
||||
&:hover {
|
||||
color: darken($mit-red, 20%);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
@include box-sizing(border-box);
|
||||
display: table-cell;
|
||||
padding: lh();
|
||||
vertical-align: top;
|
||||
width: flex-grid(9) + flex-gutter();
|
||||
overflow: hidden;
|
||||
|
||||
@media print {
|
||||
@include box-shadow(none);
|
||||
@@ -164,7 +152,6 @@ h1.top-header {
|
||||
.topbar {
|
||||
@extend .clearfix;
|
||||
border-bottom: 1px solid $border-color;
|
||||
font-size: 14px;
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
@@ -193,17 +180,17 @@ h1.top-header {
|
||||
|
||||
h2 {
|
||||
display: block;
|
||||
width: 700px;
|
||||
float: left;
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
line-height: 40px;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 40px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
text-transform: none;
|
||||
white-space: nowrap;
|
||||
width: 700px;
|
||||
|
||||
.provider {
|
||||
font: inherit;
|
||||
@@ -211,4 +198,4 @@ h1.top-header {
|
||||
color: #6d6d6d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,13 +146,13 @@ div.course-wrapper {
|
||||
@include border-radius(0);
|
||||
|
||||
a.ui-slider-handle {
|
||||
@include box-shadow(inset 0 1px 0 lighten($mit-red, 10%));
|
||||
@include box-shadow(inset 0 1px 0 lighten($pink, 10%));
|
||||
background: $mit-red url(../images/slider-bars.png) center center no-repeat;
|
||||
border: 1px solid darken($mit-red, 20%);
|
||||
border: 1px solid darken($pink, 20%);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: lighten($mit-red, 10%);
|
||||
background-color: lighten($pink, 10%);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ section.course-index {
|
||||
div#accordion {
|
||||
h3 {
|
||||
@include border-radius(0);
|
||||
border-top: 1px solid #e3e3e3;
|
||||
border-top: 1px solid lighten($border-color, 10%);
|
||||
font-size: em(16, 18);
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
@@ -34,6 +34,7 @@ section.course-index {
|
||||
}
|
||||
|
||||
&.ui-accordion-header {
|
||||
border-bottom: none;
|
||||
color: #000;
|
||||
|
||||
a {
|
||||
|
||||
@@ -17,7 +17,6 @@ div.answer-controls {
|
||||
margin-left: flex-gutter();
|
||||
|
||||
nav {
|
||||
@extend .action-link;
|
||||
float: right;
|
||||
margin-top: 34px;
|
||||
|
||||
@@ -144,7 +143,7 @@ div.answer-actions {
|
||||
text-decoration: none;
|
||||
|
||||
&.question-delete {
|
||||
// color: $mit-red;
|
||||
color: $mit-red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ form.answer-form {
|
||||
margin-left: 2.5%;
|
||||
padding-left: 1.5%;
|
||||
border-left: 1px dashed #ddd;
|
||||
color: $mit-red;;
|
||||
color: $mit-red;
|
||||
}
|
||||
|
||||
ul, ol, pre {
|
||||
|
||||
@@ -14,14 +14,6 @@ div#wiki_panel {
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
input[type="submit"]{
|
||||
@extend .light-button;
|
||||
text-transform: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
div#wiki_create_form {
|
||||
@extend .clearfix;
|
||||
padding: lh(.5) lh() lh(.5) 0;
|
||||
@@ -53,4 +45,12 @@ div#wiki_panel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input#wiki_search_input_submit {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
input#wiki_search_input {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,28 +203,23 @@
|
||||
display: block;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
z-index: 50;
|
||||
top: 0px;
|
||||
@include transition(all, 0.15s, linear);
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
border-top: 8px solid;
|
||||
border-left: 8px solid;
|
||||
border-color: rgba(0,0,0, 0.7);
|
||||
@include box-shadow(inset 0 1px 0 0 rgba(255,255,255, 0.8), -1px 0 1px 0 rgba(255,255,255, 0.8));
|
||||
content: "";
|
||||
display: block;
|
||||
height: 55px;
|
||||
left: 50%;
|
||||
margin-left: -10px;
|
||||
margin-top: -30px;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@include transform(rotate(-45deg));
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
font-size: 70px;
|
||||
line-height: 110px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: rgba(0, 0, 0, .7);
|
||||
opacity: 0;
|
||||
@include transition(all, 0.15s, linear);
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -1,52 +1,54 @@
|
||||
form {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
label {
|
||||
color: $base-font-color;
|
||||
font: italic 300 1rem/1.6rem $serif;
|
||||
margin-bottom: 5px;
|
||||
text-shadow: 0 1px rgba(255,255,255, 0.4);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
label {
|
||||
color: $base-font-color;
|
||||
font: italic 300 1rem/1.6rem $serif;
|
||||
margin-bottom: 5px;
|
||||
text-shadow: 0 1px rgba(255,255,255, 0.4);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
textarea,
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"] {
|
||||
background: rgb(250,250,250);
|
||||
border: 1px solid rgb(200,200,200);
|
||||
@include border-radius(3px);
|
||||
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6), inset 0 0 3px 0 rgba(0,0,0, 0.1));
|
||||
@include box-sizing(border-box);
|
||||
font: italic 300 1rem/1.6rem $serif;
|
||||
height: 35px;
|
||||
padding: 5px 12px;
|
||||
vertical-align: top;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
textarea,
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"] {
|
||||
background: rgb(250,250,250);
|
||||
border: 1px solid rgb(200,200,200);
|
||||
@include border-radius(3px);
|
||||
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6), inset 0 0 3px 0 rgba(0,0,0, 0.1));
|
||||
@include box-sizing(border-box);
|
||||
font: italic 300 1rem/1.6rem $serif;
|
||||
height: 35px;
|
||||
padding: 5px 12px;
|
||||
vertical-align: top;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: lighten($blue, 20%);
|
||||
@include box-shadow(0 0 6px 0 rgba($blue, 0.4), inset 0 0 4px 0 rgba(0,0,0, 0.15));
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
@include button(shiny, $blue);
|
||||
@include border-radius(3px);
|
||||
font: normal 1.2rem/1.6rem $sans-serif;
|
||||
height: 35px;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
vertical-align: top;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
&:focus {
|
||||
border-color: lighten($blue, 20%);
|
||||
@include box-shadow(0 0 6px 0 rgba($blue, 0.4), inset 0 0 4px 0 rgba(0,0,0, 0.15));
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
input[type="submit"],
|
||||
input[type="button"],
|
||||
.button {
|
||||
@include border-radius(3px);
|
||||
@include button(shiny, $blue);
|
||||
font: normal 1.2rem/1.6rem $sans-serif;
|
||||
letter-spacing: 1px;
|
||||
padding: 4px 20px;
|
||||
text-transform: uppercase;
|
||||
vertical-align: top;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
12
lms/static/sass/shared/_tooltips.scss
Normal file
12
lms/static/sass/shared/_tooltips.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
.ui-tooltip.qtip .ui-tooltip-content {
|
||||
background: rgba($pink, .8);
|
||||
border: 0;
|
||||
color: #fff;
|
||||
font: bold 12px $body-font-family;
|
||||
margin-bottom: 6px;
|
||||
margin-right: 0;
|
||||
overflow: visible;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
@@ -73,7 +73,7 @@
|
||||
%>
|
||||
<a href="${course_target}" class="cover" style="background-image: url('${course_image_url(course)}')">
|
||||
<div class="shade"></div>
|
||||
<div class="arrow"></div>
|
||||
<div class="arrow">❯</div>
|
||||
</a>
|
||||
<section class="info">
|
||||
<hgroup>
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<input type="submit" class="button" value="Create" style="display: inline-block; margin-right: 2px; font-weight: bold;" />
|
||||
<input type="submit" class="button" value="Create" />
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
@@ -120,8 +120,8 @@
|
||||
<li class="search">
|
||||
<form method="GET" action='${wiki_reverse("wiki_search_articles", course=course, namespace=namespace)}'>
|
||||
<label class="wiki_box_title">Search</label>
|
||||
<input type="text" placeholder="Search" name="value" id="wiki_search_input" style="width: 71%" value="${wiki_search_query if wiki_search_query is not UNDEFINED else '' |h}"/>
|
||||
<input type="submit" id="wiki_search_input_submit" value="Go!" style="width: 20%" />
|
||||
<input type="text" placeholder="Search" name="value" id="wiki_search_input" value="${wiki_search_query if wiki_search_query is not UNDEFINED else '' |h}"/>
|
||||
<input type="submit" id="wiki_search_input_submit" value="Go!" />
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user