Merge branch 'master' of github.com:dementrock/mitx into ccp0101/performance
Conflicts: common/lib/xmodule/xmodule/discussion_module.py lms/djangoapps/django_comment_client/forum/views.py lms/templates/discussion/_thread.html lms/urls.py
1
.gitignore
vendored
@@ -25,3 +25,4 @@ Gemfile.lock
|
||||
.env/
|
||||
lms/static/sass/*.css
|
||||
cms/static/sass/*.css
|
||||
lms/lib/comment_client/python
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -90,6 +90,16 @@ TEMPLATE_CONTEXT_PROCESSORS = (
|
||||
################################# Jasmine ###################################
|
||||
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
|
||||
|
||||
|
||||
#################### CAPA External Code Evaluation #############################
|
||||
XQUEUE_INTERFACE = {
|
||||
'url': 'http://localhost:8888',
|
||||
'django_auth': {'username': 'local',
|
||||
'password': 'local'},
|
||||
'basic_auth': None,
|
||||
}
|
||||
|
||||
|
||||
################################# Middleware ###################################
|
||||
# List of finder classes that know how to find static files in
|
||||
# various locations.
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
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']:
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
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])
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
##
|
||||
## 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)
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.auth.models import User
|
||||
import comment_client
|
||||
import comment_client as cc
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -15,8 +15,5 @@ service'''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
for user in User.objects.all().iterator():
|
||||
comment_client.update_user(user.id, {
|
||||
'id': user.id,
|
||||
'username': user.username,
|
||||
'email': user.email
|
||||
})
|
||||
cc_user = cc.User.from_django_user(user)
|
||||
cc_user.save()
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
"""
|
||||
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,10 +35,14 @@ 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 django.db.models.signals import post_save
|
||||
@@ -21,15 +50,35 @@ from django.dispatch import receiver
|
||||
|
||||
from functools import partial
|
||||
|
||||
import comment_client
|
||||
import comment_client as cc
|
||||
|
||||
import logging
|
||||
|
||||
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"
|
||||
|
||||
@@ -216,17 +265,160 @@ def add_user_to_default_group(user, group):
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def update_user_information(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
func = comment_client.create_user
|
||||
else:
|
||||
func = partial(comment_client.update_user, user_id=instance.id)
|
||||
try:
|
||||
func(attributes={
|
||||
'id': instance.id,
|
||||
'username': instance.username,
|
||||
'email': instance.email,
|
||||
})
|
||||
cc_user = cc.User.from_django_user(instance)
|
||||
cc_user.save()
|
||||
except Exception as e:
|
||||
log = logging.getLogger("mitx.discussion")
|
||||
log.error(unicode(e))
|
||||
log.error("update user info to discussion failed for user with id: " + str(instance.id))
|
||||
|
||||
########################## 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)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import datetime
|
||||
import feedparser
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import string
|
||||
import sys
|
||||
import uuid
|
||||
import feedparser
|
||||
import time
|
||||
import urllib
|
||||
import itertools
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import logout, authenticate, login
|
||||
@@ -26,17 +27,19 @@ from bs4 import BeautifulSoup
|
||||
from django.core.cache import cache
|
||||
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from student.models import Registration, UserProfile, PendingNameChange, PendingEmailChange, CourseEnrollment
|
||||
from student.models import (Registration, UserProfile,
|
||||
PendingNameChange, PendingEmailChange,
|
||||
CourseEnrollment)
|
||||
from util.cache import cache_if_anonymous
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from models import Registration, UserProfile, PendingNameChange, PendingEmailChange, CourseEnrollment
|
||||
from datetime import date
|
||||
from collections import namedtuple
|
||||
from courseware.courses import course_staff_group_name, has_staff_access_to_course, get_courses_by_university
|
||||
from courseware.courses import (course_staff_group_name, has_staff_access_to_course,
|
||||
get_courses_by_university)
|
||||
|
||||
log = logging.getLogger("mitx.student")
|
||||
Article = namedtuple('Article', 'title url author image deck publication publish_date')
|
||||
@@ -47,7 +50,8 @@ def csrf_token(context):
|
||||
csrf_token = context.get('csrf_token', '')
|
||||
if csrf_token == 'NOTPROVIDED':
|
||||
return ''
|
||||
return u'<div style="display:none"><input type="hidden" name="csrfmiddlewaretoken" value="%s" /></div>' % (csrf_token)
|
||||
return (u'<div style="display:none"><input type="hidden"'
|
||||
' name="csrfmiddlewaretoken" value="%s" /></div>' % (csrf_token))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -94,8 +98,9 @@ def main_index(extra_context = {}, user=None):
|
||||
context.update(extra_context)
|
||||
return render_to_response('index.html', context)
|
||||
|
||||
def course_from_id(id):
|
||||
course_loc = CourseDescriptor.id_to_location(id)
|
||||
def course_from_id(course_id):
|
||||
"""Return the CourseDescriptor corresponding to this course_id"""
|
||||
course_loc = CourseDescriptor.id_to_location(course_id)
|
||||
return modulestore().get_item(course_loc)
|
||||
|
||||
|
||||
@@ -127,7 +132,7 @@ def dashboard(request):
|
||||
try:
|
||||
courses.append(course_from_id(enrollment.course_id))
|
||||
except ItemNotFoundError:
|
||||
log.error("User {0} enrolled in non-existant course {1}"
|
||||
log.error("User {0} enrolled in non-existent course {1}"
|
||||
.format(user.username, enrollment.course_id))
|
||||
|
||||
message = ""
|
||||
@@ -158,18 +163,41 @@ def try_change_enrollment(request):
|
||||
|
||||
@login_required
|
||||
def change_enrollment_view(request):
|
||||
"""Delegate to change_enrollment to actually do the work."""
|
||||
return HttpResponse(json.dumps(change_enrollment(request)))
|
||||
|
||||
def enrollment_allowed(user, course):
|
||||
"""If the course has an enrollment period, check whether we are in it.
|
||||
Also respects the DARK_LAUNCH setting"""
|
||||
now = time.gmtime()
|
||||
start = course.enrollment_start
|
||||
end = course.enrollment_end
|
||||
|
||||
if (start is None or now > start) and (end is None or now < end):
|
||||
# in enrollment period.
|
||||
return True
|
||||
|
||||
if settings.MITX_FEATURES['DARK_LAUNCH']:
|
||||
if has_staff_access_to_course(user, course):
|
||||
# if dark launch, staff can enroll outside enrollment window
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def change_enrollment(request):
|
||||
if request.method != "POST":
|
||||
raise Http404
|
||||
|
||||
action = request.POST.get("enrollment_action", "")
|
||||
user = request.user
|
||||
if not user.is_authenticated():
|
||||
raise Http404
|
||||
|
||||
action = request.POST.get("enrollment_action", "")
|
||||
|
||||
course_id = request.POST.get("course_id", None)
|
||||
if course_id == None:
|
||||
return HttpResponse(json.dumps({'success': False, 'error': 'There was an error receiving the course id.'}))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'There was an error receiving the course id.'}))
|
||||
|
||||
if action == "enroll":
|
||||
# Make sure the course exists
|
||||
@@ -177,17 +205,25 @@ def change_enrollment(request):
|
||||
try:
|
||||
course = course_from_id(course_id)
|
||||
except ItemNotFoundError:
|
||||
log.error("User {0} tried to enroll in non-existant course {1}"
|
||||
log.warning("User {0} tried to enroll in non-existant course {1}"
|
||||
.format(user.username, enrollment.course_id))
|
||||
return {'success': False, 'error': 'The course requested does not exist.'}
|
||||
|
||||
if settings.MITX_FEATURES.get('ACCESS_REQUIRE_STAFF_FOR_COURSE'):
|
||||
# require that user be in the staff_* group (or be an overall admin) to be able to enroll
|
||||
# eg staff_6.002x or staff_6.00x
|
||||
if not has_staff_access_to_course(user,course):
|
||||
# require that user be in the staff_* group (or be an
|
||||
# overall admin) to be able to enroll eg staff_6.002x or
|
||||
# staff_6.00x
|
||||
if not has_staff_access_to_course(user, course):
|
||||
staff_group = course_staff_group_name(course)
|
||||
log.debug('user %s denied enrollment to %s ; not in %s' % (user,course.location.url(),staff_group))
|
||||
return {'success': False, 'error' : '%s membership required to access course.' % staff_group}
|
||||
log.debug('user %s denied enrollment to %s ; not in %s' % (
|
||||
user, course.location.url(), staff_group))
|
||||
return {'success': False,
|
||||
'error' : '%s membership required to access course.' % staff_group}
|
||||
|
||||
if not enrollment_allowed(user, course):
|
||||
return {'success': False,
|
||||
'error': 'enrollment in {} not allowed at this time'
|
||||
.format(course.display_name)}
|
||||
|
||||
enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id)
|
||||
return {'success': True}
|
||||
@@ -264,6 +300,7 @@ def logout_user(request):
|
||||
def change_setting(request):
|
||||
''' JSON call to change a profile setting: Right now, location
|
||||
'''
|
||||
# TODO (vshnayder): location is no longer used
|
||||
up = UserProfile.objects.get(user=request.user) # request.user.profile_cache
|
||||
if 'location' in request.POST:
|
||||
up.location = request.POST['location']
|
||||
@@ -272,6 +309,59 @@ def change_setting(request):
|
||||
return HttpResponse(json.dumps({'success': True,
|
||||
'location': up.location, }))
|
||||
|
||||
def _do_create_account(post_vars):
|
||||
"""
|
||||
Given cleaned post variables, create the User and UserProfile objects, as well as the
|
||||
registration for this user.
|
||||
|
||||
Returns a tuple (User, UserProfile, Registration).
|
||||
|
||||
Note: this function is also used for creating test users.
|
||||
"""
|
||||
user = User(username=post_vars['username'],
|
||||
email=post_vars['email'],
|
||||
is_active=False)
|
||||
user.set_password(post_vars['password'])
|
||||
registration = Registration()
|
||||
# TODO: Rearrange so that if part of the process fails, the whole process fails.
|
||||
# Right now, we can have e.g. no registration e-mail sent out and a zombie account
|
||||
try:
|
||||
user.save()
|
||||
except IntegrityError:
|
||||
js = {'success': False}
|
||||
# Figure out the cause of the integrity error
|
||||
if len(User.objects.filter(username=post_vars['username'])) > 0:
|
||||
js['value'] = "An account with this username already exists."
|
||||
js['field'] = 'username'
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
if len(User.objects.filter(email=post_vars['email'])) > 0:
|
||||
js['value'] = "An account with this e-mail already exists."
|
||||
js['field'] = 'email'
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
raise
|
||||
|
||||
registration.register(user)
|
||||
|
||||
profile = UserProfile(user=user)
|
||||
profile.name = post_vars['name']
|
||||
profile.level_of_education = post_vars.get('level_of_education')
|
||||
profile.gender = post_vars.get('gender')
|
||||
profile.mailing_address = post_vars.get('mailing_address')
|
||||
profile.goals = post_vars.get('goals')
|
||||
|
||||
try:
|
||||
profile.year_of_birth = int(post_vars['year_of_birth'])
|
||||
except (ValueError, KeyError):
|
||||
profile.year_of_birth = None # If they give us garbage, just ignore it instead
|
||||
# of asking them to put an integer.
|
||||
try:
|
||||
profile.save()
|
||||
except Exception:
|
||||
log.exception("UserProfile creation failed for user {0}.".format(user.id))
|
||||
return (user, profile, registration)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def create_account(request, post_override=None):
|
||||
@@ -343,50 +433,14 @@ def create_account(request, post_override=None):
|
||||
js['field'] = 'username'
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
u = User(username=post_vars['username'],
|
||||
email=post_vars['email'],
|
||||
is_active=False)
|
||||
u.set_password(post_vars['password'])
|
||||
r = Registration()
|
||||
# TODO: Rearrange so that if part of the process fails, the whole process fails.
|
||||
# Right now, we can have e.g. no registration e-mail sent out and a zombie account
|
||||
try:
|
||||
u.save()
|
||||
except IntegrityError:
|
||||
# Figure out the cause of the integrity error
|
||||
if len(User.objects.filter(username=post_vars['username'])) > 0:
|
||||
js['value'] = "An account with this username already exists."
|
||||
js['field'] = 'username'
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
if len(User.objects.filter(email=post_vars['email'])) > 0:
|
||||
js['value'] = "An account with this e-mail already exists."
|
||||
js['field'] = 'email'
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
raise
|
||||
|
||||
r.register(u)
|
||||
|
||||
up = UserProfile(user=u)
|
||||
up.name = post_vars['name']
|
||||
up.level_of_education = post_vars.get('level_of_education')
|
||||
up.gender = post_vars.get('gender')
|
||||
up.mailing_address = post_vars.get('mailing_address')
|
||||
up.goals = post_vars.get('goals')
|
||||
|
||||
try:
|
||||
up.year_of_birth = int(post_vars['year_of_birth'])
|
||||
except (ValueError, KeyError):
|
||||
up.year_of_birth = None # If they give us garbage, just ignore it instead
|
||||
# of asking them to put an integer.
|
||||
try:
|
||||
up.save()
|
||||
except Exception:
|
||||
log.exception("UserProfile creation failed for user {0}.".format(u.id))
|
||||
# Ok, looks like everything is legit. Create the account.
|
||||
ret = _do_create_account(post_vars)
|
||||
if isinstance(ret,HttpResponse): # if there was an error then return that
|
||||
return ret
|
||||
(user, profile, registration) = ret
|
||||
|
||||
d = {'name': post_vars['name'],
|
||||
'key': r.activation_key,
|
||||
'key': registration.activation_key,
|
||||
}
|
||||
|
||||
# composes activation email
|
||||
@@ -398,10 +452,11 @@ def create_account(request, post_override=None):
|
||||
try:
|
||||
if settings.MITX_FEATURES.get('REROUTE_ACTIVATION_EMAIL'):
|
||||
dest_addr = settings.MITX_FEATURES['REROUTE_ACTIVATION_EMAIL']
|
||||
message = "Activation for %s (%s): %s\n" % (u, u.email, up.name) + '-' * 80 + '\n\n' + message
|
||||
message = ("Activation for %s (%s): %s\n" % (user, user.email, profile.name) +
|
||||
'-' * 80 + '\n\n' + message)
|
||||
send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [dest_addr], fail_silently=False)
|
||||
elif not settings.GENERATE_RANDOM_USER_CREDENTIALS:
|
||||
res = u.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
|
||||
res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
|
||||
except:
|
||||
log.exception(sys.exc_info())
|
||||
js['value'] = 'Could not send activation e-mail.'
|
||||
@@ -431,24 +486,30 @@ def create_account(request, post_override=None):
|
||||
return HttpResponse(json.dumps(js), mimetype="application/json")
|
||||
|
||||
|
||||
def create_random_account(create_account_function):
|
||||
|
||||
def get_random_post_override():
|
||||
"""
|
||||
Return a dictionary suitable for passing to post_vars of _do_create_account or post_override
|
||||
of create_account, with random user info.
|
||||
"""
|
||||
def id_generator(size=6, chars=string.ascii_uppercase + string.ascii_lowercase + string.digits):
|
||||
return ''.join(random.choice(chars) for x in range(size))
|
||||
|
||||
def inner_create_random_account(request):
|
||||
post_override = {'username': "random_" + id_generator(),
|
||||
'email': id_generator(size=10, chars=string.ascii_lowercase) + "_dummy_test@mitx.mit.edu",
|
||||
'password': id_generator(),
|
||||
'location': id_generator(size=5, chars=string.ascii_uppercase),
|
||||
'name': id_generator(size=5, chars=string.ascii_lowercase) + " " + id_generator(size=7, chars=string.ascii_lowercase),
|
||||
'honor_code': u'true',
|
||||
'terms_of_service': u'true', }
|
||||
return {'username': "random_" + id_generator(),
|
||||
'email': id_generator(size=10, chars=string.ascii_lowercase) + "_dummy_test@mitx.mit.edu",
|
||||
'password': id_generator(),
|
||||
'name': (id_generator(size=5, chars=string.ascii_lowercase) + " " +
|
||||
id_generator(size=7, chars=string.ascii_lowercase)),
|
||||
'honor_code': u'true',
|
||||
'terms_of_service': u'true', }
|
||||
|
||||
return create_account_function(request, post_override=post_override)
|
||||
|
||||
def create_random_account(create_account_function):
|
||||
def inner_create_random_account(request):
|
||||
return create_account_function(request, post_override=get_random_post_override())
|
||||
|
||||
return inner_create_random_account
|
||||
|
||||
# TODO (vshnayder): do we need GENERATE_RANDOM_USER_CREDENTIALS for anything?
|
||||
if settings.GENERATE_RANDOM_USER_CREDENTIALS:
|
||||
create_account = create_random_account(create_account)
|
||||
|
||||
@@ -514,7 +575,7 @@ def reactivation_email(request):
|
||||
subject = ''.join(subject.splitlines())
|
||||
message = render_to_string('reactivation_email.txt', d)
|
||||
|
||||
res = u.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
|
||||
res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
|
||||
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
|
||||
@@ -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):
|
||||
@@ -229,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:
|
||||
@@ -295,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
|
||||
@@ -306,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'):
|
||||
@@ -393,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('<','<')
|
||||
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 = {'"': '"'}
|
||||
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:
|
||||
|
||||
30
common/lib/capa/capa/javascript_problem_generator.js
Normal file
@@ -0,0 +1,30 @@
|
||||
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()));
|
||||
26
common/lib/capa/capa/javascript_problem_grader.js
Normal file
@@ -0,0 +1,26 @@
|
||||
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));
|
||||
@@ -17,7 +17,11 @@ import random
|
||||
import re
|
||||
import requests
|
||||
import traceback
|
||||
import hashlib
|
||||
import abc
|
||||
import os
|
||||
import subprocess
|
||||
import xml.sax.saxutils as saxutils
|
||||
|
||||
# specific library imports
|
||||
from calc import evaluator, UndefinedVariable
|
||||
@@ -71,7 +75,6 @@ class LoncapaResponse(object):
|
||||
|
||||
In addition, these methods are optional:
|
||||
|
||||
- get_max_score : if defined, this is called to obtain the maximum score possible for this question
|
||||
- setup_response : find and note the answer input field IDs for the response; called by __init__
|
||||
- check_hint_condition : check to see if the student's answers satisfy a particular condition for a hint to be displayed
|
||||
- render_html : render this Response as HTML (must return XHTML compliant string)
|
||||
@@ -130,6 +133,11 @@ class LoncapaResponse(object):
|
||||
if self.max_inputfields == 1:
|
||||
self.answer_id = self.answer_ids[0] # for convenience
|
||||
|
||||
self.maxpoints = dict()
|
||||
for inputfield in self.inputfields:
|
||||
maxpoints = inputfield.get('points','1') # By default, each answerfield is worth 1 point
|
||||
self.maxpoints.update({inputfield.get('id'): int(maxpoints)})
|
||||
|
||||
self.default_answer_map = {} # dict for default answer map (provided in input elements)
|
||||
for entry in self.inputfields:
|
||||
answer = entry.get('correct_answer')
|
||||
@@ -139,6 +147,12 @@ class LoncapaResponse(object):
|
||||
if hasattr(self, 'setup_response'):
|
||||
self.setup_response()
|
||||
|
||||
def get_max_score(self):
|
||||
'''
|
||||
Return the total maximum points of all answer fields under this Response
|
||||
'''
|
||||
return sum(self.maxpoints.values())
|
||||
|
||||
def render_html(self, renderer):
|
||||
'''
|
||||
Return XHTML Element tree representation of this Response.
|
||||
@@ -272,9 +286,190 @@ class LoncapaResponse(object):
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class JavascriptResponse(LoncapaResponse):
|
||||
'''
|
||||
This response type is used when the student's answer is graded via
|
||||
Javascript using Node.js.
|
||||
'''
|
||||
|
||||
response_tag = 'javascriptresponse'
|
||||
max_inputfields = 1
|
||||
allowed_inputfields = ['javascriptinput']
|
||||
|
||||
def setup_response(self):
|
||||
|
||||
# Sets up generator, grader, display, and their dependencies.
|
||||
self.parse_xml()
|
||||
|
||||
self.compile_display_javascript()
|
||||
|
||||
self.params = self.extract_params()
|
||||
|
||||
if self.generator:
|
||||
self.problem_state = self.generate_problem_state()
|
||||
else:
|
||||
self.problem_state = None
|
||||
|
||||
self.solution = None
|
||||
|
||||
self.prepare_inputfield()
|
||||
|
||||
def compile_display_javascript(self):
|
||||
|
||||
latestTimestamp = 0
|
||||
basepath = self.system.filestore.root_path + '/js/'
|
||||
for filename in (self.display_dependencies + [self.display]):
|
||||
filepath = basepath + filename
|
||||
timestamp = os.stat(filepath).st_mtime
|
||||
if timestamp > latestTimestamp:
|
||||
latestTimestamp = timestamp
|
||||
|
||||
h = hashlib.md5()
|
||||
h.update(self.answer_id + str(self.display_dependencies))
|
||||
compiled_filename = 'compiled/' + h.hexdigest() + '.js'
|
||||
compiled_filepath = basepath + compiled_filename
|
||||
|
||||
if not os.path.exists(compiled_filepath) or os.stat(compiled_filepath).st_mtime < latestTimestamp:
|
||||
outfile = open(compiled_filepath, 'w')
|
||||
for filename in (self.display_dependencies + [self.display]):
|
||||
filepath = basepath + filename
|
||||
infile = open(filepath, 'r')
|
||||
outfile.write(infile.read())
|
||||
outfile.write(';\n')
|
||||
infile.close()
|
||||
outfile.close()
|
||||
|
||||
self.display_filename = compiled_filename
|
||||
|
||||
def parse_xml(self):
|
||||
self.generator_xml = self.xml.xpath('//*[@id=$id]//generator',
|
||||
id=self.xml.get('id'))[0]
|
||||
|
||||
self.grader_xml = self.xml.xpath('//*[@id=$id]//grader',
|
||||
id=self.xml.get('id'))[0]
|
||||
|
||||
self.display_xml = self.xml.xpath('//*[@id=$id]//display',
|
||||
id=self.xml.get('id'))[0]
|
||||
|
||||
self.xml.remove(self.generator_xml)
|
||||
self.xml.remove(self.grader_xml)
|
||||
self.xml.remove(self.display_xml)
|
||||
|
||||
self.generator = self.generator_xml.get("src")
|
||||
self.grader = self.grader_xml.get("src")
|
||||
self.display = self.display_xml.get("src")
|
||||
|
||||
if self.generator_xml.get("dependencies"):
|
||||
self.generator_dependencies = self.generator_xml.get("dependencies").split()
|
||||
else:
|
||||
self.generator_dependencies = []
|
||||
|
||||
if self.grader_xml.get("dependencies"):
|
||||
self.grader_dependencies = self.grader_xml.get("dependencies").split()
|
||||
else:
|
||||
self.grader_dependencies = []
|
||||
|
||||
if self.display_xml.get("dependencies"):
|
||||
self.display_dependencies = self.display_xml.get("dependencies").split()
|
||||
else:
|
||||
self.display_dependencies = []
|
||||
|
||||
self.display_class = self.display_xml.get("class")
|
||||
|
||||
def get_node_env(self):
|
||||
|
||||
js_dir = os.path.join(self.system.filestore.root_path, 'js')
|
||||
tmp_env = os.environ.copy()
|
||||
node_path = self.system.node_path + ":" + os.path.normpath(js_dir)
|
||||
tmp_env["NODE_PATH"] = node_path
|
||||
return tmp_env
|
||||
|
||||
|
||||
def generate_problem_state(self):
|
||||
|
||||
generator_file = os.path.dirname(os.path.normpath(__file__)) + '/javascript_problem_generator.js'
|
||||
output = subprocess.check_output(["node",
|
||||
generator_file,
|
||||
self.generator,
|
||||
json.dumps(self.generator_dependencies),
|
||||
json.dumps(str(self.system.seed)),
|
||||
json.dumps(self.params)
|
||||
],
|
||||
env=self.get_node_env()).strip()
|
||||
|
||||
return json.loads(output)
|
||||
|
||||
def extract_params(self):
|
||||
|
||||
params = {}
|
||||
|
||||
for param in self.xml.xpath('//*[@id=$id]//responseparam',
|
||||
id=self.xml.get('id')):
|
||||
|
||||
params[param.get("name")] = json.loads(param.get("value"))
|
||||
|
||||
return params
|
||||
|
||||
def prepare_inputfield(self):
|
||||
|
||||
for inputfield in self.xml.xpath('//*[@id=$id]//javascriptinput',
|
||||
id=self.xml.get('id')):
|
||||
|
||||
escapedict = {'"': '"'}
|
||||
|
||||
encoded_params = json.dumps(self.params)
|
||||
encoded_params = saxutils.escape(encoded_params, escapedict)
|
||||
inputfield.set("params", encoded_params)
|
||||
|
||||
encoded_problem_state = json.dumps(self.problem_state)
|
||||
encoded_problem_state = saxutils.escape(encoded_problem_state,
|
||||
escapedict)
|
||||
inputfield.set("problem_state", encoded_problem_state)
|
||||
|
||||
inputfield.set("display_file", self.display_filename)
|
||||
inputfield.set("display_class", self.display_class)
|
||||
|
||||
def get_score(self, student_answers):
|
||||
json_submission = student_answers[self.answer_id]
|
||||
(all_correct, evaluation, solution) = self.run_grader(json_submission)
|
||||
self.solution = solution
|
||||
correctness = 'correct' if all_correct else 'incorrect'
|
||||
return CorrectMap(self.answer_id, correctness, msg=evaluation)
|
||||
|
||||
def run_grader(self, submission):
|
||||
if submission is None or submission == '':
|
||||
submission = json.dumps(None)
|
||||
|
||||
grader_file = os.path.dirname(os.path.normpath(__file__)) + '/javascript_problem_grader.js'
|
||||
outputs = subprocess.check_output(["node",
|
||||
grader_file,
|
||||
self.grader,
|
||||
json.dumps(self.grader_dependencies),
|
||||
submission,
|
||||
json.dumps(self.problem_state),
|
||||
json.dumps(self.params)
|
||||
],
|
||||
env=self.get_node_env()).split('\n')
|
||||
|
||||
all_correct = json.loads(outputs[0].strip())
|
||||
evaluation = outputs[1].strip()
|
||||
solution = outputs[2].strip()
|
||||
return (all_correct, evaluation, solution)
|
||||
|
||||
def get_answers(self):
|
||||
if self.solution is None:
|
||||
(_, _, self.solution) = self.run_grader(None)
|
||||
|
||||
return {self.answer_id: self.solution}
|
||||
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class ChoiceResponse(LoncapaResponse):
|
||||
'''
|
||||
This Response type is used when the student chooses from a discrete set of
|
||||
This response type is used when the student chooses from a discrete set of
|
||||
choices. Currently, to be marked correct, all "correct" choices must be
|
||||
supplied by the student, and no extraneous choices may be included.
|
||||
|
||||
@@ -313,6 +508,11 @@ class ChoiceResponse(LoncapaResponse):
|
||||
In the above example, radiogroup can be replaced with checkboxgroup to allow
|
||||
the student to select more than one choice.
|
||||
|
||||
TODO: In order for the inputtypes to render properly, this response type
|
||||
must run setup_response prior to the input type rendering. Specifically, the
|
||||
choices must be given names. This behavior seems like a leaky abstraction,
|
||||
and it'd be nice to change this at some point.
|
||||
|
||||
'''
|
||||
|
||||
response_tag = 'choiceresponse'
|
||||
@@ -668,7 +868,10 @@ def sympy_check2():
|
||||
|
||||
# if there is only one box, and it's empty, then don't evaluate
|
||||
if len(idset) == 1 and not submission[0]:
|
||||
return CorrectMap(idset[0], 'incorrect', msg='<font color="red">No answer entered!</font>')
|
||||
# default to no error message on empty answer (to be consistent with other responsetypes)
|
||||
# but allow author to still have the old behavior by setting empty_answer_err attribute
|
||||
msg = '<font color="red">No answer entered!</font>' if self.xml.get('empty_answer_err') else ''
|
||||
return CorrectMap(idset[0], 'incorrect', msg=msg)
|
||||
|
||||
correct = ['unknown'] * len(idset)
|
||||
messages = [''] * len(idset)
|
||||
@@ -824,41 +1027,70 @@ class CodeResponse(LoncapaResponse):
|
||||
self.url = xml.get('url', None) # XML can override external resource (grader/queue) URL
|
||||
self.queue_name = xml.get('queuename', self.system.xqueue['default_queuename'])
|
||||
|
||||
self._parse_externalresponse_xml()
|
||||
# VS[compat]:
|
||||
# Check if XML uses the ExternalResponse format or the generic CodeResponse format
|
||||
codeparam = self.xml.find('codeparam')
|
||||
if codeparam is None:
|
||||
self._parse_externalresponse_xml()
|
||||
else:
|
||||
self._parse_coderesponse_xml(codeparam)
|
||||
|
||||
def _parse_coderesponse_xml(self,codeparam):
|
||||
'''
|
||||
Parse the new CodeResponse XML format. When successful, sets:
|
||||
self.initial_display
|
||||
self.answer (an answer to display to the student in the LMS)
|
||||
self.payload
|
||||
'''
|
||||
# Note that CodeResponse is agnostic to the specific contents of grader_payload
|
||||
grader_payload = codeparam.find('grader_payload')
|
||||
grader_payload = grader_payload.text if grader_payload is not None else ''
|
||||
self.payload = {'grader_payload': grader_payload}
|
||||
|
||||
answer_display = codeparam.find('answer_display')
|
||||
if answer_display is not None:
|
||||
self.answer = answer_display.text
|
||||
else:
|
||||
self.answer = 'No answer provided.'
|
||||
|
||||
initial_display = codeparam.find('initial_display')
|
||||
if initial_display is not None:
|
||||
self.initial_display = initial_display.text
|
||||
else:
|
||||
self.initial_display = ''
|
||||
|
||||
def _parse_externalresponse_xml(self):
|
||||
'''
|
||||
VS[compat]: Suppport for old ExternalResponse XML format. When successful, sets:
|
||||
self.code
|
||||
self.tests
|
||||
self.answer
|
||||
self.initial_display
|
||||
self.answer (an answer to display to the student in the LMS)
|
||||
self.payload
|
||||
'''
|
||||
answer = self.xml.find('answer')
|
||||
|
||||
if answer is not None:
|
||||
answer_src = answer.get('src')
|
||||
if answer_src is not None:
|
||||
self.code = self.system.filesystem.open('src/' + answer_src).read()
|
||||
code = self.system.filesystem.open('src/' + answer_src).read()
|
||||
else:
|
||||
self.code = answer.text
|
||||
code = answer.text
|
||||
else: # no <answer> stanza; get code from <script>
|
||||
self.code = self.context['script_code']
|
||||
if not self.code:
|
||||
code = self.context['script_code']
|
||||
if not code:
|
||||
msg = '%s: Missing answer script code for coderesponse' % unicode(self)
|
||||
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>')
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
self.tests = self.xml.get('tests')
|
||||
tests = self.xml.get('tests')
|
||||
|
||||
# Extract 'answer' and 'initial_display' from XML. Note that the code to be exec'ed here is:
|
||||
# (1) Internal edX code, i.e. NOT student submissions, and
|
||||
# (2) The code should only define the strings 'initial_display', 'answer', 'preamble', 'test_program'
|
||||
# following the 6.01 problem definition convention
|
||||
# following the ExternalResponse XML format
|
||||
penv = {}
|
||||
penv['__builtins__'] = globals()['__builtins__']
|
||||
try:
|
||||
exec(self.code, penv, penv)
|
||||
exec(code, penv, penv)
|
||||
except Exception as err:
|
||||
log.error('Error in CodeResponse %s: Error in problem reference code' % err)
|
||||
raise Exception(err)
|
||||
@@ -869,6 +1101,14 @@ class CodeResponse(LoncapaResponse):
|
||||
log.error("Error in CodeResponse %s: Problem reference code does not define 'answer' and/or 'initial_display' in <answer>...</answer>" % err)
|
||||
raise Exception(err)
|
||||
|
||||
# Finally, make the ExternalResponse input XML format conform to the generic exteral grader interface
|
||||
# The XML tagging of grader_payload is pyxserver-specific
|
||||
grader_payload = '<pyxserver>'
|
||||
grader_payload += '<tests>' + tests + '</tests>\n'
|
||||
grader_payload += '<processor>' + code + '</processor>'
|
||||
grader_payload += '</pyxserver>'
|
||||
self.payload = {'grader_payload': grader_payload}
|
||||
|
||||
def get_score(self, student_answers):
|
||||
try:
|
||||
submission = student_answers[self.answer_id] # Note that submission can be a file
|
||||
@@ -877,7 +1117,10 @@ class CodeResponse(LoncapaResponse):
|
||||
(err, self.answer_id, convert_files_to_filenames(student_answers)))
|
||||
raise Exception(err)
|
||||
|
||||
self.context.update({'submission': unicode(submission)})
|
||||
if is_file(submission):
|
||||
self.context.update({'submission': submission.name})
|
||||
else:
|
||||
self.context.update({'submission': submission})
|
||||
|
||||
# Prepare xqueue request
|
||||
#------------------------------------------------------------
|
||||
@@ -890,22 +1133,16 @@ class CodeResponse(LoncapaResponse):
|
||||
queue_name=self.queue_name)
|
||||
|
||||
# Generate body
|
||||
# NOTE: Currently specialized to 6.00x's pyxserver, which follows the ExternalResponse interface
|
||||
# We should define a common interface for external code graders to CodeResponse
|
||||
contents = {'xml': etree.tostring(self.xml, pretty_print=True),
|
||||
'edX_cmd': 'get_score',
|
||||
'edX_tests': self.tests,
|
||||
'processor': self.code,
|
||||
}
|
||||
|
||||
# Submit request
|
||||
contents = self.payload.copy()
|
||||
|
||||
# Submit request. When successful, 'msg' is the prior length of the queue
|
||||
if is_file(submission):
|
||||
contents.update({'edX_student_response': submission.name})
|
||||
contents.update({'student_response': submission.name})
|
||||
(error, msg) = qinterface.send_to_queue(header=xheader,
|
||||
body=json.dumps(contents),
|
||||
file_to_upload=submission)
|
||||
else:
|
||||
contents.update({'edX_student_response': submission})
|
||||
contents.update({'student_response': submission})
|
||||
(error, msg) = qinterface.send_to_queue(header=xheader,
|
||||
body=json.dumps(contents))
|
||||
|
||||
@@ -914,28 +1151,34 @@ class CodeResponse(LoncapaResponse):
|
||||
cmap.set(self.answer_id, queuekey=None,
|
||||
msg='Unable to deliver your submission to grader. (Reason: %s.) Please try again later.' % msg)
|
||||
else:
|
||||
# Non-null CorrectMap['queuekey'] indicates that the problem has been queued
|
||||
cmap.set(self.answer_id, queuekey=queuekey, msg='Submitted to grader. (Queue length: %s)' % msg)
|
||||
# Queueing mechanism flags:
|
||||
# 1) Backend: Non-null CorrectMap['queuekey'] indicates that the problem has been queued
|
||||
# 2) Frontend: correctness='incomplete' eventually trickles down through inputtypes.textbox
|
||||
# and .filesubmission to inform the browser to poll the LMS
|
||||
cmap.set(self.answer_id, queuekey=queuekey, correctness='incomplete', msg=msg)
|
||||
|
||||
return cmap
|
||||
|
||||
def update_score(self, score_msg, oldcmap, queuekey):
|
||||
|
||||
(valid_score_msg, correct, score, msg) = self._parse_score_msg(score_msg)
|
||||
(valid_score_msg, correct, points, msg) = self._parse_score_msg(score_msg)
|
||||
if not valid_score_msg:
|
||||
oldcmap.set(self.answer_id, msg='Error: Invalid grader reply.')
|
||||
return oldcmap
|
||||
|
||||
correctness = 'incorrect'
|
||||
if correct:
|
||||
correctness = 'correct'
|
||||
correctness = 'correct' if correct else 'incorrect'
|
||||
|
||||
self.context['correct'] = correctness # TODO: Find out how this is used elsewhere, if any
|
||||
|
||||
# Replace 'oldcmap' with new grading results if queuekey matches.
|
||||
# If queuekey does not match, we keep waiting for the score_msg whose key actually matches
|
||||
if oldcmap.is_right_queuekey(self.answer_id, queuekey):
|
||||
oldcmap.set(self.answer_id, correctness=correctness, msg=msg.replace(' ', ' '), queuekey=None) # Queuekey is consumed
|
||||
# Sanity check on returned points
|
||||
if points < 0:
|
||||
points = 0
|
||||
elif points > self.maxpoints[self.answer_id]:
|
||||
points = self.maxpoints[self.answer_id]
|
||||
oldcmap.set(self.answer_id, npoints=points, correctness=correctness, msg=msg.replace(' ', ' '), queuekey=None) # Queuekey is consumed
|
||||
else:
|
||||
log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' % (queuekey, self.answer_id))
|
||||
|
||||
@@ -1358,4 +1601,4 @@ class ImageResponse(LoncapaResponse):
|
||||
# TEMPORARY: List of all response subclasses
|
||||
# FIXME: To be replaced by auto-registration
|
||||
|
||||
__all__ = [CodeResponse, NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse, StringResponse, ChoiceResponse, MultipleChoiceResponse, TrueFalseResponse]
|
||||
__all__ = [CodeResponse, NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse, StringResponse, ChoiceResponse, MultipleChoiceResponse, TrueFalseResponse, JavascriptResponse]
|
||||
|
||||
@@ -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/>
|
||||
|
||||
10
common/lib/capa/capa/templates/javascriptinput.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<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>
|
||||
|
||||
@@ -7,13 +7,10 @@ import logging
|
||||
import requests
|
||||
import time
|
||||
|
||||
# TODO: Collection of parameters to be hooked into rest of edX system
|
||||
XQUEUE_LMS_AUTH = { 'username': 'LMS',
|
||||
'password': 'PaloAltoCA' }
|
||||
XQUEUE_URL = 'http://xqueue.edx.org'
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
|
||||
def make_hashkey(seed=None):
|
||||
'''
|
||||
Generate a string key by hashing
|
||||
@@ -58,15 +55,15 @@ def parse_xreply(xreply):
|
||||
return (return_code, content)
|
||||
|
||||
|
||||
class XqueueInterface:
|
||||
class XQueueInterface(object):
|
||||
'''
|
||||
Interface to the external grading system
|
||||
'''
|
||||
|
||||
def __init__(self, url=XQUEUE_URL, auth=XQUEUE_LMS_AUTH):
|
||||
def __init__(self, url, django_auth, requests_auth=None):
|
||||
self.url = url
|
||||
self.auth = auth
|
||||
self.session = requests.session()
|
||||
self.auth = django_auth
|
||||
self.session = requests.session(auth=requests_auth)
|
||||
|
||||
def send_to_queue(self, header, body, file_to_upload=None):
|
||||
'''
|
||||
@@ -117,5 +114,3 @@ class XqueueInterface:
|
||||
return (1, 'unexpected HTTP status code [%d]' % r.status_code)
|
||||
|
||||
return parse_xreply(r.text)
|
||||
|
||||
qinterface = XqueueInterface()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -150,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
|
||||
@@ -464,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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -100,7 +117,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
for s in c.get_children():
|
||||
if s.metadata.get('graded', False):
|
||||
xmoduledescriptors = list(yield_descriptor_descendents(s))
|
||||
|
||||
|
||||
# The xmoduledescriptors included here are only the ones that have scores.
|
||||
section_description = { 'section_descriptor' : s, 'xmoduledescriptors' : filter(lambda child: child.has_score, xmoduledescriptors) }
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -14,7 +14,7 @@ div.video {
|
||||
|
||||
section.video-player {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
// overflow: hidden;
|
||||
padding-bottom: 56.25%;
|
||||
position: relative;
|
||||
|
||||
@@ -45,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 {
|
||||
@@ -58,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;
|
||||
@@ -103,7 +73,7 @@ div.video {
|
||||
width: 15px;
|
||||
|
||||
&:focus, &:hover {
|
||||
background-color: lighten($mit-red, 10%);
|
||||
background-color: lighten($pink, 10%);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,9 @@ class DiscussionModule(XModule):
|
||||
}
|
||||
return self.system.render_template('discussion/_discussion_module.html', context)
|
||||
|
||||
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)
|
||||
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 isinstance(instance_state, str):
|
||||
instance_state = json.loads(instance_state)
|
||||
xml_data = etree.fromstring(definition['data'])
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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,109 @@ class @Problem
|
||||
@el.attr progress: response.progress_status
|
||||
@el.trigger('progressChanged')
|
||||
|
||||
queueing: =>
|
||||
@queued_items = @$(".xqueue")
|
||||
@num_queued_items = @queued_items.length
|
||||
if @num_queued_items > 0
|
||||
if window.queuePollerID # Only one poller 'thread' per Problem
|
||||
window.clearTimeout(window.queuePollerID)
|
||||
queuelen = @get_queuelen()
|
||||
window.queuePollerID = window.setTimeout(@poll, queuelen*10)
|
||||
|
||||
# Retrieves the minimum queue length of all queued items
|
||||
get_queuelen: =>
|
||||
minlen = Infinity
|
||||
@queued_items.each (index, qitem) ->
|
||||
len = parseInt($.text(qitem))
|
||||
if len < minlen
|
||||
minlen = len
|
||||
return minlen
|
||||
|
||||
poll: =>
|
||||
$.postWithPrefix "#{@url}/problem_get", (response) =>
|
||||
# If queueing status changed, then render
|
||||
@new_queued_items = $(response.html).find(".xqueue")
|
||||
if @new_queued_items.length isnt @num_queued_items
|
||||
@el.html(response.html)
|
||||
@executeProblemScripts () =>
|
||||
@setupInputTypes()
|
||||
@bind()
|
||||
|
||||
@num_queued_items = @new_queued_items.length
|
||||
if @num_queued_items == 0
|
||||
delete window.queuePollerID
|
||||
else
|
||||
# TODO: Some logic to dynamically adjust polling rate based on queuelen
|
||||
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()
|
||||
|
||||
executeProblemScripts: ->
|
||||
@el.find(".script_placeholder").each (index, placeholder) ->
|
||||
s = $("<script>")
|
||||
s.attr("type", "text/javascript")
|
||||
s.attr("src", $(placeholder).attr("data-src"))
|
||||
# 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: (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()
|
||||
|
||||
###
|
||||
@@ -68,9 +156,16 @@ class @Problem
|
||||
|
||||
fd = new FormData()
|
||||
|
||||
# Sanity check of file size
|
||||
file_too_large = false
|
||||
max_filesize = 4*1000*1000 # 4 MB
|
||||
|
||||
@inputs.each (index, element) ->
|
||||
if element.type is 'file'
|
||||
if element.files[0] instanceof File
|
||||
if element.files[0].size > max_filesize
|
||||
file_too_large = true
|
||||
alert 'Submission aborted! Your file "' + element.files[0].name + '" is too large (max size: ' + max_filesize/(1000*1000) + ' MB)'
|
||||
fd.append(element.id, element.files[0])
|
||||
else
|
||||
fd.append(element.id, '')
|
||||
@@ -90,7 +185,8 @@ class @Problem
|
||||
else
|
||||
alert(response.success)
|
||||
|
||||
$.ajaxWithPrefix("#{@url}/problem_check", settings)
|
||||
if not file_too_large
|
||||
$.ajaxWithPrefix("#{@url}/problem_check", settings)
|
||||
|
||||
check: =>
|
||||
Logger.log 'problem_check', @answers
|
||||
@@ -108,6 +204,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 +256,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
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import unittest
|
||||
import os
|
||||
import fs
|
||||
import json
|
||||
|
||||
import json
|
||||
import numpy
|
||||
@@ -31,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")
|
||||
)
|
||||
|
||||
|
||||
@@ -323,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())
|
||||
@@ -373,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
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<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>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// 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);
|
||||
;
|
||||
202
common/lib/xmodule/xmodule/tests/test_files/js/mersenne-twister-min.js
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
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;
|
||||
@@ -0,0 +1,21 @@
|
||||
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
|
||||
@@ -0,0 +1,49 @@
|
||||
// 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);
|
||||
@@ -0,0 +1,14 @@
|
||||
class TestProblemGenerator extends XProblemGenerator
|
||||
|
||||
constructor: (seed, @parameters = {}) ->
|
||||
|
||||
super(seed, @parameters)
|
||||
|
||||
generate: () ->
|
||||
|
||||
@problemState.value = @parameters.value
|
||||
|
||||
return @problemState
|
||||
|
||||
root = exports ? this
|
||||
root.generatorClass = TestProblemGenerator
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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);
|
||||
@@ -0,0 +1,27 @@
|
||||
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
|
||||
@@ -0,0 +1,50 @@
|
||||
// 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);
|
||||
@@ -0,0 +1,47 @@
|
||||
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
|
||||
78
common/lib/xmodule/xmodule/tests/test_files/js/xproblem.js
Normal file
@@ -0,0 +1,78 @@
|
||||
// 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)
|
||||
|
||||
@@ -227,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():
|
||||
@@ -238,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]
|
||||
|
||||
@@ -319,7 +319,8 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
# 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'
|
||||
@@ -403,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.
|
||||
@@ -423,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)
|
||||
|
||||
@@ -507,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,
|
||||
@@ -600,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'
|
||||
@@ -658,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.
|
||||
|
||||
@@ -701,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).'''
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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,
|
||||
|
||||
47
common/static/coffee/src/xproblem.coffee
Normal file
@@ -0,0 +1,47 @@
|
||||
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
|
||||
BIN
common/static/images/spinner.gif
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
202
common/static/js/vendor/mersenne-twister-min.js
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
121
doc/discussion.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Running the discussion service
|
||||
|
||||
## Instruction for Mac
|
||||
|
||||
## Installing Mongodb
|
||||
|
||||
If you haven't done so already:
|
||||
|
||||
brew install mongodb
|
||||
|
||||
Make sure that you have mongodb running. You can simply open a new terminal tab and type:
|
||||
|
||||
mongod
|
||||
|
||||
## Installing elasticsearch
|
||||
|
||||
brew install elasticsearch
|
||||
|
||||
For debugging, it's often more convenient to have elasticsearch running in a terminal tab instead of in background. To do so, simply open a new terminal tab and then type:
|
||||
|
||||
elasticsearch -f
|
||||
|
||||
## Setting up the discussion service
|
||||
|
||||
First, make sure that you have access to the [github repository](https://github.com/rll/cs_comments_service). If this were not the case, send an email to dementrock@gmail.com.
|
||||
|
||||
First go into the mitx_all directory. Then type
|
||||
|
||||
git clone git@github.com:rll/cs_comments_service.git
|
||||
cd cs_comments_service/
|
||||
|
||||
If you see a prompt asking "Do you wish to trust this .rvmrc file?", type "y"
|
||||
|
||||
Now if you see this error "Gemset 'cs_comments_service' does not exist," run the following command to create the gemset and then use the rvm environment manually:
|
||||
|
||||
rvm gemset create 'cs_comments_service'
|
||||
rvm use 1.9.3@cs_comments_service
|
||||
|
||||
Now use the following command to install required packages:
|
||||
|
||||
bundle install
|
||||
|
||||
The following command creates database indexes:
|
||||
|
||||
bundle exec rake db:init
|
||||
|
||||
Now use the following command to generate seeds (basically some random comments in Latin):
|
||||
|
||||
bundle exec rake db:seed
|
||||
|
||||
It's done! Launch the app now:
|
||||
|
||||
ruby app.rb
|
||||
|
||||
## Running the delayed job worker
|
||||
|
||||
In the discussion service, notifications are handled asynchronously using a third party gem called delayed_job. If you want to test this functionality, run the following command in a separate tab:
|
||||
|
||||
bundle exec rake jobs:work
|
||||
|
||||
## Some other useful commands
|
||||
|
||||
### generate seeds for a specific forum
|
||||
The seed generating command above assumes that you have the following discussion tags somewhere in the course data:
|
||||
|
||||
<discussion for="Welcome Video" id="video_1" discussion_category="Video"/>
|
||||
<discussion for="Lab 0: Using the Tools" id="lab_1" discussion_category="Lab"/>
|
||||
<discussion for="Lab Circuit Sandbox" id="lab_2" discussion_category="Lab"/>
|
||||
|
||||
For example, you can insert them into overview section as following:
|
||||
|
||||
<chapter name="Overview">
|
||||
<section format="Video" name="Welcome">
|
||||
<vertical>
|
||||
<video youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8"/>
|
||||
<discussion for="Welcome Video" id="video_1" discussion_category="Video"/>
|
||||
</vertical>
|
||||
</section>
|
||||
<section format="Lecture Sequence" name="System Usage Sequence">
|
||||
<%include file="sections/introseq.xml"/>
|
||||
</section>
|
||||
<section format="Lab" name="Lab0: Using the tools">
|
||||
<vertical>
|
||||
<html> See the <a href="/section/labintro"> Lab Introduction </a> or <a href="/static/handouts/schematic_tutorial.pdf">Interactive Lab Usage Handout </a> for information on how to do the lab </html>
|
||||
<problem name="Lab 0: Using the Tools" filename="Lab0" rerandomize="false"/>
|
||||
<discussion for="Lab 0: Using the Tools" id="lab_1" discussion_category="Lab"/>
|
||||
</vertical>
|
||||
</section>
|
||||
<section format="Lab" name="Circuit Sandbox">
|
||||
<vertical>
|
||||
<problem name="Circuit Sandbox" filename="Lab_sandbox" rerandomize="false"/>
|
||||
<discussion for="Lab Circuit Sandbox" id="lab_2" discussion_category="Lab"/>
|
||||
</vertical>
|
||||
</section>
|
||||
</chapter>
|
||||
|
||||
Currently, only the attribute "id" is actually used, which identifies discussion forum. In the code for the data generator, the corresponding lines are:
|
||||
|
||||
generate_comments_for("video_1")
|
||||
generate_comments_for("lab_1")
|
||||
generate_comments_for("lab_2")
|
||||
|
||||
We also have a command for generating comments within a forum with the specified id:
|
||||
|
||||
bundle exec rake db:generate_comments[type_the_discussion_id_here]
|
||||
|
||||
For instance, if you want to generate comments for the general discussion, for which the discussion id is the course id with slashes and dots replaced by underscores (you **should** do this before testing forum view) and you are in 6.002x, use the following command
|
||||
|
||||
bundle exec rake db:generate_comments[MITx_6_002x_2012_Fall]
|
||||
|
||||
### Running tests for the service
|
||||
|
||||
bundle exec rspec
|
||||
|
||||
Warning: due to an unresolved bug in the test code, testing the service will "flush" the development database. So you need to generate seed again after testing.
|
||||
|
||||
### debugging the service
|
||||
|
||||
You can use the following command to launch a console within the service environment:
|
||||
|
||||
bundle exec rake console
|
||||
BIN
lms/askbot/skins/mitx/media/images/email-sharing.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
lms/askbot/skins/mitx/media/images/facebook-sharing.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
lms/askbot/skins/mitx/media/images/google-plus-sharing.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
lms/askbot/skins/mitx/media/images/lrg/email-sharing.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
lms/askbot/skins/mitx/media/images/lrg/facebook-sharing.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
lms/askbot/skins/mitx/media/images/lrg/facebook.png
Normal file
|
After Width: | Height: | Size: 205 B |
BIN
lms/askbot/skins/mitx/media/images/lrg/google-plus-sharing.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
lms/askbot/skins/mitx/media/images/lrg/linkedin.png
Normal file
|
After Width: | Height: | Size: 229 B |
BIN
lms/askbot/skins/mitx/media/images/lrg/twitter-sharing.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
lms/askbot/skins/mitx/media/images/lrg/twitter.png
Normal file
|
After Width: | Height: | Size: 235 B |
BIN
lms/askbot/skins/mitx/media/images/lrg/youtube-sharing.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
lms/askbot/skins/mitx/media/images/social/email-sharing.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
lms/askbot/skins/mitx/media/images/social/facebook-sharing.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
BIN
lms/askbot/skins/mitx/media/images/social/twitter-sharing.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
lms/askbot/skins/mitx/media/images/social/youtube-sharing.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
lms/askbot/skins/mitx/media/images/twitter-sharing.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
lms/askbot/skins/mitx/media/images/youtube-sharing.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
@@ -3,7 +3,7 @@
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
{% spaceless %}
|
||||
<head>
|
||||
<title>{% block title %}{% endblock %} - MITX 6.002</title>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
{% include "meta/html_head_meta.html" %}
|
||||
<link rel="shortcut icon" href="{{ settings.SITE_FAVICON|media }}" />
|
||||
{% include "meta/html_head_stylesheets.html" %}
|
||||
|
||||
@@ -33,8 +33,14 @@
|
||||
<!-- Quick fix -- we should reference askbot jquery properly -->
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="{{ settings.STATIC_URL }}/js/askbot_jquery.min.js"
|
||||
src="{{'/js/jquery-1.4.3.js'|media}}"
|
||||
></script>
|
||||
{#
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="{{ settings.STATIC_ROOT }}/js/askbot_jquery.min.js"
|
||||
></script>
|
||||
#}
|
||||
<!-- History.js -->
|
||||
<script type='text/javascript' src="{{"/js/jquery.history.js"|media }}"></script>
|
||||
<script type='text/javascript' src="{{"/js/utils.js"|media }}"></script>
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
<header class="global" aria-label="Global Navigation">
|
||||
<nav>
|
||||
<h1 class="logo"><a href="${reverse('root')}"></a></h1>
|
||||
<h1 class="logo"><a href="{% url root %}"></a></h1>
|
||||
<ol class="left">
|
||||
<li class="primary">
|
||||
<a href="${reverse('courses')}">Find Courses</a>
|
||||
<a href="{% url courses %}">Find Courses</a>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<ol class="user">
|
||||
<li class="primary">
|
||||
<a href="${reverse('dashboard')}" class="user-link">
|
||||
<a href="{% url dashboard %}" class="user-link">
|
||||
<span class="avatar"></span>
|
||||
${user.username}
|
||||
{{user.username}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="primary">
|
||||
<a href="#" class="dropdown">▾</a>
|
||||
<ul class="dropdown-menu">
|
||||
## <li><a href="#">Account Settings</a></li>
|
||||
<li><a href="${reverse('help_edx')}">Help</a></li>
|
||||
<li><a href="${reverse('logout')}">Log Out</a></li>
|
||||
{# <li><a href="#">Account Settings</a></li> #}
|
||||
<li><a href="{% url help_edx %}">Help</a></li>
|
||||
<li><a href="{% url logout %}">Log Out</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
<nav>
|
||||
<section class="top">
|
||||
<section class="primary">
|
||||
<a href="${reverse('root')}" class="logo"></a>
|
||||
<a href="${reverse('courses')}">Find Courses</a>
|
||||
<a href="${reverse('about_edx')}">About</a>
|
||||
<a href="{% url root %}" class="logo"></a>
|
||||
<a href="{% url courses %}">Find Courses</a>
|
||||
<a href="{% url about_edx %}">About</a>
|
||||
<a href="http://edxonline.tumblr.com/">Blog</a>
|
||||
<a href="${reverse('jobs')}">Jobs</a>
|
||||
<a href="${reverse('contact')}">Contact</a>
|
||||
<a href="{% url jobs %}">Jobs</a>
|
||||
<a href="{% url contact %}">Contact</a>
|
||||
</section>
|
||||
|
||||
<section class="social">
|
||||
<a href="http://youtube.com/user/edxonline"><img src="${static.url('images/social/youtube-sharing.png')}" /></a>
|
||||
<a href="https://plus.google.com/108235383044095082735"><img src="${static.url('images/social/google-plus-sharing.png')}" /></a>
|
||||
<a href="http://www.facebook.com/EdxOnline"><img src="${static.url('images/social/facebook-sharing.png')}" /></a>
|
||||
<a href="https://twitter.com/edXOnline"><img src="${static.url('images/social/twitter-sharing.png')}" /></a>
|
||||
<a href="http://youtube.com/user/edxonline"><img src='{{"images/social/youtube-sharing.png"|media}}' /></a>
|
||||
<a href="https://plus.google.com/108235383044095082735"><img src="{{('images/social/google-plus-sharing.png'|media)}}" /></a>
|
||||
<a href="http://www.facebook.com/EdxOnline"><img src="{{'images/social/facebook-sharing.png'|media}}" /></a>
|
||||
<a href="https://twitter.com/edXOnline"><img src="{{'images/social/twitter-sharing.png'|media}}" /></a>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
</section>
|
||||
|
||||
<section class="secondary">
|
||||
<a href="${reverse('tos')}">Terms of Service</a>
|
||||
<a href="${reverse('privacy_edx')}">Privacy Policy</a>
|
||||
<a href="${reverse('honor')}">Honor Code</a>
|
||||
<a href="${reverse('help_edx')}">Help</a>
|
||||
<a href="{% url tos %}">Terms of Service</a>
|
||||
<a href="{% url privacy_edx %}">Privacy Policy</a>
|
||||
<a href="{% url honor %}">Honor Code</a>
|
||||
<a href="{% url help_edx %}">Help</a>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# Compute grades using real division, with no integer truncation
|
||||
from __future__ import division
|
||||
|
||||
import random
|
||||
import logging
|
||||
|
||||
@@ -13,33 +16,33 @@ log = logging.getLogger("mitx.courseware")
|
||||
|
||||
def yield_module_descendents(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
|
||||
@@ -48,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.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:
|
||||
@@ -142,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):
|
||||
@@ -152,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(
|
||||
@@ -179,7 +184,7 @@ 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
|
||||
@@ -188,7 +193,7 @@ def get_score(user, problem, student_module_cache):
|
||||
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
|
||||
|
||||
@@ -2,13 +2,14 @@ import json
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import Http404
|
||||
from django.http import HttpResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from capa.xqueue_interface import XQueueInterface
|
||||
from django.contrib.auth.models import User
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from capa.xqueue_interface import qinterface
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
from models import StudentModule, StudentModuleCache
|
||||
from static_replace import replace_urls
|
||||
@@ -16,11 +17,25 @@ from xmodule.exceptions import NotFoundError
|
||||
from xmodule.x_module import ModuleSystem
|
||||
from xmodule_modifiers import replace_static_urls, add_histogram, wrap_xmodule
|
||||
|
||||
from courseware.courses import has_staff_access_to_course
|
||||
from courseware.courses import (has_staff_access_to_course,
|
||||
has_staff_access_to_location)
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
|
||||
if settings.XQUEUE_INTERFACE['basic_auth'] is not None:
|
||||
requests_auth = HTTPBasicAuth(*settings.XQUEUE_INTERFACE['basic_auth'])
|
||||
else:
|
||||
requests_auth = None
|
||||
|
||||
xqueue_interface = XQueueInterface(
|
||||
settings.XQUEUE_INTERFACE['url'],
|
||||
settings.XQUEUE_INTERFACE['django_auth'],
|
||||
requests_auth,
|
||||
)
|
||||
|
||||
|
||||
def make_track_function(request):
|
||||
'''
|
||||
Make a tracking function that logs what happened.
|
||||
@@ -46,11 +61,12 @@ def toc_for_course(user, request, course, active_chapter, active_section):
|
||||
'format': format, 'due': due, 'active' : bool}, ...]
|
||||
|
||||
active is set for the section and chapter corresponding to the passed
|
||||
parameters. Everything else comes from the xml, or defaults to "".
|
||||
parameters, which are expected to be url_names of the chapter+section.
|
||||
Everything else comes from the xml, or defaults to "".
|
||||
|
||||
chapters with name 'hidden' are skipped.
|
||||
'''
|
||||
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, course, depth=2)
|
||||
course = get_module(user, request, course.location, student_module_cache)
|
||||
|
||||
@@ -59,8 +75,8 @@ def toc_for_course(user, request, course, active_chapter, active_section):
|
||||
sections = list()
|
||||
for section in chapter.get_display_items():
|
||||
|
||||
active = (chapter.display_name == active_chapter and
|
||||
section.display_name == active_section)
|
||||
active = (chapter.url_name == active_chapter and
|
||||
section.url_name == active_section)
|
||||
hide_from_toc = section.metadata.get('hide_from_toc', 'false').lower() == 'true'
|
||||
|
||||
if not hide_from_toc:
|
||||
@@ -73,7 +89,7 @@ def toc_for_course(user, request, course, active_chapter, active_section):
|
||||
chapters.append({'display_name': chapter.display_name,
|
||||
'url_name': chapter.url_name,
|
||||
'sections': sections,
|
||||
'active': chapter.display_name == active_chapter})
|
||||
'active': chapter.url_name == active_chapter})
|
||||
return chapters
|
||||
|
||||
|
||||
@@ -122,10 +138,10 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
position within module
|
||||
|
||||
Returns: xmodule instance
|
||||
|
||||
|
||||
'''
|
||||
descriptor = modulestore().get_item(location)
|
||||
|
||||
|
||||
#TODO Only check the cache if this module can possibly have state
|
||||
instance_module = None
|
||||
shared_module = None
|
||||
@@ -133,34 +149,45 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
if descriptor.stores_state:
|
||||
instance_module = student_module_cache.lookup(descriptor.category,
|
||||
descriptor.location.url())
|
||||
|
||||
|
||||
shared_state_key = getattr(descriptor, 'shared_state_key', None)
|
||||
if shared_state_key is not None:
|
||||
shared_module = student_module_cache.lookup(descriptor.category,
|
||||
shared_state_key)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
instance_state = instance_module.state if instance_module is not None else '{}'
|
||||
shared_state = shared_module.state if shared_module is not None else None
|
||||
|
||||
# TODO (vshnayder): fix hardcoded urls (use reverse)
|
||||
# Setup system context for module instance
|
||||
ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.location.url() + '/'
|
||||
|
||||
ajax_url = reverse('modx_dispatch',
|
||||
kwargs=dict(course_id=descriptor.location.course_id,
|
||||
id=descriptor.location.url(),
|
||||
dispatch=''),
|
||||
)
|
||||
|
||||
# ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.location.url() + '/'
|
||||
|
||||
# Fully qualified callback URL for external queueing system
|
||||
xqueue_callback_url = (request.build_absolute_uri('/') + settings.MITX_ROOT_URL +
|
||||
'xqueue/' + str(user.id) + '/' + descriptor.location.url() + '/' +
|
||||
'score_update')
|
||||
xqueue_callback_url = request.build_absolute_uri('/')[:-1] # Trailing slash provided by reverse
|
||||
xqueue_callback_url += reverse('xqueue_callback',
|
||||
kwargs=dict(course_id=descriptor.location.course_id,
|
||||
userid=str(user.id),
|
||||
id=descriptor.location.url(),
|
||||
dispatch='score_update'),
|
||||
)
|
||||
|
||||
# Default queuename is course-specific and is derived from the course that
|
||||
# contains the current module.
|
||||
# TODO: Queuename should be derived from 'course_settings.json' of each course
|
||||
xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course
|
||||
|
||||
xqueue = { 'interface': qinterface,
|
||||
'callback_url': xqueue_callback_url,
|
||||
'default_queuename': xqueue_default_queuename.replace(' ','_') }
|
||||
xqueue = {'interface': xqueue_interface,
|
||||
'callback_url': xqueue_callback_url,
|
||||
'default_queuename': xqueue_default_queuename.replace(' ', '_')}
|
||||
|
||||
def _get_module(location):
|
||||
return get_module(user, request, location,
|
||||
@@ -181,21 +208,23 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
# a module is coming through get_html and is therefore covered
|
||||
# by the replace_static_urls code below
|
||||
replace_urls=replace_urls,
|
||||
is_staff=user.is_staff,
|
||||
is_staff=has_staff_access_to_location(user, location),
|
||||
node_path=settings.NODE_PATH
|
||||
)
|
||||
# pass position specified in URL to module through ModuleSystem
|
||||
system.set('position', position)
|
||||
system.set('DEBUG',settings.DEBUG)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'):
|
||||
if has_staff_access_to_course(user, module.location.course):
|
||||
module.get_html = add_histogram(module.get_html, module)
|
||||
module.get_html = add_histogram(module.get_html, module, user)
|
||||
|
||||
return module
|
||||
|
||||
@@ -206,13 +235,13 @@ def get_instance_module(user, module, student_module_cache):
|
||||
"""
|
||||
if user.is_authenticated():
|
||||
if not module.descriptor.stores_state:
|
||||
log.exception("Attempted to get the instance_module for a module "
|
||||
log.exception("Attempted to get the instance_module for a module "
|
||||
+ str(module.id) + " which does not store state.")
|
||||
return None
|
||||
|
||||
|
||||
instance_module = student_module_cache.lookup(module.category,
|
||||
module.location.url())
|
||||
|
||||
|
||||
if not instance_module:
|
||||
instance_module = StudentModule(
|
||||
student=user,
|
||||
@@ -222,11 +251,11 @@ def get_instance_module(user, module, student_module_cache):
|
||||
max_grade=module.max_score())
|
||||
instance_module.save()
|
||||
student_module_cache.append(instance_module)
|
||||
|
||||
|
||||
return instance_module
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_shared_instance_module(user, module, student_module_cache):
|
||||
"""
|
||||
Return shared_module is a StudentModule specific to all modules with the same
|
||||
@@ -236,7 +265,7 @@ def get_shared_instance_module(user, module, student_module_cache):
|
||||
if user.is_authenticated():
|
||||
# To get the shared_state_key, we need to descriptor
|
||||
descriptor = modulestore().get_item(module.location)
|
||||
|
||||
|
||||
shared_state_key = getattr(module, 'shared_state_key', None)
|
||||
if shared_state_key is not None:
|
||||
shared_module = student_module_cache.lookup(module.category,
|
||||
@@ -251,15 +280,15 @@ def get_shared_instance_module(user, module, student_module_cache):
|
||||
student_module_cache.append(shared_module)
|
||||
else:
|
||||
shared_module = None
|
||||
|
||||
|
||||
return shared_module
|
||||
else:
|
||||
return None
|
||||
|
||||
@csrf_exempt
|
||||
def xqueue_callback(request, userid, id, dispatch):
|
||||
def xqueue_callback(request, course_id, userid, id, dispatch):
|
||||
'''
|
||||
Entry point for graded results from the queueing system.
|
||||
Entry point for graded results from the queueing system.
|
||||
'''
|
||||
# Test xqueue package, which we expect to be:
|
||||
# xpackage = {'xqueue_header': json.dumps({'lms_key':'secretkey',...}),
|
||||
@@ -308,7 +337,7 @@ def xqueue_callback(request, userid, id, dispatch):
|
||||
return HttpResponse("")
|
||||
|
||||
|
||||
def modx_dispatch(request, dispatch=None, id=None):
|
||||
def modx_dispatch(request, dispatch=None, id=None, course_id=None):
|
||||
''' Generic view for extensions. This is where AJAX calls go.
|
||||
|
||||
Arguments:
|
||||
@@ -320,18 +349,24 @@ def modx_dispatch(request, dispatch=None, id=None):
|
||||
- id -- the module id. Used to look up the XModule instance
|
||||
'''
|
||||
# ''' (fix emacs broken parsing)
|
||||
# Check for submitted files
|
||||
|
||||
# Check for submitted files and basic file size checks
|
||||
p = request.POST.copy()
|
||||
if request.FILES:
|
||||
for inputfile_id in request.FILES.keys():
|
||||
p[inputfile_id] = request.FILES[inputfile_id]
|
||||
inputfile = request.FILES[inputfile_id]
|
||||
if inputfile.size > settings.STUDENT_FILEUPLOAD_MAX_SIZE: # Bytes
|
||||
file_too_big_msg = 'Submission aborted! Your file "%s" is too large (max size: %d MB)' %\
|
||||
(inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE/(1000**2))
|
||||
return HttpResponse(json.dumps({'success': file_too_big_msg}))
|
||||
p[inputfile_id] = inputfile
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, modulestore().get_item(id))
|
||||
instance = get_module(request.user, request, id, student_module_cache)
|
||||
|
||||
instance_module = get_instance_module(request.user, instance, student_module_cache)
|
||||
shared_module = get_shared_instance_module(request.user, instance, student_module_cache)
|
||||
|
||||
|
||||
# Don't track state for anonymous users (who don't have student modules)
|
||||
if instance_module is not None:
|
||||
oldgrade = instance_module.grade
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import copy
|
||||
import json
|
||||
from path import path
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from pprint import pprint
|
||||
from nose import SkipTest
|
||||
from path import path
|
||||
from pprint import pprint
|
||||
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.conf import settings
|
||||
@@ -13,11 +16,11 @@ from django.core.urlresolvers import reverse
|
||||
from mock import patch, Mock
|
||||
from override_settings import override_settings
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from student.models import Registration
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
import xmodule.modulestore.django
|
||||
|
||||
from student.models import Registration
|
||||
from courseware.courses import course_staff_group_name
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
|
||||
@@ -55,8 +58,22 @@ def mongo_store_config(data_dir):
|
||||
}
|
||||
}
|
||||
|
||||
def xml_store_config(data_dir):
|
||||
return {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
|
||||
'OPTIONS': {
|
||||
'data_dir': data_dir,
|
||||
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
|
||||
'eager': True,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
|
||||
TEST_DATA_MODULESTORE = mongo_store_config(TEST_DATA_DIR)
|
||||
TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR)
|
||||
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
|
||||
|
||||
REAL_DATA_DIR = settings.GITHUB_REPO_ROOT
|
||||
REAL_DATA_MODULESTORE = mongo_store_config(REAL_DATA_DIR)
|
||||
@@ -88,6 +105,13 @@ class ActivateLoginTestCase(TestCase):
|
||||
self.assertTrue(data['success'])
|
||||
return resp
|
||||
|
||||
def logout(self):
|
||||
'''Logout, check that it worked.'''
|
||||
resp = self.client.get(reverse('logout'), {})
|
||||
# should redirect
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
return resp
|
||||
|
||||
def _create_account(self, username, email, pw):
|
||||
'''Try to create an account. No error checking'''
|
||||
resp = self.client.post('/create_account', {
|
||||
@@ -131,12 +155,35 @@ class ActivateLoginTestCase(TestCase):
|
||||
'''The setup function does all the work'''
|
||||
pass
|
||||
|
||||
def test_logout(self):
|
||||
'''Setup function does login'''
|
||||
self.logout()
|
||||
|
||||
|
||||
class PageLoader(ActivateLoginTestCase):
|
||||
''' Base class that adds a function to load all pages in a modulestore '''
|
||||
|
||||
def _enroll(self, course):
|
||||
"""Post to the enrollment view, and return the parsed json response"""
|
||||
resp = self.client.post('/change_enrollment', {
|
||||
'enrollment_action': 'enroll',
|
||||
'course_id': course.id,
|
||||
})
|
||||
return parse_json(resp)
|
||||
|
||||
def try_enroll(self, course):
|
||||
"""Try to enroll. Return bool success instead of asserting it."""
|
||||
data = self._enroll(course)
|
||||
print 'Enrollment in {} result: {}'.format(course.location.url(), data)
|
||||
return data['success']
|
||||
|
||||
def enroll(self, course):
|
||||
"""Enroll the currently logged-in user, and check that it worked."""
|
||||
data = self._enroll(course)
|
||||
self.assertTrue(data['success'])
|
||||
|
||||
def unenroll(self, course):
|
||||
"""Unenroll the currently logged-in user, and check that it worked."""
|
||||
resp = self.client.post('/change_enrollment', {
|
||||
'enrollment_action': 'enroll',
|
||||
'course_id': course.id,
|
||||
@@ -145,6 +192,7 @@ class PageLoader(ActivateLoginTestCase):
|
||||
self.assertTrue(data['success'])
|
||||
|
||||
def check_pages_load(self, course_name, data_dir, modstore):
|
||||
"""Make all locations in course load"""
|
||||
print "Checking course {0} in {1}".format(course_name, data_dir)
|
||||
import_from_xml(modstore, data_dir, [course_name])
|
||||
|
||||
@@ -177,7 +225,7 @@ class PageLoader(ActivateLoginTestCase):
|
||||
self.assertTrue(all_ok)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
class TestCoursesLoadTestCase(PageLoader):
|
||||
'''Check that all pages in test courses load properly'''
|
||||
|
||||
@@ -193,7 +241,279 @@ class TestCoursesLoadTestCase(PageLoader):
|
||||
self.check_pages_load('full', TEST_DATA_DIR, modulestore())
|
||||
|
||||
|
||||
# ========= TODO: check ajax interaction here too?
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
class TestViewAuth(PageLoader):
|
||||
"""Check that view authentication works properly"""
|
||||
|
||||
# NOTE: setUpClass() runs before override_settings takes effect, so
|
||||
# can't do imports there without manually hacking settings.
|
||||
|
||||
def setUp(self):
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
courses = modulestore().get_courses()
|
||||
|
||||
def find_course(name):
|
||||
"""Assumes the course is present"""
|
||||
return [c for c in courses if c.location.course==name][0]
|
||||
|
||||
self.full = find_course("full")
|
||||
self.toy = find_course("toy")
|
||||
|
||||
# Create two accounts
|
||||
self.student = 'view@test.com'
|
||||
self.instructor = 'view2@test.com'
|
||||
self.password = 'foo'
|
||||
self.create_account('u1', self.student, self.password)
|
||||
self.create_account('u2', self.instructor, self.password)
|
||||
self.activate_user(self.student)
|
||||
self.activate_user(self.instructor)
|
||||
|
||||
def check_for_get_code(self, code, url):
|
||||
resp = self.client.get(url)
|
||||
# HACK: workaround the bug that returns 200 instead of 404.
|
||||
# TODO (vshnayder): once we're returning 404s, get rid of this if.
|
||||
if code != 404:
|
||||
self.assertEqual(resp.status_code, code)
|
||||
# And 'page not found' shouldn't be in the returned page
|
||||
self.assertTrue(resp.content.lower().find('page not found') == -1)
|
||||
else:
|
||||
# look for "page not found" instead of the status code
|
||||
#print resp.content
|
||||
self.assertTrue(resp.content.lower().find('page not found') != -1)
|
||||
|
||||
def test_instructor_pages(self):
|
||||
"""Make sure only instructors for the course or staff can load the instructor
|
||||
dashboard, the grade views, and student profile pages"""
|
||||
|
||||
# First, try with an enrolled student
|
||||
self.login(self.student, self.password)
|
||||
# shouldn't work before enroll
|
||||
self.check_for_get_code(302, reverse('courseware', kwargs={'course_id': self.toy.id}))
|
||||
self.enroll(self.toy)
|
||||
self.enroll(self.full)
|
||||
# should work now
|
||||
self.check_for_get_code(200, reverse('courseware', kwargs={'course_id': self.toy.id}))
|
||||
|
||||
def instructor_urls(course):
|
||||
"list of urls that only instructors/staff should be able to see"
|
||||
urls = [reverse(name, kwargs={'course_id': course.id}) for name in (
|
||||
'instructor_dashboard',
|
||||
'gradebook',
|
||||
'grade_summary',)]
|
||||
urls.append(reverse('student_profile', kwargs={'course_id': course.id,
|
||||
'student_id': user(self.student).id}))
|
||||
return urls
|
||||
|
||||
# shouldn't be able to get to the instructor pages
|
||||
for url in instructor_urls(self.toy) + instructor_urls(self.full):
|
||||
print 'checking for 404 on {}'.format(url)
|
||||
self.check_for_get_code(404, url)
|
||||
|
||||
# Make the instructor staff in the toy course
|
||||
group_name = course_staff_group_name(self.toy)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(user(self.instructor))
|
||||
|
||||
self.logout()
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
# Now should be able to get to the toy course, but not the full course
|
||||
for url in instructor_urls(self.toy):
|
||||
print 'checking for 200 on {}'.format(url)
|
||||
self.check_for_get_code(200, url)
|
||||
|
||||
for url in instructor_urls(self.full):
|
||||
print 'checking for 404 on {}'.format(url)
|
||||
self.check_for_get_code(404, url)
|
||||
|
||||
|
||||
# now also make the instructor staff
|
||||
u = user(self.instructor)
|
||||
u.is_staff = True
|
||||
u.save()
|
||||
|
||||
# and now should be able to load both
|
||||
for url in instructor_urls(self.toy) + instructor_urls(self.full):
|
||||
print 'checking for 200 on {}'.format(url)
|
||||
self.check_for_get_code(200, url)
|
||||
|
||||
|
||||
def run_wrapped(self, test):
|
||||
"""
|
||||
test.py turns off start dates. Enable them and DARK_LAUNCH.
|
||||
Because settings is global, be careful not to mess it up for other tests
|
||||
(Can't use override_settings because we're only changing part of the
|
||||
MITX_FEATURES dict)
|
||||
"""
|
||||
oldDSD = settings.MITX_FEATURES['DISABLE_START_DATES']
|
||||
oldDL = settings.MITX_FEATURES['DARK_LAUNCH']
|
||||
|
||||
try:
|
||||
settings.MITX_FEATURES['DISABLE_START_DATES'] = False
|
||||
settings.MITX_FEATURES['DARK_LAUNCH'] = True
|
||||
test()
|
||||
finally:
|
||||
settings.MITX_FEATURES['DISABLE_START_DATES'] = oldDSD
|
||||
settings.MITX_FEATURES['DARK_LAUNCH'] = oldDL
|
||||
|
||||
|
||||
def test_dark_launch(self):
|
||||
"""Make sure that when dark launch is on, students can't access course
|
||||
pages, but instructors can"""
|
||||
self.run_wrapped(self._do_test_dark_launch)
|
||||
|
||||
def test_enrollment_period(self):
|
||||
"""Check that enrollment periods work"""
|
||||
self.run_wrapped(self._do_test_enrollment_period)
|
||||
|
||||
|
||||
def _do_test_dark_launch(self):
|
||||
"""Actually do the test, relying on settings to be right."""
|
||||
|
||||
# Make courses start in the future
|
||||
tomorrow = time.time() + 24*3600
|
||||
self.toy.start = self.toy.metadata['start'] = time.gmtime(tomorrow)
|
||||
self.full.start = self.full.metadata['start'] = time.gmtime(tomorrow)
|
||||
|
||||
self.assertFalse(self.toy.has_started())
|
||||
self.assertFalse(self.full.has_started())
|
||||
self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES'])
|
||||
self.assertTrue(settings.MITX_FEATURES['DARK_LAUNCH'])
|
||||
|
||||
def reverse_urls(names, course):
|
||||
"""Reverse a list of course urls"""
|
||||
return [reverse(name, kwargs={'course_id': course.id}) for name in names]
|
||||
|
||||
def dark_student_urls(course):
|
||||
"""
|
||||
list of urls that students should be able to see only
|
||||
after launch, but staff should see before
|
||||
"""
|
||||
urls = reverse_urls(['info', 'book', 'courseware', 'profile'], course)
|
||||
return urls
|
||||
|
||||
def light_student_urls(course):
|
||||
"""
|
||||
list of urls that students should be able to see before
|
||||
launch.
|
||||
"""
|
||||
urls = reverse_urls(['about_course'], course)
|
||||
urls.append(reverse('courses'))
|
||||
# Need separate test for change_enrollment, since it's a POST view
|
||||
#urls.append(reverse('change_enrollment'))
|
||||
|
||||
return urls
|
||||
|
||||
def instructor_urls(course):
|
||||
"""list of urls that only instructors/staff should be able to see"""
|
||||
urls = reverse_urls(['instructor_dashboard','gradebook','grade_summary'],
|
||||
course)
|
||||
urls.append(reverse('student_profile', kwargs={'course_id': course.id,
|
||||
'student_id': user(self.student).id}))
|
||||
return urls
|
||||
|
||||
def check_non_staff(course):
|
||||
"""Check that access is right for non-staff in course"""
|
||||
print '=== Checking non-staff access for {}'.format(course.id)
|
||||
for url in instructor_urls(course) + dark_student_urls(course):
|
||||
print 'checking for 404 on {}'.format(url)
|
||||
self.check_for_get_code(404, url)
|
||||
|
||||
for url in light_student_urls(course):
|
||||
print 'checking for 200 on {}'.format(url)
|
||||
self.check_for_get_code(200, url)
|
||||
|
||||
def check_staff(course):
|
||||
"""Check that access is right for staff in course"""
|
||||
print '=== Checking staff access for {}'.format(course.id)
|
||||
for url in (instructor_urls(course) +
|
||||
dark_student_urls(course) +
|
||||
light_student_urls(course)):
|
||||
print 'checking for 200 on {}'.format(url)
|
||||
self.check_for_get_code(200, url)
|
||||
|
||||
# First, try with an enrolled student
|
||||
print '=== Testing student access....'
|
||||
self.login(self.student, self.password)
|
||||
self.enroll(self.toy)
|
||||
self.enroll(self.full)
|
||||
|
||||
# shouldn't be able to get to anything except the light pages
|
||||
check_non_staff(self.toy)
|
||||
check_non_staff(self.full)
|
||||
|
||||
print '=== Testing course instructor access....'
|
||||
# Make the instructor staff in the toy course
|
||||
group_name = course_staff_group_name(self.toy)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(user(self.instructor))
|
||||
|
||||
self.logout()
|
||||
self.login(self.instructor, self.password)
|
||||
# Enroll in the classes---can't see courseware otherwise.
|
||||
self.enroll(self.toy)
|
||||
self.enroll(self.full)
|
||||
|
||||
# should now be able to get to everything for toy course
|
||||
check_non_staff(self.full)
|
||||
check_staff(self.toy)
|
||||
|
||||
print '=== Testing staff access....'
|
||||
# now also make the instructor staff
|
||||
u = user(self.instructor)
|
||||
u.is_staff = True
|
||||
u.save()
|
||||
|
||||
# and now should be able to load both
|
||||
check_staff(self.toy)
|
||||
check_staff(self.full)
|
||||
|
||||
def _do_test_enrollment_period(self):
|
||||
"""Actually do the test, relying on settings to be right."""
|
||||
|
||||
# Make courses start in the future
|
||||
tomorrow = time.time() + 24 * 3600
|
||||
nextday = tomorrow + 24 * 3600
|
||||
yesterday = time.time() - 24 * 3600
|
||||
|
||||
print "changing"
|
||||
# toy course's enrollment period hasn't started
|
||||
self.toy.enrollment_start = time.gmtime(tomorrow)
|
||||
self.toy.enrollment_end = time.gmtime(nextday)
|
||||
|
||||
# full course's has
|
||||
self.full.enrollment_start = time.gmtime(yesterday)
|
||||
self.full.enrollment_end = time.gmtime(tomorrow)
|
||||
|
||||
print "login"
|
||||
# First, try with an enrolled student
|
||||
print '=== Testing student access....'
|
||||
self.login(self.student, self.password)
|
||||
self.assertFalse(self.try_enroll(self.toy))
|
||||
self.assertTrue(self.try_enroll(self.full))
|
||||
|
||||
print '=== Testing course instructor access....'
|
||||
# Make the instructor staff in the toy course
|
||||
group_name = course_staff_group_name(self.toy)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(user(self.instructor))
|
||||
|
||||
print "logout/login"
|
||||
self.logout()
|
||||
self.login(self.instructor, self.password)
|
||||
print "Instructor should be able to enroll in toy course"
|
||||
self.assertTrue(self.try_enroll(self.toy))
|
||||
|
||||
print '=== Testing staff access....'
|
||||
# now make the instructor global staff, but not in the instructor group
|
||||
g.user_set.remove(user(self.instructor))
|
||||
u = user(self.instructor)
|
||||
u.is_staff = True
|
||||
u.save()
|
||||
|
||||
# unenroll and try again
|
||||
self.unenroll(self.toy)
|
||||
self.assertTrue(self.try_enroll(self.toy))
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=REAL_DATA_MODULESTORE)
|
||||
|
||||
@@ -36,7 +36,8 @@ from xmodule.course_module import CourseDescriptor
|
||||
from util.cache import cache, cache_if_anonymous
|
||||
from student.models import UserTestGroup, CourseEnrollment
|
||||
from courseware import grades
|
||||
from courseware.courses import check_course, get_courses_by_university
|
||||
from courseware.courses import (check_course, get_courses_by_university,
|
||||
has_staff_access_to_course_id)
|
||||
|
||||
|
||||
import comment_client
|
||||
@@ -49,6 +50,9 @@ log = logging.getLogger("mitx.courseware")
|
||||
template_imports = {'urllib': urllib}
|
||||
|
||||
def user_groups(user):
|
||||
"""
|
||||
TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately.
|
||||
"""
|
||||
if not user.is_authenticated():
|
||||
return []
|
||||
|
||||
@@ -78,64 +82,6 @@ def courses(request):
|
||||
universities = get_courses_by_university(request.user)
|
||||
return render_to_response("courses.html", {'universities': universities})
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def gradebook(request, course_id):
|
||||
if 'course_admin' not in user_groups(request.user):
|
||||
raise Http404
|
||||
course = check_course(course_id)
|
||||
|
||||
student_objects = User.objects.all()[:100]
|
||||
student_info = []
|
||||
|
||||
#TODO: Only select students who are in the course
|
||||
for student in student_objects:
|
||||
student_info.append({
|
||||
'username': student.username,
|
||||
'id': student.id,
|
||||
'email': student.email,
|
||||
'grade_summary': grades.grade(student, request, course),
|
||||
'realname': UserProfile.objects.get(user=student).name
|
||||
})
|
||||
|
||||
return render_to_response('gradebook.html', {'students': student_info, 'course': course})
|
||||
|
||||
|
||||
@login_required
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def profile(request, course_id, student_id=None):
|
||||
''' User profile. Show username, location, etc, as well as grades .
|
||||
We need to allow the user to change some of these settings .'''
|
||||
course = check_course(course_id)
|
||||
|
||||
if student_id is None:
|
||||
student = request.user
|
||||
else:
|
||||
if 'course_admin' not in user_groups(request.user):
|
||||
raise Http404
|
||||
student = User.objects.get(id=int(student_id))
|
||||
|
||||
user_info = UserProfile.objects.get(user=student)
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, course)
|
||||
course_module = get_module(request.user, request, course.location, student_module_cache)
|
||||
|
||||
courseware_summary = grades.progress_summary(student, course_module, course.grader, student_module_cache)
|
||||
grade_summary = grades.grade(request.user, request, course, student_module_cache)
|
||||
|
||||
context = {'name': user_info.name,
|
||||
'username': student.username,
|
||||
'location': user_info.location,
|
||||
'language': user_info.language,
|
||||
'email': student.email,
|
||||
'course': course,
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'courseware_summary' : courseware_summary,
|
||||
'grade_summary' : grade_summary
|
||||
}
|
||||
context.update()
|
||||
|
||||
return render_to_response('profile.html', context)
|
||||
|
||||
|
||||
def render_accordion(request, course, chapter, section):
|
||||
''' Draws navigation bar. Takes current position in accordion as
|
||||
@@ -143,19 +89,14 @@ def render_accordion(request, course, chapter, section):
|
||||
|
||||
If chapter and section are '' or None, renders a default accordion.
|
||||
|
||||
course, chapter, and section are the url_names.
|
||||
|
||||
Returns the html string'''
|
||||
|
||||
# grab the table of contents
|
||||
toc = toc_for_course(request.user, request, course, chapter, section)
|
||||
|
||||
active_chapter = 1
|
||||
for i in range(len(toc)):
|
||||
if toc[i]['active']:
|
||||
active_chapter = i
|
||||
|
||||
context = dict([('active_chapter', active_chapter),
|
||||
('toc', toc),
|
||||
('course_name', course.title),
|
||||
context = dict([('toc', toc),
|
||||
('course_id', course.id),
|
||||
('csrf', csrf(request)['csrf_token'])] + template_imports.items())
|
||||
return render_to_string('accordion.html', context)
|
||||
@@ -183,9 +124,10 @@ def index(request, course_id, chapter=None, section=None,
|
||||
|
||||
- HTTPresponse
|
||||
'''
|
||||
course = check_course(course_id)
|
||||
course = check_course(request.user, course_id)
|
||||
registered = registered_for_course(course, request.user)
|
||||
if not registered:
|
||||
# TODO (vshnayder): do course instructors need to be registered to see course?
|
||||
log.debug('User %s tried to view course %s but is not enrolled' % (request.user,course.location.url()))
|
||||
return redirect(reverse('about_course', args=[course.id]))
|
||||
|
||||
@@ -247,12 +189,10 @@ def jump_to(request, location):
|
||||
'''
|
||||
Show the page that contains a specific location.
|
||||
|
||||
If the location is invalid, return a 404.
|
||||
If the location is invalid or not in any class, return a 404.
|
||||
|
||||
If the location is valid, but not present in a course, ?
|
||||
|
||||
If the location is valid, but in a course the current user isn't registered for, ?
|
||||
TODO -- let the index view deal with it?
|
||||
Otherwise, delegates to the index view to figure out whether this user
|
||||
has access, and what they should see.
|
||||
'''
|
||||
# Complain if the location isn't valid
|
||||
try:
|
||||
@@ -268,17 +208,17 @@ def jump_to(request, location):
|
||||
except NoPathToItem:
|
||||
raise Http404("This location is not in any class: {0}".format(location))
|
||||
|
||||
# Rely on index to do all error handling
|
||||
# Rely on index to do all error handling and access control.
|
||||
return index(request, course_id, chapter, section, position)
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def course_info(request, course_id):
|
||||
'''
|
||||
"""
|
||||
Display the course's info.html, or 404 if there is no such course.
|
||||
|
||||
Assumes the course_id is in a valid format.
|
||||
'''
|
||||
course = check_course(course_id)
|
||||
"""
|
||||
course = check_course(request.user, course_id)
|
||||
|
||||
return render_to_response('info.html', {'course': course})
|
||||
|
||||
@@ -295,7 +235,7 @@ def registered_for_course(course, user):
|
||||
@ensure_csrf_cookie
|
||||
@cache_if_anonymous
|
||||
def course_about(request, course_id):
|
||||
course = check_course(course_id, course_must_be_open=False)
|
||||
course = check_course(request.user, course_id, course_must_be_open=False)
|
||||
registered = registered_for_course(course, request.user)
|
||||
return render_to_response('portal/course_about.html', {'course': course, 'registered': registered})
|
||||
|
||||
@@ -303,7 +243,10 @@ def course_about(request, course_id):
|
||||
@ensure_csrf_cookie
|
||||
@cache_if_anonymous
|
||||
def university_profile(request, org_id):
|
||||
all_courses = sorted(modulestore().get_courses(), key=lambda course: course.number)
|
||||
"""
|
||||
Return the profile for the particular org_id. 404 if it's not valid.
|
||||
"""
|
||||
all_courses = modulestore().get_courses()
|
||||
valid_org_ids = set(c.org for c in all_courses)
|
||||
if org_id not in valid_org_ids:
|
||||
raise Http404("University Profile not found for {0}".format(org_id))
|
||||
@@ -325,7 +268,7 @@ def render_notifications(request, course, notifications):
|
||||
|
||||
@login_required
|
||||
def news(request, course_id):
|
||||
course = check_course(course_id)
|
||||
course = check_course(request.user, course_id)
|
||||
|
||||
notifications = comment_client.get_notifications(request.user.id)
|
||||
|
||||
@@ -335,3 +278,102 @@ def news(request, course_id):
|
||||
}
|
||||
|
||||
return render_to_response('news.html', context)
|
||||
|
||||
@login_required
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def profile(request, course_id, student_id=None):
|
||||
""" User profile. Show username, location, etc, as well as grades .
|
||||
We need to allow the user to change some of these settings.
|
||||
|
||||
Course staff are allowed to see the profiles of students in their class.
|
||||
"""
|
||||
course = check_course(request.user, course_id)
|
||||
|
||||
if student_id is None or student_id == request.user.id:
|
||||
# always allowed to see your own profile
|
||||
student = request.user
|
||||
else:
|
||||
# Requesting access to a different student's profile
|
||||
if not has_staff_access_to_course_id(request.user, course_id):
|
||||
raise Http404
|
||||
student = User.objects.get(id=int(student_id))
|
||||
|
||||
user_info = UserProfile.objects.get(user=student)
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, course)
|
||||
course_module = get_module(request.user, request, course.location, student_module_cache)
|
||||
|
||||
courseware_summary = grades.progress_summary(student, course_module, course.grader, student_module_cache)
|
||||
grade_summary = grades.grade(request.user, request, course, student_module_cache)
|
||||
|
||||
context = {'name': user_info.name,
|
||||
'username': student.username,
|
||||
'location': user_info.location,
|
||||
'language': user_info.language,
|
||||
'email': student.email,
|
||||
'course': course,
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'courseware_summary' : courseware_summary,
|
||||
'grade_summary' : grade_summary
|
||||
}
|
||||
context.update()
|
||||
|
||||
return render_to_response('profile.html', context)
|
||||
|
||||
|
||||
|
||||
# ======== Instructor views =============================================================================
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def gradebook(request, course_id):
|
||||
"""
|
||||
Show the gradebook for this course:
|
||||
- only displayed to course staff
|
||||
- shows students who are enrolled.
|
||||
"""
|
||||
if not has_staff_access_to_course_id(request.user, course_id):
|
||||
raise Http404
|
||||
|
||||
course = check_course(request.user, course_id)
|
||||
|
||||
enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).order_by('username')
|
||||
|
||||
# TODO (vshnayder): implement pagination.
|
||||
enrolled_students = enrolled_students[:1000] # HACK!
|
||||
|
||||
student_info = [{'username': student.username,
|
||||
'id': student.id,
|
||||
'email': student.email,
|
||||
'grade_summary': grades.grade(student, request, course),
|
||||
'realname': UserProfile.objects.get(user=student).name
|
||||
}
|
||||
for student in enrolled_students]
|
||||
|
||||
return render_to_response('gradebook.html', {'students': student_info,
|
||||
'course': course, 'course_id': course_id})
|
||||
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def grade_summary(request, course_id):
|
||||
"""Display the grade summary for a course."""
|
||||
if not has_staff_access_to_course_id(request.user, course_id):
|
||||
raise Http404
|
||||
|
||||
course = check_course(request.user, course_id)
|
||||
|
||||
# For now, just a static page
|
||||
context = {'course': course }
|
||||
return render_to_response('grade_summary.html', context)
|
||||
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def instructor_dashboard(request, course_id):
|
||||
"""Display the instructor dashboard for a course."""
|
||||
if not has_staff_access_to_course_id(request.user, course_id):
|
||||
raise Http404
|
||||
|
||||
course = check_course(request.user, course_id)
|
||||
|
||||
# For now, just a static page
|
||||
context = {'course': course }
|
||||
return render_to_response('instructor_dashboard.html', context)
|
||||
|
||||
@@ -4,6 +4,7 @@ import django_comment_client.base.views
|
||||
urlpatterns = patterns('django_comment_client.base.views',
|
||||
|
||||
url(r'upload$', 'upload', name='upload'),
|
||||
url(r'users/(?P<user_id>\w+)/update_moderator_status$', 'update_moderator_status', name='update_moderator_status'),
|
||||
url(r'threads/tags/autocomplete$', 'tags_autocomplete', name='tags_autocomplete'),
|
||||
url(r'threads/(?P<thread_id>[\w\-]+)/update$', 'update_thread', name='update_thread'),
|
||||
url(r'threads/(?P<thread_id>[\w\-]+)/reply$', 'create_comment', name='create_comment'),
|
||||
|
||||
@@ -4,8 +4,10 @@ import os
|
||||
import os.path
|
||||
import logging
|
||||
import urlparse
|
||||
import functools
|
||||
|
||||
import comment_client
|
||||
import comment_client as cc
|
||||
import django_comment_client.utils as utils
|
||||
|
||||
from django.core import exceptions
|
||||
from django.contrib.auth.decorators import login_required
|
||||
@@ -14,22 +16,24 @@ from django.views.decorators import csrf
|
||||
from django.core.files.storage import get_storage_class
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from courseware.courses import check_course
|
||||
|
||||
from django_comment_client.utils import JsonResponse, JsonError, extract
|
||||
|
||||
from django_comment_client.permissions import check_permissions_by_view
|
||||
import functools
|
||||
|
||||
from django_comment_client.models import Role
|
||||
|
||||
def permitted(fn):
|
||||
@functools.wraps(fn)
|
||||
def wrapper(request, *args, **kwargs):
|
||||
def fetch_content():
|
||||
if "thread_id" in kwargs:
|
||||
content = comment_client.get_thread(kwargs["thread_id"])
|
||||
content = cc.Thread.find(kwargs["thread_id"]).to_dict()
|
||||
elif "comment_id" in kwargs:
|
||||
content = comment_client.get_comment(kwargs["comment_id"])
|
||||
content = cc.Comment.find(kwargs["comment_id"]).to_dict()
|
||||
else:
|
||||
content = None
|
||||
return content
|
||||
@@ -40,258 +44,292 @@ def permitted(fn):
|
||||
return JsonError("unauthorized")
|
||||
return wrapper
|
||||
|
||||
|
||||
def thread_author_only(fn):
|
||||
def verified_fn(request, *args, **kwargs):
|
||||
thread_id = kwargs.get('thread_id', False)
|
||||
thread = comment_client.get_thread(thread_id)
|
||||
if str(request.user.id) == str(thread['user_id']):
|
||||
return fn(request, *args, **kwargs)
|
||||
else:
|
||||
return JsonError("unauthorized")
|
||||
return verified_fn
|
||||
|
||||
def comment_author_only(fn):
|
||||
def verified_fn(request, *args, **kwargs):
|
||||
comment_id = kwargs.get('comment_id', False)
|
||||
comment = comment_client.get_comment(comment_id)
|
||||
if str(request.user.id) == str(comment['user_id']):
|
||||
return fn(request, *args, **kwargs)
|
||||
else:
|
||||
return JsonError("unauthorized")
|
||||
return verified_fn
|
||||
|
||||
def instructor_only(fn):
|
||||
def verified_fn(request, *args, **kwargs):
|
||||
if not request.user.is_staff:
|
||||
return JsonError("unauthorized")
|
||||
else:
|
||||
return fn(request, *args, **kwargs)
|
||||
return verified_fn
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def create_thread(request, course_id, commentable_id):
|
||||
attributes = extract(request.POST, ['body', 'title', 'tags'])
|
||||
attributes['user_id'] = request.user.id
|
||||
attributes['course_id'] = course_id
|
||||
if request.POST.get('anonymous', 'false').lower() == 'true':
|
||||
attributes['anonymous'] = True
|
||||
if request.POST.get('autowatch', 'false').lower() == 'true':
|
||||
attributes['auto_subscribe'] = True
|
||||
response = comment_client.create_thread(commentable_id, attributes)
|
||||
post = request.POST
|
||||
thread = cc.Thread(**extract(post, ['body', 'title', 'tags']))
|
||||
thread.anonymous = post.get('anonymous', 'false').lower() == 'true'
|
||||
thread.commentable_id = commentable_id
|
||||
thread.course_id = course_id
|
||||
thread.user_id = request.user.id
|
||||
thread.save()
|
||||
if post.get('auto_subscribe', 'false').lower() == 'true':
|
||||
user = cc.User.from_django_user(request.user)
|
||||
user.follow(thread)
|
||||
if request.is_ajax():
|
||||
context = {
|
||||
'course_id': course_id,
|
||||
'thread': response,
|
||||
'thread': thread.to_dict(),
|
||||
}
|
||||
html = render_to_string('discussion/ajax_create_thread.html', context)
|
||||
annotated_content_info = utils.get_annotated_content_info(course_id,
|
||||
thread.to_dict(),
|
||||
request.user,
|
||||
'thread')
|
||||
return JsonResponse({
|
||||
'html': html,
|
||||
'content': response,
|
||||
'content': thread.to_dict(),
|
||||
'annotated_content_info': annotated_content_info,
|
||||
})
|
||||
else:
|
||||
return JsonResponse(response)
|
||||
return JsonResponse(thread.to_dict())
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def update_thread(request, course_id, thread_id):
|
||||
attributes = extract(request.POST, ['body', 'title', 'tags'])
|
||||
response = comment_client.update_thread(thread_id, attributes)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
thread.update_attributes(**extract(request.POST, ['body', 'title', 'tags']))
|
||||
thread.save()
|
||||
if request.is_ajax():
|
||||
context = {
|
||||
'thread': response,
|
||||
'thread': thread.to_dict(),
|
||||
'course_id': course_id,
|
||||
}
|
||||
html = render_to_string('discussion/ajax_update_thread.html', context)
|
||||
annotated_content_info = utils.get_annotated_content_info(course_id,
|
||||
thread.to_dict(),
|
||||
request.user,
|
||||
'thread')
|
||||
return JsonResponse({
|
||||
'html': html,
|
||||
'content': response,
|
||||
'content': thread.to_dict(),
|
||||
'annotated_content_info': annotated_content_info,
|
||||
})
|
||||
else:
|
||||
return JsonResponse(response)
|
||||
return JsonResponse(thread.to_dict())
|
||||
|
||||
def _create_comment(request, course_id, _response_from_attributes):
|
||||
attributes = extract(request.POST, ['body'])
|
||||
attributes['user_id'] = request.user.id
|
||||
attributes['course_id'] = course_id
|
||||
if request.POST.get('anonymous', 'false').lower() == 'true':
|
||||
attributes['anonymous'] = True
|
||||
if request.POST.get('autowatch', 'false').lower() == 'true':
|
||||
attributes['auto_subscribe'] = True
|
||||
response = _response_from_attributes(attributes)
|
||||
def _create_comment(request, course_id, thread_id=None, parent_id=None):
|
||||
post = request.POST
|
||||
comment = cc.Comment(**extract(post, ['body']))
|
||||
comment.anonymous = post.get('anonymous', 'false').lower() == 'true'
|
||||
comment.user_id = request.user.id
|
||||
comment.course_id = course_id
|
||||
comment.thread_id = thread_id
|
||||
comment.parent_id = parent_id
|
||||
comment.save()
|
||||
dict_comment = comment.to_dict()
|
||||
if post.get('auto_subscribe', 'false').lower() == 'true':
|
||||
user = cc.User.from_django_user(request.user)
|
||||
user.follow(comment.thread)
|
||||
if request.is_ajax():
|
||||
context = {
|
||||
'comment': response,
|
||||
'comment': comment.to_dict(),
|
||||
'course_id': course_id,
|
||||
}
|
||||
html = render_to_string('discussion/ajax_create_comment.html', context)
|
||||
annotated_content_info = utils.get_annotated_content_info(course_id,
|
||||
comment.to_dict(),
|
||||
request.user,
|
||||
'comment')
|
||||
return JsonResponse({
|
||||
'html': html,
|
||||
'content': response,
|
||||
'content': comment.to_dict(),
|
||||
'annotated_content_info': annotated_content_info,
|
||||
})
|
||||
else:
|
||||
return JsonResponse(response)
|
||||
return JsonResponse(comment.to_dict())
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def create_comment(request, course_id, thread_id):
|
||||
def _response_from_attributes(attributes):
|
||||
return comment_client.create_comment(thread_id, attributes)
|
||||
return _create_comment(request, course_id, _response_from_attributes)
|
||||
return _create_comment(request, course_id, thread_id=thread_id)
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def delete_thread(request, course_id, thread_id):
|
||||
response = comment_client.delete_thread(thread_id)
|
||||
return JsonResponse(response)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
thread.delete()
|
||||
return JsonResponse(thread.to_dict())
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def update_comment(request, course_id, comment_id):
|
||||
attributes = extract(request.POST, ['body'])
|
||||
response = comment_client.update_comment(comment_id, attributes)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
comment.update_attributes(**extract(request.POST, ['body']))
|
||||
comment.save()
|
||||
if request.is_ajax():
|
||||
context = {
|
||||
'comment': response,
|
||||
'comment': comment.to_dict(),
|
||||
'course_id': course_id,
|
||||
}
|
||||
html = render_to_string('discussion/ajax_update_comment.html', context)
|
||||
annotated_content_info = utils.get_annotated_content_info(course_id,
|
||||
comment.to_dict(),
|
||||
request.user,
|
||||
'comment')
|
||||
return JsonResponse({
|
||||
'html': html,
|
||||
'content': response,
|
||||
'content': comment.to_dict(),
|
||||
'annotated_content_info': annotated_content_info,
|
||||
})
|
||||
else:
|
||||
return JsonResponse(response)
|
||||
return JsonResponse(comment.to_dict()),
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def endorse_comment(request, course_id, comment_id):
|
||||
attributes = extract(request.POST, ['endorsed'])
|
||||
response = comment_client.update_comment(comment_id, attributes)
|
||||
return JsonResponse(response)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
comment.endorsed = request.POST.get('endorsed', 'false').lower() == 'true'
|
||||
comment.save()
|
||||
return JsonResponse(comment.to_dict())
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def openclose_thread(request, course_id, thread_id):
|
||||
attributes = extract(request.POST, ['closed'])
|
||||
response = comment_client.update_thread(thread_id, attributes)
|
||||
return JsonResponse(response)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
thread.closed = request.POST.get('closed', 'false').lower() == 'true'
|
||||
thread.save()
|
||||
return JsonResponse(thread.to_dict())
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def create_sub_comment(request, course_id, comment_id):
|
||||
def _response_from_attributes(attributes):
|
||||
return comment_client.create_sub_comment(comment_id, attributes)
|
||||
return _create_comment(request, course_id, _response_from_attributes)
|
||||
return _create_comment(request, course_id, parent_id=comment_id)
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def delete_comment(request, course_id, comment_id):
|
||||
response = comment_client.delete_comment(comment_id)
|
||||
return JsonResponse(response)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
comment.delete()
|
||||
return JsonResponse(comment.to_dict())
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def vote_for_comment(request, course_id, comment_id, value):
|
||||
user_id = request.user.id
|
||||
response = comment_client.vote_for_comment(comment_id, user_id, value)
|
||||
return JsonResponse(response)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
user.vote(comment, value)
|
||||
return JsonResponse(comment.to_dict())
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def undo_vote_for_comment(request, course_id, comment_id):
|
||||
user_id = request.user.id
|
||||
response = comment_client.undo_vote_for_comment(comment_id, user_id)
|
||||
return JsonResponse(response)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
user.unvote(comment)
|
||||
return JsonResponse(comment.to_dict())
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def vote_for_thread(request, course_id, thread_id, value):
|
||||
user_id = request.user.id
|
||||
response = comment_client.vote_for_thread(thread_id, user_id, value)
|
||||
return JsonResponse(response)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
user.vote(thread, value)
|
||||
return JsonResponse(thread.to_dict())
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def undo_vote_for_thread(request, course_id, thread_id):
|
||||
user_id = request.user.id
|
||||
response = comment_client.undo_vote_for_thread(thread_id, user_id)
|
||||
return JsonResponse(response)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
user.unvote(thread)
|
||||
return JsonResponse(thread.to_dict())
|
||||
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def follow_thread(request, course_id, thread_id):
|
||||
user_id = request.user.id
|
||||
response = comment_client.subscribe_thread(user_id, thread_id)
|
||||
return JsonResponse(response)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
user.follow(thread)
|
||||
return JsonResponse({})
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def follow_commentable(request, course_id, commentable_id):
|
||||
user_id = request.user.id
|
||||
response = comment_client.subscribe_commentable(user_id, commentable_id)
|
||||
return JsonResponse(response)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
commentable = cc.Commentable.find(commentable_id)
|
||||
user.follow(commentable)
|
||||
return JsonResponse({})
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def follow_user(request, course_id, followed_user_id):
|
||||
user_id = request.user.id
|
||||
response = comment_client.follow(user_id, followed_user_id)
|
||||
return JsonResponse(response)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
followed_user = cc.User.find(followed_user_id)
|
||||
user.follow(followed_user)
|
||||
return JsonResponse({})
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def unfollow_thread(request, course_id, thread_id):
|
||||
user_id = request.user.id
|
||||
response = comment_client.unsubscribe_thread(user_id, thread_id)
|
||||
return JsonResponse(response)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
user.unfollow(thread)
|
||||
return JsonResponse({})
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def unfollow_commentable(request, course_id, commentable_id):
|
||||
user_id = request.user.id
|
||||
response = comment_client.unsubscribe_commentable(user_id, commentable_id)
|
||||
return JsonResponse(response)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
commentable = cc.Commentable.find(commentable_id)
|
||||
user.unfollow(commentable)
|
||||
return JsonResponse({})
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def unfollow_user(request, course_id, followed_user_id):
|
||||
user_id = request.user.id
|
||||
response = comment_client.unfollow(user_id, followed_user_id)
|
||||
return JsonResponse(response)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
followed_user = cc.User.find(followed_user_id)
|
||||
user.unfollow(followed_user)
|
||||
return JsonResponse({})
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
def unfollow_user(request, course_id, followed_user_id):
|
||||
user_id = request.user.id
|
||||
response = comment_client.unfollow(user_id, followed_user_id)
|
||||
return JsonResponse(response)
|
||||
@permitted
|
||||
def update_moderator_status(request, course_id, user_id):
|
||||
is_moderator = request.POST.get('is_moderator', '').lower()
|
||||
if is_moderator not in ["true", "false"]:
|
||||
return JsonError("Must provide is_moderator as boolean value")
|
||||
is_moderator = is_moderator == "true"
|
||||
user = User.objects.get(id=user_id)
|
||||
role = Role.objects.get(course_id=course_id, name="Moderator")
|
||||
if is_moderator:
|
||||
user.roles.add(role)
|
||||
else:
|
||||
user.roles.remove(role)
|
||||
if request.is_ajax():
|
||||
course = check_course(request.user, course_id)
|
||||
discussion_user = cc.User(id=user_id, course_id=course_id)
|
||||
context = {
|
||||
'course': course,
|
||||
'course_id': course_id,
|
||||
'user': request.user,
|
||||
'django_user': user,
|
||||
'discussion_user': discussion_user.to_dict(),
|
||||
}
|
||||
return JsonResponse({
|
||||
'html': render_to_string('discussion/ajax_user_profile.html', context)
|
||||
})
|
||||
else:
|
||||
return JsonResponse({})
|
||||
|
||||
@require_GET
|
||||
def search_similar_threads(request, course_id, commentable_id):
|
||||
text = request.GET.get('text', None)
|
||||
if text:
|
||||
return JsonResponse(
|
||||
comment_client.search_similar_threads(
|
||||
cc.search_similar_threads(
|
||||
course_id,
|
||||
recursive=False,
|
||||
query_params={
|
||||
@@ -307,7 +345,7 @@ def tags_autocomplete(request, course_id):
|
||||
value = request.GET.get('q', None)
|
||||
results = []
|
||||
if value:
|
||||
results = comment_client.tags_autocomplete(value)
|
||||
results = cc.tags_autocomplete(value)
|
||||
return JsonResponse(results)
|
||||
|
||||
@require_POST
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.conf.urls.defaults import url, patterns
|
||||
import django_comment_client.forum.views
|
||||
|
||||
urlpatterns = patterns('django_comment_client.forum.views',
|
||||
url(r'search$', 'search', name='search'),
|
||||
url(r'users/(?P<user_id>\w+)$', 'user_profile', name='user_profile'),
|
||||
url(r'(?P<discussion_id>\w+)/threads/(?P<thread_id>\w+)$', 'single_thread', name='single_thread'),
|
||||
url(r'(?P<discussion_id>\w+)/inline$', 'inline_discussion', name='inline_discussion'),
|
||||
url(r'(?P<discussion_id>\w+)$', 'forum_form_discussion', name='forum_form_discussion'),
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.http import HttpResponse
|
||||
from django.utils import simplejson
|
||||
from django.core.context_processors import csrf
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from courseware.courses import check_course
|
||||
@@ -13,12 +14,12 @@ from datehelper import time_ago_in_words
|
||||
|
||||
import django_comment_client.utils as utils
|
||||
from urllib import urlencode
|
||||
from django_comment_client.permissions import check_permissions_by_view
|
||||
|
||||
import json
|
||||
import comment_client
|
||||
import comment_client as cc
|
||||
import dateutil
|
||||
|
||||
from django_comment_client.permissions import check_permissions_by_view
|
||||
|
||||
THREADS_PER_PAGE = 5
|
||||
PAGES_NEARBY_DELTA = 2
|
||||
@@ -41,25 +42,36 @@ def render_accordion(request, course, discussion_id):
|
||||
|
||||
return render_to_string('discussion/_accordion.html', context)
|
||||
|
||||
def render_discussion(request, course_id, threads, discussion_id=None, \
|
||||
discussion_type='inline', query_params={}):
|
||||
def render_discussion(request, course_id, threads, *args, **kwargs):
|
||||
|
||||
discussion_id = kwargs.get('discussion_id')
|
||||
user_id = kwargs.get('user_id')
|
||||
discussion_type = kwargs.get('discussion_type', 'inline')
|
||||
query_params = kwargs.get('query_params', {})
|
||||
|
||||
template = {
|
||||
'inline': 'discussion/_inline.html',
|
||||
'forum': 'discussion/_forum.html',
|
||||
'user': 'discussion/_user_active_threads.html',
|
||||
}[discussion_type]
|
||||
|
||||
base_url = {
|
||||
'inline': (lambda: reverse('django_comment_client.forum.views.inline_discussion', args=[course_id, discussion_id])),
|
||||
'forum': (lambda: reverse('django_comment_client.forum.views.forum_form_discussion', args=[course_id, discussion_id])),
|
||||
'user': (lambda: reverse('django_comment_client.forum.views.user_profile', args=[course_id, user_id])),
|
||||
}[discussion_type]()
|
||||
|
||||
annotated_content_info = {thread['id']: get_annotated_content_info(course_id, thread, request.user, is_thread=True) for thread in threads}
|
||||
print "start annotating"
|
||||
annotated_content_infos = map(lambda x: utils.get_annotated_content_infos(course_id, x, request.user, type='thread'), threads)
|
||||
print "start merging annotations"
|
||||
annotated_content_info = reduce(utils.merge_dict, annotated_content_infos, {})
|
||||
print "finished annotating"
|
||||
|
||||
context = {
|
||||
'threads': threads,
|
||||
'discussion_id': discussion_id,
|
||||
'user_info': comment_client.get_user_info(request.user.id, raw=True),
|
||||
'user_id': user_id,
|
||||
'user_info': json.dumps(cc.User.from_django_user(request.user).to_dict()),
|
||||
'course_id': course_id,
|
||||
'request': request,
|
||||
'performed_search': _should_perform_search(request),
|
||||
@@ -78,6 +90,9 @@ def render_inline_discussion(*args, **kwargs):
|
||||
def render_forum_discussion(*args, **kwargs):
|
||||
return render_discussion(discussion_type='forum', *args, **kwargs)
|
||||
|
||||
def render_user_discussion(*args, **kwargs):
|
||||
return render_discussion(discussion_type='user', *args, **kwargs)
|
||||
|
||||
def get_threads(request, course_id, discussion_id):
|
||||
query_params = {
|
||||
'page': request.GET.get('page', 1),
|
||||
@@ -86,13 +101,11 @@ def get_threads(request, course_id, discussion_id):
|
||||
'sort_order': request.GET.get('sort_order', 'desc'),
|
||||
'text': request.GET.get('text', ''),
|
||||
'tags': request.GET.get('tags', ''),
|
||||
'commentable_id': discussion_id,
|
||||
'course_id': course_id,
|
||||
}
|
||||
|
||||
if _should_perform_search(request):
|
||||
query_params['commentable_id'] = discussion_id
|
||||
threads, page, num_pages = comment_client.search_threads(course_id, recursive=False, query_params=utils.strip_none(query_params))
|
||||
else:
|
||||
threads, page, num_pages = comment_client.get_threads(discussion_id, recursive=False, query_params=utils.strip_none(query_params))
|
||||
threads, page, num_pages = cc.Thread.search(query_params)
|
||||
|
||||
query_params['page'] = page
|
||||
query_params['num_pages'] = num_pages
|
||||
@@ -117,19 +130,19 @@ def render_search_bar(request, course_id, discussion_id=None, text=''):
|
||||
return render_to_string('discussion/_search_bar.html', context)
|
||||
|
||||
def forum_form_discussion(request, course_id, discussion_id):
|
||||
course = check_course(course_id)
|
||||
course = check_course(request.user, course_id)
|
||||
threads, query_params = get_threads(request, course_id, discussion_id)
|
||||
content = render_forum_discussion(request, course_id, threads, discussion_id=discussion_id, \
|
||||
query_params=query_params)
|
||||
|
||||
recent_active_threads = comment_client.search_recent_active_threads(
|
||||
recent_active_threads = cc.search_recent_active_threads(
|
||||
course_id,
|
||||
recursive=False,
|
||||
query_params={'follower_id': request.user.id,
|
||||
'commentable_id': discussion_id},
|
||||
)
|
||||
|
||||
trending_tags = comment_client.search_trending_tags(
|
||||
trending_tags = cc.search_trending_tags(
|
||||
course_id,
|
||||
query_params={'commentable_id': discussion_id},
|
||||
)
|
||||
@@ -147,7 +160,6 @@ def forum_form_discussion(request, course_id, discussion_id):
|
||||
}
|
||||
return render_to_response('discussion/index.html', context)
|
||||
|
||||
|
||||
def get_annotated_content_info(course_id, content, user, is_thread):
|
||||
permissions = {
|
||||
'editable': check_permissions_by_view(user, course_id, content, "update_thread" if is_thread else "update_comment"),
|
||||
@@ -170,15 +182,15 @@ def get_annotated_content_infos(course_id, thread, user, is_thread=True):
|
||||
|
||||
def render_single_thread(request, discussion_id, course_id, thread_id):
|
||||
|
||||
thread = comment_client.get_thread(thread_id, recursive=True)
|
||||
thread = cc.Thread.find(thread_id).retrieve(recursive=True)
|
||||
|
||||
annotated_content_info = get_annotated_content_infos(course_id, thread=thread, \
|
||||
user=request.user, is_thread=True)
|
||||
annotated_content_info = utils.get_annotated_content_infos(course_id, thread=thread.to_dict(), \
|
||||
user=request.user, type='thread')
|
||||
|
||||
context = {
|
||||
'discussion_id': discussion_id,
|
||||
'thread': thread,
|
||||
'user_info': comment_client.get_user_info(request.user.id, raw=True),
|
||||
'user_info': json.dumps(cc.User.from_django_user(request.user).to_dict()),
|
||||
'annotated_content_info': json.dumps(annotated_content_info),
|
||||
'course_id': course_id,
|
||||
'request': request,
|
||||
@@ -189,9 +201,9 @@ def single_thread(request, course_id, discussion_id, thread_id):
|
||||
|
||||
if request.is_ajax():
|
||||
|
||||
thread = comment_client.get_thread(thread_id, recursive=True)
|
||||
annotated_content_info = get_annotated_content_infos(course_id, thread, request.user)
|
||||
context = {'thread': thread}
|
||||
thread = cc.Thread.find(thread_id).retrieve(recursive=True)
|
||||
annotated_content_info = utils.get_annotated_content_infos(course_id, thread, request.user, type='thread')
|
||||
context = {'thread': thread.to_dict(), 'course_id': course_id}
|
||||
html = render_to_string('discussion/_ajax_single_thread.html', context)
|
||||
|
||||
return utils.JsonResponse({
|
||||
@@ -200,7 +212,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
|
||||
})
|
||||
|
||||
else:
|
||||
course = check_course(course_id)
|
||||
course = check_course(request.user, course_id)
|
||||
|
||||
context = {
|
||||
'discussion_id': discussion_id,
|
||||
@@ -209,30 +221,37 @@ def single_thread(request, course_id, discussion_id, thread_id):
|
||||
'content': render_single_thread(request, discussion_id, course_id, thread_id),
|
||||
'accordion': render_accordion(request, course, discussion_id),
|
||||
'course': course,
|
||||
'course_id': course.id,
|
||||
}
|
||||
|
||||
return render_to_response('discussion/index.html', context)
|
||||
|
||||
def search(request, course_id):
|
||||
def user_profile(request, course_id, user_id):
|
||||
|
||||
course = check_course(course_id)
|
||||
course = check_course(request.user, course_id)
|
||||
discussion_user = cc.User(id=user_id, course_id=course_id)
|
||||
|
||||
text = request.GET.get('text', None)
|
||||
commentable_id = request.GET.get('commentable_id', None)
|
||||
tags = request.GET.get('tags', None)
|
||||
|
||||
threads = comment_client.search_threads({
|
||||
'text': text,
|
||||
'commentable_id': commentable_id,
|
||||
'tags': tags,
|
||||
})
|
||||
|
||||
context = {
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'init': '',
|
||||
'content': render_forum_discussion(request, course_id, threads, search_text=text),
|
||||
'accordion': '',
|
||||
'course': course,
|
||||
query_params = {
|
||||
'page': request.GET.get('page', 1),
|
||||
'per_page': THREADS_PER_PAGE, # more than threads_per_page to show more activities
|
||||
}
|
||||
|
||||
return render_to_response('discussion/index.html', context)
|
||||
threads, page, num_pages = discussion_user.active_threads(query_params)
|
||||
|
||||
query_params['page'] = page
|
||||
query_params['num_pages'] = num_pages
|
||||
|
||||
content = render_user_discussion(request, course_id, threads, user_id=user_id, query_params=query_params)
|
||||
|
||||
if request.is_ajax():
|
||||
return utils.HtmlResponse(content)
|
||||
else:
|
||||
context = {
|
||||
'course': course,
|
||||
'user': request.user,
|
||||
'django_user': User.objects.get(id=user_id),
|
||||
'discussion_user': discussion_user.to_dict(),
|
||||
'content': content,
|
||||
}
|
||||
|
||||
return render_to_response('discussion/user_profile.html', context)
|
||||
|
||||
@@ -7,6 +7,7 @@ class Command(BaseCommand):
|
||||
help = 'Seed default permisssions and roles'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
administrator_role = Role.objects.get_or_create(name="Administrator", course_id="MITx/6.002x/2012_Fall")[0]
|
||||
moderator_role = Role.objects.get_or_create(name="Moderator", course_id="MITx/6.002x/2012_Fall")[0]
|
||||
student_role = Role.objects.get_or_create(name="Student", course_id="MITx/6.002x/2012_Fall")[0]
|
||||
|
||||
@@ -19,4 +20,9 @@ class Command(BaseCommand):
|
||||
"endorse_comment", "delete_comment"]:
|
||||
moderator_role.add_permission(per)
|
||||
|
||||
for per in ["manage_moderator"]:
|
||||
administrator_role.add_permission(per)
|
||||
|
||||
moderator_role.inherit_permissions(student_role)
|
||||
|
||||
administrator_role.inherit_permissions(moderator_role)
|
||||
|
||||
@@ -8,6 +8,7 @@ class Command(BaseCommand):
|
||||
help = "Show a user's roles and permissions"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
print args
|
||||
if len(args) != 1:
|
||||
raise CommandError("The number of arguments does not match. ")
|
||||
try:
|
||||
|
||||
@@ -11,7 +11,8 @@ class Role(models.Model):
|
||||
def __unicode__(self):
|
||||
return self.name + " for " + (self.course_id if self.course_id else "all courses")
|
||||
|
||||
def inherit_permissions(self, role):
|
||||
def inherit_permissions(self, role): # TODO the name of this method is a little bit confusing,
|
||||
# since it's one-off and doesn't handle inheritance later
|
||||
if role.course_id and role.course_id != self.course_id:
|
||||
logging.warning("%s cannot inheret permissions from %s due to course_id inconsistency" %
|
||||
(self, role))
|
||||
|
||||
@@ -17,7 +17,6 @@ def assign_default_role(sender, instance, **kwargs):
|
||||
logging.info("assign_default_role: adding %s as %s" % (instance.user, role))
|
||||
instance.user.roles.add(role)
|
||||
|
||||
|
||||
def has_permission(user, permission, course_id=None):
|
||||
# if user.permissions.filter(name=permission).exists():
|
||||
# return True
|
||||
@@ -30,10 +29,16 @@ def has_permission(user, permission, course_id=None):
|
||||
CONDITIONS = ['is_open', 'is_author']
|
||||
def check_condition(user, condition, course_id, data):
|
||||
def check_open(user, condition, course_id, data):
|
||||
return not data['content']['closed']
|
||||
try:
|
||||
return data and not data['content']['closed']
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def check_author(user, condition, course_id, data):
|
||||
return data['content']['user_id'] == str(user.id)
|
||||
try:
|
||||
return data and data['content']['user_id'] == str(user.id)
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
handlers = {
|
||||
'is_open' : check_open,
|
||||
@@ -86,11 +91,11 @@ VIEW_PERMISSIONS = {
|
||||
'unfollow_commentable': ['unfollow_commentable'],
|
||||
'unfollow_user' : ['unfollow_user'],
|
||||
'create_thread' : ['create_thread'],
|
||||
'update_moderator_status' : ['manage_moderator'],
|
||||
}
|
||||
|
||||
|
||||
def check_permissions_by_view(user, course_id, content, name):
|
||||
# import pdb; pdb.set_trace()
|
||||
try:
|
||||
p = VIEW_PERMISSIONS[name]
|
||||
except KeyError:
|
||||
|
||||
@@ -10,17 +10,22 @@ from django.conf import settings
|
||||
import operator
|
||||
import itertools
|
||||
|
||||
from django_comment_client.permissions import check_permissions_by_view
|
||||
|
||||
_FULLMODULES = None
|
||||
_DISCUSSIONINFO = None
|
||||
|
||||
def extract(dic, keys):
|
||||
return {k: dic[k] for k in keys}
|
||||
return {k: dic.get(k) for k in keys}
|
||||
|
||||
def strip_none(dic):
|
||||
def _is_none(v):
|
||||
return v is None or (isinstance(v, str) and len(v.strip()) == 0)
|
||||
return dict([(k, v) for k, v in dic.iteritems() if not _is_none(v)])
|
||||
|
||||
def merge_dict(dic1, dic2):
|
||||
return dict(dic1.items() + dic2.items())
|
||||
|
||||
def get_full_modules():
|
||||
global _FULLMODULES
|
||||
if not _FULLMODULES:
|
||||
@@ -101,9 +106,7 @@ def initialize_discussion_info(request, course):
|
||||
|
||||
class JsonResponse(HttpResponse):
|
||||
def __init__(self, data=None):
|
||||
content = simplejson.dumps(data,
|
||||
indent=2,
|
||||
ensure_ascii=False)
|
||||
content = simplejson.dumps(data)
|
||||
super(JsonResponse, self).__init__(content,
|
||||
mimetype='application/json; charset=utf8')
|
||||
|
||||
@@ -115,7 +118,7 @@ class JsonError(HttpResponse):
|
||||
indent=2,
|
||||
ensure_ascii=False)
|
||||
super(JsonError, self).__init__(content,
|
||||
mimetype='application/json; charset=utf8', status=500)
|
||||
mimetype='application/json; charset=utf8', status=400)
|
||||
|
||||
class HtmlResponse(HttpResponse):
|
||||
def __init__(self, html=''):
|
||||
@@ -124,3 +127,27 @@ class HtmlResponse(HttpResponse):
|
||||
class ViewNameMiddleware(object):
|
||||
def process_view(self, request, view_func, view_args, view_kwargs):
|
||||
request.view_name = view_func.__name__
|
||||
|
||||
def get_annotated_content_info(course_id, content, user, type):
|
||||
return {
|
||||
'editable': check_permissions_by_view(user, course_id, content, "update_thread" if type == 'thread' else "update_comment"),
|
||||
'can_reply': check_permissions_by_view(user, course_id, content, "create_comment" if type == 'thread' else "create_sub_comment"),
|
||||
'can_endorse': check_permissions_by_view(user, course_id, content, "endorse_comment") if type == 'comment' else False,
|
||||
'can_delete': check_permissions_by_view(user, course_id, content, "delete_thread" if type == 'thread' else "delete_comment"),
|
||||
'can_openclose': check_permissions_by_view(user, course_id, content, "openclose_thread") if type == 'thread' else False,
|
||||
}
|
||||
|
||||
def get_annotated_content_infos(course_id, thread, user, type='thread'):
|
||||
infos = {}
|
||||
def _annotate(content, type):
|
||||
infos[str(content['id'])] = get_annotated_content_info(course_id, content, user, type)
|
||||
for child in content.get('children', []):
|
||||
_annotate(child, 'comment')
|
||||
_annotate(thread, type)
|
||||
return infos
|
||||
|
||||
def pluralize(singular_term, count):
|
||||
if int(count) >= 2:
|
||||
return singular_term + 's'
|
||||
return singular_term
|
||||
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -54,3 +54,5 @@ AWS_ACCESS_KEY_ID = AUTH_TOKENS["AWS_ACCESS_KEY_ID"]
|
||||
AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"]
|
||||
|
||||
DATABASES = AUTH_TOKENS['DATABASES']
|
||||
|
||||
XQUEUE_INTERFACE = AUTH_TOKENS['XQUEUE_INTERFACE']
|
||||
|
||||
@@ -37,6 +37,7 @@ COURSEWARE_ENABLED = True
|
||||
ASKBOT_ENABLED = True
|
||||
GENERATE_RANDOM_USER_CREDENTIALS = False
|
||||
PERFSTATS = False
|
||||
DISCUSSION_SERVICE_ENABLED = True
|
||||
|
||||
# Features
|
||||
MITX_FEATURES = {
|
||||
@@ -49,13 +50,17 @@ 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,
|
||||
'ENABLE_DISCUSSION_SERVICE': True,
|
||||
|
||||
'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,
|
||||
@@ -87,6 +92,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
|
||||
@@ -114,6 +131,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
|
||||
'django.core.context_processors.csrf', #necessary for csrf protection
|
||||
)
|
||||
|
||||
STUDENT_FILEUPLOAD_MAX_SIZE = 4*1000*1000 # 4 MB
|
||||
|
||||
# FIXME:
|
||||
# We should have separate S3 staged URLs in case we need to make changes to
|
||||
@@ -207,8 +225,7 @@ STATIC_ROOT = ENV_ROOT / "staticfiles"
|
||||
STATICFILES_DIRS = [
|
||||
COMMON_ROOT / "static",
|
||||
PROJECT_ROOT / "static",
|
||||
ASKBOT_ROOT / "askbot" / "skins",
|
||||
|
||||
PROJECT_ROOT / "askbot" / "skins",
|
||||
]
|
||||
if os.path.isdir(DATA_DIR):
|
||||
STATICFILES_DIRS += [
|
||||
@@ -338,7 +355,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
|
||||
@@ -466,6 +483,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 = [
|
||||
|
||||
@@ -53,6 +53,15 @@ CACHES = {
|
||||
}
|
||||
}
|
||||
|
||||
XQUEUE_INTERFACE = {
|
||||
"url": "http://xqueue.sandbox.edx.org",
|
||||
"django_auth": {
|
||||
"username": "lms",
|
||||
"password": "***REMOVED***"
|
||||
},
|
||||
"basic_auth": ('anant', 'agarwal'),
|
||||
}
|
||||
|
||||
# Make the keyedcache startup warnings go away
|
||||
CACHE_TIMEOUT = 0
|
||||
|
||||
@@ -62,6 +71,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') ])
|
||||
|
||||
0
lms/envs/devgroups/__init__.py
Normal file
46
lms/envs/devgroups/courses.py
Normal file
@@ -0,0 +1,46 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
def askbot_url_for(course_id):
|
||||
return "courses/{0}/discussions/".format(course_id)
|
||||
|
||||
4
lms/envs/devgroups/h_cs50.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .courses import *
|
||||
|
||||
DATABASES = course_db_for('HarvardX/CS50x/2012')
|
||||
ASKBOT_URL = askbot_url_for("HarvardX/CS50x/2012")
|
||||