Merging master
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -22,3 +22,6 @@ reports/
|
||||
\#*\#
|
||||
*.egg-info
|
||||
Gemfile.lock
|
||||
.env/
|
||||
lms/static/sass/*.css
|
||||
cms/static/sass/*.css
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import json
|
||||
from django.test.client import Client
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from mock import patch, Mock
|
||||
from override_settings import override_settings
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from path import path
|
||||
|
||||
from student.models import Registration
|
||||
from django.contrib.auth.models import User
|
||||
from xmodule.modulestore.django import modulestore
|
||||
import xmodule.modulestore.django
|
||||
from xmodule.modulestore import Location
|
||||
from contentstore import import_from_xml
|
||||
import copy
|
||||
|
||||
|
||||
def parse_json(response):
|
||||
@@ -19,17 +25,78 @@ def user(email):
|
||||
'''look up a user by email'''
|
||||
return User.objects.get(email=email)
|
||||
|
||||
|
||||
def registration(email):
|
||||
'''look up registration object by email'''
|
||||
return Registration.objects.get(user__email=email)
|
||||
|
||||
class AuthTestCase(TestCase):
|
||||
|
||||
class ContentStoreTestCase(TestCase):
|
||||
def _login(self, email, pw):
|
||||
'''Login. View should always return 200. The success/fail is in the
|
||||
returned json'''
|
||||
resp = self.client.post(reverse('login_post'),
|
||||
{'email': email, 'password': pw})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
return resp
|
||||
|
||||
def login(self, email, pw):
|
||||
'''Login, check that it worked.'''
|
||||
resp = self._login(email, pw)
|
||||
data = parse_json(resp)
|
||||
self.assertTrue(data['success'])
|
||||
return resp
|
||||
|
||||
def _create_account(self, username, email, pw):
|
||||
'''Try to create an account. No error checking'''
|
||||
resp = self.client.post('/create_account', {
|
||||
'username': username,
|
||||
'email': email,
|
||||
'password': pw,
|
||||
'location': 'home',
|
||||
'language': 'Franglish',
|
||||
'name': 'Fred Weasley',
|
||||
'terms_of_service': 'true',
|
||||
'honor_code': 'true',
|
||||
})
|
||||
return resp
|
||||
|
||||
def create_account(self, username, email, pw):
|
||||
'''Create the account and check that it worked'''
|
||||
resp = self._create_account(username, email, pw)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = parse_json(resp)
|
||||
self.assertEqual(data['success'], True)
|
||||
|
||||
# Check both that the user is created, and inactive
|
||||
self.assertFalse(user(email).is_active)
|
||||
|
||||
return resp
|
||||
|
||||
def _activate_user(self, email):
|
||||
'''look up the user's activation key in the db, then hit the activate view.
|
||||
No error checking'''
|
||||
activation_key = registration(email).activation_key
|
||||
|
||||
# and now we try to activate
|
||||
resp = self.client.get(reverse('activate', kwargs={'key': activation_key}))
|
||||
return resp
|
||||
|
||||
def activate_user(self, email):
|
||||
resp = self._activate_user(email)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
# Now make sure that the user is now actually activated
|
||||
self.assertTrue(user(email).is_active)
|
||||
|
||||
|
||||
class AuthTestCase(ContentStoreTestCase):
|
||||
"""Check that various permissions-related things work"""
|
||||
|
||||
def setUp(self):
|
||||
self.email = 'a@b.com'
|
||||
self.pw = 'xyz'
|
||||
self.username = 'testuser'
|
||||
self.client = Client()
|
||||
|
||||
def check_page_get(self, url, expected):
|
||||
resp = self.client.get(url)
|
||||
@@ -53,67 +120,10 @@ class AuthTestCase(TestCase):
|
||||
data = parse_json(resp)
|
||||
self.assertEqual(data['success'], False)
|
||||
|
||||
def _create_account(self, username, email, pw):
|
||||
'''Try to create an account. No error checking'''
|
||||
resp = self.client.post('/create_account', {
|
||||
'username': username,
|
||||
'email': email,
|
||||
'password': pw,
|
||||
'location' : 'home',
|
||||
'language' : 'Franglish',
|
||||
'name' : 'Fred Weasley',
|
||||
'terms_of_service' : 'true',
|
||||
'honor_code' : 'true'})
|
||||
return resp
|
||||
|
||||
def create_account(self, username, email, pw):
|
||||
'''Create the account and check that it worked'''
|
||||
resp = self._create_account(username, email, pw)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = parse_json(resp)
|
||||
self.assertEqual(data['success'], True)
|
||||
|
||||
# Check both that the user is created, and inactive
|
||||
self.assertFalse(user(self.email).is_active)
|
||||
|
||||
return resp
|
||||
|
||||
def _activate_user(self, email):
|
||||
'''look up the user's activation key in the db, then hit the activate view.
|
||||
No error checking'''
|
||||
activation_key = registration(email).activation_key
|
||||
|
||||
# and now we try to activate
|
||||
resp = self.client.get(reverse('activate', kwargs={'key': activation_key}))
|
||||
return resp
|
||||
|
||||
def activate_user(self, email):
|
||||
resp = self._activate_user(email)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
# Now make sure that the user is now actually activated
|
||||
self.assertTrue(user(self.email).is_active)
|
||||
|
||||
def test_create_account(self):
|
||||
self.create_account(self.username, self.email, self.pw)
|
||||
self.activate_user(self.email)
|
||||
|
||||
|
||||
def _login(self, email, pw):
|
||||
'''Login. View should always return 200. The success/fail is in the
|
||||
returned json'''
|
||||
resp = self.client.post(reverse('login_post'),
|
||||
{'email': email, 'password': pw})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
return resp
|
||||
|
||||
|
||||
def login(self, email, pw):
|
||||
'''Login, check that it worked.'''
|
||||
resp = self._login(self.email, self.pw)
|
||||
data = parse_json(resp)
|
||||
self.assertTrue(data['success'])
|
||||
return resp
|
||||
|
||||
def test_login(self):
|
||||
self.create_account(self.username, self.email, self.pw)
|
||||
|
||||
@@ -121,7 +131,7 @@ class AuthTestCase(TestCase):
|
||||
resp = self._login(self.email, self.pw)
|
||||
data = parse_json(resp)
|
||||
self.assertFalse(data['success'])
|
||||
|
||||
|
||||
self.activate_user(self.email)
|
||||
|
||||
# Now login should work
|
||||
@@ -144,6 +154,9 @@ class AuthTestCase(TestCase):
|
||||
# need an activated user
|
||||
self.test_create_account()
|
||||
|
||||
# Create a new session
|
||||
self.client = Client()
|
||||
|
||||
# Not logged in. Should redirect to login.
|
||||
print 'Not logged in'
|
||||
for page in auth_pages:
|
||||
@@ -157,7 +170,6 @@ class AuthTestCase(TestCase):
|
||||
for page in simple_auth_pages:
|
||||
print "Checking '{0}'".format(page)
|
||||
self.check_page_get(page, expected=200)
|
||||
|
||||
|
||||
def test_index_auth(self):
|
||||
|
||||
@@ -166,3 +178,34 @@ class AuthTestCase(TestCase):
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
# Logged in should work.
|
||||
|
||||
TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
|
||||
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
|
||||
class EditTestCase(ContentStoreTestCase):
|
||||
"""Check that editing functionality works on example courses"""
|
||||
|
||||
def setUp(self):
|
||||
email = 'edit@test.com'
|
||||
password = 'foo'
|
||||
self.create_account('edittest', email, password)
|
||||
self.activate_user(email)
|
||||
self.login(email, password)
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
xmodule.modulestore.django.modulestore().collection.drop()
|
||||
|
||||
def check_edit_item(self, test_course_name):
|
||||
import_from_xml('common/test/data/', test_course_name)
|
||||
|
||||
for descriptor in modulestore().get_items(Location(None, None, None, None, None)):
|
||||
print "Checking ", descriptor.location.url()
|
||||
print descriptor.__class__, descriptor.location
|
||||
resp = self.client.get(reverse('edit_item'), {'id': descriptor.location.url()})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_edit_item_toy(self):
|
||||
self.check_edit_item('toy')
|
||||
|
||||
def test_edit_item_full(self):
|
||||
self.check_edit_item('full')
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
from util.json_request import expect_json
|
||||
import json
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.context_processors import csrf
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.core.urlresolvers import reverse
|
||||
from fs.osfs import OSFS
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.x_module import ModuleSystem
|
||||
from github_sync import export_to_github
|
||||
from static_replace import replace_urls
|
||||
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule_modifiers import replace_static_urls, wrap_xmodule
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from functools import partial
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ==== Public views ==================================================
|
||||
|
||||
@@ -22,7 +31,8 @@ def signup(request):
|
||||
Display the signup form.
|
||||
"""
|
||||
csrf_token = csrf(request)['csrf_token']
|
||||
return render_to_response('signup.html', {'csrf': csrf_token })
|
||||
return render_to_response('signup.html', {'csrf': csrf_token})
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def login_page(request):
|
||||
@@ -30,13 +40,17 @@ def login_page(request):
|
||||
Display the login form.
|
||||
"""
|
||||
csrf_token = csrf(request)['csrf_token']
|
||||
return render_to_response('login.html', {'csrf': csrf_token })
|
||||
|
||||
return render_to_response('login.html', {'csrf': csrf_token})
|
||||
|
||||
|
||||
# ==== Views for any logged-in user ==================================
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def index(request):
|
||||
"""
|
||||
List all courses available to the logged in user
|
||||
"""
|
||||
courses = modulestore().get_items(['i4x', None, None, 'course', None])
|
||||
return render_to_response('index.html', {
|
||||
'courses': [(course.metadata['display_name'],
|
||||
@@ -47,6 +61,7 @@ def index(request):
|
||||
for course in courses]
|
||||
})
|
||||
|
||||
|
||||
# ==== Views with per-item permissions================================
|
||||
|
||||
def has_access(user, location):
|
||||
@@ -54,32 +69,47 @@ def has_access(user, location):
|
||||
# TODO (vshnayder): actually check perms
|
||||
return user.is_active and user.is_authenticated
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def course_index(request, org, course, name):
|
||||
"""
|
||||
Display an editable course overview.
|
||||
|
||||
org, course, name: Attributes of the Location for the item to edit
|
||||
"""
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
if not has_access(request.user, location):
|
||||
raise Http404 # TODO (vshnayder): better error
|
||||
|
||||
|
||||
# TODO (cpennington): These need to be read in from the active user
|
||||
course = modulestore().get_item(location)
|
||||
weeks = course.get_children()
|
||||
return render_to_response('course_index.html', {'weeks': weeks})
|
||||
|
||||
|
||||
@login_required
|
||||
def edit_item(request):
|
||||
"""
|
||||
Display an editing page for the specified module.
|
||||
|
||||
Expects a GET request with the parameter 'id'.
|
||||
|
||||
id: A Location URL
|
||||
"""
|
||||
# TODO (vshnayder): change name from id to location in coffee+html as well.
|
||||
item_location = request.GET['id']
|
||||
print item_location, request.GET
|
||||
if not has_access(request.user, item_location):
|
||||
raise Http404 # TODO (vshnayder): better error
|
||||
|
||||
|
||||
item = modulestore().get_item(item_location)
|
||||
item.get_html = wrap_xmodule(item.get_html, item, "xmodule_edit.html")
|
||||
return render_to_response('unit.html', {
|
||||
'contents': item.get_html(),
|
||||
'js_module': item.js_module_name(),
|
||||
'js_module': item.js_module_name,
|
||||
'category': item.category,
|
||||
'name': item.name,
|
||||
'previews': get_module_previews(request, item),
|
||||
})
|
||||
|
||||
|
||||
@@ -98,6 +128,146 @@ def user_author_string(user):
|
||||
last=l,
|
||||
email=user.email)
|
||||
|
||||
|
||||
@login_required
|
||||
def preview_dispatch(request, preview_id, location, dispatch=None):
|
||||
"""
|
||||
Dispatch an AJAX action to a preview XModule
|
||||
|
||||
Expects a POST request, and passes the arguments to the module
|
||||
|
||||
preview_id (str): An identifier specifying which preview this module is used for
|
||||
location: The Location of the module to dispatch to
|
||||
dispatch: The action to execute
|
||||
"""
|
||||
|
||||
instance_state, shared_state = load_preview_state(request, preview_id, location)
|
||||
descriptor = modulestore().get_item(location)
|
||||
instance = load_preview_module(request, preview_id, descriptor, instance_state, shared_state)
|
||||
# Let the module handle the AJAX
|
||||
try:
|
||||
ajax_return = instance.handle_ajax(dispatch, request.POST)
|
||||
except NotFoundError:
|
||||
log.exception("Module indicating to user that request doesn't exist")
|
||||
raise Http404
|
||||
except:
|
||||
log.exception("error processing ajax call")
|
||||
raise
|
||||
|
||||
save_preview_state(request, preview_id, location, instance.get_instance_state(), instance.get_shared_state())
|
||||
return HttpResponse(ajax_return)
|
||||
|
||||
|
||||
def load_preview_state(request, preview_id, location):
|
||||
"""
|
||||
Load the state of a preview module from the request
|
||||
|
||||
preview_id (str): An identifier specifying which preview this module is used for
|
||||
location: The Location of the module to dispatch to
|
||||
"""
|
||||
if 'preview_states' not in request.session:
|
||||
request.session['preview_states'] = defaultdict(dict)
|
||||
|
||||
instance_state = request.session['preview_states'][preview_id, location].get('instance')
|
||||
shared_state = request.session['preview_states'][preview_id, location].get('shared')
|
||||
|
||||
return instance_state, shared_state
|
||||
|
||||
|
||||
def save_preview_state(request, preview_id, location, instance_state, shared_state):
|
||||
"""
|
||||
Load the state of a preview module to the request
|
||||
|
||||
preview_id (str): An identifier specifying which preview this module is used for
|
||||
location: The Location of the module to dispatch to
|
||||
instance_state: The instance state to save
|
||||
shared_state: The shared state to save
|
||||
"""
|
||||
if 'preview_states' not in request.session:
|
||||
request.session['preview_states'] = defaultdict(dict)
|
||||
|
||||
request.session['preview_states'][preview_id, location]['instance'] = instance_state
|
||||
request.session['preview_states'][preview_id, location]['shared'] = shared_state
|
||||
|
||||
|
||||
def render_from_lms(template_name, dictionary, context=None, namespace='main'):
|
||||
"""
|
||||
Render a template using the LMS MAKO_TEMPLATES
|
||||
"""
|
||||
return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
|
||||
|
||||
|
||||
def preview_module_system(request, preview_id, descriptor):
|
||||
"""
|
||||
Returns a ModuleSystem for the specified descriptor that is specialized for
|
||||
rendering module previews.
|
||||
|
||||
request: The active django request
|
||||
preview_id (str): An identifier specifying which preview this module is used for
|
||||
descriptor: An XModuleDescriptor
|
||||
"""
|
||||
return ModuleSystem(
|
||||
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']),
|
||||
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
|
||||
track_function=lambda type, event: None,
|
||||
filestore=descriptor.system.resources_fs,
|
||||
get_module=partial(get_preview_module, request, preview_id),
|
||||
render_template=render_from_lms,
|
||||
debug=True,
|
||||
replace_urls=replace_urls
|
||||
)
|
||||
|
||||
|
||||
def get_preview_module(request, preview_id, location):
|
||||
"""
|
||||
Returns a preview XModule at the specified location. The preview_data is chosen arbitrarily
|
||||
from the set of preview data for the descriptor specified by Location
|
||||
|
||||
request: The active django request
|
||||
preview_id (str): An identifier specifying which preview this module is used for
|
||||
location: A Location
|
||||
"""
|
||||
descriptor = modulestore().get_item(location)
|
||||
instance_state, shared_state = descriptor.get_sample_state()[0]
|
||||
return load_preview_module(request, preview_id, descriptor, instance_state, shared_state)
|
||||
|
||||
|
||||
def load_preview_module(request, preview_id, descriptor, instance_state, shared_state):
|
||||
"""
|
||||
Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state
|
||||
|
||||
request: The active django request
|
||||
preview_id (str): An identifier specifying which preview this module is used for
|
||||
descriptor: An XModuleDescriptor
|
||||
instance_state: An instance state string
|
||||
shared_state: A shared state string
|
||||
"""
|
||||
system = preview_module_system(request, preview_id, descriptor)
|
||||
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']
|
||||
)
|
||||
save_preview_state(request, preview_id, descriptor.location.url(),
|
||||
module.get_instance_state(), module.get_shared_state())
|
||||
|
||||
return module
|
||||
|
||||
|
||||
def get_module_previews(request, descriptor):
|
||||
"""
|
||||
Returns a list of preview XModule html contents. One preview is returned for each
|
||||
pair of states returned by get_sample_state() for the supplied descriptor.
|
||||
|
||||
descriptor: An XModuleDescriptor
|
||||
"""
|
||||
preview_html = []
|
||||
for idx, (instance_state, shared_state) in enumerate(descriptor.get_sample_state()):
|
||||
module = load_preview_module(request, str(idx), descriptor, instance_state, shared_state)
|
||||
preview_html.append(module.get_html())
|
||||
return preview_html
|
||||
|
||||
|
||||
@login_required
|
||||
@expect_json
|
||||
def save_item(request):
|
||||
@@ -118,4 +288,7 @@ def save_item(request):
|
||||
author_string = user_author_string(request.user)
|
||||
export_to_github(course, "CMS Edit", author_string)
|
||||
|
||||
return HttpResponse(json.dumps({}))
|
||||
descriptor = modulestore().get_item(item_location)
|
||||
preview_html = get_module_previews(request, descriptor)
|
||||
|
||||
return HttpResponse(json.dumps(preview_html))
|
||||
|
||||
@@ -13,16 +13,17 @@ from .exceptions import GithubSyncError
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def import_from_github(repo_settings):
|
||||
def setup_repo(repo_settings):
|
||||
"""
|
||||
Imports data into the modulestore based on the XML stored on github
|
||||
Reset the local github repo specified by repo_settings
|
||||
|
||||
repo_settings is a dictionary with the following keys:
|
||||
path: file system path to the local git repo
|
||||
branch: name of the branch to track on github
|
||||
origin: git url for the repository to track
|
||||
"""
|
||||
repo_path = repo_settings['path']
|
||||
data_dir, course_dir = os.path.split(repo_path)
|
||||
course_dir = repo_settings['path']
|
||||
repo_path = settings.GITHUB_REPO_ROOT / course_dir
|
||||
|
||||
if not os.path.isdir(repo_path):
|
||||
Repo.clone_from(repo_settings['origin'], repo_path)
|
||||
@@ -33,8 +34,29 @@ def import_from_github(repo_settings):
|
||||
|
||||
# Do a hard reset to the remote branch so that we have a clean import
|
||||
git_repo.git.checkout(repo_settings['branch'])
|
||||
|
||||
return git_repo
|
||||
|
||||
|
||||
def load_repo_settings(course_dir):
|
||||
"""
|
||||
Returns the repo_settings for the course stored in course_dir
|
||||
"""
|
||||
for repo_settings in settings.REPOS.values():
|
||||
if repo_settings['path'] == course_dir:
|
||||
return repo_settings
|
||||
raise InvalidRepo(course_dir)
|
||||
|
||||
|
||||
def import_from_github(repo_settings):
|
||||
"""
|
||||
Imports data into the modulestore based on the XML stored on github
|
||||
"""
|
||||
course_dir = repo_settings['path']
|
||||
git_repo = setup_repo(repo_settings)
|
||||
git_repo.head.reset('origin/%s' % repo_settings['branch'], index=True, working_tree=True)
|
||||
module_store = import_from_xml(data_dir, course_dirs=[course_dir])
|
||||
|
||||
module_store = import_from_xml(settings.GITHUB_REPO_ROOT, course_dirs=[course_dir])
|
||||
return git_repo.head.commit.hexsha, module_store.courses[course_dir]
|
||||
|
||||
|
||||
@@ -44,21 +66,23 @@ def export_to_github(course, commit_message, author_str=None):
|
||||
and push to github (if MITX_FEATURES['GITHUB_PUSH'] is True).
|
||||
If author_str is specified, uses it in the commit.
|
||||
'''
|
||||
repo_path = settings.DATA_DIR / course.metadata.get('course_dir', course.location.course)
|
||||
fs = OSFS(repo_path)
|
||||
course_dir = course.metadata.get('data_dir', course.location.course)
|
||||
repo_settings = load_repo_settings(course_dir)
|
||||
git_repo = setup_repo(repo_settings)
|
||||
|
||||
fs = OSFS(git_repo.working_dir)
|
||||
xml = course.export_to_xml(fs)
|
||||
|
||||
with fs.open('course.xml', 'w') as course_xml:
|
||||
course_xml.write(xml)
|
||||
|
||||
git_repo = Repo(repo_path)
|
||||
if git_repo.is_dirty():
|
||||
git_repo.git.add(A=True)
|
||||
if author_str is not None:
|
||||
git_repo.git.commit(m=commit_message, author=author_str)
|
||||
else:
|
||||
git_repo.git.commit(m=commit_message)
|
||||
|
||||
|
||||
origin = git_repo.remotes.origin
|
||||
if settings.MITX_FEATURES['GITHUB_PUSH']:
|
||||
push_infos = origin.push()
|
||||
|
||||
@@ -10,36 +10,37 @@ from xmodule.modulestore import Location
|
||||
from override_settings import override_settings
|
||||
from github_sync.exceptions import GithubSyncError
|
||||
|
||||
REPO_DIR = settings.GITHUB_REPO_ROOT / 'local_repo'
|
||||
WORKING_DIR = path(settings.TEST_ROOT)
|
||||
REMOTE_DIR = WORKING_DIR / 'remote_repo'
|
||||
|
||||
@override_settings(DATA_DIR=path('test_root'))
|
||||
|
||||
@override_settings(REPOS={
|
||||
'local': {
|
||||
'path': 'local_repo',
|
||||
'origin': REMOTE_DIR,
|
||||
'branch': 'master',
|
||||
}
|
||||
})
|
||||
class GithubSyncTestCase(TestCase):
|
||||
|
||||
def cleanup(self):
|
||||
shutil.rmtree(self.repo_dir, ignore_errors=True)
|
||||
shutil.rmtree(self.remote_dir, ignore_errors=True)
|
||||
shutil.rmtree(REPO_DIR, ignore_errors=True)
|
||||
shutil.rmtree(REMOTE_DIR, ignore_errors=True)
|
||||
modulestore().collection.drop()
|
||||
|
||||
def setUp(self):
|
||||
self.working_dir = path(settings.TEST_ROOT)
|
||||
self.repo_dir = self.working_dir / 'local_repo'
|
||||
self.remote_dir = self.working_dir / 'remote_repo'
|
||||
|
||||
# make sure there's no stale data lying around
|
||||
self.cleanup()
|
||||
|
||||
shutil.copytree('common/test/data/toy', self.remote_dir)
|
||||
shutil.copytree('common/test/data/toy', REMOTE_DIR)
|
||||
|
||||
remote = Repo.init(self.remote_dir)
|
||||
remote = Repo.init(REMOTE_DIR)
|
||||
remote.git.add(A=True)
|
||||
remote.git.commit(m='Initial commit')
|
||||
remote.git.config("receive.denyCurrentBranch", "ignore")
|
||||
|
||||
modulestore().collection.drop()
|
||||
|
||||
self.import_revision, self.import_course = import_from_github({
|
||||
'path': self.repo_dir,
|
||||
'origin': self.remote_dir,
|
||||
'branch': 'master',
|
||||
})
|
||||
self.import_revision, self.import_course = import_from_github(settings.REPOS['local'])
|
||||
|
||||
def tearDown(self):
|
||||
self.cleanup()
|
||||
@@ -48,7 +49,7 @@ class GithubSyncTestCase(TestCase):
|
||||
"""
|
||||
Test that importing from github will create a repo if the repo doesn't already exist
|
||||
"""
|
||||
self.assertEquals(1, len(Repo(self.repo_dir).head.reference.log()))
|
||||
self.assertEquals(1, len(Repo(REPO_DIR).head.reference.log()))
|
||||
|
||||
def test_import_contents(self):
|
||||
"""
|
||||
@@ -66,7 +67,7 @@ class GithubSyncTestCase(TestCase):
|
||||
Test that with the GITHUB_PUSH feature disabled, no content is pushed to the remote
|
||||
"""
|
||||
export_to_github(self.import_course, 'Test no-push')
|
||||
self.assertEquals(1, Repo(self.remote_dir).head.commit.count())
|
||||
self.assertEquals(1, Repo(REMOTE_DIR).head.commit.count())
|
||||
|
||||
@override_settings(MITX_FEATURES={'GITHUB_PUSH': True})
|
||||
def test_export_push(self):
|
||||
@@ -75,7 +76,7 @@ class GithubSyncTestCase(TestCase):
|
||||
"""
|
||||
self.import_course.metadata['display_name'] = 'Changed display name'
|
||||
export_to_github(self.import_course, 'Test push')
|
||||
self.assertEquals(2, Repo(self.remote_dir).head.commit.count())
|
||||
self.assertEquals(2, Repo(REMOTE_DIR).head.commit.count())
|
||||
|
||||
@override_settings(MITX_FEATURES={'GITHUB_PUSH': True})
|
||||
def test_export_conflict(self):
|
||||
@@ -84,7 +85,7 @@ class GithubSyncTestCase(TestCase):
|
||||
"""
|
||||
self.import_course.metadata['display_name'] = 'Changed display name'
|
||||
|
||||
remote = Repo(self.remote_dir)
|
||||
remote = Repo(REMOTE_DIR)
|
||||
remote.git.commit(allow_empty=True, m="Testing conflict commit")
|
||||
|
||||
self.assertRaises(GithubSyncError, export_to_github, self.import_course, 'Test push')
|
||||
|
||||
@@ -50,4 +50,3 @@ class PostReceiveTestCase(TestCase):
|
||||
import_from_github.assert_called_with(settings.REPOS['repo'])
|
||||
mock_revision, mock_course = import_from_github.return_value
|
||||
export_to_github.assert_called_with(mock_course, 'path', "Changes from cms import of revision %s" % mock_revision)
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ import os.path
|
||||
import os
|
||||
import errno
|
||||
import glob2
|
||||
import lms.envs.common
|
||||
import hashlib
|
||||
from collections import defaultdict
|
||||
from path import path
|
||||
|
||||
############################ FEATURE CONFIGURATION #############################
|
||||
@@ -43,10 +46,8 @@ PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/cms
|
||||
REPO_ROOT = PROJECT_ROOT.dirname()
|
||||
COMMON_ROOT = REPO_ROOT / "common"
|
||||
ENV_ROOT = REPO_ROOT.dirname() # virtualenv dir /mitx is in
|
||||
COURSES_ROOT = ENV_ROOT / "data"
|
||||
|
||||
# FIXME: To support multiple courses, we should walk the courses dir at startup
|
||||
DATA_DIR = COURSES_ROOT
|
||||
GITHUB_REPO_ROOT = ENV_ROOT / "data"
|
||||
|
||||
sys.path.append(REPO_ROOT)
|
||||
sys.path.append(PROJECT_ROOT / 'djangoapps')
|
||||
@@ -61,9 +62,13 @@ MAKO_MODULE_DIR = tempfile.mkdtemp('mako')
|
||||
MAKO_TEMPLATES = {}
|
||||
MAKO_TEMPLATES['main'] = [
|
||||
PROJECT_ROOT / 'templates',
|
||||
COMMON_ROOT / 'templates',
|
||||
COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates'
|
||||
]
|
||||
|
||||
for namespace, template_dirs in lms.envs.common.MAKO_TEMPLATES.iteritems():
|
||||
MAKO_TEMPLATES['lms.' + namespace] = template_dirs
|
||||
|
||||
TEMPLATE_DIRS = (
|
||||
PROJECT_ROOT / "templates",
|
||||
)
|
||||
@@ -132,26 +137,32 @@ IGNORABLE_404_ENDS = ('favicon.ico')
|
||||
|
||||
# Email
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
DEFAULT_FROM_EMAIL = 'registration@mitx.mit.edu'
|
||||
DEFAULT_FEEDBACK_EMAIL = 'feedback@mitx.mit.edu'
|
||||
DEFAULT_FROM_EMAIL = 'registration@edx.org'
|
||||
DEFAULT_FEEDBACK_EMAIL = 'feedback@edx.org'
|
||||
ADMINS = (
|
||||
('MITx Admins', 'admin@mitx.mit.edu'),
|
||||
('edX Admins', 'admin@edx.org'),
|
||||
)
|
||||
MANAGERS = ADMINS
|
||||
|
||||
# Static content
|
||||
STATIC_URL = '/static/'
|
||||
ADMIN_MEDIA_PREFIX = '/static/admin/'
|
||||
STATIC_ROOT = ENV_ROOT / "staticfiles"
|
||||
STATIC_ROOT = ENV_ROOT / "staticfiles"
|
||||
|
||||
# FIXME: We should iterate through the courses we have, adding the static
|
||||
# contents for each of them. (Right now we just use symlinks.)
|
||||
STATICFILES_DIRS = [
|
||||
COMMON_ROOT / "static",
|
||||
PROJECT_ROOT / "static",
|
||||
|
||||
# This is how you would use the textbook images locally
|
||||
# ("book", ENV_ROOT / "book_images")
|
||||
]
|
||||
if os.path.isdir(GITHUB_REPO_ROOT):
|
||||
STATICFILES_DIRS += [
|
||||
# TODO (cpennington): When courses aren't loaded from github, remove this
|
||||
(course_dir, GITHUB_REPO_ROOT / course_dir)
|
||||
for course_dir in os.listdir(GITHUB_REPO_ROOT)
|
||||
if os.path.isdir(GITHUB_REPO_ROOT / course_dir)
|
||||
]
|
||||
|
||||
# Locale/Internationalization
|
||||
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||
@@ -166,6 +177,69 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
|
||||
|
||||
STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage'
|
||||
|
||||
# Load javascript and css from all of the available descriptors, and
|
||||
# prep it for use in pipeline js
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
js_file_dir = PROJECT_ROOT / "static" / "coffee" / "module"
|
||||
css_file_dir = PROJECT_ROOT / "static" / "sass" / "module"
|
||||
module_styles_path = css_file_dir / "_module-styles.scss"
|
||||
|
||||
for dir_ in (js_file_dir, css_file_dir):
|
||||
try:
|
||||
os.makedirs(dir_)
|
||||
except OSError as exc:
|
||||
if exc.errno == errno.EEXIST:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
js_fragments = set()
|
||||
css_fragments = defaultdict(set)
|
||||
for descriptor in XModuleDescriptor.load_classes() + [RawDescriptor]:
|
||||
descriptor_js = descriptor.get_javascript()
|
||||
module_js = descriptor.module_class.get_javascript()
|
||||
|
||||
for filetype in ('coffee', 'js'):
|
||||
for idx, fragment in enumerate(descriptor_js.get(filetype, []) + module_js.get(filetype, [])):
|
||||
js_fragments.add((idx, filetype, fragment))
|
||||
|
||||
for class_ in (descriptor, descriptor.module_class):
|
||||
fragments = class_.get_css()
|
||||
for filetype in ('sass', 'scss', 'css'):
|
||||
for idx, fragment in enumerate(fragments.get(filetype, [])):
|
||||
css_fragments[idx, filetype, fragment].add(class_.__name__)
|
||||
|
||||
module_js_sources = []
|
||||
for idx, filetype, fragment in sorted(js_fragments):
|
||||
path = js_file_dir / "{idx}-{hash}.{type}".format(
|
||||
idx=idx,
|
||||
hash=hashlib.md5(fragment).hexdigest(),
|
||||
type=filetype)
|
||||
with open(path, 'w') as js_file:
|
||||
js_file.write(fragment)
|
||||
module_js_sources.append(path.replace(PROJECT_ROOT / "static/", ""))
|
||||
|
||||
css_imports = defaultdict(set)
|
||||
for (idx, filetype, fragment), classes in sorted(css_fragments.items()):
|
||||
fragment_name = "{idx}-{hash}.{type}".format(
|
||||
idx=idx,
|
||||
hash=hashlib.md5(fragment).hexdigest(),
|
||||
type=filetype)
|
||||
# Prepend _ so that sass just includes the files into a single file
|
||||
with open(css_file_dir / '_' + fragment_name, 'w') as js_file:
|
||||
js_file.write(fragment)
|
||||
|
||||
for class_ in classes:
|
||||
css_imports[class_].add(fragment_name)
|
||||
|
||||
with open(module_styles_path, 'w') as module_styles:
|
||||
for class_, fragment_names in css_imports.items():
|
||||
imports = "\n".join('@import "{0}";'.format(name) for name in fragment_names)
|
||||
module_styles.write(""".xmodule_{class_} {{ {imports} }}""".format(
|
||||
class_=class_, imports=imports
|
||||
))
|
||||
|
||||
PIPELINE_CSS = {
|
||||
'base-style': {
|
||||
'source_filenames': ['sass/base-style.scss'],
|
||||
@@ -175,33 +249,17 @@ PIPELINE_CSS = {
|
||||
|
||||
PIPELINE_ALWAYS_RECOMPILE = ['sass/base-style.scss']
|
||||
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
js_file_dir = PROJECT_ROOT / "static" / "coffee" / "module"
|
||||
try:
|
||||
os.makedirs(js_file_dir)
|
||||
except OSError as exc:
|
||||
if exc.errno == errno.EEXIST:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
module_js_sources = []
|
||||
for xmodule in XModuleDescriptor.load_classes() + [RawDescriptor]:
|
||||
js = xmodule.get_javascript()
|
||||
for filetype in ('coffee', 'js'):
|
||||
for idx, fragment in enumerate(js.get(filetype, [])):
|
||||
path = os.path.join(js_file_dir, "{name}.{idx}.{type}".format(
|
||||
name=xmodule.__name__,
|
||||
idx=idx,
|
||||
type=filetype))
|
||||
with open(path, 'w') as js_file:
|
||||
js_file.write(fragment)
|
||||
module_js_sources.append(path.replace(PROJECT_ROOT / "static/", ""))
|
||||
|
||||
PIPELINE_JS = {
|
||||
'main': {
|
||||
'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in glob2.glob(PROJECT_ROOT / 'static/coffee/src/**/*.coffee')],
|
||||
'source_filenames': [
|
||||
pth.replace(COMMON_ROOT / 'static/', '')
|
||||
for pth
|
||||
in glob2.glob(COMMON_ROOT / 'static/coffee/src/**/*.coffee')
|
||||
] + [
|
||||
pth.replace(PROJECT_ROOT / 'static/', '')
|
||||
for pth
|
||||
in glob2.glob(PROJECT_ROOT / 'static/coffee/src/**/*.coffee')
|
||||
],
|
||||
'output_filename': 'js/application.js',
|
||||
},
|
||||
'module-js': {
|
||||
|
||||
@@ -18,6 +18,7 @@ MODULESTORE = {
|
||||
'host': 'localhost',
|
||||
'db': 'xmodule',
|
||||
'collection': 'modulestore',
|
||||
'fs_root': GITHUB_REPO_ROOT,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,35 +32,35 @@ DATABASES = {
|
||||
|
||||
REPOS = {
|
||||
'edx4edx': {
|
||||
'path': DATA_DIR / "edx4edx",
|
||||
'path': "edx4edx",
|
||||
'org': 'edx',
|
||||
'course': 'edx4edx',
|
||||
'branch': 'for_cms',
|
||||
'origin': 'git@github.com:MITx/edx4edx.git',
|
||||
},
|
||||
'6002x-fall-2012': {
|
||||
'path': DATA_DIR / '6002x-fall-2012',
|
||||
'path': '6002x-fall-2012',
|
||||
'org': 'mit.edu',
|
||||
'course': '6.002x',
|
||||
'branch': 'for_cms',
|
||||
'origin': 'git@github.com:MITx/6002x-fall-2012.git',
|
||||
},
|
||||
'6.00x': {
|
||||
'path': DATA_DIR / '6.00x',
|
||||
'path': '6.00x',
|
||||
'org': 'mit.edu',
|
||||
'course': '6.00x',
|
||||
'branch': 'for_cms',
|
||||
'origin': 'git@github.com:MITx/6.00x.git',
|
||||
},
|
||||
'7.00x': {
|
||||
'path': DATA_DIR / '7.00x',
|
||||
'path': '7.00x',
|
||||
'org': 'mit.edu',
|
||||
'course': '7.00x',
|
||||
'branch': 'for_cms',
|
||||
'origin': 'git@github.com:MITx/7.00x.git',
|
||||
},
|
||||
'3.091x': {
|
||||
'path': DATA_DIR / '3.091x',
|
||||
'path': '3.091x',
|
||||
'org': 'mit.edu',
|
||||
'course': '3.091x',
|
||||
'branch': 'for_cms',
|
||||
|
||||
@@ -22,8 +22,21 @@ TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
||||
TEST_ROOT = path('test_root')
|
||||
|
||||
# Want static files in the same dir for running on jenkins.
|
||||
STATIC_ROOT = TEST_ROOT / "staticfiles"
|
||||
STATIC_ROOT = TEST_ROOT / "staticfiles"
|
||||
|
||||
GITHUB_REPO_ROOT = TEST_ROOT / "data"
|
||||
COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data"
|
||||
|
||||
# TODO (cpennington): We need to figure out how envs/test.py can inject things into common.py so that we don't have to repeat this sort of thing
|
||||
STATICFILES_DIRS = [
|
||||
COMMON_ROOT / "static",
|
||||
PROJECT_ROOT / "static",
|
||||
]
|
||||
STATICFILES_DIRS += [
|
||||
(course_dir, COMMON_TEST_DATA_ROOT / course_dir)
|
||||
for course_dir in os.listdir(COMMON_TEST_DATA_ROOT)
|
||||
if os.path.isdir(COMMON_TEST_DATA_ROOT / course_dir)
|
||||
]
|
||||
|
||||
MODULESTORE = {
|
||||
'default': {
|
||||
@@ -33,6 +46,7 @@ MODULESTORE = {
|
||||
'host': 'localhost',
|
||||
'db': 'test_xmodule',
|
||||
'collection': 'modulestore',
|
||||
'fs_root': GITHUB_REPO_ROOT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
AjaxPrefix.addAjaxPrefix(jQuery, -> CMS.prefix)
|
||||
|
||||
@CMS =
|
||||
Models: {}
|
||||
Views: {}
|
||||
|
||||
prefix: $("meta[name='path_prefix']").attr('content')
|
||||
|
||||
viewStack: []
|
||||
|
||||
start: (el) ->
|
||||
@@ -31,5 +35,13 @@ $ ->
|
||||
|
||||
$.ajaxSetup
|
||||
headers : { 'X-CSRFToken': $.cookie 'csrftoken' }
|
||||
dataType: 'json'
|
||||
|
||||
window.onTouchBasedDevice = ->
|
||||
navigator.userAgent.match /iPhone|iPod|iPad/i
|
||||
|
||||
$('body').addClass 'touch-based-device' if onTouchBasedDevice()
|
||||
|
||||
|
||||
CMS.start($('section.main-container'))
|
||||
|
||||
|
||||
@@ -4,10 +4,7 @@ class CMS.Models.Module extends Backbone.Model
|
||||
data: ''
|
||||
|
||||
loadModule: (element) ->
|
||||
try
|
||||
@module = new window[@get('type')](element)
|
||||
catch TypeError
|
||||
console.error "Unable to load #{@get('type')}." if console
|
||||
@module = XModule.loadModule($(element).find('.xmodule_edit'))
|
||||
|
||||
editUrl: ->
|
||||
"/edit_item?#{$.param(id: @get('id'))}"
|
||||
|
||||
@@ -4,4 +4,10 @@ class CMS.Views.Module extends Backbone.View
|
||||
|
||||
edit: (event) =>
|
||||
event.preventDefault()
|
||||
CMS.replaceView(new CMS.Views.ModuleEdit(model: new CMS.Models.Module(id: @$el.data('id'), type: @$el.data('type'))))
|
||||
previewType = @$el.data('preview-type')
|
||||
moduleType = @$el.data('type')
|
||||
CMS.replaceView new CMS.Views.ModuleEdit
|
||||
model: new CMS.Models.Module
|
||||
id: @$el.data('id')
|
||||
type: if moduleType == 'None' then null else moduleType
|
||||
previewType: if previewType == 'None' then null else previewType
|
||||
|
||||
@@ -11,11 +11,21 @@ class CMS.Views.ModuleEdit extends Backbone.View
|
||||
@$el.load @model.editUrl(), =>
|
||||
@model.loadModule(@el)
|
||||
|
||||
# Load preview modules
|
||||
XModule.loadModules('display')
|
||||
|
||||
save: (event) ->
|
||||
event.preventDefault()
|
||||
@model.save().success(->
|
||||
@model.save().done((previews) =>
|
||||
alert("Your changes have been saved.")
|
||||
).error(->
|
||||
previews_section = @$el.find('.previews').empty()
|
||||
$.each(previews, (idx, preview) =>
|
||||
preview_wrapper = $('<section/>', class: 'preview').append preview
|
||||
previews_section.append preview_wrapper
|
||||
)
|
||||
|
||||
XModule.loadModules('display')
|
||||
).fail(->
|
||||
alert("There was an error saving your changes. Please try again.")
|
||||
)
|
||||
|
||||
@@ -25,4 +35,10 @@ class CMS.Views.ModuleEdit extends Backbone.View
|
||||
|
||||
editSubmodule: (event) ->
|
||||
event.preventDefault()
|
||||
CMS.pushView(new CMS.Views.ModuleEdit(model: new CMS.Models.Module(id: $(event.target).data('id'), type: $(event.target).data('type'))))
|
||||
previewType = $(event.target).data('preview-type')
|
||||
moduleType = $(event.target).data('type')
|
||||
CMS.pushView new CMS.Views.ModuleEdit
|
||||
model: new CMS.Models.Module
|
||||
id: $(event.target).data('id')
|
||||
type: if moduleType == 'None' then null else moduleType
|
||||
previewType: if previewType == 'None' then null else previewType
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
class @Unit
|
||||
constructor: (@element_id, @module_id) ->
|
||||
@module = new window[$("##{@element_id}").attr('class')] 'module-html'
|
||||
|
||||
$("##{@element_id} .save-update").click (event) =>
|
||||
event.preventDefault()
|
||||
$.post("/save_item", {
|
||||
id: @module_id
|
||||
data: JSON.stringify(@module.save())
|
||||
})
|
||||
|
||||
$("##{@element_id} .cancel").click (event) =>
|
||||
event.preventDefault()
|
||||
CMS.edit_item(@module_id)
|
||||
|
||||
1
cms/static/sass/.gitignore
vendored
1
cms/static/sass/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
*.css
|
||||
module
|
||||
|
||||
@@ -10,6 +10,8 @@ $dark-blue: #50545c;
|
||||
$bright-blue: #3c8ebf;
|
||||
$orange: #f96e5b;
|
||||
$yellow: #fff8af;
|
||||
$cream: #F6EFD4;
|
||||
$mit-red: #933;
|
||||
|
||||
// Base html styles
|
||||
html {
|
||||
|
||||
@@ -4,3 +4,5 @@
|
||||
@import 'base', 'layout', 'content-types';
|
||||
@import 'calendar';
|
||||
@import 'section', 'unit';
|
||||
|
||||
@import 'module/module-styles.scss';
|
||||
|
||||
@@ -6,38 +6,35 @@
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
|
||||
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
|
||||
<%static:css group='base-style'/>
|
||||
% else:
|
||||
<link rel="stylesheet" href="${static.url('css/base-style.css')}">
|
||||
% endif
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/skins/simple/style.css')}" />
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/sets/wiki/style.css')}" />
|
||||
<title><%block name="title"></%block></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
|
||||
<meta name="path_prefix" content="${MITX_ROOT_URL}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<%include file="widgets/header.html"/>
|
||||
<%include file="courseware_vendor_js.html"/>
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/vendor/jquery.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/json2.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/markitup/jquery.markitup.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/markitup/sets/wiki/set.js')}"></script>
|
||||
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
|
||||
<%static:js group='main'/>
|
||||
% else:
|
||||
<script src="${ STATIC_URL }/js/main.js"></script>
|
||||
% endif
|
||||
|
||||
<%static:js group='module-js'/>
|
||||
<script src="${static.url('js/vendor/jquery.inlineedit.js')}"></script>
|
||||
<script src="${static.url('js/vendor/jquery.cookie.js')}"></script>
|
||||
<script src="${static.url('js/vendor/jquery.leanModal.min.js')}"></script>
|
||||
<script src="${static.url('js/vendor/jquery.tablednd.js')}"></script>
|
||||
<script type="text/javascript">
|
||||
document.write('\x3Cscript type="text/javascript" src="' +
|
||||
document.location.protocol + '//www.youtube.com/player_api">\x3C/script>');
|
||||
</script>
|
||||
|
||||
<%block name="content"></%block>
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
Someone, hopefully you, signed up for an account for edX's on-line
|
||||
offering of "${ course_title}" using this email address. If it was
|
||||
you, and you'd like to activate and use your account, copy and paste
|
||||
this address into your web browser's address bar:
|
||||
Thank you for signing up for edX! To activate your account,
|
||||
please copy and paste this address into your web browser's
|
||||
address bar:
|
||||
|
||||
% if is_secure:
|
||||
https://${ site }/activate/${ key }
|
||||
% else:
|
||||
http://edx4edx.mitx.mit.edu/activate/${ key }
|
||||
http://${ site }/activate/${ key }
|
||||
% endif
|
||||
|
||||
If you didn't request this, you don't need to do anything; you won't
|
||||
|
||||
@@ -1 +1 @@
|
||||
Your account for edX's on-line ${course_title} course
|
||||
Your account for edX
|
||||
|
||||
30
cms/templates/registration/activation_complete.html
Normal file
30
cms/templates/registration/activation_complete.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%inherit file="../base.html" />
|
||||
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<section class="container activation">
|
||||
|
||||
<section class="message">
|
||||
%if not already_active:
|
||||
<h1 class="valid">Activation Complete!</h1>
|
||||
%else:
|
||||
<h1>Account already active!</h1>
|
||||
%endif
|
||||
<hr class="horizontal-divider">
|
||||
|
||||
<p>
|
||||
%if not already_active:
|
||||
Thanks for activating your account.
|
||||
%else:
|
||||
This account has already been activated.
|
||||
%endif
|
||||
|
||||
%if user_logged_in:
|
||||
Visit your <a href="${reverse('dashboard')}">dashboard</a> to see your courses.
|
||||
%else:
|
||||
You can now <a href="#login-modal" rel="leanModal">login</a>.
|
||||
%endif
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
@@ -10,10 +10,10 @@
|
||||
<hr>
|
||||
</header>
|
||||
|
||||
<div id="enroll">
|
||||
<div id="register">
|
||||
|
||||
<form id="enroll_form" method="post">
|
||||
<div id="enroll_error" name="enroll_error"></div>
|
||||
<form id="register_form" method="post">
|
||||
<div id="register_error" name="register_error"></div>
|
||||
<label>E-mail</label>
|
||||
<input name="email" type="email" placeholder="E-mail">
|
||||
<label>Password</label>
|
||||
@@ -64,17 +64,17 @@
|
||||
});
|
||||
}
|
||||
|
||||
$('form#enroll_form').submit(function(e) {
|
||||
$('form#register_form').submit(function(e) {
|
||||
e.preventDefault();
|
||||
var submit_data = $('#enroll_form').serialize();
|
||||
var submit_data = $('#register_form').serialize();
|
||||
|
||||
postJSON('/create_account',
|
||||
submit_data,
|
||||
function(json) {
|
||||
if(json.success) {
|
||||
$('#enroll').html(json.value);
|
||||
$('#register').html(json.value);
|
||||
} else {
|
||||
$('#enroll_error').html(json.value).stop().css("background-color", "#933").animate({ backgroundColor: "#333"}, 2000);
|
||||
$('#register_error').html(json.value).stop().css("background-color", "#933").animate({ backgroundColor: "#333"}, 2000);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<section id="unit-wrapper" class="${js_module}">
|
||||
<section id="unit-wrapper">
|
||||
<header>
|
||||
<section>
|
||||
<h1 class="editable">${name}</h1>
|
||||
@@ -12,6 +12,47 @@
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<section class="meta wip">
|
||||
<section class="status-settings">
|
||||
<ul>
|
||||
<li><a href="#" class="current">Scrap</a></li>
|
||||
<li><a href="#">Draft</a></li>
|
||||
<li><a href="#">Proofed</a></li>
|
||||
<li><a href="#">Published</a></li>
|
||||
</ul>
|
||||
<a href="#" class="settings">Settings</a>
|
||||
</section>
|
||||
<section class="author">
|
||||
<dl>
|
||||
<dt>Last modified:</dt>
|
||||
<dd>mm/dd/yy</dd>
|
||||
<dt>By</dt>
|
||||
<dd>Anant Agarwal</dd>
|
||||
</dl>
|
||||
</section>
|
||||
<section class="tags">
|
||||
<div>
|
||||
<h2>Tags:</h2>
|
||||
<p class="editable">Click to edit</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Goal</h2>
|
||||
<p class="editable">Click to edit</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
${contents}
|
||||
<section class="previews">
|
||||
% for preview in previews:
|
||||
<section class="preview">
|
||||
${preview}
|
||||
</section>
|
||||
% endfor
|
||||
</section>
|
||||
<div class="actions wip">
|
||||
<a href="" class="save-update">Save & Update</a>
|
||||
<a href="#" class="cancel">Cancel</a>
|
||||
</div>
|
||||
<%include file="widgets/notes.html"/>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -1,45 +1,3 @@
|
||||
<section class="html-edit">
|
||||
<section class="meta wip">
|
||||
|
||||
<section class="status-settings">
|
||||
<ul>
|
||||
<li><a href="#" class="current">Scrap</a></li>
|
||||
<li><a href="#">Draft</a></li>
|
||||
<li><a href="#">Proofed</a></li>
|
||||
<li><a href="#">Published</a></li>
|
||||
</ul>
|
||||
<a href="#" class="settings">Settings</a>
|
||||
</section>
|
||||
|
||||
<section class="author">
|
||||
<dl>
|
||||
<dt>Last modified:</dt>
|
||||
<dd>mm/dd/yy</dd>
|
||||
<dt>By</dt>
|
||||
<dd>Anant Agarwal</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="tags">
|
||||
<div>
|
||||
<h2>Tags:</h2>
|
||||
<p class="editable">Click to edit</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Goal</h2>
|
||||
<p class="editable">Click to edit</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<textarea name="" class="edit-box" rows="8" cols="40">${data}</textarea>
|
||||
<div class="preview">${data}</div>
|
||||
|
||||
<div class="actions">
|
||||
<a href="" class="save-update">Save & Update</a>
|
||||
<a href="#" class="cancel">Cancel</a>
|
||||
</div>
|
||||
|
||||
<%include file="notes.html"/>
|
||||
</section>
|
||||
|
||||
@@ -55,7 +55,11 @@
|
||||
|
||||
<ul class="modules">
|
||||
% for module in week.get_children():
|
||||
<li class="module" data-id="${module.location.url()}" data-type="${module.js_module_name()}">
|
||||
<li class="module"
|
||||
data-id="${module.location.url()}"
|
||||
data-type="${module.js_module_name}"
|
||||
data-preview-type="${module.module_class.js_module_name}">
|
||||
|
||||
<a href="#" class="module-edit">${module.name}</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<section class="problem-edit">
|
||||
<section class="meta">
|
||||
<section class="status-settings">
|
||||
<ul>
|
||||
<li><a href="#" class="current">Scrap</a></li>
|
||||
<li><a href="#">Draft</a></li>
|
||||
<li><a href="#">Proofed</a></li>
|
||||
<li><a href="#">Published</a></li>
|
||||
</ul>
|
||||
<a href="#" class="settings">Settings</a>
|
||||
</section>
|
||||
|
||||
<section class="author">
|
||||
<dl>
|
||||
<dt>Last modified:</dt>
|
||||
<dd>mm/dd/yy</dd>
|
||||
<dt>By</dt>
|
||||
<dd>Anant Agarwal</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="tags">
|
||||
<div>
|
||||
<h2>Tags:</h2>
|
||||
<p class="editable">Click to edit</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Goal</h2>
|
||||
<p class="editable">Click to edit</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<textarea name="" id= rows="8" cols="40">Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.</textarea>
|
||||
<div class="preview">
|
||||
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<a href="#" class="cancel">Cancel</a>
|
||||
<a href="" class="save-update">Save & Update</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%include file="notes.html"/>
|
||||
</section>
|
||||
@@ -1,51 +0,0 @@
|
||||
<section class="problem-new">
|
||||
<header>
|
||||
<a href="#" class="cancel">Cancel</a>
|
||||
<a href="#" class="save-update">Save & Update</a>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<header>
|
||||
<h1 class="editable">New Problem</h1>
|
||||
|
||||
<section class="status-settings">
|
||||
<ul>
|
||||
<li><a href="#" class="current">Scrap</a></li>
|
||||
<li><a href="#">Draft</a></li>
|
||||
<li><a href="#">Proofed</a></li>
|
||||
<li><a href="#">Published</a></li>
|
||||
</ul>
|
||||
<a href="#" class="settings">Settings</a>
|
||||
|
||||
<select name="" id="">
|
||||
<option>Global</option>
|
||||
</select>
|
||||
</section>
|
||||
<section class="meta">
|
||||
<div>
|
||||
<h2>Tags:</h2>
|
||||
<p class="editable">Click to edit</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Goal</h2>
|
||||
<p class="editable">Click to edit</p>
|
||||
</div>
|
||||
</section>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<textarea name="" id= rows="8" cols="40"></textarea>
|
||||
<div class="preview">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="notes">
|
||||
<h2>Add notes</h2>
|
||||
<textarea name="" id= rows="8" cols="40"></textarea>
|
||||
<input type="submit" name="" id="" value="post" />
|
||||
</section>
|
||||
|
||||
<a href="" class="save-update">Save & Update</a>
|
||||
</section>
|
||||
</section>
|
||||
@@ -1,45 +1,3 @@
|
||||
<section class="raw-edit">
|
||||
<section class="meta wip">
|
||||
|
||||
<section class="status-settings">
|
||||
<ul>
|
||||
<li><a href="#" class="current">Scrap</a></li>
|
||||
<li><a href="#">Draft</a></li>
|
||||
<li><a href="#">Proofed</a></li>
|
||||
<li><a href="#">Published</a></li>
|
||||
</ul>
|
||||
<a href="#" class="settings">Settings</a>
|
||||
</section>
|
||||
|
||||
<section class="author">
|
||||
<dl>
|
||||
<dt>Last modified:</dt>
|
||||
<dd>mm/dd/yy</dd>
|
||||
<dt>By</dt>
|
||||
<dd>Anant Agarwal</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="tags">
|
||||
<div>
|
||||
<h2>Tags:</h2>
|
||||
<p class="editable">Click to edit</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Goal</h2>
|
||||
<p class="editable">Click to edit</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<textarea name="" class="edit-box" rows="8" cols="40">${data}</textarea>
|
||||
<pre class="preview">${data | h}</pre>
|
||||
|
||||
<div class="actions wip">
|
||||
<a href="" class="save-update">Save & Update</a>
|
||||
<a href="#" class="cancel">Cancel</a>
|
||||
</div>
|
||||
|
||||
<%include file="notes.html"/>
|
||||
<textarea name="" class="edit-box" rows="8" cols="40">${data | h}</textarea>
|
||||
</section>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<li>
|
||||
<img src="http://placehold.it/300x180" alt="" /><h5>Video-file-name</h5>
|
||||
</li>
|
||||
@@ -36,7 +36,10 @@
|
||||
<ol>
|
||||
% for child in module.get_children():
|
||||
<li class="${module.category}">
|
||||
<a href="#" class="module-edit" data-id="${child.location.url()}" data-type="${child.js_module_name()}">${child.name}</a>
|
||||
<a href="#" class="module-edit"
|
||||
data-id="${child.location.url()}"
|
||||
data-type="${child.js_module_name}"
|
||||
data-preview-type="${child.module_class.js_module_name}">${child.name}</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
%endfor
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
<section class="sequence-edit">
|
||||
<header>
|
||||
<div class="week">
|
||||
<h2><a href="">Week 1</a></h2>
|
||||
<ul>
|
||||
<li>
|
||||
<p class="editable"><strong>Goal title:</strong> This is the goal body and is where the goal will be further explained</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="editable">Lecture sequence</h1>
|
||||
<p><strong>Group type:</strong> Ordered Sequence</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="content">
|
||||
<section class="filters">
|
||||
<ul>
|
||||
<li>
|
||||
<label for="">Sort by</label>
|
||||
<select>
|
||||
<option value="">Recently Modified</option>
|
||||
</select>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<label for="">Display</label>
|
||||
<select>
|
||||
<option value="">All content</option>
|
||||
</select>
|
||||
</li>
|
||||
<li>
|
||||
<select>
|
||||
<option value="">Internal Only</option>
|
||||
</select>
|
||||
</li>
|
||||
|
||||
<li class="advanced">
|
||||
<a href="#">Advanced filters</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<input type="search" name="" id="" value="" />
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div>
|
||||
<section class="modules">
|
||||
<ol>
|
||||
<li>
|
||||
<ol>
|
||||
<li>
|
||||
<a href="" class="problem-edit">Problem title 11</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="sequence-edit">Problem Group</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 14</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="video-edit">Video 3</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li class="group">
|
||||
<header>
|
||||
<h3>
|
||||
<a href="#" class="problem-edit">Problem group</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</h3>
|
||||
</header>
|
||||
<ol>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 11</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 11</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 11</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 13</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 14</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="video-edit">Video 3</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="" class="problem-edit">Problem title 11</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="sequence-edit">Problem Group</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 14</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="video-edit">Video 3</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
|
||||
<!-- <li class="new-module"> -->
|
||||
<!-- <%include file="new-module.html"/> -->
|
||||
<!-- </li> -->
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="scratch-pad">
|
||||
<ol>
|
||||
<li>
|
||||
<header>
|
||||
<h2>Section Scratch</h2>
|
||||
</header>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 11</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 13 </a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="problem-edit"> Problem title 14</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="" class="video-edit">Video 3</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<header>
|
||||
<h2>Course Scratch</h2>
|
||||
</header>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 11</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 13 </a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="problem-edit"> Problem title 14</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="" class="video-edit">Video 3</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<!-- <li class="new-module"> -->
|
||||
<!-- <%include file="new-module.html"/> -->
|
||||
<!-- </li> -->
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -14,6 +14,8 @@ urlpatterns = ('',
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$',
|
||||
'contentstore.views.course_index', name='course_index'),
|
||||
url(r'^github_service_hook$', 'github_sync.views.github_post_receive'),
|
||||
url(r'^preview/modx/(?P<preview_id>[^/]*)/(?P<location>.*?)/(?P<dispatch>[^/]*)$',
|
||||
'contentstore.views.preview_dispatch', name='preview_dispatch')
|
||||
)
|
||||
|
||||
# User creation and updating views
|
||||
|
||||
@@ -13,6 +13,7 @@ from django.db import DEFAULT_DB_ALIAS
|
||||
|
||||
from . import app_settings
|
||||
|
||||
|
||||
def get_instance(model, instance_or_pk, timeout=None, using=None):
|
||||
"""
|
||||
Returns the ``model`` instance with a primary key of ``instance_or_pk``.
|
||||
@@ -87,6 +88,7 @@ def get_instance(model, instance_or_pk, timeout=None, using=None):
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
def delete_instance(model, *instance_or_pk):
|
||||
"""
|
||||
Purges the cache keys for the instances of this model.
|
||||
@@ -94,6 +96,7 @@ def delete_instance(model, *instance_or_pk):
|
||||
|
||||
cache.delete_many([instance_key(model, x) for x in instance_or_pk])
|
||||
|
||||
|
||||
def instance_key(model, instance_or_pk):
|
||||
"""
|
||||
Returns the cache key for this (model, instance) pair.
|
||||
|
||||
@@ -84,6 +84,7 @@ from django.contrib.auth.middleware import AuthenticationMiddleware
|
||||
|
||||
from .model import cache_model
|
||||
|
||||
|
||||
class CacheBackedAuthenticationMiddleware(AuthenticationMiddleware):
|
||||
def __init__(self):
|
||||
cache_model(User)
|
||||
|
||||
@@ -58,6 +58,7 @@ from django.db.models.signals import post_save, post_delete
|
||||
|
||||
from .core import get_instance, delete_instance
|
||||
|
||||
|
||||
def cache_model(model, timeout=None):
|
||||
if hasattr(model, 'get_cached'):
|
||||
# Already patched
|
||||
|
||||
@@ -74,6 +74,7 @@ from django.db.models.signals import post_save, post_delete
|
||||
|
||||
from .core import get_instance, delete_instance
|
||||
|
||||
|
||||
def cache_relation(descriptor, timeout=None):
|
||||
rel = descriptor.related
|
||||
related_name = '%s_cache' % rel.field.related_query_name()
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.template import resolve_variable
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
class CacheNode(Node):
|
||||
def __init__(self, nodelist, expire_time, key):
|
||||
self.nodelist = nodelist
|
||||
@@ -21,6 +22,7 @@ class CacheNode(Node):
|
||||
cache.set(key, value, expire_time)
|
||||
return value
|
||||
|
||||
|
||||
@register.tag
|
||||
def cachedeterministic(parser, token):
|
||||
"""
|
||||
@@ -42,6 +44,7 @@ def cachedeterministic(parser, token):
|
||||
raise TemplateSyntaxError(u"'%r' tag requires 2 arguments." % tokens[0])
|
||||
return CacheNode(nodelist, tokens[1], tokens[2])
|
||||
|
||||
|
||||
class ShowIfCachedNode(Node):
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
@@ -50,6 +53,7 @@ class ShowIfCachedNode(Node):
|
||||
key = resolve_variable(self.key, context)
|
||||
return cache.get(key) or ''
|
||||
|
||||
|
||||
@register.tag
|
||||
def showifcached(parser, token):
|
||||
"""
|
||||
|
||||
@@ -60,6 +60,7 @@ def csrf_response_exempt(view_func):
|
||||
PendingDeprecationWarning)
|
||||
return view_func
|
||||
|
||||
|
||||
def csrf_view_exempt(view_func):
|
||||
"""
|
||||
Marks a view function as being exempt from CSRF view protection.
|
||||
@@ -68,6 +69,7 @@ def csrf_view_exempt(view_func):
|
||||
PendingDeprecationWarning)
|
||||
return csrf_exempt(view_func)
|
||||
|
||||
|
||||
def csrf_exempt(view_func):
|
||||
"""
|
||||
Marks a view function as being exempt from the CSRF view protection.
|
||||
|
||||
@@ -6,6 +6,7 @@ from pipeline.conf import settings
|
||||
from pipeline.packager import Packager
|
||||
from pipeline.utils import guess_type
|
||||
|
||||
|
||||
def compressed_css(package_name):
|
||||
package = settings.PIPELINE_CSS.get(package_name, {})
|
||||
if package:
|
||||
@@ -20,6 +21,7 @@ def compressed_css(package_name):
|
||||
paths = packager.compile(package.paths)
|
||||
return render_individual_css(package, paths)
|
||||
|
||||
|
||||
def render_css(package, path):
|
||||
template_name = package.template_name or "mako/css.html"
|
||||
context = package.extra_context
|
||||
@@ -29,6 +31,7 @@ def render_css(package, path):
|
||||
})
|
||||
return render_to_string(template_name, context)
|
||||
|
||||
|
||||
def render_individual_css(package, paths):
|
||||
tags = [render_css(package, path) for path in paths]
|
||||
return '\n'.join(tags)
|
||||
@@ -49,6 +52,7 @@ def compressed_js(package_name):
|
||||
templates = packager.pack_templates(package)
|
||||
return render_individual_js(package, paths, templates)
|
||||
|
||||
|
||||
def render_js(package, path):
|
||||
template_name = package.template_name or "mako/js.html"
|
||||
context = package.extra_context
|
||||
@@ -58,6 +62,7 @@ def render_js(package, path):
|
||||
})
|
||||
return render_to_string(template_name, context)
|
||||
|
||||
|
||||
def render_inline_js(package, js):
|
||||
context = package.extra_context
|
||||
context.update({
|
||||
@@ -65,6 +70,7 @@ def render_inline_js(package, js):
|
||||
})
|
||||
return render_to_string("mako/inline_js.html", context)
|
||||
|
||||
|
||||
def render_individual_js(package, paths, templates=None):
|
||||
tags = [render_js(package, js) for js in paths]
|
||||
if templates:
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
<%!
|
||||
from staticfiles.storage import staticfiles_storage
|
||||
from pipeline_mako import compressed_css, compressed_js
|
||||
from static_replace import replace_urls
|
||||
%>
|
||||
|
||||
<%def name='url(file)'>${staticfiles_storage.url(file)}</%def>
|
||||
<%def name='css(group)'>${compressed_css(group)}</%def>
|
||||
<%def name='js(group)'>${compressed_js(group)}</%def>
|
||||
<%def name='replace_urls(text)'>${replace_urls(text)}</%def>
|
||||
|
||||
<%def name='css(group)'>
|
||||
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
|
||||
${compressed_css(group)}
|
||||
% else:
|
||||
% for filename in settings.PIPELINE_CSS[group]['source_filenames']:
|
||||
<link rel="stylesheet" href="${staticfiles_storage.url(filename.replace('.scss', '.css'))}" type="text/css" media="all" / >
|
||||
% endfor
|
||||
%endif
|
||||
</%def>
|
||||
<%def name='js(group)'>
|
||||
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
|
||||
${compressed_js(group)}
|
||||
% else:
|
||||
% for filename in settings.PIPELINE_JS[group]['source_filenames']:
|
||||
<script type="text/javascript" src="${staticfiles_storage.url(filename.replace('.coffee', '.js'))}"></script>
|
||||
% endfor
|
||||
%endif
|
||||
</%def>
|
||||
|
||||
@@ -15,4 +15,3 @@ admin.site.register(CourseEnrollment)
|
||||
admin.site.register(Registration)
|
||||
|
||||
admin.site.register(PendingNameChange)
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
##
|
||||
## One-off script to export 6.002x users into the edX framework
|
||||
##
|
||||
## Could be modified to be general by:
|
||||
## * Changing user_keys and up_keys to handle dates more cleanly
|
||||
## * Providing a generic set of tables, rather than just users and user profiles
|
||||
## * Handling certificates and grades
|
||||
## * Handling merge/forks of UserProfile.meta
|
||||
|
||||
|
||||
import datetime
|
||||
import json
|
||||
|
||||
import os.path
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from student.models import UserProfile
|
||||
|
||||
import mitxmako.middleware as middleware
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Exports all users and user profiles.
|
||||
Caveat: Should be looked over before any run
|
||||
for schema changes.
|
||||
|
||||
Current version grabs user_keys from
|
||||
django.contrib.auth.models.User and up_keys
|
||||
from student.userprofile. '''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
users = list(User.objects.all())
|
||||
user_profiles = list(UserProfile.objects.all())
|
||||
user_profile_dict = dict([(up.user_id, up) for up in user_profiles])
|
||||
|
||||
user_tuples = [(user_profile_dict[u.id], u) for u in users if u.id in user_profile_dict]
|
||||
|
||||
user_keys = ['id', 'username', 'email', 'password', 'is_staff',
|
||||
'is_active', 'is_superuser', 'last_login', 'date_joined',
|
||||
'password']
|
||||
up_keys = ['language', 'location', 'meta', 'name', 'id', 'user_id']
|
||||
|
||||
def extract_dict(keys, object):
|
||||
d = {}
|
||||
for key in keys:
|
||||
item = object.__getattribute__(key)
|
||||
if type(item) == datetime.datetime:
|
||||
item = item.isoformat()
|
||||
d[key] = item
|
||||
return d
|
||||
|
||||
extracted = [{'up':extract_dict(up_keys, t[0]), 'u':extract_dict(user_keys, t[1])} for t in user_tuples]
|
||||
fp = open('transfer_users.txt', 'w')
|
||||
json.dump(extracted, fp)
|
||||
fp.close()
|
||||
@@ -0,0 +1,68 @@
|
||||
##
|
||||
## One-off script to import 6.002x users into the edX framework
|
||||
## See export for more info
|
||||
|
||||
|
||||
import datetime
|
||||
import json
|
||||
|
||||
import dateutil.parser
|
||||
|
||||
import os.path
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from student.models import UserProfile
|
||||
|
||||
import mitxmako.middleware as middleware
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
def import_user(u):
|
||||
user_info = u['u']
|
||||
up_info = u['up']
|
||||
|
||||
# HACK to handle dates
|
||||
user_info['last_login'] = dateutil.parser.parse(user_info['last_login'])
|
||||
user_info['date_joined'] = dateutil.parser.parse(user_info['date_joined'])
|
||||
|
||||
user_keys = ['id', 'username', 'email', 'password', 'is_staff',
|
||||
'is_active', 'is_superuser', 'last_login', 'date_joined',
|
||||
'password']
|
||||
up_keys = ['language', 'location', 'meta', 'name', 'id', 'user_id']
|
||||
|
||||
u = User()
|
||||
for key in user_keys:
|
||||
u.__setattr__(key, user_info[key])
|
||||
u.save()
|
||||
|
||||
up = UserProfile()
|
||||
up.user = u
|
||||
for key in up_keys:
|
||||
up.__setattr__(key, up_info[key])
|
||||
up.save()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Exports all users and user profiles.
|
||||
Caveat: Should be looked over before any run
|
||||
for schema changes.
|
||||
|
||||
Current version grabs user_keys from
|
||||
django.contrib.auth.models.User and up_keys
|
||||
from student.userprofile. '''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
extracted = json.load(open('transfer_users.txt'))
|
||||
n = 0
|
||||
for u in extracted:
|
||||
import_user(u)
|
||||
if n % 100 == 0:
|
||||
print n
|
||||
n = n + 1
|
||||
@@ -17,29 +17,32 @@ import json
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
def group_from_value(groups, v):
|
||||
''' Given group: (('a',0.3),('b',0.4),('c',0.3)) And random value
|
||||
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
|
||||
'''
|
||||
sum = 0
|
||||
for (g,p) in groups:
|
||||
for (g, p) in groups:
|
||||
sum = sum + p
|
||||
if sum > v:
|
||||
return g
|
||||
return g # For round-off errors
|
||||
return g # For round-off errors
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
''' Assign users to test groups. Takes a list
|
||||
of groups:
|
||||
of groups:
|
||||
a:0.3,b:0.4,c:0.3 file.txt "Testing something"
|
||||
Will assign each user to group a, b, or c with
|
||||
probability 0.3, 0.4, 0.3. Probabilities must
|
||||
add up to 1.
|
||||
probability 0.3, 0.4, 0.3. Probabilities must
|
||||
add up to 1.
|
||||
|
||||
Will log what happened to file.txt.
|
||||
'''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) != 3:
|
||||
print "Invalid number of options"
|
||||
@@ -47,13 +50,13 @@ Will log what happened to file.txt.
|
||||
|
||||
# Extract groups from string
|
||||
group_strs = [x.split(':') for x in args[0].split(',')]
|
||||
groups = [(group,float(value)) for group,value in group_strs]
|
||||
groups = [(group, float(value)) for group, value in group_strs]
|
||||
print "Groups", groups
|
||||
|
||||
## Confirm group probabilities add up to 1
|
||||
total = sum(zip(*groups)[1])
|
||||
print "Total:", total
|
||||
if abs(total-1)>0.01:
|
||||
if abs(total - 1) > 0.01:
|
||||
print "Total not 1"
|
||||
sys.exit(-1)
|
||||
|
||||
@@ -65,15 +68,15 @@ Will log what happened to file.txt.
|
||||
|
||||
group_objects = {}
|
||||
|
||||
f = open(args[1],"a+")
|
||||
f = open(args[1], "a+")
|
||||
|
||||
## Create groups
|
||||
for group in dict(groups):
|
||||
utg = UserTestGroup()
|
||||
utg.name=group
|
||||
utg.description = json.dumps({"description":args[2]},
|
||||
{"time":datetime.datetime.utcnow().isoformat()})
|
||||
group_objects[group]=utg
|
||||
utg.name = group
|
||||
utg.description = json.dumps({"description": args[2]},
|
||||
{"time": datetime.datetime.utcnow().isoformat()})
|
||||
group_objects[group] = utg
|
||||
group_objects[group].save()
|
||||
|
||||
## Assign groups
|
||||
@@ -83,11 +86,11 @@ Will log what happened to file.txt.
|
||||
if count % 1000 == 0:
|
||||
print count
|
||||
count = count + 1
|
||||
v = random.uniform(0,1)
|
||||
group = group_from_value(groups,v)
|
||||
v = random.uniform(0, 1)
|
||||
group = group_from_value(groups, v)
|
||||
group_objects[group].users.add(user)
|
||||
f.write("Assigned user {name} ({id}) to {group}\n".format(name=user.username,
|
||||
id=user.id,
|
||||
f.write("Assigned user {name} ({id}) to {group}\n".format(name=user.username,
|
||||
id=user.id,
|
||||
group=group))
|
||||
|
||||
## Save groups
|
||||
|
||||
@@ -10,9 +10,11 @@ import mitxmako.middleware as middleware
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
''' Extract an e-mail list of all active students. '''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
#text = open(args[0]).read()
|
||||
#subject = open(args[1]).read()
|
||||
|
||||
@@ -10,18 +10,20 @@ import mitxmako.middleware as middleware
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Sends an e-mail to all users. Takes a single
|
||||
'''Sends an e-mail to all users. Takes a single
|
||||
parameter -- name of e-mail template -- located
|
||||
in templates/email. Adds a .txt for the message
|
||||
body, and an _subject.txt for the subject. '''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
#text = open(args[0]).read()
|
||||
#subject = open(args[1]).read()
|
||||
users = User.objects.all()
|
||||
text = middleware.lookup['main'].get_template('email/'+args[0]+".txt").render()
|
||||
subject = middleware.lookup['main'].get_template('email/'+args[0]+"_subject.txt").render().strip()
|
||||
text = middleware.lookup['main'].get_template('email/' + args[0] + ".txt").render()
|
||||
subject = middleware.lookup['main'].get_template('email/' + args[0] + "_subject.txt").render().strip()
|
||||
for user in users:
|
||||
if user.is_active:
|
||||
user.email_user(subject, text)
|
||||
|
||||
@@ -16,16 +16,18 @@ import datetime
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
def chunks(l, n):
|
||||
""" Yield successive n-sized chunks from l.
|
||||
"""
|
||||
for i in xrange(0, len(l), n):
|
||||
yield l[i:i+n]
|
||||
yield l[i:i + n]
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Sends an e-mail to all users in a text file.
|
||||
E.g.
|
||||
'''Sends an e-mail to all users in a text file.
|
||||
E.g.
|
||||
manage.py userlist.txt message logfile.txt rate
|
||||
userlist.txt -- list of all users
|
||||
message -- prefix for template with message
|
||||
@@ -35,28 +37,28 @@ rate -- messages per second
|
||||
log_file = None
|
||||
|
||||
def hard_log(self, text):
|
||||
self.log_file.write(datetime.datetime.utcnow().isoformat()+' -- '+text+'\n')
|
||||
self.log_file.write(datetime.datetime.utcnow().isoformat() + ' -- ' + text + '\n')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
global log_file
|
||||
(user_file, message_base, logfilename, ratestr) = args
|
||||
|
||||
|
||||
users = [u.strip() for u in open(user_file).readlines()]
|
||||
|
||||
message = middleware.lookup['main'].get_template('emails/'+message_base+"_body.txt").render()
|
||||
subject = middleware.lookup['main'].get_template('emails/'+message_base+"_subject.txt").render().strip()
|
||||
message = middleware.lookup['main'].get_template('emails/' + message_base + "_body.txt").render()
|
||||
subject = middleware.lookup['main'].get_template('emails/' + message_base + "_subject.txt").render().strip()
|
||||
rate = int(ratestr)
|
||||
|
||||
self.log_file = open(logfilename, "a+", buffering = 0)
|
||||
|
||||
i=0
|
||||
self.log_file = open(logfilename, "a+", buffering=0)
|
||||
|
||||
i = 0
|
||||
for users in chunks(users, rate):
|
||||
emails = [ (subject, message, settings.DEFAULT_FROM_EMAIL, [u]) for u in users ]
|
||||
emails = [(subject, message, settings.DEFAULT_FROM_EMAIL, [u]) for u in users]
|
||||
self.hard_log(" ".join(users))
|
||||
send_mass_mail( emails, fail_silently = False )
|
||||
send_mass_mail(emails, fail_silently=False)
|
||||
time.sleep(1)
|
||||
print datetime.datetime.utcnow().isoformat(), i
|
||||
i = i+len(users)
|
||||
i = i + len(users)
|
||||
# Emergency interruptor
|
||||
if os.path.exists("/tmp/stopemails.txt"):
|
||||
self.log_file.close()
|
||||
|
||||
@@ -13,26 +13,28 @@ from student.models import UserProfile
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
''' Extract full user information into a JSON file.
|
||||
''' Extract full user information into a JSON file.
|
||||
Pass a single filename.'''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
f = open(args[0],'w')
|
||||
f = open(args[0], 'w')
|
||||
#text = open(args[0]).read()
|
||||
#subject = open(args[1]).read()
|
||||
users = User.objects.all()
|
||||
|
||||
l = []
|
||||
for user in users:
|
||||
up = UserProfile.objects.get(user = user)
|
||||
d = { 'username':user.username,
|
||||
'email':user.email,
|
||||
'is_active':user.is_active,
|
||||
'joined':user.date_joined.isoformat(),
|
||||
'name':up.name,
|
||||
'language':up.language,
|
||||
'location':up.location}
|
||||
up = UserProfile.objects.get(user=user)
|
||||
d = {'username': user.username,
|
||||
'email': user.email,
|
||||
'is_active': user.is_active,
|
||||
'joined': user.date_joined.isoformat(),
|
||||
'name': up.name,
|
||||
'language': up.language,
|
||||
'location': up.location}
|
||||
l.append(d)
|
||||
json.dump(l,f)
|
||||
json.dump(l, f)
|
||||
f.close()
|
||||
|
||||
@@ -4,10 +4,11 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Adding model 'UserProfile'
|
||||
db.create_table('auth_userprofile', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
@@ -28,16 +29,14 @@ class Migration(SchemaMigration):
|
||||
))
|
||||
db.send_create_signal('student', ['Registration'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Deleting model 'UserProfile'
|
||||
db.delete_table('auth_userprofile')
|
||||
|
||||
# Deleting model 'Registration'
|
||||
db.delete_table('auth_registration')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,10 +4,11 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Changing field 'UserProfile.name'
|
||||
db.alter_column('auth_userprofile', 'name', self.gf('django.db.models.fields.CharField')(max_length=255))
|
||||
|
||||
@@ -32,9 +33,8 @@ class Migration(SchemaMigration):
|
||||
# Adding index on 'UserProfile', fields ['location']
|
||||
db.create_index('auth_userprofile', ['location'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Removing index on 'UserProfile', fields ['location']
|
||||
db.delete_index('auth_userprofile', ['location'])
|
||||
|
||||
@@ -59,7 +59,6 @@ class Migration(SchemaMigration):
|
||||
# Changing field 'UserProfile.location'
|
||||
db.alter_column('auth_userprofile', 'location', self.gf('django.db.models.fields.TextField')())
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,10 +4,11 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Adding model 'UserTestGroup'
|
||||
db.create_table('student_usertestgroup', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
@@ -24,16 +25,14 @@ class Migration(SchemaMigration):
|
||||
))
|
||||
db.create_unique('student_usertestgroup_users', ['usertestgroup_id', 'user_id'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Deleting model 'UserTestGroup'
|
||||
db.delete_table('student_usertestgroup')
|
||||
|
||||
# Removing M2M table for field users on 'UserTestGroup'
|
||||
db.delete_table('student_usertestgroup_users')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,18 +4,17 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
db.execute("create unique index email on auth_user (email)")
|
||||
pass
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
db.execute("drop index email on auth_user")
|
||||
pass
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,10 +4,11 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Adding model 'PendingEmailChange'
|
||||
db.create_table('student_pendingemailchange', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
@@ -29,9 +30,8 @@ class Migration(SchemaMigration):
|
||||
# Changing field 'UserProfile.user'
|
||||
db.alter_column('auth_userprofile', 'user_id', self.gf('django.db.models.fields.related.OneToOneField')(unique=True, to=orm['auth.User']))
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Deleting model 'PendingEmailChange'
|
||||
db.delete_table('student_pendingemailchange')
|
||||
|
||||
@@ -41,7 +41,6 @@ class Migration(SchemaMigration):
|
||||
# Changing field 'UserProfile.user'
|
||||
db.alter_column('auth_userprofile', 'user_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], unique=True))
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,20 +4,19 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Changing field 'UserProfile.meta'
|
||||
db.alter_column('auth_userprofile', 'meta', self.gf('django.db.models.fields.TextField')())
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Changing field 'UserProfile.meta'
|
||||
db.alter_column('auth_userprofile', 'meta', self.gf('django.db.models.fields.CharField')(max_length=255))
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,6 +4,7 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
@@ -16,12 +17,10 @@ class Migration(SchemaMigration):
|
||||
ALTER TABLE student_usertestgroup_users CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
""")
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Although this migration can't be undone, it is okay for it to be run backwards because it doesn't add/remove any fields
|
||||
pass
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -16,12 +16,10 @@ class Migration(SchemaMigration):
|
||||
))
|
||||
db.send_create_signal('student', ['CourseRegistration'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'CourseRegistration'
|
||||
db.delete_table('student_courseregistration')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
@@ -129,4 +127,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -19,7 +19,6 @@ class Migration(SchemaMigration):
|
||||
))
|
||||
db.send_create_signal('student', ['CourseEnrollment'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Adding model 'CourseRegistration'
|
||||
db.create_table('student_courseregistration', (
|
||||
@@ -32,7 +31,6 @@ class Migration(SchemaMigration):
|
||||
# Deleting model 'CourseEnrollment'
|
||||
db.delete_table('student_courseenrollment')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
@@ -140,4 +138,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -124,4 +124,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -8,20 +8,22 @@ from django.db import models
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Removing unique constraint on 'CourseEnrollment', fields ['user']
|
||||
db.delete_unique('student_courseenrollment', ['user_id'])
|
||||
|
||||
|
||||
# Changing field 'CourseEnrollment.user'
|
||||
db.alter_column('student_courseenrollment', 'user_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User']))
|
||||
# This table is dropped in a subsequent migration. This migration was causing problems when using InnoDB,
|
||||
# so we are just dropping it.
|
||||
pass
|
||||
# # Removing unique constraint on 'CourseEnrollment', fields ['user']
|
||||
# db.delete_unique('student_courseenrollment', ['user_id'])
|
||||
#
|
||||
#
|
||||
# # Changing field 'CourseEnrollment.user'
|
||||
# db.alter_column('student_courseenrollment', 'user_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User']))
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
# Changing field 'CourseEnrollment.user'
|
||||
db.alter_column('student_courseenrollment', 'user_id', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['auth.User'], unique=True))
|
||||
# Adding unique constraint on 'CourseEnrollment', fields ['user']
|
||||
db.create_unique('student_courseenrollment', ['user_id'])
|
||||
|
||||
pass
|
||||
# # Changing field 'CourseEnrollment.user'
|
||||
# db.alter_column('student_courseenrollment', 'user_id', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['auth.User'], unique=True))
|
||||
# # Adding unique constraint on 'CourseEnrollment', fields ['user']
|
||||
# db.create_unique('student_courseenrollment', ['user_id'])
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
@@ -130,4 +132,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -38,7 +38,6 @@ class Migration(SchemaMigration):
|
||||
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'UserProfile.gender'
|
||||
db.delete_column('auth_userprofile', 'gender')
|
||||
@@ -58,7 +57,6 @@ class Migration(SchemaMigration):
|
||||
# Deleting field 'UserProfile.occupation'
|
||||
db.delete_column('auth_userprofile', 'occupation')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
@@ -172,4 +170,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
# Changing field 'UserProfile.meta'
|
||||
db.alter_column('auth_userprofile', 'meta', self.gf('django.db.models.fields.TextField')())
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
# Changing field 'UserProfile.meta'
|
||||
db.alter_column('auth_userprofile', 'meta', self.gf('django.db.models.fields.CharField')(max_length=255))
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
|
||||
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
|
||||
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
|
||||
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
|
||||
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
|
||||
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'student.courseenrollment': {
|
||||
'Meta': {'object_name': 'CourseEnrollment'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.pendingemailchange': {
|
||||
'Meta': {'object_name': 'PendingEmailChange'},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.pendingnamechange': {
|
||||
'Meta': {'object_name': 'PendingNameChange'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.registration': {
|
||||
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.userprofile': {
|
||||
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
|
||||
'country': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
|
||||
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'gender': ('django.db.models.fields.CharField', [], {'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'occupation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'telephone_number': ('django.db.models.fields.CharField', [], {'max_length': '25', 'null': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.usertestgroup': {
|
||||
'Meta': {'object_name': 'UserTestGroup'},
|
||||
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
@@ -0,0 +1,130 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Deleting model 'CourseEnrollment'
|
||||
db.delete_table('student_courseenrollment')
|
||||
|
||||
def backwards(self, orm):
|
||||
# Adding model 'CourseEnrollment'
|
||||
db.create_table('student_courseenrollment', (
|
||||
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255)),
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
|
||||
))
|
||||
db.send_create_signal('student', ['CourseEnrollment'])
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
|
||||
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
|
||||
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
|
||||
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
|
||||
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
|
||||
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'student.pendingemailchange': {
|
||||
'Meta': {'object_name': 'PendingEmailChange'},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.pendingnamechange': {
|
||||
'Meta': {'object_name': 'PendingNameChange'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.registration': {
|
||||
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.userprofile': {
|
||||
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
|
||||
'country': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
|
||||
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'gender': ('django.db.models.fields.CharField', [], {'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'occupation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'telephone_number': ('django.db.models.fields.CharField', [], {'max_length': '25', 'null': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.usertestgroup': {
|
||||
'Meta': {'object_name': 'UserTestGroup'},
|
||||
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
@@ -0,0 +1,142 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding model 'CourseEnrollment'
|
||||
db.create_table('student_courseenrollment', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
|
||||
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
|
||||
))
|
||||
db.send_create_signal('student', ['CourseEnrollment'])
|
||||
|
||||
# Adding unique constraint on 'CourseEnrollment', fields ['user', 'course_id']
|
||||
db.create_unique('student_courseenrollment', ['user_id', 'course_id'])
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing unique constraint on 'CourseEnrollment', fields ['user', 'course_id']
|
||||
db.delete_unique('student_courseenrollment', ['user_id', 'course_id'])
|
||||
|
||||
# Deleting model 'CourseEnrollment'
|
||||
db.delete_table('student_courseenrollment')
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
|
||||
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
|
||||
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
|
||||
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
|
||||
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
|
||||
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'student.courseenrollment': {
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.pendingemailchange': {
|
||||
'Meta': {'object_name': 'PendingEmailChange'},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.pendingnamechange': {
|
||||
'Meta': {'object_name': 'PendingNameChange'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.registration': {
|
||||
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.userprofile': {
|
||||
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
|
||||
'country': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
|
||||
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'gender': ('django.db.models.fields.CharField', [], {'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'occupation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'telephone_number': ('django.db.models.fields.CharField', [], {'max_length': '25', 'null': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.usertestgroup': {
|
||||
'Meta': {'object_name': 'UserTestGroup'},
|
||||
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
@@ -0,0 +1,140 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding field 'CourseEnrollment.date'
|
||||
db.add_column('student_courseenrollment', 'date',
|
||||
self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Changing field 'UserProfile.country'
|
||||
db.alter_column('auth_userprofile', 'country', self.gf('django_countries.fields.CountryField')(max_length=2, null=True))
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'CourseEnrollment.date'
|
||||
db.delete_column('student_courseenrollment', 'date')
|
||||
|
||||
# Changing field 'UserProfile.country'
|
||||
db.alter_column('auth_userprofile', 'country', self.gf('django.db.models.fields.CharField')(max_length=255, null=True))
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
|
||||
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
|
||||
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
|
||||
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
|
||||
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
|
||||
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'student.courseenrollment': {
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.pendingemailchange': {
|
||||
'Meta': {'object_name': 'PendingEmailChange'},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.pendingnamechange': {
|
||||
'Meta': {'object_name': 'PendingNameChange'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.registration': {
|
||||
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.userprofile': {
|
||||
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
|
||||
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'null': 'True', 'blank': 'True'}),
|
||||
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
|
||||
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'gender': ('django.db.models.fields.CharField', [], {'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'occupation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'telephone_number': ('django.db.models.fields.CharField', [], {'max_length': '25', 'null': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.usertestgroup': {
|
||||
'Meta': {'object_name': 'UserTestGroup'},
|
||||
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
@@ -0,0 +1,132 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Rename 'date' field to 'created'
|
||||
db.rename_column('student_courseenrollment', 'date', 'created')
|
||||
|
||||
def backwards(self, orm):
|
||||
# Rename 'created' field to 'date'
|
||||
db.rename_column('student_courseenrollment', 'created', 'date')
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
|
||||
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
|
||||
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
|
||||
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
|
||||
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
|
||||
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'student.courseenrollment': {
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.pendingemailchange': {
|
||||
'Meta': {'object_name': 'PendingEmailChange'},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.pendingnamechange': {
|
||||
'Meta': {'object_name': 'PendingNameChange'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.registration': {
|
||||
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.userprofile': {
|
||||
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
|
||||
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'null': 'True', 'blank': 'True'}),
|
||||
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
|
||||
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'gender': ('django.db.models.fields.CharField', [], {'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'occupation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'telephone_number': ('django.db.models.fields.CharField', [], {'max_length': '25', 'null': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.usertestgroup': {
|
||||
'Meta': {'object_name': 'UserTestGroup'},
|
||||
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
132
common/djangoapps/student/migrations/0018_auto.py
Normal file
132
common/djangoapps/student/migrations/0018_auto.py
Normal file
@@ -0,0 +1,132 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding index on 'CourseEnrollment', fields ['created']
|
||||
db.create_index('student_courseenrollment', ['created'])
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing index on 'CourseEnrollment', fields ['created']
|
||||
db.delete_index('student_courseenrollment', ['created'])
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
|
||||
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
|
||||
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
|
||||
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
|
||||
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
|
||||
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'student.courseenrollment': {
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.pendingemailchange': {
|
||||
'Meta': {'object_name': 'PendingEmailChange'},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.pendingnamechange': {
|
||||
'Meta': {'object_name': 'PendingNameChange'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.registration': {
|
||||
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.userprofile': {
|
||||
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
|
||||
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'null': 'True', 'blank': 'True'}),
|
||||
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
|
||||
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'gender': ('django.db.models.fields.CharField', [], {'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'occupation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'telephone_number': ('django.db.models.fields.CharField', [], {'max_length': '25', 'null': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.usertestgroup': {
|
||||
'Meta': {'object_name': 'UserTestGroup'},
|
||||
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
@@ -0,0 +1,187 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Deleting field 'UserProfile.occupation'
|
||||
db.delete_column('auth_userprofile', 'occupation')
|
||||
|
||||
# Deleting field 'UserProfile.telephone_number'
|
||||
db.delete_column('auth_userprofile', 'telephone_number')
|
||||
|
||||
# Deleting field 'UserProfile.date_of_birth'
|
||||
db.delete_column('auth_userprofile', 'date_of_birth')
|
||||
|
||||
# Deleting field 'UserProfile.country'
|
||||
db.delete_column('auth_userprofile', 'country')
|
||||
|
||||
# Adding field 'UserProfile.year_of_birth'
|
||||
db.add_column('auth_userprofile', 'year_of_birth',
|
||||
self.gf('django.db.models.fields.IntegerField')(db_index=True, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'UserProfile.level_of_education'
|
||||
db.add_column('auth_userprofile', 'level_of_education',
|
||||
self.gf('django.db.models.fields.CharField')(db_index=True, max_length=6, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'UserProfile.goals'
|
||||
db.add_column('auth_userprofile', 'goals',
|
||||
self.gf('django.db.models.fields.TextField')(null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding index on 'UserProfile', fields ['gender']
|
||||
db.create_index('auth_userprofile', ['gender'])
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing index on 'UserProfile', fields ['gender']
|
||||
db.delete_index('auth_userprofile', ['gender'])
|
||||
|
||||
# Adding field 'UserProfile.occupation'
|
||||
db.add_column('auth_userprofile', 'occupation',
|
||||
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'UserProfile.telephone_number'
|
||||
db.add_column('auth_userprofile', 'telephone_number',
|
||||
self.gf('django.db.models.fields.CharField')(max_length=25, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'UserProfile.date_of_birth'
|
||||
db.add_column('auth_userprofile', 'date_of_birth',
|
||||
self.gf('django.db.models.fields.DateField')(null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'UserProfile.country'
|
||||
db.add_column('auth_userprofile', 'country',
|
||||
self.gf('django_countries.fields.CountryField')(max_length=2, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Deleting field 'UserProfile.year_of_birth'
|
||||
db.delete_column('auth_userprofile', 'year_of_birth')
|
||||
|
||||
# Deleting field 'UserProfile.level_of_education'
|
||||
db.delete_column('auth_userprofile', 'level_of_education')
|
||||
|
||||
# Deleting field 'UserProfile.goals'
|
||||
db.delete_column('auth_userprofile', 'goals')
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
|
||||
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
|
||||
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
|
||||
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
|
||||
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
|
||||
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'student.courseenrollment': {
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.pendingemailchange': {
|
||||
'Meta': {'object_name': 'PendingEmailChange'},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.pendingnamechange': {
|
||||
'Meta': {'object_name': 'PendingNameChange'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.registration': {
|
||||
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.userprofile': {
|
||||
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
|
||||
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
|
||||
'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||
'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}),
|
||||
'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'student.usertestgroup': {
|
||||
'Meta': {'object_name': 'UserTestGroup'},
|
||||
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
@@ -5,38 +5,60 @@ 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,
|
||||
|
||||
1. Go to the mitx dir
|
||||
2. ./manage.py schemamigration user --auto description_of_your_change
|
||||
3. Add the migration file created in mitx/courseware/migrations/
|
||||
2. django-admin.py schemamigration student --auto --settings=lms.envs.dev --pythonpath=. description_of_your_change
|
||||
3. Add the migration file created in mitx/common/djangoapps/student/migrations/
|
||||
"""
|
||||
from datetime import datetime
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
import json
|
||||
from django_countries import CountryField
|
||||
|
||||
#from cache_toolbox import cache_model, cache_relation
|
||||
|
||||
|
||||
class UserProfile(models.Model):
|
||||
class Meta:
|
||||
db_table = "auth_userprofile"
|
||||
|
||||
GENDER_CHOICES = (('male', 'Male'), ('female', 'Female'), ('other', 'Other'))
|
||||
|
||||
## CRITICAL TODO/SECURITY
|
||||
# Sanitize all fields.
|
||||
# This is not visible to other users, but could introduce holes later
|
||||
user = models.OneToOneField(User, unique=True, db_index=True, related_name='profile')
|
||||
name = models.CharField(blank=True, max_length=255, db_index=True)
|
||||
language = models.CharField(blank=True, max_length=255, db_index=True)
|
||||
location = models.CharField(blank=True, max_length=255, db_index=True) # TODO: What are we doing with this?
|
||||
meta = models.TextField(blank=True) # JSON dictionary for future expansion
|
||||
|
||||
meta = models.TextField(blank=True) # JSON dictionary for future expansion
|
||||
courseware = models.CharField(blank=True, max_length=255, default='course.xml')
|
||||
gender = models.CharField(blank=True, null=True, max_length=6, choices=GENDER_CHOICES)
|
||||
date_of_birth = models.DateField(blank=True, null=True)
|
||||
|
||||
# Location is no longer used, but is held here for backwards compatibility
|
||||
# for users imported from our first class.
|
||||
language = models.CharField(blank=True, max_length=255, db_index=True)
|
||||
location = models.CharField(blank=True, max_length=255, db_index=True)
|
||||
|
||||
# Optional demographic data we started capturing from Fall 2012
|
||||
this_year = datetime.now().year
|
||||
VALID_YEARS = range(this_year, this_year - 120, -1)
|
||||
year_of_birth = models.IntegerField(blank=True, null=True, db_index=True)
|
||||
GENDER_CHOICES = (('m', 'Male'), ('f', 'Female'), ('o', 'Other'))
|
||||
gender = models.CharField(blank=True, null=True, max_length=6, db_index=True,
|
||||
choices=GENDER_CHOICES)
|
||||
LEVEL_OF_EDUCATION_CHOICES = (('p_se', 'Doctorate in science or engineering'),
|
||||
('p_oth', 'Doctorate in another field'),
|
||||
('m', "Master's or professional degree"),
|
||||
('b', "Bachelor's degree"),
|
||||
('hs', "Secondary/high school"),
|
||||
('jhs', "Junior secondary/junior high/middle school"),
|
||||
('el', "Elementary/primary school"),
|
||||
('none', "None"),
|
||||
('other', "Other"))
|
||||
level_of_education = models.CharField(
|
||||
blank=True, null=True, max_length=6, db_index=True,
|
||||
choices=LEVEL_OF_EDUCATION_CHOICES
|
||||
)
|
||||
mailing_address = models.TextField(blank=True, null=True)
|
||||
country = models.CharField(blank=True, null=True, max_length=255)
|
||||
telephone_number = models.CharField(blank=True, null=True, max_length=25)
|
||||
occupation = models.CharField(blank=True, null=True, max_length=255)
|
||||
goals = models.TextField(blank=True, null=True)
|
||||
|
||||
def get_meta(self):
|
||||
js_str = self.meta
|
||||
@@ -47,9 +69,10 @@ class UserProfile(models.Model):
|
||||
|
||||
return js_str
|
||||
|
||||
def set_meta(self,js):
|
||||
def set_meta(self, js):
|
||||
self.meta = json.dumps(js)
|
||||
|
||||
|
||||
## TODO: Should be renamed to generic UserGroup, and possibly
|
||||
# Given an optional field for type of group
|
||||
class UserTestGroup(models.Model):
|
||||
@@ -57,6 +80,7 @@ class UserTestGroup(models.Model):
|
||||
name = models.CharField(blank=False, max_length=32, db_index=True)
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
|
||||
class Registration(models.Model):
|
||||
''' Allows us to wait for e-mail before user is registered. A
|
||||
registration profile is created when the user creates an
|
||||
@@ -70,8 +94,8 @@ class Registration(models.Model):
|
||||
|
||||
def register(self, user):
|
||||
# MINOR TODO: Switch to crypto-secure key
|
||||
self.activation_key=uuid.uuid4().hex
|
||||
self.user=user
|
||||
self.activation_key = uuid.uuid4().hex
|
||||
self.user = user
|
||||
self.save()
|
||||
|
||||
def activate(self):
|
||||
@@ -79,56 +103,71 @@ class Registration(models.Model):
|
||||
self.user.save()
|
||||
#self.delete()
|
||||
|
||||
|
||||
class PendingNameChange(models.Model):
|
||||
user = models.OneToOneField(User, unique=True, db_index=True)
|
||||
new_name = models.CharField(blank=True, max_length=255)
|
||||
rationale = models.CharField(blank=True, max_length=1024)
|
||||
|
||||
|
||||
class PendingEmailChange(models.Model):
|
||||
user = models.OneToOneField(User, unique=True, db_index=True)
|
||||
new_email = models.CharField(blank=True, max_length=255, db_index=True)
|
||||
activation_key = models.CharField(('activation key'), max_length=32, unique=True, db_index=True)
|
||||
|
||||
|
||||
class CourseEnrollment(models.Model):
|
||||
user = models.ForeignKey(User, db_index=True)
|
||||
course_id = models.CharField(max_length=255)
|
||||
user = models.ForeignKey(User)
|
||||
course_id = models.CharField(max_length=255, db_index=True)
|
||||
|
||||
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = (('user', 'course_id'), )
|
||||
|
||||
#cache_relation(User.profile)
|
||||
|
||||
#### Helper methods for use from python manage.py shell.
|
||||
|
||||
|
||||
def get_user(email):
|
||||
u = User.objects.get(email = email)
|
||||
up = UserProfile.objects.get(user = u)
|
||||
return u,up
|
||||
u = User.objects.get(email=email)
|
||||
up = UserProfile.objects.get(user=u)
|
||||
return u, up
|
||||
|
||||
|
||||
def user_info(email):
|
||||
u,up = get_user(email)
|
||||
u, up = get_user(email)
|
||||
print "User id", u.id
|
||||
print "Username", u.username
|
||||
print "E-mail", u.email
|
||||
print "Name", up.name
|
||||
print "Location", up.location
|
||||
print "Language", up.language
|
||||
return u,up
|
||||
return u, up
|
||||
|
||||
|
||||
def change_email(old_email, new_email):
|
||||
u = User.objects.get(email = old_email)
|
||||
u = User.objects.get(email=old_email)
|
||||
u.email = new_email
|
||||
u.save()
|
||||
|
||||
|
||||
def change_name(email, new_name):
|
||||
u,up = get_user(email)
|
||||
u, up = get_user(email)
|
||||
up.name = new_name
|
||||
up.save()
|
||||
|
||||
|
||||
def user_count():
|
||||
print "All users", User.objects.all().count()
|
||||
print "Active users", User.objects.filter(is_active = True).count()
|
||||
print "Active users", User.objects.filter(is_active=True).count()
|
||||
return User.objects.all().count()
|
||||
|
||||
|
||||
def active_user_count():
|
||||
return User.objects.filter(is_active = True).count()
|
||||
return User.objects.filter(is_active=True).count()
|
||||
|
||||
|
||||
def create_group(name, description):
|
||||
utg = UserTestGroup()
|
||||
@@ -136,29 +175,31 @@ def create_group(name, description):
|
||||
utg.description = description
|
||||
utg.save()
|
||||
|
||||
|
||||
def add_user_to_group(user, group):
|
||||
utg = UserTestGroup.objects.get(name = group)
|
||||
utg.users.add(User.objects.get(username = user))
|
||||
utg = UserTestGroup.objects.get(name=group)
|
||||
utg.users.add(User.objects.get(username=user))
|
||||
utg.save()
|
||||
|
||||
|
||||
def remove_user_from_group(user, group):
|
||||
utg = UserTestGroup.objects.get(name = group)
|
||||
utg.users.remove(User.objects.get(username = user))
|
||||
utg = UserTestGroup.objects.get(name=group)
|
||||
utg.users.remove(User.objects.get(username=user))
|
||||
utg.save()
|
||||
|
||||
default_groups = {'email_future_courses' : 'Receive e-mails about future MITx courses',
|
||||
'email_helpers' : 'Receive e-mails about how to help with MITx',
|
||||
'mitx_unenroll' : 'Fully unenrolled -- no further communications',
|
||||
'6002x_unenroll' : 'Took and dropped 6002x'}
|
||||
default_groups = {'email_future_courses': 'Receive e-mails about future MITx courses',
|
||||
'email_helpers': 'Receive e-mails about how to help with MITx',
|
||||
'mitx_unenroll': 'Fully unenrolled -- no further communications',
|
||||
'6002x_unenroll': 'Took and dropped 6002x'}
|
||||
|
||||
|
||||
def add_user_to_default_group(user, group):
|
||||
try:
|
||||
utg = UserTestGroup.objects.get(name = group)
|
||||
utg = UserTestGroup.objects.get(name=group)
|
||||
except UserTestGroup.DoesNotExist:
|
||||
utg = UserTestGroup()
|
||||
utg.name = group
|
||||
utg.description = default_groups[group]
|
||||
utg.save()
|
||||
utg.users.add(User.objects.get(username = user))
|
||||
utg.users.add(User.objects.get(username=user))
|
||||
utg.save()
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@ import random
|
||||
import string
|
||||
import sys
|
||||
import uuid
|
||||
import feedparser
|
||||
import urllib
|
||||
import itertools
|
||||
from collections import defaultdict
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import logout, authenticate, login
|
||||
@@ -19,17 +23,22 @@ from django.http import HttpResponse, Http404
|
||||
from django.shortcuts import redirect
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from django.core.urlresolvers import reverse
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
from django.core.cache import cache
|
||||
|
||||
from courseware.courses import check_course
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from student.models import Registration, UserProfile, PendingNameChange, PendingEmailChange, CourseEnrollment
|
||||
from util.cache import cache_if_anonymous
|
||||
from util.cache import cache_if_anonymous
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
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
|
||||
|
||||
log = logging.getLogger("mitx.student")
|
||||
|
||||
Article = namedtuple('Article', 'title url author image deck publication publish_date')
|
||||
|
||||
def csrf_token(context):
|
||||
''' A csrf token that can be included in a form.
|
||||
@@ -43,39 +52,146 @@ def csrf_token(context):
|
||||
@ensure_csrf_cookie
|
||||
@cache_if_anonymous
|
||||
def index(request):
|
||||
|
||||
''' Redirects to main page -- info page if user authenticated, or marketing if not
|
||||
'''
|
||||
|
||||
if settings.COURSEWARE_ENABLED and request.user.is_authenticated():
|
||||
return redirect(reverse('dashboard'))
|
||||
else:
|
||||
# TODO: Clean up how 'error' is done.
|
||||
return render_to_response('index.html', {'courses': modulestore().get_courses()})
|
||||
|
||||
feed_data = cache.get("students_index_rss_feed_data")
|
||||
if feed_data == None:
|
||||
if hasattr(settings, 'RSS_URL'):
|
||||
feed_data = urllib.urlopen(settings.RSS_URL).read()
|
||||
else:
|
||||
feed_data = render_to_string("feed.rss", None)
|
||||
cache.set("students_index_rss_feed_data", feed_data, settings.RSS_TIMEOUT)
|
||||
|
||||
feed = feedparser.parse(feed_data)
|
||||
entries = feed['entries'][0:3]
|
||||
for entry in entries:
|
||||
soup = BeautifulSoup(entry.description)
|
||||
entry.image = soup.img['src'] if soup.img else None
|
||||
entry.summary = soup.getText()
|
||||
|
||||
universities = defaultdict(list)
|
||||
courses = sorted(modulestore().get_courses(), key=lambda course: course.number)
|
||||
for course in courses:
|
||||
universities[course.org].append(course)
|
||||
|
||||
return render_to_response('index.html', {'universities': universities, 'entries': entries})
|
||||
|
||||
|
||||
def course_from_id(id):
|
||||
course_loc = CourseDescriptor.id_to_location(id)
|
||||
return modulestore().get_item(course_loc)
|
||||
|
||||
|
||||
def press(request):
|
||||
json_articles = cache.get("student_press_json_articles")
|
||||
if json_articles == None:
|
||||
if hasattr(settings, 'RSS_URL'):
|
||||
content = urllib.urlopen(settings.PRESS_URL).read()
|
||||
json_articles = json.loads(content)
|
||||
else:
|
||||
content = open(settings.PROJECT_ROOT / "templates" / "press.json").read()
|
||||
json_articles = json.loads(content)
|
||||
cache.set("student_press_json_articles", json_articles)
|
||||
articles = [Article(**article) for article in json_articles]
|
||||
return render_to_response('static_templates/press.html', {'articles': articles})
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def dashboard(request):
|
||||
csrf_token = csrf(request)['csrf_token']
|
||||
user = request.user
|
||||
enrollments = CourseEnrollment.objects.filter(user=user)
|
||||
|
||||
def course_from_id(id):
|
||||
course_loc = CourseDescriptor.id_to_location(id)
|
||||
return modulestore().get_item(course_loc)
|
||||
# Build our courses list for the user, but ignore any courses that no longer
|
||||
# exist (because the course IDs have changed). Still, we don't delete those
|
||||
# enrollments, because it could have been a data push snafu.
|
||||
courses = []
|
||||
for enrollment in enrollments:
|
||||
try:
|
||||
courses.append(course_from_id(enrollment.course_id))
|
||||
except ItemNotFoundError:
|
||||
log.error("User {0} enrolled in non-existant course {1}"
|
||||
.format(user.username, enrollment.course_id))
|
||||
|
||||
courses = [course_from_id(enrollment.course_id) for enrollment in enrollments]
|
||||
message = ""
|
||||
if not user.is_active:
|
||||
message = render_to_string('registration/activate_account_notice.html', {'email': user.email})
|
||||
|
||||
context = {'csrf': csrf_token, 'courses': courses}
|
||||
context = {'courses': courses, 'message': message}
|
||||
return render_to_response('dashboard.html', context)
|
||||
|
||||
|
||||
def try_change_enrollment(request):
|
||||
"""
|
||||
This method calls change_enrollment if the necessary POST
|
||||
parameters are present, but does not return anything. It
|
||||
simply logs the result or exception. This is usually
|
||||
called after a registration or login, as secondary action.
|
||||
It should not interrupt a successful registration or login.
|
||||
"""
|
||||
if 'enrollment_action' in request.POST:
|
||||
try:
|
||||
enrollment_output = change_enrollment(request)
|
||||
# There isn't really a way to display the results to the user, so we just log it
|
||||
# We expect the enrollment to be a success, and will show up on the dashboard anyway
|
||||
log.info("Attempted to automatically enroll after login. Results: {0}".format(enrollment_output))
|
||||
except Exception, e:
|
||||
log.exception("Exception automatically enrolling after login: {0}".format(str(e)))
|
||||
|
||||
|
||||
@login_required
|
||||
def change_enrollment_view(request):
|
||||
return HttpResponse(json.dumps(change_enrollment(request)))
|
||||
|
||||
|
||||
def change_enrollment(request):
|
||||
if request.method != "POST":
|
||||
raise Http404
|
||||
|
||||
action = request.POST.get("enrollment_action", "")
|
||||
user = request.user
|
||||
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.'}))
|
||||
|
||||
if action == "enroll":
|
||||
# Make sure the course exists
|
||||
# We don't do this check on unenroll, or a bad course id can't be unenrolled from
|
||||
try:
|
||||
course = course_from_id(course_id)
|
||||
except ItemNotFoundError:
|
||||
log.error("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.'}
|
||||
|
||||
enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id)
|
||||
return {'success': True}
|
||||
|
||||
elif action == "unenroll":
|
||||
try:
|
||||
enrollment = CourseEnrollment.objects.get(user=user, course_id=course_id)
|
||||
enrollment.delete()
|
||||
return {'success': True}
|
||||
except CourseEnrollment.DoesNotExist:
|
||||
return {'success': False, 'error': 'You are not enrolled for this course.'}
|
||||
else:
|
||||
return {'success': False, 'error': 'Invalid enrollment_action.'}
|
||||
|
||||
return {'success': False, 'error': 'We weren\'t able to unenroll you. Please try again.'}
|
||||
|
||||
|
||||
# Need different levels of logging
|
||||
@ensure_csrf_cookie
|
||||
def login_user(request, error=""):
|
||||
''' AJAX request to log in the user. '''
|
||||
if 'email' not in request.POST or 'password' not in request.POST:
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Invalid login'})) # TODO: User error message
|
||||
'value': 'There was an error receiving your login information. Please email us.'})) # TODO: User error message
|
||||
|
||||
email = request.POST['email']
|
||||
password = request.POST['password']
|
||||
@@ -84,20 +200,20 @@ def login_user(request, error=""):
|
||||
except User.DoesNotExist:
|
||||
log.warning("Login failed - Unknown user email: {0}".format(email))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Invalid login'})) # TODO: User error message
|
||||
'value': 'Email or password is incorrect.'})) # TODO: User error message
|
||||
|
||||
username = user.username
|
||||
user = authenticate(username=username, password=password)
|
||||
if user is None:
|
||||
log.warning("Login failed - password for {0} is invalid".format(email))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Invalid login'}))
|
||||
'value': 'Email or password is incorrect.'}))
|
||||
|
||||
if user is not None and user.is_active:
|
||||
try:
|
||||
login(request, user)
|
||||
if request.POST.get('remember') == 'true':
|
||||
request.session.set_expiry(None) # or change to 604800 for 7 days
|
||||
request.session.set_expiry(None) # or change to 604800 for 7 days
|
||||
log.debug("Setting user session to never expire")
|
||||
else:
|
||||
request.session.set_expiry(0)
|
||||
@@ -106,11 +222,15 @@ def login_user(request, error=""):
|
||||
log.exception(e)
|
||||
|
||||
log.info("Login success - {0} ({1})".format(username, email))
|
||||
return HttpResponse(json.dumps({'success':True}))
|
||||
|
||||
try_change_enrollment(request)
|
||||
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
log.warning("Login failed - Account not active for user {0}".format(username))
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
'error': 'Account not active. Check your e-mail.'}))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'value': 'This account has not been activated. Please check your e-mail for the activation instructions.'}))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def logout_user(request):
|
||||
@@ -118,42 +238,40 @@ def logout_user(request):
|
||||
logout(request)
|
||||
return redirect('/')
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def change_setting(request):
|
||||
''' JSON call to change a profile setting: Right now, location and language
|
||||
''' JSON call to change a profile setting: Right now, location
|
||||
'''
|
||||
up = UserProfile.objects.get(user=request.user) #request.user.profile_cache
|
||||
up = UserProfile.objects.get(user=request.user) # request.user.profile_cache
|
||||
if 'location' in request.POST:
|
||||
up.location=request.POST['location']
|
||||
if 'language' in request.POST:
|
||||
up.language=request.POST['language']
|
||||
up.location = request.POST['location']
|
||||
up.save()
|
||||
|
||||
return HttpResponse(json.dumps({'success':True,
|
||||
'language':up.language,
|
||||
'location':up.location,}))
|
||||
return HttpResponse(json.dumps({'success': True,
|
||||
'location': up.location, }))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def create_account(request, post_override=None):
|
||||
''' JSON call to enroll in the course. '''
|
||||
js={'success':False}
|
||||
js = {'success': False}
|
||||
|
||||
post_vars = post_override if post_override else request.POST
|
||||
|
||||
# Confirm we have a properly formed request
|
||||
for a in ['username', 'email', 'password', 'location', 'language', 'name']:
|
||||
for a in ['username', 'email', 'password', 'name']:
|
||||
if a not in post_vars:
|
||||
js['value'] = "Error (401 {field}). E-mail us.".format(field=a)
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
if post_vars.get('honor_code', 'false') != u'true':
|
||||
js['value']="To enroll, you must follow the honor code.".format(field=a)
|
||||
js['value'] = "To enroll, you must follow the honor code.".format(field=a)
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
|
||||
if post_vars.get('terms_of_service', 'false') != u'true':
|
||||
js['value']="You must accept the terms of service.".format(field=a)
|
||||
js['value'] = "You must accept the terms of service.".format(field=a)
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
# Confirm appropriate fields are there.
|
||||
@@ -163,25 +281,25 @@ def create_account(request, post_override=None):
|
||||
# TODO: Check password is sane
|
||||
for a in ['username', 'email', 'name', 'password', 'terms_of_service', 'honor_code']:
|
||||
if len(post_vars[a]) < 2:
|
||||
error_str = {'username' : 'Username of length 2 or greater',
|
||||
'email' : 'Properly formatted e-mail',
|
||||
'name' : 'Your legal name ',
|
||||
'password': 'Valid password ',
|
||||
'terms_of_service': 'Accepting Terms of Service',
|
||||
'honor_code': 'Agreeing to the Honor Code'}
|
||||
js['value']="{field} is required.".format(field=error_str[a])
|
||||
error_str = {'username': 'Username must be minimum of two characters long.',
|
||||
'email': 'A properly formatted e-mail is required.',
|
||||
'name': 'Your legal name must be a minimum of two characters long.',
|
||||
'password': 'A valid password is required.',
|
||||
'terms_of_service': 'Accepting Terms of Service is required.',
|
||||
'honor_code': 'Agreeing to the Honor Code is required.'}
|
||||
js['value'] = error_str[a]
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
try:
|
||||
validate_email(post_vars['email'])
|
||||
except ValidationError:
|
||||
js['value']="Valid e-mail is required.".format(field=a)
|
||||
js['value'] = "Valid e-mail is required.".format(field=a)
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
try:
|
||||
validate_slug(post_vars['username'])
|
||||
except ValidationError:
|
||||
js['value']="Username should only consist of A-Z and 0-9.".format(field=a)
|
||||
js['value'] = "Username should only consist of A-Z and 0-9.".format(field=a)
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
u = User(username=post_vars['username'],
|
||||
@@ -209,14 +327,23 @@ def create_account(request, post_override=None):
|
||||
|
||||
up = UserProfile(user=u)
|
||||
up.name = post_vars['name']
|
||||
up.language = post_vars['language']
|
||||
up.location = post_vars['location']
|
||||
up.save()
|
||||
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))
|
||||
|
||||
# TODO (vshnayder): the LMS should probably allow signups without a particular course too
|
||||
d = {'name': post_vars['name'],
|
||||
'key': r.activation_key,
|
||||
'course_title': getattr(settings, 'COURSE_TITLE', ''),
|
||||
}
|
||||
|
||||
subject = render_to_string('emails/activation_email_subject.txt', d)
|
||||
@@ -227,7 +354,7 @@ 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" % (u, u.email, up.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)
|
||||
@@ -236,49 +363,59 @@ def create_account(request, post_override=None):
|
||||
js['value'] = 'Could not send activation e-mail.'
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
js={'success': True,
|
||||
'value': render_to_string('registration/reg_complete.html', {'email': post_vars['email'],
|
||||
'csrf': csrf(request)['csrf_token']})}
|
||||
# Immediately after a user creates an account, we log them in. They are only
|
||||
# logged in until they close the browser. They can't log in again until they click
|
||||
# the activation link from the email.
|
||||
login_user = authenticate(username=post_vars['username'], password=post_vars['password'])
|
||||
login(request, login_user)
|
||||
request.session.set_expiry(0)
|
||||
|
||||
try_change_enrollment(request)
|
||||
|
||||
js = {'success': True}
|
||||
return HttpResponse(json.dumps(js), mimetype="application/json")
|
||||
|
||||
|
||||
def create_random_account(create_account_function):
|
||||
|
||||
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),
|
||||
'language' : id_generator(size=5, chars=string.ascii_uppercase) + "ish",
|
||||
'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',}
|
||||
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 create_account_function(request, post_override = post_override)
|
||||
return create_account_function(request, post_override=post_override)
|
||||
|
||||
return inner_create_random_account
|
||||
|
||||
if settings.GENERATE_RANDOM_USER_CREDENTIALS:
|
||||
create_account = create_random_account(create_account)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def activate_account(request, key):
|
||||
''' When link in activation e-mail is clicked
|
||||
'''
|
||||
r=Registration.objects.filter(activation_key=key)
|
||||
if len(r)==1:
|
||||
r = Registration.objects.filter(activation_key=key)
|
||||
if len(r) == 1:
|
||||
user_logged_in = request.user.is_authenticated()
|
||||
already_active = True
|
||||
if not r[0].user.is_active:
|
||||
r[0].activate()
|
||||
resp = render_to_response("activation_complete.html",{'csrf':csrf(request)['csrf_token']})
|
||||
return resp
|
||||
resp = render_to_response("activation_active.html",{'csrf':csrf(request)['csrf_token']})
|
||||
already_active = False
|
||||
resp = render_to_response("registration/activation_complete.html", {'user_logged_in': user_logged_in, 'already_active': already_active})
|
||||
return resp
|
||||
if len(r)==0:
|
||||
return render_to_response("activation_invalid.html",{'csrf':csrf(request)['csrf_token']})
|
||||
if len(r) == 0:
|
||||
return render_to_response("registration/activation_invalid.html", {'csrf': csrf(request)['csrf_token']})
|
||||
return HttpResponse("Unknown error. Please e-mail us to let us know how it happened.")
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def password_reset(request):
|
||||
''' Attempts to send a password reset e-mail. '''
|
||||
@@ -286,43 +423,45 @@ def password_reset(request):
|
||||
raise Http404
|
||||
form = PasswordResetForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save( use_https = request.is_secure(),
|
||||
from_email = settings.DEFAULT_FROM_EMAIL,
|
||||
request = request )
|
||||
form.save(use_https = request.is_secure(),
|
||||
from_email = settings.DEFAULT_FROM_EMAIL,
|
||||
request = request,
|
||||
domain_override = settings.SITE_NAME)
|
||||
return HttpResponse(json.dumps({'success':True,
|
||||
'value': render_to_string('registration/password_reset_done.html', {})}))
|
||||
else:
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Invalid e-mail'}))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def reactivation_email(request):
|
||||
''' Send an e-mail to reactivate a deactivated account, or to
|
||||
resend an activation e-mail. Untested. '''
|
||||
email = request.POST['email']
|
||||
try:
|
||||
user = User.objects.get(email = 'email')
|
||||
user = User.objects.get(email='email')
|
||||
except User.DoesNotExist:
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'No inactive user with this e-mail exists'}))
|
||||
|
||||
if user.is_active:
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'User is already active'}))
|
||||
|
||||
reg = Registration.objects.get(user = user)
|
||||
reg = Registration.objects.get(user=user)
|
||||
reg.register(user)
|
||||
|
||||
d={'name':UserProfile.get(user = user).name,
|
||||
'key':r.activation_key}
|
||||
d = {'name': UserProfile.get(user=user).name,
|
||||
'key': r.activation_key}
|
||||
|
||||
subject = render_to_string('reactivation_email_subject.txt',d)
|
||||
subject = render_to_string('reactivation_email_subject.txt', d)
|
||||
subject = ''.join(subject.splitlines())
|
||||
message = render_to_string('reactivation_email.txt',d)
|
||||
message = render_to_string('reactivation_email.txt', d)
|
||||
|
||||
res=u.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
|
||||
res = u.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
|
||||
|
||||
return HttpResponse(json.dumps({'success':True}))
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -336,26 +475,26 @@ def change_email_request(request):
|
||||
user = request.user
|
||||
|
||||
if not user.check_password(request.POST['password']):
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
'error':'Invalid password'}))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Invalid password'}))
|
||||
|
||||
new_email = request.POST['new_email']
|
||||
try:
|
||||
validate_email(new_email)
|
||||
except ValidationError:
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
'error':'Valid e-mail address required.'}))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Valid e-mail address required.'}))
|
||||
|
||||
if len(User.objects.filter(email = new_email)) != 0:
|
||||
if len(User.objects.filter(email=new_email)) != 0:
|
||||
## CRITICAL TODO: Handle case sensitivity for e-mails
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
'error':'An account with this e-mail already exists.'}))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'An account with this e-mail already exists.'}))
|
||||
|
||||
pec_list = PendingEmailChange.objects.filter(user = request.user)
|
||||
pec_list = PendingEmailChange.objects.filter(user=request.user)
|
||||
if len(pec_list) == 0:
|
||||
pec = PendingEmailChange()
|
||||
pec.user = user
|
||||
else :
|
||||
else:
|
||||
pec = pec_list[0]
|
||||
|
||||
pec.new_email = request.POST['new_email']
|
||||
@@ -364,20 +503,21 @@ def change_email_request(request):
|
||||
|
||||
if pec.new_email == user.email:
|
||||
pec.delete()
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
'error':'Old email is the same as the new email.'}))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Old email is the same as the new email.'}))
|
||||
|
||||
d = {'key':pec.activation_key,
|
||||
'old_email' : user.email,
|
||||
'new_email' : pec.new_email}
|
||||
d = {'key': pec.activation_key,
|
||||
'old_email': user.email,
|
||||
'new_email': pec.new_email}
|
||||
|
||||
subject = render_to_string('emails/email_change_subject.txt',d)
|
||||
subject = render_to_string('emails/email_change_subject.txt', d)
|
||||
subject = ''.join(subject.splitlines())
|
||||
message = render_to_string('emails/email_change.txt',d)
|
||||
message = render_to_string('emails/email_change.txt', d)
|
||||
|
||||
res=send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [pec.new_email])
|
||||
res = send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [pec.new_email])
|
||||
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
return HttpResponse(json.dumps({'success':True}))
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def confirm_email_change(request, key):
|
||||
@@ -385,22 +525,21 @@ def confirm_email_change(request, key):
|
||||
link is clicked. We confirm with the old e-mail, and update
|
||||
'''
|
||||
try:
|
||||
pec=PendingEmailChange.objects.get(activation_key=key)
|
||||
pec = PendingEmailChange.objects.get(activation_key=key)
|
||||
except PendingEmailChange.DoesNotExist:
|
||||
return render_to_response("invalid_email_key.html", {})
|
||||
|
||||
user = pec.user
|
||||
d = {'old_email' : user.email,
|
||||
'new_email' : pec.new_email}
|
||||
d = {'old_email': user.email,
|
||||
'new_email': pec.new_email}
|
||||
|
||||
if len(User.objects.filter(email = pec.new_email)) != 0:
|
||||
if len(User.objects.filter(email=pec.new_email)) != 0:
|
||||
return render_to_response("email_exists.html", d)
|
||||
|
||||
|
||||
subject = render_to_string('emails/email_change_subject.txt',d)
|
||||
subject = render_to_string('emails/email_change_subject.txt', d)
|
||||
subject = ''.join(subject.splitlines())
|
||||
message = render_to_string('emails/confirm_email_change.txt',d)
|
||||
up = UserProfile.objects.get( user = user )
|
||||
message = render_to_string('emails/confirm_email_change.txt', d)
|
||||
up = UserProfile.objects.get(user=user)
|
||||
meta = up.get_meta()
|
||||
if 'old_emails' not in meta:
|
||||
meta['old_emails'] = []
|
||||
@@ -414,6 +553,7 @@ def confirm_email_change(request, key):
|
||||
|
||||
return render_to_response("email_change_successful.html", d)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def change_name_request(request):
|
||||
''' Log a request for a new name. '''
|
||||
@@ -421,18 +561,18 @@ def change_name_request(request):
|
||||
raise Http404
|
||||
|
||||
try:
|
||||
pnc = PendingNameChange.objects.get(user = request.user)
|
||||
pnc = PendingNameChange.objects.get(user=request.user)
|
||||
except PendingNameChange.DoesNotExist:
|
||||
pnc = PendingNameChange()
|
||||
pnc.user = request.user
|
||||
pnc.new_name = request.POST['new_name']
|
||||
pnc.rationale = request.POST['rationale']
|
||||
if len(pnc.new_name)<2:
|
||||
return HttpResponse(json.dumps({'success':False,'error':'Name required'}))
|
||||
if len(pnc.rationale)<2:
|
||||
return HttpResponse(json.dumps({'success':False,'error':'Rationale required'}))
|
||||
if len(pnc.new_name) < 2:
|
||||
return HttpResponse(json.dumps({'success': False, 'error': 'Name required'}))
|
||||
if len(pnc.rationale) < 2:
|
||||
return HttpResponse(json.dumps({'success': False, 'error': 'Rationale required'}))
|
||||
pnc.save()
|
||||
return HttpResponse(json.dumps({'success':True}))
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -492,26 +632,3 @@ def accept_name_change(request):
|
||||
pnc.delete()
|
||||
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_if_anonymous
|
||||
def course_info(request, course_id):
|
||||
course = check_course(course_id, course_must_be_open=False)
|
||||
# This is the advertising page for a student to look at the course before signing up
|
||||
csrf_token = csrf(request)['csrf_token']
|
||||
# TODO: Couse should be a model
|
||||
return render_to_response('portal/course_about.html', {'course': course})
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def enroll(request, course_id):
|
||||
course = check_course(course_id, course_must_be_open=False)
|
||||
|
||||
user = request.user
|
||||
if not CourseEnrollment.objects.filter(user=user, course_id=course.id).exists():
|
||||
enrollment = CourseEnrollment(user=user,
|
||||
course_id=course.id)
|
||||
enrollment.save()
|
||||
return redirect(reverse('dashboard'))
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.conf import settings
|
||||
|
||||
import views
|
||||
|
||||
|
||||
class TrackMiddleware:
|
||||
def process_request(self, request):
|
||||
try:
|
||||
@@ -11,10 +12,34 @@ class TrackMiddleware:
|
||||
# names/passwords.
|
||||
if request.META['PATH_INFO'] in ['/event', '/login']:
|
||||
return
|
||||
|
||||
event = { 'GET' : dict(request.GET),
|
||||
'POST' : dict(request.POST)}
|
||||
|
||||
|
||||
# Removes passwords from the tracking logs
|
||||
# WARNING: This list needs to be changed whenever we change
|
||||
# password handling functionality.
|
||||
#
|
||||
# As of the time of this comment, only 'password' is used
|
||||
# The rest are there for future extension.
|
||||
#
|
||||
# Passwords should never be sent as GET requests, but
|
||||
# this can happen due to older browser bugs. We censor
|
||||
# this too.
|
||||
#
|
||||
# We should manually confirm no passwords make it into log
|
||||
# files when we change this.
|
||||
|
||||
censored_strings = ['password', 'newpassword', 'new_password',
|
||||
'oldpassword', 'old_password']
|
||||
post_dict = dict(request.POST)
|
||||
get_dict = dict(request.GET)
|
||||
for string in censored_strings:
|
||||
if string in post_dict:
|
||||
post_dict[string] = '*' * 8
|
||||
if string in get_dict:
|
||||
get_dict[string] = '*' * 8
|
||||
|
||||
event = {'GET': dict(get_dict),
|
||||
'POST': dict(post_dict)}
|
||||
|
||||
# TODO: Confirm no large file uploads
|
||||
event = json.dumps(event)
|
||||
event = event[:512]
|
||||
|
||||
@@ -10,61 +10,64 @@ from django.conf import settings
|
||||
|
||||
log = logging.getLogger("tracking")
|
||||
|
||||
|
||||
def log_event(event):
|
||||
event_str = json.dumps(event)
|
||||
log.info(event_str[:settings.TRACK_MAX_EVENT])
|
||||
|
||||
|
||||
def user_track(request):
|
||||
try: # TODO: Do the same for many of the optional META parameters
|
||||
try: # TODO: Do the same for many of the optional META parameters
|
||||
username = request.user.username
|
||||
except:
|
||||
except:
|
||||
username = "anonymous"
|
||||
|
||||
try:
|
||||
scookie = request.META['HTTP_COOKIE'] # Get cookies
|
||||
scookie = ";".join([c.split('=')[1] for c in scookie.split(";") if "sessionid" in c]).strip() # Extract session ID
|
||||
except:
|
||||
try:
|
||||
scookie = request.META['HTTP_COOKIE'] # Get cookies
|
||||
scookie = ";".join([c.split('=')[1] for c in scookie.split(";") if "sessionid" in c]).strip() # Extract session ID
|
||||
except:
|
||||
scookie = ""
|
||||
|
||||
try:
|
||||
try:
|
||||
agent = request.META['HTTP_USER_AGENT']
|
||||
except:
|
||||
except:
|
||||
agent = ''
|
||||
|
||||
# TODO: Move a bunch of this into log_event
|
||||
event = {
|
||||
"username" : username,
|
||||
"session" : scookie,
|
||||
"ip" : request.META['REMOTE_ADDR'],
|
||||
"event_source" : "browser",
|
||||
"event_type" : request.GET['event_type'],
|
||||
"event" : request.GET['event'],
|
||||
"agent" : agent,
|
||||
"page" : request.GET['page'],
|
||||
"username": username,
|
||||
"session": scookie,
|
||||
"ip": request.META['REMOTE_ADDR'],
|
||||
"event_source": "browser",
|
||||
"event_type": request.GET['event_type'],
|
||||
"event": request.GET['event'],
|
||||
"agent": agent,
|
||||
"page": request.GET['page'],
|
||||
"time": datetime.datetime.utcnow().isoformat(),
|
||||
}
|
||||
log_event(event)
|
||||
return HttpResponse('success')
|
||||
|
||||
|
||||
def server_track(request, event_type, event, page=None):
|
||||
try:
|
||||
try:
|
||||
username = request.user.username
|
||||
except:
|
||||
except:
|
||||
username = "anonymous"
|
||||
|
||||
try:
|
||||
try:
|
||||
agent = request.META['HTTP_USER_AGENT']
|
||||
except:
|
||||
except:
|
||||
agent = ''
|
||||
|
||||
event = {
|
||||
"username" : username,
|
||||
"ip" : request.META['REMOTE_ADDR'],
|
||||
"event_source" : "server",
|
||||
"event_type" : event_type,
|
||||
"event" : event,
|
||||
"agent" : agent,
|
||||
"page" : page,
|
||||
"username": username,
|
||||
"ip": request.META['REMOTE_ADDR'],
|
||||
"event_source": "server",
|
||||
"event_type": event_type,
|
||||
"event": event,
|
||||
"agent": agent,
|
||||
"page": page,
|
||||
"time": datetime.datetime.utcnow().isoformat(),
|
||||
}
|
||||
log_event(event)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
This module aims to give a little more fine-tuned control of caching and cache
|
||||
invalidation. Import these instead of django.core.cache.
|
||||
|
||||
Note that 'default' is being preserved for user session caching, which we're
|
||||
Note that 'default' is being preserved for user session caching, which we're
|
||||
not migrating so as not to inconvenience users by logging them all out.
|
||||
"""
|
||||
from functools import wraps
|
||||
@@ -16,26 +16,27 @@ try:
|
||||
except Exception:
|
||||
cache = cache.cache
|
||||
|
||||
|
||||
def cache_if_anonymous(view_func):
|
||||
"""
|
||||
Many of the pages in edX are identical when the user is not logged
|
||||
in, but should not be cached when the user is logged in (because
|
||||
of the navigation bar at the top with the username).
|
||||
|
||||
in, but should not be cached when the user is logged in (because
|
||||
of the navigation bar at the top with the username).
|
||||
|
||||
The django middleware cache does not handle this correctly, because
|
||||
we access the session to put the csrf token in the header. This adds
|
||||
the cookie to the vary header, and so every page is cached seperately
|
||||
for each user (because each user has a different csrf token).
|
||||
|
||||
|
||||
Note that this decorator should only be used on views that do not
|
||||
contain the csrftoken within the html. The csrf token can be included
|
||||
in the header by ordering the decorators as such:
|
||||
|
||||
|
||||
@ensure_csrftoken
|
||||
@cache_if_anonymous
|
||||
def myView(request):
|
||||
"""
|
||||
|
||||
|
||||
@wraps(view_func)
|
||||
def _decorated(request, *args, **kwargs):
|
||||
if not request.user.is_authenticated():
|
||||
@@ -45,12 +46,12 @@ def cache_if_anonymous(view_func):
|
||||
if not response:
|
||||
response = view_func(request, *args, **kwargs)
|
||||
cache.set(cache_key, response, 60 * 3)
|
||||
|
||||
|
||||
return response
|
||||
|
||||
|
||||
else:
|
||||
#Don't use the cache
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
|
||||
return _decorated
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from functools import wraps
|
||||
import copy
|
||||
import json
|
||||
|
||||
|
||||
def expect_json(view_function):
|
||||
@wraps(view_function)
|
||||
def expect_json_with_cloned_request(request, *args, **kwargs):
|
||||
|
||||
@@ -6,11 +6,13 @@ from django.utils.encoding import smart_str
|
||||
import hashlib
|
||||
import urllib
|
||||
|
||||
|
||||
def fasthash(string):
|
||||
m = hashlib.new("md4")
|
||||
m.update(string)
|
||||
return m.hexdigest()
|
||||
|
||||
|
||||
def safe_key(key, key_prefix, version):
|
||||
safe_key = urllib.quote_plus(smart_str(key))
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ from django.http import HttpResponseServerError
|
||||
|
||||
log = logging.getLogger("mitx")
|
||||
|
||||
|
||||
class ExceptionLoggingMiddleware(object):
|
||||
"""Just here to log unchecked exceptions that go all the way up the Django
|
||||
"""Just here to log unchecked exceptions that go all the way up the Django
|
||||
stack"""
|
||||
|
||||
if not settings.TEMPLATE_DEBUG:
|
||||
def process_exception(self, request, exception):
|
||||
log.exception(exception)
|
||||
return HttpResponseServerError("Server Error - Please try again later.")
|
||||
|
||||
|
||||
@@ -14,53 +14,57 @@ from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
import capa.calc
|
||||
import track.views
|
||||
|
||||
|
||||
def calculate(request):
|
||||
''' Calculator in footer of every page. '''
|
||||
equation = request.GET['equation']
|
||||
try:
|
||||
try:
|
||||
result = capa.calc.evaluator({}, {}, equation)
|
||||
except:
|
||||
event = {'error':map(str,sys.exc_info()),
|
||||
'equation':equation}
|
||||
event = {'error': map(str, sys.exc_info()),
|
||||
'equation': equation}
|
||||
track.views.server_track(request, 'error:calc', event, page='calc')
|
||||
return HttpResponse(json.dumps({'result':'Invalid syntax'}))
|
||||
return HttpResponse(json.dumps({'result':str(result)}))
|
||||
return HttpResponse(json.dumps({'result': 'Invalid syntax'}))
|
||||
return HttpResponse(json.dumps({'result': str(result)}))
|
||||
|
||||
|
||||
def send_feedback(request):
|
||||
''' Feeback mechanism in footer of every page. '''
|
||||
try:
|
||||
try:
|
||||
username = request.user.username
|
||||
email = request.user.email
|
||||
except:
|
||||
except:
|
||||
username = "anonymous"
|
||||
email = "anonymous"
|
||||
|
||||
try:
|
||||
browser = request.META['HTTP_USER_AGENT']
|
||||
except:
|
||||
browser = "Unknown"
|
||||
|
||||
feedback = render_to_string("feedback_email.txt",
|
||||
{"subject":request.POST['subject'],
|
||||
"url": request.POST['url'],
|
||||
"time": datetime.datetime.now().isoformat(),
|
||||
"feedback": request.POST['message'],
|
||||
"email":email,
|
||||
"browser":browser,
|
||||
"user":username})
|
||||
|
||||
send_mail("MITx Feedback / " +request.POST['subject'],
|
||||
feedback,
|
||||
try:
|
||||
browser = request.META['HTTP_USER_AGENT']
|
||||
except:
|
||||
browser = "Unknown"
|
||||
|
||||
feedback = render_to_string("feedback_email.txt",
|
||||
{"subject": request.POST['subject'],
|
||||
"url": request.POST['url'],
|
||||
"time": datetime.datetime.now().isoformat(),
|
||||
"feedback": request.POST['message'],
|
||||
"email": email,
|
||||
"browser": browser,
|
||||
"user": username})
|
||||
|
||||
send_mail("MITx Feedback / " + request.POST['subject'],
|
||||
feedback,
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
[ settings.DEFAULT_FEEDBACK_EMAIL ],
|
||||
fail_silently = False
|
||||
[settings.DEFAULT_FEEDBACK_EMAIL],
|
||||
fail_silently=False
|
||||
)
|
||||
return HttpResponse(json.dumps({'success':True}))
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
|
||||
def info(request):
|
||||
''' Info page (link from main header) '''
|
||||
return render_to_response("info.html", {})
|
||||
|
||||
|
||||
# From http://djangosnippets.org/snippets/1042/
|
||||
def parse_accept_header(accept):
|
||||
"""Parse the Accept header *accept*, returning a list with pairs of
|
||||
@@ -82,6 +86,7 @@ def parse_accept_header(accept):
|
||||
result.sort(lambda x, y: -cmp(x[2], y[2]))
|
||||
return result
|
||||
|
||||
|
||||
def accepts(request, media_type):
|
||||
"""Return whether this request has an Accept header that matches type"""
|
||||
accept = parse_accept_header(request.META.get("HTTP_ACCEPT", ""))
|
||||
|
||||
102
common/djangoapps/xmodule_modifiers.py
Normal file
102
common/djangoapps/xmodule_modifiers.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import json
|
||||
from django.conf import settings
|
||||
from functools import wraps
|
||||
from static_replace import replace_urls
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
|
||||
|
||||
def wrap_xmodule(get_html, module, template):
|
||||
"""
|
||||
Wraps the results of get_html in a standard <section> with identifying
|
||||
data so that the appropriate javascript module can be loaded onto it.
|
||||
|
||||
get_html: An XModule.get_html method or an XModuleDescriptor.get_html method
|
||||
module: An XModule
|
||||
template: A template that takes the variables:
|
||||
content: the results of get_html,
|
||||
class_: the module class name
|
||||
module_name: the js_module_name of the module
|
||||
"""
|
||||
|
||||
@wraps(get_html)
|
||||
def _get_html():
|
||||
return render_to_string(template, {
|
||||
'content': get_html(),
|
||||
'class_': module.__class__.__name__,
|
||||
'module_name': module.js_module_name
|
||||
})
|
||||
return _get_html
|
||||
|
||||
|
||||
def replace_static_urls(get_html, prefix):
|
||||
"""
|
||||
Updates the supplied module with a new get_html function that wraps
|
||||
the old get_html function and substitutes urls of the form /static/...
|
||||
with urls that are /static/<prefix>/...
|
||||
"""
|
||||
|
||||
@wraps(get_html)
|
||||
def _get_html():
|
||||
return replace_urls(get_html(), staticfiles_prefix=prefix)
|
||||
return _get_html
|
||||
|
||||
|
||||
def grade_histogram(module_id):
|
||||
''' Print out a histogram of grades on a given problem.
|
||||
Part of staff member debug info.
|
||||
'''
|
||||
from django.db import connection
|
||||
cursor = connection.cursor()
|
||||
|
||||
q = """SELECT courseware_studentmodule.grade,
|
||||
COUNT(courseware_studentmodule.student_id)
|
||||
FROM courseware_studentmodule
|
||||
WHERE courseware_studentmodule.module_id=%s
|
||||
GROUP BY courseware_studentmodule.grade"""
|
||||
# Passing module_id this way prevents sql-injection.
|
||||
cursor.execute(q, [module_id])
|
||||
|
||||
grades = list(cursor.fetchall())
|
||||
grades.sort(key=lambda x: x[0]) # Add ORDER BY to sql query?
|
||||
if len(grades) == 1 and grades[0][0] is None:
|
||||
return []
|
||||
return grades
|
||||
|
||||
|
||||
def add_histogram(get_html, module):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
@wraps(get_html)
|
||||
def _get_html():
|
||||
module_id = module.id
|
||||
histogram = grade_histogram(module_id)
|
||||
render_histogram = len(histogram) > 0
|
||||
|
||||
# TODO: fixme - no filename in module.xml in general (this code block for edx4edx)
|
||||
# the following if block is for summer 2012 edX course development; it will change when the CMS comes online
|
||||
if settings.MITX_FEATURES.get('DISPLAY_EDIT_LINK') and settings.DEBUG and module_xml.get('filename') is not None:
|
||||
coursename = multicourse_settings.get_coursename_from_request(request)
|
||||
github_url = multicourse_settings.get_course_github_url(coursename)
|
||||
fn = module_xml.get('filename')
|
||||
if module_xml.tag=='problem': fn = 'problems/' + fn # grrr
|
||||
edit_link = (github_url + '/tree/master/' + fn) if github_url is not None else None
|
||||
if module_xml.tag=='problem': edit_link += '.xml' # grrr
|
||||
else:
|
||||
edit_link = False
|
||||
|
||||
# Cast module.definition and module.metadata to dicts so that json can dump them
|
||||
# even though they are lazily loaded
|
||||
staff_context = {'definition': json.dumps(dict(module.definition), indent=4),
|
||||
'metadata': json.dumps(dict(module.metadata), indent=4),
|
||||
'element_id': module.location.html_id(),
|
||||
'edit_link': edit_link,
|
||||
'histogram': json.dumps(histogram),
|
||||
'render_histogram': render_histogram,
|
||||
'module_content': get_html()}
|
||||
return render_to_string("staff_problem_info.html", staff_context)
|
||||
|
||||
return _get_html
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -14,29 +14,30 @@ from pyparsing import StringEnd, Optional, Forward
|
||||
from pyparsing import CaselessLiteral, Group, StringEnd
|
||||
from pyparsing import NoMatch, stringEnd, alphanums
|
||||
|
||||
default_functions = {'sin' : numpy.sin,
|
||||
'cos' : numpy.cos,
|
||||
'tan' : numpy.tan,
|
||||
default_functions = {'sin': numpy.sin,
|
||||
'cos': numpy.cos,
|
||||
'tan': numpy.tan,
|
||||
'sqrt': numpy.sqrt,
|
||||
'log10':numpy.log10,
|
||||
'log2':numpy.log2,
|
||||
'log10': numpy.log10,
|
||||
'log2': numpy.log2,
|
||||
'ln': numpy.log,
|
||||
'arccos':numpy.arccos,
|
||||
'arcsin':numpy.arcsin,
|
||||
'arctan':numpy.arctan,
|
||||
'abs':numpy.abs
|
||||
'arccos': numpy.arccos,
|
||||
'arcsin': numpy.arcsin,
|
||||
'arctan': numpy.arctan,
|
||||
'abs': numpy.abs
|
||||
}
|
||||
default_variables = {'j':numpy.complex(0,1),
|
||||
'e':numpy.e,
|
||||
'pi':numpy.pi,
|
||||
'k':scipy.constants.k,
|
||||
'c':scipy.constants.c,
|
||||
'T':298.15,
|
||||
'q':scipy.constants.e
|
||||
default_variables = {'j': numpy.complex(0, 1),
|
||||
'e': numpy.e,
|
||||
'pi': numpy.pi,
|
||||
'k': scipy.constants.k,
|
||||
'c': scipy.constants.c,
|
||||
'T': 298.15,
|
||||
'q': scipy.constants.e
|
||||
}
|
||||
|
||||
log = logging.getLogger("mitx.courseware.capa")
|
||||
|
||||
|
||||
class UndefinedVariable(Exception):
|
||||
def raiseself(self):
|
||||
''' Helper so we can use inside of a lambda '''
|
||||
@@ -44,28 +45,31 @@ class UndefinedVariable(Exception):
|
||||
|
||||
|
||||
general_whitespace = re.compile('[^\w]+')
|
||||
|
||||
|
||||
def check_variables(string, variables):
|
||||
''' Confirm the only variables in string are defined.
|
||||
''' Confirm the only variables in string are defined.
|
||||
|
||||
Pyparsing uses a left-to-right parser, which makes the more
|
||||
elegant approach pretty hopeless.
|
||||
elegant approach pretty hopeless.
|
||||
|
||||
achar = reduce(lambda a,b:a|b ,map(Literal,alphas)) # Any alphabetic character
|
||||
undefined_variable = achar + Word(alphanums)
|
||||
undefined_variable.setParseAction(lambda x:UndefinedVariable("".join(x)).raiseself())
|
||||
varnames = varnames | undefined_variable'''
|
||||
possible_variables = re.split(general_whitespace, string) # List of all alnums in string
|
||||
possible_variables = re.split(general_whitespace, string) # List of all alnums in string
|
||||
bad_variables = list()
|
||||
for v in possible_variables:
|
||||
if len(v) == 0:
|
||||
continue
|
||||
if v[0] <= '9' and '0' <= 'v': # Skip things that begin with numbers
|
||||
if v[0] <= '9' and '0' <= 'v': # Skip things that begin with numbers
|
||||
continue
|
||||
if v not in variables:
|
||||
if v not in variables:
|
||||
bad_variables.append(v)
|
||||
if len(bad_variables)>0:
|
||||
if len(bad_variables) > 0:
|
||||
raise UndefinedVariable(' '.join(bad_variables))
|
||||
|
||||
|
||||
def evaluator(variables, functions, string, cs=False):
|
||||
''' Evaluate an expression. Variables are passed as a dictionary
|
||||
from string to value. Unary functions are passed as a dictionary
|
||||
@@ -84,147 +88,152 @@ def evaluator(variables, functions, string, cs=False):
|
||||
all_variables = copy.copy(default_variables)
|
||||
all_functions = copy.copy(default_functions)
|
||||
|
||||
if not cs:
|
||||
if not cs:
|
||||
all_variables = lower_dict(all_variables)
|
||||
all_functions = lower_dict(all_functions)
|
||||
|
||||
all_variables.update(variables)
|
||||
all_variables.update(variables)
|
||||
all_functions.update(functions)
|
||||
|
||||
if not cs:
|
||||
|
||||
if not cs:
|
||||
string_cs = string.lower()
|
||||
all_functions = lower_dict(all_functions)
|
||||
all_variables = lower_dict(all_variables)
|
||||
CasedLiteral = CaselessLiteral
|
||||
CasedLiteral = CaselessLiteral
|
||||
else:
|
||||
string_cs = string
|
||||
CasedLiteral = Literal
|
||||
|
||||
check_variables(string_cs, set(all_variables.keys()+all_functions.keys()))
|
||||
check_variables(string_cs, set(all_variables.keys() + all_functions.keys()))
|
||||
|
||||
if string.strip() == "":
|
||||
return float('nan')
|
||||
ops = { "^" : operator.pow,
|
||||
"*" : operator.mul,
|
||||
"/" : operator.truediv,
|
||||
"+" : operator.add,
|
||||
"-" : operator.sub,
|
||||
ops = {"^": operator.pow,
|
||||
"*": operator.mul,
|
||||
"/": operator.truediv,
|
||||
"+": operator.add,
|
||||
"-": operator.sub,
|
||||
}
|
||||
# We eliminated extreme ones, since they're rarely used, and potentially
|
||||
# confusing. They may also conflict with variables if we ever allow e.g.
|
||||
# confusing. They may also conflict with variables if we ever allow e.g.
|
||||
# 5R instead of 5*R
|
||||
suffixes={'%':0.01,'k':1e3,'M':1e6,'G':1e9,
|
||||
'T':1e12,#'P':1e15,'E':1e18,'Z':1e21,'Y':1e24,
|
||||
'c':1e-2,'m':1e-3,'u':1e-6,
|
||||
'n':1e-9,'p':1e-12}#,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24}
|
||||
|
||||
suffixes = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9,
|
||||
'T': 1e12,# 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24,
|
||||
'c': 1e-2, 'm': 1e-3, 'u': 1e-6,
|
||||
'n': 1e-9, 'p': 1e-12}# ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24}
|
||||
|
||||
def super_float(text):
|
||||
''' Like float, but with si extensions. 1k goes to 1000'''
|
||||
if text[-1] in suffixes:
|
||||
return float(text[:-1])*suffixes[text[-1]]
|
||||
return float(text[:-1]) * suffixes[text[-1]]
|
||||
else:
|
||||
return float(text)
|
||||
|
||||
def number_parse_action(x): # [ '7' ] -> [ 7 ]
|
||||
def number_parse_action(x): # [ '7' ] -> [ 7 ]
|
||||
return [super_float("".join(x))]
|
||||
def exp_parse_action(x): # [ 2 ^ 3 ^ 2 ] -> 512
|
||||
x = [e for e in x if isinstance(e, numbers.Number)] # Ignore ^
|
||||
|
||||
def exp_parse_action(x): # [ 2 ^ 3 ^ 2 ] -> 512
|
||||
x = [e for e in x if isinstance(e, numbers.Number)] # Ignore ^
|
||||
x.reverse()
|
||||
x=reduce(lambda a,b:b**a, x)
|
||||
x = reduce(lambda a, b: b ** a, x)
|
||||
return x
|
||||
def parallel(x): # Parallel resistors [ 1 2 ] => 2/3
|
||||
|
||||
def parallel(x): # Parallel resistors [ 1 2 ] => 2/3
|
||||
if len(x) == 1:
|
||||
return x[0]
|
||||
if 0 in x:
|
||||
return float('nan')
|
||||
x = [1./e for e in x if isinstance(e, numbers.Number)] # Ignore ||
|
||||
return 1./sum(x)
|
||||
def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0
|
||||
x = [1. / e for e in x if isinstance(e, numbers.Number)] # Ignore ||
|
||||
return 1. / sum(x)
|
||||
|
||||
def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0
|
||||
total = 0.0
|
||||
op = ops['+']
|
||||
for e in x:
|
||||
if e in set('+-'):
|
||||
op = ops[e]
|
||||
else:
|
||||
total=op(total, e)
|
||||
total = op(total, e)
|
||||
return total
|
||||
def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66
|
||||
|
||||
def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66
|
||||
prod = 1.0
|
||||
op = ops['*']
|
||||
for e in x:
|
||||
if e in set('*/'):
|
||||
op = ops[e]
|
||||
else:
|
||||
prod=op(prod, e)
|
||||
prod = op(prod, e)
|
||||
return prod
|
||||
|
||||
def func_parse_action(x):
|
||||
return [all_functions[x[0]](x[1])]
|
||||
|
||||
number_suffix=reduce(lambda a,b:a|b, map(Literal,suffixes.keys()), NoMatch()) # SI suffixes and percent
|
||||
(dot,minus,plus,times,div,lpar,rpar,exp)=map(Literal,".-+*/()^")
|
||||
|
||||
number_part=Word(nums)
|
||||
inner_number = ( number_part+Optional("."+number_part) ) | ("."+number_part) # 0.33 or 7 or .34
|
||||
number=Optional(minus | plus)+ inner_number + \
|
||||
Optional(CaselessLiteral("E")+Optional("-")+number_part)+ \
|
||||
Optional(number_suffix) # 0.33k or -17
|
||||
number=number.setParseAction( number_parse_action ) # Convert to number
|
||||
|
||||
number_suffix = reduce(lambda a, b: a | b, map(Literal, suffixes.keys()), NoMatch()) # SI suffixes and percent
|
||||
(dot, minus, plus, times, div, lpar, rpar, exp) = map(Literal, ".-+*/()^")
|
||||
|
||||
number_part = Word(nums)
|
||||
inner_number = (number_part + Optional("." + number_part)) | ("." + number_part) # 0.33 or 7 or .34
|
||||
number = Optional(minus | plus) + inner_number + \
|
||||
Optional(CaselessLiteral("E") + Optional("-") + number_part) + \
|
||||
Optional(number_suffix) # 0.33k or -17
|
||||
number = number.setParseAction(number_parse_action) # Convert to number
|
||||
|
||||
# Predefine recursive variables
|
||||
expr = Forward()
|
||||
expr = Forward()
|
||||
factor = Forward()
|
||||
|
||||
|
||||
def sreduce(f, l):
|
||||
''' Same as reduce, but handle len 1 and len 0 lists sensibly '''
|
||||
if len(l)==0:
|
||||
if len(l) == 0:
|
||||
return NoMatch()
|
||||
if len(l)==1:
|
||||
if len(l) == 1:
|
||||
return l[0]
|
||||
return reduce(f, l)
|
||||
|
||||
# Handle variables passed in. E.g. if we have {'R':0.5}, we make the substitution.
|
||||
# Handle variables passed in. E.g. if we have {'R':0.5}, we make the substitution.
|
||||
# Special case for no variables because of how we understand PyParsing is put together
|
||||
if len(all_variables)>0:
|
||||
# We sort the list so that var names (like "e2") match before
|
||||
if len(all_variables) > 0:
|
||||
# We sort the list so that var names (like "e2") match before
|
||||
# mathematical constants (like "e"). This is kind of a hack.
|
||||
all_variables_keys = sorted(all_variables.keys(), key=len, reverse=True)
|
||||
varnames = sreduce(lambda x,y:x|y, map(lambda x: CasedLiteral(x), all_variables_keys))
|
||||
varnames.setParseAction(lambda x:map(lambda y:all_variables[y], x))
|
||||
varnames = sreduce(lambda x, y: x | y, map(lambda x: CasedLiteral(x), all_variables_keys))
|
||||
varnames.setParseAction(lambda x: map(lambda y: all_variables[y], x))
|
||||
else:
|
||||
varnames=NoMatch()
|
||||
# Same thing for functions.
|
||||
if len(all_functions)>0:
|
||||
funcnames = sreduce(lambda x,y:x|y, map(lambda x: CasedLiteral(x), all_functions.keys()))
|
||||
function = funcnames+lpar.suppress()+expr+rpar.suppress()
|
||||
varnames = NoMatch()
|
||||
# Same thing for functions.
|
||||
if len(all_functions) > 0:
|
||||
funcnames = sreduce(lambda x, y: x | y, map(lambda x: CasedLiteral(x), all_functions.keys()))
|
||||
function = funcnames + lpar.suppress() + expr + rpar.suppress()
|
||||
function.setParseAction(func_parse_action)
|
||||
else:
|
||||
function = NoMatch()
|
||||
|
||||
atom = number | function | varnames | lpar+expr+rpar
|
||||
factor << (atom + ZeroOrMore(exp+atom)).setParseAction(exp_parse_action) # 7^6
|
||||
paritem = factor + ZeroOrMore(Literal('||')+factor) # 5k || 4k
|
||||
paritem=paritem.setParseAction(parallel)
|
||||
term = paritem + ZeroOrMore((times|div)+paritem) # 7 * 5 / 4 - 3
|
||||
atom = number | function | varnames | lpar + expr + rpar
|
||||
factor << (atom + ZeroOrMore(exp + atom)).setParseAction(exp_parse_action) # 7^6
|
||||
paritem = factor + ZeroOrMore(Literal('||') + factor) # 5k || 4k
|
||||
paritem = paritem.setParseAction(parallel)
|
||||
term = paritem + ZeroOrMore((times | div) + paritem) # 7 * 5 / 4 - 3
|
||||
term = term.setParseAction(prod_parse_action)
|
||||
expr << Optional((plus|minus)) + term + ZeroOrMore((plus|minus)+term) # -5 + 4 - 3
|
||||
expr=expr.setParseAction(sum_parse_action)
|
||||
return (expr+stringEnd).parseString(string)[0]
|
||||
expr << Optional((plus | minus)) + term + ZeroOrMore((plus | minus) + term) # -5 + 4 - 3
|
||||
expr = expr.setParseAction(sum_parse_action)
|
||||
return (expr + stringEnd).parseString(string)[0]
|
||||
|
||||
if __name__=='__main__':
|
||||
variables={'R1':2.0, 'R3':4.0}
|
||||
functions={'sin':numpy.sin, 'cos':numpy.cos}
|
||||
print "X",evaluator(variables, functions, "10000||sin(7+5)-6k")
|
||||
print "X",evaluator(variables, functions, "13")
|
||||
print evaluator({'R1': 2.0, 'R3':4.0}, {}, "13")
|
||||
if __name__ == '__main__':
|
||||
variables = {'R1': 2.0, 'R3': 4.0}
|
||||
functions = {'sin': numpy.sin, 'cos': numpy.cos}
|
||||
print "X", evaluator(variables, functions, "10000||sin(7+5)-6k")
|
||||
print "X", evaluator(variables, functions, "13")
|
||||
print evaluator({'R1': 2.0, 'R3': 4.0}, {}, "13")
|
||||
|
||||
print evaluator({'e1':1,'e2':1.0,'R3':7,'V0':5,'R5':15,'I1':1,'R4':6}, {},"e2")
|
||||
print evaluator({'e1': 1, 'e2': 1.0, 'R3': 7, 'V0': 5, 'R5': 15, 'I1': 1, 'R4': 6}, {}, "e2")
|
||||
|
||||
print evaluator({'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.66009498411213041}, {}, "5")
|
||||
print evaluator({},{}, "-1")
|
||||
print evaluator({},{}, "-(7+5)")
|
||||
print evaluator({},{}, "-0.33")
|
||||
print evaluator({},{}, "-.33")
|
||||
print evaluator({},{}, "5+1*j")
|
||||
print evaluator({},{}, "j||1")
|
||||
print evaluator({},{}, "e^(j*pi)")
|
||||
print evaluator({},{}, "5+7 QWSEKO")
|
||||
print evaluator({}, {}, "-1")
|
||||
print evaluator({}, {}, "-(7+5)")
|
||||
print evaluator({}, {}, "-0.33")
|
||||
print evaluator({}, {}, "-.33")
|
||||
print evaluator({}, {}, "5+1*j")
|
||||
print evaluator({}, {}, "j||1")
|
||||
print evaluator({}, {}, "e^(j*pi)")
|
||||
print evaluator({}, {}, "5+7 QWSEKO")
|
||||
|
||||
@@ -37,7 +37,7 @@ from util import contextualize_text
|
||||
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__])
|
||||
response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__])
|
||||
|
||||
entry_types = ['textline', 'schematic', 'textbox', 'imageinput', 'optioninput', 'choicegroup', 'radiogroup', 'checkboxgroup']
|
||||
solution_types = ['solution'] # extra things displayed after "show answers" is pressed
|
||||
@@ -57,13 +57,14 @@ 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 = ["responseparam", "answer", "script", "hintgroup"]
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# main class for this module
|
||||
|
||||
|
||||
class LoncapaProblem(object):
|
||||
'''
|
||||
Main class for capa Problems.
|
||||
@@ -79,7 +80,7 @@ class LoncapaProblem(object):
|
||||
- id (string): identifier for this problem; often a filename (no spaces)
|
||||
- state (dict): student state
|
||||
- seed (int): random number generator seed (int)
|
||||
- system (I4xSystem): I4xSystem instance which provides OS, rendering, and user context
|
||||
- system (ModuleSystem): ModuleSystem instance which provides OS, rendering, and user context
|
||||
|
||||
'''
|
||||
|
||||
@@ -118,7 +119,7 @@ class LoncapaProblem(object):
|
||||
# the dict has keys = xml subtree of Response, values = Response instance
|
||||
self._preprocess_problem(self.tree)
|
||||
|
||||
if not self.student_answers: # True when student_answers is an empty dict
|
||||
if not self.student_answers: # True when student_answers is an empty dict
|
||||
self.set_initial_display()
|
||||
|
||||
def do_reset(self):
|
||||
@@ -132,7 +133,7 @@ class LoncapaProblem(object):
|
||||
def set_initial_display(self):
|
||||
initial_answers = dict()
|
||||
for responder in self.responders.values():
|
||||
if hasattr(responder,'get_initial_display'):
|
||||
if hasattr(responder, 'get_initial_display'):
|
||||
initial_answers.update(responder.get_initial_display())
|
||||
|
||||
self.student_answers = initial_answers
|
||||
@@ -160,11 +161,11 @@ class LoncapaProblem(object):
|
||||
'''
|
||||
maxscore = 0
|
||||
for response, responder in self.responders.iteritems():
|
||||
if hasattr(responder,'get_max_score'):
|
||||
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
|
||||
log.debug('responder %s failed to properly return from get_max_score()' % responder) # FIXME
|
||||
raise
|
||||
else:
|
||||
maxscore += len(self.responder_answers[response])
|
||||
@@ -181,7 +182,7 @@ class LoncapaProblem(object):
|
||||
try:
|
||||
correct += self.correct_map.get_npoints(key)
|
||||
except Exception:
|
||||
log.error('key=%s, correct_map = %s' % (key,self.correct_map))
|
||||
log.error('key=%s, correct_map = %s' % (key, self.correct_map))
|
||||
raise
|
||||
|
||||
if (not self.student_answers) or len(self.student_answers) == 0:
|
||||
@@ -193,19 +194,19 @@ class LoncapaProblem(object):
|
||||
|
||||
def update_score(self, score_msg, queuekey):
|
||||
'''
|
||||
Deliver grading response (e.g. from async code checking) to
|
||||
the specific ResponseType that requested grading
|
||||
|
||||
Deliver grading response (e.g. from async code checking) to
|
||||
the specific ResponseType that requested grading
|
||||
|
||||
Returns an updated CorrectMap
|
||||
'''
|
||||
cmap = CorrectMap()
|
||||
cmap.update(self.correct_map)
|
||||
for responder in self.responders.values():
|
||||
if hasattr(responder,'update_score'):
|
||||
if hasattr(responder, 'update_score'):
|
||||
# Each LoncapaResponse will update the specific entries of 'cmap' that it's responsible for
|
||||
cmap = responder.update_score(score_msg, cmap, queuekey)
|
||||
self.correct_map.set_dict(cmap.get_dict())
|
||||
return cmap
|
||||
return cmap
|
||||
|
||||
def is_queued(self):
|
||||
'''
|
||||
@@ -232,7 +233,7 @@ class LoncapaProblem(object):
|
||||
newcmap = CorrectMap() # start new with empty CorrectMap
|
||||
# log.debug('Responders: %s' % self.responders)
|
||||
for responder in self.responders.values():
|
||||
results = responder.evaluate_answers(answers,oldcmap) # call the responsetype instance to do the actual grading
|
||||
results = responder.evaluate_answers(answers, oldcmap) # call the responsetype instance to do the actual grading
|
||||
newcmap.update(results)
|
||||
self.correct_map = newcmap
|
||||
# log.debug('%s: in grade_answers, answers=%s, cmap=%s' % (self,answers,newcmap))
|
||||
@@ -285,23 +286,23 @@ class LoncapaProblem(object):
|
||||
file = inc.get('file')
|
||||
if file is not None:
|
||||
try:
|
||||
ifp = self.system.filestore.open(file) # open using I4xSystem OSFS filestore
|
||||
ifp = self.system.filestore.open(file) # open using ModuleSystem OSFS filestore
|
||||
except Exception as err:
|
||||
log.error('Error %s in problem xml include: %s' % (err,etree.tostring(inc,pretty_print=True)))
|
||||
log.error('Cannot find file %s in %s' % (file,self.system.filestore))
|
||||
log.error('Error %s in problem xml include: %s' % (err, etree.tostring(inc, pretty_print=True)))
|
||||
log.error('Cannot find file %s in %s' % (file, self.system.filestore))
|
||||
if not self.system.get('DEBUG'): # if debugging, don't fail - just log error
|
||||
raise
|
||||
else: continue
|
||||
try:
|
||||
incxml = etree.XML(ifp.read()) # read in and convert to XML
|
||||
except Exception as err:
|
||||
log.error('Error %s in problem xml include: %s' % (err,etree.tostring(inc,pretty_print=True)))
|
||||
log.error('Error %s in problem xml include: %s' % (err, etree.tostring(inc, pretty_print=True)))
|
||||
log.error('Cannot parse XML in %s' % (file))
|
||||
if not self.system.get('DEBUG'): # if debugging, don't fail - just log error
|
||||
raise
|
||||
else: continue
|
||||
parent = inc.getparent() # insert new XML into tree in place of inlcude
|
||||
parent.insert(parent.index(inc),incxml)
|
||||
parent.insert(parent.index(inc), incxml)
|
||||
parent.remove(inc)
|
||||
log.debug('Included %s into %s' % (file, self.problem_id))
|
||||
|
||||
@@ -330,9 +331,9 @@ class LoncapaProblem(object):
|
||||
abs_dir = os.path.normpath(dir)
|
||||
log.debug("appending to path: %s" % abs_dir)
|
||||
path.append(abs_dir)
|
||||
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def _extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private
|
||||
'''
|
||||
Extract content of <script>...</script> from the problem.xml file, and exec it in the
|
||||
@@ -353,7 +354,7 @@ class LoncapaProblem(object):
|
||||
return context
|
||||
|
||||
def _execute_scripts(self, scripts, context):
|
||||
'''
|
||||
'''
|
||||
Executes scripts in the given context.
|
||||
'''
|
||||
original_path = sys.path
|
||||
@@ -391,7 +392,7 @@ class LoncapaProblem(object):
|
||||
|
||||
Used by get_html.
|
||||
'''
|
||||
if problemtree.tag=='script' and problemtree.get('type') and 'javascript' in problemtree.get('type'):
|
||||
if problemtree.tag == 'script' and problemtree.get('type') and 'javascript' in problemtree.get('type'):
|
||||
# leave javascript intact.
|
||||
return problemtree
|
||||
|
||||
@@ -424,8 +425,8 @@ class LoncapaProblem(object):
|
||||
'status': status,
|
||||
'id': problemtree.get('id'),
|
||||
'feedback': {'message': msg,
|
||||
'hint' : hint,
|
||||
'hintmode' : hintmode,
|
||||
'hint': hint,
|
||||
'hintmode': hintmode,
|
||||
}
|
||||
},
|
||||
use='capa_input')
|
||||
@@ -443,7 +444,7 @@ class LoncapaProblem(object):
|
||||
if tree.tag in html_transforms:
|
||||
tree.tag = html_transforms[problemtree.tag]['tag']
|
||||
else:
|
||||
for (key, value) in problemtree.items(): # copy attributes over if not innocufying
|
||||
for (key, value) in problemtree.items(): # copy attributes over if not innocufying
|
||||
tree.set(key, value)
|
||||
|
||||
tree.text = problemtree.text
|
||||
@@ -466,7 +467,7 @@ class LoncapaProblem(object):
|
||||
self.responders = {}
|
||||
for response in tree.xpath('//' + "|//".join(response_tag_dict)):
|
||||
response_id_str = self.problem_id + "_" + str(response_id)
|
||||
response.set('id',response_id_str) # create and save ID for this response
|
||||
response.set('id', response_id_str) # create and save ID for this response
|
||||
response_id += 1
|
||||
|
||||
answer_id = 1
|
||||
@@ -478,7 +479,7 @@ class LoncapaProblem(object):
|
||||
entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id)
|
||||
answer_id = answer_id + 1
|
||||
|
||||
responder = response_tag_dict[response.tag](response, inputfields, self.context, self.system) # instantiate capa Response
|
||||
responder = response_tag_dict[response.tag](response, inputfields, self.context, self.system) # instantiate capa Response
|
||||
self.responders[response] = responder # save in list in self
|
||||
|
||||
# get responder answers (do this only once, since there may be a performance cost, eg with externalresponse)
|
||||
@@ -487,9 +488,9 @@ class LoncapaProblem(object):
|
||||
try:
|
||||
self.responder_answers[response] = self.responders[response].get_answers()
|
||||
except:
|
||||
log.debug('responder %s failed to properly return get_answers()' % self.responders[response]) # FIXME
|
||||
log.debug('responder %s failed to properly return get_answers()' % self.responders[response]) # FIXME
|
||||
raise
|
||||
|
||||
|
||||
# <solution>...</solution> may not be associated with any specific response; give IDs for those separately
|
||||
# TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i).
|
||||
solution_id = 1
|
||||
|
||||
@@ -34,9 +34,10 @@ class DemoSystem(object):
|
||||
context_dict.update(context)
|
||||
return self.lookup.get_template(template_filename).render(**context_dict)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Check Problem Files')
|
||||
parser.add_argument("command", choices=['test', 'show']) # Watch? Render? Open?
|
||||
parser.add_argument("command", choices=['test', 'show']) # Watch? Render? Open?
|
||||
parser.add_argument("files", nargs="+", type=argparse.FileType('r'))
|
||||
parser.add_argument("--seed", required=False, type=int)
|
||||
parser.add_argument("--log-level", required=False, default="INFO",
|
||||
@@ -67,13 +68,14 @@ def main():
|
||||
|
||||
# In case we want to do anything else here.
|
||||
|
||||
|
||||
def command_show(problem):
|
||||
"""Display the text for this problem"""
|
||||
print problem.get_html()
|
||||
|
||||
|
||||
|
||||
def command_test(problem):
|
||||
# We're going to trap stdout/stderr from the problems (yes, some print)
|
||||
# We're going to trap stdout/stderr from the problems (yes, some print)
|
||||
old_stdout, old_stderr = sys.stdout, sys.stderr
|
||||
try:
|
||||
sys.stdout = StringIO()
|
||||
@@ -82,7 +84,7 @@ def command_test(problem):
|
||||
check_that_suggested_answers_work(problem)
|
||||
check_that_blanks_fail(problem)
|
||||
|
||||
log_captured_output(sys.stdout,
|
||||
log_captured_output(sys.stdout,
|
||||
"captured stdout from {0}".format(problem))
|
||||
log_captured_output(sys.stderr,
|
||||
"captured stderr from {0}".format(problem))
|
||||
@@ -91,9 +93,10 @@ def command_test(problem):
|
||||
finally:
|
||||
sys.stdout, sys.stderr = old_stdout, old_stderr
|
||||
|
||||
|
||||
def check_that_blanks_fail(problem):
|
||||
"""Leaving it blank should never work. Neither should a space."""
|
||||
blank_answers = dict((answer_id, u"")
|
||||
blank_answers = dict((answer_id, u"")
|
||||
for answer_id in problem.get_question_answers())
|
||||
grading_results = problem.grade_answers(blank_answers)
|
||||
try:
|
||||
@@ -113,7 +116,7 @@ def check_that_suggested_answers_work(problem):
|
||||
* Displayed answers use units but acceptable ones do not.
|
||||
- L1e0.xml
|
||||
- Presents itself as UndefinedVariable (when it tries to pass to calc)
|
||||
* "a or d" is what's displayed, but only "a" or "d" is accepted, not the
|
||||
* "a or d" is what's displayed, but only "a" or "d" is accepted, not the
|
||||
string "a or d".
|
||||
- L1-e00.xml
|
||||
"""
|
||||
@@ -129,14 +132,14 @@ def check_that_suggested_answers_work(problem):
|
||||
log.debug("Real answers: {0}".format(real_answers))
|
||||
if real_answers:
|
||||
try:
|
||||
real_results = dict((answer_id, result) for answer_id, result
|
||||
real_results = dict((answer_id, result) for answer_id, result
|
||||
in problem.grade_answers(all_answers).items()
|
||||
if answer_id in real_answers)
|
||||
log.debug(real_results)
|
||||
assert(all(result == 'correct'
|
||||
for answer_id, result in real_results.items()))
|
||||
except UndefinedVariable as uv_exc:
|
||||
log.error("The variable \"{0}\" specified in the ".format(uv_exc) +
|
||||
log.error("The variable \"{0}\" specified in the ".format(uv_exc) +
|
||||
"solution isn't recognized (is it a units measure?).")
|
||||
except AssertionError:
|
||||
log.error("The following generated answers were not accepted for {0}:"
|
||||
@@ -148,6 +151,7 @@ def check_that_suggested_answers_work(problem):
|
||||
log.error("Uncaught error in {0}".format(problem))
|
||||
log.exception(ex)
|
||||
|
||||
|
||||
def log_captured_output(output_stream, stream_name):
|
||||
output_stream.seek(0)
|
||||
output_text = output_stream.read()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
# Used by responsetypes and capa_problem
|
||||
|
||||
|
||||
class CorrectMap(object):
|
||||
'''
|
||||
Stores map between answer_id and response evaluation result for each question
|
||||
@@ -18,11 +19,11 @@ class CorrectMap(object):
|
||||
|
||||
Behaves as a dict.
|
||||
'''
|
||||
def __init__(self,*args,**kwargs):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.cmap = dict() # start with empty dict
|
||||
self.items = self.cmap.items
|
||||
self.keys = self.cmap.keys
|
||||
self.set(*args,**kwargs)
|
||||
self.set(*args, **kwargs)
|
||||
|
||||
def __getitem__(self, *args, **kwargs):
|
||||
return self.cmap.__getitem__(*args, **kwargs)
|
||||
@@ -35,9 +36,9 @@ class CorrectMap(object):
|
||||
self.cmap[answer_id] = {'correctness': correctness,
|
||||
'npoints': npoints,
|
||||
'msg': msg,
|
||||
'hint' : hint,
|
||||
'hintmode' : hintmode,
|
||||
'queuekey' : queuekey,
|
||||
'hint': hint,
|
||||
'hintmode': hintmode,
|
||||
'queuekey': queuekey,
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
@@ -49,69 +50,69 @@ class CorrectMap(object):
|
||||
'''
|
||||
return self.cmap
|
||||
|
||||
def set_dict(self,correct_map):
|
||||
def set_dict(self, correct_map):
|
||||
'''
|
||||
set internal dict to provided correct_map dict
|
||||
for graceful migration, if correct_map is a one-level dict, then convert it to the new
|
||||
dict of dicts format.
|
||||
'''
|
||||
if correct_map and not (type(correct_map[correct_map.keys()[0]])==dict):
|
||||
if correct_map and not (type(correct_map[correct_map.keys()[0]]) == dict):
|
||||
self.__init__() # empty current dict
|
||||
for k in correct_map: self.set(k,correct_map[k]) # create new dict entries
|
||||
for k in correct_map: self.set(k, correct_map[k]) # create new dict entries
|
||||
else:
|
||||
self.cmap = correct_map
|
||||
|
||||
def is_correct(self,answer_id):
|
||||
def is_correct(self, answer_id):
|
||||
if answer_id in self.cmap: return self.cmap[answer_id]['correctness'] == 'correct'
|
||||
return None
|
||||
|
||||
def is_queued(self,answer_id):
|
||||
def is_queued(self, answer_id):
|
||||
return answer_id in self.cmap and self.cmap[answer_id]['queuekey'] is not None
|
||||
|
||||
def is_right_queuekey(self, answer_id, test_key):
|
||||
return answer_id in self.cmap and self.cmap[answer_id]['queuekey'] == test_key
|
||||
|
||||
def get_npoints(self,answer_id):
|
||||
def get_npoints(self, answer_id):
|
||||
if self.is_correct(answer_id):
|
||||
npoints = self.cmap[answer_id].get('npoints',1) # default to 1 point if correct
|
||||
npoints = self.cmap[answer_id].get('npoints', 1) # default to 1 point if correct
|
||||
return npoints or 1
|
||||
return 0 # if not correct, return 0
|
||||
|
||||
def set_property(self,answer_id,property,value):
|
||||
def set_property(self, answer_id, property, value):
|
||||
if answer_id in self.cmap: self.cmap[answer_id][property] = value
|
||||
else: self.cmap[answer_id] = {property:value}
|
||||
else: self.cmap[answer_id] = {property: value}
|
||||
|
||||
def get_property(self,answer_id,property,default=None):
|
||||
if answer_id in self.cmap: return self.cmap[answer_id].get(property,default)
|
||||
def get_property(self, answer_id, property, default=None):
|
||||
if answer_id in self.cmap: return self.cmap[answer_id].get(property, default)
|
||||
return default
|
||||
|
||||
def get_correctness(self,answer_id):
|
||||
return self.get_property(answer_id,'correctness')
|
||||
def get_correctness(self, answer_id):
|
||||
return self.get_property(answer_id, 'correctness')
|
||||
|
||||
def get_msg(self,answer_id):
|
||||
return self.get_property(answer_id,'msg','')
|
||||
def get_msg(self, answer_id):
|
||||
return self.get_property(answer_id, 'msg', '')
|
||||
|
||||
def get_hint(self,answer_id):
|
||||
return self.get_property(answer_id,'hint','')
|
||||
def get_hint(self, answer_id):
|
||||
return self.get_property(answer_id, 'hint', '')
|
||||
|
||||
def get_hintmode(self,answer_id):
|
||||
return self.get_property(answer_id,'hintmode',None)
|
||||
def get_hintmode(self, answer_id):
|
||||
return self.get_property(answer_id, 'hintmode', None)
|
||||
|
||||
def set_hint_and_mode(self,answer_id,hint,hintmode):
|
||||
def set_hint_and_mode(self, answer_id, hint, hintmode):
|
||||
'''
|
||||
- hint : (string) HTML text for hint
|
||||
- hintmode : (string) mode for hint display ('always' or 'on_request')
|
||||
'''
|
||||
self.set_property(answer_id,'hint',hint)
|
||||
self.set_property(answer_id,'hintmode',hintmode)
|
||||
self.set_property(answer_id, 'hint', hint)
|
||||
self.set_property(answer_id, 'hintmode', hintmode)
|
||||
|
||||
def update(self,other_cmap):
|
||||
def update(self, other_cmap):
|
||||
'''
|
||||
Update this CorrectMap with the contents of another CorrectMap
|
||||
'''
|
||||
if not isinstance(other_cmap,CorrectMap):
|
||||
if not isinstance(other_cmap, CorrectMap):
|
||||
raise Exception('CorrectMap.update called with invalid argument %s' % other_cmap)
|
||||
self.cmap.update(other_cmap.get_dict())
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
""" Standard resistor codes.
|
||||
http://en.wikipedia.org/wiki/Electronic_color_code
|
||||
"""
|
||||
E6=[10,15,22,33,47,68]
|
||||
E12=[10,12,15,18,22,27,33,39,47,56,68,82]
|
||||
E24=[10,12,15,18,22,27,33,39,47,56,68,82,11,13,16,20,24,30,36,43,51,62,75,91]
|
||||
E48=[100,121,147,178,215,261,316,383,464,562,681,825,105,127,154,187,226,274,332,402,487,590,715,866,110,133,162,196,237,287,348,422,511,619,750,909,115,140,169,205,249,301,365,442,536,649,787,953]
|
||||
E96=[100,121,147,178,215,261,316,383,464,562,681,825,102,124,150,182,221,267,324,392,475,576,698,845,105,127,154,187,226,274,332,402,487,590,715,866,107,130,158,191,232,280,340,412,499,604,732,887,110,133,162,196,237,287,348,422,511,619,750,909,113,137,165,200,243,294,357,432,523,634,768,931,115,140,169,205,249,301,365,442,536,649,787,953,118,143,174,210,255,309,374,453,549,665,806,976]
|
||||
E192=[100,121,147,178,215,261,316,383,464,562,681,825,101,123,149,180,218,264,320,388,470,569,690,835,102,124,150,182,221,267,324,392,475,576,698,845,104,126,152,184,223,271,328,397,481,583,706,856,105,127,154,187,226,274,332,402,487,590,715,866,106,129,156,189,229,277,336,407,493,597,723,876,107,130,158,191,232,280,340,412,499,604,732,887,109,132,160,193,234,284,344,417,505,612,741,898,110,133,162,196,237,287,348,422,511,619,750,909,111,135,164,198,240,291,352,427,517,626,759,920,113,137,165,200,243,294,357,432,523,634,768,931,114,138,167,203,246,298,361,437,530,642,777,942,115,140,169,205,249,301,365,442,536,649,787,953,117,142,172,208,252,305,370,448,542,657,796,965,118,143,174,210,255,309,374,453,549,665,806,976,120,145,176,213,258,312,379,459,556,673,816,988]
|
||||
|
||||
E6 = [10, 15, 22, 33, 47, 68]
|
||||
E12 = [10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82]
|
||||
E24 = [10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82, 11, 13, 16, 20, 24, 30, 36, 43, 51, 62, 75, 91]
|
||||
E48 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953]
|
||||
E96 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 102, 124, 150, 182, 221, 267, 324, 392, 475, 576, 698, 845, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 107, 130, 158, 191, 232, 280, 340, 412, 499, 604, 732, 887, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 113, 137, 165, 200, 243, 294, 357, 432, 523, 634, 768, 931, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953, 118, 143, 174, 210, 255, 309, 374, 453, 549, 665, 806, 976]
|
||||
E192 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 101, 123, 149, 180, 218, 264, 320, 388, 470, 569, 690, 835, 102, 124, 150, 182, 221, 267, 324, 392, 475, 576, 698, 845, 104, 126, 152, 184, 223, 271, 328, 397, 481, 583, 706, 856, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 106, 129, 156, 189, 229, 277, 336, 407, 493, 597, 723, 876, 107, 130, 158, 191, 232, 280, 340, 412, 499, 604, 732, 887, 109, 132, 160, 193, 234, 284, 344, 417, 505, 612, 741, 898, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 111, 135, 164, 198, 240, 291, 352, 427, 517, 626, 759, 920, 113, 137, 165, 200, 243, 294, 357, 432, 523, 634, 768, 931, 114, 138, 167, 203, 246, 298, 361, 437, 530, 642, 777, 942, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953, 117, 142, 172, 208, 252, 305, 370, 448, 542, 657, 796, 965, 118, 143, 174, 210, 255, 309, 374, 453, 549, 665, 806, 976, 120, 145, 176, 213, 258, 312, 379, 459, 556, 673, 816, 988]
|
||||
|
||||
@@ -26,37 +26,39 @@ Each input type takes the xml tree as 'element', the previous answer as 'value',
|
||||
|
||||
import logging
|
||||
import re
|
||||
import shlex # for splitting quoted strings
|
||||
import shlex # for splitting quoted strings
|
||||
|
||||
from lxml import etree
|
||||
import xml.sax.saxutils as saxutils
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
|
||||
def get_input_xml_tags():
|
||||
''' Eventually, this will be for all registered input types '''
|
||||
return SimpleInput.get_xml_tags()
|
||||
|
||||
class SimpleInput():# XModule
|
||||
|
||||
class SimpleInput():# XModule
|
||||
'''
|
||||
Type for simple inputs -- plain HTML with a form element
|
||||
'''
|
||||
|
||||
xml_tags = {} ## Maps tags to functions
|
||||
xml_tags = {} # # Maps tags to functions
|
||||
|
||||
def __init__(self, system, xml, item_id = None, track_url=None, state=None, use = 'capa_input'):
|
||||
def __init__(self, system, xml, item_id=None, track_url=None, state=None, use='capa_input'):
|
||||
'''
|
||||
Instantiate a SimpleInput class. Arguments:
|
||||
|
||||
- system : I4xSystem instance which provides OS, rendering, and user context
|
||||
- system : ModuleSystem instance which provides OS, rendering, and user context
|
||||
- xml : Element tree of this Input element
|
||||
- item_id : id for this input element (assigned by capa_problem.LoncapProblem) - string
|
||||
- track_url : URL used for tracking - string
|
||||
- state : a dictionary with optional keys:
|
||||
- state : a dictionary with optional keys:
|
||||
* Value
|
||||
* ID
|
||||
* Status (answered, unanswered, unsubmitted)
|
||||
* Feedback (dictionary containing keys for hints, errors, or other
|
||||
* Feedback (dictionary containing keys for hints, errors, or other
|
||||
feedback from previous attempt)
|
||||
- use :
|
||||
'''
|
||||
@@ -66,11 +68,11 @@ class SimpleInput():# XModule
|
||||
self.system = system
|
||||
if not state: state = {}
|
||||
|
||||
## ID should only come from one place.
|
||||
## ID should only come from one place.
|
||||
## If it comes from multiple, we use state first, XML second, and parameter
|
||||
## third. Since we don't make this guarantee, we can swap this around in
|
||||
## the future if there's a more logical order.
|
||||
if item_id: self.id = item_id
|
||||
## third. Since we don't make this guarantee, we can swap this around in
|
||||
## the future if there's a more logical order.
|
||||
if item_id: self.id = item_id
|
||||
if xml.get('id'): self.id = xml.get('id')
|
||||
if 'id' in state: self.id = state['id']
|
||||
|
||||
@@ -81,14 +83,14 @@ class SimpleInput():# XModule
|
||||
self.msg = ''
|
||||
feedback = state.get('feedback')
|
||||
if feedback is not None:
|
||||
self.msg = feedback.get('message','')
|
||||
self.hint = feedback.get('hint','')
|
||||
self.hintmode = feedback.get('hintmode',None)
|
||||
|
||||
self.msg = feedback.get('message', '')
|
||||
self.hint = feedback.get('hint', '')
|
||||
self.hintmode = feedback.get('hintmode', None)
|
||||
|
||||
# put hint above msg if to be displayed
|
||||
if self.hintmode == 'always':
|
||||
self.msg = self.hint + ('<br/.>' if self.msg else '') + self.msg
|
||||
|
||||
|
||||
self.status = 'unanswered'
|
||||
if 'status' in state:
|
||||
self.status = state['status']
|
||||
@@ -104,17 +106,20 @@ class SimpleInput():# XModule
|
||||
def get_html(self):
|
||||
return self.xml_tags[self.tag](self.xml, self.value, self.status, self.system.render_template, self.msg)
|
||||
|
||||
|
||||
def register_render_function(fn, names=None, cls=SimpleInput):
|
||||
if names is None:
|
||||
SimpleInput.xml_tags[fn.__name__] = fn
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
def wrapped():
|
||||
return fn
|
||||
return wrapped
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@register_render_function
|
||||
def optioninput(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
@@ -124,7 +129,7 @@ def optioninput(element, value, status, render_template, msg=''):
|
||||
|
||||
<optioninput options="('Up','Down')" correct="Up"/><text>The location of the sky</text>
|
||||
'''
|
||||
eid=element.get('id')
|
||||
eid = element.get('id')
|
||||
options = element.get('options')
|
||||
if not options:
|
||||
raise Exception("[courseware.capa.inputtypes.optioninput] Missing options specification in " + etree.tostring(element))
|
||||
@@ -134,14 +139,14 @@ def optioninput(element, value, status, render_template, msg=''):
|
||||
oset = [x[1:-1] for x in list(oset)]
|
||||
|
||||
# osetdict = dict([('option_%s_%s' % (eid,x),oset[x]) for x in range(len(oset)) ]) # make dict with IDs
|
||||
osetdict = [(oset[x],oset[x]) for x in range(len(oset)) ] # make ordered list with (key,value) same
|
||||
osetdict = [(oset[x], oset[x]) for x in range(len(oset))] # make ordered list with (key,value) same
|
||||
# TODO: allow ordering to be randomized
|
||||
|
||||
context={'id':eid,
|
||||
'value':value,
|
||||
'state':status,
|
||||
'msg':msg,
|
||||
'options':osetdict,
|
||||
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'msg': msg,
|
||||
'options': osetdict,
|
||||
}
|
||||
|
||||
html = render_template("optioninput.html", context)
|
||||
@@ -149,6 +154,7 @@ def optioninput(element, value, status, render_template, msg=''):
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
# TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of
|
||||
# desired semantics.
|
||||
@register_render_function
|
||||
@@ -159,23 +165,23 @@ def choicegroup(element, value, status, render_template, msg=''):
|
||||
TODO: allow order of choices to be randomized, following lon-capa spec. Use "location" attribute,
|
||||
ie random, top, bottom.
|
||||
'''
|
||||
eid=element.get('id')
|
||||
eid = element.get('id')
|
||||
if element.get('type') == "MultipleChoice":
|
||||
type="radio"
|
||||
type = "radio"
|
||||
elif element.get('type') == "TrueFalse":
|
||||
type="checkbox"
|
||||
type = "checkbox"
|
||||
else:
|
||||
type="radio"
|
||||
choices=[]
|
||||
type = "radio"
|
||||
choices = []
|
||||
for choice in element:
|
||||
if not choice.tag=='choice':
|
||||
if not choice.tag == 'choice':
|
||||
raise Exception("[courseware.capa.inputtypes.choicegroup] Error only <choice> tags should be immediate children of a <choicegroup>, found %s instead" % choice.tag)
|
||||
ctext = ""
|
||||
ctext += ''.join([etree.tostring(x) for x in choice]) # TODO: what if choice[0] has math tags in it?
|
||||
ctext += ''.join([etree.tostring(x) for x in choice]) # TODO: what if choice[0] has math tags in it?
|
||||
if choice.text is not None:
|
||||
ctext += choice.text # TODO: fix order?
|
||||
choices.append((choice.get("name"),ctext))
|
||||
context={'id':eid, 'value':value, 'state':status, 'input_type':type, 'choices':choices, 'inline':True, 'name_array_suffix':''}
|
||||
choices.append((choice.get("name"), ctext))
|
||||
context = {'id': eid, 'value': value, 'state': status, 'input_type': type, 'choices': choices, 'inline': True, 'name_array_suffix': ''}
|
||||
html = render_template("choicegroup.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
@@ -193,9 +199,9 @@ def extract_choices(element):
|
||||
choices = []
|
||||
|
||||
for choice in element:
|
||||
if not choice.tag=='choice':
|
||||
if not choice.tag == 'choice':
|
||||
raise Exception("[courseware.capa.inputtypes.extract_choices] \
|
||||
Expected a <choice> tag; got %s instead"
|
||||
Expected a <choice> tag; got %s instead"
|
||||
% choice.tag)
|
||||
choice_text = ''.join([etree.tostring(x) for x in choice])
|
||||
|
||||
@@ -203,6 +209,7 @@ def extract_choices(element):
|
||||
|
||||
return choices
|
||||
|
||||
|
||||
# TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of
|
||||
# desired semantics.
|
||||
@register_render_function
|
||||
@@ -211,15 +218,16 @@ def radiogroup(element, value, status, render_template, msg=''):
|
||||
Radio button inputs: (multiple choice)
|
||||
'''
|
||||
|
||||
eid=element.get('id')
|
||||
eid = element.get('id')
|
||||
|
||||
choices = extract_choices(element)
|
||||
|
||||
context = { 'id':eid, 'value':value, 'state':status, 'input_type': 'radio', 'choices':choices, 'inline': False, 'name_array_suffix': '[]' }
|
||||
context = {'id': eid, 'value': value, 'state': status, 'input_type': 'radio', 'choices': choices, 'inline': False, 'name_array_suffix': '[]'}
|
||||
|
||||
html = render_template("choicegroup.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
|
||||
# TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of
|
||||
# desired semantics.
|
||||
@register_render_function
|
||||
@@ -228,44 +236,46 @@ def checkboxgroup(element, value, status, render_template, msg=''):
|
||||
Checkbox inputs: (select one or more choices)
|
||||
'''
|
||||
|
||||
eid=element.get('id')
|
||||
eid = element.get('id')
|
||||
|
||||
choices = extract_choices(element)
|
||||
|
||||
context = { 'id':eid, 'value':value, 'state':status, 'input_type': 'checkbox', 'choices':choices, 'inline': False, 'name_array_suffix': '[]' }
|
||||
context = {'id': eid, 'value': value, 'state': status, 'input_type': 'checkbox', 'choices': choices, 'inline': False, 'name_array_suffix': '[]'}
|
||||
|
||||
html = render_template("choicegroup.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
|
||||
@register_render_function
|
||||
def textline(element, value, status, render_template, msg=""):
|
||||
'''
|
||||
Simple text line input, with optional size specification.
|
||||
'''
|
||||
if element.get('math') or element.get('dojs'): # 'dojs' flag is temporary, for backwards compatibility with 8.02x
|
||||
return SimpleInput.xml_tags['textline_dynamath'](element,value,status,render_template,msg)
|
||||
eid=element.get('id')
|
||||
return SimpleInput.xml_tags['textline_dynamath'](element, value, status, render_template, msg)
|
||||
eid = element.get('id')
|
||||
if eid is None:
|
||||
msg = 'textline has no id: it probably appears outside of a known response type'
|
||||
msg += "\nSee problem XML source line %s" % getattr(element,'sourceline','<unavailable>')
|
||||
msg += "\nSee problem XML source line %s" % getattr(element, 'sourceline', '<unavailable>')
|
||||
raise Exception(msg)
|
||||
count = int(eid.split('_')[-2])-1 # HACK
|
||||
count = int(eid.split('_')[-2]) - 1 # HACK
|
||||
size = element.get('size')
|
||||
hidden = element.get('hidden','') # if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
escapedict = {'"': '"'}
|
||||
value = saxutils.escape(value,escapedict) # otherwise, answers with quotes in them crashes the system!
|
||||
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg': msg, 'hidden': hidden}
|
||||
value = saxutils.escape(value, escapedict) # otherwise, answers with quotes in them crashes the system!
|
||||
context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, 'msg': msg, 'hidden': hidden}
|
||||
html = render_template("textinput.html", context)
|
||||
try:
|
||||
xhtml = etree.XML(html)
|
||||
except Exception as err:
|
||||
if True: # TODO needs to be self.system.DEBUG - but can't access system
|
||||
if True: # TODO needs to be self.system.DEBUG - but can't access system
|
||||
log.debug('[inputtypes.textline] failed to parse XML for:\n%s' % html)
|
||||
raise
|
||||
return xhtml
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@register_render_function
|
||||
def textline_dynamath(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
@@ -278,16 +288,17 @@ def textline_dynamath(element, value, status, render_template, msg=''):
|
||||
uses a <span id=display_eid>`{::}`</span>
|
||||
and a hidden textarea with id=input_eid_fromjs for the mathjax rendering and return.
|
||||
'''
|
||||
eid=element.get('id')
|
||||
count = int(eid.split('_')[-2])-1 # HACK
|
||||
eid = element.get('id')
|
||||
count = int(eid.split('_')[-2]) - 1 # HACK
|
||||
size = element.get('size')
|
||||
hidden = element.get('hidden','') # if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size,
|
||||
'msg':msg, 'hidden' : hidden,
|
||||
hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size,
|
||||
'msg': msg, 'hidden': hidden,
|
||||
}
|
||||
html = render_template("textinput_dynamath.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
## TODO: Make a wrapper for <codeinput>
|
||||
@register_render_function
|
||||
@@ -297,31 +308,32 @@ def textbox(element, value, status, render_template, msg=''):
|
||||
evaluating the code, eg error messages, and output from the code tests.
|
||||
|
||||
'''
|
||||
eid=element.get('id')
|
||||
count = int(eid.split('_')[-2])-1 # HACK
|
||||
eid = element.get('id')
|
||||
count = int(eid.split('_')[-2]) - 1 # HACK
|
||||
size = element.get('size')
|
||||
rows = element.get('rows') or '30'
|
||||
cols = element.get('cols') or '80'
|
||||
mode = element.get('mode') or 'python' # mode for CodeMirror, eg "python" or "xml"
|
||||
hidden = element.get('hidden','') # if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
linenumbers = element.get('linenumbers') # for CodeMirror
|
||||
if not value: value = element.text # if no student input yet, then use the default input given by the problem
|
||||
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg':msg,
|
||||
'mode':mode, 'linenumbers':linenumbers,
|
||||
'rows':rows, 'cols':cols,
|
||||
'hidden' : hidden,
|
||||
hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
linenumbers = element.get('linenumbers') # for CodeMirror
|
||||
if not value: value = element.text # if no student input yet, then use the default input given by the problem
|
||||
context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, 'msg': msg,
|
||||
'mode': mode, 'linenumbers': linenumbers,
|
||||
'rows': rows, 'cols': cols,
|
||||
'hidden': hidden,
|
||||
}
|
||||
html = render_template("textbox.html", context)
|
||||
try:
|
||||
xhtml = etree.XML(html)
|
||||
except Exception as err:
|
||||
newmsg = 'error %s in rendering message' % (str(err).replace('<','<'))
|
||||
newmsg += '<br/>Original message: %s' % msg.replace('<','<')
|
||||
newmsg = 'error %s in rendering message' % (str(err).replace('<', '<'))
|
||||
newmsg += '<br/>Original message: %s' % msg.replace('<', '<')
|
||||
context['msg'] = newmsg
|
||||
html = render_template("textbox.html", context)
|
||||
xhtml = etree.XML(html)
|
||||
return xhtml
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
@register_render_function
|
||||
def schematic(element, value, status, render_template, msg=''):
|
||||
@@ -333,19 +345,20 @@ def schematic(element, value, status, render_template, msg=''):
|
||||
initial_value = element.get('initial_value')
|
||||
submit_analyses = element.get('submit_analyses')
|
||||
context = {
|
||||
'id':eid,
|
||||
'value':value,
|
||||
'initial_value':initial_value,
|
||||
'state':status,
|
||||
'width':width,
|
||||
'height':height,
|
||||
'parts':parts,
|
||||
'analyses':analyses,
|
||||
'submit_analyses':submit_analyses,
|
||||
'id': eid,
|
||||
'value': value,
|
||||
'initial_value': initial_value,
|
||||
'state': status,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'parts': parts,
|
||||
'analyses': analyses,
|
||||
'submit_analyses': submit_analyses,
|
||||
}
|
||||
html = render_template("schematicinput.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
### TODO: Move out of inputtypes
|
||||
@register_render_function
|
||||
@@ -357,17 +370,17 @@ def math(element, value, status, render_template, msg=''):
|
||||
Examples:
|
||||
|
||||
<m display="jsmath">$\displaystyle U(r)=4 U_0 </m>
|
||||
<m>$r_0$</m>
|
||||
<m>$r_0$</m>
|
||||
|
||||
We convert these to [mathjax]...[/mathjax] and [mathjaxinline]...[/mathjaxinline]
|
||||
|
||||
TODO: use shorter tags (but this will require converting problem XML files!)
|
||||
'''
|
||||
mathstr = re.sub('\$(.*)\$','[mathjaxinline]\\1[/mathjaxinline]',element.text)
|
||||
mathstr = re.sub('\$(.*)\$', '[mathjaxinline]\\1[/mathjaxinline]', element.text)
|
||||
mtag = 'mathjax'
|
||||
if not '\\displaystyle' in mathstr: mtag += 'inline'
|
||||
else: mathstr = mathstr.replace('\\displaystyle','')
|
||||
mathstr = mathstr.replace('mathjaxinline]','%s]'%mtag)
|
||||
else: mathstr = mathstr.replace('\\displaystyle', '')
|
||||
mathstr = mathstr.replace('mathjaxinline]', '%s]' % mtag)
|
||||
|
||||
#if '\\displaystyle' in mathstr:
|
||||
# isinline = False
|
||||
@@ -376,13 +389,13 @@ def math(element, value, status, render_template, msg=''):
|
||||
# isinline = True
|
||||
# html = render_template("mathstring.html",{'mathstr':mathstr,'isinline':isinline,'tail':element.tail})
|
||||
|
||||
html = '<html><html>%s</html><html>%s</html></html>' % (mathstr,saxutils.escape(element.tail))
|
||||
html = '<html><html>%s</html><html>%s</html></html>' % (mathstr, saxutils.escape(element.tail))
|
||||
try:
|
||||
xhtml = etree.XML(html)
|
||||
except Exception as err:
|
||||
if False: # TODO needs to be self.system.DEBUG - but can't access system
|
||||
msg = "<html><font color='red'><p>Error %s</p>" % str(err).replace('<','<')
|
||||
msg += '<p>Failed to construct math expression from <pre>%s</pre></p>' % html.replace('<','<')
|
||||
if False: # TODO needs to be self.system.DEBUG - but can't access system
|
||||
msg = "<html><font color='red'><p>Error %s</p>" % str(err).replace('<', '<')
|
||||
msg += '<p>Failed to construct math expression from <pre>%s</pre></p>' % html.replace('<', '<')
|
||||
msg += "</font></html>"
|
||||
log.error(msg)
|
||||
return etree.XML(msg)
|
||||
@@ -393,6 +406,7 @@ def math(element, value, status, render_template, msg=''):
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@register_render_function
|
||||
def solution(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
@@ -401,19 +415,20 @@ def solution(element, value, status, render_template, msg=''):
|
||||
is pressed. Note that the solution content is NOT sent with the HTML. It is obtained
|
||||
by a JSON call.
|
||||
'''
|
||||
eid=element.get('id')
|
||||
eid = element.get('id')
|
||||
size = element.get('size')
|
||||
context = {'id':eid,
|
||||
'value':value,
|
||||
'state':status,
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'size': size,
|
||||
'msg':msg,
|
||||
'msg': msg,
|
||||
}
|
||||
html = render_template("solutionspan.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@register_render_function
|
||||
def imageinput(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
@@ -429,12 +444,12 @@ def imageinput(element, value, status, render_template, msg=''):
|
||||
width = element.get('width')
|
||||
|
||||
# if value is of the form [x,y] then parse it and send along coordinates of previous answer
|
||||
m = re.match('\[([0-9]+),([0-9]+)]',value.strip().replace(' ',''))
|
||||
m = re.match('\[([0-9]+),([0-9]+)]', value.strip().replace(' ', ''))
|
||||
if m:
|
||||
(gx,gy) = [int(x)-15 for x in m.groups()]
|
||||
(gx, gy) = [int(x) - 15 for x in m.groups()]
|
||||
else:
|
||||
(gx,gy) = (0,0)
|
||||
|
||||
(gx, gy) = (0, 0)
|
||||
|
||||
context = {
|
||||
'id': eid,
|
||||
'value': value,
|
||||
@@ -443,7 +458,7 @@ def imageinput(element, value, status, render_template, msg=''):
|
||||
'src': src,
|
||||
'gx': gx,
|
||||
'gy': gy,
|
||||
'state': status, # to change
|
||||
'state': status, # to change
|
||||
'msg': msg, # to change
|
||||
}
|
||||
html = render_template("imageinput.html", context)
|
||||
|
||||
@@ -1,1304 +0,0 @@
|
||||
#
|
||||
# File: courseware/capa/responsetypes.py
|
||||
#
|
||||
'''
|
||||
Problem response evaluation. Handles checking of student responses, of a variety of types.
|
||||
|
||||
Used by capa_problem.py
|
||||
'''
|
||||
|
||||
# standard library imports
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import numbers
|
||||
import numpy
|
||||
import random
|
||||
import re
|
||||
import requests
|
||||
import time
|
||||
import traceback
|
||||
import abc
|
||||
|
||||
# specific library imports
|
||||
from calc import evaluator, UndefinedVariable
|
||||
from correctmap import CorrectMap
|
||||
from util import *
|
||||
from lxml import etree
|
||||
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Exceptions
|
||||
|
||||
class LoncapaProblemError(Exception):
|
||||
'''
|
||||
Error in specification of a problem
|
||||
'''
|
||||
pass
|
||||
|
||||
class ResponseError(Exception):
|
||||
'''
|
||||
Error for failure in processing a response
|
||||
'''
|
||||
pass
|
||||
|
||||
class StudentInputError(Exception):
|
||||
pass
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
# Main base class for CAPA responsetypes
|
||||
|
||||
class LoncapaResponse(object):
|
||||
'''
|
||||
Base class for CAPA responsetypes. Each response type (ie a capa question,
|
||||
which is part of a capa problem) is represented as a subclass,
|
||||
which should provide the following methods:
|
||||
|
||||
- get_score : evaluate the given student answers, and return a CorrectMap
|
||||
- get_answers : provide a dict of the expected answers for this problem
|
||||
|
||||
Each subclass must also define the following attributes:
|
||||
|
||||
- response_tag : xhtml tag identifying this response (used in auto-registering)
|
||||
|
||||
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)
|
||||
- __unicode__ : unicode representation of this Response
|
||||
|
||||
Each response type may also specify the following attributes:
|
||||
|
||||
- max_inputfields : (int) maximum number of answer input fields (checked in __init__ if not None)
|
||||
- allowed_inputfields : list of allowed input fields (each a string) for this Response
|
||||
- required_attributes : list of required attributes (each a string) on the main response XML stanza
|
||||
- hint_tag : xhtml tag identifying hint associated with this response inside hintgroup
|
||||
|
||||
'''
|
||||
__metaclass__=abc.ABCMeta # abc = Abstract Base Class
|
||||
|
||||
response_tag = None
|
||||
hint_tag = None
|
||||
|
||||
max_inputfields = None
|
||||
allowed_inputfields = []
|
||||
required_attributes = []
|
||||
|
||||
def __init__(self, xml, inputfields, context, system=None):
|
||||
'''
|
||||
Init is passed the following arguments:
|
||||
|
||||
- xml : ElementTree of this Response
|
||||
- inputfields : ordered list of ElementTrees for each input entry field in this Response
|
||||
- context : script processor context
|
||||
- system : I4xSystem instance which provides OS, rendering, and user context
|
||||
|
||||
'''
|
||||
self.xml = xml
|
||||
self.inputfields = inputfields
|
||||
self.context = context
|
||||
self.system = system
|
||||
|
||||
for abox in inputfields:
|
||||
if abox.tag not in self.allowed_inputfields:
|
||||
msg = "%s: cannot have input field %s" % (unicode(self),abox.tag)
|
||||
msg += "\nSee XML source line %s" % getattr(xml,'sourceline','<unavailable>')
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
if self.max_inputfields and len(inputfields)>self.max_inputfields:
|
||||
msg = "%s: cannot have more than %s input fields" % (unicode(self),self.max_inputfields)
|
||||
msg += "\nSee XML source line %s" % getattr(xml,'sourceline','<unavailable>')
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
for prop in self.required_attributes:
|
||||
if not xml.get(prop):
|
||||
msg = "Error in problem specification: %s missing required attribute %s" % (unicode(self),prop)
|
||||
msg += "\nSee XML source line %s" % getattr(xml,'sourceline','<unavailable>')
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
self.answer_ids = [x.get('id') for x in self.inputfields] # ordered list of answer_id values for this response
|
||||
if self.max_inputfields==1:
|
||||
self.answer_id = self.answer_ids[0] # for convenience
|
||||
|
||||
self.default_answer_map = {} # dict for default answer map (provided in input elements)
|
||||
for entry in self.inputfields:
|
||||
answer = entry.get('correct_answer')
|
||||
if answer:
|
||||
self.default_answer_map[entry.get('id')] = contextualize_text(answer, self.context)
|
||||
|
||||
if hasattr(self,'setup_response'):
|
||||
self.setup_response()
|
||||
|
||||
def render_html(self,renderer):
|
||||
'''
|
||||
Return XHTML Element tree representation of this Response.
|
||||
|
||||
Arguments:
|
||||
|
||||
- renderer : procedure which produces HTML given an ElementTree
|
||||
'''
|
||||
tree = etree.Element('span') # render ourself as a <span> + our content
|
||||
for item in self.xml:
|
||||
item_xhtml = renderer(item) # call provided procedure to do the rendering
|
||||
if item_xhtml is not None: tree.append(item_xhtml)
|
||||
tree.tail = self.xml.tail
|
||||
return tree
|
||||
|
||||
def evaluate_answers(self,student_answers,old_cmap):
|
||||
'''
|
||||
Called by capa_problem.LoncapaProblem to evaluate student answers, and to
|
||||
generate hints (if any).
|
||||
|
||||
Returns the new CorrectMap, with (correctness,msg,hint,hintmode) for each answer_id.
|
||||
'''
|
||||
new_cmap = self.get_score(student_answers)
|
||||
self.get_hints(student_answers, new_cmap, old_cmap)
|
||||
# log.debug('new_cmap = %s' % new_cmap)
|
||||
return new_cmap
|
||||
|
||||
def get_hints(self, student_answers, new_cmap, old_cmap):
|
||||
'''
|
||||
Generate adaptive hints for this problem based on student answers, the old CorrectMap,
|
||||
and the new CorrectMap produced by get_score.
|
||||
|
||||
Does not return anything.
|
||||
|
||||
Modifies new_cmap, by adding hints to answer_id entries as appropriate.
|
||||
'''
|
||||
hintgroup = self.xml.find('hintgroup')
|
||||
if hintgroup is None: return
|
||||
|
||||
# hint specified by function?
|
||||
hintfn = hintgroup.get('hintfn')
|
||||
if hintfn:
|
||||
'''
|
||||
Hint is determined by a function defined in the <script> context; evaluate that function to obtain
|
||||
list of hint, hintmode for each answer_id.
|
||||
|
||||
The function should take arguments (answer_ids, student_answers, new_cmap, old_cmap)
|
||||
and it should modify new_cmap as appropriate.
|
||||
|
||||
We may extend this in the future to add another argument which provides a callback procedure
|
||||
to a social hint generation system.
|
||||
|
||||
'''
|
||||
if not hintfn in self.context:
|
||||
msg = 'missing specified hint function %s in script context' % hintfn
|
||||
msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','<unavailable>')
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
try:
|
||||
self.context[hintfn](self.answer_ids, student_answers, new_cmap, old_cmap)
|
||||
except Exception as err:
|
||||
msg = 'Error %s in evaluating hint function %s' % (err,hintfn)
|
||||
msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','<unavailable>')
|
||||
raise ResponseError(msg)
|
||||
return
|
||||
|
||||
# hint specified by conditions and text dependent on conditions (a-la Loncapa design)
|
||||
# see http://help.loncapa.org/cgi-bin/fom?file=291
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# <formularesponse samples="x@-5:5#11" id="11" answer="$answer">
|
||||
# <textline size="25" />
|
||||
# <hintgroup>
|
||||
# <formulahint samples="x@-5:5#11" answer="$wrongans" name="inversegrad"></formulahint>
|
||||
# <hintpart on="inversegrad">
|
||||
# <text>You have inverted the slope in the question. The slope is
|
||||
# (y2-y1)/(x2 - x1) you have the slope as (x2-x1)/(y2-y1).</text>
|
||||
# </hintpart>
|
||||
# </hintgroup>
|
||||
# </formularesponse>
|
||||
|
||||
if self.hint_tag is not None and hintgroup.find(self.hint_tag) is not None and hasattr(self,'check_hint_condition'):
|
||||
rephints = hintgroup.findall(self.hint_tag)
|
||||
hints_to_show = self.check_hint_condition(rephints,student_answers)
|
||||
hintmode = hintgroup.get('mode','always') # can be 'on_request' or 'always' (default)
|
||||
for hintpart in hintgroup.findall('hintpart'):
|
||||
if hintpart.get('on') in hints_to_show:
|
||||
hint_text = hintpart.find('text').text
|
||||
aid = self.answer_ids[-1] # make the hint appear after the last answer box in this response
|
||||
new_cmap.set_hint_and_mode(aid,hint_text,hintmode)
|
||||
log.debug('after hint: new_cmap = %s' % new_cmap)
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_score(self, student_answers):
|
||||
'''
|
||||
Return a CorrectMap for the answers expected vs given. This includes
|
||||
(correctness, npoints, msg) for each answer_id.
|
||||
|
||||
Arguments:
|
||||
|
||||
- student_answers : dict of (answer_id,answer) where answer = student input (string)
|
||||
- old_cmap : previous CorrectMap (may be empty); useful for analyzing or recording history of responses
|
||||
|
||||
'''
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_answers(self):
|
||||
'''
|
||||
Return a dict of (answer_id,answer_text) for each answer for this question.
|
||||
'''
|
||||
pass
|
||||
|
||||
def check_hint_condition(self,hxml_set,student_answers):
|
||||
'''
|
||||
Return a list of hints to show.
|
||||
|
||||
- hxml_set : list of Element trees, each specifying a condition to be satisfied for a named hint condition
|
||||
- student_answers : dict of student answers
|
||||
|
||||
Returns a list of names of hint conditions which were satisfied. Those are used to determine which hints are displayed.
|
||||
'''
|
||||
pass
|
||||
|
||||
def setup_response(self):
|
||||
pass
|
||||
|
||||
def __unicode__(self):
|
||||
return u'LoncapaProblem Response %s' % self.xml.tag
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
class ChoiceResponse(LoncapaResponse):
|
||||
'''
|
||||
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.
|
||||
|
||||
This response type allows for two inputtypes: radiogroups and checkbox
|
||||
groups. radiogroups are used when the student should select a single answer,
|
||||
and checkbox groups are used when the student may supply 0+ answers.
|
||||
Note: it is suggested to include a "None of the above" choice when no
|
||||
answer is correct for a checkboxgroup inputtype; this ensures that a student
|
||||
must actively mark something to get credit.
|
||||
|
||||
If two choices are marked as correct with a radiogroup, the student will
|
||||
have no way to get the answer right.
|
||||
|
||||
TODO: Allow for marking choices as 'optional' and 'required', which would
|
||||
not penalize a student for including optional answers and would also allow
|
||||
for questions in which the student can supply one out of a set of correct
|
||||
answers.This would also allow for survey-style questions in which all
|
||||
answers are correct.
|
||||
|
||||
Example:
|
||||
|
||||
<choiceresponse>
|
||||
<radiogroup>
|
||||
<choice correct="false">
|
||||
<text>This is a wrong answer.</text>
|
||||
</choice>
|
||||
<choice correct="true">
|
||||
<text>This is the right answer.</text>
|
||||
</choice>
|
||||
<choice correct="false">
|
||||
<text>This is another wrong answer.</text>
|
||||
</choice>
|
||||
</radiogroup>
|
||||
</choiceresponse>
|
||||
|
||||
In the above example, radiogroup can be replaced with checkboxgroup to allow
|
||||
the student to select more than one choice.
|
||||
|
||||
'''
|
||||
|
||||
response_tag = 'choiceresponse'
|
||||
max_inputfields = 1
|
||||
allowed_inputfields = ['checkboxgroup', 'radiogroup']
|
||||
|
||||
def setup_response(self):
|
||||
|
||||
self.assign_choice_names()
|
||||
|
||||
correct_xml = self.xml.xpath('//*[@id=$id]//choice[@correct="true"]',
|
||||
id=self.xml.get('id'))
|
||||
|
||||
self.correct_choices = set([choice.get('name') for choice in correct_xml])
|
||||
|
||||
def assign_choice_names(self):
|
||||
'''
|
||||
Initialize name attributes in <choice> tags for this response.
|
||||
'''
|
||||
|
||||
for index, choice in enumerate(self.xml.xpath('//*[@id=$id]//choice',
|
||||
id=self.xml.get('id'))):
|
||||
choice.set("name", "choice_"+str(index))
|
||||
|
||||
def get_score(self, student_answers):
|
||||
|
||||
student_answer = student_answers.get(self.answer_id, [])
|
||||
|
||||
if not isinstance(student_answer, list):
|
||||
student_answer = [student_answer]
|
||||
|
||||
student_answer = set(student_answer)
|
||||
|
||||
required_selected = len(self.correct_choices - student_answer) == 0
|
||||
no_extra_selected = len(student_answer - self.correct_choices) == 0
|
||||
|
||||
correct = required_selected & no_extra_selected
|
||||
|
||||
if correct:
|
||||
return CorrectMap(self.answer_id,'correct')
|
||||
else:
|
||||
return CorrectMap(self.answer_id,'incorrect')
|
||||
|
||||
def get_answers(self):
|
||||
return { self.answer_id : self.correct_choices }
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class MultipleChoiceResponse(LoncapaResponse):
|
||||
# TODO: handle direction and randomize
|
||||
snippets = [{'snippet': '''<multiplechoiceresponse direction="vertical" randomize="yes">
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice location="random" correct="false"><span>`a+b`<br/></span></choice>
|
||||
<choice location="random" correct="true"><span><math>a+b^2</math><br/></span></choice>
|
||||
<choice location="random" correct="false"><math>a+b+c</math></choice>
|
||||
<choice location="bottom" correct="false"><math>a+b+d</math></choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
'''}]
|
||||
|
||||
response_tag = 'multiplechoiceresponse'
|
||||
max_inputfields = 1
|
||||
allowed_inputfields = ['choicegroup']
|
||||
|
||||
def setup_response(self):
|
||||
self.mc_setup_response() # call secondary setup for MultipleChoice questions, to set name attributes
|
||||
|
||||
# define correct choices (after calling secondary setup)
|
||||
xml = self.xml
|
||||
cxml = xml.xpath('//*[@id=$id]//choice[@correct="true"]',id=xml.get('id'))
|
||||
self.correct_choices = [choice.get('name') for choice in cxml]
|
||||
|
||||
def mc_setup_response(self):
|
||||
'''
|
||||
Initialize name attributes in <choice> stanzas in the <choicegroup> in this response.
|
||||
'''
|
||||
i=0
|
||||
for response in self.xml.xpath("choicegroup"):
|
||||
rtype = response.get('type')
|
||||
if rtype not in ["MultipleChoice"]:
|
||||
response.set("type", "MultipleChoice") # force choicegroup to be MultipleChoice if not valid
|
||||
for choice in list(response):
|
||||
if choice.get("name") is None:
|
||||
choice.set("name", "choice_"+str(i))
|
||||
i+=1
|
||||
else:
|
||||
choice.set("name", "choice_"+choice.get("name"))
|
||||
|
||||
def get_score(self, student_answers):
|
||||
'''
|
||||
grade student response.
|
||||
'''
|
||||
# log.debug('%s: student_answers=%s, correct_choices=%s' % (unicode(self),student_answers,self.correct_choices))
|
||||
if self.answer_id in student_answers and student_answers[self.answer_id] in self.correct_choices:
|
||||
return CorrectMap(self.answer_id,'correct')
|
||||
else:
|
||||
return CorrectMap(self.answer_id,'incorrect')
|
||||
|
||||
def get_answers(self):
|
||||
return {self.answer_id:self.correct_choices}
|
||||
|
||||
class TrueFalseResponse(MultipleChoiceResponse):
|
||||
|
||||
response_tag = 'truefalseresponse'
|
||||
|
||||
def mc_setup_response(self):
|
||||
i=0
|
||||
for response in self.xml.xpath("choicegroup"):
|
||||
response.set("type", "TrueFalse")
|
||||
for choice in list(response):
|
||||
if choice.get("name") is None:
|
||||
choice.set("name", "choice_"+str(i))
|
||||
i+=1
|
||||
else:
|
||||
choice.set("name", "choice_"+choice.get("name"))
|
||||
|
||||
def get_score(self, student_answers):
|
||||
correct = set(self.correct_choices)
|
||||
answers = set(student_answers.get(self.answer_id, []))
|
||||
|
||||
if correct == answers:
|
||||
return CorrectMap( self.answer_id , 'correct')
|
||||
|
||||
return CorrectMap(self.answer_id ,'incorrect')
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class OptionResponse(LoncapaResponse):
|
||||
'''
|
||||
TODO: handle direction and randomize
|
||||
'''
|
||||
snippets = [{'snippet': '''<optionresponse direction="vertical" randomize="yes">
|
||||
<optioninput options="('Up','Down')" correct="Up"><text>The location of the sky</text></optioninput>
|
||||
<optioninput options="('Up','Down')" correct="Down"><text>The location of the earth</text></optioninput>
|
||||
</optionresponse>'''}]
|
||||
|
||||
response_tag = 'optionresponse'
|
||||
hint_tag = 'optionhint'
|
||||
allowed_inputfields = ['optioninput']
|
||||
|
||||
def setup_response(self):
|
||||
self.answer_fields = self.inputfields
|
||||
|
||||
def get_score(self, student_answers):
|
||||
# log.debug('%s: student_answers=%s' % (unicode(self),student_answers))
|
||||
cmap = CorrectMap()
|
||||
amap = self.get_answers()
|
||||
for aid in amap:
|
||||
if aid in student_answers and student_answers[aid]==amap[aid]:
|
||||
cmap.set(aid,'correct')
|
||||
else:
|
||||
cmap.set(aid,'incorrect')
|
||||
return cmap
|
||||
|
||||
def get_answers(self):
|
||||
amap = dict([(af.get('id'),af.get('correct')) for af in self.answer_fields])
|
||||
# log.debug('%s: expected answers=%s' % (unicode(self),amap))
|
||||
return amap
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class NumericalResponse(LoncapaResponse):
|
||||
|
||||
response_tag = 'numericalresponse'
|
||||
hint_tag = 'numericalhint'
|
||||
allowed_inputfields = ['textline']
|
||||
required_attributes = ['answer']
|
||||
max_inputfields = 1
|
||||
|
||||
def setup_response(self):
|
||||
xml = self.xml
|
||||
context = self.context
|
||||
self.correct_answer = contextualize_text(xml.get('answer'), context)
|
||||
try:
|
||||
self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default',
|
||||
id=xml.get('id'))[0]
|
||||
self.tolerance = contextualize_text(self.tolerance_xml, context)
|
||||
except Exception:
|
||||
self.tolerance = 0
|
||||
try:
|
||||
self.answer_id = xml.xpath('//*[@id=$id]//textline/@id',
|
||||
id=xml.get('id'))[0]
|
||||
except Exception:
|
||||
self.answer_id = None
|
||||
|
||||
def get_score(self, student_answers):
|
||||
'''Grade a numeric response '''
|
||||
student_answer = student_answers[self.answer_id]
|
||||
try:
|
||||
correct = compare_with_tolerance (evaluator(dict(),dict(),student_answer), complex(self.correct_answer), self.tolerance)
|
||||
# We should catch this explicitly.
|
||||
# I think this is just pyparsing.ParseException, calc.UndefinedVariable:
|
||||
# But we'd need to confirm
|
||||
except:
|
||||
raise StudentInputError('Invalid input -- please use a number only')
|
||||
|
||||
if correct:
|
||||
return CorrectMap(self.answer_id,'correct')
|
||||
else:
|
||||
return CorrectMap(self.answer_id,'incorrect')
|
||||
|
||||
# TODO: add check_hint_condition(self,hxml_set,student_answers)
|
||||
|
||||
def get_answers(self):
|
||||
return {self.answer_id:self.correct_answer}
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class StringResponse(LoncapaResponse):
|
||||
|
||||
response_tag = 'stringresponse'
|
||||
hint_tag = 'stringhint'
|
||||
allowed_inputfields = ['textline']
|
||||
required_attributes = ['answer']
|
||||
max_inputfields = 1
|
||||
|
||||
def setup_response(self):
|
||||
self.correct_answer = contextualize_text(self.xml.get('answer'), self.context).strip()
|
||||
|
||||
def get_score(self, student_answers):
|
||||
'''Grade a string response '''
|
||||
student_answer = student_answers[self.answer_id].strip()
|
||||
correct = self.check_string(self.correct_answer,student_answer)
|
||||
return CorrectMap(self.answer_id,'correct' if correct else 'incorrect')
|
||||
|
||||
def check_string(self,expected,given):
|
||||
if self.xml.get('type')=='ci': return given.lower() == expected.lower()
|
||||
return given == expected
|
||||
|
||||
def check_hint_condition(self,hxml_set,student_answers):
|
||||
given = student_answers[self.answer_id].strip()
|
||||
hints_to_show = []
|
||||
for hxml in hxml_set:
|
||||
name = hxml.get('name')
|
||||
correct_answer = contextualize_text(hxml.get('answer'),self.context).strip()
|
||||
if self.check_string(correct_answer,given): hints_to_show.append(name)
|
||||
log.debug('hints_to_show = %s' % hints_to_show)
|
||||
return hints_to_show
|
||||
|
||||
def get_answers(self):
|
||||
return {self.answer_id:self.correct_answer}
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class CustomResponse(LoncapaResponse):
|
||||
'''
|
||||
Custom response. The python code to be run should be in <answer>...</answer>
|
||||
or in a <script>...</script>
|
||||
'''
|
||||
snippets = [{'snippet': '''<customresponse>
|
||||
<text>
|
||||
<br/>
|
||||
Suppose that \(I(t)\) rises from \(0\) to \(I_S\) at a time \(t_0 \neq 0\)
|
||||
In the space provided below write an algebraic expression for \(I(t)\).
|
||||
<br/>
|
||||
<textline size="5" correct_answer="IS*u(t-t0)" />
|
||||
</text>
|
||||
<answer type="loncapa/python">
|
||||
correct=['correct']
|
||||
try:
|
||||
r = str(submission[0])
|
||||
except ValueError:
|
||||
correct[0] ='incorrect'
|
||||
r = '0'
|
||||
if not(r=="IS*u(t-t0)"):
|
||||
correct[0] ='incorrect'
|
||||
</answer>
|
||||
</customresponse>'''},
|
||||
{'snippet': '''<script type="loncapa/python"><![CDATA[
|
||||
|
||||
def sympy_check2():
|
||||
messages[0] = '%s:%s' % (submission[0],fromjs[0].replace('<','<'))
|
||||
#messages[0] = str(answers)
|
||||
correct[0] = 'correct'
|
||||
|
||||
]]>
|
||||
</script>
|
||||
|
||||
<customresponse cfn="sympy_check2" type="cs" expect="2.27E-39" dojs="math" size="30" answer="2.27E-39">
|
||||
<textline size="40" dojs="math" />
|
||||
<responseparam description="Numerical Tolerance" type="tolerance" default="0.00001" name="tol"/>
|
||||
</customresponse>'''}]
|
||||
|
||||
response_tag = 'customresponse'
|
||||
allowed_inputfields = ['textline','textbox']
|
||||
|
||||
def setup_response(self):
|
||||
xml = self.xml
|
||||
|
||||
# if <customresponse> has an "expect" (or "answer") attribute then save that
|
||||
self.expect = xml.get('expect') or xml.get('answer')
|
||||
self.myid = xml.get('id')
|
||||
|
||||
log.debug('answer_ids=%s' % self.answer_ids)
|
||||
|
||||
# the <answer>...</answer> stanza should be local to the current <customresponse>. So try looking there first.
|
||||
self.code = None
|
||||
answer = None
|
||||
try:
|
||||
answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0]
|
||||
except IndexError:
|
||||
# print "xml = ",etree.tostring(xml,pretty_print=True)
|
||||
|
||||
# if we have a "cfn" attribute then look for the function specified by cfn, in the problem context
|
||||
# ie the comparison function is defined in the <script>...</script> stanza instead
|
||||
cfn = xml.get('cfn')
|
||||
if cfn:
|
||||
log.debug("cfn = %s" % cfn)
|
||||
if cfn in self.context:
|
||||
self.code = self.context[cfn]
|
||||
else:
|
||||
msg = "%s: can't find cfn %s in context" % (unicode(self),cfn)
|
||||
msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','<unavailable>')
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
if not self.code:
|
||||
if answer is None:
|
||||
# raise Exception,"[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid
|
||||
log.error("[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid)
|
||||
self.code = ''
|
||||
else:
|
||||
answer_src = answer.get('src')
|
||||
if answer_src is not None:
|
||||
self.code = self.system.filesystem.open('src/'+answer_src).read()
|
||||
else:
|
||||
self.code = answer.text
|
||||
|
||||
def get_score(self, student_answers):
|
||||
'''
|
||||
student_answers is a dict with everything from request.POST, but with the first part
|
||||
of each key removed (the string before the first "_").
|
||||
'''
|
||||
|
||||
log.debug('%s: student_answers=%s' % (unicode(self),student_answers))
|
||||
|
||||
idset = sorted(self.answer_ids) # ordered list of answer id's
|
||||
try:
|
||||
submission = [student_answers[k] for k in idset] # ordered list of answers
|
||||
except Exception as err:
|
||||
msg = '[courseware.capa.responsetypes.customresponse] error getting student answer from %s' % student_answers
|
||||
msg += '\n idset = %s, error = %s' % (idset,err)
|
||||
log.error(msg)
|
||||
raise Exception(msg)
|
||||
|
||||
# global variable in context which holds the Presentation MathML from dynamic math input
|
||||
dynamath = [ student_answers.get(k+'_dynamath',None) for k in idset ] # ordered list of dynamath responses
|
||||
|
||||
# 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>')
|
||||
|
||||
correct = ['unknown'] * len(idset)
|
||||
messages = [''] * len(idset)
|
||||
|
||||
# put these in the context of the check function evaluator
|
||||
# note that this doesn't help the "cfn" version - only the exec version
|
||||
self.context.update({'xml' : self.xml, # our subtree
|
||||
'response_id' : self.myid, # my ID
|
||||
'expect': self.expect, # expected answer (if given as attribute)
|
||||
'submission':submission, # ordered list of student answers from entry boxes in our subtree
|
||||
'idset':idset, # ordered list of ID's of all entry boxes in our subtree
|
||||
'dynamath':dynamath, # ordered list of all javascript inputs in our subtree
|
||||
'answers':student_answers, # dict of student's responses, with keys being entry box IDs
|
||||
'correct':correct, # the list to be filled in by the check function
|
||||
'messages':messages, # the list of messages to be filled in by the check function
|
||||
'options':self.xml.get('options'), # any options to be passed to the cfn
|
||||
'testdat':'hello world',
|
||||
})
|
||||
|
||||
# pass self.system.debug to cfn
|
||||
self.context['debug'] = self.system.DEBUG
|
||||
|
||||
# exec the check function
|
||||
if type(self.code)==str:
|
||||
try:
|
||||
exec self.code in self.context['global_context'], self.context
|
||||
except Exception as err:
|
||||
print "oops in customresponse (code) error %s" % err
|
||||
print "context = ",self.context
|
||||
print traceback.format_exc()
|
||||
else: # self.code is not a string; assume its a function
|
||||
|
||||
# this is an interface to the Tutor2 check functions
|
||||
fn = self.code
|
||||
ret = None
|
||||
log.debug(" submission = %s" % submission)
|
||||
try:
|
||||
answer_given = submission[0] if (len(idset)==1) else submission
|
||||
# handle variable number of arguments in check function, for backwards compatibility
|
||||
# with various Tutor2 check functions
|
||||
args = [self.expect,answer_given,student_answers,self.answer_ids[0]]
|
||||
argspec = inspect.getargspec(fn)
|
||||
nargs = len(argspec.args)-len(argspec.defaults or [])
|
||||
kwargs = {}
|
||||
for argname in argspec.args[nargs:]:
|
||||
kwargs[argname] = self.context[argname] if argname in self.context else None
|
||||
|
||||
log.debug('[customresponse] answer_given=%s' % answer_given)
|
||||
log.debug('nargs=%d, args=%s, kwargs=%s' % (nargs,args,kwargs))
|
||||
|
||||
ret = fn(*args[:nargs],**kwargs)
|
||||
except Exception as err:
|
||||
log.error("oops in customresponse (cfn) error %s" % err)
|
||||
# print "context = ",self.context
|
||||
log.error(traceback.format_exc())
|
||||
raise Exception("oops in customresponse (cfn) error %s" % err)
|
||||
log.debug("[courseware.capa.responsetypes.customresponse.get_score] ret = %s" % ret)
|
||||
if type(ret)==dict:
|
||||
correct = ['correct']*len(idset) if ret['ok'] else ['incorrect']*len(idset)
|
||||
msg = ret['msg']
|
||||
|
||||
if 1:
|
||||
# try to clean up message html
|
||||
msg = '<html>'+msg+'</html>'
|
||||
msg = msg.replace('<','<')
|
||||
#msg = msg.replace('<','<')
|
||||
msg = etree.tostring(fromstring_bs(msg,convertEntities=None),pretty_print=True)
|
||||
#msg = etree.tostring(fromstring_bs(msg),pretty_print=True)
|
||||
msg = msg.replace(' ','')
|
||||
#msg = re.sub('<html>(.*)</html>','\\1',msg,flags=re.M|re.DOTALL) # python 2.7
|
||||
msg = re.sub('(?ms)<html>(.*)</html>','\\1',msg)
|
||||
|
||||
messages[0] = msg
|
||||
else:
|
||||
correct = ['correct']*len(idset) if ret else ['incorrect']*len(idset)
|
||||
|
||||
# build map giving "correct"ness of the answer(s)
|
||||
correct_map = CorrectMap()
|
||||
for k in range(len(idset)):
|
||||
correct_map.set(idset[k], correct[k], msg=messages[k])
|
||||
return correct_map
|
||||
|
||||
def get_answers(self):
|
||||
'''
|
||||
Give correct answer expected for this response.
|
||||
|
||||
use default_answer_map from entry elements (eg textline),
|
||||
when this response has multiple entry objects.
|
||||
|
||||
but for simplicity, if an "expect" attribute was given by the content author
|
||||
ie <customresponse expect="foo" ...> then that.
|
||||
'''
|
||||
if len(self.answer_ids)>1:
|
||||
return self.default_answer_map
|
||||
if self.expect:
|
||||
return {self.answer_ids[0] : self.expect}
|
||||
return self.default_answer_map
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class SymbolicResponse(CustomResponse):
|
||||
"""
|
||||
Symbolic math response checking, using symmath library.
|
||||
"""
|
||||
snippets = [{'snippet': '''<problem>
|
||||
<text>Compute \[ \exp\left(-i \frac{\theta}{2} \left[ \begin{matrix} 0 & 1 \\ 1 & 0 \end{matrix} \right] \right) \]
|
||||
and give the resulting \(2\times 2\) matrix: <br/>
|
||||
<symbolicresponse answer="">
|
||||
<textline size="40" math="1" />
|
||||
</symbolicresponse>
|
||||
<br/>
|
||||
Your input should be typed in as a list of lists, eg <tt>[[1,2],[3,4]]</tt>.
|
||||
</text>
|
||||
</problem>'''}]
|
||||
|
||||
response_tag = 'symbolicresponse'
|
||||
|
||||
def setup_response(self):
|
||||
self.xml.set('cfn','symmath_check')
|
||||
code = "from symmath import *"
|
||||
exec code in self.context,self.context
|
||||
CustomResponse.setup_response(self)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class CodeResponse(LoncapaResponse):
|
||||
'''
|
||||
Grade student code using an external server, called 'xqueue'
|
||||
In contrast to ExternalResponse, CodeResponse has following behavior:
|
||||
1) Goes through a queueing system
|
||||
2) Does not do external request for 'get_answers'
|
||||
The XML definition of a CodeResponse is identical to that of ExternalResponse. Simply replace
|
||||
the tag 'externalresponse' with 'coderesponse'
|
||||
'''
|
||||
|
||||
response_tag = 'coderesponse'
|
||||
allowed_inputfields = ['textline', 'textbox']
|
||||
max_inputfields = 1
|
||||
|
||||
def setup_response(self):
|
||||
xml = self.xml
|
||||
self.url = xml.get('url', "http://ec2-50-17-86-200.compute-1.amazonaws.com/xqueue/submit/") # FIXME -- hardcoded url
|
||||
|
||||
answer = 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()
|
||||
else:
|
||||
self.code = answer.text
|
||||
else: # no <answer> stanza; get code from <script>
|
||||
self.code = self.context['script_code']
|
||||
if not self.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 = 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
|
||||
penv = {}
|
||||
penv['__builtins__'] = globals()['__builtins__']
|
||||
try:
|
||||
exec(self.code,penv,penv)
|
||||
except Exception as err:
|
||||
log.error('Error in CodeResponse %s: Error in problem reference code' % err)
|
||||
raise Exception(err)
|
||||
try:
|
||||
self.answer = penv['answer']
|
||||
self.initial_display = penv['initial_display']
|
||||
except Exception as err:
|
||||
log.error("Error in CodeResponse %s: Problem reference code does not define 'answer' and/or 'initial_display' in <answer>...</answer>" % err)
|
||||
raise Exception(err)
|
||||
|
||||
def get_score(self, student_answers):
|
||||
try:
|
||||
submission = [student_answers[self.answer_id]]
|
||||
except Exception as err:
|
||||
log.error('Error in CodeResponse %s: cannot get student answer for %s; student_answers=%s' % (err, self.answer_id, student_answers))
|
||||
raise Exception(err)
|
||||
|
||||
self.context.update({'submission': submission})
|
||||
extra_payload = {'edX_student_response': json.dumps(submission)}
|
||||
|
||||
r, queuekey = self._send_to_queue(extra_payload) # TODO: Perform checks on the xqueue response
|
||||
|
||||
# Non-null CorrectMap['queuekey'] indicates that the problem has been submitted
|
||||
cmap = CorrectMap()
|
||||
cmap.set(self.answer_id, queuekey=queuekey, msg='Submitted to queue')
|
||||
|
||||
return cmap
|
||||
|
||||
def update_score(self, score_msg, oldcmap, queuekey):
|
||||
# Parse 'score_msg' as XML
|
||||
try:
|
||||
rxml = etree.fromstring(score_msg)
|
||||
except Exception as err:
|
||||
msg = 'Error in CodeResponse %s: cannot parse response from xworker r.text=%s' % (err, score_msg)
|
||||
raise Exception(err)
|
||||
|
||||
ad = rxml.find('awarddetail').text
|
||||
admap = {'EXACT_ANS': 'correct', # TODO: handle other loncapa responses
|
||||
'WRONG_FORMAT': 'incorrect',
|
||||
}
|
||||
self.context['correct'] = ['correct']
|
||||
if ad in admap:
|
||||
self.context['correct'][0] = admap[ad]
|
||||
|
||||
# Replace 'oldcmap' with new grading results if queuekey matches.
|
||||
# If queuekey does not match, we keep waiting for the score_msg that will match
|
||||
if oldcmap.is_right_queuekey(self.answer_id, queuekey):
|
||||
msg = rxml.find('message').text.replace(' ',' ')
|
||||
oldcmap.set(self.answer_id, correctness=self.context['correct'][0], msg=msg, queuekey=None) # Queuekey is consumed
|
||||
else:
|
||||
log.debug('CodeResponse: queuekey %d does not match for answer_id=%s.' % (queuekey, self.answer_id))
|
||||
|
||||
return oldcmap
|
||||
|
||||
# CodeResponse differentiates from ExternalResponse in the behavior of 'get_answers'. CodeResponse.get_answers
|
||||
# does NOT require a queue submission, and the answer is computed (extracted from problem XML) locally.
|
||||
def get_answers(self):
|
||||
anshtml = '<font color="blue"><span class="code-answer"><br/><pre>%s</pre><br/></span></font>' % self.answer
|
||||
return { self.answer_id: anshtml }
|
||||
|
||||
def get_initial_display(self):
|
||||
return { self.answer_id: self.initial_display }
|
||||
|
||||
# CodeResponse._send_to_queue implements the same interface as defined for ExternalResponse's 'get_score'
|
||||
def _send_to_queue(self, extra_payload):
|
||||
# Prepare payload
|
||||
xmlstr = etree.tostring(self.xml, pretty_print=True)
|
||||
header = { 'return_url': self.system.xqueue_callback_url }
|
||||
|
||||
# Queuekey generation
|
||||
h = hashlib.md5()
|
||||
h.update(str(self.system.seed))
|
||||
h.update(str(time.time()))
|
||||
queuekey = int(h.hexdigest(),16)
|
||||
header.update({'queuekey': queuekey})
|
||||
|
||||
payload = {'xqueue_header': json.dumps(header), # TODO: 'xqueue_header' should eventually be derived from a config file
|
||||
'xml': xmlstr,
|
||||
'edX_cmd': 'get_score',
|
||||
'edX_tests': self.tests,
|
||||
'processor': self.code,
|
||||
}
|
||||
payload.update(extra_payload)
|
||||
|
||||
# Contact queue server
|
||||
try:
|
||||
r = requests.post(self.url, data=payload)
|
||||
except Exception as err:
|
||||
msg = "Error in CodeResponse %s: cannot connect to queue server url=%s" % (err, self.url)
|
||||
log.error(msg)
|
||||
raise Exception(msg)
|
||||
|
||||
return r, queuekey
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class ExternalResponse(LoncapaResponse):
|
||||
'''
|
||||
Grade the students input using an external server.
|
||||
|
||||
Typically used by coding problems.
|
||||
|
||||
'''
|
||||
snippets = [{'snippet': '''<externalresponse tests="repeat:10,generate">
|
||||
<textbox rows="10" cols="70" mode="python"/>
|
||||
<answer><![CDATA[
|
||||
initial_display = """
|
||||
def inc(x):
|
||||
"""
|
||||
|
||||
answer = """
|
||||
def inc(n):
|
||||
return n+1
|
||||
"""
|
||||
preamble = """
|
||||
import sympy
|
||||
"""
|
||||
test_program = """
|
||||
import random
|
||||
|
||||
def testInc(n = None):
|
||||
if n is None:
|
||||
n = random.randint(2, 20)
|
||||
print 'Test is: inc(%d)'%n
|
||||
return str(inc(n))
|
||||
|
||||
def main():
|
||||
f = os.fdopen(3,'w')
|
||||
test = int(sys.argv[1])
|
||||
rndlist = map(int,os.getenv('rndlist').split(','))
|
||||
random.seed(rndlist[0])
|
||||
if test == 1: f.write(testInc(0))
|
||||
elif test == 2: f.write(testInc(1))
|
||||
else: f.write(testInc())
|
||||
f.close()
|
||||
|
||||
main()
|
||||
"""
|
||||
]]>
|
||||
</answer>
|
||||
</externalresponse>'''}]
|
||||
|
||||
response_tag = 'externalresponse'
|
||||
allowed_inputfields = ['textline','textbox']
|
||||
|
||||
def setup_response(self):
|
||||
xml = self.xml
|
||||
self.url = xml.get('url') or "http://eecs1.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL
|
||||
|
||||
# answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0] # FIXME - catch errors
|
||||
answer = 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()
|
||||
else:
|
||||
self.code = answer.text
|
||||
else: # no <answer> stanza; get code from <script>
|
||||
self.code = self.context['script_code']
|
||||
if not self.code:
|
||||
msg = '%s: Missing answer script code for externalresponse' % unicode(self)
|
||||
msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','<unavailable>')
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
self.tests = xml.get('tests')
|
||||
|
||||
def do_external_request(self,cmd,extra_payload):
|
||||
'''
|
||||
Perform HTTP request / post to external server.
|
||||
|
||||
cmd = remote command to perform (str)
|
||||
extra_payload = dict of extra stuff to post.
|
||||
|
||||
Return XML tree of response (from response body)
|
||||
'''
|
||||
xmlstr = etree.tostring(self.xml, pretty_print=True)
|
||||
payload = {'xml': xmlstr,
|
||||
'edX_cmd' : cmd,
|
||||
'edX_tests': self.tests,
|
||||
'processor' : self.code,
|
||||
}
|
||||
payload.update(extra_payload)
|
||||
|
||||
try:
|
||||
r = requests.post(self.url,data=payload) # call external server
|
||||
except Exception as err:
|
||||
msg = 'Error %s - cannot connect to external server url=%s' % (err,self.url)
|
||||
log.error(msg)
|
||||
raise Exception(msg)
|
||||
|
||||
if self.system.DEBUG: log.info('response = %s' % r.text)
|
||||
|
||||
if (not r.text ) or (not r.text.strip()):
|
||||
raise Exception('Error: no response from external server url=%s' % self.url)
|
||||
|
||||
try:
|
||||
rxml = etree.fromstring(r.text) # response is XML; parse it
|
||||
except Exception as err:
|
||||
msg = 'Error %s - cannot parse response from external server r.text=%s' % (err,r.text)
|
||||
log.error(msg)
|
||||
raise Exception(msg)
|
||||
|
||||
return rxml
|
||||
|
||||
def get_score(self, student_answers):
|
||||
idset = sorted(self.answer_ids)
|
||||
cmap = CorrectMap()
|
||||
try:
|
||||
submission = [student_answers[k] for k in idset]
|
||||
except Exception as err:
|
||||
log.error('Error %s: cannot get student answer for %s; student_answers=%s' % (err,self.answer_ids,student_answers))
|
||||
raise Exception(err)
|
||||
|
||||
self.context.update({'submission':submission})
|
||||
|
||||
extra_payload = {'edX_student_response': json.dumps(submission)}
|
||||
|
||||
try:
|
||||
rxml = self.do_external_request('get_score',extra_payload)
|
||||
except Exception as err:
|
||||
log.error('Error %s' % err)
|
||||
if self.system.DEBUG:
|
||||
cmap.set_dict(dict(zip(sorted(self.answer_ids), ['incorrect'] * len(idset) )))
|
||||
cmap.set_property(self.answer_ids[0],'msg','<font color="red" size="+2">%s</font>' % str(err).replace('<','<'))
|
||||
return cmap
|
||||
|
||||
ad = rxml.find('awarddetail').text
|
||||
admap = {'EXACT_ANS':'correct', # TODO: handle other loncapa responses
|
||||
'WRONG_FORMAT': 'incorrect',
|
||||
}
|
||||
self.context['correct'] = ['correct']
|
||||
if ad in admap:
|
||||
self.context['correct'][0] = admap[ad]
|
||||
|
||||
# create CorrectMap
|
||||
for key in idset:
|
||||
idx = idset.index(key)
|
||||
msg = rxml.find('message').text.replace(' ',' ') if idx==0 else None
|
||||
cmap.set(key, self.context['correct'][idx], msg=msg)
|
||||
|
||||
return cmap
|
||||
|
||||
def get_answers(self):
|
||||
'''
|
||||
Use external server to get expected answers
|
||||
'''
|
||||
try:
|
||||
rxml = self.do_external_request('get_answers',{})
|
||||
exans = json.loads(rxml.find('expected').text)
|
||||
except Exception as err:
|
||||
log.error('Error %s' % err)
|
||||
if self.system.DEBUG:
|
||||
msg = '<font color=red size=+2>%s</font>' % str(err).replace('<','<')
|
||||
exans = [''] * len(self.answer_ids)
|
||||
exans[0] = msg
|
||||
|
||||
if not (len(exans)==len(self.answer_ids)):
|
||||
log.error('Expected %d answers from external server, only got %d!' % (len(self.answer_ids),len(exans)))
|
||||
raise Exception('Short response from external server')
|
||||
return dict(zip(self.answer_ids,exans))
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class FormulaResponse(LoncapaResponse):
|
||||
'''
|
||||
Checking of symbolic math response using numerical sampling.
|
||||
'''
|
||||
snippets = [{'snippet': '''<problem>
|
||||
|
||||
<script type="loncapa/python">
|
||||
I = "m*c^2"
|
||||
</script>
|
||||
|
||||
<text>
|
||||
<br/>
|
||||
Give an equation for the relativistic energy of an object with mass m.
|
||||
</text>
|
||||
<formularesponse type="cs" samples="m,c@1,2:3,4#10" answer="$I">
|
||||
<responseparam description="Numerical Tolerance" type="tolerance"
|
||||
default="0.00001" name="tol" />
|
||||
<textline size="40" math="1" />
|
||||
</formularesponse>
|
||||
|
||||
</problem>'''}]
|
||||
|
||||
response_tag = 'formularesponse'
|
||||
hint_tag = 'formulahint'
|
||||
allowed_inputfields = ['textline']
|
||||
required_attributes = ['answer']
|
||||
max_inputfields = 1
|
||||
|
||||
def setup_response(self):
|
||||
xml = self.xml
|
||||
context = self.context
|
||||
self.correct_answer = contextualize_text(xml.get('answer'), context)
|
||||
self.samples = contextualize_text(xml.get('samples'), context)
|
||||
try:
|
||||
self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default',
|
||||
id=xml.get('id'))[0]
|
||||
self.tolerance = contextualize_text(self.tolerance_xml, context)
|
||||
except Exception:
|
||||
self.tolerance = '0.00001'
|
||||
|
||||
ts = xml.get('type')
|
||||
if ts is None:
|
||||
typeslist = []
|
||||
else:
|
||||
typeslist = ts.split(',')
|
||||
if 'ci' in typeslist: # Case insensitive
|
||||
self.case_sensitive = False
|
||||
elif 'cs' in typeslist: # Case sensitive
|
||||
self.case_sensitive = True
|
||||
else: # Default
|
||||
self.case_sensitive = False
|
||||
|
||||
def get_score(self, student_answers):
|
||||
given = student_answers[self.answer_id]
|
||||
correctness = self.check_formula(self.correct_answer, given, self.samples)
|
||||
return CorrectMap(self.answer_id, correctness)
|
||||
|
||||
def check_formula(self,expected, given, samples):
|
||||
variables=samples.split('@')[0].split(',')
|
||||
numsamples=int(samples.split('@')[1].split('#')[1])
|
||||
sranges=zip(*map(lambda x:map(float, x.split(",")),
|
||||
samples.split('@')[1].split('#')[0].split(':')))
|
||||
|
||||
ranges=dict(zip(variables, sranges))
|
||||
for i in range(numsamples):
|
||||
instructor_variables = self.strip_dict(dict(self.context))
|
||||
student_variables = dict()
|
||||
for var in ranges: # ranges give numerical ranges for testing
|
||||
value = random.uniform(*ranges[var])
|
||||
instructor_variables[str(var)] = value
|
||||
student_variables[str(var)] = value
|
||||
#log.debug('formula: instructor_vars=%s, expected=%s' % (instructor_variables,expected))
|
||||
instructor_result = evaluator(instructor_variables,dict(),expected, cs = self.case_sensitive)
|
||||
try:
|
||||
#log.debug('formula: student_vars=%s, given=%s' % (student_variables,given))
|
||||
student_result = evaluator(student_variables,
|
||||
dict(),
|
||||
given,
|
||||
cs = self.case_sensitive)
|
||||
except UndefinedVariable as uv:
|
||||
log.debug('formularesponse: undefined variable in given=%s' % given)
|
||||
raise StudentInputError(uv.message+" not permitted in answer")
|
||||
except Exception as err:
|
||||
#traceback.print_exc()
|
||||
log.debug('formularesponse: error %s in formula' % err)
|
||||
raise StudentInputError("Error in formula")
|
||||
if numpy.isnan(student_result) or numpy.isinf(student_result):
|
||||
return "incorrect"
|
||||
if not compare_with_tolerance(student_result, instructor_result, self.tolerance):
|
||||
return "incorrect"
|
||||
return "correct"
|
||||
|
||||
def strip_dict(self, d):
|
||||
''' Takes a dict. Returns an identical dict, with all non-word
|
||||
keys and all non-numeric values stripped out. All values also
|
||||
converted to float. Used so we can safely use Python contexts.
|
||||
'''
|
||||
d=dict([(k, numpy.complex(d[k])) for k in d if type(k)==str and \
|
||||
k.isalnum() and \
|
||||
isinstance(d[k], numbers.Number)])
|
||||
return d
|
||||
|
||||
def check_hint_condition(self,hxml_set,student_answers):
|
||||
given = student_answers[self.answer_id]
|
||||
hints_to_show = []
|
||||
for hxml in hxml_set:
|
||||
samples = hxml.get('samples')
|
||||
name = hxml.get('name')
|
||||
correct_answer = contextualize_text(hxml.get('answer'),self.context)
|
||||
try:
|
||||
correctness = self.check_formula(correct_answer, given, samples)
|
||||
except Exception:
|
||||
correctness = 'incorrect'
|
||||
if correctness=='correct':
|
||||
hints_to_show.append(name)
|
||||
log.debug('hints_to_show = %s' % hints_to_show)
|
||||
return hints_to_show
|
||||
|
||||
def get_answers(self):
|
||||
return {self.answer_id:self.correct_answer}
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class SchematicResponse(LoncapaResponse):
|
||||
|
||||
response_tag = 'schematicresponse'
|
||||
allowed_inputfields = ['schematic']
|
||||
|
||||
def setup_response(self):
|
||||
xml = self.xml
|
||||
answer = xml.xpath('//*[@id=$id]//answer', id=xml.get('id'))[0]
|
||||
answer_src = answer.get('src')
|
||||
if answer_src is not None:
|
||||
self.code = self.system.filestore.open('src/'+answer_src).read() # Untested; never used
|
||||
else:
|
||||
self.code = answer.text
|
||||
|
||||
def get_score(self, student_answers):
|
||||
from capa_problem import global_context
|
||||
submission = [json.loads(student_answers[k]) for k in sorted(self.answer_ids)]
|
||||
self.context.update({'submission':submission})
|
||||
exec self.code in global_context, self.context
|
||||
cmap = CorrectMap()
|
||||
cmap.set_dict(dict(zip(sorted(self.answer_ids), self.context['correct'])))
|
||||
return cmap
|
||||
|
||||
def get_answers(self):
|
||||
# use answers provided in input elements
|
||||
return self.default_answer_map
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class ImageResponse(LoncapaResponse):
|
||||
"""
|
||||
Handle student response for image input: the input is a click on an image,
|
||||
which produces an [x,y] coordinate pair. The click is correct if it falls
|
||||
within a region specified. This region is nominally a rectangle.
|
||||
|
||||
Lon-CAPA requires that each <imageresponse> has a <foilgroup> inside it. That
|
||||
doesn't make sense to me (Ike). Instead, let's have it such that <imageresponse>
|
||||
should contain one or more <imageinput> stanzas. Each <imageinput> should specify
|
||||
a rectangle, given as an attribute, defining the correct answer.
|
||||
"""
|
||||
snippets = [{'snippet': '''<imageresponse>
|
||||
<imageinput src="image1.jpg" width="200" height="100" rectangle="(10,10)-(20,30)" />
|
||||
<imageinput src="image2.jpg" width="210" height="130" rectangle="(12,12)-(40,60)" />
|
||||
</imageresponse>'''}]
|
||||
|
||||
response_tag = 'imageresponse'
|
||||
allowed_inputfields = ['imageinput']
|
||||
|
||||
def setup_response(self):
|
||||
self.ielements = self.inputfields
|
||||
self.answer_ids = [ie.get('id') for ie in self.ielements]
|
||||
|
||||
def get_score(self, student_answers):
|
||||
correct_map = CorrectMap()
|
||||
expectedset = self.get_answers()
|
||||
|
||||
for aid in self.answer_ids: # loop through IDs of <imageinput> fields in our stanza
|
||||
given = student_answers[aid] # this should be a string of the form '[x,y]'
|
||||
|
||||
# parse expected answer
|
||||
# TODO: Compile regexp on file load
|
||||
m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]',expectedset[aid].strip().replace(' ',''))
|
||||
if not m:
|
||||
msg = 'Error in problem specification! cannot parse rectangle in %s' % (etree.tostring(self.ielements[aid],
|
||||
pretty_print=True))
|
||||
raise Exception('[capamodule.capa.responsetypes.imageinput] '+msg)
|
||||
(llx,lly,urx,ury) = [int(x) for x in m.groups()]
|
||||
|
||||
# parse given answer
|
||||
m = re.match('\[([0-9]+),([0-9]+)]',given.strip().replace(' ',''))
|
||||
if not m:
|
||||
raise Exception('[capamodule.capa.responsetypes.imageinput] error grading %s (input=%s)' % (aid,given))
|
||||
(gx,gy) = [int(x) for x in m.groups()]
|
||||
|
||||
# answer is correct if (x,y) is within the specified rectangle
|
||||
if (llx <= gx <= urx) and (lly <= gy <= ury):
|
||||
correct_map.set(aid, 'correct')
|
||||
else:
|
||||
correct_map.set(aid, 'incorrect')
|
||||
return correct_map
|
||||
|
||||
def get_answers(self):
|
||||
return dict([(ie.get('id'),ie.get('rectangle')) for ie in self.ielements])
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# 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 ]
|
||||
|
||||
@@ -4,6 +4,7 @@ from calc import evaluator, UndefinedVariable
|
||||
#
|
||||
# Utility functions used in CAPA responsetypes
|
||||
|
||||
|
||||
def compare_with_tolerance(v1, v2, tol):
|
||||
''' Compare v1 to v2 with maximum tolerance tol
|
||||
tol is relative if it ends in %; otherwise, it is absolute
|
||||
@@ -14,17 +15,18 @@ def compare_with_tolerance(v1, v2, tol):
|
||||
|
||||
'''
|
||||
relative = tol.endswith('%')
|
||||
if relative:
|
||||
tolerance_rel = evaluator(dict(),dict(),tol[:-1]) * 0.01
|
||||
if relative:
|
||||
tolerance_rel = evaluator(dict(), dict(), tol[:-1]) * 0.01
|
||||
tolerance = tolerance_rel * max(abs(v1), abs(v2))
|
||||
else:
|
||||
tolerance = evaluator(dict(),dict(),tol)
|
||||
return abs(v1-v2) <= tolerance
|
||||
else:
|
||||
tolerance = evaluator(dict(), dict(), tol)
|
||||
return abs(v1 - v2) <= tolerance
|
||||
|
||||
def contextualize_text(text, context): # private
|
||||
''' Takes a string with variables. E.g. $a+$b.
|
||||
|
||||
def contextualize_text(text, context): # private
|
||||
''' Takes a string with variables. E.g. $a+$b.
|
||||
Does a substitution of those variables from the context '''
|
||||
if not text: return text
|
||||
for key in sorted(context, lambda x,y:cmp(len(y),len(x))):
|
||||
text=text.replace('$'+key, str(context[key]))
|
||||
for key in sorted(context, lambda x, y: cmp(len(y), len(x))):
|
||||
text = text.replace('$' + key, str(context[key]))
|
||||
return text
|
||||
|
||||
@@ -13,4 +13,3 @@
|
||||
# limitations under the License.
|
||||
|
||||
lookup = None
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from django.http import HttpResponse
|
||||
from . import middleware
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def render_to_string(template_name, dictionary, context=None, namespace='main'):
|
||||
context_instance = Context(dictionary)
|
||||
# add dictionary to context_instance
|
||||
@@ -43,6 +44,7 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'):
|
||||
template = middleware.lookup[namespace].get_template(template_name)
|
||||
return template.render(**context_dictionary)
|
||||
|
||||
|
||||
def render_to_response(template_name, dictionary, context_instance=None, namespace='main', **kwargs):
|
||||
"""
|
||||
Returns a HttpResponse whose content is filled with the result of calling
|
||||
|
||||
@@ -3,7 +3,7 @@ Progress class for modules. Represents where a student is in a module.
|
||||
|
||||
Useful things to know:
|
||||
- Use Progress.to_js_status_str() to convert a progress into a simple
|
||||
status string to pass to js.
|
||||
status string to pass to js.
|
||||
- Use Progress.to_js_detail_str() to convert a progress into a more detailed
|
||||
string to pass to js.
|
||||
|
||||
@@ -11,11 +11,12 @@ In particular, these functions have a canonical handing of None.
|
||||
|
||||
For most subclassing needs, you should only need to reimplement
|
||||
frac() and __str__().
|
||||
'''
|
||||
'''
|
||||
|
||||
from collections import namedtuple
|
||||
import numbers
|
||||
|
||||
|
||||
class Progress(object):
|
||||
'''Represents a progress of a/b (a out of b done)
|
||||
|
||||
@@ -37,7 +38,7 @@ class Progress(object):
|
||||
if not (isinstance(a, numbers.Number) and
|
||||
isinstance(b, numbers.Number)):
|
||||
raise TypeError('a and b must be numbers. Passed {0}/{1}'.format(a, b))
|
||||
|
||||
|
||||
if not (0 <= a <= b and b > 0):
|
||||
raise ValueError(
|
||||
'fraction a/b = {0}/{1} must have 0 <= a <= b and b > 0'.format(a, b))
|
||||
@@ -66,13 +67,12 @@ class Progress(object):
|
||||
'''
|
||||
return self.frac()[0] > 0
|
||||
|
||||
|
||||
def inprogress(self):
|
||||
''' Returns True if fractional progress is strictly between 0 and 1.
|
||||
|
||||
subclassing note: implemented in terms of frac(), assumes sanity
|
||||
checking is done at construction time.
|
||||
'''
|
||||
'''
|
||||
(a, b) = self.frac()
|
||||
return a > 0 and a < b
|
||||
|
||||
@@ -83,15 +83,14 @@ class Progress(object):
|
||||
checking is done at construction time.
|
||||
'''
|
||||
(a, b) = self.frac()
|
||||
return a==b
|
||||
|
||||
return a == b
|
||||
|
||||
def ternary_str(self):
|
||||
''' Return a string version of this progress: either
|
||||
"none", "in_progress", or "done".
|
||||
|
||||
subclassing note: implemented in terms of frac()
|
||||
'''
|
||||
'''
|
||||
(a, b) = self.frac()
|
||||
if a == 0:
|
||||
return "none"
|
||||
@@ -111,8 +110,7 @@ class Progress(object):
|
||||
def __ne__(self, other):
|
||||
''' The opposite of equal'''
|
||||
return not self.__eq__(other)
|
||||
|
||||
|
||||
|
||||
def __str__(self):
|
||||
''' Return a string representation of this string.
|
||||
|
||||
@@ -147,7 +145,6 @@ class Progress(object):
|
||||
return "NA"
|
||||
return progress.ternary_str()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def to_js_detail_str(progress):
|
||||
'''
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import fs
|
||||
|
||||
import numpy
|
||||
|
||||
@@ -15,30 +16,24 @@ import capa.calc as calc
|
||||
import capa.capa_problem as lcp
|
||||
from capa.correctmap import CorrectMap
|
||||
from xmodule import graders, x_module
|
||||
from xmodule.x_module import ModuleSystem
|
||||
from xmodule.graders import Score, aggregate_scores
|
||||
from xmodule.progress import Progress
|
||||
from nose.plugins.skip import SkipTest
|
||||
from mock import Mock
|
||||
|
||||
class I4xSystem(object):
|
||||
'''
|
||||
This is an abstraction such that x_modules can function independent
|
||||
of the courseware (e.g. import into other types of courseware, LMS,
|
||||
or if we want to have a sandbox server for user-contributed content)
|
||||
'''
|
||||
def __init__(self):
|
||||
self.ajax_url = '/'
|
||||
self.track_function = lambda x: None
|
||||
self.filestore = None
|
||||
self.render_function = lambda x: {} # Probably incorrect
|
||||
self.module_from_xml = lambda x: None # May need a real impl...
|
||||
self.exception404 = Exception
|
||||
self.DEBUG = True
|
||||
def __repr__(self):
|
||||
return repr(self.__dict__)
|
||||
def __str__(self):
|
||||
return str(self.__dict__)
|
||||
i4xs = ModuleSystem(
|
||||
ajax_url='/',
|
||||
track_function=Mock(),
|
||||
get_module=Mock(),
|
||||
render_template=Mock(),
|
||||
replace_urls=Mock(),
|
||||
user=Mock(),
|
||||
filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))),
|
||||
debug=True,
|
||||
xqueue_callback_url='/'
|
||||
)
|
||||
|
||||
i4xs = I4xSystem()
|
||||
|
||||
class ModelsTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@@ -50,42 +45,42 @@ class ModelsTest(unittest.TestCase):
|
||||
self.assertEqual(str(vc), vc_str)
|
||||
|
||||
def test_calc(self):
|
||||
variables={'R1':2.0, 'R3':4.0}
|
||||
functions={'sin':numpy.sin, 'cos':numpy.cos}
|
||||
variables = {'R1': 2.0, 'R3': 4.0}
|
||||
functions = {'sin': numpy.sin, 'cos': numpy.cos}
|
||||
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "10000||sin(7+5)+0.5356"))<0.01)
|
||||
self.assertEqual(calc.evaluator({'R1': 2.0, 'R3':4.0}, {}, "13"), 13)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "10000||sin(7+5)+0.5356")) < 0.01)
|
||||
self.assertEqual(calc.evaluator({'R1': 2.0, 'R3': 4.0}, {}, "13"), 13)
|
||||
self.assertEqual(calc.evaluator(variables, functions, "13"), 13)
|
||||
self.assertEqual(calc.evaluator({'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.66009498411213041}, {}, "5"), 5)
|
||||
self.assertEqual(calc.evaluator({},{}, "-1"), -1)
|
||||
self.assertEqual(calc.evaluator({},{}, "-0.33"), -.33)
|
||||
self.assertEqual(calc.evaluator({},{}, "-.33"), -.33)
|
||||
self.assertEqual(calc.evaluator({}, {}, "-1"), -1)
|
||||
self.assertEqual(calc.evaluator({}, {}, "-0.33"), -.33)
|
||||
self.assertEqual(calc.evaluator({}, {}, "-.33"), -.33)
|
||||
self.assertEqual(calc.evaluator(variables, functions, "R1*R3"), 8.0)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "sin(e)-0.41"))<0.01)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "k*T/q-0.025"))<0.001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "e^(j*pi)")+1)<0.00001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "j||1")-0.5-0.5j)<0.00001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "sin(e)-0.41")) < 0.01)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "k*T/q-0.025")) < 0.001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "e^(j*pi)") + 1) < 0.00001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "j||1") - 0.5 - 0.5j) < 0.00001)
|
||||
variables['t'] = 1.0
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "t")-1.0)<0.00001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "T")-1.0)<0.00001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "t", cs=True)-1.0)<0.00001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "T", cs=True)-298)<0.2)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "t") - 1.0) < 0.00001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "T") - 1.0) < 0.00001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "t", cs=True) - 1.0) < 0.00001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "T", cs=True) - 298) < 0.2)
|
||||
exception_happened = False
|
||||
try:
|
||||
calc.evaluator({},{}, "5+7 QWSEKO")
|
||||
try:
|
||||
calc.evaluator({}, {}, "5+7 QWSEKO")
|
||||
except:
|
||||
exception_happened = True
|
||||
self.assertTrue(exception_happened)
|
||||
|
||||
try:
|
||||
calc.evaluator({'r1':5},{}, "r1+r2")
|
||||
try:
|
||||
calc.evaluator({'r1': 5}, {}, "r1+r2")
|
||||
except calc.UndefinedVariable:
|
||||
pass
|
||||
|
||||
|
||||
self.assertEqual(calc.evaluator(variables, functions, "r1*r3"), 8.0)
|
||||
|
||||
exception_happened = False
|
||||
try:
|
||||
try:
|
||||
calc.evaluator(variables, functions, "r1*r3", cs=True)
|
||||
except:
|
||||
exception_happened = True
|
||||
@@ -94,55 +89,58 @@ class ModelsTest(unittest.TestCase):
|
||||
#-----------------------------------------------------------------------------
|
||||
# tests of capa_problem inputtypes
|
||||
|
||||
|
||||
class MultiChoiceTest(unittest.TestCase):
|
||||
def test_MC_grade(self):
|
||||
multichoice_file = os.path.dirname(__file__)+"/test_files/multichoice.xml"
|
||||
multichoice_file = os.path.dirname(__file__) + "/test_files/multichoice.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(multichoice_file).read(), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':'choice_foil3'}
|
||||
correct_answers = {'1_2_1': 'choice_foil3'}
|
||||
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
|
||||
false_answers = {'1_2_1':'choice_foil2'}
|
||||
false_answers = {'1_2_1': 'choice_foil2'}
|
||||
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
|
||||
|
||||
def test_MC_bare_grades(self):
|
||||
multichoice_file = os.path.dirname(__file__)+"/test_files/multi_bare.xml"
|
||||
multichoice_file = os.path.dirname(__file__) + "/test_files/multi_bare.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(multichoice_file).read(), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':'choice_2'}
|
||||
correct_answers = {'1_2_1': 'choice_2'}
|
||||
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
|
||||
false_answers = {'1_2_1':'choice_1'}
|
||||
false_answers = {'1_2_1': 'choice_1'}
|
||||
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
|
||||
|
||||
|
||||
def test_TF_grade(self):
|
||||
truefalse_file = os.path.dirname(__file__)+"/test_files/truefalse.xml"
|
||||
truefalse_file = os.path.dirname(__file__) + "/test_files/truefalse.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(truefalse_file).read(), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':['choice_foil2', 'choice_foil1']}
|
||||
correct_answers = {'1_2_1': ['choice_foil2', 'choice_foil1']}
|
||||
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
|
||||
false_answers = {'1_2_1':['choice_foil1']}
|
||||
false_answers = {'1_2_1': ['choice_foil1']}
|
||||
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
|
||||
false_answers = {'1_2_1':['choice_foil1', 'choice_foil3']}
|
||||
false_answers = {'1_2_1': ['choice_foil1', 'choice_foil3']}
|
||||
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
|
||||
false_answers = {'1_2_1':['choice_foil3']}
|
||||
false_answers = {'1_2_1': ['choice_foil3']}
|
||||
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
|
||||
false_answers = {'1_2_1':['choice_foil1', 'choice_foil2', 'choice_foil3']}
|
||||
false_answers = {'1_2_1': ['choice_foil1', 'choice_foil2', 'choice_foil3']}
|
||||
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
|
||||
|
||||
|
||||
|
||||
class ImageResponseTest(unittest.TestCase):
|
||||
def test_ir_grade(self):
|
||||
imageresponse_file = os.path.dirname(__file__)+"/test_files/imageresponse.xml"
|
||||
imageresponse_file = os.path.dirname(__file__) + "/test_files/imageresponse.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(imageresponse_file).read(), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':'(490,11)-(556,98)',
|
||||
'1_2_2':'(242,202)-(296,276)'}
|
||||
test_answers = {'1_2_1':'[500,20]',
|
||||
'1_2_2':'[250,300]',
|
||||
correct_answers = {'1_2_1': '(490,11)-(556,98)',
|
||||
'1_2_2': '(242,202)-(296,276)'}
|
||||
test_answers = {'1_2_1': '[500,20]',
|
||||
'1_2_2': '[250,300]',
|
||||
}
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_2'), 'incorrect')
|
||||
|
||||
|
||||
|
||||
class SymbolicResponseTest(unittest.TestCase):
|
||||
def test_sr_grade(self):
|
||||
raise SkipTest() # This test fails due to dependencies on a local copy of snuggletex-webapp. Until we have figured that out, we'll just skip this test
|
||||
symbolicresponse_file = os.path.dirname(__file__)+"/test_files/symbolicresponse.xml"
|
||||
raise SkipTest() # This test fails due to dependencies on a local copy of snuggletex-webapp. Until we have figured that out, we'll just skip this test
|
||||
symbolicresponse_file = os.path.dirname(__file__) + "/test_files/symbolicresponse.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(symbolicresponse_file).read(), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':'cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]',
|
||||
correct_answers = {'1_2_1': 'cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]',
|
||||
'1_2_1_dynamath': '''
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true">
|
||||
@@ -215,8 +213,8 @@ class SymbolicResponseTest(unittest.TestCase):
|
||||
</math>
|
||||
''',
|
||||
}
|
||||
wrong_answers = {'1_2_1':'2',
|
||||
'1_2_1_dynamath':'''
|
||||
wrong_answers = {'1_2_1': '2',
|
||||
'1_2_1_dynamath': '''
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true">
|
||||
<mn>2</mn>
|
||||
@@ -225,7 +223,8 @@ class SymbolicResponseTest(unittest.TestCase):
|
||||
}
|
||||
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(wrong_answers).get_correctness('1_2_1'), 'incorrect')
|
||||
|
||||
|
||||
|
||||
class OptionResponseTest(unittest.TestCase):
|
||||
'''
|
||||
Run this with
|
||||
@@ -233,120 +232,124 @@ class OptionResponseTest(unittest.TestCase):
|
||||
python manage.py test courseware.OptionResponseTest
|
||||
'''
|
||||
def test_or_grade(self):
|
||||
optionresponse_file = os.path.dirname(__file__)+"/test_files/optionresponse.xml"
|
||||
optionresponse_file = os.path.dirname(__file__) + "/test_files/optionresponse.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(optionresponse_file).read(), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':'True',
|
||||
'1_2_2':'False'}
|
||||
test_answers = {'1_2_1':'True',
|
||||
'1_2_2':'True',
|
||||
correct_answers = {'1_2_1': 'True',
|
||||
'1_2_2': 'False'}
|
||||
test_answers = {'1_2_1': 'True',
|
||||
'1_2_2': 'True',
|
||||
}
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_2'), 'incorrect')
|
||||
|
||||
|
||||
class FormulaResponseWithHintTest(unittest.TestCase):
|
||||
'''
|
||||
Test Formula response problem with a hint
|
||||
This problem also uses calc.
|
||||
'''
|
||||
def test_or_grade(self):
|
||||
problem_file = os.path.dirname(__file__)+"/test_files/formularesponse_with_hint.xml"
|
||||
problem_file = os.path.dirname(__file__) + "/test_files/formularesponse_with_hint.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':'2.5*x-5.0'}
|
||||
test_answers = {'1_2_1':'0.4*x-5.0'}
|
||||
correct_answers = {'1_2_1': '2.5*x-5.0'}
|
||||
test_answers = {'1_2_1': '0.4*x-5.0'}
|
||||
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
|
||||
cmap = test_lcp.grade_answers(test_answers)
|
||||
self.assertEquals(cmap.get_correctness('1_2_1'), 'incorrect')
|
||||
self.assertTrue('You have inverted' in cmap.get_hint('1_2_1'))
|
||||
|
||||
|
||||
class StringResponseWithHintTest(unittest.TestCase):
|
||||
'''
|
||||
Test String response problem with a hint
|
||||
'''
|
||||
def test_or_grade(self):
|
||||
problem_file = os.path.dirname(__file__)+"/test_files/stringresponse_with_hint.xml"
|
||||
problem_file = os.path.dirname(__file__) + "/test_files/stringresponse_with_hint.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':'Michigan'}
|
||||
test_answers = {'1_2_1':'Minnesota'}
|
||||
correct_answers = {'1_2_1': 'Michigan'}
|
||||
test_answers = {'1_2_1': 'Minnesota'}
|
||||
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
|
||||
cmap = test_lcp.grade_answers(test_answers)
|
||||
self.assertEquals(cmap.get_correctness('1_2_1'), 'incorrect')
|
||||
self.assertTrue('St. Paul' in cmap.get_hint('1_2_1'))
|
||||
|
||||
|
||||
class CodeResponseTest(unittest.TestCase):
|
||||
'''
|
||||
Test CodeResponse
|
||||
|
||||
'''
|
||||
def test_update_score(self):
|
||||
problem_file = os.path.dirname(__file__)+"/test_files/coderesponse.xml"
|
||||
problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs)
|
||||
|
||||
|
||||
# CodeResponse requires internal CorrectMap state. Build it now in the 'queued' state
|
||||
old_cmap = CorrectMap()
|
||||
answer_ids = sorted(test_lcp.get_question_answers().keys())
|
||||
numAnswers = len(answer_ids)
|
||||
for i in range(numAnswers):
|
||||
old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuekey=1000+i))
|
||||
old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuekey=1000 + i))
|
||||
|
||||
# Message format inherited from ExternalResponse
|
||||
correct_score_msg = "<edxgrade><awarddetail>EXACT_ANS</awarddetail><message>MESSAGE</message></edxgrade>"
|
||||
incorrect_score_msg = "<edxgrade><awarddetail>WRONG_FORMAT</awarddetail><message>MESSAGE</message></edxgrade>"
|
||||
xserver_msgs = {'correct': correct_score_msg,
|
||||
'incorrect': incorrect_score_msg,
|
||||
correct_score_msg = "<edxgrade><awarddetail>EXACT_ANS</awarddetail><message>MESSAGE</message></edxgrade>"
|
||||
incorrect_score_msg = "<edxgrade><awarddetail>WRONG_FORMAT</awarddetail><message>MESSAGE</message></edxgrade>"
|
||||
xserver_msgs = {'correct': correct_score_msg,
|
||||
'incorrect': incorrect_score_msg,
|
||||
}
|
||||
|
||||
# Incorrect queuekey, state should not be updated
|
||||
for correctness in ['correct', 'incorrect']:
|
||||
test_lcp.correct_map = CorrectMap()
|
||||
test_lcp.correct_map.update(old_cmap) # Deep copy
|
||||
test_lcp.correct_map = CorrectMap()
|
||||
test_lcp.correct_map.update(old_cmap) # Deep copy
|
||||
|
||||
test_lcp.update_score(xserver_msgs[correctness], queuekey=0)
|
||||
self.assertEquals(test_lcp.correct_map.get_dict(), old_cmap.get_dict()) # Deep comparison
|
||||
self.assertEquals(test_lcp.correct_map.get_dict(), old_cmap.get_dict()) # Deep comparison
|
||||
|
||||
for i in range(numAnswers):
|
||||
self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[i])) # Should be still queued, since message undelivered
|
||||
self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[i])) # Should be still queued, since message undelivered
|
||||
|
||||
# Correct queuekey, state should be updated
|
||||
for correctness in ['correct', 'incorrect']:
|
||||
for i in range(numAnswers): # Target specific answer_id's
|
||||
test_lcp.correct_map = CorrectMap()
|
||||
for i in range(numAnswers): # Target specific answer_id's
|
||||
test_lcp.correct_map = CorrectMap()
|
||||
test_lcp.correct_map.update(old_cmap)
|
||||
|
||||
new_cmap = CorrectMap()
|
||||
new_cmap.update(old_cmap)
|
||||
new_cmap.set(answer_id=answer_ids[i], correctness=correctness, msg='MESSAGE', queuekey=None)
|
||||
|
||||
test_lcp.update_score(xserver_msgs[correctness], queuekey=1000+i)
|
||||
test_lcp.update_score(xserver_msgs[correctness], queuekey=1000 + i)
|
||||
self.assertEquals(test_lcp.correct_map.get_dict(), new_cmap.get_dict())
|
||||
|
||||
for j in range(numAnswers):
|
||||
if j == i:
|
||||
self.assertFalse(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be dequeued, message delivered
|
||||
self.assertFalse(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be dequeued, message delivered
|
||||
else:
|
||||
self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be queued, message undelivered
|
||||
self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be queued, message undelivered
|
||||
|
||||
|
||||
class ChoiceResponseTest(unittest.TestCase):
|
||||
|
||||
def test_cr_rb_grade(self):
|
||||
problem_file = os.path.dirname(__file__)+"/test_files/choiceresponse_radio.xml"
|
||||
problem_file = os.path.dirname(__file__) + "/test_files/choiceresponse_radio.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':'choice_2',
|
||||
'1_3_1':['choice_2', 'choice_3']}
|
||||
test_answers = {'1_2_1':'choice_2',
|
||||
'1_3_1':'choice_2',
|
||||
correct_answers = {'1_2_1': 'choice_2',
|
||||
'1_3_1': ['choice_2', 'choice_3']}
|
||||
test_answers = {'1_2_1': 'choice_2',
|
||||
'1_3_1': 'choice_2',
|
||||
}
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_1'), 'incorrect')
|
||||
|
||||
def test_cr_cb_grade(self):
|
||||
problem_file = os.path.dirname(__file__)+"/test_files/choiceresponse_checkbox.xml"
|
||||
problem_file = os.path.dirname(__file__) + "/test_files/choiceresponse_checkbox.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':'choice_2',
|
||||
'1_3_1':['choice_2', 'choice_3'],
|
||||
'1_4_1':['choice_2', 'choice_3']}
|
||||
test_answers = {'1_2_1':'choice_2',
|
||||
'1_3_1':'choice_2',
|
||||
'1_4_1':['choice_2', 'choice_3'],
|
||||
correct_answers = {'1_2_1': 'choice_2',
|
||||
'1_3_1': ['choice_2', 'choice_3'],
|
||||
'1_4_1': ['choice_2', 'choice_3']}
|
||||
test_answers = {'1_2_1': 'choice_2',
|
||||
'1_3_1': 'choice_2',
|
||||
'1_4_1': ['choice_2', 'choice_3'],
|
||||
}
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_1'), 'incorrect')
|
||||
@@ -355,11 +358,12 @@ class ChoiceResponseTest(unittest.TestCase):
|
||||
#-----------------------------------------------------------------------------
|
||||
# Grading tests
|
||||
|
||||
|
||||
class GradesheetTest(unittest.TestCase):
|
||||
|
||||
def test_weighted_grading(self):
|
||||
scores = []
|
||||
Score.__sub__=lambda me, other: (me.earned - other.earned) + (me.possible - other.possible)
|
||||
Score.__sub__ = lambda me, other: (me.earned - other.earned) + (me.possible - other.possible)
|
||||
|
||||
all, graded = aggregate_scores(scores)
|
||||
self.assertEqual(all, Score(earned=0, possible=0, graded=False, section="summary"))
|
||||
@@ -380,197 +384,194 @@ class GradesheetTest(unittest.TestCase):
|
||||
self.assertAlmostEqual(all, Score(earned=5, possible=15, graded=False, section="summary"))
|
||||
self.assertAlmostEqual(graded, Score(earned=5, possible=10, graded=True, section="summary"))
|
||||
|
||||
|
||||
class GraderTest(unittest.TestCase):
|
||||
|
||||
empty_gradesheet = {
|
||||
}
|
||||
|
||||
|
||||
incomplete_gradesheet = {
|
||||
'Homework': [],
|
||||
'Lab': [],
|
||||
'Midterm' : [],
|
||||
'Midterm': [],
|
||||
}
|
||||
|
||||
|
||||
test_gradesheet = {
|
||||
'Homework': [Score(earned=2, possible=20.0, graded=True, section='hw1'),
|
||||
Score(earned=16, possible=16.0, graded=True, section='hw2')],
|
||||
#The dropped scores should be from the assignments that don't exist yet
|
||||
|
||||
'Lab': [Score(earned=1, possible=2.0, graded=True, section='lab1'), #Dropped
|
||||
|
||||
'Lab': [Score(earned=1, possible=2.0, graded=True, section='lab1'), # Dropped
|
||||
Score(earned=1, possible=1.0, graded=True, section='lab2'),
|
||||
Score(earned=1, possible=1.0, graded=True, section='lab3'),
|
||||
Score(earned=5, possible=25.0, graded=True, section='lab4'), #Dropped
|
||||
Score(earned=3, possible=4.0, graded=True, section='lab5'), #Dropped
|
||||
Score(earned=5, possible=25.0, graded=True, section='lab4'), # Dropped
|
||||
Score(earned=3, possible=4.0, graded=True, section='lab5'), # Dropped
|
||||
Score(earned=6, possible=7.0, graded=True, section='lab6'),
|
||||
Score(earned=5, possible=6.0, graded=True, section='lab7')],
|
||||
|
||||
'Midterm' : [Score(earned=50.5, possible=100, graded=True, section="Midterm Exam"),],
|
||||
|
||||
'Midterm': [Score(earned=50.5, possible=100, graded=True, section="Midterm Exam"), ],
|
||||
}
|
||||
|
||||
|
||||
def test_SingleSectionGrader(self):
|
||||
midtermGrader = graders.SingleSectionGrader("Midterm", "Midterm Exam")
|
||||
lab4Grader = graders.SingleSectionGrader("Lab", "lab4")
|
||||
badLabGrader = graders.SingleSectionGrader("Lab", "lab42")
|
||||
|
||||
for graded in [midtermGrader.grade(self.empty_gradesheet),
|
||||
midtermGrader.grade(self.incomplete_gradesheet),
|
||||
|
||||
for graded in [midtermGrader.grade(self.empty_gradesheet),
|
||||
midtermGrader.grade(self.incomplete_gradesheet),
|
||||
badLabGrader.grade(self.test_gradesheet)]:
|
||||
self.assertEqual( len(graded['section_breakdown']), 1 )
|
||||
self.assertEqual( graded['percent'], 0.0 )
|
||||
|
||||
self.assertEqual(len(graded['section_breakdown']), 1)
|
||||
self.assertEqual(graded['percent'], 0.0)
|
||||
|
||||
graded = midtermGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.505 )
|
||||
self.assertEqual( len(graded['section_breakdown']), 1 )
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.505)
|
||||
self.assertEqual(len(graded['section_breakdown']), 1)
|
||||
|
||||
graded = lab4Grader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.2 )
|
||||
self.assertEqual( len(graded['section_breakdown']), 1 )
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.2)
|
||||
self.assertEqual(len(graded['section_breakdown']), 1)
|
||||
|
||||
def test_AssignmentFormatGrader(self):
|
||||
homeworkGrader = graders.AssignmentFormatGrader("Homework", 12, 2)
|
||||
noDropGrader = graders.AssignmentFormatGrader("Homework", 12, 0)
|
||||
#Even though the minimum number is 3, this should grade correctly when 7 assignments are found
|
||||
overflowGrader = graders.AssignmentFormatGrader("Lab", 3, 2)
|
||||
labGrader = graders.AssignmentFormatGrader("Lab", 7, 3)
|
||||
|
||||
|
||||
|
||||
#Test the grading of an empty gradesheet
|
||||
for graded in [ homeworkGrader.grade(self.empty_gradesheet),
|
||||
for graded in [homeworkGrader.grade(self.empty_gradesheet),
|
||||
noDropGrader.grade(self.empty_gradesheet),
|
||||
homeworkGrader.grade(self.incomplete_gradesheet),
|
||||
noDropGrader.grade(self.incomplete_gradesheet) ]:
|
||||
self.assertAlmostEqual( graded['percent'], 0.0 )
|
||||
noDropGrader.grade(self.incomplete_gradesheet)]:
|
||||
self.assertAlmostEqual(graded['percent'], 0.0)
|
||||
#Make sure the breakdown includes 12 sections, plus one summary
|
||||
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
|
||||
|
||||
|
||||
self.assertEqual(len(graded['section_breakdown']), 12 + 1)
|
||||
|
||||
graded = homeworkGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.11 ) # 100% + 10% / 10 assignments
|
||||
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.11) # 100% + 10% / 10 assignments
|
||||
self.assertEqual(len(graded['section_breakdown']), 12 + 1)
|
||||
|
||||
graded = noDropGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.0916666666666666 ) # 100% + 10% / 12 assignments
|
||||
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.0916666666666666) # 100% + 10% / 12 assignments
|
||||
self.assertEqual(len(graded['section_breakdown']), 12 + 1)
|
||||
|
||||
graded = overflowGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.8880952380952382 ) # 100% + 10% / 5 assignments
|
||||
self.assertEqual( len(graded['section_breakdown']), 7 + 1 )
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.8880952380952382) # 100% + 10% / 5 assignments
|
||||
self.assertEqual(len(graded['section_breakdown']), 7 + 1)
|
||||
|
||||
graded = labGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.9226190476190477 )
|
||||
self.assertEqual( len(graded['section_breakdown']), 7 + 1 )
|
||||
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.9226190476190477)
|
||||
self.assertEqual(len(graded['section_breakdown']), 7 + 1)
|
||||
|
||||
def test_WeightedSubsectionsGrader(self):
|
||||
#First, a few sub graders
|
||||
homeworkGrader = graders.AssignmentFormatGrader("Homework", 12, 2)
|
||||
labGrader = graders.AssignmentFormatGrader("Lab", 7, 3)
|
||||
midtermGrader = graders.SingleSectionGrader("Midterm", "Midterm Exam")
|
||||
|
||||
weightedGrader = graders.WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.25), (labGrader, labGrader.category, 0.25),
|
||||
(midtermGrader, midtermGrader.category, 0.5)] )
|
||||
|
||||
overOneWeightsGrader = graders.WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.5), (labGrader, labGrader.category, 0.5),
|
||||
(midtermGrader, midtermGrader.category, 0.5)] )
|
||||
|
||||
|
||||
weightedGrader = graders.WeightedSubsectionsGrader([(homeworkGrader, homeworkGrader.category, 0.25), (labGrader, labGrader.category, 0.25),
|
||||
(midtermGrader, midtermGrader.category, 0.5)])
|
||||
|
||||
overOneWeightsGrader = graders.WeightedSubsectionsGrader([(homeworkGrader, homeworkGrader.category, 0.5), (labGrader, labGrader.category, 0.5),
|
||||
(midtermGrader, midtermGrader.category, 0.5)])
|
||||
|
||||
#The midterm should have all weight on this one
|
||||
zeroWeightsGrader = graders.WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.0), (labGrader, labGrader.category, 0.0),
|
||||
(midtermGrader, midtermGrader.category, 0.5)] )
|
||||
|
||||
zeroWeightsGrader = graders.WeightedSubsectionsGrader([(homeworkGrader, homeworkGrader.category, 0.0), (labGrader, labGrader.category, 0.0),
|
||||
(midtermGrader, midtermGrader.category, 0.5)])
|
||||
|
||||
#This should always have a final percent of zero
|
||||
allZeroWeightsGrader = graders.WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.0), (labGrader, labGrader.category, 0.0),
|
||||
(midtermGrader, midtermGrader.category, 0.0)] )
|
||||
|
||||
emptyGrader = graders.WeightedSubsectionsGrader( [] )
|
||||
|
||||
allZeroWeightsGrader = graders.WeightedSubsectionsGrader([(homeworkGrader, homeworkGrader.category, 0.0), (labGrader, labGrader.category, 0.0),
|
||||
(midtermGrader, midtermGrader.category, 0.0)])
|
||||
|
||||
emptyGrader = graders.WeightedSubsectionsGrader([])
|
||||
|
||||
graded = weightedGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.5106547619047619 )
|
||||
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 3 )
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.5106547619047619)
|
||||
self.assertEqual(len(graded['section_breakdown']), (12 + 1) + (7 + 1) + 1)
|
||||
self.assertEqual(len(graded['grade_breakdown']), 3)
|
||||
|
||||
graded = overOneWeightsGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.7688095238095238 )
|
||||
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 3 )
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.7688095238095238)
|
||||
self.assertEqual(len(graded['section_breakdown']), (12 + 1) + (7 + 1) + 1)
|
||||
self.assertEqual(len(graded['grade_breakdown']), 3)
|
||||
|
||||
graded = zeroWeightsGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.2525 )
|
||||
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 3 )
|
||||
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.2525)
|
||||
self.assertEqual(len(graded['section_breakdown']), (12 + 1) + (7 + 1) + 1)
|
||||
self.assertEqual(len(graded['grade_breakdown']), 3)
|
||||
|
||||
graded = allZeroWeightsGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.0 )
|
||||
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 3 )
|
||||
|
||||
for graded in [ weightedGrader.grade(self.empty_gradesheet),
|
||||
self.assertAlmostEqual(graded['percent'], 0.0)
|
||||
self.assertEqual(len(graded['section_breakdown']), (12 + 1) + (7 + 1) + 1)
|
||||
self.assertEqual(len(graded['grade_breakdown']), 3)
|
||||
|
||||
for graded in [weightedGrader.grade(self.empty_gradesheet),
|
||||
weightedGrader.grade(self.incomplete_gradesheet),
|
||||
zeroWeightsGrader.grade(self.empty_gradesheet),
|
||||
allZeroWeightsGrader.grade(self.empty_gradesheet)]:
|
||||
self.assertAlmostEqual( graded['percent'], 0.0 )
|
||||
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 3 )
|
||||
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.0)
|
||||
self.assertEqual(len(graded['section_breakdown']), (12 + 1) + (7 + 1) + 1)
|
||||
self.assertEqual(len(graded['grade_breakdown']), 3)
|
||||
|
||||
graded = emptyGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.0 )
|
||||
self.assertEqual( len(graded['section_breakdown']), 0 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 0 )
|
||||
self.assertAlmostEqual(graded['percent'], 0.0)
|
||||
self.assertEqual(len(graded['section_breakdown']), 0)
|
||||
self.assertEqual(len(graded['grade_breakdown']), 0)
|
||||
|
||||
def test_graderFromConf(self):
|
||||
|
||||
|
||||
#Confs always produce a graders.WeightedSubsectionsGrader, so we test this by repeating the test
|
||||
#in test_graders.WeightedSubsectionsGrader, but generate the graders with confs.
|
||||
|
||||
|
||||
weightedGrader = graders.grader_from_conf([
|
||||
{
|
||||
'type' : "Homework",
|
||||
'min_count' : 12,
|
||||
'drop_count' : 2,
|
||||
'short_label' : "HW",
|
||||
'weight' : 0.25,
|
||||
'type': "Homework",
|
||||
'min_count': 12,
|
||||
'drop_count': 2,
|
||||
'short_label': "HW",
|
||||
'weight': 0.25,
|
||||
},
|
||||
{
|
||||
'type' : "Lab",
|
||||
'min_count' : 7,
|
||||
'drop_count' : 3,
|
||||
'category' : "Labs",
|
||||
'weight' : 0.25
|
||||
'type': "Lab",
|
||||
'min_count': 7,
|
||||
'drop_count': 3,
|
||||
'category': "Labs",
|
||||
'weight': 0.25
|
||||
},
|
||||
{
|
||||
'type' : "Midterm",
|
||||
'name' : "Midterm Exam",
|
||||
'short_label' : "Midterm",
|
||||
'weight' : 0.5,
|
||||
'type': "Midterm",
|
||||
'name': "Midterm Exam",
|
||||
'short_label': "Midterm",
|
||||
'weight': 0.5,
|
||||
},
|
||||
])
|
||||
|
||||
|
||||
emptyGrader = graders.grader_from_conf([])
|
||||
|
||||
|
||||
graded = weightedGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.5106547619047619 )
|
||||
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 3 )
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.5106547619047619)
|
||||
self.assertEqual(len(graded['section_breakdown']), (12 + 1) + (7 + 1) + 1)
|
||||
self.assertEqual(len(graded['grade_breakdown']), 3)
|
||||
|
||||
graded = emptyGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.0 )
|
||||
self.assertEqual( len(graded['section_breakdown']), 0 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 0 )
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.0)
|
||||
self.assertEqual(len(graded['section_breakdown']), 0)
|
||||
self.assertEqual(len(graded['grade_breakdown']), 0)
|
||||
|
||||
#Test that graders can also be used instead of lists of dictionaries
|
||||
homeworkGrader = graders.AssignmentFormatGrader("Homework", 12, 2)
|
||||
homeworkGrader2 = graders.grader_from_conf(homeworkGrader)
|
||||
|
||||
|
||||
graded = homeworkGrader2.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.11 )
|
||||
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.11)
|
||||
self.assertEqual(len(graded['section_breakdown']), 12 + 1)
|
||||
|
||||
#TODO: How do we test failure cases? The parser only logs an error when it can't parse something. Maybe it should throw exceptions?
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Module progress tests
|
||||
|
||||
|
||||
|
||||
class ProgressTest(unittest.TestCase):
|
||||
''' Test that basic Progress objects work. A Progress represents a
|
||||
fraction between 0 and 1.
|
||||
@@ -589,7 +590,7 @@ class ProgressTest(unittest.TestCase):
|
||||
|
||||
p = Progress(2.5, 5.0)
|
||||
p = Progress(3.7, 12.3333)
|
||||
|
||||
|
||||
# These shouldn't
|
||||
self.assertRaises(ValueError, Progress, 0, 0)
|
||||
self.assertRaises(ValueError, Progress, 2, 0)
|
||||
@@ -634,7 +635,7 @@ class ProgressTest(unittest.TestCase):
|
||||
self.assertTrue(self.done.done())
|
||||
self.assertFalse(self.half_done.done())
|
||||
self.assertFalse(self.not_started.done())
|
||||
|
||||
|
||||
def test_str(self):
|
||||
self.assertEqual(str(self.not_started), "0/17")
|
||||
self.assertEqual(str(self.part_done), "2/6")
|
||||
@@ -647,7 +648,7 @@ class ProgressTest(unittest.TestCase):
|
||||
|
||||
def test_to_js_status(self):
|
||||
'''Test the Progress.to_js_status_str() method'''
|
||||
|
||||
|
||||
self.assertEqual(Progress.to_js_status_str(self.not_started), "none")
|
||||
self.assertEqual(Progress.to_js_status_str(self.half_done), "in_progress")
|
||||
self.assertEqual(Progress.to_js_status_str(self.done), "done")
|
||||
@@ -672,7 +673,7 @@ class ProgressTest(unittest.TestCase):
|
||||
self.assertEqual(add(p, p), (0, 4))
|
||||
self.assertEqual(add(p, p2), (1, 5))
|
||||
self.assertEqual(add(p2, p3), (3, 8))
|
||||
|
||||
|
||||
self.assertEqual(add(p2, pNone), p2.frac())
|
||||
self.assertEqual(add(pNone, p2), p2.frac())
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@ import json
|
||||
import logging
|
||||
import traceback
|
||||
import re
|
||||
import StringIO
|
||||
import os
|
||||
|
||||
from datetime import timedelta
|
||||
from lxml import etree
|
||||
from pkg_resources import resource_string
|
||||
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from progress import Progress
|
||||
from capa.capa_problem import LoncapaProblem
|
||||
from capa.responsetypes import StudentInputError
|
||||
@@ -71,6 +71,12 @@ class CapaModule(XModule):
|
||||
'''
|
||||
icon_class = 'problem'
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee')],
|
||||
'js': [resource_string(__name__, 'js/src/capa/imageinput.js'),
|
||||
resource_string(__name__, 'js/src/capa/schematic.js')]}
|
||||
js_module_name = "Problem"
|
||||
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
|
||||
|
||||
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
|
||||
|
||||
@@ -198,12 +204,12 @@ class CapaModule(XModule):
|
||||
if self.system.DEBUG:
|
||||
log.exception(err)
|
||||
msg = '[courseware.capa.capa_module] <font size="+1" color="red">Failed to generate HTML for problem %s</font>' % (self.location.url())
|
||||
msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<','<')
|
||||
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<','<')
|
||||
msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<', '<')
|
||||
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '<')
|
||||
html = msg
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
content = {'name': self.metadata['display_name'],
|
||||
'html': html,
|
||||
'weight': self.weight,
|
||||
@@ -319,8 +325,8 @@ class CapaModule(XModule):
|
||||
|
||||
if self.show_answer == 'always':
|
||||
return True
|
||||
#TODO: Not 404
|
||||
raise self.system.exception404
|
||||
|
||||
return False
|
||||
|
||||
def update_score(self, get):
|
||||
"""
|
||||
@@ -336,7 +342,7 @@ class CapaModule(XModule):
|
||||
score_msg = get['response']
|
||||
self.lcp.update_score(score_msg, queuekey)
|
||||
|
||||
return dict() # No AJAX return is needed
|
||||
return dict() # No AJAX return is needed
|
||||
|
||||
def get_answer(self, get):
|
||||
'''
|
||||
@@ -347,7 +353,7 @@ class CapaModule(XModule):
|
||||
Returns the answers: {'answers' : answers}
|
||||
'''
|
||||
if not self.answer_available():
|
||||
raise self.system.exception404
|
||||
raise NotFoundError('Answer is not available')
|
||||
else:
|
||||
answers = self.lcp.get_question_answers()
|
||||
return {'answers': answers}
|
||||
@@ -379,7 +385,7 @@ class CapaModule(XModule):
|
||||
if not name.endswith('[]'):
|
||||
answers[name] = get[key]
|
||||
else:
|
||||
name = name[:-2]
|
||||
name = name[:-2]
|
||||
answers[name] = get.getlist(key)
|
||||
|
||||
return answers
|
||||
@@ -403,15 +409,14 @@ class CapaModule(XModule):
|
||||
if self.closed():
|
||||
event_info['failure'] = 'closed'
|
||||
self.system.track_function('save_problem_check_fail', event_info)
|
||||
# TODO (vshnayder): probably not 404?
|
||||
raise self.system.exception404
|
||||
raise NotFoundError('Problem is closed')
|
||||
|
||||
# Problem submitted. Student should reset before checking
|
||||
# again.
|
||||
if self.lcp.done and self.rerandomize == "always":
|
||||
event_info['failure'] = 'unreset'
|
||||
self.system.track_function('save_problem_check_fail', event_info)
|
||||
raise self.system.exception404
|
||||
raise NotFoundError('Problem must be reset before it can be checked again')
|
||||
|
||||
try:
|
||||
old_state = self.lcp.get_state()
|
||||
@@ -421,7 +426,7 @@ class CapaModule(XModule):
|
||||
# TODO (vshnayder): why is this line here?
|
||||
#self.lcp = LoncapaProblem(self.definition['data'],
|
||||
# id=lcp_id, state=old_state, system=self.system)
|
||||
traceback.print_exc()
|
||||
log.exception("StudentInputError in capa_module:problem_check")
|
||||
return {'success': inst.message}
|
||||
except Exception, err:
|
||||
# TODO: why is this line here?
|
||||
@@ -430,8 +435,8 @@ class CapaModule(XModule):
|
||||
if self.system.DEBUG:
|
||||
msg = "Error checking problem: " + str(err)
|
||||
msg += '\nTraceback:\n' + traceback.format_exc()
|
||||
return {'success':msg}
|
||||
traceback.print_exc()
|
||||
return {'success': msg}
|
||||
log.exception("Error in capa_module problem checking")
|
||||
raise Exception("error in capa_module")
|
||||
|
||||
self.attempts = self.attempts + 1
|
||||
|
||||
@@ -1,35 +1,31 @@
|
||||
import time
|
||||
import dateutil.parser
|
||||
from fs.errors import ResourceNotFoundError
|
||||
import logging
|
||||
from path import path
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.seq_module import SequenceDescriptor, SequenceModule
|
||||
from fs.errors import ResourceNotFoundError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class CourseDescriptor(SequenceDescriptor):
|
||||
module_class = SequenceModule
|
||||
|
||||
def __init__(self, system, definition=None, **kwargs):
|
||||
super(CourseDescriptor, self).__init__(system, definition, **kwargs)
|
||||
|
||||
|
||||
try:
|
||||
self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M")
|
||||
except KeyError:
|
||||
self.start = time.gmtime(0) #The epoch
|
||||
self.start = time.gmtime(0) # The epoch
|
||||
log.critical("Course loaded without a start date. " + str(self.id))
|
||||
except ValueError, e:
|
||||
self.start = time.gmtime(0) #The epoch
|
||||
self.start = time.gmtime(0) # The epoch
|
||||
log.critical("Course loaded with a bad start date. " + str(self.id) + " '" + str(e) + "'")
|
||||
|
||||
|
||||
def has_started(self):
|
||||
return time.gmtime() > self.start
|
||||
|
||||
|
||||
@classmethod
|
||||
def id_to_location(cls, course_id):
|
||||
org, course, name = course_id.split('/')
|
||||
@@ -39,83 +35,22 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
def id(self):
|
||||
return "/".join([self.location.org, self.location.course, self.location.name])
|
||||
|
||||
@property
|
||||
def start_date_text(self):
|
||||
return time.strftime("%b %d, %Y", self.start)
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self.metadata['display_name']
|
||||
|
||||
|
||||
@property
|
||||
def number(self):
|
||||
return self.location.course
|
||||
|
||||
@property
|
||||
def instructors(self):
|
||||
return self.get_about_section("instructors").split("\n")
|
||||
|
||||
@property
|
||||
def wiki_namespace(self):
|
||||
return self.location.course
|
||||
|
||||
def get_about_section(self, section_key):
|
||||
"""
|
||||
This returns the snippet of html to be rendered on the course about page, given the key for the section.
|
||||
Valid keys:
|
||||
- overview
|
||||
- title
|
||||
- university
|
||||
- number
|
||||
- short_description
|
||||
- description
|
||||
- key_dates (includes start, end, exams, etc)
|
||||
- video
|
||||
- course_staff_short
|
||||
- course_staff_extended
|
||||
- requirements
|
||||
- syllabus
|
||||
- textbook
|
||||
- faq
|
||||
- more_info
|
||||
"""
|
||||
|
||||
# Many of these are stored as html files instead of some semantic markup. This can change without effecting
|
||||
# this interface when we find a good format for defining so many snippets of text/html.
|
||||
|
||||
# TODO: Remove number, instructors from this list
|
||||
if section_key in ['short_description', 'description', 'key_dates', 'video', 'course_staff_short', 'course_staff_extended',
|
||||
'requirements', 'syllabus', 'textbook', 'faq', 'more_info', 'number', 'instructors', 'overview']:
|
||||
try:
|
||||
with self.system.resources_fs.open(path("about") / section_key + ".html") as htmlFile:
|
||||
return htmlFile.read().decode('utf-8')
|
||||
except ResourceNotFoundError:
|
||||
log.exception("Missing about section {key} in course {url}".format(key=section_key, url=self.location.url()))
|
||||
return "! About section missing !"
|
||||
elif section_key == "title":
|
||||
return self.metadata.get('display_name', self.name)
|
||||
elif section_key == "university":
|
||||
return self.location.org
|
||||
elif section_key == "number":
|
||||
return self.number
|
||||
|
||||
raise KeyError("Invalid about key " + str(section_key))
|
||||
|
||||
def get_info_section(self, section_key):
|
||||
"""
|
||||
This returns the snippet of html to be rendered on the course info page, given the key for the section.
|
||||
Valid keys:
|
||||
- handouts
|
||||
- guest_handouts
|
||||
- updates
|
||||
- guest_updates
|
||||
"""
|
||||
|
||||
# Many of these are stored as html files instead of some semantic markup. This can change without effecting
|
||||
# this interface when we find a good format for defining so many snippets of text/html.
|
||||
|
||||
if section_key in ['handouts', 'guest_handouts', 'updates', 'guest_updates']:
|
||||
try:
|
||||
with self.system.resources_fs.open(path("info") / section_key + ".html") as htmlFile:
|
||||
return htmlFile.read().decode('utf-8')
|
||||
except ResourceNotFoundError:
|
||||
log.exception("Missing info section {key} in course {url}".format(key=section_key, url=self.location.url()))
|
||||
return "! Info section missing !"
|
||||
|
||||
raise KeyError("Invalid about key " + str(section_key))
|
||||
@property
|
||||
def org(self):
|
||||
return self.location.org
|
||||
|
||||
264
common/lib/xmodule/xmodule/css/capa/display.scss
Normal file
264
common/lib/xmodule/xmodule/css/capa/display.scss
Normal file
@@ -0,0 +1,264 @@
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
width: flex-grid(2, 9);
|
||||
padding-right: flex-gutter(9);
|
||||
border-right: 1px dashed #ddd;
|
||||
@include box-sizing(border-box);
|
||||
display: table-cell;
|
||||
vertical-align: top;
|
||||
|
||||
&.problem-header {
|
||||
section.staff {
|
||||
margin-top: 30px;
|
||||
font-size: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width:1120px) {
|
||||
display: block;
|
||||
width: auto;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
@media print {
|
||||
display: block;
|
||||
width: auto;
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
section.problem {
|
||||
display: table-cell;
|
||||
width: flex-grid(7, 9);
|
||||
padding-left: flex-gutter(9);
|
||||
|
||||
@media screen and (max-width:1120px) {
|
||||
display: block;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media print {
|
||||
display: block;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
|
||||
canvas, img {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
p.status {
|
||||
text-indent: -9999px;
|
||||
margin: -1px 0 0 10px;
|
||||
}
|
||||
|
||||
&.unanswered {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
background: url('../images/unanswered-icon.png') center center no-repeat;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&.correct, &.ui-icon-check {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
background: url('../images/correct-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
input {
|
||||
border-color: green;
|
||||
}
|
||||
}
|
||||
|
||||
&.incorrect, &.ui-icon-close {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
background: url('../images/incorrect-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
text-indent: -9999px;
|
||||
}
|
||||
|
||||
input {
|
||||
border-color: red;
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
margin-bottom: lh(.5);
|
||||
}
|
||||
|
||||
p.answer {
|
||||
@include inline-block();
|
||||
margin-bottom: 0;
|
||||
margin-left: 10px;
|
||||
|
||||
&:before {
|
||||
content: "Answer: ";
|
||||
font-weight: bold;
|
||||
display: inline;
|
||||
|
||||
}
|
||||
&:empty {
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.equation {
|
||||
clear: both;
|
||||
padding: 6px;
|
||||
background: #eee;
|
||||
|
||||
span {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
&.unanswered, &.ui-icon-bullet {
|
||||
@include inline-block();
|
||||
background: url('../images/unanswered-icon.png') center center no-repeat;
|
||||
height: 14px;
|
||||
position: relative;
|
||||
top: 4px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
&.correct, &.ui-icon-check {
|
||||
@include inline-block();
|
||||
background: url('../images/correct-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
&.incorrect, &.ui-icon-close {
|
||||
@include inline-block();
|
||||
background: url('../images/incorrect-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc outside none;
|
||||
margin-bottom: lh();
|
||||
margin-left: .75em;
|
||||
margin-left: .75rem;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal outside none;
|
||||
margin-bottom: lh();
|
||||
margin-left: .75em;
|
||||
margin-left: .75rem;
|
||||
}
|
||||
|
||||
dl {
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
dl dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
dl dd {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-left: .5em;
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
li {
|
||||
line-height: 1.4em;
|
||||
margin-bottom: lh(.5);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: lh();
|
||||
}
|
||||
|
||||
table {
|
||||
margin-bottom: lh();
|
||||
width: 100%;
|
||||
// border: 1px solid #ddd;
|
||||
border-collapse: collapse;
|
||||
|
||||
th {
|
||||
// border-bottom: 2px solid #ccc;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td {
|
||||
// border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
caption, th, td {
|
||||
padding: .25em .75em .25em 0;
|
||||
padding: .25rem .75rem .25rem 0;
|
||||
}
|
||||
|
||||
caption {
|
||||
background: #f1f1f1;
|
||||
margin-bottom: .75em;
|
||||
margin-bottom: .75rem;
|
||||
padding: .75em 0;
|
||||
padding: .75rem 0;
|
||||
}
|
||||
|
||||
tr, td, th {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
hr {
|
||||
background: #ddd;
|
||||
border: none;
|
||||
clear: both;
|
||||
color: #ddd;
|
||||
float: none;
|
||||
height: 1px;
|
||||
margin: 0 0 .75rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#{$all-text-inputs} {
|
||||
display: inline;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
// this supports a deprecated element and should be removed once the center tag is removed
|
||||
center {
|
||||
display: block;
|
||||
margin: lh() 0;
|
||||
border: 1px solid #ccc;
|
||||
padding: lh();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
nav.sequence-nav {
|
||||
// TODO (cpennington): This doesn't work anymore. XModules aren't able to
|
||||
// import from external sources.
|
||||
@extend .topbar;
|
||||
|
||||
border-bottom: 1px solid darken($cream, 20%);
|
||||
margin-bottom: $body-line-height;
|
||||
position: relative;
|
||||
@@ -87,60 +90,66 @@ nav.sequence-nav {
|
||||
}
|
||||
|
||||
//video
|
||||
&.seq_video_inactive {
|
||||
@extend .inactive;
|
||||
background-image: url('../images/sequence-nav/video-icon-normal.png');
|
||||
background-position: center;
|
||||
}
|
||||
&.seq_video {
|
||||
&.inactive {
|
||||
@extend .inactive;
|
||||
background-image: url('../images/sequence-nav/video-icon-normal.png');
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
&.seq_video_visited {
|
||||
@extend .visited;
|
||||
background-image: url('../images/sequence-nav/video-icon-visited.png');
|
||||
background-position: center;
|
||||
}
|
||||
&.visited {
|
||||
@extend .visited;
|
||||
background-image: url('../images/sequence-nav/video-icon-visited.png');
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
&.seq_video_active {
|
||||
@extend .active;
|
||||
background-image: url('../images/sequence-nav/video-icon-current.png');
|
||||
background-position: center;
|
||||
&.active {
|
||||
@extend .active;
|
||||
background-image: url('../images/sequence-nav/video-icon-current.png');
|
||||
background-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
//other
|
||||
&.seq_other_inactive {
|
||||
@extend .inactive;
|
||||
background-image: url('../images/sequence-nav/document-icon-normal.png');
|
||||
background-position: center;
|
||||
}
|
||||
&.seq_other {
|
||||
&.inactive {
|
||||
@extend .inactive;
|
||||
background-image: url('../images/sequence-nav/document-icon-normal.png');
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
&.seq_other_visited {
|
||||
@extend .visited;
|
||||
background-image: url('../images/sequence-nav/document-icon-visited.png');
|
||||
background-position: center;
|
||||
}
|
||||
&.visited {
|
||||
@extend .visited;
|
||||
background-image: url('../images/sequence-nav/document-icon-visited.png');
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
&.seq_other_active {
|
||||
@extend .active;
|
||||
background-image: url('../images/sequence-nav/document-icon-current.png');
|
||||
background-position: center;
|
||||
&.active {
|
||||
@extend .active;
|
||||
background-image: url('../images/sequence-nav/document-icon-current.png');
|
||||
background-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
//vertical & problems
|
||||
&.seq_vertical_inactive, &.seq_problem_inactive {
|
||||
@extend .inactive;
|
||||
background-image: url('../images/sequence-nav/list-icon-normal.png');
|
||||
background-position: center;
|
||||
}
|
||||
&.seq_vertical, &.seq_problem {
|
||||
&.inactive {
|
||||
@extend .inactive;
|
||||
background-image: url('../images/sequence-nav/list-icon-normal.png');
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
&.seq_vertical_visited, &.seq_problem_visited {
|
||||
@extend .visited;
|
||||
background-image: url('../images/sequence-nav/list-icon-visited.png');
|
||||
background-position: center;
|
||||
}
|
||||
&.visited {
|
||||
@extend .visited;
|
||||
background-image: url('../images/sequence-nav/list-icon-visited.png');
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
&.seq_vertical_active, &.seq_problem_active {
|
||||
@extend .active;
|
||||
background-image: url('../images/sequence-nav/list-icon-current.png');
|
||||
background-position: center;
|
||||
&.active {
|
||||
@extend .active;
|
||||
background-image: url('../images/sequence-nav/list-icon-current.png');
|
||||
background-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
@@ -254,82 +263,73 @@ nav.sequence-nav {
|
||||
}
|
||||
}
|
||||
|
||||
div.seq_contents {
|
||||
display: none;
|
||||
}
|
||||
|
||||
section.course-content {
|
||||
position: relative;
|
||||
nav.sequence-bottom {
|
||||
margin: lh(2) 0 0;
|
||||
text-align: center;
|
||||
|
||||
ol.vert-mod {
|
||||
nav.sequence-nav {
|
||||
margin-top: -15px;
|
||||
@include border-radius(0);
|
||||
}
|
||||
}
|
||||
ul {
|
||||
@extend .clearfix;
|
||||
background-color: darken(#F6EFD4, 5%);
|
||||
background-color: darken($cream, 5%);
|
||||
border: 1px solid darken(#f6efd4, 20%);
|
||||
@include border-radius(3px);
|
||||
@include box-shadow(inset 0 0 0 1px lighten(#f6efd4, 5%));
|
||||
@include inline-block();
|
||||
|
||||
nav.sequence-bottom {
|
||||
margin: lh(2) 0 0;
|
||||
text-align: center;
|
||||
li {
|
||||
float: left;
|
||||
|
||||
ul {
|
||||
@extend .clearfix;
|
||||
background-color: darken(#F6EFD4, 5%);
|
||||
background-color: darken($cream, 5%);
|
||||
border: 1px solid darken(#f6efd4, 20%);
|
||||
@include border-radius(3px);
|
||||
@include box-shadow(inset 0 0 0 1px lighten(#f6efd4, 5%));
|
||||
@include inline-block();
|
||||
&.prev, &.next {
|
||||
margin-bottom: 0;
|
||||
|
||||
li {
|
||||
float: left;
|
||||
a {
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
border-bottom: none;
|
||||
display: block;
|
||||
padding: lh(.5) 4px;
|
||||
text-indent: -9999px;
|
||||
@include transition(all, .2s, $ease-in-out-quad);
|
||||
width: 45px;
|
||||
|
||||
&.prev, &.next {
|
||||
margin-bottom: 0;
|
||||
&:hover {
|
||||
background-color: $cream;
|
||||
color: darken($cream, 60%);
|
||||
opacity: .5;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a {
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
border-bottom: none;
|
||||
display: block;
|
||||
padding: lh(.5) 4px;
|
||||
text-indent: -9999px;
|
||||
@include transition(all, .2s, $ease-in-out-quad);
|
||||
width: 45px;
|
||||
|
||||
&:hover {
|
||||
background-color: $cream;
|
||||
color: darken($cream, 60%);
|
||||
opacity: .5;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background-color: lighten($cream, 10%);
|
||||
opacity: .4;
|
||||
}
|
||||
&.disabled {
|
||||
background-color: lighten($cream, 10%);
|
||||
opacity: .4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.prev {
|
||||
a {
|
||||
background-image: url('../images/sequence-nav/previous-icon.png');
|
||||
border-right: 1px solid darken(#f6efd4, 20%);
|
||||
&.prev {
|
||||
a {
|
||||
background-image: url('../images/sequence-nav/previous-icon.png');
|
||||
border-right: 1px solid darken(#f6efd4, 20%);
|
||||
|
||||
&:hover {
|
||||
background-color: none;
|
||||
}
|
||||
&:hover {
|
||||
background-color: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.next {
|
||||
a {
|
||||
background-image: url('../images/sequence-nav/next-icon.png');
|
||||
&.next {
|
||||
a {
|
||||
background-image: url('../images/sequence-nav/next-icon.png');
|
||||
|
||||
&:hover {
|
||||
background-color: none;
|
||||
}
|
||||
&:hover {
|
||||
background-color: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
551
common/lib/xmodule/xmodule/css/video/display.scss
Normal file
551
common/lib/xmodule/xmodule/css/video/display.scss
Normal file
@@ -0,0 +1,551 @@
|
||||
div.video {
|
||||
@include clearfix();
|
||||
background: #f3f3f3;
|
||||
border-bottom: 1px solid #e1e1e1;
|
||||
border-top: 1px solid #e1e1e1;
|
||||
display: block;
|
||||
margin: 0 (-(lh()));
|
||||
padding: 6px lh();
|
||||
|
||||
article.video-wrapper {
|
||||
float: left;
|
||||
margin-right: flex-gutter(9);
|
||||
width: flex-grid(6, 9);
|
||||
|
||||
section.video-player {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
padding-bottom: 56.25%;
|
||||
padding-top: 30px;
|
||||
position: relative;
|
||||
|
||||
object, iframe {
|
||||
border: none;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
section.video-controls {
|
||||
@extend .clearfix;
|
||||
background: #333;
|
||||
border: 1px solid #000;
|
||||
border-top: 0;
|
||||
color: #ccc;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
ul, div {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
div.slider {
|
||||
@extend .clearfix;
|
||||
background: #c2c2c2;
|
||||
border: none;
|
||||
border-bottom: 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;
|
||||
@include transition(height 2.0s ease-in-out);
|
||||
|
||||
div.ui-widget-header {
|
||||
background: #777;
|
||||
@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;
|
||||
@include background-size(50%);
|
||||
border: 1px solid darken($mit-red, 20%);
|
||||
@include border-radius(15px);
|
||||
@include box-shadow(inset 0 1px 0 lighten($mit-red, 10%));
|
||||
cursor: pointer;
|
||||
height: 15px;
|
||||
margin-left: -7px;
|
||||
top: -4px;
|
||||
@include transition(height 2.0s ease-in-out, width 2.0s ease-in-out);
|
||||
width: 15px;
|
||||
|
||||
&:focus, &:hover {
|
||||
background-color: lighten($mit-red, 10%);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul.vcr {
|
||||
@extend .dullify;
|
||||
float: left;
|
||||
list-style: none;
|
||||
margin-right: lh();
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
float: left;
|
||||
margin-bottom: 0;
|
||||
|
||||
|
||||
a {
|
||||
border-bottom: none;
|
||||
border-right: 1px solid #000;
|
||||
@include box-shadow(1px 0 0 #555);
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
line-height: 46px;
|
||||
padding: 0 lh(.75);
|
||||
text-indent: -9999px;
|
||||
@include transition(background-color, opacity);
|
||||
width: 14px;
|
||||
background: url('../images/vcr.png') 15px 15px no-repeat;
|
||||
|
||||
&:empty {
|
||||
height: 46px;
|
||||
background: url('../images/vcr.png') 15px 15px no-repeat;
|
||||
}
|
||||
|
||||
&.play {
|
||||
background-position: 17px -114px;
|
||||
|
||||
&:hover {
|
||||
background-color: #444;
|
||||
}
|
||||
}
|
||||
|
||||
&.pause {
|
||||
background-position: 16px -50px;
|
||||
|
||||
&:hover {
|
||||
background-color: #444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.vidtime {
|
||||
padding-left: lh(.75);
|
||||
font-weight: bold;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
padding-left: lh(.75);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.secondary-controls {
|
||||
@extend .dullify;
|
||||
float: right;
|
||||
|
||||
div.speeds {
|
||||
float: left;
|
||||
position: relative;
|
||||
|
||||
&.open {
|
||||
&>a {
|
||||
background: url('../images/open-arrow.png') 10px center no-repeat;
|
||||
}
|
||||
|
||||
ol.video_speeds {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&>a {
|
||||
background: url('../images/closed-arrow.png') 10px center no-repeat;
|
||||
border-left: 1px solid #000;
|
||||
border-right: 1px solid #000;
|
||||
@include box-shadow(1px 0 0 #555, inset 1px 0 0 #555);
|
||||
@include clearfix();
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
margin-right: 0;
|
||||
padding-left: 15px;
|
||||
position: relative;
|
||||
@include transition();
|
||||
-webkit-font-smoothing: antialiased;
|
||||
width: 110px;
|
||||
|
||||
h3 {
|
||||
color: #999;
|
||||
float: left;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
letter-spacing: 1px;
|
||||
padding: 0 lh(.25) 0 lh(.5);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
p.active {
|
||||
float: left;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0;
|
||||
padding: 0 lh(.5) 0 0;
|
||||
}
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
opacity: 1;
|
||||
background-color: #444;
|
||||
}
|
||||
}
|
||||
|
||||
// fix for now
|
||||
ol.video_speeds {
|
||||
@include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444);
|
||||
@include transition();
|
||||
background-color: #444;
|
||||
border: 1px solid #000;
|
||||
bottom: 46px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
width: 125px;
|
||||
z-index: 10;
|
||||
|
||||
li {
|
||||
@include box-shadow( 0 1px 0 #555);
|
||||
border-bottom: 1px solid #000;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
|
||||
a {
|
||||
border: 0;
|
||||
color: #fff;
|
||||
display: block;
|
||||
padding: lh(.5);
|
||||
|
||||
&:hover {
|
||||
background-color: #666;
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@include box-shadow(none);
|
||||
border-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.volume {
|
||||
float: left;
|
||||
position: relative;
|
||||
|
||||
&.open {
|
||||
.volume-slider-container {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.muted {
|
||||
&>a {
|
||||
background: url('../images/mute.png') 10px center no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
> a {
|
||||
background: url('../images/volume.png') 10px center no-repeat;
|
||||
border-right: 1px solid #000;
|
||||
@include box-shadow(1px 0 0 #555, inset 1px 0 0 #555);
|
||||
@include clearfix();
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
height: 46px;
|
||||
margin-right: 0;
|
||||
padding-left: 15px;
|
||||
position: relative;
|
||||
@include transition();
|
||||
-webkit-font-smoothing: antialiased;
|
||||
width: 30px;
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
background-color: #444;
|
||||
}
|
||||
}
|
||||
|
||||
.volume-slider-container {
|
||||
@include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444);
|
||||
@include transition();
|
||||
background-color: #444;
|
||||
border: 1px solid #000;
|
||||
bottom: 46px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
width: 45px;
|
||||
height: 125px;
|
||||
margin-left: -1px;
|
||||
z-index: 10;
|
||||
|
||||
.volume-slider {
|
||||
height: 100px;
|
||||
border: 0;
|
||||
width: 5px;
|
||||
margin: 14px auto;
|
||||
background: #666;
|
||||
border: 1px solid #000;
|
||||
@include box-shadow(0 1px 0 #333);
|
||||
|
||||
a.ui-slider-handle {
|
||||
background: $mit-red url(../images/slider-handle.png) center center no-repeat;
|
||||
@include background-size(50%);
|
||||
border: 1px solid darken($mit-red, 20%);
|
||||
@include border-radius(15px);
|
||||
@include box-shadow(inset 0 1px 0 lighten($mit-red, 10%));
|
||||
cursor: pointer;
|
||||
height: 15px;
|
||||
left: -6px;
|
||||
@include transition(height 2.0s ease-in-out, width 2.0s ease-in-out);
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
.ui-slider-range {
|
||||
background: #ddd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.add-fullscreen {
|
||||
background: url(../images/fullscreen.png) center no-repeat;
|
||||
border-right: 1px solid #000;
|
||||
@include box-shadow(1px 0 0 #555, inset 1px 0 0 #555);
|
||||
color: #797979;
|
||||
display: block;
|
||||
float: left;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
margin-left: 0;
|
||||
padding: 0 lh(.5);
|
||||
text-indent: -9999px;
|
||||
@include transition();
|
||||
width: 30px;
|
||||
|
||||
&:hover {
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
a.hide-subtitles {
|
||||
background: url('../images/cc.png') center no-repeat;
|
||||
color: #797979;
|
||||
display: block;
|
||||
float: left;
|
||||
font-weight: 800;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
margin-left: 0;
|
||||
opacity: 1;
|
||||
padding: 0 lh(.5);
|
||||
position: relative;
|
||||
text-indent: -9999px;
|
||||
@include transition();
|
||||
-webkit-font-smoothing: antialiased;
|
||||
width: 30px;
|
||||
|
||||
&:hover {
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.off {
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover section.video-controls {
|
||||
ul, div {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
div.slider {
|
||||
height: 14px;
|
||||
margin-top: -7px;
|
||||
|
||||
a.ui-slider-handle {
|
||||
@include border-radius(20px);
|
||||
height: 20px;
|
||||
margin-left: -10px;
|
||||
top: -4px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ol.subtitles {
|
||||
float: left;
|
||||
max-height: 460px;
|
||||
overflow: auto;
|
||||
width: flex-grid(3, 9);
|
||||
|
||||
li {
|
||||
border: 0;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
margin-bottom: 8px;
|
||||
padding: 0;
|
||||
|
||||
&.current {
|
||||
color: #333;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $mit-red;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.closed {
|
||||
@extend .trans;
|
||||
|
||||
article.video-wrapper {
|
||||
width: flex-grid(9,9);
|
||||
}
|
||||
|
||||
ol.subtitles {
|
||||
width: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&.fullscreen {
|
||||
background: rgba(#000, .95);
|
||||
border: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 999;
|
||||
|
||||
&.closed {
|
||||
ol.subtitles {
|
||||
right: -(flex-grid(4));
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
a.exit {
|
||||
color: #aaa;
|
||||
display: none;
|
||||
font-style: 12px;
|
||||
left: 20px;
|
||||
letter-spacing: 1px;
|
||||
position: absolute;
|
||||
text-transform: uppercase;
|
||||
top: 20px;
|
||||
|
||||
&::after {
|
||||
content: "✖";
|
||||
@include inline-block();
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $mit-red;
|
||||
}
|
||||
}
|
||||
|
||||
div.tc-wrapper {
|
||||
article.video-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
object, iframe {
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
section.video-controls {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 9999;
|
||||
}
|
||||
}
|
||||
|
||||
ol.subtitles {
|
||||
background: rgba(#000, .8);
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
max-width: flex-grid(3);
|
||||
padding: lh();
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
@include transition();
|
||||
|
||||
li {
|
||||
color: #aaa;
|
||||
|
||||
&.current {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,5 @@
|
||||
class InvalidDefinitionError(Exception):
|
||||
pass
|
||||
|
||||
class NotFoundError(Exception):
|
||||
pass
|
||||
|
||||
@@ -48,7 +48,7 @@ def grader_from_conf(conf):
|
||||
"""
|
||||
if isinstance(conf, CourseGrader):
|
||||
return conf
|
||||
|
||||
|
||||
subgraders = []
|
||||
for subgraderconf in conf:
|
||||
subgraderconf = subgraderconf.copy()
|
||||
@@ -57,45 +57,45 @@ def grader_from_conf(conf):
|
||||
if 'min_count' in subgraderconf:
|
||||
#This is an AssignmentFormatGrader
|
||||
subgrader = AssignmentFormatGrader(**subgraderconf)
|
||||
subgraders.append( (subgrader, subgrader.category, weight) )
|
||||
subgraders.append((subgrader, subgrader.category, weight))
|
||||
elif 'name' in subgraderconf:
|
||||
#This is an SingleSectionGrader
|
||||
subgrader = SingleSectionGrader(**subgraderconf)
|
||||
subgraders.append( (subgrader, subgrader.category, weight) )
|
||||
subgraders.append((subgrader, subgrader.category, weight))
|
||||
else:
|
||||
raise ValueError("Configuration has no appropriate grader class.")
|
||||
|
||||
|
||||
except (TypeError, ValueError) as error:
|
||||
errorString = "Unable to parse grader configuration:\n " + str(subgraderconf) + "\n Error was:\n " + str(error)
|
||||
log.critical(errorString)
|
||||
raise ValueError(errorString)
|
||||
|
||||
return WeightedSubsectionsGrader( subgraders )
|
||||
|
||||
return WeightedSubsectionsGrader(subgraders)
|
||||
|
||||
|
||||
class CourseGrader(object):
|
||||
"""
|
||||
A course grader takes the totaled scores for each graded section (that a student has
|
||||
started) in the course. From these scores, the grader calculates an overall percentage
|
||||
A course grader takes the totaled scores for each graded section (that a student has
|
||||
started) in the course. From these scores, the grader calculates an overall percentage
|
||||
grade. The grader should also generate information about how that score was calculated,
|
||||
to be displayed in graphs or charts.
|
||||
|
||||
|
||||
A grader has one required method, grade(), which is passed a grade_sheet. The grade_sheet
|
||||
contains scores for all graded section that the student has started. If a student has
|
||||
a score of 0 for that section, it may be missing from the grade_sheet. The grade_sheet
|
||||
is keyed by section format. Each value is a list of Score namedtuples for each section
|
||||
that has the matching section format.
|
||||
|
||||
|
||||
The grader outputs a dictionary with the following keys:
|
||||
- percent: Contaisn a float value, which is the final percentage score for the student.
|
||||
- section_breakdown: This is a list of dictionaries which provide details on sections
|
||||
that were graded. These are used for display in a graph or chart. The format for a
|
||||
that were graded. These are used for display in a graph or chart. The format for a
|
||||
section_breakdown dictionary is explained below.
|
||||
- grade_breakdown: This is a list of dictionaries which provide details on the contributions
|
||||
of the final percentage grade. This is a higher level breakdown, for when the grade is constructed
|
||||
of a few very large sections (such as Homeworks, Labs, a Midterm, and a Final). The format for
|
||||
a grade_breakdown is explained below. This section is optional.
|
||||
|
||||
|
||||
A dictionary in the section_breakdown list has the following keys:
|
||||
percent: A float percentage for the section.
|
||||
label: A short string identifying the section. Preferably fixed-length. E.g. "HW 3".
|
||||
@@ -104,71 +104,72 @@ class CourseGrader(object):
|
||||
in the display (for example, by color).
|
||||
prominent: A boolean value indicating that this section should be displayed as more prominent
|
||||
than other items.
|
||||
|
||||
|
||||
A dictionary in the grade_breakdown list has the following keys:
|
||||
percent: A float percentage in the breakdown. All percents should add up to the final percentage.
|
||||
detail: A string explanation of this breakdown. E.g. "Homework - 10% of a possible 15%"
|
||||
category: A string identifying the category. Items with the same category are grouped together
|
||||
in the display (for example, by color).
|
||||
|
||||
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
|
||||
@abc.abstractmethod
|
||||
def grade(self, grade_sheet):
|
||||
raise NotImplementedError
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class WeightedSubsectionsGrader(CourseGrader):
|
||||
"""
|
||||
This grader takes a list of tuples containing (grader, category_name, weight) and computes
|
||||
a final grade by totalling the contribution of each sub grader and multiplying it by the
|
||||
given weight. For example, the sections may be
|
||||
given weight. For example, the sections may be
|
||||
[ (homeworkGrader, "Homework", 0.15), (labGrader, "Labs", 0.15), (midtermGrader, "Midterm", 0.30), (finalGrader, "Final", 0.40) ]
|
||||
All items in section_breakdown for each subgrader will be combined. A grade_breakdown will be
|
||||
composed using the score from each grader.
|
||||
|
||||
|
||||
Note that the sum of the weights is not take into consideration. If the weights add up to
|
||||
a value > 1, the student may end up with a percent > 100%. This allows for sections that
|
||||
are extra credit.
|
||||
"""
|
||||
def __init__(self, sections):
|
||||
self.sections = sections
|
||||
|
||||
|
||||
def grade(self, grade_sheet):
|
||||
total_percent = 0.0
|
||||
section_breakdown = []
|
||||
grade_breakdown = []
|
||||
|
||||
|
||||
for subgrader, category, weight in self.sections:
|
||||
subgrade_result = subgrader.grade(grade_sheet)
|
||||
|
||||
|
||||
weightedPercent = subgrade_result['percent'] * weight
|
||||
section_detail = "{0} = {1:.1%} of a possible {2:.0%}".format(category, weightedPercent, weight)
|
||||
|
||||
|
||||
total_percent += weightedPercent
|
||||
section_breakdown += subgrade_result['section_breakdown']
|
||||
grade_breakdown.append( {'percent' : weightedPercent, 'detail' : section_detail, 'category' : category} )
|
||||
|
||||
return {'percent' : total_percent,
|
||||
'section_breakdown' : section_breakdown,
|
||||
'grade_breakdown' : grade_breakdown}
|
||||
grade_breakdown.append({'percent': weightedPercent, 'detail': section_detail, 'category': category})
|
||||
|
||||
return {'percent': total_percent,
|
||||
'section_breakdown': section_breakdown,
|
||||
'grade_breakdown': grade_breakdown}
|
||||
|
||||
|
||||
class SingleSectionGrader(CourseGrader):
|
||||
"""
|
||||
This grades a single section with the format 'type' and the name 'name'.
|
||||
|
||||
|
||||
If the name is not appropriate for the short short_label or category, they each may
|
||||
be specified individually.
|
||||
"""
|
||||
def __init__(self, type, name, short_label = None, category = None):
|
||||
def __init__(self, type, name, short_label=None, category=None):
|
||||
self.type = type
|
||||
self.name = name
|
||||
self.short_label = short_label or name
|
||||
self.category = category or name
|
||||
|
||||
|
||||
def grade(self, grade_sheet):
|
||||
foundScore = None
|
||||
if self.type in grade_sheet:
|
||||
@@ -176,58 +177,59 @@ class SingleSectionGrader(CourseGrader):
|
||||
if score.section == self.name:
|
||||
foundScore = score
|
||||
break
|
||||
|
||||
|
||||
if foundScore:
|
||||
percent = foundScore.earned / float(foundScore.possible)
|
||||
detail = "{name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format( name = self.name,
|
||||
percent = percent,
|
||||
earned = float(foundScore.earned),
|
||||
possible = float(foundScore.possible))
|
||||
|
||||
percent = foundScore.earned / float(foundScore.possible)
|
||||
detail = "{name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(name=self.name,
|
||||
percent=percent,
|
||||
earned=float(foundScore.earned),
|
||||
possible=float(foundScore.possible))
|
||||
|
||||
else:
|
||||
percent = 0.0
|
||||
detail = "{name} - 0% (?/?)".format(name = self.name)
|
||||
|
||||
detail = "{name} - 0% (?/?)".format(name=self.name)
|
||||
|
||||
breakdown = [{'percent': percent, 'label': self.short_label, 'detail': detail, 'category': self.category, 'prominent': True}]
|
||||
|
||||
return {'percent' : percent,
|
||||
'section_breakdown' : breakdown,
|
||||
|
||||
return {'percent': percent,
|
||||
'section_breakdown': breakdown,
|
||||
#No grade_breakdown here
|
||||
}
|
||||
|
||||
|
||||
|
||||
class AssignmentFormatGrader(CourseGrader):
|
||||
"""
|
||||
Grades all sections matching the format 'type' with an equal weight. A specified
|
||||
number of lowest scores can be dropped from the calculation. The minimum number of
|
||||
sections in this format must be specified (even if those sections haven't been
|
||||
written yet).
|
||||
|
||||
|
||||
min_count defines how many assignments are expected throughout the course. Placeholder
|
||||
scores (of 0) will be inserted if the number of matching sections in the course is < min_count.
|
||||
If there number of matching sections in the course is > min_count, min_count will be ignored.
|
||||
|
||||
category should be presentable to the user, but may not appear. When the grade breakdown is
|
||||
|
||||
category should be presentable to the user, but may not appear. When the grade breakdown is
|
||||
displayed, scores from the same category will be similar (for example, by color).
|
||||
|
||||
|
||||
section_type is a string that is the type of a singular section. For example, for Labs it
|
||||
would be "Lab". This defaults to be the same as category.
|
||||
|
||||
|
||||
short_label is similar to section_type, but shorter. For example, for Homework it would be
|
||||
"HW".
|
||||
|
||||
|
||||
"""
|
||||
def __init__(self, type, min_count, drop_count, category = None, section_type = None, short_label = None):
|
||||
def __init__(self, type, min_count, drop_count, category=None, section_type=None, short_label=None):
|
||||
self.type = type
|
||||
self.min_count = min_count
|
||||
self.drop_count = drop_count
|
||||
self.category = category or self.type
|
||||
self.section_type = section_type or self.type
|
||||
self.short_label = short_label or self.type
|
||||
|
||||
|
||||
def grade(self, grade_sheet):
|
||||
def totalWithDrops(breakdown, drop_count):
|
||||
#create an array of tuples with (index, mark), sorted by mark['percent'] descending
|
||||
sorted_breakdown = sorted( enumerate(breakdown), key=lambda x: -x[1]['percent'] )
|
||||
sorted_breakdown = sorted(enumerate(breakdown), key=lambda x: -x[1]['percent'])
|
||||
# A list of the indices of the dropped scores
|
||||
dropped_indices = []
|
||||
if drop_count > 0:
|
||||
@@ -236,44 +238,42 @@ class AssignmentFormatGrader(CourseGrader):
|
||||
for index, mark in enumerate(breakdown):
|
||||
if index not in dropped_indices:
|
||||
aggregate_score += mark['percent']
|
||||
|
||||
|
||||
if (len(breakdown) - drop_count > 0):
|
||||
aggregate_score /= len(breakdown) - drop_count
|
||||
|
||||
|
||||
return aggregate_score, dropped_indices
|
||||
|
||||
|
||||
#Figure the homework scores
|
||||
scores = grade_sheet.get(self.type, [])
|
||||
breakdown = []
|
||||
for i in range( max(self.min_count, len(scores)) ):
|
||||
for i in range(max(self.min_count, len(scores))):
|
||||
if i < len(scores):
|
||||
percentage = scores[i].earned / float(scores[i].possible)
|
||||
summary = "{section_type} {index} - {name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(index = i+1,
|
||||
section_type = self.section_type,
|
||||
name = scores[i].section,
|
||||
percent = percentage,
|
||||
earned = float(scores[i].earned),
|
||||
possible = float(scores[i].possible) )
|
||||
summary = "{section_type} {index} - {name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(index=i + 1,
|
||||
section_type=self.section_type,
|
||||
name=scores[i].section,
|
||||
percent=percentage,
|
||||
earned=float(scores[i].earned),
|
||||
possible=float(scores[i].possible))
|
||||
else:
|
||||
percentage = 0
|
||||
summary = "{section_type} {index} Unreleased - 0% (?/?)".format(index = i+1, section_type = self.section_type)
|
||||
|
||||
short_label = "{short_label} {index:02d}".format(index = i+1, short_label = self.short_label)
|
||||
|
||||
breakdown.append( {'percent': percentage, 'label': short_label, 'detail': summary, 'category': self.category} )
|
||||
|
||||
summary = "{section_type} {index} Unreleased - 0% (?/?)".format(index=i + 1, section_type=self.section_type)
|
||||
|
||||
short_label = "{short_label} {index:02d}".format(index=i + 1, short_label=self.short_label)
|
||||
|
||||
breakdown.append({'percent': percentage, 'label': short_label, 'detail': summary, 'category': self.category})
|
||||
|
||||
total_percent, dropped_indices = totalWithDrops(breakdown, self.drop_count)
|
||||
|
||||
|
||||
for dropped_index in dropped_indices:
|
||||
breakdown[dropped_index]['mark'] = {'detail': "The lowest {drop_count} {section_type} scores are dropped.".format(drop_count = self.drop_count, section_type=self.section_type) }
|
||||
|
||||
|
||||
total_detail = "{section_type} Average = {percent:.0%}".format(percent = total_percent, section_type = self.section_type)
|
||||
total_label = "{short_label} Avg".format(short_label = self.short_label)
|
||||
breakdown.append( {'percent': total_percent, 'label': total_label, 'detail': total_detail, 'category': self.category, 'prominent': True} )
|
||||
|
||||
|
||||
return {'percent' : total_percent,
|
||||
'section_breakdown' : breakdown,
|
||||
breakdown[dropped_index]['mark'] = {'detail': "The lowest {drop_count} {section_type} scores are dropped.".format(drop_count=self.drop_count, section_type=self.section_type)}
|
||||
|
||||
total_detail = "{section_type} Average = {percent:.0%}".format(percent=total_percent, section_type=self.section_type)
|
||||
total_label = "{short_label} Avg".format(short_label=self.short_label)
|
||||
breakdown.append({'percent': total_percent, 'label': total_label, 'detail': total_detail, 'category': self.category, 'prominent': True})
|
||||
|
||||
return {'percent': total_percent,
|
||||
'section_breakdown': breakdown,
|
||||
#No grade_breakdown here
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ from lxml import etree
|
||||
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from pkg_resources import resource_string
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
@@ -26,9 +25,6 @@ class HtmlDescriptor(RawDescriptor):
|
||||
module_class = HtmlModule
|
||||
filename_extension = "html"
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/module/html.coffee')]}
|
||||
js_module = 'HTML'
|
||||
|
||||
# TODO (cpennington): Delete this method once all fall 2012 course are being
|
||||
# edited in the cms
|
||||
@classmethod
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user