Merge branch 'master' into feature/tomg/fall-design
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -26,3 +26,5 @@ Gemfile.lock
|
||||
lms/static/sass/*.css
|
||||
cms/static/sass/*.css
|
||||
lms/lib/comment_client/python
|
||||
nosetests.xml
|
||||
cover_html/
|
||||
|
||||
@@ -14,6 +14,10 @@ $yellow: #fff8af;
|
||||
$cream: #F6EFD4;
|
||||
$border-color: #ddd;
|
||||
|
||||
// edX colors
|
||||
$blue: rgb(29,157,217);
|
||||
$pink: rgb(182,37,104);
|
||||
|
||||
@mixin hide-text {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
|
||||
@@ -140,7 +140,20 @@ def dashboard(request):
|
||||
if not user.is_active:
|
||||
message = render_to_string('registration/activate_account_notice.html', {'email': user.email})
|
||||
|
||||
context = {'courses': courses, 'message': message}
|
||||
|
||||
# Global staff can see what courses errored on their dashboard
|
||||
staff_access = False
|
||||
errored_courses = {}
|
||||
if has_access(user, 'global', 'staff'):
|
||||
# Show any courses that errored on load
|
||||
staff_access = True
|
||||
errored_courses = modulestore().get_errored_courses()
|
||||
|
||||
context = {'courses': courses,
|
||||
'message': message,
|
||||
'staff_access': staff_access,
|
||||
'errored_courses': errored_courses,}
|
||||
|
||||
return render_to_response('dashboard.html', context)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pytz
|
||||
import datetime
|
||||
import dateutil.parser
|
||||
|
||||
@@ -84,15 +85,33 @@ def server_track(request, event_type, event, page=None):
|
||||
"time": datetime.datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
if event_type=="/event_logs" and request.user.is_staff: # don't log
|
||||
if event_type.startswith("/event_logs") and request.user.is_staff: # don't log
|
||||
return
|
||||
log_event(event)
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def view_tracking_log(request):
|
||||
def view_tracking_log(request,args=''):
|
||||
if not request.user.is_staff:
|
||||
return redirect('/')
|
||||
record_instances = TrackingLog.objects.all().order_by('-time')[0:100]
|
||||
nlen = 100
|
||||
username = ''
|
||||
if args:
|
||||
for arg in args.split('/'):
|
||||
if arg.isdigit():
|
||||
nlen = int(arg)
|
||||
if arg.startswith('username='):
|
||||
username = arg[9:]
|
||||
|
||||
record_instances = TrackingLog.objects.all().order_by('-time')
|
||||
if username:
|
||||
record_instances = record_instances.filter(username=username)
|
||||
record_instances = record_instances[0:nlen]
|
||||
|
||||
# fix dtstamp
|
||||
fmt = '%a %d-%b-%y %H:%M:%S' # "%Y-%m-%d %H:%M:%S %Z%z"
|
||||
for rinst in record_instances:
|
||||
rinst.dtstr = rinst.time.replace(tzinfo=pytz.utc).astimezone(pytz.timezone('US/Eastern')).strftime(fmt)
|
||||
|
||||
return render_to_response('tracking_log.html',{'records':record_instances})
|
||||
|
||||
|
||||
@@ -150,6 +150,7 @@ def optioninput(element, value, status, render_template, msg=''):
|
||||
'state': status,
|
||||
'msg': msg,
|
||||
'options': osetdict,
|
||||
'inline': element.get('inline',''),
|
||||
}
|
||||
|
||||
html = render_template("optioninput.html", context)
|
||||
@@ -206,7 +207,7 @@ def extract_choices(element):
|
||||
raise Exception("[courseware.capa.inputtypes.extract_choices] \
|
||||
Expected a <choice> tag; got %s instead"
|
||||
% choice.tag)
|
||||
choice_text = ''.join([x.text for x in choice])
|
||||
choice_text = ''.join([etree.tostring(x) for x in choice])
|
||||
|
||||
choices.append((choice.get("name"), choice_text))
|
||||
|
||||
@@ -294,7 +295,9 @@ def textline(element, value, status, render_template, msg=""):
|
||||
hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
escapedict = {'"': '"'}
|
||||
value = saxutils.escape(value, escapedict) # otherwise, answers with quotes in them crashes the system!
|
||||
context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, 'msg': msg, 'hidden': hidden}
|
||||
context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, 'msg': msg, 'hidden': hidden,
|
||||
'inline': element.get('inline',''),
|
||||
}
|
||||
html = render_template("textinput.html", context)
|
||||
try:
|
||||
xhtml = etree.XML(html)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<section id="textinput_${id}" class="textinput">
|
||||
<% doinline = "inline" if inline else "" %>
|
||||
|
||||
<section id="textinput_${id}" class="textinput ${doinline}" >
|
||||
% if state == 'unsubmitted':
|
||||
<div class="unanswered" id="status_${id}">
|
||||
<div class="unanswered ${doinline}" id="status_${id}">
|
||||
% elif state == 'correct':
|
||||
<div class="correct" id="status_${id}">
|
||||
<div class="correct ${doinline}" id="status_${id}">
|
||||
% elif state == 'incorrect':
|
||||
<div class="incorrect" id="status_${id}">
|
||||
<div class="incorrect ${doinline}" id="status_${id}">
|
||||
% elif state == 'incomplete':
|
||||
<div class="incorrect" id="status_${id}">
|
||||
<div class="incorrect ${doinline}" id="status_${id}">
|
||||
% endif
|
||||
% if hidden:
|
||||
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
|
||||
|
||||
@@ -18,7 +18,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
class Textbook:
|
||||
def __init__(self, title, book_url):
|
||||
self.title = title
|
||||
self.book_url = book_url
|
||||
self.book_url = book_url
|
||||
self.table_of_contents = self._get_toc_from_s3()
|
||||
|
||||
@classmethod
|
||||
@@ -30,11 +30,11 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
return self.table_of_contents
|
||||
|
||||
def _get_toc_from_s3(self):
|
||||
'''
|
||||
"""
|
||||
Accesses the textbook's table of contents (default name "toc.xml") at the URL self.book_url
|
||||
|
||||
Returns XML tree representation of the table of contents
|
||||
'''
|
||||
"""
|
||||
toc_url = self.book_url + 'toc.xml'
|
||||
|
||||
# Get the table of contents from S3
|
||||
@@ -72,6 +72,22 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
self.enrollment_start = self._try_parse_time("enrollment_start")
|
||||
self.enrollment_end = self._try_parse_time("enrollment_end")
|
||||
|
||||
# NOTE: relies on the modulestore to call set_grading_policy() right after
|
||||
# init. (Modulestore is in charge of figuring out where to load the policy from)
|
||||
|
||||
|
||||
def set_grading_policy(self, policy_str):
|
||||
"""Parse the policy specified in policy_str, and save it"""
|
||||
try:
|
||||
self._grading_policy = load_grading_policy(policy_str)
|
||||
except:
|
||||
self.system.error_tracker("Failed to load grading policy")
|
||||
# Setting this to an empty dictionary will lead to errors when
|
||||
# grading needs to happen, but should allow course staff to see
|
||||
# the error log.
|
||||
self._grading_policy = {}
|
||||
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
textbooks = []
|
||||
@@ -87,25 +103,11 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
|
||||
@property
|
||||
def grader(self):
|
||||
return self.__grading_policy['GRADER']
|
||||
return self._grading_policy['GRADER']
|
||||
|
||||
@property
|
||||
def grade_cutoffs(self):
|
||||
return self.__grading_policy['GRADE_CUTOFFS']
|
||||
|
||||
@lazyproperty
|
||||
def __grading_policy(self):
|
||||
policy_string = ""
|
||||
|
||||
try:
|
||||
with self.system.resources_fs.open("grading_policy.json") as grading_policy_file:
|
||||
policy_string = grading_policy_file.read()
|
||||
except (IOError, ResourceNotFoundError):
|
||||
log.warning("Unable to load course settings file from grading_policy.json in course " + self.id)
|
||||
|
||||
grading_policy = load_grading_policy(policy_string)
|
||||
|
||||
return grading_policy
|
||||
return self._grading_policy['GRADE_CUTOFFS']
|
||||
|
||||
@lazyproperty
|
||||
def grading_context(self):
|
||||
|
||||
@@ -27,6 +27,10 @@ section.problem {
|
||||
}
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
div {
|
||||
p {
|
||||
&.answer {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import abc
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
@@ -14,11 +15,11 @@ def load_grading_policy(course_policy_string):
|
||||
"""
|
||||
This loads a grading policy from a string (usually read from a file),
|
||||
which can be a JSON object or an empty string.
|
||||
|
||||
|
||||
The JSON object can have the keys GRADER and GRADE_CUTOFFS. If either is
|
||||
missing, it reverts to the default.
|
||||
"""
|
||||
|
||||
|
||||
default_policy_string = """
|
||||
{
|
||||
"GRADER" : [
|
||||
@@ -56,7 +57,7 @@ def load_grading_policy(course_policy_string):
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
# Load the global settings as a dictionary
|
||||
grading_policy = json.loads(default_policy_string)
|
||||
|
||||
@@ -64,15 +65,15 @@ def load_grading_policy(course_policy_string):
|
||||
course_policy = {}
|
||||
if course_policy_string:
|
||||
course_policy = json.loads(course_policy_string)
|
||||
|
||||
|
||||
# Override any global settings with the course settings
|
||||
grading_policy.update(course_policy)
|
||||
|
||||
# Here is where we should parse any configurations, so that we can fail early
|
||||
grading_policy['GRADER'] = grader_from_conf(grading_policy['GRADER'])
|
||||
|
||||
|
||||
return grading_policy
|
||||
|
||||
|
||||
|
||||
def aggregate_scores(scores, section_name="summary"):
|
||||
"""
|
||||
@@ -130,9 +131,11 @@ def grader_from_conf(conf):
|
||||
raise ValueError("Configuration has no appropriate grader class.")
|
||||
|
||||
except (TypeError, ValueError) as error:
|
||||
errorString = "Unable to parse grader configuration:\n " + str(subgraderconf) + "\n Error was:\n " + str(error)
|
||||
log.critical(errorString)
|
||||
raise ValueError(errorString)
|
||||
# Add info and re-raise
|
||||
msg = ("Unable to parse grader configuration:\n " +
|
||||
str(subgraderconf) +
|
||||
"\n Error was:\n " + str(error))
|
||||
raise ValueError(msg), None, sys.exc_info()[2]
|
||||
|
||||
return WeightedSubsectionsGrader(subgraders)
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
# online and has imported all current (fall 2012) courses from xml
|
||||
if not system.resources_fs.exists(filepath):
|
||||
candidates = cls.backcompat_paths(filepath)
|
||||
log.debug("candidates = {0}".format(candidates))
|
||||
#log.debug("candidates = {0}".format(candidates))
|
||||
for candidate in candidates:
|
||||
if system.resources_fs.exists(candidate):
|
||||
filepath = candidate
|
||||
|
||||
@@ -4,16 +4,17 @@ import os
|
||||
import re
|
||||
|
||||
from collections import defaultdict
|
||||
from cStringIO import StringIO
|
||||
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
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.mako_module import MakoDescriptorSystem
|
||||
from cStringIO import StringIO
|
||||
from xmodule.x_module import XModuleDescriptor, XMLParsingSystem
|
||||
|
||||
from . import ModuleStoreBase, Location
|
||||
from .exceptions import ItemNotFoundError
|
||||
@@ -134,12 +135,12 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
self.data_dir = path(data_dir)
|
||||
self.modules = defaultdict(dict) # course_id -> dict(location -> XModuleDescriptor)
|
||||
self.courses = {} # course_dir -> XModuleDescriptor for the course
|
||||
self.errored_courses = {} # course_dir -> errorlog, for dirs that failed to load
|
||||
|
||||
if default_class is None:
|
||||
self.default_class = None
|
||||
else:
|
||||
module_path, _, class_name = default_class.rpartition('.')
|
||||
#log.debug('module_path = %s' % module_path)
|
||||
class_ = getattr(import_module(module_path), class_name)
|
||||
self.default_class = class_
|
||||
|
||||
@@ -162,18 +163,27 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
'''
|
||||
Load a course, keeping track of errors as we go along.
|
||||
'''
|
||||
# Special-case code here, since we don't have a location for the
|
||||
# course before it loads.
|
||||
# So, make a tracker to track load-time errors, then put in the right
|
||||
# place after the course loads and we have its location
|
||||
errorlog = make_error_tracker()
|
||||
course_descriptor = None
|
||||
try:
|
||||
# Special-case code here, since we don't have a location for the
|
||||
# course before it loads.
|
||||
# So, make a tracker to track load-time errors, then put in the right
|
||||
# place after the course loads and we have its location
|
||||
errorlog = make_error_tracker()
|
||||
course_descriptor = self.load_course(course_dir, errorlog.tracker)
|
||||
except Exception as e:
|
||||
msg = "Failed to load course '{0}': {1}".format(course_dir, str(e))
|
||||
log.exception(msg)
|
||||
errorlog.tracker(msg)
|
||||
|
||||
if course_descriptor is not None:
|
||||
self.courses[course_dir] = course_descriptor
|
||||
self._location_errors[course_descriptor.location] = errorlog
|
||||
except:
|
||||
msg = "Failed to load course '%s'" % course_dir
|
||||
log.exception(msg)
|
||||
else:
|
||||
# Didn't load course. Instead, save the errors elsewhere.
|
||||
self.errored_courses[course_dir] = errorlog
|
||||
|
||||
|
||||
|
||||
def __unicode__(self):
|
||||
'''
|
||||
@@ -202,6 +212,28 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
return {}
|
||||
|
||||
|
||||
def read_grading_policy(self, paths, tracker):
|
||||
"""Load a grading policy from the specified paths, in order, if it exists."""
|
||||
# Default to a blank policy
|
||||
policy_str = ""
|
||||
|
||||
for policy_path in paths:
|
||||
if not os.path.exists(policy_path):
|
||||
continue
|
||||
log.debug("Loading grading policy from {0}".format(policy_path))
|
||||
try:
|
||||
with open(policy_path) as grading_policy_file:
|
||||
policy_str = grading_policy_file.read()
|
||||
# if we successfully read the file, stop looking at backups
|
||||
break
|
||||
except (IOError):
|
||||
msg = "Unable to load course settings file from '{0}'".format(policy_path)
|
||||
tracker(msg)
|
||||
log.warning(msg)
|
||||
|
||||
return policy_str
|
||||
|
||||
|
||||
def load_course(self, course_dir, tracker):
|
||||
"""
|
||||
Load a course into this module store
|
||||
@@ -242,9 +274,16 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
course = course_dir
|
||||
|
||||
url_name = course_data.get('url_name', course_data.get('slug'))
|
||||
policy_dir = None
|
||||
if url_name:
|
||||
policy_path = self.data_dir / course_dir / 'policies' / '{0}.json'.format(url_name)
|
||||
policy_dir = self.data_dir / course_dir / 'policies' / url_name
|
||||
policy_path = policy_dir / 'policy.json'
|
||||
policy = self.load_policy(policy_path, tracker)
|
||||
|
||||
# VS[compat]: remove once courses use the policy dirs.
|
||||
if policy == {}:
|
||||
old_policy_path = self.data_dir / course_dir / 'policies' / '{0}.json'.format(url_name)
|
||||
policy = self.load_policy(old_policy_path, tracker)
|
||||
else:
|
||||
policy = {}
|
||||
# VS[compat] : 'name' is deprecated, but support it for now...
|
||||
@@ -268,6 +307,15 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
# after we have the course descriptor.
|
||||
XModuleDescriptor.compute_inherited_metadata(course_descriptor)
|
||||
|
||||
# Try to load grading policy
|
||||
paths = [self.data_dir / course_dir / 'grading_policy.json']
|
||||
if policy_dir:
|
||||
paths = [policy_dir / 'grading_policy.json'] + paths
|
||||
|
||||
policy_str = self.read_grading_policy(paths, tracker)
|
||||
course_descriptor.set_grading_policy(policy_str)
|
||||
|
||||
|
||||
log.debug('========> Done with course import from {0}'.format(course_dir))
|
||||
return course_descriptor
|
||||
|
||||
@@ -318,6 +366,12 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
"""
|
||||
return self.courses.values()
|
||||
|
||||
def get_errored_courses(self):
|
||||
"""
|
||||
Return a dictionary of course_dir -> [(msg, exception_str)], for each
|
||||
course_dir where course loading failed.
|
||||
"""
|
||||
return dict( (k, self.errored_courses[k].errors) for k in self.errored_courses)
|
||||
|
||||
def create_item(self, location):
|
||||
raise NotImplementedError("XMLModuleStores are read-only")
|
||||
|
||||
@@ -233,6 +233,9 @@ class ImportTestCase(unittest.TestCase):
|
||||
self.assertEqual(toy_ch.display_name, "Overview")
|
||||
self.assertEqual(two_toys_ch.display_name, "Two Toy Overview")
|
||||
|
||||
# Also check that the grading policy loaded
|
||||
self.assertEqual(two_toys.grade_cutoffs['C'], 0.5999)
|
||||
|
||||
|
||||
def test_definition_loading(self):
|
||||
"""When two courses share the same org and course name and
|
||||
|
||||
@@ -38,7 +38,7 @@ def get_metadata_from_xml(xml_object, remove=True):
|
||||
if meta is None:
|
||||
return ''
|
||||
dmdata = meta.text
|
||||
log.debug('meta for %s loaded: %s' % (xml_object,dmdata))
|
||||
#log.debug('meta for %s loaded: %s' % (xml_object,dmdata))
|
||||
if remove:
|
||||
xml_object.remove(meta)
|
||||
return dmdata
|
||||
|
||||
1
common/test/data/test_start_date/README.md
Normal file
1
common/test/data/test_start_date/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Simple course. If start dates are on, non-staff users should see Overview, but not Ch 2.
|
||||
1
common/test/data/test_start_date/course.xml
Symbolic link
1
common/test/data/test_start_date/course.xml
Symbolic link
@@ -0,0 +1 @@
|
||||
roots/2012_Fall.xml
|
||||
15
common/test/data/test_start_date/course/2012_Fall.xml
Normal file
15
common/test/data/test_start_date/course/2012_Fall.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<course>
|
||||
<chapter url_name="Overview">
|
||||
<videosequence url_name="Toy_Videos">
|
||||
<html url_name="toylab"/>
|
||||
<video url_name="Video_Resources" youtube="1.0:1bK-WdDi6Qw"/>
|
||||
</videosequence>
|
||||
<video url_name="Welcome" youtube="1.0:p2Q6BrNhdh8"/>
|
||||
</chapter>
|
||||
<chapter url_name="Ch2">
|
||||
<html url_name="test_html">
|
||||
<h2>Welcome</h2>
|
||||
</html>
|
||||
</chapter>
|
||||
|
||||
</course>
|
||||
3
common/test/data/test_start_date/html/toylab.html
Normal file
3
common/test/data/test_start_date/html/toylab.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<b>Lab 2A: Superposition Experiment</b>
|
||||
|
||||
<p>Isn't the toy course great?</p>
|
||||
1
common/test/data/test_start_date/html/toylab.xml
Normal file
1
common/test/data/test_start_date/html/toylab.xml
Normal file
@@ -0,0 +1 @@
|
||||
<html filename="toylab.html"/>
|
||||
27
common/test/data/test_start_date/policies/2012_Fall.json
Normal file
27
common/test/data/test_start_date/policies/2012_Fall.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"course/2012_Fall": {
|
||||
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
|
||||
"start": "2011-07-17T12:00",
|
||||
"display_name": "Toy Course"
|
||||
},
|
||||
"chapter/Overview": {
|
||||
"display_name": "Overview"
|
||||
},
|
||||
"chapter/Ch2": {
|
||||
"display_name": "Chapter 2",
|
||||
"start": "2015-07-17T12:00"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
1
common/test/data/test_start_date/roots/2012_Fall.xml
Normal file
1
common/test/data/test_start_date/roots/2012_Fall.xml
Normal file
@@ -0,0 +1 @@
|
||||
<course org="edX" course="test_start_date" url_name="2012_Fall"/>
|
||||
1
common/test/data/toy/html/toylab.xml
Normal file
1
common/test/data/toy/html/toylab.xml
Normal file
@@ -0,0 +1 @@
|
||||
<html filename="toylab.html"/>
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"GRADER" : [
|
||||
{
|
||||
"type" : "Homework",
|
||||
"min_count" : 12,
|
||||
"drop_count" : 2,
|
||||
"short_label" : "HW",
|
||||
"weight" : 0.15
|
||||
},
|
||||
{
|
||||
"type" : "Lab",
|
||||
"min_count" : 12,
|
||||
"drop_count" : 2,
|
||||
"category" : "Labs",
|
||||
"weight" : 0.15
|
||||
},
|
||||
{
|
||||
"type" : "Midterm",
|
||||
"name" : "Midterm Exam",
|
||||
"short_label" : "Midterm",
|
||||
"weight" : 0.3
|
||||
},
|
||||
{
|
||||
"type" : "Final",
|
||||
"name" : "Final Exam",
|
||||
"short_label" : "Final",
|
||||
"weight" : 0.4
|
||||
}
|
||||
],
|
||||
"GRADE_CUTOFFS" : {
|
||||
"A" : 0.87,
|
||||
"B" : 0.7,
|
||||
"C" : 0.5999
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,9 @@ def has_access(user, obj, action):
|
||||
if isinstance(obj, Location):
|
||||
return _has_access_location(user, obj, action)
|
||||
|
||||
if isinstance(obj, basestring):
|
||||
return _has_access_string(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(): '{0}'"
|
||||
@@ -238,6 +241,30 @@ def _has_access_location(user, location, action):
|
||||
return _dispatch(checkers, action, user, location)
|
||||
|
||||
|
||||
def _has_access_string(user, perm, action):
|
||||
"""
|
||||
Check if user has certain special access, specified as string. Valid strings:
|
||||
|
||||
'global'
|
||||
|
||||
Valid actions:
|
||||
|
||||
'staff' -- global staff access.
|
||||
"""
|
||||
|
||||
def check_staff():
|
||||
if perm != 'global':
|
||||
debug("Deny: invalid permission '%s'", perm)
|
||||
return False
|
||||
return _has_global_staff_access(user)
|
||||
|
||||
checkers = {
|
||||
'staff': check_staff
|
||||
}
|
||||
|
||||
return _dispatch(checkers, action, user, perm)
|
||||
|
||||
|
||||
##### Internal helper methods below
|
||||
|
||||
def _dispatch(table, action, user, obj):
|
||||
@@ -266,6 +293,15 @@ def _course_staff_group_name(location):
|
||||
"""
|
||||
return 'staff_%s' % Location(location).course
|
||||
|
||||
def _has_global_staff_access(user):
|
||||
if user.is_staff:
|
||||
debug("Allow: user.is_staff")
|
||||
return True
|
||||
else:
|
||||
debug("Deny: not user.is_staff")
|
||||
return False
|
||||
|
||||
|
||||
def _has_staff_access_to_location(user, location):
|
||||
'''
|
||||
Returns True if the given user has staff access to a location. For now this
|
||||
|
||||
@@ -30,7 +30,6 @@ def get_course_by_id(course_id):
|
||||
raise Http404("Course not found.")
|
||||
|
||||
|
||||
|
||||
def get_course_with_access(user, course_id, action):
|
||||
"""
|
||||
Given a course_id, look up the corresponding course descriptor,
|
||||
@@ -142,6 +141,35 @@ def get_course_info_section(course, section_key):
|
||||
|
||||
raise KeyError("Invalid about key " + str(section_key))
|
||||
|
||||
# TODO: Fix this such that these are pulled in as extra course-specific tabs.
|
||||
# arjun will address this by the end of October if no one does so prior to
|
||||
# then.
|
||||
def get_course_syllabus_section(course, section_key):
|
||||
"""
|
||||
This returns the snippet of html to be rendered on the syllabus page,
|
||||
given the key for the section.
|
||||
|
||||
Valid keys:
|
||||
- syllabus
|
||||
- guest_syllabus
|
||||
"""
|
||||
|
||||
# Many of these are stored as html files instead of some semantic
|
||||
# markup. This can change without effecting this interface when we find a
|
||||
# good format for defining so many snippets of text/html.
|
||||
|
||||
if section_key in ['syllabus', 'guest_syllabus']:
|
||||
try:
|
||||
with course.system.resources_fs.open(path("syllabus") / section_key + ".html") as htmlFile:
|
||||
return replace_urls(htmlFile.read().decode('utf-8'),
|
||||
course.metadata['data_dir'])
|
||||
except ResourceNotFoundError:
|
||||
log.exception("Missing syllabus section {key} in course {url}".format(
|
||||
key=section_key, url=course.location.url()))
|
||||
return "! Syllabus missing !"
|
||||
|
||||
raise KeyError("Invalid about key " + str(section_key))
|
||||
|
||||
|
||||
def get_courses_by_university(user, domain=None):
|
||||
'''
|
||||
|
||||
@@ -29,6 +29,8 @@ def grade(student, request, course, student_module_cache=None):
|
||||
output from the course grader, augmented with the final letter
|
||||
grade. The keys in the output are:
|
||||
|
||||
course: a CourseDescriptor
|
||||
|
||||
- grade : A final letter grade.
|
||||
- percent : The final percent for the class (rounded up).
|
||||
- section_breakdown : A breakdown of each section that makes
|
||||
@@ -42,7 +44,7 @@ def grade(student, request, course, student_module_cache=None):
|
||||
grading_context = course.grading_context
|
||||
|
||||
if student_module_cache == None:
|
||||
student_module_cache = StudentModuleCache(student, grading_context['all_descriptors'])
|
||||
student_module_cache = StudentModuleCache(course.id, student, grading_context['all_descriptors'])
|
||||
|
||||
totaled_scores = {}
|
||||
# This next complicated loop is just to collect the totaled_scores, which is
|
||||
@@ -56,7 +58,8 @@ def grade(student, request, course, student_module_cache=None):
|
||||
should_grade_section = False
|
||||
# If we haven't seen a single problem in the section, we don't have to grade it at all! We can assume 0%
|
||||
for moduledescriptor in section['xmoduledescriptors']:
|
||||
if student_module_cache.lookup(moduledescriptor.category, moduledescriptor.location.url() ):
|
||||
if student_module_cache.lookup(
|
||||
course.id, moduledescriptor.category, moduledescriptor.location.url()):
|
||||
should_grade_section = True
|
||||
break
|
||||
|
||||
@@ -64,10 +67,9 @@ def grade(student, request, course, student_module_cache=None):
|
||||
scores = []
|
||||
# TODO: We need the request to pass into here. If we could forgo that, our arguments
|
||||
# would be simpler
|
||||
course_id = CourseDescriptor.location_to_id(course.location)
|
||||
section_module = get_module(student, request,
|
||||
section_descriptor.location, student_module_cache,
|
||||
course_id)
|
||||
course.id)
|
||||
if section_module is None:
|
||||
# student doesn't have access to this module, or something else
|
||||
# went wrong.
|
||||
@@ -76,7 +78,7 @@ def grade(student, request, course, student_module_cache=None):
|
||||
# TODO: We may be able to speed this up by only getting a list of children IDs from section_module
|
||||
# Then, we may not need to instatiate any problems if they are already in the database
|
||||
for module in yield_module_descendents(section_module):
|
||||
(correct, total) = get_score(student, module, student_module_cache)
|
||||
(correct, total) = get_score(course.id, student, module, student_module_cache)
|
||||
if correct is None and total is None:
|
||||
continue
|
||||
|
||||
@@ -171,7 +173,9 @@ def progress_summary(student, course, grader, student_module_cache):
|
||||
graded = s.metadata.get('graded', False)
|
||||
scores = []
|
||||
for module in yield_module_descendents(s):
|
||||
(correct, total) = get_score(student, module, student_module_cache)
|
||||
# course is a module, not a descriptor...
|
||||
course_id = course.descriptor.id
|
||||
(correct, total) = get_score(course_id, student, module, student_module_cache)
|
||||
if correct is None and total is None:
|
||||
continue
|
||||
|
||||
@@ -200,7 +204,7 @@ def progress_summary(student, course, grader, student_module_cache):
|
||||
return chapters
|
||||
|
||||
|
||||
def get_score(user, problem, student_module_cache):
|
||||
def get_score(course_id, user, problem, student_module_cache):
|
||||
"""
|
||||
Return the score for a user on a problem, as a tuple (correct, total).
|
||||
|
||||
@@ -215,10 +219,11 @@ def get_score(user, problem, student_module_cache):
|
||||
correct = 0.0
|
||||
|
||||
# If the ID is not in the cache, add the item
|
||||
instance_module = get_instance_module(user, problem, student_module_cache)
|
||||
instance_module = get_instance_module(course_id, user, problem, student_module_cache)
|
||||
# instance_module = student_module_cache.lookup(problem.category, problem.id)
|
||||
# if instance_module is None:
|
||||
# instance_module = StudentModule(module_type=problem.category,
|
||||
# course_id=????,
|
||||
# module_state_key=problem.id,
|
||||
# student=user,
|
||||
# state=None,
|
||||
|
||||
@@ -84,6 +84,7 @@ class Command(BaseCommand):
|
||||
# TODO (cpennington): Get coursename in a legitimate way
|
||||
course_location = 'i4x://edx/6002xs12/course/6.002_Spring_2012'
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
||||
course_id,
|
||||
sample_user, modulestore().get_item(course_location))
|
||||
course = get_module(sample_user, None, course_location, student_module_cache)
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
# NOTE (vshnayder): This constraint has the wrong field order, so it doesn't actually
|
||||
# do anything in sqlite. Migration 0004 actually removes this index for sqlite.
|
||||
# Removing unique constraint on 'StudentModule', fields ['module_id', 'module_type', 'student']
|
||||
db.delete_unique('courseware_studentmodule', ['module_id', 'module_type', 'student_id'])
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding field 'StudentModule.course_id'
|
||||
db.add_column('courseware_studentmodule', 'course_id',
|
||||
self.gf('django.db.models.fields.CharField')(default="", max_length=255, db_index=True),
|
||||
keep_default=False)
|
||||
|
||||
# Removing unique constraint on 'StudentModule', fields ['module_id', 'student']
|
||||
db.delete_unique('courseware_studentmodule', ['module_id', 'student_id'])
|
||||
|
||||
# NOTE: manually remove this constaint (from 0001)--0003 tries, but fails for sqlite.
|
||||
# Removing unique constraint on 'StudentModule', fields ['module_id', 'module_type', 'student']
|
||||
if db.backend_name == "sqlite3":
|
||||
db.delete_unique('courseware_studentmodule', ['student_id', 'module_id', 'module_type'])
|
||||
|
||||
# Adding unique constraint on 'StudentModule', fields ['course_id', 'module_state_key', 'student']
|
||||
db.create_unique('courseware_studentmodule', ['student_id', 'module_id', 'course_id'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing unique constraint on 'StudentModule', fields ['studnet_id', 'module_state_key', 'course_id']
|
||||
db.delete_unique('courseware_studentmodule', ['student_id', 'module_id', 'course_id'])
|
||||
|
||||
# Deleting field 'StudentModule.course_id'
|
||||
db.delete_column('courseware_studentmodule', 'course_id')
|
||||
|
||||
# Adding unique constraint on 'StudentModule', fields ['module_id', 'student']
|
||||
db.create_unique('courseware_studentmodule', ['module_id', 'student_id'])
|
||||
|
||||
# Adding unique constraint on 'StudentModule', fields ['module_id', 'module_type', 'student']
|
||||
db.create_unique('courseware_studentmodule', ['student_id', 'module_id', 'module_type'])
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
|
||||
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
|
||||
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
|
||||
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
|
||||
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
|
||||
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'courseware.studentmodule': {
|
||||
'Meta': {'unique_together': "(('course_id', 'student', 'module_state_key'),)", 'object_name': 'StudentModule'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'done': ('django.db.models.fields.CharField', [], {'default': "'na'", 'max_length': '8', 'db_index': 'True'}),
|
||||
'grade': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'max_grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'module_state_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'module_id'", 'db_index': 'True'}),
|
||||
'module_type': ('django.db.models.fields.CharField', [], {'default': "'problem'", 'max_length': '32', 'db_index': 'True'}),
|
||||
'state': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['courseware']
|
||||
@@ -22,6 +22,9 @@ from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class StudentModule(models.Model):
|
||||
"""
|
||||
Keeps student state for a particular module in a particular course.
|
||||
"""
|
||||
# For a homework problem, contains a JSON
|
||||
# object consisting of state
|
||||
MODULE_TYPES = (('problem', 'problem'),
|
||||
@@ -37,9 +40,10 @@ class StudentModule(models.Model):
|
||||
# Filename for homeworks, etc.
|
||||
module_state_key = models.CharField(max_length=255, db_index=True, db_column='module_id')
|
||||
student = models.ForeignKey(User, db_index=True)
|
||||
course_id = models.CharField(max_length=255, db_index=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = (('student', 'module_state_key'),)
|
||||
unique_together = (('student', 'module_state_key', 'course_id'),)
|
||||
|
||||
## Internal state of the object
|
||||
state = models.TextField(null=True, blank=True)
|
||||
@@ -57,7 +61,8 @@ class StudentModule(models.Model):
|
||||
modified = models.DateTimeField(auto_now=True, db_index=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return '/'.join([self.module_type, self.student.username, self.module_state_key, str(self.state)[:20]])
|
||||
return '/'.join([self.course_id, self.module_type,
|
||||
self.student.username, self.module_state_key, str(self.state)[:20]])
|
||||
|
||||
|
||||
# TODO (cpennington): Remove these once the LMS switches to using XModuleDescriptors
|
||||
@@ -67,20 +72,20 @@ class StudentModuleCache(object):
|
||||
"""
|
||||
A cache of StudentModules for a specific student
|
||||
"""
|
||||
def __init__(self, user, descriptors, select_for_update=False):
|
||||
def __init__(self, course_id, user, descriptors, select_for_update=False):
|
||||
'''
|
||||
Find any StudentModule objects that are needed by any descriptor
|
||||
in descriptors. Avoids making multiple queries to the database.
|
||||
Note: Only modules that have store_state = True or have shared
|
||||
state will have a StudentModule.
|
||||
|
||||
|
||||
Arguments
|
||||
user: The user for which to fetch maching StudentModules
|
||||
descriptors: An array of XModuleDescriptors.
|
||||
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)
|
||||
module_ids = self._get_module_state_keys(descriptors)
|
||||
|
||||
# This works around a limitation in sqlite3 on the number of parameters
|
||||
# that can be put into a single query
|
||||
@@ -89,78 +94,86 @@ class StudentModuleCache(object):
|
||||
for id_chunk in [module_ids[i:i + chunk_size] for i in xrange(0, len(module_ids), chunk_size)]:
|
||||
if select_for_update:
|
||||
self.cache.extend(StudentModule.objects.select_for_update().filter(
|
||||
course_id=course_id,
|
||||
student=user,
|
||||
module_state_key__in=id_chunk)
|
||||
)
|
||||
else:
|
||||
self.cache.extend(StudentModule.objects.filter(
|
||||
course_id=course_id,
|
||||
student=user,
|
||||
module_state_key__in=id_chunk)
|
||||
)
|
||||
|
||||
else:
|
||||
self.cache = []
|
||||
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def cache_for_descriptor_descendents(cls, user, descriptor, depth=None, descriptor_filter=lambda descriptor: True, select_for_update=False):
|
||||
def cache_for_descriptor_descendents(cls, course_id, user, descriptor, depth=None,
|
||||
descriptor_filter=lambda descriptor: True,
|
||||
select_for_update=False):
|
||||
"""
|
||||
course_id: the course in the context of which we want StudentModules.
|
||||
user: the django user for whom to load modules.
|
||||
descriptor: An XModuleDescriptor
|
||||
depth is the number of levels of descendent modules to load StudentModules for, in addition to
|
||||
the supplied descriptor. If depth is None, load all descendent StudentModules
|
||||
descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
|
||||
descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
|
||||
should be cached
|
||||
select_for_update: Flag indicating whether the rows should be locked until end of transaction
|
||||
"""
|
||||
|
||||
|
||||
def get_child_descriptors(descriptor, depth, descriptor_filter):
|
||||
if descriptor_filter(descriptor):
|
||||
descriptors = [descriptor]
|
||||
else:
|
||||
descriptors = []
|
||||
|
||||
|
||||
if depth is None or depth > 0:
|
||||
new_depth = depth - 1 if depth is not None else depth
|
||||
|
||||
for child in descriptor.get_children():
|
||||
descriptors.extend(get_child_descriptors(child, new_depth, descriptor_filter))
|
||||
|
||||
|
||||
return descriptors
|
||||
|
||||
|
||||
|
||||
|
||||
descriptors = get_child_descriptors(descriptor, depth, descriptor_filter)
|
||||
|
||||
return StudentModuleCache(user, descriptors, select_for_update)
|
||||
|
||||
|
||||
return StudentModuleCache(course_id, user, descriptors, select_for_update)
|
||||
|
||||
def _get_module_state_keys(self, descriptors):
|
||||
'''
|
||||
Get a list of the state_keys needed for StudentModules
|
||||
required for this module descriptor
|
||||
|
||||
descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
|
||||
|
||||
descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
|
||||
should be cached
|
||||
'''
|
||||
keys = []
|
||||
for descriptor in descriptors:
|
||||
if descriptor.stores_state:
|
||||
keys.append(descriptor.location.url())
|
||||
|
||||
|
||||
shared_state_key = getattr(descriptor, 'shared_state_key', None)
|
||||
if shared_state_key is not None:
|
||||
keys.append(shared_state_key)
|
||||
|
||||
return keys
|
||||
|
||||
def lookup(self, module_type, module_state_key):
|
||||
def lookup(self, course_id, module_type, module_state_key):
|
||||
'''
|
||||
Look for a student module with the given type and id in the cache.
|
||||
Look for a student module with the given course_id, type, and id in the cache.
|
||||
|
||||
cache -- list of student modules
|
||||
|
||||
returns first found object, or None
|
||||
'''
|
||||
for o in self.cache:
|
||||
if o.module_type == module_type and o.module_state_key == module_state_key:
|
||||
if (o.course_id == course_id and
|
||||
o.module_type == module_type and
|
||||
o.module_state_key == module_state_key):
|
||||
return o
|
||||
return None
|
||||
|
||||
|
||||
@@ -70,7 +70,8 @@ def toc_for_course(user, request, course, active_chapter, active_section, course
|
||||
None if this is not the case.
|
||||
'''
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, course, depth=2)
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
||||
course_id, user, course, depth=2)
|
||||
course = get_module(user, request, course.location, student_module_cache, course_id)
|
||||
|
||||
chapters = list()
|
||||
@@ -159,12 +160,13 @@ def get_module(user, request, location, student_module_cache, course_id, positio
|
||||
shared_module = None
|
||||
if user.is_authenticated():
|
||||
if descriptor.stores_state:
|
||||
instance_module = student_module_cache.lookup(descriptor.category,
|
||||
descriptor.location.url())
|
||||
instance_module = student_module_cache.lookup(
|
||||
course_id, descriptor.category, descriptor.location.url())
|
||||
|
||||
shared_state_key = getattr(descriptor, 'shared_state_key', None)
|
||||
if shared_state_key is not None:
|
||||
shared_module = student_module_cache.lookup(descriptor.category,
|
||||
shared_module = student_module_cache.lookup(course_id,
|
||||
descriptor.category,
|
||||
shared_state_key)
|
||||
|
||||
|
||||
@@ -241,7 +243,7 @@ def get_module(user, request, location, student_module_cache, course_id, positio
|
||||
|
||||
return module
|
||||
|
||||
def get_instance_module(user, module, student_module_cache):
|
||||
def get_instance_module(course_id, user, module, student_module_cache):
|
||||
"""
|
||||
Returns instance_module is a StudentModule specific to this module for this student,
|
||||
or None if this is an anonymous user
|
||||
@@ -252,11 +254,12 @@ def get_instance_module(user, module, student_module_cache):
|
||||
+ str(module.id) + " which does not store state.")
|
||||
return None
|
||||
|
||||
instance_module = student_module_cache.lookup(module.category,
|
||||
module.location.url())
|
||||
instance_module = student_module_cache.lookup(
|
||||
course_id, module.category, module.location.url())
|
||||
|
||||
if not instance_module:
|
||||
instance_module = StudentModule(
|
||||
course_id=course_id,
|
||||
student=user,
|
||||
module_type=module.category,
|
||||
module_state_key=module.id,
|
||||
@@ -285,6 +288,7 @@ def get_shared_instance_module(course_id, user, module, student_module_cache):
|
||||
shared_state_key)
|
||||
if not shared_module:
|
||||
shared_module = StudentModule(
|
||||
course_id=course_id,
|
||||
student=user,
|
||||
module_type=descriptor.category,
|
||||
module_state_key=shared_state_key,
|
||||
@@ -317,14 +321,14 @@ def xqueue_callback(request, course_id, userid, id, dispatch):
|
||||
# Retrieve target StudentModule
|
||||
user = User.objects.get(id=userid)
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(course_id,
|
||||
user, modulestore().get_instance(course_id, id), depth=0, select_for_update=True)
|
||||
instance = get_module(user, request, id, student_module_cache, course_id)
|
||||
if instance is None:
|
||||
log.debug("No module {0} for user {1}--access denied?".format(id, user))
|
||||
raise Http404
|
||||
|
||||
instance_module = get_instance_module(user, instance, student_module_cache)
|
||||
instance_module = get_instance_module(course_id, user, instance, student_module_cache)
|
||||
|
||||
if instance_module is None:
|
||||
log.debug("Couldn't find instance of module '%s' for user '%s'", id, user)
|
||||
@@ -387,7 +391,7 @@ def modx_dispatch(request, dispatch, location, course_id):
|
||||
return HttpResponse(json.dumps({'success': file_too_big_msg}))
|
||||
p[fileinput_id] = inputfiles
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(course_id,
|
||||
request.user, modulestore().get_instance(course_id, location))
|
||||
|
||||
instance = get_module(request.user, request, location, student_module_cache, course_id)
|
||||
@@ -397,7 +401,7 @@ def modx_dispatch(request, dispatch, location, course_id):
|
||||
log.debug("No module {0} for user {1}--access denied?".format(location, user))
|
||||
raise Http404
|
||||
|
||||
instance_module = get_instance_module(request.user, instance, student_module_cache)
|
||||
instance_module = get_instance_module(course_id, request.user, instance, student_module_cache)
|
||||
shared_module = get_shared_instance_module(course_id, request.user, instance, student_module_cache)
|
||||
|
||||
# Don't track state for anonymous users (who don't have student modules)
|
||||
|
||||
@@ -149,8 +149,7 @@ def index(request, course_id, chapter=None, section=None,
|
||||
section_descriptor = get_section(course, chapter, section)
|
||||
if section_descriptor is not None:
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
||||
request.user,
|
||||
section_descriptor)
|
||||
course_id, request.user, section_descriptor)
|
||||
module = get_module(request.user, request,
|
||||
section_descriptor.location,
|
||||
student_module_cache, course_id)
|
||||
@@ -233,6 +232,19 @@ def course_info(request, course_id):
|
||||
return render_to_response('courseware/info.html', {'course': course,
|
||||
'staff_access': staff_access,})
|
||||
|
||||
# TODO arjun: remove when custom tabs in place, see courseware/syllabus.py
|
||||
@ensure_csrf_cookie
|
||||
def syllabus(request, course_id):
|
||||
"""
|
||||
Display the course's syllabus.html, or 404 if there is no such course.
|
||||
|
||||
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('courseware/syllabus.html', {'course': course,
|
||||
'staff_access': staff_access,})
|
||||
|
||||
def registered_for_course(course, user):
|
||||
'''Return CourseEnrollment if user is registered for course, else False'''
|
||||
@@ -310,7 +322,8 @@ def progress(request, course_id, student_id=None):
|
||||
raise Http404
|
||||
student = User.objects.get(id=int(student_id))
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, course)
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
||||
course_id, request.user, course)
|
||||
course_module = get_module(request.user, request, course.location,
|
||||
student_module_cache, course_id)
|
||||
|
||||
|
||||
0
lms/djangoapps/lms_migration/management/__init__.py
Normal file
0
lms/djangoapps/lms_migration/management/__init__.py
Normal file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# File: create_groups.py
|
||||
#
|
||||
# Create all staff_* groups for classes in data directory.
|
||||
|
||||
import os, sys, string, re
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User, Group
|
||||
from path import path
|
||||
from lxml import etree
|
||||
|
||||
def create_groups():
|
||||
'''
|
||||
Create staff and instructor groups for all classes in the data_dir
|
||||
'''
|
||||
|
||||
data_dir = settings.DATA_DIR
|
||||
print "data_dir = %s" % data_dir
|
||||
|
||||
for course_dir in os.listdir(data_dir):
|
||||
|
||||
if course_dir.startswith('.'):
|
||||
continue
|
||||
if not os.path.isdir(path(data_dir) / course_dir):
|
||||
continue
|
||||
|
||||
cxfn = path(data_dir) / course_dir / 'course.xml'
|
||||
try:
|
||||
coursexml = etree.parse(cxfn)
|
||||
except Exception as err:
|
||||
print "Oops, cannot read %s, skipping" % cxfn
|
||||
continue
|
||||
cxmlroot = coursexml.getroot()
|
||||
course = cxmlroot.get('course') # TODO (vshnayder!!): read metadata from policy file(s) instead of from course.xml
|
||||
if course is None:
|
||||
print "oops, can't get course id for %s" % course_dir
|
||||
continue
|
||||
print "course=%s for course_dir=%s" % (course,course_dir)
|
||||
|
||||
create_group('staff_%s' % course) # staff group
|
||||
create_group('instructor_%s' % course) # instructor group (can manage staff group list)
|
||||
|
||||
def create_group(gname):
|
||||
if Group.objects.filter(name=gname):
|
||||
print " group exists for %s" % gname
|
||||
return
|
||||
g = Group(name=gname)
|
||||
g.save()
|
||||
print " created group %s" % gname
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Create groups associated with all courses in data_dir."
|
||||
|
||||
def handle(self, *args, **options):
|
||||
create_groups()
|
||||
146
lms/djangoapps/lms_migration/management/commands/create_user.py
Normal file
146
lms/djangoapps/lms_migration/management/commands/create_user.py
Normal file
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# File: create_user.py
|
||||
#
|
||||
# Create user. Prompt for groups and ExternalAuthMap
|
||||
|
||||
import os, sys, string, re
|
||||
import datetime
|
||||
from getpass import getpass
|
||||
import json
|
||||
from random import choice
|
||||
import readline
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from student.models import UserProfile, Registration
|
||||
from external_auth.models import ExternalAuthMap
|
||||
from django.contrib.auth.models import User, Group
|
||||
|
||||
class MyCompleter(object): # Custom completer
|
||||
|
||||
def __init__(self, options):
|
||||
self.options = sorted(options)
|
||||
|
||||
def complete(self, text, state):
|
||||
if state == 0: # on first trigger, build possible matches
|
||||
if text: # cache matches (entries that start with entered text)
|
||||
self.matches = [s for s in self.options
|
||||
if s and s.startswith(text)]
|
||||
else: # no text entered, all matches possible
|
||||
self.matches = self.options[:]
|
||||
|
||||
# return match indexed by state
|
||||
try:
|
||||
return self.matches[state]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def GenPasswd(length=8, chars=string.letters + string.digits):
|
||||
return ''.join([choice(chars) for i in range(length)])
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# main command
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Create user, interactively; can add ExternalAuthMap for MIT user if email@MIT.EDU resolves properly."
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
while True:
|
||||
uname = raw_input('username: ')
|
||||
if User.objects.filter(username=uname):
|
||||
print "username %s already taken" % uname
|
||||
else:
|
||||
break
|
||||
|
||||
make_eamap = False
|
||||
if raw_input('Create MIT ExternalAuth? [n] ').lower()=='y':
|
||||
email = '%s@MIT.EDU' % uname
|
||||
if not email.endswith('@MIT.EDU'):
|
||||
print "Failed - email must be @MIT.EDU"
|
||||
sys.exit(-1)
|
||||
mit_domain = 'ssl:MIT'
|
||||
if ExternalAuthMap.objects.filter(external_id = email, external_domain = mit_domain):
|
||||
print "Failed - email %s already exists as external_id" % email
|
||||
sys.exit(-1)
|
||||
make_eamap = True
|
||||
password = GenPasswd(12)
|
||||
|
||||
# get name from kerberos
|
||||
kname = os.popen("finger %s | grep 'name:'" % email).read().strip().split('name: ')[1].strip()
|
||||
name = raw_input('Full name: [%s] ' % kname).strip()
|
||||
if name=='':
|
||||
name = kname
|
||||
print "name = %s" % name
|
||||
else:
|
||||
while True:
|
||||
password = getpass()
|
||||
password2 = getpass()
|
||||
if password == password2:
|
||||
break
|
||||
print "Oops, passwords do not match, please retry"
|
||||
|
||||
while True:
|
||||
email = raw_input('email: ')
|
||||
if User.objects.filter(email=email):
|
||||
print "email %s already taken" % email
|
||||
else:
|
||||
break
|
||||
|
||||
name = raw_input('Full name: ')
|
||||
|
||||
|
||||
user = User(username=uname, email=email, is_active=True)
|
||||
user.set_password(password)
|
||||
try:
|
||||
user.save()
|
||||
except IntegrityError:
|
||||
print "Oops, failed to create user %s, IntegrityError" % user
|
||||
raise
|
||||
|
||||
r = Registration()
|
||||
r.register(user)
|
||||
|
||||
up = UserProfile(user=user)
|
||||
up.name = name
|
||||
up.save()
|
||||
|
||||
if make_eamap:
|
||||
credentials = "/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN=%s/emailAddress=%s" % (name,email)
|
||||
eamap = ExternalAuthMap(external_id = email,
|
||||
external_email = email,
|
||||
external_domain = mit_domain,
|
||||
external_name = name,
|
||||
internal_password = password,
|
||||
external_credentials = json.dumps(credentials),
|
||||
)
|
||||
eamap.user = user
|
||||
eamap.dtsignup = datetime.datetime.now()
|
||||
eamap.save()
|
||||
|
||||
print "User %s created successfully!" % user
|
||||
|
||||
if not raw_input('Add user %s to any groups? [n] ' % user).lower()=='y':
|
||||
sys.exit(0)
|
||||
|
||||
print "Here are the groups available:"
|
||||
|
||||
groups = [str(g.name) for g in Group.objects.all()]
|
||||
print groups
|
||||
|
||||
completer = MyCompleter(groups)
|
||||
readline.set_completer(completer.complete)
|
||||
readline.parse_and_bind('tab: complete')
|
||||
|
||||
while True:
|
||||
gname = raw_input("Add group (tab to autocomplete, empty line to end): ")
|
||||
if not gname:
|
||||
break
|
||||
if not gname in groups:
|
||||
print "Unknown group %s" % gname
|
||||
continue
|
||||
g = Group.objects.get(name=gname)
|
||||
user.groups.add(g)
|
||||
print "Added %s to group %s" % (user,g)
|
||||
|
||||
print "Done!"
|
||||
@@ -2,13 +2,21 @@
|
||||
# migration tools for content team to go from stable-edx4edx to LMS+CMS
|
||||
#
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pprint import pprint
|
||||
import xmodule.modulestore.django as xmodule_django
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.conf import settings
|
||||
import track.views
|
||||
|
||||
try:
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
except ImportError:
|
||||
from django.contrib.csrf.middleware import csrf_exempt
|
||||
|
||||
log = logging.getLogger("mitx.lms_migrate")
|
||||
LOCAL_DEBUG = True
|
||||
@@ -18,6 +26,15 @@ def escape(s):
|
||||
"""escape HTML special characters in string"""
|
||||
return str(s).replace('<','<').replace('>','>')
|
||||
|
||||
def getip(request):
|
||||
'''
|
||||
Extract IP address of requester from header, even if behind proxy
|
||||
'''
|
||||
ip = request.META.get('HTTP_X_REAL_IP','') # nginx reverse proxy
|
||||
if not ip:
|
||||
ip = request.META.get('REMOTE_ADDR','None')
|
||||
return ip
|
||||
|
||||
def manage_modulestores(request,reload_dir=None):
|
||||
'''
|
||||
Manage the static in-memory modulestores.
|
||||
@@ -32,9 +49,7 @@ def manage_modulestores(request,reload_dir=None):
|
||||
#----------------------------------------
|
||||
# check on IP address of requester
|
||||
|
||||
ip = request.META.get('HTTP_X_REAL_IP','') # nginx reverse proxy
|
||||
if not ip:
|
||||
ip = request.META.get('REMOTE_ADDR','None')
|
||||
ip = getip(request)
|
||||
|
||||
if LOCAL_DEBUG:
|
||||
html += '<h3>IP address: %s ' % ip
|
||||
@@ -48,7 +63,7 @@ def manage_modulestores(request,reload_dir=None):
|
||||
html += 'Permission denied'
|
||||
html += "</body></html>"
|
||||
log.debug('request denied, ALLOWED_IPS=%s' % ALLOWED_IPS)
|
||||
return HttpResponse(html)
|
||||
return HttpResponse(html, status=403)
|
||||
|
||||
#----------------------------------------
|
||||
# reload course if specified
|
||||
@@ -108,3 +123,66 @@ def manage_modulestores(request,reload_dir=None):
|
||||
|
||||
html += "</body></html>"
|
||||
return HttpResponse(html)
|
||||
|
||||
@csrf_exempt
|
||||
def gitreload(request, reload_dir=None):
|
||||
'''
|
||||
This can be used as a github WebHook Service Hook, for reloading of the content repo used by the LMS.
|
||||
|
||||
If reload_dir is not None, then instruct the xml loader to reload that course directory.
|
||||
'''
|
||||
html = "<html><body>"
|
||||
ip = getip(request)
|
||||
|
||||
html += '<h3>IP address: %s ' % ip
|
||||
html += '<h3>User: %s ' % request.user
|
||||
|
||||
ALLOWED_IPS = [] # allow none by default
|
||||
if hasattr(settings,'ALLOWED_GITRELOAD_IPS'): # allow override in settings
|
||||
ALLOWED_IPS = ALLOWED_GITRELOAD_IPS
|
||||
|
||||
if not (ip in ALLOWED_IPS or 'any' in ALLOWED_IPS):
|
||||
if request.user and request.user.is_staff:
|
||||
log.debug('request allowed because user=%s is staff' % request.user)
|
||||
else:
|
||||
html += 'Permission denied'
|
||||
html += "</body></html>"
|
||||
log.debug('request denied from %s, ALLOWED_IPS=%s' % (ip,ALLOWED_IPS))
|
||||
return HttpResponse(html)
|
||||
|
||||
#----------------------------------------
|
||||
# see if request is from github (POST with JSON)
|
||||
|
||||
if reload_dir is None and 'payload' in request.POST:
|
||||
payload = request.POST['payload']
|
||||
log.debug("payload=%s" % payload)
|
||||
gitargs = json.loads(payload)
|
||||
log.debug("gitargs=%s" % gitargs)
|
||||
reload_dir = gitargs['repository']['name']
|
||||
log.debug("github reload_dir=%s" % reload_dir)
|
||||
gdir = settings.DATA_DIR / reload_dir
|
||||
if not os.path.exists(gdir):
|
||||
log.debug("====> ERROR in gitreload - no such directory %s" % reload_dir)
|
||||
return HttpResponse('Error')
|
||||
cmd = "cd %s; git reset --hard HEAD; git clean -f -d; git pull origin; chmod g+w course.xml" % gdir
|
||||
log.debug(os.popen(cmd).read())
|
||||
if hasattr(settings,'GITRELOAD_HOOK'): # hit this hook after reload, if set
|
||||
gh = settings.GITRELOAD_HOOK
|
||||
if gh:
|
||||
ghurl = '%s/%s' % (gh,reload_dir)
|
||||
r = requests.get(ghurl)
|
||||
log.debug("GITRELOAD_HOOK to %s: %s" % (ghurl, r.text))
|
||||
|
||||
#----------------------------------------
|
||||
# reload course if specified
|
||||
|
||||
if reload_dir is not None:
|
||||
def_ms = modulestore()
|
||||
if reload_dir not in def_ms.courses:
|
||||
html += "<h2><font color='red'>Error: '%s' is not a valid course directory</font></h2>" % reload_dir
|
||||
else:
|
||||
html += "<h2><font color='blue'>Reloaded course directory '%s'</font></h2>" % reload_dir
|
||||
def_ms.try_load_course(reload_dir)
|
||||
track.views.server_track(request, 'reloaded %s' % reload_dir, {}, page='migrate')
|
||||
|
||||
return HttpResponse(html)
|
||||
|
||||
@@ -47,6 +47,7 @@ LOGGING = get_logger_config(LOG_DIR,
|
||||
syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514),
|
||||
debug=False)
|
||||
|
||||
COURSE_LISTINGS = ENV_TOKENS['COURSE_LISTINGS']
|
||||
|
||||
############################## SECURE AUTH ITEMS ###############################
|
||||
# Secret things: passwords, access keys, etc.
|
||||
|
||||
@@ -55,6 +55,10 @@ MITX_FEATURES = {
|
||||
# course_ids (see dev_int.py for an example)
|
||||
'SUBDOMAIN_COURSE_LISTINGS' : False,
|
||||
|
||||
# TODO: This will be removed once course-specific tabs are in place. see
|
||||
# courseware/courses.py
|
||||
'ENABLE_SYLLABUS' : True,
|
||||
|
||||
'ENABLE_TEXTBOOK' : True,
|
||||
'ENABLE_DISCUSSION' : False,
|
||||
'ENABLE_DISCUSSION_SERVICE': True,
|
||||
@@ -260,6 +264,14 @@ USE_L10N = True
|
||||
# Messages
|
||||
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
|
||||
|
||||
#################################### GITHUB #######################################
|
||||
# gitreload is used in LMS-workflow to pull content from github
|
||||
# gitreload requests are only allowed from these IP addresses, which are
|
||||
# the advertised public IPs of the github WebHook servers.
|
||||
# These are listed, eg at https://github.com/MITx/mitx/admin/hooks
|
||||
|
||||
ALLOWED_GITRELOAD_IPS = ['207.97.227.253', '50.57.128.197', '108.171.174.178']
|
||||
|
||||
#################################### AWS #######################################
|
||||
# S3BotoStorage insists on a timeout for uploaded assets. We should make it
|
||||
# permanent instead, but rather than trying to figure out exactly where that
|
||||
|
||||
@@ -73,6 +73,8 @@ MITX_FEATURES['ENABLE_LMS_MIGRATION'] = True
|
||||
MITX_FEATURES['ACCESS_REQUIRE_STAFF_FOR_COURSE'] = False # require that user be in the staff_* group to be able to enroll
|
||||
MITX_FEATURES['USE_XQA_SERVER'] = 'http://xqa:server@content-qa.mitx.mit.edu/xqa'
|
||||
|
||||
INSTALLED_APPS += ('lms_migration',)
|
||||
|
||||
LMS_MIGRATION_ALLOWED_IPS = ['127.0.0.1']
|
||||
|
||||
################################ OpenID Auth #################################
|
||||
|
||||
@@ -17,14 +17,19 @@ MITX_FEATURES['ENABLE_TEXTBOOK'] = False
|
||||
MITX_FEATURES['ENABLE_DISCUSSION'] = False
|
||||
MITX_FEATURES['ACCESS_REQUIRE_STAFF_FOR_COURSE'] = True # require that user be in the staff_* group to be able to enroll
|
||||
|
||||
MITX_FEATURES['DISABLE_START_DATES'] = True
|
||||
# MITX_FEATURES['USE_DJANGO_PIPELINE']=False # don't recompile scss
|
||||
|
||||
myhost = socket.gethostname()
|
||||
if ('edxvm' in myhost) or ('ocw' in myhost):
|
||||
MITX_FEATURES['DISABLE_LOGIN_BUTTON'] = True # auto-login with MIT certificate
|
||||
MITX_FEATURES['USE_XQA_SERVER'] = 'https://qisx.mit.edu/xqa' # needs to be ssl or browser blocks it
|
||||
MITX_FEATURES['USE_DJANGO_PIPELINE']=False # don't recompile scss
|
||||
|
||||
if ('domU' in myhost):
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
MITX_FEATURES['REROUTE_ACTIVATION_EMAIL'] = 'ichuang@mitx.mit.edu' # nonempty string = address for all activation emails
|
||||
MITX_FEATURES['USE_DJANGO_PIPELINE']=False # don't recompile scss
|
||||
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') # django 1.4 for nginx ssl proxy
|
||||
|
||||
@@ -33,4 +38,9 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') # django 1.4 fo
|
||||
|
||||
INSTALLED_APPS = tuple([ app for app in INSTALLED_APPS if not app.startswith('debug_toolbar') ])
|
||||
MIDDLEWARE_CLASSES = tuple([ mcl for mcl in MIDDLEWARE_CLASSES if not mcl.startswith('debug_toolbar') ])
|
||||
TEMPLATE_LOADERS = tuple([ app for app in TEMPLATE_LOADERS if not app.startswith('askbot') ])
|
||||
#TEMPLATE_LOADERS = tuple([ app for app in TEMPLATE_LOADERS if not app.startswith('askbot') ])
|
||||
#TEMPLATE_LOADERS = tuple([ app for app in TEMPLATE_LOADERS if not app.startswith('mitxmako') ])
|
||||
TEMPLATE_LOADERS = (
|
||||
'django.template.loaders.filesystem.Loader',
|
||||
'django.template.loaders.app_directories.Loader',
|
||||
)
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
// pages
|
||||
@import "course/info";
|
||||
@import "course/syllabus"; // TODO arjun replace w/ custom tabs, see courseware/courses.py
|
||||
@import "course/textbook";
|
||||
@import "course/profile";
|
||||
@import "course/gradebook";
|
||||
|
||||
64
lms/static/sass/course/_syllabus.scss
Normal file
64
lms/static/sass/course/_syllabus.scss
Normal file
@@ -0,0 +1,64 @@
|
||||
div.syllabus {
|
||||
|
||||
padding: 0px 10px;
|
||||
|
||||
text-align: center;
|
||||
|
||||
h1 {
|
||||
@extend .top-header
|
||||
}
|
||||
|
||||
.notes {
|
||||
width: 740px;
|
||||
margin: 0px auto 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
|
||||
text-align: left;
|
||||
|
||||
margin: 10px auto;
|
||||
|
||||
thead {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
tr.first {
|
||||
td {
|
||||
padding-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
|
||||
vertical-align: middle;
|
||||
|
||||
padding: 5px 10px;
|
||||
|
||||
&.day, &.due {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.no_class {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.important {
|
||||
color: red;
|
||||
}
|
||||
|
||||
&.week_separator {
|
||||
padding: 0px;
|
||||
|
||||
hr {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -58,18 +58,20 @@ div.course-wrapper {
|
||||
@extend h1.top-header;
|
||||
@include border-radius(0 4px 0 0);
|
||||
margin-bottom: -16px;
|
||||
border-bottom: 0;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
float: right;
|
||||
margin-right: 0;
|
||||
margin-top: 8px;
|
||||
margin: 12px 0 0;
|
||||
text-align: right;
|
||||
padding-right: 0;
|
||||
border-right: 0;
|
||||
font-size: em(14, 24);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -274,6 +274,17 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.prerequisites, .syllabus {
|
||||
ul {
|
||||
li {
|
||||
font: normal 1em/1.6em $serif;
|
||||
}
|
||||
ul {
|
||||
margin: 5px 0px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.faq {
|
||||
@include clearfix;
|
||||
|
||||
@@ -19,6 +19,9 @@ def url_class(url):
|
||||
<ol class="course-tabs">
|
||||
<li class="courseware"><a href="${reverse('courseware', args=[course.id])}" class="${url_class('courseware')}">Courseware</a></li>
|
||||
<li class="info"><a href="${reverse('info', args=[course.id])}" class="${url_class('info')}">Course Info</a></li>
|
||||
% if settings.MITX_FEATURES.get('ENABLE_SYLLABUS'):
|
||||
<li class="syllabus"><a href="${reverse('syllabus', args=[course.id])}" class="${url_class('syllabus')}">Syllabus</a></li>
|
||||
% endif
|
||||
% if user.is_authenticated():
|
||||
% if settings.MITX_FEATURES.get('ENABLE_TEXTBOOK'):
|
||||
% for index, textbook in enumerate(course.textbooks):
|
||||
|
||||
24
lms/templates/courseware/syllabus.html
Normal file
24
lms/templates/courseware/syllabus.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<%inherit file="/main.html" />
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
|
||||
<%block name="headextra">
|
||||
<%static:css group='course'/>
|
||||
</%block>
|
||||
|
||||
<%block name="title"><title>${course.number} Course Info</title></%block>
|
||||
|
||||
<%include file="/courseware/course_navigation.html" args="active_page='syllabus'" />
|
||||
<%!
|
||||
from courseware.courses import get_course_syllabus_section
|
||||
%>
|
||||
|
||||
<section class="container">
|
||||
<div class="syllabus">
|
||||
<h1> Syllabus </h1>
|
||||
% if user.is_authenticated():
|
||||
${get_course_syllabus_section(course, 'syllabus')}
|
||||
% else:
|
||||
${get_course_syllabus_section(course, 'guest_syllabus')}
|
||||
% endif
|
||||
</div>
|
||||
</section>
|
||||
@@ -107,6 +107,21 @@
|
||||
</section>
|
||||
% endif
|
||||
|
||||
% if staff_access and len(errored_courses) > 0:
|
||||
<div id="course-errors">
|
||||
<h2>Course-loading errors</h2>
|
||||
|
||||
% for course_dir, errors in errored_courses.items():
|
||||
<h3>${course_dir | h}</h3>
|
||||
<ul>
|
||||
% for (msg, err) in errors:
|
||||
<li>${msg}
|
||||
<ul><li><pre>${err}</pre></li></ul>
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
% endfor
|
||||
% endif
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<table border="1"><tr><th>datetime</th><th>username</th><th>ipaddr</th><th>source</th><th>type</th></tr>
|
||||
% for rec in records:
|
||||
<tr>
|
||||
<td>${rec.time}</td>
|
||||
<td>${rec.dtstr}</td>
|
||||
<td>${rec.username}</td>
|
||||
<td>${rec.ip}</td>
|
||||
<td>${rec.event_source}</td>
|
||||
|
||||
@@ -126,6 +126,8 @@ if settings.COURSEWARE_ENABLED:
|
||||
#Inside the course
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/info$',
|
||||
'courseware.views.course_info', name="info"),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/syllabus$',
|
||||
'courseware.views.syllabus', name="syllabus"), # TODO arjun remove when custom tabs in place, see courseware/courses.py
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book/(?P<book_index>[^/]*)/$',
|
||||
'staticbook.views.index', name="book"),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book/(?P<book_index>[^/]*)/(?P<page>[^/]*)$',
|
||||
@@ -217,11 +219,14 @@ if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
|
||||
urlpatterns += (
|
||||
url(r'^migrate/modules$', 'lms_migration.migrate.manage_modulestores'),
|
||||
url(r'^migrate/reload/(?P<reload_dir>[^/]+)$', 'lms_migration.migrate.manage_modulestores'),
|
||||
url(r'^gitreload$', 'lms_migration.migrate.gitreload'),
|
||||
url(r'^gitreload/(?P<reload_dir>[^/]+)$', 'lms_migration.migrate.gitreload'),
|
||||
)
|
||||
|
||||
if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'):
|
||||
urlpatterns += (
|
||||
url(r'^event_logs$', 'track.views.view_tracking_log'),
|
||||
url(r'^event_logs/(?P<args>.+)$', 'track.views.view_tracking_log'),
|
||||
)
|
||||
|
||||
urlpatterns = patterns(*urlpatterns)
|
||||
|
||||
@@ -28,6 +28,7 @@ django-override-settings
|
||||
mock>=0.8, <0.9
|
||||
PyYAML
|
||||
South
|
||||
pytz
|
||||
django-celery
|
||||
django-countries
|
||||
django-kombu
|
||||
|
||||
46
setup-test-dirs.sh
Executable file
46
setup-test-dirs.sh
Executable file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Create symlinks from ~/mitx_all/data or $ROOT/data, with root passed as first arg
|
||||
# to all the test courses in mitx/common/test/data/
|
||||
|
||||
# posix compliant sanity check
|
||||
if [ -z $BASH ] || [ $BASH = "/bin/sh" ]; then
|
||||
echo "Please use the bash interpreter to run this script"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ROOT="${1:-$HOME/mitx_all}"
|
||||
|
||||
if [[ ! -d "$ROOT" ]]; then
|
||||
echo "'$ROOT' is not a directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$ROOT/mitx" ]]; then
|
||||
echo "'$ROOT' is not the root mitx_all directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$ROOT/data" ]]; then
|
||||
echo "'$ROOT' is not the root mitx_all directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "ROOT is $ROOT"
|
||||
|
||||
cd $ROOT/data
|
||||
|
||||
for course in $(/bin/ls ../mitx/common/test/data/)
|
||||
do
|
||||
# Get rid of the symlink if it already exists
|
||||
if [[ -L "$course" ]]; then
|
||||
echo "Removing link to '$course'"
|
||||
rm -f $course
|
||||
fi
|
||||
echo "Make link to '$course'"
|
||||
# Create it
|
||||
ln -s "../mitx/common/test/data/$course"
|
||||
done
|
||||
|
||||
# go back to where we came from
|
||||
cd -
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# File: create_groups.py
|
||||
#
|
||||
# Create all staff_* groups for classes in data directory.
|
||||
|
||||
import os, sys, string, re
|
||||
|
||||
sys.path.append(os.path.abspath('.'))
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.dev'
|
||||
|
||||
try:
|
||||
from lms.envs.dev import *
|
||||
except Exception as err:
|
||||
print "Run this script from the top-level mitx directory (mitx_all/mitx), not a subdirectory."
|
||||
sys.exit(-1)
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User, Group
|
||||
from path import path
|
||||
from lxml import etree
|
||||
|
||||
data_dir = settings.DATA_DIR
|
||||
print "data_dir = %s" % data_dir
|
||||
|
||||
for course_dir in os.listdir(data_dir):
|
||||
# print course_dir
|
||||
if not os.path.isdir(path(data_dir) / course_dir):
|
||||
continue
|
||||
|
||||
cxfn = path(data_dir) / course_dir / 'course.xml'
|
||||
coursexml = etree.parse(cxfn)
|
||||
cxmlroot = coursexml.getroot()
|
||||
course = cxmlroot.get('course')
|
||||
if course is None:
|
||||
print "oops, can't get course id for %s" % course_dir
|
||||
continue
|
||||
print "course=%s for course_dir=%s" % (course,course_dir)
|
||||
|
||||
gname = 'staff_%s' % course
|
||||
if Group.objects.filter(name=gname):
|
||||
print "group exists for %s" % gname
|
||||
continue
|
||||
g = Group(name=gname)
|
||||
g.save()
|
||||
print "created group %s" % gname
|
||||
@@ -1,149 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# File: create_user.py
|
||||
#
|
||||
# Create user. Prompt for groups and ExternalAuthMap
|
||||
|
||||
import os, sys, string, re
|
||||
import datetime
|
||||
from getpass import getpass
|
||||
import json
|
||||
import readline
|
||||
|
||||
sys.path.append(os.path.abspath('.'))
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.dev'
|
||||
|
||||
try:
|
||||
from lms.envs.dev import *
|
||||
except Exception as err:
|
||||
print "Run this script from the top-level mitx directory (mitx_all/mitx), not a subdirectory."
|
||||
sys.exit(-1)
|
||||
|
||||
from student.models import UserProfile, Registration
|
||||
from external_auth.models import ExternalAuthMap
|
||||
from django.contrib.auth.models import User, Group
|
||||
from random import choice
|
||||
|
||||
class MyCompleter(object): # Custom completer
|
||||
|
||||
def __init__(self, options):
|
||||
self.options = sorted(options)
|
||||
|
||||
def complete(self, text, state):
|
||||
if state == 0: # on first trigger, build possible matches
|
||||
if text: # cache matches (entries that start with entered text)
|
||||
self.matches = [s for s in self.options
|
||||
if s and s.startswith(text)]
|
||||
else: # no text entered, all matches possible
|
||||
self.matches = self.options[:]
|
||||
|
||||
# return match indexed by state
|
||||
try:
|
||||
return self.matches[state]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def GenPasswd(length=8, chars=string.letters + string.digits):
|
||||
return ''.join([choice(chars) for i in range(length)])
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# main
|
||||
|
||||
while True:
|
||||
uname = raw_input('username: ')
|
||||
if User.objects.filter(username=uname):
|
||||
print "username %s already taken" % uname
|
||||
else:
|
||||
break
|
||||
|
||||
make_eamap = False
|
||||
if raw_input('Create MIT ExternalAuth? [n] ').lower()=='y':
|
||||
email = '%s@MIT.EDU' % uname
|
||||
if not email.endswith('@MIT.EDU'):
|
||||
print "Failed - email must be @MIT.EDU"
|
||||
sys.exit(-1)
|
||||
mit_domain = 'ssl:MIT'
|
||||
if ExternalAuthMap.objects.filter(external_id = email, external_domain = mit_domain):
|
||||
print "Failed - email %s already exists as external_id" % email
|
||||
sys.exit(-1)
|
||||
make_eamap = True
|
||||
password = GenPasswd(12)
|
||||
|
||||
# get name from kerberos
|
||||
kname = os.popen("finger %s | grep 'name:'" % email).read().strip().split('name: ')[1].strip()
|
||||
name = raw_input('Full name: [%s] ' % kname).strip()
|
||||
if name=='':
|
||||
name = kname
|
||||
print "name = %s" % name
|
||||
else:
|
||||
while True:
|
||||
password = getpass()
|
||||
password2 = getpass()
|
||||
if password == password2:
|
||||
break
|
||||
print "Oops, passwords do not match, please retry"
|
||||
|
||||
while True:
|
||||
email = raw_input('email: ')
|
||||
if User.objects.filter(email=email):
|
||||
print "email %s already taken" % email
|
||||
else:
|
||||
break
|
||||
|
||||
name = raw_input('Full name: ')
|
||||
|
||||
|
||||
user = User(username=uname, email=email, is_active=True)
|
||||
user.set_password(password)
|
||||
try:
|
||||
user.save()
|
||||
except IntegrityError:
|
||||
print "Oops, failed to create user %s, IntegrityError" % user
|
||||
raise
|
||||
|
||||
r = Registration()
|
||||
r.register(user)
|
||||
|
||||
up = UserProfile(user=user)
|
||||
up.name = name
|
||||
up.save()
|
||||
|
||||
if make_eamap:
|
||||
credentials = "/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN=%s/emailAddress=%s" % (name,email)
|
||||
eamap = ExternalAuthMap(external_id = email,
|
||||
external_email = email,
|
||||
external_domain = mit_domain,
|
||||
external_name = name,
|
||||
internal_password = password,
|
||||
external_credentials = json.dumps(credentials),
|
||||
)
|
||||
eamap.user = user
|
||||
eamap.dtsignup = datetime.datetime.now()
|
||||
eamap.save()
|
||||
|
||||
print "User %s created successfully!" % user
|
||||
|
||||
if not raw_input('Add user %s to any groups? [n] ' % user).lower()=='y':
|
||||
sys.exit(0)
|
||||
|
||||
print "Here are the groups available:"
|
||||
|
||||
groups = [str(g.name) for g in Group.objects.all()]
|
||||
print groups
|
||||
|
||||
completer = MyCompleter(groups)
|
||||
readline.set_completer(completer.complete)
|
||||
readline.parse_and_bind('tab: complete')
|
||||
|
||||
while True:
|
||||
gname = raw_input("Add group (tab to autocomplete, empty line to end): ")
|
||||
if not gname:
|
||||
break
|
||||
if not gname in groups:
|
||||
print "Unknown group %s" % gname
|
||||
continue
|
||||
g = Group.objects.get(name=gname)
|
||||
user.groups.add(g)
|
||||
print "Added %s to group %s" % (user,g)
|
||||
|
||||
print "Done!"
|
||||
Reference in New Issue
Block a user