Commit 801df378 by Calen Pennington

Merge remote-tracking branch 'origin/master' into arjun/javascript_response

Conflicts:
	common/lib/xmodule/xmodule/js/src/capa/display.coffee
	common/lib/xmodule/xmodule/tests/__init__.py
	common/lib/xmodule/xmodule/x_module.py
	lms/djangoapps/courseware/module_render.py
parents b9f2d4bc bd95c03d
Subproject commit 1c3381046c78e055439ba1c78e0df48410fcc13e Subproject commit e56ae380846f7c6cdaeacfc58880fab103540491
...@@ -249,7 +249,7 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_ ...@@ -249,7 +249,7 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
module = descriptor.xmodule_constructor(system)(instance_state, shared_state) module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
module.get_html = replace_static_urls( module.get_html = replace_static_urls(
wrap_xmodule(module.get_html, module, "xmodule_display.html"), wrap_xmodule(module.get_html, module, "xmodule_display.html"),
module.metadata['data_dir'] module.metadata['data_dir'], module
) )
save_preview_state(request, preview_id, descriptor.location.url(), save_preview_state(request, preview_id, descriptor.location.url(),
module.get_instance_state(), module.get_shared_state()) module.get_instance_state(), module.get_shared_state())
......
...@@ -83,7 +83,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( ...@@ -83,7 +83,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.request', 'django.core.context_processors.request',
'django.core.context_processors.static', 'django.core.context_processors.static',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'django.core.context_processors.auth', # this is required for admin 'django.contrib.auth.context_processors.auth', # this is required for admin
'django.core.context_processors.csrf', # necessary for csrf protection 'django.core.context_processors.csrf', # necessary for csrf protection
) )
...@@ -121,6 +121,7 @@ MIDDLEWARE_CLASSES = ( ...@@ -121,6 +121,7 @@ MIDDLEWARE_CLASSES = (
) )
############################ SIGNAL HANDLERS ################################ ############################ SIGNAL HANDLERS ################################
# This is imported to register the exception signal handling that logs exceptions
import monitoring.exceptions # noqa import monitoring.exceptions # noqa
############################ DJANGO_BUILTINS ################################ ############################ DJANGO_BUILTINS ################################
......
...@@ -55,6 +55,17 @@ DATABASES = { ...@@ -55,6 +55,17 @@ DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "cms.db", 'NAME': ENV_ROOT / "db" / "cms.db",
},
# The following are for testing purposes...
'edX/toy/2012_Fall': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "course1.db",
},
'edx/full/6.002_Spring_2012': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "course2.db",
} }
} }
......
...@@ -2,7 +2,7 @@ describe "CMS", -> ...@@ -2,7 +2,7 @@ describe "CMS", ->
beforeEach -> beforeEach ->
CMS.unbind() CMS.unbind()
it "should iniitalize Models", -> it "should initialize Models", ->
expect(CMS.Models).toBeDefined() expect(CMS.Models).toBeDefined()
it "should initialize Views", -> it "should initialize Views", ->
......
...@@ -11,14 +11,25 @@ describe "CMS.Models.Module", -> ...@@ -11,14 +11,25 @@ describe "CMS.Models.Module", ->
@fakeModule = jasmine.createSpy("fakeModuleObject") @fakeModule = jasmine.createSpy("fakeModuleObject")
window.FakeModule = jasmine.createSpy("FakeModule").andReturn(@fakeModule) window.FakeModule = jasmine.createSpy("FakeModule").andReturn(@fakeModule)
@module = new CMS.Models.Module(type: "FakeModule") @module = new CMS.Models.Module(type: "FakeModule")
@stubElement = $("<div>") @stubDiv = $('<div />')
@module.loadModule(@stubElement) @stubElement = $('<div class="xmodule_edit" />')
@stubElement.data('type', "FakeModule")
@stubDiv.append(@stubElement)
@module.loadModule(@stubDiv)
afterEach -> afterEach ->
window.FakeModule = undefined window.FakeModule = undefined
it "initialize the module", -> it "initialize the module", ->
expect(window.FakeModule).toHaveBeenCalledWith(@stubElement) expect(window.FakeModule).toHaveBeenCalled()
# Need to compare underlying nodes, because jquery selectors
# aren't equal even when they point to the same node.
# http://stackoverflow.com/questions/9505437/how-to-test-jquery-with-jasmine-for-element-id-if-used-as-this
expectedNode = @stubElement[0]
actualNode = window.FakeModule.mostRecentCall.args[0][0]
expect(actualNode).toEqual(expectedNode)
expect(@module.module).toEqual(@fakeModule) expect(@module.module).toEqual(@fakeModule)
describe "when the module does not exists", -> describe "when the module does not exists", ->
...@@ -32,7 +43,8 @@ describe "CMS.Models.Module", -> ...@@ -32,7 +43,8 @@ describe "CMS.Models.Module", ->
window.console = @previousConsole window.console = @previousConsole
it "print out error to log", -> it "print out error to log", ->
expect(window.console.error).toHaveBeenCalledWith("Unable to load HTML.") expect(window.console.error).toHaveBeenCalled()
expect(window.console.error.mostRecentCall.args[0]).toMatch("^Unable to load")
describe "editUrl", -> describe "editUrl", ->
......
...@@ -8,11 +8,11 @@ describe "CMS.Views.ModuleEdit", -> ...@@ -8,11 +8,11 @@ describe "CMS.Views.ModuleEdit", ->
<a href="#" class="cancel">cancel</a> <a href="#" class="cancel">cancel</a>
<ol> <ol>
<li> <li>
<a href="#" class="module-edit" data-id="i4x://mitx.edu/course/module" data-type="html">submodule</a> <a href="#" class="module-edit" data-id="i4x://mitx/course/html/module" data-type="html">submodule</a>
</li> </li>
</ol> </ol>
</div> </div>
""" """ #"
CMS.unbind() CMS.unbind()
describe "defaults", -> describe "defaults", ->
...@@ -27,7 +27,7 @@ describe "CMS.Views.ModuleEdit", -> ...@@ -27,7 +27,7 @@ describe "CMS.Views.ModuleEdit", ->
@stubModule.editUrl.andReturn("/edit_item?id=stub_module") @stubModule.editUrl.andReturn("/edit_item?id=stub_module")
new CMS.Views.ModuleEdit(el: $("#module-edit"), model: @stubModule) new CMS.Views.ModuleEdit(el: $("#module-edit"), model: @stubModule)
it "load the edit from via ajax and pass to the model", -> it "load the edit via ajax and pass to the model", ->
expect($.fn.load).toHaveBeenCalledWith("/edit_item?id=stub_module", jasmine.any(Function)) expect($.fn.load).toHaveBeenCalledWith("/edit_item?id=stub_module", jasmine.any(Function))
if $.fn.load.mostRecentCall if $.fn.load.mostRecentCall
$.fn.load.mostRecentCall.args[1]() $.fn.load.mostRecentCall.args[1]()
...@@ -37,9 +37,9 @@ describe "CMS.Views.ModuleEdit", -> ...@@ -37,9 +37,9 @@ describe "CMS.Views.ModuleEdit", ->
beforeEach -> beforeEach ->
@stubJqXHR = jasmine.createSpy("stubJqXHR") @stubJqXHR = jasmine.createSpy("stubJqXHR")
@stubJqXHR.success = jasmine.createSpy("stubJqXHR.success").andReturn(@stubJqXHR) @stubJqXHR.success = jasmine.createSpy("stubJqXHR.success").andReturn(@stubJqXHR)
@stubJqXHR.error= jasmine.createSpy("stubJqXHR.success").andReturn(@stubJqXHR) @stubJqXHR.error = jasmine.createSpy("stubJqXHR.error").andReturn(@stubJqXHR)
@stubModule.save = jasmine.createSpy("stubModule.save").andReturn(@stubJqXHR) @stubModule.save = jasmine.createSpy("stubModule.save").andReturn(@stubJqXHR)
new CMS.Views.ModuleEdit(el: $("#module-edit"), model: @stubModule) new CMS.Views.ModuleEdit(el: $(".module-edit"), model: @stubModule)
spyOn(window, "alert") spyOn(window, "alert")
$(".save-update").click() $(".save-update").click()
...@@ -77,5 +77,5 @@ describe "CMS.Views.ModuleEdit", -> ...@@ -77,5 +77,5 @@ describe "CMS.Views.ModuleEdit", ->
expect(CMS.pushView).toHaveBeenCalledWith @view expect(CMS.pushView).toHaveBeenCalledWith @view
expect(CMS.Views.ModuleEdit).toHaveBeenCalledWith model: @model expect(CMS.Views.ModuleEdit).toHaveBeenCalledWith model: @model
expect(CMS.Models.Module).toHaveBeenCalledWith expect(CMS.Models.Module).toHaveBeenCalledWith
id: "i4x://mitx.edu/course/module" id: "i4x://mitx/course/html/module"
type: "html" type: "html"
describe "CMS.Views.Module", -> describe "CMS.Views.Module", ->
beforeEach -> beforeEach ->
setFixtures """ setFixtures """
<div id="module" data-id="i4x://mitx.edu/course/module" data-type="html"> <div id="module" data-id="i4x://mitx/course/html/module" data-type="html">
<a href="#" class="module-edit">edit</a> <a href="#" class="module-edit">edit</a>
</div> </div>
""" """
...@@ -20,5 +20,5 @@ describe "CMS.Views.Module", -> ...@@ -20,5 +20,5 @@ describe "CMS.Views.Module", ->
expect(CMS.replaceView).toHaveBeenCalledWith @view expect(CMS.replaceView).toHaveBeenCalledWith @view
expect(CMS.Views.ModuleEdit).toHaveBeenCalledWith model: @model expect(CMS.Views.ModuleEdit).toHaveBeenCalledWith model: @model
expect(CMS.Models.Module).toHaveBeenCalledWith expect(CMS.Models.Module).toHaveBeenCalledWith
id: "i4x://mitx.edu/course/module" id: "i4x://mitx/course/html/module"
type: "html" type: "html"
describe "CMS.Views.Week", -> describe "CMS.Views.Week", ->
beforeEach -> beforeEach ->
setFixtures """ setFixtures """
<div id="week" data-id="i4x://mitx.edu/course/week"> <div id="week" data-id="i4x://mitx/course/chapter/week">
<div class="editable"></div> <div class="editable"></div>
<textarea class="editable-textarea"></textarea> <textarea class="editable-textarea"></textarea>
<a href="#" class="week-edit" >edit</a> <a href="#" class="week-edit" >edit</a>
......
...@@ -4,7 +4,8 @@ class CMS.Models.Module extends Backbone.Model ...@@ -4,7 +4,8 @@ class CMS.Models.Module extends Backbone.Model
data: '' data: ''
loadModule: (element) -> loadModule: (element) ->
@module = XModule.loadModule($(element).find('.xmodule_edit')) elt = $(element).find('.xmodule_edit').first()
@module = XModule.loadModule(elt)
editUrl: -> editUrl: ->
"/edit_item?#{$.param(id: @get('id'))}" "/edit_item?#{$.param(id: @get('id'))}"
......
...@@ -13,6 +13,16 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -13,6 +13,16 @@ class CMS.Views.ModuleEdit extends Backbone.View
# Load preview modules # Load preview modules
XModule.loadModules('display') XModule.loadModules('display')
@enableDrag()
enableDrag: ->
# Enable dragging things in the #sortable div (if there is one)
if $("#sortable").length > 0
$("#sortable").sortable({
placeholder: "ui-state-highlight"
})
$("#sortable").disableSelection();
save: (event) -> save: (event) ->
event.preventDefault() event.preventDefault()
...@@ -32,6 +42,7 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -32,6 +42,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
cancel: (event) -> cancel: (event) ->
event.preventDefault() event.preventDefault()
CMS.popView() CMS.popView()
@enableDrag()
editSubmodule: (event) -> editSubmodule: (event) ->
event.preventDefault() event.preventDefault()
...@@ -42,3 +53,4 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -42,3 +53,4 @@ class CMS.Views.ModuleEdit extends Backbone.View
id: $(event.target).data('id') id: $(event.target).data('id')
type: if moduleType == 'None' then null else moduleType type: if moduleType == 'None' then null else moduleType
previewType: if previewType == 'None' then null else previewType previewType: if previewType == 'None' then null else previewType
@enableDrag()
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
<section class="modules"> <section class="modules">
<ol> <ol>
<li> <li>
<ol> <ol id="sortable">
% for child in module.get_children(): % for child in module.get_children():
<li class="${module.category}"> <li class="${module.category}">
<a href="#" class="module-edit" <a href="#" class="module-edit"
......
from django.conf import settings from django.conf import settings
from django.conf.urls.defaults import patterns, include, url from django.conf.urls import patterns, include, url
import django.contrib.auth.views import django.contrib.auth.views
......
from staticfiles.storage import staticfiles_storage
from mitxmako.shortcuts import render_to_string from mitxmako.shortcuts import render_to_string
from pipeline.conf import settings from pipeline.conf import settings
from pipeline.packager import Packager from pipeline.packager import Packager
from pipeline.utils import guess_type from pipeline.utils import guess_type
from static_replace import try_staticfiles_lookup
def compressed_css(package_name): def compressed_css(package_name):
...@@ -25,9 +24,11 @@ def compressed_css(package_name): ...@@ -25,9 +24,11 @@ def compressed_css(package_name):
def render_css(package, path): def render_css(package, path):
template_name = package.template_name or "mako/css.html" template_name = package.template_name or "mako/css.html"
context = package.extra_context context = package.extra_context
url = try_staticfiles_lookup(path)
context.update({ context.update({
'type': guess_type(path, 'text/css'), 'type': guess_type(path, 'text/css'),
'url': staticfiles_storage.url(path) 'url': url,
}) })
return render_to_string(template_name, context) return render_to_string(template_name, context)
...@@ -58,7 +59,7 @@ def render_js(package, path): ...@@ -58,7 +59,7 @@ def render_js(package, path):
context = package.extra_context context = package.extra_context
context.update({ context.update({
'type': guess_type(path, 'text/javascript'), 'type': guess_type(path, 'text/javascript'),
'url': staticfiles_storage.url(path) 'url': try_staticfiles_lookup(path)
}) })
return render_to_string(template_name, context) return render_to_string(template_name, context)
......
...@@ -3,7 +3,13 @@ from staticfiles.storage import staticfiles_storage ...@@ -3,7 +3,13 @@ from staticfiles.storage import staticfiles_storage
from pipeline_mako import compressed_css, compressed_js from pipeline_mako import compressed_css, compressed_js
%> %>
<%def name='url(file)'>${staticfiles_storage.url(file)}</%def> <%def name='url(file)'>
<%
try:
url = staticfiles_storage.url(file)
except:
url = file
%>${url}</%def>
<%def name='css(group)'> <%def name='css(group)'>
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: % if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
......
from staticfiles.storage import staticfiles_storage import logging
import re import re
from staticfiles.storage import staticfiles_storage
from staticfiles import finders
from django.conf import settings
log = logging.getLogger(__name__)
def try_staticfiles_lookup(path):
"""
Try to lookup a path in staticfiles_storage. If it fails, return
a dead link instead of raising an exception.
"""
try:
url = staticfiles_storage.url(path)
except Exception as err:
log.warning("staticfiles_storage couldn't find path {}: {}".format(
path, str(err)))
# Just return a dead link--don't kill everything.
url = "file_not_found"
return url
def replace(static_url, prefix=None): def replace(static_url, prefix=None):
if prefix is None: if prefix is None:
...@@ -9,10 +29,19 @@ def replace(static_url, prefix=None): ...@@ -9,10 +29,19 @@ def replace(static_url, prefix=None):
prefix = prefix + '/' prefix = prefix + '/'
quote = static_url.group('quote') quote = static_url.group('quote')
if staticfiles_storage.exists(static_url.group('rest')):
servable = (
# If in debug mode, we'll serve up anything that the finders can find
(settings.DEBUG and finders.find(static_url.group('rest'), True)) or
# Otherwise, we'll only serve up stuff that the storages can find
staticfiles_storage.exists(static_url.group('rest'))
)
if servable:
return static_url.group(0) return static_url.group(0)
else: else:
url = staticfiles_storage.url(prefix + static_url.group('rest')) # don't error if file can't be found
url = try_staticfiles_lookup(prefix + static_url.group('rest'))
return "".join([quote, url, quote]) return "".join([quote, url, quote])
......
##
## A script to create some dummy users
from django.core.management.base import BaseCommand
from django.conf import settings
from django.contrib.auth.models import User
from student.models import UserProfile, CourseEnrollment
from student.views import _do_create_account, get_random_post_override
def create(n, course_id):
"""Create n users, enrolling them in course_id if it's not None"""
for i in range(n):
(user, user_profile, _) = _do_create_account(get_random_post_override())
if course_id is not None:
CourseEnrollment.objects.create(user=user, course_id=course_id)
class Command(BaseCommand):
help = """Create N new users, with random parameters.
Usage: create_random_users.py N [course_id_to_enroll_in].
Examples:
create_random_users.py 1
create_random_users.py 10 MITx/6.002x/2012_Fall
create_random_users.py 100 HarvardX/CS50x/2012
"""
def handle(self, *args, **options):
if len(args) < 1 or len(args) > 2:
print Command.help
return
n = int(args[0])
course_id = args[1] if len(args) == 2 else None
create(n, course_id)
""" """
WE'RE USING MIGRATIONS! Models for Student Information
Replication Notes
In our live deployment, we intend to run in a scenario where there is a pool of
Portal servers that hold the canoncial user information and that user
information is replicated to slave Course server pools. Each Course has a set of
servers that serves only its content and has users that are relevant only to it.
We replicate the following tables into the Course DBs where the user is
enrolled. Only the Portal servers should ever write to these models.
* UserProfile
* CourseEnrollment
We do a partial replication of:
* User -- Askbot extends this and uses the extra fields, so we replicate only
the stuff that comes with basic django_auth and ignore the rest.)
There are a couple different scenarios:
1. There's an update of User or UserProfile -- replicate it to all Course DBs
that the user is enrolled in (found via CourseEnrollment).
2. There's a change in CourseEnrollment. We need to push copies of UserProfile,
CourseEnrollment, and the base fields in User
Migration Notes
If you make changes to this model, be sure to create an appropriate migration 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, file and check it in at the same time as your model changes. To do that,
...@@ -10,16 +35,41 @@ file and check it in at the same time as your model changes. To do that, ...@@ -10,16 +35,41 @@ file and check it in at the same time as your model changes. To do that,
""" """
from datetime import datetime from datetime import datetime
import json import json
import logging
import uuid import uuid
from django.db import models from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from django_countries import CountryField from django_countries import CountryField
from xmodule.modulestore.django import modulestore
#from cache_toolbox import cache_model, cache_relation #from cache_toolbox import cache_model, cache_relation
log = logging.getLogger(__name__)
class UserProfile(models.Model): class UserProfile(models.Model):
"""This is where we store all the user demographic fields. We have a
separate table for this rather than extending the built-in Django auth_user.
Notes:
* Some fields are legacy ones from the first run of 6.002, from which
we imported many users.
* Fields like name and address are intentionally open ended, to account
for international variations. An unfortunate side-effect is that we
cannot efficiently sort on last names for instance.
Replication:
* Only the Portal servers should ever modify this information.
* All fields are replicated into relevant Course databases
Some of the fields are legacy ones that were captured during the initial
MITx fall prototype.
"""
class Meta: class Meta:
db_table = "auth_userprofile" db_table = "auth_userprofile"
...@@ -203,3 +253,154 @@ def add_user_to_default_group(user, group): ...@@ -203,3 +253,154 @@ def add_user_to_default_group(user, group):
utg.save() utg.save()
utg.users.add(User.objects.get(username=user)) utg.users.add(User.objects.get(username=user))
utg.save() utg.save()
########################## REPLICATION SIGNALS #################################
@receiver(post_save, sender=User)
def replicate_user_save(sender, **kwargs):
user_obj = kwargs['instance']
return replicate_model(User.save, user_obj, user_obj.id)
@receiver(post_save, sender=CourseEnrollment)
def replicate_enrollment_save(sender, **kwargs):
"""This is called when a Student enrolls in a course. It has to do the
following:
1. Make sure the User is copied into the Course DB. It may already exist
(someone deleting and re-adding a course). This has to happen first or
the foreign key constraint breaks.
2. Replicate the CourseEnrollment.
3. Replicate the UserProfile.
"""
if not is_portal():
return
enrollment_obj = kwargs['instance']
log.debug("Replicating user because of new enrollment")
replicate_user(enrollment_obj.user, enrollment_obj.course_id)
log.debug("Replicating enrollment because of new enrollment")
replicate_model(CourseEnrollment.save, enrollment_obj, enrollment_obj.user_id)
log.debug("Replicating user profile because of new enrollment")
user_profile = UserProfile.objects.get(user_id=enrollment_obj.user_id)
replicate_model(UserProfile.save, user_profile, enrollment_obj.user_id)
@receiver(post_delete, sender=CourseEnrollment)
def replicate_enrollment_delete(sender, **kwargs):
enrollment_obj = kwargs['instance']
return replicate_model(CourseEnrollment.delete, enrollment_obj, enrollment_obj.user_id)
@receiver(post_save, sender=UserProfile)
def replicate_userprofile_save(sender, **kwargs):
"""We just updated the UserProfile (say an update to the name), so push that
change to all Course DBs that we're enrolled in."""
user_profile_obj = kwargs['instance']
return replicate_model(UserProfile.save, user_profile_obj, user_profile_obj.user_id)
######### Replication functions #########
USER_FIELDS_TO_COPY = ["id", "username", "first_name", "last_name", "email",
"password", "is_staff", "is_active", "is_superuser",
"last_login", "date_joined"]
def replicate_user(portal_user, course_db_name):
"""Replicate a User to the correct Course DB. This is more complicated than
it should be because Askbot extends the auth_user table and adds its own
fields. So we need to only push changes to the standard fields and leave
the rest alone so that Askbot changes at the Course DB level don't get
overridden.
"""
try:
# If the user exists in the Course DB, update the appropriate fields and
# save it back out to the Course DB.
course_user = User.objects.using(course_db_name).get(id=portal_user.id)
for field in USER_FIELDS_TO_COPY:
setattr(course_user, field, getattr(portal_user, field))
mark_handled(course_user)
log.debug("User {0} found in Course DB, replicating fields to {1}"
.format(course_user, course_db_name))
course_user.save(using=course_db_name) # Just being explicit.
except User.DoesNotExist:
# Otherwise, just make a straight copy to the Course DB.
mark_handled(portal_user)
log.debug("User {0} not found in Course DB, creating copy in {1}"
.format(portal_user, course_db_name))
portal_user.save(using=course_db_name)
def replicate_model(model_method, instance, user_id):
"""
model_method is the model action that we want replicated. For instance,
UserProfile.save
"""
if not should_replicate(instance):
return
mark_handled(instance)
course_db_names = db_names_to_replicate_to(user_id)
log.debug("Replicating {0} for user {1} to DBs: {2}"
.format(model_method, user_id, course_db_names))
for db_name in course_db_names:
model_method(instance, using=db_name)
######### Replication Helpers #########
def is_valid_course_id(course_id):
"""Right now, the only database that's not a course database is 'default'.
I had nicer checking in here originally -- it would scan the courses that
were in the system and only let you choose that. But it was annoying to run
tests with, since we don't have course data for some for our course test
databases. Hence the lazy version.
"""
return course_id != 'default'
def is_portal():
"""Are we in the portal pool? Only Portal servers are allowed to replicate
their changes. For now, only Portal servers see multiple DBs, so we use
that to decide."""
return len(settings.DATABASES) > 1
def db_names_to_replicate_to(user_id):
"""Return a list of DB names that this user_id is enrolled in."""
return [c.course_id
for c in CourseEnrollment.objects.filter(user_id=user_id)
if is_valid_course_id(c.course_id)]
def marked_handled(instance):
"""Have we marked this instance as being handled to avoid infinite loops
caused by saving models in post_save hooks for the same models?"""
return hasattr(instance, '_do_not_copy_to_course_db')
def mark_handled(instance):
"""You have to mark your instance with this function or else we'll go into
an infinite loop since we're putting listeners on Model saves/deletes and
the act of replication requires us to call the same model method.
We create a _replicated attribute to differentiate the first save of this
model vs. the duplicate save we force on to the course database. Kind of
a hack -- suggestions welcome.
"""
instance._do_not_copy_to_course_db = True
def should_replicate(instance):
"""Should this instance be replicated? We need to be a Portal server and
the instance has to not have been marked_handled."""
if marked_handled(instance):
# Basically, avoid an infinite loop. You should
log.debug("{0} should not be replicated because it's been marked"
.format(instance))
return False
if not is_portal():
log.debug("{0} should not be replicated because we're not a portal."
.format(instance))
return False
return True
...@@ -4,13 +4,178 @@ when you run "manage.py test". ...@@ -4,13 +4,178 @@ when you run "manage.py test".
Replace this with more appropriate tests for your application. Replace this with more appropriate tests for your application.
""" """
from datetime import datetime
from django.test import TestCase from django.test import TestCase
from .models import User, UserProfile, CourseEnrollment, replicate_user, USER_FIELDS_TO_COPY
COURSE_1 = 'edX/toy/2012_Fall'
COURSE_2 = 'edx/full/6.002_Spring_2012'
class ReplicationTest(TestCase):
multi_db = True
def test_user_replication(self):
"""Test basic user replication."""
portal_user = User.objects.create_user('rusty', 'rusty@edx.org', 'fakepass')
portal_user.first_name='Rusty'
portal_user.last_name='Skids'
portal_user.is_staff=True
portal_user.is_active=True
portal_user.is_superuser=True
portal_user.last_login=datetime(2012, 1, 1)
portal_user.date_joined=datetime(2011, 1, 1)
# This is an Askbot field and will break if askbot is not included
if hasattr(portal_user, 'seen_response_count'):
portal_user.seen_response_count = 10
portal_user.save(using='default')
# We replicate this user to Course 1, then pull the same user and verify
# that the fields copied over properly.
replicate_user(portal_user, COURSE_1)
course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
# Make sure the fields we care about got copied over for this user.
for field in USER_FIELDS_TO_COPY:
self.assertEqual(getattr(portal_user, field),
getattr(course_user, field),
"{0} not copied from {1} to {2}".format(
field, portal_user, course_user
))
if hasattr(portal_user, 'seen_response_count'):
# Since it's the first copy over of User data, we should have all of it
self.assertEqual(portal_user.seen_response_count,
course_user.seen_response_count)
# But if we replicate again, the user already exists in the Course DB,
# so it shouldn't update the seen_response_count (which is Askbot
# controlled).
# This hasattr lameness is here because we don't want this test to be
# triggered when we're being run by CMS tests (Askbot doesn't exist
# there, so the test will fail).
if hasattr(portal_user, 'seen_response_count'):
portal_user.seen_response_count = 20
replicate_user(portal_user, COURSE_1)
course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
self.assertEqual(portal_user.seen_response_count, 20)
self.assertEqual(course_user.seen_response_count, 10)
# Another replication should work for an email change however, since
# it's a field we care about.
portal_user.email = "clyde@edx.org"
replicate_user(portal_user, COURSE_1)
course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
self.assertEqual(portal_user.email, course_user.email)
# During this entire time, the user data should never have made it over
# to COURSE_2
self.assertRaises(User.DoesNotExist,
User.objects.using(COURSE_2).get,
id=portal_user.id)
def test_enrollment_for_existing_user_info(self):
"""Test the effect of Enrolling in a class if you've already got user
data to be copied over."""
# Create our User
portal_user = User.objects.create_user('jack', 'jack@edx.org', 'fakepass')
portal_user.first_name = "Jack"
portal_user.save()
# Set up our UserProfile info
portal_user_profile = UserProfile.objects.create(
user=portal_user,
name="Jack Foo",
level_of_education=None,
gender='m',
mailing_address=None,
goals="World domination",
)
portal_user_profile.save()
# Now let's see if creating a CourseEnrollment copies all the relevant
# data.
portal_enrollment = CourseEnrollment.objects.create(user=portal_user,
course_id=COURSE_1)
portal_enrollment.save()
# Grab all the copies we expect
course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
self.assertEquals(portal_user, course_user)
self.assertRaises(User.DoesNotExist,
User.objects.using(COURSE_2).get,
id=portal_user.id)
course_enrollment = CourseEnrollment.objects.using(COURSE_1).get(id=portal_enrollment.id)
self.assertEquals(portal_enrollment, course_enrollment)
self.assertRaises(CourseEnrollment.DoesNotExist,
CourseEnrollment.objects.using(COURSE_2).get,
id=portal_enrollment.id)
course_user_profile = UserProfile.objects.using(COURSE_1).get(id=portal_user_profile.id)
self.assertEquals(portal_user_profile, course_user_profile)
self.assertRaises(UserProfile.DoesNotExist,
UserProfile.objects.using(COURSE_2).get,
id=portal_user_profile.id)
def test_enrollment_for_user_info_after_enrollment(self):
"""Test the effect of modifying User data after you've enrolled."""
# Create our User
portal_user = User.objects.create_user('patty', 'patty@edx.org', 'fakepass')
portal_user.first_name = "Patty"
portal_user.save()
# Set up our UserProfile info
portal_user_profile = UserProfile.objects.create(
user=portal_user,
name="Patty Foo",
level_of_education=None,
gender='f',
mailing_address=None,
goals="World peace",
)
portal_user_profile.save()
# Now let's see if creating a CourseEnrollment copies all the relevant
# data when things are saved.
portal_enrollment = CourseEnrollment.objects.create(user=portal_user,
course_id=COURSE_1)
portal_enrollment.save()
portal_user.last_name = "Bar"
portal_user.save()
portal_user_profile.gender = 'm'
portal_user_profile.save()
# Grab all the copies we expect, and make sure it doesn't end up in
# places we don't expect.
course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
self.assertEquals(portal_user, course_user)
self.assertRaises(User.DoesNotExist,
User.objects.using(COURSE_2).get,
id=portal_user.id)
course_enrollment = CourseEnrollment.objects.using(COURSE_1).get(id=portal_enrollment.id)
self.assertEquals(portal_enrollment, course_enrollment)
self.assertRaises(CourseEnrollment.DoesNotExist,
CourseEnrollment.objects.using(COURSE_2).get,
id=portal_enrollment.id)
course_user_profile = UserProfile.objects.using(COURSE_1).get(id=portal_user_profile.id)
self.assertEquals(portal_user_profile, course_user_profile)
self.assertRaises(UserProfile.DoesNotExist,
UserProfile.objects.using(COURSE_2).get,
id=portal_user_profile.id)
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.assertEqual(1 + 1, 2)
...@@ -94,8 +94,9 @@ def main_index(extra_context = {}, user=None): ...@@ -94,8 +94,9 @@ def main_index(extra_context = {}, user=None):
context.update(extra_context) context.update(extra_context)
return render_to_response('index.html', context) return render_to_response('index.html', context)
def course_from_id(id): def course_from_id(course_id):
course_loc = CourseDescriptor.id_to_location(id) """Return the CourseDescriptor corresponding to this course_id"""
course_loc = CourseDescriptor.id_to_location(course_id)
return modulestore().get_item(course_loc) return modulestore().get_item(course_loc)
...@@ -127,7 +128,7 @@ def dashboard(request): ...@@ -127,7 +128,7 @@ def dashboard(request):
try: try:
courses.append(course_from_id(enrollment.course_id)) courses.append(course_from_id(enrollment.course_id))
except ItemNotFoundError: except ItemNotFoundError:
log.error("User {0} enrolled in non-existant course {1}" log.error("User {0} enrolled in non-existent course {1}"
.format(user.username, enrollment.course_id)) .format(user.username, enrollment.course_id))
message = "" message = ""
...@@ -158,15 +159,19 @@ def try_change_enrollment(request): ...@@ -158,15 +159,19 @@ def try_change_enrollment(request):
@login_required @login_required
def change_enrollment_view(request): def change_enrollment_view(request):
"""Delegate to change_enrollment to actually do the work."""
return HttpResponse(json.dumps(change_enrollment(request))) return HttpResponse(json.dumps(change_enrollment(request)))
def change_enrollment(request): def change_enrollment(request):
if request.method != "POST": if request.method != "POST":
raise Http404 raise Http404
action = request.POST.get("enrollment_action", "")
user = request.user user = request.user
if not user.is_authenticated():
raise Http404
action = request.POST.get("enrollment_action", "")
course_id = request.POST.get("course_id", None) course_id = request.POST.get("course_id", None)
if course_id == None: if course_id == None:
return HttpResponse(json.dumps({'success': False, 'error': 'There was an error receiving the course id.'})) return HttpResponse(json.dumps({'success': False, 'error': 'There was an error receiving the course id.'}))
...@@ -177,14 +182,14 @@ def change_enrollment(request): ...@@ -177,14 +182,14 @@ def change_enrollment(request):
try: try:
course = course_from_id(course_id) course = course_from_id(course_id)
except ItemNotFoundError: except ItemNotFoundError:
log.error("User {0} tried to enroll in non-existant course {1}" log.warning("User {0} tried to enroll in non-existant course {1}"
.format(user.username, enrollment.course_id)) .format(user.username, enrollment.course_id))
return {'success': False, 'error': 'The course requested does not exist.'} return {'success': False, 'error': 'The course requested does not exist.'}
if settings.MITX_FEATURES.get('ACCESS_REQUIRE_STAFF_FOR_COURSE'): if settings.MITX_FEATURES.get('ACCESS_REQUIRE_STAFF_FOR_COURSE'):
# require that user be in the staff_* group (or be an overall admin) to be able to enroll # require that user be in the staff_* group (or be an overall admin) to be able to enroll
# eg staff_6.002x or staff_6.00x # eg staff_6.002x or staff_6.00x
if not has_staff_access_to_course(user,course): if not has_staff_access_to_course(user, course):
staff_group = course_staff_group_name(course) staff_group = course_staff_group_name(course)
log.debug('user %s denied enrollment to %s ; not in %s' % (user,course.location.url(),staff_group)) log.debug('user %s denied enrollment to %s ; not in %s' % (user,course.location.url(),staff_group))
return {'success': False, 'error' : '%s membership required to access course.' % staff_group} return {'success': False, 'error' : '%s membership required to access course.' % staff_group}
...@@ -264,6 +269,7 @@ def logout_user(request): ...@@ -264,6 +269,7 @@ def logout_user(request):
def change_setting(request): def change_setting(request):
''' JSON call to change a profile setting: Right now, location ''' JSON call to change a profile setting: Right now, location
''' '''
# TODO (vshnayder): location is no longer used
up = UserProfile.objects.get(user=request.user) # request.user.profile_cache up = UserProfile.objects.get(user=request.user) # request.user.profile_cache
if 'location' in request.POST: if 'location' in request.POST:
up.location = request.POST['location'] up.location = request.POST['location']
...@@ -272,6 +278,59 @@ def change_setting(request): ...@@ -272,6 +278,59 @@ def change_setting(request):
return HttpResponse(json.dumps({'success': True, return HttpResponse(json.dumps({'success': True,
'location': up.location, })) 'location': up.location, }))
def _do_create_account(post_vars):
"""
Given cleaned post variables, create the User and UserProfile objects, as well as the
registration for this user.
Returns a tuple (User, UserProfile, Registration).
Note: this function is also used for creating test users.
"""
user = User(username=post_vars['username'],
email=post_vars['email'],
is_active=False)
user.set_password(post_vars['password'])
registration = Registration()
# TODO: Rearrange so that if part of the process fails, the whole process fails.
# Right now, we can have e.g. no registration e-mail sent out and a zombie account
try:
user.save()
except IntegrityError:
js = {'success': False}
# Figure out the cause of the integrity error
if len(User.objects.filter(username=post_vars['username'])) > 0:
js['value'] = "An account with this username already exists."
js['field'] = 'username'
return HttpResponse(json.dumps(js))
if len(User.objects.filter(email=post_vars['email'])) > 0:
js['value'] = "An account with this e-mail already exists."
js['field'] = 'email'
return HttpResponse(json.dumps(js))
raise
registration.register(user)
profile = UserProfile(user=user)
profile.name = post_vars['name']
profile.level_of_education = post_vars.get('level_of_education')
profile.gender = post_vars.get('gender')
profile.mailing_address = post_vars.get('mailing_address')
profile.goals = post_vars.get('goals')
try:
profile.year_of_birth = int(post_vars['year_of_birth'])
except (ValueError, KeyError):
profile.year_of_birth = None # If they give us garbage, just ignore it instead
# of asking them to put an integer.
try:
profile.save()
except Exception:
log.exception("UserProfile creation failed for user {0}.".format(user.id))
return (user, profile, registration)
@ensure_csrf_cookie @ensure_csrf_cookie
def create_account(request, post_override=None): def create_account(request, post_override=None):
...@@ -343,50 +402,14 @@ def create_account(request, post_override=None): ...@@ -343,50 +402,14 @@ def create_account(request, post_override=None):
js['field'] = 'username' js['field'] = 'username'
return HttpResponse(json.dumps(js)) return HttpResponse(json.dumps(js))
u = User(username=post_vars['username'], # Ok, looks like everything is legit. Create the account.
email=post_vars['email'], ret = _do_create_account(post_vars)
is_active=False) if isinstance(ret,HttpResponse): # if there was an error then return that
u.set_password(post_vars['password']) return ret
r = Registration() (user, profile, registration) = ret
# TODO: Rearrange so that if part of the process fails, the whole process fails.
# Right now, we can have e.g. no registration e-mail sent out and a zombie account
try:
u.save()
except IntegrityError:
# Figure out the cause of the integrity error
if len(User.objects.filter(username=post_vars['username'])) > 0:
js['value'] = "An account with this username already exists."
js['field'] = 'username'
return HttpResponse(json.dumps(js))
if len(User.objects.filter(email=post_vars['email'])) > 0:
js['value'] = "An account with this e-mail already exists."
js['field'] = 'email'
return HttpResponse(json.dumps(js))
raise
r.register(u)
up = UserProfile(user=u)
up.name = post_vars['name']
up.level_of_education = post_vars.get('level_of_education')
up.gender = post_vars.get('gender')
up.mailing_address = post_vars.get('mailing_address')
up.goals = post_vars.get('goals')
try:
up.year_of_birth = int(post_vars['year_of_birth'])
except (ValueError, KeyError):
up.year_of_birth = None # If they give us garbage, just ignore it instead
# of asking them to put an integer.
try:
up.save()
except Exception:
log.exception("UserProfile creation failed for user {0}.".format(u.id))
d = {'name': post_vars['name'], d = {'name': post_vars['name'],
'key': r.activation_key, 'key': registration.activation_key,
} }
# composes activation email # composes activation email
...@@ -398,10 +421,11 @@ def create_account(request, post_override=None): ...@@ -398,10 +421,11 @@ def create_account(request, post_override=None):
try: try:
if settings.MITX_FEATURES.get('REROUTE_ACTIVATION_EMAIL'): if settings.MITX_FEATURES.get('REROUTE_ACTIVATION_EMAIL'):
dest_addr = settings.MITX_FEATURES['REROUTE_ACTIVATION_EMAIL'] dest_addr = settings.MITX_FEATURES['REROUTE_ACTIVATION_EMAIL']
message = "Activation for %s (%s): %s\n" % (u, u.email, up.name) + '-' * 80 + '\n\n' + message message = ("Activation for %s (%s): %s\n" % (user, user.email, profile.name) +
'-' * 80 + '\n\n' + message)
send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [dest_addr], fail_silently=False) send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [dest_addr], fail_silently=False)
elif not settings.GENERATE_RANDOM_USER_CREDENTIALS: elif not settings.GENERATE_RANDOM_USER_CREDENTIALS:
res = u.email_user(subject, message, settings.DEFAULT_FROM_EMAIL) res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
except: except:
log.exception(sys.exc_info()) log.exception(sys.exc_info())
js['value'] = 'Could not send activation e-mail.' js['value'] = 'Could not send activation e-mail.'
...@@ -431,24 +455,30 @@ def create_account(request, post_override=None): ...@@ -431,24 +455,30 @@ def create_account(request, post_override=None):
return HttpResponse(json.dumps(js), mimetype="application/json") return HttpResponse(json.dumps(js), mimetype="application/json")
def create_random_account(create_account_function): def get_random_post_override():
"""
Return a dictionary suitable for passing to post_vars of _do_create_account or post_override
of create_account, with random user info.
"""
def id_generator(size=6, chars=string.ascii_uppercase + string.ascii_lowercase + string.digits): def id_generator(size=6, chars=string.ascii_uppercase + string.ascii_lowercase + string.digits):
return ''.join(random.choice(chars) for x in range(size)) return ''.join(random.choice(chars) for x in range(size))
def inner_create_random_account(request): return {'username': "random_" + id_generator(),
post_override = {'username': "random_" + id_generator(), 'email': id_generator(size=10, chars=string.ascii_lowercase) + "_dummy_test@mitx.mit.edu",
'email': id_generator(size=10, chars=string.ascii_lowercase) + "_dummy_test@mitx.mit.edu", 'password': id_generator(),
'password': id_generator(), 'name': (id_generator(size=5, chars=string.ascii_lowercase) + " " +
'location': id_generator(size=5, chars=string.ascii_uppercase), id_generator(size=7, chars=string.ascii_lowercase)),
'name': id_generator(size=5, chars=string.ascii_lowercase) + " " + id_generator(size=7, chars=string.ascii_lowercase), 'honor_code': u'true',
'honor_code': u'true', 'terms_of_service': u'true', }
'terms_of_service': u'true', }
return create_account_function(request, post_override=post_override) def create_random_account(create_account_function):
def inner_create_random_account(request):
return create_account_function(request, post_override=get_random_post_override())
return inner_create_random_account return inner_create_random_account
# TODO (vshnayder): do we need GENERATE_RANDOM_USER_CREDENTIALS for anything?
if settings.GENERATE_RANDOM_USER_CREDENTIALS: if settings.GENERATE_RANDOM_USER_CREDENTIALS:
create_account = create_random_account(create_account) create_account = create_random_account(create_account)
...@@ -514,7 +544,7 @@ def reactivation_email(request): ...@@ -514,7 +544,7 @@ def reactivation_email(request):
subject = ''.join(subject.splitlines()) 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 = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
return HttpResponse(json.dumps({'success': True})) return HttpResponse(json.dumps({'success': True}))
......
...@@ -9,7 +9,7 @@ def expect_json(view_function): ...@@ -9,7 +9,7 @@ def expect_json(view_function):
if request.META['CONTENT_TYPE'] == "application/json": if request.META['CONTENT_TYPE'] == "application/json":
cloned_request = copy.copy(request) cloned_request = copy.copy(request)
cloned_request.POST = cloned_request.POST.copy() cloned_request.POST = cloned_request.POST.copy()
cloned_request.POST.update(json.loads(request.raw_post_data)) cloned_request.POST.update(json.loads(request.body))
return view_function(cloned_request, *args, **kwargs) return view_function(cloned_request, *args, **kwargs)
else: else:
return view_function(request, *args, **kwargs) return view_function(request, *args, **kwargs)
......
import logging
from django.conf import settings
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
stack"""
if not settings.TEMPLATE_DEBUG:
def process_exception(self, request, exception):
log.exception(exception)
return HttpResponseServerError("Server Error - Please try again later.")
...@@ -34,7 +34,7 @@ def wrap_xmodule(get_html, module, template): ...@@ -34,7 +34,7 @@ def wrap_xmodule(get_html, module, template):
return _get_html return _get_html
def replace_static_urls(get_html, prefix): def replace_static_urls(get_html, prefix, module):
""" """
Updates the supplied module with a new get_html function that wraps Updates the supplied module with a new get_html function that wraps
the old get_html function and substitutes urls of the form /static/... the old get_html function and substitutes urls of the form /static/...
...@@ -69,14 +69,14 @@ def grade_histogram(module_id): ...@@ -69,14 +69,14 @@ def grade_histogram(module_id):
return grades return grades
def add_histogram(get_html, module): def add_histogram(get_html, module, user):
""" """
Updates the supplied module with a new get_html function that wraps Updates the supplied module with a new get_html function that wraps
the output of the old get_html function with additional information the output of the old get_html function with additional information
for admin users only, including a histogram of student answers and the for admin users only, including a histogram of student answers and the
definition of the xmodule definition of the xmodule
Does nothing if module is a SequenceModule Does nothing if module is a SequenceModule or a VerticalModule.
""" """
@wraps(get_html) @wraps(get_html)
def _get_html(): def _get_html():
...@@ -90,19 +90,27 @@ def add_histogram(get_html, module): ...@@ -90,19 +90,27 @@ def add_histogram(get_html, module):
# TODO (ichuang): Remove after fall 2012 LMS migration done # TODO (ichuang): Remove after fall 2012 LMS migration done
if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'): if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
[filepath, filename] = module.definition.get('filename','') [filepath, filename] = module.definition.get('filename', ['', None])
osfs = module.system.filestore osfs = module.system.filestore
if filename is not None and osfs.exists(filename): if filename is not None and osfs.exists(filename):
filepath = filename # if original, unmangled filename exists then use it (github doesn't like symlinks) # if original, unmangled filename exists then use it (github
# doesn't like symlinks)
filepath = filename
data_dir = osfs.root_path.rsplit('/')[-1] data_dir = osfs.root_path.rsplit('/')[-1]
edit_link = "https://github.com/MITx/%s/tree/master/%s" % (data_dir,filepath) giturl = module.metadata.get('giturl','https://github.com/MITx')
edit_link = "%s/%s/tree/master/%s" % (giturl,data_dir,filepath)
else: else:
edit_link = False edit_link = False
staff_context = {'definition': module.definition.get('data'), staff_context = {'definition': module.definition.get('data'),
'metadata': json.dumps(module.metadata, indent=4), 'metadata': json.dumps(module.metadata, indent=4),
'element_id': module.location.html_id(), 'location': module.location,
'xqa_key': module.metadata.get('xqa_key',''),
'category': str(module.__class__.__name__),
'element_id': module.location.html_id().replace('-','_'),
'edit_link': edit_link, 'edit_link': edit_link,
'user': user,
'xqa_server' : settings.MITX_FEATURES.get('USE_XQA_SERVER','http://xqa:server@content-qa.mitx.mit.edu/xqa'),
'histogram': json.dumps(histogram), 'histogram': json.dumps(histogram),
'render_histogram': render_histogram, 'render_histogram': render_histogram,
'module_content': get_html()} 'module_content': get_html()}
......
...@@ -203,8 +203,9 @@ class LoncapaProblem(object): ...@@ -203,8 +203,9 @@ class LoncapaProblem(object):
cmap.update(self.correct_map) cmap.update(self.correct_map)
for responder in self.responders.values(): 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 # Each LoncapaResponse will update its specific entries in cmap
cmap = responder.update_score(score_msg, cmap, queuekey) # cmap is passed by reference
responder.update_score(score_msg, cmap, queuekey)
self.correct_map.set_dict(cmap.get_dict()) self.correct_map.set_dict(cmap.get_dict())
return cmap return cmap
...@@ -228,14 +229,14 @@ class LoncapaProblem(object): ...@@ -228,14 +229,14 @@ class LoncapaProblem(object):
Calls the Response for each question in this problem, to do the actual grading. Calls the Response for each question in this problem, to do the actual grading.
''' '''
self.student_answers = convert_files_to_filenames(answers) self.student_answers = convert_files_to_filenames(answers)
oldcmap = self.correct_map # old CorrectMap oldcmap = self.correct_map # old CorrectMap
newcmap = CorrectMap() # start new with empty CorrectMap newcmap = CorrectMap() # start new with empty CorrectMap
# log.debug('Responders: %s' % self.responders) # log.debug('Responders: %s' % self.responders)
for responder in self.responders.values(): # Call each responsetype instance to do actual grading for responder in self.responders.values(): # Call each responsetype instance to do actual grading
if 'filesubmission' in responder.allowed_inputfields: # File objects are passed only if responsetype if 'filesubmission' in responder.allowed_inputfields: # File objects are passed only if responsetype
# explicitly allows for file submissions # explicitly allows for file submissions
results = responder.evaluate_answers(answers, oldcmap) results = responder.evaluate_answers(answers, oldcmap)
else: else:
...@@ -294,9 +295,9 @@ class LoncapaProblem(object): ...@@ -294,9 +295,9 @@ class LoncapaProblem(object):
try: try:
ifp = self.system.filestore.open(file) # open using ModuleSystem OSFS filestore ifp = self.system.filestore.open(file) # open using ModuleSystem OSFS filestore
except Exception as err: except Exception as err:
log.error('Error %s in problem xml include: %s' % ( log.warning('Error %s in problem xml include: %s' % (
err, etree.tostring(inc, pretty_print=True))) err, etree.tostring(inc, pretty_print=True)))
log.error('Cannot find file %s in %s' % ( log.warning('Cannot find file %s in %s' % (
file, self.system.filestore)) file, self.system.filestore))
# if debugging, don't fail - just log error # if debugging, don't fail - just log error
# TODO (vshnayder): need real error handling, display to users # TODO (vshnayder): need real error handling, display to users
...@@ -305,11 +306,11 @@ class LoncapaProblem(object): ...@@ -305,11 +306,11 @@ class LoncapaProblem(object):
else: else:
continue continue
try: try:
incxml = etree.XML(ifp.read()) # read in and convert to XML incxml = etree.XML(ifp.read()) # read in and convert to XML
except Exception as err: except Exception as err:
log.error('Error %s in problem xml include: %s' % ( log.warning('Error %s in problem xml include: %s' % (
err, etree.tostring(inc, pretty_print=True))) err, etree.tostring(inc, pretty_print=True)))
log.error('Cannot parse XML in %s' % (file)) log.warning('Cannot parse XML in %s' % (file))
# if debugging, don't fail - just log error # if debugging, don't fail - just log error
# TODO (vshnayder): same as above # TODO (vshnayder): same as above
if not self.system.get('DEBUG'): if not self.system.get('DEBUG'):
...@@ -392,9 +393,10 @@ class LoncapaProblem(object): ...@@ -392,9 +393,10 @@ class LoncapaProblem(object):
context['script_code'] += code # store code source in context context['script_code'] += code # store code source in context
try: try:
exec code in context, context # use "context" for global context; thus defs in code are global within code exec code in context, context # use "context" for global context; thus defs in code are global within code
except Exception: except Exception as err:
log.exception("Error while execing script code: " + code) log.exception("Error while execing script code: " + code)
raise responsetypes.LoncapaProblemError("Error while executing script code") msg = "Error while executing script code: %s" % str(err).replace('<','&lt;')
raise responsetypes.LoncapaProblemError(msg)
finally: finally:
sys.path = original_path sys.path = original_path
......
...@@ -205,7 +205,7 @@ def extract_choices(element): ...@@ -205,7 +205,7 @@ def extract_choices(element):
raise Exception("[courseware.capa.inputtypes.extract_choices] \ raise Exception("[courseware.capa.inputtypes.extract_choices] \
Expected a <choice> tag; got %s instead" Expected a <choice> tag; got %s instead"
% choice.tag) % choice.tag)
choice_text = ''.join([etree.tostring(x) for x in choice]) choice_text = ''.join([x.text for x in choice])
choices.append((choice.get("name"), choice_text)) choices.append((choice.get("name"), choice_text))
...@@ -336,9 +336,19 @@ def filesubmission(element, value, status, render_template, msg=''): ...@@ -336,9 +336,19 @@ def filesubmission(element, value, status, render_template, msg=''):
Upload a single file (e.g. for programming assignments) Upload a single file (e.g. for programming assignments)
''' '''
eid = element.get('id') eid = element.get('id')
context = { 'id': eid, 'state': status, 'msg': msg, 'value': value, }
# Check if problem has been queued
queue_len = 0
if status == 'incomplete': # Flag indicating that the problem has been queued, 'msg' is length of queue
status = 'queued'
queue_len = msg
msg = 'Submitted to grader. (Queue length: %s)' % queue_len
context = { 'id': eid, 'state': status, 'msg': msg, 'value': value,
'queue_len': queue_len
}
html = render_template("filesubmission.html", context) html = render_template("filesubmission.html", context)
return etree.XML(html) return etree.XML(html)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -359,9 +369,16 @@ def textbox(element, value, status, render_template, msg=''): ...@@ -359,9 +369,16 @@ def textbox(element, value, status, render_template, msg=''):
if not value: value = element.text # if no student input yet, then use the default input given by the problem if not value: value = element.text # if no student input yet, then use the default input given by the problem
# Check if problem has been queued
queue_len = 0
if status == 'incomplete': # Flag indicating that the problem has been queued, 'msg' is length of queue
status = 'queued'
queue_len = msg
msg = 'Submitted to grader. (Queue length: %s)' % queue_len
# For CodeMirror # For CodeMirror
mode = element.get('mode') or 'python' # mode, eg "python" or "xml" mode = element.get('mode','python')
linenumbers = element.get('linenumbers','true') # for CodeMirror linenumbers = element.get('linenumbers','true')
tabsize = element.get('tabsize','4') tabsize = element.get('tabsize','4')
tabsize = int(tabsize) tabsize = int(tabsize)
...@@ -369,6 +386,7 @@ def textbox(element, value, status, render_template, msg=''): ...@@ -369,6 +386,7 @@ def textbox(element, value, status, render_template, msg=''):
'mode': mode, 'linenumbers': linenumbers, 'mode': mode, 'linenumbers': linenumbers,
'rows': rows, 'cols': cols, 'rows': rows, 'cols': cols,
'hidden': hidden, 'tabsize': tabsize, 'hidden': hidden, 'tabsize': tabsize,
'queue_len': queue_len,
} }
html = render_template("textbox.html", context) html = render_template("textbox.html", context)
try: try:
......
...@@ -990,6 +990,12 @@ class CodeResponse(LoncapaResponse): ...@@ -990,6 +990,12 @@ class CodeResponse(LoncapaResponse):
''' '''
Grade student code using an external queueing server, called 'xqueue' Grade student code using an external queueing server, called 'xqueue'
Expects 'xqueue' dict in ModuleSystem with the following keys:
system.xqueue = { 'interface': XqueueInterface object,
'callback_url': Per-StudentModule callback URL where results are posted (string),
'default_queuename': Default queuename to submit request (string)
}
External requests are only submitted for student submission grading External requests are only submitted for student submission grading
(i.e. and not for getting reference answers) (i.e. and not for getting reference answers)
''' '''
...@@ -999,10 +1005,27 @@ class CodeResponse(LoncapaResponse): ...@@ -999,10 +1005,27 @@ class CodeResponse(LoncapaResponse):
max_inputfields = 1 max_inputfields = 1
def setup_response(self): def setup_response(self):
'''
Configure CodeResponse from XML. Supports both CodeResponse and ExternalResponse XML
TODO: Determines whether in synchronous or asynchronous (queued) mode
'''
xml = self.xml xml = self.xml
self.url = xml.get('url', None) # XML can override external resource (grader/queue) URL
self.queue_name = xml.get('queuename', self.system.xqueue['default_queuename']) self.queue_name = xml.get('queuename', self.system.xqueue['default_queuename'])
answer = xml.find('answer') self._parse_externalresponse_xml()
def _parse_externalresponse_xml(self):
'''
VS[compat]: Suppport for old ExternalResponse XML format. When successful, sets:
self.code
self.tests
self.answer
self.initial_display
'''
answer = self.xml.find('answer')
if answer is not None: if answer is not None:
answer_src = answer.get('src') answer_src = answer.get('src')
if answer_src is not None: if answer_src is not None:
...@@ -1016,7 +1039,7 @@ class CodeResponse(LoncapaResponse): ...@@ -1016,7 +1039,7 @@ class CodeResponse(LoncapaResponse):
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>') msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>')
raise LoncapaProblemError(msg) raise LoncapaProblemError(msg)
self.tests = xml.get('tests') self.tests = self.xml.get('tests')
# Extract 'answer' and 'initial_display' from XML. Note that the code to be exec'ed here is: # 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 # (1) Internal edX code, i.e. NOT student submissions, and
...@@ -1063,15 +1086,16 @@ class CodeResponse(LoncapaResponse): ...@@ -1063,15 +1086,16 @@ class CodeResponse(LoncapaResponse):
'edX_cmd': 'get_score', 'edX_cmd': 'get_score',
'edX_tests': self.tests, 'edX_tests': self.tests,
'processor': self.code, 'processor': self.code,
'edX_student_response': unicode(submission), # unicode on File object returns its filename
} }
# Submit request # Submit request. When successful, 'msg' is the prior length of the queue
if hasattr(submission, 'read'): # Test for whether submission is a file if is_file(submission):
contents.update({'edX_student_response': submission.name})
(error, msg) = qinterface.send_to_queue(header=xheader, (error, msg) = qinterface.send_to_queue(header=xheader,
body=json.dumps(contents), body=json.dumps(contents),
file_to_upload=submission) file_to_upload=submission)
else: else:
contents.update({'edX_student_response': submission})
(error, msg) = qinterface.send_to_queue(header=xheader, (error, msg) = qinterface.send_to_queue(header=xheader,
body=json.dumps(contents)) body=json.dumps(contents))
...@@ -1080,33 +1104,31 @@ class CodeResponse(LoncapaResponse): ...@@ -1080,33 +1104,31 @@ class CodeResponse(LoncapaResponse):
cmap.set(self.answer_id, queuekey=None, cmap.set(self.answer_id, queuekey=None,
msg='Unable to deliver your submission to grader. (Reason: %s.) Please try again later.' % msg) msg='Unable to deliver your submission to grader. (Reason: %s.) Please try again later.' % msg)
else: else:
# Non-null CorrectMap['queuekey'] indicates that the problem has been queued # Queueing mechanism flags:
cmap.set(self.answer_id, queuekey=queuekey, msg='Submitted to grader. (Queue length: %s)' % msg) # 1) Backend: Non-null CorrectMap['queuekey'] indicates that the problem has been queued
# 2) Frontend: correctness='incomplete' eventually trickles down through inputtypes.textbox
# and .filesubmission to inform the browser to poll the LMS
cmap.set(self.answer_id, queuekey=queuekey, correctness='incomplete', msg=msg)
return cmap return cmap
def update_score(self, score_msg, oldcmap, queuekey): 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)
# The following process is lifted directly from ExternalResponse (valid_score_msg, correct, score, msg) = self._parse_score_msg(score_msg)
ad = rxml.find('awarddetail').text if not valid_score_msg:
admap = {'EXACT_ANS': 'correct', # TODO: handle other loncapa responses oldcmap.set(self.answer_id, msg='Error: Invalid grader reply.')
'WRONG_FORMAT': 'incorrect', return oldcmap
}
self.context['correct'] = ['correct'] correctness = 'incorrect'
if ad in admap: if correct:
self.context['correct'][0] = admap[ad] correctness = 'correct'
self.context['correct'] = correctness # TODO: Find out how this is used elsewhere, if any
# Replace 'oldcmap' with new grading results if queuekey matches. # Replace 'oldcmap' with new grading results if queuekey matches.
# If queuekey does not match, we keep waiting for the score_msg whose key actually matches # If queuekey does not match, we keep waiting for the score_msg whose key actually matches
if oldcmap.is_right_queuekey(self.answer_id, queuekey): if oldcmap.is_right_queuekey(self.answer_id, queuekey):
msg = rxml.find('message').text.replace('&nbsp;', '&#160;') oldcmap.set(self.answer_id, correctness=correctness, msg=msg.replace('&nbsp;', '&#160;'), queuekey=None) # Queuekey is consumed
oldcmap.set(self.answer_id, correctness=self.context['correct'][0], msg=msg, queuekey=None) # Queuekey is consumed
else: else:
log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' % (queuekey, self.answer_id)) log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' % (queuekey, self.answer_id))
...@@ -1119,6 +1141,31 @@ class CodeResponse(LoncapaResponse): ...@@ -1119,6 +1141,31 @@ class CodeResponse(LoncapaResponse):
def get_initial_display(self): def get_initial_display(self):
return {self.answer_id: self.initial_display} return {self.answer_id: self.initial_display}
def _parse_score_msg(self, score_msg):
'''
Grader reply is a JSON-dump of the following dict
{ 'correct': True/False,
'score': # TODO -- Partial grading
'msg': grader_msg }
Returns (valid_score_msg, correct, score, msg):
valid_score_msg: Flag indicating valid score_msg format (Boolean)
correct: Correctness of submission (Boolean)
score: # TODO: Implement partial grading
msg: Message from grader to display to student (string)
'''
fail = (False, False, -1, '')
try:
score_result = json.loads(score_msg)
except (TypeError, ValueError):
return fail
if not isinstance(score_result, dict):
return fail
for tag in ['correct', 'score', 'msg']:
if not score_result.has_key(tag):
return fail
return (True, score_result['correct'], score_result['score'], score_result['msg'])
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
......
...@@ -6,8 +6,9 @@ ...@@ -6,8 +6,9 @@
<span class="correct" id="status_${id}"></span> <span class="correct" id="status_${id}"></span>
% elif state == 'incorrect': % elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"></span>
% elif state == 'incomplete': % elif state == 'queued':
<span class="incorrect" id="status_${id}"></span> <span class="processing" id="status_${id}"></span>
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif % endif
<span class="debug">(${state})</span> <span class="debug">(${state})</span>
<br/> <br/>
......
...@@ -13,11 +13,12 @@ ...@@ -13,11 +13,12 @@
<span class="correct" id="status_${id}"></span> <span class="correct" id="status_${id}"></span>
% elif state == 'incorrect': % elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"></span>
% elif state == 'incomplete': % elif state == 'queued':
<span class="incorrect" id="status_${id}"></span> <span class="processing" id="status_${id}"></span>
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif % endif
% if hidden: % if hidden:
<div style="display:none;" name="${hidden}" inputid="input_${id}" /> <div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif % endif
<br/> <br/>
<span class="debug">(${state})</span> <span class="debug">(${state})</span>
......
...@@ -39,5 +39,18 @@ def convert_files_to_filenames(answers): ...@@ -39,5 +39,18 @@ def convert_files_to_filenames(answers):
''' '''
new_answers = dict() new_answers = dict()
for answer_id in answers.keys(): for answer_id in answers.keys():
new_answers[answer_id] = unicode(answers[answer_id]) if is_file(answers[answer_id]):
new_answers[answer_id] = answers[answer_id].name
else:
new_answers[answer_id] = answers[answer_id]
return new_answers return new_answers
def is_file(file_to_test):
'''
Duck typing to check if 'file_to_test' is a File object
'''
is_file = True
for method in ['read', 'name']:
if not hasattr(file_to_test, method):
is_file = False
return is_file
...@@ -67,7 +67,6 @@ class XqueueInterface: ...@@ -67,7 +67,6 @@ class XqueueInterface:
self.url = url self.url = url
self.auth = auth self.auth = auth
self.session = requests.session() self.session = requests.session()
self._login()
def send_to_queue(self, header, body, file_to_upload=None): def send_to_queue(self, header, body, file_to_upload=None):
''' '''
......
...@@ -11,18 +11,19 @@ DEFAULT = "_DEFAULT_GROUP" ...@@ -11,18 +11,19 @@ DEFAULT = "_DEFAULT_GROUP"
def group_from_value(groups, v): def group_from_value(groups, v):
''' Given group: (('a',0.3),('b',0.4),('c',0.3)) And random value """
Given group: (('a', 0.3), ('b', 0.4), ('c', 0.3)) and random value v
in [0,1], return the associated group (in the above case, return in [0,1], return the associated group (in the above case, return
'a' if v<0.3, 'b' if 0.3<=v<0.7, and 'c' if v>0.7 'a' if v < 0.3, 'b' if 0.3 <= v < 0.7, and 'c' if v > 0.7
''' """
sum = 0 sum = 0
for (g, p) in groups: for (g, p) in groups:
sum = sum + p sum = sum + p
if sum > v: if sum > v:
return g return g
# Round off errors might cause us to run to the end of the list # Round off errors might cause us to run to the end of the list.
# If the do, return the last element # If the do, return the last element.
return g return g
...@@ -31,8 +32,8 @@ class ABTestModule(XModule): ...@@ -31,8 +32,8 @@ class ABTestModule(XModule):
Implements an A/B test with an aribtrary number of competing groups Implements an A/B test with an aribtrary number of competing groups
""" """
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs)
if shared_state is None: if shared_state is None:
...@@ -103,7 +104,9 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor): ...@@ -103,7 +104,9 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
experiment = xml_object.get('experiment') experiment = xml_object.get('experiment')
if experiment is None: if experiment is None:
raise InvalidDefinitionError("ABTests must specify an experiment. Not found in:\n{xml}".format(xml=etree.tostring(xml_object, pretty_print=True))) raise InvalidDefinitionError(
"ABTests must specify an experiment. Not found in:\n{xml}"
.format(xml=etree.tostring(xml_object, pretty_print=True)))
definition = { definition = {
'data': { 'data': {
...@@ -127,7 +130,9 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor): ...@@ -127,7 +130,9 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
definition['data']['group_content'][name] = child_content_urls definition['data']['group_content'][name] = child_content_urls
definition['children'].extend(child_content_urls) definition['children'].extend(child_content_urls)
default_portion = 1 - sum(portion for (name, portion) in definition['data']['group_portions'].items()) default_portion = 1 - sum(
portion for (name, portion) in definition['data']['group_portions'].items())
if default_portion < 0: if default_portion < 0:
raise InvalidDefinitionError("ABTest portions must add up to less than or equal to 1") raise InvalidDefinitionError("ABTest portions must add up to less than or equal to 1")
......
...@@ -11,13 +11,13 @@ from datetime import timedelta ...@@ -11,13 +11,13 @@ from datetime import timedelta
from lxml import etree from lxml import etree
from pkg_resources import resource_string 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.capa_problem import LoncapaProblem
from capa.responsetypes import StudentInputError from capa.responsetypes import StudentInputError
from capa.util import convert_files_to_filenames from capa.util import convert_files_to_filenames
from progress import Progress
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
...@@ -80,9 +80,9 @@ class CapaModule(XModule): ...@@ -80,9 +80,9 @@ class CapaModule(XModule):
js_module_name = "Problem" js_module_name = "Problem"
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]} css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
def __init__(self, system, location, definition, instance_state=None, def __init__(self, system, location, definition, descriptor, instance_state=None,
shared_state=None, **kwargs): shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, instance_state, XModule.__init__(self, system, location, definition, descriptor, instance_state,
shared_state, **kwargs) shared_state, **kwargs)
self.attempts = 0 self.attempts = 0
...@@ -119,9 +119,9 @@ class CapaModule(XModule): ...@@ -119,9 +119,9 @@ class CapaModule(XModule):
if self.show_answer == "": if self.show_answer == "":
self.show_answer = "closed" self.show_answer = "closed"
if instance_state != None: if instance_state is not None:
instance_state = json.loads(instance_state) instance_state = json.loads(instance_state)
if instance_state != None and 'attempts' in instance_state: if instance_state is not None and 'attempts' in instance_state:
self.attempts = instance_state['attempts'] self.attempts = instance_state['attempts']
self.name = only_one(dom2.xpath('/problem/@name')) self.name = only_one(dom2.xpath('/problem/@name'))
...@@ -130,16 +130,18 @@ class CapaModule(XModule): ...@@ -130,16 +130,18 @@ class CapaModule(XModule):
if weight_string: if weight_string:
self.weight = float(weight_string) self.weight = float(weight_string)
else: else:
self.weight = 1 self.weight = None
if self.rerandomize == 'never': if self.rerandomize == 'never':
seed = 1 seed = 1
elif self.rerandomize == "per_student" and hasattr(system, 'id'): elif self.rerandomize == "per_student" and hasattr(self.system, 'id'):
seed = system.id seed = system.id
else: else:
seed = None seed = None
try: try:
# TODO (vshnayder): move as much as possible of this work and error
# checking to descriptor load time
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(), self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
instance_state, seed=seed, system=self.system) instance_state, seed=seed, system=self.system)
except Exception as err: except Exception as err:
...@@ -148,7 +150,7 @@ class CapaModule(XModule): ...@@ -148,7 +150,7 @@ class CapaModule(XModule):
# TODO (vshnayder): do modules need error handlers too? # TODO (vshnayder): do modules need error handlers too?
# We shouldn't be switching on DEBUG. # We shouldn't be switching on DEBUG.
if self.system.DEBUG: if self.system.DEBUG:
log.error(msg) log.warning(msg)
# TODO (vshnayder): This logic should be general, not here--and may # TODO (vshnayder): This logic should be general, not here--and may
# want to preserve the data instead of replacing it. # want to preserve the data instead of replacing it.
# e.g. in the CMS # e.g. in the CMS
...@@ -238,7 +240,7 @@ class CapaModule(XModule): ...@@ -238,7 +240,7 @@ class CapaModule(XModule):
content = {'name': self.metadata['display_name'], content = {'name': self.metadata['display_name'],
'html': html, 'html': html,
'weight': self.weight, 'weight': self.weight,
} }
# We using strings as truthy values, because the terminology of the # We using strings as truthy values, because the terminology of the
# check button is context-specific. # check button is context-specific.
...@@ -426,7 +428,7 @@ class CapaModule(XModule): ...@@ -426,7 +428,7 @@ class CapaModule(XModule):
event_info = dict() event_info = dict()
event_info['state'] = self.lcp.get_state() event_info['state'] = self.lcp.get_state()
event_info['problem_id'] = self.location.url() event_info['problem_id'] = self.location.url()
answers = self.make_dict_of_responses(get) answers = self.make_dict_of_responses(get)
event_info['answers'] = convert_files_to_filenames(answers) event_info['answers'] = convert_files_to_filenames(answers)
...@@ -563,6 +565,14 @@ class CapaDescriptor(RawDescriptor): ...@@ -563,6 +565,14 @@ class CapaDescriptor(RawDescriptor):
module_class = CapaModule module_class = CapaModule
stores_state = True
has_score = True
# Capa modules have some additional metadata:
# TODO (vshnayder): do problems have any other metadata? Do they
# actually use type and points?
metadata_attributes = RawDescriptor.metadata_attributes + ('type', 'points')
# VS[compat] # VS[compat]
# TODO (cpennington): Delete this method once all fall 2012 course are being # TODO (cpennington): Delete this method once all fall 2012 course are being
# edited in the cms # edited in the cms
...@@ -572,8 +582,3 @@ class CapaDescriptor(RawDescriptor): ...@@ -572,8 +582,3 @@ class CapaDescriptor(RawDescriptor):
'problems/' + path[8:], 'problems/' + path[8:],
path[8:], path[8:],
] ]
@classmethod
def split_to_file(cls, xml_object):
'''Problems always written in their own files'''
return True
...@@ -3,6 +3,7 @@ import time ...@@ -3,6 +3,7 @@ import time
import dateutil.parser import dateutil.parser
import logging import logging
from xmodule.util.decorators import lazyproperty
from xmodule.graders import load_grading_policy from xmodule.graders import load_grading_policy
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor, SequenceModule from xmodule.seq_module import SequenceDescriptor, SequenceModule
...@@ -12,13 +13,9 @@ log = logging.getLogger(__name__) ...@@ -12,13 +13,9 @@ log = logging.getLogger(__name__)
class CourseDescriptor(SequenceDescriptor): class CourseDescriptor(SequenceDescriptor):
module_class = SequenceModule module_class = SequenceModule
metadata_attributes = SequenceDescriptor.metadata_attributes + ('org', 'course')
def __init__(self, system, definition=None, **kwargs): def __init__(self, system, definition=None, **kwargs):
super(CourseDescriptor, self).__init__(system, definition, **kwargs) super(CourseDescriptor, self).__init__(system, definition, **kwargs)
self._grader = None
self._grade_cutoffs = None
msg = None msg = None
try: try:
...@@ -39,34 +36,84 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -39,34 +36,84 @@ class CourseDescriptor(SequenceDescriptor):
def has_started(self): def has_started(self):
return time.gmtime() > self.start return time.gmtime() > self.start
@property @property
def grader(self): def grader(self):
self.__load_grading_policy() return self.__grading_policy['GRADER']
return self._grader
@property @property
def grade_cutoffs(self): def grade_cutoffs(self):
self.__load_grading_policy() return self.__grading_policy['GRADE_CUTOFFS']
return self._grade_cutoffs
@lazyproperty
def __grading_policy(self):
def __load_grading_policy(self): policy_string = ""
if not self._grader or not self._grade_cutoffs:
policy_string = "" try:
with self.system.resources_fs.open("grading_policy.json") as grading_policy_file:
try: policy_string = grading_policy_file.read()
with self.system.resources_fs.open("grading_policy.json") as grading_policy_file: except (IOError, ResourceNotFoundError):
policy_string = grading_policy_file.read() log.warning("Unable to load course settings file from grading_policy.json in course " + self.id)
except (IOError, ResourceNotFoundError):
log.warning("Unable to load course settings file from grading_policy.json in course " + self.id) grading_policy = load_grading_policy(policy_string)
grading_policy = load_grading_policy(policy_string) return grading_policy
self._grader = grading_policy['GRADER']
self._grade_cutoffs = grading_policy['GRADE_CUTOFFS'] @lazyproperty
def grading_context(self):
"""
This returns a dictionary with keys necessary for quickly grading
a student. They are used by grades.grade()
The grading context has two keys:
graded_sections - This contains the sections that are graded, as
well as all possible children modules that can affect the
grading. This allows some sections to be skipped if the student
hasn't seen any part of it.
The format is a dictionary keyed by section-type. The values are
arrays of dictionaries containing
"section_descriptor" : The section descriptor
"xmoduledescriptors" : An array of xmoduledescriptors that
could possibly be in the section, for any student
all_descriptors - This contains a list of all xmodules that can
effect grading a student. This is used to efficiently fetch
all the xmodule state for a StudentModuleCache without walking
the descriptor tree again.
"""
all_descriptors = []
graded_sections = {}
def yield_descriptor_descendents(module_descriptor):
for child in module_descriptor.get_children():
yield child
for module_descriptor in yield_descriptor_descendents(child):
yield module_descriptor
for c in self.get_children():
sections = []
for s in c.get_children():
if s.metadata.get('graded', False):
xmoduledescriptors = list(yield_descriptor_descendents(s))
# The xmoduledescriptors included here are only the ones that have scores.
section_description = { 'section_descriptor' : s, 'xmoduledescriptors' : filter(lambda child: child.has_score, xmoduledescriptors) }
section_format = s.metadata.get('format', "")
graded_sections[ section_format ] = graded_sections.get( section_format, [] ) + [section_description]
all_descriptors.extend(xmoduledescriptors)
all_descriptors.append(s)
return { 'graded_sections' : graded_sections,
'all_descriptors' : all_descriptors,}
@staticmethod @staticmethod
def id_to_location(course_id): def id_to_location(course_id):
'''Convert the given course_id (org/course/name) to a location object. '''Convert the given course_id (org/course/name) to a location object.
......
...@@ -49,6 +49,8 @@ padding-left: flex-gutter(9); ...@@ -49,6 +49,8 @@ padding-left: flex-gutter(9);
} }
} }
div { div {
p.status { p.status {
text-indent: -9999px; text-indent: -9999px;
...@@ -64,6 +66,16 @@ div { ...@@ -64,6 +66,16 @@ div {
} }
} }
&.processing {
p.status {
@include inline-block();
background: url('../images/spinner.gif') center center no-repeat;
height: 20px;
width: 20px;
text-indent: -9999px;
}
}
&.correct, &.ui-icon-check { &.correct, &.ui-icon-check {
p.status { p.status {
@include inline-block(); @include inline-block();
...@@ -134,6 +146,15 @@ div { ...@@ -134,6 +146,15 @@ div {
width: 14px; width: 14px;
} }
&.processing, &.ui-icon-check {
@include inline-block();
background: url('../images/spinner.gif') center center no-repeat;
height: 20px;
position: relative;
top: 6px;
width: 25px;
}
&.correct, &.ui-icon-check { &.correct, &.ui-icon-check {
@include inline-block(); @include inline-block();
background: url('../images/correct-icon.png') center center no-repeat; background: url('../images/correct-icon.png') center center no-repeat;
......
...@@ -3,9 +3,9 @@ nav.sequence-nav { ...@@ -3,9 +3,9 @@ nav.sequence-nav {
// import from external sources. // import from external sources.
@extend .topbar; @extend .topbar;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
@include border-top-right-radius(4px);
margin: (-(lh())) (-(lh())) lh() (-(lh())); margin: (-(lh())) (-(lh())) lh() (-(lh()));
position: relative; position: relative;
@include border-top-right-radius(4px);
ol { ol {
@include box-sizing(border-box); @include box-sizing(border-box);
...@@ -242,9 +242,11 @@ nav.sequence-bottom { ...@@ -242,9 +242,11 @@ nav.sequence-bottom {
border: 1px solid $border-color; border: 1px solid $border-color;
@include border-radius(3px); @include border-radius(3px);
@include inline-block(); @include inline-block();
width: 100px;
li { li {
float: left; float: left;
width: 50%;
&.prev, &.next { &.prev, &.next {
margin-bottom: 0; margin-bottom: 0;
...@@ -252,12 +254,11 @@ nav.sequence-bottom { ...@@ -252,12 +254,11 @@ nav.sequence-bottom {
a { a {
background-position: center center; background-position: center center;
background-repeat: no-repeat; background-repeat: no-repeat;
border-bottom: none; border: none;
display: block; display: block;
padding: lh(.5) 4px; padding: lh(.5) 4px;
text-indent: -9999px; text-indent: -9999px;
@include transition(all, .2s, $ease-in-out-quad); @include transition(all, .2s, $ease-in-out-quad);
width: 45px;
&:hover { &:hover {
background-color: #ddd; background-color: #ddd;
...@@ -275,7 +276,7 @@ nav.sequence-bottom { ...@@ -275,7 +276,7 @@ nav.sequence-bottom {
&.prev { &.prev {
a { a {
background-image: url('../images/sequence-nav/previous-icon.png'); background-image: url('../images/sequence-nav/previous-icon.png');
border-right: 1px solid lighten($border-color, 10%); border-right: 1px solid lighten(#c6c6c6, 10%);
&:hover { &:hover {
background-color: none; background-color: none;
......
...@@ -16,7 +16,6 @@ div.video { ...@@ -16,7 +16,6 @@ div.video {
height: 0; height: 0;
overflow: hidden; overflow: hidden;
padding-bottom: 56.25%; padding-bottom: 56.25%;
padding-top: 30px;
position: relative; position: relative;
object, iframe { object, iframe {
...@@ -207,7 +206,7 @@ div.video { ...@@ -207,7 +206,7 @@ div.video {
h3 { h3 {
color: #999; color: #999;
float: left; float: left;
font-size: 12px; font-size: em(14);
font-weight: normal; font-weight: normal;
letter-spacing: 1px; letter-spacing: 1px;
padding: 0 lh(.25) 0 lh(.5); padding: 0 lh(.25) 0 lh(.5);
...@@ -221,6 +220,7 @@ div.video { ...@@ -221,6 +220,7 @@ div.video {
margin-bottom: 0; margin-bottom: 0;
padding: 0 lh(.5) 0 0; padding: 0 lh(.5) 0 0;
line-height: 46px; line-height: 46px;
color: #fff;
} }
&:hover, &:active, &:focus { &:hover, &:active, &:focus {
...@@ -462,7 +462,8 @@ div.video { ...@@ -462,7 +462,8 @@ div.video {
} }
ol.subtitles { ol.subtitles {
width: 0px; width: 0;
height: 0;
} }
} }
......
import sys import hashlib
import logging import logging
import random
import string
import sys
from pkg_resources import resource_string from pkg_resources import resource_string
from lxml import etree from lxml import etree
...@@ -24,6 +27,14 @@ class ErrorModule(XModule): ...@@ -24,6 +27,14 @@ class ErrorModule(XModule):
'is_staff' : self.system.is_staff, 'is_staff' : self.system.is_staff,
}) })
def displayable_items(self):
"""Hide errors in the profile and table of contents for non-staff
users.
"""
if self.system.is_staff:
return [self]
return []
class ErrorDescriptor(EditingDescriptor): class ErrorDescriptor(EditingDescriptor):
""" """
Module that provides a raw editing view of broken xml. Module that provides a raw editing view of broken xml.
...@@ -35,7 +46,8 @@ class ErrorDescriptor(EditingDescriptor): ...@@ -35,7 +46,8 @@ class ErrorDescriptor(EditingDescriptor):
error_msg='Error not available'): error_msg='Error not available'):
'''Create an instance of this descriptor from the supplied data. '''Create an instance of this descriptor from the supplied data.
Does not try to parse the data--just stores it. Does not require that xml_data be parseable--just stores it and exports
as-is if not.
Takes an extra, optional, parameter--the error that caused an Takes an extra, optional, parameter--the error that caused an
issue. (should be a string, or convert usefully into one). issue. (should be a string, or convert usefully into one).
...@@ -45,6 +57,13 @@ class ErrorDescriptor(EditingDescriptor): ...@@ -45,6 +57,13 @@ class ErrorDescriptor(EditingDescriptor):
definition = {'data': inner} definition = {'data': inner}
inner['error_msg'] = str(error_msg) inner['error_msg'] = str(error_msg)
# Pick a unique url_name -- the sha1 hash of the xml_data.
# NOTE: We could try to pull out the url_name of the errored descriptor,
# but url_names aren't guaranteed to be unique between descriptor types,
# and ErrorDescriptor can wrap any type. When the wrapped module is fixed,
# it will be written out with the original url_name.
url_name = hashlib.sha1(xml_data).hexdigest()
try: try:
# If this is already an error tag, don't want to re-wrap it. # If this is already an error tag, don't want to re-wrap it.
xml_obj = etree.fromstring(xml_data) xml_obj = etree.fromstring(xml_data)
...@@ -63,8 +82,9 @@ class ErrorDescriptor(EditingDescriptor): ...@@ -63,8 +82,9 @@ class ErrorDescriptor(EditingDescriptor):
inner['contents'] = xml_data inner['contents'] = xml_data
# TODO (vshnayder): Do we need a unique slug here? Just pick a random # TODO (vshnayder): Do we need a unique slug here? Just pick a random
# 64-bit num? # 64-bit num?
location = ['i4x', org, course, 'error', 'slug'] location = ['i4x', org, course, 'error', url_name]
metadata = {} # stays in the xml_data # real metadata stays in the xml_data, but add a display name
metadata = {'display_name': 'Error ' + url_name}
return cls(system, definition, location=location, metadata=metadata) return cls(system, definition, location=location, metadata=metadata)
......
...@@ -13,13 +13,14 @@ from .html_checker import check_html ...@@ -13,13 +13,14 @@ from .html_checker import check_html
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
class HtmlModule(XModule): class HtmlModule(XModule):
def get_html(self): def get_html(self):
return self.html return self.html
def __init__(self, system, location, definition, def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs): instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs) instance_state, shared_state, **kwargs)
self.html = self.definition['data'] self.html = self.definition['data']
...@@ -36,18 +37,19 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -36,18 +37,19 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
# are being edited in the cms # are being edited in the cms
@classmethod @classmethod
def backcompat_paths(cls, path): def backcompat_paths(cls, path):
origpath = path
if path.endswith('.html.xml'): if path.endswith('.html.xml'):
path = path[:-9] + '.html' #backcompat--look for html instead of xml path = path[:-9] + '.html' # backcompat--look for html instead of xml
candidates = [] candidates = []
while os.sep in path: while os.sep in path:
candidates.append(path) candidates.append(path)
_, _, path = path.partition(os.sep) _, _, path = path.partition(os.sep)
# also look for .html versions instead of .xml # also look for .html versions instead of .xml
if origpath.endswith('.xml'): nc = []
candidates.append(origpath[:-4] + '.html') for candidate in candidates:
return candidates if candidate.endswith('.xml'):
nc.append(candidate[:-4] + '.html')
return candidates + nc
# NOTE: html descriptors are special. We do not want to parse and # NOTE: html descriptors are special. We do not want to parse and
# export them ourselves, because that can break things (e.g. lxml # export them ourselves, because that can break things (e.g. lxml
...@@ -69,7 +71,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -69,7 +71,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
if filename is None: if filename is None:
definition_xml = copy.deepcopy(xml_object) definition_xml = copy.deepcopy(xml_object)
cls.clean_metadata_from_xml(definition_xml) cls.clean_metadata_from_xml(definition_xml)
return {'data' : stringify_children(definition_xml)} return {'data': stringify_children(definition_xml)}
else: else:
filepath = cls._format_filepath(xml_object.tag, filename) filepath = cls._format_filepath(xml_object.tag, filename)
...@@ -80,7 +82,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -80,7 +82,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
# online and has imported all current (fall 2012) courses from xml # online and has imported all current (fall 2012) courses from xml
if not system.resources_fs.exists(filepath): if not system.resources_fs.exists(filepath):
candidates = cls.backcompat_paths(filepath) candidates = cls.backcompat_paths(filepath)
#log.debug("candidates = {0}".format(candidates)) log.debug("candidates = {0}".format(candidates))
for candidate in candidates: for candidate in candidates:
if system.resources_fs.exists(candidate): if system.resources_fs.exists(candidate):
filepath = candidate filepath = candidate
...@@ -95,7 +97,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -95,7 +97,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
log.warning(msg) log.warning(msg)
system.error_tracker("Warning: " + msg) system.error_tracker("Warning: " + msg)
definition = {'data' : html} definition = {'data': html}
# TODO (ichuang): remove this after migration # TODO (ichuang): remove this after migration
# for Fall 2012 LMS migration: keep filename (and unmangled filename) # for Fall 2012 LMS migration: keep filename (and unmangled filename)
...@@ -109,17 +111,11 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -109,17 +111,11 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
# add more info and re-raise # add more info and re-raise
raise Exception(msg), None, sys.exc_info()[2] raise Exception(msg), None, sys.exc_info()[2]
@classmethod
def split_to_file(cls, xml_object):
'''Never include inline html'''
return True
# TODO (vshnayder): make export put things in the right places. # TODO (vshnayder): make export put things in the right places.
def definition_to_xml(self, resource_fs): def definition_to_xml(self, resource_fs):
'''If the contents are valid xml, write them to filename.xml. Otherwise, '''If the contents are valid xml, write them to filename.xml. Otherwise,
write just the <html filename=""> tag to filename.xml, and the html write just <html filename="" [meta-attrs="..."]> to filename.xml, and the html
string to filename.html. string to filename.html.
''' '''
try: try:
...@@ -138,4 +134,3 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -138,4 +134,3 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
elt = etree.Element('html') elt = etree.Element('html')
elt.set("filename", self.url_name) elt.set("filename", self.url_name)
return elt return elt
...@@ -13,7 +13,10 @@ class @Problem ...@@ -13,7 +13,10 @@ class @Problem
bind: => bind: =>
MathJax.Hub.Queue ["Typeset", MathJax.Hub] MathJax.Hub.Queue ["Typeset", MathJax.Hub]
window.update_schematics() window.update_schematics()
@inputs = @$("[id^=input_#{@element_id.replace(/problem_/, '')}_]")
problem_prefix = @element_id.replace(/problem_/,'')
@inputs = @$("[id^=input_#{problem_prefix}_]")
@$('section.action input:button').click @refreshAnswers @$('section.action input:button').click @refreshAnswers
@$('section.action input.check').click @check_fd @$('section.action input.check').click @check_fd
#@$('section.action input.check').click @check #@$('section.action input.check').click @check
...@@ -27,18 +30,40 @@ class @Problem ...@@ -27,18 +30,40 @@ class @Problem
@el.attr progress: response.progress_status @el.attr progress: response.progress_status
@el.trigger('progressChanged') @el.trigger('progressChanged')
queueing: =>
@queued_items = @$(".xqueue")
if @queued_items.length > 0
if window.queuePollerID # Only one poller 'thread' per Problem
window.clearTimeout(window.queuePollerID)
window.queuePollerID = window.setTimeout(@poll, 100)
poll: =>
$.postWithPrefix "#{@url}/problem_get", (response) =>
@el.html(response.html)
@executeProblemScripts()
@bind()
@queued_items = @$(".xqueue")
if @queued_items.length == 0
delete window.queuePollerID
else
# TODO: Dynamically adjust timeout interval based on @queued_items.value
window.queuePollerID = window.setTimeout(@poll, 1000)
render: (content) -> render: (content) ->
if content if content
@el.html(content) @el.html(content)
@executeProblemScripts () => @executeProblemScripts () =>
@setupInputTypes() @setupInputTypes()
@bind() @bind()
@queueing()
else else
$.postWithPrefix "#{@url}/problem_get", (response) => $.postWithPrefix "#{@url}/problem_get", (response) =>
@el.html(response.html) @el.html(response.html)
@executeProblemScripts () => @executeProblemScripts () =>
@setupInputTypes() @setupInputTypes()
@bind() @bind()
@queueing()
# TODO add hooks for problem types here by inspecting response.html and doing # TODO add hooks for problem types here by inspecting response.html and doing
# stuff if a div w a class is found # stuff if a div w a class is found
......
...@@ -91,6 +91,13 @@ class @Sequence ...@@ -91,6 +91,13 @@ class @Sequence
event.preventDefault() event.preventDefault()
new_position = $(event.target).data('element') new_position = $(event.target).data('element')
Logger.log "seq_goto", old: @position, new: new_position, id: @id Logger.log "seq_goto", old: @position, new: new_position, id: @id
# On Sequence chage, destroy any existing polling thread
# for queued submissions, see ../capa/display.coffee
if window.queuePollerID
window.clearTimeout(window.queuePollerID)
delete window.queuePollerID
@render new_position @render new_position
next: (event) => next: (event) =>
......
...@@ -190,6 +190,13 @@ class Location(_LocationBase): ...@@ -190,6 +190,13 @@ class Location(_LocationBase):
return "Location%s" % repr(tuple(self)) return "Location%s" % repr(tuple(self))
@property
def course_id(self):
"""Return the ID of the Course that this item belongs to by looking
at the location URL hierachy"""
return "/".join([self.org, self.course, self.name])
class ModuleStore(object): class ModuleStore(object):
""" """
An abstract interface for a database backend that stores XModuleDescriptor An abstract interface for a database backend that stores XModuleDescriptor
......
...@@ -55,6 +55,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -55,6 +55,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
if json_data is None: if json_data is None:
return self.modulestore.get_item(location) return self.modulestore.get_item(location)
else: else:
# TODO (vshnayder): metadata inheritance is somewhat broken because mongo, doesn't
# always load an entire course. We're punting on this until after launch, and then
# will build a proper course policy framework.
return XModuleDescriptor.load_from_json(json_data, self, self.default_class) return XModuleDescriptor.load_from_json(json_data, self, self.default_class)
......
...@@ -50,8 +50,9 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -50,8 +50,9 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
# have been imported into the cms from xml # have been imported into the cms from xml
xml = clean_out_mako_templating(xml) xml = clean_out_mako_templating(xml)
xml_data = etree.fromstring(xml) xml_data = etree.fromstring(xml)
except: except Exception as err:
log.exception("Unable to parse xml: {xml}".format(xml=xml)) log.warning("Unable to parse xml: {err}, xml: {xml}".format(
err=str(err), xml=xml))
raise raise
# VS[compat]. Take this out once course conversion is done # VS[compat]. Take this out once course conversion is done
...@@ -188,26 +189,37 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -188,26 +189,37 @@ class XMLModuleStore(ModuleStoreBase):
course_file = StringIO(clean_out_mako_templating(course_file.read())) course_file = StringIO(clean_out_mako_templating(course_file.read()))
course_data = etree.parse(course_file).getroot() course_data = etree.parse(course_file).getroot()
org = course_data.get('org') org = course_data.get('org')
if org is None: if org is None:
log.error("No 'org' attribute set for course in {dir}. " msg = ("No 'org' attribute set for course in {dir}. "
"Using default 'edx'".format(dir=course_dir)) "Using default 'edx'".format(dir=course_dir))
log.warning(msg)
tracker(msg)
org = 'edx' org = 'edx'
course = course_data.get('course') course = course_data.get('course')
if course is None: if course is None:
log.error("No 'course' attribute set for course in {dir}." msg = ("No 'course' attribute set for course in {dir}."
" Using default '{default}'".format( " Using default '{default}'".format(
dir=course_dir, dir=course_dir,
default=course_dir default=course_dir
)) ))
log.warning(msg)
tracker(msg)
course = course_dir course = course_dir
system = ImportSystem(self, org, course, course_dir, tracker) system = ImportSystem(self, org, course, course_dir, tracker)
course_descriptor = system.process_xml(etree.tostring(course_data)) course_descriptor = system.process_xml(etree.tostring(course_data))
# NOTE: The descriptors end up loading somewhat bottom up, which
# breaks metadata inheritance via get_children(). Instead
# (actually, in addition to, for now), we do a final inheritance pass
# after we have the course descriptor.
XModuleDescriptor.compute_inherited_metadata(course_descriptor)
log.debug('========> Done with course import from {0}'.format(course_dir)) log.debug('========> Done with course import from {0}'.format(course_dir))
return course_descriptor return course_descriptor
......
...@@ -25,9 +25,9 @@ class SequenceModule(XModule): ...@@ -25,9 +25,9 @@ class SequenceModule(XModule):
css = {'scss': [resource_string(__name__, 'css/sequence/display.scss')]} css = {'scss': [resource_string(__name__, 'css/sequence/display.scss')]}
js_module_name = "Sequence" js_module_name = "Sequence"
def __init__(self, system, location, definition, instance_state=None, def __init__(self, system, location, definition, descriptor, instance_state=None,
shared_state=None, **kwargs): shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs) instance_state, shared_state, **kwargs)
self.position = 1 self.position = 1
...@@ -107,6 +107,8 @@ class SequenceModule(XModule): ...@@ -107,6 +107,8 @@ class SequenceModule(XModule):
class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor): class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
mako_template = 'widgets/sequence-edit.html' mako_template = 'widgets/sequence-edit.html'
module_class = SequenceModule module_class = SequenceModule
stores_state = True # For remembering where in the sequence the student is
@classmethod @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
...@@ -122,16 +124,3 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor): ...@@ -122,16 +124,3 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
etree.fromstring(child.export_to_xml(resource_fs))) etree.fromstring(child.export_to_xml(resource_fs)))
return xml_object return xml_object
@classmethod
def split_to_file(cls, xml_object):
# Note: if we end up needing subclasses, can port this logic there.
yes = ('chapter',)
no = ('course',)
if xml_object.tag in yes:
return True
elif xml_object.tag in no:
return False
# otherwise maybe--delegate to superclass.
return XmlDescriptor.split_to_file(xml_object)
...@@ -26,12 +26,23 @@ class CustomTagModule(XModule): ...@@ -26,12 +26,23 @@ class CustomTagModule(XModule):
More information given in <a href="/book/234">the text</a> More information given in <a href="/book/234">the text</a>
""" """
def __init__(self, system, location, definition, def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs): instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs) instance_state, shared_state, **kwargs)
xmltree = etree.fromstring(self.definition['data']) def get_html(self):
return self.descriptor.rendered_html
class CustomTagDescriptor(RawDescriptor):
""" Descriptor for custom tags. Loads the template when created."""
module_class = CustomTagModule
@staticmethod
def render_template(system, xml_data):
'''Render the template, given the definition xml_data'''
xmltree = etree.fromstring(xml_data)
if 'impl' in xmltree.attrib: if 'impl' in xmltree.attrib:
template_name = xmltree.attrib['impl'] template_name = xmltree.attrib['impl']
else: else:
...@@ -45,13 +56,20 @@ class CustomTagModule(XModule): ...@@ -45,13 +56,20 @@ class CustomTagModule(XModule):
.format(location)) .format(location))
params = dict(xmltree.items()) params = dict(xmltree.items())
with self.system.filestore.open( with system.resources_fs.open('custom_tags/{name}'
'custom_tags/{name}'.format(name=template_name)) as template: .format(name=template_name)) as template:
self.html = Template(template.read()).render(**params) return Template(template.read()).render(**params)
def get_html(self):
return self.html
def __init__(self, system, definition, **kwargs):
'''Render and save the template for this descriptor instance'''
super(CustomTagDescriptor, self).__init__(system, definition, **kwargs)
self.rendered_html = self.render_template(system, definition['data'])
def export_to_file(self):
"""
Custom tags are special: since they're already pointers, we don't want
to export them in a file with yet another layer of indirection.
"""
return False
class CustomTagDescriptor(RawDescriptor):
module_class = CustomTagModule
...@@ -10,12 +10,14 @@ import os ...@@ -10,12 +10,14 @@ import os
import fs import fs
import json import json
import json
import numpy import numpy
import xmodule import xmodule
import capa.calc as calc import capa.calc as calc
import capa.capa_problem as lcp import capa.capa_problem as lcp
from capa.correctmap import CorrectMap from capa.correctmap import CorrectMap
from capa.util import convert_files_to_filenames
from xmodule import graders, x_module from xmodule import graders, x_module
from xmodule.x_module import ModuleSystem from xmodule.x_module import ModuleSystem
from xmodule.graders import Score, aggregate_scores from xmodule.graders import Score, aggregate_scores
...@@ -32,7 +34,7 @@ i4xs = ModuleSystem( ...@@ -32,7 +34,7 @@ i4xs = ModuleSystem(
user=Mock(), user=Mock(),
filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))+"/test_files"), filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))+"/test_files"),
debug=True, debug=True,
xqueue=None, xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue'},
is_staff=False, is_staff=False,
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules") node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules")
) )
...@@ -280,7 +282,6 @@ class StringResponseWithHintTest(unittest.TestCase): ...@@ -280,7 +282,6 @@ class StringResponseWithHintTest(unittest.TestCase):
class CodeResponseTest(unittest.TestCase): class CodeResponseTest(unittest.TestCase):
''' '''
Test CodeResponse Test CodeResponse
''' '''
def test_update_score(self): 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"
...@@ -293,9 +294,14 @@ class CodeResponseTest(unittest.TestCase): ...@@ -293,9 +294,14 @@ class CodeResponseTest(unittest.TestCase):
for i in range(numAnswers): 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 # TODO: Message format inherited from ExternalResponse
correct_score_msg = "<edxgrade><awarddetail>EXACT_ANS</awarddetail><message>MESSAGE</message></edxgrade>" #correct_score_msg = "<edxgrade><awarddetail>EXACT_ANS</awarddetail><message>MESSAGE</message></edxgrade>"
incorrect_score_msg = "<edxgrade><awarddetail>WRONG_FORMAT</awarddetail><message>MESSAGE</message></edxgrade>" #incorrect_score_msg = "<edxgrade><awarddetail>WRONG_FORMAT</awarddetail><message>MESSAGE</message></edxgrade>"
# New message format common to external graders
correct_score_msg = json.dumps({'correct':True, 'score':1, 'msg':'MESSAGE'})
incorrect_score_msg = json.dumps({'correct':False, 'score':0, 'msg':'MESSAGE'})
xserver_msgs = {'correct': correct_score_msg, xserver_msgs = {'correct': correct_score_msg,
'incorrect': incorrect_score_msg, 'incorrect': incorrect_score_msg,
} }
...@@ -329,7 +335,18 @@ class CodeResponseTest(unittest.TestCase): ...@@ -329,7 +335,18 @@ class CodeResponseTest(unittest.TestCase):
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: 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
def test_convert_files_to_filenames(self):
problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml"
fp = open(problem_file)
answers_with_file = {'1_2_1': 'String-based answer',
'1_3_1': ['answer1', 'answer2', 'answer3'],
'1_4_1': fp}
answers_converted = convert_files_to_filenames(answers_with_file)
self.assertEquals(answers_converted['1_2_1'], 'String-based answer')
self.assertEquals(answers_converted['1_3_1'], ['answer1', 'answer2', 'answer3'])
self.assertEquals(answers_converted['1_4_1'], fp.name)
class ChoiceResponseTest(unittest.TestCase): class ChoiceResponseTest(unittest.TestCase):
...@@ -712,6 +729,6 @@ class ModuleProgressTest(unittest.TestCase): ...@@ -712,6 +729,6 @@ class ModuleProgressTest(unittest.TestCase):
''' '''
def test_xmodule_default(self): def test_xmodule_default(self):
'''Make sure default get_progress exists, returns None''' '''Make sure default get_progress exists, returns None'''
xm = x_module.XModule(i4xs, 'a://b/c/d/e', {}) xm = x_module.XModule(i4xs, 'a://b/c/d/e', None, {})
p = xm.get_progress() p = xm.get_progress()
self.assertEqual(p, None) self.assertEqual(p, None)
from xmodule.modulestore.xml import XMLModuleStore import unittest
from nose.tools import assert_equals
from nose import SkipTest
from tempfile import mkdtemp
from fs.osfs import OSFS from fs.osfs import OSFS
from nose.tools import assert_equals, assert_true
from path import path
from tempfile import mkdtemp
from shutil import copytree
from xmodule.modulestore.xml import XMLModuleStore
# from ~/mitx_all/mitx/common/lib/xmodule/xmodule/tests/
# to ~/mitx_all/mitx/common/test
TEST_DIR = path(__file__).abspath().dirname()
for i in range(4):
TEST_DIR = TEST_DIR.dirname()
TEST_DIR = TEST_DIR / 'test'
DATA_DIR = TEST_DIR / 'data'
def strip_metadata(descriptor, key):
"""
Recursively strips tag from all children.
"""
print "strip {key} from {desc}".format(key=key, desc=descriptor.location.url())
descriptor.metadata.pop(key, None)
for d in descriptor.get_children():
strip_metadata(d, key)
def strip_filenames(descriptor):
"""
Recursively strips 'filename' from all children's definitions.
"""
print "strip filename from {desc}".format(desc=descriptor.location.url())
descriptor.definition.pop('filename', None)
for d in descriptor.get_children():
strip_filenames(d)
class RoundTripTestCase(unittest.TestCase):
'''Check that our test courses roundtrip properly'''
def check_export_roundtrip(self, data_dir, course_dir):
root_dir = path(mkdtemp())
print "Copying test course to temp dir {0}".format(root_dir)
data_dir = path(data_dir)
copytree(data_dir / course_dir, root_dir / course_dir)
print "Starting import"
initial_import = XMLModuleStore(root_dir, eager=True, course_dirs=[course_dir])
courses = initial_import.get_courses()
self.assertEquals(len(courses), 1)
initial_course = courses[0]
# export to the same directory--that way things like the custom_tags/ folder
# will still be there.
print "Starting export"
fs = OSFS(root_dir)
export_fs = fs.makeopendir(course_dir)
xml = initial_course.export_to_xml(export_fs)
with export_fs.open('course.xml', 'w') as course_xml:
course_xml.write(xml)
print "Starting second import"
second_import = XMLModuleStore(root_dir, eager=True, course_dirs=[course_dir])
courses2 = second_import.get_courses()
self.assertEquals(len(courses2), 1)
exported_course = courses2[0]
print "Checking course equality"
# HACK: data_dir metadata tags break equality because they
# aren't real metadata, and depend on paths. Remove them.
strip_metadata(initial_course, 'data_dir')
strip_metadata(exported_course, 'data_dir')
# HACK: filenames change when changing file formats
# during imports from old-style courses. Ignore them.
strip_filenames(initial_course)
strip_filenames(exported_course)
self.assertEquals(initial_course, exported_course)
def check_export_roundtrip(data_dir): print "Checking key equality"
print "Starting import" self.assertEquals(sorted(initial_import.modules.keys()),
initial_import = XMLModuleStore('org', 'course', data_dir, eager=True) sorted(second_import.modules.keys()))
initial_course = initial_import.course
print "Starting export" print "Checking module equality"
export_dir = mkdtemp() for location in initial_import.modules.keys():
fs = OSFS(export_dir) print "Checking", location
xml = initial_course.export_to_xml(fs) if location.category == 'html':
with fs.open('course.xml', 'w') as course_xml: print ("Skipping html modules--they can't import in"
course_xml.write(xml) " final form without writing files...")
continue
self.assertEquals(initial_import.modules[location],
second_import.modules[location])
print "Starting second import"
second_import = XMLModuleStore('org', 'course', export_dir, eager=True)
print "Checking key equality" def setUp(self):
assert_equals(initial_import.modules.keys(), second_import.modules.keys()) self.maxDiff = None
print "Checking module equality" def test_toy_roundtrip(self):
for location in initial_import.modules.keys(): self.check_export_roundtrip(DATA_DIR, "toy")
print "Checking", location
assert_equals(initial_import.modules[location], second_import.modules[location])
def test_simple_roundtrip(self):
self.check_export_roundtrip(DATA_DIR, "simple")
def test_toy_roundtrip(): def test_full_roundtrip(self):
dir = "" self.check_export_roundtrip(DATA_DIR, "full")
# TODO: add paths and make this run.
raise SkipTest()
check_export_roundtrip(dir)
...@@ -5,10 +5,14 @@ from fs.memoryfs import MemoryFS ...@@ -5,10 +5,14 @@ from fs.memoryfs import MemoryFS
from lxml import etree from lxml import etree
from xmodule.x_module import XMLParsingSystem, XModuleDescriptor from xmodule.x_module import XMLParsingSystem, XModuleDescriptor
from xmodule.xml_module import is_pointer_tag
from xmodule.errortracker import make_error_tracker from xmodule.errortracker import make_error_tracker
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.xml import XMLModuleStore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from .test_export import DATA_DIR
ORG = 'test_org' ORG = 'test_org'
COURSE = 'test_course' COURSE = 'test_course'
...@@ -46,22 +50,17 @@ class DummySystem(XMLParsingSystem): ...@@ -46,22 +50,17 @@ class DummySystem(XMLParsingSystem):
raise Exception("Shouldn't be called") raise Exception("Shouldn't be called")
class ImportTestCase(unittest.TestCase): class ImportTestCase(unittest.TestCase):
'''Make sure module imports work properly, including for malformed inputs''' '''Make sure module imports work properly, including for malformed inputs'''
@staticmethod @staticmethod
def get_system(): def get_system():
'''Get a dummy system''' '''Get a dummy system'''
return DummySystem() return DummySystem()
def test_fallback(self): def test_fallback(self):
'''Make sure that malformed xml loads as an ErrorDescriptor.''' '''Check that malformed xml loads as an ErrorDescriptor.'''
bad_xml = '''<sequential display_name="oops"><video url="hi"></sequential>''' bad_xml = '''<sequential display_name="oops"><video url="hi"></sequential>'''
system = self.get_system() system = self.get_system()
descriptor = XModuleDescriptor.load_from_xml(bad_xml, system, 'org', 'course', descriptor = XModuleDescriptor.load_from_xml(bad_xml, system, 'org', 'course',
...@@ -70,6 +69,22 @@ class ImportTestCase(unittest.TestCase): ...@@ -70,6 +69,22 @@ class ImportTestCase(unittest.TestCase):
self.assertEqual(descriptor.__class__.__name__, self.assertEqual(descriptor.__class__.__name__,
'ErrorDescriptor') 'ErrorDescriptor')
def test_unique_url_names(self):
'''Check that each error gets its very own url_name'''
bad_xml = '''<sequential display_name="oops"><video url="hi"></sequential>'''
bad_xml2 = '''<sequential url_name="oops"><video url="hi"></sequential>'''
system = self.get_system()
descriptor1 = XModuleDescriptor.load_from_xml(bad_xml, system, 'org',
'course', None)
descriptor2 = XModuleDescriptor.load_from_xml(bad_xml2, system, 'org',
'course', None)
self.assertNotEqual(descriptor1.location, descriptor2.location)
def test_reimport(self): def test_reimport(self):
'''Make sure an already-exported error xml tag loads properly''' '''Make sure an already-exported error xml tag loads properly'''
...@@ -111,30 +126,84 @@ class ImportTestCase(unittest.TestCase): ...@@ -111,30 +126,84 @@ class ImportTestCase(unittest.TestCase):
xml_out = etree.fromstring(xml_str_out) xml_out = etree.fromstring(xml_str_out)
self.assertEqual(xml_out.tag, 'sequential') self.assertEqual(xml_out.tag, 'sequential')
def test_metadata_inherit(self): def test_metadata_import_export(self):
"""Make sure metadata inherits properly""" """Two checks:
- unknown metadata is preserved across import-export
- inherited metadata doesn't leak to children.
"""
system = self.get_system() system = self.get_system()
v = "1 hour" v = '1 hour'
start_xml = '''<course graceperiod="{grace}" url_name="test1" display_name="myseq"> org = 'foo'
<chapter url="hi" url_name="ch" display_name="CH"> course = 'bbhh'
<html url_name="h" display_name="H">Two houses, ...</html></chapter> url_name = 'test1'
</course>'''.format(grace=v) start_xml = '''
<course org="{org}" course="{course}"
graceperiod="{grace}" url_name="{url_name}" unicorn="purple">
<chapter url="hi" url_name="ch" display_name="CH">
<html url_name="h" display_name="H">Two houses, ...</html>
</chapter>
</course>'''.format(grace=v, org=org, course=course, url_name=url_name)
descriptor = XModuleDescriptor.load_from_xml(start_xml, system, descriptor = XModuleDescriptor.load_from_xml(start_xml, system,
'org', 'course') org, course)
print "Errors: {0}".format(system.errorlog.errors)
print descriptor, descriptor.metadata print descriptor, descriptor.metadata
self.assertEqual(descriptor.metadata['graceperiod'], v) self.assertEqual(descriptor.metadata['graceperiod'], v)
self.assertEqual(descriptor.metadata['unicorn'], 'purple')
# Check that the child inherits correctly # Check that the child inherits graceperiod correctly
child = descriptor.get_children()[0] child = descriptor.get_children()[0]
self.assertEqual(child.metadata['graceperiod'], v) self.assertEqual(child.metadata['graceperiod'], v)
# Now export and see if the chapter tag has a graceperiod attribute # check that the child does _not_ inherit any unicorns
self.assertTrue('unicorn' not in child.metadata)
# Now export and check things
resource_fs = MemoryFS() resource_fs = MemoryFS()
exported_xml = descriptor.export_to_xml(resource_fs) exported_xml = descriptor.export_to_xml(resource_fs)
# Check that the exported xml is just a pointer
print "Exported xml:", exported_xml print "Exported xml:", exported_xml
root = etree.fromstring(exported_xml) pointer = etree.fromstring(exported_xml)
chapter_tag = root[0] self.assertTrue(is_pointer_tag(pointer))
self.assertEqual(chapter_tag.tag, 'chapter') # but it's a special case course pointer
self.assertFalse('graceperiod' in chapter_tag.attrib) self.assertEqual(pointer.attrib['course'], course)
self.assertEqual(pointer.attrib['org'], org)
# Does the course still have unicorns?
with resource_fs.open('course/{url_name}.xml'.format(url_name=url_name)) as f:
course_xml = etree.fromstring(f.read())
self.assertEqual(course_xml.attrib['unicorn'], 'purple')
# the course and org tags should be _only_ in the pointer
self.assertTrue('course' not in course_xml.attrib)
self.assertTrue('org' not in course_xml.attrib)
# did we successfully strip the url_name from the definition contents?
self.assertTrue('url_name' not in course_xml.attrib)
# Does the chapter tag now have a graceperiod attribute?
# hardcoded path to child
with resource_fs.open('chapter/ch.xml') as f:
chapter_xml = etree.fromstring(f.read())
self.assertEqual(chapter_xml.tag, 'chapter')
self.assertFalse('graceperiod' in chapter_xml.attrib)
def test_metadata_inherit(self):
"""Make sure that metadata is inherited properly"""
print "Starting import"
initial_import = XMLModuleStore(DATA_DIR, eager=True, course_dirs=['toy'])
courses = initial_import.get_courses()
self.assertEquals(len(courses), 1)
course = courses[0]
def check_for_key(key, node):
"recursive check for presence of key"
print "Checking {}".format(node.location.url())
self.assertTrue(key in node.metadata)
for c in node.get_children():
check_for_key(key, c)
check_for_key('graceperiod', course)
def lazyproperty(fn):
"""
Use this decorator for lazy generation of properties that
are expensive to compute. From http://stackoverflow.com/a/3013910/86828
Example:
class Test(object):
@lazyproperty
def a(self):
print 'generating "a"'
return range(5)
Interactive Session:
>>> t = Test()
>>> t.__dict__
{}
>>> t.a
generating "a"
[0, 1, 2, 3, 4]
>>> t.__dict__
{'_lazy_a': [0, 1, 2, 3, 4]}
>>> t.a
[0, 1, 2, 3, 4]
"""
attr_name = '_lazy_' + fn.__name__
@property
def _lazyprop(self):
if not hasattr(self, attr_name):
setattr(self, attr_name, fn(self))
return getattr(self, attr_name)
return _lazyprop
\ No newline at end of file
...@@ -10,8 +10,8 @@ class_priority = ['video', 'problem'] ...@@ -10,8 +10,8 @@ class_priority = ['video', 'problem']
class VerticalModule(XModule): class VerticalModule(XModule):
''' Layout module for laying out submodules vertically.''' ''' Layout module for laying out submodules vertically.'''
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs)
self.contents = None self.contents = None
def get_html(self): def get_html(self):
......
...@@ -23,9 +23,9 @@ class VideoModule(XModule): ...@@ -23,9 +23,9 @@ class VideoModule(XModule):
css = {'scss': [resource_string(__name__, 'css/video/display.scss')]} css = {'scss': [resource_string(__name__, 'css/video/display.scss')]}
js_module_name = "Video" js_module_name = "Video"
def __init__(self, system, location, definition, def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs): instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs) instance_state, shared_state, **kwargs)
xmltree = etree.fromstring(self.definition['data']) xmltree = etree.fromstring(self.definition['data'])
self.youtube = xmltree.get('youtube') self.youtube = xmltree.get('youtube')
...@@ -80,3 +80,5 @@ class VideoModule(XModule): ...@@ -80,3 +80,5 @@ class VideoModule(XModule):
class VideoDescriptor(RawDescriptor): class VideoDescriptor(RawDescriptor):
module_class = VideoModule module_class = VideoModule
stores_state = True
...@@ -6,6 +6,7 @@ from fs.errors import ResourceNotFoundError ...@@ -6,6 +6,7 @@ from fs.errors import ResourceNotFoundError
from functools import partial from functools import partial
from lxml import etree from lxml import etree
from lxml.etree import XMLSyntaxError from lxml.etree import XMLSyntaxError
from pprint import pprint
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
...@@ -142,7 +143,7 @@ class XModule(HTMLSnippet): ...@@ -142,7 +143,7 @@ class XModule(HTMLSnippet):
# in the module # in the module
icon_class = 'other' icon_class = 'other'
def __init__(self, system, location, definition, def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs): instance_state=None, shared_state=None, **kwargs):
''' '''
Construct a new xmodule Construct a new xmodule
...@@ -165,6 +166,10 @@ class XModule(HTMLSnippet): ...@@ -165,6 +166,10 @@ class XModule(HTMLSnippet):
'children': is a list of Location-like values for child modules that 'children': is a list of Location-like values for child modules that
this module depends on this module depends on
descriptor: the XModuleDescriptor that this module is an instance of.
TODO (vshnayder): remove the definition parameter and location--they
can come from the descriptor.
instance_state: A string of serialized json that contains the state of instance_state: A string of serialized json that contains the state of
this module for current student accessing the system, or None if this module for current student accessing the system, or None if
no state has been saved no state has been saved
...@@ -188,6 +193,7 @@ class XModule(HTMLSnippet): ...@@ -188,6 +193,7 @@ class XModule(HTMLSnippet):
self.system = system self.system = system
self.location = Location(location) self.location = Location(location)
self.definition = definition self.definition = definition
self.descriptor = descriptor
self.instance_state = instance_state self.instance_state = instance_state
self.shared_state = shared_state self.shared_state = shared_state
self.id = self.location.url() self.id = self.location.url()
...@@ -303,10 +309,18 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -303,10 +309,18 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
entry_point = "xmodule.v1" entry_point = "xmodule.v1"
module_class = XModule module_class = XModule
# Attributes for inpsection of the descriptor
stores_state = False # Indicates whether the xmodule state should be
# stored in a database (independent of shared state)
has_score = False # This indicates whether the xmodule is a problem-type.
# It should respond to max_score() and grade(). It can be graded or ungraded
# (like a practice problem).
# A list of metadata that this module can inherit from its parent module # A list of metadata that this module can inherit from its parent module
inheritable_metadata = ( inheritable_metadata = (
'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize', 'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize',
# TODO (ichuang): used for Fall 2012 xqa server access
'xqa_key',
# TODO: This is used by the XMLModuleStore to provide for locations for # TODO: This is used by the XMLModuleStore to provide for locations for
# static files, and will need to be removed when that code is removed # static files, and will need to be removed when that code is removed
'data_dir' 'data_dir'
...@@ -390,6 +404,18 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -390,6 +404,18 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
return dict((k,v) for k,v in self.metadata.items() return dict((k,v) for k,v in self.metadata.items()
if k not in self._inherited_metadata) if k not in self._inherited_metadata)
@staticmethod
def compute_inherited_metadata(node):
"""Given a descriptor, traverse all of its descendants and do metadata
inheritance. Should be called on a CourseDescriptor after importing a
course.
NOTE: This means that there is no such thing as lazy loading at the
moment--this accesses all the children."""
for c in node.get_children():
c.inherit_metadata(node.metadata)
XModuleDescriptor.compute_inherited_metadata(c)
def inherit_metadata(self, metadata): def inherit_metadata(self, metadata):
""" """
Updates this module with metadata inherited from a containing module. Updates this module with metadata inherited from a containing module.
...@@ -410,6 +436,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -410,6 +436,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
self._child_instances = [] self._child_instances = []
for child_loc in self.definition.get('children', []): for child_loc in self.definition.get('children', []):
child = self.system.load_item(child_loc) child = self.system.load_item(child_loc)
# TODO (vshnayder): this should go away once we have
# proper inheritance support in mongo. The xml
# datastore does all inheritance on course load.
child.inherit_metadata(self.metadata) child.inherit_metadata(self.metadata)
self._child_instances.append(child) self._child_instances.append(child)
...@@ -426,6 +455,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -426,6 +455,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
system, system,
self.location, self.location,
self.definition, self.definition,
self,
metadata=self.metadata metadata=self.metadata
) )
...@@ -493,7 +523,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -493,7 +523,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
# Put import here to avoid circular import errors # Put import here to avoid circular import errors
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
msg = "Error loading from xml." msg = "Error loading from xml."
log.exception(msg) log.warning(msg + " " + str(err))
system.error_tracker(msg) system.error_tracker(msg)
err_msg = msg + "\n" + exc_info_to_str(sys.exc_info()) err_msg = msg + "\n" + exc_info_to_str(sys.exc_info())
descriptor = ErrorDescriptor.from_xml(xml_data, system, org, course, descriptor = ErrorDescriptor.from_xml(xml_data, system, org, course,
...@@ -550,9 +580,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -550,9 +580,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
if not eq: if not eq:
for attr in self.equality_attributes: for attr in self.equality_attributes:
print(getattr(self, attr, None), pprint((getattr(self, attr, None),
getattr(other, attr, None), getattr(other, attr, None),
getattr(self, attr, None) == getattr(other, attr, None)) getattr(self, attr, None) == getattr(other, attr, None)))
return eq return eq
...@@ -586,9 +616,10 @@ class DescriptorSystem(object): ...@@ -586,9 +616,10 @@ class DescriptorSystem(object):
try: try:
x = access_some_resource() x = access_some_resource()
check_some_format(x) check_some_format(x)
except SomeProblem: except SomeProblem as err:
msg = 'Grommet {0} is broken'.format(x) msg = 'Grommet {0} is broken: {1}'.format(x, str(err))
log.exception(msg) # don't rely on handler to log log.warning(msg) # don't rely on tracker to log
# NOTE: we generally don't want content errors logged as errors
self.system.error_tracker(msg) self.system.error_tracker(msg)
# work around # work around
return 'Oops, couldn't load grommet' return 'Oops, couldn't load grommet'
...@@ -643,7 +674,7 @@ class ModuleSystem(object): ...@@ -643,7 +674,7 @@ class ModuleSystem(object):
user=None, user=None,
filestore=None, filestore=None,
debug=False, debug=False,
xqueue = None, xqueue=None,
is_staff=False, is_staff=False,
node_path=""): node_path=""):
''' '''
...@@ -668,7 +699,7 @@ class ModuleSystem(object): ...@@ -668,7 +699,7 @@ class ModuleSystem(object):
filestore - A filestore ojbect. Defaults to an instance of OSFS based filestore - A filestore ojbect. Defaults to an instance of OSFS based
at settings.DATA_DIR. at settings.DATA_DIR.
xqueue - Dict containing XqueueInterface object, as well as parameters xqueue - Dict containing XqueueInterface object, as well as parameters
for the specific StudentModule for the specific StudentModule
replace_urls - TEMPORARY - A function like static_replace.replace_urls replace_urls - TEMPORARY - A function like static_replace.replace_urls
......
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xmodule.modulestore import Location from xmodule.modulestore import Location
from lxml import etree from lxml import etree
import json
import copy import copy
import logging import logging
import traceback import traceback
...@@ -11,22 +12,52 @@ import sys ...@@ -11,22 +12,52 @@ import sys
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
_AttrMapBase = namedtuple('_AttrMap', 'metadata_key to_metadata from_metadata')
def is_pointer_tag(xml_obj):
"""
Check if xml_obj is a pointer tag: <blah url_name="something" />.
No children, one attribute named url_name.
Special case for course roots: the pointer is
<course url_name="something" org="myorg" course="course">
xml_obj: an etree Element
Returns a bool.
"""
if xml_obj.tag != "course":
expected_attr = set(['url_name'])
else:
expected_attr = set(['url_name', 'course', 'org'])
actual_attr = set(xml_obj.attrib.keys())
return len(xml_obj) == 0 and actual_attr == expected_attr
def get_metadata_from_xml(xml_object, remove=True):
meta = xml_object.find('meta')
if meta is None:
return ''
dmdata = meta.text
log.debug('meta for %s loaded: %s' % (xml_object,dmdata))
if remove:
xml_object.remove(meta)
return dmdata
_AttrMapBase = namedtuple('_AttrMap', 'from_xml to_xml')
class AttrMap(_AttrMapBase): class AttrMap(_AttrMapBase):
""" """
A class that specifies a metadata_key, and two functions: A class that specifies two functions:
to_metadata: convert value from the xml representation into from_xml: convert value from the xml representation into
an internal python representation an internal python representation
from_metadata: convert the internal python representation into to_xml: convert the internal python representation into
the value to store in the xml. the value to store in the xml.
""" """
def __new__(_cls, metadata_key, def __new__(_cls, from_xml=lambda x: x,
to_metadata=lambda x: x, to_xml=lambda x: x):
from_metadata=lambda x: x): return _AttrMapBase.__new__(_cls, from_xml, to_xml)
return _AttrMapBase.__new__(_cls, metadata_key, to_metadata, from_metadata)
class XmlDescriptor(XModuleDescriptor): class XmlDescriptor(XModuleDescriptor):
...@@ -39,19 +70,29 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -39,19 +70,29 @@ class XmlDescriptor(XModuleDescriptor):
# The attributes will be removed from the definition xml passed # The attributes will be removed from the definition xml passed
# to definition_from_xml, and from the xml returned by definition_to_xml # to definition_from_xml, and from the xml returned by definition_to_xml
# Note -- url_name isn't in this list because it's handled specially on
# import and export.
# TODO (vshnayder): Do we need a list of metadata we actually
# understand? And if we do, is this the place?
# Related: What's the right behavior for clean_metadata?
metadata_attributes = ('format', 'graceperiod', 'showanswer', 'rerandomize', metadata_attributes = ('format', 'graceperiod', 'showanswer', 'rerandomize',
'start', 'due', 'graded', 'display_name', 'url_name', 'hide_from_toc', 'start', 'due', 'graded', 'display_name', 'url_name', 'hide_from_toc',
'ispublic', # if True, then course is listed for all users; see 'ispublic', # if True, then course is listed for all users; see
'xqa_key', # for xqaa server access
# VS[compat] Remove once unused. # VS[compat] Remove once unused.
'name', 'slug') 'name', 'slug')
metadata_to_strip = ('data_dir',
# VS[compat] -- remove the below attrs once everything is in the CMS
'course', 'org', 'url_name', 'filename')
# A dictionary mapping xml attribute names AttrMaps that describe how # A dictionary mapping xml attribute names AttrMaps that describe how
# to import and export them # to import and export them
xml_attribute_map = { xml_attribute_map = {
# type conversion: want True/False in python, "true"/"false" in xml # type conversion: want True/False in python, "true"/"false" in xml
'graded': AttrMap('graded', 'graded': AttrMap(lambda val: val == 'true',
lambda val: val == 'true',
lambda val: str(val).lower()), lambda val: str(val).lower()),
} }
...@@ -102,11 +143,31 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -102,11 +143,31 @@ class XmlDescriptor(XModuleDescriptor):
return etree.parse(file_object).getroot() return etree.parse(file_object).getroot()
@classmethod @classmethod
def load_file(cls, filepath, fs, location):
'''
Open the specified file in fs, and call cls.file_to_xml on it,
returning the lxml object.
Add details and reraise on error.
'''
try:
with fs.open(filepath) as file:
return cls.file_to_xml(file)
except Exception as err:
# Add info about where we are, but keep the traceback
msg = 'Unable to load file contents at path %s for item %s: %s ' % (
filepath, location.url(), str(err))
raise Exception, msg, sys.exc_info()[2]
@classmethod
def load_definition(cls, xml_object, system, location): def load_definition(cls, xml_object, system, location):
'''Load a descriptor definition from the specified xml_object. '''Load a descriptor definition from the specified xml_object.
Subclasses should not need to override this except in special Subclasses should not need to override this except in special
cases (e.g. html module)''' cases (e.g. html module)'''
# VS[compat] -- the filename tag should go away once everything is
# converted. (note: make sure html files still work once this goes away)
filename = xml_object.get('filename') filename = xml_object.get('filename')
if filename is None: if filename is None:
definition_xml = copy.deepcopy(xml_object) definition_xml = copy.deepcopy(xml_object)
...@@ -120,25 +181,20 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -120,25 +181,20 @@ class XmlDescriptor(XModuleDescriptor):
# again in the correct format. This should go away once the CMS is # again in the correct format. This should go away once the CMS is
# online and has imported all current (fall 2012) courses from xml # online and has imported all current (fall 2012) courses from xml
if not system.resources_fs.exists(filepath) and hasattr( if not system.resources_fs.exists(filepath) and hasattr(
cls, cls, 'backcompat_paths'):
'backcompat_paths'):
candidates = cls.backcompat_paths(filepath) candidates = cls.backcompat_paths(filepath)
for candidate in candidates: for candidate in candidates:
if system.resources_fs.exists(candidate): if system.resources_fs.exists(candidate):
filepath = candidate filepath = candidate
break break
try: definition_xml = cls.load_file(filepath, system.resources_fs, location)
with system.resources_fs.open(filepath) as file:
definition_xml = cls.file_to_xml(file)
except Exception:
msg = 'Unable to load file contents at path %s for item %s' % (
filepath, location.url())
# Add info about where we are, but keep the traceback
raise Exception, msg, sys.exc_info()[2]
definition_metadata = get_metadata_from_xml(definition_xml)
cls.clean_metadata_from_xml(definition_xml) cls.clean_metadata_from_xml(definition_xml)
definition = cls.definition_from_xml(definition_xml, system) definition = cls.definition_from_xml(definition_xml, system)
if definition_metadata:
definition['definition_metadata'] = definition_metadata
# TODO (ichuang): remove this after migration # TODO (ichuang): remove this after migration
# for Fall 2012 LMS migration: keep filename (and unmangled filename) # for Fall 2012 LMS migration: keep filename (and unmangled filename)
...@@ -146,6 +202,28 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -146,6 +202,28 @@ class XmlDescriptor(XModuleDescriptor):
return definition return definition
@classmethod
def load_metadata(cls, xml_object):
"""
Read the metadata attributes from this xml_object.
Returns a dictionary {key: value}.
"""
metadata = {}
for attr in xml_object.attrib:
val = xml_object.get(attr)
if val is not None:
# VS[compat]. Remove after all key translations done
attr = cls._translate(attr)
if attr in cls.metadata_to_strip:
# don't load these
continue
attr_map = cls.xml_attribute_map.get(attr, AttrMap())
metadata[attr] = attr_map.from_xml(val)
return metadata
@classmethod @classmethod
def from_xml(cls, xml_data, system, org=None, course=None): def from_xml(cls, xml_data, system, org=None, course=None):
...@@ -160,26 +238,38 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -160,26 +238,38 @@ class XmlDescriptor(XModuleDescriptor):
url identifiers url identifiers
""" """
xml_object = etree.fromstring(xml_data) xml_object = etree.fromstring(xml_data)
# VS[compat] -- just have the url_name lookup once translation is done # VS[compat] -- just have the url_name lookup, once translation is done
slug = xml_object.get('url_name', xml_object.get('slug')) url_name = xml_object.get('url_name', xml_object.get('slug'))
location = Location('i4x', org, course, xml_object.tag, slug) location = Location('i4x', org, course, xml_object.tag, url_name)
def load_metadata(): # VS[compat] -- detect new-style each-in-a-file mode
metadata = {} if is_pointer_tag(xml_object):
for attr in cls.metadata_attributes: # new style:
val = xml_object.get(attr) # read the actual definition file--named using url_name
if val is not None: filepath = cls._format_filepath(xml_object.tag, url_name)
# VS[compat]. Remove after all key translations done definition_xml = cls.load_file(filepath, system.resources_fs, location)
attr = cls._translate(attr) else:
definition_xml = xml_object # this is just a pointer, not the real definition content
attr_map = cls.xml_attribute_map.get(attr, AttrMap(attr))
metadata[attr_map.metadata_key] = attr_map.to_metadata(val) definition = cls.load_definition(definition_xml, system, location) # note this removes metadata
return metadata # VS[compat] -- make Ike's github preview links work in both old and
# new file layouts
definition = cls.load_definition(xml_object, system, location) if is_pointer_tag(xml_object):
metadata = load_metadata() # new style -- contents actually at filepath
# VS[compat] -- just have the url_name lookup once translation is done definition['filename'] = [filepath, filepath]
slug = xml_object.get('url_name', xml_object.get('slug'))
metadata = cls.load_metadata(definition_xml)
# move definition metadata into dict
dmdata = definition.get('definition_metadata','')
if dmdata:
metadata['definition_metadata_raw'] = dmdata
try:
metadata.update(json.loads(dmdata))
except Exception as err:
log.debug('Error %s in loading metadata %s' % (err,dmdata))
metadata['definition_metadata_err'] = str(err)
return cls( return cls(
system, system,
definition, definition,
...@@ -193,19 +283,14 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -193,19 +283,14 @@ class XmlDescriptor(XModuleDescriptor):
name=name, name=name,
ext=cls.filename_extension) ext=cls.filename_extension)
@classmethod def export_to_file(self):
def split_to_file(cls, xml_object): """If this returns True, write the definition of this descriptor to a separate
''' file.
Decide whether to write this object to a separate file or not.
xml_object: an xml definition of an instance of cls. NOTE: Do not override this without a good reason. It is here specifically for customtag...
"""
return True
This default implementation will split if this has more than 7
descendant tags.
Can be overridden by subclasses.
'''
return len(list(xml_object.iter())) > 7
def export_to_xml(self, resource_fs): def export_to_xml(self, resource_fs):
""" """
...@@ -227,42 +312,43 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -227,42 +312,43 @@ class XmlDescriptor(XModuleDescriptor):
xml_object = self.definition_to_xml(resource_fs) xml_object = self.definition_to_xml(resource_fs)
self.__class__.clean_metadata_from_xml(xml_object) self.__class__.clean_metadata_from_xml(xml_object)
# Set the tag first, so it's right if writing to a file # Set the tag so we get the file path right
xml_object.tag = self.category xml_object.tag = self.category
# Write it to a file if necessary def val_for_xml(attr):
if self.split_to_file(xml_object): """Get the value for this attribute that we want to store.
# Put this object in its own file (Possible format conversion through an AttrMap).
"""
attr_map = self.xml_attribute_map.get(attr, AttrMap())
return attr_map.to_xml(self.own_metadata[attr])
# Add the non-inherited metadata
for attr in sorted(self.own_metadata):
# don't want e.g. data_dir
if attr not in self.metadata_to_strip:
xml_object.set(attr, val_for_xml(attr))
if self.export_to_file():
# Write the definition to a file
filepath = self.__class__._format_filepath(self.category, self.url_name) filepath = self.__class__._format_filepath(self.category, self.url_name)
resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True) resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True)
with resource_fs.open(filepath, 'w') as file: with resource_fs.open(filepath, 'w') as file:
file.write(etree.tostring(xml_object, pretty_print=True)) file.write(etree.tostring(xml_object, pretty_print=True))
# ...and remove all of its children here
for child in xml_object:
xml_object.remove(child)
# also need to remove the text of this object.
xml_object.text = ''
# and the tail for good measure...
xml_object.tail = ''
xml_object.set('filename', self.url_name) # And return just a pointer with the category and filename.
record_object = etree.Element(self.category)
# Add the metadata else:
xml_object.set('url_name', self.url_name) record_object = xml_object
for attr in self.metadata_attributes:
attr_map = self.xml_attribute_map.get(attr, AttrMap(attr))
metadata_key = attr_map.metadata_key
if (metadata_key not in self.metadata or record_object.set('url_name', self.url_name)
metadata_key in self._inherited_metadata):
continue
val = attr_map.from_metadata(self.metadata[metadata_key]) # Special case for course pointers:
xml_object.set(attr, val) if self.category == 'course':
# add org and course attributes on the pointer tag
record_object.set('org', self.location.org)
record_object.set('course', self.location.course)
# Now we just have to make it beautiful return etree.tostring(record_object, pretty_print=True)
return etree.tostring(xml_object, pretty_print=True)
def definition_to_xml(self, resource_fs): def definition_to_xml(self, resource_fs):
""" """
......
common/static/images/sequence-nav/edit.png

168 Bytes | W: | H:

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

171 Bytes | W: | H:

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

276 Bytes | W: | H:

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

292 Bytes | W: | H:

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

229 Bytes | W: | H:

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

234 Bytes | W: | H:

common/static/images/sequence-nav/view.png
common/static/images/sequence-nav/view.png
common/static/images/sequence-nav/view.png
common/static/images/sequence-nav/view.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<chapter name="Overview"> <chapter name="Overview">
<video name="Welcome" youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8"/> <video name="Welcome" youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8"/>
<videosequence format="Lecture Sequence" name="A simple sequence"> <videosequence format="Lecture Sequence" name="A simple sequence">
<html id="toylab" filename="toylab"/> <html name="toylab" filename="toylab"/>
<video name="S0V1: Video Resources" youtube="0.75:EuzkdzfR0i8,1.0:1bK-WdDi6Qw,1.25:0v1VzoDVUTM,1.50:Bxk_-ZJb240"/> <video name="S0V1: Video Resources" youtube="0.75:EuzkdzfR0i8,1.0:1bK-WdDi6Qw,1.25:0v1VzoDVUTM,1.50:Bxk_-ZJb240"/>
</videosequence> </videosequence>
<section name="Lecture 2"> <section name="Lecture 2">
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
<chapter name="Chapter 2"> <chapter name="Chapter 2">
<section name="Problem Set 1"> <section name="Problem Set 1">
<sequential> <sequential>
<problem type="lecture" showanswer="attempted" rerandomize="true" title="A simple coding problem" name="Simple coding problem" filename="ps01-simple"/> <problem type="lecture" showanswer="attempted" rerandomize="true" display_name="A simple coding problem" name="Simple coding problem" filename="ps01-simple"/>
</sequential> </sequential>
</section> </section>
<video name="Lost Video" youtube="1.0:TBvX7HzxexQ"/> <video name="Lost Video" youtube="1.0:TBvX7HzxexQ"/>
......
...@@ -28,3 +28,40 @@ Check out the course data directories that you want to work with into the ...@@ -28,3 +28,40 @@ Check out the course data directories that you want to work with into the
Replace `../data` with your `GITHUB_REPO_ROOT` if it's not the default value. Replace `../data` with your `GITHUB_REPO_ROOT` if it's not the default value.
This will import all courses in your data directory into mongodb This will import all courses in your data directory into mongodb
## Unit tests
This runs all the tests (long, uses collectstatic):
rake test
If if you aren't changing static files, can run `rake test` once, then run
rake fasttest_{lms,cms}
xmodule can be tested independently, with this:
rake test_common/lib/xmodule
To see all available rake commands, do this:
rake -T
To run a single django test class:
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/courseware/tests/tests.py:TestViewAuth
To run a single django test:
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/courseware/tests/tests.py:TestViewAuth.test_dark_launch
To run a single nose test file:
nosetests common/lib/xmodule/xmodule/tests/test_stringify.py
To run a single nose test:
nosetests common/lib/xmodule/xmodule/tests/test_stringify.py:test_stringify
...@@ -52,7 +52,7 @@ def certificate_request(request): ...@@ -52,7 +52,7 @@ def certificate_request(request):
return return_error(survey_response['error']) return return_error(survey_response['error'])
grade = None grade = None
student_gradesheet = grades.grade_sheet(request.user) student_gradesheet = grades.grade(request.user, request, course)
grade = student_gradesheet['grade'] grade = student_gradesheet['grade']
if not grade: if not grade:
...@@ -65,7 +65,7 @@ def certificate_request(request): ...@@ -65,7 +65,7 @@ def certificate_request(request):
else: else:
#This is not a POST, we should render the page with the form #This is not a POST, we should render the page with the form
grade_sheet = grades.grade_sheet(request.user) student_gradesheet = grades.grade(request.user, request, course)
certificate_state = certificate_state_for_student(request.user, grade_sheet['grade']) certificate_state = certificate_state_for_student(request.user, grade_sheet['grade'])
if certificate_state['state'] != "requestable": if certificate_state['state'] != "requestable":
......
...@@ -8,19 +8,19 @@ from django.conf import settings ...@@ -8,19 +8,19 @@ from django.conf import settings
from django.http import Http404 from django.http import Http404
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from static_replace import replace_urls from static_replace import replace_urls, try_staticfiles_lookup
from staticfiles.storage import staticfiles_storage
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def check_course(course_id, course_must_be_open=True, course_required=True): def check_course(user, course_id, course_must_be_open=True, course_required=True):
""" """
Given a course_id, this returns the course object. By default, Given a django user and a course_id, this returns the course
if the course is not found or the course is not open yet, this object. By default, if the course is not found or the course is
method will raise a 404. not open yet, this method will raise a 404.
If course_must_be_open is False, the course will be returned If course_must_be_open is False, the course will be returned
without a 404 even if it is not open. without a 404 even if it is not open.
...@@ -28,6 +28,10 @@ def check_course(course_id, course_must_be_open=True, course_required=True): ...@@ -28,6 +28,10 @@ def check_course(course_id, course_must_be_open=True, course_required=True):
If course_required is False, a course_id of None is acceptable. The If course_required is False, a course_id of None is acceptable. The
course returned will be None. Even if the course is not required, course returned will be None. Even if the course is not required,
if a course_id is given that does not exist a 404 will be raised. if a course_id is given that does not exist a 404 will be raised.
This behavior is modified by MITX_FEATURES['DARK_LAUNCH']:
if dark launch is enabled, course_must_be_open is ignored for
users that have staff access.
""" """
course = None course = None
if course_required or course_id: if course_required or course_id:
...@@ -39,19 +43,29 @@ def check_course(course_id, course_must_be_open=True, course_required=True): ...@@ -39,19 +43,29 @@ def check_course(course_id, course_must_be_open=True, course_required=True):
raise Http404("Course not found.") raise Http404("Course not found.")
started = course.has_started() or settings.MITX_FEATURES['DISABLE_START_DATES'] started = course.has_started() or settings.MITX_FEATURES['DISABLE_START_DATES']
if course_must_be_open and not started:
must_be_open = course_must_be_open
if (settings.MITX_FEATURES['DARK_LAUNCH'] and
has_staff_access_to_course(user, course)):
must_be_open = False
if must_be_open and not started:
raise Http404("This course has not yet started.") raise Http404("This course has not yet started.")
return course return course
def course_image_url(course): def course_image_url(course):
return staticfiles_storage.url(course.metadata['data_dir'] + "/images/course_image.jpg") """Try to look up the image url for the course. If it's not found,
log an error and return the dead link"""
path = course.metadata['data_dir'] + "/images/course_image.jpg"
return try_staticfiles_lookup(path)
def get_course_about_section(course, section_key): def get_course_about_section(course, section_key):
""" """
This returns the snippet of html to be rendered on the course about page, given the key for the section. This returns the snippet of html to be rendered on the course about page,
given the key for the section.
Valid keys: Valid keys:
- overview - overview
- title - title
...@@ -70,18 +84,23 @@ def get_course_about_section(course, section_key): ...@@ -70,18 +84,23 @@ def get_course_about_section(course, section_key):
- more_info - more_info
""" """
# Many of these are stored as html files instead of some semantic markup. This can change without effecting # Many of these are stored as html files instead of some semantic
# this interface when we find a good format for defining so many snippets of text/html. # 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 # TODO: Remove number, instructors from this list
if section_key in ['short_description', 'description', 'key_dates', 'video', 'course_staff_short', 'course_staff_extended', if section_key in ['short_description', 'description', 'key_dates', 'video',
'requirements', 'syllabus', 'textbook', 'faq', 'more_info', 'number', 'instructors', 'overview', 'course_staff_short', 'course_staff_extended',
'effort', 'end_date', 'prerequisites']: 'requirements', 'syllabus', 'textbook', 'faq', 'more_info',
'number', 'instructors', 'overview',
'effort', 'end_date', 'prerequisites']:
try: try:
with course.system.resources_fs.open(path("about") / section_key + ".html") as htmlFile: with course.system.resources_fs.open(path("about") / section_key + ".html") as htmlFile:
return replace_urls(htmlFile.read().decode('utf-8'), course.metadata['data_dir']) return replace_urls(htmlFile.read().decode('utf-8'),
course.metadata['data_dir'])
except ResourceNotFoundError: except ResourceNotFoundError:
log.warning("Missing about section {key} in course {url}".format(key=section_key, url=course.location.url())) log.warning("Missing about section {key} in course {url}".format(
key=section_key, url=course.location.url()))
return None return None
elif section_key == "title": elif section_key == "title":
return course.metadata.get('display_name', course.url_name) return course.metadata.get('display_name', course.url_name)
...@@ -95,7 +114,9 @@ def get_course_about_section(course, section_key): ...@@ -95,7 +114,9 @@ def get_course_about_section(course, section_key):
def get_course_info_section(course, section_key): def get_course_info_section(course, section_key):
""" """
This returns the snippet of html to be rendered on the course info page, given the key for the section. This returns the snippet of html to be rendered on the course info page,
given the key for the section.
Valid keys: Valid keys:
- handouts - handouts
- guest_handouts - guest_handouts
...@@ -103,48 +124,71 @@ def get_course_info_section(course, section_key): ...@@ -103,48 +124,71 @@ def get_course_info_section(course, section_key):
- guest_updates - guest_updates
""" """
# Many of these are stored as html files instead of some semantic markup. This can change without effecting # Many of these are stored as html files instead of some semantic
# this interface when we find a good format for defining so many snippets of text/html. # 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']: if section_key in ['handouts', 'guest_handouts', 'updates', 'guest_updates']:
try: try:
with course.system.resources_fs.open(path("info") / section_key + ".html") as htmlFile: with course.system.resources_fs.open(path("info") / section_key + ".html") as htmlFile:
return replace_urls(htmlFile.read().decode('utf-8'), course.metadata['data_dir']) return replace_urls(htmlFile.read().decode('utf-8'),
course.metadata['data_dir'])
except ResourceNotFoundError: except ResourceNotFoundError:
log.exception("Missing info section {key} in course {url}".format(key=section_key, url=course.location.url())) log.exception("Missing info section {key} in course {url}".format(
key=section_key, url=course.location.url()))
return "! Info section missing !" return "! Info section missing !"
raise KeyError("Invalid about key " + str(section_key)) raise KeyError("Invalid about key " + str(section_key))
def course_staff_group_name(course): def course_staff_group_name(course):
''' '''
course should be either a CourseDescriptor instance, or a string (the .course entry of a Location) course should be either a CourseDescriptor instance, or a string (the
.course entry of a Location)
''' '''
if isinstance(course,str): if isinstance(course, str) or isinstance(course, unicode):
coursename = course coursename = course
else: else:
coursename = course.metadata.get('data_dir','UnknownCourseName') # should be a CourseDescriptor, so grab its location.course:
if not coursename: # Fall 2012: not all course.xml have metadata correct yet coursename = course.location.course
coursename = course.metadata.get('course','')
return 'staff_%s' % coursename return 'staff_%s' % coursename
def has_staff_access_to_course(user,course): def has_staff_access_to_course(user, course):
''' '''
Returns True if the given user has staff access to the course. Returns True if the given user has staff access to the course.
This means that user is in the staff_* group, or is an overall admin. This means that user is in the staff_* group, or is an overall admin.
TODO (vshnayder): this needs to be changed to allow per-course_id permissions, not per-course
(e.g. staff in 2012 is different from 2013, but maybe some people always have access)
course is the course field of the location being accessed.
''' '''
if user is None or (not user.is_authenticated()) or course is None: if user is None or (not user.is_authenticated()) or course is None:
return False return False
if user.is_staff: if user.is_staff:
return True return True
user_groups = [x[1] for x in user.groups.values_list()] # note this is the Auth group, not UserTestGroup
# note this is the Auth group, not UserTestGroup
user_groups = [x[1] for x in user.groups.values_list()]
staff_group = course_staff_group_name(course) staff_group = course_staff_group_name(course)
log.debug('course %s user %s groups %s' % (staff_group, user, user_groups))
if staff_group in user_groups: if staff_group in user_groups:
return True return True
return False return False
def has_access_to_course(user,course): def has_staff_access_to_course_id(user, course_id):
"""Helper method that takes a course_id instead of a course name"""
loc = CourseDescriptor.id_to_location(course_id)
return has_staff_access_to_course(user, loc.course)
def has_staff_access_to_location(user, location):
"""Helper method that checks whether the user has staff access to
the course of the location.
location: something that can be passed to Location
"""
return has_staff_access_to_course(user, Location(location).course)
def has_access_to_course(user, course):
'''course is the .course element of a location'''
if course.metadata.get('ispublic'): if course.metadata.get('ispublic'):
return True return True
return has_staff_access_to_course(user,course) return has_staff_access_to_course(user,course)
...@@ -154,7 +198,8 @@ def get_courses_by_university(user): ...@@ -154,7 +198,8 @@ def get_courses_by_university(user):
Returns dict of lists of courses available, keyed by course.org (ie university). Returns dict of lists of courses available, keyed by course.org (ie university).
Courses are sorted by course.number. Courses are sorted by course.number.
if ACCESS_REQUIRE_STAFF_FOR_COURSE then list only includes those accessible to user. if ACCESS_REQUIRE_STAFF_FOR_COURSE then list only includes those accessible
to user.
''' '''
# TODO: Clean up how 'error' is done. # TODO: Clean up how 'error' is done.
# filter out any courses that errored. # filter out any courses that errored.
...@@ -163,9 +208,9 @@ def get_courses_by_university(user): ...@@ -163,9 +208,9 @@ def get_courses_by_university(user):
courses = sorted(courses, key=lambda course: course.number) courses = sorted(courses, key=lambda course: course.number)
universities = defaultdict(list) universities = defaultdict(list)
for course in courses: for course in courses:
if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'): if settings.MITX_FEATURES.get('ACCESS_REQUIRE_STAFF_FOR_COURSE'):
if not has_access_to_course(user,course): if not has_access_to_course(user,course):
continue continue
universities[course.org].append(course) universities[course.org].append(course)
return universities return universities
# Compute grades using real division, with no integer truncation
from __future__ import division
import random import random
import logging import logging
from django.conf import settings from django.conf import settings
from models import StudentModuleCache
from module_render import get_module, get_instance_module
from xmodule import graders from xmodule import graders
from xmodule.graders import Score from xmodule.graders import Score
from models import StudentModule from models import StudentModule
_log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
def yield_module_descendents(module):
stack = module.get_display_items()
while len(stack) > 0:
next_module = stack.pop()
stack.extend( next_module.get_display_items() )
yield next_module
def grade_sheet(student, course, grader, student_module_cache): def grade(student, request, course, student_module_cache=None):
"""
This grades a student as quickly as possible. It retuns the
output from the course grader, augmented with the final letter
grade. The keys in the output are:
- grade : A final letter grade.
- percent : The final percent for the class (rounded up).
- section_breakdown : A breakdown of each section that makes
up the grade. (For display)
- grade_breakdown : A breakdown of the major components that
make up the final grade. (For display)
More information on the format is in the docstring for CourseGrader.
""" """
This pulls a summary of all problems in the course. It returns a dictionary
with two datastructures:
- courseware_summary is a summary of all sections with problems in the grading_context = course.grading_context
course. It is organized as an array of chapters, each containing an array of
sections, each containing an array of scores. This contains information for if student_module_cache == None:
graded and ungraded problems, and is good for displaying a course summary student_module_cache = StudentModuleCache(student, grading_context['all_descriptors'])
with due dates, etc.
totaled_scores = {}
# This next complicated loop is just to collect the totaled_scores, which is
# passed to the grader
for section_format, sections in grading_context['graded_sections'].iteritems():
format_scores = []
for section in sections:
section_descriptor = section['section_descriptor']
section_name = section_descriptor.metadata.get('display_name')
should_grade_section = False
# If we haven't seen a single problem in the section, we don't have to grade it at all! We can assume 0%
for moduledescriptor in section['xmoduledescriptors']:
if student_module_cache.lookup(moduledescriptor.category, moduledescriptor.location.url() ):
should_grade_section = True
break
if should_grade_section:
scores = []
# TODO: We need the request to pass into here. If we could forgo that, our arguments
# would be simpler
section_module = get_module(student, request, section_descriptor.location, student_module_cache)
# TODO: We may be able to speed this up by only getting a list of children IDs from section_module
# Then, we may not need to instatiate any problems if they are already in the database
for module in yield_module_descendents(section_module):
(correct, total) = get_score(student, module, student_module_cache)
if correct is None and total is None:
continue
if settings.GENERATE_PROFILE_SCORES:
if total > 1:
correct = random.randrange(max(total - 2, 1), total + 1)
else:
correct = total
graded = module.metadata.get("graded", False)
if not total > 0:
#We simply cannot grade a problem that is 12/0, because we might need it as a percentage
graded = False
scores.append(Score(correct, total, graded, module.metadata.get('display_name')))
section_total, graded_total = graders.aggregate_scores(scores, section_name)
else:
section_total = Score(0.0, 1.0, False, section_name)
graded_total = Score(0.0, 1.0, True, section_name)
#Add the graded total to totaled_scores
if graded_total.possible > 0:
format_scores.append(graded_total)
else:
log.exception("Unable to grade a section with a total possible score of zero. " + str(section_descriptor.location))
- grade_summary is the output from the course grader. More information on totaled_scores[section_format] = format_scores
the format is in the docstring for CourseGrader.
grade_summary = course.grader.grade(totaled_scores)
# We round the grade here, to make sure that the grade is an whole percentage and
# doesn't get displayed differently than it gets grades
grade_summary['percent'] = round(grade_summary['percent'] * 100 + 0.05) / 100
letter_grade = grade_for_percentage(course.grade_cutoffs, grade_summary['percent'])
grade_summary['grade'] = letter_grade
return grade_summary
def grade_for_percentage(grade_cutoffs, percentage):
"""
Returns a letter grade 'A' 'B' 'C' or None.
Arguments
- grade_cutoffs is a dictionary mapping a grade to the lowest
possible percentage to earn that grade.
- percentage is the final percent across all problems in a course
"""
letter_grade = None
for possible_grade in ['A', 'B', 'C']:
if percentage >= grade_cutoffs[possible_grade]:
letter_grade = possible_grade
break
return letter_grade
def progress_summary(student, course, grader, student_module_cache):
"""
This pulls a summary of all problems in the course.
Returns
- courseware_summary is a summary of all sections with problems in the course.
It is organized as an array of chapters, each containing an array of sections,
each containing an array of scores. This contains information for graded and
ungraded problems, and is good for displaying a course summary with due dates,
etc.
Arguments: Arguments:
student: A User object for the student to grade student: A User object for the student to grade
...@@ -30,49 +144,30 @@ def grade_sheet(student, course, grader, student_module_cache): ...@@ -30,49 +144,30 @@ def grade_sheet(student, course, grader, student_module_cache):
student_module_cache: A StudentModuleCache initialized with all student_module_cache: A StudentModuleCache initialized with all
instance_modules for the student instance_modules for the student
""" """
totaled_scores = {}
chapters = [] chapters = []
for c in course.get_children(): for c in course.get_children():
# Don't include chapters that aren't displayable (e.g. due to error)
if c not in c.displayable_items():
continue
sections = [] sections = []
for s in c.get_children(): for s in c.get_children():
def yield_descendents(module): # Same for sections
yield module if s not in s.displayable_items():
for child in module.get_display_items(): continue
for module in yield_descendents(child):
yield module
graded = s.metadata.get('graded', False) graded = s.metadata.get('graded', False)
scores = [] scores = []
for module in yield_descendents(s): for module in yield_module_descendents(s):
(correct, total) = get_score(student, module, student_module_cache) (correct, total) = get_score(student, module, student_module_cache)
if correct is None and total is None: if correct is None and total is None:
continue continue
if settings.GENERATE_PROFILE_SCORES:
if total > 1:
correct = random.randrange(max(total - 2, 1), total + 1)
else:
correct = total
if not total > 0:
#We simply cannot grade a problem that is 12/0, because we
#might need it as a percentage
graded = False
scores.append(Score(correct, total, graded, scores.append(Score(correct, total, graded,
module.metadata.get('display_name'))) module.metadata.get('display_name')))
section_total, graded_total = graders.aggregate_scores( section_total, graded_total = graders.aggregate_scores(
scores, s.metadata.get('display_name')) scores, s.metadata.get('display_name'))
#Add the graded total to totaled_scores
format = s.metadata.get('format', "") format = s.metadata.get('format', "")
if format and graded_total.possible > 0:
format_scores = totaled_scores.get(format, [])
format_scores.append(graded_total)
totaled_scores[format] = format_scores
sections.append({ sections.append({
'display_name': s.display_name, 'display_name': s.display_name,
'url_name': s.url_name, 'url_name': s.url_name,
...@@ -88,34 +183,36 @@ def grade_sheet(student, course, grader, student_module_cache): ...@@ -88,34 +183,36 @@ def grade_sheet(student, course, grader, student_module_cache):
'url_name': c.url_name, 'url_name': c.url_name,
'sections': sections}) 'sections': sections})
grade_summary = grader.grade(totaled_scores) return chapters
return {'courseware_summary': chapters,
'grade_summary': grade_summary}
def get_score(user, problem, cache): def get_score(user, problem, student_module_cache):
""" """
Return the score for a user on a problem Return the score for a user on a problem, as a tuple (correct, total).
user: a Student object user: a Student object
problem: an XModule problem: an XModule
cache: A StudentModuleCache cache: A StudentModuleCache
""" """
if not (problem.descriptor.stores_state and problem.descriptor.has_score):
# These are not problems, and do not have a score
return (None, None)
correct = 0.0 correct = 0.0
# If the ID is not in the cache, add the item # If the ID is not in the cache, add the item
instance_module = cache.lookup(problem.category, problem.id) instance_module = get_instance_module(user, problem, student_module_cache)
if instance_module is None: # instance_module = student_module_cache.lookup(problem.category, problem.id)
instance_module = StudentModule(module_type=problem.category, # if instance_module is None:
module_state_key=problem.id, # instance_module = StudentModule(module_type=problem.category,
student=user, # module_state_key=problem.id,
state=None, # student=user,
grade=0, # state=None,
max_grade=problem.max_score(), # grade=0,
done='i') # max_grade=problem.max_score(),
cache.append(instance_module) # done='i')
instance_module.save() # cache.append(instance_module)
# instance_module.save()
# If this problem is ungraded/ungradable, bail # If this problem is ungraded/ungradable, bail
if instance_module.max_grade is None: if instance_module.max_grade is None:
...@@ -126,8 +223,11 @@ def get_score(user, problem, cache): ...@@ -126,8 +223,11 @@ def get_score(user, problem, cache):
if correct is not None and total is not None: if correct is not None and total is not None:
#Now we re-weight the problem, if specified #Now we re-weight the problem, if specified
weight = getattr(problem, 'weight', 1) weight = getattr(problem, 'weight', None)
if weight != 1: if weight is not None:
if total == 0:
log.exception("Cannot reweight a problem with zero weight. Problem: " + str(instance_module))
return (correct, total)
correct = correct * weight / total correct = correct * weight / total
total = weight total = weight
......
...@@ -78,8 +78,8 @@ class Command(BaseCommand): ...@@ -78,8 +78,8 @@ class Command(BaseCommand):
# TODO (cpennington): Get coursename in a legitimate way # TODO (cpennington): Get coursename in a legitimate way
course_location = 'i4x://edx/6002xs12/course/6.002_Spring_2012' course_location = 'i4x://edx/6002xs12/course/6.002_Spring_2012'
student_module_cache = StudentModuleCache(sample_user, modulestore().get_item(course_location)) student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(sample_user, modulestore().get_item(course_location))
(course, _, _, _) = get_module(sample_user, None, course_location, student_module_cache) course = get_module(sample_user, None, course_location, student_module_cache)
to_run = [ to_run = [
#TODO (vshnayder) : make check_rendering work (use module_render.py), #TODO (vshnayder) : make check_rendering work (use module_render.py),
......
...@@ -67,17 +67,19 @@ class StudentModuleCache(object): ...@@ -67,17 +67,19 @@ class StudentModuleCache(object):
""" """
A cache of StudentModules for a specific student A cache of StudentModules for a specific student
""" """
def __init__(self, user, descriptor, depth=None): def __init__(self, user, descriptors):
''' '''
Find any StudentModule objects that are needed by any child modules of the Find any StudentModule objects that are needed by any descriptor
supplied descriptor. Avoids making multiple queries to the database in descriptors. Avoids making multiple queries to the database.
Note: Only modules that have store_state = True or have shared
descriptor: An XModuleDescriptor state will have a StudentModule.
depth is the number of levels of descendent modules to load StudentModules for, in addition to
the supplied descriptor. If depth is None, load all descendent StudentModules Arguments
user: The user for which to fetch maching StudentModules
descriptors: An array of XModuleDescriptors.
''' '''
if user.is_authenticated(): if user.is_authenticated():
module_ids = self._get_module_state_keys(descriptor, depth) module_ids = self._get_module_state_keys(descriptors)
# This works around a limitation in sqlite3 on the number of parameters # This works around a limitation in sqlite3 on the number of parameters
# that can be put into a single query # that can be put into a single query
...@@ -91,27 +93,53 @@ class StudentModuleCache(object): ...@@ -91,27 +93,53 @@ class StudentModuleCache(object):
else: else:
self.cache = [] self.cache = []
def _get_module_state_keys(self, descriptor, depth):
''' @classmethod
Get a list of the state_keys needed for StudentModules def cache_for_descriptor_descendents(cls, user, descriptor, depth=None, descriptor_filter=lambda descriptor: True):
required for this module descriptor """
descriptor: An XModuleDescriptor descriptor: An XModuleDescriptor
depth is the number of levels of descendent modules to load StudentModules for, in addition to depth is the number of levels of descendent modules to load StudentModules for, in addition to
the supplied descriptor. If depth is None, load all descendent StudentModules the supplied descriptor. If depth is None, load all descendent StudentModules
descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
should be cached
"""
def get_child_descriptors(descriptor, depth, descriptor_filter):
if descriptor_filter(descriptor):
descriptors = [descriptor]
else:
descriptors = []
if depth is None or depth > 0:
new_depth = depth - 1 if depth is not None else depth
for child in descriptor.get_children():
descriptors.extend(get_child_descriptors(child, new_depth, descriptor_filter))
return descriptors
descriptors = get_child_descriptors(descriptor, depth, descriptor_filter)
return StudentModuleCache(user, descriptors)
def _get_module_state_keys(self, descriptors):
''' '''
keys = [descriptor.location.url()] Get a list of the state_keys needed for StudentModules
required for this module descriptor
shared_state_key = getattr(descriptor, 'shared_state_key', None)
if shared_state_key is not None: descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
keys.append(shared_state_key) should be cached
'''
if depth is None or depth > 0: keys = []
new_depth = depth - 1 if depth is not None else depth for descriptor in descriptors:
if descriptor.stores_state:
for child in descriptor.get_children(): keys.append(descriptor.location.url())
keys.extend(self._get_module_state_keys(child, new_depth))
shared_state_key = getattr(descriptor, 'shared_state_key', None)
if shared_state_key is not None:
keys.append(shared_state_key)
return keys return keys
......
...@@ -2,6 +2,7 @@ import json ...@@ -2,6 +2,7 @@ import json
import logging import logging
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse
from django.http import Http404 from django.http import Http404
from django.http import HttpResponse from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
...@@ -16,7 +17,8 @@ from xmodule.exceptions import NotFoundError ...@@ -16,7 +17,8 @@ from xmodule.exceptions import NotFoundError
from xmodule.x_module import ModuleSystem from xmodule.x_module import ModuleSystem
from xmodule_modifiers import replace_static_urls, add_histogram, wrap_xmodule from xmodule_modifiers import replace_static_urls, add_histogram, wrap_xmodule
from courseware.courses import has_staff_access_to_course from courseware.courses import (has_staff_access_to_course,
has_staff_access_to_location)
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
...@@ -46,21 +48,22 @@ def toc_for_course(user, request, course, active_chapter, active_section): ...@@ -46,21 +48,22 @@ def toc_for_course(user, request, course, active_chapter, active_section):
'format': format, 'due': due, 'active' : bool}, ...] 'format': format, 'due': due, 'active' : bool}, ...]
active is set for the section and chapter corresponding to the passed active is set for the section and chapter corresponding to the passed
parameters. Everything else comes from the xml, or defaults to "". parameters, which are expected to be url_names of the chapter+section.
Everything else comes from the xml, or defaults to "".
chapters with name 'hidden' are skipped. chapters with name 'hidden' are skipped.
''' '''
student_module_cache = StudentModuleCache(user, course, depth=2) student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, course, depth=2)
(course, _, _, _) = get_module(user, request, course.location, student_module_cache) course = get_module(user, request, course.location, student_module_cache)
chapters = list() chapters = list()
for chapter in course.get_display_items(): for chapter in course.get_display_items():
sections = list() sections = list()
for section in chapter.get_display_items(): for section in chapter.get_display_items():
active = (chapter.display_name == active_chapter and active = (chapter.url_name == active_chapter and
section.display_name == active_section) section.url_name == active_section)
hide_from_toc = section.metadata.get('hide_from_toc', 'false').lower() == 'true' hide_from_toc = section.metadata.get('hide_from_toc', 'false').lower() == 'true'
if not hide_from_toc: if not hide_from_toc:
...@@ -73,7 +76,7 @@ def toc_for_course(user, request, course, active_chapter, active_section): ...@@ -73,7 +76,7 @@ def toc_for_course(user, request, course, active_chapter, active_section):
chapters.append({'display_name': chapter.display_name, chapters.append({'display_name': chapter.display_name,
'url_name': chapter.url_name, 'url_name': chapter.url_name,
'sections': sections, 'sections': sections,
'active': chapter.display_name == active_chapter}) 'active': chapter.url_name == active_chapter})
return chapters return chapters
...@@ -121,37 +124,48 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -121,37 +124,48 @@ def get_module(user, request, location, student_module_cache, position=None):
- position : extra information from URL for user-specified - position : extra information from URL for user-specified
position within module position within module
Returns: Returns: xmodule instance
- a tuple (xmodule instance, instance_module, shared_module, module category).
instance_module is a StudentModule specific to this module for this student,
or None if this is an anonymous user
shared_module is a StudentModule specific to all modules with the same
'shared_state_key' attribute, or None if the module does not elect to
share state
''' '''
descriptor = modulestore().get_item(location) descriptor = modulestore().get_item(location)
instance_module = student_module_cache.lookup(descriptor.category, #TODO Only check the cache if this module can possibly have state
instance_module = None
shared_module = None
if user.is_authenticated():
if descriptor.stores_state:
instance_module = student_module_cache.lookup(descriptor.category,
descriptor.location.url()) descriptor.location.url())
shared_state_key = getattr(descriptor, 'shared_state_key', None) shared_state_key = getattr(descriptor, 'shared_state_key', None)
if shared_state_key is not None: if shared_state_key is not None:
shared_module = student_module_cache.lookup(descriptor.category, shared_module = student_module_cache.lookup(descriptor.category,
shared_state_key) shared_state_key)
else:
shared_module = None
instance_state = instance_module.state if instance_module is not None else None instance_state = instance_module.state if instance_module is not None else None
shared_state = shared_module.state if shared_module is not None else None shared_state = shared_module.state if shared_module is not None else None
# TODO (vshnayder): fix hardcoded urls (use reverse) # TODO (vshnayder): fix hardcoded urls (use reverse)
# Setup system context for module instance # Setup system context for module instance
ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.location.url() + '/'
ajax_url = reverse('modx_dispatch',
kwargs=dict(course_id=descriptor.location.course_id,
id=descriptor.location.url(),
dispatch=''),
)
# ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.location.url() + '/'
# Fully qualified callback URL for external queueing system # Fully qualified callback URL for external queueing system
xqueue_callback_url = (request.build_absolute_uri('/') + settings.MITX_ROOT_URL + xqueue_callback_url = request.build_absolute_uri('/')[:-1] # Trailing slash provided by reverse
'xqueue/' + str(user.id) + '/' + descriptor.location.url() + '/' + xqueue_callback_url += reverse('xqueue_callback',
'score_update') kwargs=dict(course_id=descriptor.location.course_id,
userid=str(user.id),
id=descriptor.location.url(),
dispatch='score_update'),
)
# Default queuename is course-specific and is derived from the course that # Default queuename is course-specific and is derived from the course that
# contains the current module. # contains the current module.
...@@ -163,9 +177,8 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -163,9 +177,8 @@ def get_module(user, request, location, student_module_cache, position=None):
'default_queuename': xqueue_default_queuename.replace(' ','_') } 'default_queuename': xqueue_default_queuename.replace(' ','_') }
def _get_module(location): def _get_module(location):
(module, _, _, _) = get_module(user, request, location, return get_module(user, request, location,
student_module_cache, position) student_module_cache, position)
return module
# TODO (cpennington): When modules are shared between courses, the static # TODO (cpennington): When modules are shared between courses, the static
# prefix is going to have to be specific to the module, not the directory # prefix is going to have to be specific to the module, not the directory
...@@ -182,71 +195,108 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -182,71 +195,108 @@ def get_module(user, request, location, student_module_cache, position=None):
# a module is coming through get_html and is therefore covered # a module is coming through get_html and is therefore covered
# by the replace_static_urls code below # by the replace_static_urls code below
replace_urls=replace_urls, replace_urls=replace_urls,
is_staff=user.is_staff, is_staff=has_staff_access_to_location(user, location),
node_path=settings.NODE_PATH node_path=settings.NODE_PATH
) )
# pass position specified in URL to module through ModuleSystem # pass position specified in URL to module through ModuleSystem
system.set('position', position) system.set('position', position)
system.set('DEBUG',settings.DEBUG)
module = descriptor.xmodule_constructor(system)(instance_state, shared_state) module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
module.get_html = replace_static_urls( module.get_html = replace_static_urls(
wrap_xmodule(module.get_html, module, 'xmodule_display.html'), wrap_xmodule(module.get_html, module, 'xmodule_display.html'),
module.metadata['data_dir'] module.metadata['data_dir'], module
) )
if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'): if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'):
if has_staff_access_to_course(user, module.location.course): if has_staff_access_to_course(user, module.location.course):
module.get_html = add_histogram(module.get_html, module) module.get_html = add_histogram(module.get_html, module, user)
return module
# If StudentModule for this instance wasn't already in the database, def get_instance_module(user, module, student_module_cache):
# and this isn't a guest user, create it. """
Returns instance_module is a StudentModule specific to this module for this student,
or None if this is an anonymous user
"""
if user.is_authenticated(): if user.is_authenticated():
if not module.descriptor.stores_state:
log.exception("Attempted to get the instance_module for a module "
+ str(module.id) + " which does not store state.")
return None
instance_module = student_module_cache.lookup(module.category,
module.location.url())
if not instance_module: if not instance_module:
instance_module = StudentModule( instance_module = StudentModule(
student=user, student=user,
module_type=descriptor.category, module_type=module.category,
module_state_key=module.id, module_state_key=module.id,
state=module.get_instance_state(), state=module.get_instance_state(),
max_grade=module.max_score()) max_grade=module.max_score())
instance_module.save() instance_module.save()
# Add to cache. The caller and the system context have references
# to it, so the change persists past the return
student_module_cache.append(instance_module) student_module_cache.append(instance_module)
if not shared_module and shared_state_key is not None:
shared_module = StudentModule(
student=user,
module_type=descriptor.category,
module_state_key=shared_state_key,
state=module.get_shared_state())
shared_module.save()
student_module_cache.append(shared_module)
return (module, instance_module, shared_module, descriptor.category) return instance_module
else:
return None
def get_shared_instance_module(user, module, student_module_cache):
"""
Return shared_module is a StudentModule specific to all modules with the same
'shared_state_key' attribute, or None if the module does not elect to
share state
"""
if user.is_authenticated():
# To get the shared_state_key, we need to descriptor
descriptor = modulestore().get_item(module.location)
shared_state_key = getattr(module, 'shared_state_key', None)
if shared_state_key is not None:
shared_module = student_module_cache.lookup(module.category,
shared_state_key)
if not shared_module:
shared_module = StudentModule(
student=user,
module_type=descriptor.category,
module_state_key=shared_state_key,
state=module.get_shared_state())
shared_module.save()
student_module_cache.append(shared_module)
else:
shared_module = None
return shared_module
else:
return None
@csrf_exempt @csrf_exempt
def xqueue_callback(request, userid, id, dispatch): def xqueue_callback(request, course_id, userid, id, dispatch):
''' '''
Entry point for graded results from the queueing system. Entry point for graded results from the queueing system.
''' '''
# Parse xqueue response # Test xqueue package, which we expect to be:
# xpackage = {'xqueue_header': json.dumps({'lms_key':'secretkey',...}),
# 'xqueue_body' : 'Message from grader}
get = request.POST.copy() get = request.POST.copy()
try: for key in ['xqueue_header', 'xqueue_body']:
header = json.loads(get['xqueue_header']) if not get.has_key(key):
except Exception as err: return Http404
msg = "Error in xqueue_callback %s: Invalid return format" % err header = json.loads(get['xqueue_header'])
raise Exception(msg) if not isinstance(header, dict) or not header.has_key('lms_key'):
return Http404
# Retrieve target StudentModule # Retrieve target StudentModule
user = User.objects.get(id=userid) user = User.objects.get(id=userid)
student_module_cache = StudentModuleCache(user, modulestore().get_item(id)) student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, modulestore().get_item(id))
instance, instance_module, shared_module, module_type = get_module(request.user, request, id, student_module_cache) instance = get_module(user, request, id, student_module_cache)
instance_module = get_instance_module(user, instance, student_module_cache)
if instance_module is None: if instance_module is None:
log.debug("Couldn't find module '%s' for user '%s'", log.debug("Couldn't find module '%s' for user '%s'", id, user)
id, request.user)
raise Http404 raise Http404
oldgrade = instance_module.grade oldgrade = instance_module.grade
...@@ -274,7 +324,7 @@ def xqueue_callback(request, userid, id, dispatch): ...@@ -274,7 +324,7 @@ def xqueue_callback(request, userid, id, dispatch):
return HttpResponse("") return HttpResponse("")
def modx_dispatch(request, dispatch=None, id=None): def modx_dispatch(request, dispatch=None, id=None, course_id=None):
''' Generic view for extensions. This is where AJAX calls go. ''' Generic view for extensions. This is where AJAX calls go.
Arguments: Arguments:
...@@ -286,15 +336,17 @@ def modx_dispatch(request, dispatch=None, id=None): ...@@ -286,15 +336,17 @@ def modx_dispatch(request, dispatch=None, id=None):
- id -- the module id. Used to look up the XModule instance - id -- the module id. Used to look up the XModule instance
''' '''
# ''' (fix emacs broken parsing) # ''' (fix emacs broken parsing)
# Check for submitted files # Check for submitted files
p = request.POST.copy() p = request.POST.copy()
if request.FILES: if request.FILES:
for inputfile_id in request.FILES.keys(): for inputfile_id in request.FILES.keys():
p[inputfile_id] = request.FILES[inputfile_id] p[inputfile_id] = request.FILES[inputfile_id]
student_module_cache = StudentModuleCache(request.user, modulestore().get_item(id)) student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, modulestore().get_item(id))
instance, instance_module, shared_module, module_type = get_module(request.user, request, id, student_module_cache) instance = get_module(request.user, request, id, student_module_cache)
instance_module = get_instance_module(request.user, instance, student_module_cache)
shared_module = get_shared_instance_module(request.user, instance, student_module_cache)
# Don't track state for anonymous users (who don't have student modules) # Don't track state for anonymous users (who don't have student modules)
if instance_module is not None: if instance_module is not None:
......
import copy import copy
import json import json
from path import path
import os import os
import sys
import time
from pprint import pprint
from nose import SkipTest from nose import SkipTest
from path import path
from pprint import pprint
from django.contrib.auth.models import User, Group
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.conf import settings from django.conf import settings
...@@ -13,11 +16,11 @@ from django.core.urlresolvers import reverse ...@@ -13,11 +16,11 @@ from django.core.urlresolvers import reverse
from mock import patch, Mock from mock import patch, Mock
from override_settings import override_settings from override_settings import override_settings
from django.contrib.auth.models import User import xmodule.modulestore.django
from student.models import Registration
from student.models import Registration
from courseware.courses import course_staff_group_name
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
...@@ -88,6 +91,13 @@ class ActivateLoginTestCase(TestCase): ...@@ -88,6 +91,13 @@ class ActivateLoginTestCase(TestCase):
self.assertTrue(data['success']) self.assertTrue(data['success'])
return resp return resp
def logout(self):
'''Logout, check that it worked.'''
resp = self.client.get(reverse('logout'), {})
# should redirect
self.assertEqual(resp.status_code, 302)
return resp
def _create_account(self, username, email, pw): def _create_account(self, username, email, pw):
'''Try to create an account. No error checking''' '''Try to create an account. No error checking'''
resp = self.client.post('/create_account', { resp = self.client.post('/create_account', {
...@@ -131,14 +141,33 @@ class ActivateLoginTestCase(TestCase): ...@@ -131,14 +141,33 @@ class ActivateLoginTestCase(TestCase):
'''The setup function does all the work''' '''The setup function does all the work'''
pass pass
def test_logout(self):
'''Setup function does login'''
self.logout()
class PageLoader(ActivateLoginTestCase): class PageLoader(ActivateLoginTestCase):
''' Base class that adds a function to load all pages in a modulestore ''' ''' Base class that adds a function to load all pages in a modulestore '''
def enroll(self, course):
"""Enroll the currently logged-in user, and check that it worked."""
resp = self.client.post('/change_enrollment', {
'enrollment_action': 'enroll',
'course_id': course.id,
})
data = parse_json(resp)
self.assertTrue(data['success'])
def check_pages_load(self, course_name, data_dir, modstore): def check_pages_load(self, course_name, data_dir, modstore):
print "Checking course {0} in {1}".format(course_name, data_dir) print "Checking course {0} in {1}".format(course_name, data_dir)
import_from_xml(modstore, data_dir, [course_name]) import_from_xml(modstore, data_dir, [course_name])
# enroll in the course before trying to access pages
courses = modstore.get_courses()
self.assertEqual(len(courses), 1)
course = courses[0]
self.enroll(course)
n = 0 n = 0
num_bad = 0 num_bad = 0
all_ok = True all_ok = True
...@@ -178,7 +207,222 @@ class TestCoursesLoadTestCase(PageLoader): ...@@ -178,7 +207,222 @@ class TestCoursesLoadTestCase(PageLoader):
self.check_pages_load('full', TEST_DATA_DIR, modulestore()) self.check_pages_load('full', TEST_DATA_DIR, modulestore())
# ========= TODO: check ajax interaction here too? @override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
class TestViewAuth(PageLoader):
"""Check that view authentication works properly"""
# NOTE: setUpClass() runs before override_settings takes effect, so
# can't do imports there without manually hacking settings.
def setUp(self):
print "sys.path: {}".format(sys.path)
xmodule.modulestore.django._MODULESTORES = {}
modulestore().collection.drop()
import_from_xml(modulestore(), TEST_DATA_DIR, ['toy'])
import_from_xml(modulestore(), TEST_DATA_DIR, ['full'])
courses = modulestore().get_courses()
# get the two courses sorted out
courses.sort(key=lambda c: c.location.course)
[self.full, self.toy] = courses
# Create two accounts
self.student = 'view@test.com'
self.instructor = 'view2@test.com'
self.password = 'foo'
self.create_account('u1', self.student, self.password)
self.create_account('u2', self.instructor, self.password)
self.activate_user(self.student)
self.activate_user(self.instructor)
def check_for_get_code(self, code, url):
resp = self.client.get(url)
# HACK: workaround the bug that returns 200 instead of 404.
# TODO (vshnayder): once we're returning 404s, get rid of this if.
if code != 404:
self.assertEqual(resp.status_code, code)
# And 'page not found' shouldn't be in the returned page
self.assertTrue(resp.content.lower().find('page not found') == -1)
else:
# look for "page not found" instead of the status code
#print resp.content
self.assertTrue(resp.content.lower().find('page not found') != -1)
def test_instructor_pages(self):
"""Make sure only instructors for the course or staff can load the instructor
dashboard, the grade views, and student profile pages"""
# First, try with an enrolled student
self.login(self.student, self.password)
# shouldn't work before enroll
self.check_for_get_code(302, reverse('courseware', kwargs={'course_id': self.toy.id}))
self.enroll(self.toy)
self.enroll(self.full)
# should work now
self.check_for_get_code(200, reverse('courseware', kwargs={'course_id': self.toy.id}))
def instructor_urls(course):
"list of urls that only instructors/staff should be able to see"
urls = [reverse(name, kwargs={'course_id': course.id}) for name in (
'instructor_dashboard',
'gradebook',
'grade_summary',)]
urls.append(reverse('student_profile', kwargs={'course_id': course.id,
'student_id': user(self.student).id}))
return urls
# shouldn't be able to get to the instructor pages
for url in instructor_urls(self.toy) + instructor_urls(self.full):
print 'checking for 404 on {}'.format(url)
self.check_for_get_code(404, url)
# Make the instructor staff in the toy course
group_name = course_staff_group_name(self.toy)
g = Group.objects.create(name=group_name)
g.user_set.add(user(self.instructor))
self.logout()
self.login(self.instructor, self.password)
# Now should be able to get to the toy course, but not the full course
for url in instructor_urls(self.toy):
print 'checking for 200 on {}'.format(url)
self.check_for_get_code(200, url)
for url in instructor_urls(self.full):
print 'checking for 404 on {}'.format(url)
self.check_for_get_code(404, url)
# now also make the instructor staff
u = user(self.instructor)
u.is_staff = True
u.save()
# and now should be able to load both
for url in instructor_urls(self.toy) + instructor_urls(self.full):
print 'checking for 200 on {}'.format(url)
self.check_for_get_code(200, url)
def test_dark_launch(self):
"""Make sure that when dark launch is on, students can't access course
pages, but instructors can"""
# test.py turns off start dates, enable them and set them correctly.
# Because settings is global, be careful not to mess it up for other tests
# (Can't use override_settings because we're only changing part of the
# MITX_FEATURES dict)
oldDSD = settings.MITX_FEATURES['DISABLE_START_DATES']
oldDL = settings.MITX_FEATURES['DARK_LAUNCH']
try:
settings.MITX_FEATURES['DISABLE_START_DATES'] = False
settings.MITX_FEATURES['DARK_LAUNCH'] = True
self._do_test_dark_launch()
finally:
settings.MITX_FEATURES['DISABLE_START_DATES'] = oldDSD
settings.MITX_FEATURES['DARK_LAUNCH'] = oldDL
def _do_test_dark_launch(self):
"""Actually do the test, relying on settings to be right."""
# Make courses start in the future
tomorrow = time.time() + 24*3600
self.toy.start = self.toy.metadata['start'] = time.gmtime(tomorrow)
self.full.start = self.full.metadata['start'] = time.gmtime(tomorrow)
self.assertFalse(self.toy.has_started())
self.assertFalse(self.full.has_started())
self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES'])
self.assertTrue(settings.MITX_FEATURES['DARK_LAUNCH'])
def reverse_urls(names, course):
return [reverse(name, kwargs={'course_id': course.id}) for name in names]
def dark_student_urls(course):
"""
list of urls that students should be able to see only
after launch, but staff should see before
"""
urls = reverse_urls(['info', 'book', 'courseware', 'profile'], course)
return urls
def light_student_urls(course):
"""
list of urls that students should be able to see before
launch.
"""
urls = reverse_urls(['about_course'], course)
urls.append(reverse('courses'))
# Need separate test for change_enrollment, since it's a POST view
#urls.append(reverse('change_enrollment'))
return urls
def instructor_urls(course):
"""list of urls that only instructors/staff should be able to see"""
urls = reverse_urls(['instructor_dashboard','gradebook','grade_summary'],
course)
urls.append(reverse('student_profile', kwargs={'course_id': course.id,
'student_id': user(self.student).id}))
return urls
def check_non_staff(course):
"""Check that access is right for non-staff in course"""
print '=== Checking non-staff access for {}'.format(course.id)
for url in instructor_urls(course) + dark_student_urls(course):
print 'checking for 404 on {}'.format(url)
self.check_for_get_code(404, url)
for url in light_student_urls(course):
print 'checking for 200 on {}'.format(url)
self.check_for_get_code(200, url)
def check_staff(course):
"""Check that access is right for staff in course"""
print '=== Checking staff access for {}'.format(course.id)
for url in (instructor_urls(course) +
dark_student_urls(course) +
light_student_urls(course)):
print 'checking for 200 on {}'.format(url)
self.check_for_get_code(200, url)
# First, try with an enrolled student
print '=== Testing student access....'
self.login(self.student, self.password)
self.enroll(self.toy)
self.enroll(self.full)
# shouldn't be able to get to anything except the light pages
check_non_staff(self.toy)
check_non_staff(self.full)
print '=== Testing course instructor access....'
# Make the instructor staff in the toy course
group_name = course_staff_group_name(self.toy)
g = Group.objects.create(name=group_name)
g.user_set.add(user(self.instructor))
self.logout()
self.login(self.instructor, self.password)
# Enroll in the classes---can't see courseware otherwise.
self.enroll(self.toy)
self.enroll(self.full)
# should now be able to get to everything for toy course
check_non_staff(self.full)
check_staff(self.toy)
print '=== Testing staff access....'
# now also make the instructor staff
u = user(self.instructor)
u.is_staff = True
u.save()
# and now should be able to load both
check_staff(self.toy)
check_staff(self.full)
@override_settings(MODULESTORE=REAL_DATA_MODULESTORE) @override_settings(MODULESTORE=REAL_DATA_MODULESTORE)
......
...@@ -27,15 +27,18 @@ from xmodule.course_module import CourseDescriptor ...@@ -27,15 +27,18 @@ from xmodule.course_module import CourseDescriptor
from util.cache import cache, cache_if_anonymous from util.cache import cache, cache_if_anonymous
from student.models import UserTestGroup, CourseEnrollment from student.models import UserTestGroup, CourseEnrollment
from courseware import grades from courseware import grades
from courseware.courses import check_course, get_courses_by_university from courseware.courses import (check_course, get_courses_by_university,
has_staff_access_to_course_id)
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
template_imports = {'urllib': urllib} template_imports = {'urllib': urllib}
def user_groups(user): def user_groups(user):
"""
TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately.
"""
if not user.is_authenticated(): if not user.is_authenticated():
return [] return []
...@@ -45,6 +48,8 @@ def user_groups(user): ...@@ -45,6 +48,8 @@ def user_groups(user):
# Kill caching on dev machines -- we switch groups a lot # Kill caching on dev machines -- we switch groups a lot
group_names = cache.get(key) group_names = cache.get(key)
if settings.DEBUG:
group_names = None
if group_names is None: if group_names is None:
group_names = [u.name for u in UserTestGroup.objects.filter(users=user)] group_names = [u.name for u in UserTestGroup.objects.filter(users=user)]
...@@ -63,60 +68,6 @@ def courses(request): ...@@ -63,60 +68,6 @@ def courses(request):
universities = get_courses_by_university(request.user) universities = get_courses_by_university(request.user)
return render_to_response("courses.html", {'universities': universities}) return render_to_response("courses.html", {'universities': universities})
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def gradebook(request, course_id):
if 'course_admin' not in user_groups(request.user):
raise Http404
course = check_course(course_id)
student_objects = User.objects.all()[:100]
student_info = []
for student in student_objects:
student_module_cache = StudentModuleCache(student, course)
course, _, _, _ = get_module(request.user, request, course.location, student_module_cache)
student_info.append({
'username': student.username,
'id': student.id,
'email': student.email,
'grade_info': grades.grade_sheet(student, course, student_module_cache),
'realname': UserProfile.objects.get(user=student).name
})
return render_to_response('gradebook.html', {'students': student_info, 'course': course})
@login_required
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def profile(request, course_id, student_id=None):
''' User profile. Show username, location, etc, as well as grades .
We need to allow the user to change some of these settings .'''
course = check_course(course_id)
if student_id is None:
student = request.user
else:
if 'course_admin' not in user_groups(request.user):
raise Http404
student = User.objects.get(id=int(student_id))
user_info = UserProfile.objects.get(user=student)
student_module_cache = StudentModuleCache(request.user, course)
course_module, _, _, _ = get_module(request.user, request, course.location, student_module_cache)
context = {'name': user_info.name,
'username': student.username,
'location': user_info.location,
'language': user_info.language,
'email': student.email,
'course': course,
'csrf': csrf(request)['csrf_token']
}
context.update(grades.grade_sheet(student, course_module, course.grader, student_module_cache))
return render_to_response('profile.html', context)
def render_accordion(request, course, chapter, section): def render_accordion(request, course, chapter, section):
''' Draws navigation bar. Takes current position in accordion as ''' Draws navigation bar. Takes current position in accordion as
...@@ -124,19 +75,14 @@ def render_accordion(request, course, chapter, section): ...@@ -124,19 +75,14 @@ def render_accordion(request, course, chapter, section):
If chapter and section are '' or None, renders a default accordion. If chapter and section are '' or None, renders a default accordion.
course, chapter, and section are the url_names.
Returns the html string''' Returns the html string'''
# grab the table of contents # grab the table of contents
toc = toc_for_course(request.user, request, course, chapter, section) toc = toc_for_course(request.user, request, course, chapter, section)
active_chapter = 1 context = dict([('toc', toc),
for i in range(len(toc)):
if toc[i]['active']:
active_chapter = i
context = dict([('active_chapter', active_chapter),
('toc', toc),
('course_name', course.title),
('course_id', course.id), ('course_id', course.id),
('csrf', csrf(request)['csrf_token'])] + template_imports.items()) ('csrf', csrf(request)['csrf_token'])] + template_imports.items())
return render_to_string('accordion.html', context) return render_to_string('accordion.html', context)
...@@ -164,9 +110,10 @@ def index(request, course_id, chapter=None, section=None, ...@@ -164,9 +110,10 @@ def index(request, course_id, chapter=None, section=None,
- HTTPresponse - HTTPresponse
''' '''
course = check_course(course_id) course = check_course(request.user, course_id)
registered = registered_for_course(course, request.user) registered = registered_for_course(course, request.user)
if not registered: if not registered:
# TODO (vshnayder): do course instructors need to be registered to see course?
log.debug('User %s tried to view course %s but is not enrolled' % (request.user,course.location.url())) log.debug('User %s tried to view course %s but is not enrolled' % (request.user,course.location.url()))
return redirect(reverse('about_course', args=[course.id])) return redirect(reverse('about_course', args=[course.id]))
...@@ -184,11 +131,12 @@ def index(request, course_id, chapter=None, section=None, ...@@ -184,11 +131,12 @@ def index(request, course_id, chapter=None, section=None,
if look_for_module: if look_for_module:
section_descriptor = get_section(course, chapter, section) section_descriptor = get_section(course, chapter, section)
if section_descriptor is not None: if section_descriptor is not None:
student_module_cache = StudentModuleCache(request.user, student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
request.user,
section_descriptor) section_descriptor)
module, _, _, _ = get_module(request.user, request, module = get_module(request.user, request,
section_descriptor.location, section_descriptor.location,
student_module_cache) student_module_cache)
context['content'] = module.get_html() context['content'] = module.get_html()
else: else:
log.warning("Couldn't find a section descriptor for course_id '{0}'," log.warning("Couldn't find a section descriptor for course_id '{0}',"
...@@ -227,12 +175,10 @@ def jump_to(request, location): ...@@ -227,12 +175,10 @@ def jump_to(request, location):
''' '''
Show the page that contains a specific location. Show the page that contains a specific location.
If the location is invalid, return a 404. If the location is invalid or not in any class, return a 404.
If the location is valid, but not present in a course, ? Otherwise, delegates to the index view to figure out whether this user
has access, and what they should see.
If the location is valid, but in a course the current user isn't registered for, ?
TODO -- let the index view deal with it?
''' '''
# Complain if the location isn't valid # Complain if the location isn't valid
try: try:
...@@ -248,17 +194,17 @@ def jump_to(request, location): ...@@ -248,17 +194,17 @@ def jump_to(request, location):
except NoPathToItem: except NoPathToItem:
raise Http404("This location is not in any class: {0}".format(location)) raise Http404("This location is not in any class: {0}".format(location))
# Rely on index to do all error handling # Rely on index to do all error handling and access control.
return index(request, course_id, chapter, section, position) return index(request, course_id, chapter, section, position)
@ensure_csrf_cookie @ensure_csrf_cookie
def course_info(request, course_id): def course_info(request, course_id):
''' """
Display the course's info.html, or 404 if there is no such course. Display the course's info.html, or 404 if there is no such course.
Assumes the course_id is in a valid format. Assumes the course_id is in a valid format.
''' """
course = check_course(course_id) course = check_course(request.user, course_id)
return render_to_response('info.html', {'course': course}) return render_to_response('info.html', {'course': course})
...@@ -275,7 +221,7 @@ def registered_for_course(course, user): ...@@ -275,7 +221,7 @@ def registered_for_course(course, user):
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_if_anonymous @cache_if_anonymous
def course_about(request, course_id): def course_about(request, course_id):
course = check_course(course_id, course_must_be_open=False) course = check_course(request.user, course_id, course_must_be_open=False)
registered = registered_for_course(course, request.user) registered = registered_for_course(course, request.user)
return render_to_response('portal/course_about.html', {'course': course, 'registered': registered}) return render_to_response('portal/course_about.html', {'course': course, 'registered': registered})
...@@ -283,7 +229,10 @@ def course_about(request, course_id): ...@@ -283,7 +229,10 @@ def course_about(request, course_id):
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_if_anonymous @cache_if_anonymous
def university_profile(request, org_id): def university_profile(request, org_id):
all_courses = sorted(modulestore().get_courses(), key=lambda course: course.number) """
Return the profile for the particular org_id. 404 if it's not valid.
"""
all_courses = modulestore().get_courses()
valid_org_ids = set(c.org for c in all_courses) valid_org_ids = set(c.org for c in all_courses)
if org_id not in valid_org_ids: if org_id not in valid_org_ids:
raise Http404("University Profile not found for {0}".format(org_id)) raise Http404("University Profile not found for {0}".format(org_id))
...@@ -294,3 +243,104 @@ def university_profile(request, org_id): ...@@ -294,3 +243,104 @@ def university_profile(request, org_id):
template_file = "university_profile/{0}.html".format(org_id).lower() template_file = "university_profile/{0}.html".format(org_id).lower()
return render_to_response(template_file, context) return render_to_response(template_file, context)
@login_required
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def profile(request, course_id, student_id=None):
""" User profile. Show username, location, etc, as well as grades .
We need to allow the user to change some of these settings.
Course staff are allowed to see the profiles of students in their class.
"""
course = check_course(request.user, course_id)
if student_id is None or student_id == request.user.id:
# always allowed to see your own profile
student = request.user
else:
# Requesting access to a different student's profile
if not has_staff_access_to_course_id(request.user, course_id):
raise Http404
student = User.objects.get(id=int(student_id))
user_info = UserProfile.objects.get(user=student)
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, course)
course_module = get_module(request.user, request, course.location, student_module_cache)
courseware_summary = grades.progress_summary(student, course_module, course.grader, student_module_cache)
grade_summary = grades.grade(request.user, request, course, student_module_cache)
context = {'name': user_info.name,
'username': student.username,
'location': user_info.location,
'language': user_info.language,
'email': student.email,
'course': course,
'csrf': csrf(request)['csrf_token'],
'courseware_summary' : courseware_summary,
'grade_summary' : grade_summary
}
context.update()
return render_to_response('profile.html', context)
# ======== Instructor views =============================================================================
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def gradebook(request, course_id):
"""
Show the gradebook for this course:
- only displayed to course staff
- shows students who are enrolled.
"""
if not has_staff_access_to_course_id(request.user, course_id):
raise Http404
course = check_course(request.user, course_id)
enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).order_by('username')
# TODO (vshnayder): implement pagination.
enrolled_students = enrolled_students[:1000] # HACK!
student_info = [{'username': student.username,
'id': student.id,
'email': student.email,
'grade_summary': grades.grade(student, request, course),
'realname': UserProfile.objects.get(user=student).name
}
for student in enrolled_students]
return render_to_response('gradebook.html', {'students': student_info,
'course': course, 'course_id': course_id})
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def grade_summary(request, course_id):
"""Display the grade summary for a course."""
if not has_staff_access_to_course_id(request.user, course_id):
raise Http404
course = check_course(request.user, course_id)
# For now, just a static page
context = {'course': course }
return render_to_response('grade_summary.html', context)
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def instructor_dashboard(request, course_id):
"""Display the instructor dashboard for a course."""
if not has_staff_access_to_course_id(request.user, course_id):
raise Http404
course = check_course(request.user, course_id)
# For now, just a static page
context = {'course': course }
return render_to_response('instructor_dashboard.html', context)
from django.conf.urls.defaults import * from django.conf.urls import *
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^$', 'heartbeat.views.heartbeat', name='heartbeat'), url(r'^$', 'heartbeat.views.heartbeat', name='heartbeat'),
......
from django.conf.urls.defaults import patterns, url from django.conf.urls import patterns, url
namespace_regex = r"[a-zA-Z\d._-]+" namespace_regex = r"[a-zA-Z\d._-]+"
article_slug = r'/(?P<article_path>' + namespace_regex + r'/[a-zA-Z\d_-]*)' article_slug = r'/(?P<article_path>' + namespace_regex + r'/[a-zA-Z\d_-]*)'
......
...@@ -51,7 +51,7 @@ def update_template_dictionary(dictionary, request=None, course=None, article=No ...@@ -51,7 +51,7 @@ def update_template_dictionary(dictionary, request=None, course=None, article=No
def view(request, article_path, course_id=None): def view(request, article_path, course_id=None):
course = check_course(course_id, course_required=False) course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, article_path, course) (article, err) = get_article(request, article_path, course)
if err: if err:
...@@ -67,7 +67,7 @@ def view(request, article_path, course_id=None): ...@@ -67,7 +67,7 @@ def view(request, article_path, course_id=None):
def view_revision(request, revision_number, article_path, course_id=None): def view_revision(request, revision_number, article_path, course_id=None):
course = check_course(course_id, course_required=False) course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, article_path, course) (article, err) = get_article(request, article_path, course)
if err: if err:
...@@ -91,7 +91,7 @@ def view_revision(request, revision_number, article_path, course_id=None): ...@@ -91,7 +91,7 @@ def view_revision(request, revision_number, article_path, course_id=None):
def root_redirect(request, course_id=None): def root_redirect(request, course_id=None):
course = check_course(course_id, course_required=False) course = check_course(request.user, course_id, course_required=False)
#TODO: Add a default namespace to settings. #TODO: Add a default namespace to settings.
namespace = course.wiki_namespace if course else "edX" namespace = course.wiki_namespace if course else "edX"
...@@ -109,7 +109,7 @@ def root_redirect(request, course_id=None): ...@@ -109,7 +109,7 @@ def root_redirect(request, course_id=None):
def create(request, article_path, course_id=None): def create(request, article_path, course_id=None):
course = check_course(course_id, course_required=False) course = check_course(request.user, course_id, course_required=False)
article_path_components = article_path.split('/') article_path_components = article_path.split('/')
...@@ -170,7 +170,7 @@ def create(request, article_path, course_id=None): ...@@ -170,7 +170,7 @@ def create(request, article_path, course_id=None):
def edit(request, article_path, course_id=None): def edit(request, article_path, course_id=None):
course = check_course(course_id, course_required=False) course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, article_path, course) (article, err) = get_article(request, article_path, course)
if err: if err:
...@@ -218,7 +218,7 @@ def edit(request, article_path, course_id=None): ...@@ -218,7 +218,7 @@ def edit(request, article_path, course_id=None):
def history(request, article_path, page=1, course_id=None): def history(request, article_path, page=1, course_id=None):
course = check_course(course_id, course_required=False) course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, article_path, course) (article, err) = get_article(request, article_path, course)
if err: if err:
...@@ -300,7 +300,7 @@ def history(request, article_path, page=1, course_id=None): ...@@ -300,7 +300,7 @@ def history(request, article_path, page=1, course_id=None):
def revision_feed(request, page=1, namespace=None, course_id=None): def revision_feed(request, page=1, namespace=None, course_id=None):
course = check_course(course_id, course_required=False) course = check_course(request.user, course_id, course_required=False)
page_size = 10 page_size = 10
...@@ -333,7 +333,7 @@ def revision_feed(request, page=1, namespace=None, course_id=None): ...@@ -333,7 +333,7 @@ def revision_feed(request, page=1, namespace=None, course_id=None):
def search_articles(request, namespace=None, course_id=None): def search_articles(request, namespace=None, course_id=None):
course = check_course(course_id, course_required=False) course = check_course(request.user, course_id, course_required=False)
# blampe: We should check for the presence of other popular django search # blampe: We should check for the presence of other popular django search
# apps and use those if possible. Only fall back on this as a last resort. # apps and use those if possible. Only fall back on this as a last resort.
...@@ -382,7 +382,7 @@ def search_articles(request, namespace=None, course_id=None): ...@@ -382,7 +382,7 @@ def search_articles(request, namespace=None, course_id=None):
def search_add_related(request, course_id, slug, namespace): def search_add_related(request, course_id, slug, namespace):
course = check_course(course_id, course_required=False) course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, slug, namespace if namespace else course_id) (article, err) = get_article(request, slug, namespace if namespace else course_id)
if err: if err:
...@@ -415,7 +415,7 @@ def search_add_related(request, course_id, slug, namespace): ...@@ -415,7 +415,7 @@ def search_add_related(request, course_id, slug, namespace):
def add_related(request, course_id, slug, namespace): def add_related(request, course_id, slug, namespace):
course = check_course(course_id, course_required=False) course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, slug, namespace if namespace else course_id) (article, err) = get_article(request, slug, namespace if namespace else course_id)
if err: if err:
...@@ -439,7 +439,7 @@ def add_related(request, course_id, slug, namespace): ...@@ -439,7 +439,7 @@ def add_related(request, course_id, slug, namespace):
def remove_related(request, course_id, namespace, slug, related_id): def remove_related(request, course_id, namespace, slug, related_id):
course = check_course(course_id, course_required=False) course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, slug, namespace if namespace else course_id) (article, err) = get_article(request, slug, namespace if namespace else course_id)
...@@ -462,7 +462,7 @@ def remove_related(request, course_id, namespace, slug, related_id): ...@@ -462,7 +462,7 @@ def remove_related(request, course_id, namespace, slug, related_id):
def random_article(request, course_id=None): def random_article(request, course_id=None):
course = check_course(course_id, course_required=False) course = check_course(request.user, course_id, course_required=False)
from random import randint from random import randint
num_arts = Article.objects.count() num_arts = Article.objects.count()
......
...@@ -2,12 +2,14 @@ from django.contrib.auth.decorators import login_required ...@@ -2,12 +2,14 @@ from django.contrib.auth.decorators import login_required
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from courseware.courses import check_course from courseware.courses import check_course
from lxml import etree
@login_required @login_required
def index(request, course_id, page=0): def index(request, course_id, page=0):
course = check_course(course_id) course = check_course(request.user, course_id)
return render_to_response('staticbook.html', {'page': int(page), 'course': course}) raw_table_of_contents = open('lms/templates/book_toc.xml', 'r') # TODO: This will need to come from S3
table_of_contents = etree.parse(raw_table_of_contents).getroot()
return render_to_response('staticbook.html', {'page': int(page), 'course': course, 'table_of_contents': table_of_contents})
def index_shifted(request, course_id, page): def index_shifted(request, course_id, page):
......
...@@ -48,6 +48,7 @@ MITX_FEATURES = { ...@@ -48,6 +48,7 @@ MITX_FEATURES = {
## DO NOT SET TO True IN THIS FILE ## DO NOT SET TO True IN THIS FILE
## Doing so will cause all courses to be released on production ## Doing so will cause all courses to be released on production
'DISABLE_START_DATES': False, # When True, all courses will be active, regardless of start date 'DISABLE_START_DATES': False, # When True, all courses will be active, regardless of start date
'DARK_LAUNCH': False, # When True, courses will be active for staff only
'ENABLE_TEXTBOOK' : True, 'ENABLE_TEXTBOOK' : True,
'ENABLE_DISCUSSION' : True, 'ENABLE_DISCUSSION' : True,
...@@ -55,6 +56,8 @@ MITX_FEATURES = { ...@@ -55,6 +56,8 @@ MITX_FEATURES = {
'ENABLE_SQL_TRACKING_LOGS': False, 'ENABLE_SQL_TRACKING_LOGS': False,
'ENABLE_LMS_MIGRATION': False, 'ENABLE_LMS_MIGRATION': False,
'DISABLE_LOGIN_BUTTON': False, # used in systems where login is automatic, eg MIT SSL
# extrernal access methods # extrernal access methods
'ACCESS_REQUIRE_STAFF_FOR_COURSE': False, 'ACCESS_REQUIRE_STAFF_FOR_COURSE': False,
'AUTH_USE_OPENID': False, 'AUTH_USE_OPENID': False,
...@@ -121,7 +124,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( ...@@ -121,7 +124,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
#'django.core.context_processors.i18n', #'django.core.context_processors.i18n',
'askbot.user_messages.context_processors.user_messages',#must be before auth 'askbot.user_messages.context_processors.user_messages',#must be before auth
'django.core.context_processors.auth', #this is required for admin 'django.contrib.auth.context_processors.auth', #this is required for admin
'django.core.context_processors.csrf', #necessary for csrf protection 'django.core.context_processors.csrf', #necessary for csrf protection
) )
...@@ -169,6 +172,9 @@ COURSE_SETTINGS = {'6.002x_Fall_2012': {'number' : '6.002x', ...@@ -169,6 +172,9 @@ COURSE_SETTINGS = {'6.002x_Fall_2012': {'number' : '6.002x',
} }
} }
# IP addresses that are allowed to reload the course, etc.
# TODO (vshnayder): Will probably need to change as we get real access control in.
LMS_MIGRATION_ALLOWED_IPS = []
############################### XModule Store ################################## ############################### XModule Store ##################################
MODULESTORE = { MODULESTORE = {
...@@ -182,6 +188,9 @@ MODULESTORE = { ...@@ -182,6 +188,9 @@ MODULESTORE = {
} }
} }
############################ SIGNAL HANDLERS ################################
# This is imported to register the exception signal handling that logs exceptions
import monitoring.exceptions # noqa
############################### DJANGO BUILT-INS ############################### ############################### DJANGO BUILT-INS ###############################
# Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here # Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here
...@@ -294,7 +303,6 @@ TEMPLATE_LOADERS = ( ...@@ -294,7 +303,6 @@ TEMPLATE_LOADERS = (
) )
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = (
'util.middleware.ExceptionLoggingMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
......
...@@ -62,6 +62,7 @@ SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' ...@@ -62,6 +62,7 @@ SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
################################ LMS Migration ################################# ################################ LMS Migration #################################
MITX_FEATURES['ENABLE_LMS_MIGRATION'] = True MITX_FEATURES['ENABLE_LMS_MIGRATION'] = True
MITX_FEATURES['ACCESS_REQUIRE_STAFF_FOR_COURSE'] = False # require that user be in the staff_* group to be able to enroll MITX_FEATURES['ACCESS_REQUIRE_STAFF_FOR_COURSE'] = False # require that user be in the staff_* group to be able to enroll
MITX_FEATURES['USE_XQA_SERVER'] = 'http://xqa:server@content-qa.mitx.mit.edu/xqa'
LMS_MIGRATION_ALLOWED_IPS = ['127.0.0.1'] LMS_MIGRATION_ALLOWED_IPS = ['127.0.0.1']
......
...@@ -10,9 +10,27 @@ sessions. Assumes structure: ...@@ -10,9 +10,27 @@ sessions. Assumes structure:
from .common import * from .common import *
from .logsettings import get_logger_config from .logsettings import get_logger_config
from .dev import * from .dev import *
import socket
WIKI_ENABLED = False WIKI_ENABLED = False
MITX_FEATURES['ENABLE_TEXTBOOK'] = False MITX_FEATURES['ENABLE_TEXTBOOK'] = False
MITX_FEATURES['ENABLE_DISCUSSION'] = False MITX_FEATURES['ENABLE_DISCUSSION'] = False
MITX_FEATURES['ACCESS_REQUIRE_STAFF_FOR_COURSE'] = True # require that user be in the staff_* group to be able to enroll MITX_FEATURES['ACCESS_REQUIRE_STAFF_FOR_COURSE'] = True # require that user be in the staff_* group to be able to enroll
myhost = socket.gethostname()
if ('edxvm' in myhost) or ('ocw' in myhost):
MITX_FEATURES['DISABLE_LOGIN_BUTTON'] = True # auto-login with MIT certificate
MITX_FEATURES['USE_XQA_SERVER'] = 'https://qisx.mit.edu/xqa' # needs to be ssl or browser blocks it
if ('domU' in myhost):
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
MITX_FEATURES['REROUTE_ACTIVATION_EMAIL'] = 'ichuang@mitx.mit.edu' # nonempty string = address for all activation emails
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') # django 1.4 for nginx ssl proxy
#-----------------------------------------------------------------------------
# disable django debug toolbars
INSTALLED_APPS = tuple([ app for app in INSTALLED_APPS if not app.startswith('debug_toolbar') ])
MIDDLEWARE_CLASSES = tuple([ mcl for mcl in MIDDLEWARE_CLASSES if not mcl.startswith('debug_toolbar') ])
TEMPLATE_LOADERS = tuple([ app for app in TEMPLATE_LOADERS if not app.startswith('askbot') ])
from ..dev import *
CLASSES_TO_DBS = {
'BerkeleyX/CS169.1x/2012_Fall' : "cs169.db",
'BerkeleyX/CS188.1x/2012_Fall' : "cs188_1.db",
'HarvardX/CS50x/2012' : "cs50.db",
'HarvardX/PH207x/2012_Fall' : "ph207.db",
'MITx/3.091x/2012_Fall' : "3091.db",
'MITx/6.002x/2012_Fall' : "6002.db",
'MITx/6.00x/2012_Fall' : "600.db",
}
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': '127.0.0.1:11211',
'KEY_FUNCTION': 'util.memcache.safe_key',
},
'general': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': '127.0.0.1:11211',
'KEY_PREFIX' : 'general',
'VERSION' : 5,
'KEY_FUNCTION': 'util.memcache.safe_key',
}
}
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
def path_for_db(db_name):
return ENV_ROOT / "db" / db_name
def course_db_for(course_id):
db_name = CLASSES_TO_DBS[course_id]
return {
'default' : {
'ENGINE' : 'django.db.backends.sqlite3',
'NAME' : path_for_db(db_name)
}
}
from .courses import *
DATABASES = course_db_for('HarvardX/CS50x/2012')
\ No newline at end of file
from .courses import *
DATABASES = course_db_for('MITx/6.002x/2012_Fall')
\ No newline at end of file
"""
Note that for this to work at all, you must have memcached running (or you won't
get shared sessions)
"""
from courses import *
# Move this to a shared file later:
for class_id, db_name in CLASSES_TO_DBS.items():
DATABASES[class_id] = {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': path_for_db(db_name)
}
...@@ -25,6 +25,7 @@ SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead ...@@ -25,6 +25,7 @@ SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
# Nose Test Runner # Nose Test Runner
INSTALLED_APPS += ('django_nose',) INSTALLED_APPS += ('django_nose',)
NOSE_ARGS = ['--cover-erase', '--with-xunit', '--with-xcoverage', '--cover-html', NOSE_ARGS = ['--cover-erase', '--with-xunit', '--with-xcoverage', '--cover-html',
# '-v', '--pdb', # When really stuck, uncomment to start debugger on error
'--cover-inclusive', '--cover-html-dir', '--cover-inclusive', '--cover-html-dir',
os.environ.get('NOSE_COVER_HTML_DIR', 'cover_html')] os.environ.get('NOSE_COVER_HTML_DIR', 'cover_html')]
for app in os.listdir(PROJECT_ROOT / 'djangoapps'): for app in os.listdir(PROJECT_ROOT / 'djangoapps'):
...@@ -66,6 +67,17 @@ DATABASES = { ...@@ -66,6 +67,17 @@ DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': PROJECT_ROOT / "db" / "mitx.db", 'NAME': PROJECT_ROOT / "db" / "mitx.db",
},
# The following are for testing purposes...
'edX/toy/2012_Fall': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "course1.db",
},
'edx/full/6.002_Spring_2012': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "course2.db",
} }
} }
......
...@@ -189,6 +189,10 @@ p.mini { ...@@ -189,6 +189,10 @@ p.mini {
color: #999; color: #999;
} }
img.help-tooltip {
cursor: help;
}
p img, h1 img, h2 img, h3 img, h4 img, td img { p img, h1 img, h2 img, h3 img, h4 img, td img {
vertical-align: middle; vertical-align: middle;
} }
...@@ -259,7 +263,7 @@ tfoot td { ...@@ -259,7 +263,7 @@ tfoot td {
color: #666; color: #666;
padding: 2px 5px; padding: 2px 5px;
font-size: 11px; font-size: 11px;
background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x; background: #e1e1e1 url(../img/nav-bg.gif) top left repeat-x;
border-left: 1px solid #ddd; border-left: 1px solid #ddd;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
} }
...@@ -305,25 +309,84 @@ tr.alt { ...@@ -305,25 +309,84 @@ tr.alt {
/* SORTABLE TABLES */ /* SORTABLE TABLES */
thead th {
padding: 2px 5px;
line-height: normal;
}
thead th a:link, thead th a:visited { thead th a:link, thead th a:visited {
color: #666; color: #666;
}
thead th.sorted {
background: #c5c5c5 url(../img/nav-bg-selected.gif) top left repeat-x;
}
table thead th .text span {
padding: 2px 5px;
display:block;
}
table thead th .text a {
display: block;
cursor: pointer;
padding: 2px 5px;
}
table thead th.sortable:hover {
background: white url(../img/nav-bg-reverse.gif) 0 -5px repeat-x;
}
thead th.sorted a.sortremove {
visibility: hidden;
}
table thead th.sorted:hover a.sortremove {
visibility: visible;
}
table thead th.sorted .sortoptions {
display: block; display: block;
padding: 4px 5px 0 5px;
float: right;
text-align: right;
}
table thead th.sorted .sortpriority {
font-size: .8em;
min-width: 12px;
text-align: center;
vertical-align: top;
}
table thead th.sorted .sortoptions a {
width: 14px;
height: 12px;
display: inline-block;
}
table thead th.sorted .sortoptions a.sortremove {
background: url(../img/sorting-icons.gif) -4px -5px no-repeat;
}
table thead th.sorted .sortoptions a.sortremove:hover {
background: url(../img/sorting-icons.gif) -4px -27px no-repeat;
} }
table thead th.sorted { table thead th.sorted .sortoptions a.ascending {
background-position: bottom left !important; background: url(../img/sorting-icons.gif) -5px -50px no-repeat;
} }
table thead th.sorted a { table thead th.sorted .sortoptions a.ascending:hover {
padding-right: 13px; background: url(../img/sorting-icons.gif) -5px -72px no-repeat;
} }
table thead th.ascending a { table thead th.sorted .sortoptions a.descending {
background: url(../img/admin/arrow-up.gif) right .4em no-repeat; background: url(../img/sorting-icons.gif) -5px -94px no-repeat;
} }
table thead th.descending a { table thead th.sorted .sortoptions a.descending:hover {
background: url(../img/admin/arrow-down.gif) right .4em no-repeat; background: url(../img/sorting-icons.gif) -5px -115px no-repeat;
} }
/* ORDERABLE TABLES */ /* ORDERABLE TABLES */
...@@ -334,7 +397,7 @@ table.orderable tbody tr td:hover { ...@@ -334,7 +397,7 @@ table.orderable tbody tr td:hover {
table.orderable tbody tr td:first-child { table.orderable tbody tr td:first-child {
padding-left: 14px; padding-left: 14px;
background-image: url(../img/admin/nav-bg-grabber.gif); background-image: url(../img/nav-bg-grabber.gif);
background-repeat: repeat-y; background-repeat: repeat-y;
} }
...@@ -364,7 +427,7 @@ input[type=text], input[type=password], textarea, select, .vTextField { ...@@ -364,7 +427,7 @@ input[type=text], input[type=password], textarea, select, .vTextField {
/* FORM BUTTONS */ /* FORM BUTTONS */
.button, input[type=submit], input[type=button], .submit-row input { .button, input[type=submit], input[type=button], .submit-row input {
background: white url(../img/admin/nav-bg.gif) bottom repeat-x; background: white url(../img/nav-bg.gif) bottom repeat-x;
padding: 3px 5px; padding: 3px 5px;
color: black; color: black;
border: 1px solid #bbb; border: 1px solid #bbb;
...@@ -372,31 +435,31 @@ input[type=text], input[type=password], textarea, select, .vTextField { ...@@ -372,31 +435,31 @@ input[type=text], input[type=password], textarea, select, .vTextField {
} }
.button:active, input[type=submit]:active, input[type=button]:active { .button:active, input[type=submit]:active, input[type=button]:active {
background-image: url(../img/admin/nav-bg-reverse.gif); background-image: url(../img/nav-bg-reverse.gif);
background-position: top; background-position: top;
} }
.button[disabled], input[type=submit][disabled], input[type=button][disabled] { .button[disabled], input[type=submit][disabled], input[type=button][disabled] {
background-image: url(../img/admin/nav-bg.gif); background-image: url(../img/nav-bg.gif);
background-position: bottom; background-position: bottom;
opacity: 0.4; opacity: 0.4;
} }
.button.default, input[type=submit].default, .submit-row input.default { .button.default, input[type=submit].default, .submit-row input.default {
border: 2px solid #5b80b2; border: 2px solid #5b80b2;
background: #7CA0C7 url(../img/admin/default-bg.gif) bottom repeat-x; background: #7CA0C7 url(../img/default-bg.gif) bottom repeat-x;
font-weight: bold; font-weight: bold;
color: white; color: white;
float: right; float: right;
} }
.button.default:active, input[type=submit].default:active { .button.default:active, input[type=submit].default:active {
background-image: url(../img/admin/default-bg-reverse.gif); background-image: url(../img/default-bg-reverse.gif);
background-position: top; background-position: top;
} }
.button[disabled].default, input[type=submit][disabled].default, input[type=button][disabled].default { .button[disabled].default, input[type=submit][disabled].default, input[type=button][disabled].default {
background-image: url(../img/admin/default-bg.gif); background-image: url(../img/default-bg.gif);
background-position: bottom; background-position: bottom;
opacity: 0.4; opacity: 0.4;
} }
...@@ -433,7 +496,7 @@ input[type=text], input[type=password], textarea, select, .vTextField { ...@@ -433,7 +496,7 @@ input[type=text], input[type=password], textarea, select, .vTextField {
font-size: 11px; font-size: 11px;
text-align: left; text-align: left;
font-weight: bold; font-weight: bold;
background: #7CA0C7 url(../img/admin/default-bg.gif) top left repeat-x; background: #7CA0C7 url(../img/default-bg.gif) top left repeat-x;
color: white; color: white;
} }
...@@ -455,15 +518,15 @@ ul.messagelist li { ...@@ -455,15 +518,15 @@ ul.messagelist li {
margin: 0 0 3px 0; margin: 0 0 3px 0;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
color: #666; color: #666;
background: #ffc url(../img/admin/icon_success.gif) 5px .3em no-repeat; background: #ffc url(../img/icon_success.gif) 5px .3em no-repeat;
} }
ul.messagelist li.warning{ ul.messagelist li.warning{
background-image: url(../img/admin/icon_alert.gif); background-image: url(../img/icon_alert.gif);
} }
ul.messagelist li.error{ ul.messagelist li.error{
background-image: url(../img/admin/icon_error.gif); background-image: url(../img/icon_error.gif);
} }
.errornote { .errornote {
...@@ -473,7 +536,7 @@ ul.messagelist li.error{ ...@@ -473,7 +536,7 @@ ul.messagelist li.error{
margin: 0 0 3px 0; margin: 0 0 3px 0;
border: 1px solid red; border: 1px solid red;
color: red; color: red;
background: #ffc url(../img/admin/icon_error.gif) 5px .3em no-repeat; background: #ffc url(../img/icon_error.gif) 5px .3em no-repeat;
} }
ul.errorlist { ul.errorlist {
...@@ -488,7 +551,7 @@ ul.errorlist { ...@@ -488,7 +551,7 @@ ul.errorlist {
margin: 0 0 3px 0; margin: 0 0 3px 0;
border: 1px solid red; border: 1px solid red;
color: white; color: white;
background: red url(../img/admin/icon_alert.gif) 5px .3em no-repeat; background: red url(../img/icon_alert.gif) 5px .3em no-repeat;
} }
.errorlist li a { .errorlist li a {
...@@ -524,7 +587,7 @@ div.system-message p.system-message-title { ...@@ -524,7 +587,7 @@ div.system-message p.system-message-title {
padding: 4px 5px 4px 25px; padding: 4px 5px 4px 25px;
margin: 0; margin: 0;
color: red; color: red;
background: #ffc url(../img/admin/icon_error.gif) 5px .3em no-repeat; background: #ffc url(../img/icon_error.gif) 5px .3em no-repeat;
} }
.description { .description {
...@@ -535,7 +598,7 @@ div.system-message p.system-message-title { ...@@ -535,7 +598,7 @@ div.system-message p.system-message-title {
/* BREADCRUMBS */ /* BREADCRUMBS */
div.breadcrumbs { div.breadcrumbs {
background: white url(../img/admin/nav-bg-reverse.gif) 0 -10px repeat-x; background: white url(../img/nav-bg-reverse.gif) 0 -10px repeat-x;
padding: 2px 8px 3px 8px; padding: 2px 8px 3px 8px;
font-size: 11px; font-size: 11px;
color: #999; color: #999;
...@@ -548,17 +611,17 @@ div.breadcrumbs { ...@@ -548,17 +611,17 @@ div.breadcrumbs {
.addlink { .addlink {
padding-left: 12px; padding-left: 12px;
background: url(../img/admin/icon_addlink.gif) 0 .2em no-repeat; background: url(../img/icon_addlink.gif) 0 .2em no-repeat;
} }
.changelink { .changelink {
padding-left: 12px; padding-left: 12px;
background: url(../img/admin/icon_changelink.gif) 0 .2em no-repeat; background: url(../img/icon_changelink.gif) 0 .2em no-repeat;
} }
.deletelink { .deletelink {
padding-left: 12px; padding-left: 12px;
background: url(../img/admin/icon_deletelink.gif) 0 .25em no-repeat; background: url(../img/icon_deletelink.gif) 0 .25em no-repeat;
} }
a.deletelink:link, a.deletelink:visited { a.deletelink:link, a.deletelink:visited {
...@@ -593,14 +656,14 @@ a.deletelink:hover { ...@@ -593,14 +656,14 @@ a.deletelink:hover {
.object-tools li { .object-tools li {
display: block; display: block;
float: left; float: left;
background: url(../img/admin/tool-left.gif) 0 0 no-repeat; background: url(../img/tool-left.gif) 0 0 no-repeat;
padding: 0 0 0 8px; padding: 0 0 0 8px;
margin-left: 2px; margin-left: 2px;
height: 16px; height: 16px;
} }
.object-tools li:hover { .object-tools li:hover {
background: url(../img/admin/tool-left_over.gif) 0 0 no-repeat; background: url(../img/tool-left_over.gif) 0 0 no-repeat;
} }
.object-tools a:link, .object-tools a:visited { .object-tools a:link, .object-tools a:visited {
...@@ -609,29 +672,29 @@ a.deletelink:hover { ...@@ -609,29 +672,29 @@ a.deletelink:hover {
color: white; color: white;
padding: .1em 14px .1em 8px; padding: .1em 14px .1em 8px;
height: 14px; height: 14px;
background: #999 url(../img/admin/tool-right.gif) 100% 0 no-repeat; background: #999 url(../img/tool-right.gif) 100% 0 no-repeat;
} }
.object-tools a:hover, .object-tools li:hover a { .object-tools a:hover, .object-tools li:hover a {
background: #5b80b2 url(../img/admin/tool-right_over.gif) 100% 0 no-repeat; background: #5b80b2 url(../img/tool-right_over.gif) 100% 0 no-repeat;
} }
.object-tools a.viewsitelink, .object-tools a.golink { .object-tools a.viewsitelink, .object-tools a.golink {
background: #999 url(../img/admin/tooltag-arrowright.gif) top right no-repeat; background: #999 url(../img/tooltag-arrowright.gif) top right no-repeat;
padding-right: 28px; padding-right: 28px;
} }
.object-tools a.viewsitelink:hover, .object-tools a.golink:hover { .object-tools a.viewsitelink:hover, .object-tools a.golink:hover {
background: #5b80b2 url(../img/admin/tooltag-arrowright_over.gif) top right no-repeat; background: #5b80b2 url(../img/tooltag-arrowright_over.gif) top right no-repeat;
} }
.object-tools a.addlink { .object-tools a.addlink {
background: #999 url(../img/admin/tooltag-add.gif) top right no-repeat; background: #999 url(../img/tooltag-add.gif) top right no-repeat;
padding-right: 28px; padding-right: 28px;
} }
.object-tools a.addlink:hover { .object-tools a.addlink:hover {
background: #5b80b2 url(../img/admin/tooltag-add_over.gif) top right no-repeat; background: #5b80b2 url(../img/tooltag-add_over.gif) top right no-repeat;
} }
/* OBJECT HISTORY */ /* OBJECT HISTORY */
...@@ -766,7 +829,7 @@ table#change-history tbody th { ...@@ -766,7 +829,7 @@ table#change-history tbody th {
} }
#content-related .module h2 { #content-related .module h2 {
background: #eee url(../img/admin/nav-bg.gif) bottom left repeat-x; background: #eee url(../img/nav-bg.gif) bottom left repeat-x;
color: #666; color: #666;
} }
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
} }
.change-list .filtered { .change-list .filtered {
background: white url(../img/admin/changelist-bg.gif) top right repeat-y !important; background: white url(../img/changelist-bg.gif) top right repeat-y !important;
} }
.change-list .filtered .results, .change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull { .change-list .filtered .results, .change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull {
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
color: #666; color: #666;
border-top: 1px solid #eee; border-top: 1px solid #eee;
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
background: white url(../img/admin/nav-bg.gif) 0 180% repeat-x; background: white url(../img/nav-bg.gif) 0 180% repeat-x;
overflow: hidden; overflow: hidden;
} }
...@@ -51,6 +51,7 @@ ...@@ -51,6 +51,7 @@
/* CHANGELIST TABLES */ /* CHANGELIST TABLES */
#changelist table thead th { #changelist table thead th {
padding: 0;
white-space: nowrap; white-space: nowrap;
vertical-align: middle; vertical-align: middle;
} }
...@@ -82,7 +83,7 @@ ...@@ -82,7 +83,7 @@
#changelist #toolbar { #changelist #toolbar {
padding: 3px; padding: 3px;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x; background: #e1e1e1 url(../img/nav-bg.gif) top left repeat-x;
color: #666; color: #666;
} }
...@@ -156,7 +157,7 @@ ...@@ -156,7 +157,7 @@
.change-list ul.toplinks { .change-list ul.toplinks {
display: block; display: block;
background: white url(../img/admin/nav-bg-reverse.gif) 0 -10px repeat-x; background: white url(../img/nav-bg-reverse.gif) 0 -10px repeat-x;
border-top: 1px solid white; border-top: 1px solid white;
float: left; float: left;
padding: 0 !important; padding: 0 !important;
...@@ -165,11 +166,10 @@ ...@@ -165,11 +166,10 @@
} }
.change-list ul.toplinks li { .change-list ul.toplinks li {
float: left;
width: 9em;
padding: 3px 6px; padding: 3px 6px;
font-weight: bold; font-weight: bold;
list-style-type: none; list-style-type: none;
display: inline-block;
} }
.change-list ul.toplinks .date-back a { .change-list ul.toplinks .date-back a {
...@@ -246,7 +246,7 @@ ...@@ -246,7 +246,7 @@
padding: 3px; padding: 3px;
border-top: 1px solid #fff; border-top: 1px solid #fff;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
background: white url(../img/admin/nav-bg-reverse.gif) 0 -10px repeat-x; background: white url(../img/nav-bg-reverse.gif) 0 -10px repeat-x;
} }
#changelist .actions.selected { #changelist .actions.selected {
......
...@@ -140,7 +140,7 @@ fieldset.collapsed h2, fieldset.collapsed { ...@@ -140,7 +140,7 @@ fieldset.collapsed h2, fieldset.collapsed {
} }
fieldset.collapsed h2 { fieldset.collapsed h2 {
background-image: url(../img/admin/nav-bg.gif); background-image: url(../img/nav-bg.gif);
background-position: bottom left; background-position: bottom left;
color: #999; color: #999;
} }
...@@ -161,12 +161,16 @@ fieldset.monospace textarea { ...@@ -161,12 +161,16 @@ fieldset.monospace textarea {
.submit-row { .submit-row {
padding: 5px 7px; padding: 5px 7px;
text-align: right; text-align: right;
background: white url(../img/admin/nav-bg.gif) 0 100% repeat-x; background: white url(../img/nav-bg.gif) 0 100% repeat-x;
border: 1px solid #ccc; border: 1px solid #ccc;
margin: 5px 0; margin: 5px 0;
overflow: hidden; overflow: hidden;
} }
body.popup .submit-row {
overflow: auto;
}
.submit-row input { .submit-row input {
margin: 0 0 0 5px; margin: 0 0 0 5px;
} }
...@@ -180,7 +184,7 @@ fieldset.monospace textarea { ...@@ -180,7 +184,7 @@ fieldset.monospace textarea {
} }
.submit-row .deletelink { .submit-row .deletelink {
background: url(../img/admin/icon_deletelink.gif) 0 50% no-repeat; background: url(../img/icon_deletelink.gif) 0 50% no-repeat;
padding-left: 14px; padding-left: 14px;
} }
...@@ -247,7 +251,7 @@ fieldset.monospace textarea { ...@@ -247,7 +251,7 @@ fieldset.monospace textarea {
color: #666; color: #666;
padding: 3px 5px; padding: 3px 5px;
font-size: 11px; font-size: 11px;
background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x; background: #e1e1e1 url(../img/nav-bg.gif) top left repeat-x;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
} }
...@@ -332,7 +336,7 @@ fieldset.monospace textarea { ...@@ -332,7 +336,7 @@ fieldset.monospace textarea {
color: #666; color: #666;
padding: 3px 5px; padding: 3px 5px;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x; background: #e1e1e1 url(../img/nav-bg.gif) top left repeat-x;
} }
.inline-group .tabular tr.add-row td { .inline-group .tabular tr.add-row td {
...@@ -343,7 +347,7 @@ fieldset.monospace textarea { ...@@ -343,7 +347,7 @@ fieldset.monospace textarea {
.inline-group ul.tools a.add, .inline-group ul.tools a.add,
.inline-group div.add-row a, .inline-group div.add-row a,
.inline-group .tabular tr.add-row td a { .inline-group .tabular tr.add-row td a {
background: url(../img/admin/icon_addlink.gif) 0 50% no-repeat; background: url(../img/icon_addlink.gif) 0 50% no-repeat;
padding-left: 14px; padding-left: 14px;
font-size: 11px; font-size: 11px;
outline: 0; /* Remove dotted border around link */ outline: 0; /* Remove dotted border around link */
...@@ -352,9 +356,3 @@ fieldset.monospace textarea { ...@@ -352,9 +356,3 @@ fieldset.monospace textarea {
.empty-form { .empty-form {
display: none; display: none;
} }
/* IE7 specific bug fixes */
.submit-row input {
float: right;
}
\ No newline at end of file
...@@ -53,5 +53,11 @@ ...@@ -53,5 +53,11 @@
/* IE doesn't know alpha transparency in PNGs */ /* IE doesn't know alpha transparency in PNGs */
.inline-deletelink { .inline-deletelink {
background: transparent url(../img/admin/inline-delete-8bit.png) no-repeat; background: transparent url(../img/inline-delete-8bit.png) no-repeat;
}
/* IE7 doesn't support inline-block */
.change-list ul.toplinks li {
zoom: 1;
*display: inline;
} }
\ No newline at end of file
...@@ -52,3 +52,6 @@ body.login { ...@@ -52,3 +52,6 @@ body.login {
padding: 1em 0 0 9.4em; padding: 1em 0 0 9.4em;
} }
.login .password-reset-link {
text-align: center;
}
...@@ -80,15 +80,8 @@ div.breadcrumbs { ...@@ -80,15 +80,8 @@ div.breadcrumbs {
/* SORTABLE TABLES */ /* SORTABLE TABLES */
table thead th.sorted .sortoptions {
table thead th.sorted a { float: left;
padding-left: 13px;
padding-right: 0px;
}
table thead th.ascending a,
table thead th.descending a {
background-position: left;
} }
/* dashboard styles */ /* dashboard styles */
...@@ -100,12 +93,8 @@ table thead th.descending a { ...@@ -100,12 +93,8 @@ table thead th.descending a {
/* changelists styles */ /* changelists styles */
.change-list ul.toplinks li {
float: right;
}
.change-list .filtered { .change-list .filtered {
background: white url(../img/admin/changelist-bg_rtl.gif) top left repeat-y !important; background: white url(../img/changelist-bg_rtl.gif) top left repeat-y !important;
} }
.change-list .filtered table { .change-list .filtered table {
...@@ -162,7 +151,7 @@ table thead th.descending a { ...@@ -162,7 +151,7 @@ table thead th.descending a {
} }
.submit-row .deletelink { .submit-row .deletelink {
background: url(../img/admin/icon_deletelink.gif) 0 50% no-repeat; background: url(../img/icon_deletelink.gif) 0 50% no-repeat;
padding-right: 14px; padding-right: 14px;
} }
...@@ -183,6 +172,7 @@ input[type=submit].default, .submit-row input.default { ...@@ -183,6 +172,7 @@ input[type=submit].default, .submit-row input.default {
fieldset .field-box { fieldset .field-box {
float: right; float: right;
margin-left: 20px; margin-left: 20px;
margin-right: 0;
} }
.errorlist li { .errorlist li {
...@@ -236,9 +226,20 @@ fieldset .field-box { ...@@ -236,9 +226,20 @@ fieldset .field-box {
padding-left: inherit; padding-left: inherit;
left: 10px; left: 10px;
right: inherit; right: inherit;
float:left;
} }
.inline-related h3 span.delete label { .inline-related h3 span.delete label {
margin-left: inherit; margin-left: inherit;
margin-right: 2px; margin-right: 2px;
} }
/* IE7 specific bug fixes */
div.colM {
position: relative;
}
.submit-row input {
float: left;
}
\ No newline at end of file
...@@ -17,12 +17,16 @@ ...@@ -17,12 +17,16 @@
margin-bottom: 5px; margin-bottom: 5px;
} }
.selector-chosen select {
border-top: none;
}
.selector-available h2, .selector-chosen h2 { .selector-available h2, .selector-chosen h2 {
border: 1px solid #ccc; border: 1px solid #ccc;
} }
.selector .selector-available h2 { .selector .selector-available h2 {
background: white url(../img/admin/nav-bg.gif) bottom left repeat-x; background: white url(../img/nav-bg.gif) bottom left repeat-x;
color: #666; color: #666;
} }
...@@ -37,8 +41,10 @@ ...@@ -37,8 +41,10 @@
text-align: left; text-align: left;
} }
.selector .selector-chosen .selector-filter { .selector .selector-filter label,
padding: 4px 5px; .inline-group .aligned .selector .selector-filter label {
width: 16px;
padding: 2px;
} }
.selector .selector-available input { .selector .selector-available input {
...@@ -49,8 +55,8 @@ ...@@ -49,8 +55,8 @@
float: left; float: left;
width: 22px; width: 22px;
height: 50px; height: 50px;
background: url(../img/admin/chooser-bg.gif) top center no-repeat; background: url(../img/chooser-bg.gif) top center no-repeat;
margin: 8em 3px 0 3px; margin: 10em 5px 0 5px;
padding: 0; padding: 0;
} }
...@@ -61,7 +67,7 @@ ...@@ -61,7 +67,7 @@
} }
.selector select { .selector select {
margin-bottom: 5px; margin-bottom: 10px;
margin-top: 0; margin-top: 0;
} }
...@@ -74,38 +80,66 @@ ...@@ -74,38 +80,66 @@
} }
.selector-add { .selector-add {
background: url(../img/admin/selector-add.gif) top center no-repeat; background: url(../img/selector-icons.gif) 0 -161px no-repeat;
cursor: default;
margin-bottom: 2px; margin-bottom: 2px;
} }
.active.selector-add {
background: url(../img/selector-icons.gif) 0 -187px no-repeat;
cursor: pointer;
}
.selector-remove { .selector-remove {
background: url(../img/admin/selector-remove.gif) top center no-repeat; background: url(../img/selector-icons.gif) 0 -109px no-repeat;
cursor: default;
}
.active.selector-remove {
background: url(../img/selector-icons.gif) 0 -135px no-repeat;
cursor: pointer;
} }
a.selector-chooseall, a.selector-clearall { a.selector-chooseall, a.selector-clearall {
display: block; display: inline-block;
width: 6em;
text-align: left; text-align: left;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
font-weight: bold; font-weight: bold;
color: #666; color: #666;
}
a.selector-chooseall {
padding: 3px 18px 3px 0;
}
a.selector-clearall {
padding: 3px 0 3px 18px; padding: 3px 0 3px 18px;
} }
a.selector-chooseall:hover, a.selector-clearall:hover { a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
color: #036; color: #036;
} }
a.selector-chooseall { a.selector-chooseall {
width: 7em; background: url(../img/selector-icons.gif) right -263px no-repeat;
background: url(../img/admin/selector-addall.gif) left center no-repeat; cursor: default;
}
a.active.selector-chooseall {
background: url(../img/selector-icons.gif) right -289px no-repeat;
cursor: pointer;
} }
a.selector-clearall { a.selector-clearall {
background: url(../img/admin/selector-removeall.gif) left center no-repeat; background: url(../img/selector-icons.gif) left -211px no-repeat;
cursor: default;
} }
a.active.selector-clearall {
background: url(../img/selector-icons.gif) left -237px no-repeat;
cursor: pointer;
}
/* STACKED SELECTORS */ /* STACKED SELECTORS */
...@@ -135,7 +169,7 @@ a.selector-clearall { ...@@ -135,7 +169,7 @@ a.selector-clearall {
height: 22px; height: 22px;
width: 50px; width: 50px;
margin: 0 0 3px 40%; margin: 0 0 3px 40%;
background: url(../img/admin/chooser_stacked-bg.gif) top center no-repeat; background: url(../img/chooser_stacked-bg.gif) top center no-repeat;
} }
.stacked .selector-chooser li { .stacked .selector-chooser li {
...@@ -148,13 +182,24 @@ a.selector-clearall { ...@@ -148,13 +182,24 @@ a.selector-clearall {
} }
.stacked .selector-add { .stacked .selector-add {
background-image: url(../img/admin/selector_stacked-add.gif); background: url(../img/selector-icons.gif) 0 -57px no-repeat;
cursor: default;
}
.stacked .active.selector-add {
background: url(../img/selector-icons.gif) 0 -83px no-repeat;
cursor: pointer;
} }
.stacked .selector-remove { .stacked .selector-remove {
background-image: url(../img/admin/selector_stacked-remove.gif); background: url(../img/selector-icons.gif) 0 -5px no-repeat;
cursor: default;
} }
.stacked .active.selector-remove {
background: url(../img/selector-icons.gif) 0 -31px no-repeat;
cursor: pointer;
}
/* DATE AND TIME */ /* DATE AND TIME */
...@@ -231,7 +276,7 @@ span.clearable-file-input label { ...@@ -231,7 +276,7 @@ span.clearable-file-input label {
padding: 0; padding: 0;
border-collapse: collapse; border-collapse: collapse;
background: white; background: white;
width: 99%; width: 100%;
} }
.calendar caption, .calendarbox h2 { .calendar caption, .calendarbox h2 {
...@@ -246,7 +291,7 @@ span.clearable-file-input label { ...@@ -246,7 +291,7 @@ span.clearable-file-input label {
color: #666; color: #666;
padding: 2px 3px; padding: 2px 3px;
text-align: center; text-align: center;
background: #e1e1e1 url(../img/admin/nav-bg.gif) 0 50% repeat-x; background: #e1e1e1 url(../img/nav-bg.gif) 0 50% repeat-x;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
} }
...@@ -314,7 +359,7 @@ span.clearable-file-input label { ...@@ -314,7 +359,7 @@ span.clearable-file-input label {
position: absolute; position: absolute;
font-weight: bold; font-weight: bold;
font-size: 12px; font-size: 12px;
background: #C9DBED url(../img/admin/default-bg.gif) bottom left repeat-x; background: #C9DBED url(../img/default-bg.gif) bottom left repeat-x;
padding: 1px 4px 2px 4px; padding: 1px 4px 2px 4px;
color: white; color: white;
} }
...@@ -335,15 +380,19 @@ span.clearable-file-input label { ...@@ -335,15 +380,19 @@ span.clearable-file-input label {
.calendar-cancel { .calendar-cancel {
margin: 0 !important; margin: 0 !important;
padding: 0; padding: 0 !important;
font-size: 10px; font-size: 10px;
background: #e1e1e1 url(../img/admin/nav-bg.gif) 0 50% repeat-x; background: #e1e1e1 url(../img/nav-bg.gif) 0 50% repeat-x;
border-top: 1px solid #ddd; border-top: 1px solid #ddd;
} }
.calendar-cancel:hover {
background: #e1e1e1 url(../img/nav-bg-reverse.gif) 0 50% repeat-x;
}
.calendar-cancel a { .calendar-cancel a {
padding: 2px; color: black;
color: #999; display: block;
} }
ul.timelist, .timelist li { ul.timelist, .timelist li {
...@@ -374,7 +423,7 @@ ul.orderer li { ...@@ -374,7 +423,7 @@ ul.orderer li {
border-width: 0 1px 1px 0; border-width: 0 1px 1px 0;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
background: #e2e2e2 url(../img/admin/nav-bg-grabber.gif) repeat-y; background: #e2e2e2 url(../img/nav-bg-grabber.gif) repeat-y;
} }
ul.orderer li:hover { ul.orderer li:hover {
...@@ -406,7 +455,7 @@ ul.orderer li.selected { ...@@ -406,7 +455,7 @@ ul.orderer li.selected {
} }
ul.orderer li.deleted { ul.orderer li.deleted {
background: #bbb url(../img/admin/deleted-overlay.gif); background: #bbb url(../img/deleted-overlay.gif);
} }
ul.orderer li.deleted a:link, ul.orderer li.deleted a:visited { ul.orderer li.deleted a:link, ul.orderer li.deleted a:visited {
...@@ -414,7 +463,7 @@ ul.orderer li.deleted a:link, ul.orderer li.deleted a:visited { ...@@ -414,7 +463,7 @@ ul.orderer li.deleted a:link, ul.orderer li.deleted a:visited {
} }
ul.orderer li.deleted .inline-deletelink { ul.orderer li.deleted .inline-deletelink {
background-image: url(../img/admin/inline-restore.png); background-image: url(../img/inline-restore.png);
} }
ul.orderer li.deleted:hover, ul.orderer li.deleted a.selector:hover { ul.orderer li.deleted:hover, ul.orderer li.deleted a.selector:hover {
...@@ -426,7 +475,7 @@ ul.orderer li.deleted:hover, ul.orderer li.deleted a.selector:hover { ...@@ -426,7 +475,7 @@ ul.orderer li.deleted:hover, ul.orderer li.deleted a.selector:hover {
.inline-deletelink { .inline-deletelink {
float: right; float: right;
text-indent: -9999px; text-indent: -9999px;
background: transparent url(../img/admin/inline-delete.png) no-repeat; background: transparent url(../img/inline-delete.png) no-repeat;
width: 15px; width: 15px;
height: 15px; height: 15px;
border: 0px none; border: 0px none;
...@@ -465,11 +514,11 @@ ul.orderer li.deleted:hover, ul.orderer li.deleted a.selector:hover { ...@@ -465,11 +514,11 @@ ul.orderer li.deleted:hover, ul.orderer li.deleted a.selector:hover {
} }
.editinline tr.deleted { .editinline tr.deleted {
background: #ddd url(../img/admin/deleted-overlay.gif); background: #ddd url(../img/deleted-overlay.gif);
} }
.editinline tr.deleted .inline-deletelink { .editinline tr.deleted .inline-deletelink {
background-image: url(../img/admin/inline-restore.png); background-image: url(../img/inline-restore.png);
} }
.editinline tr.deleted td:hover { .editinline tr.deleted td:hover {
...@@ -500,13 +549,13 @@ ul.orderer li.deleted:hover, ul.orderer li.deleted a.selector:hover { ...@@ -500,13 +549,13 @@ ul.orderer li.deleted:hover, ul.orderer li.deleted a.selector:hover {
.editinline-stacked .inline-splitter { .editinline-stacked .inline-splitter {
float: left; float: left;
width: 9px; width: 9px;
background: #f8f8f8 url(../img/admin/inline-splitter-bg.gif) 50% 50% no-repeat; background: #f8f8f8 url(../img/inline-splitter-bg.gif) 50% 50% no-repeat;
border-right: 1px solid #ccc; border-right: 1px solid #ccc;
} }
.editinline-stacked .controls { .editinline-stacked .controls {
clear: both; clear: both;
background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x; background: #e1e1e1 url(../img/nav-bg.gif) top left repeat-x;
padding: 3px 4px; padding: 3px 4px;
font-size: 11px; font-size: 11px;
border-top: 1px solid #ddd; border-top: 1px solid #ddd;
......
/* /*
SelectFilter2 - Turns a multiple-select box into a filter interface. SelectFilter2 - Turns a multiple-select box into a filter interface.
Different than SelectFilter because this is coupled to the admin framework.
Requires core.js, SelectBox.js and addevent.js. Requires core.js, SelectBox.js and addevent.js.
*/ */
(function($) {
function findForm(node) { function findForm(node) {
// returns the node of the form containing the given node // returns the node of the form containing the given node
if (node.tagName.toLowerCase() != 'form') { if (node.tagName.toLowerCase() != 'form') {
...@@ -14,7 +12,7 @@ function findForm(node) { ...@@ -14,7 +12,7 @@ function findForm(node) {
return node; return node;
} }
var SelectFilter = { window.SelectFilter = {
init: function(field_id, field_name, is_stacked, admin_media_prefix) { init: function(field_id, field_name, is_stacked, admin_media_prefix) {
if (field_id.match(/__prefix__/)){ if (field_id.match(/__prefix__/)){
// Don't intialize on empty forms. // Don't intialize on empty forms.
...@@ -44,41 +42,42 @@ var SelectFilter = { ...@@ -44,41 +42,42 @@ var SelectFilter = {
// <div class="selector-available"> // <div class="selector-available">
var selector_available = quickElement('div', selector_div, ''); var selector_available = quickElement('div', selector_div, '');
selector_available.className = 'selector-available'; selector_available.className = 'selector-available';
quickElement('h2', selector_available, interpolate(gettext('Available %s'), [field_name])); var title_available = quickElement('h2', selector_available, interpolate(gettext('Available %s') + ' ', [field_name]));
var filter_p = quickElement('p', selector_available, ''); quickElement('img', title_available, '', 'src', admin_media_prefix + 'img/icon-unknown.gif', 'width', '10', 'height', '10', 'class', 'help help-tooltip', 'title', interpolate(gettext('This is the list of available %s. You may choose some by selecting them in the box below and then clicking the "Choose" arrow between the two boxes.'), [field_name]));
var filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter');
filter_p.className = 'selector-filter'; filter_p.className = 'selector-filter';
var search_filter_label = quickElement('label', filter_p, '', 'for', field_id + "_input", 'style', 'width:16px;padding:2px'); var search_filter_label = quickElement('label', filter_p, '', 'for', field_id + "_input");
var search_selector_img = quickElement('img', search_filter_label, '', 'src', admin_media_prefix + 'img/admin/selector-search.gif'); var search_selector_img = quickElement('img', search_filter_label, '', 'src', admin_media_prefix + 'img/selector-search.gif', 'class', 'help-tooltip', 'alt', '', 'title', interpolate(gettext("Type into this box to filter down the list of available %s."), [field_name]));
search_selector_img.alt = gettext("Filter");
filter_p.appendChild(document.createTextNode(' ')); filter_p.appendChild(document.createTextNode(' '));
var filter_input = quickElement('input', filter_p, '', 'type', 'text'); var filter_input = quickElement('input', filter_p, '', 'type', 'text', 'placeholder', gettext("Filter"));
filter_input.id = field_id + '_input'; filter_input.id = field_id + '_input';
selector_available.appendChild(from_box); selector_available.appendChild(from_box);
var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'href', 'javascript: (function(){ SelectBox.move_all("' + field_id + '_from", "' + field_id + '_to"); })()'); var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', 'javascript: (function(){ SelectBox.move_all("' + field_id + '_from", "' + field_id + '_to"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_add_all_link');
choose_all.className = 'selector-chooseall'; choose_all.className = 'selector-chooseall';
// <ul class="selector-chooser"> // <ul class="selector-chooser">
var selector_chooser = quickElement('ul', selector_div, ''); var selector_chooser = quickElement('ul', selector_div, '');
selector_chooser.className = 'selector-chooser'; selector_chooser.className = 'selector-chooser';
var add_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Add'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_from","' + field_id + '_to");})()'); var add_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Choose'), 'title', gettext('Choose'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_from","' + field_id + '_to"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_add_link');
add_link.className = 'selector-add'; add_link.className = 'selector-add';
var remove_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Remove'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_to","' + field_id + '_from");})()'); var remove_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Remove'), 'title', gettext('Remove'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_to","' + field_id + '_from"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_remove_link');
remove_link.className = 'selector-remove'; remove_link.className = 'selector-remove';
// <div class="selector-chosen"> // <div class="selector-chosen">
var selector_chosen = quickElement('div', selector_div, ''); var selector_chosen = quickElement('div', selector_div, '');
selector_chosen.className = 'selector-chosen'; selector_chosen.className = 'selector-chosen';
quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s'), [field_name])); var title_chosen = quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s') + ' ', [field_name]));
var selector_filter = quickElement('p', selector_chosen, gettext('Select your choice(s) and click ')); quickElement('img', title_chosen, '', 'src', admin_media_prefix + 'img/icon-unknown.gif', 'width', '10', 'height', '10', 'class', 'help help-tooltip', 'title', interpolate(gettext('This is the list of chosen %s. You may remove some by selecting them in the box below and then clicking the "Remove" arrow between the two boxes.'), [field_name]));
selector_filter.className = 'selector-filter';
quickElement('img', selector_filter, '', 'src', admin_media_prefix + (is_stacked ? 'img/admin/selector_stacked-add.gif':'img/admin/selector-add.gif'), 'alt', 'Add');
var to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', 'multiple', 'size', from_box.size, 'name', from_box.getAttribute('name')); var to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', 'multiple', 'size', from_box.size, 'name', from_box.getAttribute('name'));
to_box.className = 'filtered'; to_box.className = 'filtered';
var clear_all = quickElement('a', selector_chosen, gettext('Clear all'), 'href', 'javascript: (function() { SelectBox.move_all("' + field_id + '_to", "' + field_id + '_from");})()'); var clear_all = quickElement('a', selector_chosen, gettext('Remove all'), 'title', interpolate(gettext('Click to remove all chosen %s at once.'), [field_name]), 'href', 'javascript: (function() { SelectBox.move_all("' + field_id + '_to", "' + field_id + '_from"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_remove_all_link');
clear_all.className = 'selector-clearall'; clear_all.className = 'selector-clearall';
from_box.setAttribute('name', from_box.getAttribute('name') + '_old'); from_box.setAttribute('name', from_box.getAttribute('name') + '_old');
...@@ -86,16 +85,46 @@ var SelectFilter = { ...@@ -86,16 +85,46 @@ var SelectFilter = {
// Set up the JavaScript event handlers for the select box filter interface // Set up the JavaScript event handlers for the select box filter interface
addEvent(filter_input, 'keyup', function(e) { SelectFilter.filter_key_up(e, field_id); }); addEvent(filter_input, 'keyup', function(e) { SelectFilter.filter_key_up(e, field_id); });
addEvent(filter_input, 'keydown', function(e) { SelectFilter.filter_key_down(e, field_id); }); addEvent(filter_input, 'keydown', function(e) { SelectFilter.filter_key_down(e, field_id); });
addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id + '_from', field_id + '_to'); }); addEvent(from_box, 'change', function(e) { SelectFilter.refresh_icons(field_id) });
addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id + '_to', field_id + '_from'); }); addEvent(to_box, 'change', function(e) { SelectFilter.refresh_icons(field_id) });
addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id + '_from', field_id + '_to'); SelectFilter.refresh_icons(field_id); });
addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id + '_to', field_id + '_from'); SelectFilter.refresh_icons(field_id); });
addEvent(findForm(from_box), 'submit', function() { SelectBox.select_all(field_id + '_to'); }); addEvent(findForm(from_box), 'submit', function() { SelectBox.select_all(field_id + '_to'); });
SelectBox.init(field_id + '_from'); SelectBox.init(field_id + '_from');
SelectBox.init(field_id + '_to'); SelectBox.init(field_id + '_to');
// Move selected from_box options to to_box // Move selected from_box options to to_box
SelectBox.move(field_id + '_from', field_id + '_to'); SelectBox.move(field_id + '_from', field_id + '_to');
if (!is_stacked) {
// In horizontal mode, give the same height to the two boxes.
var j_from_box = $(from_box);
var j_to_box = $(to_box);
var resize_filters = function() { j_to_box.height($(filter_p).outerHeight() + j_from_box.outerHeight()); }
if (j_from_box.outerHeight() > 0) {
resize_filters(); // This fieldset is already open. Resize now.
} else {
// This fieldset is probably collapsed. Wait for its 'show' event.
j_to_box.closest('fieldset').one('show.fieldset', resize_filters);
}
}
// Initial icon refresh
SelectFilter.refresh_icons(field_id);
},
refresh_icons: function(field_id) {
var from = $('#' + field_id + '_from');
var to = $('#' + field_id + '_to');
var is_from_selected = from.find('option:selected').length > 0;
var is_to_selected = to.find('option:selected').length > 0;
// Active if at least one item is selected
$('#' + field_id + '_add_link').toggleClass('active', is_from_selected);
$('#' + field_id + '_remove_link').toggleClass('active', is_to_selected);
// Active if the corresponding box isn't empty
$('#' + field_id + '_add_all_link').toggleClass('active', from.find('option').length > 0);
$('#' + field_id + '_remove_all_link').toggleClass('active', to.find('option').length > 0);
}, },
filter_key_up: function(event, field_id) { filter_key_up: function(event, field_id) {
from = document.getElementById(field_id + '_from'); var from = document.getElementById(field_id + '_from');
// don't submit form if user pressed Enter // don't submit form if user pressed Enter
if ((event.which && event.which == 13) || (event.keyCode && event.keyCode == 13)) { if ((event.which && event.which == 13) || (event.keyCode && event.keyCode == 13)) {
from.selectedIndex = 0; from.selectedIndex = 0;
...@@ -109,7 +138,7 @@ var SelectFilter = { ...@@ -109,7 +138,7 @@ var SelectFilter = {
return true; return true;
}, },
filter_key_down: function(event, field_id) { filter_key_down: function(event, field_id) {
from = document.getElementById(field_id + '_from'); var from = document.getElementById(field_id + '_from');
// right arrow -- move across // right arrow -- move across
if ((event.which && event.which == 39) || (event.keyCode && event.keyCode == 39)) { if ((event.which && event.which == 39) || (event.keyCode && event.keyCode == 39)) {
var old_index = from.selectedIndex; var old_index = from.selectedIndex;
...@@ -128,3 +157,5 @@ var SelectFilter = { ...@@ -128,3 +157,5 @@ var SelectFilter = {
return true; return true;
} }
} }
})(django.jQuery);
(function(a){a.fn.actions=function(h){var b=a.extend({},a.fn.actions.defaults,h),e=a(this),f=false;checker=function(c){c?showQuestion():reset();a(e).attr("checked",c).parent().parent().toggleClass(b.selectedClass,c)};updateCounter=function(){var c=a(e).filter(":checked").length;a(b.counterContainer).html(interpolate(ngettext("%(sel)s of %(cnt)s selected","%(sel)s of %(cnt)s selected",c),{sel:c,cnt:_actions_icnt},true));a(b.allToggle).attr("checked",function(){if(c==e.length){value=true;showQuestion()}else{value= (function(a){a.fn.actions=function(g){var b=a.extend({},a.fn.actions.defaults,g),f=a(this),e=!1;checker=function(c){c?showQuestion():reset();a(f).attr("checked",c).parent().parent().toggleClass(b.selectedClass,c)};updateCounter=function(){var c=a(f).filter(":checked").length;a(b.counterContainer).html(interpolate(ngettext("%(sel)s of %(cnt)s selected","%(sel)s of %(cnt)s selected",c),{sel:c,cnt:_actions_icnt},!0));a(b.allToggle).attr("checked",function(){c==f.length?(value=!0,showQuestion()):(value=
false;clearAcross()}return value})};showQuestion=function(){a(b.acrossClears).hide();a(b.acrossQuestions).show();a(b.allContainer).hide()};showClear=function(){a(b.acrossClears).show();a(b.acrossQuestions).hide();a(b.actionContainer).toggleClass(b.selectedClass);a(b.allContainer).show();a(b.counterContainer).hide()};reset=function(){a(b.acrossClears).hide();a(b.acrossQuestions).hide();a(b.allContainer).hide();a(b.counterContainer).show()};clearAcross=function(){reset();a(b.acrossInput).val(0);a(b.actionContainer).removeClass(b.selectedClass)}; !1,clearAcross());return value})};showQuestion=function(){a(b.acrossClears).hide();a(b.acrossQuestions).show();a(b.allContainer).hide()};showClear=function(){a(b.acrossClears).show();a(b.acrossQuestions).hide();a(b.actionContainer).toggleClass(b.selectedClass);a(b.allContainer).show();a(b.counterContainer).hide()};reset=function(){a(b.acrossClears).hide();a(b.acrossQuestions).hide();a(b.allContainer).hide();a(b.counterContainer).show()};clearAcross=function(){reset();a(b.acrossInput).val(0);a(b.actionContainer).removeClass(b.selectedClass)};
a(b.counterContainer).show();a(this).filter(":checked").each(function(){a(this).parent().parent().toggleClass(b.selectedClass);updateCounter();a(b.acrossInput).val()==1&&showClear()});a(b.allToggle).show().click(function(){checker(a(this).attr("checked"));updateCounter()});a("div.actions span.question a").click(function(c){c.preventDefault();a(b.acrossInput).val(1);showClear()});a("div.actions span.clear a").click(function(c){c.preventDefault();a(b.allToggle).attr("checked",false);clearAcross();checker(0); a(b.counterContainer).show();a(this).filter(":checked").each(function(){a(this).parent().parent().toggleClass(b.selectedClass);updateCounter();1==a(b.acrossInput).val()&&showClear()});a(b.allToggle).show().click(function(){checker(a(this).attr("checked"));updateCounter()});a("div.actions span.question a").click(function(c){c.preventDefault();a(b.acrossInput).val(1);showClear()});a("div.actions span.clear a").click(function(c){c.preventDefault();a(b.allToggle).attr("checked",!1);clearAcross();checker(0);
updateCounter()});lastChecked=null;a(e).click(function(c){if(!c)c=window.event;var d=c.target?c.target:c.srcElement;if(lastChecked&&a.data(lastChecked)!=a.data(d)&&c.shiftKey==true){var g=false;a(lastChecked).attr("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked);a(e).each(function(){if(a.data(this)==a.data(lastChecked)||a.data(this)==a.data(d))g=g?false:true;g&&a(this).attr("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked)})}a(d).parent().parent().toggleClass(b.selectedClass, updateCounter()});lastChecked=null;a(f).click(function(c){if(!c)c=window.event;var d=c.target?c.target:c.srcElement;if(lastChecked&&a.data(lastChecked)!=a.data(d)&&!0==c.shiftKey){var e=!1;a(lastChecked).attr("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked);a(f).each(function(){if(a.data(this)==a.data(lastChecked)||a.data(this)==a.data(d))e=e?!1:!0;e&&a(this).attr("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked)})}a(d).parent().parent().toggleClass(b.selectedClass,
d.checked);lastChecked=d;updateCounter()});a("form#changelist-form table#result_list tr").find("td:gt(0) :input").change(function(){f=true});a('form#changelist-form button[name="index"]').click(function(){if(f)return confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."))});a('form#changelist-form input[name="_save"]').click(function(){var c=false;a("div.actions select option:selected").each(function(){if(a(this).val())c= d.checked);lastChecked=d;updateCounter()});a("form#changelist-form table#result_list tr").find("td:gt(0) :input").change(function(){e=!0});a('form#changelist-form button[name="index"]').click(function(){if(e)return confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."))});a('form#changelist-form input[name="_save"]').click(function(){var b=!1;a("div.actions select option:selected").each(function(){a(this).val()&&(b=!0)});
true});if(c)return f?confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action.")):confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button."))})};a.fn.actions.defaults={actionContainer:"div.actions",counterContainer:"span.action-counter",allContainer:"div.actions span.all",acrossInput:"div.actions input.select-across", if(b)return e?confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action.")):confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button."))})};a.fn.actions.defaults={actionContainer:"div.actions",counterContainer:"span.action-counter",allContainer:"div.actions span.all",acrossInput:"div.actions input.select-across",
acrossQuestions:"div.actions span.question",acrossClears:"div.actions span.clear",allToggle:"#action-toggle",selectedClass:"selected"}})(django.jQuery); acrossQuestions:"div.actions span.question",acrossClears:"div.actions span.clear",allToggle:"#action-toggle",selectedClass:"selected"}})(django.jQuery);
...@@ -50,7 +50,7 @@ var DateTimeShortcuts = { ...@@ -50,7 +50,7 @@ var DateTimeShortcuts = {
var clock_link = document.createElement('a'); var clock_link = document.createElement('a');
clock_link.setAttribute('href', 'javascript:DateTimeShortcuts.openClock(' + num + ');'); clock_link.setAttribute('href', 'javascript:DateTimeShortcuts.openClock(' + num + ');');
clock_link.id = DateTimeShortcuts.clockLinkName + num; clock_link.id = DateTimeShortcuts.clockLinkName + num;
quickElement('img', clock_link, '', 'src', DateTimeShortcuts.admin_media_prefix + 'img/admin/icon_clock.gif', 'alt', gettext('Clock')); quickElement('img', clock_link, '', 'src', DateTimeShortcuts.admin_media_prefix + 'img/icon_clock.gif', 'alt', gettext('Clock'));
shortcuts_span.appendChild(document.createTextNode('\240')); shortcuts_span.appendChild(document.createTextNode('\240'));
shortcuts_span.appendChild(now_link); shortcuts_span.appendChild(now_link);
shortcuts_span.appendChild(document.createTextNode('\240|\240')); shortcuts_span.appendChild(document.createTextNode('\240|\240'));
...@@ -79,17 +79,24 @@ var DateTimeShortcuts = { ...@@ -79,17 +79,24 @@ var DateTimeShortcuts = {
addEvent(clock_box, 'click', DateTimeShortcuts.cancelEventPropagation); addEvent(clock_box, 'click', DateTimeShortcuts.cancelEventPropagation);
quickElement('h2', clock_box, gettext('Choose a time')); quickElement('h2', clock_box, gettext('Choose a time'));
time_list = quickElement('ul', clock_box, ''); var time_list = quickElement('ul', clock_box, '');
time_list.className = 'timelist'; time_list.className = 'timelist';
time_format = get_format('TIME_INPUT_FORMATS')[0]; var time_format = get_format('TIME_INPUT_FORMATS')[0];
quickElement("a", quickElement("li", time_list, ""), gettext("Now"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().strftime('" + time_format + "'));"); quickElement("a", quickElement("li", time_list, ""), gettext("Now"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().strftime('" + time_format + "'));");
quickElement("a", quickElement("li", time_list, ""), gettext("Midnight"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,0,0,0,0).strftime('" + time_format + "'));"); quickElement("a", quickElement("li", time_list, ""), gettext("Midnight"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,0,0,0,0).strftime('" + time_format + "'));");
quickElement("a", quickElement("li", time_list, ""), gettext("6 a.m."), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,6,0,0,0).strftime('" + time_format + "'));"); quickElement("a", quickElement("li", time_list, ""), gettext("6 a.m."), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,6,0,0,0).strftime('" + time_format + "'));");
quickElement("a", quickElement("li", time_list, ""), gettext("Noon"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,12,0,0,0).strftime('" + time_format + "'));"); quickElement("a", quickElement("li", time_list, ""), gettext("Noon"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,12,0,0,0).strftime('" + time_format + "'));");
cancel_p = quickElement('p', clock_box, ''); var cancel_p = quickElement('p', clock_box, '');
cancel_p.className = 'calendar-cancel'; cancel_p.className = 'calendar-cancel';
quickElement('a', cancel_p, gettext('Cancel'), 'href', 'javascript:DateTimeShortcuts.dismissClock(' + num + ');'); quickElement('a', cancel_p, gettext('Cancel'), 'href', 'javascript:DateTimeShortcuts.dismissClock(' + num + ');');
django.jQuery(document).bind('keyup', function(event) {
if (event.which == 27) {
// ESC key closes popup
DateTimeShortcuts.dismissClock(num);
event.preventDefault();
}
});
}, },
openClock: function(num) { openClock: function(num) {
var clock_box = document.getElementById(DateTimeShortcuts.clockDivName+num) var clock_box = document.getElementById(DateTimeShortcuts.clockDivName+num)
...@@ -138,7 +145,7 @@ var DateTimeShortcuts = { ...@@ -138,7 +145,7 @@ var DateTimeShortcuts = {
var cal_link = document.createElement('a'); var cal_link = document.createElement('a');
cal_link.setAttribute('href', 'javascript:DateTimeShortcuts.openCalendar(' + num + ');'); cal_link.setAttribute('href', 'javascript:DateTimeShortcuts.openCalendar(' + num + ');');
cal_link.id = DateTimeShortcuts.calendarLinkName + num; cal_link.id = DateTimeShortcuts.calendarLinkName + num;
quickElement('img', cal_link, '', 'src', DateTimeShortcuts.admin_media_prefix + 'img/admin/icon_calendar.gif', 'alt', gettext('Calendar')); quickElement('img', cal_link, '', 'src', DateTimeShortcuts.admin_media_prefix + 'img/icon_calendar.gif', 'alt', gettext('Calendar'));
shortcuts_span.appendChild(document.createTextNode('\240')); shortcuts_span.appendChild(document.createTextNode('\240'));
shortcuts_span.appendChild(today_link); shortcuts_span.appendChild(today_link);
shortcuts_span.appendChild(document.createTextNode('\240|\240')); shortcuts_span.appendChild(document.createTextNode('\240|\240'));
...@@ -195,6 +202,13 @@ var DateTimeShortcuts = { ...@@ -195,6 +202,13 @@ var DateTimeShortcuts = {
var cancel_p = quickElement('p', cal_box, ''); var cancel_p = quickElement('p', cal_box, '');
cancel_p.className = 'calendar-cancel'; cancel_p.className = 'calendar-cancel';
quickElement('a', cancel_p, gettext('Cancel'), 'href', 'javascript:DateTimeShortcuts.dismissCalendar(' + num + ');'); quickElement('a', cancel_p, gettext('Cancel'), 'href', 'javascript:DateTimeShortcuts.dismissCalendar(' + num + ');');
django.jQuery(document).bind('keyup', function(event) {
if (event.which == 27) {
// ESC key closes popup
DateTimeShortcuts.dismissCalendar(num);
event.preventDefault();
}
});
}, },
openCalendar: function(num) { openCalendar: function(num) {
var cal_box = document.getElementById(DateTimeShortcuts.calendarDivName1+num) var cal_box = document.getElementById(DateTimeShortcuts.calendarDivName1+num)
......
...@@ -74,11 +74,12 @@ function dismissAddAnotherPopup(win, newId, newRepr) { ...@@ -74,11 +74,12 @@ function dismissAddAnotherPopup(win, newId, newRepr) {
var name = windowname_to_id(win.name); var name = windowname_to_id(win.name);
var elem = document.getElementById(name); var elem = document.getElementById(name);
if (elem) { if (elem) {
if (elem.nodeName == 'SELECT') { var elemName = elem.nodeName.toUpperCase();
if (elemName == 'SELECT') {
var o = new Option(newRepr, newId); var o = new Option(newRepr, newId);
elem.options[elem.options.length] = o; elem.options[elem.options.length] = o;
o.selected = true; o.selected = true;
} else if (elem.nodeName == 'INPUT') { } else if (elemName == 'INPUT') {
if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) { if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) {
elem.value += ',' + newId; elem.value += ',' + newId;
} else { } else {
......
...@@ -11,7 +11,7 @@ function reorder_init() { ...@@ -11,7 +11,7 @@ function reorder_init() {
setOrder(input.value.split(',')); setOrder(input.value.split(','));
input.disabled = true; input.disabled = true;
draw(); draw();
// Now initialise the dragging behaviour // Now initialize the dragging behavior
var limit = (lis.length - 1) * height; var limit = (lis.length - 1) * height;
for (var i = 0; i < lis.length; i++) { for (var i = 0; i < lis.length; i++) {
var li = lis[i]; var li = lis[i];
......
...@@ -3,9 +3,8 @@ ...@@ -3,9 +3,8 @@
// Add anchor tag for Show/Hide link // Add anchor tag for Show/Hide link
$("fieldset.collapse").each(function(i, elem) { $("fieldset.collapse").each(function(i, elem) {
// Don't hide if fields in this fieldset have errors // Don't hide if fields in this fieldset have errors
if ( $(elem).find("div.errors").length == 0 ) { if ($(elem).find("div.errors").length == 0) {
$(elem).addClass("collapsed"); $(elem).addClass("collapsed").find("h2").first().append(' (<a id="fieldsetcollapser' +
$(elem).find("h2").first().append(' (<a id="fieldsetcollapser' +
i +'" class="collapse-toggle" href="#">' + gettext("Show") + i +'" class="collapse-toggle" href="#">' + gettext("Show") +
'</a>)'); '</a>)');
} }
...@@ -13,13 +12,11 @@ ...@@ -13,13 +12,11 @@
// Add toggle to anchor tag // Add toggle to anchor tag
$("fieldset.collapse a.collapse-toggle").toggle( $("fieldset.collapse a.collapse-toggle").toggle(
function() { // Show function() { // Show
$(this).text(gettext("Hide")); $(this).text(gettext("Hide")).closest("fieldset").removeClass("collapsed").trigger("show.fieldset", [$(this).attr("id")]);
$(this).closest("fieldset").removeClass("collapsed");
return false; return false;
}, },
function() { // Hide function() { // Hide
$(this).text(gettext("Show")); $(this).text(gettext("Show")).closest("fieldset").addClass("collapsed").trigger("hide.fieldset", [$(this).attr("id")]);
$(this).closest("fieldset").addClass("collapsed");
return false; return false;
} }
); );
......
(function(a){a(document).ready(function(){a("fieldset.collapse").each(function(c,b){if(a(b).find("div.errors").length==0){a(b).addClass("collapsed");a(b).find("h2").first().append(' (<a id="fieldsetcollapser'+c+'" class="collapse-toggle" href="#">'+gettext("Show")+"</a>)")}});a("fieldset.collapse a.collapse-toggle").toggle(function(){a(this).text(gettext("Hide"));a(this).closest("fieldset").removeClass("collapsed");return false},function(){a(this).text(gettext("Show"));a(this).closest("fieldset").addClass("collapsed"); (function(a){a(document).ready(function(){a("fieldset.collapse").each(function(c,b){0==a(b).find("div.errors").length&&a(b).addClass("collapsed").find("h2").first().append(' (<a id="fieldsetcollapser'+c+'" class="collapse-toggle" href="#">'+gettext("Show")+"</a>)")});a("fieldset.collapse a.collapse-toggle").toggle(function(){a(this).text(gettext("Hide")).closest("fieldset").removeClass("collapsed").trigger("show.fieldset",[a(this).attr("id")]);return!1},function(){a(this).text(gettext("Show")).closest("fieldset").addClass("collapsed").trigger("hide.fieldset",
return false})})})(django.jQuery); [a(this).attr("id")]);return!1})})})(django.jQuery);
...@@ -108,12 +108,6 @@ function findPosY(obj) { ...@@ -108,12 +108,6 @@ function findPosY(obj) {
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// Date object extensions // Date object extensions
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
Date.prototype.getCorrectYear = function() {
// Date.getYear() is unreliable --
// see http://www.quirksmode.org/js/introdate.html#year
var y = this.getYear() % 100;
return (y < 38) ? y + 2000 : y + 1900;
}
Date.prototype.getTwelveHours = function() { Date.prototype.getTwelveHours = function() {
hours = this.getHours(); hours = this.getHours();
...@@ -149,10 +143,6 @@ Date.prototype.getTwoDigitSecond = function() { ...@@ -149,10 +143,6 @@ Date.prototype.getTwoDigitSecond = function() {
return (this.getSeconds() < 10) ? '0' + this.getSeconds() : this.getSeconds(); return (this.getSeconds() < 10) ? '0' + this.getSeconds() : this.getSeconds();
} }
Date.prototype.getISODate = function() {
return this.getCorrectYear() + '-' + this.getTwoDigitMonth() + '-' + this.getTwoDigitDate();
}
Date.prototype.getHourMinute = function() { Date.prototype.getHourMinute = function() {
return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute(); return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute();
} }
......
(function(b){b.fn.formset=function(g){var a=b.extend({},b.fn.formset.defaults,g),k=function(c,f,d){var e=new RegExp("("+f+"-(\\d+|__prefix__))");f=f+"-"+d;b(c).attr("for")&&b(c).attr("for",b(c).attr("for").replace(e,f));if(c.id)c.id=c.id.replace(e,f);if(c.name)c.name=c.name.replace(e,f)};g=b("#id_"+a.prefix+"-TOTAL_FORMS").attr("autocomplete","off");var l=parseInt(g.val()),h=b("#id_"+a.prefix+"-MAX_NUM_FORMS").attr("autocomplete","off");g=h.val()==""||h.val()-g.val()>0;b(this).each(function(){b(this).not("."+ (function(b){b.fn.formset=function(c){var a=b.extend({},b.fn.formset.defaults,c),j=function(a,e,d){var i=RegExp("("+e+"-(\\d+|__prefix__))"),e=e+"-"+d;b(a).attr("for")&&b(a).attr("for",b(a).attr("for").replace(i,e));if(a.id)a.id=a.id.replace(i,e);if(a.name)a.name=a.name.replace(i,e)},c=b("#id_"+a.prefix+"-TOTAL_FORMS").attr("autocomplete","off"),g=parseInt(c.val()),f=b("#id_"+a.prefix+"-MAX_NUM_FORMS").attr("autocomplete","off"),c=""==f.val()||0<f.val()-c.val();b(this).each(function(){b(this).not("."+
a.emptyCssClass).addClass(a.formCssClass)});if(b(this).length&&g){var j;if(b(this).attr("tagName")=="TR"){g=this.eq(0).children().length;b(this).parent().append('<tr class="'+a.addCssClass+'"><td colspan="'+g+'"><a href="javascript:void(0)">'+a.addText+"</a></tr>");j=b(this).parent().find("tr:last a")}else{b(this).filter(":last").after('<div class="'+a.addCssClass+'"><a href="javascript:void(0)">'+a.addText+"</a></div>");j=b(this).filter(":last").next().find("a")}j.click(function(){var c=b("#id_"+ a.emptyCssClass).addClass(a.formCssClass)});if(b(this).length&&c){var h;"TR"==b(this).attr("tagName")?(c=this.eq(0).children().length,b(this).parent().append('<tr class="'+a.addCssClass+'"><td colspan="'+c+'"><a href="javascript:void(0)">'+a.addText+"</a></tr>"),h=b(this).parent().find("tr:last a")):(b(this).filter(":last").after('<div class="'+a.addCssClass+'"><a href="javascript:void(0)">'+a.addText+"</a></div>"),h=b(this).filter(":last").next().find("a"));h.click(function(){var c=b("#id_"+a.prefix+
a.prefix+"-TOTAL_FORMS"),f=b("#"+a.prefix+"-empty"),d=f.clone(true);d.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+l);if(d.is("tr"))d.children(":last").append('<div><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></div>");else d.is("ul")||d.is("ol")?d.append('<li><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></li>"):d.children(":first").append('<span><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+ "-TOTAL_FORMS"),e=b("#"+a.prefix+"-empty"),d=e.clone(!0);d.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+g);d.is("tr")?d.children(":last").append('<div><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></div>"):d.is("ul")||d.is("ol")?d.append('<li><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></li>"):d.children(":first").append('<span><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+
a.deleteText+"</a></span>");d.find("*").each(function(){k(this,a.prefix,c.val())});d.insertBefore(b(f));b(c).val(parseInt(c.val())+1);l+=1;h.val()!=""&&h.val()-c.val()<=0&&j.parent().hide();d.find("a."+a.deleteCssClass).click(function(){var e=b(this).parents("."+a.formCssClass);e.remove();l-=1;a.removed&&a.removed(e);e=b("."+a.formCssClass);b("#id_"+a.prefix+"-TOTAL_FORMS").val(e.length);if(h.val()==""||h.val()-e.length>0)j.parent().show();for(var i=0,m=e.length;i<m;i++){k(b(e).get(i),a.prefix,i); "</a></span>");d.find("*").each(function(){j(this,a.prefix,c.val())});d.insertBefore(b(e));b(c).val(parseInt(c.val())+1);g+=1;""!=f.val()&&0>=f.val()-c.val()&&h.parent().hide();d.find("a."+a.deleteCssClass).click(function(){var c=b(this).parents("."+a.formCssClass);c.remove();g-=1;a.removed&&a.removed(c);c=b("."+a.formCssClass);b("#id_"+a.prefix+"-TOTAL_FORMS").val(c.length);(""==f.val()||0<f.val()-c.length)&&h.parent().show();for(var d=0,e=c.length;d<e;d++)j(b(c).get(d),a.prefix,d),b(c.get(d)).find("*").each(function(){j(this,
b(e.get(i)).find("*").each(function(){k(this,a.prefix,i)})}return false});a.added&&a.added(d);return false})}return this};b.fn.formset.defaults={prefix:"form",addText:"add another",deleteText:"remove",addCssClass:"add-row",deleteCssClass:"delete-row",emptyCssClass:"empty-row",formCssClass:"dynamic-form",added:null,removed:null}})(django.jQuery); a.prefix,d)});return!1});a.added&&a.added(d);return!1})}return this};b.fn.formset.defaults={prefix:"form",addText:"add another",deleteText:"remove",addCssClass:"add-row",deleteCssClass:"delete-row",emptyCssClass:"empty-row",formCssClass:"dynamic-form",added:null,removed:null}})(django.jQuery);
// Puts the included jQuery into our own namespace /* Puts the included jQuery into our own namespace using noConflict and passing
* it 'true'. This ensures that the included jQuery doesn't pollute the global
* namespace (i.e. this preserves pre-existing values for both window.$ and
* window.jQuery).
*/
var django = { var django = {
"jQuery": jQuery.noConflict(true) "jQuery": jQuery.noConflict(true)
}; };
(function(a){a.fn.prepopulate=function(d,g){return this.each(function(){var b=a(this);b.data("_changed",false);b.change(function(){b.data("_changed",true)});var c=function(){if(b.data("_changed")!=true){var e=[];a.each(d,function(h,f){a(f).val().length>0&&e.push(a(f).val())});b.val(URLify(e.join(" "),g))}};a(d.join(",")).keyup(c).change(c).focus(c)})}})(django.jQuery); (function(a){a.fn.prepopulate=function(d,e){return this.each(function(){var b=a(this);b.data("_changed",!1);b.change(function(){b.data("_changed",!0)});var c=function(){if(!0!=b.data("_changed")){var c=[];a.each(d,function(b,d){0<a(d).val().length&&c.push(a(d).val())});b.val(URLify(c.join(" "),e))}};a(d.join(",")).keyup(c).change(c).focus(c)})}})(django.jQuery);
class @Navigation class @Navigation
constructor: -> constructor: ->
if $('#accordion').length if $('#accordion').length
# First look for an active section
active = $('#accordion ul:has(li.active)').index('#accordion ul') active = $('#accordion ul:has(li.active)').index('#accordion ul')
# if we didn't find one, look for an active chapter
if active < 0
active = $('#accordion h3.active').index('#accordion h3')
# if that didn't work either, default to 0
if active < 0
active = 0
$('#accordion').bind('accordionchange', @log).accordion $('#accordion').bind('accordionchange', @log).accordion
active: if active >= 0 then active else 1 active: active
header: 'h3' header: 'h3'
autoHeight: false autoHeight: false
$('#open_close_accordion a').click @toggle $('#open_close_accordion a').click @toggle
......
...@@ -89,3 +89,9 @@ ...@@ -89,3 +89,9 @@
border: 1px solid rgb(6, 65, 18); border: 1px solid rgb(6, 65, 18);
color: rgb(255, 255, 255); color: rgb(255, 255, 255);
} }
.global {
h2 {
display: none;
}
}
\ No newline at end of file
...@@ -20,7 +20,7 @@ div.info-wrapper { ...@@ -20,7 +20,7 @@ div.info-wrapper {
> li { > li {
@extend .clearfix; @extend .clearfix;
border-bottom: 1px solid #e3e3e3; border-bottom: 1px solid lighten($border-color, 10%);
margin-bottom: lh(); margin-bottom: lh();
padding-bottom: lh(.5); padding-bottom: lh(.5);
list-style-type: disk; list-style-type: disk;
...@@ -76,97 +76,97 @@ div.info-wrapper { ...@@ -76,97 +76,97 @@ div.info-wrapper {
h1 { h1 {
@extend .bottom-border; @extend .bottom-border;
padding: lh(.5) lh(.5); padding: lh(.5) lh(.5);
} margin-bottom: 0;
header {
// h1 {
// font-weight: 100;
// font-style: italic;
// }
p {
color: #666;
font-size: 12px;
margin-bottom: 0;
margin-top: 4px;
}
} }
ol { ol {
background: none;
list-style: none;
padding-left: 0;
li { li {
@extend .clearfix; a {
background: none; display: block;
border-bottom: 1px solid #d3d3d3; padding-left: lh(.5);
@include box-shadow(0 1px 0 #eee); padding-right: 0;
@include box-sizing(border-box); }
padding: em(7) lh(.75);
position: relative;
&.expandable, &.expandable,
&.collapsable { &.collapsable {
h4 { h4 {
font-style: $body-font-size;
font-weight: normal; font-weight: normal;
padding-left: 18px; font-size: 1em;
padding: lh(.25) 0 lh(.25) lh(1.5);
}
}
&.multiple {
padding: lh(.5) 0 lh(.5) lh(.5);
a {
@include inline-block;
padding: 0;
&:hover {
background: transparent;
}
} }
} }
ul { ul {
background: none; background: none;
margin: em(7) (-(lh(.75))) 0; margin: 0;
li { li {
border-bottom: 0; border-bottom: 0;
border-top: 1px solid #d3d3d3; border-top: 1px solid $border-color;
@include box-shadow(inset 0 1px 0 #eee); @include box-shadow(inset 0 1px 0 #eee);
padding-left: lh(1.5); font-size: 1em;
} padding: lh(.5) 0 lh(.5) lh(.5);
}
&:hover { a {
background-color: #e9e9e9; @include inline-block;
padding: 0;
&:hover {
background: transparent;
}
}
}
} }
div.hitarea { div.hitarea {
background-image: url('../images/treeview-default.gif'); background-image: url('../images/treeview-default.gif');
display: block; display: block;
height: 100%; height: 100%;
left: lh(.75);
margin-left: 0; margin-left: 0;
max-height: 20px; max-height: 30px;
position: absolute; position: absolute;
width: 100%; width: 100%;
&:hover { &:hover {
opacity: 0.6; opacity: 0.6;
filter: alpha(opacity=60); filter: alpha(opacity=60);
+ h4 {
background-color: #e3e3e3;
}
} }
&.expandable-hitarea { &.expandable-hitarea {
background-position: -80px 1px; background-position: -72px 7px;
} }
&.collapsable-hitarea { &.collapsable-hitarea {
background-position: -64px -21px; background-position: -55px -15px;
} }
} }
h3 { h3 {
border-bottom: 0; border-bottom: 0;
@include box-shadow(none); @include box-shadow(none);
color: #999; color: #aaa;
font-size: $body-font-size; font-size: 1em;
font-weight: bold; margin-bottom: em(6);
text-transform: uppercase;
} }
p { p {
font-size: $body-font-size;
letter-spacing: 0; letter-spacing: 0;
margin: 0; margin: 0;
text-transform: none; text-transform: none;
...@@ -189,17 +189,6 @@ div.info-wrapper { ...@@ -189,17 +189,6 @@ div.info-wrapper {
} }
} }
} }
a {
color: lighten($text-color, 10%);
@include inline-block();
text-decoration: none;
@include transition();
&:hover {
color: $mit-red;
}
}
} }
} }
} }
......
...@@ -10,38 +10,26 @@ div.profile-wrapper { ...@@ -10,38 +10,26 @@ div.profile-wrapper {
header { header {
@extend .bottom-border; @extend .bottom-border;
margin: 0 ; margin: 0;
padding: lh(.5) lh(); padding: lh(.5);
h1 { h1 {
font-size: 18px;
margin: 0; margin: 0;
padding-right: 30px; padding-right: 30px;
} }
a {
color: #999;
font-size: 12px;
position: absolute;
right: lh(.5);
text-transform: uppercase;
top: 13px;
&:hover {
color: #555;
}
}
} }
ul { ul {
list-style: none; list-style: none;
padding: 0;
margin: 0;
li { li {
border-bottom: 1px solid #d3d3d3; border-bottom: 1px solid #d3d3d3;
@include box-shadow(0 1px 0 #eee); @include box-shadow(0 1px 0 #eee);
color: lighten($text-color, 10%); color: lighten($text-color, 10%);
display: block; display: block;
padding: 7px lh(); padding: lh(.5) 0 lh(.5) lh(.5);
position: relative; position: relative;
text-decoration: none; text-decoration: none;
@include transition(); @include transition();
...@@ -144,11 +132,14 @@ div.profile-wrapper { ...@@ -144,11 +132,14 @@ div.profile-wrapper {
@extend .content; @extend .content;
header { header {
@extend h1.top-header;
@extend .clearfix; @extend .clearfix;
@extend h1.top-header;
margin-bottom: lh();
h1 { h1 {
float: left; float: left;
font-size: 1em;
font-weight: 100;
margin: 0; margin: 0;
} }
} }
...@@ -162,6 +153,7 @@ div.profile-wrapper { ...@@ -162,6 +153,7 @@ div.profile-wrapper {
border-top: 1px solid #e3e3e3; border-top: 1px solid #e3e3e3;
list-style: none; list-style: none;
margin-top: lh(); margin-top: lh();
padding-left: 0;
> li { > li {
@extend .clearfix; @extend .clearfix;
...@@ -178,9 +170,11 @@ div.profile-wrapper { ...@@ -178,9 +170,11 @@ div.profile-wrapper {
border-right: 1px dashed #ddd; border-right: 1px dashed #ddd;
@include box-sizing(border-box); @include box-sizing(border-box);
display: table-cell; display: table-cell;
letter-spacing: 0;
margin: 0; margin: 0;
padding: 0; padding: 0;
padding-right: flex-gutter(9); padding-right: flex-gutter(9);
text-transform: none;
width: flex-grid(2, 9); width: flex-grid(2, 9);
} }
...@@ -203,14 +197,39 @@ div.profile-wrapper { ...@@ -203,14 +197,39 @@ div.profile-wrapper {
h3 { h3 {
color: #666; color: #666;
span {
color: #999;
font-size: em(14);
font-weight: 100;
}
}
p {
color: #999;
font-size: em(14);
} }
ol { section.scores {
list-style: none; margin: lh(.5) 0;
h3 {
font-size: em(14);
@include inline-block;
}
li { ol {
display: inline-block; list-style: none;
padding-right: 1em; margin: 0;
padding: 0;
@include inline-block;
li {
@include inline-block;
font-size: em(14);
font-weight: normal;
padding-right: 1em;
}
} }
} }
} }
......
...@@ -7,19 +7,46 @@ div.book-wrapper { ...@@ -7,19 +7,46 @@ div.book-wrapper {
@include box-sizing(border-box); @include box-sizing(border-box);
ul#booknav { ul#booknav {
font-size: 12px; font-size: em(14);
a { .chapter-number {
color: #000;
}
&:hover { .chapter {
color: #666; float: left;
} width: 87%;
line-height: 1.4em;
}
.page-number {
float: right;
width: 12%;
font-size: .8em;
line-height: 2.1em;
text-align: right;
color: #9a9a9a;
opacity: 0;
@include transition(opacity .15s);
} }
li { li {
background: none; background: none;
padding-left: 30px; border-bottom: 0;
padding-left: lh();
a {
padding: 0;
@include clearfix;
&:hover {
background-color: transparent;
.page-number {
opacity: 1;
}
}
}
div.hitarea { div.hitarea {
background-image: url('../images/treeview-default.gif'); background-image: url('../images/treeview-default.gif');
...@@ -35,12 +62,18 @@ div.book-wrapper { ...@@ -35,12 +62,18 @@ div.book-wrapper {
ul { ul {
background: none; background: none;
margin-top: lh(.25);
border-top: 1px solid $border-color;
padding-top: lh(.25);
li {
padding-bottom: lh(.25);
}
} }
} }
> li { > li {
border-bottom: 1px solid #d3d3d3; border-bottom: 1px solid $border-color;
@include box-shadow(0 1px 0 #eee);
padding: 7px 7px 7px 30px; padding: 7px 7px 7px 30px;
} }
} }
...@@ -48,9 +81,11 @@ div.book-wrapper { ...@@ -48,9 +81,11 @@ div.book-wrapper {
section.book { section.book {
@extend .content; @extend .content;
padding-bottom: 0;
padding-right: 0;
padding-top: 0;
nav { nav {
@extend .topbar;
@extend .clearfix; @extend .clearfix;
a { a {
...@@ -62,42 +97,62 @@ div.book-wrapper { ...@@ -62,42 +97,62 @@ div.book-wrapper {
@extend .clearfix; @extend .clearfix;
li { li {
position: absolute;
height: 100%;
width: flex-grid(2, 8);
a {
background-color: rgba(#000, .7);
background-position: center;
background-repeat: no-repeat;
@include box-sizing(border-box);
display: table;
height: 100%;
opacity: 0;
filter: alpha(opacity=0);
text-indent: -9999px;
@include transition;
vertical-align: middle;
width: 100%;
&:hover {
opacity: 1;
filter: alpha(opacity=100);
}
}
&.last { &.last {
display: block; left: 0;
float: left;
a { a {
border-left: 0; background-image: url('../images/textbook/textbook-left.png');
border-right: 1px solid darken(#f6efd4, 20%);
@include box-shadow(inset -1px 0 0 lighten(#f6efd4, 5%));
} }
} }
&.next { &.next {
display: block; right: 0;
float: right;
}
&:hover { a {
background: none; background-image: url('../images/textbook/textbook-right.png');
}
} }
} }
} }
&.bottom-nav { &.bottom-nav {
border-bottom: 0; border-bottom: 0;
border-top: 1px solid #EDDFAA;
margin-bottom: -(lh()); margin-bottom: -(lh());
margin-top: lh(); margin-top: lh();
} }
} }
section.page { section.page {
border: 1px solid $border-color;
position: relative;
text-align: center; text-align: center;
img { img {
border: 1px solid $border-color;
max-width: 100%; max-width: 100%;
} }
} }
......
body { body {
font-family: $sans-serif; min-width: 980px;
} }
h1, h2, h3, h4, h5, h6 { body, h1, h2, h3, h4, h5, h6, p, p a:link, p a:visited, a {
font-family: $sans-serif; font-family: $sans-serif;
} }
...@@ -19,3 +19,14 @@ table { ...@@ -19,3 +19,14 @@ table {
table-layout: fixed; table-layout: fixed;
} }
} }
form {
label {
display: block;
}
}
::selection, ::-moz-selection, ::-webkit-selection {
background:#444;
color:#fff;
}
h1.top-header { h1.top-header {
border-bottom: 1px solid #e3e3e3; border-bottom: 1px solid #e3e3e3;
text-align: left; text-align: left;
font-size: 24px; font-size: em(24);
font-weight: 100; font-weight: 100;
padding-bottom: lh(); padding-bottom: lh();
} }
...@@ -51,7 +51,6 @@ h1.top-header { ...@@ -51,7 +51,6 @@ h1.top-header {
.sidebar { .sidebar {
border-right: 1px solid #C8C8C8; border-right: 1px solid #C8C8C8;
@include box-shadow(inset -1px 0 0 #e6e6e6);
@include box-sizing(border-box); @include box-sizing(border-box);
display: table-cell; display: table-cell;
font-family: $sans-serif; font-family: $sans-serif;
...@@ -60,7 +59,7 @@ h1.top-header { ...@@ -60,7 +59,7 @@ h1.top-header {
width: flex-grid(3); width: flex-grid(3);
h1, h2 { h1, h2 {
font-size: em(18); font-size: em(20);
font-weight: 100; font-weight: 100;
letter-spacing: 0; letter-spacing: 0;
text-transform: none; text-transform: none;
...@@ -75,7 +74,7 @@ h1.top-header { ...@@ -75,7 +74,7 @@ h1.top-header {
} }
.bottom-border { .bottom-border {
border-bottom: 1px solid #d3d3d3; border-bottom: 1px solid $border-color;
} }
@media print { @media print {
...@@ -89,22 +88,16 @@ h1.top-header { ...@@ -89,22 +88,16 @@ h1.top-header {
font-weight: normal; font-weight: normal;
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
font-size: 1em;
a { a {
color: lighten($text-color, 10%);
display: block; display: block;
font-size: $body-font-size;
padding: 7px 7px 7px 30px;
text-decoration: none; text-decoration: none;
@include transition(); @include transition();
} }
// span.ui-icon {
// background-image: url(../images/ui-icons_454545_256x240.png);
// }
&.active { &.active {
border-bottom: 1px solid #d3d3d3; @extend .bottom-border;
color: #000; color: #000;
font-weight: bold; font-weight: bold;
...@@ -114,6 +107,33 @@ h1.top-header { ...@@ -114,6 +107,33 @@ h1.top-header {
} }
} }
ul, ol {
list-style: none;
margin: 0;
padding-left: 0;
li {
@extend .bottom-border;
@extend .clearfix;
background: none;
position: relative;
padding: 0;
a {
display: block;
line-height: lh();
font-size: 1em;
@include box-sizing(border-box);
padding: lh(.25) lh(.5) lh(.25) 0;
&:hover {
color: #666;
background: #f6f6f6;
}
}
}
}
header#open_close_accordion { header#open_close_accordion {
position: relative; position: relative;
...@@ -165,3 +185,30 @@ h1.top-header { ...@@ -165,3 +185,30 @@ h1.top-header {
.tran { .tran {
@include transition( all, .2s, $ease-in-out-quad); @include transition( all, .2s, $ease-in-out-quad);
} }
.global {
.find-courses-button {
display: none;
}
h2 {
display: block;
width: 700px;
float: left;
font-size: 0.9em;
font-weight: 600;
line-height: 40px;
letter-spacing: 0;
text-transform: none;
text-shadow: 0 1px 0 #fff;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
.provider {
font: inherit;
font-weight: bold;
color: #6d6d6d;
}
}
}
\ No newline at end of file
...@@ -105,12 +105,14 @@ div.course-wrapper { ...@@ -105,12 +105,14 @@ div.course-wrapper {
ul { ul {
margin: 0; margin: 0;
@include clearfix(); @include clearfix();
padding: 0;
li { li {
width: flex-grid(3, 9); width: flex-grid(3, 9);
float: left; float: left;
margin-right: flex-gutter(9); margin-right: flex-gutter(9);
margin-bottom: lh(); margin-bottom: lh();
line-height: lh();
&:nth-child(3n) { &:nth-child(3n) {
margin-right: 0; margin-right: 0;
......
...@@ -11,10 +11,10 @@ section.course-index { ...@@ -11,10 +11,10 @@ section.course-index {
} }
div#accordion { div#accordion {
h3 { h3 {
@include border-radius(0); @include border-radius(0);
border-top: 1px solid #e3e3e3; border-top: 1px solid #e3e3e3;
font-size: em(16, 18);
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
...@@ -23,8 +23,14 @@ section.course-index { ...@@ -23,8 +23,14 @@ section.course-index {
} }
&:hover { &:hover {
color: #666;
background: #f6f6f6; background: #f6f6f6;
text-decoration: none; }
&.ui-state-hover {
a {
color: #666;
}
} }
&.ui-accordion-header { &.ui-accordion-header {
...@@ -33,8 +39,6 @@ section.course-index { ...@@ -33,8 +39,6 @@ section.course-index {
a { a {
@include border-radius(0); @include border-radius(0);
@include box-shadow(none); @include box-shadow(none);
color: lighten($text-color, 10%);
font-size: $body-font-size;
} }
&.ui-state-active { &.ui-state-active {
...@@ -45,30 +49,35 @@ section.course-index { ...@@ -45,30 +49,35 @@ section.course-index {
background: none; background: none;
} }
} }
span.ui-icon {
background-image: url("/static/images/ui-icons_222222_256x240.png");
opacity: .3;
}
} }
} }
ul.ui-accordion-content { ul.ui-accordion-content {
@include border-radius(0);
background: transparent; background: transparent;
border: none; border: none;
font-size: 12px; @include border-radius(0);
font-size: em(14, 18);
margin: 0; margin: 0;
padding: 1em 1.5em; padding: 1em 1.5em;
li { li {
border-bottom: 0;
@include border-radius(0); @include border-radius(0);
margin-bottom: lh(.5); margin-bottom: lh(.5);
a { a {
border: 1px solid transparent;
background: transparent; background: transparent;
border: 1px solid transparent;
@include border-radius(4px); @include border-radius(4px);
position: relative; display: block;
padding: 5px 36px 5px 10px; padding: 5px 36px 5px 10px;
position: relative;
text-decoration: none; text-decoration: none;
display: block;
color: #666;
p { p {
font-weight: bold; font-weight: bold;
...@@ -84,16 +93,16 @@ section.course-index { ...@@ -84,16 +93,16 @@ section.course-index {
&:after { &:after {
background: transparent; background: transparent;
border-top: 1px solid rgb(180,180,180);
border-right: 1px solid rgb(180,180,180); border-right: 1px solid rgb(180,180,180);
border-top: 1px solid rgb(180,180,180);
content: ""; content: "";
display: block; display: block;
height: 12px; height: 12px;
margin-top: -6px; margin-top: -6px;
opacity: 0; opacity: 0;
position: absolute; position: absolute;
top: 50%;
right: 30px; right: 30px;
top: 50%;
@include transform(rotate(45deg)); @include transform(rotate(45deg));
width: 12px; width: 12px;
} }
...@@ -142,7 +151,6 @@ section.course-index { ...@@ -142,7 +151,6 @@ section.course-index {
span.subtitle { span.subtitle {
font-weight: normal; font-weight: normal;
} }
} }
} }
} }
......
...@@ -2,80 +2,49 @@ div#wiki_panel { ...@@ -2,80 +2,49 @@ div#wiki_panel {
@extend .sidebar; @extend .sidebar;
overflow: auto; overflow: auto;
h2 {
@extend .bottom-border;
font-size: 18px;
margin: 0 ;
padding: lh(.5) lh();
}
input[type="button"] {
background: transparent;
border: none;
@include box-shadow(none);
color: #666;
font-size: 14px;
font-weight: bold;
margin: 0px;
padding: 7px lh();
text-align: left;
@include transition();
width: 100%;
}
ul { ul {
li { li {
@include box-shadow(inset 0 1px 0 0 #eee);
border-top: 1px solid #d3d3d3;
&:hover {
background: #efefef;
@include background-image(linear-gradient(-90deg, rgb(245,245,245), rgb(225,225,225)));
}
&:first-child {
border: none;
}
&.search { &.search {
padding: 10px lh(); padding: 10px lh() 10px 0;
label { label {
display: none; display: none;
} }
} }
}
}
&.create-article { form {
h3 { input[type="submit"]{
} @extend .light-button;
} text-transform: none;
text-shadow: none;
a {
color: #666;
font-size: 14px;
padding: 7px lh();
}
} }
} }
div#wiki_create_form { div#wiki_create_form {
@extend .clearfix; @extend .clearfix;
background: #dadada; padding: lh(.5) lh() lh(.5) 0;
border-bottom: 1px solid #d3d3d3;
padding: 15px; label {
font-family: $sans-serif;
margin-bottom: lh(.5);
}
input[type="text"] { input[type="text"] {
@include box-sizing(border-box); @include box-sizing(border-box);
display: block; display: block;
margin-bottom: 6px;
width: 100%; width: 100%;
margin-bottom: lh(.5);
} }
ul { ul {
list-style: none; list-style: none;
margin: 0;
li { li {
float: left; float: left;
border-bottom: 0;
&#cancel { &#cancel {
float: right; float: right;
......
body {
margin: 0;
padding: 0; }
.wrapper, .subpage, section.copyright, section.tos, section.privacy-policy, section.honor-code, header.announcement div, section.index-content, footer {
margin: 0;
overflow: hidden; }
div#enroll form {
display: none; }
/*
html5doctor.com Reset Stylesheet
v1.6.1
Last Updated: 2010-09-17
Author: Richard Clark - http://richclarkdesign.com
Twitter: @rich_clark
*/
html, body, div, span, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
abbr, address, cite, code,
del, dfn, em, img, ins, kbd, q, samp,
small, strong, var,
b, i,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, figcaption, figure,
footer, header, hgroup, menu, nav, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
outline: 0;
font-size: 100%;
vertical-align: baseline;
background: transparent; }
body {
line-height: 1; }
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block; }
nav ul {
list-style: none; }
blockquote, q {
quotes: none; }
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none; }
a {
margin: 0;
padding: 0;
font-size: 100%;
vertical-align: baseline;
background: transparent; }
/* change colours to suit your needs */
ins {
background-color: #ff9;
color: #000;
text-decoration: none; }
/* change colours to suit your needs */
mark {
background-color: #ff9;
color: #000;
font-style: italic;
font-weight: bold; }
del {
text-decoration: line-through; }
abbr[title], dfn[title] {
border-bottom: 1px dotted;
cursor: help; }
table {
border-collapse: collapse;
border-spacing: 0; }
/* change border colour to suit your needs */
hr {
display: block;
height: 1px;
border: 0;
border-top: 1px solid #cccccc;
margin: 1em 0;
padding: 0; }
input, select {
vertical-align: middle; }
/* Generated by Font Squirrel (http://www.fontsquirrel.com) on January 25, 2012 05:06:34 PM America/New_York */
@font-face {
font-family: 'Open Sans';
src: url("../fonts/OpenSans-Regular-webfont.eot");
src: url("../fonts/OpenSans-Regular-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/OpenSans-Regular-webfont.woff") format("woff"), url("../fonts/OpenSans-Regular-webfont.ttf") format("truetype"), url("../fonts/OpenSans-Regular-webfont.svg#OpenSansRegular") format("svg");
font-weight: 600;
font-style: normal; }
@font-face {
font-family: 'Open Sans';
src: url("../fonts/OpenSans-Italic-webfont.eot");
src: url("../fonts/OpenSans-Italic-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/OpenSans-Italic-webfont.woff") format("woff"), url("../fonts/OpenSans-Italic-webfont.ttf") format("truetype"), url("../fonts/OpenSans-Italic-webfont.svg#OpenSansItalic") format("svg");
font-weight: 400;
font-style: italic; }
@font-face {
font-family: 'Open Sans';
src: url("../fonts/OpenSans-Bold-webfont.eot");
src: url("../fonts/OpenSans-Bold-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/OpenSans-Bold-webfont.woff") format("woff"), url("../fonts/OpenSans-Bold-webfont.ttf") format("truetype"), url("../fonts/OpenSans-Bold-webfont.svg#OpenSansBold") format("svg");
font-weight: 700;
font-style: normal; }
@font-face {
font-family: 'Open Sans';
src: url("../fonts/OpenSans-BoldItalic-webfont.eot");
src: url("../fonts/OpenSans-BoldItalic-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/OpenSans-BoldItalic-webfont.woff") format("woff"), url("../fonts/OpenSans-BoldItalic-webfont.ttf") format("truetype"), url("../fonts/OpenSans-BoldItalic-webfont.svg#OpenSansBoldItalic") format("svg");
font-weight: 700;
font-style: italic; }
@font-face {
font-family: 'Open Sans';
src: url("../fonts/OpenSans-ExtraBold-webfont.eot");
src: url("../fonts/OpenSans-ExtraBold-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/OpenSans-ExtraBold-webfont.woff") format("woff"), url("../fonts/OpenSans-ExtraBold-webfont.ttf") format("truetype"), url("../fonts/OpenSans-ExtraBold-webfont.svg#OpenSansExtrabold") format("svg");
font-weight: 800;
font-style: normal; }
@font-face {
font-family: 'Open Sans';
src: url("../fonts/OpenSans-ExtraBoldItalic-webfont.eot");
src: url("../fonts/OpenSans-ExtraBoldItalic-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/OpenSans-ExtraBoldItalic-webfont.woff") format("woff"), url("../fonts/OpenSans-ExtraBoldItalic-webfont.ttf") format("truetype"), url("../fonts/OpenSans-ExtraBoldItalic-webfont.svg#OpenSansExtraboldItalic") format("svg");
font-weight: 800;
font-style: italic; }
.wrapper, .subpage, section.copyright, section.tos, section.privacy-policy, section.honor-code, header.announcement div, footer, section.index-content {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
margin: 0 auto;
max-width: 1400px;
padding: 25.888px;
width: 100%; }
.subpage > div, section.copyright > div, section.tos > div, section.privacy-policy > div, section.honor-code > div {
padding-left: 34.171%; }
@media screen and (max-width: 940px) {
.subpage > div, section.copyright > div, section.tos > div, section.privacy-policy > div, section.honor-code > div {
padding-left: 0; } }
.subpage > div p, section.copyright > div p, section.tos > div p, section.privacy-policy > div p, section.honor-code > div p {
margin-bottom: 25.888px;
line-height: 25.888px; }
.subpage > div h1, section.copyright > div h1, section.tos > div h1, section.privacy-policy > div h1, section.honor-code > div h1 {
margin-bottom: 12.944px; }
.subpage > div h2, section.copyright > div h2, section.tos > div h2, section.privacy-policy > div h2, section.honor-code > div h2 {
font: 18px "Open Sans", Helvetica, Arial, sans-serif;
color: #000;
margin-bottom: 12.944px; }
.subpage > div ul, section.copyright > div ul, section.tos > div ul, section.privacy-policy > div ul, section.honor-code > div ul {
list-style: disc outside none; }
.subpage > div ul li, section.copyright > div ul li, section.tos > div ul li, section.privacy-policy > div ul li, section.honor-code > div ul li {
list-style: disc outside none;
line-height: 25.888px; }
.subpage > div dl, section.copyright > div dl, section.tos > div dl, section.privacy-policy > div dl, section.honor-code > div dl {
margin-bottom: 25.888px; }
.subpage > div dl dd, section.copyright > div dl dd, section.tos > div dl dd, section.privacy-policy > div dl dd, section.honor-code > div dl dd {
margin-bottom: 12.944px; }
.clearfix:after, .subpage:after, section.copyright:after, section.tos:after, section.privacy-policy:after, section.honor-code:after, header.announcement div section:after, footer:after, section.index-content:after, section.index-content section:after, section.index-content section.about section:after, div.leanModal_box#enroll ol:after {
content: ".";
display: block;
height: 0;
clear: both;
visibility: hidden; }
.button, header.announcement div section.course section a, section.index-content section.course a, section.index-content section.staff a, section.index-content section.about-course section.cta a.enroll {
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
-ms-border-radius: 3px;
-o-border-radius: 3px;
border-radius: 3px;
display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: baseline;
zoom: 1;
*display: inline;
*vertical-align: auto;
-webkit-transition-property: all;
-moz-transition-property: all;
-ms-transition-property: all;
-o-transition-property: all;
transition-property: all;
-webkit-transition-duration: 0.15s;
-moz-transition-duration: 0.15s;
-ms-transition-duration: 0.15s;
-o-transition-duration: 0.15s;
transition-duration: 0.15s;
-webkit-transition-timing-function: ease-out;
-moz-transition-timing-function: ease-out;
-ms-transition-timing-function: ease-out;
-o-transition-timing-function: ease-out;
transition-timing-function: ease-out;
-webkit-transition-delay: 0;
-moz-transition-delay: 0;
-ms-transition-delay: 0;
-o-transition-delay: 0;
transition-delay: 0;
background-color: #993333;
border: 1px solid #732626;
color: #fff;
margin: 25.888px 0 12.944px;
padding: 6.472px 12.944px;
text-decoration: none;
font-style: normal;
-webkit-box-shadow: inset 0 1px 0 #b83d3d;
-moz-box-shadow: inset 0 1px 0 #b83d3d;
box-shadow: inset 0 1px 0 #b83d3d;
-webkit-font-smoothing: antialiased; }
.button:hover, header.announcement div section.course section a:hover, section.index-content section.course a:hover, section.index-content section.staff a:hover, section.index-content section.about-course section.cta a.enroll:hover {
background-color: #732626;
border-color: #4d1919; }
.button span, header.announcement div section.course section a span, section.index-content section.course a span, section.index-content section.staff a span, section.index-content section.about-course section.cta a.enroll span {
font-family: Garamond, Baskerville, "Baskerville Old Face", "Hoefler Text", "Times New Roman", serif;
font-style: italic; }
p.ie-warning {
display: block !important;
line-height: 1.3em;
background: yellow;
margin-bottom: 25.888px;
padding: 25.888px; }
body {
background-color: #fff;
color: #444;
font: 16px Georgia, serif; }
body :focus {
outline-color: #ccc; }
body h1 {
font: 800 24px "Open Sans", Helvetica, Arial, sans-serif; }
body li {
margin-bottom: 25.888px; }
body em {
font-style: italic; }
body a {
color: #993333;
font-style: italic;
text-decoration: none; }
body a:hover, body a:focus {
color: #732626; }
body input[type="email"], body input[type="number"], body input[type="password"], body input[type="search"], body input[type="tel"], body input[type="text"], body input[type="url"], body input[type="color"], body input[type="date"], body input[type="datetime"], body input[type="datetime-local"], body input[type="month"], body input[type="time"], body input[type="week"], body textarea {
-webkit-box-shadow: 0 -1px 0 white;
-moz-box-shadow: 0 -1px 0 white;
box-shadow: 0 -1px 0 white;
background-color: #eeeeee;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #eeeeee), color-stop(100%, white));
background-image: -webkit-linear-gradient(top, #eeeeee, white);
background-image: -moz-linear-gradient(top, #eeeeee, white);
background-image: -ms-linear-gradient(top, #eeeeee, white);
background-image: -o-linear-gradient(top, #eeeeee, white);
background-image: linear-gradient(top, #eeeeee, white);
border: 1px solid #999;
font: 16px Georgia, serif;
padding: 4px;
width: 100%; }
body input[type="email"]:focus, body input[type="number"]:focus, body input[type="password"]:focus, body input[type="search"]:focus, body input[type="tel"]:focus, body input[type="text"]:focus, body input[type="url"]:focus, body input[type="color"]:focus, body input[type="date"]:focus, body input[type="datetime"]:focus, body input[type="datetime-local"]:focus, body input[type="month"]:focus, body input[type="time"]:focus, body input[type="week"]:focus, body textarea:focus {
border-color: #993333; }
header.announcement {
-webkit-background-size: cover;
-moz-background-size: cover;
-ms-background-size: cover;
-o-background-size: cover;
background-size: cover;
background: #333;
border-bottom: 1px solid #000;
color: #fff;
-webkit-font-smoothing: antialiased; }
header.announcement.home {
background: #e3e3e3 url("../images/marketing/shot-5-medium.jpg"); }
@media screen and (min-width: 1200px) {
header.announcement.home {
background: #e3e3e3 url("../images/marketing/shot-5-large.jpg"); } }
header.announcement.home div {
padding: 258.88px 25.888px 77.664px; }
@media screen and (max-width:780px) {
header.announcement.home div {
padding: 64.72px 25.888px 51.776px; } }
header.announcement.home div nav h1 {
margin-right: 0; }
header.announcement.home div nav a.login {
display: none; }
header.announcement.course {
background: #e3e3e3 url("../images/marketing/course-bg-small.jpg"); }
@media screen and (min-width: 1200px) {
header.announcement.course {
background: #e3e3e3 url("../images/marketing/course-bg-large.jpg"); } }
@media screen and (max-width: 1199px) and (min-width: 700px) {
header.announcement.course {
background: #e3e3e3 url("../images/marketing/course-bg-medium.jpg"); } }
header.announcement.course div {
padding: 103.552px 25.888px 51.776px; }
@media screen and (max-width:780px) {
header.announcement.course div {
padding: 64.72px 25.888px 51.776px; } }
header.announcement div {
position: relative; }
header.announcement div nav {
position: absolute;
top: 0;
right: 25.888px;
-webkit-border-radius: 0 0 3px 3px;
-moz-border-radius: 0 0 3px 3px;
-ms-border-radius: 0 0 3px 3px;
-o-border-radius: 0 0 3px 3px;
border-radius: 0 0 3px 3px;
background: #333;
background: rgba(0, 0, 0, 0.7);
padding: 12.944px 25.888px; }
header.announcement div nav h1 {
display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: baseline;
zoom: 1;
*display: inline;
*vertical-align: auto;
margin-right: 12.944px; }
header.announcement div nav h1 a {
font: italic 800 18px "Open Sans", Helvetica, Arial, sans-serif;
color: #fff;
text-decoration: none; }
header.announcement div nav h1 a:hover, header.announcement div nav h1 a:focus {
color: #999; }
header.announcement div nav a.login {
text-decoration: none;
color: #fff;
font-size: 12px;
font-style: normal;
font-family: "Open Sans", Helvetica, Arial, sans-serif; }
header.announcement div nav a.login:hover, header.announcement div nav a.login:focus {
color: #999; }
header.announcement div section {
background: #993333;
display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: baseline;
zoom: 1;
*display: inline;
*vertical-align: auto;
margin-left: 34.171%;
padding: 25.888px 38.832px; }
@media screen and (max-width: 780px) {
header.announcement div section {
margin-left: 0; } }
header.announcement div section h1 {
font-family: "Open Sans";
font-size: 30px;
font-weight: 800;
display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: baseline;
zoom: 1;
*display: inline;
*vertical-align: auto;
line-height: 1.2em;
margin: 0 25.888px 0 0; }
header.announcement div section h2 {
font-family: "Open Sans";
font-size: 24px;
font-weight: 400;
display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: baseline;
zoom: 1;
*display: inline;
*vertical-align: auto;
line-height: 1.2em; }
header.announcement div section.course section {
float: left;
margin-left: 0;
margin-right: 3.817%;
padding: 0;
width: 48.092%; }
@media screen and (max-width: 780px) {
header.announcement div section.course section {
float: none;
width: 100%;
margin-right: 0; } }
header.announcement div section.course section a {
background-color: #4d1919;
border-color: #260d0d;
-webkit-box-shadow: inset 0 1px 0 #732626, 0 1px 0 #ac3939;
-moz-box-shadow: inset 0 1px 0 #732626, 0 1px 0 #ac3939;
box-shadow: inset 0 1px 0 #732626, 0 1px 0 #ac3939;
display: block;
padding: 12.944px 25.888px;
text-align: center; }
header.announcement div section.course section a:hover {
background-color: #732626;
border-color: #4d1919; }
header.announcement div section.course p {
width: 48.092%;
line-height: 25.888px;
float: left; }
@media screen and (max-width: 780px) {
header.announcement div section.course p {
float: none;
width: 100%; } }
footer {
padding-top: 0; }
footer div.footer-wrapper {
border-top: 1px solid #e5e5e5;
padding: 25.888px 0;
background: url("../images/marketing/mit-logo.png") right center no-repeat; }
@media screen and (max-width: 780px) {
footer div.footer-wrapper {
background-position: left bottom;
padding-bottom: 77.664px; } }
footer div.footer-wrapper a {
color: #888;
text-decoration: none;
-webkit-transition-property: all;
-moz-transition-property: all;
-ms-transition-property: all;
-o-transition-property: all;
transition-property: all;
-webkit-transition-duration: 0.15s;
-moz-transition-duration: 0.15s;
-ms-transition-duration: 0.15s;
-o-transition-duration: 0.15s;
transition-duration: 0.15s;
-webkit-transition-timing-function: ease-out;
-moz-transition-timing-function: ease-out;
-ms-transition-timing-function: ease-out;
-o-transition-timing-function: ease-out;
transition-timing-function: ease-out;
-webkit-transition-delay: 0;
-moz-transition-delay: 0;
-ms-transition-delay: 0;
-o-transition-delay: 0;
transition-delay: 0; }
footer div.footer-wrapper a:hover, footer div.footer-wrapper a:focus {
color: #666; }
footer div.footer-wrapper p {
display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: baseline;
zoom: 1;
*display: inline;
*vertical-align: auto;
margin-right: 25.888px; }
footer div.footer-wrapper ul {
display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: baseline;
zoom: 1;
*display: inline;
*vertical-align: auto; }
@media screen and (max-width: 780px) {
footer div.footer-wrapper ul {
margin-top: 25.888px; } }
footer div.footer-wrapper ul li {
display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: baseline;
zoom: 1;
*display: inline;
*vertical-align: auto;
margin-bottom: 0; }
footer div.footer-wrapper ul li:after {
content: ' |';
display: inline;
color: #ccc; }
footer div.footer-wrapper ul li:last-child:after {
content: none; }
footer div.footer-wrapper ul.social {
float: right;
margin-right: 60px;
position: relative;
top: -5px; }
@media screen and (max-width: 780px) {
footer div.footer-wrapper ul.social {
float: none; } }
footer div.footer-wrapper ul.social li {
float: left;
margin-right: 12.944px; }
footer div.footer-wrapper ul.social li:after {
content: none;
display: none; }
footer div.footer-wrapper ul.social li a {
display: block;
height: 29px;
width: 28px;
text-indent: -9999px; }
footer div.footer-wrapper ul.social li a:hover {
opacity: .8; }
footer div.footer-wrapper ul.social li.twitter a {
background: url("../images/marketing/twitter.png") 0 0 no-repeat; }
footer div.footer-wrapper ul.social li.facebook a {
background: url("../images/marketing/facebook.png") 0 0 no-repeat; }
footer div.footer-wrapper ul.social li.linkedin a {
background: url("../images/marketing/linkedin.png") 0 0 no-repeat; }
section.index-content section {
float: left; }
@media screen and (max-width: 780px) {
section.index-content section {
float: none;
width: auto;
margin-right: 0; } }
section.index-content section h1 {
font-size: 800 24px "Open Sans";
margin-bottom: 25.888px; }
section.index-content section p {
line-height: 25.888px;
margin-bottom: 25.888px; }
section.index-content section ul {
margin: 0; }
section.index-content section.about {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
border-right: 1px solid #e5e5e5;
margin-right: 2.513%;
padding-right: 1.256%;
width: 65.829%; }
@media screen and (max-width: 780px) {
section.index-content section.about {
width: 100%;
border-right: 0;
margin-right: 0;
padding-right: 0; } }
section.index-content section.about section {
margin-bottom: 25.888px; }
section.index-content section.about section p {
width: 48.092%;
float: left; }
@media screen and (max-width: 780px) {
section.index-content section.about section p {
float: none;
width: auto; } }
section.index-content section.about section p:nth-child(odd) {
margin-right: 3.817%; }
@media screen and (max-width: 780px) {
section.index-content section.about section p:nth-child(odd) {
margin-right: 0; } }
section.index-content section.about section.intro section {
margin-bottom: 0; }
section.index-content section.about section.intro section.intro-text {
margin-right: 3.817%;
width: 48.092%; }
@media screen and (max-width: 780px) {
section.index-content section.about section.intro section.intro-text {
margin-right: 0;
width: auto; } }
section.index-content section.about section.intro section.intro-text p {
margin-right: 0;
width: auto;
float: none; }
section.index-content section.about section.intro section.intro-video {
width: 48.092%; }
@media screen and (max-width: 780px) {
section.index-content section.about section.intro section.intro-video {
width: auto; } }
section.index-content section.about section.intro section.intro-video a {
display: block;
width: 100%; }
section.index-content section.about section.intro section.intro-video a img {
width: 100%; }
section.index-content section.about section.intro section.intro-video a span {
display: none; }
section.index-content section.about section.features {
border-top: 1px solid #E5E5E5;
padding-top: 25.888px;
margin-bottom: 0; }
section.index-content section.about section.features h2 {
text-transform: uppercase;
letter-spacing: 1px;
color: #888;
margin-bottom: 25.888px;
font-weight: normal;
font-size: 14px; }
section.index-content section.about section.features h2 span {
text-transform: none; }
section.index-content section.about section.features p {
width: auto;
clear: both; }
section.index-content section.about section.features p strong {
font-family: "Open sans";
font-weight: 800; }
section.index-content section.about section.features p a {
color: #993333;
text-decoration: none;
-webkit-transition-property: all;
-moz-transition-property: all;
-ms-transition-property: all;
-o-transition-property: all;
transition-property: all;
-webkit-transition-duration: 0.15s;
-moz-transition-duration: 0.15s;
-ms-transition-duration: 0.15s;
-o-transition-duration: 0.15s;
transition-duration: 0.15s;
-webkit-transition-timing-function: ease-out;
-moz-transition-timing-function: ease-out;
-ms-transition-timing-function: ease-out;
-o-transition-timing-function: ease-out;
transition-timing-function: ease-out;
-webkit-transition-delay: 0;
-moz-transition-delay: 0;
-ms-transition-delay: 0;
-o-transition-delay: 0;
transition-delay: 0; }
section.index-content section.about section.features p a:hover, section.index-content section.about section.features p a:focus {
color: #602020; }
section.index-content section.about section.features ul {
margin-bottom: 0; }
section.index-content section.about section.features ul li {
line-height: 25.888px;
width: 48.092%;
float: left;
margin-bottom: 12.944px; }
@media screen and (max-width: 780px) {
section.index-content section.about section.features ul li {
width: auto;
float: none; } }
section.index-content section.about section.features ul li:nth-child(odd) {
margin-right: 3.817%; }
@media screen and (max-width: 780px) {
section.index-content section.about section.features ul li:nth-child(odd) {
margin-right: 0; } }
section.index-content section.course, section.index-content section.staff {
width: 31.658%; }
@media screen and (max-width: 780px) {
section.index-content section.course, section.index-content section.staff {
width: auto; } }
section.index-content section.course h1, section.index-content section.staff h1 {
color: #888;
font: normal 16px Georgia, serif;
font-size: 14px;
letter-spacing: 1px;
margin-bottom: 25.888px;
text-transform: uppercase; }
section.index-content section.course h2, section.index-content section.staff h2 {
font: 800 24px "Open Sans", Helvetica, Arial, sans-serif; }
section.index-content section.course h3, section.index-content section.staff h3 {
font: 400 18px "Open Sans", Helvetica, Arial, sans-serif; }
section.index-content section.course a span.arrow, section.index-content section.staff a span.arrow {
color: rgba(255, 255, 255, 0.6);
font-style: normal;
display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: baseline;
zoom: 1;
*display: inline;
*vertical-align: auto;
padding-left: 10px; }
section.index-content section.course ul, section.index-content section.staff ul {
list-style: none; }
section.index-content section.course ul li img, section.index-content section.staff ul li img {
float: left;
margin-right: 12.944px; }
section.index-content section.course h2 {
padding-top: 129.44px;
background: url("../images/marketing/circuits-bg.jpg") 0 0 no-repeat;
-webkit-background-size: contain;
-moz-background-size: contain;
-ms-background-size: contain;
-o-background-size: contain;
background-size: contain; }
@media screen and (max-width: 998px) and (min-width: 781px) {
section.index-content section.course h2 {
background: url("../images/marketing/circuits-medium-bg.jpg") 0 0 no-repeat; } }
@media screen and (max-width: 780px) {
section.index-content section.course h2 {
padding-top: 129.44px;
background: url("../images/marketing/circuits-bg.jpg") 0 0 no-repeat; } }
@media screen and (min-width: 500px) and (max-width: 781px) {
section.index-content section.course h2 {
padding-top: 207.104px; } }
section.index-content section.course div.announcement p.announcement-button a {
margin-top: 0; }
section.index-content section.course div.announcement img {
max-width: 100%;
margin-bottom: 25.888px; }
section.index-content section.about-course {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
border-right: 1px solid #e5e5e5;
margin-right: 2.513%;
padding-right: 1.256%;
width: 65.829%; }
@media screen and (max-width: 780px) {
section.index-content section.about-course {
width: auto;
border-right: 0;
margin-right: 0;
padding-right: 0; } }
section.index-content section.about-course section {
width: 48.092%; }
@media screen and (max-width: 780px) {
section.index-content section.about-course section {
width: auto; } }
section.index-content section.about-course section.about-info {
margin-right: 3.817%; }
@media screen and (max-width: 780px) {
section.index-content section.about-course section.about-info {
margin-right: 0; } }
section.index-content section.about-course section.requirements {
clear: both;
width: 100%;
border-top: 1px solid #E5E5E5;
padding-top: 25.888px;
margin-bottom: 0; }
section.index-content section.about-course section.requirements p {
float: left;
width: 48.092%;
margin-right: 3.817%; }
@media screen and (max-width: 780px) {
section.index-content section.about-course section.requirements p {
margin-right: 0;
float: none;
width: auto; } }
section.index-content section.about-course section.requirements p:nth-child(odd) {
margin-right: 0; }
section.index-content section.about-course section.cta {
width: 100%;
text-align: center; }
section.index-content section.about-course section.cta a.enroll {
padding: 12.944px 51.776px;
display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: baseline;
zoom: 1;
*display: inline;
*vertical-align: auto;
text-align: center;
font: 800 18px "Open Sans", Helvetica, Arial, sans-serif; }
section.index-content section.staff h1 {
margin-top: 25.888px; }
#lean_overlay {
background: #000;
display: none;
height: 100%;
left: 0px;
position: fixed;
top: 0px;
width: 100%;
z-index: 100; }
div.leanModal_box {
background: #fff;
border: none;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
-ms-border-radius: 3px;
-o-border-radius: 3px;
border-radius: 3px;
-webkit-box-shadow: 0 0 6px black;
-moz-box-shadow: 0 0 6px black;
box-shadow: 0 0 6px black;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
display: none;
padding: 51.776px;
text-align: left; }
div.leanModal_box a.modal_close {
color: #aaa;
display: block;
font-style: normal;
height: 14px;
position: absolute;
right: 12px;
top: 12px;
width: 14px;
z-index: 2; }
div.leanModal_box a.modal_close:hover {
color: #993333;
text-decoration: none; }
div.leanModal_box h1 {
border-bottom: 1px solid #eee;
font-size: 24px;
margin-bottom: 25.888px;
margin-top: 0;
padding-bottom: 25.888px;
text-align: left; }
div.leanModal_box#enroll {
max-width: 600px; }
div.leanModal_box#enroll ol {
padding-top: 25.888px; }
div.leanModal_box#enroll ol li.terms, div.leanModal_box#enroll ol li.honor-code {
float: none;
width: auto; }
div.leanModal_box#enroll ol li div.tip {
display: none; }
div.leanModal_box#enroll ol li:hover div.tip {
background: #333;
color: #fff;
display: block;
font-size: 16px;
line-height: 25.888px;
margin: 0 0 0 -10px;
padding: 10px;
position: absolute;
-webkit-font-smoothing: antialiased;
width: 500px; }
div.leanModal_box form {
text-align: left; }
div.leanModal_box form div#enroll_error, div.leanModal_box form div#login_error, div.leanModal_box form div#pwd_error {
background-color: #333333;
border: black;
color: #fff;
font-family: "Open sans";
font-weight: bold;
letter-spacing: 1px;
margin: -25.888px -25.888px 25.888px;
padding: 12.944px;
text-shadow: 0 1px 0 #1a1a1a;
-webkit-font-smoothing: antialiased; }
div.leanModal_box form div#enroll_error:empty, div.leanModal_box form div#login_error:empty, div.leanModal_box form div#pwd_error:empty {
padding: 0; }
div.leanModal_box form ol {
list-style: none;
margin-bottom: 25.888px; }
div.leanModal_box form ol li {
margin-bottom: 12.944px; }
div.leanModal_box form ol li.terms, div.leanModal_box form ol li.remember {
border-top: 1px solid #eee;
clear: both;
float: none;
padding-top: 25.888px;
width: auto; }
div.leanModal_box form ol li.honor-code {
float: none;
width: auto; }
div.leanModal_box form ol li label {
display: block;
font-weight: bold; }
div.leanModal_box form ol li input[type="email"], div.leanModal_box form ol li input[type="number"], div.leanModal_box form ol li input[type="password"], div.leanModal_box form ol li input[type="search"], div.leanModal_box form ol li input[type="tel"], div.leanModal_box form ol li input[type="text"], div.leanModal_box form ol li input[type="url"], div.leanModal_box form ol li input[type="color"], div.leanModal_box form ol li input[type="date"], div.leanModal_box form ol li input[type="datetime"], div.leanModal_box form ol li input[type="datetime-local"], div.leanModal_box form ol li input[type="month"], div.leanModal_box form ol li input[type="time"], div.leanModal_box form ol li input[type="week"], div.leanModal_box form ol li textarea {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
width: 100%; }
div.leanModal_box form ol li input[type="checkbox"] {
margin-right: 10px; }
div.leanModal_box form ol li ul {
list-style: disc outside none;
margin: 12.944px 0 25.888px 25.888px; }
div.leanModal_box form ol li ul li {
color: #666;
float: none;
font-size: 14px;
list-style: disc outside none;
margin-bottom: 12.944px; }
div.leanModal_box form input[type="button"], div.leanModal_box form input[type="submit"] {
border: 1px solid #691b1b;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
-ms-border-radius: 3px;
-o-border-radius: 3px;
border-radius: 3px;
-webkit-box-shadow: inset 0 1px 0 0 #bc5c5c;
-moz-box-shadow: inset 0 1px 0 0 #bc5c5c;
box-shadow: inset 0 1px 0 0 #bc5c5c;
color: white;
display: inline;
font-size: 11px;
font-weight: bold;
background-color: #993333;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #993333), color-stop(100%, #761e1e));
background-image: -webkit-linear-gradient(top, #993333, #761e1e);
background-image: -moz-linear-gradient(top, #993333, #761e1e);
background-image: -ms-linear-gradient(top, #993333, #761e1e);
background-image: -o-linear-gradient(top, #993333, #761e1e);
background-image: linear-gradient(top, #993333, #761e1e);
padding: 6px 18px 7px;
text-shadow: 0 1px 0 #5d1414;
-webkit-background-clip: padding-box;
font-size: 18px;
padding: 12.944px; }
div.leanModal_box form input[type="button"]:hover, div.leanModal_box form input[type="submit"]:hover {
-webkit-box-shadow: inset 0 1px 0 0 #a44141;
-moz-box-shadow: inset 0 1px 0 0 #a44141;
box-shadow: inset 0 1px 0 0 #a44141;
cursor: pointer;
background-color: #823030;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #823030), color-stop(100%, #691c1c));
background-image: -webkit-linear-gradient(top, #823030, #691c1c);
background-image: -moz-linear-gradient(top, #823030, #691c1c);
background-image: -ms-linear-gradient(top, #823030, #691c1c);
background-image: -o-linear-gradient(top, #823030, #691c1c);
background-image: linear-gradient(top, #823030, #691c1c); }
div.leanModal_box form input[type="button"]:active, div.leanModal_box form input[type="submit"]:active {
border: 1px solid #691b1b;
-webkit-box-shadow: inset 0 0 8px 4px #5c1919, inset 0 0 8px 4px #5c1919, 0 1px 1px 0 #eeeeee;
-moz-box-shadow: inset 0 0 8px 4px #5c1919, inset 0 0 8px 4px #5c1919, 0 1px 1px 0 #eeeeee;
box-shadow: inset 0 0 8px 4px #5c1919, inset 0 0 8px 4px #5c1919, 0 1px 1px 0 #eeeeee; }
div#login {
min-width: 400px; }
div#login header {
border-bottom: 1px solid #ddd;
margin-bottom: 25.888px;
padding-bottom: 25.888px; }
div#login header h1 {
border-bottom: 0;
padding-bottom: 0;
margin-bottom: 6.472px; }
div#login ol li {
float: none;
width: auto; }
div.lost-password {
margin-top: 25.888px;
text-align: left; }
div.lost-password a {
color: #999; }
div.lost-password a:hover {
color: #444; }
div#pwd_reset p {
margin-bottom: 25.888px; }
div#pwd_reset input[type="email"] {
margin-bottom: 25.888px; }
div#apply_name_change,
div#change_email,
div#unenroll,
div#deactivate-account {
max-width: 700px; }
div#apply_name_change ul,
div#change_email ul,
div#unenroll ul,
div#deactivate-account ul {
list-style: none; }
div#apply_name_change ul li,
div#change_email ul li,
div#unenroll ul li,
div#deactivate-account ul li {
margin-bottom: 12.944px; }
div#apply_name_change ul li textarea, div#apply_name_change ul li input[type="email"], div#apply_name_change ul li input[type="number"], div#apply_name_change ul li input[type="password"], div#apply_name_change ul li input[type="search"], div#apply_name_change ul li input[type="tel"], div#apply_name_change ul li input[type="text"], div#apply_name_change ul li input[type="url"], div#apply_name_change ul li input[type="color"], div#apply_name_change ul li input[type="date"], div#apply_name_change ul li input[type="datetime"], div#apply_name_change ul li input[type="datetime-local"], div#apply_name_change ul li input[type="month"], div#apply_name_change ul li input[type="time"], div#apply_name_change ul li input[type="week"],
div#change_email ul li textarea,
div#change_email ul li input[type="email"],
div#change_email ul li input[type="number"],
div#change_email ul li input[type="password"],
div#change_email ul li input[type="search"],
div#change_email ul li input[type="tel"],
div#change_email ul li input[type="text"],
div#change_email ul li input[type="url"],
div#change_email ul li input[type="color"],
div#change_email ul li input[type="date"],
div#change_email ul li input[type="datetime"],
div#change_email ul li input[type="datetime-local"],
div#change_email ul li input[type="month"],
div#change_email ul li input[type="time"],
div#change_email ul li input[type="week"],
div#unenroll ul li textarea,
div#unenroll ul li input[type="email"],
div#unenroll ul li input[type="number"],
div#unenroll ul li input[type="password"],
div#unenroll ul li input[type="search"],
div#unenroll ul li input[type="tel"],
div#unenroll ul li input[type="text"],
div#unenroll ul li input[type="url"],
div#unenroll ul li input[type="color"],
div#unenroll ul li input[type="date"],
div#unenroll ul li input[type="datetime"],
div#unenroll ul li input[type="datetime-local"],
div#unenroll ul li input[type="month"],
div#unenroll ul li input[type="time"],
div#unenroll ul li input[type="week"],
div#deactivate-account ul li textarea,
div#deactivate-account ul li input[type="email"],
div#deactivate-account ul li input[type="number"],
div#deactivate-account ul li input[type="password"],
div#deactivate-account ul li input[type="search"],
div#deactivate-account ul li input[type="tel"],
div#deactivate-account ul li input[type="text"],
div#deactivate-account ul li input[type="url"],
div#deactivate-account ul li input[type="color"],
div#deactivate-account ul li input[type="date"],
div#deactivate-account ul li input[type="datetime"],
div#deactivate-account ul li input[type="datetime-local"],
div#deactivate-account ul li input[type="month"],
div#deactivate-account ul li input[type="time"],
div#deactivate-account ul li input[type="week"] {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
display: block;
width: 100%; }
div#apply_name_change ul li textarea,
div#change_email ul li textarea,
div#unenroll ul li textarea,
div#deactivate-account ul li textarea {
height: 60px; }
div#apply_name_change ul li input[type="submit"],
div#change_email ul li input[type="submit"],
div#unenroll ul li input[type="submit"],
div#deactivate-account ul li input[type="submit"] {
white-space: normal; }
div#feedback_div form ol li {
float: none;
width: 100%; }
div#feedback_div form ol li textarea#feedback_message {
height: 100px; }
...@@ -135,6 +135,18 @@ ...@@ -135,6 +135,18 @@
font-weight: 700; font-weight: 700;
margin-bottom: 15px; margin-bottom: 15px;
} }
ul {
padding-left: 50px;
}
li {
font-family: $serif;
font-size: 1em;
line-height: 1.6em;
color: #3c3c3c;
margin-bottom: 0.2em;
}
} }
} }
} }
......
...@@ -299,3 +299,7 @@ ...@@ -299,3 +299,7 @@
} }
} }
} }
.leanModal_box {
@extend .modal;
}
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%def name="make_chapter(chapter)"> <%def name="make_chapter(chapter)">
<h3><a href="#">${chapter['display_name']}</a></h3> <h3 ${' class="active"' if 'active' in chapter and chapter['active'] else ''}><a href="#">${chapter['display_name']}</a>
</h3>
<ul> <ul>
% for section in chapter['sections']: % for section in chapter['sections']:
......
<li><a href="javascript:goto_page(9)"> Contents ix </a>
<li><a href="javascript:goto_page(1)"> Preamble i </a>
<ul> <li><a href="javascript:goto_page(1)"> Comments on the Book i</a>
<li><a href="javascript:goto_page(4)"> About the Authors iv</a>
<li><a href="javascript:goto_page(7)"> Dedication vii</a>
<li><a href="javascript:goto_page(19)"> Preface xix </a>
<li><a href="javascript:goto_page(19)"> Approach xix </a>
<li><a href="javascript:goto_page(21)"> Overview xxi </a>
<li><a href="javascript:goto_page(23)"> Course Organization xxiii </a>
<li><a href="javascript:goto_page(23)"> Web Supplements xxiii </a>
<li><a href="javascript:goto_page(24)"> Acknowledgments xxiv </a>
</ul>
<li><a href="javascript:goto_page(27)"> 1 The Circuit Abstraction 3 </a>
<ul> <li><a href="javascript:goto_page(27)"> 1.1 The Power of Abstraction 3 </a>
<li><a href="javascript:goto_page(29)"> 1.2 The Lumped Circuit Abstraction 4 </a>
<li><a href="javascript:goto_page(33)"> 1.3 The Lumped Matter Discipline 9 </a>
<li><a href="javascript:goto_page(37)"> 1.4 Limitations of the Lumped Circuit Abstraction 13 </a>
<li><a href="javascript:goto_page(39)"> 1.5 Practical Two-Terminal Elements 15 </a>
<ul> <li><a href="javascript:goto_page(40)"> 1.5.1 Batteries 16 </a>
<li><a href="javascript:goto_page(42)"> 1.5.2 Linear Resistors 18 </a>
<li><a href="javascript:goto_page(49)"> 1.5.3 Associated Variables Convention 25 </a>
</ul> <li><a href="javascript:goto_page(53)"> 1.6 Ideal Two-Terminal Elements 29 </a>
<ul> <li><a href="javascript:goto_page(54)"> 1.6.1 Ideal Voltage Sources, Wires and Resistors 30 </a>
<li><a href="javascript:goto_page(56)"> 1.6.2 Element Laws 32 </a>
<li><a href="javascript:goto_page(57)"> 1.6.3 The Current Source 33 </a>
</ul> <li><a href="javascript:goto_page(60)"> 1.7 Modeling Physical Elements 36 </a>
<li><a href="javascript:goto_page(64)"> 1.8 Signal Representation 40 </a>
<ul> <li><a href="javascript:goto_page(65)"> 1.8.1 Analog Signals 41 </a>
<li><a href="javascript:goto_page(66)"> 1.8.3 Digital Signals 42 </a>
</ul> <li><a href="javascript:goto_page(70)"> 1.9 Summary 46 </a>
</ul> <li><a href="javascript:goto_page(77)"> 2 Resistive Networks 53 </a>
<ul> <li><a href="javascript:goto_page(78)"> 2.1 Terminology 54 </a>
<li><a href="javascript:goto_page(79)"> 2.2 Kirchhoff's Laws 55 </a>
<ul> <li><a href="javascript:goto_page(80)"> 2.2.1 KCL 56 </a>
<li><a href="javascript:goto_page(84)"> 2.2.1 KVL 60 </a>
</ul> <li><a href="javascript:goto_page(90)"> 2.3 Circuit Analysis: Basic Method 66 </a>
<ul> <li><a href="javascript:goto_page(91)"> 2.3.1 Single-Resistor Circuits 67 </a>
<li><a href="javascript:goto_page(94)"> 2.3.2 Quick Intuitive Analysis of Single-Resistor Circuits 70 </a>
<li><a href="javascript:goto_page(95)"> 2.3.3 Energy Conservation 71 </a>
<li><a href="javascript:goto_page(97)"> 2.3.4 Voltage and Current Dividers 73 </a>
<li><a href="javascript:goto_page(99)"> 2.3.4.1 Voltage Dividers 73 </a>
<li><a href="javascript:goto_page(100)"> 2.3.4.2 Resistors in Series 76 </a>
<li><a href="javascript:goto_page(104)"> 2.3.4.3 Current Dividers 80 </a>
<li><a href="javascript:goto_page(108)"> 2.3.4.4 Resistors in Parallel 82 </a>
<li><a href="javascript:goto_page(108)"> 2.3.5 A More Complex Circuit 84 </a>
</ul> <li><a href="javascript:goto_page(113)"> 2.4 Intuitive Method of Circuit Analysis 89 </a>
<li><a href="javascript:goto_page(119)"> 2.5 More Examples 95 </a>
<li><a href="javascript:goto_page(122)"> 2.6 Dependent Sources and the Control Concept 98 </a>
<ul> <li><a href="javascript:goto_page(126)"> 2.6.1 Circuits with Dependent Sources 102 </a>
</ul> <li><a href="javascript:goto_page(131)"> 2.7 A Formulation Suitable for a Computer Solution * 107 </a>
<li><a href="javascript:goto_page(132)"> 2.8 Summary 108 </a>
</ul> <li><a href="javascript:goto_page(143)"> 3 Network Theorems 119 </a>
<ul> <li><a href="javascript:goto_page(143)"> 3.1 Introduction 119 </a>
<li><a href="javascript:goto_page(143)"> 3.2 The Node Voltage 119 </a>
<li><a href="javascript:goto_page(149)"> 3.3 The Node Method 125 </a>
<ul> <li><a href="javascript:goto_page(154)"> 3.3.1 Node Method: A Second Example 130 </a>
<li><a href="javascript:goto_page(159)"> 3.3.2 Floating Independent Voltage Sources 135 </a>
<li><a href="javascript:goto_page(163)"> 3.3.3 Dependent Sources and the Node Method 139 </a>
<li><a href="javascript:goto_page(169)"> 3.3.4 The Conductance and Source Matrices * 145 </a>
</ul> <li><a href="javascript:goto_page(169)"> 3.4 Loop Method * 145 </a>
<li><a href="javascript:goto_page(169)"> 3.5 Superposition 145 </a>
<ul> <li><a href="javascript:goto_page(176)"> 3.5.1 Superposition Rules for Dependent Sources 152 </a>
</ul> <li><a href="javascript:goto_page(182)"> 3.6 Thevenin's Theorem and Norton's Theorem 158 </a>
<ul> <li><a href="javascript:goto_page(182)"> 3.6.1 The Thevenin Equivalent Network 158 </a>
<li><a href="javascript:goto_page(192)"> 3.6.2 The Norton Equivalent Network 168 </a>
<li><a href="javascript:goto_page(195)"> 3.6.3 More Examples 171 </a>
</ul> <li><a href="javascript:goto_page(201)"> 3.7 Summary 177 </a>
</ul> <li><a href="javascript:goto_page(217)"> 4 Analysis of Nonlinear Circuits 193 </a>
<ul> <li><a href="javascript:goto_page(217)"> 4.1 Introduction to Nonlinear Elements 193 </a>
<li><a href="javascript:goto_page(221)"> 4.2 Analytical Solutions 197 </a>
<li><a href="javascript:goto_page(227)"> 4.3 Graphical Analysis 203 </a>
<li><a href="javascript:goto_page(230)"> 4.4 Piecewise Linear Analysis 206 </a>
<ul> <li><a href="javascript:goto_page(238)"> 4.4.1 Improved Piecewise Linear Models for Nonlinear Elements * 214 </a>
</ul> <li><a href="javascript:goto_page(238)"> 4.5 Incremental Analysis 214 </a>
<li><a href="javascript:goto_page(253)"> 4.6 Summary 229 </a>
</ul> <li><a href="javascript:goto_page(267)"> 5 The Digital Abstraction 243 </a>
<ul> <li><a href="javascript:goto_page(269)"> 5.1 Voltage Levels and the Static Discipline 245 </a>
<li><a href="javascript:goto_page(256+24)"> 5.2 Boolean Logic 256 </a>
<li><a href="javascript:goto_page(258+24)"> 5.3 Combinational Gates 258 </a>
<li><a href="javascript:goto_page(261+24)"> 5.4 Standard Sum-of-Products Representation 261 </a>
<li><a href="javascript:goto_page(262+24)"> 5.5 Simplifying Logic Expressions * 262 </a>
<li><a href="javascript:goto_page(267+24)"> 5.6 Number Representation 267 </a>
<li><a href="javascript:goto_page(274+24)"> 5.7 Summary 274 </a>
</ul> <li><a href="javascript:goto_page(285+24)"> 6 The MOSFET Switch 285 </a>
<ul> <li><a href="javascript:goto_page(285+24)"> 6.1 The Switch 285 </a>
<li><a href="javascript:goto_page(288+24)"> 6.2 Logic Functions Using Switches 288 </a>
<li><a href="javascript:goto_page(298+24)"> 6.3 The MOSFET Device and Its S Model 298 </a>
<li><a href="javascript:goto_page(291+24)"> 6.4 MOSFET Switch Implementation of Logic Gates 291 </a>
<li><a href="javascript:goto_page(296+24)"> 6.5 Static Analysis Using the S Model 296 </a>
<li><a href="javascript:goto_page(300+24)"> 6.6 The SR Model of the MOSFET 300 </a>
<li><a href="javascript:goto_page(301+24)"> 6.7 Physical Structure of the MOSFET * 301 </a>
<li><a href="javascript:goto_page(306+24)"> 6.8 Static Analysis Using the SR Model 306 </a>
<ul> <li><a href="javascript:goto_page(311+24)"> 6.8.1 Static Analysis of the NAND Gate Using the SR Model 311 </a>
</ul> <li><a href="javascript:goto_page(314+24)"> 6.9 Signal Restoration 314 </a>
<ul> <li><a href="javascript:goto_page(314+24)"> 6.9.1 Signal Restoration and Gain 314 </a>
<li><a href="javascript:goto_page(317+24)"> 6.9.2 Signal Restoration and Nonlinearity 317 </a>
<li><a href="javascript:goto_page(318+24)"> 6.9.3 Buffer Characteristics and the Static Discipline 318 </a>
<li><a href="javascript:goto_page(319+24)"> 6.9.4 Inverter Transfer Characteristics and the Static Discipline 319 </a>
</ul> <li><a href="javascript:goto_page(320+24)"> 6.10 Power Consumption in Logic Gates 320 </a>
<li><a href="javascript:goto_page(321+24)"> 6.11 Active Pullups 321 </a>
<li><a href="javascript:goto_page(322+24)"> 6.12 Summary 322 </a>
</ul> <li><a href="javascript:goto_page(331+24)"> 7 The MOSFET Amplifier 331 </a>
<ul> <li><a href="javascript:goto_page(332+24)"> 7.1 Signal Amplification 332 </a>
<li><a href="javascript:goto_page(332+24)"> 7.2 Review of Dependent Sources 332 </a>
<li><a href="javascript:goto_page(335+24)"> 7.3 Actual MOSFET Characteristics 335 </a>
<li><a href="javascript:goto_page(340+24)"> 7.4 The Switch Current Source (SCS) MOSFET Model 340 </a>
<li><a href="javascript:goto_page(344+24)"> 7.5 The MOSFET Amplifier 344 </a>
<ul> <li><a href="javascript:goto_page(349+24)"> 7.5.1 Biasing the MOSFET Amplifier 349 </a>
<li><a href="javascript:goto_page(352+24)"> 7.5.2 The Amplifier Abstraction and the Saturation Discipline 352 </a>
</ul> <li><a href="javascript:goto_page(353+24)"> 7.6 Large Signal Analysis of the MOSFET Amplifier 353 </a>
<ul> <li><a href="javascript:goto_page(353+24)"> 7.6.1 v_IN versus v_OUT in the Saturation Region 353 </a>
<li><a href="javascript:goto_page(356+24)"> 7.6.2 Valid Input and Output Voltage Ranges 356 </a>
<li><a href="javascript:goto_page(363+24)"> 7.6.3 Alternative Method for Valid Input and Output Voltage Ranges 363 </a>z
</ul> <li><a href="javascript:goto_page(385+24)"> 7.7 Operating Point Selection 385 </a>
<li><a href="javascript:goto_page(386+24)"> 7.8 Switch Unified (SU) MOSFET Model * 386 </a>
<li><a href="javascript:goto_page(389+24)"> 7.9 Summary 389 </a>
</ul> <li><a href="javascript:goto_page(405+24)"> 8 The Small Signal Model 405 </a>
<ul> <li><a href="javascript:goto_page(405+24)"> 8.1 Overview of the Nonlinear MOSFET Amplifier 405 </a>
<li><a href="javascript:goto_page(405+24)"> 8.2 The Small Signal Model 405 </a>
<ul> <li><a href="javascript:goto_page(413+24)"> 8.2.1 Small Signal Circuit Representation 413 </a>
<li><a href="javascript:goto_page(418+24)"> 8.3.2 Small Signal Circuit for the MOSFET Amplifier 418 </a>
<li><a href="javascript:goto_page(420+24)"> 8.2.3 Selecting an Operating Point 420 </a>
<li><a href="javascript:goto_page(423+24)"> 8.2.4 Input and Output Resistance, Current and Power Gain 423 </a>
</ul> <li><a href="javascript:goto_page(447+24)"> 8.3 Summary 447 </a>
</ul> <li><a href="javascript:goto_page(457+24)"> 9 Energy Storage Elements 457 </a>
<ul> <li><a href="javascript:goto_page(461+24)"> 1-Sep Constitutive Laws 461 </a>
<ul> <li><a href="javascript:goto_page(461+24)"> 9.1.1 Capacitors 461 </a>
<li><a href="javascript:goto_page(466+24)"> 9.1.2 Inductors 466 </a>
</ul> <li><a href="javascript:goto_page(470+24)"> 9.2 Series & Parallel Connections 470 </a>
<ul> <li><a href="javascript:goto_page(471+24)"> 9.2.1 Capacitors 471 </a>
<li><a href="javascript:goto_page(472+24)"> 9.2.2 Inductors 472 </a>
</ul> <li><a href="javascript:goto_page(473+24)"> 9.3 Special Examples 473 </a>
<ul> <li><a href="javascript:goto_page(473+24)"> 9.3.1 MOSFET Gate Capacitance 473 </a>
<li><a href="javascript:goto_page(476+24)"> 9.3.2 Wiring Loop Inductance 476 </a>
<li><a href="javascript:goto_page(477+24)"> 9.3.3 IC Wiring Capacitance and Inductance 477 </a>
<li><a href="javascript:goto_page(478+24)"> 9.3.4 Transformers * 478 </a>
</ul> <li><a href="javascript:goto_page(480+24)"> 9.4 Simple Circuit Examples 480 </a>
<ul> <li><a href="javascript:goto_page(482+24)"> 9.4.1 Sinusoidal Inputs * 482 </a>
<li><a href="javascript:goto_page(482+24)"> 9.4.2 Step Inputs 482 </a>
<li><a href="javascript:goto_page(488+24)"> 9.4.3 Impulse Inputs 488 </a>
<li><a href="javascript:goto_page(489+24)"> 9.4.4 Role Reversal * 489 </a>
</ul> <li><a href="javascript:goto_page(489+24)"> 9.5 Energy, Charge and Flux Conservation 489 </a>
<li><a href="javascript:goto_page(492+24)"> 9.6 Summary 492 </a>
</ul> <li><a href="javascript:goto_page(503+24)"> 10 First-order Transients 503 </a>
<ul> <li><a href="javascript:goto_page(504+24)"> 10.1.1 Analysis of RC Circuits 504 </a>
<ul> <li><a href="javascript:goto_page(504+24)"> 10.1.2 Parallel RC Circuit, Step Input 504 </a>
<li><a href="javascript:goto_page(509+24)"> 10.1.3 RC Discharge Transient 509 </a>
<li><a href="javascript:goto_page(511+24)"> 10.1.4 Series RC Circuit, Step Input 511 </a>
<li><a href="javascript:goto_page(515+24)"> 10.2 Series RC Circuit, Square Wave Input 515 </a>
</ul> <li><a href="javascript:goto_page(517+24)"> 10.2.1 Analysis of RL Circuits 517 </a>
<ul> <li><a href="javascript:goto_page(517+24)"> 10.3 Series RL Circuit, Step Input 517 </a>
</ul> <li><a href="javascript:goto_page(520+24)"> 10.4 Intuitive Analysis 520 </a>
<li><a href="javascript:goto_page(525+24)"> 10.4.1 Propagation Delay and the Digital Abstraction 525 </a>
<ul> <li><a href="javascript:goto_page(527+24)"> 10.4.2 Definitions 527 </a>
<li><a href="javascript:goto_page(529+24)"> 10.5 Computing t_pd from the SRC MOSFET Model 529 </a>
</ul> <li><a href="javascript:goto_page(538+24)"> 10.5.1 State and State Variables * 538 </a>
<ul> <li><a href="javascript:goto_page(538+24)"> 10.5.2 The Concept of State 538 </a>
<li><a href="javascript:goto_page(540+24)"> 10.5.3 Computer Analysis using the State Equation 540 </a>
<li><a href="javascript:goto_page(541+24)"> 10.5.4 Zero-input and Zero-state Response 541 </a>
<li><a href="javascript:goto_page(544+24)"> 10.6 Solution by Integrating Factors* 544 </a>
</ul> <li><a href="javascript:goto_page(545+24)"> 10.6.1 Additional Examples 545 </a>
<ul> <li><a href="javascript:goto_page(545+24)"> 10.6.2 Effect of Wire Inductance in Digital Circuits 545 </a>
<li><a href="javascript:goto_page(545+24)"> 10.6.3 Ramp Inputs and Linearity 545 </a>
<li><a href="javascript:goto_page(550+24)"> 10.6.4 Response of an RC Circuit to Short Pulses and the Impulse Response 550 </a>
<li><a href="javascript:goto_page(553+24)"> 10.6.5 Intuitive Method for the Impulse Response 553 </a>
<li><a href="javascript:goto_page(554+24)"> 10.6.6 Clock Signals and Clock Fanout 554 </a>
<li><a href="javascript:goto_page(558+24)"> 10.6.7 RC Response to Decaying Exponential * 558 </a>
<li><a href="javascript:goto_page(558+24)"> 10.7 Series RL Circuit with Sinewave Input 558 </a>
</ul> <li><a href="javascript:goto_page(561+24)"> 10.7.1 Digital Memory 561 </a>
<ul> <li><a href="javascript:goto_page(561+24)"> 10.7.2 The Concept of Digital State 561 </a>
<li><a href="javascript:goto_page(562+24)"> 10.7.3 An Abstract Digital Memory Element 562 </a>
<li><a href="javascript:goto_page(563+24)"> 10.7.4 Design of the Digital Memory Element 563 </a>
<li><a href="javascript:goto_page(567+24)"> 10.7.5 A Static Memory Element 567 </a>
</ul> <li><a href="javascript:goto_page(568+24)"> 10.8 Summary 568 </a>
</ul> <li><a href="javascript:goto_page(595+24)"> 11 Energy and Power in Digital Circuits 595 </a>
<ul> <li><a href="javascript:goto_page(595+24)"> 11.1 Power and Energy Relations for a Simple RC Circuit 595 </a>
<li><a href="javascript:goto_page(597+24)"> 11.2 Average Power in an RC Circuit 597 </a>
<ul> <li><a href="javascript:goto_page(599+24)"> 11.2.1 Energy Dissipated during Interval T_1 599 </a>
<li><a href="javascript:goto_page(601+24)"> 11.2.2 Energy Dissipated during Interval T_2 601 </a>
<li><a href="javascript:goto_page(603+24)"> 11.2.3 Total Energy Dissipated 603 </a>
</ul> <li><a href="javascript:goto_page(604+24)"> 11.3 Power Dissipation in Logic Gates 604 </a>
<ul> <li><a href="javascript:goto_page(604+24)"> 11.3.1 Static Power Dissipation 604 </a>
<li><a href="javascript:goto_page(605+24)"> 11.3.2 Total Power Dissipation 605 </a>
</ul> <li><a href="javascript:goto_page(611+24)"> 11.4 NMOS Logic 611 </a>
<li><a href="javascript:goto_page(611+24)"> 11.5 CMOS Logic 611 </a>
<ul> <li><a href="javascript:goto_page(616+24)"> 11.5.1 CMOS Logic Gate Design 616 </a>
</ul> <li><a href="javascript:goto_page(618+24)"> 11.6 Summary 618 </a>
</ul> <li><a href="javascript:goto_page(625+24)"> 12 Transients in Second Order Circuits 625 </a>
<ul> <li><a href="javascript:goto_page(627+24)"> 12.1 Undriven LC Circuit 627 </a>
<li><a href="javascript:goto_page(640+24)"> 12.2 Undriven, Series RLC Circuit 640 </a>
<ul> <li><a href="javascript:goto_page(644+24)"> 12.2.1 Under-Damped Dynamics 644 </a>
<li><a href="javascript:goto_page(648+24)"> 12.2.2 Over-Damped Dynamics 648 </a>
<li><a href="javascript:goto_page(649+24)"> 12.2.3 Critically-Damped Dynamics 649 </a>
</ul> <li><a href="javascript:goto_page(651+24)"> 12.3 Stored Energy in Transient, Series RLC Circuit 651 </a>
<li><a href="javascript:goto_page(654+24)"> 12.4 Undriven, Parallel RLC Circuit * 654 </a>
<ul> <li><a href="javascript:goto_page(654+24)"> 12.4.1 Under-Damped Dynamics 654 </a>
<li><a href="javascript:goto_page(654+24)"> 12.4.2 Over-Damped Dynamics 654 </a>
<li><a href="javascript:goto_page(654+24)"> 12.4.3 Critically-Damped Dynamics 654 </a>
</ul> <li><a href="javascript:goto_page(654+24)"> 12.5 Driven, Series RLC Circuit 654 </a>
<ul> <li><a href="javascript:goto_page(657+24)"> 12.5.1 Step Response 657 </a>
<li><a href="javascript:goto_page(661+24)"> 12.5.2 Impulse Response * 661 </a>
</ul> <li><a href="javascript:goto_page(678+24)"> 12.6 Driven, Parallel RLC Circuit * 678 </a>
<ul> <li><a href="javascript:goto_page(678+24)"> 12.6.1 Step Response 678 </a>
<li><a href="javascript:goto_page(678+24)"> 12.6.2 Impulse Response 678 </a>
</ul> <li><a href="javascript:goto_page(678+24)"> 12.7 Intuitive Analysis of Second-Order Circuits 678 </a>
<li><a href="javascript:goto_page(684+24)"> 12.8 Two-Capacitor Or Two-Inductor Circuits 684 </a>
<li><a href="javascript:goto_page(689+24)"> 12.9 State-Variable Method * 689 </a>
<li><a href="javascript:goto_page(691+24)"> 12.10 State-Space Analysis * 691 </a>
<ul> <li><a href="javascript:goto_page(691+24)"> 12.10.1 Numerical Solution * 691 </a>
</ul> <li><a href="javascript:goto_page(691+24)"> 12.11 Higher-Order Circuits* 691 </a>
<li><a href="javascript:goto_page(692+24)"> 12.12 Summary 692 </a>
</ul> <li><a href="javascript:goto_page(703+24)"> 13 Sinusoidal Steady State 703 </a>
<ul> <li><a href="javascript:goto_page(703+24)"> 13.1 Introduction 703 </a>
<li><a href="javascript:goto_page(706+24)"> 13.2 Analysis using Complex Exponential Drive 706 </a>
<ul> <li><a href="javascript:goto_page(706+24)"> 13.2.1 Homogeneous Solution 706 </a>
<li><a href="javascript:goto_page(707+24)"> 13.2.2 Particular Solution 707 </a>
<li><a href="javascript:goto_page(710+24)"> 13.2.3 Complete Solution 710 </a>
<li><a href="javascript:goto_page(710+24)"> 13.2.4 Sinusoidal Steady State Response 710 </a>
</ul> <li><a href="javascript:goto_page(712+24)"> 13.3 The Boxes: Impedance 712 </a>
<ul> <li><a href="javascript:goto_page(718+24)"> 13.3.1 Example: Series RL Circuit 718 </a>
<li><a href="javascript:goto_page(722+24)"> 13.3.2 Example: Another RC Circuit 722 </a>
<li><a href="javascript:goto_page(724+24)"> 13.3.3 Example: RC Circuit with Two Capacitors 724 </a>
<li><a href="javascript:goto_page(729+24)"> 13.3.4 Example: Analysis of Small Signal Amplifier with Capacitive Load 729 </a>
</ul> <li><a href="javascript:goto_page(731+24)"> 13.4 Frequency Response: Magnitude/Phase vs. Frequency 731 </a>
<ul> <li><a href="javascript:goto_page(732+24)"> 13.4.1 Frequency Response of Capacitors, Inductor 732 </a>
<li><a href="javascript:goto_page(737+24)"> 13.4.2 Intuitively Sketching th 737 </a>
<li><a href="javascript:goto_page(741+24)"> 13.4.3 The Bode Plot: Sketching the Frequency Response of General Functions * 741 </a>
</ul> <li><a href="javascript:goto_page(742+24)"> 13.5 Filters 742 </a>
<ul> <li><a href="javascript:goto_page(744+24)"> 13.5.1 Filter Design Example: Crossover Network 744 </a>
<li><a href="javascript:goto_page(746+24)"> 13.5.2 Decoupling Amplifier Stages 746 </a>
</ul> <li><a href="javascript:goto_page(751+24)"> 13.6 Time Domain 751 </a>
<ul> <li><a href="javascript:goto_page(751+24)"> 13.6.1 Frequency Domain Analysis 751 </a>
<li><a href="javascript:goto_page(754+24)"> 13.6.2 Time Domain Analysis 754 </a>
<li><a href="javascript:goto_page(756+24)"> 13.6.3 Comparing Time Domain and Frequency Domain Analyses 756 </a>
</ul> <li><a href="javascript:goto_page(757+24)"> 13.7 Power and Energy in an Impedance 757 </a>
<ul> <li><a href="javascript:goto_page(758+24)"> 13.7.1 Arbitrary Impedance 758 </a>
<li><a href="javascript:goto_page(760+24)"> 13.7.2 Pure Resistance 760 </a>
<li><a href="javascript:goto_page(761+24)"> 13.7.3 Pure Reactance 761 </a>
<li><a href="javascript:goto_page(763+24)"> 13.7.4 Example: Power in an RC Circuit 763 </a>
</ul> <li><a href="javascript:goto_page(765+24)"> 13.8 Summary 765 </a>
</ul> <li><a href="javascript:goto_page(777+24)"> 14 Sinusoidal Steady State: Resonance 777 </a>
<ul> <li><a href="javascript:goto_page(777+24)"> 14.1 Parallel RLC, Sinusoidal Response 777 </a>
<ul> <li><a href="javascript:goto_page(778+24)"> 14.1.1 Homogeneous Solution 778 </a>
<li><a href="javascript:goto_page(780+24)"> 14.1.2 Particular Solution 780 </a>
<li><a href="javascript:goto_page(781+24)"> 14.1.3 Total Solution for the Parallel RLC Circuit 781 </a>
</ul> <li><a href="javascript:goto_page(783+24)"> 14.2 Frequency Response for Resonant Systems 783 </a>
<ul> <li><a href="javascript:goto_page(792+24)"> 14.2.1 The Resonant Region of the Frequency Response 792 </a>
</ul> <li><a href="javascript:goto_page(801+24)"> 14.3 Series RLC 801 </a>
<li><a href="javascript:goto_page(808+24)"> 14.4 The Bode Plot for Resonant Functions * 808 </a>
<li><a href="javascript:goto_page(808+24)"> 14.5 Filter Examples 808 </a>
<ul> <li><a href="javascript:goto_page(809+24)"> 14.5.1 Bandpass Filter 809 </a>
<li><a href="javascript:goto_page(810+24)"> 14.5.2 Lowpass Filter 810 </a>
<li><a href="javascript:goto_page(812+24)"> 14.5.3 Highpass Filter 812 </a>
<li><a href="javascript:goto_page(815+24)"> 14.5.4 Notch Filter 815 </a>
</ul> <li><a href="javascript:goto_page(816+24)"> 14.6 Stored Energy in a Resonant Circuit 816 </a>
<li><a href="javascript:goto_page(821+24)"> 14.7 Summary 821 </a>
</ul> <li><a href="javascript:goto_page(837+24)"> 15 The Operational Amplifier Abstraction 837 </a>
<ul> <li><a href="javascript:goto_page(837+24)"> 15.1 Introduction 837 </a>
<ul> <li><a href="javascript:goto_page(838+24)"> 15.1.1 Historical Perspective 838 </a>
</ul> <li><a href="javascript:goto_page(839+24)"> 15.2 Device Properties of the Operational Amplifier 839 </a>
<ul> <li><a href="javascript:goto_page(839+24)"> 15.2 The Op Amp Model 839 </a>
</ul> <li><a href="javascript:goto_page(842+24)"> 15.3 Simple Op Amp Circuits 842 </a>
<ul> <li><a href="javascript:goto_page(842+24)"> 15.3.1 The Non-inverting Op Amp 842 </a>
<li><a href="javascript:goto_page(844+24)"> 15.3.2 A Second Example: The Inverting Connection 844 </a>
<li><a href="javascript:goto_page(846+24)"> 15.3.3 Sensitivity 846 </a>
<li><a href="javascript:goto_page(847+24)"> 15.3.4 A Special Case: The Voltage Follower 847 </a>
<li><a href="javascript:goto_page(848+24)"> 15.3.5 An Additional Constraint: v+ - v- ~ 0 848 </a>
</ul> <li><a href="javascript:goto_page(849+24)"> 15.4 Input and Output Resistances 849 </a>
<ul> <li><a href="javascript:goto_page(849+24)"> 15.4.1 Output Resistance, Inverting Op Amp 849 </a>
<li><a href="javascript:goto_page(851+24)"> 15.4.2 Input Resistance, Inverting Connection 851 </a>
<li><a href="javascript:goto_page(853+24)"> 15.4.3 Input and Output R for Non-Inverting Op Amp 853 </a>
<li><a href="javascript:goto_page(855+24)"> 15.4.4 Generalization on Input Resistance * 855 </a>
<li><a href="javascript:goto_page(855+24)"> 15.4.5 Example: Op Amp Current Source 855 </a>
</ul> <li><a href="javascript:goto_page(857+24)"> 15.5 Additional Examples 857 </a>
<ul> <li><a href="javascript:goto_page(858+24)"> 15.5.1 Adder 858 </a>
<li><a href="javascript:goto_page(858+24)"> 15.5.2 Subtracter 858 </a>
</ul> <li><a href="javascript:goto_page(859+24)"> 15.6 Op Amp RC Circuits 859 </a>
<ul> <li><a href="javascript:goto_page(859+24)"> 15.6.1 Op Amp Integrator 859 </a>
<li><a href="javascript:goto_page(862+24)"> 15.6.2 Op Amp Differentiator 862 </a>
<li><a href="javascript:goto_page(863+24)"> 15.6.3 An RC Active Filter 863 </a>
<li><a href="javascript:goto_page(865+24)"> 15.6.4 The RC Active Filter -- Impedance Analysis 865 </a>
<li><a href="javascript:goto_page(866+24)"> 15.6.5 Sallen-Key Filter 866 </a>
</ul> <li><a href="javascript:goto_page(866+24)"> 15.7 Op Amp in Saturation 866 </a>
<ul> <li><a href="javascript:goto_page(867+24)"> 15.7.1 Op Amp Integrator in Saturation 867 </a>
</ul> <li><a href="javascript:goto_page(869+24)"> 15.8 Positive Feedback 869 </a>
<ul> <li><a href="javascript:goto_page(869+24)"> 15.8.1 RC Oscillator 869 </a>
</ul> <li><a href="javascript:goto_page(872+24)"> 15.9 Two-ports* 872 </a>
<li><a href="javascript:goto_page(873+24)"> 15.10 Summary 873 </a>
</ul> <li><a href="javascript:goto_page(905+24)"> 16 Diodes 905 </a>
<ul> <li><a href="javascript:goto_page(905+24)"> 16.1 Introduction 905 </a>
<li><a href="javascript:goto_page(905+24)"> 16.2 Semiconductor Diode Characteristics 905 </a>
<li><a href="javascript:goto_page(908+24)"> 16.3 Analysis of Diode Circuits 908 </a>
<ul> <li><a href="javascript:goto_page(908+24)"> 16.3.1 Method of Assumed States 908 </a>
</ul> <li><a href="javascript:goto_page(912+24)"> 16.4 Nonlinear Analysis with RL and RC 912 </a>
<ul> <li><a href="javascript:goto_page(912+24)"> 16.4.1 Peak Detector 912 </a>
<li><a href="javascript:goto_page(915+24)"> 16.4.2 Example: Clamping Circuit 915 </a>
<li><a href="javascript:goto_page(918+24)"> 16.4.3 A Switched Power Supply Using a Diode 918 </a>
</ul> <li><a href="javascript:goto_page(918+24)"> 16.5 Additional Examples 918 </a>
<ul> <li><a href="javascript:goto_page(918+24)"> 16.5.1 Piecewise Linear Example: Clipping Circuit 918 </a>
<li><a href="javascript:goto_page(918+24)"> 16.5.2 Exponentiation Circuit 918 </a>
<li><a href="javascript:goto_page(918+24)"> 16.5.3 Piecewise Linear Example: Limiter 918 </a>
<li><a href="javascript:goto_page(918+24)"> 16.5.4 Example: Full-Wave Diode Bridge 918 </a>
<li><a href="javascript:goto_page(918+24)"> 16.5.5 Incremental Example: Zener Diode Regulator 918 </a>
<li><a href="javascript:goto_page(918+24)"> 16.5.6 Incremental Example: Diode Attenuator 918 </a>
</ul> <li><a href="javascript:goto_page(919+24)"> 16.6 Summary 919 </a>
</ul> <li><a href="javascript:goto_page(927+24)"> A1 Maxwell's Equations and the LMD 927 </a>
<ul> <li><a href="javascript:goto_page(927+24)"> A.1 The Lumped Matter Discipline 927 </a>
<ul> <li><a href="javascript:goto_page(927+24)"> A.1.1 The First Constraint of the Lumped Matter Discipline 927 </a>
<li><a href="javascript:goto_page(930+24)"> A.1.2 The Second Constraint of the Lumped Matter Discipline 930 </a>
<li><a href="javascript:goto_page(932+24)"> A.1.3 The Third Constraint of the Lumped Matter Discipline 932 </a>
<li><a href="javascript:goto_page(933+24)"> A.1.4 The Lumped Matter Discipline Applied to Circuits 933 </a>
</ul> <li><a href="javascript:goto_page(934+24)"> A.2 Deriving Kirchhoff's Laws 934 </a>
<li><a href="javascript:goto_page(936+24)"> A.3 Deriving the Resistance of a Piece of Material 936 </a>
</ul> <li><a href="javascript:goto_page(941+24)"> B Trigonometric Functions & Identities 941 </a>
<ul> <li><a href="javascript:goto_page(941+24)"> B.1 Negative Arguments 941 </a>
<li><a href="javascript:goto_page(942+24)"> B.2 Phase-Shifted Arguments 942 </a>
<li><a href="javascript:goto_page(942+24)"> B.3 Sum and Difference Arguments 942 </a>
<li><a href="javascript:goto_page(943+24)"> B.4 Products 943 </a>
<li><a href="javascript:goto_page(943+24)"> B.5 Half-Angle & Twice-Angle Arguments 943 </a>
<li><a href="javascript:goto_page(943+24)"> B.6 Squares 943 </a>
<li><a href="javascript:goto_page(943+24)"> B.7 Miscellaneous 943 </a>
<li><a href="javascript:goto_page(944+24)"> B.8 Taylor Series Expansions 944 </a>
<li><a href="javascript:goto_page(944+24)"> B.9 Relations to e^j\theta 944 </a>
</ul> <li><a href="javascript:goto_page(947+24)"> C Complex Numbers 947 </a>
<ul> <li><a href="javascript:goto_page(947+24)"> C.1 Magnitude and Phase 947 </a>
<li><a href="javascript:goto_page(948+24)"> C.2 Polar Representation 948 </a>
<li><a href="javascript:goto_page(949+24)"> C.3 Addition and Subtraction 949 </a>
<li><a href="javascript:goto_page(949+24)"> C.4 Multiplication and Division 949 </a>
<li><a href="javascript:goto_page(950+24)"> C.5 Complex Conjugate 950 </a>
<li><a href="javascript:goto_page(951+24)"> C.6 Properties of e^j\theta 951 </a>
<li><a href="javascript:goto_page(951+24)"> C.7 Rotation 951 </a>
<li><a href="javascript:goto_page(952+24)"> C.8 Complex Functions of Time 952 </a>
<li><a href="javascript:goto_page(952+24)"> C.9 Numerical Examples 952 </a>
</ul> <li><a href="javascript:goto_page(957+24)"> D Solving Simultaneous Linear Equations 957 </a>
<li><a href="javascript:goto_page(959+24)"> Answers to Selected Problems 959 </a>
<li><a href="javascript:goto_page(971+24)"> Figure Acknowledgments 971 </a>
<li><a href="javascript:goto_page(973+24)"> Index 973 </a>
<?xml version="1.0"?>
<table_of_contents>
<entry page="9" page_label="ix" name="Contents"/>
<entry page="1" page_label="i" name="Preamble">
<entry page="1" page_label="i" name="Comments on the Book"/>
<entry page="4" page_label="iv" name="About the Authors"/>
<entry page="7" page_label="vii" name="Dedication"/>
<entry page="19" page_label="xix" name="Preface"/>
<entry page="19" page_label="xix" name="Approach"/>
<entry page="21" page_label="xxi" name="Overview"/>
<entry page="23" page_label="xxiii" name="Course Organization"/>
<entry page="23" page_label="xxiii" name="Web Supplements"/>
<entry page="24" page_label="xxiv" name="Acknowledgments"/>
</entry>
<entry page="27" page_label="3" name="The Circuit Abstraction" chapter="1">
<entry page="27" page_label="3" name="The Power of Abstraction" chapter="1.1"/>
<entry page="29" page_label="4" name="The Lumped Circuit Abstraction" chapter="1.2"/>
<entry page="33" page_label="9" name="The Lumped Matter Discipline" chapter="1.3"/>
<entry page="37" page_label="13" name="Limitations of the Lumped Circuit Abstraction" chapter="1.4"/>
<entry page="39" page_label="15" name="Practical Two-Terminal Elements" chapter="1.5">
<entry page="40" page_label="16" name="Batteries" chapter="1.5.1"/>
<entry page="42" page_label="18" name="Linear Resistors" chapter="1.5.2"/>
<entry page="49" page_label="25" name="Associated Variables Convention" chapter="1.5.3"/>
</entry>
<entry page="53" page_label="29" name="Ideal Two-Terminal Elements" chapter="1.6">
<entry page="54" page_label="30" name="Ideal Voltage Sources, Wires and Resistors" chapter="1.6.1"/>
<entry page="56" page_label="32" name="Element Laws" chapter="1.6.2"/>
<entry page="57" page_label="33" name="The Current Source" chapter="1.6.3"/>
</entry>
<entry page="60" page_label="36" name="Modeling Physical Elements" chapter="1.7"/>
<entry page="64" page_label="40" name="Signal Representation" chapter="1.8">
<entry page="65" page_label="41" name="Analog Signals" chapter="1.8.1"/>
<entry page="66" page_label="42" name="Digital Signals" chapter="1.8.3"/>
</entry>
<entry page="70" page_label="46" name="Summary" chapter="1.9"/>
</entry>
<entry page="77" page_label="53" name="Resistive Networks" chapter="2">
<entry page="78" page_label="54" name="Terminology" chapter="2.1"/>
<entry page="79" page_label="55" name="Kirchhoff's Laws" chapter="2.2">
<entry page="80" page_label="56" name="KCL" chapter="2.2.1"/>
<entry page="84" page_label="60" name="KVL" chapter="2.2.1"/>
</entry>
<entry page="90" page_label="66" name="Circuit Analysis: Basic Method" chapter="2.3">
<entry page="91" page_label="67" name="Single-Resistor Circuits" chapter="2.3.1"/>
<entry page="94" page_label="70" name="Quick Intuitive Analysis of Single-Resistor Circuits" chapter="2.3.2"/>
<entry page="95" page_label="71" name="Energy Conservation" chapter="2.3.3"/>
<entry page="97" page_label="73" name="Voltage and Current Dividers" chapter="2.3.4"/>
<entry page="99" page_label="73" name="Voltage Dividers" chapter="2.3.4.1"/>
<entry page="100" page_label="76" name="Resistors in Series" chapter="2.3.4.2"/>
<entry page="104" page_label="80" name="Current Dividers" chapter="2.3.4.3"/>
<entry page="108" page_label="82" name="Resistors in Parallel" chapter="2.3.4.4"/>
<entry page="108" page_label="84" name="A More Complex Circuit" chapter="2.3.5"/>
</entry>
<entry page="113" page_label="89" name="Intuitive Method of Circuit Analysis" chapter="2.4"/>
<entry page="119" page_label="95" name="More Examples" chapter="2.5"/>
<entry page="122" page_label="98" name="Dependent Sources and the Control Concept" chapter="2.6">
<entry page="126" page_label="102" name="Circuits with Dependent Sources" chapter="2.6.1"/>
</entry>
<entry page="131" page_label="107" name="A Formulation Suitable for a Computer Solution *" chapter="2.7"/>
<entry page="132" page_label="108" name="Summary" chapter="2.8"/>
</entry>
<entry page="143" page_label="119" name="Network Theorems" chapter="3">
<entry page="143" page_label="119" name="Introduction" chapter="3.1"/>
<entry page="143" page_label="119" name="The Node Voltage" chapter="3.2"/>
<entry page="149" page_label="125" name="The Node Method" chapter="3.3">
<entry page="154" page_label="130" name="Node Method: A Second Example" chapter="3.3.1"/>
<entry page="159" page_label="135" name="Floating Independent Voltage Sources" chapter="3.3.2"/>
<entry page="163" page_label="139" name="Dependent Sources and the Node Method" chapter="3.3.3"/>
<entry page="169" page_label="145" name="The Conductance and Source Matrices *" chapter="3.3.4"/>
</entry>
<entry page="169" page_label="145" name="Loop Method *" chapter="3.4"/>
<entry page="169" page_label="145" name="Superposition" chapter="3.5">
<entry page="176" page_label="152" name="Superposition Rules for Dependent Sources" chapter="3.5.1"/>
</entry>
<entry page="182" page_label="158" name="Thevenin's Theorem and Norton's Theorem" chapter="3.6">
<entry page="182" page_label="158" name="The Thevenin Equivalent Network" chapter="3.6.1"/>
<entry page="192" page_label="168" name="The Norton Equivalent Network" chapter="3.6.2"/>
<entry page="195" page_label="171" name="More Examples" chapter="3.6.3"/>
</entry>
<entry page="201" page_label="177" name="Summary" chapter="3.7"/>
</entry>
<entry page="217" page_label="193" name="Analysis of Nonlinear Circuits" chapter="4">
<entry page="217" page_label="193" name="Introduction to Nonlinear Elements" chapter="4.1"/>
<entry page="221" page_label="197" name="Analytical Solutions" chapter="4.2"/>
<entry page="227" page_label="203" name="Graphical Analysis" chapter="4.3"/>
<entry page="230" page_label="206" name="Piecewise Linear Analysis" chapter="4.4">
<entry page="238" page_label="214" name="Improved Piecewise Linear Models for Nonlinear Elements *" chapter="4.4.1"/>
</entry>
<entry page="238" page_label="214" name="Incremental Analysis" chapter="4.5"/>
<entry page="253" page_label="229" name="Summary" chapter="4.6"/>
</entry>
<entry page="267" page_label="243" name="The Digital Abstraction" chapter="5">
<entry page="269" page_label="245" name="Voltage Levels and the Static Discipline" chapter="5.1"/>
<entry page="280" page_label="256" name="Boolean Logic" chapter="5.2"/>
<entry page="282" page_label="258" name="Combinational Gates" chapter="5.3"/>
<entry page="285" page_label="261" name="Standard Sum-of-Products Representation" chapter="5.4"/>
<entry page="286" page_label="262" name="Simplifying Logic Expressions *" chapter="5.5"/>
<entry page="291" page_label="267" name="Number Representation" chapter="5.6"/>
<entry page="298" page_label="274" name="Summary" chapter="5.7"/>
</entry>
<entry page="309" page_label="285" name="The MOSFET Switch" chapter="6">
<entry page="309" page_label="285" name="The Switch" chapter="6.1"/>
<entry page="312" page_label="288" name="Logic Functions Using Switches" chapter="6.2"/>
<entry page="322" page_label="298" name="The MOSFET Device and Its S Model" chapter="6.3"/>
<entry page="315" page_label="291" name="MOSFET Switch Implementation of Logic Gates" chapter="6.4"/>
<entry page="320" page_label="296" name="Static Analysis Using the S Model" chapter="6.5"/>
<entry page="324" page_label="300" name="The SR Model of the MOSFET" chapter="6.6"/>
<entry page="325" page_label="301" name="Physical Structure of the MOSFET *" chapter="6.7"/>
<entry page="330" page_label="306" name="Static Analysis Using the SR Model" chapter="6.8">
<entry page="335" page_label="311" name="Static Analysis of the NAND Gate Using the SR Model" chapter="6.8.1"/>
</entry>
<entry page="338" page_label="314" name="Signal Restoration" chapter="6.9">
<entry page="338" page_label="314" name="Signal Restoration and Gain" chapter="6.9.1"/>
<entry page="341" page_label="317" name="Signal Restoration and Nonlinearity" chapter="6.9.2"/>
<entry page="342" page_label="318" name="Buffer Characteristics and the Static Discipline" chapter="6.9.3"/>
<entry page="343" page_label="319" name="Inverter Transfer Characteristics and the Static Discipline" chapter="6.9.4"/>
</entry>
<entry page="344" page_label="320" name="Power Consumption in Logic Gates" chapter="6.10"/>
<entry page="345" page_label="321" name="Active Pullups" chapter="6.11"/>
<entry page="346" page_label="322" name="Summary" chapter="6.12"/>
</entry>
<entry page="355" page_label="331" name="The MOSFET Amplifier" chapter="7">
<entry page="356" page_label="332" name="Signal Amplification" chapter="7.1"/>
<entry page="356" page_label="332" name="Review of Dependent Sources" chapter="7.2"/>
<entry page="359" page_label="335" name="Actual MOSFET Characteristics" chapter="7.3"/>
<entry page="364" page_label="340" name="The Switch Current Source (SCS) MOSFET Model" chapter="7.4"/>
<entry page="368" page_label="344" name="The MOSFET Amplifier" chapter="7.5">
<entry page="373" page_label="349" name="Biasing the MOSFET Amplifier" chapter="7.5.1"/>
<entry page="376" page_label="352" name="The Amplifier Abstraction and the Saturation Discipline" chapter="7.5.2"/>
</entry>
<entry page="377" page_label="353" name="Large Signal Analysis of the MOSFET Amplifier" chapter="7.6">
<entry page="377" page_label="353" name="v_IN versus v_OUT in the Saturation Region" chapter="7.6.1"/>
<entry page="380" page_label="356" name="Valid Input and Output Voltage Ranges" chapter="7.6.2"/>
<entry page="387" page_label="363" name="Alternative Method for Valid Input and Output Voltage Ranges" chapter="7.6.3"/>
</entry>
<entry page="409" page_label="385" name="Operating Point Selection" chapter="7.7"/>
<entry page="410" page_label="386" name="Switch Unified (SU) MOSFET Model *" chapter="7.8"/>
<entry page="413" page_label="389" name="Summary" chapter="7.9"/>
</entry>
<entry page="429" page_label="405" name="The Small Signal Model" chapter="8">
<entry page="429" page_label="405" name="Overview of the Nonlinear MOSFET Amplifier" chapter="8.1"/>
<entry page="429" page_label="405" name="The Small Signal Model" chapter="8.2">
<entry page="437" page_label="413" name="Small Signal Circuit Representation" chapter="8.2.1"/>
<entry page="442" page_label="418" name="Small Signal Circuit for the MOSFET Amplifier" chapter="8.3.2"/>
<entry page="444" page_label="420" name="Selecting an Operating Point" chapter="8.2.3"/>
<entry page="447" page_label="423" name="Input and Output Resistance, Current and Power Gain" chapter="8.2.4"/>
</entry>
<entry page="471" page_label="447" name="Summary" chapter="8.3"/>
</entry>
<entry page="481" page_label="457" name="Energy Storage Elements" chapter="9">
<entry page="485" page_label="461" name="Constitutive Laws" chapter="1-Sep">
<entry page="485" page_label="461" name="Capacitors" chapter="9.1.1"/>
<entry page="490" page_label="466" name="Inductors" chapter="9.1.2"/>
</entry>
<entry page="494" page_label="470" name="Series &amp; Parallel Connections" chapter="9.2">
<entry page="495" page_label="471" name="Capacitors" chapter="9.2.1"/>
<entry page="496" page_label="472" name="Inductors" chapter="9.2.2"/>
</entry>
<entry page="497" page_label="473" name="Special Examples" chapter="9.3">
<entry page="497" page_label="473" name="MOSFET Gate Capacitance" chapter="9.3.1"/>
<entry page="500" page_label="476" name="Wiring Loop Inductance" chapter="9.3.2"/>
<entry page="501" page_label="477" name="IC Wiring Capacitance and Inductance" chapter="9.3.3"/>
<entry page="502" page_label="478" name="Transformers *" chapter="9.3.4"/>
</entry>
<entry page="504" page_label="480" name="Simple Circuit Examples" chapter="9.4">
<entry page="506" page_label="482" name="Sinusoidal Inputs *" chapter="9.4.1"/>
<entry page="506" page_label="482" name="Step Inputs" chapter="9.4.2"/>
<entry page="512" page_label="488" name="Impulse Inputs" chapter="9.4.3"/>
<entry page="513" page_label="489" name="Role Reversal *" chapter="9.4.4"/>
</entry>
<entry page="513" page_label="489" name="Energy, Charge and Flux Conservation" chapter="9.5"/>
<entry page="516" page_label="492" name="Summary" chapter="9.6"/>
</entry>
<entry page="527" page_label="503" name="First-order Transients" chapter="10">
<entry page="528" page_label="504" name="Analysis of RC Circuits" chapter="10.1.1">
<entry page="528" page_label="504" name="Parallel RC Circuit, Step Input" chapter="10.1.2"/>
<entry page="533" page_label="509" name="RC Discharge Transient" chapter="10.1.3"/>
<entry page="535" page_label="511" name="Series RC Circuit, Step Input" chapter="10.1.4"/>
<entry page="539" page_label="515" name="Series RC Circuit, Square Wave Input" chapter="10.2"/>
</entry>
<entry page="541" page_label="517" name="Analysis of RL Circuits" chapter="10.2.1">
<entry page="541" page_label="517" name="Series RL Circuit, Step Input" chapter="10.3"/>
</entry>
<entry page="544" page_label="520" name="Intuitive Analysis" chapter="10.4"/>
<entry page="549" page_label="525" name="Propagation Delay and the Digital Abstraction" chapter="10.4.1">
<entry page="551" page_label="527" name="Definitions" chapter="10.4.2"/>
<entry page="553" page_label="529" name="Computing t_pd from the SRC MOSFET Model" chapter="10.5"/>
</entry>
<entry page="562" page_label="538" name="State and State Variables *" chapter="10.5.1">
<entry page="562" page_label="538" name="The Concept of State" chapter="10.5.2"/>
<entry page="564" page_label="540" name="Computer Analysis using the State Equation" chapter="10.5.3"/>
<entry page="565" page_label="541" name="Zero-input and Zero-state Response" chapter="10.5.4"/>
<entry page="568" page_label="544" name="Solution by Integrating Factors*" chapter="10.6"/>
</entry>
<entry page="569" page_label="545" name="Additional Examples" chapter="10.6.1">
<entry page="569" page_label="545" name="Effect of Wire Inductance in Digital Circuits" chapter="10.6.2"/>
<entry page="569" page_label="545" name="Ramp Inputs and Linearity" chapter="10.6.3"/>
<entry page="574" page_label="550" name="Response of an RC Circuit to Short Pulses and the Impulse Response" chapter="10.6.4"/>
<entry page="577" page_label="553" name="Intuitive Method for the Impulse Response" chapter="10.6.5"/>
<entry page="578" page_label="554" name="Clock Signals and Clock Fanout" chapter="10.6.6"/>
<entry page="582" page_label="558" name="RC Response to Decaying Exponential *" chapter="10.6.7"/>
<entry page="582" page_label="558" name="Series RL Circuit with Sinewave Input" chapter="10.7"/>
</entry>
<entry page="585" page_label="561" name="Digital Memory" chapter="10.7.1">
<entry page="585" page_label="561" name="The Concept of Digital State" chapter="10.7.2"/>
<entry page="586" page_label="562" name="An Abstract Digital Memory Element" chapter="10.7.3"/>
<entry page="587" page_label="563" name="Design of the Digital Memory Element" chapter="10.7.4"/>
<entry page="591" page_label="567" name="A Static Memory Element" chapter="10.7.5"/>
</entry>
<entry page="592" page_label="568" name="Summary" chapter="10.8"/>
</entry>
<entry page="619" page_label="595" name="Energy and Power in Digital Circuits" chapter="11">
<entry page="619" page_label="595" name="Power and Energy Relations for a Simple RC Circuit" chapter="11.1"/>
<entry page="621" page_label="597" name="Average Power in an RC Circuit" chapter="11.2">
<entry page="623" page_label="599" name="Energy Dissipated during Interval T_1" chapter="11.2.1"/>
<entry page="625" page_label="601" name="Energy Dissipated during Interval T_2" chapter="11.2.2"/>
<entry page="627" page_label="603" name="Total Energy Dissipated" chapter="11.2.3"/>
</entry>
<entry page="628" page_label="604" name="Power Dissipation in Logic Gates" chapter="11.3">
<entry page="628" page_label="604" name="Static Power Dissipation" chapter="11.3.1"/>
<entry page="629" page_label="605" name="Total Power Dissipation" chapter="11.3.2"/>
</entry>
<entry page="635" page_label="611" name="NMOS Logic" chapter="11.4"/>
<entry page="635" page_label="611" name="CMOS Logic" chapter="11.5">
<entry page="640" page_label="616" name="CMOS Logic Gate Design" chapter="11.5.1"/>
</entry>
<entry page="642" page_label="618" name="Summary" chapter="11.6"/>
</entry>
<entry page="649" page_label="625" name="Transients in Second Order Circuits" chapter="12">
<entry page="651" page_label="627" name="Undriven LC Circuit" chapter="12.1"/>
<entry page="664" page_label="640" name="Undriven, Series RLC Circuit" chapter="12.2">
<entry page="668" page_label="644" name="Under-Damped Dynamics" chapter="12.2.1"/>
<entry page="672" page_label="648" name="Over-Damped Dynamics" chapter="12.2.2"/>
<entry page="673" page_label="649" name="Critically-Damped Dynamics" chapter="12.2.3"/>
</entry>
<entry page="675" page_label="651" name="Stored Energy in Transient, Series RLC Circuit" chapter="12.3"/>
<entry page="678" page_label="654" name="Undriven, Parallel RLC Circuit *" chapter="12.4">
<entry page="678" page_label="654" name="Under-Damped Dynamics" chapter="12.4.1"/>
<entry page="678" page_label="654" name="Over-Damped Dynamics" chapter="12.4.2"/>
<entry page="678" page_label="654" name="Critically-Damped Dynamics" chapter="12.4.3"/>
</entry>
<entry page="678" page_label="654" name="Driven, Series RLC Circuit" chapter="12.5">
<entry page="681" page_label="657" name="Step Response" chapter="12.5.1"/>
<entry page="685" page_label="661" name="Impulse Response *" chapter="12.5.2"/>
</entry>
<entry page="702" page_label="678" name="Driven, Parallel RLC Circuit *" chapter="12.6">
<entry page="702" page_label="678" name="Step Response" chapter="12.6.1"/>
<entry page="702" page_label="678" name="Impulse Response" chapter="12.6.2"/>
</entry>
<entry page="702" page_label="678" name="Intuitive Analysis of Second-Order Circuits" chapter="12.7"/>
<entry page="708" page_label="684" name="Two-Capacitor Or Two-Inductor Circuits" chapter="12.8"/>
<entry page="713" page_label="689" name="State-Variable Method *" chapter="12.9"/>
<entry page="715" page_label="691" name="State-Space Analysis *" chapter="12.10">
<entry page="715" page_label="691" name="Numerical Solution *" chapter="12.10.1"/>
</entry>
<entry page="715" page_label="691" name="Higher-Order Circuits*" chapter="12.11"/>
<entry page="716" page_label="692" name="Summary" chapter="12.12"/>
</entry>
<entry page="727" page_label="703" name="Sinusoidal Steady State" chapter="13">
<entry page="727" page_label="703" name="Introduction" chapter="13.1"/>
<entry page="730" page_label="706" name="Analysis using Complex Exponential Drive" chapter="13.2">
<entry page="730" page_label="706" name="Homogeneous Solution" chapter="13.2.1"/>
<entry page="731" page_label="707" name="Particular Solution" chapter="13.2.2"/>
<entry page="734" page_label="710" name="Complete Solution" chapter="13.2.3"/>
<entry page="734" page_label="710" name="Sinusoidal Steady State Response" chapter="13.2.4"/>
</entry>
<entry page="736" page_label="712" name="The Boxes: Impedance" chapter="13.3">
<entry page="742" page_label="718" name="Example: Series RL Circuit" chapter="13.3.1"/>
<entry page="746" page_label="722" name="Example: Another RC Circuit" chapter="13.3.2"/>
<entry page="748" page_label="724" name="Example: RC Circuit with Two Capacitors" chapter="13.3.3"/>
<entry page="753" page_label="729" name="Example: Analysis of Small Signal Amplifier with Capacitive Load" chapter="13.3.4"/>
</entry>
<entry page="755" page_label="731" name="Frequency Response: Magnitude/Phase vs. Frequency" chapter="13.4">
<entry page="756" page_label="732" name="Frequency Response of Capacitors, Inductor" chapter="13.4.1"/>
<entry page="761" page_label="737" name="Intuitively Sketching th" chapter="13.4.2"/>
<entry page="765" page_label="741" name="The Bode Plot: Sketching the Frequency Response of General Functions *" chapter="13.4.3"/>
</entry>
<entry page="766" page_label="742" name="Filters" chapter="13.5">
<entry page="768" page_label="744" name="Filter Design Example: Crossover Network" chapter="13.5.1"/>
<entry page="770" page_label="746" name="Decoupling Amplifier Stages" chapter="13.5.2"/>
</entry>
<entry page="775" page_label="751" name="Time Domain" chapter="13.6">
<entry page="775" page_label="751" name="Frequency Domain Analysis" chapter="13.6.1"/>
<entry page="778" page_label="754" name="Time Domain Analysis" chapter="13.6.2"/>
<entry page="780" page_label="756" name="Comparing Time Domain and Frequency Domain Analyses" chapter="13.6.3"/>
</entry>
<entry page="781" page_label="757" name="Power and Energy in an Impedance" chapter="13.7">
<entry page="782" page_label="758" name="Arbitrary Impedance" chapter="13.7.1"/>
<entry page="784" page_label="760" name="Pure Resistance" chapter="13.7.2"/>
<entry page="785" page_label="761" name="Pure Reactance" chapter="13.7.3"/>
<entry page="787" page_label="763" name="Example: Power in an RC Circuit" chapter="13.7.4"/>
</entry>
<entry page="789" page_label="765" name="Summary" chapter="13.8"/>
</entry>
<entry page="801" page_label="777" name="Sinusoidal Steady State: Resonance" chapter="14">
<entry page="801" page_label="777" name="Parallel RLC, Sinusoidal Response" chapter="14.1">
<entry page="802" page_label="778" name="Homogeneous Solution" chapter="14.1.1"/>
<entry page="804" page_label="780" name="Particular Solution" chapter="14.1.2"/>
<entry page="805" page_label="781" name="Total Solution for the Parallel RLC Circuit" chapter="14.1.3"/>
</entry>
<entry page="807" page_label="783" name="Frequency Response for Resonant Systems" chapter="14.2">
<entry page="816" page_label="792" name="The Resonant Region of the Frequency Response" chapter="14.2.1"/>
</entry>
<entry page="825" page_label="801" name="Series RLC" chapter="14.3"/>
<entry page="832" page_label="808" name="The Bode Plot for Resonant Functions *" chapter="14.4"/>
<entry page="832" page_label="808" name="Filter Examples" chapter="14.5">
<entry page="833" page_label="809" name="Bandpass Filter" chapter="14.5.1"/>
<entry page="834" page_label="810" name="Lowpass Filter" chapter="14.5.2"/>
<entry page="836" page_label="812" name="Highpass Filter" chapter="14.5.3"/>
<entry page="839" page_label="815" name="Notch Filter" chapter="14.5.4"/>
</entry>
<entry page="840" page_label="816" name="Stored Energy in a Resonant Circuit" chapter="14.6"/>
<entry page="845" page_label="821" name="Summary" chapter="14.7"/>
</entry>
<entry page="861" page_label="837" name="The Operational Amplifier Abstraction" chapter="15">
<entry page="861" page_label="837" name="Introduction" chapter="15.1">
<entry page="862" page_label="838" name="Historical Perspective" chapter="15.1.1"/>
</entry>
<entry page="863" page_label="839" name="Device Properties of the Operational Amplifier" chapter="15.2">
<entry page="863" page_label="839" name="The Op Amp Model" chapter="15.2"/>
</entry>
<entry page="866" page_label="842" name="Simple Op Amp Circuits" chapter="15.3">
<entry page="866" page_label="842" name="The Non-inverting Op Amp" chapter="15.3.1"/>
<entry page="868" page_label="844" name="A Second Example: The Inverting Connection" chapter="15.3.2"/>
<entry page="870" page_label="846" name="Sensitivity" chapter="15.3.3"/>
<entry page="871" page_label="847" name="A Special Case: The Voltage Follower" chapter="15.3.4"/>
<entry page="872" page_label="848" name="An Additional Constraint: v+ - v- ~ 0" chapter="15.3.5"/>
</entry>
<entry page="873" page_label="849" name="Input and Output Resistances" chapter="15.4">
<entry page="873" page_label="849" name="Output Resistance, Inverting Op Amp" chapter="15.4.1"/>
<entry page="875" page_label="851" name="Input Resistance, Inverting Connection" chapter="15.4.2"/>
<entry page="877" page_label="853" name="Input and Output R for Non-Inverting Op Amp" chapter="15.4.3"/>
<entry page="879" page_label="855" name="Generalization on Input Resistance *" chapter="15.4.4"/>
<entry page="879" page_label="855" name="Example: Op Amp Current Source" chapter="15.4.5"/>
</entry>
<entry page="881" page_label="857" name="Additional Examples" chapter="15.5">
<entry page="882" page_label="858" name="Adder" chapter="15.5.1"/>
<entry page="882" page_label="858" name="Subtracter" chapter="15.5.2"/>
</entry>
<entry page="883" page_label="859" name="Op Amp RC Circuits" chapter="15.6">
<entry page="883" page_label="859" name="Op Amp Integrator" chapter="15.6.1"/>
<entry page="886" page_label="862" name="Op Amp Differentiator" chapter="15.6.2"/>
<entry page="887" page_label="863" name="An RC Active Filter" chapter="15.6.3"/>
<entry page="889" page_label="865" name="The RC Active Filter -- Impedance Analysis" chapter="15.6.4"/>
<entry page="890" page_label="866" name="Sallen-Key Filter" chapter="15.6.5"/>
</entry>
<entry page="890" page_label="866" name="Op Amp in Saturation" chapter="15.7">
<entry page="891" page_label="867" name="Op Amp Integrator in Saturation" chapter="15.7.1"/>
</entry>
<entry page="893" page_label="869" name="Positive Feedback" chapter="15.8">
<entry page="893" page_label="869" name="RC Oscillator" chapter="15.8.1"/>
</entry>
<entry page="896" page_label="872" name="Two-ports*" chapter="15.9"/>
<entry page="897" page_label="873" name="Summary" chapter="15.10"/>
</entry>
<entry page="929" page_label="905" name="Diodes" chapter="16">
<entry page="929" page_label="905" name="Introduction" chapter="16.1"/>
<entry page="929" page_label="905" name="Semiconductor Diode Characteristics" chapter="16.2"/>
<entry page="932" page_label="908" name="Analysis of Diode Circuits" chapter="16.3">
<entry page="932" page_label="908" name="Method of Assumed States" chapter="16.3.1"/>
</entry>
<entry page="936" page_label="912" name="Nonlinear Analysis with RL and RC" chapter="16.4">
<entry page="936" page_label="912" name="Peak Detector" chapter="16.4.1"/>
<entry page="939" page_label="915" name="Example: Clamping Circuit" chapter="16.4.2"/>
<entry page="942" page_label="918" name="A Switched Power Supply Using a Diode" chapter="16.4.3"/>
</entry>
<entry page="942" page_label="918" name="Additional Examples" chapter="16.5">
<entry page="942" page_label="918" name="Piecewise Linear Example: Clipping Circuit" chapter="16.5.1"/>
<entry page="942" page_label="918" name="Exponentiation Circuit" chapter="16.5.2"/>
<entry page="942" page_label="918" name="Piecewise Linear Example: Limiter" chapter="16.5.3"/>
<entry page="942" page_label="918" name="Example: Full-Wave Diode Bridge" chapter="16.5.4"/>
<entry page="942" page_label="918" name="Incremental Example: Zener Diode Regulator" chapter="16.5.5"/>
<entry page="942" page_label="918" name="Incremental Example: Diode Attenuator" chapter="16.5.6"/>
</entry>
<entry page="943" page_label="919" name="Summary" chapter="16.6"/>
</entry>
<entry page="951" page_label="927" name="Maxwell's Equations and the LMD" chapter="A1">
<entry page="951" page_label="927" name="The Lumped Matter Discipline" chapter="A.1">
<entry page="951" page_label="927" name="The First Constraint of the Lumped Matter Discipline" chapter="A.1.1"/>
<entry page="954" page_label="930" name="The Second Constraint of the Lumped Matter Discipline" chapter="A.1.2"/>
<entry page="956" page_label="932" name="The Third Constraint of the Lumped Matter Discipline" chapter="A.1.3"/>
<entry page="957" page_label="933" name="The Lumped Matter Discipline Applied to Circuits" chapter="A.1.4"/>
</entry>
<entry page="958" page_label="934" name="Deriving Kirchhoff's Laws" chapter="A.2"/>
<entry page="960" page_label="936" name="Deriving the Resistance of a Piece of Material" chapter="A.3"/>
</entry>
<entry page="965" page_label="941" name="Trigonometric Functions &amp; Identities" chapter="B">
<entry page="965" page_label="941" name="Negative Arguments" chapter="B.1"/>
<entry page="966" page_label="942" name="Phase-Shifted Arguments" chapter="B.2"/>
<entry page="966" page_label="942" name="Sum and Difference Arguments" chapter="B.3"/>
<entry page="967" page_label="943" name="Products" chapter="B.4"/>
<entry page="967" page_label="943" name="Half-Angle &amp; Twice-Angle Arguments" chapter="B.5"/>
<entry page="967" page_label="943" name="Squares" chapter="B.6"/>
<entry page="967" page_label="943" name="Miscellaneous" chapter="B.7"/>
<entry page="968" page_label="944" name="Taylor Series Expansions" chapter="B.8"/>
<entry page="968" page_label="944" name="Relations to e^j\theta" chapter="B.9"/>
</entry>
<entry page="971" page_label="947" name="Complex Numbers" chapter="C">
<entry page="971" page_label="947" name="Magnitude and Phase" chapter="C.1"/>
<entry page="972" page_label="948" name="Polar Representation" chapter="C.2"/>
<entry page="973" page_label="949" name="Addition and Subtraction" chapter="C.3"/>
<entry page="973" page_label="949" name="Multiplication and Division" chapter="C.4"/>
<entry page="974" page_label="950" name="Complex Conjugate" chapter="C.5"/>
<entry page="975" page_label="951" name="Properties of e^j\theta" chapter="C.6"/>
<entry page="975" page_label="951" name="Rotation" chapter="C.7"/>
<entry page="976" page_label="952" name="Complex Functions of Time" chapter="C.8"/>
<entry page="976" page_label="952" name="Numerical Examples" chapter="C.9"/>
</entry>
<entry page="981" page_label="957" name="Solving Simultaneous Linear Equations" chapter="D"/>
<entry page="983" page_label="959" name="Answers to Selected Problems"/>
<entry page="995" page_label="971" name="Figure Acknowledgments"/>
<entry page="997" page_label="973" name="Index"/>
</table_of_contents>
...@@ -7,6 +7,7 @@ def url_class(url): ...@@ -7,6 +7,7 @@ def url_class(url):
return "" return ""
%> %>
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%! from courseware.courses import has_staff_access_to_course_id %>
<nav class="${active_page} course-material"> <nav class="${active_page} course-material">
<div class="inner-wrapper"> <div class="inner-wrapper">
...@@ -16,10 +17,10 @@ def url_class(url): ...@@ -16,10 +17,10 @@ def url_class(url):
% if user.is_authenticated(): % if user.is_authenticated():
% if settings.MITX_FEATURES.get('ENABLE_TEXTBOOK'): % if settings.MITX_FEATURES.get('ENABLE_TEXTBOOK'):
<li class="book"><a href="${reverse('book', args=[course.id])}" class="${url_class('book')}">Textbook</a></li> <li class="book"><a href="${reverse('book', args=[course.id])}" class="${url_class('book')}">Textbook</a></li>
% endif % endif
% if settings.MITX_FEATURES.get('ENABLE_DISCUSSION'): % if settings.MITX_FEATURES.get('ENABLE_DISCUSSION'):
<li class="discussion"><a href="${reverse('questions')}">Discussion</a></li> <li class="discussion"><a href="${reverse('questions')}">Discussion</a></li>
% endif % endif
% endif % endif
% if settings.WIKI_ENABLED: % if settings.WIKI_ENABLED:
<li class="wiki"><a href="${reverse('wiki_root', args=[course.id])}" class="${url_class('wiki')}">Wiki</a></li> <li class="wiki"><a href="${reverse('wiki_root', args=[course.id])}" class="${url_class('wiki')}">Wiki</a></li>
...@@ -27,6 +28,10 @@ def url_class(url): ...@@ -27,6 +28,10 @@ def url_class(url):
% if user.is_authenticated(): % if user.is_authenticated():
<li class="profile"><a href="${reverse('profile', args=[course.id])}" class="${url_class('profile')}">Profile</a></li> <li class="profile"><a href="${reverse('profile', args=[course.id])}" class="${url_class('profile')}">Profile</a></li>
% endif % endif
% if has_staff_access_to_course_id(user, course.id):
<li class="instructor"><a href="${reverse('instructor_dashboard', args=[course.id])}" class="${url_class('instructor')}">Instructor</a></li>
% endif
</ol> </ol>
</div> </div>
</nav> </nav>
\ No newline at end of file
<%inherit file="main.html" />
<%! from django.core.urlresolvers import reverse %>
<%namespace name='static' file='static_content.html'/>
<%include file="course_navigation.html" args="active_page=''" />
<section class="container">
<div class="gradebook-summary-wrapper">
<section class="gradebook-summary-content">
<h1>Grade summary</h1>
<p>Not implemented yet</p>
</section>
</div>
</section>
<%inherit file="main.html" /> <%inherit file="main.html" />
<%! from django.core.urlresolvers import reverse %>
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<%block name="js_extra"> <%block name="js_extra">
...@@ -8,30 +9,32 @@ ...@@ -8,30 +9,32 @@
</%block> </%block>
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/>
<style type="text/css"> <style type="text/css">
.grade_a {color:green;} .grade_A {color:green;}
.grade_b {color:Chocolate;} .grade_B {color:Chocolate;}
.grade_c {color:DarkSlateGray;} .grade_C {color:DarkSlateGray;}
.grade_f {color:DimGray;} .grade_F {color:DimGray;}
.grade_none {color:LightGray;} .grade_None {color:LightGray;}
</style> </style>
</%block> </%block>
<%include file="navigation.html" args="active_page=''" /> <%include file="course_navigation.html" args="active_page=''" />
<section class="container"> <section class="container">
<div class="gradebook-wrapper"> <div class="gradebook-wrapper">
<section class="gradebook-content"> <section class="gradebook-content">
<h1>Gradebook</h1> <h1>Gradebook</h1>
%if len(students) > 0: %if len(students) > 0:
<table> <table>
<% <%
templateSummary = students[0]['grade_info']['grade_summary'] templateSummary = students[0]['grade_summary']
%> %>
<tr> <!-- Header Row --> <tr> <!-- Header Row -->
<th>Student</th> <th>Student</th>
%for section in templateSummary['section_breakdown']: %for section in templateSummary['section_breakdown']:
...@@ -39,29 +42,32 @@ ...@@ -39,29 +42,32 @@
%endfor %endfor
<th>Total</th> <th>Total</th>
</tr> </tr>
<%def name="percent_data(percentage)"> <%def name="percent_data(fraction)">
<% <%
data_class = "grade_none" letter_grade = 'None'
if percentage > .87: if fraction > 0:
data_class = "grade_a" letter_grade = 'F'
elif percentage > .70: for grade in ['A', 'B', 'C']:
data_class = "grade_b" if fraction >= course.grade_cutoffs[grade]:
elif percentage > .6: letter_grade = grade
data_class = "grade_c" break
elif percentage > 0:
data_class = "grade_f" data_class = "grade_" + letter_grade
%> %>
<td class="${data_class}">${ "{0:.0%}".format( percentage ) }</td> <td class="${data_class}" data-percent="${fraction}">${ "{0:.0f}".format( 100 * fraction ) }</td>
</%def> </%def>
%for student in students: %for student in students:
<tr> <tr>
<td><a href="/profile/${student['id']}/">${student['username']}</a></td> <td><a href="${reverse('student_profile',
%for section in student['grade_info']['grade_summary']['section_breakdown']: kwargs=dict(course_id=course_id,
student_id=student['id']))}">
${student['username']}</a></td>
%for section in student['grade_summary']['section_breakdown']:
${percent_data( section['percent'] )} ${percent_data( section['percent'] )}
%endfor %endfor
<th>${percent_data( student['grade_info']['grade_summary']['percent'])}</th> <th>${percent_data( student['grade_summary']['percent'])}</th>
</tr> </tr>
%endfor %endfor
</table> </table>
......
<%inherit file="main.html" />
<%! from django.core.urlresolvers import reverse %>
<%namespace name='static' file='static_content.html'/>
<%block name="headextra">
<%static:css group='course'/>
</%block>
<%include file="course_navigation.html" args="active_page='instructor'" />
<section class="container">
<div class="instructor-dashboard-wrapper">
<section class="instructor-dashboard-content">
<h1>Instructor Dashboard</h1>
<p>
<a href="${reverse('gradebook', kwargs=dict(course_id=course.id))}">Gradebook</a>
<p>
<a href="${reverse('grade_summary', kwargs=dict(course_id=course.id))}">Grade summary</a>
</section>
</div>
</section>
...@@ -27,9 +27,11 @@ ...@@ -27,9 +27,11 @@
<span>Not enrolled? <a href="#signup-modal" class="close-login" rel="leanModal">Sign up.</a></span> <span>Not enrolled? <a href="#signup-modal" class="close-login" rel="leanModal">Sign up.</a></span>
<a href="#forgot-password-modal" rel="leanModal" class="pwd-reset">Forgot password?</a> <a href="#forgot-password-modal" rel="leanModal" class="pwd-reset">Forgot password?</a>
</p> </p>
% if settings.MITX_FEATURES.get('AUTH_USE_OPENID'):
<p> <p>
<a href="${MITX_ROOT_URL}/openid/login/">login via openid</a> <a href="${MITX_ROOT_URL}/openid/login/">login via openid</a>
</p> </p>
% endif
</section> </section>
<div class="close-modal"> <div class="close-modal">
......
...@@ -25,9 +25,9 @@ ...@@ -25,9 +25,9 @@
<%include file="navigation.html" /> <%include file="navigation.html" />
<section class="content-wrapper"> <section class="content-wrapper">
${self.body()} ${self.body()}
<%block name="bodyextra"/>
</section> </section>
<%block name="bodyextra"/>
<%include file="footer.html" /> <%include file="footer.html" />
<%static:js group='application'/> <%static:js group='application'/>
......
...@@ -7,7 +7,12 @@ ...@@ -7,7 +7,12 @@
<header class="global" aria-label="Global Navigation"> <header class="global" aria-label="Global Navigation">
<nav> <nav>
<h1 class="logo"><a href="${reverse('root')}"></a></h1> <h1 class="logo"><a href="${reverse('root')}"></a></h1>
<ol class="left">
%if course:
<h2><span class="provider">${course.org}:</span> ${course.number} ${course.title}</h2>
%endif
<ol class="left find-courses-button">
<li class="primary"> <li class="primary">
<a href="${reverse('courses')}">Find Courses</a> <a href="${reverse('courses')}">Find Courses</a>
</li> </li>
...@@ -37,11 +42,15 @@ ...@@ -37,11 +42,15 @@
<a href="${reverse('about_edx')}">About</a> <a href="${reverse('about_edx')}">About</a>
<a href="http://edxonline.tumblr.com/">Blog</a> <a href="http://edxonline.tumblr.com/">Blog</a>
<a href="${reverse('jobs')}">Jobs</a> <a href="${reverse('jobs')}">Jobs</a>
% if not settings.MITX_FEATURES['DISABLE_LOGIN_BUTTON']:
<a href="#login-modal" id="login" rel="leanModal">Log In</a> <a href="#login-modal" id="login" rel="leanModal">Log In</a>
% endif
</li> </li>
% if not settings.MITX_FEATURES['DISABLE_LOGIN_BUTTON']:
<li class="primary"> <li class="primary">
<a href="#signup-modal" id="signup" rel="leanModal">Sign Up</a> <a href="#signup-modal" id="signup" rel="leanModal">Sign Up</a>
</li> </li>
% endif
</ol> </ol>
</nav> </nav>
%endif %endif
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
if(json.success) { if(json.success) {
location.href="${reverse('dashboard')}"; location.href="${reverse('dashboard')}";
}else{ }else{
$('#register_message).html("<p><font color='red'>" + json.error + "</font></p>") $('#register_message').html("<p><font color='red'>" + json.error + "</font></p>");
} }
}); });
})(this) })(this)
......
...@@ -18,99 +18,98 @@ ...@@ -18,99 +18,98 @@
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.stack.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.stack.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.symbol.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.symbol.js')}"></script>
<script> <script>
${profile_graphs.body(grade_summary, "grade-detail-graph")} ${profile_graphs.body(grade_summary, course.grade_cutoffs, "grade-detail-graph")}
</script> </script>
<script> <script>
var loc=true; // Activate on clicks? Not if already clicked. var loc=true; // Activate on clicks? Not if already clicked.
var lang=true; var lang=true;
$(function() { $(function() {
$("#change_location").click(function() { $("#change_location").click(function() {
$(this).hide(); $(this).hide();
log_event("profile", {"type":"location_show", "old":$("#location_sub").text()}); log_event("profile", {"type":"location_show", "old":$("#location_sub").text()});
if(loc) { if(loc) {
$("#description").html('<div>'+ $("#description").html('<div>'+
"Preferred format is city, state, country (so for us, "+ "Preferred format is city, state, country (so for us, "+
"&quot;Cambridge, Massachusetts, USA&quot;), but give "+ "&quot;Cambridge, Massachusetts, USA&quot;), but give "+
"as much or as little detail as you want. </div>"); "as much or as little detail as you want. </div>");
loc=false; loc=false;
$("#location_sub").html('<form>'+'<input id="id_loc_text" type="text" name="loc_text" />'+
'<input type="submit" id="change_loc_button" value="Save" />'+'</form>');
$("#change_loc_button").click(function() {
$("#change_location").show();
postJSON('/change_setting', {'location':$("#id_loc_text").attr("value")}, function(json) {
$("#location_sub").text(json.location);
loc=true;
$("#description").html("");
log_event("profile", {"type":"location_change", "new":json.location});
});
});
}
});
$('#change_password').click(function(){
$('.modal').trigger('click');
log_event("profile", {"type":"password_show"});
});
$('#pwd_reset_button').click(function() {
$.postWithPrefix('/password_reset/',{ "csrfmiddlewaretoken" : "${ csrf }",
"email" : $('#id_email').val()}, function(data){
$("#password_reset_complete_link").click();
log_event("profile", {"type":"password_send"});
});
});
$("#location_sub").html('<form>'+'<input id="id_loc_text" type="text" name="loc_text" />'+
'<input type="submit" id="change_loc_button" value="Save" />'+'</form>');
$("#change_email_form").submit(function(){ $("#change_loc_button").click(function() {
var new_email = $('#new_email_field').val(); $("#change_location").show();
var new_password = $('#new_email_password').val();
postJSON('/change_setting', {'location':$("#id_loc_text").attr("value")}, function(json) {
postJSON('/change_email',{"new_email":new_email, $("#location_sub").text(json.location);
"password":new_password}, loc=true;
function(data){ $("#description").html("");
if(data.success){ log_event("profile", {"type":"location_change", "new":json.location});
$("#change_email").html("<h1>Please verify your new email</h1><p>You'll receive a confirmation in your in-box. Please click the link in the email to confirm the email change.</p>"); });
} else { });
$("#change_email_error").html(data.error); }
} });
});
log_event("profile", {"type":"email_change_request", $('#change_password').click(function(){
"old_email":"${email}", $('.modal').trigger('click');
"new_email":new_email}); log_event("profile", {"type":"password_show"});
return false;
}); });
$("#change_name_form").submit(function(){ $('#pwd_reset_button').click(function() {
var new_name = $('#new_name_field').val(); $.postWithPrefix('/password_reset/',{ "csrfmiddlewaretoken" : "${ csrf }",
var rationale = $('#name_rationale_field').val(); "email" : $('#id_email').val()}, function(data){
$("#password_reset_complete_link").click();
postJSON('/change_name',{"new_name":new_name, log_event("profile", {"type":"password_send"});
"rationale":rationale},
function(data){
if(data.success){
$("#apply_name_change").html("<h1>Your request has been submitted.</h1><p>We'll send you an e-mail when approve the change or need further information. Please allow for up to a week for us to process your request.</p>");
} else {
$("#change_name_error").html(data.error);
}
});
log_event("profile", {"type":"name_change_request",
"new_name":new_name,
"rationale":rationale});
return false;
}); });
});
$("#change_email_form").submit(function(){
var new_email = $('#new_email_field').val();
var new_password = $('#new_email_password').val();
postJSON('/change_email',{"new_email":new_email,
"password":new_password},
function(data){
if(data.success){
$("#change_email").html("<h1>Please verify your new email</h1><p>You'll receive a confirmation in your in-box. Please click the link in the email to confirm the email change.</p>");
} else {
$("#change_email_error").html(data.error);
}
});
log_event("profile", {"type":"email_change_request",
"old_email":"${email}",
"new_email":new_email});
return false;
});
$("#change_name_form").submit(function(){
var new_name = $('#new_name_field').val();
var rationale = $('#name_rationale_field').val();
postJSON('/change_name',{"new_name":new_name,
"rationale":rationale},
function(data){
if(data.success){
$("#apply_name_change").html("<h1>Your request has been submitted.</h1><p>We'll send you an e-mail when approve the change or need further information. Please allow for up to a week for us to process your request.</p>");
} else {
$("#change_name_error").html(data.error);
}
});
log_event("profile", {"type":"name_change_request",
"new_name":new_name,
"rationale":rationale});
return false;
});
}); });
</script> </script>
</%block> </%block>
<%include file="course_navigation.html" args="active_page='profile'" /> <%include file="course_navigation.html" args="active_page='profile'" />
<section class="container"> <section class="container">
...@@ -138,20 +137,27 @@ $(function() { ...@@ -138,20 +137,27 @@ $(function() {
percentageString = "{0:.0%}".format( float(earned)/total) if earned > 0 and total > 0 else "" percentageString = "{0:.0%}".format( float(earned)/total) if earned > 0 and total > 0 else ""
%> %>
<h3><a href="${reverse('courseware_section', kwargs={'course_id' : course.id, 'chapter' : chapter['url_name'], 'section' : section['url_name']})}"> <h3><a href="${reverse('courseware_section', kwargs=dict(course_id=course.id, chapter=chapter['url_name'], section=section['url_name']))}">
${ section['display_name'] }</a> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )}</h3> ${ section['display_name'] }</a><span> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )}</span></h3>
${section['format']} <p>
%if 'due' in section and section['due']!="": ${section['format']}
due ${section['due']}
%endif %if 'due' in section and section['due']!="":
<em>
due ${section['due']}
</em>
%endif
</p>
%if len(section['scores']) > 0: %if len(section['scores']) > 0:
<ol class="scores"> <section class="scores">
${ "Problem Scores: " if section['graded'] else "Practice Scores: "} <h3> ${ "Problem Scores: " if section['graded'] else "Practice Scores: "} </h3>
%for score in section['scores']: <ol>
<li class="score">${"{0:.3n}/{1:.3n}".format(float(score.earned),float(score.possible))}</li> %for score in section['scores']:
%endfor <li>${"{0:.3n}/{1:.3n}".format(float(score.earned),float(score.possible))}</li>
</ol> %endfor
</ol>
</section>
%endif %endif
</li> <!--End section--> </li> <!--End section-->
...@@ -202,7 +208,7 @@ $(function() { ...@@ -202,7 +208,7 @@ $(function() {
</div> </div>
</section> </section>
<div id="password_reset_complete" class="leanModal_box"> <div id="password_reset_complete" class="modal">
<a href="#password_reset_complete" rel="leanModal" id="password_reset_complete_link"></a> <a href="#password_reset_complete" rel="leanModal" id="password_reset_complete_link"></a>
<h1>Password Reset Email Sent</h1> <h1>Password Reset Email Sent</h1>
<p> <p>
...@@ -210,83 +216,78 @@ $(function() { ...@@ -210,83 +216,78 @@ $(function() {
</p> </p>
</div> </div>
<div id="apply_name_change" class="leanModal_box"> <div id="apply_name_change" class="modal">
<h1>Apply to change your name</h1> <div class="inner-wrapper">
<form id="change_name_form"> <header>
<div id="change_name_error"> </div> <h2>Apply to change your name</h2>
<fieldset> <hr />
<p>To uphold the credibility of <span class="edx">edX</span> certificates, name changes must go through an approval process. A member of the course staff will review your request, and if approved, update your information. Please allow up to a week for your request to be processed. Thank you.</p> </header>
<ul> <form id="change_name_form">
<li> <div id="change_name_error"> </div>
<label>Enter your desired full name, as it will appear on the <span class="edx">edX</span> Certificate: </label> <fieldset>
<input id="new_name_field" value="" type="text" /> <p>To uphold the credibility of <span class="edx">edX</span> certificates, name changes must go through an approval process. A member of the course staff will review your request, and if approved, update your information. Please allow up to a week for your request to be processed. Thank you.</p>
</li> <label>Enter your desired full name, as it will appear on the <span class="edx">edX</span> Certificate: </label>
<li> <input id="new_name_field" value="" type="text" />
<label>Reason for name change:</label> <label>Reason for name change:</label>
<textarea id="name_rationale_field" value=""></textarea> <textarea id="name_rationale_field" value=""></textarea>
</li> <input type="submit" id="submit">
<li> </fieldset>
<input type="submit" id="submit"> </form>
</li> </div>
</ul>
</fieldset>
</form>
</div> </div>
<div id="change_email" class="leanModal_box"> <div id="change_email" class="modal">
<h1>Change e-mail</h1> <div class="inner-wrapper">
<header>
<h2>Change e-mail</h2>
<hr />
</header>
<div id="apply_name_change_error"></div> <div id="apply_name_change_error"></div>
<form id="change_email_form"> <form id="change_email_form">
<div id="change_email_error"> </div> <div id="change_email_error"> </div>
<fieldset> <fieldset>
<ul> <label> Please enter your new email address: </label>
<li> <input id="new_email_field" type="email" value="" />
<label> Please enter your new email address: </label> <label> Please confirm your password: </label>
<input id="new_email_field" type="email" value="" /> <input id="new_email_password" value="" type="password" />
</li> <p>We will send a confirmation to both ${email} and your new e-mail as part of the process.</p>
<input type="submit" id="submit_email_change" />
<li>
<label> Please confirm your password: </label>
<input id="new_email_password" value="" type="password" />
</li>
<li>
<p>We will send a confirmation to both ${email} and your new e-mail as part of the process.</p>
<input type="submit" id="submit_email_change" />
</li>
</ul>
</fieldset>
</form>
</div>
<div id="deactivate-account" class="leanModal_box">
<h1>Deactivate <span class="edx">edX</span> Account</h1>
<p>Once you deactivate you&rsquo;re MIT<em>x</em> account you will no longer recieve updates and new class announcements from MIT<em>x</em>.</p>
<p>If you&rsquo;d like to still get updates and new class announcements you can just <a href="#unenroll" rel="leanModal">unenroll</a> and keep your account active.</p>
<form id="unenroll_form">
<div id="unenroll_error"> </div>
<fieldset>
<ul>
<li>
<input type="submit" id="" value="Yes, I don't want an edX account or hear about any new classes or updates to edX" />
</li>
</ul>
</fieldset> </fieldset>
</form> </form>
</div>
</div> </div>
<div id="unenroll" class="leanModal_box"> <div id="deactivate-account" class="modal">
<h1>Unenroll from 6.002x</h1> <div class="inner-wrapper">
<p>Please note: you will still receive updates and new class announcements from ed<em>X</em>. If you don&rsquo;t wish to receive any more updates or announcements <a href="#deactivate-account" rel="leanModal">deactivate your account</a>.</p> <header>
<h2>Deactivate <span class="edx">edX</span> Account</h2>
<hr />
</header>
<p>Once you deactivate you&rsquo;re MIT<em>x</em> account you will no longer recieve updates and new class announcements from MIT<em>x</em>.</p>
<p>If you&rsquo;d like to still get updates and new class announcements you can just <a href="#unenroll" rel="leanModal">unenroll</a> and keep your account active.</p>
<form id="unenroll_form">
<div id="unenroll_error"> </div>
<fieldset>
<input type="submit" id="" value="Yes, I don't want an edX account or hear about any new classes or updates to edX" />
</fieldset>
</form>
</div>
</div>
<form id="unenroll_form"> <div id="unenroll" class="modal">
<div id="unenroll_error"> </div> <div class="inner-wrapper">
<fieldset> <header>
<ul> <h2>Unenroll from 6.002x</h2>
<li> <hr />
<input type="submit" id="" value="Yes, I want to unenroll from 6.002x but still hear about any new classes or updates to edX" /> </header>
</li> <p>Please note: you will still receive updates and new class announcements from ed<em>X</em>. If you don&rsquo;t wish to receive any more updates or announcements <a href="#deactivate-account" rel="leanModal">deactivate your account</a>.</p>
</ul>
</fieldset> <form id="unenroll_form">
</form> <div id="unenroll_error"> </div>
<fieldset>
<input type="submit" id="" value="Yes, I want to unenroll from 6.002x but still hear about any new classes or updates to edX" />
</fieldset>
</form>
</div>
</div> </div>
<%page args="grade_summary, graph_div_id, **kwargs"/> <%page args="grade_summary, grade_cutoffs, graph_div_id, **kwargs"/>
<%! <%!
import json import json
import math
%> %>
$(function () { $(function () {
...@@ -89,8 +90,16 @@ $(function () { ...@@ -89,8 +90,16 @@ $(function () {
ticks += [ [overviewBarX, "Total"] ] ticks += [ [overviewBarX, "Total"] ]
tickIndex += 1 + sectionSpacer tickIndex += 1 + sectionSpacer
totalScore = grade_summary['percent'] totalScore = math.floor(grade_summary['percent'] * 100) / 100 #We floor it to the nearest percent, 80.9 won't show up like a 90 (an A)
detail_tooltips['Dropped Scores'] = dropped_score_tooltips detail_tooltips['Dropped Scores'] = dropped_score_tooltips
## ----------------------------- Grade cutoffs ------------------------- ##
grade_cutoff_ticks = [ [1, "100%"], [0, "0%"] ]
for grade in ['A', 'B', 'C']:
percent = grade_cutoffs[grade]
grade_cutoff_ticks.append( [ percent, "{0} {1:.0%}".format(grade, percent) ] )
%> %>
var series = ${ json.dumps( series ) }; var series = ${ json.dumps( series ) };
...@@ -98,6 +107,7 @@ $(function () { ...@@ -98,6 +107,7 @@ $(function () {
var bottomTicks = ${ json.dumps(bottomTicks) }; var bottomTicks = ${ json.dumps(bottomTicks) };
var detail_tooltips = ${ json.dumps(detail_tooltips) }; var detail_tooltips = ${ json.dumps(detail_tooltips) };
var droppedScores = ${ json.dumps(droppedScores) }; var droppedScores = ${ json.dumps(droppedScores) };
var grade_cutoff_ticks = ${ json.dumps(grade_cutoff_ticks) }
//Alwasy be sure that one series has the xaxis set to 2, or the second xaxis labels won't show up //Alwasy be sure that one series has the xaxis set to 2, or the second xaxis labels won't show up
series.push( {label: 'Dropped Scores', data: droppedScores, points: {symbol: "cross", show: true, radius: 3}, bars: {show: false}, color: "#333"} ); series.push( {label: 'Dropped Scores', data: droppedScores, points: {symbol: "cross", show: true, radius: 3}, bars: {show: false}, color: "#333"} );
...@@ -107,10 +117,10 @@ $(function () { ...@@ -107,10 +117,10 @@ $(function () {
lines: {show: false, steps: false }, lines: {show: false, steps: false },
bars: {show: true, barWidth: 0.8, align: 'center', lineWidth: 0, fill: .8 },}, bars: {show: true, barWidth: 0.8, align: 'center', lineWidth: 0, fill: .8 },},
xaxis: {tickLength: 0, min: 0.0, max: ${tickIndex - sectionSpacer}, ticks: ticks, labelAngle: 90}, xaxis: {tickLength: 0, min: 0.0, max: ${tickIndex - sectionSpacer}, ticks: ticks, labelAngle: 90},
yaxis: {ticks: [[1, "100%"], [0.87, "A 87%"], [0.7, "B 70%"], [0.6, "C 60%"], [0, "0%"]], min: 0.0, max: 1.0, labelWidth: 50}, yaxis: {ticks: grade_cutoff_ticks, min: 0.0, max: 1.0, labelWidth: 50},
grid: { hoverable: true, clickable: true, borderWidth: 1, grid: { hoverable: true, clickable: true, borderWidth: 1,
markings: [ {yaxis: {from: 0.87, to: 1 }, color: "#ddd"}, {yaxis: {from: 0.7, to: 0.87 }, color: "#e9e9e9"}, markings: [ {yaxis: {from: ${grade_cutoffs['A']}, to: 1 }, color: "#ddd"}, {yaxis: {from: ${grade_cutoffs['B']}, to: ${grade_cutoffs['A']} }, color: "#e9e9e9"},
{yaxis: {from: 0.6, to: 0.7 }, color: "#f3f3f3"}, ] }, {yaxis: {from: ${grade_cutoffs['C']}, to: ${grade_cutoffs['B']} }, color: "#f3f3f3"}, ] },
legend: {show: false}, legend: {show: false},
}; };
......
...@@ -71,9 +71,9 @@ ...@@ -71,9 +71,9 @@
}); });
</script> </script>
<%block name="wiki_head"/> <%block name="wiki_head"/>
</%block> </%block>
<%block name="bodyextra"> <%block name="bodyextra">
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
<div class="wiki-wrapper"> <div class="wiki-wrapper">
<%block name="wiki_panel"> <%block name="wiki_panel">
<div aria-label="Wiki Navigation" id="wiki_panel"> <div aria-label="Wiki Navigation" id="wiki_panel">
<h2>Course Wiki</h2> <h2>Course Wiki</h2>
<ul class="action"> <ul class="action">
<li> <li>
<h3> <h3>
...@@ -101,12 +101,12 @@ ...@@ -101,12 +101,12 @@
<div id="wiki_create_form"> <div id="wiki_create_form">
<% <%
baseURL = wiki_reverse("wiki_create", course=course, kwargs={"article_path" : namespace + "/" }) baseURL = wiki_reverse("wiki_create", course=course, kwargs={"article_path" : namespace + "/" })
%> %>
<form method="GET" onsubmit="this.action='${baseURL}' + this.wiki_article_name.value.replace(/([^a-zA-Z0-9\-])/g, '');"> <form method="GET" onsubmit="this.action='${baseURL}' + this.wiki_article_name.value.replace(/([^a-zA-Z0-9\-])/g, '');">
<div> <div>
<label for="id_wiki_article_name">Title of article</label> <label for="id_wiki_article_name">Title of article</label>
<input type="text" name="wiki_article_name" id="id_wiki_article_name" /><br/> <input type="text" name="wiki_article_name" id="id_wiki_article_name" />
</div> </div>
<ul> <ul>
<li> <li>
...@@ -130,31 +130,31 @@ ...@@ -130,31 +130,31 @@
</%block> </%block>
<section class="wiki-body"> <section class="wiki-body">
%if wiki_article is not UNDEFINED: %if wiki_article is not UNDEFINED:
<header> <header>
%if wiki_article.locked: %if wiki_article.locked:
<p><strong>This article has been locked</strong></p> <p><strong>This article has been locked</strong></p>
%endif
<p>Last modified: ${wiki_article.modified_on.strftime("%b %d, %Y, %I:%M %p")}</p>
%endif
%if wiki_article is not UNDEFINED:
<ul>
<li>
<a href="${ wiki_reverse('wiki_view', wiki_article, course)}" class="view">View</a>
</li>
<li>
<a href="${ wiki_reverse('wiki_edit', wiki_article, course)}" class="edit">Edit</a>
</li>
<li>
<a href="${ wiki_reverse('wiki_history', wiki_article, course)}" class="history">History</a>
</li>
</ul>
</header>
%endif %endif
<p>Last modified: ${wiki_article.modified_on.strftime("%b %d, %Y, %I:%M %p")}</p>
%endif
%if wiki_article is not UNDEFINED:
<ul>
<li>
<a href="${ wiki_reverse('wiki_view', wiki_article, course)}" class="view">View</a>
</li>
<li>
<a href="${ wiki_reverse('wiki_edit', wiki_article, course)}" class="edit">Edit</a>
</li>
<li>
<a href="${ wiki_reverse('wiki_history', wiki_article, course)}" class="history">History</a>
</li>
</ul>
</header>
%endif
<%block name="wiki_page_title"/> <%block name="wiki_page_title"/>
<%block name="wiki_body"/> <%block name="wiki_body"/>
......
...@@ -65,12 +65,10 @@ ${"Edit " + wiki_title + " - " if wiki_title is not UNDEFINED else ""}MITx 6.002 ...@@ -65,12 +65,10 @@ ${"Edit " + wiki_title + " - " if wiki_title is not UNDEFINED else ""}MITx 6.002
</div> </div>
${wiki_form} ${wiki_form}
%if create_article: %if create_article:
<input type="submit" id="submit_edit" value="Create article" /></td> <input type="submit" id="submit_edit" value="Create article" />
%else: %else:
<input type="submit" id="submit_edit" name="edit" value="Save Changes" /> <input type="submit" id="submit_edit" name="edit" value="Save Changes" />
<input type="submit" id="submit_delete" name="delete" value="Delete article" /> <input type="submit" id="submit_delete" name="delete" value="Delete article" />
%endif
</form>
<%include file="simplewiki_instructions.html"/> <%include file="simplewiki_instructions.html"/>
......
...@@ -26,7 +26,7 @@ Displaying all articles ...@@ -26,7 +26,7 @@ Displaying all articles
<li><h3><a href="${wiki_reverse("wiki_view", article, course)}">${article.title} ${'(Deleted)' if article_deleted else ''}</a></h3></li> <li><h3><a href="${wiki_reverse("wiki_view", article, course)}">${article.title} ${'(Deleted)' if article_deleted else ''}</a></h3></li>
%endfor %endfor
%if not wiki_search_results: %if not wiki_search_results:
No articles matching <b>${wiki_search_query if wiki_search_query is not UNDEFINED else ""} </b>! No articles matching <b>${wiki_search_query if wiki_search_query is not UNDEFINED else ""} </b>!
%endif %endif
</ul> </ul>
......
${module_content} ${module_content}
%if edit_link: %if edit_link:
<div><a href="${edit_link}">Edit</a></div> <div><a href="${edit_link}">Edit</a> / <a href="#${element_id}_xqa-modal" onclick="getlog_${element_id}()" id="${element_id}_xqa_log">QA</a></div>
% endif % endif
<div class="staff_info"> <div><a href="#${element_id}_debug" id="${element_id}_trig">Staff Debug Info</a></div>
<section id="${element_id}_xqa-modal" class="modal xqa-modal" style="width:80%; left:20%; height:80%; overflow:auto" >
<div class="inner-wrapper">
<header>
<h2>edX Content Quality Assessment</h2>
</header>
<form id="${element_id}_xqa_form" class="xqa_form">
<label>Comment</label>
<input id="${element_id}_xqa_entry" type="text" placeholder="comment">
<label>Tag</label>
<span style="color:black;vertical-align: -10pt">Optional tag (eg "done" or "broken"):&nbsp; </span>
<input id="${element_id}_xqa_tag" type="text" placeholder="tag" style="width:80px;display:inline">
<div class="submit">
<button name="submit" type="submit">Add comment</button>
</div>
<hr>
<div id="${element_id}_xqa_log_data"></div>
</form>
</div>
</section>
<section class="modal staff-modal" id="${element_id}_debug" style="width:80%; left:20%; height:80%; overflow:auto;" >
<div class="inner-wrapper" style="color:black">
<header>
<h2>Staff Debug</h2>
</header>
<div class="staff_info">
location = ${location | h}
github = <a href="${edit_link}">${edit_link | h}</a>
definition = <pre>${definition | h}</pre> definition = <pre>${definition | h}</pre>
metadata = ${metadata | h} metadata = ${metadata | h}
</div> category = ${category | h}
%if render_histogram: </div>
<div id="histogram_${element_id}" class="histogram" data-histogram="${histogram}"></div> %if render_histogram:
%endif <div id="histogram_${element_id}" class="histogram" data-histogram="${histogram}"></div>
%endif
</div>
</section>
<div id="${element_id}_setup"></div>
## leanModal needs to be included here otherwise this breaks when in a <vertical>
<script type="text/javascript" src="/static/js/vendor/jquery.leanModal.min.js"></script>
<script type="text/javascript">
function setup_debug_${element_id}(){
$('#${element_id}_trig').leanModal();
$('#${element_id}_xqa_log').leanModal();
$('#${element_id}_xqa_form').submit(sendlog_${element_id});
}
setup_debug_${element_id}();
function sendlog_${element_id}(){
var xqaLog = {authkey: '${xqa_key}',
location: '${location}',
%if edit_link:
giturl: '${edit_link}',
%endif
category : '${category}',
username : '${user.username}',
return : 'query',
format : 'html',
email : '${user.email}',
tag:$('#${element_id}_xqa_tag').val(),
entry: $('#${element_id}_xqa_entry').val()};
$.ajax({
url: '${xqa_server}/log',
type: 'GET',
contentType: 'application/json',
data: JSON.stringify(xqaLog),
crossDomain: true,
dataType: 'jsonp',
beforeSend: function (xhr) { xhr.setRequestHeader ("Authorization", "Basic eHFhOmFnYXJ3YWw="); },
timeout : 1000,
success: function(result) {
$('#${element_id}_xqa_log_data').html(result);
},
error: function() {
alert('Error: cannot connect to XQA server');
console.log('error!');
}
});
return false;
};
function getlog_${element_id}(){
var xqaQuery = {authkey: '${xqa_key}',
location: '${location}',
format: 'html'};
$.ajax({
url: '${xqa_server}/query',
type: 'GET',
contentType: 'application/json',
data: JSON.stringify(xqaQuery),
crossDomain: true,
dataType: 'jsonp',
timeout : 1000,
success: function(result) {
$('#${element_id}_xqa_log_data').html(result);
},
error: function() {
alert('Error: cannot connect to XQA server');
}
});
};
</script>
...@@ -29,49 +29,75 @@ ...@@ -29,49 +29,75 @@
</div> </div>
</section> </section>
<hr class="horizontal-divider"> <hr class="horizontal-divider">
<section class="jobs-wrapper"> <section class="jobs-wrapper">
<section class="jobs-listing"> <section class="jobs-listing">
<article id="edx-fellow" class="job"> <article id="edx-fellow" class="job">
<div class="inner-wrapper"> <div class="inner-wrapper">
<h3>EdX Fellow</h3> <h3>EdX Fellow</h3>
<p>EdX Fellows are immersed in developing innovative solutions for online teaching, learning and research. They partner with faculty and staff from edX universities in the development, implementation and evaluation of online courses. We're looking for candidates with recent masters or doctorate degrees in the social sciences, humanities, natural sciences, engineering, or education. We welcome new ways of thinking about both the promises and practices of online learning.</p> <p>EdX Fellows focus on the development of innovative solutions for online teaching, learning, and research. They create and manage partnerships with faculty and staff from edX universities in the development, implementation, and evaluation of online courses and related learning products. EdX is seeking candidates with doctoral degrees in the social sciences, humanities, natural sciences, engineering, or education, who are committed to the development of innovative pedagogies to improve online teaching and learning.</p>
<p>An ideal candidate will have:</p>
<ul>
<li>experience in teaching and developing online courses, preferably in higher education</li>
<li>exceptional written and communication skills</li>
<li>experience in facilitating and convening teams of higher education faculty</li>
<li>a broad knowledge of, and experience with, research in online learning</li> <li>exceptional organizational and communication skills</li>
<li>proven success in digital project management.</li>
<li>strong background in working with LMS &amp; CMS environments</li>
</ul>
<p>Ability to work in a fast-paced, highly collaborative environment is essential.</p> <p>Ability to work in a fast-paced, highly collaborative environment is essential.</p>
<p>If you are interested in this position, please send an email to <a href='mailto:jobs@edx.org'>jobs@edx.org</a>.</p> <p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
</div> </div>
</article> </article>
<article id="course-manager" class="job">
<article id="platform-developer" class="job">
<div class="inner-wrapper"> <div class="inner-wrapper">
<h3>Platform Developer</h3> <h3>EdX Course Manager</h3>
<p>Platform Developers build the core learning platform that powers edX, writing both front-end and back-end code. They tackle a wide range of technical challenges, and so the best candidates will have a strong background in one or more of the following areas: machine learning, education, user interaction design, big data, social network analysis, and devops. Specialists are encouraged to apply, but team members often wear many hats. Our ideal candidate would have excellent coding skills, a proven history of delivering projects, and a deep research background.</p> <p>Course Managers support edX Fellows and related content staff in the creation and implementation of online courses and other learning products. Course Managers are involved in the complete life-cycle of edX courses, from initial concept through development, launch, and data collection. EdX is seeking Course Managers who have a masters or doctorate degree.</p>
<p>If you are interested in this position, please send an email to <a href='mailto:jobs@edx.org'>jobs@edx.org</a></p> <p>An ideal candidate will have:</p>
<ul>
<li>significant operational experience with online teaching and learning environments; CMS, LMS systems, and with API feature sets.</li>
<li>a broad knowledge of higher education content disciplines</li>
<li>experience with innovative instructional design practices</li>
<li>exceptional organizational and communication skills</li>
<li>proven success in digital project management</li>
<li>a working knowledge of basic computer programming skills, e.g. Python, XML, HTML5</li>
</ul>
<p>Ability to work in a fast-paced, highly collaborative environment is essential.</p>
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a></p>
</div> </div>
</article> </article>
<article id="content-engineer" class="job"> <article id="content-engineer" class="job">
<div class="inner-wrapper"> <div class="inner-wrapper">
<h3>Content Engineer</h3> <h3>EdX Content Engineer</h3>
<p>Content Engineers develop sophisticated, domain-specific tools that enable professors to deliver the best possible educational experience in their classes. Examples include circuit schematic editors, scientific simulators of every kind, and peer collaboration tools. Content Engineers are dedicated to pushing the boundaries of what can be taught and assessed online, and will work closely with edX Fellows and course staff.</p> <p>Content Engineers support edX Fellows and edX Course Managers in the overall technical development of course content, assessments, and domain-specific online tools. Tasks include developing graders for rich problems, designing automated tools for import of problems from other formats, as well as creating new ways for students to interact with domain-specific problems in the system.</p>
<p>Strong JavaScript skills are required. A deep interest and background in pedagogy and education is highly desired. Knowledge of GWT, Backbone.js, and Python a plus.</p> <p>A candidate must have:</p>
<p>If you are interested in this position, please send an email to <a href='mailto:jobs@edx.org'>jobs@edx.org</a>.</p> <ul>
<li>Python or JavaScript development experience</li>
<li>A deep interest in pedagogy and education</li>
</ul>
<p>Knowledge of GWT or Backbone.js a plus.</p> <p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
</div>
</article>
<article id="platform-developer" class="job">
<div class="inner-wrapper">
<h3>Platform Developer</h3>
<p>Platform Developers build the core learning platform that powers edX, writing both front-end and back-end code. They tackle a wide range of technical challenges, and so the best candidates will have a strong background in one or more of the following areas: machine learning, education, user interaction design, big data, social network analysis, and devops. Specialists are encouraged to apply, but team members often wear many hats. Our ideal candidate would have excellent coding skills, a proven history of delivering projects, and a deep research background.</p>
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a></p>
</div> </div>
</article> </article>
</section> </section>
<section class="jobs-sidebar"> <section class="jobs-sidebar">
<h2>Positions</h2> <h2>Positions</h2>
<nav> <nav>
<a href="#edx-fellow">EdX Fellow</a> <a href="#edx-fellow">EdX Fellow</a>
<a href="#course-manager">EdX Course Manager</a>
<a href="#content-engineer">EdX Content Engineer</a>
<a href="#platform-developer">Platform Developer</a> <a href="#platform-developer">Platform Developer</a>
<a href="#content-engineer">Content Engineer</a>
</nav> </nav>
<h2>How to Apply</h2> <h2>How to Apply</h2>
<p>E-mail your resume, coverletter and any other materials to <a href="mailto:jobs@edx.org">jobs@edx.org</a></p> <p>E-mail your resume, coverletter and any other materials to <a href="mailto:jobs@edx.org">jobs@edx.org</a></p>
<h2>Our Location</h2> <h2>Our Location</h2>
<p>11 Cambridge Center <br/> <p>11 Cambridge Center <br>
Cambridge, MA 02142</p> Cambridge, MA 02142</p>
</section> </section>
</section> </section>
</section> </section>
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='course'/>
<%static:js group='courseware'/>
</%block> </%block>
<%block name="js_extra"> <%block name="js_extra">
...@@ -71,7 +72,31 @@ $("#open_close_accordion a").click(function(){ ...@@ -71,7 +72,31 @@ $("#open_close_accordion a").click(function(){
</header> </header>
<ul id="booknav" class="treeview-booknav"> <ul id="booknav" class="treeview-booknav">
<%include file="book_toc.html" /> <%def name="print_entry(entry)">
<li>
<a href="javascript:goto_page(${entry.get('page')})">
<span class="chapter">
%if entry.get('chapter'):
<span class="chapter-number">${entry.get('chapter')}.</span> ${entry.get('name')}
%else:
${entry.get('name')}
%endif
</span>
<span class="page-number">${entry.get('page_label')}</span>
</a>
% if len(entry) > 0:
<ul>
% for child in entry:
${print_entry(child)}
% endfor
</ul>
% endif
</li>
</%def>
% for entry in table_of_contents:
${print_entry(entry)}
% endfor
</ul> </ul>
</section> </section>
...@@ -89,17 +114,6 @@ $("#open_close_accordion a").click(function(){ ...@@ -89,17 +114,6 @@ $("#open_close_accordion a").click(function(){
</nav> </nav>
<img id="bookpage" src="${ settings.BOOK_URL }p${ "%03i"%(page) }.png"> <img id="bookpage" src="${ settings.BOOK_URL }p${ "%03i"%(page) }.png">
<nav class="bottom-nav">
<ul>
<li class="last">
<a href="javascript:prev_page()">Previous page</a>
</li>
<li class="next">
<a href="javascript:next_page()">Next page</a>
</li>
</ul>
</nav>
</section> </section>
</section> </section>
</div> </div>
......
from django.conf import settings from django.conf import settings
from django.conf.urls.defaults import patterns, include, url from django.conf.urls import patterns, include, url
from django.contrib import admin from django.contrib import admin
from django.conf.urls.static import static from django.conf.urls.static import static
import django.contrib.auth.views import django.contrib.auth.views
# Uncomment the next two lines to enable the admin: # Uncomment the next two lines to enable the admin:
...@@ -15,7 +14,7 @@ urlpatterns = ('', ...@@ -15,7 +14,7 @@ urlpatterns = ('',
url(r'^dashboard$', 'student.views.dashboard', name="dashboard"), url(r'^dashboard$', 'student.views.dashboard', name="dashboard"),
url(r'^admin_dashboard$', 'dashboard.views.dashboard'), url(r'^admin_dashboard$', 'dashboard.views.dashboard'),
url(r'^change_email$', 'student.views.change_email_request'), url(r'^change_email$', 'student.views.change_email_request'),
url(r'^email_confirm/(?P<key>[^/]*)$', 'student.views.confirm_email_change'), url(r'^email_confirm/(?P<key>[^/]*)$', 'student.views.confirm_email_change'),
url(r'^change_name$', 'student.views.change_name_request'), url(r'^change_name$', 'student.views.change_name_request'),
...@@ -85,7 +84,6 @@ urlpatterns = ('', ...@@ -85,7 +84,6 @@ urlpatterns = ('',
(r'^pressrelease$', 'django.views.generic.simple.redirect_to', {'url': '/press/uc-berkeley-joins-edx'}), (r'^pressrelease$', 'django.views.generic.simple.redirect_to', {'url': '/press/uc-berkeley-joins-edx'}),
(r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/static/images/favicon.ico'}), (r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/static/images/favicon.ico'}),
# TODO: These urls no longer work. They need to be updated before they are re-enabled # TODO: These urls no longer work. They need to be updated before they are re-enabled
...@@ -98,16 +96,21 @@ if settings.PERFSTATS: ...@@ -98,16 +96,21 @@ if settings.PERFSTATS:
if settings.COURSEWARE_ENABLED: if settings.COURSEWARE_ENABLED:
urlpatterns += ( urlpatterns += (
# Hook django-masquerade, allowing staff to view site as other users
url(r'^masquerade/', include('masquerade.urls')), url(r'^masquerade/', include('masquerade.urls')),
url(r'^jump_to/(?P<location>.*)$', 'courseware.views.jump_to', name="jump_to"), url(r'^jump_to/(?P<location>.*)$', 'courseware.views.jump_to', name="jump_to"),
url(r'^modx/(?P<id>.*?)/(?P<dispatch>[^/]*)$', 'courseware.module_render.modx_dispatch'), #reset_problem'), url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/modx/(?P<id>.*?)/(?P<dispatch>[^/]*)$',
url(r'^xqueue/(?P<userid>[^/]*)/(?P<id>.*?)/(?P<dispatch>[^/]*)$', 'courseware.module_render.xqueue_callback'), 'courseware.module_render.modx_dispatch',
url(r'^change_setting$', 'student.views.change_setting'), name='modx_dispatch'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/xqueue/(?P<userid>[^/]*)/(?P<id>.*?)/(?P<dispatch>[^/]*)$',
'courseware.module_render.xqueue_callback',
name='xqueue_callback'),
url(r'^change_setting$', 'student.views.change_setting',
name='change_setting'),
# TODO: These views need to be updated before they work # TODO: These views need to be updated before they work
# url(r'^calculate$', 'util.views.calculate'), # url(r'^calculate$', 'util.views.calculate'),
# url(r'^gradebook$', 'courseware.views.gradebook'),
# TODO: We should probably remove the circuit package. I believe it was only used in the old way of saving wiki circuits for the wiki # TODO: We should probably remove the circuit package. I believe it was only used in the old way of saving wiki circuits for the wiki
# url(r'^edit_circuit/(?P<circuit>[^/]*)$', 'circuit.views.edit_circuit'), # url(r'^edit_circuit/(?P<circuit>[^/]*)$', 'circuit.views.edit_circuit'),
# url(r'^save_circuit/(?P<circuit>[^/]*)$', 'circuit.views.save_circuit'), # url(r'^save_circuit/(?P<circuit>[^/]*)$', 'circuit.views.save_circuit'),
...@@ -131,12 +134,24 @@ if settings.COURSEWARE_ENABLED: ...@@ -131,12 +134,24 @@ if settings.COURSEWARE_ENABLED:
'staticbook.views.index_shifted'), 'staticbook.views.index_shifted'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/?$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/?$',
'courseware.views.index', name="courseware"), 'courseware.views.index', name="courseware"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/(?P<chapter>[^/]*)/$',
'courseware.views.index', name="courseware_chapter"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/(?P<chapter>[^/]*)/(?P<section>[^/]*)/$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/(?P<chapter>[^/]*)/(?P<section>[^/]*)/$',
'courseware.views.index', name="courseware_section"), 'courseware.views.index', name="courseware_section"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile$',
'courseware.views.profile', name="profile"), 'courseware.views.profile', name="profile"),
# Takes optional student_id for instructor use--shows profile as that student sees it.
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile/(?P<student_id>[^/]*)/$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile/(?P<student_id>[^/]*)/$',
'courseware.views.profile'), 'courseware.views.profile', name="student_profile"),
# For the instructor
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor$',
'courseware.views.instructor_dashboard', name="instructor_dashboard"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/gradebook$',
'courseware.views.gradebook', name='gradebook'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/grade_summary$',
'courseware.views.grade_summary', name='grade_summary'),
) )
# Multicourse wiki # Multicourse wiki
......
# Mapping of
#
# From the /mitx directory:
# /usr/local/Cellar/nginx/1.2.2/sbin/nginx -p `pwd`/ -c nginx.conf
worker_processes 1;
events {
worker_connections 1024;
}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# server_tokens off;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
include /usr/local/etc/nginx/mime.types;
default_type application/octet-stream;
##
# Gzip Settings
##
gzip on;
gzip_disable "msie6";
upstream portal {
server localhost:8000;
}
upstream course_harvardx_cs50_2012 {
server localhost:8001;
}
upstream course_mitx_6002_2012_fall {
server localhost:8002;
}
# Mostly copied from our existing server...
server {
listen 8100 default_server;
rewrite ^(.*)/favicon.ico$ /static/images/favicon.ico last;
# Our catchall
location / {
proxy_pass http://portal;
}
location /courses/HarvardX/CS50x/2012/ {
proxy_pass http://course_harvardx_cs50_2012;
}
location /courses/MITx/6.002x/2012_Fall/ {
proxy_pass http://course_mitx_6002_2012_fall;
}
}
}
...@@ -83,13 +83,21 @@ end ...@@ -83,13 +83,21 @@ end
task :pylint => "pylint_#{system}" task :pylint => "pylint_#{system}"
end end
$failed_tests = 0
def run_tests(system, report_dir) def run_tests(system, report_dir, stop_on_failure=true)
ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml") ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
ENV['NOSE_COVER_HTML_DIR'] = File.join(report_dir, "cover") ENV['NOSE_COVER_HTML_DIR'] = File.join(report_dir, "cover")
sh(django_admin(system, :test, 'test', *Dir["#{system}/djangoapps/*"].each)) dirs = Dir["common/djangoapps/*"] + Dir["#{system}/djangoapps/*"]
sh(django_admin(system, :test, 'test', *dirs.each)) do |ok, res|
if !ok and stop_on_failure
abort "Test failed!"
end
$failed_tests += 1 unless ok
end
end end
TEST_TASKS = []
[:lms, :cms].each do |system| [:lms, :cms].each do |system|
report_dir = File.join(REPORT_DIR, system.to_s) report_dir = File.join(REPORT_DIR, system.to_s)
...@@ -97,15 +105,16 @@ end ...@@ -97,15 +105,16 @@ end
# Per System tasks # Per System tasks
desc "Run all django tests on our djangoapps for the #{system}" desc "Run all django tests on our djangoapps for the #{system}"
task "test_#{system}" => ["clean_test_files", "#{system}:collectstatic:test", "fasttest_#{system}"] task "test_#{system}", [:stop_on_failure] => ["clean_test_files", "#{system}:collectstatic:test", "fasttest_#{system}"]
# Have a way to run the tests without running collectstatic -- useful when debugging without # Have a way to run the tests without running collectstatic -- useful when debugging without
# messing with static files. # messing with static files.
task "fasttest_#{system}" => [report_dir, :predjango] do task "fasttest_#{system}", [:stop_on_failure] => [report_dir, :predjango] do |t, args|
run_tests(system, report_dir) args.with_defaults(:stop_on_failure => 'true')
run_tests(system, report_dir, args.stop_on_failure)
end end
task :test => "test_#{system}" TEST_TASKS << "test_#{system}"
desc <<-desc desc <<-desc
Start the #{system} locally with the specified environment (defaults to dev). Start the #{system} locally with the specified environment (defaults to dev).
...@@ -142,7 +151,17 @@ Dir["common/lib/*"].each do |lib| ...@@ -142,7 +151,17 @@ Dir["common/lib/*"].each do |lib|
ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml") ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
sh("nosetests #{lib} --cover-erase --with-xunit --with-xcoverage --cover-html --cover-inclusive --cover-package #{File.basename(lib)} --cover-html-dir #{File.join(report_dir, "cover")}") sh("nosetests #{lib} --cover-erase --with-xunit --with-xcoverage --cover-html --cover-inclusive --cover-package #{File.basename(lib)} --cover-html-dir #{File.join(report_dir, "cover")}")
end end
task :test => task_name TEST_TASKS << task_name
end
task :test do
TEST_TASKS.each do |task|
Rake::Task[task].invoke(false)
end
if $failed_tests > 0
abort "Tests failed!"
end
end end
task :runserver => :lms task :runserver => :lms
......
...@@ -18,6 +18,7 @@ except Exception as err: ...@@ -18,6 +18,7 @@ except Exception as err:
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from path import path from path import path
from lxml import etree
data_dir = settings.DATA_DIR data_dir = settings.DATA_DIR
print "data_dir = %s" % data_dir print "data_dir = %s" % data_dir
...@@ -26,7 +27,17 @@ for course_dir in os.listdir(data_dir): ...@@ -26,7 +27,17 @@ for course_dir in os.listdir(data_dir):
# print course_dir # print course_dir
if not os.path.isdir(path(data_dir) / course_dir): if not os.path.isdir(path(data_dir) / course_dir):
continue continue
gname = 'staff_%s' % course_dir
cxfn = path(data_dir) / course_dir / 'course.xml'
coursexml = etree.parse(cxfn)
cxmlroot = coursexml.getroot()
course = cxmlroot.get('course')
if course is None:
print "oops, can't get course id for %s" % course_dir
continue
print "course=%s for course_dir=%s" % (course,course_dir)
gname = 'staff_%s' % course
if Group.objects.filter(name=gname): if Group.objects.filter(name=gname):
print "group exists for %s" % gname print "group exists for %s" % gname
continue continue
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment