Commit 76235d90 by kimth

Merge branch 'master' into kimth/generic-coderesponse

parents ac51cbc3 0847b46c
...@@ -17,8 +17,8 @@ def try_staticfiles_lookup(path): ...@@ -17,8 +17,8 @@ def try_staticfiles_lookup(path):
except Exception as err: except Exception as err:
log.warning("staticfiles_storage couldn't find path {}: {}".format( log.warning("staticfiles_storage couldn't find path {}: {}".format(
path, str(err))) path, str(err)))
# Just return a dead link--don't kill everything. # Just return the original path; don't kill everything.
url = "file_not_found" url = path
return url return url
......
...@@ -257,8 +257,11 @@ def add_user_to_default_group(user, group): ...@@ -257,8 +257,11 @@ def add_user_to_default_group(user, group):
########################## REPLICATION SIGNALS ################################# ########################## REPLICATION SIGNALS #################################
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def replicate_user_save(sender, **kwargs): def replicate_user_save(sender, **kwargs):
user_obj = kwargs['instance'] user_obj = kwargs['instance']
return replicate_model(User.save, user_obj, user_obj.id) 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) @receiver(post_save, sender=CourseEnrollment)
def replicate_enrollment_save(sender, **kwargs): def replicate_enrollment_save(sender, **kwargs):
...@@ -287,8 +290,8 @@ def replicate_enrollment_save(sender, **kwargs): ...@@ -287,8 +290,8 @@ def replicate_enrollment_save(sender, **kwargs):
@receiver(post_delete, sender=CourseEnrollment) @receiver(post_delete, sender=CourseEnrollment)
def replicate_enrollment_delete(sender, **kwargs): def replicate_enrollment_delete(sender, **kwargs):
enrollment_obj = kwargs['instance'] enrollment_obj = kwargs['instance']
return replicate_model(CourseEnrollment.delete, enrollment_obj, enrollment_obj.user_id) return replicate_model(CourseEnrollment.delete, enrollment_obj, enrollment_obj.user_id)
@receiver(post_save, sender=UserProfile) @receiver(post_save, sender=UserProfile)
def replicate_userprofile_save(sender, **kwargs): def replicate_userprofile_save(sender, **kwargs):
...@@ -311,23 +314,20 @@ def replicate_user(portal_user, course_db_name): ...@@ -311,23 +314,20 @@ def replicate_user(portal_user, course_db_name):
overridden. overridden.
""" """
try: 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) 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}" log.debug("User {0} found in Course DB, replicating fields to {1}"
.format(course_user, course_db_name)) .format(course_user, course_db_name))
course_user.save(using=course_db_name) # Just being explicit.
except User.DoesNotExist: 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}" log.debug("User {0} not found in Course DB, creating copy in {1}"
.format(portal_user, course_db_name)) .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): def replicate_model(model_method, instance, user_id):
""" """
...@@ -337,13 +337,14 @@ 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): if not should_replicate(instance):
return return
mark_handled(instance)
course_db_names = db_names_to_replicate_to(user_id) course_db_names = db_names_to_replicate_to(user_id)
log.debug("Replicating {0} for user {1} to DBs: {2}" log.debug("Replicating {0} for user {1} to DBs: {2}"
.format(model_method, user_id, course_db_names)) .format(model_method, user_id, course_db_names))
mark_handled(instance)
for db_name in course_db_names: for db_name in course_db_names:
model_method(instance, using=db_name) model_method(instance, using=db_name)
unmark(instance)
######### Replication Helpers ######### ######### Replication Helpers #########
...@@ -371,7 +372,7 @@ def db_names_to_replicate_to(user_id): ...@@ -371,7 +372,7 @@ def db_names_to_replicate_to(user_id):
def marked_handled(instance): def marked_handled(instance):
"""Have we marked this instance as being handled to avoid infinite loops """Have we marked this instance as being handled to avoid infinite loops
caused by saving models in post_save hooks for the same models?""" 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): def mark_handled(instance):
"""You have to mark your instance with this function or else we'll go into """You have to mark your instance with this function or else we'll go into
...@@ -384,6 +385,11 @@ def mark_handled(instance): ...@@ -384,6 +385,11 @@ def mark_handled(instance):
""" """
instance._do_not_copy_to_course_db = True 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): def should_replicate(instance):
"""Should this instance be replicated? We need to be a Portal server and """Should this instance be replicated? We need to be a Portal server and
the instance has to not have been marked_handled.""" the instance has to not have been marked_handled."""
...@@ -398,9 +404,3 @@ def should_replicate(instance): ...@@ -398,9 +404,3 @@ def should_replicate(instance):
return False return False
return True return True
...@@ -4,6 +4,7 @@ when you run "manage.py test". ...@@ -4,6 +4,7 @@ when you run "manage.py test".
Replace this with more appropriate tests for your application. Replace this with more appropriate tests for your application.
""" """
import logging
from datetime import datetime from datetime import datetime
from django.test import TestCase from django.test import TestCase
...@@ -13,6 +14,8 @@ from .models import User, UserProfile, CourseEnrollment, replicate_user, USER_FI ...@@ -13,6 +14,8 @@ from .models import User, UserProfile, CourseEnrollment, replicate_user, USER_FI
COURSE_1 = 'edX/toy/2012_Fall' COURSE_1 = 'edX/toy/2012_Fall'
COURSE_2 = 'edx/full/6.002_Spring_2012' COURSE_2 = 'edx/full/6.002_Spring_2012'
log = logging.getLogger(__name__)
class ReplicationTest(TestCase): class ReplicationTest(TestCase):
multi_db = True multi_db = True
...@@ -47,23 +50,18 @@ class ReplicationTest(TestCase): ...@@ -47,23 +50,18 @@ class ReplicationTest(TestCase):
field, portal_user, course_user 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 # 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 # triggered when we're being run by CMS tests (Askbot doesn't exist
# there, so the test will fail). # 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'): if hasattr(portal_user, 'seen_response_count'):
portal_user.seen_response_count = 20 portal_user.seen_response_count = 20
replicate_user(portal_user, COURSE_1) replicate_user(portal_user, COURSE_1)
course_user = User.objects.using(COURSE_1).get(id=portal_user.id) course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
self.assertEqual(portal_user.seen_response_count, 20) 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 # Another replication should work for an email change however, since
# it's a field we care about. # it's a field we care about.
...@@ -123,6 +121,25 @@ class ReplicationTest(TestCase): ...@@ -123,6 +121,25 @@ class ReplicationTest(TestCase):
UserProfile.objects.using(COURSE_2).get, UserProfile.objects.using(COURSE_2).get,
id=portal_user_profile.id) 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): def test_enrollment_for_user_info_after_enrollment(self):
"""Test the effect of modifying User data after you've enrolled.""" """Test the effect of modifying User data after you've enrolled."""
......
import datetime import datetime
import feedparser
import itertools
import json import json
import logging import logging
import random import random
import string import string
import sys import sys
import uuid import time
import feedparser
import urllib import urllib
import itertools import uuid
from django.conf import settings from django.conf import settings
from django.contrib.auth import logout, authenticate, login from django.contrib.auth import logout, authenticate, login
...@@ -26,17 +27,19 @@ from bs4 import BeautifulSoup ...@@ -26,17 +27,19 @@ from bs4 import BeautifulSoup
from django.core.cache import cache from django.core.cache import cache
from django_future.csrf import ensure_csrf_cookie 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 util.cache import cache_if_anonymous
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from models import Registration, UserProfile, PendingNameChange, PendingEmailChange, CourseEnrollment
from datetime import date from datetime import date
from collections import namedtuple 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") log = logging.getLogger("mitx.student")
Article = namedtuple('Article', 'title url author image deck publication publish_date') Article = namedtuple('Article', 'title url author image deck publication publish_date')
...@@ -47,7 +50,8 @@ def csrf_token(context): ...@@ -47,7 +50,8 @@ def csrf_token(context):
csrf_token = context.get('csrf_token', '') csrf_token = context.get('csrf_token', '')
if csrf_token == 'NOTPROVIDED': if csrf_token == 'NOTPROVIDED':
return '' 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 @ensure_csrf_cookie
...@@ -162,6 +166,24 @@ def change_enrollment_view(request): ...@@ -162,6 +166,24 @@ def change_enrollment_view(request):
"""Delegate to change_enrollment to actually do the work.""" """Delegate to change_enrollment to actually do the work."""
return HttpResponse(json.dumps(change_enrollment(request))) 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): def change_enrollment(request):
if request.method != "POST": if request.method != "POST":
raise Http404 raise Http404
...@@ -174,7 +196,8 @@ def change_enrollment(request): ...@@ -174,7 +196,8 @@ def change_enrollment(request):
course_id = request.POST.get("course_id", None) course_id = request.POST.get("course_id", None)
if 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": if action == "enroll":
# Make sure the course exists # Make sure the course exists
...@@ -187,12 +210,20 @@ def change_enrollment(request): ...@@ -187,12 +210,20 @@ def change_enrollment(request):
return {'success': False, 'error': 'The course requested does not exist.'} return {'success': False, 'error': 'The course requested does not exist.'}
if settings.MITX_FEATURES.get('ACCESS_REQUIRE_STAFF_FOR_COURSE'): 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 # require that user be in the staff_* group (or be an
# eg staff_6.002x or staff_6.00x # overall admin) to be able to enroll eg staff_6.002x or
# staff_6.00x
if not has_staff_access_to_course(user, course): if not has_staff_access_to_course(user, course):
staff_group = course_staff_group_name(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)) log.debug('user %s denied enrollment to %s ; not in %s' % (
return {'success': False, 'error' : '%s membership required to access course.' % staff_group} 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) enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id)
return {'success': True} return {'success': True}
......
...@@ -21,18 +21,35 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -21,18 +21,35 @@ class CourseDescriptor(SequenceDescriptor):
try: try:
self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M") self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M")
except KeyError: except KeyError:
self.start = time.gmtime(0) #The epoch
msg = "Course loaded without a start date. id = %s" % self.id msg = "Course loaded without a start date. id = %s" % self.id
log.critical(msg)
except ValueError as e: except ValueError as e:
self.start = time.gmtime(0) #The epoch
msg = "Course loaded with a bad start date. %s '%s'" % (self.id, e) 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. # Don't call the tracker from the exception handler.
if msg is not None: if msg is not None:
self.start = time.gmtime(0) # The epoch
log.critical(msg)
system.error_tracker(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): def has_started(self):
return time.gmtime() > self.start return time.gmtime() > self.start
...@@ -100,7 +117,7 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -100,7 +117,7 @@ class CourseDescriptor(SequenceDescriptor):
for s in c.get_children(): for s in c.get_children():
if s.metadata.get('graded', False): if s.metadata.get('graded', False):
xmoduledescriptors = list(yield_descriptor_descendents(s)) xmoduledescriptors = list(yield_descriptor_descendents(s))
# The xmoduledescriptors included here are only the ones that have scores. # The xmoduledescriptors included here are only the ones that have scores.
section_description = { 'section_descriptor' : s, 'xmoduledescriptors' : filter(lambda child: child.has_score, xmoduledescriptors) } section_description = { 'section_descriptor' : s, 'xmoduledescriptors' : filter(lambda child: child.has_score, xmoduledescriptors) }
......
...@@ -89,6 +89,19 @@ div { ...@@ -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 { &.incorrect, &.ui-icon-close {
p.status { p.status {
@include inline-block(); @include inline-block();
...@@ -146,7 +159,7 @@ div { ...@@ -146,7 +159,7 @@ div {
width: 14px; width: 14px;
} }
&.processing, &.ui-icon-check { &.processing, &.ui-icon-processing {
@include inline-block(); @include inline-block();
background: url('../images/spinner.gif') center center no-repeat; background: url('../images/spinner.gif') center center no-repeat;
height: 20px; height: 20px;
......
...@@ -14,7 +14,7 @@ div.video { ...@@ -14,7 +14,7 @@ div.video {
section.video-player { section.video-player {
height: 0; height: 0;
overflow: hidden; // overflow: hidden;
padding-bottom: 56.25%; padding-bottom: 56.25%;
position: relative; position: relative;
...@@ -45,12 +45,13 @@ div.video { ...@@ -45,12 +45,13 @@ div.video {
div.slider { div.slider {
@extend .clearfix; @extend .clearfix;
background: #c2c2c2; background: #c2c2c2;
border: none; border: 1px solid #000;
border-bottom: 1px solid #000;
@include border-radius(0); @include border-radius(0);
border-top: 1px solid #000; border-top: 1px solid #000;
@include box-shadow(inset 0 1px 0 #eee, 0 1px 0 #555); @include box-shadow(inset 0 1px 0 #eee, 0 1px 0 #555);
height: 7px; height: 7px;
margin-left: -1px;
margin-right: -1px;
@include transition(height 2.0s ease-in-out); @include transition(height 2.0s ease-in-out);
div.ui-widget-header { div.ui-widget-header {
...@@ -58,43 +59,12 @@ div.video { ...@@ -58,43 +59,12 @@ div.video {
@include box-shadow(inset 0 1px 0 #999); @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 { 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%); @include background-size(50%);
border: 1px solid darken($mit-red, 20%); border: 1px solid darken($pink, 20%);
@include border-radius(15px); @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; cursor: pointer;
height: 15px; height: 15px;
margin-left: -7px; margin-left: -7px;
...@@ -103,7 +73,7 @@ div.video { ...@@ -103,7 +73,7 @@ div.video {
width: 15px; width: 15px;
&:focus, &:hover { &:focus, &:hover {
background-color: lighten($mit-red, 10%); background-color: lighten($pink, 10%);
outline: none; outline: none;
} }
} }
......
...@@ -227,7 +227,7 @@ class XModule(HTMLSnippet): ...@@ -227,7 +227,7 @@ class XModule(HTMLSnippet):
def get_display_items(self): def get_display_items(self):
''' '''
Returns a list of descendent module instances that will display Returns a list of descendent module instances that will display
immediately inside this module immediately inside this module.
''' '''
items = [] items = []
for child in self.get_children(): for child in self.get_children():
...@@ -238,7 +238,7 @@ class XModule(HTMLSnippet): ...@@ -238,7 +238,7 @@ class XModule(HTMLSnippet):
def displayable_items(self): def displayable_items(self):
''' '''
Returns list of displayable modules contained by this module. If this 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] return [self]
......
...@@ -145,15 +145,11 @@ def progress_summary(student, course, grader, student_module_cache): ...@@ -145,15 +145,11 @@ def progress_summary(student, course, grader, student_module_cache):
instance_modules for the student instance_modules for the student
""" """
chapters = [] chapters = []
for c in course.get_children(): # Don't include chapters that aren't displayable (e.g. due to error)
# Don't include chapters that aren't displayable (e.g. due to error) for c in course.get_display_items():
if c not in c.displayable_items():
continue
sections = [] sections = []
for s in c.get_children(): for s in c.get_display_items():
# Same for sections # Same for sections
if s not in s.displayable_items():
continue
graded = s.metadata.get('graded', False) graded = s.metadata.get('graded', False)
scores = [] scores = []
for module in yield_module_descendents(s): for module in yield_module_descendents(s):
......
...@@ -58,8 +58,22 @@ def mongo_store_config(data_dir): ...@@ -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_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_DIR = settings.GITHUB_REPO_ROOT
REAL_DATA_MODULESTORE = mongo_store_config(REAL_DATA_DIR) REAL_DATA_MODULESTORE = mongo_store_config(REAL_DATA_DIR)
...@@ -149,8 +163,27 @@ class ActivateLoginTestCase(TestCase): ...@@ -149,8 +163,27 @@ class ActivateLoginTestCase(TestCase):
class PageLoader(ActivateLoginTestCase): class PageLoader(ActivateLoginTestCase):
''' Base class that adds a function to load all pages in a modulestore ''' ''' 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): def enroll(self, course):
"""Enroll the currently logged-in user, and check that it worked.""" """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', { resp = self.client.post('/change_enrollment', {
'enrollment_action': 'enroll', 'enrollment_action': 'enroll',
'course_id': course.id, 'course_id': course.id,
...@@ -159,6 +192,7 @@ class PageLoader(ActivateLoginTestCase): ...@@ -159,6 +192,7 @@ class PageLoader(ActivateLoginTestCase):
self.assertTrue(data['success']) self.assertTrue(data['success'])
def check_pages_load(self, course_name, data_dir, modstore): 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) print "Checking course {0} in {1}".format(course_name, data_dir)
import_from_xml(modstore, data_dir, [course_name]) import_from_xml(modstore, data_dir, [course_name])
...@@ -191,7 +225,7 @@ class PageLoader(ActivateLoginTestCase): ...@@ -191,7 +225,7 @@ class PageLoader(ActivateLoginTestCase):
self.assertTrue(all_ok) self.assertTrue(all_ok)
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestCoursesLoadTestCase(PageLoader): class TestCoursesLoadTestCase(PageLoader):
'''Check that all pages in test courses load properly''' '''Check that all pages in test courses load properly'''
...@@ -207,7 +241,7 @@ class TestCoursesLoadTestCase(PageLoader): ...@@ -207,7 +241,7 @@ class TestCoursesLoadTestCase(PageLoader):
self.check_pages_load('full', TEST_DATA_DIR, modulestore()) 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): class TestViewAuth(PageLoader):
"""Check that view authentication works properly""" """Check that view authentication works properly"""
...@@ -215,15 +249,15 @@ class TestViewAuth(PageLoader): ...@@ -215,15 +249,15 @@ class TestViewAuth(PageLoader):
# can't do imports there without manually hacking settings. # can't do imports there without manually hacking settings.
def setUp(self): def setUp(self):
print "sys.path: {}".format(sys.path)
xmodule.modulestore.django._MODULESTORES = {} 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() courses = modulestore().get_courses()
# get the two courses sorted out
courses.sort(key=lambda c: c.location.course) def find_course(name):
[self.full, self.toy] = courses """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 # Create two accounts
self.student = 'view@test.com' self.student = 'view@test.com'
...@@ -304,26 +338,35 @@ class TestViewAuth(PageLoader): ...@@ -304,26 +338,35 @@ class TestViewAuth(PageLoader):
self.check_for_get_code(200, url) self.check_for_get_code(200, url)
def test_dark_launch(self): def run_wrapped(self, test):
"""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 DARK_LAUNCH.
Because settings is global, be careful not to mess it up for other tests
# test.py turns off start dates, enable them and set them correctly. (Can't use override_settings because we're only changing part of the
# Because settings is global, be careful not to mess it up for other tests MITX_FEATURES dict)
# (Can't use override_settings because we're only changing part of the """
# MITX_FEATURES dict)
oldDSD = settings.MITX_FEATURES['DISABLE_START_DATES'] oldDSD = settings.MITX_FEATURES['DISABLE_START_DATES']
oldDL = settings.MITX_FEATURES['DARK_LAUNCH'] oldDL = settings.MITX_FEATURES['DARK_LAUNCH']
try: try:
settings.MITX_FEATURES['DISABLE_START_DATES'] = False settings.MITX_FEATURES['DISABLE_START_DATES'] = False
settings.MITX_FEATURES['DARK_LAUNCH'] = True settings.MITX_FEATURES['DARK_LAUNCH'] = True
self._do_test_dark_launch() test()
finally: finally:
settings.MITX_FEATURES['DISABLE_START_DATES'] = oldDSD settings.MITX_FEATURES['DISABLE_START_DATES'] = oldDSD
settings.MITX_FEATURES['DARK_LAUNCH'] = oldDL 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): def _do_test_dark_launch(self):
"""Actually do the test, relying on settings to be right.""" """Actually do the test, relying on settings to be right."""
...@@ -338,6 +381,7 @@ class TestViewAuth(PageLoader): ...@@ -338,6 +381,7 @@ class TestViewAuth(PageLoader):
self.assertTrue(settings.MITX_FEATURES['DARK_LAUNCH']) self.assertTrue(settings.MITX_FEATURES['DARK_LAUNCH'])
def reverse_urls(names, course): def reverse_urls(names, course):
"""Reverse a list of course urls"""
return [reverse(name, kwargs={'course_id': course.id}) for name in names] return [reverse(name, kwargs={'course_id': course.id}) for name in names]
def dark_student_urls(course): def dark_student_urls(course):
...@@ -424,6 +468,53 @@ class TestViewAuth(PageLoader): ...@@ -424,6 +468,53 @@ class TestViewAuth(PageLoader):
check_staff(self.toy) check_staff(self.toy)
check_staff(self.full) 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) @override_settings(MODULESTORE=REAL_DATA_MODULESTORE)
class RealCoursesLoadTestCase(PageLoader): class RealCoursesLoadTestCase(PageLoader):
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
@import 'base/base'; @import 'base/base';
@import 'base/extends'; @import 'base/extends';
@import 'base/animations'; @import 'base/animations';
@import 'shared/tooltips';
// Course base / layout styles // Course base / layout styles
@import 'course/layout/courseware_subnav'; @import 'course/layout/courseware_subnav';
......
...@@ -15,15 +15,15 @@ div.info-wrapper { ...@@ -15,15 +15,15 @@ div.info-wrapper {
> ol { > ol {
list-style: none; list-style: none;
padding-left: 0;
margin-bottom: lh(); margin-bottom: lh();
padding-left: 0;
> li { > li {
@extend .clearfix; @extend .clearfix;
border-bottom: 1px solid lighten($border-color, 10%); border-bottom: 1px solid lighten($border-color, 10%);
list-style-type: disk;
margin-bottom: lh(); margin-bottom: lh();
padding-bottom: lh(.5); padding-bottom: lh(.5);
list-style-type: disk;
&:first-child { &:first-child {
margin: 0 (-(lh(.5))) lh(); margin: 0 (-(lh(.5))) lh();
...@@ -41,10 +41,10 @@ div.info-wrapper { ...@@ -41,10 +41,10 @@ div.info-wrapper {
h2 { h2 {
float: left; float: left;
margin: 0 flex-gutter() 0 0;
width: flex-grid(2, 9);
font-size: $body-font-size; font-size: $body-font-size;
font-weight: bold; font-weight: bold;
margin: 0 flex-gutter() 0 0;
width: flex-grid(2, 9);
} }
section.update-description { section.update-description {
...@@ -68,15 +68,15 @@ div.info-wrapper { ...@@ -68,15 +68,15 @@ div.info-wrapper {
section.handouts { section.handouts {
@extend .sidebar; @extend .sidebar;
border-left: 1px solid #d3d3d3; border-left: 1px solid $border-color;
@include border-radius(0 4px 4px 0); @include border-radius(0 4px 4px 0);
@include box-shadow(none);
border-right: 0; border-right: 0;
@include box-shadow(none);
h1 { h1 {
@extend .bottom-border; @extend .bottom-border;
padding: lh(.5) lh(.5);
margin-bottom: 0; margin-bottom: 0;
padding: lh(.5) lh(.5);
} }
ol { ol {
...@@ -90,8 +90,9 @@ div.info-wrapper { ...@@ -90,8 +90,9 @@ div.info-wrapper {
&.expandable, &.expandable,
&.collapsable { &.collapsable {
h4 { h4 {
font-weight: normal; color: $blue;
font-size: 1em; font-size: 1em;
font-weight: normal;
padding: lh(.25) 0 lh(.25) lh(1.5); padding: lh(.25) 0 lh(.25) lh(1.5);
} }
} }
...@@ -145,7 +146,8 @@ div.info-wrapper { ...@@ -145,7 +146,8 @@ div.info-wrapper {
filter: alpha(opacity=60); filter: alpha(opacity=60);
+ h4 { + h4 {
background-color: #e3e3e3; @extend a:hover;
text-decoration: underline;
} }
} }
......
...@@ -3,6 +3,7 @@ body { ...@@ -3,6 +3,7 @@ body {
} }
body, h1, h2, h3, h4, h5, h6, p, p a:link, p a:visited, a { body, h1, h2, h3, h4, h5, h6, p, p a:link, p a:visited, a {
text-align: left;
font-family: $sans-serif; font-family: $sans-serif;
} }
......
...@@ -25,24 +25,12 @@ h1.top-header { ...@@ -25,24 +25,12 @@ h1.top-header {
} }
} }
.action-link {
a {
color: $mit-red;
&:hover {
color: darken($mit-red, 20%);
text-decoration: none;
}
}
}
.content { .content {
@include box-sizing(border-box); @include box-sizing(border-box);
display: table-cell; display: table-cell;
padding: lh(); padding: lh();
vertical-align: top; vertical-align: top;
width: flex-grid(9) + flex-gutter(); width: flex-grid(9) + flex-gutter();
overflow: hidden;
@media print { @media print {
@include box-shadow(none); @include box-shadow(none);
...@@ -164,7 +152,6 @@ h1.top-header { ...@@ -164,7 +152,6 @@ h1.top-header {
.topbar { .topbar {
@extend .clearfix; @extend .clearfix;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
font-size: 14px;
@media print { @media print {
display: none; display: none;
...@@ -193,17 +180,17 @@ h1.top-header { ...@@ -193,17 +180,17 @@ h1.top-header {
h2 { h2 {
display: block; display: block;
width: 700px;
float: left; float: left;
font-size: 0.9em; font-size: 0.9em;
font-weight: 600; font-weight: 600;
line-height: 40px;
letter-spacing: 0; letter-spacing: 0;
text-transform: none; line-height: 40px;
overflow: hidden;
text-overflow: ellipsis;
text-shadow: 0 1px 0 #fff; text-shadow: 0 1px 0 #fff;
text-transform: none;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; width: 700px;
overflow: hidden;
.provider { .provider {
font: inherit; font: inherit;
...@@ -211,4 +198,4 @@ h1.top-header { ...@@ -211,4 +198,4 @@ h1.top-header {
color: #6d6d6d; color: #6d6d6d;
} }
} }
} }
\ No newline at end of file
...@@ -146,13 +146,13 @@ div.course-wrapper { ...@@ -146,13 +146,13 @@ div.course-wrapper {
@include border-radius(0); @include border-radius(0);
a.ui-slider-handle { 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; 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; cursor: pointer;
&:hover, &:focus { &:hover, &:focus {
background-color: lighten($mit-red, 10%); background-color: lighten($pink, 10%);
outline: none; outline: none;
} }
} }
......
...@@ -13,7 +13,7 @@ section.course-index { ...@@ -13,7 +13,7 @@ section.course-index {
div#accordion { div#accordion {
h3 { h3 {
@include border-radius(0); @include border-radius(0);
border-top: 1px solid #e3e3e3; border-top: 1px solid lighten($border-color, 10%);
font-size: em(16, 18); font-size: em(16, 18);
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
...@@ -34,6 +34,7 @@ section.course-index { ...@@ -34,6 +34,7 @@ section.course-index {
} }
&.ui-accordion-header { &.ui-accordion-header {
border-bottom: none;
color: #000; color: #000;
a { a {
......
...@@ -17,7 +17,6 @@ div.answer-controls { ...@@ -17,7 +17,6 @@ div.answer-controls {
margin-left: flex-gutter(); margin-left: flex-gutter();
nav { nav {
@extend .action-link;
float: right; float: right;
margin-top: 34px; margin-top: 34px;
...@@ -144,7 +143,7 @@ div.answer-actions { ...@@ -144,7 +143,7 @@ div.answer-actions {
text-decoration: none; text-decoration: none;
&.question-delete { &.question-delete {
// color: $mit-red; color: $mit-red;
} }
} }
} }
......
...@@ -92,7 +92,7 @@ form.answer-form { ...@@ -92,7 +92,7 @@ form.answer-form {
margin-left: 2.5%; margin-left: 2.5%;
padding-left: 1.5%; padding-left: 1.5%;
border-left: 1px dashed #ddd; border-left: 1px dashed #ddd;
color: $mit-red;; color: $mit-red;
} }
ul, ol, pre { ul, ol, pre {
......
...@@ -14,14 +14,6 @@ div#wiki_panel { ...@@ -14,14 +14,6 @@ div#wiki_panel {
} }
} }
form {
input[type="submit"]{
@extend .light-button;
text-transform: none;
text-shadow: none;
}
}
div#wiki_create_form { div#wiki_create_form {
@extend .clearfix; @extend .clearfix;
padding: lh(.5) lh() lh(.5) 0; padding: lh(.5) lh() lh(.5) 0;
...@@ -53,4 +45,12 @@ div#wiki_panel { ...@@ -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 @@ ...@@ -203,28 +203,23 @@
display: block; display: block;
left: 0px; left: 0px;
position: absolute; position: absolute;
z-index: 50;
top: 0px; top: 0px;
@include transition(all, 0.15s, linear); @include transition(all, 0.15s, linear);
right: 0px; right: 0px;
} }
.arrow { .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; position: absolute;
top: 50%; z-index: 100;
@include transform(rotate(-45deg)); 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); @include transition(all, 0.15s, linear);
width: 55px;
} }
&:hover { &:hover {
......
form { form {
font-size: 1em; font-size: 1em;
}
label { label {
color: $base-font-color; color: $base-font-color;
font: italic 300 1rem/1.6rem $serif; font: italic 300 1rem/1.6rem $serif;
margin-bottom: 5px; margin-bottom: 5px;
text-shadow: 0 1px rgba(255,255,255, 0.4); text-shadow: 0 1px rgba(255,255,255, 0.4);
-webkit-font-smoothing: antialiased; -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 { textarea,
margin-right: 0px; 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;
&:focus { &:last-child {
border-color: lighten($blue, 20%); margin-right: 0px;
@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 { &:focus {
height: 60px; 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;
} }
}
input[type="submit"] { textarea {
@include button(shiny, $blue); height: 60px;
@include border-radius(3px); }
font: normal 1.2rem/1.6rem $sans-serif;
height: 35px; input[type="submit"],
letter-spacing: 1px; input[type="button"],
text-transform: uppercase; .button {
vertical-align: top; @include border-radius(3px);
-webkit-font-smoothing: antialiased; @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;
} }
.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 @@ ...@@ -73,7 +73,7 @@
%> %>
<a href="${course_target}" class="cover" style="background-image: url('${course_image_url(course)}')"> <a href="${course_target}" class="cover" style="background-image: url('${course_image_url(course)}')">
<div class="shade"></div> <div class="shade"></div>
<div class="arrow"></div> <div class="arrow">&#10095;</div>
</a> </a>
<section class="info"> <section class="info">
<hgroup> <hgroup>
......
...@@ -110,7 +110,7 @@ ...@@ -110,7 +110,7 @@
</div> </div>
<ul> <ul>
<li> <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> </li>
</ul> </ul>
</form> </form>
...@@ -120,8 +120,8 @@ ...@@ -120,8 +120,8 @@
<li class="search"> <li class="search">
<form method="GET" action='${wiki_reverse("wiki_search_articles", course=course, namespace=namespace)}'> <form method="GET" action='${wiki_reverse("wiki_search_articles", course=course, namespace=namespace)}'>
<label class="wiki_box_title">Search</label> <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="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!" style="width: 20%" /> <input type="submit" id="wiki_search_input_submit" value="Go!" />
</form> </form>
</li> </li>
</ul> </ul>
......
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