diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 31be96ad7b..6d653db6cc 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -215,9 +215,6 @@ def preview_module_system(request, preview_id, descriptor): render_template=render_from_lms, debug=True, replace_urls=replace_urls, - # TODO (vshnayder): All CMS users get staff view by default - # is that what we want? - is_staff=True, ) diff --git a/cms/envs/common.py b/cms/envs/common.py index 6faecafec1..1767202141 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -90,6 +90,16 @@ TEMPLATE_CONTEXT_PROCESSORS = ( ################################# Jasmine ################################### JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' + +#################### CAPA External Code Evaluation ############################# +XQUEUE_INTERFACE = { + 'url': 'http://localhost:8888', + 'django_auth': {'username': 'local', + 'password': 'local'}, + 'basic_auth': None, +} + + ################################# Middleware ################################### # List of finder classes that know how to find static files in # various locations. diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index ea1770109b..b6aa62e03d 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -38,8 +38,8 @@ from xmodule.modulestore.exceptions import ItemNotFoundError from datetime import date from collections import namedtuple -from courseware.courses import (course_staff_group_name, has_staff_access_to_course, - get_courses_by_university) +from courseware.courses import get_courses_by_university +from courseware.access import has_access log = logging.getLogger("mitx.student") Article = namedtuple('Article', 'title url author image deck publication publish_date') @@ -166,22 +166,6 @@ def change_enrollment_view(request): """Delegate to change_enrollment to actually do the work.""" return HttpResponse(json.dumps(change_enrollment(request))) -def enrollment_allowed(user, course): - """If the course has an enrollment period, check whether we are in it. - Also respects the DARK_LAUNCH setting""" - now = time.gmtime() - start = course.enrollment_start - end = course.enrollment_end - - if (start is None or now > start) and (end is None or now < end): - # in enrollment period. - return True - - if settings.MITX_FEATURES['DARK_LAUNCH']: - if has_staff_access_to_course(user, course): - # if dark launch, staff can enroll outside enrollment window - return True - return False def change_enrollment(request): @@ -209,18 +193,7 @@ def change_enrollment(request): .format(user.username, enrollment.course_id)) return {'success': False, 'error': 'The course requested does not exist.'} - if settings.MITX_FEATURES.get('ACCESS_REQUIRE_STAFF_FOR_COURSE'): - # require that user be in the staff_* group (or be an - # overall admin) to be able to enroll eg staff_6.002x or - # staff_6.00x - if not has_staff_access_to_course(user, course): - staff_group = course_staff_group_name(course) - log.debug('user %s denied enrollment to %s ; not in %s' % ( - user, course.location.url(), staff_group)) - return {'success': False, - 'error' : '%s membership required to access course.' % staff_group} - - if not enrollment_allowed(user, course): + if not has_access(user, course, 'enroll'): return {'success': False, 'error': 'enrollment in {} not allowed at this time' .format(course.display_name)} diff --git a/common/lib/capa/capa/xqueue_interface.py b/common/lib/capa/capa/xqueue_interface.py index 70f086120e..2847968a89 100644 --- a/common/lib/capa/capa/xqueue_interface.py +++ b/common/lib/capa/capa/xqueue_interface.py @@ -7,13 +7,10 @@ import logging import requests import time -# TODO: Collection of parameters to be hooked into rest of edX system -XQUEUE_LMS_AUTH = { 'username': 'LMS', - 'password': 'PaloAltoCA' } -XQUEUE_URL = 'http://xqueue.edx.org' log = logging.getLogger('mitx.' + __name__) + def make_hashkey(seed=None): ''' Generate a string key by hashing @@ -58,15 +55,15 @@ def parse_xreply(xreply): return (return_code, content) -class XqueueInterface: +class XQueueInterface(object): ''' Interface to the external grading system ''' - def __init__(self, url=XQUEUE_URL, auth=XQUEUE_LMS_AUTH): + def __init__(self, url, django_auth, requests_auth=None): self.url = url - self.auth = auth - self.session = requests.session() + self.auth = django_auth + self.session = requests.session(auth=requests_auth) def send_to_queue(self, header, body, file_to_upload=None): ''' @@ -117,5 +114,3 @@ class XqueueInterface: return (1, 'unexpected HTTP status code [%d]' % r.status_code) return parse_xreply(r.text) - -qinterface = XqueueInterface() diff --git a/common/lib/xmodule/xmodule/abtest_module.py b/common/lib/xmodule/xmodule/abtest_module.py index 035413a402..ca00db4c9a 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 [self.system.get_module(child) - for child - in self.definition['data']['group_content'][self.group]] + return filter(None, [self.system.get_module(child) + for child + in self.definition['data']['group_content'][self.group]]) # TODO (cpennington): Use Groups should be a first class object, rather than being diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index c613283683..40eec1f70f 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -1,12 +1,12 @@ from fs.errors import ResourceNotFoundError import time -import dateutil.parser import logging from xmodule.util.decorators import lazyproperty from xmodule.graders import load_grading_policy from xmodule.modulestore import Location from xmodule.seq_module import SequenceDescriptor, SequenceModule +from xmodule.timeparse import parse_time, stringify_time log = logging.getLogger(__name__) @@ -18,38 +18,15 @@ class CourseDescriptor(SequenceDescriptor): super(CourseDescriptor, self).__init__(system, definition, **kwargs) msg = None - try: - self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M") - except KeyError: - msg = "Course loaded without a start date. id = %s" % self.id - except ValueError as e: - msg = "Course loaded with a bad start date. %s '%s'" % (self.id, e) - - # Don't call the tracker from the exception handler. - if msg is not None: - self.start = time.gmtime(0) # The epoch + if self.start is None: + msg = "Course loaded without a valid start date. id = %s" % self.id + # hack it -- start in 1970 + self.metadata['start'] = stringify_time(time.gmtime(0)) log.critical(msg) system.error_tracker(msg) - def try_parse_time(key): - """ - Parse an optional metadata key: if present, must be valid. - Return None if not present. - """ - if key in self.metadata: - try: - return time.strptime(self.metadata[key], "%Y-%m-%dT%H:%M") - except ValueError as e: - msg = "Course %s loaded with a bad metadata key %s '%s'" % ( - self.id, self.metadata[key], e) - log.warning(msg) - return None - - self.enrollment_start = try_parse_time("enrollment_start") - self.enrollment_end = try_parse_time("enrollment_end") - - - + self.enrollment_start = self._try_parse_time("enrollment_start") + self.enrollment_end = self._try_parse_time("enrollment_end") def has_started(self): return time.gmtime() > self.start @@ -154,6 +131,7 @@ class CourseDescriptor(SequenceDescriptor): @property def id(self): + """Return the course_id for this course""" return self.location_to_id(self.location) @property diff --git a/common/lib/xmodule/xmodule/error_module.py b/common/lib/xmodule/xmodule/error_module.py index 20301ee460..bdd7179a0a 100644 --- a/common/lib/xmodule/xmodule/error_module.py +++ b/common/lib/xmodule/xmodule/error_module.py @@ -24,16 +24,8 @@ class ErrorModule(XModule): return self.system.render_template('module-error.html', { 'data' : self.definition['data']['contents'], 'error' : self.definition['data']['error_msg'], - 'is_staff' : self.system.is_staff, }) - def displayable_items(self): - """Hide errors in the profile and table of contents for non-staff - users. - """ - if self.system.is_staff: - return [self] - return [] class ErrorDescriptor(EditingDescriptor): """ diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 6b39805d1a..c00b680eba 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -32,22 +32,37 @@ class @Problem queueing: => @queued_items = @$(".xqueue") - if @queued_items.length > 0 + @num_queued_items = @queued_items.length + if @num_queued_items > 0 if window.queuePollerID # Only one poller 'thread' per Problem window.clearTimeout(window.queuePollerID) - window.queuePollerID = window.setTimeout(@poll, 100) + queuelen = @get_queuelen() + window.queuePollerID = window.setTimeout(@poll, queuelen*10) + # Retrieves the minimum queue length of all queued items + get_queuelen: => + minlen = Infinity + @queued_items.each (index, qitem) -> + len = parseInt($.text(qitem)) + if len < minlen + minlen = len + return minlen + poll: => $.postWithPrefix "#{@url}/problem_get", (response) => - @queued_items = $(response.html).find(".xqueue") - if @queued_items.length == 0 + # If queueing status changed, then render + @new_queued_items = $(response.html).find(".xqueue") + if @new_queued_items.length isnt @num_queued_items @el.html(response.html) @executeProblemScripts () => @setupInputTypes() @bind() + + @num_queued_items = @new_queued_items.length + if @num_queued_items == 0 delete window.queuePollerID else - # TODO: Dynamically adjust timeout interval based on @queued_items.value + # TODO: Some logic to dynamically adjust polling rate based on queuelen window.queuePollerID = window.setTimeout(@poll, 1000) render: (content) -> @@ -141,9 +156,16 @@ class @Problem fd = new FormData() + # Sanity check of file size + file_too_large = false + max_filesize = 4*1000*1000 # 4 MB + @inputs.each (index, element) -> if element.type is 'file' if element.files[0] instanceof File + if element.files[0].size > max_filesize + file_too_large = true + alert 'Submission aborted! Your file "' + element.files[0].name + '" is too large (max size: ' + max_filesize/(1000*1000) + ' MB)' fd.append(element.id, element.files[0]) else fd.append(element.id, '') @@ -163,7 +185,8 @@ class @Problem else alert(response.success) - $.ajaxWithPrefix("#{@url}/problem_check", settings) + if not file_too_large + $.ajaxWithPrefix("#{@url}/problem_check", settings) check: => Logger.log 'problem_check', @answers diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index 7b2bd6bc2b..f08b0f8d6e 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -35,7 +35,6 @@ i4xs = ModuleSystem( filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))+"/test_files"), debug=True, xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue'}, - is_staff=False, node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules") ) @@ -336,7 +335,7 @@ class CodeResponseTest(unittest.TestCase): self.assertFalse(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be dequeued, message delivered else: self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be queued, message undelivered - + def test_convert_files_to_filenames(self): problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml" fp = open(problem_file) @@ -347,7 +346,7 @@ class CodeResponseTest(unittest.TestCase): self.assertEquals(answers_converted['1_2_1'], 'String-based answer') self.assertEquals(answers_converted['1_3_1'], ['answer1', 'answer2', 'answer3']) self.assertEquals(answers_converted['1_4_1'], fp.name) - + class ChoiceResponseTest(unittest.TestCase): diff --git a/common/lib/xmodule/xmodule/timeparse.py b/common/lib/xmodule/xmodule/timeparse.py new file mode 100644 index 0000000000..117105d085 --- /dev/null +++ b/common/lib/xmodule/xmodule/timeparse.py @@ -0,0 +1,19 @@ +""" +Helper functions for handling time in the format we like. +""" +import time + +TIME_FORMAT = "%Y-%m-%dT%H:%M" + +def parse_time(time_str): + """ + Takes a time string in TIME_FORMAT, returns + it as a time_struct. Raises ValueError if the string is not in the right format. + """ + return time.strptime(time_str, TIME_FORMAT) + +def stringify_time(time_struct): + """ + Convert a time struct to a string + """ + return time.strftime(TIME_FORMAT, time_struct) diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 071e453901..06449dc37f 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -8,8 +8,9 @@ from lxml import etree from lxml.etree import XMLSyntaxError from pprint import pprint -from xmodule.modulestore import Location from xmodule.errortracker import exc_info_to_str +from xmodule.modulestore import Location +from xmodule.timeparse import parse_time log = logging.getLogger('mitx.' + __name__) @@ -218,9 +219,11 @@ class XModule(HTMLSnippet): Return module instances for all the children of this module. ''' if self._loaded_children is None: - self._loaded_children = [ - self.system.get_module(child) - for child in self.definition.get('children', [])] + # 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', [])]) return self._loaded_children @@ -396,6 +399,15 @@ class XModuleDescriptor(Plugin, HTMLSnippet): return self.metadata.get('display_name', self.url_name.replace('_', ' ')) + @property + def start(self): + """ + If self.metadata contains start, return it. Else return None. + """ + if 'start' not in self.metadata: + return None + return self._try_parse_time('start') + @property def own_metadata(self): """ @@ -596,6 +608,24 @@ class XModuleDescriptor(Plugin, HTMLSnippet): metadata=self.metadata )) + # ================================ Internal helpers ======================= + + def _try_parse_time(self, key): + """ + Parse an optional metadata key containing a time: if present, complain + if it doesn't parse. + Return None if not present or invalid. + """ + if key in self.metadata: + try: + return parse_time(self.metadata[key]) + except ValueError as e: + msg = "Descriptor {} loaded with a bad metadata key '{}': '{}'".format( + self.location.url(), self.metadata[key], e) + log.warning(msg) + return None + + class DescriptorSystem(object): def __init__(self, load_item, resources_fs, error_tracker, **kwargs): @@ -675,7 +705,6 @@ class ModuleSystem(object): filestore=None, debug=False, xqueue=None, - is_staff=False, node_path=""): ''' Create a closure around the system environment. @@ -688,7 +717,8 @@ class ModuleSystem(object): files. Update or remove. get_module - function that takes (location) and returns a corresponding - module instance object. + module instance object. If the current user does not have + access to that location, returns None. render_template - a function that takes (template_file, context), and returns rendered html. @@ -705,9 +735,6 @@ class ModuleSystem(object): replace_urls - TEMPORARY - A function like static_replace.replace_urls that capa_module can use to fix up the static urls in ajax results. - - is_staff - Is the user making the request a staff user? - TODO (vshnayder): this will need to change once we have real user roles. ''' self.ajax_url = ajax_url self.xqueue = xqueue @@ -718,7 +745,6 @@ class ModuleSystem(object): self.DEBUG = self.debug = debug self.seed = user.id if user is not None else 0 self.replace_urls = replace_urls - self.is_staff = is_staff self.node_path = node_path def get(self, attr): diff --git a/doc/development.md b/doc/development.md index 44965cb0de..590a935405 100644 --- a/doc/development.md +++ b/doc/development.md @@ -65,3 +65,4 @@ To run a single nose test: nosetests common/lib/xmodule/xmodule/tests/test_stringify.py:test_stringify +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 diff --git a/lms/askbot/skins/mitx/media/images/email-sharing.png b/lms/askbot/skins/mitx/media/images/email-sharing.png new file mode 100644 index 0000000000..57fcee00e9 Binary files /dev/null and b/lms/askbot/skins/mitx/media/images/email-sharing.png differ diff --git a/lms/askbot/skins/mitx/media/images/facebook-sharing.png b/lms/askbot/skins/mitx/media/images/facebook-sharing.png new file mode 100644 index 0000000000..a82612c342 Binary files /dev/null and b/lms/askbot/skins/mitx/media/images/facebook-sharing.png differ diff --git a/lms/askbot/skins/mitx/media/images/google-plus-sharing.png b/lms/askbot/skins/mitx/media/images/google-plus-sharing.png new file mode 100644 index 0000000000..04f21d7860 Binary files /dev/null and b/lms/askbot/skins/mitx/media/images/google-plus-sharing.png differ diff --git a/lms/askbot/skins/mitx/media/images/lrg/email-sharing.png b/lms/askbot/skins/mitx/media/images/lrg/email-sharing.png new file mode 100644 index 0000000000..2e5e972c2b Binary files /dev/null and b/lms/askbot/skins/mitx/media/images/lrg/email-sharing.png differ diff --git a/lms/askbot/skins/mitx/media/images/lrg/facebook-sharing.png b/lms/askbot/skins/mitx/media/images/lrg/facebook-sharing.png new file mode 100644 index 0000000000..782e830680 Binary files /dev/null and b/lms/askbot/skins/mitx/media/images/lrg/facebook-sharing.png differ diff --git a/lms/askbot/skins/mitx/media/images/lrg/facebook.png b/lms/askbot/skins/mitx/media/images/lrg/facebook.png new file mode 100644 index 0000000000..f024b7c8ae Binary files /dev/null and b/lms/askbot/skins/mitx/media/images/lrg/facebook.png differ diff --git a/lms/askbot/skins/mitx/media/images/lrg/google-plus-sharing.png b/lms/askbot/skins/mitx/media/images/lrg/google-plus-sharing.png new file mode 100644 index 0000000000..ddf2abef83 Binary files /dev/null and b/lms/askbot/skins/mitx/media/images/lrg/google-plus-sharing.png differ diff --git a/lms/askbot/skins/mitx/media/images/lrg/linkedin.png b/lms/askbot/skins/mitx/media/images/lrg/linkedin.png new file mode 100644 index 0000000000..34261b05e5 Binary files /dev/null and b/lms/askbot/skins/mitx/media/images/lrg/linkedin.png differ diff --git a/lms/askbot/skins/mitx/media/images/lrg/twitter-sharing.png b/lms/askbot/skins/mitx/media/images/lrg/twitter-sharing.png new file mode 100644 index 0000000000..55b29fafc0 Binary files /dev/null and b/lms/askbot/skins/mitx/media/images/lrg/twitter-sharing.png differ diff --git a/lms/askbot/skins/mitx/media/images/lrg/twitter.png b/lms/askbot/skins/mitx/media/images/lrg/twitter.png new file mode 100644 index 0000000000..3d1856f834 Binary files /dev/null and b/lms/askbot/skins/mitx/media/images/lrg/twitter.png differ diff --git a/lms/askbot/skins/mitx/media/images/lrg/youtube-sharing.png b/lms/askbot/skins/mitx/media/images/lrg/youtube-sharing.png new file mode 100644 index 0000000000..111aa685a8 Binary files /dev/null and b/lms/askbot/skins/mitx/media/images/lrg/youtube-sharing.png differ diff --git a/lms/askbot/skins/mitx/media/images/social/email-sharing.png b/lms/askbot/skins/mitx/media/images/social/email-sharing.png new file mode 100644 index 0000000000..57fcee00e9 Binary files /dev/null and b/lms/askbot/skins/mitx/media/images/social/email-sharing.png differ diff --git a/lms/askbot/skins/mitx/media/images/social/facebook-sharing.png b/lms/askbot/skins/mitx/media/images/social/facebook-sharing.png new file mode 100644 index 0000000000..a82612c342 Binary files /dev/null and b/lms/askbot/skins/mitx/media/images/social/facebook-sharing.png differ diff --git a/lms/askbot/skins/mitx/media/images/social/google-plus-sharing.png b/lms/askbot/skins/mitx/media/images/social/google-plus-sharing.png new file mode 100644 index 0000000000..04f21d7860 Binary files /dev/null and b/lms/askbot/skins/mitx/media/images/social/google-plus-sharing.png differ diff --git a/lms/askbot/skins/mitx/media/images/social/twitter-sharing.png b/lms/askbot/skins/mitx/media/images/social/twitter-sharing.png new file mode 100644 index 0000000000..7d171d0fef Binary files /dev/null and b/lms/askbot/skins/mitx/media/images/social/twitter-sharing.png differ diff --git a/lms/askbot/skins/mitx/media/images/social/youtube-sharing.png b/lms/askbot/skins/mitx/media/images/social/youtube-sharing.png new file mode 100644 index 0000000000..a26b18121b Binary files /dev/null and b/lms/askbot/skins/mitx/media/images/social/youtube-sharing.png differ diff --git a/lms/askbot/skins/mitx/media/images/twitter-sharing.png b/lms/askbot/skins/mitx/media/images/twitter-sharing.png new file mode 100644 index 0000000000..7d171d0fef Binary files /dev/null and b/lms/askbot/skins/mitx/media/images/twitter-sharing.png differ diff --git a/lms/askbot/skins/mitx/media/images/youtube-sharing.png b/lms/askbot/skins/mitx/media/images/youtube-sharing.png new file mode 100644 index 0000000000..a26b18121b Binary files /dev/null and b/lms/askbot/skins/mitx/media/images/youtube-sharing.png differ diff --git a/lms/askbot/skins/mitx/templates/base.html b/lms/askbot/skins/mitx/templates/base.html index 18ca213cb7..a344009c60 100644 --- a/lms/askbot/skins/mitx/templates/base.html +++ b/lms/askbot/skins/mitx/templates/base.html @@ -3,7 +3,7 @@ {% spaceless %} - {% block title %}{% endblock %} - MITX 6.002 + {% block title %}{% endblock %} {% include "meta/html_head_meta.html" %} {% include "meta/html_head_stylesheets.html" %} diff --git a/lms/askbot/skins/mitx/templates/meta/bottom_scripts.html b/lms/askbot/skins/mitx/templates/meta/bottom_scripts.html index d9d5eb97dd..4e189f598c 100644 --- a/lms/askbot/skins/mitx/templates/meta/bottom_scripts.html +++ b/lms/askbot/skins/mitx/templates/meta/bottom_scripts.html @@ -33,8 +33,14 @@ +{# + +#} diff --git a/lms/askbot/skins/mitx/templates/navigation.jinja.html b/lms/askbot/skins/mitx/templates/navigation.jinja.html index 59c7148184..686ae3a724 100644 --- a/lms/askbot/skins/mitx/templates/navigation.jinja.html +++ b/lms/askbot/skins/mitx/templates/navigation.jinja.html @@ -1,25 +1,25 @@