diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py
index 35e59db0ca..9e41d31c77 100644
--- a/common/djangoapps/external_auth/views.py
+++ b/common/djangoapps/external_auth/views.py
@@ -157,7 +157,7 @@ def edXauth_signup(request, eamap=None):
log.debug('ExtAuth: doing signup for %s' % eamap.external_email)
- return student_views.main_index(request, extra_context=context)
+ return student_views.index(request, extra_context=context)
#-----------------------------------------------------------------------------
# MIT SSL
@@ -193,7 +193,7 @@ def edXauth_ssl_login(request):
The certificate provides user email and fullname; this populates the ExternalAuthMap.
The user is nevertheless still asked to complete the edX signup.
- Else continues on with student.views.main_index, and no authentication.
+ Else continues on with student.views.index, and no authentication.
"""
certkey = "SSL_CLIENT_S_DN" # specify the request.META field to use
@@ -207,7 +207,7 @@ def edXauth_ssl_login(request):
pass
if not cert:
# no certificate information - go onward to main index
- return student_views.main_index(request)
+ return student_views.index(request)
(user, email, fullname) = ssl_dn_extract_info(cert)
@@ -217,4 +217,4 @@ def edXauth_ssl_login(request):
credentials=cert,
email=email,
fullname=fullname,
- retfun = functools.partial(student_views.main_index, request))
+ retfun = functools.partial(student_views.index, request))
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 0069935b0b..e7864337b3 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -22,7 +22,6 @@ from django.db import IntegrityError
from django.http import HttpResponse, Http404
from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string
-from django.core.urlresolvers import reverse
from bs4 import BeautifulSoup
from django.core.cache import cache
@@ -30,7 +29,6 @@ from django_future.csrf import ensure_csrf_cookie
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
@@ -54,23 +52,7 @@ def csrf_token(context):
' name="csrfmiddlewaretoken" value="%s" />' % (csrf_token))
-@ensure_csrf_cookie
-@cache_if_anonymous
-def index(request):
-
- ''' Redirects to main page -- info page if user authenticated, or marketing if not
- '''
-
- if settings.COURSEWARE_ENABLED and request.user.is_authenticated():
- return redirect(reverse('dashboard'))
-
- if settings.MITX_FEATURES.get('AUTH_USE_MIT_CERTIFICATES'):
- from external_auth.views import edXauth_ssl_login
- return edXauth_ssl_login(request)
-
- return main_index(request, user=request.user)
-
-def main_index(request, extra_context={}, user=None):
+def index(request, extra_context={}, user=None):
'''
Render the edX main page.
diff --git a/common/djangoapps/util/cache.py b/common/djangoapps/util/cache.py
index 85b8ed3369..89b5dffd5e 100644
--- a/common/djangoapps/util/cache.py
+++ b/common/djangoapps/util/cache.py
@@ -9,6 +9,7 @@ from functools import wraps
from django.core import cache
+
# If we can't find a 'general' CACHE defined in settings.py, we simply fall back
# to returning the default cache. This will happen with dev machines.
try:
@@ -41,7 +42,10 @@ def cache_if_anonymous(view_func):
def _decorated(request, *args, **kwargs):
if not request.user.is_authenticated():
#Use the cache
- cache_key = "cache_if_anonymous." + request.path
+ # same view accessed through different domain names may
+ # return different things, so include the domain name in the key.
+ domain = str(request.META.get('HTTP_HOST')) + '.'
+ cache_key = domain + "cache_if_anonymous." + request.path
response = cache.get(cache_key)
if not response:
response = view_func(request, *args, **kwargs)
diff --git a/common/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py
index 4b3050e227..86443520c2 100644
--- a/common/djangoapps/xmodule_modifiers.py
+++ b/common/djangoapps/xmodule_modifiers.py
@@ -112,11 +112,14 @@ def add_histogram(get_html, module, user):
edit_link = "%s/%s/tree/master/%s" % (giturl,data_dir,filepath)
else:
edit_link = False
+ source_file = module.metadata.get('source_file','') # source used to generate the problem XML, eg latex or word
staff_context = {'definition': module.definition.get('data'),
'metadata': json.dumps(module.metadata, indent=4),
'location': module.location,
'xqa_key': module.metadata.get('xqa_key',''),
+ 'source_file' : source_file,
+ 'source_url': '%s/%s/tree/master/%s' % (giturl,data_dir,source_file),
'category': str(module.__class__.__name__),
'element_id': module.location.html_id().replace('-','_'),
'edit_link': edit_link,
diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py
index cc67389da9..b2d56b48ca 100644
--- a/common/lib/capa/capa/responsetypes.py
+++ b/common/lib/capa/capa/responsetypes.py
@@ -557,7 +557,7 @@ class ChoiceResponse(LoncapaResponse):
return CorrectMap(self.answer_id, 'incorrect')
def get_answers(self):
- return {self.answer_id: self.correct_choices}
+ return {self.answer_id: list(self.correct_choices)}
#-----------------------------------------------------------------------------
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index e6da87b5c6..d2ed3912a4 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -390,9 +390,19 @@ class CapaModule(XModule):
raise NotFoundError('Answer is not available')
else:
answers = self.lcp.get_question_answers()
+
# answers (eg ) may have embedded images
- answers = dict( (k,self.system.replace_urls(answers[k], self.metadata['data_dir'])) for k in answers )
- return {'answers': answers}
+ # but be careful, some problems are using non-string answer dicts
+ new_answers = dict()
+ for answer_id in answers:
+ try:
+ new_answer = {answer_id: self.system.replace_urls(answers[answer_id], self.metadata['data_dir'])}
+ except TypeError:
+ log.debug('Unable to perform URL substitution on answers[%s]: %s' % (answer_id, answers[answer_id]))
+ new_answer = {answer_id: answers[answer_id]}
+ new_answers.update(new_answer)
+
+ return {'answers': new_answers}
# Figure out if we should move these to capa_problem?
def get_problem(self, get):
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 5cc4a09165..3896affca1 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -60,6 +60,8 @@ class CourseDescriptor(SequenceDescriptor):
def __init__(self, system, definition=None, **kwargs):
super(CourseDescriptor, self).__init__(system, definition, **kwargs)
self.textbooks = self.definition['data']['textbooks']
+
+ self.wiki_slug = self.definition['data']['wiki_slug'] or self.location.course
msg = None
if self.start is None:
@@ -94,8 +96,19 @@ class CourseDescriptor(SequenceDescriptor):
for textbook in xml_object.findall("textbook"):
textbooks.append(cls.Textbook.from_xml_object(textbook))
xml_object.remove(textbook)
+
+ #Load the wiki tag if it exists
+ wiki_slug = None
+ wiki_tag = xml_object.find("wiki")
+ if wiki_tag is not None:
+ wiki_slug = wiki_tag.attrib.get("slug", default=None)
+ xml_object.remove(wiki_tag)
+
definition = super(CourseDescriptor, cls).definition_from_xml(xml_object, system)
+
definition.setdefault('data', {})['textbooks'] = textbooks
+ definition['data']['wiki_slug'] = wiki_slug
+
return definition
def has_started(self):
@@ -197,6 +210,19 @@ class CourseDescriptor(SequenceDescriptor):
def start_date_text(self):
return time.strftime("%b %d, %Y", self.start)
+ # An extra property is used rather than the wiki_slug/number because
+ # there are courses that change the number for different runs. This allows
+ # courses to share the same css_class across runs even if they have
+ # different numbers.
+ #
+ # TODO get rid of this as soon as possible or potentially build in a robust
+ # way to add in course-specific styling. There needs to be a discussion
+ # about the right way to do this, but arjun will address this ASAP. Also
+ # note that the courseware template needs to change when this is removed.
+ @property
+ def css_class(self):
+ return self.metadata.get('css_class', '')
+
@property
def title(self):
return self.display_name
@@ -205,10 +231,6 @@ class CourseDescriptor(SequenceDescriptor):
def number(self):
return self.location.course
- @property
- def wiki_slug(self):
- return self.location.course
-
@property
def org(self):
return self.location.org
diff --git a/common/lib/xmodule/xmodule/error_module.py b/common/lib/xmodule/xmodule/error_module.py
index bdd7179a0a..f8e2467910 100644
--- a/common/lib/xmodule/xmodule/error_module.py
+++ b/common/lib/xmodule/xmodule/error_module.py
@@ -15,18 +15,39 @@ from xmodule.errortracker import exc_info_to_str
log = logging.getLogger(__name__)
+# NOTE: This is not the most beautiful design in the world, but there's no good
+# way to tell if the module is being used in a staff context or not. Errors that get discovered
+# at course load time are turned into ErrorDescriptor objects, and automatically hidden from students.
+# Unfortunately, we can also have errors when loading modules mid-request, and then we need to decide
+# what to show, and the logic for that belongs in the LMS (e.g. in get_module), so the error handler
+# decides whether to create a staff or not-staff module.
+
class ErrorModule(XModule):
def get_html(self):
- '''Show an error.
+ '''Show an error to staff.
TODO (vshnayder): proper style, divs, etc.
'''
# staff get to see all the details
return self.system.render_template('module-error.html', {
+ 'staff_access' : True,
'data' : self.definition['data']['contents'],
'error' : self.definition['data']['error_msg'],
})
+class NonStaffErrorModule(XModule):
+ def get_html(self):
+ '''Show an error to a student.
+ TODO (vshnayder): proper style, divs, etc.
+ '''
+ # staff get to see all the details
+ return self.system.render_template('module-error.html', {
+ 'staff_access' : False,
+ 'data' : "",
+ 'error' : "",
+ })
+
+
class ErrorDescriptor(EditingDescriptor):
"""
Module that provides a raw editing view of broken xml.
@@ -99,3 +120,9 @@ class ErrorDescriptor(EditingDescriptor):
err_node = etree.SubElement(root, 'error_msg')
err_node.text = self.definition['data']['error_msg']
return etree.tostring(root)
+
+class NonStaffErrorDescriptor(ErrorDescriptor):
+ """
+ Module that provides non-staff error messages.
+ """
+ module_class = NonStaffErrorModule
diff --git a/lms/djangoapps/branding/__init__.py b/lms/djangoapps/branding/__init__.py
new file mode 100644
index 0000000000..3a4acd2964
--- /dev/null
+++ b/lms/djangoapps/branding/__init__.py
@@ -0,0 +1,52 @@
+
+from xmodule.modulestore.django import modulestore
+from xmodule.course_module import CourseDescriptor
+from django.conf import settings
+
+
+def get_subdomain(domain):
+ return domain.split(".")[0]
+
+
+def get_visible_courses(domain=None):
+ """
+ Return the set of CourseDescriptors that should be visible in this branded instance
+ """
+ courses = [c for c in modulestore().get_courses()
+ if isinstance(c, CourseDescriptor)]
+ courses = sorted(courses, key=lambda course: course.number)
+
+ if domain and settings.MITX_FEATURES.get('SUBDOMAIN_COURSE_LISTINGS'):
+ subdomain = get_subdomain(domain)
+ if subdomain not in settings.COURSE_LISTINGS:
+ subdomain = 'default'
+ visible_ids = frozenset(settings.COURSE_LISTINGS[subdomain])
+ return [course for course in courses if course.id in visible_ids]
+ else:
+ return courses
+
+
+def get_university(domain=None):
+ """
+ Return the university name specified for the domain, or None
+ if no university was specified
+ """
+ if not settings.MITX_FEATURES['SUBDOMAIN_BRANDING'] or domain is None:
+ return None
+
+ subdomain = get_subdomain(domain)
+ return settings.SUBDOMAIN_BRANDING.get(subdomain)
+
+
+def get_logo_url(domain=None):
+ """
+ Return the url for the branded logo image to be used
+ """
+ university = get_university(domain)
+
+ if university is None:
+ return '/static/images/header-logo.png'
+
+ return '/static/images/{uni}-on-edx-logo.png'.format(
+ uni=university
+ )
diff --git a/lms/djangoapps/branding/views.py b/lms/djangoapps/branding/views.py
new file mode 100644
index 0000000000..e32eb92138
--- /dev/null
+++ b/lms/djangoapps/branding/views.py
@@ -0,0 +1,45 @@
+from django.conf import settings
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django_future.csrf import ensure_csrf_cookie
+
+import student.views
+import branding
+import courseware.views
+from util.cache import cache_if_anonymous
+
+
+@ensure_csrf_cookie
+@cache_if_anonymous
+def index(request):
+ '''
+ Redirects to main page -- info page if user authenticated, or marketing if not
+ '''
+
+ if settings.COURSEWARE_ENABLED and request.user.is_authenticated():
+ return redirect(reverse('dashboard'))
+
+ if settings.MITX_FEATURES.get('AUTH_USE_MIT_CERTIFICATES'):
+ from external_auth.views import edXauth_ssl_login
+ return edXauth_ssl_login(request)
+
+ university = branding.get_university(request.META.get('HTTP_HOST'))
+ if university is None:
+ return student.views.index(request, user=request.user)
+
+ return courseware.views.university_profile(request, university)
+
+
+@ensure_csrf_cookie
+@cache_if_anonymous
+def courses(request):
+ """
+ Render the "find courses" page. If subdomain branding is on, this is the
+ university profile page, otherwise it's the edX courseware.views.courses page
+ """
+
+ university = branding.get_university(request.META.get('HTTP_HOST'))
+ if university is None:
+ return courseware.views.courses(request)
+
+ return courseware.views.university_profile(request, university)
diff --git a/lms/djangoapps/course_wiki/course_nav.py b/lms/djangoapps/course_wiki/course_nav.py
index 1d124972c7..122f9ebb54 100644
--- a/lms/djangoapps/course_wiki/course_nav.py
+++ b/lms/djangoapps/course_wiki/course_nav.py
@@ -5,6 +5,7 @@ from django.http import Http404
from django.shortcuts import redirect
from wiki.models import reverse as wiki_reverse
+from courseware.access import has_access
from courseware.courses import get_course_with_access
@@ -135,7 +136,9 @@ def context_processor(request):
try:
course = get_course_with_access(request.user, course_id, 'load')
- return {'course' : course}
+ staff_access = has_access(request.user, course, 'staff')
+ return {'course' : course,
+ 'staff_access': staff_access}
except Http404:
# We couldn't access the course for whatever reason. It is too late to change
# the URL here, so we just leave the course context. The middleware shouldn't
diff --git a/lms/djangoapps/course_wiki/views.py b/lms/djangoapps/course_wiki/views.py
index cfe802bbd7..ff6e08abf5 100644
--- a/lms/djangoapps/course_wiki/views.py
+++ b/lms/djangoapps/course_wiki/views.py
@@ -80,8 +80,8 @@ def course_wiki_redirect(request, course_id):
urlpath = URLPath.create_article(
root,
course_slug,
- title=course.number,
- content="{0}\n===\nThis is the wiki for **{1}**'s _{2}_.".format(course.number, course.org, course.title),
+ title=course_slug,
+ content="This is the wiki for **{0}**'s _{1}_.".format(course.org, course.title),
user_message="Course page automatically created.",
user=None,
ip_address=None,
@@ -114,7 +114,7 @@ def get_or_create_root():
"===",
"Visit a course wiki to add an article."))
- root = URLPath.create_root(title="edX Wiki",
+ root = URLPath.create_root(title="Wiki",
content=starting_content)
article = root.article
article.group = None
diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py
index 281580cf33..dbe4ff376d 100644
--- a/lms/djangoapps/courseware/access.py
+++ b/lms/djangoapps/courseware/access.py
@@ -13,7 +13,6 @@ from xmodule.modulestore import Location
from xmodule.timeparse import parse_time
from xmodule.x_module import XModule, XModuleDescriptor
-
DEBUG_ACCESS = False
log = logging.getLogger(__name__)
diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py
index c92cbb1425..e5ef915e25 100644
--- a/lms/djangoapps/courseware/courses.py
+++ b/lms/djangoapps/courseware/courses.py
@@ -13,6 +13,7 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
from static_replace import replace_urls, try_staticfiles_lookup
from courseware.access import has_access
+import branding
log = logging.getLogger(__name__)
@@ -141,9 +142,10 @@ def get_course_info_section(course, section_key):
raise KeyError("Invalid about key " + str(section_key))
+
# TODO: Fix this such that these are pulled in as extra course-specific tabs.
# arjun will address this by the end of October if no one does so prior to
-# then.
+# then.
def get_course_syllabus_section(course, section_key):
"""
This returns the snippet of html to be rendered on the syllabus page,
@@ -178,24 +180,11 @@ def get_courses_by_university(user, domain=None):
'''
# TODO: Clean up how 'error' is done.
# filter out any courses that errored.
- courses = [c for c in modulestore().get_courses()
- if isinstance(c, CourseDescriptor)]
- courses = sorted(courses, key=lambda course: course.number)
-
- if domain and settings.MITX_FEATURES.get('SUBDOMAIN_COURSE_LISTINGS'):
- subdomain = domain.split(".")[0]
- if subdomain not in settings.COURSE_LISTINGS:
- subdomain = 'default'
- visible_courses = frozenset(settings.COURSE_LISTINGS[subdomain])
- else:
- visible_courses = frozenset(c.id for c in courses)
+ visible_courses = branding.get_visible_courses(domain)
universities = defaultdict(list)
- for course in courses:
+ for course in visible_courses:
if not has_access(user, course, 'see_exists'):
continue
- if course.id not in visible_courses:
- continue
universities[course.org].append(course)
return universities
-
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index 65e30475f2..7967452647 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -1,5 +1,6 @@
import json
import logging
+import sys
from django.conf import settings
from django.contrib.auth.models import User
@@ -15,10 +16,12 @@ from courseware.access import has_access
from mitxmako.shortcuts import render_to_string
from models import StudentModule, StudentModuleCache
from static_replace import replace_urls
+from xmodule.errortracker import exc_info_to_str
from xmodule.exceptions import NotFoundError
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.x_module import ModuleSystem
+from xmodule.error_module import ErrorDescriptor, NonStaffErrorDescriptor
from xmodule_modifiers import replace_course_urls, replace_static_urls, add_histogram, wrap_xmodule
log = logging.getLogger("mitx.courseware")
@@ -73,6 +76,8 @@ def toc_for_course(user, request, course, active_chapter, active_section, course
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
course_id, user, course, depth=2)
course = get_module(user, request, course.location, student_module_cache, course_id)
+ if course is None:
+ return None
chapters = list()
for chapter in course.get_display_items():
@@ -131,9 +136,9 @@ def get_section(course_module, chapter, section):
return section_module
-
def get_module(user, request, location, student_module_cache, course_id, position=None):
- ''' Get an instance of the xmodule class identified by location,
+ """
+ Get an instance of the xmodule class identified by location,
setting the state based on an existing StudentModule, or creating one if none
exists.
@@ -146,9 +151,22 @@ def get_module(user, request, location, student_module_cache, course_id, positio
- position : extra information from URL for user-specified
position within module
- Returns: xmodule instance
+ Returns: xmodule instance, or None if the user does not have access to the
+ module. If there's an error, will try to return an instance of ErrorModule
+ if possible. If not possible, return None.
+ """
+ try:
+ return _get_module(user, request, location, student_module_cache, course_id, position)
+ except:
+ # Something has gone terribly wrong, but still not letting it turn into a 500.
+ log.exception("Error in get_module")
+ return None
- '''
+def _get_module(user, request, location, student_module_cache, course_id, position=None):
+ """
+ Actually implement get_module. See docstring there for details.
+ """
+ location = Location(location)
descriptor = modulestore().get_instance(course_id, location)
# Short circuit--if the user shouldn't have access, bail without doing any work
@@ -198,7 +216,7 @@ def get_module(user, request, location, student_module_cache, course_id, positio
'callback_url': xqueue_callback_url,
'default_queuename': xqueue_default_queuename.replace(' ', '_')}
- def _get_module(location):
+ def inner_get_module(location):
"""
Delegate to get_module. It does an access check, so may return None
"""
@@ -214,7 +232,7 @@ def get_module(user, request, location, student_module_cache, course_id, positio
xqueue=xqueue,
# TODO (cpennington): Figure out how to share info between systems
filestore=descriptor.system.resources_fs,
- get_module=_get_module,
+ get_module=inner_get_module,
user=user,
# TODO (cpennington): This should be removed when all html from
# a module is coming through get_html and is therefore covered
@@ -226,7 +244,22 @@ def get_module(user, request, location, student_module_cache, course_id, positio
system.set('position', position)
system.set('DEBUG', settings.DEBUG)
- module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
+ try:
+ module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
+ except:
+ log.exception("Error creating module from descriptor {0}".format(descriptor))
+
+ # make an ErrorDescriptor -- assuming that the descriptor's system is ok
+ import_system = descriptor.system
+ if has_access(user, location, 'staff'):
+ err_descriptor = ErrorDescriptor.from_xml(str(descriptor), import_system,
+ error_msg=exc_info_to_str(sys.exc_info()))
+ else:
+ err_descriptor = NonStaffErrorDescriptor.from_xml(str(descriptor), import_system,
+ error_msg=exc_info_to_str(sys.exc_info()))
+
+ # Make an error module
+ return err_descriptor.xmodule_constructor(system)(None, None)
module.get_html = replace_static_urls(
wrap_xmodule(module.get_html, module, 'xmodule_display.html'),
diff --git a/lms/envs/common.py b/lms/envs/common.py
index a217f0e7b9..1cc6ae8d89 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -55,9 +55,14 @@ MITX_FEATURES = {
# course_ids (see dev_int.py for an example)
'SUBDOMAIN_COURSE_LISTINGS' : False,
+ # When True, will override certain branding with university specific values
+ # Expects a SUBDOMAIN_BRANDING dictionary that maps the subdomain to the
+ # university to use for branding purposes
+ 'SUBDOMAIN_BRANDING': False,
+
# TODO: This will be removed once course-specific tabs are in place. see
# courseware/courses.py
- 'ENABLE_SYLLABUS' : True,
+ 'ENABLE_SYLLABUS' : True,
'ENABLE_TEXTBOOK' : True,
'ENABLE_DISCUSSION' : False,
@@ -66,7 +71,7 @@ MITX_FEATURES = {
'ENABLE_SQL_TRACKING_LOGS': False,
'ENABLE_LMS_MIGRATION': False,
- 'DISABLE_LOGIN_BUTTON': False, # used in systems where login is automatic, eg MIT SSL
+ 'DISABLE_LOGIN_BUTTON': False, # used in systems where login is automatic, eg MIT SSL
# extrernal access methods
'ACCESS_REQUIRE_STAFF_FOR_COURSE': False,
@@ -199,6 +204,11 @@ COURSE_SETTINGS = {'6.002x_Fall_2012': {'number' : '6.002x',
# TODO (vshnayder): Will probably need to change as we get real access control in.
LMS_MIGRATION_ALLOWED_IPS = []
+######################## subdomain specific settings ###########################
+COURSE_LISTINGS = {}
+SUBDOMAIN_BRANDING = {}
+
+
############################### XModule Store ##################################
MODULESTORE = {
'default': {
@@ -318,6 +328,7 @@ WIKI_ACCOUNT_HANDLING = False
WIKI_EDITOR = 'course_wiki.editors.CodeMirror'
WIKI_SHOW_MAX_CHILDREN = 0 # We don't use the little menu that shows children of an article in the breadcrumb
WIKI_ANONYMOUS = False # Don't allow anonymous access until the styling is figured out
+WIKI_CAN_CHANGE_PERMISSIONS = lambda article, user: user.has_perm('wiki.assign')
################################# Jasmine ###################################
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
diff --git a/lms/envs/dev.py b/lms/envs/dev.py
index b269d293dd..d798815543 100644
--- a/lms/envs/dev.py
+++ b/lms/envs/dev.py
@@ -15,6 +15,8 @@ TEMPLATE_DEBUG = True
MITX_FEATURES['DISABLE_START_DATES'] = True
MITX_FEATURES['ENABLE_SQL_TRACKING_LOGS'] = True
+MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = True
+MITX_FEATURES['SUBDOMAIN_BRANDING'] = True
WIKI_ENABLED = True
@@ -68,6 +70,28 @@ CACHE_TIMEOUT = 0
# Dummy secret key for dev
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
+COURSE_LISTINGS = {
+ 'default': ['BerkeleyX/CS169.1x/2012_Fall',
+ 'BerkeleyX/CS188.1x/2012_Fall',
+ 'HarvardX/CS50x/2012',
+ 'HarvardX/PH207x/2012_Fall',
+ 'MITx/3.091x/2012_Fall',
+ 'MITx/6.002x/2012_Fall',
+ 'MITx/6.00x/2012_Fall'],
+ 'berkeley': ['BerkeleyX/CS169.1x/Cal_2012_Fall',
+ 'BerkeleyX/CS188.1x/Cal_2012_Fall'],
+ 'harvard': ['HarvardX/CS50x/2012H'],
+ 'mit': [],
+ 'sjsu': ['MITx/6.002x-EE98/2012_Fall_SJSU'],
+}
+
+SUBDOMAIN_BRANDING = {
+ 'sjsu': 'MITx',
+ 'mit': 'MITx',
+ 'berkeley': 'BerkeleyX',
+ 'harvard': 'HarvardX',
+}
+
################################ LMS Migration #################################
MITX_FEATURES['ENABLE_LMS_MIGRATION'] = True
MITX_FEATURES['ACCESS_REQUIRE_STAFF_FOR_COURSE'] = False # require that user be in the staff_* group to be able to enroll
diff --git a/lms/static/images/BerkeleyX-on-edx-logo.png b/lms/static/images/BerkeleyX-on-edx-logo.png
new file mode 100644
index 0000000000..6c5a828503
Binary files /dev/null and b/lms/static/images/BerkeleyX-on-edx-logo.png differ
diff --git a/lms/static/images/HarvardX-on-edx-logo.png b/lms/static/images/HarvardX-on-edx-logo.png
new file mode 100644
index 0000000000..a4d40f52b6
Binary files /dev/null and b/lms/static/images/HarvardX-on-edx-logo.png differ
diff --git a/lms/static/images/MITx-on-edx-logo.png b/lms/static/images/MITx-on-edx-logo.png
new file mode 100644
index 0000000000..156a08bcbc
Binary files /dev/null and b/lms/static/images/MITx-on-edx-logo.png differ
diff --git a/lms/static/sass/course/layout/_courseware_header.scss b/lms/static/sass/course/layout/_courseware_header.scss
index aa5d07fc44..95af67473a 100644
--- a/lms/static/sass/course/layout/_courseware_header.scss
+++ b/lms/static/sass/course/layout/_courseware_header.scss
@@ -100,13 +100,6 @@ header.global.slim {
top: -12px;
width: 1px;
}
-
- a {
- width: 48px;
- height: 24px;
- background: url(../images/small-header-logo.png) no-repeat !important;
- }
-
}
.find-courses-button {
@@ -143,4 +136,4 @@ header.global.slim {
font-weight: bold;
letter-spacing: 0;
}
-}
\ No newline at end of file
+}
diff --git a/lms/static/sass/course/wiki/_wiki.scss b/lms/static/sass/course/wiki/_wiki.scss
index e88d5e79af..7fa6df1281 100644
--- a/lms/static/sass/course/wiki/_wiki.scss
+++ b/lms/static/sass/course/wiki/_wiki.scss
@@ -203,6 +203,17 @@ section.wiki {
font-size: 0.9em;
font-family: Monaco, monospace;
}
+
+ .toc {
+ background-color: $sidebar-color;
+ padding: 9px;
+ margin: 10px 0;
+ @include border-radius(5px);
+
+ ul {
+ margin: 0;
+ }
+ }
}
@@ -220,7 +231,7 @@ section.wiki {
padding: 40px 40px;
@include box-sizing(border-box);
- .timestamp {
+ .timestamp{
margin-top: 15px;
padding: 15px 0 0 10px;
border-top: 1px solid $light-gray;
@@ -234,6 +245,26 @@ section.wiki {
.date {
font-size: 0.9em;
}
+
+ }
+
+ .see-children {
+ padding: 15px 0 0;
+ border-top: 1px solid $light-gray;
+ margin-top: 15px;
+
+ a {
+ display: block;
+ padding: 2px 4px 2px 10px;
+ border-radius: 3px;
+ font-size: 0.9em;
+ line-height: 25px;
+
+ &:hover {
+ background-color: #f6f6f6;
+ text-decoration: none;
+ }
+ }
}
}
@@ -663,6 +694,59 @@ section.wiki {
margin-top: 9px;
}
+ /*-----------------
+
+ Directory
+
+ -----------------*/
+ .directory-toolbar {
+ background-color: $sidebar-color;
+ padding: 9px;
+ margin: 0 -9px 20px;
+ @include border-radius(5px);
+
+ .well-small {
+ @include clearfix;
+
+ a {
+ @include inline-block;
+ }
+ }
+
+ + p {
+ font-size: 0.9em;
+ color: #aaa;
+ }
+ }
+
+ .filter-clear {
+ margin-right: 10px;
+ margin-top: 10px;
+ font-size: .9em;
+
+ a {
+ color: #aaa;
+
+ &:hover {
+ color: #777;
+ }
+ }
+ }
+
+ .table.table-striped {
+ width: 100%;
+ margin-top: 20px;
+
+ th, td {
+ border-bottom: 1px solid $light-gray;
+ padding: 8px;
+ }
+
+ tr:nth-child(even) {
+ background: #F6F6F6;
+ }
+ }
+
@@ -788,6 +872,39 @@ section.wiki {
text-decoration: none;
}
}
+
+ .missing {
+ max-width: 400px;
+ margin: lh(2) auto;
+ display: block;
+ overflow: hidden;
+ background: $pink;
+ padding: lh();
+ @include box-shadow(inset 0 0 0 1px lighten($pink, 10%));
+ border: 1px solid darken($pink, 15%);
+
+ p {
+ color: #fff;
+
+ a {
+ display: block;
+ background: darken($pink, 8%);
+ margin: lh() (-(lh())) (-(lh()));
+ padding: lh();
+ border-top: 1px solid darken($pink, 15%);
+ color: #fff;
+ font-weight: bold;
+ font-size: em(18);
+ @include transition;
+ text-align: center;
+ -webkit-font-smoothing: antialiased;
+
+ &:hover {
+ background: darken($pink, 12%);
+ }
+ }
+ }
+ }
}
.modal-backdrop {
diff --git a/lms/static/sass/shared/_header.scss b/lms/static/sass/shared/_header.scss
index 116761ddc8..49c9ac250b 100644
--- a/lms/static/sass/shared/_header.scss
+++ b/lms/static/sass/shared/_header.scss
@@ -19,7 +19,7 @@ header.global {
h1.logo {
float: left;
- margin: 6px 15px 0px 0px;
+ margin: 0px 15px 0px 0px;
padding-right: 20px;
position: relative;
@@ -46,12 +46,7 @@ header.global {
}
a {
- @include background-image(url('/static/images/header-logo.png'));
- background-position: 0 0;
- background-repeat: no-repeat;
display: block;
- height: 31px;
- width: 64px;
}
}
diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html
index d5bdb9b302..58998d7c1d 100644
--- a/lms/templates/courseware/courseware.html
+++ b/lms/templates/courseware/courseware.html
@@ -1,6 +1,6 @@
<%inherit file="/main.html" />
<%namespace name='static' file='/static_content.html'/>
-<%block name="bodyclass">courseware%block>
+<%block name="bodyclass">courseware ${course.css_class}%block>
<%block name="title">${course.number} Courseware %block>
<%block name="headextra">
diff --git a/lms/templates/footer.html b/lms/templates/footer.html
index 85ed6e1769..52c2b45526 100644
--- a/lms/templates/footer.html
+++ b/lms/templates/footer.html
@@ -6,7 +6,7 @@
-
+
Find Courses
About
Blog
diff --git a/lms/templates/module-error.html b/lms/templates/module-error.html
index 2a51f5b11a..8855c5be48 100644
--- a/lms/templates/module-error.html
+++ b/lms/templates/module-error.html
@@ -2,10 +2,19 @@
There has been an error on the MITx servers
We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible. Please email us at technical@mitx.mit.edu to report any problems or downtime.
-Details below:
+% if staff_access:
+Details
-Error: ${error | h}
+Error:
+
+${error | h}
+
+
-Raw data: ${data | h}
+Raw data:
+
${data | h}
+
+
+% endif
diff --git a/lms/templates/name_changes.html b/lms/templates/name_changes.html
index 92c9ec1d61..da5d3b241b 100644
--- a/lms/templates/name_changes.html
+++ b/lms/templates/name_changes.html
@@ -1,5 +1,4 @@
<%inherit file="main.html" />
-<%include file="navigation.html" args="active_page=''" />
-
-
- {% endif %}
-
- {% endif %}
-
-{% endblock %}
-
diff --git a/lms/templates/wiki/includes/article_menu.html b/lms/templates/wiki/includes/article_menu.html
index 5131b4c3e9..0d505ccebd 100644
--- a/lms/templates/wiki/includes/article_menu.html
+++ b/lms/templates/wiki/includes/article_menu.html
@@ -35,6 +35,10 @@
%endif
%endfor
+
+<%doc>
+The settings link has been disabled because the notifications app hasn't been integrated yet and those are the only useful settings.
+
%if not user.is_anonymous():
@@ -43,4 +47,6 @@
%endif
+%doc>
+
diff --git a/lms/templates/wiki/plugins/attachments/index.html b/lms/templates/wiki/plugins/attachments/index.html
index d448392933..464a63e5fe 100644
--- a/lms/templates/wiki/plugins/attachments/index.html
+++ b/lms/templates/wiki/plugins/attachments/index.html
@@ -115,7 +115,13 @@
{% if attachment.current_revision.user %}{{ attachment.current_revision.user }}{% else %}{% if user|is_moderator %}{{ attachment.current_revision.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
{{ attachment.current_revision.get_size|filesizeformat }}
- {{ attachment.attachmentrevision_set.all.count }} {% trans "revisions" %}
+
+
+
+
+ {% trans "File history" %} ({{ attachment.attachmentrevision_set.all.count }} {% trans "revisions" %})
+
+
diff --git a/lms/templates/wiki/preview_inline.html b/lms/templates/wiki/preview_inline.html
index 9248454137..a5c6668d16 100644
--- a/lms/templates/wiki/preview_inline.html
+++ b/lms/templates/wiki/preview_inline.html
@@ -10,22 +10,31 @@
{% if revision %}
- {% trans "Previewing revision" %}: {{ revision.created }} (#{{ revision.revision_number }}) by {% if revision.user %}{{ revision.user }}{% else %}{% if user|is_moderator %}{{ revision.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
+ {% trans "Previewing revision" %}:
+ {% include "wiki/includes/revision_info.html" %}
{% endif %}
{% if merge %}
{% trans "Previewing merge between" %}:
- {{ merge1.created }} (#{{ merge1.revision_number }}) by {% if merge1.user %}{{ merge1.user }}{% else %}{% if user|is_moderator %}{{ merge1.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
+ {% include "wiki/includes/revision_info.html" with revision=merge1 %}
{% trans "and" %}
- {{ merge1.created }} (#{{ merge1.revision_number }}) by {% if merge1.user %}{{ merge1.user }}{% else %}{% if user|is_moderator %}{{ merge1.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
+ {% include "wiki/includes/revision_info.html" with revision=merge2 %}
{% endif %}
- {% wiki_render article content %}
+ {% if revision and revision.deleted %}
+
+
This revision has been deleted.
+
Restoring to this revision will mark the article as deleted.
+
+ {% else %}
+ {% wiki_render article content %}
+ {% endif %}
+
diff --git a/lms/templates/wiki/settings.html b/lms/templates/wiki/settings.html
deleted file mode 100644
index 93e92cbc61..0000000000
--- a/lms/templates/wiki/settings.html
+++ /dev/null
@@ -1,52 +0,0 @@
-{% extends "wiki/base.html" %}
-{% load wiki_tags i18n %}
-{% load url from future %}
-
-{% block pagetitle %}{% trans "Settings" %}: {{ article.current_revision.title }}{% endblock %}
-
-{% block wiki_breadcrumbs %}
- {% include "wiki/includes/breadcrumbs.html" %}
-{% endblock %}
-
-{% block wiki_contents %}
-
-
-
- {% if selected_tab != "edit" %}
- {{ article.current_revision.title }}
- {% endif %}
-
- {% for form in forms %}
-
- {% endfor %}
-
-
-
-
-
-
-
- {% with "settings" as selected %}
- {% include "wiki/includes/article_menu.html" %}
- {% endwith %}
-
-
- {% trans "Last modified:" %}
- {{ article.current_revision.modified }}
-
-
-
-
-{% endblock %}
-
diff --git a/lms/templates/wiki/wiki-404.html b/lms/templates/wiki/wiki-404.html
deleted file mode 100644
index a18ea3786a..0000000000
--- a/lms/templates/wiki/wiki-404.html
+++ /dev/null
@@ -1,18 +0,0 @@
-{% extends "wiki/base.html" %}
-{% load wiki_tags i18n %}
-{% load url from future %}
-
-{% block pagetitle %}{{ article.current_revision.title }}{% endblock %}
-
-{% block wiki_breadcrumbs %}
-{% include "wiki/includes/breadcrumbs.html" %}
-{% endblock %}
-
-{% block wiki_contents %}
-
-
-
-{% endblock %}
diff --git a/lms/urls.py b/lms/urls.py
index 6e6ad4300e..cadf75420f 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -10,7 +10,7 @@ if settings.DEBUG:
admin.autodiscover()
urlpatterns = ('',
- url(r'^$', 'student.views.index', name="root"), # Main marketing page, or redirect to courseware
+ url(r'^$', 'branding.views.index', name="root"), # Main marketing page, or redirect to courseware
url(r'^dashboard$', 'student.views.dashboard', name="dashboard"),
url(r'^admin_dashboard$', 'dashboard.views.dashboard'),
@@ -115,7 +115,7 @@ if settings.COURSEWARE_ENABLED:
# url(r'^edit_circuit/(?P[^/]*)$', 'circuit.views.edit_circuit'),
# url(r'^save_circuit/(?P[^/]*)$', 'circuit.views.save_circuit'),
- url(r'^courses/?$', 'courseware.views.courses', name="courses"),
+ url(r'^courses/?$', 'branding.views.courses', name="courses"),
url(r'^change_enrollment$',
'student.views.change_enrollment_view', name="change_enrollment"),
diff --git a/rakefile b/rakefile
index 9eaa4534f2..053abf56a8 100644
--- a/rakefile
+++ b/rakefile
@@ -200,6 +200,8 @@ task :package do
"--exclude=**/.git/**",
"--exclude=**/*.pyc",
"--exclude=**/reports/**",
+ "--exclude=**/test_root/**",
+ "--exclude=**/.coverage/**",
"-C", "#{REPO_ROOT}",
"--provides=#{PACKAGE_NAME}",
"--name=#{NORMALIZED_DEPLOY_NAME}",
diff --git a/repo-requirements.txt b/repo-requirements.txt
index cb016b5222..fcfb063077 100644
--- a/repo-requirements.txt
+++ b/repo-requirements.txt
@@ -1,6 +1,6 @@
-e git://github.com/MITx/django-staticfiles.git@6d2504e5c8#egg=django-staticfiles
-e git://github.com/MITx/django-pipeline.git#egg=django-pipeline
--e git://github.com/benjaoming/django-wiki.git@02275fb4#egg=django-wiki
+-e git://github.com/benjaoming/django-wiki.git@63003aa#egg=django-wiki
-e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
-e common/lib/capa
-e common/lib/xmodule
diff --git a/requirements.txt b/requirements.txt
index c5cafe64b8..72b13e63ba 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,7 +2,7 @@ django>=1.4,<1.5
pip
numpy
scipy
-markdown
+Markdown<2.3.0
pygments
lxml
boto
@@ -43,5 +43,7 @@ django-ses
django-storages
django-threaded-multihost
django-sekizai<0.7
+django-mptt>=0.5.3
+sorl-thumbnail
networkx
-r repo-requirements.txt