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 ...@@ -26,3 +26,5 @@ Gemfile.lock
lms/static/sass/*.css lms/static/sass/*.css
cms/static/sass/*.css cms/static/sass/*.css
lms/lib/comment_client/python lms/lib/comment_client/python
nosetests.xml
cover_html/
...@@ -14,6 +14,10 @@ $yellow: #fff8af; ...@@ -14,6 +14,10 @@ $yellow: #fff8af;
$cream: #F6EFD4; $cream: #F6EFD4;
$border-color: #ddd; $border-color: #ddd;
// edX colors
$blue: rgb(29,157,217);
$pink: rgb(182,37,104);
@mixin hide-text { @mixin hide-text {
background-color: transparent; background-color: transparent;
border: 0; border: 0;
......
...@@ -263,7 +263,7 @@ def add_user_to_default_group(user, group): ...@@ -263,7 +263,7 @@ def add_user_to_default_group(user, group):
utg.users.add(User.objects.get(username=user)) utg.users.add(User.objects.get(username=user))
utg.save() utg.save()
@receiver(post_save, sender=User) # @receiver(post_save, sender=User)
def update_user_information(sender, instance, created, **kwargs): def update_user_information(sender, instance, created, **kwargs):
try: try:
cc_user = cc.User.from_django_user(instance) cc_user = cc.User.from_django_user(instance)
...@@ -274,7 +274,7 @@ def update_user_information(sender, instance, created, **kwargs): ...@@ -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)) log.error("update user info to discussion failed for user with id: " + str(instance.id))
########################## REPLICATION SIGNALS ################################# ########################## REPLICATION SIGNALS #################################
@receiver(post_save, sender=User) # @receiver(post_save, sender=User)
def replicate_user_save(sender, **kwargs): def replicate_user_save(sender, **kwargs):
user_obj = kwargs['instance'] user_obj = kwargs['instance']
if not should_replicate(user_obj): if not should_replicate(user_obj):
...@@ -282,7 +282,7 @@ def replicate_user_save(sender, **kwargs): ...@@ -282,7 +282,7 @@ def replicate_user_save(sender, **kwargs):
for course_db_name in db_names_to_replicate_to(user_obj.id): for course_db_name in db_names_to_replicate_to(user_obj.id):
replicate_user(user_obj, course_db_name) replicate_user(user_obj, course_db_name)
@receiver(post_save, sender=CourseEnrollment) # @receiver(post_save, sender=CourseEnrollment)
def replicate_enrollment_save(sender, **kwargs): def replicate_enrollment_save(sender, **kwargs):
"""This is called when a Student enrolls in a course. It has to do the """This is called when a Student enrolls in a course. It has to do the
following: following:
...@@ -308,12 +308,12 @@ def replicate_enrollment_save(sender, **kwargs): ...@@ -308,12 +308,12 @@ def replicate_enrollment_save(sender, **kwargs):
user_profile = UserProfile.objects.get(user_id=enrollment_obj.user_id) user_profile = UserProfile.objects.get(user_id=enrollment_obj.user_id)
replicate_model(UserProfile.save, user_profile, 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): def replicate_enrollment_delete(sender, **kwargs):
enrollment_obj = kwargs['instance'] enrollment_obj = kwargs['instance']
return replicate_model(CourseEnrollment.delete, enrollment_obj, enrollment_obj.user_id) 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): def replicate_userprofile_save(sender, **kwargs):
"""We just updated the UserProfile (say an update to the name), so push that """We just updated the UserProfile (say an update to the name), so push that
change to all Course DBs that we're enrolled in.""" change to all Course DBs that we're enrolled in."""
......
...@@ -8,6 +8,7 @@ import logging ...@@ -8,6 +8,7 @@ import logging
from datetime import datetime from datetime import datetime
from django.test import TestCase from django.test import TestCase
from nose.plugins.skip import SkipTest
from .models import User, UserProfile, CourseEnrollment, replicate_user, USER_FIELDS_TO_COPY from .models import User, UserProfile, CourseEnrollment, replicate_user, USER_FIELDS_TO_COPY
...@@ -22,6 +23,7 @@ class ReplicationTest(TestCase): ...@@ -22,6 +23,7 @@ class ReplicationTest(TestCase):
def test_user_replication(self): def test_user_replication(self):
"""Test basic user replication.""" """Test basic user replication."""
raise SkipTest()
portal_user = User.objects.create_user('rusty', 'rusty@edx.org', 'fakepass') portal_user = User.objects.create_user('rusty', 'rusty@edx.org', 'fakepass')
portal_user.first_name='Rusty' portal_user.first_name='Rusty'
portal_user.last_name='Skids' portal_user.last_name='Skids'
...@@ -80,6 +82,7 @@ class ReplicationTest(TestCase): ...@@ -80,6 +82,7 @@ class ReplicationTest(TestCase):
def test_enrollment_for_existing_user_info(self): def test_enrollment_for_existing_user_info(self):
"""Test the effect of Enrolling in a class if you've already got user """Test the effect of Enrolling in a class if you've already got user
data to be copied over.""" data to be copied over."""
raise SkipTest()
# Create our User # Create our User
portal_user = User.objects.create_user('jack', 'jack@edx.org', 'fakepass') portal_user = User.objects.create_user('jack', 'jack@edx.org', 'fakepass')
portal_user.first_name = "Jack" portal_user.first_name = "Jack"
...@@ -143,6 +146,8 @@ class ReplicationTest(TestCase): ...@@ -143,6 +146,8 @@ class ReplicationTest(TestCase):
def test_enrollment_for_user_info_after_enrollment(self): def test_enrollment_for_user_info_after_enrollment(self):
"""Test the effect of modifying User data after you've enrolled.""" """Test the effect of modifying User data after you've enrolled."""
raise SkipTest()
# Create our User # Create our User
portal_user = User.objects.create_user('patty', 'patty@edx.org', 'fakepass') portal_user = User.objects.create_user('patty', 'patty@edx.org', 'fakepass')
portal_user.first_name = "Patty" portal_user.first_name = "Patty"
......
...@@ -140,7 +140,20 @@ def dashboard(request): ...@@ -140,7 +140,20 @@ def dashboard(request):
if not user.is_active: if not user.is_active:
message = render_to_string('registration/activate_account_notice.html', {'email': user.email}) 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) return render_to_response('dashboard.html', context)
......
import json import json
import logging import logging
import os import os
import pytz
import datetime import datetime
import dateutil.parser import dateutil.parser
...@@ -84,15 +85,33 @@ def server_track(request, event_type, event, page=None): ...@@ -84,15 +85,33 @@ def server_track(request, event_type, event, page=None):
"time": datetime.datetime.utcnow().isoformat(), "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 return
log_event(event) log_event(event)
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def view_tracking_log(request): def view_tracking_log(request,args=''):
if not request.user.is_staff: if not request.user.is_staff:
return redirect('/') 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}) 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', ...@@ -29,6 +29,7 @@ Each input type takes the xml tree as 'element', the previous answer as 'value',
import logging import logging
import re import re
import shlex # for splitting quoted strings import shlex # for splitting quoted strings
import json
from lxml import etree from lxml import etree
import xml.sax.saxutils as saxutils import xml.sax.saxutils as saxutils
...@@ -149,6 +150,7 @@ def optioninput(element, value, status, render_template, msg=''): ...@@ -149,6 +150,7 @@ def optioninput(element, value, status, render_template, msg=''):
'state': status, 'state': status,
'msg': msg, 'msg': msg,
'options': osetdict, 'options': osetdict,
'inline': element.get('inline',''),
} }
html = render_template("optioninput.html", context) html = render_template("optioninput.html", context)
...@@ -205,7 +207,7 @@ def extract_choices(element): ...@@ -205,7 +207,7 @@ def extract_choices(element):
raise Exception("[courseware.capa.inputtypes.extract_choices] \ raise Exception("[courseware.capa.inputtypes.extract_choices] \
Expected a <choice> tag; got %s instead" Expected a <choice> tag; got %s instead"
% choice.tag) % 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)) choices.append((choice.get("name"), choice_text))
...@@ -293,7 +295,9 @@ def textline(element, value, status, render_template, msg=""): ...@@ -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 hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden
escapedict = {'"': '&quot;'} escapedict = {'"': '&quot;'}
value = saxutils.escape(value, escapedict) # otherwise, answers with quotes in them crashes the system! 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) html = render_template("textinput.html", context)
try: try:
xhtml = etree.XML(html) xhtml = etree.XML(html)
...@@ -336,6 +340,11 @@ def filesubmission(element, value, status, render_template, msg=''): ...@@ -336,6 +340,11 @@ def filesubmission(element, value, status, render_template, msg=''):
Upload a single file (e.g. for programming assignments) Upload a single file (e.g. for programming assignments)
''' '''
eid = element.get('id') 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 # Check if problem has been queued
queue_len = 0 queue_len = 0
...@@ -345,7 +354,8 @@ def filesubmission(element, value, status, render_template, msg=''): ...@@ -345,7 +354,8 @@ def filesubmission(element, value, status, render_template, msg=''):
msg = 'Submitted to grader. (Queue length: %s)' % queue_len msg = 'Submitted to grader. (Queue length: %s)' % queue_len
context = { 'id': eid, 'state': status, 'msg': msg, 'value': value, 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) html = render_template("filesubmission.html", context)
return etree.XML(html) return etree.XML(html)
......
<section id="filesubmission_${id}" class="filesubmission"> <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': % if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct': % elif state == 'correct':
......
<section id="textinput_${id}" class="textinput"> <% doinline = "inline" if inline else "" %>
<section id="textinput_${id}" class="textinput ${doinline}" >
% if state == 'unsubmitted': % if state == 'unsubmitted':
<div class="unanswered" id="status_${id}"> <div class="unanswered ${doinline}" id="status_${id}">
% elif state == 'correct': % elif state == 'correct':
<div class="correct" id="status_${id}"> <div class="correct ${doinline}" id="status_${id}">
% elif state == 'incorrect': % elif state == 'incorrect':
<div class="incorrect" id="status_${id}"> <div class="incorrect ${doinline}" id="status_${id}">
% elif state == 'incomplete': % elif state == 'incomplete':
<div class="incorrect" id="status_${id}"> <div class="incorrect ${doinline}" id="status_${id}">
% endif % endif
% if hidden: % if hidden:
<div style="display:none;" name="${hidden}" inputid="input_${id}" /> <div style="display:none;" name="${hidden}" inputid="input_${id}" />
......
...@@ -68,7 +68,7 @@ class SemanticSectionDescriptor(XModuleDescriptor): ...@@ -68,7 +68,7 @@ class SemanticSectionDescriptor(XModuleDescriptor):
the child element the child element
""" """
xml_object = etree.fromstring(xml_data) 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)) .format(xml_object.tag))
if len(xml_object) == 1: if len(xml_object) == 1:
......
...@@ -18,7 +18,7 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -18,7 +18,7 @@ class CourseDescriptor(SequenceDescriptor):
class Textbook: class Textbook:
def __init__(self, title, book_url): def __init__(self, title, book_url):
self.title = title self.title = title
self.book_url = book_url self.book_url = book_url
self.table_of_contents = self._get_toc_from_s3() self.table_of_contents = self._get_toc_from_s3()
@classmethod @classmethod
...@@ -30,11 +30,11 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -30,11 +30,11 @@ class CourseDescriptor(SequenceDescriptor):
return self.table_of_contents return self.table_of_contents
def _get_toc_from_s3(self): def _get_toc_from_s3(self):
''' """
Accesses the textbook's table of contents (default name "toc.xml") at the URL self.book_url 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 Returns XML tree representation of the table of contents
''' """
toc_url = self.book_url + 'toc.xml' toc_url = self.book_url + 'toc.xml'
# Get the table of contents from S3 # Get the table of contents from S3
...@@ -72,6 +72,22 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -72,6 +72,22 @@ class CourseDescriptor(SequenceDescriptor):
self.enrollment_start = self._try_parse_time("enrollment_start") self.enrollment_start = self._try_parse_time("enrollment_start")
self.enrollment_end = self._try_parse_time("enrollment_end") 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 @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
textbooks = [] textbooks = []
...@@ -87,25 +103,11 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -87,25 +103,11 @@ class CourseDescriptor(SequenceDescriptor):
@property @property
def grader(self): def grader(self):
return self.__grading_policy['GRADER'] return self._grading_policy['GRADER']
@property @property
def grade_cutoffs(self): def grade_cutoffs(self):
return self.__grading_policy['GRADE_CUTOFFS'] 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
@lazyproperty @lazyproperty
def grading_context(self): def grading_context(self):
......
...@@ -27,6 +27,10 @@ section.problem { ...@@ -27,6 +27,10 @@ section.problem {
} }
} }
.inline {
display: inline;
}
div { div {
p { p {
&.answer { &.answer {
......
import abc import abc
import json import json
import logging import logging
import sys
from collections import namedtuple from collections import namedtuple
...@@ -14,11 +15,11 @@ def load_grading_policy(course_policy_string): ...@@ -14,11 +15,11 @@ def load_grading_policy(course_policy_string):
""" """
This loads a grading policy from a string (usually read from a file), This loads a grading policy from a string (usually read from a file),
which can be a JSON object or an empty string. which can be a JSON object or an empty string.
The JSON object can have the keys GRADER and GRADE_CUTOFFS. If either is The JSON object can have the keys GRADER and GRADE_CUTOFFS. If either is
missing, it reverts to the default. missing, it reverts to the default.
""" """
default_policy_string = """ default_policy_string = """
{ {
"GRADER" : [ "GRADER" : [
...@@ -56,7 +57,7 @@ def load_grading_policy(course_policy_string): ...@@ -56,7 +57,7 @@ def load_grading_policy(course_policy_string):
} }
} }
""" """
# Load the global settings as a dictionary # Load the global settings as a dictionary
grading_policy = json.loads(default_policy_string) grading_policy = json.loads(default_policy_string)
...@@ -64,15 +65,15 @@ def load_grading_policy(course_policy_string): ...@@ -64,15 +65,15 @@ def load_grading_policy(course_policy_string):
course_policy = {} course_policy = {}
if course_policy_string: if course_policy_string:
course_policy = json.loads(course_policy_string) course_policy = json.loads(course_policy_string)
# Override any global settings with the course settings # Override any global settings with the course settings
grading_policy.update(course_policy) grading_policy.update(course_policy)
# Here is where we should parse any configurations, so that we can fail early # Here is where we should parse any configurations, so that we can fail early
grading_policy['GRADER'] = grader_from_conf(grading_policy['GRADER']) grading_policy['GRADER'] = grader_from_conf(grading_policy['GRADER'])
return grading_policy return grading_policy
def aggregate_scores(scores, section_name="summary"): def aggregate_scores(scores, section_name="summary"):
""" """
...@@ -130,9 +131,11 @@ def grader_from_conf(conf): ...@@ -130,9 +131,11 @@ def grader_from_conf(conf):
raise ValueError("Configuration has no appropriate grader class.") raise ValueError("Configuration has no appropriate grader class.")
except (TypeError, ValueError) as error: except (TypeError, ValueError) as error:
errorString = "Unable to parse grader configuration:\n " + str(subgraderconf) + "\n Error was:\n " + str(error) # Add info and re-raise
log.critical(errorString) msg = ("Unable to parse grader configuration:\n " +
raise ValueError(errorString) str(subgraderconf) +
"\n Error was:\n " + str(error))
raise ValueError(msg), None, sys.exc_info()[2]
return WeightedSubsectionsGrader(subgraders) return WeightedSubsectionsGrader(subgraders)
......
...@@ -86,7 +86,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -86,7 +86,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
# online and has imported all current (fall 2012) courses from xml # online and has imported all current (fall 2012) courses from xml
if not system.resources_fs.exists(filepath): if not system.resources_fs.exists(filepath):
candidates = cls.backcompat_paths(filepath) candidates = cls.backcompat_paths(filepath)
log.debug("candidates = {0}".format(candidates)) #log.debug("candidates = {0}".format(candidates))
for candidate in candidates: for candidate in candidates:
if system.resources_fs.exists(candidate): if system.resources_fs.exists(candidate):
filepath = candidate filepath = candidate
......
...@@ -160,24 +160,42 @@ class @Problem ...@@ -160,24 +160,42 @@ class @Problem
max_filesize = 4*1000*1000 # 4 MB max_filesize = 4*1000*1000 # 4 MB
file_too_large = false file_too_large = false
file_not_selected = false file_not_selected = false
required_files_not_submitted = false
unallowed_file_submitted = false
errors = []
@inputs.each (index, element) -> @inputs.each (index, element) ->
if element.type is 'file' if element.type is 'file'
required_files = $(element).data("required_files")
allowed_files = $(element).data("allowed_files")
for file in element.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 if file.size > max_filesize
file_too_large = true 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) fd.append(element.id, file)
if element.files.length == 0 if element.files.length == 0
file_not_selected = true file_not_selected = true
fd.append(element.id, '') # In case we want to allow submissions with no file 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 else
fd.append(element.id, element.value) fd.append(element.id, element.value)
if file_not_selected 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 = settings =
type: "POST" type: "POST"
......
...@@ -3,6 +3,7 @@ class @Sequence ...@@ -3,6 +3,7 @@ class @Sequence
@el = $(element).find('.sequence') @el = $(element).find('.sequence')
@contents = @$('.seq_contents') @contents = @$('.seq_contents')
@id = @el.data('id') @id = @el.data('id')
@modx_url = @el.data('course_modx_root')
@initProgress() @initProgress()
@bind() @bind()
@render parseInt(@el.data('position')) @render parseInt(@el.data('position'))
...@@ -76,13 +77,14 @@ class @Sequence ...@@ -76,13 +77,14 @@ class @Sequence
if @position != new_position if @position != new_position
if @position != undefined if @position != undefined
@mark_visited @position @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 @mark_active new_position
@$('#seq_content').html @contents.eq(new_position - 1).text() @$('#seq_content').html @contents.eq(new_position - 1).text()
XModule.loadModules('display', @$('#seq_content')) 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 @position = new_position
@toggleArrows() @toggleArrows()
@hookUpProgressEvent() @hookUpProgressEvent()
...@@ -91,7 +93,7 @@ class @Sequence ...@@ -91,7 +93,7 @@ class @Sequence
event.preventDefault() event.preventDefault()
new_position = $(event.target).data('element') new_position = $(event.target).data('element')
Logger.log "seq_goto", old: @position, new: new_position, id: @id Logger.log "seq_goto", old: @position, new: new_position, id: @id
# On Sequence chage, destroy any existing polling thread # On Sequence chage, destroy any existing polling thread
# for queued submissions, see ../capa/display.coffee # for queued submissions, see ../capa/display.coffee
if window.queuePollerID if window.queuePollerID
......
...@@ -4,16 +4,17 @@ import os ...@@ -4,16 +4,17 @@ import os
import re import re
from collections import defaultdict from collections import defaultdict
from cStringIO import StringIO
from fs.osfs import OSFS from fs.osfs import OSFS
from importlib import import_module from importlib import import_module
from lxml import etree from lxml import etree
from lxml.html import HtmlComment from lxml.html import HtmlComment
from path import path from path import path
from xmodule.errortracker import ErrorLog, make_error_tracker from xmodule.errortracker import ErrorLog, make_error_tracker
from xmodule.x_module import XModuleDescriptor, XMLParsingSystem
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.mako_module import MakoDescriptorSystem from xmodule.mako_module import MakoDescriptorSystem
from cStringIO import StringIO from xmodule.x_module import XModuleDescriptor, XMLParsingSystem
from . import ModuleStoreBase, Location from . import ModuleStoreBase, Location
from .exceptions import ItemNotFoundError from .exceptions import ItemNotFoundError
...@@ -134,12 +135,12 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -134,12 +135,12 @@ class XMLModuleStore(ModuleStoreBase):
self.data_dir = path(data_dir) self.data_dir = path(data_dir)
self.modules = defaultdict(dict) # course_id -> dict(location -> XModuleDescriptor) self.modules = defaultdict(dict) # course_id -> dict(location -> XModuleDescriptor)
self.courses = {} # course_dir -> XModuleDescriptor for the course 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: if default_class is None:
self.default_class = None self.default_class = None
else: else:
module_path, _, class_name = default_class.rpartition('.') module_path, _, class_name = default_class.rpartition('.')
#log.debug('module_path = %s' % module_path)
class_ = getattr(import_module(module_path), class_name) class_ = getattr(import_module(module_path), class_name)
self.default_class = class_ self.default_class = class_
...@@ -162,18 +163,27 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -162,18 +163,27 @@ class XMLModuleStore(ModuleStoreBase):
''' '''
Load a course, keeping track of errors as we go along. 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: 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) 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.courses[course_dir] = course_descriptor
self._location_errors[course_descriptor.location] = errorlog self._location_errors[course_descriptor.location] = errorlog
except: else:
msg = "Failed to load course '%s'" % course_dir # Didn't load course. Instead, save the errors elsewhere.
log.exception(msg) self.errored_courses[course_dir] = errorlog
def __unicode__(self): def __unicode__(self):
''' '''
...@@ -202,6 +212,28 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -202,6 +212,28 @@ class XMLModuleStore(ModuleStoreBase):
return {} 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): def load_course(self, course_dir, tracker):
""" """
Load a course into this module store Load a course into this module store
...@@ -242,9 +274,16 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -242,9 +274,16 @@ class XMLModuleStore(ModuleStoreBase):
course = course_dir course = course_dir
url_name = course_data.get('url_name', course_data.get('slug')) url_name = course_data.get('url_name', course_data.get('slug'))
policy_dir = None
if url_name: 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) 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: else:
policy = {} policy = {}
# VS[compat] : 'name' is deprecated, but support it for now... # VS[compat] : 'name' is deprecated, but support it for now...
...@@ -268,6 +307,15 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -268,6 +307,15 @@ class XMLModuleStore(ModuleStoreBase):
# after we have the course descriptor. # after we have the course descriptor.
XModuleDescriptor.compute_inherited_metadata(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)) log.debug('========> Done with course import from {0}'.format(course_dir))
return course_descriptor return course_descriptor
...@@ -318,6 +366,12 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -318,6 +366,12 @@ class XMLModuleStore(ModuleStoreBase):
""" """
return self.courses.values() 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): def create_item(self, location):
raise NotImplementedError("XMLModuleStores are read-only") raise NotImplementedError("XMLModuleStores are read-only")
......
...@@ -233,6 +233,9 @@ class ImportTestCase(unittest.TestCase): ...@@ -233,6 +233,9 @@ class ImportTestCase(unittest.TestCase):
self.assertEqual(toy_ch.display_name, "Overview") self.assertEqual(toy_ch.display_name, "Overview")
self.assertEqual(two_toys_ch.display_name, "Two Toy 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): def test_definition_loading(self):
"""When two courses share the same org and course name and """When two courses share the same org and course name and
......
...@@ -38,7 +38,7 @@ def get_metadata_from_xml(xml_object, remove=True): ...@@ -38,7 +38,7 @@ def get_metadata_from_xml(xml_object, remove=True):
if meta is None: if meta is None:
return '' return ''
dmdata = meta.text 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: if remove:
xml_object.remove(meta) xml_object.remove(meta)
return dmdata 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): ...@@ -80,8 +80,8 @@ def course_wiki_redirect(request, course_id):
urlpath = URLPath.create_article( urlpath = URLPath.create_article(
root, root,
course_slug, course_slug,
title=course.title, title=course.number,
content="This is the wiki for " + course.title + ".", 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_message="Course page automatically created.",
user=None, user=None,
ip_address=None, ip_address=None,
......
...@@ -63,6 +63,9 @@ def has_access(user, obj, action): ...@@ -63,6 +63,9 @@ def has_access(user, obj, action):
if isinstance(obj, Location): if isinstance(obj, Location):
return _has_access_location(user, obj, action) 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 # Passing an unknown object here is a coding error, so rather than
# returning a default, complain. # returning a default, complain.
raise TypeError("Unknown object type in has_access(): '{0}'" raise TypeError("Unknown object type in has_access(): '{0}'"
...@@ -238,6 +241,30 @@ def _has_access_location(user, location, action): ...@@ -238,6 +241,30 @@ def _has_access_location(user, location, action):
return _dispatch(checkers, action, user, location) 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 ##### Internal helper methods below
def _dispatch(table, action, user, obj): def _dispatch(table, action, user, obj):
...@@ -266,6 +293,15 @@ def _course_staff_group_name(location): ...@@ -266,6 +293,15 @@ def _course_staff_group_name(location):
""" """
return 'staff_%s' % Location(location).course 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): def _has_staff_access_to_location(user, location):
''' '''
Returns True if the given user has staff access to a location. For now this 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): ...@@ -30,7 +30,6 @@ def get_course_by_id(course_id):
raise Http404("Course not found.") raise Http404("Course not found.")
def get_course_with_access(user, course_id, action): def get_course_with_access(user, course_id, action):
""" """
Given a course_id, look up the corresponding course descriptor, Given a course_id, look up the corresponding course descriptor,
...@@ -142,6 +141,35 @@ def get_course_info_section(course, section_key): ...@@ -142,6 +141,35 @@ def get_course_info_section(course, section_key):
raise KeyError("Invalid about key " + str(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): def get_courses_by_university(user, domain=None):
''' '''
......
...@@ -29,6 +29,8 @@ def grade(student, request, course, student_module_cache=None): ...@@ -29,6 +29,8 @@ def grade(student, request, course, student_module_cache=None):
output from the course grader, augmented with the final letter output from the course grader, augmented with the final letter
grade. The keys in the output are: grade. The keys in the output are:
course: a CourseDescriptor
- grade : A final letter grade. - grade : A final letter grade.
- percent : The final percent for the class (rounded up). - percent : The final percent for the class (rounded up).
- section_breakdown : A breakdown of each section that makes - section_breakdown : A breakdown of each section that makes
...@@ -42,7 +44,7 @@ def grade(student, request, course, student_module_cache=None): ...@@ -42,7 +44,7 @@ def grade(student, request, course, student_module_cache=None):
grading_context = course.grading_context grading_context = course.grading_context
if student_module_cache == None: 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 = {} totaled_scores = {}
# This next complicated loop is just to collect the totaled_scores, which is # 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): ...@@ -56,7 +58,8 @@ def grade(student, request, course, student_module_cache=None):
should_grade_section = False 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% # 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']: 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 should_grade_section = True
break break
...@@ -64,10 +67,9 @@ def grade(student, request, course, student_module_cache=None): ...@@ -64,10 +67,9 @@ def grade(student, request, course, student_module_cache=None):
scores = [] scores = []
# TODO: We need the request to pass into here. If we could forgo that, our arguments # TODO: We need the request to pass into here. If we could forgo that, our arguments
# would be simpler # would be simpler
course_id = CourseDescriptor.location_to_id(course.location)
section_module = get_module(student, request, section_module = get_module(student, request,
section_descriptor.location, student_module_cache, section_descriptor.location, student_module_cache,
course_id) course.id)
if section_module is None: if section_module is None:
# student doesn't have access to this module, or something else # student doesn't have access to this module, or something else
# went wrong. # went wrong.
...@@ -76,7 +78,7 @@ def grade(student, request, course, student_module_cache=None): ...@@ -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 # 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 # Then, we may not need to instatiate any problems if they are already in the database
for module in yield_module_descendents(section_module): 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: if correct is None and total is None:
continue continue
...@@ -155,13 +157,25 @@ def progress_summary(student, course, grader, student_module_cache): ...@@ -155,13 +157,25 @@ def progress_summary(student, course, grader, student_module_cache):
chapters = [] chapters = []
# Don't include chapters that aren't displayable (e.g. due to error) # Don't include chapters that aren't displayable (e.g. due to error)
for c in course.get_display_items(): 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 = [] sections = []
for s in c.get_display_items(): 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 # Same for sections
graded = s.metadata.get('graded', False) graded = s.metadata.get('graded', False)
scores = [] scores = []
for module in yield_module_descendents(s): 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: if correct is None and total is None:
continue continue
...@@ -190,7 +204,7 @@ def progress_summary(student, course, grader, student_module_cache): ...@@ -190,7 +204,7 @@ def progress_summary(student, course, grader, student_module_cache):
return chapters 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). 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): ...@@ -205,10 +219,11 @@ def get_score(user, problem, student_module_cache):
correct = 0.0 correct = 0.0
# If the ID is not in the cache, add the item # 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) # instance_module = student_module_cache.lookup(problem.category, problem.id)
# if instance_module is None: # if instance_module is None:
# instance_module = StudentModule(module_type=problem.category, # instance_module = StudentModule(module_type=problem.category,
# course_id=????,
# module_state_key=problem.id, # module_state_key=problem.id,
# student=user, # student=user,
# state=None, # state=None,
......
...@@ -84,6 +84,7 @@ class Command(BaseCommand): ...@@ -84,6 +84,7 @@ class Command(BaseCommand):
# TODO (cpennington): Get coursename in a legitimate way # TODO (cpennington): Get coursename in a legitimate way
course_location = 'i4x://edx/6002xs12/course/6.002_Spring_2012' course_location = 'i4x://edx/6002xs12/course/6.002_Spring_2012'
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents( student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
course_id,
sample_user, modulestore().get_item(course_location)) sample_user, modulestore().get_item(course_location))
course = get_module(sample_user, None, course_location, student_module_cache) course = get_module(sample_user, None, course_location, student_module_cache)
......
...@@ -9,6 +9,8 @@ class Migration(SchemaMigration): ...@@ -9,6 +9,8 @@ class Migration(SchemaMigration):
def forwards(self, orm): 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'] # Removing unique constraint on 'StudentModule', fields ['module_id', 'module_type', 'student']
db.delete_unique('courseware_studentmodule', ['module_id', 'module_type', 'student_id']) 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 ...@@ -22,6 +22,9 @@ from django.contrib.auth.models import User
class StudentModule(models.Model): class StudentModule(models.Model):
"""
Keeps student state for a particular module in a particular course.
"""
# For a homework problem, contains a JSON # For a homework problem, contains a JSON
# object consisting of state # object consisting of state
MODULE_TYPES = (('problem', 'problem'), MODULE_TYPES = (('problem', 'problem'),
...@@ -37,9 +40,10 @@ class StudentModule(models.Model): ...@@ -37,9 +40,10 @@ class StudentModule(models.Model):
# Filename for homeworks, etc. # Filename for homeworks, etc.
module_state_key = models.CharField(max_length=255, db_index=True, db_column='module_id') module_state_key = models.CharField(max_length=255, db_index=True, db_column='module_id')
student = models.ForeignKey(User, db_index=True) student = models.ForeignKey(User, db_index=True)
course_id = models.CharField(max_length=255, db_index=True)
class Meta: class Meta:
unique_together = (('student', 'module_state_key'),) unique_together = (('student', 'module_state_key', 'course_id'),)
## Internal state of the object ## Internal state of the object
state = models.TextField(null=True, blank=True) state = models.TextField(null=True, blank=True)
...@@ -57,7 +61,8 @@ class StudentModule(models.Model): ...@@ -57,7 +61,8 @@ class StudentModule(models.Model):
modified = models.DateTimeField(auto_now=True, db_index=True) modified = models.DateTimeField(auto_now=True, db_index=True)
def __unicode__(self): 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 # TODO (cpennington): Remove these once the LMS switches to using XModuleDescriptors
...@@ -67,20 +72,20 @@ class StudentModuleCache(object): ...@@ -67,20 +72,20 @@ class StudentModuleCache(object):
""" """
A cache of StudentModules for a specific student 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 Find any StudentModule objects that are needed by any descriptor
in descriptors. Avoids making multiple queries to the database. in descriptors. Avoids making multiple queries to the database.
Note: Only modules that have store_state = True or have shared Note: Only modules that have store_state = True or have shared
state will have a StudentModule. state will have a StudentModule.
Arguments Arguments
user: The user for which to fetch maching StudentModules user: The user for which to fetch maching StudentModules
descriptors: An array of XModuleDescriptors. descriptors: An array of XModuleDescriptors.
select_for_update: Flag indicating whether the rows should be locked until end of transaction select_for_update: Flag indicating whether the rows should be locked until end of transaction
''' '''
if user.is_authenticated(): 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 # This works around a limitation in sqlite3 on the number of parameters
# that can be put into a single query # that can be put into a single query
...@@ -89,78 +94,86 @@ class StudentModuleCache(object): ...@@ -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)]: for id_chunk in [module_ids[i:i + chunk_size] for i in xrange(0, len(module_ids), chunk_size)]:
if select_for_update: if select_for_update:
self.cache.extend(StudentModule.objects.select_for_update().filter( self.cache.extend(StudentModule.objects.select_for_update().filter(
course_id=course_id,
student=user, student=user,
module_state_key__in=id_chunk) module_state_key__in=id_chunk)
) )
else: else:
self.cache.extend(StudentModule.objects.filter( self.cache.extend(StudentModule.objects.filter(
course_id=course_id,
student=user, student=user,
module_state_key__in=id_chunk) module_state_key__in=id_chunk)
) )
else: else:
self.cache = [] self.cache = []
@classmethod @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 descriptor: An XModuleDescriptor
depth is the number of levels of descendent modules to load StudentModules for, in addition to 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 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 should be cached
select_for_update: Flag indicating whether the rows should be locked until end of transaction select_for_update: Flag indicating whether the rows should be locked until end of transaction
""" """
def get_child_descriptors(descriptor, depth, descriptor_filter): def get_child_descriptors(descriptor, depth, descriptor_filter):
if descriptor_filter(descriptor): if descriptor_filter(descriptor):
descriptors = [descriptor] descriptors = [descriptor]
else: else:
descriptors = [] descriptors = []
if depth is None or depth > 0: if depth is None or depth > 0:
new_depth = depth - 1 if depth is not None else depth new_depth = depth - 1 if depth is not None else depth
for child in descriptor.get_children(): for child in descriptor.get_children():
descriptors.extend(get_child_descriptors(child, new_depth, descriptor_filter)) descriptors.extend(get_child_descriptors(child, new_depth, descriptor_filter))
return descriptors return descriptors
descriptors = get_child_descriptors(descriptor, depth, descriptor_filter) 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): def _get_module_state_keys(self, descriptors):
''' '''
Get a list of the state_keys needed for StudentModules Get a list of the state_keys needed for StudentModules
required for this module descriptor 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 should be cached
''' '''
keys = [] keys = []
for descriptor in descriptors: for descriptor in descriptors:
if descriptor.stores_state: if descriptor.stores_state:
keys.append(descriptor.location.url()) keys.append(descriptor.location.url())
shared_state_key = getattr(descriptor, 'shared_state_key', None) shared_state_key = getattr(descriptor, 'shared_state_key', None)
if shared_state_key is not None: if shared_state_key is not None:
keys.append(shared_state_key) keys.append(shared_state_key)
return keys 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 cache -- list of student modules
returns first found object, or None returns first found object, or None
''' '''
for o in self.cache: 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 o
return None return None
......
...@@ -70,7 +70,8 @@ def toc_for_course(user, request, course, active_chapter, active_section, course ...@@ -70,7 +70,8 @@ def toc_for_course(user, request, course, active_chapter, active_section, course
None if this is not the case. 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) course = get_module(user, request, course.location, student_module_cache, course_id)
chapters = list() chapters = list()
...@@ -159,12 +160,13 @@ def get_module(user, request, location, student_module_cache, course_id, positio ...@@ -159,12 +160,13 @@ def get_module(user, request, location, student_module_cache, course_id, positio
shared_module = None shared_module = None
if user.is_authenticated(): if user.is_authenticated():
if descriptor.stores_state: if descriptor.stores_state:
instance_module = student_module_cache.lookup(descriptor.category, instance_module = student_module_cache.lookup(
descriptor.location.url()) course_id, descriptor.category, descriptor.location.url())
shared_state_key = getattr(descriptor, 'shared_state_key', None) shared_state_key = getattr(descriptor, 'shared_state_key', None)
if shared_state_key is not 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) shared_state_key)
...@@ -241,7 +243,7 @@ def get_module(user, request, location, student_module_cache, course_id, positio ...@@ -241,7 +243,7 @@ def get_module(user, request, location, student_module_cache, course_id, positio
return module 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, Returns instance_module is a StudentModule specific to this module for this student,
or None if this is an anonymous user or None if this is an anonymous user
...@@ -252,11 +254,12 @@ def get_instance_module(user, module, student_module_cache): ...@@ -252,11 +254,12 @@ def get_instance_module(user, module, student_module_cache):
+ str(module.id) + " which does not store state.") + str(module.id) + " which does not store state.")
return None return None
instance_module = student_module_cache.lookup(module.category, instance_module = student_module_cache.lookup(
module.location.url()) course_id, module.category, module.location.url())
if not instance_module: if not instance_module:
instance_module = StudentModule( instance_module = StudentModule(
course_id=course_id,
student=user, student=user,
module_type=module.category, module_type=module.category,
module_state_key=module.id, module_state_key=module.id,
...@@ -285,6 +288,7 @@ def get_shared_instance_module(course_id, user, module, student_module_cache): ...@@ -285,6 +288,7 @@ def get_shared_instance_module(course_id, user, module, student_module_cache):
shared_state_key) shared_state_key)
if not shared_module: if not shared_module:
shared_module = StudentModule( shared_module = StudentModule(
course_id=course_id,
student=user, student=user,
module_type=descriptor.category, module_type=descriptor.category,
module_state_key=shared_state_key, module_state_key=shared_state_key,
...@@ -317,14 +321,14 @@ def xqueue_callback(request, course_id, userid, id, dispatch): ...@@ -317,14 +321,14 @@ def xqueue_callback(request, course_id, userid, id, dispatch):
# Retrieve target StudentModule # Retrieve target StudentModule
user = User.objects.get(id=userid) 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) user, modulestore().get_instance(course_id, id), depth=0, select_for_update=True)
instance = get_module(user, request, id, student_module_cache, course_id) instance = get_module(user, request, id, student_module_cache, course_id)
if instance is None: if instance is None:
log.debug("No module {0} for user {1}--access denied?".format(id, user)) log.debug("No module {0} for user {1}--access denied?".format(id, user))
raise Http404 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: if instance_module is None:
log.debug("Couldn't find instance of module '%s' for user '%s'", id, user) 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): ...@@ -387,7 +391,7 @@ def modx_dispatch(request, dispatch, location, course_id):
return HttpResponse(json.dumps({'success': file_too_big_msg})) return HttpResponse(json.dumps({'success': file_too_big_msg}))
p[fileinput_id] = inputfiles 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)) request.user, modulestore().get_instance(course_id, location))
instance = get_module(request.user, request, location, student_module_cache, course_id) instance = get_module(request.user, request, location, student_module_cache, course_id)
...@@ -397,7 +401,7 @@ def modx_dispatch(request, dispatch, location, 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)) log.debug("No module {0} for user {1}--access denied?".format(location, user))
raise Http404 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) 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) # 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, ...@@ -149,8 +149,7 @@ def index(request, course_id, chapter=None, section=None,
section_descriptor = get_section(course, chapter, section) section_descriptor = get_section(course, chapter, section)
if section_descriptor is not None: if section_descriptor is not None:
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents( student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
request.user, course_id, request.user, section_descriptor)
section_descriptor)
module = get_module(request.user, request, module = get_module(request.user, request,
section_descriptor.location, section_descriptor.location,
student_module_cache, course_id) student_module_cache, course_id)
...@@ -233,6 +232,19 @@ def course_info(request, course_id): ...@@ -233,6 +232,19 @@ def course_info(request, course_id):
return render_to_response('courseware/info.html', {'course': course, return render_to_response('courseware/info.html', {'course': course,
'staff_access': staff_access,}) '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): def registered_for_course(course, user):
'''Return CourseEnrollment if user is registered for course, else False''' '''Return CourseEnrollment if user is registered for course, else False'''
...@@ -310,7 +322,8 @@ def progress(request, course_id, student_id=None): ...@@ -310,7 +322,8 @@ def progress(request, course_id, student_id=None):
raise Http404 raise Http404
student = User.objects.get(id=int(student_id)) 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, course_module = get_module(request.user, request, course.location,
student_module_cache, course_id) student_module_cache, course_id)
......
...@@ -8,6 +8,7 @@ from django.contrib.auth.models import User ...@@ -8,6 +8,7 @@ from django.contrib.auth.models import User
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from courseware.courses import get_course_with_access from courseware.courses import get_course_with_access
from courseware.access import has_access
from urllib import urlencode from urllib import urlencode
from django_comment_client.permissions import check_permissions_by_view from django_comment_client.permissions import check_permissions_by_view
...@@ -156,8 +157,9 @@ def forum_form_discussion(request, course_id): ...@@ -156,8 +157,9 @@ def forum_form_discussion(request, course_id):
'content': content, 'content': content,
'recent_active_threads': recent_active_threads, 'recent_active_threads': recent_active_threads,
'trending_tags': trending_tags, '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) return render_to_response('discussion/index.html', context)
def render_single_thread(request, discussion_id, course_id, thread_id): 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 ...@@ -11,9 +11,9 @@ from util.cache import cache
@receiver(post_save, sender=CourseEnrollment) @receiver(post_save, sender=CourseEnrollment)
def assign_default_role(sender, instance, **kwargs): def assign_default_role(sender, instance, **kwargs):
if instance.user.is_staff: 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: 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)) logging.info("assign_default_role: adding %s as %s" % (instance.user, role))
instance.user.roles.add(role) instance.user.roles.add(role)
......
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils import unittest from django.test import TestCase
from student.models import CourseEnrollment, \ from student.models import CourseEnrollment, \
replicate_enrollment_save, \ replicate_enrollment_save, \
replicate_enrollment_delete, \ replicate_enrollment_delete, \
...@@ -13,120 +13,11 @@ import random ...@@ -13,120 +13,11 @@ import random
from .permissions import has_permission from .permissions import has_permission
from .models import Role, Permission from .models import Role, Permission
# code adapted from https://github.com/justquick/django-activity-stream/issues/88 class PermissionsTestCase(TestCase):
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):
def random_str(self, length=15, chars=string.ascii_uppercase + string.digits): def random_str(self, length=15, chars=string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for x in range(length)) return ''.join(random.choice(chars) for x in range(length))
def setUp(self): 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.course_id = "MITx/6.002x/2012_Fall"
self.moderator_role = Role.objects.get_or_create(name="Moderator", course_id=self.course_id)[0] self.moderator_role = Role.objects.get_or_create(name="Moderator", course_id=self.course_id)[0]
...@@ -144,9 +35,10 @@ class PermissionsTestCase(NoSignalTestCase): ...@@ -144,9 +35,10 @@ class PermissionsTestCase(NoSignalTestCase):
def tearDown(self): def tearDown(self):
self.student_enrollment.delete() self.student_enrollment.delete()
self.moderator_enrollment.delete() self.moderator_enrollment.delete()
self.student.delete()
self.moderator.delete() # Do we need to have this? We shouldn't be deleting students, ever
super(PermissionsTestCase, self).tearDown() # self.student.delete()
# self.moderator.delete()
def testDefaultRoles(self): def testDefaultRoles(self):
self.assertTrue(self.student_role in self.student.roles.all()) self.assertTrue(self.student_role in self.student.roles.all())
......
...@@ -6,41 +6,53 @@ ...@@ -6,41 +6,53 @@
import os, sys, string, re import os, sys, string, re
sys.path.append(os.path.abspath('.')) from django.core.management.base import BaseCommand
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.conf import settings
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from path import path from path import path
from lxml import etree from lxml import etree
data_dir = settings.DATA_DIR def create_groups():
print "data_dir = %s" % data_dir '''
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): if course_dir.startswith('.'):
# print course_dir continue
if not os.path.isdir(path(data_dir) / course_dir): if not os.path.isdir(path(data_dir) / course_dir):
continue 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' create_group('staff_%s' % course) # staff group
coursexml = etree.parse(cxfn) create_group('instructor_%s' % course) # instructor group (can manage staff group list)
cxmlroot = coursexml.getroot()
course = cxmlroot.get('course') def create_group(gname):
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): if Group.objects.filter(name=gname):
print "group exists for %s" % gname print " group exists for %s" % gname
continue return
g = Group(name=gname) g = Group(name=gname)
g.save() 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 ...@@ -8,21 +8,13 @@ import os, sys, string, re
import datetime import datetime
from getpass import getpass from getpass import getpass
import json import json
from random import choice
import readline import readline
sys.path.append(os.path.abspath('.')) from django.core.management.base import BaseCommand
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 student.models import UserProfile, Registration
from external_auth.models import ExternalAuthMap from external_auth.models import ExternalAuthMap
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from random import choice
class MyCompleter(object): # Custom completer class MyCompleter(object): # Custom completer
...@@ -47,103 +39,108 @@ def GenPasswd(length=8, chars=string.letters + string.digits): ...@@ -47,103 +39,108 @@ def GenPasswd(length=8, chars=string.letters + string.digits):
return ''.join([choice(chars) for i in range(length)]) return ''.join([choice(chars) for i in range(length)])
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# main # main command
while True: class Command(BaseCommand):
uname = raw_input('username: ') help = "Create user, interactively; can add ExternalAuthMap for MIT user if email@MIT.EDU resolves properly."
if User.objects.filter(username=uname):
print "username %s already taken" % uname
else:
break
make_eamap = False def handle(self, *args, **options):
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: while True:
email = raw_input('email: ') uname = raw_input('username: ')
if User.objects.filter(email=email): if User.objects.filter(username=uname):
print "email %s already taken" % email 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: 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) user = User(username=uname, email=email, is_active=True)
try: user.set_password(password)
user.save() try:
except IntegrityError: user.save()
print "Oops, failed to create user %s, IntegrityError" % user except IntegrityError:
raise print "Oops, failed to create user %s, IntegrityError" % user
raise
r = Registration()
r.register(user) r = Registration()
r.register(user)
up = UserProfile(user=user)
up.name = name up = UserProfile(user=user)
up.save() 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) if make_eamap:
eamap = ExternalAuthMap(external_id = email, credentials = "/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN=%s/emailAddress=%s" % (name,email)
external_email = email, eamap = ExternalAuthMap(external_id = email,
external_domain = mit_domain, external_email = email,
external_name = name, external_domain = mit_domain,
internal_password = password, external_name = name,
external_credentials = json.dumps(credentials), internal_password = password,
) external_credentials = json.dumps(credentials),
eamap.user = user )
eamap.dtsignup = datetime.datetime.now() eamap.user = user
eamap.save() eamap.dtsignup = datetime.datetime.now()
eamap.save()
print "User %s created successfully!" % user
print "User %s created successfully!" % user
if not raw_input('Add user %s to any groups? [n] ' % user).lower()=='y':
sys.exit(0) if not raw_input('Add user %s to any groups? [n] ' % user).lower()=='y':
sys.exit(0)
print "Here are the groups available:"
print "Here are the groups available:"
groups = [str(g.name) for g in Group.objects.all()]
print groups groups = [str(g.name) for g in Group.objects.all()]
print groups
completer = MyCompleter(groups)
readline.set_completer(completer.complete) completer = MyCompleter(groups)
readline.parse_and_bind('tab: complete') 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): ") while True:
if not gname: gname = raw_input("Add group (tab to autocomplete, empty line to end): ")
break if not gname:
if not gname in groups: break
print "Unknown group %s" % gname if not gname in groups:
continue print "Unknown group %s" % gname
g = Group.objects.get(name=gname) continue
user.groups.add(g) g = Group.objects.get(name=gname)
print "Added %s to group %s" % (user,g) user.groups.add(g)
print "Added %s to group %s" % (user,g)
print "Done!"
print "Done!"
...@@ -2,13 +2,21 @@ ...@@ -2,13 +2,21 @@
# migration tools for content team to go from stable-edx4edx to LMS+CMS # migration tools for content team to go from stable-edx4edx to LMS+CMS
# #
import json
import logging import logging
import os
from pprint import pprint from pprint import pprint
import xmodule.modulestore.django as xmodule_django import xmodule.modulestore.django as xmodule_django
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from django.http import HttpResponse from django.http import HttpResponse
from django.conf import settings 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") log = logging.getLogger("mitx.lms_migrate")
LOCAL_DEBUG = True LOCAL_DEBUG = True
...@@ -18,6 +26,15 @@ def escape(s): ...@@ -18,6 +26,15 @@ def escape(s):
"""escape HTML special characters in string""" """escape HTML special characters in string"""
return str(s).replace('<','&lt;').replace('>','&gt;') 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): def manage_modulestores(request,reload_dir=None):
''' '''
Manage the static in-memory modulestores. Manage the static in-memory modulestores.
...@@ -32,9 +49,7 @@ def manage_modulestores(request,reload_dir=None): ...@@ -32,9 +49,7 @@ def manage_modulestores(request,reload_dir=None):
#---------------------------------------- #----------------------------------------
# check on IP address of requester # check on IP address of requester
ip = request.META.get('HTTP_X_REAL_IP','') # nginx reverse proxy ip = getip(request)
if not ip:
ip = request.META.get('REMOTE_ADDR','None')
if LOCAL_DEBUG: if LOCAL_DEBUG:
html += '<h3>IP address: %s ' % ip html += '<h3>IP address: %s ' % ip
...@@ -48,7 +63,7 @@ def manage_modulestores(request,reload_dir=None): ...@@ -48,7 +63,7 @@ def manage_modulestores(request,reload_dir=None):
html += 'Permission denied' html += 'Permission denied'
html += "</body></html>" html += "</body></html>"
log.debug('request denied, ALLOWED_IPS=%s' % ALLOWED_IPS) log.debug('request denied, ALLOWED_IPS=%s' % ALLOWED_IPS)
return HttpResponse(html) return HttpResponse(html, status=403)
#---------------------------------------- #----------------------------------------
# reload course if specified # reload course if specified
...@@ -108,3 +123,66 @@ def manage_modulestores(request,reload_dir=None): ...@@ -108,3 +123,66 @@ def manage_modulestores(request,reload_dir=None):
html += "</body></html>" html += "</body></html>"
return HttpResponse(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 ...@@ -39,7 +39,7 @@ def update_template_dictionary(dictionary, request=None, course=None, article=No
if course: if course:
dictionary['course'] = course dictionary['course'] = course
if 'namespace' not in dictionary: if 'namespace' not in dictionary:
dictionary['namespace'] = course.wiki_namespace dictionary['namespace'] = "edX"
else: else:
dictionary['course'] = None dictionary['course'] = None
...@@ -99,7 +99,7 @@ def root_redirect(request, course_id=None): ...@@ -99,7 +99,7 @@ def root_redirect(request, course_id=None):
course = get_opt_course_with_access(request.user, course_id, 'load') course = get_opt_course_with_access(request.user, course_id, 'load')
#TODO: Add a default namespace to settings. #TODO: Add a default namespace to settings.
namespace = course.wiki_namespace if course else "edX" namespace = "edX"
try: try:
root = Article.get_root(namespace) root = Article.get_root(namespace)
...@@ -479,7 +479,7 @@ def not_found(request, article_path, course): ...@@ -479,7 +479,7 @@ def not_found(request, article_path, course):
"""Generate a NOT FOUND message for some URL""" """Generate a NOT FOUND message for some URL"""
d = {'wiki_err_notfound': True, d = {'wiki_err_notfound': True,
'article_path': article_path, 'article_path': article_path,
'namespace': course.wiki_namespace} 'namespace': "edX"}
update_template_dictionary(d, request, course) update_template_dictionary(d, request, course)
return render_to_response('simplewiki/simplewiki_error.html', d) return render_to_response('simplewiki/simplewiki_error.html', d)
......
...@@ -47,6 +47,7 @@ LOGGING = get_logger_config(LOG_DIR, ...@@ -47,6 +47,7 @@ LOGGING = get_logger_config(LOG_DIR,
syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514), syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514),
debug=False) debug=False)
COURSE_LISTINGS = ENV_TOKENS['COURSE_LISTINGS']
############################## SECURE AUTH ITEMS ############################### ############################## SECURE AUTH ITEMS ###############################
# Secret things: passwords, access keys, etc. # Secret things: passwords, access keys, etc.
......
...@@ -55,6 +55,10 @@ MITX_FEATURES = { ...@@ -55,6 +55,10 @@ MITX_FEATURES = {
# course_ids (see dev_int.py for an example) # course_ids (see dev_int.py for an example)
'SUBDOMAIN_COURSE_LISTINGS' : False, '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_TEXTBOOK' : True,
'ENABLE_DISCUSSION' : False, 'ENABLE_DISCUSSION' : False,
'ENABLE_DISCUSSION_SERVICE': True, 'ENABLE_DISCUSSION_SERVICE': True,
...@@ -260,6 +264,14 @@ USE_L10N = True ...@@ -260,6 +264,14 @@ USE_L10N = True
# Messages # Messages
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' 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 ####################################### #################################### AWS #######################################
# S3BotoStorage insists on a timeout for uploaded assets. We should make it # S3BotoStorage insists on a timeout for uploaded assets. We should make it
# permanent instead, but rather than trying to figure out exactly where that # permanent instead, but rather than trying to figure out exactly where that
...@@ -304,6 +316,8 @@ SIMPLE_WIKI_REQUIRE_LOGIN_VIEW = False ...@@ -304,6 +316,8 @@ SIMPLE_WIKI_REQUIRE_LOGIN_VIEW = False
################################# WIKI ################################### ################################# WIKI ###################################
WIKI_ACCOUNT_HANDLING = False WIKI_ACCOUNT_HANDLING = False
WIKI_EDITOR = 'course_wiki.editors.CodeMirror' 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 ###################################
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
...@@ -569,7 +583,7 @@ INSTALLED_APPS = ( ...@@ -569,7 +583,7 @@ INSTALLED_APPS = (
'course_wiki', # Our customizations 'course_wiki', # Our customizations
'mptt', 'mptt',
'sekizai', 'sekizai',
'wiki.plugins.attachments', #'wiki.plugins.attachments',
'wiki.plugins.notifications', 'wiki.plugins.notifications',
'course_wiki.plugins.markdownedx', 'course_wiki.plugins.markdownedx',
......
...@@ -73,6 +73,8 @@ MITX_FEATURES['ENABLE_LMS_MIGRATION'] = True ...@@ -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['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' 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'] LMS_MIGRATION_ALLOWED_IPS = ['127.0.0.1']
################################ OpenID Auth ################################# ################################ OpenID Auth #################################
......
...@@ -17,14 +17,19 @@ MITX_FEATURES['ENABLE_TEXTBOOK'] = False ...@@ -17,14 +17,19 @@ MITX_FEATURES['ENABLE_TEXTBOOK'] = False
MITX_FEATURES['ENABLE_DISCUSSION'] = 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['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() myhost = socket.gethostname()
if ('edxvm' in myhost) or ('ocw' in myhost): if ('edxvm' in myhost) or ('ocw' in myhost):
MITX_FEATURES['DISABLE_LOGIN_BUTTON'] = True # auto-login with MIT certificate 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_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): if ('domU' in myhost):
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 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['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 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 ...@@ -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') ]) 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') ]) 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 = { ...@@ -84,11 +84,17 @@ DATABASES = {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "course1.db", 'NAME': ENV_ROOT / "db" / "course1.db",
}, },
'edx/full/6.002_Spring_2012': { 'edx/full/6.002_Spring_2012': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "course2.db", 'NAME': ENV_ROOT / "db" / "course2.db",
} },
'edX/toy/TT_2012_Fall': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "course3.db",
},
} }
CACHES = { CACHES = {
......
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
// pages // pages
@import "course/info"; @import "course/info";
@import "course/syllabus"; // TODO arjun replace w/ custom tabs, see courseware/courses.py
@import "course/textbook"; @import "course/textbook";
@import "course/profile"; @import "course/profile";
@import "course/gradebook"; @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 { ...@@ -58,18 +58,20 @@ div.course-wrapper {
@extend h1.top-header; @extend h1.top-header;
@include border-radius(0 4px 0 0); @include border-radius(0 4px 0 0);
margin-bottom: -16px; margin-bottom: -16px;
border-bottom: 0;
h1 { h1 {
margin: 0; margin: 0;
font-size: 1em;
} }
h2 { h2 {
float: right; float: right;
margin-right: 0; margin: 12px 0 0;
margin-top: 8px;
text-align: right; text-align: right;
padding-right: 0; padding-right: 0;
border-right: 0; border-right: 0;
font-size: em(14, 24);
} }
} }
......
...@@ -274,6 +274,17 @@ ...@@ -274,6 +274,17 @@
} }
} }
} }
.prerequisites, .syllabus {
ul {
li {
font: normal 1em/1.6em $serif;
}
ul {
margin: 5px 0px 10px;
}
}
}
.faq { .faq {
@include clearfix; @include clearfix;
......
...@@ -19,6 +19,9 @@ def url_class(url): ...@@ -19,6 +19,9 @@ def url_class(url):
<ol class="course-tabs"> <ol class="course-tabs">
<li class="courseware"><a href="${reverse('courseware', args=[course.id])}" class="${url_class('courseware')}">Courseware</a></li> <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> <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 user.is_authenticated():
% if settings.MITX_FEATURES.get('ENABLE_TEXTBOOK'): % if settings.MITX_FEATURES.get('ENABLE_TEXTBOOK'):
% for index, textbook in enumerate(course.textbooks): % for index, textbook in enumerate(course.textbooks):
...@@ -27,7 +30,7 @@ def url_class(url): ...@@ -27,7 +30,7 @@ def url_class(url):
% endif % endif
% if settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'): % 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="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 % endif
% if settings.MITX_FEATURES.get('ENABLE_DISCUSSION'): % if settings.MITX_FEATURES.get('ENABLE_DISCUSSION'):
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
<%static:js group='courseware'/> <%static:js group='courseware'/>
<%include file="discussion/_js_dependencies.html" /> <%include file="../discussion/_js_dependencies.html" />
<%include file="/mathjax_include.html" /> <%include file="/mathjax_include.html" />
<!-- TODO: http://docs.jquery.com/Plugins/Validation --> <!-- TODO: http://docs.jquery.com/Plugins/Validation -->
...@@ -59,8 +59,8 @@ ...@@ -59,8 +59,8 @@
<div id="course-errors"> <div id="course-errors">
<ul> <ul>
% for (msg, err) in course_errors: % for (msg, err) in course_errors:
<li>${msg} <li>${msg | h}
<ul><li><pre>${err}</pre></li></ul> <ul><li><pre>${err | h}</pre></li></ul>
</li> </li>
% endfor % endfor
</ul> </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 @@ ...@@ -107,6 +107,21 @@
</section> </section>
% endif % 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>
</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"> <nav aria-label="Section Navigation" class="sequence-nav">
<ol id="sequence-list"> <ol id="sequence-list">
% for idx, item in enumerate(items): % for idx, item in enumerate(items):
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
</nav> </nav>
% for item in items: % for item in items:
<div class="seq_contents">${item['content'] | h}</div> <div class="seq_contents tex2jax_ignore">${item['content'] | h}</div>
% endfor % endfor
<div id="seq_content"></div> <div id="seq_content"></div>
......
...@@ -69,6 +69,7 @@ ${"Edit " + wiki_title + " - " if wiki_title is not UNDEFINED else ""}MITx 6.002 ...@@ -69,6 +69,7 @@ ${"Edit " + wiki_title + " - " if wiki_title is not UNDEFINED else ""}MITx 6.002
%else: %else:
<input type="submit" id="submit_edit" name="edit" value="Save Changes" /> <input type="submit" id="submit_edit" name="edit" value="Save Changes" />
<input type="submit" id="submit_delete" name="delete" value="Delete article" /> <input type="submit" id="submit_delete" name="delete" value="Delete article" />
%endif
<%include file="simplewiki_instructions.html"/> <%include file="simplewiki_instructions.html"/>
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<table border="1"><tr><th>datetime</th><th>username</th><th>ipaddr</th><th>source</th><th>type</th></tr> <table border="1"><tr><th>datetime</th><th>username</th><th>ipaddr</th><th>source</th><th>type</th></tr>
% for rec in records: % for rec in records:
<tr> <tr>
<td>${rec.time}</td> <td>${rec.dtstr}</td>
<td>${rec.username}</td> <td>${rec.username}</td>
<td>${rec.ip}</td> <td>${rec.ip}</td>
<td>${rec.event_source}</td> <td>${rec.event_source}</td>
......
...@@ -52,6 +52,9 @@ ...@@ -52,6 +52,9 @@
</style> </style>
{% endaddtoblock %} {% 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"> <form method="GET">
<div class="tab-content" style="overflow: visible;"> <div class="tab-content" style="overflow: visible;">
...@@ -60,20 +63,14 @@ ...@@ -60,20 +63,14 @@
<div class="accordion-group"> <div class="accordion-group">
<div class="accordion-heading"> <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 }}'))"> <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 %} <span class="icon-plus"></span>
{% if revision == article.current_revision %} {% include "wiki/includes/revision_info.html" with current_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 %}
<div style="color: #CCC;"> <div style="color: #CCC;">
<small> <small>
{% if revision.user_message %} {% if revision.user_message %}
{{ revision.user_message }} {{ revision.user_message }}
{% elif revision.automatic_log %}
{{ revision.automatic_log }}
{% else %} {% else %}
({% trans "no log message" %}) ({% trans "no log message" %})
{% endif %} {% endif %}
...@@ -84,21 +81,12 @@ ...@@ -84,21 +81,12 @@
<div class="bar" style="width: 100%;"></div> <div class="bar" style="width: 100%;"></div>
</div> </div>
<div class="pull-right" style="vertical-align: middle; margin: 8px 3px;"> <div class="pull-right" style="vertical-align: middle; margin: 8px 3px;">
{% if revision == article.current_revision %} {% if not revision == article.current_revision %}
<a href="#" class="btn disabled">
<span class="icon-lock"></span>
{% trans "Preview this version" %}
</a>
{% else %}
<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 %}')"> <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> <span class="icon-eye-open"></span>
{% trans "Preview this version" %} {% trans "Preview this revision" %}
</button> </button>
{% endif %} {% 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 %} {% 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 %}" /> <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 @@ ...@@ -157,7 +145,7 @@
<input type="hidden" name="r" value="" /> <input type="hidden" name="r" value="" />
<div class="modal hide fade" id="previewModal"> <div class="modal hide fade" id="previewModal">
<div class="modal-body"> <div class="modal-body">
<iframe name="previewWindow" frameborder="0"></iframe> <iframe name="previewWindow"></iframe>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<a href="#" class="btn btn-large" data-dismiss="modal"> <a href="#" class="btn btn-large" data-dismiss="modal">
...@@ -184,7 +172,7 @@ ...@@ -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> <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>
<div class="modal-body"> <div class="modal-body">
<iframe name="mergeWindow" frameborder="0"></iframe> <iframe name="mergeWindow"></iframe>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<a href="#" class="btn btn-large" data-dismiss="modal"> <a href="#" class="btn btn-large" data-dismiss="modal">
......
{% load i18n %}{% load url from future %} ## mako
{% if urlpath %} <%! from django.core.urlresolvers import reverse %>
%if urlpath is not Undefined and urlpath:
<header> <header>
<ul class="breadcrumb pull-left" class=""> <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> # The create button links to the highest ancestor we have edit priveleges to
{% endfor %} create_article_root = None
<li class="active"><a href="{% url 'wiki:get' path=urlpath.path %}">{{ article.current_revision.title }}</a></li> %>
%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> </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"> <div class="global-functions pull-right">
<form class="search-wiki pull-left"> <!-- <form class="search-wiki pull-left">
<input type="search" placeholder="search wiki" /> <input type="search" placeholder="search wiki" />
</form> </form> -->
<a class="add-article-btn btn pull-left" href="{% url 'wiki:create' path=urlpath.path %}" style="padding: 7px;"> %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> <span class="icon-plus"></span>
{% trans "Add article" %} Add article
</a> </a>
%endif
</div> </div>
</header> </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: ...@@ -126,6 +126,8 @@ if settings.COURSEWARE_ENABLED:
#Inside the course #Inside the course
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/info$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/info$',
'courseware.views.course_info', name="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>[^/]*)/$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book/(?P<book_index>[^/]*)/$',
'staticbook.views.index', name="book"), 'staticbook.views.index', name="book"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book/(?P<book_index>[^/]*)/(?P<page>[^/]*)$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book/(?P<book_index>[^/]*)/(?P<page>[^/]*)$',
...@@ -217,11 +219,14 @@ if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'): ...@@ -217,11 +219,14 @@ if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
urlpatterns += ( urlpatterns += (
url(r'^migrate/modules$', 'lms_migration.migrate.manage_modulestores'), url(r'^migrate/modules$', 'lms_migration.migrate.manage_modulestores'),
url(r'^migrate/reload/(?P<reload_dir>[^/]+)$', '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'): if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'):
urlpatterns += ( urlpatterns += (
url(r'^event_logs$', 'track.views.view_tracking_log'), url(r'^event_logs$', 'track.views.view_tracking_log'),
url(r'^event_logs/(?P<args>.+)$', 'track.views.view_tracking_log'),
) )
urlpatterns = patterns(*urlpatterns) urlpatterns = patterns(*urlpatterns)
......
-e git://github.com/MITx/django-staticfiles.git@6d2504e5c8#egg=django-staticfiles -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/MITx/django-pipeline.git#egg=django-pipeline
-e git://github.com/benjaoming/django-wiki.git@c145596#egg=django-wiki -e git://github.com/benjaoming/django-wiki.git@02275fb4#egg=django-wiki
-e git://github.com/dementrock/pystache_custom.git#egg=pystache_custom -e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
-e common/lib/capa -e common/lib/capa
-e common/lib/xmodule -e common/lib/xmodule
...@@ -28,6 +28,7 @@ django-override-settings ...@@ -28,6 +28,7 @@ django-override-settings
mock>=0.8, <0.9 mock>=0.8, <0.9
PyYAML PyYAML
South South
pytz
django-celery django-celery
django-countries django-countries
django-kombu 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