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/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py index 0aeaa59d69..4b3050e227 100644 --- a/common/djangoapps/xmodule_modifiers.py +++ b/common/djangoapps/xmodule_modifiers.py @@ -34,6 +34,17 @@ def wrap_xmodule(get_html, module, template): return _get_html +def replace_course_urls(get_html, course_id, module): + """ + Updates the supplied module with a new get_html function that wraps + the old get_html function and substitutes urls of the form /course/... + with urls that are /courses//... + """ + @wraps(get_html) + def _get_html(): + return replace_urls(get_html(), staticfiles_prefix='/courses/'+course_id, replace_prefix='/course/') + return _get_html + def replace_static_urls(get_html, prefix, module): """ Updates the supplied module with a new get_html function that wraps 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/backcompat_module.py b/common/lib/xmodule/xmodule/backcompat_module.py index c49f23b99e..ed2bdb837a 100644 --- a/common/lib/xmodule/xmodule/backcompat_module.py +++ b/common/lib/xmodule/xmodule/backcompat_module.py @@ -21,6 +21,7 @@ def process_includes(fn): xml_object = etree.fromstring(xml_data) next_include = xml_object.find('include') while next_include is not None: + system.error_tracker("WARNING: the tag is deprecated, and will go away.") file = next_include.get('file') parent = next_include.getparent() @@ -67,6 +68,8 @@ class SemanticSectionDescriptor(XModuleDescriptor): the child element """ xml_object = etree.fromstring(xml_data) + system.error_tracker("WARNING: the <{}> tag is deprecated. Please do not use in new content." + .format(xml_object.tag)) if len(xml_object) == 1: for (key, val) in xml_object.items(): @@ -74,7 +77,7 @@ class SemanticSectionDescriptor(XModuleDescriptor): return system.process_xml(etree.tostring(xml_object[0])) else: - xml_object.tag = 'sequence' + xml_object.tag = 'sequential' return system.process_xml(etree.tostring(xml_object)) @@ -83,10 +86,14 @@ class TranslateCustomTagDescriptor(XModuleDescriptor): def from_xml(cls, xml_data, system, org=None, course=None): """ Transforms the xml_data from <$custom_tag attr="" attr=""/> to - $custom_tag + """ xml_object = etree.fromstring(xml_data) + system.error_tracker('WARNING: the <{tag}> tag is deprecated. ' + 'Instead, use . ' + .format(tag=xml_object.tag)) + tag = xml_object.tag xml_object.tag = 'customtag' xml_object.attrib['impl'] = tag diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 46e02542c8..b90a94279c 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -237,7 +237,7 @@ class CapaModule(XModule): else: raise - content = {'name': self.metadata['display_name'], + content = {'name': self.display_name, 'html': html, 'weight': self.weight, } @@ -376,14 +376,17 @@ class CapaModule(XModule): ''' For the "show answer" button. - TODO: show answer events should be logged here, not just in the problem.js - Returns the answers: {'answers' : answers} ''' + event_info = dict() + event_info['problem_id'] = self.location.url() + self.system.track_function('show_answer', event_info) if not self.answer_available(): 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} # Figure out if we should move these to capa_problem? @@ -464,7 +467,7 @@ class CapaModule(XModule): return {'success': msg} log.exception("Error in capa_module problem checking") raise Exception("error in capa_module") - + self.attempts = self.attempts + 1 self.lcp.done = True diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 40eec1f70f..4729de905d 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -1,6 +1,7 @@ from fs.errors import ResourceNotFoundError import time import logging +from lxml import etree from xmodule.util.decorators import lazyproperty from xmodule.graders import load_grading_policy @@ -10,12 +11,28 @@ from xmodule.timeparse import parse_time, stringify_time log = logging.getLogger(__name__) - class CourseDescriptor(SequenceDescriptor): module_class = SequenceModule + class Textbook: + def __init__(self, title, table_of_contents_url): + self.title = title + self.table_of_contents_url = table_of_contents_url + + @classmethod + def from_xml_object(cls, xml_object): + return cls(xml_object.get('title'), xml_object.get('table_of_contents_url')) + + @property + def table_of_contents(self): + raw_table_of_contents = open(self.table_of_contents_url, 'r') # TODO: This will need to come from S3 + table_of_contents = etree.parse(raw_table_of_contents).getroot() + return table_of_contents + + def __init__(self, system, definition=None, **kwargs): super(CourseDescriptor, self).__init__(system, definition, **kwargs) + self.textbooks = self.definition['data']['textbooks'] msg = None if self.start is None: @@ -28,6 +45,16 @@ class CourseDescriptor(SequenceDescriptor): self.enrollment_start = self._try_parse_time("enrollment_start") self.enrollment_end = self._try_parse_time("enrollment_end") + @classmethod + def definition_from_xml(cls, xml_object, system): + textbooks = [] + for textbook in xml_object.findall("textbook"): + textbooks.append(cls.Textbook.from_xml_object(textbook)) + xml_object.remove(textbook) + definition = super(CourseDescriptor, cls).definition_from_xml(xml_object, system) + definition.setdefault('data', {})['textbooks'] = textbooks + return definition + def has_started(self): return time.gmtime() > self.start @@ -53,7 +80,6 @@ class CourseDescriptor(SequenceDescriptor): return grading_policy - @lazyproperty def grading_context(self): """ @@ -140,7 +166,7 @@ class CourseDescriptor(SequenceDescriptor): @property def title(self): - return self.metadata['display_name'] + return self.display_name @property def number(self): diff --git a/common/lib/xmodule/xmodule/css/sequence/display.scss b/common/lib/xmodule/xmodule/css/sequence/display.scss index 97a8e0e4b3..a745129a68 100644 --- a/common/lib/xmodule/xmodule/css/sequence/display.scss +++ b/common/lib/xmodule/xmodule/css/sequence/display.scss @@ -4,7 +4,7 @@ nav.sequence-nav { @extend .topbar; border-bottom: 1px solid $border-color; @include border-top-right-radius(4px); - margin: (-(lh())) (-(lh())) lh() (-(lh())); + margin: 0 0 lh() (-(lh())); position: relative; ol { diff --git a/common/lib/xmodule/xmodule/css/video/display.scss b/common/lib/xmodule/xmodule/css/video/display.scss index 8d0c4ac522..9e32de941a 100644 --- a/common/lib/xmodule/xmodule/css/video/display.scss +++ b/common/lib/xmodule/xmodule/css/video/display.scss @@ -4,7 +4,7 @@ div.video { border-bottom: 1px solid #e1e1e1; border-top: 1px solid #e1e1e1; display: block; - margin: 0 (-(lh())); + margin: 0 0 0 (-(lh())); padding: 6px lh(); article.video-wrapper { diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py index 24f0441ee0..cb94444b7a 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py @@ -112,8 +112,8 @@ class TestMongoModuleStore(object): should_work = ( ("i4x://edX/toy/video/Welcome", ("edX/toy/2012_Fall", "Overview", "Welcome", None)), - ("i4x://edX/toy/html/toylab", - ("edX/toy/2012_Fall", "Overview", "Toy_Videos", None)), + ("i4x://edX/toy/chapter/Overview", + ("edX/toy/2012_Fall", "Overview", None, None)), ) for location, expected in should_work: assert_equals(path_to_location(self.store, location), expected) diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index 8c4c373d4f..971124e413 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 @@ -5,6 +6,7 @@ import re from fs.osfs import OSFS from importlib import import_module from lxml import etree +from lxml.html import HtmlComment from path import path from xmodule.errortracker import ErrorLog, make_error_tracker from xmodule.x_module import XModuleDescriptor, XMLParsingSystem @@ -15,9 +17,10 @@ from cStringIO import StringIO from . import ModuleStoreBase, Location from .exceptions import ItemNotFoundError -etree.set_default_parser( - etree.XMLParser(dtd_validation=False, load_dtd=False, - remove_comments=True, remove_blank_text=True)) +edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False, + remove_comments=True, remove_blank_text=True) + +etree.set_default_parser(edx_xml_parser) log = logging.getLogger('mitx.' + __name__) @@ -30,7 +33,8 @@ def clean_out_mako_templating(xml_string): return xml_string class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): - def __init__(self, xmlstore, org, course, course_dir, error_tracker, **kwargs): + def __init__(self, xmlstore, org, course, course_dir, + policy, error_tracker, **kwargs): """ A class that handles loading from xml. Does some munging to ensure that all elements have unique slugs. @@ -96,7 +100,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): MakoDescriptorSystem.__init__(self, load_item, resources_fs, error_tracker, render_template, **kwargs) XMLParsingSystem.__init__(self, load_item, resources_fs, - error_tracker, process_xml, **kwargs) + error_tracker, process_xml, policy, **kwargs) class XMLModuleStore(ModuleStoreBase): @@ -149,7 +153,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 +174,28 @@ 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: + log.debug("Loading policy from {}".format(policy_path)) + 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): """ @@ -188,7 +213,7 @@ class XMLModuleStore(ModuleStoreBase): # been imported into the cms from xml course_file = StringIO(clean_out_mako_templating(course_file.read())) - course_data = etree.parse(course_file).getroot() + course_data = etree.parse(course_file,parser=edx_xml_parser).getroot() org = course_data.get('org') @@ -211,9 +236,17 @@ class XMLModuleStore(ModuleStoreBase): tracker(msg) course = course_dir - system = ImportSystem(self, org, course, course_dir, tracker) + url_name = course_data.get('url_name') + if url_name: + policy_path = self.data_dir / course_dir / 'policies' / '{}.json'.format(url_name) + policy = self.load_policy(policy_path, tracker) + else: + policy = {} + + system = ImportSystem(self, org, course, course_dir, policy, tracker) course_descriptor = system.process_xml(etree.tostring(course_data)) + # 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/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 2986c948d3..fee4d53700 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -76,7 +76,7 @@ class SequenceModule(XModule): contents.append({ 'content': child.get_html(), 'title': "\n".join( - grand_child.metadata['display_name'].strip() + grand_child.display_name.strip() for grand_child in child.get_children() if 'display_name' in grand_child.metadata ), @@ -107,7 +107,7 @@ class SequenceModule(XModule): class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor): mako_template = 'widgets/sequence-edit.html' module_class = SequenceModule - + stores_state = True # For remembering where in the sequence the student is @classmethod diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py index 1da618f6a4..dfa75f9137 100644 --- a/common/lib/xmodule/xmodule/tests/test_import.py +++ b/common/lib/xmodule/xmodule/tests/test_import.py @@ -42,9 +42,9 @@ class DummySystem(XMLParsingSystem): descriptor.get_children() return descriptor - + policy = {} XMLParsingSystem.__init__(self, load_item, self.resources_fs, - self.errorlog.tracker, process_xml) + self.errorlog.tracker, process_xml, policy) def render_template(self, template, context): raise Exception("Shouldn't be called") diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 06449dc37f..d4d61f4aa1 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,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet): return dict((k,v) for k,v in self.metadata.items() if k not in self._inherited_metadata) + @staticmethod def compute_inherited_metadata(node): """Given a descriptor, traverse all of its descendants and do metadata @@ -671,16 +680,19 @@ class DescriptorSystem(object): class XMLParsingSystem(DescriptorSystem): - def __init__(self, load_item, resources_fs, error_tracker, process_xml, **kwargs): + def __init__(self, load_item, resources_fs, error_tracker, process_xml, policy, **kwargs): """ load_item, resources_fs, error_tracker: see DescriptorSystem + policy: a policy dictionary for overriding xml metadata + process_xml: Takes an xml string, and returns a XModuleDescriptor created from that xml """ DescriptorSystem.__init__(self, load_item, resources_fs, error_tracker, **kwargs) self.process_xml = process_xml + self.policy = policy class ModuleSystem(object): diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index 399d5d3f91..c7042efda2 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -1,4 +1,4 @@ -from xmodule.x_module import XModuleDescriptor +from xmodule.x_module import (XModuleDescriptor, policy_key) from xmodule.modulestore import Location from lxml import etree import json @@ -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: @@ -270,6 +270,11 @@ class XmlDescriptor(XModuleDescriptor): log.debug('Error %s in loading metadata %s' % (err,dmdata)) metadata['definition_metadata_err'] = str(err) + # Set/override any metadata specified by policy + k = policy_key(location) + if k in system.policy: + metadata.update(system.policy[k]) + return cls( system, definition, diff --git a/common/test/data/toy/course.xml b/common/test/data/toy/course.xml deleted file mode 100644 index 270a1eb27f..0000000000 --- a/common/test/data/toy/course.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/common/test/data/toy/course.xml b/common/test/data/toy/course.xml new file mode 120000 index 0000000000..49041310f6 --- /dev/null +++ b/common/test/data/toy/course.xml @@ -0,0 +1 @@ +roots/2012_Fall.xml \ No newline at end of file diff --git a/common/test/data/toy/course/2012_Fall.xml b/common/test/data/toy/course/2012_Fall.xml new file mode 100644 index 0000000000..d34eb9d56a --- /dev/null +++ b/common/test/data/toy/course/2012_Fall.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/common/test/data/toy/policies/2012_Fall.json b/common/test/data/toy/policies/2012_Fall.json new file mode 100644 index 0000000000..6c501d66f8 --- /dev/null +++ b/common/test/data/toy/policies/2012_Fall.json @@ -0,0 +1,23 @@ +{ + "course/2012_Fall": { + "graceperiod": "2 days 5 hours 59 minutes 59 seconds", + "start": "2015-07-17T12:00", + "display_name": "Toy Course" + }, + "chapter/Overview": { + "display_name": "Overview" + }, + "videosequence/Toy_Videos": { + "display_name": "Toy Videos", + "format": "Lecture Sequence" + }, + "html/toylab": { + "display_name": "Toy lab" + }, + "video/Video_Resources": { + "display_name": "Video Resources" + }, + "video/Welcome": { + "display_name": "Welcome" + } +} diff --git a/common/test/data/toy/roots/2012_Fall.xml b/common/test/data/toy/roots/2012_Fall.xml new file mode 100644 index 0000000000..b71528809b --- /dev/null +++ b/common/test/data/toy/roots/2012_Fall.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/common/xml_cleanup.py b/common/xml_cleanup.py new file mode 100755 index 0000000000..8e794b97c2 --- /dev/null +++ b/common/xml_cleanup.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python + +""" +Victor's xml cleanup script. A big pile of useful hacks. Do not use +without carefully reading the code and deciding that this is what you want. + +In particular, the remove-meta option is only intended to be used after pulling out a policy +using the metadata_to_json management command. +""" + +import os, fnmatch, re, sys +from lxml import etree +from collections import defaultdict + +INVALID_CHARS = re.compile(r"[^\w.-]") + +def clean(value): + """ + Return value, made into a form legal for locations + """ + return re.sub('_+', '_', INVALID_CHARS.sub('_', value)) + + +# category -> set of url_names for that category that we've already seen +used_names = defaultdict(set) + +def clean_unique(category, name): + cleaned = clean(name) + if cleaned not in used_names[category]: + used_names[category].add(cleaned) + return cleaned + x = 1 + while cleaned + str(x) in used_names[category]: + x += 1 + + # Found one! + cleaned = cleaned + str(x) + used_names[category].add(cleaned) + return cleaned + +def cleanup(filepath, remove_meta): + # Keys that are exported to the policy file, and so + # can be removed from the xml afterward + to_remove = ('format', 'display_name', + 'graceperiod', 'showanswer', 'rerandomize', + 'start', 'due', 'graded', 'hide_from_toc', + 'ispublic', 'xqa_key') + + try: + print "Cleaning {}".format(filepath) + with open(filepath) as f: + parser = etree.XMLParser(remove_comments=False) + xml = etree.parse(filepath, parser=parser) + except: + print "Error parsing file {}".format(filepath) + return + + for node in xml.iter(tag=etree.Element): + attrs = node.attrib + if 'url_name' in attrs: + used_names[node.tag].add(attrs['url_name']) + if 'name' in attrs: + # Replace name with an identical display_name, and a unique url_name + name = attrs['name'] + attrs['display_name'] = name + attrs['url_name'] = clean_unique(node.tag, name) + del attrs['name'] + + if 'url_name' in attrs and 'slug' in attrs: + print "WARNING: {} has both slug and url_name" + + if ('url_name' in attrs and 'filename' in attrs and + len(attrs)==2 and attrs['url_name'] == attrs['filename']): + # This is a pointer tag in disguise. Get rid of the filename. + print 'turning {}.{} into a pointer tag'.format(node.tag, attrs['url_name']) + del attrs['filename'] + + if remove_meta: + for attr in to_remove: + if attr in attrs: + del attrs[attr] + + + with open(filepath, "w") as f: + f.write(etree.tostring(xml)) + + +def find_replace(directory, filePattern, remove_meta): + for path, dirs, files in os.walk(os.path.abspath(directory)): + for filename in fnmatch.filter(files, filePattern): + filepath = os.path.join(path, filename) + cleanup(filepath, remove_meta) + + +def main(args): + usage = "xml_cleanup [dir] [remove-meta]" + n = len(args) + if n < 1 or n > 2 or (n == 2 and args[1] != 'remove-meta'): + print usage + return + + remove_meta = False + if n == 2: + remove_meta = True + + find_replace(args[0], '*.xml', remove_meta) + + +if __name__ == '__main__': + main(sys.argv[1:]) + + diff --git a/doc/development.md b/doc/development.md index 590a935405..cb71278c40 100644 --- a/doc/development.md +++ b/doc/development.md @@ -66,3 +66,9 @@ To run a single nose test: Very handy: if you uncomment the `--pdb` argument in `NOSE_ARGS` in `lms/envs/test.py`, it will drop you into pdb on error. This lets you go up and down the stack and see what the values of the variables are. Check out http://docs.python.org/library/pdb.html + +## Content development + +If you change course content, while running the LMS in dev mode, it is unnecessary to restart to refresh the modulestore. + +Instead, hit /migrate/modules to see a list of all modules loaded, and click on links (eg /migrate/reload/edx4edx) to reload a course. diff --git a/lms/askbot/skins/common/templates/question/answer_controls.html b/lms/askbot/skins/common/templates/question/answer_controls.html index 52c4836e1e..f1896e0d95 100644 --- a/lms/askbot/skins/common/templates/question/answer_controls.html +++ b/lms/askbot/skins/common/templates/question/answer_controls.html @@ -31,7 +31,7 @@ {% spaceless %} - {% if answer.deleted %}{% trans %}undelete{% endtrans %}{% else %}✖{% endif %} + {% if answer.deleted %}{% trans %}undelete{% endtrans %}{% else %}delete{% endif %} {% endspaceless %} {% endif %} diff --git a/lms/askbot/skins/common/templates/question/question_controls.html b/lms/askbot/skins/common/templates/question/question_controls.html index af30d43419..2409371c65 100644 --- a/lms/askbot/skins/common/templates/question/question_controls.html +++ b/lms/askbot/skins/common/templates/question/question_controls.html @@ -40,6 +40,5 @@ {% endif %} {% if request.user|can_delete_post(question) %}{{ pipe() }} - {% if question.deleted %}{% trans %}undelete{% endtrans %}{% else %}✖{% endif %} - + {% if question.deleted %}{% trans %}undelete{% endtrans %}{% else %}delete{% endif %} {% endif %} diff --git a/lms/askbot/skins/mitx/templates/base.html b/lms/askbot/skins/mitx/templates/base.html index a344009c60..4c5a36bd46 100644 --- a/lms/askbot/skins/mitx/templates/base.html +++ b/lms/askbot/skins/mitx/templates/base.html @@ -18,11 +18,14 @@ {% include "widgets/system_messages.html" %} {% include "debug_header.html" %} {% include "widgets/header.html" %} {# Logo, user tool navigation and meta navitation #} - {# include "widgets/secondary_header.html" #} {# Scope selector, search input and ask button #} -
+
+ {# include "widgets/secondary_header.html" #} {# Scope selector, search input and ask button #} + +
{% block body %} {% endblock %} +
{% if settings.FOOTER_MODE == 'default' %} diff --git a/lms/askbot/skins/mitx/templates/meta/html_head_stylesheets.html b/lms/askbot/skins/mitx/templates/meta/html_head_stylesheets.html index 3ec11b59fd..a6a3d3cd46 100644 --- a/lms/askbot/skins/mitx/templates/meta/html_head_stylesheets.html +++ b/lms/askbot/skins/mitx/templates/meta/html_head_stylesheets.html @@ -1,4 +1,3 @@ {% load extra_filters_jinja %} - {{ 'application' | compressed_css }} {{ 'course' | compressed_css }} 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 261140dec7..4389a5f169 100644 --- a/lms/djangoapps/courseware/models.py +++ b/lms/djangoapps/courseware/models.py @@ -77,7 +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 + select_for_update: Flag indicating whether the rows should be locked until end of transaction ''' if user.is_authenticated(): module_ids = self._get_module_state_keys(descriptors) @@ -110,7 +110,7 @@ class StudentModuleCache(object): 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 + select_for_update: Flag indicating whether the rows should be locked until end of transaction """ def get_child_descriptors(descriptor, depth, descriptor_filter): diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index dae9d7a952..f58170552c 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -19,7 +19,7 @@ from xmodule.exceptions import NotFoundError from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore from xmodule.x_module import ModuleSystem -from xmodule_modifiers import replace_static_urls, add_histogram, wrap_xmodule +from xmodule_modifiers import replace_course_urls, replace_static_urls, add_histogram, wrap_xmodule log = logging.getLogger("mitx.courseware") @@ -48,7 +48,7 @@ def make_track_function(request): return f -def toc_for_course(user, request, course, active_chapter, active_section): +def toc_for_course(user, request, course, active_chapter, active_section, course_id=None): ''' Create a table of contents from the module store @@ -71,7 +71,7 @@ def toc_for_course(user, request, course, active_chapter, active_section): ''' student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, course, depth=2) - course = get_module(user, request, course.location, student_module_cache) + course = get_module(user, request, course.location, student_module_cache, course_id=course_id) chapters = list() for chapter in course.get_display_items(): @@ -127,7 +127,7 @@ def get_section(course_module, chapter, section): return section_module -def get_module(user, request, location, student_module_cache, position=None): +def get_module(user, request, location, student_module_cache, position=None, course_id=None): ''' Get an instance of the xmodule class identified by location, setting the state based on an existing StudentModule, or creating one if none exists. @@ -144,6 +144,14 @@ def get_module(user, request, location, student_module_cache, position=None): ''' descriptor = modulestore().get_item(location) + + # NOTE: + # A 'course_id' is understood to be the triplet (org, course, run), for example + # (MITx, 6.002x, 2012_Spring). + # At the moment generic XModule does not contain enough information to replicate + # the triplet (it is missing 'run'), so we must pass down course_id + if course_id is None: + course_id = descriptor.location.course_id # Will NOT produce (org, course, run) for non-CourseModule's # Short circuit--if the user shouldn't have access, bail without doing any work if not has_access(user, descriptor, 'load'): @@ -167,7 +175,7 @@ def get_module(user, request, location, student_module_cache, position=None): # Setup system context for module instance ajax_url = reverse('modx_dispatch', - kwargs=dict(course_id=descriptor.location.course_id, + kwargs=dict(course_id=course_id, id=descriptor.location.url(), dispatch=''), ) @@ -175,7 +183,7 @@ def get_module(user, request, location, student_module_cache, position=None): # Fully qualified callback URL for external queueing system xqueue_callback_url = request.build_absolute_uri('/')[:-1] # Trailing slash provided by reverse xqueue_callback_url += reverse('xqueue_callback', - kwargs=dict(course_id=descriptor.location.course_id, + kwargs=dict(course_id=course_id, userid=str(user.id), id=descriptor.location.url(), dispatch='score_update'), @@ -195,7 +203,7 @@ def get_module(user, request, location, student_module_cache, position=None): Delegate to get_module. It does an access check, so may return None """ return get_module(user, request, location, - student_module_cache, position) + student_module_cache, position, course_id=course_id) # TODO (cpennington): When modules are shared between courses, the static # prefix is going to have to be specific to the module, not the directory @@ -225,6 +233,10 @@ def get_module(user, request, location, student_module_cache, position=None): module.metadata['data_dir'], module ) + # Allow URLs of the form '/course/' refer to the root of multicourse directory + # hierarchy of this course + module.get_html = replace_course_urls(module.get_html, course_id, module) + if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'): if has_access(user, module, 'staff'): module.get_html = add_histogram(module.get_html, module, user) @@ -370,7 +382,7 @@ def modx_dispatch(request, dispatch=None, id=None, course_id=None): p[inputfile_id] = inputfile student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, modulestore().get_item(id)) - instance = get_module(request.user, request, id, student_module_cache) + instance = get_module(request.user, request, id, student_module_cache, course_id=course_id) if instance is None: # Either permissions just changed, or someone is trying to be clever # and load something they shouldn't have access to. diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index ab63872170..f5a93475b9 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -33,6 +33,7 @@ log = logging.getLogger("mitx.courseware") template_imports = {'urllib': urllib} + def user_groups(user): """ TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately. @@ -63,11 +64,12 @@ 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}) -def render_accordion(request, course, chapter, section): +def render_accordion(request, course, chapter, section, course_id=None): ''' Draws navigation bar. Takes current position in accordion as parameter. @@ -78,7 +80,7 @@ def render_accordion(request, course, chapter, section): Returns the html string''' # grab the table of contents - toc = toc_for_course(request.user, request, course, chapter, section) + toc = toc_for_course(request.user, request, course, chapter, section, course_id=course_id) context = dict([('toc', toc), ('course_id', course.id), @@ -110,6 +112,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? @@ -119,11 +122,12 @@ def index(request, course_id, chapter=None, section=None, try: context = { 'csrf': csrf(request)['csrf_token'], - 'accordion': render_accordion(request, course, chapter, section), + 'accordion': render_accordion(request, course, chapter, section, course_id=course_id), '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 @@ -135,7 +139,7 @@ def index(request, course_id, chapter=None, section=None, section_descriptor) module = get_module(request.user, request, section_descriptor.location, - student_module_cache) + student_module_cache, course_id=course_id) if module is None: # User is probably being clever and trying to access something # they don't have access to. @@ -166,7 +170,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") @@ -208,8 +213,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): @@ -241,7 +248,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() @@ -257,20 +265,21 @@ 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)) user_info = UserProfile.objects.get(user=student) student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, course) - course_module = get_module(request.user, request, course.location, student_module_cache) + course_module = get_module(request.user, request, course.location, student_module_cache, course_id=course_id) courseware_summary = grades.progress_summary(student, course_module, course.grader, student_module_cache) grade_summary = grades.grade(request.user, request, course, student_module_cache) @@ -282,8 +291,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() @@ -316,7 +326,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) @@ -325,7 +338,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) @@ -335,6 +349,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/lms_migration/migrate.py b/lms/djangoapps/lms_migration/migrate.py index 2bf893507b..dfdf86b4ac 100644 --- a/lms/djangoapps/lms_migration/migrate.py +++ b/lms/djangoapps/lms_migration/migrate.py @@ -35,7 +35,7 @@ def manage_modulestores(request,reload_dir=None): ip = request.META.get('HTTP_X_REAL_IP','') # nginx reverse proxy if not ip: ip = request.META.get('REMOTE_ADDR','None') - + if LOCAL_DEBUG: html += '

IP address: %s ' % ip html += '

User: %s ' % request.user @@ -48,7 +48,7 @@ def manage_modulestores(request,reload_dir=None): html += 'Permission denied' html += "" log.debug('request denied, ALLOWED_IPS=%s' % ALLOWED_IPS) - return HttpResponse(html) + return HttpResponse(html) #---------------------------------------- # reload course if specified @@ -74,10 +74,10 @@ def manage_modulestores(request,reload_dir=None): #---------------------------------------- dumpfields = ['definition','location','metadata'] - + for cdir, course in def_ms.courses.items(): html += '
' - html += '

Course: %s (%s)

' % (course.metadata['display_name'],cdir) + html += '

Course: %s (%s)

' % (course.display_name,cdir) for field in dumpfields: data = getattr(course,field) @@ -89,7 +89,7 @@ def manage_modulestores(request,reload_dir=None): html += '' else: html += '
  • %s
' % escape(data) - + #---------------------------------------- @@ -107,4 +107,4 @@ def manage_modulestores(request,reload_dir=None): log.debug('def_ms=%s' % unicode(def_ms)) html += "" - return HttpResponse(html) + return HttpResponse(html) 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..aaafb60dd8 100644 --- a/lms/djangoapps/staticbook/views.py +++ b/lms/djangoapps/staticbook/views.py @@ -1,18 +1,22 @@ 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): +def index(request, course_id, book_index, 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 - table_of_contents = etree.parse(raw_table_of_contents).getroot() + staff_access = has_access(request.user, course, 'staff') + + textbook = course.textbooks[int(book_index)] + table_of_contents = textbook.table_of_contents + 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): return index(request, course_id=course_id, page=int(page) + 24) diff --git a/lms/envs/common.py b/lms/envs/common.py index 48fe931869..c412a3c8cd 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..6720c2050d 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": "https://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/static/images/askbot/vote-arrow-down-activate.png b/lms/static/images/askbot/vote-arrow-down-activate.png deleted file mode 100644 index 354c49dca6..0000000000 Binary files a/lms/static/images/askbot/vote-arrow-down-activate.png and /dev/null differ diff --git a/lms/static/images/askbot/vote-arrow-down.png b/lms/static/images/askbot/vote-arrow-down.png index e67524077a..501c2abfae 100644 Binary files a/lms/static/images/askbot/vote-arrow-down.png and b/lms/static/images/askbot/vote-arrow-down.png differ diff --git a/lms/static/images/askbot/vote-arrow-up-activate.png b/lms/static/images/askbot/vote-arrow-up-activate.png deleted file mode 100644 index aa411c70e1..0000000000 Binary files a/lms/static/images/askbot/vote-arrow-up-activate.png and /dev/null differ diff --git a/lms/static/images/askbot/vote-arrow-up.png b/lms/static/images/askbot/vote-arrow-up.png index a35946cc51..22de384120 100644 Binary files a/lms/static/images/askbot/vote-arrow-up.png and b/lms/static/images/askbot/vote-arrow-up.png differ diff --git a/lms/static/sass/course/_info.scss b/lms/static/sass/course/_info.scss index 1651ad4da8..e68e386696 100644 --- a/lms/static/sass/course/_info.scss +++ b/lms/static/sass/course/_info.scss @@ -69,10 +69,15 @@ div.info-wrapper { section.handouts { @extend .sidebar; border-left: 1px solid $border-color; - @include border-radius(0 4px 4px 0); border-right: 0; + @include border-radius(0 4px 4px 0); @include box-shadow(none); + &:after { + left: -1px; + right: auto; + } + h1 { @extend .bottom-border; margin-bottom: 0; diff --git a/lms/static/sass/course/_profile.scss b/lms/static/sass/course/_profile.scss index 006bd902e5..5b1d6ee068 100644 --- a/lms/static/sass/course/_profile.scss +++ b/lms/static/sass/course/_profile.scss @@ -8,6 +8,11 @@ div.profile-wrapper { @include border-radius(0px 4px 4px 0); border-right: 0; + &:after { + left: -1px; + right: auto; + } + header { @extend .bottom-border; margin: 0; diff --git a/lms/static/sass/course/_textbook.scss b/lms/static/sass/course/_textbook.scss index 8e88f8befd..8bbfa67b1c 100644 --- a/lms/static/sass/course/_textbook.scss +++ b/lms/static/sass/course/_textbook.scss @@ -10,7 +10,6 @@ div.book-wrapper { font-size: em(14); .chapter-number { - } .chapter { @@ -81,9 +80,8 @@ div.book-wrapper { section.book { @extend .content; - padding-bottom: 0; padding-right: 0; - padding-top: 0; + padding-left: lh(); nav { @extend .clearfix; diff --git a/lms/static/sass/course/base/_base.scss b/lms/static/sass/course/base/_base.scss index 034e047754..9f0be9c298 100644 --- a/lms/static/sass/course/base/_base.scss +++ b/lms/static/sass/course/base/_base.scss @@ -2,7 +2,7 @@ body { min-width: 980px; } -body, h1, h2, h3, h4, h5, h6, p, p a:link, p a:visited, a { +body, h1, h2, h3, h4, h5, h6, p, p a:link, p a:visited, a, label { text-align: left; font-family: $sans-serif; } @@ -27,6 +27,14 @@ form { } } +img { + max-width: 100%; +} + +.container { + padding: em(40) 0; +} + ::selection, ::-moz-selection, ::-webkit-selection { background:#444; color:#fff; diff --git a/lms/static/sass/course/base/_extends.scss b/lms/static/sass/course/base/_extends.scss index 04eaf73094..78618df83d 100644 --- a/lms/static/sass/course/base/_extends.scss +++ b/lms/static/sass/course/base/_extends.scss @@ -12,10 +12,13 @@ h1.top-header { @include box-shadow(inset 0 1px 0 #fff); color: #666; cursor: pointer; - font: normal $body-font-size $body-font-family; + font: 400 $body-font-size $body-font-family; @include linear-gradient(#fff, lighten(#888, 40%)); padding: 4px 8px; text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: 0; -webkit-font-smoothing: antialiased; &:hover, &:focus { @@ -28,7 +31,7 @@ h1.top-header { .content { @include box-sizing(border-box); display: table-cell; - padding: lh(); + padding-right: lh(); vertical-align: top; width: flex-grid(9) + flex-gutter(); @@ -46,6 +49,18 @@ h1.top-header { vertical-align: top; width: flex-grid(3); + &:after { + width: 1px; + height: 100%; + @include position(absolute, 0px -1px 0px 0); + content: ""; + @include background-image(linear-gradient(top, #fff, rgba(#fff, 0)), linear-gradient(top, rgba(#fff, 0), #fff)); + background-position: top, bottom; + @include background-size(1px 20px); + background-repeat: no-repeat; + display: block; + } + h1, h2 { font-size: em(20); font-weight: 100; @@ -134,7 +149,7 @@ h1.top-header { position: absolute; right: -1px; text-indent: -9999px; - top: 6px; + top: 12px; width: 16px; z-index: 99; diff --git a/lms/static/sass/course/courseware/_courseware.scss b/lms/static/sass/course/courseware/_courseware.scss index 198902c146..24bea4dd84 100644 --- a/lms/static/sass/course/courseware/_courseware.scss +++ b/lms/static/sass/course/courseware/_courseware.scss @@ -12,7 +12,8 @@ div.course-wrapper { section.course-content { @extend .content; - @include border-radius(0 4px 4px 0); + padding-right: 0; + padding-left: lh(); h1 { margin: 0 0 lh(); diff --git a/lms/static/sass/course/discussion/_answers.scss b/lms/static/sass/course/discussion/_answers.scss index 8ab22aa833..73660dd336 100644 --- a/lms/static/sass/course/discussion/_answers.scss +++ b/lms/static/sass/course/discussion/_answers.scss @@ -7,9 +7,16 @@ div.answer-controls { padding-left: flex-grid(1.1); width: 100%; + div.answer-count { display: inline-block; float: left; + + h1 { + margin-bottom: 0; + font-size: em(24); + font-weight: 100; + } } div.answer-sort { @@ -18,7 +25,7 @@ div.answer-controls { nav { float: right; - margin-top: 34px; + margin-top: 10px; a { &.on span{ @@ -44,8 +51,9 @@ div.answer-block { width: 100%; img.answer-img-accept { - margin: 10px 0px 10px 16px; + margin: 10px 0px 10px 11px; } + div.answer-container { @extend div.question-container; @@ -130,21 +138,19 @@ div.answer-own { div.answer-actions { margin: 0; - padding:8px 8px 8px 0; + padding:8px 0 8px 8px; text-align: right; border-top: 1px solid #efefef; span.sep { - color: #EDDFAA; + color: $border-color; } a { cursor: pointer; text-decoration: none; - - &.question-delete { - color: $mit-red; - } + @extend a:link; + font-size: em(14); } } diff --git a/lms/static/sass/course/discussion/_badges.scss b/lms/static/sass/course/discussion/_badges.scss index d74dd93d13..65d8cbf513 100644 --- a/lms/static/sass/course/discussion/_badges.scss +++ b/lms/static/sass/course/discussion/_badges.scss @@ -22,6 +22,8 @@ div#award-list{ } ul.badge-list { + padding-left: 0; + li.badge { border-bottom: 1px solid #eee; @extend .clearfix; @@ -70,12 +72,17 @@ ul.badge-list { .bronze, .badge3 { color: #cc9933; } -div.badge-desc { - > div { - margin-bottom: 20px; - span { - font-size: 18px; - @include border-radius(10px); - } + +div.discussion-wrapper aside { + div.badge-desc { + border-top: 0; + + > div { + margin-bottom: 20px; + span { + font-size: 18px; + @include border-radius(10px); + } + } } } diff --git a/lms/static/sass/course/discussion/_discussion.scss b/lms/static/sass/course/discussion/_discussion.scss index 7b0aa601d9..e12f308ae2 100644 --- a/lms/static/sass/course/discussion/_discussion.scss +++ b/lms/static/sass/course/discussion/_discussion.scss @@ -8,7 +8,7 @@ body.askbot { @include box-sizing(border-box); display: table-cell; min-width: 650px; - padding: lh(); + padding-right: lh(); vertical-align: top; width: flex-grid(9) + flex-gutter(); diff --git a/lms/static/sass/course/discussion/_forms.scss b/lms/static/sass/course/discussion/_forms.scss index ae02ab3b20..43ba07df19 100644 --- a/lms/static/sass/course/discussion/_forms.scss +++ b/lms/static/sass/course/discussion/_forms.scss @@ -5,6 +5,11 @@ form.answer-form { border-top: 1px solid #ddd; overflow: hidden; padding-left: flex-grid(1.1); + padding-top: lh(); + + p { + margin-bottom: lh(); + } textarea { @include box-sizing(border-box); @@ -121,7 +126,6 @@ form.question-form { border: none; padding: 15px 0 0 0; - input[type="text"] { @include box-sizing(border-box); width: flex-grid(6); @@ -131,6 +135,11 @@ form.question-form { margin-top: 10px; } + input[value="Cancel"] { + @extend .light-button; + float: right; + } + div#question-list { background-color: rgba(255,255,255,0.95); @include box-sizing(border-box); diff --git a/lms/static/sass/course/discussion/_modals.scss b/lms/static/sass/course/discussion/_modals.scss index 5a7e6db1e5..f1d1fd78cf 100644 --- a/lms/static/sass/course/discussion/_modals.scss +++ b/lms/static/sass/course/discussion/_modals.scss @@ -1,5 +1,4 @@ // Style for modal boxes that pop up to notify the user of various events - .vote-notification { background-color: darken($mit-red, 7%); @include border-radius(4px); diff --git a/lms/static/sass/course/discussion/_profile.scss b/lms/static/sass/course/discussion/_profile.scss index 010a03ffd6..f20b51b72b 100644 --- a/lms/static/sass/course/discussion/_profile.scss +++ b/lms/static/sass/course/discussion/_profile.scss @@ -9,9 +9,9 @@ body.user-profile-page { } ul.sub-info { - // border-top: 1px solid #ddd; margin-top: lh(); list-style: none; + padding: 0; > li { display: table-cell; @@ -57,6 +57,7 @@ body.user-profile-page { ul { list-style: none; + padding: 0; &.user-stats-table { list-style: none; @@ -72,37 +73,28 @@ body.user-profile-page { margin-bottom: 30px; li { - background-position: 10px center; + background-position: 10px -10px; background-repeat: no-repeat; - @include border-radius(4px); display: inline-block; - height: 20px; - padding: 10px 10px 10px 40px; + padding: 2px 10px 2px 40px; + margin-bottom: lh(.5); + border: 1px solid lighten($border-color, 10%); &.up { - background-color:#d1e3a8; - background-image: url(../images/askbot/vote-arrow-up-activate.png); + background-image: url(../images/askbot/vote-arrow-up.png); margin-right: 6px; - - span.vote-count { - color: #3f6c3e; - } } &.down { - background-image: url(../images/askbot/vote-arrow-down-activate.png); - background-color:#eac6ad; - - span.vote-count { - color: $mit-red; - } - + background-image: url(../images/askbot/vote-arrow-down.png); } } } &.badges { @include inline-block(); + padding: 0; + margin: 0; a { background-color: #e3e3e3; diff --git a/lms/static/sass/course/discussion/_question-view.scss b/lms/static/sass/course/discussion/_question-view.scss index 4c2acaf9be..f7657dbf97 100644 --- a/lms/static/sass/course/discussion/_question-view.scss +++ b/lms/static/sass/course/discussion/_question-view.scss @@ -1,15 +1,16 @@ // Styles for the single question view div.question-header { + @include clearfix(); div.official-stamp { background: $mit-red; color: #fff; font-size: 12px; + margin-left: -1px; margin-top: 10px; padding: 2px 5px; text-align: center; - margin-left: -1px; } div.vote-buttons { @@ -19,40 +20,40 @@ div.question-header { width: flex-grid(0.7,9); ul { - li { - background-position: center; - background-repeat: no-repeat; - cursor: pointer; - font-weight: bold; - height: 20px; - list-style: none; - padding: 10px; - text-align: center; - width: 70%; + padding: 0; + margin: 0; - &.post-vote { - @include border-radius(4px); - @include box-shadow(inset 0 1px 0px #fff); - } + li { + background-repeat: no-repeat; + color: #999; + font-size: em(20); + font-weight: bold; + list-style: none; + text-align: center; &.question-img-upvote, &.answer-img-upvote { background-image: url(../images/askbot/vote-arrow-up.png); - @include box-shadow(inset 0 1px 0px rgba(255, 255, 255, 0.5)); + background-position: center 0; + cursor: pointer; + height: 12px; + margin-bottom: lh(.5); &:hover, &.on { - background-color:#d1e3a8; - border-color: darken(#D1E3A8, 20%); - background-image: url(../images/askbot/vote-arrow-up-activate.png); + background-image: url(../images/askbot/vote-arrow-up.png); + background-position: center -22px; } } &.question-img-downvote, &.answer-img-downvote { + cursor: pointer; background-image: url(../images/askbot/vote-arrow-down.png); + background-position: center 0; + height: 12px; + margin-top: lh(.5); &:hover, &.on { - background-color:#EAC6AD; - border-color: darken(#EAC6AD, 20%); - background-image: url(../images/askbot/vote-arrow-down-activate.png); + background-image: url(../images/askbot/vote-arrow-down.png); + background-position: center -22px; } } } @@ -66,12 +67,19 @@ div.question-header { h1 { margin-top: 0; + font-weight: 100; + line-height: 1.1em; + + a { + font-weight: 100; + line-height: 1.1em; + } } div.meta-bar { border-bottom: 1px solid #eee; display: block; - margin: 10px 0; + margin: lh(.5) 0 lh(); overflow: hidden; padding: 5px 0 10px; @@ -89,11 +97,8 @@ div.question-header { width: flex-grid(4,8); a { - &.question-delete { - color: $mit-red; - text-decoration: none; - cursor: pointer; - } + @extend a:link; + cursor: pointer; } span.sep { @@ -155,7 +160,7 @@ div.question-header { } div.change-date { - font-size: 12px; + font-size: em(14); margin-bottom: 2px; } @@ -179,13 +184,13 @@ div.question-header { display: inline-block; padding: 0 0 3% 0; width: 100%; + margin-top: lh(2); div.comments-content { - font-size: 13px; - background: #efefef; + border-top: 1px solid lighten($border-color, 10%); .block { - border-top: 1px solid #ddd; + border-top: 1px solid lighten($border-color, 10%); padding: 15px; display: block; @@ -197,10 +202,10 @@ div.question-header { padding-top: 10px; span.official-comment { - background: $mit-red; + background: $pink; color: #fff; display: block; - font-size: 12px; + font-size: em(12); margin: 0 0 10px -5%; padding:2px 5px 2px 5%; text-align: left; @@ -212,6 +217,10 @@ div.question-header { form.post-comments { padding: 15px; + button { + color: #fff; + } + button:last-child { margin-left: 10px; @extend .light-button; @@ -232,7 +241,6 @@ div.question-header { border: none; @include box-shadow(none); display: inline-block; - margin-top: -8px; padding:0 2% 0 0; text-align: center; width: 5%; @@ -278,16 +286,14 @@ div.question-header { } div.comment-delete { - // display: inline; - color: $mit-red; + @extend a:link; cursor: pointer; - font-size: 15px; } div.comment-edit { @include transform(rotate(50deg)); cursor: pointer; - font-size: 16px; + a.edit-icon { color: #555; text-decoration: none; @@ -305,13 +311,13 @@ div.question-header { div.comment-meta { text-align: right; + margin-top: lh(.5); a.author { font-weight: bold; } a.edit { - font-size: 12px; padding: 2px 10px; } } @@ -334,12 +340,10 @@ div.question-header { } div.controls { - border-top: 1px solid #efefef; text-align: right; a { display: inline-block; - font-size: 12px; margin: 10px 10px 10px 0; } } diff --git a/lms/static/sass/course/discussion/_questions.scss b/lms/static/sass/course/discussion/_questions.scss index 4f855cd092..1b77231bba 100644 --- a/lms/static/sass/course/discussion/_questions.scss +++ b/lms/static/sass/course/discussion/_questions.scss @@ -1,17 +1,24 @@ // Styles for the default question list view div.question-list-header { + @extend h1.top-header; display: block; margin-bottom: 0px; + padding-bottom: lh(.5); overflow: hidden; width: flex-grid(9,9); - @extend h1.top-header; h1 { margin: 0; + font-size: 1em; + font-weight: 100; + padding-bottom: lh(.5); > a.light-button { float: right; + font-size: em(14, 24); + letter-spacing: 0; + font-weight: 400; } } @@ -49,8 +56,11 @@ div.question-list-header { nav { @extend .action-link; float: right; + font-size: em(16, 24); a { + font-size: 1em; + &.on span{ font-weight: bold; } @@ -82,6 +92,7 @@ div.question-list-header { a { color: #555; + font-size: em(14, 24); } } @@ -90,12 +101,10 @@ div.question-list-header { } ul.tags { - li { - background: #fff; - - &:before { - border-color: transparent #fff transparent transparent; - } + span, div { + line-height: 1em; + margin-left: 6px; + cursor: pointer; } } } @@ -103,26 +112,15 @@ div.question-list-header { ul.question-list, div#question-list { width: flex-grid(9,9); + padding-left: 0; + margin: 0; li.single-question { border-bottom: 1px solid #eee; list-style: none; - padding: 10px lh(); - margin-left: (-(lh())); + padding: lh() 0; width: 100%; - &:hover { - background: #F3F3F3; - - ul.tags li { - background: #ddd; - - &:before { - border-color: transparent #ddd transparent transparent; - } - } - } - &:first-child { border-top: 0; } @@ -133,14 +131,19 @@ ul.question-list, div#question-list { &.question-body { @include box-sizing(border-box); margin-right: flex-gutter(); - width: flex-grid(5.5,9); + width: flex-grid(5,9); h2 { - font-size: 16px; + font-size: em(20); font-weight: bold; letter-spacing: 0; - margin: 0px 0 15px 0; + margin: 0 0 lh() 0; text-transform: none; + line-height: lh(); + + a { + line-height: lh(); + } } p.excerpt { @@ -151,40 +154,41 @@ ul.question-list, div#question-list { div.user-info { display: inline-block; vertical-align: top; - margin-bottom: 10px; + margin: lh() 0 0 0; + line-height: lh(); span.relative-time { font-weight: normal; - } - - a { - color: $mit-red; + line-height: lh(); } } ul.tags { display: inline-block; + margin: lh() 0 0 0; + padding: 0; } } &.question-meta { float: right; - margin-top: 10px; - width: flex-grid(3.5,9); - + width: flex-grid(3,9); ul { - text-align: right; + @include clearfix; + margin: 0; + padding: 0; + list-style: none; li { - border: 1px solid #ddd; + border: 1px solid lighten($border-color, 10%); + @include box-sizing(border-box); @include box-shadow(0 1px 0 #fff); - display: inline-block; height:60px; - @include linear-gradient(#fff, #f5f5f5); - margin-right: 10px; - width: 60px; + float: left; + margin-right: flex-gutter(3); + width: flex-grid(1,3); &:last-child { margin-right: 0px; @@ -196,31 +200,22 @@ ul.question-list, div#question-list { } } - &.views { - } - &.answers { &.accepted { - - @include linear-gradient(#fff, lighten( #c4dfbe, 12% )); - border-color: #c4dfbe; + border-color: lighten($border-color, 10%); span, div { color: darken(#c4dfbe, 35%); } } + &.no-answers { - - span, div { - color: lighten($mit-red, 20%); + color: $pink; } } } - &.votes { - } - span, div { @include box-sizing(border-box); color: #888; diff --git a/lms/static/sass/course/discussion/_sidebar.scss b/lms/static/sass/course/discussion/_sidebar.scss index 5ff8ce2c55..59dcfaf449 100644 --- a/lms/static/sass/course/discussion/_sidebar.scss +++ b/lms/static/sass/course/discussion/_sidebar.scss @@ -2,25 +2,31 @@ div.discussion-wrapper aside { @extend .sidebar; - border-left: 1px solid #d3d3d3; - @include border-radius(0 4px 4px 0); - border-right: 1px solid #f6f6f6; - @include box-shadow(inset 1px 0 0 #f6f6f6); - padding: lh(); + border-left: 1px solid $border-color; + border-right: 0; width: flex-grid(3); + &:after { + left: -1px; + right: auto; + } + &.main-sidebar { min-width:200px; } h1 { @extend .bottom-border; - margin: (-(lh())) (-(lh())) 0; padding: lh(.5) lh(); + margin-bottom: em(16, 20); } h2 { - color: #4D4D4D; + color: #3C3C3C; + font-size: 1em; + font-style: normal; + font-weight: bold; + margin-bottom: 1em; &.first { margin-top: 0px; @@ -36,6 +42,9 @@ div.discussion-wrapper aside { input[type="submit"] { width: 27%; float: right; + text-align: center; + padding-left: 0; + padding-right: 0; } input[type="text"] { @@ -45,24 +54,30 @@ div.discussion-wrapper aside { div.box { display: block; - margin: lh(.5) 0; + padding: lh(.5) lh(); + border-top: 1px solid lighten($border-color, 10%); - &:last-child { - @include box-shadow(none); - border: 0; + &:first-child { + border-top: 0; } - h2 { - text-transform: uppercase; - font-weight: bold; - font-size: 14px; - letter-spacing: 1px; + ul#related-tags { + position: relative; + left: -10px; - &:not(.first) { - @include box-shadow(inset 0 1px 0 #eee); - border-top: 1px solid #d3d3d3; - margin: 0 (-(lh())) 0; - padding: lh(.5) lh(); + li { + border-bottom: 0; + background: #eee; + padding: 6px 10px 6px 5px; + + a { + padding: 0; + line-height: 12px; + + &:hover { + background: transparent; + } + } } } @@ -85,9 +100,6 @@ div.discussion-wrapper aside { } } - img.gravatar { - @include border-radius(3px); - } } &.tag-selector { @@ -100,17 +112,19 @@ div.discussion-wrapper aside { div.search-box { margin-top: lh(.5); + input { @include box-sizing(border-box); display: inline; } input[type='submit'] { - @include box-shadow(none); - opacity: 0.5; background: url(../images/askbot/search-icon.png) no-repeat center; border: 0; + @include box-shadow(none); margin-left: 3px; + opacity: 0.5; + padding: 6px 0 0; position: absolute; text-indent: -9999px; width: 24px; @@ -131,30 +145,26 @@ div.discussion-wrapper aside { } input#clear { - @include box-shadow(none); - @include border-radius(15px); + background: none; border: none; - background: #bbb; - color: #fff; + @include border-radius(0); + @include box-shadow(none); + color: #999; display: inline; - font-size: 10px; - margin-left: -25px; + font-size: 12px; + font-weight: bold; + height: 19px; + line-height: 1em; + margin: { + left: -25px; + top: 8px; + } padding: 2px 5px; + text-shadow: none; } } div#tagSelector { - h2 { - @include box-shadow(inset 0 1px 0 #eee); - border-top: 1px solid #d3d3d3; - margin: 0 (-(lh())) 0; - padding: lh(.5) lh(); - text-transform: uppercase; - font-weight: bold; - font-size: 14px; - letter-spacing: 1px; - } - ul { margin: 0; } @@ -167,11 +177,17 @@ div.discussion-wrapper aside { p.choice { @include inline-block(); margin-right: lh(.5); + margin-top: 0; } } + + label { + font-style: normal; + font-weight: 400; + } } - // Question view sopecific + // Question view specific div.follow-buttons { margin-top: 20px; @@ -187,12 +203,15 @@ div.discussion-wrapper aside { div.question-stats { + border-top: 0; + ul { color: #777; list-style: none; li { padding: 7px 0 0; + border: 0; &:last-child { @include box-shadow(none); @@ -216,19 +235,20 @@ div.discussion-wrapper aside { } div.karma { - background: #eee; - border: 1px solid #D3D3D3; - @include border-radius(3px); + border: 1px solid $border-color; @include box-sizing(border-box); - @include box-shadow(inset 0 0 0 1px #fff, 0 1px 0 #fff); padding: lh(.4) 0; text-align: center; width: flex-grid(1, 3); float: right; - strong { - display: block; - font-style: 20px; + p { + text-align: center; + + strong { + display: block; + font-style: 20px; + } } } @@ -255,8 +275,6 @@ div.discussion-wrapper aside { overflow: visible; ul { - font-size: 14px; - h2 { margin:0 (-(lh())) 5px (-(lh())); padding: lh(.5) lh(); @@ -265,40 +283,29 @@ div.discussion-wrapper aside { } div.question-tips, div.markdown { - ul { - margin-left: 8%; - } - + ul, ol { - margin-left: 8%; - } - } - div.markdown ul li { - margin: 20px 0; - - &:first-child { margin: 0; - } + padding: 0; - ol li { - margin: 0; + li { + border-bottom: 0; + line-height: lh(); + margin-bottom: em(8); + } } } div.view-profile { - h2 { - border-top: 0; - @include box-shadow(none); - } + border-top: 0; a { - width: 100%; - @include box-sizing(border-box); - text-align: center; - padding: 10px; - display: block; - margin-top: 10px; @extend .light-button; + @include box-sizing(border-box); + display: block; + text-align: center; + width: 100%; + margin-top: lh(.5); &:first-child { margin-top: 0; diff --git a/lms/static/sass/course/discussion/_tags.scss b/lms/static/sass/course/discussion/_tags.scss index a8d4d0f034..5cf78a4816 100644 --- a/lms/static/sass/course/discussion/_tags.scss +++ b/lms/static/sass/course/discussion/_tags.scss @@ -3,6 +3,7 @@ ul.tags { list-style: none; display: inline; + padding: 0; li, a { position: relative; @@ -10,19 +11,17 @@ ul.tags { li { background: #eee; - @include border-radius(4px); - @include box-shadow(0px 1px 0px #ccc); color: #555; display: inline-block; font-size: 12px; margin-bottom: 5px; margin-left: 15px; - padding: 3px 10px 5px 5px; + padding: 6px 10px 6px 5px; &:before { border-color:transparent #eee transparent transparent; border-style:solid; - border-width:12px 12px 12px 0; + border-width:12px 10px 12px 0; content:""; height:0; left:-10px; @@ -31,25 +30,6 @@ ul.tags { width:0; } - span.delete-icon, div.delete-icon { - background: #555; - @include border-radius(0 4px 4px 0); - clear: none; - color: #eee; - cursor: pointer; - display: inline; - float: none; - left: 10px; - opacity: 0.5; - padding: 4px 6px; - position: relative; - top: 1px; - - &:hover { - opacity: 1; - } - } - a { color: #555; text-decoration: none; @@ -61,11 +41,4 @@ ul.tags { span.tag-number { display: none; - // @include border-radius(3px); - // background: #555; - // font-size: 10px; - // margin: 0 3px; - // padding: 2px 5px; - // color: #eee; - // opacity: 0.5; } diff --git a/lms/static/sass/shared/_forms.scss b/lms/static/sass/shared/_forms.scss index 842ffb0086..d6a5f482e3 100644 --- a/lms/static/sass/shared/_forms.scss +++ b/lms/static/sass/shared/_forms.scss @@ -42,6 +42,7 @@ textarea { input[type="submit"], input[type="button"], +button, .button { @include border-radius(3px); @include button(shiny, $blue); diff --git a/lms/templates/course_navigation.html b/lms/templates/course_navigation.html index 5ca51b9039..55b0d50699 100644 --- a/lms/templates/course_navigation.html +++ b/lms/templates/course_navigation.html @@ -21,8 +21,10 @@ def url_class(url):
  • Course Info
  • % if user.is_authenticated(): % if settings.MITX_FEATURES.get('ENABLE_TEXTBOOK'): -
  • Textbook
  • -% endif + % for index, textbook in enumerate(course.textbooks): +
  • ${textbook.title}
  • + % endfor +% endif % if settings.MITX_FEATURES.get('ENABLE_DISCUSSION'):
  • Discussion
  • % endif @@ -33,7 +35,7 @@ def url_class(url): % if user.is_authenticated():
  • Profile
  • % endif -% if has_access(user, course, 'staff'): +% if staff_access:
  • Instructor
  • % endif diff --git a/lms/templates/problem.html b/lms/templates/problem.html index 6363274d24..ed49b3bd5d 100644 --- a/lms/templates/problem.html +++ b/lms/templates/problem.html @@ -1,7 +1,7 @@ <%namespace name='static' file='static_content.html'/>

    ${ problem['name'] } - % if problem['weight'] != 1: + % if problem['weight'] != 1 and problem['weight'] != None: : ${ problem['weight'] } points % endif

    diff --git a/lms/urls.py b/lms/urls.py index 14e0fa0658..89ef4babc4 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -126,9 +126,9 @@ if settings.COURSEWARE_ENABLED: #Inside the course url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/info$', 'courseware.views.course_info', name="info"), - url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/book$', + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/book/(?P[^/]*)/$', 'staticbook.views.index', name="book"), - url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/book/(?P[^/]*)$', + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/book/(?P[^/]*)/(?P[^/]*)$', 'staticbook.views.index'), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/book-shifted/(?P[^/]*)$', 'staticbook.views.index_shifted'),