Commit f63862c7 by Bridger Maxwell

Merge remote-tracking branch 'origin/master' into feature/bridger/new_wiki

parents a04e4bfb 013ffb70
......@@ -249,7 +249,7 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
module.get_html = replace_static_urls(
wrap_xmodule(module.get_html, module, "xmodule_display.html"),
module.metadata['data_dir']
module.metadata['data_dir'], module
)
save_preview_state(request, preview_id, descriptor.location.url(),
module.get_instance_state(), module.get_shared_state())
......
......@@ -55,6 +55,17 @@ DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "cms.db",
},
# The following are for testing purposes...
'edX/toy/2012_Fall': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "course1.db",
},
'edx/full/6.002_Spring_2012': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "course2.db",
}
}
......
......@@ -2,7 +2,7 @@ describe "CMS", ->
beforeEach ->
CMS.unbind()
it "should iniitalize Models", ->
it "should initialize Models", ->
expect(CMS.Models).toBeDefined()
it "should initialize Views", ->
......
......@@ -11,14 +11,25 @@ describe "CMS.Models.Module", ->
@fakeModule = jasmine.createSpy("fakeModuleObject")
window.FakeModule = jasmine.createSpy("FakeModule").andReturn(@fakeModule)
@module = new CMS.Models.Module(type: "FakeModule")
@stubElement = $("<div>")
@module.loadModule(@stubElement)
@stubDiv = $('<div />')
@stubElement = $('<div class="xmodule_edit" />')
@stubElement.data('type', "FakeModule")
@stubDiv.append(@stubElement)
@module.loadModule(@stubDiv)
afterEach ->
window.FakeModule = undefined
it "initialize the module", ->
expect(window.FakeModule).toHaveBeenCalledWith(@stubElement)
expect(window.FakeModule).toHaveBeenCalled()
# Need to compare underlying nodes, because jquery selectors
# aren't equal even when they point to the same node.
# http://stackoverflow.com/questions/9505437/how-to-test-jquery-with-jasmine-for-element-id-if-used-as-this
expectedNode = @stubElement[0]
actualNode = window.FakeModule.mostRecentCall.args[0][0]
expect(actualNode).toEqual(expectedNode)
expect(@module.module).toEqual(@fakeModule)
describe "when the module does not exists", ->
......@@ -32,7 +43,8 @@ describe "CMS.Models.Module", ->
window.console = @previousConsole
it "print out error to log", ->
expect(window.console.error).toHaveBeenCalledWith("Unable to load HTML.")
expect(window.console.error).toHaveBeenCalled()
expect(window.console.error.mostRecentCall.args[0]).toMatch("^Unable to load")
describe "editUrl", ->
......
......@@ -8,11 +8,11 @@ describe "CMS.Views.ModuleEdit", ->
<a href="#" class="cancel">cancel</a>
<ol>
<li>
<a href="#" class="module-edit" data-id="i4x://mitx.edu/course/module" data-type="html">submodule</a>
<a href="#" class="module-edit" data-id="i4x://mitx/course/html/module" data-type="html">submodule</a>
</li>
</ol>
</div>
"""
""" #"
CMS.unbind()
describe "defaults", ->
......@@ -27,7 +27,7 @@ describe "CMS.Views.ModuleEdit", ->
@stubModule.editUrl.andReturn("/edit_item?id=stub_module")
new CMS.Views.ModuleEdit(el: $("#module-edit"), model: @stubModule)
it "load the edit from via ajax and pass to the model", ->
it "load the edit via ajax and pass to the model", ->
expect($.fn.load).toHaveBeenCalledWith("/edit_item?id=stub_module", jasmine.any(Function))
if $.fn.load.mostRecentCall
$.fn.load.mostRecentCall.args[1]()
......@@ -37,9 +37,9 @@ describe "CMS.Views.ModuleEdit", ->
beforeEach ->
@stubJqXHR = jasmine.createSpy("stubJqXHR")
@stubJqXHR.success = jasmine.createSpy("stubJqXHR.success").andReturn(@stubJqXHR)
@stubJqXHR.error= jasmine.createSpy("stubJqXHR.success").andReturn(@stubJqXHR)
@stubJqXHR.error = jasmine.createSpy("stubJqXHR.error").andReturn(@stubJqXHR)
@stubModule.save = jasmine.createSpy("stubModule.save").andReturn(@stubJqXHR)
new CMS.Views.ModuleEdit(el: $("#module-edit"), model: @stubModule)
new CMS.Views.ModuleEdit(el: $(".module-edit"), model: @stubModule)
spyOn(window, "alert")
$(".save-update").click()
......@@ -77,5 +77,5 @@ describe "CMS.Views.ModuleEdit", ->
expect(CMS.pushView).toHaveBeenCalledWith @view
expect(CMS.Views.ModuleEdit).toHaveBeenCalledWith model: @model
expect(CMS.Models.Module).toHaveBeenCalledWith
id: "i4x://mitx.edu/course/module"
id: "i4x://mitx/course/html/module"
type: "html"
describe "CMS.Views.Module", ->
beforeEach ->
setFixtures """
<div id="module" data-id="i4x://mitx.edu/course/module" data-type="html">
<div id="module" data-id="i4x://mitx/course/html/module" data-type="html">
<a href="#" class="module-edit">edit</a>
</div>
"""
......@@ -20,5 +20,5 @@ describe "CMS.Views.Module", ->
expect(CMS.replaceView).toHaveBeenCalledWith @view
expect(CMS.Views.ModuleEdit).toHaveBeenCalledWith model: @model
expect(CMS.Models.Module).toHaveBeenCalledWith
id: "i4x://mitx.edu/course/module"
id: "i4x://mitx/course/html/module"
type: "html"
describe "CMS.Views.Week", ->
beforeEach ->
setFixtures """
<div id="week" data-id="i4x://mitx.edu/course/week">
<div id="week" data-id="i4x://mitx/course/chapter/week">
<div class="editable"></div>
<textarea class="editable-textarea"></textarea>
<a href="#" class="week-edit" >edit</a>
......
......@@ -4,7 +4,8 @@ class CMS.Models.Module extends Backbone.Model
data: ''
loadModule: (element) ->
@module = XModule.loadModule($(element).find('.xmodule_edit'))
elt = $(element).find('.xmodule_edit').first()
@module = XModule.loadModule(elt)
editUrl: ->
"/edit_item?#{$.param(id: @get('id'))}"
......
......@@ -13,6 +13,16 @@ class CMS.Views.ModuleEdit extends Backbone.View
# Load preview modules
XModule.loadModules('display')
@enableDrag()
enableDrag: ->
# Enable dragging things in the #sortable div (if there is one)
if $("#sortable").length > 0
$("#sortable").sortable({
placeholder: "ui-state-highlight"
})
$("#sortable").disableSelection();
save: (event) ->
event.preventDefault()
......@@ -32,6 +42,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
cancel: (event) ->
event.preventDefault()
CMS.popView()
@enableDrag()
editSubmodule: (event) ->
event.preventDefault()
......@@ -42,3 +53,4 @@ class CMS.Views.ModuleEdit extends Backbone.View
id: $(event.target).data('id')
type: if moduleType == 'None' then null else moduleType
previewType: if previewType == 'None' then null else previewType
@enableDrag()
......@@ -33,7 +33,7 @@
<section class="modules">
<ol>
<li>
<ol>
<ol id="sortable">
% for child in module.get_children():
<li class="${module.category}">
<a href="#" class="module-edit"
......
from staticfiles.storage import staticfiles_storage
from mitxmako.shortcuts import render_to_string
from pipeline.conf import settings
from pipeline.packager import Packager
from pipeline.utils import guess_type
from static_replace import try_staticfiles_lookup
def compressed_css(package_name):
......@@ -25,9 +24,11 @@ def compressed_css(package_name):
def render_css(package, path):
template_name = package.template_name or "mako/css.html"
context = package.extra_context
url = try_staticfiles_lookup(path)
context.update({
'type': guess_type(path, 'text/css'),
'url': staticfiles_storage.url(path)
'url': url,
})
return render_to_string(template_name, context)
......@@ -58,7 +59,7 @@ def render_js(package, path):
context = package.extra_context
context.update({
'type': guess_type(path, 'text/javascript'),
'url': staticfiles_storage.url(path)
'url': try_staticfiles_lookup(path)
})
return render_to_string(template_name, context)
......
......@@ -3,7 +3,13 @@ from staticfiles.storage import staticfiles_storage
from pipeline_mako import compressed_css, compressed_js
%>
<%def name='url(file)'>${staticfiles_storage.url(file)}</%def>
<%def name='url(file)'>
<%
try:
url = staticfiles_storage.url(file)
except:
url = file
%>${url}</%def>
<%def name='css(group)'>
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
......
from staticfiles.storage import staticfiles_storage
import logging
import re
from staticfiles.storage import staticfiles_storage
from staticfiles import finders
from django.conf import settings
log = logging.getLogger(__name__)
def try_staticfiles_lookup(path):
"""
Try to lookup a path in staticfiles_storage. If it fails, return
a dead link instead of raising an exception.
"""
try:
url = staticfiles_storage.url(path)
except Exception as err:
log.warning("staticfiles_storage couldn't find path {}: {}".format(
path, str(err)))
# Just return the original path; don't kill everything.
url = path
return url
def replace(static_url, prefix=None):
if prefix is None:
......@@ -9,10 +29,19 @@ def replace(static_url, prefix=None):
prefix = prefix + '/'
quote = static_url.group('quote')
if staticfiles_storage.exists(static_url.group('rest')):
servable = (
# If in debug mode, we'll serve up anything that the finders can find
(settings.DEBUG and finders.find(static_url.group('rest'), True)) or
# Otherwise, we'll only serve up stuff that the storages can find
staticfiles_storage.exists(static_url.group('rest'))
)
if servable:
return static_url.group(0)
else:
url = staticfiles_storage.url(prefix + static_url.group('rest'))
# don't error if file can't be found
url = try_staticfiles_lookup(prefix + static_url.group('rest'))
return "".join([quote, url, quote])
......
##
## A script to create some dummy users
from django.core.management.base import BaseCommand
from django.conf import settings
from django.contrib.auth.models import User
from student.models import UserProfile, CourseEnrollment
from student.views import _do_create_account, get_random_post_override
def create(n, course_id):
"""Create n users, enrolling them in course_id if it's not None"""
for i in range(n):
(user, user_profile, _) = _do_create_account(get_random_post_override())
if course_id is not None:
CourseEnrollment.objects.create(user=user, course_id=course_id)
class Command(BaseCommand):
help = """Create N new users, with random parameters.
Usage: create_random_users.py N [course_id_to_enroll_in].
Examples:
create_random_users.py 1
create_random_users.py 10 MITx/6.002x/2012_Fall
create_random_users.py 100 HarvardX/CS50x/2012
"""
def handle(self, *args, **options):
if len(args) < 1 or len(args) > 2:
print Command.help
return
n = int(args[0])
course_id = args[1] if len(args) == 2 else None
create(n, course_id)
"""
WE'RE USING MIGRATIONS!
Models for Student Information
Replication Notes
In our live deployment, we intend to run in a scenario where there is a pool of
Portal servers that hold the canoncial user information and that user
information is replicated to slave Course server pools. Each Course has a set of
servers that serves only its content and has users that are relevant only to it.
We replicate the following tables into the Course DBs where the user is
enrolled. Only the Portal servers should ever write to these models.
* UserProfile
* CourseEnrollment
We do a partial replication of:
* User -- Askbot extends this and uses the extra fields, so we replicate only
the stuff that comes with basic django_auth and ignore the rest.)
There are a couple different scenarios:
1. There's an update of User or UserProfile -- replicate it to all Course DBs
that the user is enrolled in (found via CourseEnrollment).
2. There's a change in CourseEnrollment. We need to push copies of UserProfile,
CourseEnrollment, and the base fields in User
Migration Notes
If you make changes to this model, be sure to create an appropriate migration
file and check it in at the same time as your model changes. To do that,
......@@ -10,16 +35,41 @@ file and check it in at the same time as your model changes. To do that,
"""
from datetime import datetime
import json
import logging
import uuid
from django.db import models
from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from django_countries import CountryField
from xmodule.modulestore.django import modulestore
#from cache_toolbox import cache_model, cache_relation
log = logging.getLogger(__name__)
class UserProfile(models.Model):
"""This is where we store all the user demographic fields. We have a
separate table for this rather than extending the built-in Django auth_user.
Notes:
* Some fields are legacy ones from the first run of 6.002, from which
we imported many users.
* Fields like name and address are intentionally open ended, to account
for international variations. An unfortunate side-effect is that we
cannot efficiently sort on last names for instance.
Replication:
* Only the Portal servers should ever modify this information.
* All fields are replicated into relevant Course databases
Some of the fields are legacy ones that were captured during the initial
MITx fall prototype.
"""
class Meta:
db_table = "auth_userprofile"
......@@ -203,3 +253,154 @@ def add_user_to_default_group(user, group):
utg.save()
utg.users.add(User.objects.get(username=user))
utg.save()
########################## REPLICATION SIGNALS #################################
@receiver(post_save, sender=User)
def replicate_user_save(sender, **kwargs):
user_obj = kwargs['instance']
if not should_replicate(user_obj):
return
for course_db_name in db_names_to_replicate_to(user_obj.id):
replicate_user(user_obj, course_db_name)
@receiver(post_save, sender=CourseEnrollment)
def replicate_enrollment_save(sender, **kwargs):
"""This is called when a Student enrolls in a course. It has to do the
following:
1. Make sure the User is copied into the Course DB. It may already exist
(someone deleting and re-adding a course). This has to happen first or
the foreign key constraint breaks.
2. Replicate the CourseEnrollment.
3. Replicate the UserProfile.
"""
if not is_portal():
return
enrollment_obj = kwargs['instance']
log.debug("Replicating user because of new enrollment")
replicate_user(enrollment_obj.user, enrollment_obj.course_id)
log.debug("Replicating enrollment because of new enrollment")
replicate_model(CourseEnrollment.save, enrollment_obj, enrollment_obj.user_id)
log.debug("Replicating user profile because of new enrollment")
user_profile = UserProfile.objects.get(user_id=enrollment_obj.user_id)
replicate_model(UserProfile.save, user_profile, enrollment_obj.user_id)
@receiver(post_delete, sender=CourseEnrollment)
def replicate_enrollment_delete(sender, **kwargs):
enrollment_obj = kwargs['instance']
return replicate_model(CourseEnrollment.delete, enrollment_obj, enrollment_obj.user_id)
@receiver(post_save, sender=UserProfile)
def replicate_userprofile_save(sender, **kwargs):
"""We just updated the UserProfile (say an update to the name), so push that
change to all Course DBs that we're enrolled in."""
user_profile_obj = kwargs['instance']
return replicate_model(UserProfile.save, user_profile_obj, user_profile_obj.user_id)
######### Replication functions #########
USER_FIELDS_TO_COPY = ["id", "username", "first_name", "last_name", "email",
"password", "is_staff", "is_active", "is_superuser",
"last_login", "date_joined"]
def replicate_user(portal_user, course_db_name):
"""Replicate a User to the correct Course DB. This is more complicated than
it should be because Askbot extends the auth_user table and adds its own
fields. So we need to only push changes to the standard fields and leave
the rest alone so that Askbot changes at the Course DB level don't get
overridden.
"""
try:
course_user = User.objects.using(course_db_name).get(id=portal_user.id)
log.debug("User {0} found in Course DB, replicating fields to {1}"
.format(course_user, course_db_name))
except User.DoesNotExist:
log.debug("User {0} not found in Course DB, creating copy in {1}"
.format(portal_user, course_db_name))
course_user = User()
for field in USER_FIELDS_TO_COPY:
setattr(course_user, field, getattr(portal_user, field))
mark_handled(course_user)
course_user.save(using=course_db_name)
unmark(course_user)
def replicate_model(model_method, instance, user_id):
"""
model_method is the model action that we want replicated. For instance,
UserProfile.save
"""
if not should_replicate(instance):
return
course_db_names = db_names_to_replicate_to(user_id)
log.debug("Replicating {0} for user {1} to DBs: {2}"
.format(model_method, user_id, course_db_names))
mark_handled(instance)
for db_name in course_db_names:
model_method(instance, using=db_name)
unmark(instance)
######### Replication Helpers #########
def is_valid_course_id(course_id):
"""Right now, the only database that's not a course database is 'default'.
I had nicer checking in here originally -- it would scan the courses that
were in the system and only let you choose that. But it was annoying to run
tests with, since we don't have course data for some for our course test
databases. Hence the lazy version.
"""
return course_id != 'default'
def is_portal():
"""Are we in the portal pool? Only Portal servers are allowed to replicate
their changes. For now, only Portal servers see multiple DBs, so we use
that to decide."""
return len(settings.DATABASES) > 1
def db_names_to_replicate_to(user_id):
"""Return a list of DB names that this user_id is enrolled in."""
return [c.course_id
for c in CourseEnrollment.objects.filter(user_id=user_id)
if is_valid_course_id(c.course_id)]
def marked_handled(instance):
"""Have we marked this instance as being handled to avoid infinite loops
caused by saving models in post_save hooks for the same models?"""
return hasattr(instance, '_do_not_copy_to_course_db') and instance._do_not_copy_to_course_db
def mark_handled(instance):
"""You have to mark your instance with this function or else we'll go into
an infinite loop since we're putting listeners on Model saves/deletes and
the act of replication requires us to call the same model method.
We create a _replicated attribute to differentiate the first save of this
model vs. the duplicate save we force on to the course database. Kind of
a hack -- suggestions welcome.
"""
instance._do_not_copy_to_course_db = True
def unmark(instance):
"""If we don't unmark a model after we do replication, then consecutive
save() calls won't be properly replicated."""
instance._do_not_copy_to_course_db = False
def should_replicate(instance):
"""Should this instance be replicated? We need to be a Portal server and
the instance has to not have been marked_handled."""
if marked_handled(instance):
# Basically, avoid an infinite loop. You should
log.debug("{0} should not be replicated because it's been marked"
.format(instance))
return False
if not is_portal():
log.debug("{0} should not be replicated because we're not a portal."
.format(instance))
return False
return True
......@@ -4,13 +4,195 @@ when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
import logging
from datetime import datetime
from django.test import TestCase
from .models import User, UserProfile, CourseEnrollment, replicate_user, USER_FIELDS_TO_COPY
COURSE_1 = 'edX/toy/2012_Fall'
COURSE_2 = 'edx/full/6.002_Spring_2012'
log = logging.getLogger(__name__)
class ReplicationTest(TestCase):
multi_db = True
def test_user_replication(self):
"""Test basic user replication."""
portal_user = User.objects.create_user('rusty', 'rusty@edx.org', 'fakepass')
portal_user.first_name='Rusty'
portal_user.last_name='Skids'
portal_user.is_staff=True
portal_user.is_active=True
portal_user.is_superuser=True
portal_user.last_login=datetime(2012, 1, 1)
portal_user.date_joined=datetime(2011, 1, 1)
# This is an Askbot field and will break if askbot is not included
if hasattr(portal_user, 'seen_response_count'):
portal_user.seen_response_count = 10
portal_user.save(using='default')
# We replicate this user to Course 1, then pull the same user and verify
# that the fields copied over properly.
replicate_user(portal_user, COURSE_1)
course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
# Make sure the fields we care about got copied over for this user.
for field in USER_FIELDS_TO_COPY:
self.assertEqual(getattr(portal_user, field),
getattr(course_user, field),
"{0} not copied from {1} to {2}".format(
field, portal_user, course_user
))
# This hasattr lameness is here because we don't want this test to be
# triggered when we're being run by CMS tests (Askbot doesn't exist
# there, so the test will fail).
#
# seen_response_count isn't a field we care about, so it shouldn't have
# been copied over.
if hasattr(portal_user, 'seen_response_count'):
portal_user.seen_response_count = 20
replicate_user(portal_user, COURSE_1)
course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
self.assertEqual(portal_user.seen_response_count, 20)
self.assertEqual(course_user.seen_response_count, 0)
# Another replication should work for an email change however, since
# it's a field we care about.
portal_user.email = "clyde@edx.org"
replicate_user(portal_user, COURSE_1)
course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
self.assertEqual(portal_user.email, course_user.email)
# During this entire time, the user data should never have made it over
# to COURSE_2
self.assertRaises(User.DoesNotExist,
User.objects.using(COURSE_2).get,
id=portal_user.id)
def test_enrollment_for_existing_user_info(self):
"""Test the effect of Enrolling in a class if you've already got user
data to be copied over."""
# Create our User
portal_user = User.objects.create_user('jack', 'jack@edx.org', 'fakepass')
portal_user.first_name = "Jack"
portal_user.save()
# Set up our UserProfile info
portal_user_profile = UserProfile.objects.create(
user=portal_user,
name="Jack Foo",
level_of_education=None,
gender='m',
mailing_address=None,
goals="World domination",
)
portal_user_profile.save()
# Now let's see if creating a CourseEnrollment copies all the relevant
# data.
portal_enrollment = CourseEnrollment.objects.create(user=portal_user,
course_id=COURSE_1)
portal_enrollment.save()
# Grab all the copies we expect
course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
self.assertEquals(portal_user, course_user)
self.assertRaises(User.DoesNotExist,
User.objects.using(COURSE_2).get,
id=portal_user.id)
course_enrollment = CourseEnrollment.objects.using(COURSE_1).get(id=portal_enrollment.id)
self.assertEquals(portal_enrollment, course_enrollment)
self.assertRaises(CourseEnrollment.DoesNotExist,
CourseEnrollment.objects.using(COURSE_2).get,
id=portal_enrollment.id)
course_user_profile = UserProfile.objects.using(COURSE_1).get(id=portal_user_profile.id)
self.assertEquals(portal_user_profile, course_user_profile)
self.assertRaises(UserProfile.DoesNotExist,
UserProfile.objects.using(COURSE_2).get,
id=portal_user_profile.id)
log.debug("Make sure our seen_response_count is not replicated.")
if hasattr(portal_user, 'seen_response_count'):
portal_user.seen_response_count = 200
course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
self.assertEqual(portal_user.seen_response_count, 200)
self.assertEqual(course_user.seen_response_count, 0)
portal_user.save()
course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
self.assertEqual(portal_user.seen_response_count, 200)
self.assertEqual(course_user.seen_response_count, 0)
portal_user.email = 'jim@edx.org'
portal_user.save()
course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
self.assertEqual(portal_user.email, 'jim@edx.org')
self.assertEqual(course_user.email, 'jim@edx.org')
def test_enrollment_for_user_info_after_enrollment(self):
"""Test the effect of modifying User data after you've enrolled."""
# Create our User
portal_user = User.objects.create_user('patty', 'patty@edx.org', 'fakepass')
portal_user.first_name = "Patty"
portal_user.save()
# Set up our UserProfile info
portal_user_profile = UserProfile.objects.create(
user=portal_user,
name="Patty Foo",
level_of_education=None,
gender='f',
mailing_address=None,
goals="World peace",
)
portal_user_profile.save()
# Now let's see if creating a CourseEnrollment copies all the relevant
# data when things are saved.
portal_enrollment = CourseEnrollment.objects.create(user=portal_user,
course_id=COURSE_1)
portal_enrollment.save()
portal_user.last_name = "Bar"
portal_user.save()
portal_user_profile.gender = 'm'
portal_user_profile.save()
# Grab all the copies we expect, and make sure it doesn't end up in
# places we don't expect.
course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
self.assertEquals(portal_user, course_user)
self.assertRaises(User.DoesNotExist,
User.objects.using(COURSE_2).get,
id=portal_user.id)
course_enrollment = CourseEnrollment.objects.using(COURSE_1).get(id=portal_enrollment.id)
self.assertEquals(portal_enrollment, course_enrollment)
self.assertRaises(CourseEnrollment.DoesNotExist,
CourseEnrollment.objects.using(COURSE_2).get,
id=portal_enrollment.id)
course_user_profile = UserProfile.objects.using(COURSE_1).get(id=portal_user_profile.id)
self.assertEquals(portal_user_profile, course_user_profile)
self.assertRaises(UserProfile.DoesNotExist,
UserProfile.objects.using(COURSE_2).get,
id=portal_user_profile.id)
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.assertEqual(1 + 1, 2)
......@@ -34,7 +34,7 @@ def wrap_xmodule(get_html, module, template):
return _get_html
def replace_static_urls(get_html, prefix):
def replace_static_urls(get_html, prefix, module):
"""
Updates the supplied module with a new get_html function that wraps
the old get_html function and substitutes urls of the form /static/...
......@@ -69,14 +69,14 @@ def grade_histogram(module_id):
return grades
def add_histogram(get_html, module):
def add_histogram(get_html, module, user):
"""
Updates the supplied module with a new get_html function that wraps
the output of the old get_html function with additional information
for admin users only, including a histogram of student answers and the
definition of the xmodule
Does nothing if module is a SequenceModule
Does nothing if module is a SequenceModule or a VerticalModule.
"""
@wraps(get_html)
def _get_html():
......@@ -97,14 +97,20 @@ def add_histogram(get_html, module):
# doesn't like symlinks)
filepath = filename
data_dir = osfs.root_path.rsplit('/')[-1]
edit_link = "https://github.com/MITx/%s/tree/master/%s" % (data_dir,filepath)
giturl = module.metadata.get('giturl','https://github.com/MITx')
edit_link = "%s/%s/tree/master/%s" % (giturl,data_dir,filepath)
else:
edit_link = False
staff_context = {'definition': module.definition.get('data'),
'metadata': json.dumps(module.metadata, indent=4),
'element_id': module.location.html_id(),
'location': module.location,
'xqa_key': module.metadata.get('xqa_key',''),
'category': str(module.__class__.__name__),
'element_id': module.location.html_id().replace('-','_'),
'edit_link': edit_link,
'user': user,
'xqa_server' : settings.MITX_FEATURES.get('USE_XQA_SERVER','http://xqa:server@content-qa.mitx.mit.edu/xqa'),
'histogram': json.dumps(histogram),
'render_histogram': render_histogram,
'module_content': get_html()}
......
......@@ -39,9 +39,9 @@ import responsetypes
# dict of tagname, Response Class -- this should come from auto-registering
response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__])
entry_types = ['textline', 'schematic', 'textbox', 'imageinput', 'optioninput', 'choicegroup', 'radiogroup', 'checkboxgroup', 'filesubmission']
entry_types = ['textline', 'schematic', 'textbox', 'imageinput', 'optioninput', 'choicegroup', 'radiogroup', 'checkboxgroup', 'filesubmission', 'javascriptinput']
solution_types = ['solution'] # extra things displayed after "show answers" is pressed
response_properties = ["responseparam", "answer"] # these get captured as student responses
response_properties = ["codeparam", "responseparam", "answer"] # these get captured as student responses
# special problem tags which should be turned into innocuous HTML
html_transforms = {'problem': {'tag': 'div'},
......@@ -57,7 +57,7 @@ global_context = {'random': random,
'eia': eia}
# These should be removed from HTML output, including all subelements
html_problem_semantics = ["responseparam", "answer", "script", "hintgroup"]
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup"]
log = logging.getLogger('mitx.' + __name__)
......@@ -154,21 +154,10 @@ class LoncapaProblem(object):
def get_max_score(self):
'''
Return maximum score for this problem.
We do this by counting the number of answers available for each question
in the problem. If the Response for a question has a get_max_score() method
then we call that and add its return value to the count. That can be
used to give complex problems (eg programming questions) multiple points.
'''
maxscore = 0
for response, responder in self.responders.iteritems():
if hasattr(responder, 'get_max_score'):
try:
maxscore += responder.get_max_score()
except Exception:
log.debug('responder %s failed to properly return from get_max_score()' % responder) # FIXME
raise
else:
maxscore += len(self.responder_answers[response])
maxscore += responder.get_max_score()
return maxscore
def get_score(self):
......@@ -203,8 +192,9 @@ class LoncapaProblem(object):
cmap.update(self.correct_map)
for responder in self.responders.values():
if hasattr(responder, 'update_score'):
# Each LoncapaResponse will update the specific entries of 'cmap' that it's responsible for
cmap = responder.update_score(score_msg, cmap, queuekey)
# Each LoncapaResponse will update its specific entries in cmap
# cmap is passed by reference
responder.update_score(score_msg, cmap, queuekey)
self.correct_map.set_dict(cmap.get_dict())
return cmap
......@@ -228,14 +218,14 @@ class LoncapaProblem(object):
Calls the Response for each question in this problem, to do the actual grading.
'''
self.student_answers = convert_files_to_filenames(answers)
oldcmap = self.correct_map # old CorrectMap
newcmap = CorrectMap() # start new with empty CorrectMap
# log.debug('Responders: %s' % self.responders)
for responder in self.responders.values(): # Call each responsetype instance to do actual grading
if 'filesubmission' in responder.allowed_inputfields: # File objects are passed only if responsetype
if 'filesubmission' in responder.allowed_inputfields: # File objects are passed only if responsetype
# explicitly allows for file submissions
results = responder.evaluate_answers(answers, oldcmap)
else:
......@@ -294,9 +284,9 @@ class LoncapaProblem(object):
try:
ifp = self.system.filestore.open(file) # open using ModuleSystem OSFS filestore
except Exception as err:
log.error('Error %s in problem xml include: %s' % (
log.warning('Error %s in problem xml include: %s' % (
err, etree.tostring(inc, pretty_print=True)))
log.error('Cannot find file %s in %s' % (
log.warning('Cannot find file %s in %s' % (
file, self.system.filestore))
# if debugging, don't fail - just log error
# TODO (vshnayder): need real error handling, display to users
......@@ -305,11 +295,11 @@ class LoncapaProblem(object):
else:
continue
try:
incxml = etree.XML(ifp.read()) # read in and convert to XML
incxml = etree.XML(ifp.read()) # read in and convert to XML
except Exception as err:
log.error('Error %s in problem xml include: %s' % (
log.warning('Error %s in problem xml include: %s' % (
err, etree.tostring(inc, pretty_print=True)))
log.error('Cannot parse XML in %s' % (file))
log.warning('Cannot parse XML in %s' % (file))
# if debugging, don't fail - just log error
# TODO (vshnayder): same as above
if not self.system.get('DEBUG'):
......@@ -392,9 +382,10 @@ class LoncapaProblem(object):
context['script_code'] += code # store code source in context
try:
exec code in context, context # use "context" for global context; thus defs in code are global within code
except Exception:
except Exception as err:
log.exception("Error while execing script code: " + code)
raise responsetypes.LoncapaProblemError("Error while executing script code")
msg = "Error while executing script code: %s" % str(err).replace('<','&lt;')
raise responsetypes.LoncapaProblemError(msg)
finally:
sys.path = original_path
......
......@@ -11,6 +11,7 @@ Module containing the problem elements which render into input objects
- choicegroup
- radiogroup
- checkboxgroup
- javascriptinput
- imageinput (for clickable image)
- optioninput (for option list)
- filesubmission (upload a file)
......@@ -246,6 +247,34 @@ def checkboxgroup(element, value, status, render_template, msg=''):
html = render_template("choicegroup.html", context)
return etree.XML(html)
@register_render_function
def javascriptinput(element, value, status, render_template, msg='null'):
'''
Hidden field for javascript to communicate via; also loads the required
scripts for rendering the problem and passes data to the problem.
'''
eid = element.get('id')
params = element.get('params')
problem_state = element.get('problem_state')
display_class = element.get('display_class')
display_file = element.get('display_file')
# Need to provide a value that JSON can parse if there is no
# student-supplied value yet.
if value == "":
value = 'null'
escapedict = {'"': '&quot;'}
value = saxutils.escape(value, escapedict)
msg = saxutils.escape(msg, escapedict)
context = {'id': eid, 'params': params, 'display_file': display_file,
'display_class': display_class, 'problem_state': problem_state,
'value': value, 'evaluation': msg,
}
html = render_template("javascriptinput.html", context)
return etree.XML(html)
@register_render_function
def textline(element, value, status, render_template, msg=""):
......@@ -307,9 +336,19 @@ def filesubmission(element, value, status, render_template, msg=''):
Upload a single file (e.g. for programming assignments)
'''
eid = element.get('id')
context = { 'id': eid, 'state': status, 'msg': msg, 'value': value, }
# Check if problem has been queued
queue_len = 0
if status == 'incomplete': # Flag indicating that the problem has been queued, 'msg' is length of queue
status = 'queued'
queue_len = msg
msg = 'Submitted to grader. (Queue length: %s)' % queue_len
context = { 'id': eid, 'state': status, 'msg': msg, 'value': value,
'queue_len': queue_len
}
html = render_template("filesubmission.html", context)
return etree.XML(html)
return etree.XML(html)
#-----------------------------------------------------------------------------
......@@ -330,9 +369,16 @@ def textbox(element, value, status, render_template, msg=''):
if not value: value = element.text # if no student input yet, then use the default input given by the problem
# Check if problem has been queued
queue_len = 0
if status == 'incomplete': # Flag indicating that the problem has been queued, 'msg' is length of queue
status = 'queued'
queue_len = msg
msg = 'Submitted to grader. (Queue length: %s)' % queue_len
# For CodeMirror
mode = element.get('mode') or 'python' # mode, eg "python" or "xml"
linenumbers = element.get('linenumbers','true') # for CodeMirror
mode = element.get('mode','python')
linenumbers = element.get('linenumbers','true')
tabsize = element.get('tabsize','4')
tabsize = int(tabsize)
......@@ -340,6 +386,7 @@ def textbox(element, value, status, render_template, msg=''):
'mode': mode, 'linenumbers': linenumbers,
'rows': rows, 'cols': cols,
'hidden': hidden, 'tabsize': tabsize,
'queue_len': queue_len,
}
html = render_template("textbox.html", context)
try:
......
require('coffee-script');
var importAll = function (modulePath) {
module = require(modulePath);
for(key in module){
global[key] = module[key];
}
}
importAll("mersenne-twister-min");
importAll("xproblem");
generatorModulePath = process.argv[2];
dependencies = JSON.parse(process.argv[3]);
seed = process.argv[4];
params = JSON.parse(process.argv[5]);
if(seed==null){
seed = 4;
}else{
seed = parseInt(seed);
}
for(var i = 0; i < dependencies.length; i++){
importAll(dependencies[i]);
}
generatorModule = require(generatorModulePath);
generatorClass = generatorModule.generatorClass;
generator = new generatorClass(seed, params);
console.log(JSON.stringify(generator.generate()));
require('coffee-script');
var importAll = function (modulePath) {
module = require(modulePath);
for(key in module){
global[key] = module[key];
}
}
importAll("xproblem");
graderModulePath = process.argv[2];
dependencies = JSON.parse(process.argv[3]);
submission = JSON.parse(process.argv[4]);
problemState = JSON.parse(process.argv[5]);
params = JSON.parse(process.argv[6]);
for(var i = 0; i < dependencies.length; i++){
importAll(dependencies[i]);
}
graderModule = require(graderModulePath);
graderClass = graderModule.graderClass;
grader = new graderClass(submission, problemState, params);
console.log(JSON.stringify(grader.grade()));
console.log(JSON.stringify(grader.evaluation));
console.log(JSON.stringify(grader.solution));
......@@ -6,8 +6,9 @@
<span class="correct" id="status_${id}"></span>
% elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span>
% elif state == 'incomplete':
<span class="incorrect" id="status_${id}"></span>
% elif state == 'queued':
<span class="processing" id="status_${id}"></span>
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif
<span class="debug">(${state})</span>
<br/>
......
<form class="javascriptinput capa_inputtype">
<input type="hidden" name="input_${id}" id="input_${id}" class="javascriptinput_input"/>
<div class="javascriptinput_data" data-display_class="${display_class}"
data-problem_state="${problem_state}" data-params="${params}"
data-submission="${value}" data-evaluation="${evaluation}">
</div>
<div class="script_placeholder" data-src="/static/js/${display_file}"></div>
<div class="javascriptinput_container"></div>
</form>
......@@ -13,11 +13,12 @@
<span class="correct" id="status_${id}"></span>
% elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span>
% elif state == 'incomplete':
<span class="incorrect" id="status_${id}"></span>
% elif state == 'queued':
<span class="processing" id="status_${id}"></span>
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif
% if hidden:
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif
<br/>
<span class="debug">(${state})</span>
......
......@@ -11,18 +11,19 @@ DEFAULT = "_DEFAULT_GROUP"
def group_from_value(groups, v):
''' Given group: (('a',0.3),('b',0.4),('c',0.3)) And random value
"""
Given group: (('a', 0.3), ('b', 0.4), ('c', 0.3)) and random value v
in [0,1], return the associated group (in the above case, return
'a' if v<0.3, 'b' if 0.3<=v<0.7, and 'c' if v>0.7
'''
'a' if v < 0.3, 'b' if 0.3 <= v < 0.7, and 'c' if v > 0.7
"""
sum = 0
for (g, p) in groups:
sum = sum + p
if sum > v:
return g
# Round off errors might cause us to run to the end of the list
# If the do, return the last element
# Round off errors might cause us to run to the end of the list.
# If the do, return the last element.
return g
......@@ -31,8 +32,8 @@ class ABTestModule(XModule):
Implements an A/B test with an aribtrary number of competing groups
"""
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs)
if shared_state is None:
......
......@@ -11,13 +11,13 @@ from datetime import timedelta
from lxml import etree
from pkg_resources import resource_string
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError
from progress import Progress
from capa.capa_problem import LoncapaProblem
from capa.responsetypes import StudentInputError
from capa.util import convert_files_to_filenames
from progress import Progress
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError
log = logging.getLogger("mitx.courseware")
......@@ -80,9 +80,9 @@ class CapaModule(XModule):
js_module_name = "Problem"
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
def __init__(self, system, location, definition, instance_state=None,
def __init__(self, system, location, definition, descriptor, instance_state=None,
shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, instance_state,
XModule.__init__(self, system, location, definition, descriptor, instance_state,
shared_state, **kwargs)
self.attempts = 0
......@@ -134,12 +134,14 @@ class CapaModule(XModule):
if self.rerandomize == 'never':
seed = 1
elif self.rerandomize == "per_student" and hasattr(system, 'id'):
elif self.rerandomize == "per_student" and hasattr(self.system, 'id'):
seed = system.id
else:
seed = None
try:
# TODO (vshnayder): move as much as possible of this work and error
# checking to descriptor load time
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
instance_state, seed=seed, system=self.system)
except Exception as err:
......@@ -148,7 +150,7 @@ class CapaModule(XModule):
# TODO (vshnayder): do modules need error handlers too?
# We shouldn't be switching on DEBUG.
if self.system.DEBUG:
log.error(msg)
log.warning(msg)
# TODO (vshnayder): This logic should be general, not here--and may
# want to preserve the data instead of replacing it.
# e.g. in the CMS
......@@ -426,7 +428,7 @@ class CapaModule(XModule):
event_info = dict()
event_info['state'] = self.lcp.get_state()
event_info['problem_id'] = self.location.url()
answers = self.make_dict_of_responses(get)
event_info['answers'] = convert_files_to_filenames(answers)
......@@ -462,7 +464,7 @@ class CapaModule(XModule):
return {'success': msg}
log.exception("Error in capa_module problem checking")
raise Exception("error in capa_module")
self.attempts = self.attempts + 1
self.lcp.done = True
......@@ -563,6 +565,9 @@ class CapaDescriptor(RawDescriptor):
module_class = CapaModule
stores_state = True
has_score = True
# Capa modules have some additional metadata:
# TODO (vshnayder): do problems have any other metadata? Do they
# actually use type and points?
......
......@@ -21,18 +21,35 @@ class CourseDescriptor(SequenceDescriptor):
try:
self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M")
except KeyError:
self.start = time.gmtime(0) #The epoch
msg = "Course loaded without a start date. id = %s" % self.id
log.critical(msg)
except ValueError as e:
self.start = time.gmtime(0) #The epoch
msg = "Course loaded with a bad start date. %s '%s'" % (self.id, e)
log.critical(msg)
# Don't call the tracker from the exception handler.
if msg is not None:
self.start = time.gmtime(0) # The epoch
log.critical(msg)
system.error_tracker(msg)
def try_parse_time(key):
"""
Parse an optional metadata key: if present, must be valid.
Return None if not present.
"""
if key in self.metadata:
try:
return time.strptime(self.metadata[key], "%Y-%m-%dT%H:%M")
except ValueError as e:
msg = "Course %s loaded with a bad metadata key %s '%s'" % (
self.id, self.metadata[key], e)
log.warning(msg)
return None
self.enrollment_start = try_parse_time("enrollment_start")
self.enrollment_end = try_parse_time("enrollment_end")
def has_started(self):
return time.gmtime() > self.start
......@@ -99,10 +116,10 @@ class CourseDescriptor(SequenceDescriptor):
sections = []
for s in c.get_children():
if s.metadata.get('graded', False):
# TODO: Only include modules that have a score here
xmoduledescriptors = [child for child in yield_descriptor_descendents(s)]
xmoduledescriptors = list(yield_descriptor_descendents(s))
section_description = { 'section_descriptor' : s, 'xmoduledescriptors' : xmoduledescriptors}
# The xmoduledescriptors included here are only the ones that have scores.
section_description = { 'section_descriptor' : s, 'xmoduledescriptors' : filter(lambda child: child.has_score, xmoduledescriptors) }
section_format = s.metadata.get('format', "")
graded_sections[ section_format ] = graded_sections.get( section_format, [] ) + [section_description]
......
......@@ -49,6 +49,8 @@ padding-left: flex-gutter(9);
}
}
div {
p.status {
text-indent: -9999px;
......@@ -64,6 +66,16 @@ div {
}
}
&.processing {
p.status {
@include inline-block();
background: url('../images/spinner.gif') center center no-repeat;
height: 20px;
width: 20px;
text-indent: -9999px;
}
}
&.correct, &.ui-icon-check {
p.status {
@include inline-block();
......@@ -77,6 +89,19 @@ div {
}
}
&.processing {
p.status {
@include inline-block();
background: url('../images/spinner.gif') center center no-repeat;
height: 20px;
width: 20px;
}
input {
border-color: #aaa;
}
}
&.incorrect, &.ui-icon-close {
p.status {
@include inline-block();
......@@ -134,6 +159,15 @@ div {
width: 14px;
}
&.processing, &.ui-icon-processing {
@include inline-block();
background: url('../images/spinner.gif') center center no-repeat;
height: 20px;
position: relative;
top: 6px;
width: 25px;
}
&.correct, &.ui-icon-check {
@include inline-block();
background: url('../images/correct-icon.png') center center no-repeat;
......
......@@ -3,9 +3,9 @@ nav.sequence-nav {
// import from external sources.
@extend .topbar;
border-bottom: 1px solid $border-color;
@include border-top-right-radius(4px);
margin: (-(lh())) (-(lh())) lh() (-(lh()));
position: relative;
@include border-top-right-radius(4px);
ol {
@include box-sizing(border-box);
......@@ -242,9 +242,11 @@ nav.sequence-bottom {
border: 1px solid $border-color;
@include border-radius(3px);
@include inline-block();
width: 100px;
li {
float: left;
width: 50%;
&.prev, &.next {
margin-bottom: 0;
......@@ -252,12 +254,11 @@ nav.sequence-bottom {
a {
background-position: center center;
background-repeat: no-repeat;
border-bottom: none;
border: none;
display: block;
padding: lh(.5) 4px;
text-indent: -9999px;
@include transition(all, .2s, $ease-in-out-quad);
width: 45px;
&:hover {
background-color: #ddd;
......@@ -275,7 +276,7 @@ nav.sequence-bottom {
&.prev {
a {
background-image: url('../images/sequence-nav/previous-icon.png');
border-right: 1px solid lighten($border-color, 10%);
border-right: 1px solid lighten(#c6c6c6, 10%);
&:hover {
background-color: none;
......
......@@ -14,9 +14,8 @@ div.video {
section.video-player {
height: 0;
overflow: hidden;
// overflow: hidden;
padding-bottom: 56.25%;
padding-top: 30px;
position: relative;
object, iframe {
......@@ -46,12 +45,13 @@ div.video {
div.slider {
@extend .clearfix;
background: #c2c2c2;
border: none;
border-bottom: 1px solid #000;
border: 1px solid #000;
@include border-radius(0);
border-top: 1px solid #000;
@include box-shadow(inset 0 1px 0 #eee, 0 1px 0 #555);
height: 7px;
margin-left: -1px;
margin-right: -1px;
@include transition(height 2.0s ease-in-out);
div.ui-widget-header {
......@@ -59,43 +59,12 @@ div.video {
@include box-shadow(inset 0 1px 0 #999);
}
.ui-tooltip.qtip .ui-tooltip-content {
background: $mit-red;
border: 1px solid darken($mit-red, 20%);
@include border-radius(2px);
@include box-shadow(inset 0 1px 0 lighten($mit-red, 10%));
color: #fff;
font: bold 12px $body-font-family;
margin-bottom: 6px;
margin-right: 0;
overflow: visible;
padding: 4px;
text-align: center;
text-shadow: 0 -1px 0 darken($mit-red, 10%);
-webkit-font-smoothing: antialiased;
&::after {
background: $mit-red;
border-bottom: 1px solid darken($mit-red, 20%);
border-right: 1px solid darken($mit-red, 20%);
bottom: -5px;
content: " ";
display: block;
height: 7px;
left: 50%;
margin-left: -3px;
position: absolute;
@include transform(rotate(45deg));
width: 7px;
}
}
a.ui-slider-handle {
background: $mit-red url(../images/slider-handle.png) center center no-repeat;
background: $pink url(../images/slider-handle.png) center center no-repeat;
@include background-size(50%);
border: 1px solid darken($mit-red, 20%);
border: 1px solid darken($pink, 20%);
@include border-radius(15px);
@include box-shadow(inset 0 1px 0 lighten($mit-red, 10%));
@include box-shadow(inset 0 1px 0 lighten($pink, 10%));
cursor: pointer;
height: 15px;
margin-left: -7px;
......@@ -104,7 +73,7 @@ div.video {
width: 15px;
&:focus, &:hover {
background-color: lighten($mit-red, 10%);
background-color: lighten($pink, 10%);
outline: none;
}
}
......@@ -463,7 +432,8 @@ div.video {
}
ol.subtitles {
width: 0px;
width: 0;
height: 0;
}
}
......
......@@ -27,6 +27,14 @@ class ErrorModule(XModule):
'is_staff' : self.system.is_staff,
})
def displayable_items(self):
"""Hide errors in the profile and table of contents for non-staff
users.
"""
if self.system.is_staff:
return [self]
return []
class ErrorDescriptor(EditingDescriptor):
"""
Module that provides a raw editing view of broken xml.
......@@ -75,7 +83,8 @@ class ErrorDescriptor(EditingDescriptor):
# TODO (vshnayder): Do we need a unique slug here? Just pick a random
# 64-bit num?
location = ['i4x', org, course, 'error', url_name]
metadata = {} # stays in the xml_data
# real metadata stays in the xml_data, but add a display name
metadata = {'display_name': 'Error ' + url_name}
return cls(system, definition, location=location, metadata=metadata)
......
......@@ -18,9 +18,9 @@ class HtmlModule(XModule):
def get_html(self):
return self.html
def __init__(self, system, location, definition,
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition,
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
self.html = self.definition['data']
......
class @Problem
constructor: (element) ->
@el = $(element).find('.problems-wrapper')
@id = @el.data('problem-id')
......@@ -12,7 +13,10 @@ class @Problem
bind: =>
MathJax.Hub.Queue ["Typeset", MathJax.Hub]
window.update_schematics()
@inputs = @$("[id^=input_#{@element_id.replace(/problem_/, '')}_]")
problem_prefix = @element_id.replace(/problem_/,'')
@inputs = @$("[id^=input_#{problem_prefix}_]")
@$('section.action input:button').click @refreshAnswers
@$('section.action input.check').click @check_fd
#@$('section.action input.check').click @check
......@@ -26,25 +30,94 @@ class @Problem
@el.attr progress: response.progress_status
@el.trigger('progressChanged')
queueing: =>
@queued_items = @$(".xqueue")
if @queued_items.length > 0
if window.queuePollerID # Only one poller 'thread' per Problem
window.clearTimeout(window.queuePollerID)
window.queuePollerID = window.setTimeout(@poll, 100)
poll: =>
$.postWithPrefix "#{@url}/problem_get", (response) =>
@queued_items = $(response.html).find(".xqueue")
if @queued_items.length == 0
@el.html(response.html)
@executeProblemScripts () =>
@setupInputTypes()
@bind()
delete window.queuePollerID
else
# TODO: Dynamically adjust timeout interval based on @queued_items.value
window.queuePollerID = window.setTimeout(@poll, 1000)
render: (content) ->
if content
@el.html(content)
@bind()
@executeProblemScripts () =>
@setupInputTypes()
@bind()
@queueing()
else
$.postWithPrefix "#{@url}/problem_get", (response) =>
@el.html(response.html)
@executeProblemScripts()
@bind()
@executeProblemScripts () =>
@setupInputTypes()
@bind()
@queueing()
# TODO add hooks for problem types here by inspecting response.html and doing
# stuff if a div w a class is found
setupInputTypes: =>
@el.find(".capa_inputtype").each (index, inputtype) =>
classes = $(inputtype).attr('class').split(' ')
for cls in classes
setupMethod = @inputtypeSetupMethods[cls]
setupMethod(inputtype) if setupMethod?
executeProblemScripts: ->
@el.find(".script_placeholder").each (index, placeholder) ->
s = $("<script>")
s.attr("type", "text/javascript")
s.attr("src", $(placeholder).attr("data-src"))
executeProblemScripts: (callback=null) ->
placeholders = @el.find(".script_placeholder")
if placeholders.length == 0
callback()
return
completed = (false for i in [1..placeholders.length])
callbackCalled = false
# This is required for IE8 support.
completionHandlerGeneratorIE = (index) =>
return () ->
if (this.readyState == 'complete' || this.readyState == 'loaded')
#completionHandlerGenerator.call(self, index)()
completionHandlerGenerator(index)()
completionHandlerGenerator = (index) =>
return () =>
allComplete = true
completed[index] = true
for flag in completed
if not flag
allComplete = false
break
if allComplete and not callbackCalled
callbackCalled = true
callback() if callback?
placeholders.each (index, placeholder) ->
s = document.createElement('script')
s.setAttribute('src', $(placeholder).attr("data-src"))
s.setAttribute('type', "text/javascript")
s.onload = completionHandlerGenerator(index)
# s.onload does not fire in IE8; this does.
s.onreadystatechange = completionHandlerGeneratorIE(index)
# Need to use the DOM elements directly or the scripts won't execute
# properly.
$('head')[0].appendChild(s[0])
$('head')[0].appendChild(s)
$(placeholder).remove()
###
......@@ -108,6 +181,9 @@ class @Problem
@render(response.html)
@updateProgress response
# TODO this needs modification to deal with javascript responses; perhaps we
# need something where responsetypes can define their own behavior when show
# is called.
show: =>
if !@el.hasClass 'showed'
Logger.log 'problem_show', problem: @id
......@@ -157,3 +233,20 @@ class @Problem
@$(".CodeMirror").each (index, element) ->
element.CodeMirror.save() if element.CodeMirror.save
@answers = @inputs.serialize()
inputtypeSetupMethods:
javascriptinput: (element) =>
data = $(element).find(".javascriptinput_data")
params = data.data("params")
submission = data.data("submission")
evaluation = data.data("evaluation")
problemState = data.data("problem_state")
displayClass = window[data.data('display_class')]
container = $(element).find(".javascriptinput_container")
submissionField = $(element).find(".javascriptinput_input")
display = new displayClass(problemState, submission, evaluation, container, submissionField, params)
display.render()
......@@ -91,6 +91,13 @@ class @Sequence
event.preventDefault()
new_position = $(event.target).data('element')
Logger.log "seq_goto", old: @position, new: new_position, id: @id
# On Sequence chage, destroy any existing polling thread
# for queued submissions, see ../capa/display.coffee
if window.queuePollerID
window.clearTimeout(window.queuePollerID)
delete window.queuePollerID
@render new_position
next: (event) =>
......
......@@ -190,6 +190,13 @@ class Location(_LocationBase):
return "Location%s" % repr(tuple(self))
@property
def course_id(self):
"""Return the ID of the Course that this item belongs to by looking
at the location URL hierachy"""
return "/".join([self.org, self.course, self.name])
class ModuleStore(object):
"""
An abstract interface for a database backend that stores XModuleDescriptor
......
......@@ -55,6 +55,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
if json_data is None:
return self.modulestore.get_item(location)
else:
# TODO (vshnayder): metadata inheritance is somewhat broken because mongo, doesn't
# always load an entire course. We're punting on this until after launch, and then
# will build a proper course policy framework.
return XModuleDescriptor.load_from_json(json_data, self, self.default_class)
......
......@@ -50,8 +50,9 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
# have been imported into the cms from xml
xml = clean_out_mako_templating(xml)
xml_data = etree.fromstring(xml)
except:
log.exception("Unable to parse xml: {xml}".format(xml=xml))
except Exception as err:
log.warning("Unable to parse xml: {err}, xml: {xml}".format(
err=str(err), xml=xml))
raise
# VS[compat]. Take this out once course conversion is done
......@@ -194,7 +195,7 @@ class XMLModuleStore(ModuleStoreBase):
if org is None:
msg = ("No 'org' attribute set for course in {dir}. "
"Using default 'edx'".format(dir=course_dir))
log.error(msg)
log.warning(msg)
tracker(msg)
org = 'edx'
......@@ -206,13 +207,19 @@ class XMLModuleStore(ModuleStoreBase):
dir=course_dir,
default=course_dir
))
log.error(msg)
log.warning(msg)
tracker(msg)
course = course_dir
system = ImportSystem(self, org, course, course_dir, tracker)
course_descriptor = system.process_xml(etree.tostring(course_data))
# NOTE: The descriptors end up loading somewhat bottom up, which
# breaks metadata inheritance via get_children(). Instead
# (actually, in addition to, for now), we do a final inheritance pass
# after we have the course descriptor.
XModuleDescriptor.compute_inherited_metadata(course_descriptor)
log.debug('========> Done with course import from {0}'.format(course_dir))
return course_descriptor
......
......@@ -25,9 +25,9 @@ class SequenceModule(XModule):
css = {'scss': [resource_string(__name__, 'css/sequence/display.scss')]}
js_module_name = "Sequence"
def __init__(self, system, location, definition, instance_state=None,
def __init__(self, system, location, definition, descriptor, instance_state=None,
shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition,
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
self.position = 1
......@@ -107,6 +107,8 @@ class SequenceModule(XModule):
class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
mako_template = 'widgets/sequence-edit.html'
module_class = SequenceModule
stores_state = True # For remembering where in the sequence the student is
@classmethod
def definition_from_xml(cls, xml_object, system):
......
......@@ -26,12 +26,23 @@ class CustomTagModule(XModule):
More information given in <a href="/book/234">the text</a>
"""
def __init__(self, system, location, definition,
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition,
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
xmltree = etree.fromstring(self.definition['data'])
def get_html(self):
return self.descriptor.rendered_html
class CustomTagDescriptor(RawDescriptor):
""" Descriptor for custom tags. Loads the template when created."""
module_class = CustomTagModule
@staticmethod
def render_template(system, xml_data):
'''Render the template, given the definition xml_data'''
xmltree = etree.fromstring(xml_data)
if 'impl' in xmltree.attrib:
template_name = xmltree.attrib['impl']
else:
......@@ -45,13 +56,20 @@ class CustomTagModule(XModule):
.format(location))
params = dict(xmltree.items())
with self.system.filestore.open(
'custom_tags/{name}'.format(name=template_name)) as template:
self.html = Template(template.read()).render(**params)
with system.resources_fs.open('custom_tags/{name}'
.format(name=template_name)) as template:
return Template(template.read()).render(**params)
def get_html(self):
return self.html
def __init__(self, system, definition, **kwargs):
'''Render and save the template for this descriptor instance'''
super(CustomTagDescriptor, self).__init__(system, definition, **kwargs)
self.rendered_html = self.render_template(system, definition['data'])
def export_to_file(self):
"""
Custom tags are special: since they're already pointers, we don't want
to export them in a file with yet another layer of indirection.
"""
return False
class CustomTagDescriptor(RawDescriptor):
module_class = CustomTagModule
......@@ -8,7 +8,9 @@
import unittest
import os
import fs
import json
import json
import numpy
import xmodule
......@@ -30,10 +32,11 @@ i4xs = ModuleSystem(
render_template=Mock(),
replace_urls=Mock(),
user=Mock(),
filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))),
filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))+"/test_files"),
debug=True,
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue'},
is_staff=False
is_staff=False,
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules")
)
......@@ -291,9 +294,14 @@ class CodeResponseTest(unittest.TestCase):
for i in range(numAnswers):
old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuekey=1000 + i))
# Message format inherited from ExternalResponse
correct_score_msg = "<edxgrade><awarddetail>EXACT_ANS</awarddetail><message>MESSAGE</message></edxgrade>"
incorrect_score_msg = "<edxgrade><awarddetail>WRONG_FORMAT</awarddetail><message>MESSAGE</message></edxgrade>"
# TODO: Message format inherited from ExternalResponse
#correct_score_msg = "<edxgrade><awarddetail>EXACT_ANS</awarddetail><message>MESSAGE</message></edxgrade>"
#incorrect_score_msg = "<edxgrade><awarddetail>WRONG_FORMAT</awarddetail><message>MESSAGE</message></edxgrade>"
# New message format common to external graders
correct_score_msg = json.dumps({'correct':True, 'score':1, 'msg':'MESSAGE'})
incorrect_score_msg = json.dumps({'correct':False, 'score':0, 'msg':'MESSAGE'})
xserver_msgs = {'correct': correct_score_msg,
'incorrect': incorrect_score_msg,
}
......@@ -317,7 +325,8 @@ class CodeResponseTest(unittest.TestCase):
new_cmap = CorrectMap()
new_cmap.update(old_cmap)
new_cmap.set(answer_id=answer_ids[i], correctness=correctness, msg='MESSAGE', queuekey=None)
npoints = 1 if correctness=='correct' else 0
new_cmap.set(answer_id=answer_ids[i], npoints=npoints, correctness=correctness, msg='MESSAGE', queuekey=None)
test_lcp.update_score(xserver_msgs[correctness], queuekey=1000 + i)
self.assertEquals(test_lcp.correct_map.get_dict(), new_cmap.get_dict())
......@@ -367,6 +376,19 @@ class ChoiceResponseTest(unittest.TestCase):
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_1'), 'incorrect')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_4_1'), 'correct')
class JavascriptResponseTest(unittest.TestCase):
def test_jr_grade(self):
problem_file = os.path.dirname(__file__) + "/test_files/javascriptresponse.xml"
coffee_file_path = os.path.dirname(__file__) + "/test_files/js/*.coffee"
os.system("coffee -c %s" % (coffee_file_path))
test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs)
correct_answers = {'1_2_1': json.dumps({0: 4})}
incorrect_answers = {'1_2_1': json.dumps({0: 5})}
self.assertEquals(test_lcp.grade_answers(incorrect_answers).get_correctness('1_2_1'), 'incorrect')
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
#-----------------------------------------------------------------------------
# Grading tests
......@@ -708,6 +730,6 @@ class ModuleProgressTest(unittest.TestCase):
'''
def test_xmodule_default(self):
'''Make sure default get_progress exists, returns None'''
xm = x_module.XModule(i4xs, 'a://b/c/d/e', {})
xm = x_module.XModule(i4xs, 'a://b/c/d/e', None, {})
p = xm.get_progress()
self.assertEqual(p, None)
......@@ -4,6 +4,7 @@ from fs.osfs import OSFS
from nose.tools import assert_equals, assert_true
from path import path
from tempfile import mkdtemp
from shutil import copytree
from xmodule.modulestore.xml import XMLModuleStore
......@@ -40,27 +41,32 @@ def strip_filenames(descriptor):
class RoundTripTestCase(unittest.TestCase):
'''Check that our test courses roundtrip properly'''
def check_export_roundtrip(self, data_dir, course_dir):
root_dir = path(mkdtemp())
print "Copying test course to temp dir {0}".format(root_dir)
data_dir = path(data_dir)
copytree(data_dir / course_dir, root_dir / course_dir)
print "Starting import"
initial_import = XMLModuleStore(data_dir, eager=True, course_dirs=[course_dir])
initial_import = XMLModuleStore(root_dir, eager=True, course_dirs=[course_dir])
courses = initial_import.get_courses()
self.assertEquals(len(courses), 1)
initial_course = courses[0]
# export to the same directory--that way things like the custom_tags/ folder
# will still be there.
print "Starting export"
export_dir = mkdtemp()
print "export_dir: {0}".format(export_dir)
fs = OSFS(export_dir)
export_course_dir = 'export'
export_fs = fs.makeopendir(export_course_dir)
fs = OSFS(root_dir)
export_fs = fs.makeopendir(course_dir)
xml = initial_course.export_to_xml(export_fs)
with export_fs.open('course.xml', 'w') as course_xml:
course_xml.write(xml)
print "Starting second import"
second_import = XMLModuleStore(export_dir, eager=True,
course_dirs=[export_course_dir])
second_import = XMLModuleStore(root_dir, eager=True, course_dirs=[course_dir])
courses2 = second_import.get_courses()
self.assertEquals(len(courses2), 1)
......
<problem>
<javascriptresponse>
<generator src="test_problem_generator.js"/>
<grader src="test_problem_grader.js"/>
<display class="TestProblemDisplay" src="test_problem_display.js"/>
<responseparam name="value" value="4"/>
<javascriptinput>
</javascriptinput>
</javascriptresponse>
</problem>
// Generated by CoffeeScript 1.3.3
(function() {
var MinimaxProblemDisplay, root,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
MinimaxProblemDisplay = (function(_super) {
__extends(MinimaxProblemDisplay, _super);
function MinimaxProblemDisplay(state, submission, evaluation, container, submissionField, parameters) {
this.state = state;
this.submission = submission;
this.evaluation = evaluation;
this.container = container;
this.submissionField = submissionField;
this.parameters = parameters != null ? parameters : {};
MinimaxProblemDisplay.__super__.constructor.call(this, this.state, this.submission, this.evaluation, this.container, this.submissionField, this.parameters);
}
MinimaxProblemDisplay.prototype.render = function() {};
MinimaxProblemDisplay.prototype.createSubmission = function() {
var id, value, _ref, _results;
this.newSubmission = {};
if (this.submission != null) {
_ref = this.submission;
_results = [];
for (id in _ref) {
value = _ref[id];
_results.push(this.newSubmission[id] = value);
}
return _results;
}
};
MinimaxProblemDisplay.prototype.getCurrentSubmission = function() {
return this.newSubmission;
};
return MinimaxProblemDisplay;
})(XProblemDisplay);
root = typeof exports !== "undefined" && exports !== null ? exports : this;
root.TestProblemDisplay = TestProblemDisplay;
}).call(this);
;
/*
I've wrapped Makoto Matsumoto and Takuji Nishimura's code in a namespace
so it's better encapsulated. Now you can have multiple random number generators
and they won't stomp all over eachother's state.
If you want to use this as a substitute for Math.random(), use the random()
method like so:
var m = new MersenneTwister();
var randomNumber = m.random();
You can also call the other genrand_{foo}() methods on the instance.
If you want to use a specific seed in order to get a repeatable random
sequence, pass an integer into the constructor:
var m = new MersenneTwister(123);
and that will always produce the same random sequence.
Sean McCullough (banksean@gmail.com)
*/
/*
A C-program for MT19937, with initialization improved 2002/1/26.
Coded by Takuji Nishimura and Makoto Matsumoto.
Before using, initialize the state by using init_genrand(seed)
or init_by_array(init_key, key_length).
Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura,
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. The names of its contributors may not be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Any feedback is very welcome.
http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html
email: m-mat @ math.sci.hiroshima-u.ac.jp (remove space)
*/
var MersenneTwister = function(seed) {
if (seed == undefined) {
seed = new Date().getTime();
}
/* Period parameters */
this.N = 624;
this.M = 397;
this.MATRIX_A = 0x9908b0df; /* constant vector a */
this.UPPER_MASK = 0x80000000; /* most significant w-r bits */
this.LOWER_MASK = 0x7fffffff; /* least significant r bits */
this.mt = new Array(this.N); /* the array for the state vector */
this.mti=this.N+1; /* mti==N+1 means mt[N] is not initialized */
this.init_genrand(seed);
}
/* initializes mt[N] with a seed */
MersenneTwister.prototype.init_genrand = function(s) {
this.mt[0] = s >>> 0;
for (this.mti=1; this.mti<this.N; this.mti++) {
var s = this.mt[this.mti-1] ^ (this.mt[this.mti-1] >>> 30);
this.mt[this.mti] = (((((s & 0xffff0000) >>> 16) * 1812433253) << 16) + (s & 0x0000ffff) * 1812433253)
+ this.mti;
/* See Knuth TAOCP Vol2. 3rd Ed. P.106 for multiplier. */
/* In the previous versions, MSBs of the seed affect */
/* only MSBs of the array mt[]. */
/* 2002/01/09 modified by Makoto Matsumoto */
this.mt[this.mti] >>>= 0;
/* for >32 bit machines */
}
}
/* initialize by an array with array-length */
/* init_key is the array for initializing keys */
/* key_length is its length */
/* slight change for C++, 2004/2/26 */
MersenneTwister.prototype.init_by_array = function(init_key, key_length) {
var i, j, k;
this.init_genrand(19650218);
i=1; j=0;
k = (this.N>key_length ? this.N : key_length);
for (; k; k--) {
var s = this.mt[i-1] ^ (this.mt[i-1] >>> 30)
this.mt[i] = (this.mt[i] ^ (((((s & 0xffff0000) >>> 16) * 1664525) << 16) + ((s & 0x0000ffff) * 1664525)))
+ init_key[j] + j; /* non linear */
this.mt[i] >>>= 0; /* for WORDSIZE > 32 machines */
i++; j++;
if (i>=this.N) { this.mt[0] = this.mt[this.N-1]; i=1; }
if (j>=key_length) j=0;
}
for (k=this.N-1; k; k--) {
var s = this.mt[i-1] ^ (this.mt[i-1] >>> 30);
this.mt[i] = (this.mt[i] ^ (((((s & 0xffff0000) >>> 16) * 1566083941) << 16) + (s & 0x0000ffff) * 1566083941))
- i; /* non linear */
this.mt[i] >>>= 0; /* for WORDSIZE > 32 machines */
i++;
if (i>=this.N) { this.mt[0] = this.mt[this.N-1]; i=1; }
}
this.mt[0] = 0x80000000; /* MSB is 1; assuring non-zero initial array */
}
/* generates a random number on [0,0xffffffff]-interval */
MersenneTwister.prototype.genrand_int32 = function() {
var y;
var mag01 = new Array(0x0, this.MATRIX_A);
/* mag01[x] = x * MATRIX_A for x=0,1 */
if (this.mti >= this.N) { /* generate N words at one time */
var kk;
if (this.mti == this.N+1) /* if init_genrand() has not been called, */
this.init_genrand(5489); /* a default initial seed is used */
for (kk=0;kk<this.N-this.M;kk++) {
y = (this.mt[kk]&this.UPPER_MASK)|(this.mt[kk+1]&this.LOWER_MASK);
this.mt[kk] = this.mt[kk+this.M] ^ (y >>> 1) ^ mag01[y & 0x1];
}
for (;kk<this.N-1;kk++) {
y = (this.mt[kk]&this.UPPER_MASK)|(this.mt[kk+1]&this.LOWER_MASK);
this.mt[kk] = this.mt[kk+(this.M-this.N)] ^ (y >>> 1) ^ mag01[y & 0x1];
}
y = (this.mt[this.N-1]&this.UPPER_MASK)|(this.mt[0]&this.LOWER_MASK);
this.mt[this.N-1] = this.mt[this.M-1] ^ (y >>> 1) ^ mag01[y & 0x1];
this.mti = 0;
}
y = this.mt[this.mti++];
/* Tempering */
y ^= (y >>> 11);
y ^= (y << 7) & 0x9d2c5680;
y ^= (y << 15) & 0xefc60000;
y ^= (y >>> 18);
return y >>> 0;
}
/* generates a random number on [0,0x7fffffff]-interval */
MersenneTwister.prototype.genrand_int31 = function() {
return (this.genrand_int32()>>>1);
}
/* generates a random number on [0,1]-real-interval */
MersenneTwister.prototype.genrand_real1 = function() {
return this.genrand_int32()*(1.0/4294967295.0);
/* divided by 2^32-1 */
}
/* generates a random number on [0,1)-real-interval */
MersenneTwister.prototype.random = function() {
return this.genrand_int32()*(1.0/4294967296.0);
/* divided by 2^32 */
}
/* generates a random number on (0,1)-real-interval */
MersenneTwister.prototype.genrand_real3 = function() {
return (this.genrand_int32() + 0.5)*(1.0/4294967296.0);
/* divided by 2^32 */
}
/* generates a random number on [0,1) with 53-bit resolution*/
MersenneTwister.prototype.genrand_res53 = function() {
var a=this.genrand_int32()>>>5, b=this.genrand_int32()>>>6;
return(a*67108864.0+b)*(1.0/9007199254740992.0);
}
/* These real versions are due to Isaku Wada, 2002/01/09 added */
if(typeof exports == 'undefined'){
var root = this;
} else {
var root = exports;
}
root.MersenneTwister = MersenneTwister;
class MinimaxProblemDisplay extends XProblemDisplay
constructor: (@state, @submission, @evaluation, @container, @submissionField, @parameters={}) ->
super(@state, @submission, @evaluation, @container, @submissionField, @parameters)
render: () ->
createSubmission: () ->
@newSubmission = {}
if @submission?
for id, value of @submission
@newSubmission[id] = value
getCurrentSubmission: () ->
return @newSubmission
root = exports ? this
root.TestProblemDisplay = TestProblemDisplay
// Generated by CoffeeScript 1.3.3
(function() {
var MinimaxProblemDisplay, root,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
MinimaxProblemDisplay = (function(_super) {
__extends(MinimaxProblemDisplay, _super);
function MinimaxProblemDisplay(state, submission, evaluation, container, submissionField, parameters) {
this.state = state;
this.submission = submission;
this.evaluation = evaluation;
this.container = container;
this.submissionField = submissionField;
this.parameters = parameters != null ? parameters : {};
MinimaxProblemDisplay.__super__.constructor.call(this, this.state, this.submission, this.evaluation, this.container, this.submissionField, this.parameters);
}
MinimaxProblemDisplay.prototype.render = function() {};
MinimaxProblemDisplay.prototype.createSubmission = function() {
var id, value, _ref, _results;
this.newSubmission = {};
if (this.submission != null) {
_ref = this.submission;
_results = [];
for (id in _ref) {
value = _ref[id];
_results.push(this.newSubmission[id] = value);
}
return _results;
}
};
MinimaxProblemDisplay.prototype.getCurrentSubmission = function() {
return this.newSubmission;
};
return MinimaxProblemDisplay;
})(XProblemDisplay);
root = typeof exports !== "undefined" && exports !== null ? exports : this;
root.TestProblemDisplay = TestProblemDisplay;
}).call(this);
class TestProblemGenerator extends XProblemGenerator
constructor: (seed, @parameters = {}) ->
super(seed, @parameters)
generate: () ->
@problemState.value = @parameters.value
return @problemState
root = exports ? this
root.generatorClass = TestProblemGenerator
// Generated by CoffeeScript 1.3.3
(function() {
var TestProblemGenerator, root,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
TestProblemGenerator = (function(_super) {
__extends(TestProblemGenerator, _super);
function TestProblemGenerator(seed, parameters) {
this.parameters = parameters != null ? parameters : {};
TestProblemGenerator.__super__.constructor.call(this, seed, this.parameters);
}
TestProblemGenerator.prototype.generate = function() {
this.problemState.value = this.parameters.value;
return this.problemState;
};
return TestProblemGenerator;
})(XProblemGenerator);
root = typeof exports !== "undefined" && exports !== null ? exports : this;
root.generatorClass = TestProblemGenerator;
}).call(this);
class TestProblemGrader extends XProblemGrader
constructor: (@submission, @problemState, @parameters={}) ->
super(@submission, @problemState, @parameters)
solve: () ->
@solution = {0: @problemState.value}
grade: () ->
if not @solution?
@solve()
allCorrect = true
for id, value of @solution
valueCorrect = if @submission? then (value == @submission[id]) else false
@evaluation[id] = valueCorrect
if not valueCorrect
allCorrect = false
return allCorrect
root = exports ? this
root.graderClass = TestProblemGrader
// Generated by CoffeeScript 1.3.3
(function() {
var TestProblemGrader, root,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
TestProblemGrader = (function(_super) {
__extends(TestProblemGrader, _super);
function TestProblemGrader(submission, problemState, parameters) {
this.submission = submission;
this.problemState = problemState;
this.parameters = parameters != null ? parameters : {};
TestProblemGrader.__super__.constructor.call(this, this.submission, this.problemState, this.parameters);
}
TestProblemGrader.prototype.solve = function() {
return this.solution = {
0: this.problemState.value
};
};
TestProblemGrader.prototype.grade = function() {
var allCorrect, id, value, valueCorrect, _ref;
if (!(this.solution != null)) {
this.solve();
}
allCorrect = true;
_ref = this.solution;
for (id in _ref) {
value = _ref[id];
valueCorrect = this.submission != null ? value === this.submission[id] : false;
this.evaluation[id] = valueCorrect;
if (!valueCorrect) {
allCorrect = false;
}
}
return allCorrect;
};
return TestProblemGrader;
})(XProblemGrader);
root = typeof exports !== "undefined" && exports !== null ? exports : this;
root.graderClass = TestProblemGrader;
}).call(this);
class XProblemGenerator
constructor: (seed, @parameters={}) ->
@random = new MersenneTwister(seed)
@problemState = {}
generate: () ->
console.error("Abstract method called: XProblemGenerator.generate")
class XProblemDisplay
constructor: (@state, @submission, @evaluation, @container, @submissionField, @parameters={}) ->
render: () ->
console.error("Abstract method called: XProblemDisplay.render")
updateSubmission: () ->
@submissionField.val(JSON.stringify(@getCurrentSubmission()))
getCurrentSubmission: () ->
console.error("Abstract method called: XProblemDisplay.getCurrentSubmission")
class XProblemGrader
constructor: (@submission, @problemState, @parameters={}) ->
@solution = null
@evaluation = {}
solve: () ->
console.error("Abstract method called: XProblemGrader.solve")
grade: () ->
console.error("Abstract method called: XProblemGrader.grade")
root = exports ? this
root.XProblemGenerator = XProblemGenerator
root.XProblemDisplay = XProblemDisplay
root.XProblemGrader = XProblemGrader
// Generated by CoffeeScript 1.3.3
(function() {
var XProblemDisplay, XProblemGenerator, XProblemGrader, root;
XProblemGenerator = (function() {
function XProblemGenerator(seed, parameters) {
this.parameters = parameters != null ? parameters : {};
this.random = new MersenneTwister(seed);
this.problemState = {};
}
XProblemGenerator.prototype.generate = function() {
return console.error("Abstract method called: XProblemGenerator.generate");
};
return XProblemGenerator;
})();
XProblemDisplay = (function() {
function XProblemDisplay(state, submission, evaluation, container, submissionField, parameters) {
this.state = state;
this.submission = submission;
this.evaluation = evaluation;
this.container = container;
this.submissionField = submissionField;
this.parameters = parameters != null ? parameters : {};
}
XProblemDisplay.prototype.render = function() {
return console.error("Abstract method called: XProblemDisplay.render");
};
XProblemDisplay.prototype.updateSubmission = function() {
return this.submissionField.val(JSON.stringify(this.getCurrentSubmission()));
};
XProblemDisplay.prototype.getCurrentSubmission = function() {
return console.error("Abstract method called: XProblemDisplay.getCurrentSubmission");
};
return XProblemDisplay;
})();
XProblemGrader = (function() {
function XProblemGrader(submission, problemState, parameters) {
this.submission = submission;
this.problemState = problemState;
this.parameters = parameters != null ? parameters : {};
this.solution = null;
this.evaluation = {};
}
XProblemGrader.prototype.solve = function() {
return console.error("Abstract method called: XProblemGrader.solve");
};
XProblemGrader.prototype.grade = function() {
return console.error("Abstract method called: XProblemGrader.grade");
};
return XProblemGrader;
})();
root = typeof exports !== "undefined" && exports !== null ? exports : this;
root.XProblemGenerator = XProblemGenerator;
root.XProblemDisplay = XProblemDisplay;
root.XProblemGrader = XProblemGrader;
}).call(this);
......@@ -8,8 +8,11 @@ from xmodule.x_module import XMLParsingSystem, XModuleDescriptor
from xmodule.xml_module import is_pointer_tag
from xmodule.errortracker import make_error_tracker
from xmodule.modulestore import Location
from xmodule.modulestore.xml import XMLModuleStore
from xmodule.modulestore.exceptions import ItemNotFoundError
from .test_export import DATA_DIR
ORG = 'test_org'
COURSE = 'test_course'
......@@ -185,3 +188,22 @@ class ImportTestCase(unittest.TestCase):
chapter_xml = etree.fromstring(f.read())
self.assertEqual(chapter_xml.tag, 'chapter')
self.assertFalse('graceperiod' in chapter_xml.attrib)
def test_metadata_inherit(self):
"""Make sure that metadata is inherited properly"""
print "Starting import"
initial_import = XMLModuleStore(DATA_DIR, eager=True, course_dirs=['toy'])
courses = initial_import.get_courses()
self.assertEquals(len(courses), 1)
course = courses[0]
def check_for_key(key, node):
"recursive check for presence of key"
print "Checking {}".format(node.location.url())
self.assertTrue(key in node.metadata)
for c in node.get_children():
check_for_key(key, c)
check_for_key('graceperiod', course)
......@@ -10,8 +10,8 @@ class_priority = ['video', 'problem']
class VerticalModule(XModule):
''' Layout module for laying out submodules vertically.'''
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs)
self.contents = None
def get_html(self):
......
......@@ -23,9 +23,9 @@ class VideoModule(XModule):
css = {'scss': [resource_string(__name__, 'css/video/display.scss')]}
js_module_name = "Video"
def __init__(self, system, location, definition,
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition,
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
xmltree = etree.fromstring(self.definition['data'])
self.youtube = xmltree.get('youtube')
......@@ -80,3 +80,5 @@ class VideoModule(XModule):
class VideoDescriptor(RawDescriptor):
module_class = VideoModule
stores_state = True
......@@ -143,7 +143,7 @@ class XModule(HTMLSnippet):
# in the module
icon_class = 'other'
def __init__(self, system, location, definition,
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs):
'''
Construct a new xmodule
......@@ -166,6 +166,10 @@ class XModule(HTMLSnippet):
'children': is a list of Location-like values for child modules that
this module depends on
descriptor: the XModuleDescriptor that this module is an instance of.
TODO (vshnayder): remove the definition parameter and location--they
can come from the descriptor.
instance_state: A string of serialized json that contains the state of
this module for current student accessing the system, or None if
no state has been saved
......@@ -189,6 +193,7 @@ class XModule(HTMLSnippet):
self.system = system
self.location = Location(location)
self.definition = definition
self.descriptor = descriptor
self.instance_state = instance_state
self.shared_state = shared_state
self.id = self.location.url()
......@@ -222,7 +227,7 @@ class XModule(HTMLSnippet):
def get_display_items(self):
'''
Returns a list of descendent module instances that will display
immediately inside this module
immediately inside this module.
'''
items = []
for child in self.get_children():
......@@ -233,7 +238,7 @@ class XModule(HTMLSnippet):
def displayable_items(self):
'''
Returns list of displayable modules contained by this module. If this
module is visible, should return [self]
module is visible, should return [self].
'''
return [self]
......@@ -304,10 +309,18 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
entry_point = "xmodule.v1"
module_class = XModule
# Attributes for inpsection of the descriptor
stores_state = False # Indicates whether the xmodule state should be
# stored in a database (independent of shared state)
has_score = False # This indicates whether the xmodule is a problem-type.
# It should respond to max_score() and grade(). It can be graded or ungraded
# (like a practice problem).
# A list of metadata that this module can inherit from its parent module
inheritable_metadata = (
'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize',
# TODO (ichuang): used for Fall 2012 xqa server access
'xqa_key',
# TODO: This is used by the XMLModuleStore to provide for locations for
# static files, and will need to be removed when that code is removed
'data_dir'
......@@ -391,6 +404,18 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
return dict((k,v) for k,v in self.metadata.items()
if k not in self._inherited_metadata)
@staticmethod
def compute_inherited_metadata(node):
"""Given a descriptor, traverse all of its descendants and do metadata
inheritance. Should be called on a CourseDescriptor after importing a
course.
NOTE: This means that there is no such thing as lazy loading at the
moment--this accesses all the children."""
for c in node.get_children():
c.inherit_metadata(node.metadata)
XModuleDescriptor.compute_inherited_metadata(c)
def inherit_metadata(self, metadata):
"""
Updates this module with metadata inherited from a containing module.
......@@ -411,6 +436,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
self._child_instances = []
for child_loc in self.definition.get('children', []):
child = self.system.load_item(child_loc)
# TODO (vshnayder): this should go away once we have
# proper inheritance support in mongo. The xml
# datastore does all inheritance on course load.
child.inherit_metadata(self.metadata)
self._child_instances.append(child)
......@@ -427,6 +455,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
system,
self.location,
self.definition,
self,
metadata=self.metadata
)
......@@ -494,7 +523,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
# Put import here to avoid circular import errors
from xmodule.error_module import ErrorDescriptor
msg = "Error loading from xml."
log.exception(msg)
log.warning(msg + " " + str(err))
system.error_tracker(msg)
err_msg = msg + "\n" + exc_info_to_str(sys.exc_info())
descriptor = ErrorDescriptor.from_xml(xml_data, system, org, course,
......@@ -587,9 +616,10 @@ class DescriptorSystem(object):
try:
x = access_some_resource()
check_some_format(x)
except SomeProblem:
msg = 'Grommet {0} is broken'.format(x)
log.exception(msg) # don't rely on handler to log
except SomeProblem as err:
msg = 'Grommet {0} is broken: {1}'.format(x, str(err))
log.warning(msg) # don't rely on tracker to log
# NOTE: we generally don't want content errors logged as errors
self.system.error_tracker(msg)
# work around
return 'Oops, couldn't load grommet'
......@@ -645,7 +675,8 @@ class ModuleSystem(object):
filestore=None,
debug=False,
xqueue=None,
is_staff=False):
is_staff=False,
node_path=""):
'''
Create a closure around the system environment.
......@@ -668,7 +699,7 @@ class ModuleSystem(object):
filestore - A filestore ojbect. Defaults to an instance of OSFS based
at settings.DATA_DIR.
xqueue - Dict containing XqueueInterface object, as well as parameters
xqueue - Dict containing XqueueInterface object, as well as parameters
for the specific StudentModule
replace_urls - TEMPORARY - A function like static_replace.replace_urls
......@@ -688,6 +719,7 @@ class ModuleSystem(object):
self.seed = user.id if user is not None else 0
self.replace_urls = replace_urls
self.is_staff = is_staff
self.node_path = node_path
def get(self, attr):
''' provide uniform access to attributes (like etree).'''
......
from xmodule.x_module import XModuleDescriptor
from xmodule.modulestore import Location
from lxml import etree
import json
import copy
import logging
import traceback
......@@ -32,7 +33,15 @@ def is_pointer_tag(xml_obj):
actual_attr = set(xml_obj.attrib.keys())
return len(xml_obj) == 0 and actual_attr == expected_attr
def get_metadata_from_xml(xml_object, remove=True):
meta = xml_object.find('meta')
if meta is None:
return ''
dmdata = meta.text
log.debug('meta for %s loaded: %s' % (xml_object,dmdata))
if remove:
xml_object.remove(meta)
return dmdata
_AttrMapBase = namedtuple('_AttrMap', 'from_xml to_xml')
......@@ -71,6 +80,7 @@ class XmlDescriptor(XModuleDescriptor):
metadata_attributes = ('format', 'graceperiod', 'showanswer', 'rerandomize',
'start', 'due', 'graded', 'display_name', 'url_name', 'hide_from_toc',
'ispublic', # if True, then course is listed for all users; see
'xqa_key', # for xqaa server access
# VS[compat] Remove once unused.
'name', 'slug')
......@@ -180,8 +190,11 @@ class XmlDescriptor(XModuleDescriptor):
definition_xml = cls.load_file(filepath, system.resources_fs, location)
definition_metadata = get_metadata_from_xml(definition_xml)
cls.clean_metadata_from_xml(definition_xml)
definition = cls.definition_from_xml(definition_xml, system)
if definition_metadata:
definition['definition_metadata'] = definition_metadata
# TODO (ichuang): remove this after migration
# for Fall 2012 LMS migration: keep filename (and unmangled filename)
......@@ -236,9 +249,9 @@ class XmlDescriptor(XModuleDescriptor):
filepath = cls._format_filepath(xml_object.tag, url_name)
definition_xml = cls.load_file(filepath, system.resources_fs, location)
else:
definition_xml = xml_object
definition_xml = xml_object # this is just a pointer, not the real definition content
definition = cls.load_definition(definition_xml, system, location)
definition = cls.load_definition(definition_xml, system, location) # note this removes metadata
# VS[compat] -- make Ike's github preview links work in both old and
# new file layouts
if is_pointer_tag(xml_object):
......@@ -246,6 +259,17 @@ class XmlDescriptor(XModuleDescriptor):
definition['filename'] = [filepath, filepath]
metadata = cls.load_metadata(definition_xml)
# move definition metadata into dict
dmdata = definition.get('definition_metadata','')
if dmdata:
metadata['definition_metadata_raw'] = dmdata
try:
metadata.update(json.loads(dmdata))
except Exception as err:
log.debug('Error %s in loading metadata %s' % (err,dmdata))
metadata['definition_metadata_err'] = str(err)
return cls(
system,
definition,
......@@ -259,6 +283,15 @@ class XmlDescriptor(XModuleDescriptor):
name=name,
ext=cls.filename_extension)
def export_to_file(self):
"""If this returns True, write the definition of this descriptor to a separate
file.
NOTE: Do not override this without a good reason. It is here specifically for customtag...
"""
return True
def export_to_xml(self, resource_fs):
"""
Returns an xml string representing this module, and all modules
......@@ -295,14 +328,18 @@ class XmlDescriptor(XModuleDescriptor):
if attr not in self.metadata_to_strip:
xml_object.set(attr, val_for_xml(attr))
# Write the definition to a file
filepath = self.__class__._format_filepath(self.category, self.url_name)
resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True)
with resource_fs.open(filepath, 'w') as file:
file.write(etree.tostring(xml_object, pretty_print=True))
if self.export_to_file():
# Write the definition to a file
filepath = self.__class__._format_filepath(self.category, self.url_name)
resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True)
with resource_fs.open(filepath, 'w') as file:
file.write(etree.tostring(xml_object, pretty_print=True))
# And return just a pointer with the category and filename.
record_object = etree.Element(self.category)
else:
record_object = xml_object
# And return just a pointer with the category and filename.
record_object = etree.Element(self.category)
record_object.set('url_name', self.url_name)
# Special case for course pointers:
......
class XProblemGenerator
constructor: (seed, @parameters={}) ->
@random = new MersenneTwister(seed)
@problemState = {}
generate: () ->
console.error("Abstract method called: XProblemGenerator.generate")
class XProblemDisplay
constructor: (@state, @submission, @evaluation, @container, @submissionField, @parameters={}) ->
render: () ->
console.error("Abstract method called: XProblemDisplay.render")
updateSubmission: () ->
@submissionField.val(JSON.stringify(@getCurrentSubmission()))
getCurrentSubmission: () ->
console.error("Abstract method called: XProblemDisplay.getCurrentSubmission")
class XProblemGrader
constructor: (@submission, @problemState, @parameters={}) ->
@solution = null
@evaluation = {}
solve: () ->
console.error("Abstract method called: XProblemGrader.solve")
grade: () ->
console.error("Abstract method called: XProblemGrader.grade")
root = exports ? this
root.XProblemGenerator = XProblemGenerator
root.XProblemDisplay = XProblemDisplay
root.XProblemGrader = XProblemGrader
common/static/images/sequence-nav/edit.png

168 Bytes | W: | H:

common/static/images/sequence-nav/edit.png

171 Bytes | W: | H:

common/static/images/sequence-nav/edit.png
common/static/images/sequence-nav/edit.png
common/static/images/sequence-nav/edit.png
common/static/images/sequence-nav/edit.png
  • 2-up
  • Swipe
  • Onion skin
common/static/images/sequence-nav/history.png

276 Bytes | W: | H:

common/static/images/sequence-nav/history.png

292 Bytes | W: | H:

common/static/images/sequence-nav/history.png
common/static/images/sequence-nav/history.png
common/static/images/sequence-nav/history.png
common/static/images/sequence-nav/history.png
  • 2-up
  • Swipe
  • Onion skin
common/static/images/sequence-nav/view.png

229 Bytes | W: | H:

common/static/images/sequence-nav/view.png

234 Bytes | W: | H:

common/static/images/sequence-nav/view.png
common/static/images/sequence-nav/view.png
common/static/images/sequence-nav/view.png
common/static/images/sequence-nav/view.png
  • 2-up
  • Swipe
  • Onion skin
/*
I've wrapped Makoto Matsumoto and Takuji Nishimura's code in a namespace
so it's better encapsulated. Now you can have multiple random number generators
and they won't stomp all over eachother's state.
If you want to use this as a substitute for Math.random(), use the random()
method like so:
var m = new MersenneTwister();
var randomNumber = m.random();
You can also call the other genrand_{foo}() methods on the instance.
If you want to use a specific seed in order to get a repeatable random
sequence, pass an integer into the constructor:
var m = new MersenneTwister(123);
and that will always produce the same random sequence.
Sean McCullough (banksean@gmail.com)
*/
/*
A C-program for MT19937, with initialization improved 2002/1/26.
Coded by Takuji Nishimura and Makoto Matsumoto.
Before using, initialize the state by using init_genrand(seed)
or init_by_array(init_key, key_length).
Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura,
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. The names of its contributors may not be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Any feedback is very welcome.
http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html
email: m-mat @ math.sci.hiroshima-u.ac.jp (remove space)
*/
var MersenneTwister = function(seed) {
if (seed == undefined) {
seed = new Date().getTime();
}
/* Period parameters */
this.N = 624;
this.M = 397;
this.MATRIX_A = 0x9908b0df; /* constant vector a */
this.UPPER_MASK = 0x80000000; /* most significant w-r bits */
this.LOWER_MASK = 0x7fffffff; /* least significant r bits */
this.mt = new Array(this.N); /* the array for the state vector */
this.mti=this.N+1; /* mti==N+1 means mt[N] is not initialized */
this.init_genrand(seed);
}
/* initializes mt[N] with a seed */
MersenneTwister.prototype.init_genrand = function(s) {
this.mt[0] = s >>> 0;
for (this.mti=1; this.mti<this.N; this.mti++) {
var s = this.mt[this.mti-1] ^ (this.mt[this.mti-1] >>> 30);
this.mt[this.mti] = (((((s & 0xffff0000) >>> 16) * 1812433253) << 16) + (s & 0x0000ffff) * 1812433253)
+ this.mti;
/* See Knuth TAOCP Vol2. 3rd Ed. P.106 for multiplier. */
/* In the previous versions, MSBs of the seed affect */
/* only MSBs of the array mt[]. */
/* 2002/01/09 modified by Makoto Matsumoto */
this.mt[this.mti] >>>= 0;
/* for >32 bit machines */
}
}
/* initialize by an array with array-length */
/* init_key is the array for initializing keys */
/* key_length is its length */
/* slight change for C++, 2004/2/26 */
MersenneTwister.prototype.init_by_array = function(init_key, key_length) {
var i, j, k;
this.init_genrand(19650218);
i=1; j=0;
k = (this.N>key_length ? this.N : key_length);
for (; k; k--) {
var s = this.mt[i-1] ^ (this.mt[i-1] >>> 30)
this.mt[i] = (this.mt[i] ^ (((((s & 0xffff0000) >>> 16) * 1664525) << 16) + ((s & 0x0000ffff) * 1664525)))
+ init_key[j] + j; /* non linear */
this.mt[i] >>>= 0; /* for WORDSIZE > 32 machines */
i++; j++;
if (i>=this.N) { this.mt[0] = this.mt[this.N-1]; i=1; }
if (j>=key_length) j=0;
}
for (k=this.N-1; k; k--) {
var s = this.mt[i-1] ^ (this.mt[i-1] >>> 30);
this.mt[i] = (this.mt[i] ^ (((((s & 0xffff0000) >>> 16) * 1566083941) << 16) + (s & 0x0000ffff) * 1566083941))
- i; /* non linear */
this.mt[i] >>>= 0; /* for WORDSIZE > 32 machines */
i++;
if (i>=this.N) { this.mt[0] = this.mt[this.N-1]; i=1; }
}
this.mt[0] = 0x80000000; /* MSB is 1; assuring non-zero initial array */
}
/* generates a random number on [0,0xffffffff]-interval */
MersenneTwister.prototype.genrand_int32 = function() {
var y;
var mag01 = new Array(0x0, this.MATRIX_A);
/* mag01[x] = x * MATRIX_A for x=0,1 */
if (this.mti >= this.N) { /* generate N words at one time */
var kk;
if (this.mti == this.N+1) /* if init_genrand() has not been called, */
this.init_genrand(5489); /* a default initial seed is used */
for (kk=0;kk<this.N-this.M;kk++) {
y = (this.mt[kk]&this.UPPER_MASK)|(this.mt[kk+1]&this.LOWER_MASK);
this.mt[kk] = this.mt[kk+this.M] ^ (y >>> 1) ^ mag01[y & 0x1];
}
for (;kk<this.N-1;kk++) {
y = (this.mt[kk]&this.UPPER_MASK)|(this.mt[kk+1]&this.LOWER_MASK);
this.mt[kk] = this.mt[kk+(this.M-this.N)] ^ (y >>> 1) ^ mag01[y & 0x1];
}
y = (this.mt[this.N-1]&this.UPPER_MASK)|(this.mt[0]&this.LOWER_MASK);
this.mt[this.N-1] = this.mt[this.M-1] ^ (y >>> 1) ^ mag01[y & 0x1];
this.mti = 0;
}
y = this.mt[this.mti++];
/* Tempering */
y ^= (y >>> 11);
y ^= (y << 7) & 0x9d2c5680;
y ^= (y << 15) & 0xefc60000;
y ^= (y >>> 18);
return y >>> 0;
}
/* generates a random number on [0,0x7fffffff]-interval */
MersenneTwister.prototype.genrand_int31 = function() {
return (this.genrand_int32()>>>1);
}
/* generates a random number on [0,1]-real-interval */
MersenneTwister.prototype.genrand_real1 = function() {
return this.genrand_int32()*(1.0/4294967295.0);
/* divided by 2^32-1 */
}
/* generates a random number on [0,1)-real-interval */
MersenneTwister.prototype.random = function() {
return this.genrand_int32()*(1.0/4294967296.0);
/* divided by 2^32 */
}
/* generates a random number on (0,1)-real-interval */
MersenneTwister.prototype.genrand_real3 = function() {
return (this.genrand_int32() + 0.5)*(1.0/4294967296.0);
/* divided by 2^32 */
}
/* generates a random number on [0,1) with 53-bit resolution*/
MersenneTwister.prototype.genrand_res53 = function() {
var a=this.genrand_int32()>>>5, b=this.genrand_int32()>>>6;
return(a*67108864.0+b)*(1.0/9007199254740992.0);
}
/* These real versions are due to Isaku Wada, 2002/01/09 added */
if(typeof exports == 'undefined'){
var root = this;
} else {
var root = exports;
}
root.MersenneTwister = MersenneTwister;
......@@ -190,7 +190,7 @@ case `uname -s` in
}
distro=`lsb_release -cs`
case $distro in
lisa|natty|oneiric|precise)
maya|lisa|natty|oneiric|precise)
output "Installing ubuntu requirements"
sudo apt-get -y update
sudo apt-get -y install $APT_PKGS
......
......@@ -34,12 +34,34 @@ This will import all courses in your data directory into mongodb
This runs all the tests (long, uses collectstatic):
rake test
If if you aren't changing static files, can run `rake test` once, then run
rake fasttest_{lms,cms}
xmodule can be tested independently, with this:
rake test_common/lib/xmodule
To see all available rake commands, do this:
rake -T
\ No newline at end of file
To run a single django test class:
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/courseware/tests/tests.py:TestViewAuth
To run a single django test:
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/courseware/tests/tests.py:TestViewAuth.test_dark_launch
To run a single nose test file:
nosetests common/lib/xmodule/xmodule/tests/test_stringify.py
To run a single nose test:
nosetests common/lib/xmodule/xmodule/tests/test_stringify.py:test_stringify
......@@ -8,19 +8,19 @@ from django.conf import settings
from django.http import Http404
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
from static_replace import replace_urls
from staticfiles.storage import staticfiles_storage
from static_replace import replace_urls, try_staticfiles_lookup
log = logging.getLogger(__name__)
def check_course(course_id, course_must_be_open=True, course_required=True):
def check_course(user, course_id, course_must_be_open=True, course_required=True):
"""
Given a course_id, this returns the course object. By default,
if the course is not found or the course is not open yet, this
method will raise a 404.
Given a django user and a course_id, this returns the course
object. By default, if the course is not found or the course is
not open yet, this method will raise a 404.
If course_must_be_open is False, the course will be returned
without a 404 even if it is not open.
......@@ -28,6 +28,10 @@ def check_course(course_id, course_must_be_open=True, course_required=True):
If course_required is False, a course_id of None is acceptable. The
course returned will be None. Even if the course is not required,
if a course_id is given that does not exist a 404 will be raised.
This behavior is modified by MITX_FEATURES['DARK_LAUNCH']:
if dark launch is enabled, course_must_be_open is ignored for
users that have staff access.
"""
course = None
if course_required or course_id:
......@@ -39,16 +43,23 @@ def check_course(course_id, course_must_be_open=True, course_required=True):
raise Http404("Course not found.")
started = course.has_started() or settings.MITX_FEATURES['DISABLE_START_DATES']
if course_must_be_open and not started:
must_be_open = course_must_be_open
if (settings.MITX_FEATURES['DARK_LAUNCH'] and
has_staff_access_to_course(user, course)):
must_be_open = False
if must_be_open and not started:
raise Http404("This course has not yet started.")
return course
def course_image_url(course):
return staticfiles_storage.url(course.metadata['data_dir'] +
"/images/course_image.jpg")
"""Try to look up the image url for the course. If it's not found,
log an error and return the dead link"""
path = course.metadata['data_dir'] + "/images/course_image.jpg"
return try_staticfiles_lookup(path)
def get_course_about_section(course, section_key):
"""
......@@ -145,6 +156,8 @@ def has_staff_access_to_course(user, course):
'''
Returns True if the given user has staff access to the course.
This means that user is in the staff_* group, or is an overall admin.
TODO (vshnayder): this needs to be changed to allow per-course_id permissions, not per-course
(e.g. staff in 2012 is different from 2013, but maybe some people always have access)
course is the course field of the location being accessed.
'''
......@@ -156,13 +169,26 @@ def has_staff_access_to_course(user, course):
# note this is the Auth group, not UserTestGroup
user_groups = [x[1] for x in user.groups.values_list()]
staff_group = course_staff_group_name(course)
log.debug('course %s, staff_group %s, user %s, groups %s' % (
course, staff_group, user, user_groups))
if staff_group in user_groups:
return True
return False
def has_access_to_course(user,course):
def has_staff_access_to_course_id(user, course_id):
"""Helper method that takes a course_id instead of a course name"""
loc = CourseDescriptor.id_to_location(course_id)
return has_staff_access_to_course(user, loc.course)
def has_staff_access_to_location(user, location):
"""Helper method that checks whether the user has staff access to
the course of the location.
location: something that can be passed to Location
"""
return has_staff_access_to_course(user, Location(location).course)
def has_access_to_course(user, course):
'''course is the .course element of a location'''
if course.metadata.get('ispublic'):
return True
return has_staff_access_to_course(user,course)
......
# Compute grades using real division, with no integer truncation
from __future__ import division
import random
import logging
......@@ -12,32 +15,34 @@ from models import StudentModule
log = logging.getLogger("mitx.courseware")
def yield_module_descendents(module):
for child in module.get_display_items():
yield child
for module in yield_module_descendents(child):
yield module
stack = module.get_display_items()
while len(stack) > 0:
next_module = stack.pop()
stack.extend( next_module.get_display_items() )
yield next_module
def grade(student, request, course, student_module_cache=None):
"""
This grades a student as quickly as possible. It retuns the
This grades a student as quickly as possible. It retuns the
output from the course grader, augmented with the final letter
grade. The keys in the output are:
- grade : A final letter grade.
- percent : The final percent for the class (rounded up).
- section_breakdown : A breakdown of each section that makes
up the grade. (For display)
- grade_breakdown : A breakdown of the major components that
make up the final grade. (For display)
More information on the format is in the docstring for CourseGrader.
"""
grading_context = course.grading_context
if student_module_cache == None:
student_module_cache = StudentModuleCache(student, grading_context['all_descriptors'])
totaled_scores = {}
# This next complicated loop is just to collect the totaled_scores, which is
# passed to the grader
......@@ -46,91 +51,91 @@ def grade(student, request, course, student_module_cache=None):
for section in sections:
section_descriptor = section['section_descriptor']
section_name = section_descriptor.metadata.get('display_name')
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() ):
should_grade_section = True
break
if should_grade_section:
scores = []
# TODO: We need the request to pass into here. If we could forgo that, our arguments
# would be simpler
section_module = get_module(student, request, section_descriptor.location, student_module_cache)
# 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):
for module in yield_module_descendents(section_module):
(correct, total) = get_score(student, module, student_module_cache)
if correct is None and total is None:
continue
if settings.GENERATE_PROFILE_SCORES:
if total > 1:
correct = random.randrange(max(total - 2, 1), total + 1)
else:
correct = total
graded = module.metadata.get("graded", False)
if not total > 0:
#We simply cannot grade a problem that is 12/0, because we might need it as a percentage
graded = False
scores.append(Score(correct, total, graded, module.metadata.get('display_name')))
section_total, graded_total = graders.aggregate_scores(scores, section_name)
else:
section_total = Score(0.0, 1.0, False, section_name)
graded_total = Score(0.0, 1.0, True, section_name)
#Add the graded total to totaled_scores
if graded_total.possible > 0:
format_scores.append(graded_total)
else:
log.exception("Unable to grade a section with a total possible score of zero. " + str(section_descriptor.id))
log.exception("Unable to grade a section with a total possible score of zero. " + str(section_descriptor.location))
totaled_scores[section_format] = format_scores
grade_summary = course.grader.grade(totaled_scores)
# We round the grade here, to make sure that the grade is an whole percentage and
# doesn't get displayed differently than it gets grades
grade_summary['percent'] = round(grade_summary['percent'] * 100 + 0.05) / 100
letter_grade = grade_for_percentage(course.grade_cutoffs, grade_summary['percent'])
grade_summary['grade'] = letter_grade
return grade_summary
def grade_for_percentage(grade_cutoffs, percentage):
"""
Returns a letter grade 'A' 'B' 'C' or None.
Arguments
- grade_cutoffs is a dictionary mapping a grade to the lowest
possible percentage to earn that grade.
- percentage is the final percent across all problems in a course
"""
letter_grade = None
for possible_grade in ['A', 'B', 'C']:
if percentage >= grade_cutoffs[possible_grade]:
letter_grade = possible_grade
break
return letter_grade
return letter_grade
def progress_summary(student, course, grader, student_module_cache):
"""
This pulls a summary of all problems in the course.
Returns
- courseware_summary is a summary of all sections with problems in the course.
It is organized as an array of chapters, each containing an array of sections,
each containing an array of scores. This contains information for graded and
ungraded problems, and is good for displaying a course summary with due dates,
- courseware_summary is a summary of all sections with problems in the course.
It is organized as an array of chapters, each containing an array of sections,
each containing an array of scores. This contains information for graded and
ungraded problems, and is good for displaying a course summary with due dates,
etc.
Arguments:
......@@ -140,9 +145,11 @@ def progress_summary(student, course, grader, student_module_cache):
instance_modules for the student
"""
chapters = []
for c in course.get_children():
# Don't include chapters that aren't displayable (e.g. due to error)
for c in course.get_display_items():
sections = []
for s in c.get_children():
for s in c.get_display_items():
# Same for sections
graded = s.metadata.get('graded', False)
scores = []
for module in yield_module_descendents(s):
......@@ -150,7 +157,7 @@ def progress_summary(student, course, grader, student_module_cache):
if correct is None and total is None:
continue
scores.append(Score(correct, total, graded,
scores.append(Score(correct, total, graded,
module.metadata.get('display_name')))
section_total, graded_total = graders.aggregate_scores(
......@@ -177,12 +184,16 @@ def progress_summary(student, course, grader, student_module_cache):
def get_score(user, problem, student_module_cache):
"""
Return the score for a user on a problem
Return the score for a user on a problem, as a tuple (correct, total).
user: a Student object
problem: an XModule
cache: A StudentModuleCache
"""
if not (problem.descriptor.stores_state and problem.descriptor.has_score):
# These are not problems, and do not have a score
return (None, None)
correct = 0.0
# If the ID is not in the cache, add the item
......
......@@ -69,10 +69,10 @@ class StudentModuleCache(object):
"""
def __init__(self, user, descriptors):
'''
Find any StudentModule objects that are needed by any child modules of the
supplied descriptor, or caches only the StudentModule objects specifically
for every descriptor in descriptors. Avoids making multiple queries to the
database.
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
......@@ -134,7 +134,8 @@ class StudentModuleCache(object):
'''
keys = []
for descriptor in descriptors:
keys.append(descriptor.location.url())
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:
......
......@@ -51,7 +51,7 @@ def update_template_dictionary(dictionary, request=None, course=None, article=No
def view(request, article_path, course_id=None):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, article_path, course)
if err:
......@@ -67,7 +67,7 @@ def view(request, article_path, course_id=None):
def view_revision(request, revision_number, article_path, course_id=None):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, article_path, course)
if err:
......@@ -91,7 +91,7 @@ def view_revision(request, revision_number, article_path, course_id=None):
def root_redirect(request, course_id=None):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
#TODO: Add a default namespace to settings.
namespace = course.wiki_namespace if course else "edX"
......@@ -109,7 +109,7 @@ def root_redirect(request, course_id=None):
def create(request, article_path, course_id=None):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
article_path_components = article_path.split('/')
......@@ -170,7 +170,7 @@ def create(request, article_path, course_id=None):
def edit(request, article_path, course_id=None):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, article_path, course)
if err:
......@@ -218,7 +218,7 @@ def edit(request, article_path, course_id=None):
def history(request, article_path, page=1, course_id=None):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, article_path, course)
if err:
......@@ -300,7 +300,7 @@ def history(request, article_path, page=1, course_id=None):
def revision_feed(request, page=1, namespace=None, course_id=None):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
page_size = 10
......@@ -333,7 +333,7 @@ def revision_feed(request, page=1, namespace=None, course_id=None):
def search_articles(request, namespace=None, course_id=None):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
# blampe: We should check for the presence of other popular django search
# apps and use those if possible. Only fall back on this as a last resort.
......@@ -382,7 +382,7 @@ def search_articles(request, namespace=None, course_id=None):
def search_add_related(request, course_id, slug, namespace):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, slug, namespace if namespace else course_id)
if err:
......@@ -415,7 +415,7 @@ def search_add_related(request, course_id, slug, namespace):
def add_related(request, course_id, slug, namespace):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, slug, namespace if namespace else course_id)
if err:
......@@ -439,7 +439,7 @@ def add_related(request, course_id, slug, namespace):
def remove_related(request, course_id, namespace, slug, related_id):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, slug, namespace if namespace else course_id)
......@@ -462,7 +462,7 @@ def remove_related(request, course_id, namespace, slug, related_id):
def random_article(request, course_id=None):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
from random import randint
num_arts = Article.objects.count()
......
......@@ -6,7 +6,7 @@ from lxml import etree
@login_required
def index(request, course_id, page=0):
course = check_course(course_id)
course = check_course(request.user, course_id)
raw_table_of_contents = open('lms/templates/book_toc.xml', 'r') # TODO: This will need to come from S3
table_of_contents = etree.parse(raw_table_of_contents).getroot()
return render_to_response('staticbook.html', {'page': int(page), 'course': course, 'table_of_contents': table_of_contents})
......
......@@ -48,6 +48,7 @@ MITX_FEATURES = {
## DO NOT SET TO True IN THIS FILE
## Doing so will cause all courses to be released on production
'DISABLE_START_DATES': False, # When True, all courses will be active, regardless of start date
'DARK_LAUNCH': False, # When True, courses will be active for staff only
'ENABLE_TEXTBOOK' : True,
'ENABLE_DISCUSSION' : True,
......@@ -55,6 +56,8 @@ MITX_FEATURES = {
'ENABLE_SQL_TRACKING_LOGS': False,
'ENABLE_LMS_MIGRATION': False,
'DISABLE_LOGIN_BUTTON': False, # used in systems where login is automatic, eg MIT SSL
# extrernal access methods
'ACCESS_REQUIRE_STAFF_FOR_COURSE': False,
'AUTH_USE_OPENID': False,
......@@ -86,6 +89,18 @@ sys.path.append(PROJECT_ROOT / 'lib')
sys.path.append(COMMON_ROOT / 'djangoapps')
sys.path.append(COMMON_ROOT / 'lib')
# For Node.js
system_node_path = os.environ.get("NODE_PATH", None)
if system_node_path is None:
system_node_path = "/usr/local/lib/node_modules"
node_paths = [COMMON_ROOT / "static/js/vendor",
COMMON_ROOT / "static/coffee/src",
system_node_path
]
NODE_PATH = ':'.join(node_paths)
################################## MITXWEB #####################################
# This is where we stick our compiled template files. Most of the app uses Mako
# templates
......@@ -347,7 +362,7 @@ PIPELINE_ALWAYS_RECOMPILE = ['sass/application.scss', 'sass/ie.scss', 'sass/cour
courseware_only_js = [
PROJECT_ROOT / 'static/coffee/src/' + pth + '.coffee'
for pth
in ['courseware', 'histogram', 'navigation', 'time', ]
in ['courseware', 'histogram', 'navigation', 'time']
]
courseware_only_js += [
pth for pth
......@@ -475,6 +490,7 @@ if os.path.isdir(DATA_DIR):
js_timestamp = os.stat(js_dir / new_filename).st_mtime
if coffee_timestamp <= js_timestamp:
continue
os.system("rm %s" % (js_dir / new_filename))
os.system("coffee -c %s" % (js_dir / filename))
PIPELINE_COMPILERS = [
......
......@@ -62,6 +62,7 @@ SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
################################ LMS Migration #################################
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'
LMS_MIGRATION_ALLOWED_IPS = ['127.0.0.1']
......
......@@ -10,14 +10,27 @@ sessions. Assumes structure:
from .common import *
from .logsettings import get_logger_config
from .dev import *
import socket
WIKI_ENABLED = False
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
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
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
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') # django 1.4 for nginx ssl proxy
#-----------------------------------------------------------------------------
# disable django debug toolbars
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') ])
from ..dev import *
CLASSES_TO_DBS = {
'BerkeleyX/CS169.1x/2012_Fall' : "cs169.db",
'BerkeleyX/CS188.1x/2012_Fall' : "cs188_1.db",
'HarvardX/CS50x/2012' : "cs50.db",
'HarvardX/PH207x/2012_Fall' : "ph207.db",
'MITx/3.091x/2012_Fall' : "3091.db",
'MITx/6.002x/2012_Fall' : "6002.db",
'MITx/6.00x/2012_Fall' : "600.db",
}
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': '127.0.0.1:11211',
'KEY_FUNCTION': 'util.memcache.safe_key',
},
'general': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': '127.0.0.1:11211',
'KEY_PREFIX' : 'general',
'VERSION' : 5,
'KEY_FUNCTION': 'util.memcache.safe_key',
}
}
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
def path_for_db(db_name):
return ENV_ROOT / "db" / db_name
def course_db_for(course_id):
db_name = CLASSES_TO_DBS[course_id]
return {
'default' : {
'ENGINE' : 'django.db.backends.sqlite3',
'NAME' : path_for_db(db_name)
}
}
from .courses import *
DATABASES = course_db_for('HarvardX/CS50x/2012')
\ No newline at end of file
from .courses import *
DATABASES = course_db_for('MITx/6.002x/2012_Fall')
\ No newline at end of file
"""
Note that for this to work at all, you must have memcached running (or you won't
get shared sessions)
"""
from courses import *
# Move this to a shared file later:
for class_id, db_name in CLASSES_TO_DBS.items():
DATABASES[class_id] = {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': path_for_db(db_name)
}
......@@ -25,6 +25,7 @@ SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
# Nose Test Runner
INSTALLED_APPS += ('django_nose',)
NOSE_ARGS = ['--cover-erase', '--with-xunit', '--with-xcoverage', '--cover-html',
# '-v', '--pdb', # When really stuck, uncomment to start debugger on error
'--cover-inclusive', '--cover-html-dir',
os.environ.get('NOSE_COVER_HTML_DIR', 'cover_html')]
for app in os.listdir(PROJECT_ROOT / 'djangoapps'):
......@@ -66,6 +67,17 @@ DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': PROJECT_ROOT / "db" / "mitx.db",
},
# The following are for testing purposes...
'edX/toy/2012_Fall': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "course1.db",
},
'edx/full/6.002_Spring_2012': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "course2.db",
}
}
......
......@@ -189,6 +189,10 @@ p.mini {
color: #999;
}
img.help-tooltip {
cursor: help;
}
p img, h1 img, h2 img, h3 img, h4 img, td img {
vertical-align: middle;
}
......@@ -259,7 +263,7 @@ tfoot td {
color: #666;
padding: 2px 5px;
font-size: 11px;
background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x;
background: #e1e1e1 url(../img/nav-bg.gif) top left repeat-x;
border-left: 1px solid #ddd;
border-bottom: 1px solid #ddd;
}
......@@ -305,25 +309,84 @@ tr.alt {
/* SORTABLE TABLES */
thead th {
padding: 2px 5px;
line-height: normal;
}
thead th a:link, thead th a:visited {
color: #666;
}
thead th.sorted {
background: #c5c5c5 url(../img/nav-bg-selected.gif) top left repeat-x;
}
table thead th .text span {
padding: 2px 5px;
display:block;
}
table thead th .text a {
display: block;
cursor: pointer;
padding: 2px 5px;
}
table thead th.sortable:hover {
background: white url(../img/nav-bg-reverse.gif) 0 -5px repeat-x;
}
thead th.sorted a.sortremove {
visibility: hidden;
}
table thead th.sorted:hover a.sortremove {
visibility: visible;
}
table thead th.sorted .sortoptions {
display: block;
padding: 4px 5px 0 5px;
float: right;
text-align: right;
}
table thead th.sorted .sortpriority {
font-size: .8em;
min-width: 12px;
text-align: center;
vertical-align: top;
}
table thead th.sorted .sortoptions a {
width: 14px;
height: 12px;
display: inline-block;
}
table thead th.sorted .sortoptions a.sortremove {
background: url(../img/sorting-icons.gif) -4px -5px no-repeat;
}
table thead th.sorted .sortoptions a.sortremove:hover {
background: url(../img/sorting-icons.gif) -4px -27px no-repeat;
}
table thead th.sorted {
background-position: bottom left !important;
table thead th.sorted .sortoptions a.ascending {
background: url(../img/sorting-icons.gif) -5px -50px no-repeat;
}
table thead th.sorted a {
padding-right: 13px;
table thead th.sorted .sortoptions a.ascending:hover {
background: url(../img/sorting-icons.gif) -5px -72px no-repeat;
}
table thead th.ascending a {
background: url(../img/admin/arrow-up.gif) right .4em no-repeat;
table thead th.sorted .sortoptions a.descending {
background: url(../img/sorting-icons.gif) -5px -94px no-repeat;
}
table thead th.descending a {
background: url(../img/admin/arrow-down.gif) right .4em no-repeat;
table thead th.sorted .sortoptions a.descending:hover {
background: url(../img/sorting-icons.gif) -5px -115px no-repeat;
}
/* ORDERABLE TABLES */
......@@ -334,7 +397,7 @@ table.orderable tbody tr td:hover {
table.orderable tbody tr td:first-child {
padding-left: 14px;
background-image: url(../img/admin/nav-bg-grabber.gif);
background-image: url(../img/nav-bg-grabber.gif);
background-repeat: repeat-y;
}
......@@ -364,7 +427,7 @@ input[type=text], input[type=password], textarea, select, .vTextField {
/* FORM BUTTONS */
.button, input[type=submit], input[type=button], .submit-row input {
background: white url(../img/admin/nav-bg.gif) bottom repeat-x;
background: white url(../img/nav-bg.gif) bottom repeat-x;
padding: 3px 5px;
color: black;
border: 1px solid #bbb;
......@@ -372,31 +435,31 @@ input[type=text], input[type=password], textarea, select, .vTextField {
}
.button:active, input[type=submit]:active, input[type=button]:active {
background-image: url(../img/admin/nav-bg-reverse.gif);
background-image: url(../img/nav-bg-reverse.gif);
background-position: top;
}
.button[disabled], input[type=submit][disabled], input[type=button][disabled] {
background-image: url(../img/admin/nav-bg.gif);
background-image: url(../img/nav-bg.gif);
background-position: bottom;
opacity: 0.4;
}
.button.default, input[type=submit].default, .submit-row input.default {
border: 2px solid #5b80b2;
background: #7CA0C7 url(../img/admin/default-bg.gif) bottom repeat-x;
background: #7CA0C7 url(../img/default-bg.gif) bottom repeat-x;
font-weight: bold;
color: white;
float: right;
}
.button.default:active, input[type=submit].default:active {
background-image: url(../img/admin/default-bg-reverse.gif);
background-image: url(../img/default-bg-reverse.gif);
background-position: top;
}
.button[disabled].default, input[type=submit][disabled].default, input[type=button][disabled].default {
background-image: url(../img/admin/default-bg.gif);
background-image: url(../img/default-bg.gif);
background-position: bottom;
opacity: 0.4;
}
......@@ -433,7 +496,7 @@ input[type=text], input[type=password], textarea, select, .vTextField {
font-size: 11px;
text-align: left;
font-weight: bold;
background: #7CA0C7 url(../img/admin/default-bg.gif) top left repeat-x;
background: #7CA0C7 url(../img/default-bg.gif) top left repeat-x;
color: white;
}
......@@ -455,15 +518,15 @@ ul.messagelist li {
margin: 0 0 3px 0;
border-bottom: 1px solid #ddd;
color: #666;
background: #ffc url(../img/admin/icon_success.gif) 5px .3em no-repeat;
background: #ffc url(../img/icon_success.gif) 5px .3em no-repeat;
}
ul.messagelist li.warning{
background-image: url(../img/admin/icon_alert.gif);
background-image: url(../img/icon_alert.gif);
}
ul.messagelist li.error{
background-image: url(../img/admin/icon_error.gif);
background-image: url(../img/icon_error.gif);
}
.errornote {
......@@ -473,7 +536,7 @@ ul.messagelist li.error{
margin: 0 0 3px 0;
border: 1px solid red;
color: red;
background: #ffc url(../img/admin/icon_error.gif) 5px .3em no-repeat;
background: #ffc url(../img/icon_error.gif) 5px .3em no-repeat;
}
ul.errorlist {
......@@ -488,7 +551,7 @@ ul.errorlist {
margin: 0 0 3px 0;
border: 1px solid red;
color: white;
background: red url(../img/admin/icon_alert.gif) 5px .3em no-repeat;
background: red url(../img/icon_alert.gif) 5px .3em no-repeat;
}
.errorlist li a {
......@@ -524,7 +587,7 @@ div.system-message p.system-message-title {
padding: 4px 5px 4px 25px;
margin: 0;
color: red;
background: #ffc url(../img/admin/icon_error.gif) 5px .3em no-repeat;
background: #ffc url(../img/icon_error.gif) 5px .3em no-repeat;
}
.description {
......@@ -535,7 +598,7 @@ div.system-message p.system-message-title {
/* BREADCRUMBS */
div.breadcrumbs {
background: white url(../img/admin/nav-bg-reverse.gif) 0 -10px repeat-x;
background: white url(../img/nav-bg-reverse.gif) 0 -10px repeat-x;
padding: 2px 8px 3px 8px;
font-size: 11px;
color: #999;
......@@ -548,17 +611,17 @@ div.breadcrumbs {
.addlink {
padding-left: 12px;
background: url(../img/admin/icon_addlink.gif) 0 .2em no-repeat;
background: url(../img/icon_addlink.gif) 0 .2em no-repeat;
}
.changelink {
padding-left: 12px;
background: url(../img/admin/icon_changelink.gif) 0 .2em no-repeat;
background: url(../img/icon_changelink.gif) 0 .2em no-repeat;
}
.deletelink {
padding-left: 12px;
background: url(../img/admin/icon_deletelink.gif) 0 .25em no-repeat;
background: url(../img/icon_deletelink.gif) 0 .25em no-repeat;
}
a.deletelink:link, a.deletelink:visited {
......@@ -593,14 +656,14 @@ a.deletelink:hover {
.object-tools li {
display: block;
float: left;
background: url(../img/admin/tool-left.gif) 0 0 no-repeat;
background: url(../img/tool-left.gif) 0 0 no-repeat;
padding: 0 0 0 8px;
margin-left: 2px;
height: 16px;
}
.object-tools li:hover {
background: url(../img/admin/tool-left_over.gif) 0 0 no-repeat;
background: url(../img/tool-left_over.gif) 0 0 no-repeat;
}
.object-tools a:link, .object-tools a:visited {
......@@ -609,29 +672,29 @@ a.deletelink:hover {
color: white;
padding: .1em 14px .1em 8px;
height: 14px;
background: #999 url(../img/admin/tool-right.gif) 100% 0 no-repeat;
background: #999 url(../img/tool-right.gif) 100% 0 no-repeat;
}
.object-tools a:hover, .object-tools li:hover a {
background: #5b80b2 url(../img/admin/tool-right_over.gif) 100% 0 no-repeat;
background: #5b80b2 url(../img/tool-right_over.gif) 100% 0 no-repeat;
}
.object-tools a.viewsitelink, .object-tools a.golink {
background: #999 url(../img/admin/tooltag-arrowright.gif) top right no-repeat;
background: #999 url(../img/tooltag-arrowright.gif) top right no-repeat;
padding-right: 28px;
}
.object-tools a.viewsitelink:hover, .object-tools a.golink:hover {
background: #5b80b2 url(../img/admin/tooltag-arrowright_over.gif) top right no-repeat;
background: #5b80b2 url(../img/tooltag-arrowright_over.gif) top right no-repeat;
}
.object-tools a.addlink {
background: #999 url(../img/admin/tooltag-add.gif) top right no-repeat;
background: #999 url(../img/tooltag-add.gif) top right no-repeat;
padding-right: 28px;
}
.object-tools a.addlink:hover {
background: #5b80b2 url(../img/admin/tooltag-add_over.gif) top right no-repeat;
background: #5b80b2 url(../img/tooltag-add_over.gif) top right no-repeat;
}
/* OBJECT HISTORY */
......@@ -766,7 +829,7 @@ table#change-history tbody th {
}
#content-related .module h2 {
background: #eee url(../img/admin/nav-bg.gif) bottom left repeat-x;
background: #eee url(../img/nav-bg.gif) bottom left repeat-x;
color: #666;
}
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