Commit fe091b7b by Rocky Duan

Merge branch 'master' of github.com:MITx/mitx into feature/rocky/discussion_api_key

parents cf28dbea 214fb918
......@@ -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;
......
......@@ -263,7 +263,7 @@ def add_user_to_default_group(user, group):
utg.users.add(User.objects.get(username=user))
utg.save()
@receiver(post_save, sender=User)
# @receiver(post_save, sender=User)
def update_user_information(sender, instance, created, **kwargs):
try:
cc_user = cc.User.from_django_user(instance)
......@@ -274,7 +274,7 @@ def update_user_information(sender, instance, created, **kwargs):
log.error("update user info to discussion failed for user with id: " + str(instance.id))
########################## REPLICATION SIGNALS #################################
@receiver(post_save, sender=User)
# @receiver(post_save, sender=User)
def replicate_user_save(sender, **kwargs):
user_obj = kwargs['instance']
if not should_replicate(user_obj):
......@@ -282,7 +282,7 @@ def replicate_user_save(sender, **kwargs):
for course_db_name in db_names_to_replicate_to(user_obj.id):
replicate_user(user_obj, course_db_name)
@receiver(post_save, sender=CourseEnrollment)
# @receiver(post_save, sender=CourseEnrollment)
def replicate_enrollment_save(sender, **kwargs):
"""This is called when a Student enrolls in a course. It has to do the
following:
......@@ -308,12 +308,12 @@ def replicate_enrollment_save(sender, **kwargs):
user_profile = UserProfile.objects.get(user_id=enrollment_obj.user_id)
replicate_model(UserProfile.save, user_profile, enrollment_obj.user_id)
@receiver(post_delete, sender=CourseEnrollment)
# @receiver(post_delete, sender=CourseEnrollment)
def replicate_enrollment_delete(sender, **kwargs):
enrollment_obj = kwargs['instance']
return replicate_model(CourseEnrollment.delete, enrollment_obj, enrollment_obj.user_id)
@receiver(post_save, sender=UserProfile)
# @receiver(post_save, sender=UserProfile)
def replicate_userprofile_save(sender, **kwargs):
"""We just updated the UserProfile (say an update to the name), so push that
change to all Course DBs that we're enrolled in."""
......
......@@ -8,6 +8,7 @@ import logging
from datetime import datetime
from django.test import TestCase
from nose.plugins.skip import SkipTest
from .models import User, UserProfile, CourseEnrollment, replicate_user, USER_FIELDS_TO_COPY
......@@ -22,6 +23,7 @@ class ReplicationTest(TestCase):
def test_user_replication(self):
"""Test basic user replication."""
raise SkipTest()
portal_user = User.objects.create_user('rusty', 'rusty@edx.org', 'fakepass')
portal_user.first_name='Rusty'
portal_user.last_name='Skids'
......@@ -80,6 +82,7 @@ class ReplicationTest(TestCase):
def test_enrollment_for_existing_user_info(self):
"""Test the effect of Enrolling in a class if you've already got user
data to be copied over."""
raise SkipTest()
# Create our User
portal_user = User.objects.create_user('jack', 'jack@edx.org', 'fakepass')
portal_user.first_name = "Jack"
......@@ -143,6 +146,8 @@ class ReplicationTest(TestCase):
def test_enrollment_for_user_info_after_enrollment(self):
"""Test the effect of modifying User data after you've enrolled."""
raise SkipTest()
# Create our User
portal_user = User.objects.create_user('patty', 'patty@edx.org', 'fakepass')
portal_user.first_name = "Patty"
......
......@@ -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)
......
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})
......@@ -29,6 +29,7 @@ Each input type takes the xml tree as 'element', the previous answer as 'value',
import logging
import re
import shlex # for splitting quoted strings
import json
from lxml import etree
import xml.sax.saxutils as saxutils
......@@ -149,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)
......@@ -205,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))
......@@ -293,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 = {'"': '&quot;'}
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)
......@@ -336,6 +340,11 @@ def filesubmission(element, value, status, render_template, msg=''):
Upload a single file (e.g. for programming assignments)
'''
eid = element.get('id')
escapedict = {'"': '&quot;'}
allowed_files = json.dumps(element.get('allowed_files', '').split())
allowed_files = saxutils.escape(allowed_files, escapedict)
required_files = json.dumps(element.get('required_files', '').split())
required_files = saxutils.escape(required_files, escapedict)
# Check if problem has been queued
queue_len = 0
......@@ -345,7 +354,8 @@ def filesubmission(element, value, status, render_template, msg=''):
msg = 'Submitted to grader. (Queue length: %s)' % queue_len
context = { 'id': eid, 'state': status, 'msg': msg, 'value': value,
'queue_len': queue_len
'queue_len': queue_len, 'allowed_files': allowed_files,
'required_files': required_files
}
html = render_template("filesubmission.html", context)
return etree.XML(html)
......
<section id="filesubmission_${id}" class="filesubmission">
<input type="file" name="input_${id}" id="input_${id}" value="${value}" multiple="multiple" /><br />
<input type="file" name="input_${id}" id="input_${id}" value="${value}" multiple="multiple" data-required_files="${required_files}" data-allowed_files="${allowed_files}"/><br />
% if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct':
......
<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}" />
......
......@@ -68,7 +68,7 @@ class SemanticSectionDescriptor(XModuleDescriptor):
the child element
"""
xml_object = etree.fromstring(xml_data)
system.error_tracker("WARNING: the &lt;{0}> tag is deprecated. Please do not use in new content."
system.error_tracker("WARNING: the <{0}> tag is deprecated. Please do not use in new content."
.format(xml_object.tag))
if len(xml_object) == 1:
......
......@@ -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 {
......
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
......
......@@ -160,24 +160,42 @@ class @Problem
max_filesize = 4*1000*1000 # 4 MB
file_too_large = false
file_not_selected = false
required_files_not_submitted = false
unallowed_file_submitted = false
errors = []
@inputs.each (index, element) ->
if element.type is 'file'
required_files = $(element).data("required_files")
allowed_files = $(element).data("allowed_files")
for file in element.files
if allowed_files.length != 0 and file.name not in allowed_files
unallowed_file_submitted = true
errors.push "You submitted #{file.name}; only #{allowed_files} are allowed."
if file.name in required_files
required_files.splice(required_files.indexOf(file.name), 1)
if file.size > max_filesize
file_too_large = true
alert 'Submission aborted! Your file "' + file.name '" is too large (max size: ' + max_filesize/(1000*1000) + ' MB)'
errors.push 'Your file "' + file.name '" is too large (max size: ' + max_filesize/(1000*1000) + ' MB)'
fd.append(element.id, file)
if element.files.length == 0
file_not_selected = true
fd.append(element.id, '') # In case we want to allow submissions with no file
if required_files.length != 0
required_files_not_submitted = true
errors.push "You did not submit the required files: #{required_files}."
else
fd.append(element.id, element.value)
if file_not_selected
alert 'Submission aborted! You did not select any files to submit'
errors.push 'You did not select any files to submit'
if errors.length > 0
alert errors.join("\n")
abort_submission = file_too_large or file_not_selected
abort_submission = file_too_large or file_not_selected or unallowed_file_submitted or required_files_not_submitted
settings =
type: "POST"
......
......@@ -3,6 +3,7 @@ class @Sequence
@el = $(element).find('.sequence')
@contents = @$('.seq_contents')
@id = @el.data('id')
@modx_url = @el.data('course_modx_root')
@initProgress()
@bind()
@render parseInt(@el.data('position'))
......@@ -76,13 +77,14 @@ class @Sequence
if @position != new_position
if @position != undefined
@mark_visited @position
$.postWithPrefix "/modx/#{@id}/goto_position", position: new_position
modx_full_url = @modx_url + '/' + @id + '/goto_position'
$.postWithPrefix modx_full_url, position: new_position
@mark_active new_position
@$('#seq_content').html @contents.eq(new_position - 1).text()
XModule.loadModules('display', @$('#seq_content'))
MathJax.Hub.Queue(["Typeset", MathJax.Hub])
MathJax.Hub.Queue(["Typeset", MathJax.Hub, "seq_content"]) # NOTE: Actually redundant. Some other MathJax call also being performed
@position = new_position
@toggleArrows()
@hookUpProgressEvent()
......@@ -91,7 +93,7 @@ class @Sequence
event.preventDefault()
new_position = $(event.target).data('element')
Logger.log "seq_goto", old: @position, new: new_position, id: @id
# On Sequence chage, destroy any existing polling thread
# for queued submissions, see ../capa/display.coffee
if window.queuePollerID
......
......@@ -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
......
Simple course. If start dates are on, non-staff users should see Overview, but not Ch 2.
roots/2012_Fall.xml
\ No newline at end of file
<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>
<b>Lab 2A: Superposition Experiment</b>
<p>Isn't the toy course great?</p>
<html filename="toylab.html"/>
\ No newline at end of file
{
"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"
}
}
<course org="edX" course="test_start_date" url_name="2012_Fall"/>
\ No newline at end of file
<html filename="toylab.html"/>
\ No newline at end of file
{
"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
}
}
......@@ -80,8 +80,8 @@ def course_wiki_redirect(request, course_id):
urlpath = URLPath.create_article(
root,
course_slug,
title=course.title,
content="This is the wiki for " + course.title + ".",
title=course.number,
content="{0}\n===\nThis is the wiki for **{1}**'s _{2}_.".format(course.number, course.org, course.title),
user_message="Course page automatically created.",
user=None,
ip_address=None,
......
......@@ -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
......@@ -155,13 +157,25 @@ def progress_summary(student, course, grader, student_module_cache):
chapters = []
# Don't include chapters that aren't displayable (e.g. due to error)
for c in course.get_display_items():
# Skip if the chapter is hidden
hidden = c.metadata.get('hide_from_toc','false')
if hidden.lower() == 'true':
continue
sections = []
for s in c.get_display_items():
# Skip if the section is hidden
hidden = s.metadata.get('hide_from_toc','false')
if hidden.lower() == 'true':
continue
# Same for sections
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
......@@ -190,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).
......@@ -205,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'])
......
# -*- 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)
......
......@@ -8,6 +8,7 @@ from django.contrib.auth.models import User
from mitxmako.shortcuts import render_to_response, render_to_string
from courseware.courses import get_course_with_access
from courseware.access import has_access
from urllib import urlencode
from django_comment_client.permissions import check_permissions_by_view
......@@ -156,8 +157,9 @@ def forum_form_discussion(request, course_id):
'content': content,
'recent_active_threads': recent_active_threads,
'trending_tags': trending_tags,
'staff_access' : has_access(request.user, course, 'staff'),
}
print "start rendering.."
# print "start rendering.."
return render_to_response('discussion/index.html', context)
def render_single_thread(request, discussion_id, course_id, thread_id):
......
"""
This must be run only after seed_permissions_roles.py!
Creates default roles for all users currently in the database. Just runs through
Enrollments.
"""
from django.core.management.base import BaseCommand, CommandError
from student.models import CourseEnrollment
from django_comment_client.permissions import assign_default_role
class Command(BaseCommand):
args = 'course_id'
help = 'Seed default permisssions and roles'
def handle(self, *args, **options):
if len(args) != 0:
raise CommandError("This Command takes no arguments")
print "Updated roles for ",
for i, enrollment in enumerate(CourseEnrollment.objects.all(), start=1):
assign_default_role(None, enrollment)
if i % 1000 == 0:
print "{0}...".format(i),
print
\ No newline at end of file
......@@ -11,9 +11,9 @@ from util.cache import cache
@receiver(post_save, sender=CourseEnrollment)
def assign_default_role(sender, instance, **kwargs):
if instance.user.is_staff:
role = Role.objects.get(course_id=instance.course_id, name="Moderator")
role = Role.objects.get_or_create(course_id=instance.course_id, name="Moderator")[0]
else:
role = Role.objects.get(course_id=instance.course_id, name="Student")
role = Role.objects.get_or_create(course_id=instance.course_id, name="Student")[0]
logging.info("assign_default_role: adding %s as %s" % (instance.user, role))
instance.user.roles.add(role)
......
from django.contrib.auth.models import User
from django.utils import unittest
from django.test import TestCase
from student.models import CourseEnrollment, \
replicate_enrollment_save, \
replicate_enrollment_delete, \
......@@ -13,120 +13,11 @@ import random
from .permissions import has_permission
from .models import Role, Permission
# code adapted from https://github.com/justquick/django-activity-stream/issues/88
class NoSignalTestCase(unittest.TestCase):
def _receiver_in_lookup_keys(self, receiver, lookup_keys):
"""
Evaluate if the receiver is in the provided lookup_keys; instantly terminates when found.
"""
for key in lookup_keys:
if (receiver[0][0] == key[0] or key[0] is None) and receiver[0][1] == key[1]:
return True
return False
def _find_allowed_receivers(self, receivers, lookup_keys):
"""
Searches the receivers, keeping any that have a lookup_key in the lookup_keys list
"""
kept_receivers = []
for receiver in receivers:
if self._receiver_in_lookup_keys(receiver, lookup_keys):
kept_receivers.append(receiver)
return kept_receivers
def _create_lookup_keys(self, sender_receivers_tuple_list):
"""
Creates a signal lookup keys from the provided array of tuples.
"""
lookup_keys = []
for keep in sender_receivers_tuple_list:
receiver = keep[0]
sender = keep[1]
lookup_key = (_make_id(receiver) if receiver else receiver, _make_id(sender))
lookup_keys.append(lookup_key)
return lookup_keys
def _remove_disallowed_receivers(self, receivers, lookup_keys):
"""
Searches the receivers, discarding any that have a lookup_key in the lookup_keys list
"""
kept_receivers = []
for receiver in receivers:
if not self._receiver_in_lookup_keys(receiver, lookup_keys):
kept_receivers.append(receiver)
return kept_receivers
def setUp(self, sender_receivers_to_keep=None, sender_receivers_to_discard=None):
"""
Turns off signals from other apps
The `sender_receivers_to_keep` can be set to an array of tuples (reciever, sender,), preserving matching signals.
The `sender_receivers_to_discard` can be set to an array of tuples (reciever, sender,), discarding matching signals.
with both, you can set the `receiver` to None if you want to target all signals for a model
"""
self.m2m_changed_receivers = m2m_changed.receivers
self.pre_delete_receivers = pre_delete.receivers
self.pre_save_receivers = pre_save.receivers
self.post_delete_receivers = post_delete.receivers
self.post_save_receivers = post_save.receivers
new_m2m_changed_receivers = []
new_pre_delete_receivers = []
new_pre_save_receivers = []
new_post_delete_receivers = []
new_post_save_receivers = []
if sender_receivers_to_keep:
lookup_keys = self._create_lookup_keys(sender_receivers_to_keep)
new_m2m_changed_receivers = self._find_allowed_receivers(self.m2m_changed_receivers, lookup_keys)
new_pre_delete_receivers = self._find_allowed_receivers(self.pre_delete_receivers, lookup_keys)
new_pre_save_receivers = self._find_allowed_receivers(self.pre_save_receivers, lookup_keys)
new_post_delete_receivers = self._find_allowed_receivers(self.post_delete_receivers, lookup_keys)
new_post_save_receivers = self._find_allowed_receivers(self.post_save_receivers, lookup_keys)
if sender_receivers_to_discard:
lookup_keys = self._create_lookup_keys(sender_receivers_to_discard)
new_m2m_changed_receivers = self._remove_disallowed_receivers(new_m2m_changed_receivers or self.m2m_changed_receivers, lookup_keys)
new_pre_delete_receivers = self._remove_disallowed_receivers(new_pre_delete_receivers or self.pre_delete_receivers, lookup_keys)
new_pre_save_receivers = self._remove_disallowed_receivers(new_pre_save_receivers or self.pre_save_receivers, lookup_keys)
new_post_delete_receivers = self._remove_disallowed_receivers(new_post_delete_receivers or self.post_delete_receivers, lookup_keys)
new_post_save_receivers = self._remove_disallowed_receivers(new_post_save_receivers or self.post_save_receivers, lookup_keys)
m2m_changed.receivers = new_m2m_changed_receivers
pre_delete.receivers = new_pre_delete_receivers
pre_save.receivers = new_pre_save_receivers
post_delete.receivers = new_post_delete_receivers
post_save.receivers = new_post_save_receivers
super(NoSignalTestCase, self).setUp()
def tearDown(self):
"""
Restores the signals that were turned off.
"""
super(NoSignalTestCase, self).tearDown()
m2m_changed.receivers = self.m2m_changed_receivers
pre_delete.receivers = self.pre_delete_receivers
pre_save.receivers = self.pre_save_receivers
post_delete.receivers = self.post_delete_receivers
post_save.receivers = self.post_save_receivers
class PermissionsTestCase(NoSignalTestCase):
class PermissionsTestCase(TestCase):
def random_str(self, length=15, chars=string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for x in range(length))
def setUp(self):
sender_receivers_to_discard = [
(replicate_enrollment_save, CourseEnrollment),
(replicate_enrollment_delete, CourseEnrollment),
(update_user_information, User),
(replicate_user_save, User),
]
super(PermissionsTestCase, self).setUp(sender_receivers_to_discard=sender_receivers_to_discard)
self.course_id = "MITx/6.002x/2012_Fall"
self.moderator_role = Role.objects.get_or_create(name="Moderator", course_id=self.course_id)[0]
......@@ -144,9 +35,10 @@ class PermissionsTestCase(NoSignalTestCase):
def tearDown(self):
self.student_enrollment.delete()
self.moderator_enrollment.delete()
self.student.delete()
self.moderator.delete()
super(PermissionsTestCase, self).tearDown()
# Do we need to have this? We shouldn't be deleting students, ever
# self.student.delete()
# self.moderator.delete()
def testDefaultRoles(self):
self.assertTrue(self.student_role in self.student.roles.all())
......
......@@ -6,41 +6,53 @@
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.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
data_dir = settings.DATA_DIR
print "data_dir = %s" % data_dir
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):
for course_dir in os.listdir(data_dir):
# print course_dir
if not os.path.isdir(path(data_dir) / course_dir):
continue
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)
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
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
continue
print " group exists for %s" % gname
return
g = Group(name=gname)
g.save()
print "created group %s" % gname
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()
......@@ -8,21 +8,13 @@ import os, sys, string, re
import datetime
from getpass import getpass
import json
from random import choice
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 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
from random import choice
class MyCompleter(object): # Custom completer
......@@ -47,103 +39,108 @@ def GenPasswd(length=8, chars=string.letters + string.digits):
return ''.join([choice(chars) for i in range(length)])
#-----------------------------------------------------------------------------
# main
# main command
while True:
uname = raw_input('username: ')
if User.objects.filter(username=uname):
print "username %s already taken" % uname
else:
break
class Command(BaseCommand):
help = "Create user, interactively; can add ExternalAuthMap for MIT user if email@MIT.EDU resolves properly."
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"
def handle(self, *args, **options):
while True:
email = raw_input('email: ')
if User.objects.filter(email=email):
print "email %s already taken" % email
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:
break
while True:
password = getpass()
password2 = getpass()
if password == password2:
break
print "Oops, passwords do not match, please retry"
name = raw_input('Full name: ')
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!"
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('<','&lt;').replace('>','&gt;')
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)
......@@ -39,7 +39,7 @@ def update_template_dictionary(dictionary, request=None, course=None, article=No
if course:
dictionary['course'] = course
if 'namespace' not in dictionary:
dictionary['namespace'] = course.wiki_namespace
dictionary['namespace'] = "edX"
else:
dictionary['course'] = None
......@@ -99,7 +99,7 @@ def root_redirect(request, course_id=None):
course = get_opt_course_with_access(request.user, course_id, 'load')
#TODO: Add a default namespace to settings.
namespace = course.wiki_namespace if course else "edX"
namespace = "edX"
try:
root = Article.get_root(namespace)
......@@ -479,7 +479,7 @@ def not_found(request, article_path, course):
"""Generate a NOT FOUND message for some URL"""
d = {'wiki_err_notfound': True,
'article_path': article_path,
'namespace': course.wiki_namespace}
'namespace': "edX"}
update_template_dictionary(d, request, course)
return render_to_response('simplewiki/simplewiki_error.html', d)
......
......@@ -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
......@@ -304,6 +316,8 @@ SIMPLE_WIKI_REQUIRE_LOGIN_VIEW = False
################################# WIKI ###################################
WIKI_ACCOUNT_HANDLING = False
WIKI_EDITOR = 'course_wiki.editors.CodeMirror'
WIKI_SHOW_MAX_CHILDREN = 0 # We don't use the little menu that shows children of an article in the breadcrumb
WIKI_ANONYMOUS = False # Don't allow anonymous access until the styling is figured out
################################# Jasmine ###################################
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
......@@ -569,7 +583,7 @@ INSTALLED_APPS = (
'course_wiki', # Our customizations
'mptt',
'sekizai',
'wiki.plugins.attachments',
#'wiki.plugins.attachments',
'wiki.plugins.notifications',
'course_wiki.plugins.markdownedx',
......
......@@ -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',
)
......@@ -84,11 +84,17 @@ DATABASES = {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "course1.db",
},
'edx/full/6.002_Spring_2012': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "course2.db",
}
},
'edX/toy/TT_2012_Fall': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "course3.db",
},
}
CACHES = {
......
......@@ -29,6 +29,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";
......
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):
......@@ -27,7 +30,7 @@ def url_class(url):
% endif
% if settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
<li class="discussion"><a href="${reverse('django_comment_client.forum.views.forum_form_discussion', args=[course.id])}" class="${url_class('discussion')}">Discussion</a></li>
<li class="news"><a href="${reverse('news', args=[course.id])}" class="${url_class('news')}">News</a></li>
## <li class="news"><a href="${reverse('news', args=[course.id])}" class="${url_class('news')}">News</a></li>
% endif
% if settings.MITX_FEATURES.get('ENABLE_DISCUSSION'):
......
......@@ -21,7 +21,7 @@
<%static:js group='courseware'/>
<%include file="discussion/_js_dependencies.html" />
<%include file="../discussion/_js_dependencies.html" />
<%include file="/mathjax_include.html" />
<!-- TODO: http://docs.jquery.com/Plugins/Validation -->
......@@ -59,8 +59,8 @@
<div id="course-errors">
<ul>
% for (msg, err) in course_errors:
<li>${msg}
<ul><li><pre>${err}</pre></li></ul>
<li>${msg | h}
<ul><li><pre>${err | h}</pre></li></ul>
</li>
% endfor
</ul>
......
<%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>
......
<div id="sequence_${element_id}" class="sequence" data-id="${item_id}" data-position="${position}" >
<div id="sequence_${element_id}" class="sequence" data-id="${item_id}" data-position="${position}" data-course_modx_root="/course/modx" >
<nav aria-label="Section Navigation" class="sequence-nav">
<ol id="sequence-list">
% for idx, item in enumerate(items):
......@@ -21,7 +21,7 @@
</nav>
% for item in items:
<div class="seq_contents">${item['content'] | h}</div>
<div class="seq_contents tex2jax_ignore">${item['content'] | h}</div>
% endfor
<div id="seq_content"></div>
......
......@@ -69,6 +69,7 @@ ${"Edit " + wiki_title + " - " if wiki_title is not UNDEFINED else ""}MITx 6.002
%else:
<input type="submit" id="submit_edit" name="edit" value="Save Changes" />
<input type="submit" id="submit_delete" name="delete" value="Delete article" />
%endif
<%include file="simplewiki_instructions.html"/>
......
......@@ -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>
......
......@@ -52,6 +52,9 @@
</style>
{% endaddtoblock %}
<p class="lead">
{% trans "Click each revision to see a list of edited lines. Click the Preview button to see how the article looked at this stage. At the bottom of this page, you can change to a particular revision or merge an old revision with the current one." %}
</p>
<form method="GET">
<div class="tab-content" style="overflow: visible;">
......@@ -60,20 +63,14 @@
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" style="float: left;" href="#collapse{{ revision.revision_number }}" onclick="get_diff_json('{% url 'wiki:diff' revision.id %}', $('#collapse{{ revision.revision_number }}'))">
{{ revision.created }} (#{{ revision.revision_number }}) by {% if revision.user %}{{ revision.user }}{% else %}{% if user|is_moderator %}{{ revision.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
{% if revision == article.current_revision %}
<strong>*</strong>
{% endif %}
{% if revision.deleted %}
<span class="badge badge-important">{% trans "deleted" %}</span>
{% endif %}
{% if revision.previous_revision.deleted and not revision.deleted %}
<span class="badge badge-success">{% trans "restored" %}</span>
{% endif %}
<span class="icon-plus"></span>
{% include "wiki/includes/revision_info.html" with current_revision=article.current_revision %}
<div style="color: #CCC;">
<small>
{% if revision.user_message %}
{{ revision.user_message }}
{% elif revision.automatic_log %}
{{ revision.automatic_log }}
{% else %}
({% trans "no log message" %})
{% endif %}
......@@ -84,21 +81,12 @@
<div class="bar" style="width: 100%;"></div>
</div>
<div class="pull-right" style="vertical-align: middle; margin: 8px 3px;">
{% if revision == article.current_revision %}
<a href="#" class="btn disabled">
<span class="icon-lock"></span>
{% trans "Preview this version" %}
</a>
{% else %}
{% if not revision == article.current_revision %}
<button type="submit" class="btn" onclick="$('#previewModal').modal('show'); this.form.target='previewWindow'; this.form.r.value='{{ revision.id }}'; this.form.action='{% url 'wiki:preview_revision' article.id %}'; $('#previewModal .switch-to-revision').attr('href', '{% url 'wiki:change_revision' path=urlpath.path article_id=article.id revision_id=revision.id %}')">
<span class="icon-eye-open"></span>
{% trans "Preview this version" %}
{% trans "Preview this revision" %}
</button>
{% endif %}
<a class="btn btn-info" href="#collapse{{ revision.revision_number }}" onclick="get_diff_json('{% url 'wiki:diff' revision_id=revision.id %}', $('#collapse{{ revision.revision_number }}'))">
<span class="icon-list-alt"></span>
{% trans "Show changes" %}
</a>
{% if article|can_write:user %}
<input type="radio"{% if revision == article.current_revision %} disabled="true"{% endif %} style="margin: 0 10px;" value="{{ revision.id }}" name="revision_id" switch-button-href="{% url 'wiki:change_revision' path=urlpath.path revision_id=revision.id %}" merge-button-href="{% url 'wiki:merge_revision_preview' article_id=article.id revision_id=revision.id %}" merge-button-commit-href="{% url 'wiki:merge_revision' path=urlpath.path article_id=article.id revision_id=revision.id %}" />
......@@ -157,7 +145,7 @@
<input type="hidden" name="r" value="" />
<div class="modal hide fade" id="previewModal">
<div class="modal-body">
<iframe name="previewWindow" frameborder="0"></iframe>
<iframe name="previewWindow"></iframe>
</div>
<div class="modal-footer">
<a href="#" class="btn btn-large" data-dismiss="modal">
......@@ -184,7 +172,7 @@
<p class="lead"><span class="icon-info-sign"></span> {% trans "When you merge a revision with the current, all data will be retained from both versions and merged at its approximate location from each revision." %} <strong>{% trans "After this, it's important to do a manual review." %}</strong></p>
</div>
<div class="modal-body">
<iframe name="mergeWindow" frameborder="0"></iframe>
<iframe name="mergeWindow"></iframe>
</div>
<div class="modal-footer">
<a href="#" class="btn btn-large" data-dismiss="modal">
......
{% load i18n %}{% load url from future %}
{% if urlpath %}
## mako
<%! from django.core.urlresolvers import reverse %>
%if urlpath is not Undefined and urlpath:
<header>
<ul class="breadcrumb pull-left" class="">
{% for ancestor in urlpath.get_ancestors.all %}
<li><a href="{% url 'wiki:get' path=ancestor.path %}">{{ ancestor.article.current_revision.title }}</a></li>
{% endfor %}
<li class="active"><a href="{% url 'wiki:get' path=urlpath.path %}">{{ article.current_revision.title }}</a></li>
<%
# The create button links to the highest ancestor we have edit priveleges to
create_article_root = None
%>
%for ancestor in urlpath.cached_ancestors:
<li><a href="${reverse('wiki:get', kwargs={'path' : ancestor.path})}">${ancestor.article.current_revision.title}</a></li>
<%
if not create_article_root and ancestor.article.can_write(user):
create_article_root = ancestor
%>
%endfor
<li class="active"><a href="${reverse('wiki:get', kwargs={'path' : urlpath.path})}">${article.current_revision.title}</a></li>
<%
if not create_article_root and urlpath.article.can_write(user):
create_article_root = urlpath
%>
</ul>
<div class="pull-left" style="margin-left: 10px;">
<div class="btn-group">
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#" style="padding: 7px;" title="{% trans "Sub-articles for" %} {{ article.current_revision.title }}">
<span class="icon-list"></span>
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
{% for child in children_slice %}
<li>
<a href="{% url 'wiki:get' path=child.path %}">
{{ child.article.current_revision.title }}
</a>
</li>
{% empty %}
<li><a href="#"><em>{% trans "No sub-articles" %}</em></a></li>
{% endfor %}
{% if children_slice_more %}
<li><a href="#"><em>{% trans "...and more" %}</em></a></li>
{% endif %}
<li class="divider"></li>
<li>
<a href="" onclick="alert('TODO')">{% trans "List sub-pages" %} &raquo;</a>
</li>
</ul>
</div>
</div>
<div class="global-functions pull-right">
<form class="search-wiki pull-left">
<!-- <form class="search-wiki pull-left">
<input type="search" placeholder="search wiki" />
</form>
<a class="add-article-btn btn pull-left" href="{% url 'wiki:create' path=urlpath.path %}" style="padding: 7px;">
</form> -->
%if create_article_root:
<a class="add-article-btn btn pull-left" href="${reverse('wiki:create', kwargs={'path' : create_article_root.path})}" style="padding: 7px;">
<span class="icon-plus"></span>
{% trans "Add article" %}
Add article
</a>
%endif
</div>
</header>
{% endif %}
%endif
{% extends "wiki/base.html" %}
{% load wiki_tags i18n %}
{% load url from future %}
{% block pagetitle %}{{ article.current_revision.title }}{% endblock %}
{% block wiki_breadcrumbs %}
{% include "wiki/includes/breadcrumbs.html" %}
{% endblock %}
{% block wiki_contents %}
<div class="missing-wrapper">
<p>This article was not found, and neither was the parent. <a href="#">Go back to the main wiki article.</a></p>
<button type="submit">Create a new article</button>
</div>
{% endblock %}
......@@ -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)
......
-e git://github.com/MITx/django-staticfiles.git@6d2504e5c8#egg=django-staticfiles
-e git://github.com/MITx/django-pipeline.git#egg=django-pipeline
-e git://github.com/benjaoming/django-wiki.git@c145596#egg=django-wiki
-e git://github.com/dementrock/pystache_custom.git#egg=pystache_custom
-e git://github.com/benjaoming/django-wiki.git@02275fb4#egg=django-wiki
-e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
-e common/lib/capa
-e common/lib/xmodule
......@@ -28,6 +28,7 @@ django-override-settings
mock>=0.8, <0.9
PyYAML
South
pytz
django-celery
django-countries
django-kombu
......
#!/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 -
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment