Commit bd536f8a by Tom Giannattasio

Merge branch 'master' into feature/tomg/fall-design

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