diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index 0425f3e158..35e59db0ca 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -1,3 +1,4 @@ +import functools import json import logging import random @@ -156,7 +157,7 @@ def edXauth_signup(request, eamap=None): log.debug('ExtAuth: doing signup for %s' % eamap.external_email) - return student_views.main_index(extra_context=context) + return student_views.main_index(request, extra_context=context) #----------------------------------------------------------------------------- # MIT SSL @@ -206,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() + return student_views.main_index(request) (user, email, fullname) = ssl_dn_extract_info(cert) @@ -216,4 +217,4 @@ def edXauth_ssl_login(request): credentials=cert, email=email, fullname=fullname, - retfun = student_views.main_index) + retfun = functools.partial(student_views.main_index, request)) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index b6aa62e03d..a99b46fd13 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -68,9 +68,9 @@ def index(request): from external_auth.views import edXauth_ssl_login return edXauth_ssl_login(request) - return main_index(user=request.user) + return main_index(request, user=request.user) -def main_index(extra_context = {}, user=None): +def main_index(request, extra_context={}, user=None): ''' Render the edX main page. @@ -93,7 +93,8 @@ def main_index(extra_context = {}, user=None): entry.summary = soup.getText() # The course selection work is done in courseware.courses. - universities = get_courses_by_university(None) + universities = get_courses_by_university(None, + domain=request.META.get('HTTP_HOST')) context = {'universities': universities, 'entries': entries} context.update(extra_context) return render_to_response('index.html', context) diff --git a/common/lib/xmodule/xmodule/abtest_module.py b/common/lib/xmodule/xmodule/abtest_module.py index ca00db4c9a..ceca6ff9ed 100644 --- a/common/lib/xmodule/xmodule/abtest_module.py +++ b/common/lib/xmodule/xmodule/abtest_module.py @@ -49,9 +49,9 @@ class ABTestModule(XModule): return json.dumps({'group': self.group}) def displayable_items(self): - return filter(None, [self.system.get_module(child) - for child - in self.definition['data']['group_content'][self.group]]) + child_locations = self.definition['data']['group_content'][self.group] + children = [self.system.get_module(loc) for loc in child_locations] + return [c for c in children if c is not None] # TODO (cpennington): Use Groups should be a first class object, rather than being diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index 8c4c373d4f..0c0d750882 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -1,3 +1,4 @@ +import json import logging import os import re @@ -149,7 +150,7 @@ class XMLModuleStore(ModuleStoreBase): for course_dir in course_dirs: self.try_load_course(course_dir) - def try_load_course(self,course_dir): + def try_load_course(self, course_dir): ''' Load a course, keeping track of errors as we go along. ''' @@ -170,7 +171,27 @@ class XMLModuleStore(ModuleStoreBase): ''' String representation - for debugging ''' - return 'data_dir=%s, %d courses, %d modules' % (self.data_dir,len(self.courses),len(self.modules)) + return 'data_dir=%s, %d courses, %d modules' % ( + self.data_dir, len(self.courses), len(self.modules)) + + def load_policy(self, policy_path, tracker): + """ + Attempt to read a course policy from policy_path. If the file + exists, but is invalid, log an error and return {}. + + If the policy loads correctly, returns the deserialized version. + """ + if not os.path.exists(policy_path): + return {} + try: + with open(policy_path) as f: + return json.load(f) + except (IOError, ValueError) as err: + msg = "Error loading course policy from {}".format(policy_path) + tracker(msg) + log.warning(msg + " " + str(err)) + return {} + def load_course(self, course_dir, tracker): """ @@ -214,6 +235,11 @@ class XMLModuleStore(ModuleStoreBase): system = ImportSystem(self, org, course, course_dir, tracker) course_descriptor = system.process_xml(etree.tostring(course_data)) + policy_path = self.data_dir / course_dir / 'policy.json' + + policy = self.load_policy(policy_path, tracker) + XModuleDescriptor.apply_policy(course_descriptor, policy) + # NOTE: The descriptors end up loading somewhat bottom up, which # breaks metadata inheritance via get_children(). Instead # (actually, in addition to, for now), we do a final inheritance pass diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 06449dc37f..54c68c4d07 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -219,11 +219,11 @@ class XModule(HTMLSnippet): Return module instances for all the children of this module. ''' if self._loaded_children is None: + child_locations = self.definition.get('children', []) + children = [self.system.get_module(loc) for loc in child_locations] # get_module returns None if the current user doesn't have access # to the location. - self._loaded_children = filter(None, - [self.system.get_module(child) - for child in self.definition.get('children', [])]) + self._loaded_children = [c for c in children if c is not None] return self._loaded_children @@ -298,6 +298,14 @@ class XModule(HTMLSnippet): return "" +def policy_key(location): + """ + Get the key for a location in a policy file. (Since the policy file is + specific to a course, it doesn't need the full location url). + """ + return '{cat}/{name}'.format(cat=location.category, name=location.name) + + class XModuleDescriptor(Plugin, HTMLSnippet): """ An XModuleDescriptor is a specification for an element of a course. This @@ -416,6 +424,24 @@ class XModuleDescriptor(Plugin, HTMLSnippet): return dict((k,v) for k,v in self.metadata.items() if k not in self._inherited_metadata) + + @staticmethod + def apply_policy(node, policy): + """ + Given a descriptor, traverse all its descendants and update its metadata + with the policy. + + Notes: + - this does not propagate inherited metadata. The caller should + call compute_inherited_metadata after applying the policy. + - metadata specified in the policy overrides metadata in the xml + """ + k = policy_key(node.location) + if k in policy: + node.metadata.update(policy[k]) + for c in node.get_children(): + XModuleDescriptor.apply_policy(c, policy) + @staticmethod def compute_inherited_metadata(node): """Given a descriptor, traverse all of its descendants and do metadata diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index 399d5d3f91..3c2f3269ca 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -166,7 +166,7 @@ class XmlDescriptor(XModuleDescriptor): Subclasses should not need to override this except in special cases (e.g. html module)''' - # VS[compat] -- the filename tag should go away once everything is + # VS[compat] -- the filename attr should go away once everything is # converted. (note: make sure html files still work once this goes away) filename = xml_object.get('filename') if filename is None: diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 9605c827de..e588f807da 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -65,9 +65,10 @@ def has_access(user, obj, action): # Passing an unknown object here is a coding error, so rather than # returning a default, complain. - raise TypeError("Unknown object type in has_access(). Object type: '{}'" + raise TypeError("Unknown object type in has_access(): '{}'" .format(type(obj))) + # ================ Implementation helpers ================================ def _has_access_course_desc(user, course, action): @@ -83,8 +84,12 @@ def _has_access_course_desc(user, course, action): 'staff' -- staff access to course. """ def can_load(): - "Can this user load this course?" - # delegate to generic descriptor check + """ + Can this user load this course? + + NOTE: this is not checking whether user is actually enrolled in the course. + """ + # delegate to generic descriptor check to check start dates return _has_access_descriptor(user, course, action) def can_enroll(): @@ -169,6 +174,12 @@ def _has_access_descriptor(user, descriptor, action): has_access(), it will not do the right thing. """ def can_load(): + """ + NOTE: This does not check that the student is enrolled in the course + that contains this module. We may or may not want to allow non-enrolled + students to see modules. If not, views should check the course, so we + don't have to hit the enrollments table on every module load. + """ # If start dates are off, can always load if settings.MITX_FEATURES['DISABLE_START_DATES']: debug("Allow: DISABLE_START_DATES") @@ -196,8 +207,6 @@ def _has_access_descriptor(user, descriptor, action): return _dispatch(checkers, action, user, descriptor) - - def _has_access_xmodule(user, xmodule, action): """ Check if user has access to this xmodule. diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 2e74853760..f0b82a3c9c 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -2,8 +2,8 @@ from collections import defaultdict from fs.errors import ResourceNotFoundError from functools import wraps import logging -from path import path +from path import path from django.conf import settings from django.http import Http404 @@ -142,7 +142,8 @@ def get_course_info_section(course, section_key): raise KeyError("Invalid about key " + str(section_key)) -def get_courses_by_university(user): + +def get_courses_by_university(user, domain=None): ''' Returns dict of lists of courses available, keyed by course.org (ie university). Courses are sorted by course.number. @@ -152,9 +153,21 @@ def get_courses_by_university(user): 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) + universities = defaultdict(list) for course in courses: - if has_access(user, course, 'see_exists'): - universities[course.org].append(course) + 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/management/commands/metadata_to_json.py b/lms/djangoapps/courseware/management/commands/metadata_to_json.py new file mode 100644 index 0000000000..0f48e93319 --- /dev/null +++ b/lms/djangoapps/courseware/management/commands/metadata_to_json.py @@ -0,0 +1,98 @@ +""" +A script to walk a course xml tree, generate a dictionary of all the metadata, +and print it out as a json dict. +""" +import os +import sys +import json + +from collections import OrderedDict +from path import path + +from django.core.management.base import BaseCommand + +from xmodule.modulestore.xml import XMLModuleStore +from xmodule.x_module import policy_key + +def import_course(course_dir, verbose=True): + course_dir = path(course_dir) + data_dir = course_dir.dirname() + course_dirs = [course_dir.basename()] + + # No default class--want to complain if it doesn't find plugins for any + # module. + modulestore = XMLModuleStore(data_dir, + default_class=None, + eager=True, + course_dirs=course_dirs) + + def str_of_err(tpl): + (msg, exc_str) = tpl + return '{msg}\n{exc}'.format(msg=msg, exc=exc_str) + + courses = modulestore.get_courses() + + n = len(courses) + if n != 1: + sys.stderr.write('ERROR: Expect exactly 1 course. Loaded {n}: {lst}\n'.format( + n=n, lst=courses)) + return None + + course = courses[0] + errors = modulestore.get_item_errors(course.location) + if len(errors) != 0: + sys.stderr.write('ERRORs during import: {}\n'.format('\n'.join(map(str_of_err, errors)))) + + return course + +def node_metadata(node): + # make a copy + to_export = ('format', 'display_name', + 'graceperiod', 'showanswer', 'rerandomize', + 'start', 'due', 'graded', 'hide_from_toc', + 'ispublic', 'xqa_key') + + orig = node.own_metadata + d = {k: orig[k] for k in to_export if k in orig} + return d + +def get_metadata(course): + d = OrderedDict({}) + queue = [course] + while len(queue) > 0: + node = queue.pop() + d[policy_key(node.location)] = node_metadata(node) + # want to print first children first, so put them at the end + # (we're popping from the end) + queue.extend(reversed(node.get_children())) + return d + + +def print_metadata(course_dir, output): + course = import_course(course_dir) + if course: + meta = get_metadata(course) + result = json.dumps(meta, indent=4) + if output: + with file(output, 'w') as f: + f.write(result) + else: + print result + + +class Command(BaseCommand): + help = """Imports specified course.xml and prints its +metadata as a json dict. + +Usage: metadata_to_json PATH-TO-COURSE-DIR OUTPUT-PATH + +if OUTPUT-PATH isn't given, print to stdout. +""" + def handle(self, *args, **options): + n = len(args) + if n < 1 or n > 2: + print Command.help + return + + output_path = args[1] if n > 1 else None + print_metadata(args[0], output_path) diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py index 5fae05c177..261140dec7 100644 --- a/lms/djangoapps/courseware/models.py +++ b/lms/djangoapps/courseware/models.py @@ -67,7 +67,7 @@ class StudentModuleCache(object): """ A cache of StudentModules for a specific student """ - def __init__(self, user, descriptors, acquire_lock=False): + def __init__(self, user, descriptors, select_for_update=False): ''' Find any StudentModule objects that are needed by any descriptor in descriptors. Avoids making multiple queries to the database. @@ -77,6 +77,7 @@ class StudentModuleCache(object): Arguments user: The user for which to fetch maching StudentModules descriptors: An array of XModuleDescriptors. + select_for_update: Flag indicating whether the row should be locked until end of transaction ''' if user.is_authenticated(): module_ids = self._get_module_state_keys(descriptors) @@ -86,7 +87,7 @@ class StudentModuleCache(object): self.cache = [] chunk_size = 500 for id_chunk in [module_ids[i:i + chunk_size] for i in xrange(0, len(module_ids), chunk_size)]: - if acquire_lock: + if select_for_update: self.cache.extend(StudentModule.objects.select_for_update().filter( student=user, module_state_key__in=id_chunk) @@ -102,13 +103,14 @@ class StudentModuleCache(object): @classmethod - def cache_for_descriptor_descendents(cls, user, descriptor, depth=None, descriptor_filter=lambda descriptor: True, acquire_lock=False): + def cache_for_descriptor_descendents(cls, user, descriptor, depth=None, descriptor_filter=lambda descriptor: True, select_for_update=False): """ descriptor: An XModuleDescriptor depth is the number of levels of descendent modules to load StudentModules for, in addition to the supplied descriptor. If depth is None, load all descendent StudentModules descriptor_filter is a function that accepts a descriptor and return wether the StudentModule should be cached + select_for_update: Flag indicating whether the row should be locked until end of transaction """ def get_child_descriptors(descriptor, depth, descriptor_filter): @@ -128,7 +130,7 @@ class StudentModuleCache(object): descriptors = get_child_descriptors(descriptor, depth, descriptor_filter) - return StudentModuleCache(user, descriptors, acquire_lock) + return StudentModuleCache(user, descriptors, select_for_update) def _get_module_state_keys(self, descriptors): ''' diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index b84bdb2310..f58170552c 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -320,8 +320,8 @@ def xqueue_callback(request, course_id, userid, id, dispatch): user = User.objects.get(id=userid) student_module_cache = StudentModuleCache.cache_for_descriptor_descendents( - user, modulestore().get_item(id), depth=0, acquire_lock=True) - instance = get_module(user, request, id, student_module_cache, course_id=course_id) + user, modulestore().get_item(id), depth=0, select_for_update=True) + instance = get_module(user, request, id, student_module_cache) if instance is None: log.debug("No module {} for user {}--access denied?".format(id, user)) raise Http404 diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 6da6720622..20a1443f53 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -65,7 +65,8 @@ def courses(request): ''' Render "find courses" page. The course selection work is done in courseware.courses. ''' - universities = get_courses_by_university(request.user) + universities = get_courses_by_university(request.user, + domain=request.META.get('HTTP_HOST')) return render_to_response("courses.html", {'universities': universities}) @@ -112,6 +113,7 @@ def index(request, course_id, chapter=None, section=None, - HTTPresponse """ course = get_course_with_access(request.user, course_id, 'load') + staff_access = has_access(request.user, course, 'staff') registered = registered_for_course(course, request.user) if not registered: # TODO (vshnayder): do course instructors need to be registered to see course? @@ -125,7 +127,8 @@ def index(request, course_id, chapter=None, section=None, 'COURSE_TITLE': course.title, 'course': course, 'init': '', - 'content': '' + 'content': '', + 'staff_access': staff_access, } look_for_module = chapter is not None and section is not None @@ -168,7 +171,8 @@ def index(request, course_id, chapter=None, section=None, position=position )) try: - result = render_to_response('courseware-error.html', {}) + result = render_to_response('courseware-error.html', + {'staff_access': staff_access}) except: result = HttpResponse("There was an unrecoverable error") @@ -210,8 +214,10 @@ def course_info(request, course_id): Assumes the course_id is in a valid format. """ course = get_course_with_access(request.user, course_id, 'load') + staff_access = has_access(request.user, course, 'staff') - return render_to_response('info.html', {'course': course}) + return render_to_response('info.html', {'course': course, + 'staff_access': staff_access,}) def registered_for_course(course, user): @@ -243,7 +249,8 @@ def university_profile(request, org_id): raise Http404("University Profile not found for {0}".format(org_id)) # Only grab courses for this org... - courses = get_courses_by_university(request.user)[org_id] + courses = get_courses_by_university(request.user, + domain=request.META.get('HTTP_HOST'))[org_id] context = dict(courses=courses, org_id=org_id) template_file = "university_profile/{0}.html".format(org_id).lower() @@ -259,13 +266,14 @@ def profile(request, course_id, student_id=None): Course staff are allowed to see the profiles of students in their class. """ course = get_course_with_access(request.user, course_id, 'load') + staff_access = has_access(request.user, course, 'staff') if student_id is None or student_id == request.user.id: # always allowed to see your own profile student = request.user else: # Requesting access to a different student's profile - if not has_access(request.user, course, 'staff'): + if not staff_access: raise Http404 student = User.objects.get(id=int(student_id)) @@ -284,8 +292,9 @@ def profile(request, course_id, student_id=None): 'email': student.email, 'course': course, 'csrf': csrf(request)['csrf_token'], - 'courseware_summary' : courseware_summary, - 'grade_summary' : grade_summary + 'courseware_summary': courseware_summary, + 'grade_summary': grade_summary, + 'staff_access': staff_access, } context.update() @@ -318,7 +327,10 @@ def gradebook(request, course_id): for student in enrolled_students] return render_to_response('gradebook.html', {'students': student_info, - 'course': course, 'course_id': course_id}) + 'course': course, + 'course_id': course_id, + # Checked above + 'staff_access': True,}) @cache_control(no_cache=True, no_store=True, must_revalidate=True) @@ -327,7 +339,8 @@ def grade_summary(request, course_id): course = get_course_with_access(request.user, course_id, 'staff') # For now, just a static page - context = {'course': course } + context = {'course': course, + 'staff_access': True,} return render_to_response('grade_summary.html', context) @@ -337,6 +350,7 @@ def instructor_dashboard(request, course_id): course = get_course_with_access(request.user, course_id, 'staff') # For now, just a static page - context = {'course': course } + context = {'course': course, + 'staff_access': True,} return render_to_response('instructor_dashboard.html', context) diff --git a/lms/djangoapps/simplewiki/views.py b/lms/djangoapps/simplewiki/views.py index 2ee76a1868..ac807b13ed 100644 --- a/lms/djangoapps/simplewiki/views.py +++ b/lms/djangoapps/simplewiki/views.py @@ -10,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _ from mitxmako.shortcuts import render_to_response from courseware.courses import get_opt_course_with_access +from courseware.access import has_access from xmodule.course_module import CourseDescriptor from xmodule.modulestore.django import modulestore @@ -49,6 +50,10 @@ def update_template_dictionary(dictionary, request=None, course=None, article=No if request: dictionary.update(csrf(request)) + if request and course: + dictionary['staff_access'] = has_access(request.user, course, 'staff') + else: + dictionary['staff_access'] = False def view(request, article_path, course_id=None): course = get_opt_course_with_access(request.user, course_id, 'load') diff --git a/lms/djangoapps/staticbook/views.py b/lms/djangoapps/staticbook/views.py index aec3fb1448..2e19ab6425 100644 --- a/lms/djangoapps/staticbook/views.py +++ b/lms/djangoapps/staticbook/views.py @@ -1,17 +1,23 @@ from django.contrib.auth.decorators import login_required from mitxmako.shortcuts import render_to_response +from courseware.access import has_access from courseware.courses import get_course_with_access from lxml import etree @login_required def index(request, course_id, page=0): course = get_course_with_access(request.user, course_id, 'load') - raw_table_of_contents = open('lms/templates/book_toc.xml', 'r') # TODO: This will need to come from S3 + staff_access = has_access(request.user, course, 'staff') + + # TODO: This will need to come from S3 + raw_table_of_contents = open('lms/templates/book_toc.xml', 'r') table_of_contents = etree.parse(raw_table_of_contents).getroot() + return render_to_response('staticbook.html', {'page': int(page), 'course': course, - 'table_of_contents': table_of_contents}) + 'table_of_contents': table_of_contents, + 'staff_access': staff_access}) def index_shifted(request, course_id, page): diff --git a/lms/envs/common.py b/lms/envs/common.py index 303e73ea81..45818c0ff2 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -49,6 +49,11 @@ MITX_FEATURES = { ## Doing so will cause all courses to be released on production 'DISABLE_START_DATES': False, # When True, all courses will be active, regardless of start date + # When True, will only publicly list courses by the subdomain. Expects you + # to define COURSE_LISTINGS, a dictionary mapping subdomains to lists of + # course_ids (see dev_int.py for an example) + 'SUBDOMAIN_COURSE_LISTINGS' : False, + 'ENABLE_TEXTBOOK' : True, 'ENABLE_DISCUSSION' : True, @@ -61,6 +66,7 @@ MITX_FEATURES = { 'ACCESS_REQUIRE_STAFF_FOR_COURSE': False, 'AUTH_USE_OPENID': False, 'AUTH_USE_MIT_CERTIFICATES' : False, + } # Used for A/B testing diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 882a82b8f0..8457b50374 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -54,7 +54,7 @@ CACHES = { } XQUEUE_INTERFACE = { - "url": "http://xqueue.sandbox.edx.org", + "url": "http://sandbox-xqueue.edx.org", "django_auth": { "username": "lms", "password": "***REMOVED***" diff --git a/lms/envs/dev_int.py b/lms/envs/dev_int.py new file mode 100644 index 0000000000..12123e12d4 --- /dev/null +++ b/lms/envs/dev_int.py @@ -0,0 +1,32 @@ +""" +This enables use of course listings by subdomain. To see it in action, point the +following domains to 127.0.0.1 in your /etc/hosts file: + + berkeley.dev + harvard.dev + mit.dev + +Note that OS X has a bug where using *.local domains is excruciatingly slow, so +use *.dev domains instead for local testing. +""" +from .dev import * + +MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = True + +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/2012_Fall', + 'BerkeleyX/CS188.1x/2012_Fall'], + + 'harvard' : ['HarvardX/CS50x/2012'], + + 'mit' : ['MITx/3.091x/2012_Fall', + 'MITx/6.00x/2012_Fall'] +} diff --git a/lms/envs/test.py b/lms/envs/test.py index 187cb5c68e..11534b3f4d 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -51,7 +51,7 @@ GITHUB_REPO_ROOT = ENV_ROOT / "data" XQUEUE_INTERFACE = { - "url": "http://xqueue.sandbox.edx.org", + "url": "http://sandbox-xqueue.edx.org", "django_auth": { "username": "lms", "password": "***REMOVED***" diff --git a/lms/templates/course_navigation.html b/lms/templates/course_navigation.html index dd1c8d93b9..9e93b2fb14 100644 --- a/lms/templates/course_navigation.html +++ b/lms/templates/course_navigation.html @@ -28,7 +28,7 @@ def url_class(url): % if user.is_authenticated():
  • Profile
  • % endif -% if has_access(user, course, 'staff'): +% if staff_access:
  • Instructor
  • % endif