From 6472e0ebe338584dd83959df0c27fd70d73159d8 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 14 Sep 2012 14:29:31 -0400 Subject: [PATCH 01/52] If there's a ?next param, redirect there instead of dashboard after login --- lms/templates/login_modal.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lms/templates/login_modal.html b/lms/templates/login_modal.html index d7d327178c..0b19399fc0 100644 --- a/lms/templates/login_modal.html +++ b/lms/templates/login_modal.html @@ -46,7 +46,11 @@ (function() { $(document).delegate('#login_form', 'ajax:success', function(data, json, xhr) { if(json.success) { - location.href="${reverse('dashboard')}"; + % if request.REQUEST.get('next', False): + location.href="${request.REQUEST.get('next')}"; + % else: + location.href="${reverse('dashboard')}"; + % endif } else { if($('#login_error').length == 0) { $('#login_form').prepend(''); From a788db53e90de9475dfa889ee883ac4f04a257e0 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 14 Sep 2012 14:31:04 -0400 Subject: [PATCH 02/52] Show login modal if there's a next param to index, university index * also replace the external-auth-related popping up of the signup form with jquery. Presumably it takes care of the browser specific stuff. --- common/djangoapps/student/views.py | 2 ++ lms/djangoapps/courseware/views.py | 3 +++ lms/templates/index.html | 30 ++++++------------------------ 3 files changed, 11 insertions(+), 24 deletions(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index cbb12e44cc..a70349bec3 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -82,6 +82,8 @@ def index(request, extra_context={}, user=None): domain=domain) context = {'universities': universities, 'entries': entries} context.update(extra_context) + if request.REQUEST.get('next', False): + context['show_login_immediately'] = True return render_to_response('index.html', context) def course_from_id(course_id): diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index c474da8d8b..7da5d06741 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -404,6 +404,9 @@ def university_profile(request, org_id): context = dict(courses=courses, org_id=org_id) template_file = "university_profile/{0}.html".format(org_id).lower() + if request.REQUEST.get('next', False): + context['show_login_immediately'] = True + return render_to_response(template_file, context) def render_notifications(request, course, notifications): diff --git a/lms/templates/index.html b/lms/templates/index.html index 0ee00b57c0..8cabe62f09 100644 --- a/lms/templates/index.html +++ b/lms/templates/index.html @@ -147,28 +147,10 @@ % if show_signup_immediately is not UNDEFINED: -% endif +% elif show_login_immediately is not UNDEFINED: + +% endif \ No newline at end of file From 261948f38a7709e1972d5a140f2fd6b7470d3351 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 17 Sep 2012 13:19:24 -0400 Subject: [PATCH 03/52] Make textbooks in the course definition be stored as data, rather than objects, and turn them into objects during module instatiation --- common/lib/xmodule/xmodule/course_module.py | 28 +++++++++------------ 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 7aa904205d..5ea0c13d65 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -22,9 +22,6 @@ class CourseDescriptor(SequenceDescriptor): self.book_url = book_url self.table_of_contents = self._get_toc_from_s3() - @classmethod - def from_xml_object(cls, xml_object): - return cls(xml_object.get('title'), xml_object.get('book_url')) @property def table_of_contents(self): @@ -57,10 +54,18 @@ class CourseDescriptor(SequenceDescriptor): return table_of_contents - def __init__(self, system, definition=None, **kwargs): super(CourseDescriptor, self).__init__(system, definition, **kwargs) - self.textbooks = self.definition['data']['textbooks'] + + self.textbooks = [] + for title, book_url in self.definition['data']['textbooks']: + try: + self.textbooks.append(self.Textbook(title, book_url)) + except: + # If we can't get to S3 (e.g. on a train with no internet), don't break + # the rest of the courseware. + log.exception("Couldn't load textbook ({0}, {1})".format(title, book_url)) + continue self.wiki_slug = self.definition['data']['wiki_slug'] or self.location.course @@ -82,7 +87,6 @@ class CourseDescriptor(SequenceDescriptor): # disable the syllabus content for courses that do not provide a syllabus self.syllabus_present = self.system.resources_fs.exists(path('syllabus')) - def set_grading_policy(self, policy_str): """Parse the policy specified in policy_str, and save it""" try: @@ -94,19 +98,11 @@ class CourseDescriptor(SequenceDescriptor): # the error log. self._grading_policy = {} - @classmethod def definition_from_xml(cls, xml_object, system): textbooks = [] for textbook in xml_object.findall("textbook"): - try: - txt = cls.Textbook.from_xml_object(textbook) - except: - # If we can't get to S3 (e.g. on a train with no internet), don't break - # the rest of the courseware. - log.exception("Couldn't load textbook") - continue - textbooks.append(txt) + textbooks = (textbook.get('title'), textbook.get('book_url')) xml_object.remove(textbook) #Load the wiki tag if it exists @@ -116,7 +112,7 @@ class CourseDescriptor(SequenceDescriptor): 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 = super(CourseDescriptor, cls).definition_from_xml(xml_object, system) definition.setdefault('data', {})['textbooks'] = textbooks definition['data']['wiki_slug'] = wiki_slug From 34541b5e7c4b597b5f87231345640c64602398cb Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 17 Sep 2012 13:49:42 -0400 Subject: [PATCH 04/52] Make a list of textbooks, don't just store a single one --- common/lib/xmodule/xmodule/course_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 5ea0c13d65..3af65d1bd6 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -102,7 +102,7 @@ class CourseDescriptor(SequenceDescriptor): def definition_from_xml(cls, xml_object, system): textbooks = [] for textbook in xml_object.findall("textbook"): - textbooks = (textbook.get('title'), textbook.get('book_url')) + textbooks.append((textbook.get('title'), textbook.get('book_url'))) xml_object.remove(textbook) #Load the wiki tag if it exists From 0a55e1eae13789cc3cad1f693cfc072351f44af5 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 17 Sep 2012 13:51:33 -0400 Subject: [PATCH 05/52] Add sub-environments for the lms that is running alongside the lms for debugging --- lms/envs/cms/__init__.py | 0 lms/envs/{with_cms.py => cms/aws.py} | 0 lms/envs/cms/dev.py | 19 +++++++++++++++++++ rakefile | 4 ++-- 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 lms/envs/cms/__init__.py rename lms/envs/{with_cms.py => cms/aws.py} (100%) create mode 100644 lms/envs/cms/dev.py diff --git a/lms/envs/cms/__init__.py b/lms/envs/cms/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/envs/with_cms.py b/lms/envs/cms/aws.py similarity index 100% rename from lms/envs/with_cms.py rename to lms/envs/cms/aws.py diff --git a/lms/envs/cms/dev.py b/lms/envs/cms/dev.py new file mode 100644 index 0000000000..6e4697cccb --- /dev/null +++ b/lms/envs/cms/dev.py @@ -0,0 +1,19 @@ +""" +Settings for the LMS that runs alongside the CMS on AWS +""" + +from ..dev import * + +MODULESTORE = { + 'default': { + 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', + 'OPTIONS': { + 'default_class': 'xmodule.raw_module.RawDescriptor', + 'host': 'localhost', + 'db': 'xmodule', + 'collection': 'modulestore', + 'fs_root': DATA_DIR, + 'render_template': 'mitxmako.shortcuts.render_to_string', + } + } +} diff --git a/rakefile b/rakefile index 9e0bbcbfa4..be5ef1d9e7 100644 --- a/rakefile +++ b/rakefile @@ -125,8 +125,8 @@ TEST_TASKS = [] end # Per environment tasks - Dir["#{system}/envs/*.py"].each do |env_file| - env = File.basename(env_file).gsub(/\.py/, '') + Dir["#{system}/envs/**/*.py"].each do |env_file| + env = env_file.gsub("#{system}/envs/", '').gsub(/\.py/, '').gsub('/', '.') desc "Attempt to import the settings file #{system}.envs.#{env} and report any errors" task "#{system}:check_settings:#{env}" => :predjango do sh("echo 'import #{system}.envs.#{env}' | #{django_admin(system, env, 'shell')}") From d17e2aada90f69e30940345037f87d76d3c1b033 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 17 Sep 2012 14:19:07 -0400 Subject: [PATCH 06/52] Remove trailing '/' from ajax_urls --- cms/djangoapps/contentstore/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index d701db33a3..505b897497 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -207,7 +207,7 @@ def preview_module_system(request, preview_id, descriptor): descriptor: An XModuleDescriptor """ return ModuleSystem( - ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']), + ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'), # TODO (cpennington): Do we want to track how instructors are using the preview problems? track_function=lambda type, event: None, filestore=descriptor.system.resources_fs, From c0ed79397647f90458fb80a7552f1980fd2679d3 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 17 Sep 2012 14:19:23 -0400 Subject: [PATCH 07/52] Add an empty get_errored_courses function for the mongo modulestore --- common/lib/xmodule/xmodule/modulestore/mongo.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index 7aa05e474f..33901947a6 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -316,3 +316,9 @@ class MongoModuleStore(ModuleStoreBase): {'_id': True}) return [i['_id'] for i in items] + def get_errored_courses(self): + """ + This function doesn't make sense for the mongo modulestore, as courses + are loaded on demand, rather than up front + """ + return {} From f45fa578a26c6274e21391c6698be9ff7e1c356d Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 17 Sep 2012 15:56:09 -0400 Subject: [PATCH 08/52] onreset rerandomize option --- common/lib/xmodule/xmodule/capa_module.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index a891474581..0d810af87a 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -179,6 +179,8 @@ class CapaModule(XModule): return "per_student" elif rerandomize == "never": return "never" + elif rerandomize == "onreset": + return "onreset" else: raise Exception("Invalid rerandomize attribute " + rerandomize) @@ -307,7 +309,7 @@ class CapaModule(XModule): save_button = False # Only show the reset button if pressing it will show different values - if self.rerandomize != 'always': + if self.rerandomize not in ["always", "onreset"]: reset_button = False # User hasn't submitted an answer yet -- we don't want resets @@ -617,7 +619,7 @@ class CapaModule(XModule): return "Refresh the page and make an attempt before resetting." self.lcp.do_reset() - if self.rerandomize == "always": + if self.rerandomize in ["always", "onreset"]: # reset random number generator seed (note the self.lcp.get_state() # in next line) self.lcp.seed = None From 0fdd0a005fdfab2679bb9fca5b11161d4c1dce1c Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 17 Sep 2012 20:40:06 -0400 Subject: [PATCH 09/52] Add docs to xml_format --- doc/xml-format.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/xml-format.md b/doc/xml-format.md index 29c60fea99..0dd2c59417 100644 --- a/doc/xml-format.md +++ b/doc/xml-format.md @@ -274,6 +274,7 @@ __Inherited:__ * `showanswer` - When to show answer. For 'attempted', will show answer after first attempt. Values: never, attempted, answered, closed. Default: closed. Optional. * `graded` - Whether this section will count towards the students grade. "true" or "false". Defaults to "false". * `rerandomise` - Randomize question on each attempt. Values: 'always' (students see a different version of the problem after each attempt to solve it) + 'onreset' (students see a different version of the problem when they reset, but are not forced to reset after each check) 'never' (all students see the same version of the problem) 'per_student' (individual students see the same version of the problem each time the look at it, but that version is different from what other students see) Default: 'always'. Optional. From d9219c608610f123b4c345b170e3676194c4dea8 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Sun, 16 Sep 2012 21:55:05 -0400 Subject: [PATCH 10/52] Custom tabs * specify in tabs list in course policy - active page tracking now done in tabs.py - properly handle the fact that there may be multiple textbooks * Still need: - wiki pages - (if that's delayed, special-case syllabus support) --- common/lib/xmodule/xmodule/course_module.py | 11 +- common/lib/xmodule/xmodule/modulestore/xml.py | 1 - doc/xml-format.md | 47 +++- lms/djangoapps/courseware/tabs.py | 233 ++++++++++++++++++ lms/djangoapps/staticbook/views.py | 6 +- .../courseware/course_navigation.html | 49 +--- lms/templates/staticbook.html | 2 +- 7 files changed, 301 insertions(+), 48 deletions(-) create mode 100644 lms/djangoapps/courseware/tabs.py diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 7aa904205d..f0751c5462 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -1,9 +1,9 @@ from fs.errors import ResourceNotFoundError -import time import logging -import requests from lxml import etree from path import path # NOTE (THK): Only used for detecting presence of syllabus +import requests +import time from xmodule.util.decorators import lazyproperty from xmodule.graders import load_grading_policy @@ -134,6 +134,13 @@ class CourseDescriptor(SequenceDescriptor): def grade_cutoffs(self): return self._grading_policy['GRADE_CUTOFFS'] + @property + def tabs(self): + """ + Return the tabs config, as a python object, or None if not specified. + """ + return self.metadata.get('tabs') + @property def show_calculator(self): return self.metadata.get("show_calculator", None) == "Yes" diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index 82b0abd8ab..874c7d3d7f 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -414,7 +414,6 @@ class XMLModuleStore(ModuleStoreBase): policy_str = self.read_grading_policy(paths, tracker) course_descriptor.set_grading_policy(policy_str) - log.debug('========> Done with course import from {0}'.format(course_dir)) return course_descriptor diff --git a/doc/xml-format.md b/doc/xml-format.md index 29c60fea99..181a814069 100644 --- a/doc/xml-format.md +++ b/doc/xml-format.md @@ -219,6 +219,13 @@ Values are dictionaries of the form {"metadata-key" : "metadata-value"}. * The order in which things appear does not matter, though it may be helpful to organize the file in the same order as things appear in the content. * NOTE: json is picky about commas. If you have trailing commas before closing braces, it will complain and refuse to parse the file. This can be irritating at first. +Supported fields at the course level: + +* "start" -- specify the start date for the course. Format-by-example: "2012-09-05T12:00". +* "enrollment_start", "enrollment_end" -- when can students enroll? (if not specified, can enroll anytime). Same format as "start". +* "tabs" -- have custom tabs in the courseware. See below for details on config. +* TODO: there are others + ### Grading policy file contents TODO: This needs to be improved, but for now here's a sketch of how grading works: @@ -340,7 +347,45 @@ If you look at some older xml, you may see some tags or metadata attributes that # Static links -if your content links (e.g. in an html file) to `"static/blah/ponies.jpg"`, we will look for this in `YOUR_COURSE_DIR/blah/ponies.jpg`. Note that this is not looking in a `static/` subfolder in your course dir. This may (should?) change at some point. Links that include `/course` will be rewritten to the root of your course in the courseware (e.g. `courses/{org}/{course}/{url_name}/` in the current url structure). This is useful for linking to the course wiki, for example. +If your content links (e.g. in an html file) to `"static/blah/ponies.jpg"`, we will look for this... + +* If your course dir has a `static/` subdirectory, we will look in `YOUR_COURSE_DIR/static/blah/ponies.jpg`. This is the prefered organization, as it does not expose anything except what's in `static/` to the world. +* If your course dir does not have a `static/` subdirectory, we will look in `YOUR_COURSE_DIR/blah/ponies.jpg`. This is the old organization, and requires that the web server allow access to everything in the couse dir. To switch to the new organization, move all your static content into a new `static/` dir (e.g. if you currently have things in `images/`, `css/`, and `special/`, create a dir called `static/`, and move `images/, css/, and special/` there). + +Links that include `/course` will be rewritten to the root of your course in the courseware (e.g. `courses/{org}/{course}/{url_name}/` in the current url structure). This is useful for linking to the course wiki, for example. + +# Tabs + +If you want to customize the courseware tabs displayed for your course, specify a "tabs" list in the course-level policy. e.g.: + +"tabs" : [ +{ "type": "courseware"}, # no name--always "Courseware" for consistency between courses +{"name": "Course Info", + "type": course_info"}, +{"name": "My Discussion", + "type": external_"link", + "link": "http://www.mydiscussion.org/blah"}, +{"name": "Progress", +"type": "Progress"}, +{"name": "Wonderwiki", + "type": "wiki"}, +{"type": "textbooks"} # generates one tab per textbook, taking names from the textbook titles +] + + +* If you specify any tabs, you must specify all tabs. They will appear in the order given. +* The first two tabs must have types "courseware" and "course_info", in that order. Otherwise, we'll refuse to load the course. +* An Instructor tab will be automatically added at the end for course staff users. + +## Supported tab types: + +* "courseware". No other parameters. +* "course_info". Parameter "name". +* "wiki". Parameter "name". +* "discussion". Parameter "name". +* "external_link". Parameters "name", "link". +* "textbooks". No parameters--generates tab names from book titles. +* "progress". Parameter "name". # Tips for content developers diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py new file mode 100644 index 0000000000..01753625b8 --- /dev/null +++ b/lms/djangoapps/courseware/tabs.py @@ -0,0 +1,233 @@ +""" +Tabs configuration. By the time the tab is being rendered, it's just a name, +link, and css class (CourseTab tuple). Tabs are specified in course policy. +Each tab has a type, and possibly some type-specific parameters. + +To add a new tab type, add a TabImpl to the VALID_TAB_TYPES dict below--it will +contain a validation function that checks whether config for the tab type is +valid, and a generator function that takes the config, user, and course, and +actually generates the CourseTab. +""" + +from collections import namedtuple +import logging + +from django.conf import settings +from django.core.urlresolvers import reverse + +from courseware.access import has_access + +log = logging.getLogger(__name__) + +class InvalidTabsException(Exception): + """ + A complaint about invalid tabs. + """ + pass + +CourseTab = namedtuple('CourseTab', 'name link is_active') + +# encapsulate implementation for a tab: +# - a validation function: takes the config dict and raises +# InvalidTabsException if required fields are missing or otherwise +# wrong. (e.g. "is there a 'name' field?). Validators can assume +# that the type field is valid. +# +# - a function that takes a config, a user, and a course, and active_page and +# return a list of CourseTabs. (e.g. "return a CourseTab with specified +# name, link to courseware, and is_active=True/False"). The function can +# assume that it is only called with configs of the appropriate type that +# have passed the corresponding validator. +TabImpl = namedtuple('TabImpl', 'validator generator') + + +##### Generators for various tabs. + +def _tab(name, view_name, is_active, extra_args=[]): + """Return a CourseTab when link is reverse of css class with course_id""" + return CourseTab(name, reverse(class_name, args=[course.id]), class_name) + +def _courseware(tab, user, course, active_page): + link = reverse('courseware', args=[course.id]) + return [CourseTab('Courseware', link, active_page == "courseware")] + +def _course_info(tab, user, course, active_page): + link = reverse('info', args=[course.id]) + return [CourseTab(tab['name'], link, active_page == "info")] + +def _progress(tab, user, course, active_page): + if user.is_authenticated(): + link = reverse('progress', args=[course.id]) + return [CourseTab(tab['name'], link, active_page == "progress")] + return [] + +def _wiki(tab, user, course, active_page): + if settings.WIKI_ENABLED: + link = reverse('course_wiki', args=[course.id]) + return [CourseTab(tab['name'], link, active_page == 'wiki')] + return [] + +def _discussion(tab, user, course, active_page): + """ + This tab format only supports the new Berkeley discussion forums. + """ + if settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'): + link = reverse('django_comment_client.forum.views.forum_form_discussion', + args=[course.id]) + return [CourseTab(tab['name'], link, active_page=='discussion')] + return [] + +def _external_link(tab, user, course, active_page): + # external links are never active + return [CourseTab(tab['name'], tab['link'], False)] + + +def _textbooks(tab, user, course, active_page): + """ + Generates one tab per textbook. Only displays if user is authenticated. + """ + if user.is_authenticated() and settings.MITX_FEATURES.get('ENABLE_TEXTBOOK'): + # since there can be more than one textbook, active_page is e.g. "book/0". + return [CourseTab(textbook.title, reverse('book', args=[course.id, index]), + active_page=="textbook/{0}".format(index)) + for index, textbook in enumerate(course.textbooks)] + return [] + +#### Validators + + +def key_checker(expected_keys): + """ + Returns a function that checks that specified keys are present in a dict + """ + def check(d): + for k in expected_keys: + if k not in d: + raise InvalidTabsException("Key {0} not present in {1}" + .format(k, d)) + return check + + +need_name = key_checker(['name']) + +def null_validator(d): + """ + Don't check anything--use for tabs that don't need any params. (e.g. textbook) + """ + pass + +##### The main tab config dict. + +# type -> TabImpl +VALID_TAB_TYPES = { + 'courseware': TabImpl(null_validator, _courseware), + 'course_info': TabImpl(need_name, _course_info), + 'wiki': TabImpl(need_name, _wiki), + 'discussion': TabImpl(need_name, _discussion), + 'external_link': TabImpl(key_checker(['name', 'link']), _external_link), + 'textbooks': TabImpl(null_validator, _textbooks), + 'progress': TabImpl(need_name, _progress), + } + + +### External interface below this. + +def validate_tabs(course): + """ + Check that the tabs set for the specified course is valid. If it + isn't, raise InvalidTabsException with the complaint. + + Specific rules checked: + - if no tabs specified, that's fine + - if tabs specified, first two must have type 'courseware' and 'course_info', in that order. + - All the tabs must have a type in VALID_TAB_TYPES. + + """ + tabs = course.tabs + if tabs is None: + return + + if len(tabs) < 2: + raise InvalidTabsException("Expected at least two tabs. tabs: '{0}'".format(tabs)) + if tabs[0]['type'] != 'courseware': + raise InvalidTabsException( + "Expected first tab to have type 'courseware'. tabs: '{0}'".format(tabs)) + if tabs[1]['type'] != 'course_info': + raise InvalidTabsException( + "Expected second tab to have type 'course_info'. tabs: '{0}'".format(tabs)) + for t in tabs: + if t['type'] not in VALID_TAB_TYPES: + raise InvalidTabsException("Unknown tab type {0}. Known types: {1}" + .format(t['type'], VALID_TAB_TYPES)) + # the type-specific validator checks the rest of the tab config + VALID_TAB_TYPES[t['type']].validator(t) + + # Possible other checks: make sure tabs that should only appear once (e.g. courseware) + # are actually unique (otherwise, will break active tag code) + + +def get_course_tabs(user, course, active_page): + """ + Return the tabs to show a particular user, as a list of CourseTab items. + """ + if not course.tabs: + return get_default_tabs(user, course, active_page) + + # TODO (vshnayder): There needs to be a place to call this right after course + # load, but not from inside xmodule, since that doesn't (and probably + # shouldn't) know about the details of what tabs are supported, etc. + validate_tabs(course) + + tabs = [] + for tab in course.tabs: + # expect handlers to return lists--handles things that are turned off + # via feature flags, and things like 'textbook' which might generate + # multiple tabs. + gen = VALID_TAB_TYPES[tab['type']].generator + tabs.extend(gen(tab, user, course, active_page)) + + # Instructor tab is special--automatically added if user is staff for the course + if has_access(user, course, 'staff'): + tabs.append(CourseTab('Instructor', + reverse('instructor_dashboard', args=[course.id]), + active_page == 'instructor')) + return tabs + + +def get_default_tabs(user, course, active_page): + + # When calling the various _tab methods, can omit the 'type':'blah' from the + # first arg, since that's only used for dispatch + tabs = [] + tabs.extend(_courseware({''}, user, course, active_page)) + tabs.extend(_course_info({'name': 'Course Info'}, user, course, active_page)) + + if hasattr(course, 'syllabus_present') and course.syllabus_present: + link = reverse('syllabus', args=[course.id]) + tabs.append(CourseTab('Syllabus', link, active_page=='syllabus')) + + tabs.extend(_textbooks({}, user, course, active_page)) + + ## If they have a discussion link specified, use that even if we feature + ## flag discussions off. Disabling that is mostly a server safety feature + ## at this point, and we don't need to worry about external sites. + if course.discussion_link: + tabs.append(CourseTab('Discussion', course.discussion_link, active_page == 'discussion')) + elif settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'): + link = reverse('django_comment_client.forum.views.forum_form_discussion', + args=[course.id]) + tabs.append(CourseTab('Discussion', link, active_page == 'discussion')) + elif settings.MITX_FEATURES.get('ENABLE_DISCUSSION'): + ## This is Askbot, which we should be retiring soon... + tabs.append(CourseTab('Discussion', reverse('questions'), active_page == 'discussion')) + + tabs.extend(_wiki({'name': 'Wiki', 'type': 'wiki'}, user, course, active_page)) + + if user.is_authenticated() and not course.hide_progress_tab: + tabs.extend(_progress({'name': 'Progress'}, user, course, active_page)) + + if has_access(user, course, 'staff'): + link = reverse('instructor_dashboard', args=[course.id]) + tabs.append(CourseTab('Instructor', link, active_page=='instructor')) + + return tabs diff --git a/lms/djangoapps/staticbook/views.py b/lms/djangoapps/staticbook/views.py index d68117dd8a..37087af597 100644 --- a/lms/djangoapps/staticbook/views.py +++ b/lms/djangoapps/staticbook/views.py @@ -11,11 +11,13 @@ def index(request, course_id, book_index, page=0): course = get_course_with_access(request.user, course_id, 'load') staff_access = has_access(request.user, course, 'staff') - textbook = course.textbooks[int(book_index)] + book_index = int(book_index) + textbook = course.textbooks[book_index] table_of_contents = textbook.table_of_contents return render_to_response('staticbook.html', - {'page': int(page), 'course': course, 'book_url': textbook.book_url, + {'book_index': book_index, 'page': int(page), + 'course': course, 'book_url': textbook.book_url, 'table_of_contents': table_of_contents, 'staff_access': staff_access}) diff --git a/lms/templates/courseware/course_navigation.html b/lms/templates/courseware/course_navigation.html index ffa8b0cadd..5ae69908fb 100644 --- a/lms/templates/courseware/course_navigation.html +++ b/lms/templates/courseware/course_navigation.html @@ -6,54 +6,21 @@ if active_page == None and active_page_context is not UNDEFINED: # If active_page is not passed in as an argument, it may be in the context as active_page_context active_page = active_page_context -def url_class(url): - if url == active_page: +def url_class(is_active): + if is_active: return "active" return "" %> -<%! from django.core.urlresolvers import reverse %> -<%! from courseware.access import has_access %> +<%! from courseware.tabs import get_course_tabs %>