perform a new merge from master, resolve conflicts
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "common/test/phantom-jasmine"]
|
||||
path = common/test/phantom-jasmine
|
||||
url = https://github.com/jcarver989/phantom-jasmine.git
|
||||
@@ -2,11 +2,13 @@
|
||||
[run]
|
||||
data_file = reports/cms/.coverage
|
||||
source = cms
|
||||
omit = cms/envs/*, cms/manage.py
|
||||
|
||||
[report]
|
||||
ignore_errors = True
|
||||
|
||||
[html]
|
||||
title = CMS Python Test Coverage Report
|
||||
directory = reports/cms/cover
|
||||
|
||||
[xml]
|
||||
|
||||
@@ -36,7 +36,7 @@ file and check it in at the same time as your model changes. To do that,
|
||||
3. Add the migration file created in mitx/common/djangoapps/student/migrations/
|
||||
"""
|
||||
from datetime import datetime
|
||||
from hashlib import sha1
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
@@ -49,7 +49,6 @@ from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
import comment_client as cc
|
||||
from django_comment_client.models import Role
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -197,14 +196,13 @@ def unique_id_for_user(user):
|
||||
"""
|
||||
Return a unique id for a user, suitable for inserting into
|
||||
e.g. personalized survey links.
|
||||
|
||||
Currently happens to be implemented as a sha1 hash of the username
|
||||
(and thus assumes that usernames don't change).
|
||||
"""
|
||||
# Using the user id as the salt because it's sort of random, and is already
|
||||
# in the db.
|
||||
salt = str(user.id)
|
||||
return sha1(salt + user.username).hexdigest()
|
||||
# include the secret key as a salt, and to make the ids unique accross
|
||||
# different LMS installs.
|
||||
h = hashlib.md5()
|
||||
h.update(settings.SECRET_KEY)
|
||||
h.update(str(user.id))
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
## TODO: Should be renamed to generic UserGroup, and possibly
|
||||
@@ -263,15 +261,6 @@ class CourseEnrollment(models.Model):
|
||||
return "[CourseEnrollment] %s: %s (%s)" % (self.user, self.course_id, self.created)
|
||||
|
||||
|
||||
@receiver(post_save, sender=CourseEnrollment)
|
||||
def assign_default_role(sender, instance, **kwargs):
|
||||
if instance.user.is_staff:
|
||||
role = Role.objects.get_or_create(course_id=instance.course_id, name="Moderator")[0]
|
||||
else:
|
||||
role = Role.objects.get_or_create(course_id=instance.course_id, name="Student")[0]
|
||||
|
||||
logging.info("assign_default_role: adding %s as %s" % (instance.user, role))
|
||||
instance.user.roles.add(role)
|
||||
|
||||
#cache_relation(User.profile)
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from datetime import date
|
||||
from collections import namedtuple
|
||||
|
||||
from courseware.courses import get_courses_by_university
|
||||
from courseware.courses import get_courses
|
||||
from courseware.access import has_access
|
||||
|
||||
from statsd import statsd
|
||||
@@ -74,16 +74,21 @@ def index(request, extra_context={}, user=None):
|
||||
domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False
|
||||
if domain==False: # do explicit check, because domain=None is valid
|
||||
domain = request.META.get('HTTP_HOST')
|
||||
universities = get_courses_by_university(None,
|
||||
domain=domain)
|
||||
|
||||
courses = get_courses(None, domain=domain)
|
||||
|
||||
# Sort courses by how far are they from they start day
|
||||
key = lambda course: course.metadata['days_to_start']
|
||||
courses = sorted(courses, key=key, reverse=True)
|
||||
|
||||
# Get the 3 most recent news
|
||||
top_news = _get_news(top=3)
|
||||
|
||||
context = {'universities': universities, 'news': top_news}
|
||||
context = {'courses': courses, 'news': top_news}
|
||||
context.update(extra_context)
|
||||
return render_to_response('index.html', context)
|
||||
|
||||
|
||||
def course_from_id(course_id):
|
||||
"""Return the CourseDescriptor corresponding to this course_id"""
|
||||
course_loc = CourseDescriptor.id_to_location(course_id)
|
||||
@@ -333,6 +338,14 @@ def change_enrollment(request):
|
||||
return {'success': False, 'error': 'We weren\'t able to unenroll you. Please try again.'}
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def accounts_login(request, error=""):
|
||||
|
||||
|
||||
return render_to_response('accounts_login.html', { 'error': error })
|
||||
|
||||
|
||||
|
||||
# Need different levels of logging
|
||||
@ensure_csrf_cookie
|
||||
def login_user(request, error=""):
|
||||
|
||||
48
common/djangoapps/track/migrations/0001_initial.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding model 'TrackingLog'
|
||||
db.create_table('track_trackinglog', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('dtcreated', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
|
||||
('username', self.gf('django.db.models.fields.CharField')(max_length=32, blank=True)),
|
||||
('ip', self.gf('django.db.models.fields.CharField')(max_length=32, blank=True)),
|
||||
('event_source', self.gf('django.db.models.fields.CharField')(max_length=32)),
|
||||
('event_type', self.gf('django.db.models.fields.CharField')(max_length=32, blank=True)),
|
||||
('event', self.gf('django.db.models.fields.TextField')(blank=True)),
|
||||
('agent', self.gf('django.db.models.fields.CharField')(max_length=256, blank=True)),
|
||||
('page', self.gf('django.db.models.fields.CharField')(max_length=32, null=True, blank=True)),
|
||||
('time', self.gf('django.db.models.fields.DateTimeField')()),
|
||||
))
|
||||
db.send_create_signal('track', ['TrackingLog'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'TrackingLog'
|
||||
db.delete_table('track_trackinglog')
|
||||
|
||||
|
||||
models = {
|
||||
'track.trackinglog': {
|
||||
'Meta': {'object_name': 'TrackingLog'},
|
||||
'agent': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}),
|
||||
'dtcreated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'event': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'event_source': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
|
||||
'event_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'ip': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
|
||||
'page': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
|
||||
'time': ('django.db.models.fields.DateTimeField', [], {}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['track']
|
||||
@@ -0,0 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding field 'TrackingLog.host'
|
||||
db.add_column('track_trackinglog', 'host',
|
||||
self.gf('django.db.models.fields.CharField')(default='', max_length=64, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
# Changing field 'TrackingLog.event_type'
|
||||
db.alter_column('track_trackinglog', 'event_type', self.gf('django.db.models.fields.CharField')(max_length=512))
|
||||
|
||||
# Changing field 'TrackingLog.page'
|
||||
db.alter_column('track_trackinglog', 'page', self.gf('django.db.models.fields.CharField')(max_length=512, null=True))
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'TrackingLog.host'
|
||||
db.delete_column('track_trackinglog', 'host')
|
||||
|
||||
|
||||
# Changing field 'TrackingLog.event_type'
|
||||
db.alter_column('track_trackinglog', 'event_type', self.gf('django.db.models.fields.CharField')(max_length=32))
|
||||
|
||||
# Changing field 'TrackingLog.page'
|
||||
db.alter_column('track_trackinglog', 'page', self.gf('django.db.models.fields.CharField')(max_length=32, null=True))
|
||||
|
||||
models = {
|
||||
'track.trackinglog': {
|
||||
'Meta': {'object_name': 'TrackingLog'},
|
||||
'agent': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}),
|
||||
'dtcreated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'event': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'event_source': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
|
||||
'event_type': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}),
|
||||
'host': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'ip': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
|
||||
'page': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}),
|
||||
'time': ('django.db.models.fields.DateTimeField', [], {}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['track']
|
||||
1
common/djangoapps/track/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -7,11 +7,12 @@ class TrackingLog(models.Model):
|
||||
username = models.CharField(max_length=32,blank=True)
|
||||
ip = models.CharField(max_length=32,blank=True)
|
||||
event_source = models.CharField(max_length=32)
|
||||
event_type = models.CharField(max_length=32,blank=True)
|
||||
event_type = models.CharField(max_length=512,blank=True)
|
||||
event = models.TextField(blank=True)
|
||||
agent = models.CharField(max_length=256,blank=True)
|
||||
page = models.CharField(max_length=32,blank=True,null=True)
|
||||
page = models.CharField(max_length=512,blank=True,null=True)
|
||||
time = models.DateTimeField('event time')
|
||||
host = models.CharField(max_length=64,blank=True)
|
||||
|
||||
def __unicode__(self):
|
||||
s = "[%s] %s@%s: %s | %s | %s | %s" % (self.time, self.username, self.ip, self.event_source,
|
||||
|
||||
@@ -17,7 +17,7 @@ from track.models import TrackingLog
|
||||
|
||||
log = logging.getLogger("tracking")
|
||||
|
||||
LOGFIELDS = ['username','ip','event_source','event_type','event','agent','page','time']
|
||||
LOGFIELDS = ['username','ip','event_source','event_type','event','agent','page','time','host']
|
||||
|
||||
def log_event(event):
|
||||
event_str = json.dumps(event)
|
||||
@@ -58,6 +58,7 @@ def user_track(request):
|
||||
"agent": agent,
|
||||
"page": request.GET['page'],
|
||||
"time": datetime.datetime.utcnow().isoformat(),
|
||||
"host": request.META['SERVER_NAME'],
|
||||
}
|
||||
log_event(event)
|
||||
return HttpResponse('success')
|
||||
@@ -83,6 +84,7 @@ def server_track(request, event_type, event, page=None):
|
||||
"agent": agent,
|
||||
"page": page,
|
||||
"time": datetime.datetime.utcnow().isoformat(),
|
||||
"host": request.META['SERVER_NAME'],
|
||||
}
|
||||
|
||||
if event_type.startswith("/event_logs") and request.user.is_staff: # don't log
|
||||
|
||||
@@ -4,6 +4,11 @@ import json
|
||||
|
||||
|
||||
def expect_json(view_function):
|
||||
"""
|
||||
View decorator for simplifying handing of requests that expect json. If the request's
|
||||
CONTENT_TYPE is application/json, parses the json dict from request.body, and updates
|
||||
request.POST with the contents.
|
||||
"""
|
||||
@wraps(view_function)
|
||||
def expect_json_with_cloned_request(request, *args, **kwargs):
|
||||
# cdodge: fix postback errors in CMS. The POST 'content-type' header can include additional information
|
||||
|
||||
@@ -7,6 +7,7 @@ source = common/lib/capa
|
||||
ignore_errors = True
|
||||
|
||||
[html]
|
||||
title = Capa Python Test Coverage Report
|
||||
directory = reports/common/lib/capa/cover
|
||||
|
||||
[xml]
|
||||
|
||||
@@ -53,7 +53,7 @@ response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__])
|
||||
solution_tags = ['solution']
|
||||
|
||||
# these get captured as student responses
|
||||
response_properties = ["codeparam", "responseparam", "answer"]
|
||||
response_properties = ["codeparam", "responseparam", "answer", "openendedparam"]
|
||||
|
||||
# special problem tags which should be turned into innocuous HTML
|
||||
html_transforms = {'problem': {'tag': 'div'},
|
||||
@@ -72,7 +72,7 @@ global_context = {'random': random,
|
||||
'miller': chem.miller}
|
||||
|
||||
# These should be removed from HTML output, including all subelements
|
||||
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup"]
|
||||
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam","openendedrubric"]
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
|
||||
@@ -733,3 +733,53 @@ class ChemicalEquationInput(InputTypeBase):
|
||||
return {'previewer': '/static/js/capa/chemical_equation_preview.js',}
|
||||
|
||||
registry.register(ChemicalEquationInput)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class OpenEndedInput(InputTypeBase):
|
||||
"""
|
||||
A text area input for code--uses codemirror, does syntax highlighting, special tab handling,
|
||||
etc.
|
||||
"""
|
||||
|
||||
template = "openendedinput.html"
|
||||
tags = ['openendedinput']
|
||||
|
||||
# pulled out for testing
|
||||
submitted_msg = ("Feedback not yet available. Reload to check again. "
|
||||
"Once the problem is graded, this message will be "
|
||||
"replaced with the grader's feedback")
|
||||
|
||||
@classmethod
|
||||
def get_attributes(cls):
|
||||
"""
|
||||
Convert options to a convenient format.
|
||||
"""
|
||||
return [Attribute('rows', '30'),
|
||||
Attribute('cols', '80'),
|
||||
Attribute('hidden', ''),
|
||||
]
|
||||
|
||||
def setup(self):
|
||||
"""
|
||||
Implement special logic: handle queueing state, and default input.
|
||||
"""
|
||||
# if no student input yet, then use the default input given by the problem
|
||||
if not self.value:
|
||||
self.value = self.xml.text
|
||||
|
||||
# Check if problem has been queued
|
||||
self.queue_len = 0
|
||||
# Flag indicating that the problem has been queued, 'msg' is length of queue
|
||||
if self.status == 'incomplete':
|
||||
self.status = 'queued'
|
||||
self.queue_len = self.msg
|
||||
self.msg = self.submitted_msg
|
||||
|
||||
def _extra_context(self):
|
||||
"""Defined queue_len, add it """
|
||||
return {'queue_len': self.queue_len,}
|
||||
|
||||
registry.register(OpenEndedInput)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
@@ -8,21 +8,23 @@ Used by capa_problem.py
|
||||
'''
|
||||
|
||||
# standard library imports
|
||||
import abc
|
||||
import cgi
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import numbers
|
||||
import numpy
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import requests
|
||||
import traceback
|
||||
import hashlib
|
||||
import abc
|
||||
import os
|
||||
import subprocess
|
||||
import traceback
|
||||
import xml.sax.saxutils as saxutils
|
||||
|
||||
from collections import namedtuple
|
||||
from shapely.geometry import Point, MultiPoint
|
||||
|
||||
# specific library imports
|
||||
@@ -1105,6 +1107,15 @@ class SymbolicResponse(CustomResponse):
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
"""
|
||||
valid: Flag indicating valid score_msg format (Boolean)
|
||||
correct: Correctness of submission (Boolean)
|
||||
score: Points to be assigned (numeric, can be float)
|
||||
msg: Message from grader to display to student (string)
|
||||
"""
|
||||
ScoreMessage = namedtuple('ScoreMessage',
|
||||
['valid', 'correct', 'points', 'msg'])
|
||||
|
||||
|
||||
class CodeResponse(LoncapaResponse):
|
||||
"""
|
||||
@@ -1144,7 +1155,7 @@ class CodeResponse(LoncapaResponse):
|
||||
else:
|
||||
self._parse_coderesponse_xml(codeparam)
|
||||
|
||||
def _parse_coderesponse_xml(self,codeparam):
|
||||
def _parse_coderesponse_xml(self, codeparam):
|
||||
'''
|
||||
Parse the new CodeResponse XML format. When successful, sets:
|
||||
self.initial_display
|
||||
@@ -1156,17 +1167,9 @@ class CodeResponse(LoncapaResponse):
|
||||
grader_payload = grader_payload.text if grader_payload is not None else ''
|
||||
self.payload = {'grader_payload': grader_payload}
|
||||
|
||||
answer_display = codeparam.find('answer_display')
|
||||
if answer_display is not None:
|
||||
self.answer = answer_display.text
|
||||
else:
|
||||
self.answer = 'No answer provided.'
|
||||
|
||||
initial_display = codeparam.find('initial_display')
|
||||
if initial_display is not None:
|
||||
self.initial_display = initial_display.text
|
||||
else:
|
||||
self.initial_display = ''
|
||||
self.initial_display = find_with_default(codeparam, 'initial_display', '')
|
||||
self.answer = find_with_default(codeparam, 'answer_display',
|
||||
'No answer provided.')
|
||||
|
||||
def _parse_externalresponse_xml(self):
|
||||
'''
|
||||
@@ -1732,9 +1735,9 @@ class ImageResponse(LoncapaResponse):
|
||||
|
||||
Regions is list of lists [region1, region2, region3, ...] where regionN
|
||||
is disordered list of points: [[1,1], [100,100], [50,50], [20, 70]].
|
||||
|
||||
|
||||
If there is only one region in the list, simpler notation can be used:
|
||||
regions="[[10,10], [30,30], [10, 30], [30, 10]]" (without explicitly
|
||||
regions="[[10,10], [30,30], [10, 30], [30, 10]]" (without explicitly
|
||||
setting outer list)
|
||||
|
||||
Returns:
|
||||
@@ -1816,6 +1819,347 @@ class ImageResponse(LoncapaResponse):
|
||||
return (dict([(ie.get('id'), ie.get('rectangle')) for ie in self.ielements]),
|
||||
dict([(ie.get('id'), ie.get('regions')) for ie in self.ielements]))
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class OpenEndedResponse(LoncapaResponse):
|
||||
"""
|
||||
Grade student open ended responses using an external grading system,
|
||||
accessed through the xqueue system.
|
||||
|
||||
Expects 'xqueue' dict in ModuleSystem with the following keys that are
|
||||
needed by OpenEndedResponse:
|
||||
|
||||
system.xqueue = { 'interface': XqueueInterface object,
|
||||
'callback_url': Per-StudentModule callback URL
|
||||
where results are posted (string),
|
||||
}
|
||||
|
||||
External requests are only submitted for student submission grading
|
||||
(i.e. and not for getting reference answers)
|
||||
|
||||
By default, uses the OpenEndedResponse.DEFAULT_QUEUE queue.
|
||||
"""
|
||||
|
||||
DEFAULT_QUEUE = 'open-ended'
|
||||
response_tag = 'openendedresponse'
|
||||
allowed_inputfields = ['openendedinput']
|
||||
max_inputfields = 1
|
||||
|
||||
def setup_response(self):
|
||||
'''
|
||||
Configure OpenEndedResponse from XML.
|
||||
'''
|
||||
xml = self.xml
|
||||
self.url = xml.get('url', None)
|
||||
self.queue_name = xml.get('queuename', self.DEFAULT_QUEUE)
|
||||
|
||||
# The openendedparam tag encapsulates all grader settings
|
||||
oeparam = self.xml.find('openendedparam')
|
||||
prompt = self.xml.find('prompt')
|
||||
rubric = self.xml.find('openendedrubric')
|
||||
|
||||
if oeparam is None:
|
||||
raise ValueError("No oeparam found in problem xml.")
|
||||
if prompt is None:
|
||||
raise ValueError("No prompt found in problem xml.")
|
||||
if rubric is None:
|
||||
raise ValueError("No rubric found in problem xml.")
|
||||
|
||||
self._parse(oeparam, prompt, rubric)
|
||||
|
||||
@staticmethod
|
||||
def stringify_children(node):
|
||||
"""
|
||||
Modify code from stringify_children in xmodule. Didn't import directly
|
||||
in order to avoid capa depending on xmodule (seems to be avoided in
|
||||
code)
|
||||
"""
|
||||
parts=[node.text if node.text is not None else '']
|
||||
for p in node.getchildren():
|
||||
parts.append(etree.tostring(p, with_tail=True, encoding='unicode'))
|
||||
|
||||
return ' '.join(parts)
|
||||
|
||||
def _parse(self, oeparam, prompt, rubric):
|
||||
'''
|
||||
Parse OpenEndedResponse XML:
|
||||
self.initial_display
|
||||
self.payload - dict containing keys --
|
||||
'grader' : path to grader settings file, 'problem_id' : id of the problem
|
||||
|
||||
self.answer - What to display when show answer is clicked
|
||||
'''
|
||||
# Note that OpenEndedResponse is agnostic to the specific contents of grader_payload
|
||||
prompt_string = self.stringify_children(prompt)
|
||||
rubric_string = self.stringify_children(rubric)
|
||||
|
||||
grader_payload = oeparam.find('grader_payload')
|
||||
grader_payload = grader_payload.text if grader_payload is not None else ''
|
||||
|
||||
#Update grader payload with student id. If grader payload not json, error.
|
||||
try:
|
||||
parsed_grader_payload = json.loads(grader_payload)
|
||||
# NOTE: self.system.location is valid because the capa_module
|
||||
# __init__ adds it (easiest way to get problem location into
|
||||
# response types)
|
||||
except TypeError, ValueError:
|
||||
log.exception("Grader payload %r is not a json object!", grader_payload)
|
||||
parsed_grader_payload.update({
|
||||
'location' : self.system.location,
|
||||
'course_id' : self.system.course_id,
|
||||
'prompt' : prompt_string,
|
||||
'rubric' : rubric_string,
|
||||
})
|
||||
updated_grader_payload = json.dumps(parsed_grader_payload)
|
||||
|
||||
self.payload = {'grader_payload': updated_grader_payload}
|
||||
|
||||
self.initial_display = find_with_default(oeparam, 'initial_display', '')
|
||||
self.answer = find_with_default(oeparam, 'answer_display', 'No answer given.')
|
||||
try:
|
||||
self.max_score = int(find_with_default(oeparam, 'max_score', 1))
|
||||
except ValueError:
|
||||
self.max_score = 1
|
||||
|
||||
def get_score(self, student_answers):
|
||||
|
||||
try:
|
||||
submission = student_answers[self.answer_id]
|
||||
except KeyError:
|
||||
msg = ('Cannot get student answer for answer_id: {0}. student_answers {1}'
|
||||
.format(self.answer_id, student_answers))
|
||||
log.exception(msg)
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
# Prepare xqueue request
|
||||
#------------------------------------------------------------
|
||||
|
||||
qinterface = self.system.xqueue['interface']
|
||||
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
|
||||
|
||||
anonymous_student_id = self.system.anonymous_student_id
|
||||
|
||||
# Generate header
|
||||
queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime +
|
||||
anonymous_student_id +
|
||||
self.answer_id)
|
||||
|
||||
xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'],
|
||||
lms_key=queuekey,
|
||||
queue_name=self.queue_name)
|
||||
|
||||
self.context.update({'submission': submission})
|
||||
|
||||
contents = self.payload.copy()
|
||||
|
||||
# Metadata related to the student submission revealed to the external grader
|
||||
student_info = {'anonymous_student_id': anonymous_student_id,
|
||||
'submission_time': qtime,
|
||||
}
|
||||
|
||||
#Update contents with student response and student info
|
||||
contents.update({
|
||||
'student_info': json.dumps(student_info),
|
||||
'student_response': submission,
|
||||
'max_score' : self.max_score
|
||||
})
|
||||
|
||||
# Submit request. When successful, 'msg' is the prior length of the queue
|
||||
(error, msg) = qinterface.send_to_queue(header=xheader,
|
||||
body=json.dumps(contents))
|
||||
|
||||
# State associated with the queueing request
|
||||
queuestate = {'key': queuekey,
|
||||
'time': qtime,}
|
||||
|
||||
cmap = CorrectMap()
|
||||
if error:
|
||||
cmap.set(self.answer_id, queuestate=None,
|
||||
msg='Unable to deliver your submission to grader. (Reason: {0}.)'
|
||||
' Please try again later.'.format(msg))
|
||||
else:
|
||||
# Queueing mechanism flags:
|
||||
# 1) Backend: Non-null CorrectMap['queuestate'] indicates that
|
||||
# the problem has been queued
|
||||
# 2) Frontend: correctness='incomplete' eventually trickles down
|
||||
# through inputtypes.textbox and .filesubmission to inform the
|
||||
# browser that the submission is queued (and it could e.g. poll)
|
||||
cmap.set(self.answer_id, queuestate=queuestate,
|
||||
correctness='incomplete', msg=msg)
|
||||
|
||||
return cmap
|
||||
|
||||
def update_score(self, score_msg, oldcmap, queuekey):
|
||||
log.debug(score_msg)
|
||||
score_msg = self._parse_score_msg(score_msg)
|
||||
if not score_msg.valid:
|
||||
oldcmap.set(self.answer_id,
|
||||
msg = 'Invalid grader reply. Please contact the course staff.')
|
||||
return oldcmap
|
||||
|
||||
correctness = 'correct' if score_msg.correct else 'incorrect'
|
||||
|
||||
# TODO: Find out how this is used elsewhere, if any
|
||||
self.context['correct'] = correctness
|
||||
|
||||
# Replace 'oldcmap' with new grading results if queuekey matches. If queuekey
|
||||
# does not match, we keep waiting for the score_msg whose key actually matches
|
||||
if oldcmap.is_right_queuekey(self.answer_id, queuekey):
|
||||
# Sanity check on returned points
|
||||
points = score_msg.points
|
||||
if points < 0:
|
||||
points = 0
|
||||
|
||||
# Queuestate is consumed, so reset it to None
|
||||
oldcmap.set(self.answer_id, npoints=points, correctness=correctness,
|
||||
msg = score_msg.msg.replace(' ', ' '), queuestate=None)
|
||||
else:
|
||||
log.debug('OpenEndedResponse: queuekey {0} does not match for answer_id={1}.'.format(
|
||||
queuekey, self.answer_id))
|
||||
|
||||
return oldcmap
|
||||
|
||||
def get_answers(self):
|
||||
anshtml = '<span class="openended-answer"><pre><code>{0}</code></pre></span>'.format(self.answer)
|
||||
return {self.answer_id: anshtml}
|
||||
|
||||
def get_initial_display(self):
|
||||
return {self.answer_id: self.initial_display}
|
||||
|
||||
def _convert_longform_feedback_to_html(self, response_items):
|
||||
"""
|
||||
Take in a dictionary, and return html strings for display to student.
|
||||
Input:
|
||||
response_items: Dictionary with keys success, feedback.
|
||||
if success is True, feedback should be a dictionary, with keys for
|
||||
types of feedback, and the corresponding feedback values.
|
||||
if success is False, feedback is actually an error string.
|
||||
|
||||
NOTE: this will need to change when we integrate peer grading, because
|
||||
that will have more complex feedback.
|
||||
|
||||
Output:
|
||||
String -- html that can be displayed to the student.
|
||||
"""
|
||||
|
||||
# We want to display available feedback in a particular order.
|
||||
# This dictionary specifies which goes first--lower first.
|
||||
priorities = {# These go at the start of the feedback
|
||||
'spelling': 0,
|
||||
'grammar': 1,
|
||||
# needs to be after all the other feedback
|
||||
'markup_text': 3}
|
||||
|
||||
default_priority = 2
|
||||
|
||||
def get_priority(elt):
|
||||
"""
|
||||
Args:
|
||||
elt: a tuple of feedback-type, feedback
|
||||
Returns:
|
||||
the priority for this feedback type
|
||||
"""
|
||||
return priorities.get(elt[0], default_priority)
|
||||
|
||||
def format_feedback(feedback_type, value):
|
||||
return """
|
||||
<div class="{feedback_type}">
|
||||
{value}
|
||||
</div>
|
||||
""".format(feedback_type=feedback_type, value=value)
|
||||
|
||||
# TODO (vshnayder): design and document the details of this format so
|
||||
# that we can do proper escaping here (e.g. are the graders allowed to
|
||||
# include HTML?)
|
||||
|
||||
for tag in ['success', 'feedback']:
|
||||
if tag not in response_items:
|
||||
return format_feedback('errors', 'Error getting feedback')
|
||||
|
||||
feedback_items = response_items['feedback']
|
||||
try:
|
||||
feedback = json.loads(feedback_items)
|
||||
except (TypeError, ValueError):
|
||||
log.exception("feedback_items have invalid json %r", feedback_items)
|
||||
return format_feedback('errors', 'Could not parse feedback')
|
||||
|
||||
if response_items['success']:
|
||||
if len(feedback) == 0:
|
||||
return format_feedback('errors', 'No feedback available')
|
||||
|
||||
feedback_lst = sorted(feedback.items(), key=get_priority)
|
||||
return u"\n".join(format_feedback(k, v) for k, v in feedback_lst)
|
||||
else:
|
||||
return format_feedback('errors', response_items['feedback'])
|
||||
|
||||
|
||||
def _format_feedback(self, response_items):
|
||||
"""
|
||||
Input:
|
||||
Dictionary called feedback. Must contain keys seen below.
|
||||
Output:
|
||||
Return error message or feedback template
|
||||
"""
|
||||
|
||||
feedback = self._convert_longform_feedback_to_html(response_items)
|
||||
|
||||
if not response_items['success']:
|
||||
return self.system.render_template("open_ended_error.html",
|
||||
{'errors' : feedback})
|
||||
|
||||
feedback_template = self.system.render_template("open_ended_feedback.html", {
|
||||
'grader_type': response_items['grader_type'],
|
||||
'score': response_items['score'],
|
||||
'feedback': feedback,
|
||||
})
|
||||
|
||||
return feedback_template
|
||||
|
||||
|
||||
def _parse_score_msg(self, score_msg):
|
||||
"""
|
||||
Grader reply is a JSON-dump of the following dict
|
||||
{ 'correct': True/False,
|
||||
'score': Numeric value (floating point is okay) to assign to answer
|
||||
'msg': grader_msg
|
||||
'feedback' : feedback from grader
|
||||
}
|
||||
|
||||
Returns (valid_score_msg, correct, score, msg):
|
||||
valid_score_msg: Flag indicating valid score_msg format (Boolean)
|
||||
correct: Correctness of submission (Boolean)
|
||||
score: Points to be assigned (numeric, can be float)
|
||||
"""
|
||||
fail = ScoreMessage(valid=False, correct=False, points=0, msg='')
|
||||
try:
|
||||
score_result = json.loads(score_msg)
|
||||
except (TypeError, ValueError):
|
||||
log.error("External grader message should be a JSON-serialized dict."
|
||||
" Received score_msg = {0}".format(score_msg))
|
||||
return fail
|
||||
|
||||
if not isinstance(score_result, dict):
|
||||
log.error("External grader message should be a JSON-serialized dict."
|
||||
" Received score_result = {0}".format(score_result))
|
||||
return fail
|
||||
|
||||
for tag in ['score', 'feedback', 'grader_type', 'success']:
|
||||
if tag not in score_result:
|
||||
log.error("External grader message is missing required tag: {0}"
|
||||
.format(tag))
|
||||
return fail
|
||||
|
||||
feedback = self._format_feedback(score_result)
|
||||
|
||||
# HACK: for now, just assume it's correct if you got more than 2/3.
|
||||
# Also assumes that score_result['score'] is an integer.
|
||||
score_ratio = int(score_result['score']) / self.max_score
|
||||
correct = (score_ratio >= 0.66)
|
||||
|
||||
#Currently ignore msg and only return feedback (which takes the place of msg)
|
||||
return ScoreMessage(valid=True, correct=correct,
|
||||
points=score_result['score'], msg=feedback)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# TEMPORARY: List of all response subclasses
|
||||
# FIXME: To be replaced by auto-registration
|
||||
|
||||
@@ -1832,4 +2176,5 @@ __all__ = [CodeResponse,
|
||||
ChoiceResponse,
|
||||
MultipleChoiceResponse,
|
||||
TrueFalseResponse,
|
||||
JavascriptResponse]
|
||||
JavascriptResponse,
|
||||
OpenEndedResponse]
|
||||
|
||||
32
common/lib/capa/capa/templates/openendedinput.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<section id="openended_${id}" class="openended">
|
||||
<textarea rows="${rows}" cols="${cols}" name="input_${id}" class="short-form-response" id="input_${id}"
|
||||
% if hidden:
|
||||
style="display:none;"
|
||||
% endif
|
||||
>${value|h}</textarea>
|
||||
|
||||
<div class="grader-status">
|
||||
% if status == 'unsubmitted':
|
||||
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span>
|
||||
% elif status == 'correct':
|
||||
<span class="correct" id="status_${id}">Correct</span>
|
||||
% elif status == 'incorrect':
|
||||
<span class="incorrect" id="status_${id}">Incorrect</span>
|
||||
% elif status == 'queued':
|
||||
<span class="grading" id="status_${id}">Submitted for grading</span>
|
||||
% endif
|
||||
|
||||
% if hidden:
|
||||
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
|
||||
% endif
|
||||
</div>
|
||||
|
||||
<span id="answer_${id}"></span>
|
||||
|
||||
% if status == 'queued':
|
||||
<input name="reload" class="reload" type="button" value="Recheck for Feedback" onclick="document.location.reload(true);" />
|
||||
% endif
|
||||
<div class="external-grader-message">
|
||||
${msg|n}
|
||||
</div>
|
||||
</section>
|
||||
@@ -65,3 +65,25 @@ def is_file(file_to_test):
|
||||
Duck typing to check if 'file_to_test' is a File object
|
||||
'''
|
||||
return all(hasattr(file_to_test, method) for method in ['read', 'name'])
|
||||
|
||||
|
||||
def find_with_default(node, path, default):
|
||||
"""
|
||||
Look for a child of node using , and return its text if found.
|
||||
Otherwise returns default.
|
||||
|
||||
Arguments:
|
||||
node: lxml node
|
||||
path: xpath search expression
|
||||
default: value to return if nothing found
|
||||
|
||||
Returns:
|
||||
node.find(path).text if the find succeeds, default otherwise.
|
||||
|
||||
"""
|
||||
v = node.find(path)
|
||||
if v is not None:
|
||||
return v.text
|
||||
else:
|
||||
return default
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ def parse_xreply(xreply):
|
||||
|
||||
return_code = xreply['return_code']
|
||||
content = xreply['content']
|
||||
|
||||
return (return_code, content)
|
||||
|
||||
|
||||
@@ -80,7 +81,11 @@ class XQueueInterface(object):
|
||||
|
||||
# Log in, then try again
|
||||
if error and (msg == 'login_required'):
|
||||
self._login()
|
||||
(error, content) = self._login()
|
||||
if error != 0:
|
||||
# when the login fails
|
||||
log.debug("Failed to login to queue: %s", content)
|
||||
return (error, content)
|
||||
if files_to_upload is not None:
|
||||
# Need to rewind file pointers
|
||||
for f in files_to_upload:
|
||||
|
||||
@@ -7,6 +7,7 @@ source = common/lib/xmodule
|
||||
ignore_errors = True
|
||||
|
||||
[html]
|
||||
title = XModule Python Test Coverage Report
|
||||
directory = reports/common/lib/xmodule/cover
|
||||
|
||||
[xml]
|
||||
|
||||
@@ -145,6 +145,11 @@ class CapaModule(XModule):
|
||||
else:
|
||||
self.seed = None
|
||||
|
||||
# Need the problem location in openendedresponse to send out. Adding
|
||||
# it to the system here seems like the least clunky way to get it
|
||||
# there.
|
||||
self.system.set('location', self.location.url())
|
||||
|
||||
try:
|
||||
# TODO (vshnayder): move as much as possible of this work and error
|
||||
# checking to descriptor load time
|
||||
|
||||
@@ -366,7 +366,20 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
|
||||
@property
|
||||
def start_date_text(self):
|
||||
displayed_start = self._try_parse_time('advertised_start') or self.start
|
||||
parsed_advertised_start = self._try_parse_time('advertised_start')
|
||||
|
||||
# If the advertised start isn't a real date string, we assume it's free
|
||||
# form text...
|
||||
if parsed_advertised_start is None and \
|
||||
('advertised_start' in self.metadata):
|
||||
return self.metadata['advertised_start']
|
||||
|
||||
displayed_start = parsed_advertised_start or self.start
|
||||
|
||||
# If we have neither an advertised start or a real start, just return TBD
|
||||
if not displayed_start:
|
||||
return "TBD"
|
||||
|
||||
return time.strftime("%b %d, %Y", displayed_start)
|
||||
|
||||
@property
|
||||
|
||||
@@ -121,16 +121,6 @@ section.problem {
|
||||
}
|
||||
}
|
||||
|
||||
&.processing {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
background: url('../images/spinner.gif') center center no-repeat;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
text-indent: -9999px;
|
||||
}
|
||||
}
|
||||
|
||||
&.correct, &.ui-icon-check {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
@@ -250,6 +240,13 @@ section.problem {
|
||||
}
|
||||
}
|
||||
|
||||
.reload
|
||||
{
|
||||
float:right;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
|
||||
.grader-status {
|
||||
padding: 9px;
|
||||
background: #F6F6F6;
|
||||
@@ -266,6 +263,13 @@ section.problem {
|
||||
margin: -7px 7px 0 0;
|
||||
}
|
||||
|
||||
.grading {
|
||||
background: url('../images/info-icon.png') left center no-repeat;
|
||||
padding-left: 25px;
|
||||
text-indent: 0px;
|
||||
margin: 0px 7px 0 0;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 20px;
|
||||
text-transform: capitalize;
|
||||
@@ -685,6 +689,21 @@ section.problem {
|
||||
color: #B00;
|
||||
}
|
||||
}
|
||||
|
||||
.markup-text{
|
||||
margin: 5px;
|
||||
padding: 20px 0px 15px 50px;
|
||||
border-top: 1px solid #DDD;
|
||||
border-left: 20px solid #FAFAFA;
|
||||
|
||||
bs {
|
||||
color: #BB0000;
|
||||
}
|
||||
|
||||
bg {
|
||||
color: #BDA046;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1953,7 +1953,7 @@ cktsim = (function() {
|
||||
var module = {
|
||||
'Circuit': Circuit,
|
||||
'parse_number': parse_number,
|
||||
'parse_source': parse_source,
|
||||
'parse_source': parse_source
|
||||
}
|
||||
return module;
|
||||
}());
|
||||
@@ -2068,7 +2068,7 @@ schematic = (function() {
|
||||
'n': [NFet, 'NFet'],
|
||||
'p': [PFet, 'PFet'],
|
||||
's': [Probe, 'Voltage Probe'],
|
||||
'a': [Ammeter, 'Current Probe'],
|
||||
'a': [Ammeter, 'Current Probe']
|
||||
};
|
||||
|
||||
// global clipboard
|
||||
@@ -5502,7 +5502,7 @@ schematic = (function() {
|
||||
'magenta' : 'rgb(255,64,255)',
|
||||
'yellow': 'rgb(255,255,64)',
|
||||
'black': 'rgb(0,0,0)',
|
||||
'x-axis': undefined,
|
||||
'x-axis': undefined
|
||||
};
|
||||
|
||||
function Probe(x,y,rotation,color,offset) {
|
||||
@@ -6100,7 +6100,7 @@ schematic = (function() {
|
||||
'Amplitude',
|
||||
'Frequency (Hz)',
|
||||
'Delay until sin starts (secs)',
|
||||
'Phase offset (degrees)'],
|
||||
'Phase offset (degrees)']
|
||||
}
|
||||
|
||||
// build property editor div
|
||||
@@ -6300,7 +6300,7 @@ schematic = (function() {
|
||||
|
||||
var module = {
|
||||
'Schematic': Schematic,
|
||||
'component_slider': component_slider,
|
||||
'component_slider': component_slider
|
||||
}
|
||||
return module;
|
||||
}());
|
||||
|
||||
@@ -2,6 +2,8 @@ class @Video
|
||||
constructor: (element) ->
|
||||
@el = $(element).find('.video')
|
||||
@id = @el.attr('id').replace(/video_/, '')
|
||||
@start = @el.data('start')
|
||||
@end = @el.data('end')
|
||||
@caption_data_dir = @el.data('caption-data-dir')
|
||||
@caption_asset_path = @el.data('caption-asset-path')
|
||||
@show_captions = @el.data('show-captions') == "true"
|
||||
|
||||
@@ -36,14 +36,21 @@ class @VideoPlayer extends Subview
|
||||
@volumeControl = new VideoVolumeControl el: @$('.secondary-controls')
|
||||
@speedControl = new VideoSpeedControl el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed()
|
||||
@progressSlider = new VideoProgressSlider el: @$('.slider')
|
||||
@playerVars =
|
||||
controls: 0
|
||||
wmode: 'transparent'
|
||||
rel: 0
|
||||
showinfo: 0
|
||||
enablejsapi: 1
|
||||
modestbranding: 1
|
||||
if @video.start
|
||||
@playerVars.start = @video.start
|
||||
if @video.end
|
||||
# work in AS3, not HMLT5. but iframe use AS3
|
||||
@playerVars.end = @video.end
|
||||
|
||||
@player = new YT.Player @video.id,
|
||||
playerVars:
|
||||
controls: 0
|
||||
wmode: 'transparent'
|
||||
rel: 0
|
||||
showinfo: 0
|
||||
enablejsapi: 1
|
||||
modestbranding: 1
|
||||
playerVars: @playerVars
|
||||
videoId: @video.youtubeId()
|
||||
events:
|
||||
onReady: @onReady
|
||||
|
||||
@@ -352,6 +352,12 @@ class ModuleStore(object):
|
||||
course_filter = Location("i4x", category="course")
|
||||
return self.get_items(course_filter)
|
||||
|
||||
def get_course(self, course_id):
|
||||
'''
|
||||
Look for a specific course id. Returns the course descriptor, or None if not found.
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def get_parent_locations(self, location):
|
||||
'''Find all locations that are the parents of this location. Needed
|
||||
for path_to_location().
|
||||
@@ -413,3 +419,10 @@ class ModuleStoreBase(ModuleStore):
|
||||
|
||||
errorlog = self._get_errorlog(location)
|
||||
return errorlog.errors
|
||||
|
||||
def get_course(self, course_id):
|
||||
"""Default impl--linear search through course list"""
|
||||
for c in self.get_courses():
|
||||
if c.id == course_id:
|
||||
return c
|
||||
return None
|
||||
|
||||
@@ -373,6 +373,14 @@ class SelfAssessmentModule(XModule):
|
||||
def save_answer(self, get):
|
||||
"""
|
||||
After the answer is submitted, show the rubric.
|
||||
|
||||
Args:
|
||||
get: the GET dictionary passed to the ajax request. Should contain
|
||||
a key 'student_answer'
|
||||
|
||||
Returns:
|
||||
Dictionary with keys 'success' and either 'error' (if not success),
|
||||
or 'rubric_html' (if success).
|
||||
"""
|
||||
# Check to see if attempts are less than max
|
||||
if self.attempts > self.max_attempts:
|
||||
|
||||
@@ -10,7 +10,7 @@ from xmodule.progress import Progress
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from pkg_resources import resource_string
|
||||
|
||||
log = logging.getLogger("mitx.common.lib.seq_module")
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# HACK: This shouldn't be hard-coded to two types
|
||||
# OBSOLETE: This obsoletes 'type'
|
||||
|
||||
@@ -10,6 +10,9 @@ from xmodule.modulestore.mongo import MongoModuleStore
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
|
||||
import datetime
|
||||
import time
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -36,6 +39,7 @@ class VideoModule(XModule):
|
||||
self.show_captions = xmltree.get('show_captions', 'true')
|
||||
self.source = self._get_source(xmltree)
|
||||
self.track = self._get_track(xmltree)
|
||||
self.start_time, self.end_time = self._get_timeframe(xmltree)
|
||||
|
||||
if instance_state is not None:
|
||||
state = json.loads(instance_state)
|
||||
@@ -45,11 +49,11 @@ class VideoModule(XModule):
|
||||
def _get_source(self, xmltree):
|
||||
# find the first valid source
|
||||
return self._get_first_external(xmltree, 'source')
|
||||
|
||||
|
||||
def _get_track(self, xmltree):
|
||||
# find the first valid track
|
||||
return self._get_first_external(xmltree, 'track')
|
||||
|
||||
|
||||
def _get_first_external(self, xmltree, tag):
|
||||
"""
|
||||
Will return the first valid element
|
||||
@@ -64,6 +68,23 @@ class VideoModule(XModule):
|
||||
break
|
||||
return result
|
||||
|
||||
def _get_timeframe(self, xmltree):
|
||||
""" Converts 'from' and 'to' parameters in video tag to seconds.
|
||||
If there are no parameters, returns empty string. """
|
||||
|
||||
def parse_time(s):
|
||||
"""Converts s in '12:34:45' format to seconds. If s is
|
||||
None, returns empty string"""
|
||||
if s is None:
|
||||
return ''
|
||||
else:
|
||||
x = time.strptime(s, '%H:%M:%S')
|
||||
return datetime.timedelta(hours=x.tm_hour,
|
||||
minutes=x.tm_min,
|
||||
seconds=x.tm_sec).total_seconds()
|
||||
|
||||
return parse_time(xmltree.get('from')), parse_time(xmltree.get('to'))
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
'''
|
||||
Handle ajax calls to this video.
|
||||
@@ -108,12 +129,14 @@ class VideoModule(XModule):
|
||||
'id': self.location.html_id(),
|
||||
'position': self.position,
|
||||
'source': self.source,
|
||||
'track' : self.track,
|
||||
'track': self.track,
|
||||
'display_name': self.display_name,
|
||||
# TODO (cpennington): This won't work when we move to data that isn't on the filesystem
|
||||
'data_dir': self.metadata['data_dir'],
|
||||
'caption_asset_path': caption_asset_path,
|
||||
'show_captions': self.show_captions
|
||||
'show_captions': self.show_captions,
|
||||
'start': self.start_time,
|
||||
'end': self.end_time
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -829,7 +829,8 @@ class ModuleSystem(object):
|
||||
debug=False,
|
||||
xqueue=None,
|
||||
node_path="",
|
||||
anonymous_student_id=''):
|
||||
anonymous_student_id='',
|
||||
course_id=None):
|
||||
'''
|
||||
Create a closure around the system environment.
|
||||
|
||||
@@ -864,6 +865,8 @@ class ModuleSystem(object):
|
||||
ajax results.
|
||||
|
||||
anonymous_student_id - Used for tracking modules with student id
|
||||
|
||||
course_id - the course_id containing this module
|
||||
'''
|
||||
self.ajax_url = ajax_url
|
||||
self.xqueue = xqueue
|
||||
@@ -876,6 +879,7 @@ class ModuleSystem(object):
|
||||
self.replace_urls = replace_urls
|
||||
self.node_path = node_path
|
||||
self.anonymous_student_id = anonymous_student_id
|
||||
self.course_id = course_id
|
||||
self.user_is_staff = user is not None and user.is_staff
|
||||
|
||||
def get(self, attr):
|
||||
|
||||
@@ -249,7 +249,10 @@ class @DiscussionUtil
|
||||
$3
|
||||
else if RE_DISPLAYMATH.test(text)
|
||||
text = text.replace RE_DISPLAYMATH, ($0, $1, $2, $3) ->
|
||||
processedText += $1 + processor("$$" + $2 + "$$", 'display')
|
||||
#processedText += $1 + processor("$$" + $2 + "$$", 'display')
|
||||
#bug fix, ordering is off
|
||||
processedText = processor("$$" + $2 + "$$", 'display') + processedText
|
||||
processedText = $1 + processedText
|
||||
$3
|
||||
else
|
||||
processedText += text
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
// content-box | border-box | inherit
|
||||
-webkit-box-sizing: $box;
|
||||
-moz-box-sizing: $box;
|
||||
box-sizing: $box;
|
||||
box-sizing: $box; *behavior: url(/static/scripts/boxsizing.htc)
|
||||
}
|
||||
|
||||
@@ -90,18 +90,18 @@ clone_repos() {
|
||||
fi
|
||||
}
|
||||
|
||||
### START
|
||||
|
||||
PROG=${0##*/}
|
||||
BASE="$HOME/mitx_all"
|
||||
PYTHON_DIR="$BASE/python"
|
||||
RUBY_DIR="$BASE/ruby"
|
||||
RUBY_VER="1.9.3"
|
||||
NUMPY_VER="1.6.2"
|
||||
SCIPY_VER="0.10.1"
|
||||
BREW_FILE="$BASE/mitx/brew-formulas.txt"
|
||||
APT_REPOS_FILE="$BASE/mitx/apt-repos.txt"
|
||||
APT_PKGS_FILE="$BASE/mitx/apt-packages.txt"
|
||||
LOG="/var/tmp/install-$(date +%Y%m%d-%H%M%S).log"
|
||||
|
||||
|
||||
# Read arguments
|
||||
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
error "This script should not be run using sudo or as the root user"
|
||||
usage
|
||||
@@ -163,18 +163,14 @@ info
|
||||
output "Press return to begin or control-C to abort"
|
||||
read dummy
|
||||
|
||||
# log all stdout and stderr
|
||||
|
||||
# Log all stdout and stderr
|
||||
|
||||
exec > >(tee $LOG)
|
||||
exec 2>&1
|
||||
|
||||
if ! grep -q "export rvm_path=$RUBY_DIR" ~/.rvmrc; then
|
||||
if [[ -f $HOME/.rvmrc ]]; then
|
||||
output "Copying existing .rvmrc to .rvmrc.bak"
|
||||
cp $HOME/.rvmrc $HOME/.rvmrc.bak
|
||||
fi
|
||||
output "Creating $HOME/.rvmrc so rvm uses $RUBY_DIR"
|
||||
echo "export rvm_path=$RUBY_DIR" > $HOME/.rvmrc
|
||||
fi
|
||||
|
||||
# Install basic system requirements
|
||||
|
||||
mkdir -p $BASE
|
||||
case `uname -s` in
|
||||
@@ -187,19 +183,7 @@ case `uname -s` in
|
||||
distro=`lsb_release -cs`
|
||||
case $distro in
|
||||
maya|lisa|natty|oneiric|precise|quantal)
|
||||
output "Installing ubuntu requirements"
|
||||
|
||||
# DEBIAN_FRONTEND=noninteractive is required for silent mysql-server installation
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# add repositories
|
||||
cat $APT_REPOS_FILE | xargs -n 1 sudo add-apt-repository -y
|
||||
sudo apt-get -y update
|
||||
|
||||
# install packages listed in APT_PKGS_FILE
|
||||
cat $APT_PKGS_FILE | xargs sudo apt-get -y install
|
||||
|
||||
clone_repos
|
||||
sudo apt-get install git
|
||||
;;
|
||||
*)
|
||||
error "Unsupported distribution - $distro"
|
||||
@@ -207,8 +191,8 @@ case `uname -s` in
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
Darwin)
|
||||
|
||||
Darwin)
|
||||
if [[ ! -w /usr/local ]]; then
|
||||
cat<<EO
|
||||
|
||||
@@ -235,39 +219,6 @@ EO
|
||||
brew install git
|
||||
}
|
||||
|
||||
clone_repos
|
||||
|
||||
output "Installing OSX requirements"
|
||||
if [[ ! -r $BREW_FILE ]]; then
|
||||
error "$BREW_FILE does not exist, needed to install brew deps"
|
||||
exit 1
|
||||
fi
|
||||
# brew errors if the package is already installed
|
||||
for pkg in $(cat $BREW_FILE); do
|
||||
grep $pkg <(brew list) &>/dev/null || {
|
||||
output "Installing $pkg"
|
||||
brew install $pkg
|
||||
}
|
||||
done
|
||||
|
||||
# paths where brew likes to install python scripts
|
||||
PATH=/usr/local/share/python:/usr/local/bin:$PATH
|
||||
|
||||
command -v pip &>/dev/null || {
|
||||
output "Installing pip"
|
||||
easy_install pip
|
||||
}
|
||||
|
||||
if ! grep -Eq ^1.7 <(virtualenv --version 2>/dev/null); then
|
||||
output "Installing virtualenv >1.7"
|
||||
pip install 'virtualenv>1.7' virtualenvwrapper
|
||||
fi
|
||||
|
||||
command -v coffee &>/dev/null || {
|
||||
output "Installing coffee script"
|
||||
curl --insecure https://npmjs.org/install.sh | sh
|
||||
npm install -g coffee-script
|
||||
}
|
||||
;;
|
||||
*)
|
||||
error "Unsupported platform"
|
||||
@@ -275,19 +226,54 @@ EO
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
# Clone MITx repositories
|
||||
|
||||
clone_repos
|
||||
|
||||
|
||||
# Install system-level dependencies
|
||||
|
||||
bash $BASE/mitx/install-system-req.sh
|
||||
|
||||
|
||||
# Install Ruby RVM
|
||||
|
||||
output "Installing rvm and ruby"
|
||||
|
||||
if ! grep -q "export rvm_path=$RUBY_DIR" ~/.rvmrc; then
|
||||
if [[ -f $HOME/.rvmrc ]]; then
|
||||
output "Copying existing .rvmrc to .rvmrc.bak"
|
||||
cp $HOME/.rvmrc $HOME/.rvmrc.bak
|
||||
fi
|
||||
output "Creating $HOME/.rvmrc so rvm uses $RUBY_DIR"
|
||||
echo "export rvm_path=$RUBY_DIR" > $HOME/.rvmrc
|
||||
fi
|
||||
|
||||
curl -sL get.rvm.io | bash -s -- --version 1.15.7
|
||||
source $RUBY_DIR/scripts/rvm
|
||||
# skip the intro
|
||||
LESS="-E" rvm install $RUBY_VER --with-readline
|
||||
|
||||
output "Installing gem bundler"
|
||||
gem install bundler
|
||||
|
||||
output "Installing ruby packages"
|
||||
# hack :(
|
||||
cd $BASE/mitx || true
|
||||
bundle install
|
||||
|
||||
cd $BASE
|
||||
|
||||
# Install Python virtualenv
|
||||
|
||||
output "Installing python virtualenv"
|
||||
|
||||
case `uname -s` in
|
||||
Darwin)
|
||||
# Add brew's path
|
||||
PATH=/usr/local/share/python:/usr/local/bin:$PATH
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ $systempkgs ]]; then
|
||||
virtualenv --system-site-packages "$PYTHON_DIR"
|
||||
else
|
||||
@@ -296,9 +282,14 @@ else
|
||||
virtualenv "$PYTHON_DIR"
|
||||
fi
|
||||
|
||||
# change to mitx python virtualenv
|
||||
# activate mitx python virtualenv
|
||||
source $PYTHON_DIR/bin/activate
|
||||
|
||||
# compile numpy and scipy if requested
|
||||
|
||||
NUMPY_VER="1.6.2"
|
||||
SCIPY_VER="0.10.1"
|
||||
|
||||
if [[ -n $compile ]]; then
|
||||
output "Downloading numpy and scipy"
|
||||
curl -sL -o numpy.tar.gz http://downloads.sourceforge.net/project/numpy/NumPy/${NUMPY_VER}/numpy-${NUMPY_VER}.tar.gz
|
||||
@@ -330,18 +321,25 @@ case `uname -s` in
|
||||
esac
|
||||
|
||||
output "Installing MITx pre-requirements"
|
||||
pip install -r mitx/pre-requirements.txt
|
||||
# Need to be in the mitx dir to get the paths to local modules right
|
||||
pip install -r $BASE/mitx/pre-requirements.txt
|
||||
|
||||
output "Installing MITx requirements"
|
||||
cd mitx
|
||||
# Need to be in the mitx dir to get the paths to local modules right
|
||||
cd $BASE/mitx
|
||||
pip install -r requirements.txt
|
||||
|
||||
mkdir "$BASE/log" || true
|
||||
mkdir "$BASE/db" || true
|
||||
|
||||
|
||||
# Configure Git
|
||||
|
||||
output "Fixing your git default settings"
|
||||
git config --global push.default current
|
||||
|
||||
|
||||
### DONE
|
||||
|
||||
cat<<END
|
||||
Success!!
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ To run a single nose test:
|
||||
|
||||
Very handy: if you uncomment the `--pdb` argument in `NOSE_ARGS` in `lms/envs/test.py`, it will drop you into pdb on error. This lets you go up and down the stack and see what the values of the variables are. Check out http://docs.python.org/library/pdb.html
|
||||
|
||||
|
||||
### Javascript Tests
|
||||
|
||||
These commands start a development server with jasmine testing enabled, and launch your default browser
|
||||
@@ -105,6 +106,15 @@ Run the following to see a list of all rake tasks available and their arguments
|
||||
|
||||
rake -T
|
||||
|
||||
## Testing using queue servers
|
||||
|
||||
When testing problems that use a queue server on AWS (e.g. sandbox-xqueue.edx.org), you'll need to run your server on your public IP, like so.
|
||||
|
||||
`django-admin.py runserver --settings=lms.envs.dev --pythonpath=. 0.0.0.0:8000`
|
||||
|
||||
When you connect to the LMS, you need to use the public ip. Use `ifconfig` to figure out the numnber, and connect e.g. to `http://18.3.4.5:8000/`
|
||||
|
||||
|
||||
## Content development
|
||||
|
||||
If you change course content, while running the LMS in dev mode, it is unnecessary to restart to refresh the modulestore.
|
||||
|
||||
@@ -418,6 +418,10 @@ If you want to customize the courseware tabs displayed for your course, specify
|
||||
* "external_link". Parameters "name", "link".
|
||||
* "textbooks". No parameters--generates tab names from book titles.
|
||||
* "progress". Parameter "name".
|
||||
* "static_tab". Parameters "name", 'url_slug'--will look for tab contents in
|
||||
'tabs/{course_url_name}/{tab url_slug}.html'
|
||||
* "staff_grading". No parameters. If specified, displays the staff grading tab for instructors.
|
||||
|
||||
|
||||
# Tips for content developers
|
||||
|
||||
@@ -429,9 +433,7 @@ before the week 1 material to make it easy to find in the file.
|
||||
|
||||
* Come up with a consistent pattern for url_names, so that it's easy to know where to look for any piece of content. It will also help to come up with a standard way of splitting your content files. As a point of departure, we suggest splitting chapters, sequences, html, and problems into separate files.
|
||||
|
||||
* A heads up: our content management system will allow you to develop content through a web browser, but will be backed by this same xml at first. Once that happens, every element will be in its own file to make access and updates faster.
|
||||
|
||||
* Prefer the most "semantic" name for containers: e.g., use problemset rather than vertical for a problem set. That way, if we decide to display problem sets differently, we don't have to change the xml.
|
||||
* Prefer the most "semantic" name for containers: e.g., use problemset rather than sequential for a problem set. That way, if we decide to display problem sets differently, we don't have to change the xml.
|
||||
|
||||
# Other file locations (info and about)
|
||||
|
||||
|
||||
108
install-system-req.sh
Executable file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# posix compliant sanity check
|
||||
if [ -z $BASH ] || [ $BASH = "/bin/sh" ]; then
|
||||
echo "Please use the bash interpreter to run this script"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
error() {
|
||||
printf '\E[31m'; echo "$@"; printf '\E[0m'
|
||||
}
|
||||
output() {
|
||||
printf '\E[36m'; echo "$@"; printf '\E[0m'
|
||||
}
|
||||
|
||||
|
||||
### START
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
BREW_FILE=$DIR/"brew-formulas.txt"
|
||||
APT_REPOS_FILE=$DIR/"apt-repos.txt"
|
||||
APT_PKGS_FILE=$DIR/"apt-packages.txt"
|
||||
|
||||
case `uname -s` in
|
||||
[Ll]inux)
|
||||
command -v lsb_release &>/dev/null || {
|
||||
error "Please install lsb-release."
|
||||
exit 1
|
||||
}
|
||||
|
||||
distro=`lsb_release -cs`
|
||||
case $distro in
|
||||
maya|lisa|natty|oneiric|precise|quantal)
|
||||
output "Installing Ubuntu requirements"
|
||||
|
||||
# DEBIAN_FRONTEND=noninteractive is required for silent mysql-server installation
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# add repositories
|
||||
cat $APT_REPOS_FILE | xargs -n 1 sudo add-apt-repository -y
|
||||
sudo apt-get -y update
|
||||
|
||||
# install packages listed in APT_PKGS_FILE
|
||||
cat $APT_PKGS_FILE | xargs sudo apt-get -y install
|
||||
;;
|
||||
*)
|
||||
error "Unsupported distribution - $distro"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
Darwin)
|
||||
|
||||
if [[ ! -w /usr/local ]]; then
|
||||
cat<<EO
|
||||
|
||||
You need to be able to write to /usr/local for
|
||||
the installation of brew and brew packages.
|
||||
|
||||
Either make sure the group you are in (most likely 'staff')
|
||||
can write to that directory or simply execute the following
|
||||
and re-run the script:
|
||||
|
||||
$ sudo chown -R $USER /usr/local
|
||||
EO
|
||||
|
||||
exit 1
|
||||
|
||||
fi
|
||||
|
||||
output "Installing OSX requirements"
|
||||
if [[ ! -r $BREW_FILE ]]; then
|
||||
error "$BREW_FILE does not exist, needed to install brew"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# brew errors if the package is already installed
|
||||
for pkg in $(cat $BREW_FILE); do
|
||||
grep $pkg <(brew list) &>/dev/null || {
|
||||
output "Installing $pkg"
|
||||
brew install $pkg
|
||||
}
|
||||
done
|
||||
|
||||
# paths where brew likes to install python scripts
|
||||
PATH=/usr/local/share/python:/usr/local/bin:$PATH
|
||||
|
||||
command -v pip &>/dev/null || {
|
||||
output "Installing pip"
|
||||
easy_install pip
|
||||
}
|
||||
|
||||
if ! grep -Eq ^1.7 <(virtualenv --version 2>/dev/null); then
|
||||
output "Installing virtualenv >1.7"
|
||||
pip install 'virtualenv>1.7' virtualenvwrapper
|
||||
fi
|
||||
|
||||
command -v coffee &>/dev/null || {
|
||||
output "Installing coffee script"
|
||||
curl --insecure https://npmjs.org/install.sh | sh
|
||||
npm install -g coffee-script
|
||||
}
|
||||
;;
|
||||
*)
|
||||
error "Unsupported platform"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -2,11 +2,13 @@
|
||||
[run]
|
||||
data_file = reports/lms/.coverage
|
||||
source = lms
|
||||
omit = lms/envs/*
|
||||
|
||||
[report]
|
||||
ignore_errors = True
|
||||
|
||||
[html]
|
||||
title = LMS Python Test Coverage Report
|
||||
directory = reports/lms/cover
|
||||
|
||||
[xml]
|
||||
|
||||
@@ -43,7 +43,8 @@ def has_access(user, obj, action, course_context=None):
|
||||
|
||||
user: a Django user object. May be anonymous.
|
||||
|
||||
obj: The object to check access for. For now, a module or descriptor.
|
||||
obj: The object to check access for. A module, descriptor, location, or
|
||||
certain special strings (e.g. 'global')
|
||||
|
||||
action: A string specifying the action that the client is trying to perform.
|
||||
|
||||
|
||||
@@ -236,11 +236,51 @@ def get_courses_by_university(user, domain=None):
|
||||
'''
|
||||
# TODO: Clean up how 'error' is done.
|
||||
# filter out any courses that errored.
|
||||
visible_courses = branding.get_visible_courses(domain)
|
||||
visible_courses = get_courses(user, domain)
|
||||
|
||||
universities = defaultdict(list)
|
||||
for course in visible_courses:
|
||||
if not has_access(user, course, 'see_exists'):
|
||||
continue
|
||||
universities[course.org].append(course)
|
||||
|
||||
return universities
|
||||
|
||||
|
||||
def get_courses(user, domain=None):
|
||||
'''
|
||||
Returns a list of courses available, sorted by course.number
|
||||
'''
|
||||
courses = branding.get_visible_courses(domain)
|
||||
courses = [c for c in courses if has_access(user, c, 'see_exists')]
|
||||
|
||||
# Add metadata about the start day and if the course is new
|
||||
for course in courses:
|
||||
days_to_start = _get_course_days_to_start(course)
|
||||
|
||||
metadata = course.metadata
|
||||
metadata['days_to_start'] = days_to_start
|
||||
metadata['is_new'] = course.metadata.get('is_new', days_to_start > 1)
|
||||
|
||||
courses = sorted(courses, key=lambda course:course.number)
|
||||
return courses
|
||||
|
||||
|
||||
def _get_course_days_to_start(course):
|
||||
from datetime import datetime as dt
|
||||
from time import mktime, gmtime
|
||||
|
||||
convert_to_datetime = lambda ts: dt.fromtimestamp(mktime(ts))
|
||||
|
||||
start_date = convert_to_datetime(course.start)
|
||||
|
||||
# If the course has a valid advertised date, use that instead
|
||||
advertised_start = course.metadata.get('advertised_start', None)
|
||||
if advertised_start:
|
||||
try:
|
||||
start_date = dt.strptime(advertised_start, "%Y-%m-%dT%H:%M")
|
||||
except ValueError:
|
||||
pass # Invalid date, keep using course.start
|
||||
|
||||
now = convert_to_datetime(gmtime())
|
||||
days_to_start = (start_date - now).days
|
||||
|
||||
return days_to_start
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import pyparsing
|
||||
@@ -20,6 +19,7 @@ from mitxmako.shortcuts import render_to_string
|
||||
from models import StudentModule, StudentModuleCache
|
||||
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
|
||||
from static_replace import replace_urls
|
||||
from student.models import unique_id_for_user
|
||||
from xmodule.errortracker import exc_info_to_str
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.modulestore import Location
|
||||
@@ -157,12 +157,6 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
|
||||
if not has_access(user, descriptor, 'load', course_id):
|
||||
return None
|
||||
|
||||
# Anonymized student identifier
|
||||
h = hashlib.md5()
|
||||
h.update(settings.SECRET_KEY)
|
||||
h.update(str(user.id))
|
||||
anonymous_student_id = h.hexdigest()
|
||||
|
||||
# Only check the cache if this module can possibly have state
|
||||
instance_module = None
|
||||
shared_module = None
|
||||
@@ -235,7 +229,8 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
|
||||
# by the replace_static_urls code below
|
||||
replace_urls=replace_urls,
|
||||
node_path=settings.NODE_PATH,
|
||||
anonymous_student_id=anonymous_student_id,
|
||||
anonymous_student_id=unique_id_for_user(user),
|
||||
course_id=course_id,
|
||||
)
|
||||
# pass position specified in URL to module through ModuleSystem
|
||||
system.set('position', position)
|
||||
|
||||
@@ -45,7 +45,7 @@ CourseTab = namedtuple('CourseTab', 'name link is_active')
|
||||
# wrong. (e.g. "is there a 'name' field?). Validators can assume
|
||||
# that the type field is valid.
|
||||
#
|
||||
# - a function that takes a config, a user, and a course, and active_page and
|
||||
# - a function that takes a config, a user, and a course, an active_page and
|
||||
# return a list of CourseTabs. (e.g. "return a CourseTab with specified
|
||||
# name, link to courseware, and is_active=True/False"). The function can
|
||||
# assume that it is only called with configs of the appropriate type that
|
||||
@@ -106,6 +106,14 @@ def _textbooks(tab, user, course, active_page):
|
||||
for index, textbook in enumerate(course.textbooks)]
|
||||
return []
|
||||
|
||||
|
||||
def _staff_grading(tab, user, course, active_page):
|
||||
if has_access(user, course, 'staff'):
|
||||
link = reverse('staff_grading', args=[course.id])
|
||||
return [CourseTab('Staff grading', link, active_page == "staff_grading")]
|
||||
return []
|
||||
|
||||
|
||||
#### Validators
|
||||
|
||||
|
||||
@@ -141,6 +149,7 @@ VALID_TAB_TYPES = {
|
||||
'textbooks': TabImpl(null_validator, _textbooks),
|
||||
'progress': TabImpl(need_name, _progress),
|
||||
'static_tab': TabImpl(key_checker(['name', 'url_slug']), _static_tab),
|
||||
'staff_grading': TabImpl(null_validator, _staff_grading),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -215,13 +215,27 @@ class PageLoader(ActivateLoginTestCase):
|
||||
|
||||
def check_for_get_code(self, code, url):
|
||||
"""
|
||||
Check that we got the expected code. Hacks around our broken 404
|
||||
handling.
|
||||
Check that we got the expected code when accessing url via GET.
|
||||
Returns the response.
|
||||
"""
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, code,
|
||||
"got code {0} for url '{1}'. Expected code {2}"
|
||||
.format(resp.status_code, url, code))
|
||||
return resp
|
||||
|
||||
|
||||
def check_for_post_code(self, code, url, data={}):
|
||||
"""
|
||||
Check that we got the expected code when accessing url via POST.
|
||||
Returns the response.
|
||||
"""
|
||||
resp = self.client.post(url, data)
|
||||
self.assertEqual(resp.status_code, code,
|
||||
"got code {0} for url '{1}'. Expected code {2}"
|
||||
.format(resp.status_code, url, code))
|
||||
return resp
|
||||
|
||||
|
||||
|
||||
def check_pages_load(self, module_store):
|
||||
@@ -345,14 +359,10 @@ class TestNavigation(PageLoader):
|
||||
|
||||
def setUp(self):
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
courses = modulestore().get_courses()
|
||||
|
||||
def find_course(course_id):
|
||||
"""Assumes the course is present"""
|
||||
return [c for c in courses if c.id==course_id][0]
|
||||
|
||||
self.full = find_course("edX/full/6.002_Spring_2012")
|
||||
self.toy = find_course("edX/toy/2012_Fall")
|
||||
# Assume courses are there
|
||||
self.full = modulestore().get_course("edX/full/6.002_Spring_2012")
|
||||
self.toy = modulestore().get_course("edX/toy/2012_Fall")
|
||||
|
||||
# Create two accounts
|
||||
self.student = 'view@test.com'
|
||||
@@ -403,14 +413,9 @@ class TestViewAuth(PageLoader):
|
||||
|
||||
def setUp(self):
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
courses = modulestore().get_courses()
|
||||
|
||||
def find_course(course_id):
|
||||
"""Assumes the course is present"""
|
||||
return [c for c in courses if c.id==course_id][0]
|
||||
|
||||
self.full = find_course("edX/full/6.002_Spring_2012")
|
||||
self.toy = find_course("edX/toy/2012_Fall")
|
||||
self.full = modulestore().get_course("edX/full/6.002_Spring_2012")
|
||||
self.toy = modulestore().get_course("edX/toy/2012_Fall")
|
||||
|
||||
# Create two accounts
|
||||
self.student = 'view@test.com'
|
||||
@@ -688,46 +693,46 @@ class TestCourseGrader(PageLoader):
|
||||
return [c for c in courses if c.id==course_id][0]
|
||||
|
||||
self.graded_course = find_course("edX/graded/2012_Fall")
|
||||
|
||||
|
||||
# create a test student
|
||||
self.student = 'view@test.com'
|
||||
self.password = 'foo'
|
||||
self.create_account('u1', self.student, self.password)
|
||||
self.activate_user(self.student)
|
||||
self.enroll(self.graded_course)
|
||||
|
||||
|
||||
self.student_user = user(self.student)
|
||||
|
||||
|
||||
self.factory = RequestFactory()
|
||||
|
||||
|
||||
def get_grade_summary(self):
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
||||
self.graded_course.id, self.student_user, self.graded_course)
|
||||
|
||||
fake_request = self.factory.get(reverse('progress',
|
||||
kwargs={'course_id': self.graded_course.id}))
|
||||
|
||||
return grades.grade(self.student_user, fake_request,
|
||||
self.graded_course, student_module_cache)
|
||||
|
||||
def get_homework_scores(self):
|
||||
return self.get_grade_summary()['totaled_scores']['Homework']
|
||||
|
||||
def get_progress_summary(self):
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
||||
self.graded_course.id, self.student_user, self.graded_course)
|
||||
|
||||
|
||||
fake_request = self.factory.get(reverse('progress',
|
||||
kwargs={'course_id': self.graded_course.id}))
|
||||
|
||||
progress_summary = grades.progress_summary(self.student_user, fake_request,
|
||||
return grades.grade(self.student_user, fake_request,
|
||||
self.graded_course, student_module_cache)
|
||||
|
||||
def get_homework_scores(self):
|
||||
return self.get_grade_summary()['totaled_scores']['Homework']
|
||||
|
||||
def get_progress_summary(self):
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
||||
self.graded_course.id, self.student_user, self.graded_course)
|
||||
|
||||
fake_request = self.factory.get(reverse('progress',
|
||||
kwargs={'course_id': self.graded_course.id}))
|
||||
|
||||
progress_summary = grades.progress_summary(self.student_user, fake_request,
|
||||
self.graded_course, student_module_cache)
|
||||
return progress_summary
|
||||
|
||||
|
||||
def check_grade_percent(self, percent):
|
||||
grade_summary = self.get_grade_summary()
|
||||
self.assertEqual(percent, grade_summary['percent'])
|
||||
|
||||
self.assertEqual(grade_summary['percent'], percent)
|
||||
|
||||
def submit_question_answer(self, problem_url_name, responses):
|
||||
"""
|
||||
The field names of a problem are hard to determine. This method only works
|
||||
@@ -737,96 +742,96 @@ class TestCourseGrader(PageLoader):
|
||||
input_i4x-edX-graded-problem-H1P3_2_2
|
||||
"""
|
||||
problem_location = "i4x://edX/graded/problem/{0}".format(problem_url_name)
|
||||
|
||||
modx_url = reverse('modx_dispatch',
|
||||
|
||||
modx_url = reverse('modx_dispatch',
|
||||
kwargs={
|
||||
'course_id' : self.graded_course.id,
|
||||
'location' : problem_location,
|
||||
'dispatch' : 'problem_check', }
|
||||
)
|
||||
|
||||
|
||||
resp = self.client.post(modx_url, {
|
||||
'input_i4x-edX-graded-problem-{0}_2_1'.format(problem_url_name): responses[0],
|
||||
'input_i4x-edX-graded-problem-{0}_2_2'.format(problem_url_name): responses[1],
|
||||
})
|
||||
print "modx_url" , modx_url, "responses" , responses
|
||||
print "resp" , resp
|
||||
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
def problem_location(self, problem_url_name):
|
||||
return "i4x://edX/graded/problem/{0}".format(problem_url_name)
|
||||
|
||||
|
||||
def reset_question_answer(self, problem_url_name):
|
||||
problem_location = self.problem_location(problem_url_name)
|
||||
|
||||
modx_url = reverse('modx_dispatch',
|
||||
|
||||
modx_url = reverse('modx_dispatch',
|
||||
kwargs={
|
||||
'course_id' : self.graded_course.id,
|
||||
'location' : problem_location,
|
||||
'dispatch' : 'problem_reset', }
|
||||
)
|
||||
|
||||
|
||||
resp = self.client.post(modx_url)
|
||||
return resp
|
||||
|
||||
return resp
|
||||
|
||||
def test_get_graded(self):
|
||||
#### Check that the grader shows we have 0% in the course
|
||||
self.check_grade_percent(0)
|
||||
|
||||
|
||||
#### Submit the answers to a few problems as ajax calls
|
||||
def earned_hw_scores():
|
||||
"""Global scores, each Score is a Problem Set"""
|
||||
return [s.earned for s in self.get_homework_scores()]
|
||||
|
||||
|
||||
def score_for_hw(hw_url_name):
|
||||
hw_section = [section for section
|
||||
in self.get_progress_summary()[0]['sections']
|
||||
if section.get('url_name') == hw_url_name][0]
|
||||
return [s.earned for s in hw_section['scores']]
|
||||
|
||||
|
||||
# Only get half of the first problem correct
|
||||
self.submit_question_answer('H1P1', ['Correct', 'Incorrect'])
|
||||
self.check_grade_percent(0.06)
|
||||
self.assertEqual(earned_hw_scores(), [1.0, 0, 0]) # Order matters
|
||||
self.assertEqual(score_for_hw('Homework1'), [1.0, 0.0])
|
||||
|
||||
|
||||
# Get both parts of the first problem correct
|
||||
self.reset_question_answer('H1P1')
|
||||
self.submit_question_answer('H1P1', ['Correct', 'Correct'])
|
||||
self.check_grade_percent(0.13)
|
||||
self.assertEqual(earned_hw_scores(), [2.0, 0, 0])
|
||||
self.assertEqual(score_for_hw('Homework1'), [2.0, 0.0])
|
||||
|
||||
|
||||
# This problem is shown in an ABTest
|
||||
self.submit_question_answer('H1P2', ['Correct', 'Correct'])
|
||||
self.check_grade_percent(0.25)
|
||||
self.assertEqual(earned_hw_scores(), [4.0, 0.0, 0])
|
||||
self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0])
|
||||
|
||||
self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0])
|
||||
|
||||
# This problem is hidden in an ABTest. Getting it correct doesn't change total grade
|
||||
self.submit_question_answer('H1P3', ['Correct', 'Correct'])
|
||||
self.check_grade_percent(0.25)
|
||||
self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0])
|
||||
|
||||
|
||||
# On the second homework, we only answer half of the questions.
|
||||
# Then it will be dropped when homework three becomes the higher percent
|
||||
# This problem is also weighted to be 4 points (instead of default of 2)
|
||||
# If the problem was unweighted the percent would have been 0.38 so we
|
||||
# If the problem was unweighted the percent would have been 0.38 so we
|
||||
# know it works.
|
||||
self.submit_question_answer('H2P1', ['Correct', 'Correct'])
|
||||
self.check_grade_percent(0.42)
|
||||
self.assertEqual(earned_hw_scores(), [4.0, 4.0, 0])
|
||||
|
||||
self.assertEqual(earned_hw_scores(), [4.0, 4.0, 0])
|
||||
|
||||
# Third homework
|
||||
self.submit_question_answer('H3P1', ['Correct', 'Correct'])
|
||||
self.check_grade_percent(0.42) # Score didn't change
|
||||
self.assertEqual(earned_hw_scores(), [4.0, 4.0, 2.0])
|
||||
|
||||
self.assertEqual(earned_hw_scores(), [4.0, 4.0, 2.0])
|
||||
|
||||
self.submit_question_answer('H3P2', ['Correct', 'Correct'])
|
||||
self.check_grade_percent(0.5) # Now homework2 dropped. Score changes
|
||||
self.assertEqual(earned_hw_scores(), [4.0, 4.0, 4.0])
|
||||
|
||||
self.assertEqual(earned_hw_scores(), [4.0, 4.0, 4.0])
|
||||
|
||||
# Now we answer the final question (worth half of the grade)
|
||||
self.submit_question_answer('FinalQuestion', ['Correct', 'Correct'])
|
||||
self.check_grade_percent(1.0) # Hooray! We got 100%
|
||||
|
||||
@@ -17,7 +17,7 @@ from django.views.decorators.cache import cache_control
|
||||
|
||||
from courseware import grades
|
||||
from courseware.access import has_access
|
||||
from courseware.courses import (get_course_with_access, get_courses_by_university)
|
||||
from courseware.courses import (get_courses, get_course_with_access, get_courses_by_university)
|
||||
import courseware.tabs as tabs
|
||||
from courseware.models import StudentModuleCache
|
||||
from module_render import toc_for_course, get_module, get_instance_module
|
||||
@@ -61,16 +61,19 @@ def user_groups(user):
|
||||
return group_names
|
||||
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_if_anonymous
|
||||
def courses(request):
|
||||
'''
|
||||
Render "find courses" page. The course selection work is done in courseware.courses.
|
||||
'''
|
||||
universities = get_courses_by_university(request.user,
|
||||
domain=request.META.get('HTTP_HOST'))
|
||||
return render_to_response("courseware/courses.html", {'universities': universities})
|
||||
courses = get_courses(request.user, domain=request.META.get('HTTP_HOST'))
|
||||
|
||||
# Sort courses by how far are they from they start day
|
||||
key = lambda course: course.metadata['days_to_start']
|
||||
courses = sorted(courses, key=key, reverse=True)
|
||||
|
||||
return render_to_response("courseware/courses.html", {'courses': courses})
|
||||
|
||||
|
||||
def render_accordion(request, course, chapter, section):
|
||||
@@ -317,7 +320,7 @@ def jump_to(request, course_id, location):
|
||||
except NoPathToItem:
|
||||
raise Http404("This location is not in any class: {0}".format(location))
|
||||
|
||||
# choose the appropriate view (and provide the necessary args) based on the
|
||||
# choose the appropriate view (and provide the necessary args) based on the
|
||||
# args provided by the redirect.
|
||||
# Rely on index to do all error handling and access control.
|
||||
if chapter is None:
|
||||
@@ -328,7 +331,7 @@ def jump_to(request, course_id, location):
|
||||
return redirect('courseware_section', course_id=course_id, chapter=chapter, section=section)
|
||||
else:
|
||||
return redirect('courseware_position', course_id=course_id, chapter=chapter, section=section, position=position)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def course_info(request, course_id):
|
||||
"""
|
||||
@@ -435,6 +438,11 @@ def university_profile(request, org_id):
|
||||
# Only grab courses for this org...
|
||||
courses = get_courses_by_university(request.user,
|
||||
domain=request.META.get('HTTP_HOST'))[org_id]
|
||||
|
||||
# Sort courses by how far are they from they start day
|
||||
key = lambda course: course.metadata['days_to_start']
|
||||
courses = sorted(courses, key=key, reverse=True)
|
||||
|
||||
context = dict(courses=courses, org_id=org_id)
|
||||
template_file = "university_profile/{0}.html".format(org_id).lower()
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ import logging
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from student.models import CourseEnrollment
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from courseware.courses import get_course_by_id
|
||||
|
||||
@@ -10,6 +13,18 @@ FORUM_ROLE_MODERATOR = 'Moderator'
|
||||
FORUM_ROLE_COMMUNITY_TA = 'Community TA'
|
||||
FORUM_ROLE_STUDENT = 'Student'
|
||||
|
||||
|
||||
@receiver(post_save, sender=CourseEnrollment)
|
||||
def assign_default_role(sender, instance, **kwargs):
|
||||
if instance.user.is_staff:
|
||||
role = Role.objects.get_or_create(course_id=instance.course_id, name="Moderator")[0]
|
||||
else:
|
||||
role = Role.objects.get_or_create(course_id=instance.course_id, name="Student")[0]
|
||||
|
||||
logging.info("assign_default_role: adding %s as %s" % (instance.user, role))
|
||||
instance.user.roles.add(role)
|
||||
|
||||
|
||||
class Role(models.Model):
|
||||
name = models.CharField(max_length=30, null=False, blank=False)
|
||||
users = models.ManyToManyField(User, related_name="roles")
|
||||
|
||||
@@ -17,6 +17,7 @@ from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.search import path_to_location
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# TODO these should be cached via django's caching rather than in-memory globals
|
||||
_FULLMODULES = None
|
||||
@@ -141,6 +142,15 @@ def initialize_discussion_info(course):
|
||||
|
||||
for location, module in all_modules.items():
|
||||
if location.category == 'discussion':
|
||||
skip_module = False
|
||||
for key in ('id', 'discussion_category', 'for'):
|
||||
if key not in module.metadata:
|
||||
log.warning("Required key '%s' not in discussion %s, leaving out of category map" % (key, module.location))
|
||||
skip_module = True
|
||||
|
||||
if skip_module:
|
||||
continue
|
||||
|
||||
id = module.metadata['id']
|
||||
category = module.metadata['discussion_category']
|
||||
title = module.metadata['for']
|
||||
@@ -245,7 +255,7 @@ class QueryCountDebugMiddleware(object):
|
||||
query_time = query.get('duration', 0) / 1000
|
||||
total_time += float(query_time)
|
||||
|
||||
logging.info('%s queries run, total %s seconds' % (len(connection.queries), total_time))
|
||||
log.info('%s queries run, total %s seconds' % (len(connection.queries), total_time))
|
||||
return response
|
||||
|
||||
def get_ability(course_id, content, user):
|
||||
@@ -317,7 +327,7 @@ def extend_content(content):
|
||||
user = User.objects.get(pk=content['user_id'])
|
||||
roles = dict(('name', role.name.lower()) for role in user.roles.filter(course_id=content['course_id']))
|
||||
except user.DoesNotExist:
|
||||
logging.error('User ID {0} in comment content {1} but not in our DB.'.format(content.get('user_id'), content.get('id')))
|
||||
log.error('User ID {0} in comment content {1} but not in our DB.'.format(content.get('user_id'), content.get('id')))
|
||||
|
||||
content_info = {
|
||||
'displayed_title': content.get('highlighted_title') or content.get('title', ''),
|
||||
|
||||
25
lms/djangoapps/instructor/grading.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
LMS part of instructor grading:
|
||||
|
||||
- views + ajax handling
|
||||
- calls the instructor grading service
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StaffGrading(object):
|
||||
"""
|
||||
Wrap up functionality for staff grading of submissions--interface exposes get_html, ajax views.
|
||||
"""
|
||||
def __init__(self, course):
|
||||
self.course = course
|
||||
|
||||
def get_html(self):
|
||||
return "<b>Instructor grading!</b>"
|
||||
# context = {}
|
||||
# return render_to_string('courseware/instructor_grading_view.html', context)
|
||||
|
||||
390
lms/djangoapps/instructor/staff_grading_service.py
Normal file
@@ -0,0 +1,390 @@
|
||||
"""
|
||||
This module provides views that proxy to the staff grading backend service.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
from requests.exceptions import RequestException, ConnectionError, HTTPError
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse, Http404
|
||||
|
||||
from courseware.access import has_access
|
||||
from util.json_request import expect_json
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GradingServiceError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MockStaffGradingService(object):
|
||||
"""
|
||||
A simple mockup of a staff grading service, testing.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.cnt = 0
|
||||
|
||||
def get_next(self,course_id, location, grader_id):
|
||||
self.cnt += 1
|
||||
return json.dumps({'success': True,
|
||||
'submission_id': self.cnt,
|
||||
'submission': 'Test submission {cnt}'.format(cnt=self.cnt),
|
||||
'num_graded': 3,
|
||||
'min_for_ml': 5,
|
||||
'num_pending': 4,
|
||||
'prompt': 'This is a fake prompt',
|
||||
'ml_error_info': 'ML info',
|
||||
'max_score': 2 + self.cnt % 3,
|
||||
'rubric': 'A rubric'})
|
||||
|
||||
def get_problem_list(self, course_id, grader_id):
|
||||
self.cnt += 1
|
||||
return json.dumps({'success': True,
|
||||
'problem_list': [
|
||||
json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo1', \
|
||||
'problem_name': "Problem 1", 'num_graded': 3, 'num_pending': 5, 'min_for_ml': 10}),
|
||||
json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo2', \
|
||||
'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5, 'min_for_ml': 10})
|
||||
]})
|
||||
|
||||
|
||||
def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped):
|
||||
return self.get_next(course_id, 'fake location', grader_id)
|
||||
|
||||
|
||||
class StaffGradingService(object):
|
||||
"""
|
||||
Interface to staff grading backend.
|
||||
"""
|
||||
def __init__(self, config):
|
||||
self.username = config['username']
|
||||
self.password = config['password']
|
||||
self.url = config['url']
|
||||
|
||||
self.login_url = self.url + '/login/'
|
||||
self.get_next_url = self.url + '/get_next_submission/'
|
||||
self.save_grade_url = self.url + '/save_grade/'
|
||||
self.get_problem_list_url = self.url + '/get_problem_list/'
|
||||
|
||||
self.session = requests.session()
|
||||
|
||||
|
||||
def _login(self):
|
||||
"""
|
||||
Log into the staff grading service.
|
||||
|
||||
Raises requests.exceptions.HTTPError if something goes wrong.
|
||||
|
||||
Returns the decoded json dict of the response.
|
||||
"""
|
||||
response = self.session.post(self.login_url,
|
||||
{'username': self.username,
|
||||
'password': self.password,})
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json
|
||||
|
||||
|
||||
def _try_with_login(self, operation):
|
||||
"""
|
||||
Call operation(), which should return a requests response object. If
|
||||
the request fails with a 'login_required' error, call _login() and try
|
||||
the operation again.
|
||||
|
||||
Returns the result of operation(). Does not catch exceptions.
|
||||
"""
|
||||
response = operation()
|
||||
if (response.json
|
||||
and response.json.get('success') == False
|
||||
and response.json.get('error') == 'login_required'):
|
||||
# apparrently we aren't logged in. Try to fix that.
|
||||
r = self._login()
|
||||
if r and not r.get('success'):
|
||||
log.warning("Couldn't log into staff_grading backend. Response: %s",
|
||||
r)
|
||||
# try again
|
||||
response = operation()
|
||||
response.raise_for_status()
|
||||
|
||||
return response
|
||||
|
||||
def get_problem_list(self, course_id, grader_id):
|
||||
"""
|
||||
Get the list of problems for a given course.
|
||||
|
||||
Args:
|
||||
course_id: course id that we want the problems of
|
||||
grader_id: who is grading this? The anonymous user_id of the grader.
|
||||
|
||||
Returns:
|
||||
json string with the response from the service. (Deliberately not
|
||||
writing out the fields here--see the docs on the staff_grading view
|
||||
in the grading_controller repo)
|
||||
|
||||
Raises:
|
||||
GradingServiceError: something went wrong with the connection.
|
||||
"""
|
||||
op = lambda: self.session.get(self.get_problem_list_url,
|
||||
allow_redirects = False,
|
||||
params={'course_id': course_id,
|
||||
'grader_id': grader_id})
|
||||
try:
|
||||
r = self._try_with_login(op)
|
||||
except (RequestException, ConnectionError, HTTPError) as err:
|
||||
# reraise as promised GradingServiceError, but preserve stacktrace.
|
||||
raise GradingServiceError, str(err), sys.exc_info()[2]
|
||||
|
||||
return r.text
|
||||
|
||||
|
||||
def get_next(self, course_id, location, grader_id):
|
||||
"""
|
||||
Get the next thing to grade.
|
||||
|
||||
Args:
|
||||
course_id: the course that this problem belongs to
|
||||
location: location of the problem that we are grading and would like the
|
||||
next submission for
|
||||
grader_id: who is grading this? The anonymous user_id of the grader.
|
||||
|
||||
Returns:
|
||||
json string with the response from the service. (Deliberately not
|
||||
writing out the fields here--see the docs on the staff_grading view
|
||||
in the grading_controller repo)
|
||||
|
||||
Raises:
|
||||
GradingServiceError: something went wrong with the connection.
|
||||
"""
|
||||
op = lambda: self.session.get(self.get_next_url,
|
||||
allow_redirects=False,
|
||||
params={'location': location,
|
||||
'grader_id': grader_id})
|
||||
try:
|
||||
r = self._try_with_login(op)
|
||||
except (RequestException, ConnectionError, HTTPError) as err:
|
||||
# reraise as promised GradingServiceError, but preserve stacktrace.
|
||||
raise GradingServiceError, str(err), sys.exc_info()[2]
|
||||
|
||||
return r.text
|
||||
|
||||
|
||||
def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped):
|
||||
"""
|
||||
Save a score and feedback for a submission.
|
||||
|
||||
Returns:
|
||||
json dict with keys
|
||||
'success': bool
|
||||
'error': error msg, if something went wrong.
|
||||
|
||||
Raises:
|
||||
GradingServiceError if there's a problem connecting.
|
||||
"""
|
||||
try:
|
||||
data = {'course_id': course_id,
|
||||
'submission_id': submission_id,
|
||||
'score': score,
|
||||
'feedback': feedback,
|
||||
'grader_id': grader_id,
|
||||
'skipped': skipped}
|
||||
|
||||
op = lambda: self.session.post(self.save_grade_url, data=data,
|
||||
allow_redirects=False)
|
||||
r = self._try_with_login(op)
|
||||
except (RequestException, ConnectionError, HTTPError) as err:
|
||||
# reraise as promised GradingServiceError, but preserve stacktrace.
|
||||
raise GradingServiceError, str(err), sys.exc_info()[2]
|
||||
|
||||
return r.text
|
||||
|
||||
# don't initialize until grading_service() is called--means that just
|
||||
# importing this file doesn't create objects that may not have the right config
|
||||
_service = None
|
||||
|
||||
def grading_service():
|
||||
"""
|
||||
Return a staff grading service instance--if settings.MOCK_STAFF_GRADING is True,
|
||||
returns a mock one, otherwise a real one.
|
||||
|
||||
Caches the result, so changing the setting after the first call to this
|
||||
function will have no effect.
|
||||
"""
|
||||
global _service
|
||||
if _service is not None:
|
||||
return _service
|
||||
|
||||
if settings.MOCK_STAFF_GRADING:
|
||||
_service = MockStaffGradingService()
|
||||
else:
|
||||
_service = StaffGradingService(settings.STAFF_GRADING_INTERFACE)
|
||||
|
||||
return _service
|
||||
|
||||
def _err_response(msg):
|
||||
"""
|
||||
Return a HttpResponse with a json dump with success=False, and the given error message.
|
||||
"""
|
||||
return HttpResponse(json.dumps({'success': False, 'error': msg}),
|
||||
mimetype="application/json")
|
||||
|
||||
|
||||
def _check_access(user, course_id):
|
||||
"""
|
||||
Raise 404 if user doesn't have staff access to course_id
|
||||
"""
|
||||
course_location = CourseDescriptor.id_to_location(course_id)
|
||||
if not has_access(user, course_location, 'staff'):
|
||||
raise Http404
|
||||
|
||||
return
|
||||
|
||||
|
||||
def get_next(request, course_id):
|
||||
"""
|
||||
Get the next thing to grade for course_id and with the location specified
|
||||
in the .
|
||||
|
||||
Returns a json dict with the following keys:
|
||||
|
||||
'success': bool
|
||||
|
||||
'submission_id': a unique identifier for the submission, to be passed back
|
||||
with the grade.
|
||||
|
||||
'submission': the submission, rendered as read-only html for grading
|
||||
|
||||
'rubric': the rubric, also rendered as html.
|
||||
|
||||
'message': if there was no submission available, but nothing went wrong,
|
||||
there will be a message field.
|
||||
|
||||
'error': if success is False, will have an error message with more info.
|
||||
"""
|
||||
_check_access(request.user, course_id)
|
||||
|
||||
required = set(['location'])
|
||||
if request.method != 'POST':
|
||||
raise Http404
|
||||
actual = set(request.POST.keys())
|
||||
missing = required - actual
|
||||
if len(missing) > 0:
|
||||
return _err_response('Missing required keys {0}'.format(
|
||||
', '.join(missing)))
|
||||
grader_id = request.user.id
|
||||
p = request.POST
|
||||
location = p['location']
|
||||
|
||||
return HttpResponse(_get_next(course_id, request.user.id, location),
|
||||
mimetype="application/json")
|
||||
|
||||
|
||||
def get_problem_list(request, course_id):
|
||||
"""
|
||||
Get all the problems for the given course id
|
||||
|
||||
Returns a json dict with the following keys:
|
||||
success: bool
|
||||
|
||||
problem_list: a list containing json dicts with the following keys:
|
||||
each dict represents a different problem in the course
|
||||
|
||||
location: the location of the problem
|
||||
|
||||
problem_name: the name of the problem
|
||||
|
||||
num_graded: the number of responses that have been graded
|
||||
|
||||
num_pending: the number of responses that are sitting in the queue
|
||||
|
||||
min_for_ml: the number of responses that need to be graded before
|
||||
the ml can be run
|
||||
|
||||
"""
|
||||
_check_access(request.user, course_id)
|
||||
try:
|
||||
response = grading_service().get_problem_list(course_id, request.user.id)
|
||||
return HttpResponse(response,
|
||||
mimetype="application/json")
|
||||
except GradingServiceError:
|
||||
log.exception("Error from grading service. server url: {0}"
|
||||
.format(grading_service().url))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Could not connect to grading service'}))
|
||||
|
||||
|
||||
def _get_next(course_id, grader_id, location):
|
||||
"""
|
||||
Implementation of get_next (also called from save_grade) -- returns a json string
|
||||
"""
|
||||
try:
|
||||
return grading_service().get_next(course_id, location, grader_id)
|
||||
except GradingServiceError:
|
||||
log.exception("Error from grading service. server url: {0}"
|
||||
.format(grading_service().url))
|
||||
return json.dumps({'success': False,
|
||||
'error': 'Could not connect to grading service'})
|
||||
|
||||
|
||||
@expect_json
|
||||
def save_grade(request, course_id):
|
||||
"""
|
||||
Save the grade and feedback for a submission, and, if all goes well, return
|
||||
the next thing to grade.
|
||||
|
||||
Expects the following POST parameters:
|
||||
'score': int
|
||||
'feedback': string
|
||||
'submission_id': int
|
||||
|
||||
Returns the same thing as get_next, except that additional error messages
|
||||
are possible if something goes wrong with saving the grade.
|
||||
"""
|
||||
_check_access(request.user, course_id)
|
||||
|
||||
if request.method != 'POST':
|
||||
raise Http404
|
||||
|
||||
required = set(['score', 'feedback', 'submission_id', 'location'])
|
||||
actual = set(request.POST.keys())
|
||||
missing = required - actual
|
||||
if len(missing) > 0:
|
||||
return _err_response('Missing required keys {0}'.format(
|
||||
', '.join(missing)))
|
||||
|
||||
grader_id = request.user.id
|
||||
p = request.POST
|
||||
|
||||
|
||||
location = p['location']
|
||||
skipped = 'skipped' in p
|
||||
try:
|
||||
result_json = grading_service().save_grade(course_id,
|
||||
grader_id,
|
||||
p['submission_id'],
|
||||
p['score'],
|
||||
p['feedback'],
|
||||
skipped)
|
||||
except GradingServiceError:
|
||||
log.exception("Error saving grade")
|
||||
return _err_response('Could not connect to grading service')
|
||||
|
||||
try:
|
||||
result = json.loads(result_json)
|
||||
except ValueError:
|
||||
log.exception("save_grade returned broken json: %s", result_json)
|
||||
return _err_response('Grading service returned mal-formatted data.')
|
||||
|
||||
if not result.get('success', False):
|
||||
log.warning('Got success=False from grading service. Response: %s', result_json)
|
||||
return _err_response('Grading service failed')
|
||||
|
||||
# Ok, save_grade seemed to work. Get the next submission to grade.
|
||||
return HttpResponse(_get_next(course_id, grader_id, location),
|
||||
mimetype="application/json")
|
||||
|
||||
@@ -8,15 +8,24 @@ Notes for running by hand:
|
||||
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/instructor
|
||||
"""
|
||||
|
||||
import courseware.tests.tests as ct
|
||||
|
||||
import json
|
||||
|
||||
from nose import SkipTest
|
||||
from mock import patch, Mock
|
||||
|
||||
from override_settings import override_settings
|
||||
|
||||
from django.contrib.auth.models import \
|
||||
Group # Need access to internal func to put users in the right group
|
||||
# Need access to internal func to put users in the right group
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, \
|
||||
FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_STUDENT
|
||||
from django_comment_client.utils import has_forum_access
|
||||
|
||||
from instructor import staff_grading_service
|
||||
from courseware.access import _course_staff_group_name
|
||||
import courseware.tests.tests as ct
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -31,14 +40,9 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader):
|
||||
|
||||
def setUp(self):
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
courses = modulestore().get_courses()
|
||||
|
||||
def find_course(name):
|
||||
"""Assumes the course is present"""
|
||||
return [c for c in courses if c.location.course==name][0]
|
||||
|
||||
self.full = find_course("full")
|
||||
self.toy = find_course("toy")
|
||||
self.full = modulestore().get_course("edX/full/6.002_Spring_2012")
|
||||
self.toy = modulestore().get_course("edX/toy/2012_Fall")
|
||||
|
||||
# Create two accounts
|
||||
self.student = 'view@test.com'
|
||||
@@ -49,9 +53,12 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader):
|
||||
self.activate_user(self.student)
|
||||
self.activate_user(self.instructor)
|
||||
|
||||
group_name = _course_staff_group_name(self.toy.location)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(ct.user(self.instructor))
|
||||
def make_instructor(course):
|
||||
group_name = _course_staff_group_name(course.location)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(ct.user(self.instructor))
|
||||
|
||||
make_instructor(self.toy)
|
||||
|
||||
self.logout()
|
||||
self.login(self.instructor, self.password)
|
||||
@@ -67,18 +74,21 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader):
|
||||
|
||||
self.assertEqual(response['Content-Type'],'text/csv',msg)
|
||||
|
||||
cdisp = response['Content-Disposition'].replace('TT_2012','2012') # jenkins course_id is TT_2012_Fall instead of 2012_Fall?
|
||||
msg += "cdisp = '{0}'\n".format(cdisp)
|
||||
self.assertEqual(cdisp,'attachment; filename=grades_edX/toy/2012_Fall.csv',msg)
|
||||
cdisp = response['Content-Disposition']
|
||||
msg += "Content-Disposition = '%s'\n" % cdisp
|
||||
self.assertEqual(cdisp, 'attachment; filename=grades_{0}.csv'.format(course.id), msg)
|
||||
|
||||
body = response.content.replace('\r','')
|
||||
msg += "body = '{0}'\n".format(body)
|
||||
|
||||
# All the not-actually-in-the-course hw and labs come from the
|
||||
# default grading policy string in graders.py
|
||||
expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm","Final"
|
||||
"2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0.0","0.0"
|
||||
'''
|
||||
self.assertEqual(body, expected_body, msg)
|
||||
|
||||
|
||||
|
||||
FORUM_ROLES = [ FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA ]
|
||||
FORUM_ADMIN_ACTION_SUFFIX = { FORUM_ROLE_ADMINISTRATOR : 'admin', FORUM_ROLE_MODERATOR : 'moderator', FORUM_ROLE_COMMUNITY_TA : 'community TA'}
|
||||
FORUM_ADMIN_USER = { FORUM_ROLE_ADMINISTRATOR : 'forumadmin', FORUM_ROLE_MODERATOR : 'forummoderator', FORUM_ROLE_COMMUNITY_TA : 'forummoderator'}
|
||||
@@ -89,22 +99,22 @@ def action_name(operation, rolename):
|
||||
else:
|
||||
return '{0} forum {1}'.format(operation, FORUM_ADMIN_ACTION_SUFFIX[rolename])
|
||||
|
||||
|
||||
_mock_service = staff_grading_service.MockStaffGradingService()
|
||||
|
||||
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
|
||||
class TestInstructorDashboardForumAdmin(ct.PageLoader):
|
||||
'''
|
||||
Check for change in forum admin role memberships
|
||||
'''
|
||||
|
||||
|
||||
def setUp(self):
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
courses = modulestore().get_courses()
|
||||
|
||||
def find_course(name):
|
||||
"""Assumes the course is present"""
|
||||
return [c for c in courses if c.location.course==name][0]
|
||||
|
||||
self.full = find_course("full")
|
||||
self.toy = find_course("toy")
|
||||
self.course_id = "edX/toy/2012_Fall"
|
||||
self.toy = modulestore().get_course(self.course_id)
|
||||
|
||||
# Create two accounts
|
||||
self.student = 'view@test.com'
|
||||
@@ -123,6 +133,8 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader):
|
||||
self.login(self.instructor, self.password)
|
||||
self.enroll(self.toy)
|
||||
|
||||
|
||||
|
||||
def initialize_roles(self, course_id):
|
||||
self.admin_role = Role.objects.get_or_create(name=FORUM_ROLE_ADMINISTRATOR, course_id=course_id)[0]
|
||||
self.moderator_role = Role.objects.get_or_create(name=FORUM_ROLE_MODERATOR, course_id=course_id)[0]
|
||||
@@ -209,3 +221,96 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader):
|
||||
added_roles.sort()
|
||||
roles = ', '.join(added_roles)
|
||||
self.assertTrue(response.content.find('<td>{0}</td>'.format(roles))>=0, 'not finding roles "{0}"'.format(roles))
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
|
||||
class TestStaffGradingService(ct.PageLoader):
|
||||
'''
|
||||
Check that staff grading service proxy works. Basically just checking the
|
||||
access control and error handling logic -- all the actual work is on the
|
||||
backend.
|
||||
'''
|
||||
def setUp(self):
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
|
||||
self.student = 'view@test.com'
|
||||
self.instructor = 'view2@test.com'
|
||||
self.password = 'foo'
|
||||
self.location = 'TestLocation'
|
||||
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)
|
||||
|
||||
self.course_id = "edX/toy/2012_Fall"
|
||||
self.toy = modulestore().get_course(self.course_id)
|
||||
def make_instructor(course):
|
||||
group_name = _course_staff_group_name(course.location)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(ct.user(self.instructor))
|
||||
|
||||
make_instructor(self.toy)
|
||||
|
||||
self.mock_service = staff_grading_service.grading_service()
|
||||
|
||||
self.logout()
|
||||
|
||||
def test_access(self):
|
||||
"""
|
||||
Make sure only staff have access.
|
||||
"""
|
||||
self.login(self.student, self.password)
|
||||
|
||||
# both get and post should return 404
|
||||
for view_name in ('staff_grading_get_next', 'staff_grading_save_grade'):
|
||||
url = reverse(view_name, kwargs={'course_id': self.course_id})
|
||||
self.check_for_get_code(404, url)
|
||||
self.check_for_post_code(404, url)
|
||||
|
||||
|
||||
def test_get_next(self):
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id})
|
||||
data = {'location': self.location}
|
||||
|
||||
r = self.check_for_post_code(200, url, data)
|
||||
d = json.loads(r.content)
|
||||
self.assertTrue(d['success'])
|
||||
self.assertEquals(d['submission_id'], self.mock_service.cnt)
|
||||
self.assertIsNotNone(d['submission'])
|
||||
self.assertIsNotNone(d['num_graded'])
|
||||
self.assertIsNotNone(d['min_for_ml'])
|
||||
self.assertIsNotNone(d['num_pending'])
|
||||
self.assertIsNotNone(d['prompt'])
|
||||
self.assertIsNotNone(d['ml_error_info'])
|
||||
self.assertIsNotNone(d['max_score'])
|
||||
self.assertIsNotNone(d['rubric'])
|
||||
|
||||
|
||||
def test_save_grade(self):
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id})
|
||||
|
||||
data = {'score': '12',
|
||||
'feedback': 'great!',
|
||||
'submission_id': '123',
|
||||
'location': self.location}
|
||||
r = self.check_for_post_code(200, url, data)
|
||||
d = json.loads(r.content)
|
||||
self.assertTrue(d['success'], str(d))
|
||||
self.assertEquals(d['submission_id'], self.mock_service.cnt)
|
||||
|
||||
def test_get_problem_list(self):
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
url = reverse('staff_grading_get_problem_list', kwargs={'course_id': self.course_id})
|
||||
data = {}
|
||||
|
||||
r = self.check_for_post_code(200, url, data)
|
||||
d = json.loads(r.content)
|
||||
self.assertTrue(d['success'], str(d))
|
||||
self.assertIsNotNone(d['problem_list'])
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from django.http import HttpResponse
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.cache import cache_control
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from courseware import grades
|
||||
from courseware.access import has_access, get_access_group_name
|
||||
@@ -27,7 +28,10 @@ from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundErr
|
||||
from xmodule.modulestore.search import path_to_location
|
||||
import track.views
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
from .grading import StaffGrading
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
template_imports = {'urllib': urllib}
|
||||
|
||||
@@ -87,7 +91,7 @@ def instructor_dashboard(request, course_id):
|
||||
try:
|
||||
group = Group.objects.get(name=staffgrp)
|
||||
except Group.DoesNotExist:
|
||||
group = Group(name=staffgrp) # create the group
|
||||
group = Group(name=staffgrp) # create the group
|
||||
group.save()
|
||||
return group
|
||||
|
||||
@@ -377,7 +381,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True,
|
||||
enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).prefetch_related("groups").order_by('username')
|
||||
|
||||
header = ['ID', 'Username', 'Full Name', 'edX email', 'External email']
|
||||
if get_grades:
|
||||
if get_grades and enrolled_students.count() > 0:
|
||||
# just to construct the header
|
||||
gradeset = grades.grade(enrolled_students[0], request, course, keep_raw_scores=get_raw_scores)
|
||||
# log.debug('student {0} gradeset {1}'.format(enrolled_students[0], gradeset))
|
||||
@@ -409,6 +413,29 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True,
|
||||
return datatable
|
||||
|
||||
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def staff_grading(request, course_id):
|
||||
"""
|
||||
Show the instructor grading interface.
|
||||
"""
|
||||
course = get_course_with_access(request.user, course_id, 'staff')
|
||||
|
||||
grading = StaffGrading(course)
|
||||
|
||||
ajax_url = reverse('staff_grading', kwargs={'course_id': course_id})
|
||||
if not ajax_url.endswith('/'):
|
||||
ajax_url += '/'
|
||||
|
||||
return render_to_response('instructor/staff_grading.html', {
|
||||
'view_html': grading.get_html(),
|
||||
'course': course,
|
||||
'course_id': course_id,
|
||||
'ajax_url': ajax_url,
|
||||
# Checked above
|
||||
'staff_access': True, })
|
||||
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def gradebook(request, course_id):
|
||||
"""
|
||||
|
||||
@@ -83,5 +83,7 @@ XQUEUE_INTERFACE = AUTH_TOKENS['XQUEUE_INTERFACE']
|
||||
MODULESTORE = AUTH_TOKENS.get('MODULESTORE', MODULESTORE)
|
||||
CONTENTSTORE = AUTH_TOKENS.get('CONTENTSTORE', CONTENTSTORE)
|
||||
|
||||
STAFF_GRADING_INTERFACE = AUTH_TOKENS.get('STAFF_GRADING_INTERFACE')
|
||||
|
||||
PEARSON_TEST_USER = "pearsontest"
|
||||
PEARSON_TEST_PASSWORD = AUTH_TOKENS.get("PEARSON_TEST_PASSWORD")
|
||||
|
||||
@@ -185,6 +185,9 @@ DEBUG_TRACK_LOG = False
|
||||
|
||||
MITX_ROOT_URL = ''
|
||||
|
||||
LOGIN_REDIRECT_URL = MITX_ROOT_URL + '/accounts/login'
|
||||
LOGIN_URL = MITX_ROOT_URL + '/accounts/login'
|
||||
|
||||
COURSE_NAME = "6.002_Spring_2012"
|
||||
COURSE_NUMBER = "6.002x"
|
||||
COURSE_TITLE = "Circuits and Electronics"
|
||||
@@ -321,6 +324,13 @@ WIKI_USE_BOOTSTRAP_SELECT_WIDGET = False
|
||||
WIKI_LINK_LIVE_LOOKUPS = False
|
||||
WIKI_LINK_DEFAULT_LEVEL = 2
|
||||
|
||||
################################# Staff grading config #####################
|
||||
|
||||
STAFF_GRADING_INTERFACE = None
|
||||
# Used for testing, debugging
|
||||
MOCK_STAFF_GRADING = False
|
||||
|
||||
|
||||
################################# Jasmine ###################################
|
||||
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
|
||||
|
||||
@@ -387,6 +397,7 @@ courseware_js = (
|
||||
)
|
||||
|
||||
main_vendor_js = [
|
||||
'js/vendor/json2.js',
|
||||
'js/vendor/jquery.min.js',
|
||||
'js/vendor/jquery-ui.min.js',
|
||||
'js/vendor/jquery.cookie.js',
|
||||
@@ -397,6 +408,8 @@ main_vendor_js = [
|
||||
|
||||
discussion_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/discussion/**/*.coffee'))
|
||||
|
||||
staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.coffee'))
|
||||
|
||||
PIPELINE_CSS = {
|
||||
'application': {
|
||||
'source_filenames': ['sass/application.scss'],
|
||||
@@ -425,8 +438,9 @@ PIPELINE_JS = {
|
||||
'source_filenames': sorted(
|
||||
set(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/**/*.coffee') +
|
||||
rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/**/*.coffee')) -
|
||||
set(courseware_js + discussion_js)
|
||||
set(courseware_js + discussion_js + staff_grading_js)
|
||||
) + [
|
||||
|
||||
'js/form.ext.js',
|
||||
'js/my_courses_dropdown.js',
|
||||
'js/toggle_login_modal.js',
|
||||
@@ -451,9 +465,12 @@ PIPELINE_JS = {
|
||||
'source_filenames': discussion_js,
|
||||
'output_filename': 'js/discussion.js'
|
||||
},
|
||||
'staff_grading' : {
|
||||
'source_filenames': staff_grading_js,
|
||||
'output_filename': 'js/staff_grading.js'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
PIPELINE_DISABLE_WRAPPER = True
|
||||
|
||||
# Compile all coffee files in course data directories if they are out of date.
|
||||
|
||||
@@ -39,7 +39,7 @@ DATABASES = {
|
||||
}
|
||||
|
||||
CACHES = {
|
||||
# This is the cache used for most things.
|
||||
# This is the cache used for most things.
|
||||
# In staging/prod envs, the sessions also live here.
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
@@ -106,7 +106,13 @@ VIRTUAL_UNIVERSITIES = []
|
||||
|
||||
COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE"
|
||||
|
||||
################################# Staff grading config #####################
|
||||
|
||||
STAFF_GRADING_INTERFACE = {
|
||||
'url': 'http://127.0.0.1:3033/staff_grading',
|
||||
'username': 'lms',
|
||||
'password': 'abcd',
|
||||
}
|
||||
|
||||
################################ LMS Migration #################################
|
||||
MITX_FEATURES['ENABLE_LMS_MIGRATION'] = True
|
||||
|
||||
@@ -44,12 +44,6 @@ STATUS_MESSAGE_PATH = TEST_ROOT / "status_message.json"
|
||||
COURSES_ROOT = TEST_ROOT / "data"
|
||||
DATA_DIR = COURSES_ROOT
|
||||
|
||||
LOGGING = get_logger_config(TEST_ROOT / "log",
|
||||
logging_env="dev",
|
||||
tracking_filename="tracking.log",
|
||||
dev_env=True,
|
||||
debug=True)
|
||||
|
||||
COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data"
|
||||
# Where the content data is checked out. This may not exist on jenkins.
|
||||
GITHUB_REPO_ROOT = ENV_ROOT / "data"
|
||||
@@ -65,6 +59,10 @@ XQUEUE_INTERFACE = {
|
||||
}
|
||||
XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds
|
||||
|
||||
|
||||
# Don't rely on a real staff grading backend
|
||||
MOCK_STAFF_GRADING = True
|
||||
|
||||
# TODO (cpennington): We need to figure out how envs/test.py can inject things
|
||||
# into common.py so that we don't have to repeat this sort of thing
|
||||
STATICFILES_DIRS = [
|
||||
@@ -99,7 +97,7 @@ DATABASES = {
|
||||
}
|
||||
|
||||
CACHES = {
|
||||
# This is the cache used for most things.
|
||||
# This is the cache used for most things.
|
||||
# In staging/prod envs, the sessions also live here.
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
/* IE 6 & 7 */
|
||||
|
||||
/* Proper fixed width for dashboard in IE6 */
|
||||
|
||||
.dashboard #content {
|
||||
*width: 768px;
|
||||
}
|
||||
|
||||
.dashboard #content-main {
|
||||
*width: 535px;
|
||||
}
|
||||
|
||||
/* IE 6 ONLY */
|
||||
|
||||
/* Keep header from flowing off the page */
|
||||
|
||||
#container {
|
||||
_position: static;
|
||||
}
|
||||
|
||||
/* Put the right sidebars back on the page */
|
||||
|
||||
.colMS #content-related {
|
||||
_margin-right: 0;
|
||||
_margin-left: 10px;
|
||||
_position: static;
|
||||
}
|
||||
|
||||
/* Put the left sidebars back on the page */
|
||||
|
||||
.colSM #content-related {
|
||||
_margin-right: 10px;
|
||||
_margin-left: -115px;
|
||||
_position: static;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
_height: 1%;
|
||||
}
|
||||
|
||||
/* Fix right margin for changelist filters in IE6 */
|
||||
|
||||
#changelist-filter ul {
|
||||
_margin-right: -10px;
|
||||
}
|
||||
|
||||
/* IE ignores min-height, but treats height as if it were min-height */
|
||||
|
||||
.change-list .filtered {
|
||||
_height: 400px;
|
||||
}
|
||||
|
||||
/* IE doesn't know alpha transparency in PNGs */
|
||||
|
||||
.inline-deletelink {
|
||||
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;
|
||||
}
|
||||
@@ -32,8 +32,18 @@ $ ->
|
||||
|
||||
$('#login').click ->
|
||||
$('#login_form input[name="email"]').focus()
|
||||
_gaq.push(['_trackPageview', '/login'])
|
||||
false
|
||||
|
||||
$('#signup').click ->
|
||||
$('#signup-modal input[name="email"]').focus()
|
||||
_gaq.push(['_trackPageview', '/signup'])
|
||||
false
|
||||
|
||||
# fix for ie
|
||||
if !Array::indexOf
|
||||
Array::indexOf = (obj, start = 0) ->
|
||||
for ele, i in this[start..]
|
||||
if ele is obj
|
||||
return i + start
|
||||
return -1
|
||||
|
||||
404
lms/static/coffee/src/staff_grading/staff_grading.coffee
Normal file
@@ -0,0 +1,404 @@
|
||||
# wrap everything in a class in case we want to use inside xmodules later
|
||||
|
||||
get_random_int: (min, max) ->
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
|
||||
# states
|
||||
state_grading = "grading"
|
||||
state_graded = "graded"
|
||||
state_no_data = "no_data"
|
||||
state_error = "error"
|
||||
|
||||
class StaffGradingBackend
|
||||
constructor: (ajax_url, mock_backend) ->
|
||||
@ajax_url = ajax_url
|
||||
@mock_backend = mock_backend
|
||||
if @mock_backend
|
||||
@mock_cnt = 0
|
||||
|
||||
mock: (cmd, data) ->
|
||||
# Return a mock response to cmd and data
|
||||
# should take a location as an argument
|
||||
if cmd == 'get_next'
|
||||
@mock_cnt++
|
||||
switch data.location
|
||||
when 'i4x://MITx/3.091x/problem/open_ended_demo1'
|
||||
response =
|
||||
success: true
|
||||
problem_name: 'Problem 1'
|
||||
num_graded: 3
|
||||
min_for_ml: 5
|
||||
num_pending: 4
|
||||
prompt: '''
|
||||
<h2>S11E3: Metal Bands</h2>
|
||||
<p>Shown below are schematic band diagrams for two different metals. Both diagrams appear different, yet both of the elements are undisputably metallic in nature.</p>
|
||||
<img width="480" src="/static/images/LSQimages/shaded_metal_bands.png"/>
|
||||
<p>* Why is it that both sodium and magnesium behave as metals, even though the s-band of magnesium is filled? </p>
|
||||
<p>This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question.</p>
|
||||
'''
|
||||
submission: '''
|
||||
Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.
|
||||
|
||||
The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.
|
||||
'''
|
||||
rubric: '''
|
||||
<ul>
|
||||
<li>Metals tend to be good electronic conductors, meaning that they have a large number of electrons which are able to access empty (mobile) energy states within the material.</li>
|
||||
<li>Sodium has a half-filled s-band, so there are a number of empty states immediately above the highest occupied energy levels within the band.</li>
|
||||
<li>Magnesium has a full s-band, but the the s-band and p-band overlap in magnesium. Thus are still a large number of available energy states immediately above the s-band highest occupied energy level.</li>
|
||||
</ul>
|
||||
|
||||
<p>Please score your response according to how many of the above components you identified:</p>
|
||||
'''
|
||||
submission_id: @mock_cnt
|
||||
max_score: 2 + @mock_cnt % 3
|
||||
ml_error_info : 'ML accuracy info: ' + @mock_cnt
|
||||
when 'i4x://MITx/3.091x/problem/open_ended_demo2'
|
||||
response =
|
||||
success: true
|
||||
problem_name: 'Problem 2'
|
||||
num_graded: 2
|
||||
min_for_ml: 5
|
||||
num_pending: 4
|
||||
prompt: 'This is a fake second problem'
|
||||
submission: 'This is the best submission ever! ' + @mock_cnt
|
||||
rubric: 'I am a rubric for grading things! ' + @mock_cnt
|
||||
submission_id: @mock_cnt
|
||||
max_score: 2 + @mock_cnt % 3
|
||||
ml_error_info : 'ML accuracy info: ' + @mock_cnt
|
||||
else
|
||||
response =
|
||||
success: false
|
||||
|
||||
|
||||
else if cmd == 'save_grade'
|
||||
console.log("eval: #{data.score} pts, Feedback: #{data.feedback}")
|
||||
response =
|
||||
@mock('get_next', {location: data.location})
|
||||
# get_problem_list
|
||||
# should get back a list of problem_ids, problem_names, num_graded, min_for_ml
|
||||
else if cmd == 'get_problem_list'
|
||||
@mock_cnt = 1
|
||||
response =
|
||||
success: true
|
||||
problem_list: [
|
||||
{location: 'i4x://MITx/3.091x/problem/open_ended_demo1', \
|
||||
problem_name: "Problem 1", num_graded: 3, num_pending: 5, min_for_ml: 10},
|
||||
{location: 'i4x://MITx/3.091x/problem/open_ended_demo2', \
|
||||
problem_name: "Problem 2", num_graded: 1, num_pending: 5, min_for_ml: 10}
|
||||
]
|
||||
else
|
||||
response =
|
||||
success: false
|
||||
error: 'Unknown command ' + cmd
|
||||
|
||||
if @mock_cnt % 5 == 0
|
||||
response =
|
||||
success: true
|
||||
message: 'No more submissions'
|
||||
|
||||
|
||||
if @mock_cnt % 7 == 0
|
||||
response =
|
||||
success: false
|
||||
error: 'An error for testing'
|
||||
|
||||
return response
|
||||
|
||||
|
||||
post: (cmd, data, callback) ->
|
||||
if @mock_backend
|
||||
callback(@mock(cmd, data))
|
||||
else
|
||||
# TODO: replace with postWithPrefix when that's loaded
|
||||
$.post(@ajax_url + cmd, data, callback)
|
||||
.error => callback({success: false, error: "Error occured while performing this operation"})
|
||||
|
||||
|
||||
class StaffGrading
|
||||
constructor: (backend) ->
|
||||
@backend = backend
|
||||
|
||||
# all the jquery selectors
|
||||
|
||||
@problem_list_container = $('.problem-list-container')
|
||||
@problem_list = $('.problem-list')
|
||||
|
||||
@error_container = $('.error-container')
|
||||
@message_container = $('.message-container')
|
||||
|
||||
@prompt_name_container = $('.prompt-name')
|
||||
@prompt_container = $('.prompt-container')
|
||||
@prompt_wrapper = $('.prompt-wrapper')
|
||||
|
||||
@submission_container = $('.submission-container')
|
||||
@submission_wrapper = $('.submission-wrapper')
|
||||
|
||||
@rubric_container = $('.rubric-container')
|
||||
@rubric_wrapper = $('.rubric-wrapper')
|
||||
@grading_wrapper = $('.grading-wrapper')
|
||||
|
||||
@feedback_area = $('.feedback-area')
|
||||
@score_selection_container = $('.score-selection-container')
|
||||
|
||||
@submit_button = $('.submit-button')
|
||||
@action_button = $('.action-button')
|
||||
@skip_button = $('.skip-button')
|
||||
|
||||
@problem_meta_info = $('.problem-meta-info-container')
|
||||
@meta_info_wrapper = $('.meta-info-wrapper')
|
||||
@ml_error_info_container = $('.ml-error-info-container')
|
||||
|
||||
@breadcrumbs = $('.breadcrumbs')
|
||||
|
||||
# model state
|
||||
@state = state_no_data
|
||||
@submission_id = null
|
||||
@prompt = ''
|
||||
@submission = ''
|
||||
@rubric = ''
|
||||
@error_msg = ''
|
||||
@message = ''
|
||||
@max_score = 0
|
||||
@ml_error_info= ''
|
||||
@location = ''
|
||||
@prompt_name = ''
|
||||
@min_for_ml = 0
|
||||
@num_graded = 0
|
||||
@num_pending = 0
|
||||
|
||||
@score = null
|
||||
@problems = null
|
||||
|
||||
# action handlers
|
||||
@submit_button.click @submit
|
||||
# TODO: fix this to do something more intelligent
|
||||
@action_button.click @submit
|
||||
@skip_button.click @skip_and_get_next
|
||||
|
||||
# send initial request automatically
|
||||
@get_problem_list()
|
||||
|
||||
|
||||
setup_score_selection: =>
|
||||
# first, get rid of all the old inputs, if any.
|
||||
@score_selection_container.html('Choose score: ')
|
||||
|
||||
# Now create new labels and inputs for each possible score.
|
||||
for score in [0..@max_score]
|
||||
id = 'score-' + score
|
||||
label = """<label for="#{id}">#{score}</label>"""
|
||||
|
||||
input = """
|
||||
<input type="radio" name="score-selection" id="#{id}" value="#{score}"/>
|
||||
""" # " fix broken parsing in emacs
|
||||
@score_selection_container.append(input + label)
|
||||
|
||||
# And now hook up an event handler again
|
||||
$("input[name='score-selection']").change @graded_callback
|
||||
|
||||
|
||||
set_button_text: (text) =>
|
||||
@action_button.attr('value', text)
|
||||
|
||||
graded_callback: (event) =>
|
||||
@score = event.target.value
|
||||
@state = state_graded
|
||||
@message = ''
|
||||
@render_view()
|
||||
|
||||
ajax_callback: (response) =>
|
||||
# always clear out errors and messages on transition.
|
||||
@error_msg = ''
|
||||
@message = ''
|
||||
|
||||
if response.success
|
||||
if response.problem_list
|
||||
@problems = response.problem_list
|
||||
else if response.submission
|
||||
@data_loaded(response)
|
||||
else
|
||||
@no_more(response.message)
|
||||
else
|
||||
@error(response.error)
|
||||
|
||||
@render_view()
|
||||
|
||||
get_next_submission: (location) ->
|
||||
@location = location
|
||||
@list_view = false
|
||||
@backend.post('get_next', {location: location}, @ajax_callback)
|
||||
|
||||
skip_and_get_next: () =>
|
||||
data =
|
||||
score: @score
|
||||
feedback: @feedback_area.val()
|
||||
submission_id: @submission_id
|
||||
location: @location
|
||||
skipped: true
|
||||
@backend.post('save_grade', data, @ajax_callback)
|
||||
|
||||
get_problem_list: () ->
|
||||
@list_view = true
|
||||
@backend.post('get_problem_list', {}, @ajax_callback)
|
||||
|
||||
submit_and_get_next: () ->
|
||||
data =
|
||||
score: @score
|
||||
feedback: @feedback_area.val()
|
||||
submission_id: @submission_id
|
||||
location: @location
|
||||
|
||||
@backend.post('save_grade', data, @ajax_callback)
|
||||
|
||||
error: (msg) ->
|
||||
@error_msg = msg
|
||||
@state = state_error
|
||||
|
||||
data_loaded: (response) ->
|
||||
@prompt = response.prompt
|
||||
@submission = response.submission
|
||||
@rubric = response.rubric
|
||||
@submission_id = response.submission_id
|
||||
@feedback_area.val('')
|
||||
@max_score = response.max_score
|
||||
@score = null
|
||||
@ml_error_info=response.ml_error_info
|
||||
@prompt_name = response.problem_name
|
||||
@num_graded = response.num_graded
|
||||
@min_for_ml = response.min_for_ml
|
||||
@num_pending = response.num_pending
|
||||
@state = state_grading
|
||||
if not @max_score?
|
||||
@error("No max score specified for submission.")
|
||||
|
||||
no_more: (message) ->
|
||||
@prompt = null
|
||||
@prompt_name = ''
|
||||
@num_graded = 0
|
||||
@min_for_ml = 0
|
||||
@submission = null
|
||||
@rubric = null
|
||||
@ml_error_info = null
|
||||
@submission_id = null
|
||||
@message = message
|
||||
@score = null
|
||||
@max_score = 0
|
||||
@state = state_no_data
|
||||
|
||||
|
||||
render_view: () ->
|
||||
# clear the problem list and breadcrumbs
|
||||
@problem_list.html('')
|
||||
@breadcrumbs.html('')
|
||||
@problem_list_container.toggle(@list_view)
|
||||
if @backend.mock_backend
|
||||
@message = @message + "<p>NOTE: Mocking backend.</p>"
|
||||
@message_container.html(@message)
|
||||
@error_container.html(@error_msg)
|
||||
@message_container.toggle(@message != "")
|
||||
@error_container.toggle(@error_msg != "")
|
||||
|
||||
|
||||
# only show the grading elements when we are not in list view or the state
|
||||
# is invalid
|
||||
show_grading_elements = !(@list_view || @state == state_error ||
|
||||
@state == state_no_data)
|
||||
@prompt_wrapper.toggle(show_grading_elements)
|
||||
@submission_wrapper.toggle(show_grading_elements)
|
||||
@rubric_wrapper.toggle(show_grading_elements)
|
||||
@grading_wrapper.toggle(show_grading_elements)
|
||||
@meta_info_wrapper.toggle(show_grading_elements)
|
||||
@action_button.hide()
|
||||
|
||||
if @list_view
|
||||
@render_list()
|
||||
else
|
||||
@render_problem()
|
||||
|
||||
problem_link:(problem) ->
|
||||
link = $('<a>').attr('href', "javascript:void(0)").append(
|
||||
"#{problem.problem_name} (#{problem.num_graded} graded, #{problem.num_pending} pending)")
|
||||
.click =>
|
||||
@get_next_submission problem.location
|
||||
|
||||
make_paragraphs: (text) ->
|
||||
paragraph_split = text.split(/\n\s*\n/)
|
||||
new_text = ''
|
||||
for paragraph in paragraph_split
|
||||
new_text += "<p>#{paragraph}</p>"
|
||||
return new_text
|
||||
|
||||
render_list: () ->
|
||||
for problem in @problems
|
||||
@problem_list.append($('<li>').append(@problem_link(problem)))
|
||||
|
||||
render_problem: () ->
|
||||
# make the view elements match the state. Idempotent.
|
||||
show_submit_button = true
|
||||
show_action_button = true
|
||||
|
||||
problem_list_link = $('<a>').attr('href', 'javascript:void(0);')
|
||||
.append("< Back to problem list")
|
||||
.click => @get_problem_list()
|
||||
|
||||
# set up the breadcrumbing
|
||||
@breadcrumbs.append(problem_list_link)
|
||||
|
||||
|
||||
if @state == state_error
|
||||
@set_button_text('Try loading again')
|
||||
show_action_button = true
|
||||
|
||||
else if @state == state_grading
|
||||
@ml_error_info_container.html(@ml_error_info)
|
||||
meta_list = $("<ul>")
|
||||
meta_list.append("<li><span class='meta-info'>Pending - </span> #{@num_pending}</li>")
|
||||
meta_list.append("<li><span class='meta-info'>Graded - </span> #{@num_graded}</li>")
|
||||
meta_list.append("<li><span class='meta-info'>Needed for ML - </span> #{Math.max(@min_for_ml - @num_graded, 0)}</li>")
|
||||
@problem_meta_info.html(meta_list)
|
||||
|
||||
@prompt_container.html(@prompt)
|
||||
@prompt_name_container.html("#{@prompt_name}")
|
||||
@submission_container.html(@make_paragraphs(@submission))
|
||||
@rubric_container.html(@rubric)
|
||||
|
||||
# no submit button until user picks grade.
|
||||
show_submit_button = false
|
||||
show_action_button = false
|
||||
|
||||
@setup_score_selection()
|
||||
|
||||
else if @state == state_graded
|
||||
@set_button_text('Submit')
|
||||
show_action_button = false
|
||||
|
||||
else if @state == state_no_data
|
||||
@message_container.html(@message)
|
||||
@set_button_text('Re-check for submissions')
|
||||
|
||||
else
|
||||
@error('System got into invalid state ' + @state)
|
||||
|
||||
@submit_button.toggle(show_submit_button)
|
||||
@action_button.toggle(show_action_button)
|
||||
|
||||
submit: (event) =>
|
||||
event.preventDefault()
|
||||
|
||||
if @state == state_error
|
||||
@get_next_submission(@location)
|
||||
else if @state == state_graded
|
||||
@submit_and_get_next()
|
||||
else if @state == state_no_data
|
||||
@get_next_submission(@location)
|
||||
else
|
||||
@error('System got into invalid state for submission: ' + @state)
|
||||
|
||||
|
||||
# for now, just create an instance and load it...
|
||||
mock_backend = false
|
||||
ajax_url = $('.staff-grading').data('ajax_url')
|
||||
backend = new StaffGradingBackend(ajax_url, mock_backend)
|
||||
|
||||
$(document).ready(() -> new StaffGrading(backend))
|
||||
45
lms/static/coffee/src/staff_grading/test_grading.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
|
||||
<html> <head>
|
||||
<title></title>
|
||||
<!-- <script src="http://code.jquery.com/jquery-latest.js"></script> -->
|
||||
<script src="../../../admin/js/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="staff_grading.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="staff-grading" data-ajax_url="/some_url/">
|
||||
<h1>Staff grading</h1>
|
||||
|
||||
<div class="error-container">
|
||||
</div>
|
||||
|
||||
<div class="message-container">
|
||||
</div>
|
||||
|
||||
<section class="submission-wrapper">
|
||||
<h3>Submission</h3>
|
||||
<div class="submission-container">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rubric-wrapper">
|
||||
<h3>Rubric</h3>
|
||||
<div class="rubric-container">
|
||||
</div>
|
||||
|
||||
<div class="evaluation">
|
||||
<textarea name="feedback" placeholder="Feedback for student..."
|
||||
class="feedback-area" cols="70" rows="10"></textarea>
|
||||
<p class="score-selection-container">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<div class="submission">
|
||||
<input type="button" value="Submit" class="submit-button" name="show"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body> </html>
|
||||
BIN
lms/static/images/logo-edx-support.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
lms/static/images/press/releases/georgetown-seal_240x180.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 15 KiB |
BIN
lms/static/images/university/georgetown/georgetown.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
lms/static/images/university/ut/ut-rollover_350x150.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
@@ -28,7 +28,7 @@
|
||||
CSRFProtection: function(xhr) {
|
||||
var token = $.cookie('csrftoken');
|
||||
if (token) xhr.setRequestHeader('X-CSRFToken', token);
|
||||
},
|
||||
}
|
||||
}
|
||||
$.ajaxPrefilter(function(options, originalOptions, xhr){ if ( !options.crossDomain ) { form_ext.CSRFProtection(xhr); }});
|
||||
$(document).delegate('form', 'submit', function(e) {
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
@import "course/profile";
|
||||
@import "course/gradebook";
|
||||
@import "course/tabs";
|
||||
@import "course/staff_grading";
|
||||
|
||||
// instructor
|
||||
@import "course/instructor/instructor";
|
||||
|
||||
86
lms/static/sass/course/_staff_grading.scss
Normal file
@@ -0,0 +1,86 @@
|
||||
div.staff-grading {
|
||||
textarea.feedback-area {
|
||||
height: 75px;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
div {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
margin: 10px;
|
||||
padding: 5px;
|
||||
display: inline-block;
|
||||
min-width: 50px;
|
||||
background-color: #CCC;
|
||||
text-size: 1.5em;
|
||||
}
|
||||
|
||||
/* Toggled State */
|
||||
input[type=radio]:checked + label {
|
||||
background: #666;
|
||||
color: white;
|
||||
}
|
||||
|
||||
input[name='score-selection'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
ul
|
||||
{
|
||||
li
|
||||
{
|
||||
margin: 16px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.prompt-information-container,
|
||||
.submission-wrapper,
|
||||
.rubric-wrapper,
|
||||
.grading-container
|
||||
{
|
||||
border: 1px solid gray;
|
||||
padding: 15px;
|
||||
}
|
||||
.error-container
|
||||
{
|
||||
background-color: #FFCCCC;
|
||||
padding: 15px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
.meta-info-wrapper
|
||||
{
|
||||
background-color: #eee;
|
||||
padding:15px;
|
||||
h3
|
||||
{
|
||||
font-size:1em;
|
||||
}
|
||||
ul
|
||||
{
|
||||
list-style-type: none;
|
||||
font-size: .85em;
|
||||
li
|
||||
{
|
||||
margin: 5px 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.message-container
|
||||
{
|
||||
background-color: $yellow;
|
||||
padding: 10px;
|
||||
margin-left:0px;
|
||||
}
|
||||
|
||||
.breadcrumbs
|
||||
{
|
||||
margin-top:20px;
|
||||
margin-left:0px;
|
||||
margin-bottom:5px;
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
padding: 40px;
|
||||
}
|
||||
@@ -222,15 +222,15 @@ div.course-wrapper {
|
||||
|
||||
}
|
||||
|
||||
textarea.short-form-response {
|
||||
height: 200px;
|
||||
padding: 5px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
section.self-assessment {
|
||||
textarea.answer {
|
||||
height: 200px;
|
||||
padding: 5px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
textarea.hint {
|
||||
height: 100px;
|
||||
padding: 5px;
|
||||
|
||||
@@ -154,4 +154,32 @@ header.global ol.user > li.primary a.dropdown {
|
||||
|
||||
.ie-banner {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
div.course-wrapper {
|
||||
display: block !important;
|
||||
|
||||
section.course-content,
|
||||
section.course-index {
|
||||
display: block !important;
|
||||
float: left;
|
||||
}
|
||||
|
||||
section.course-content {
|
||||
width: 71.27%;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
float: left !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.sequence-nav ol {
|
||||
display: block !important;
|
||||
|
||||
li {
|
||||
float: left !important;
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
@@ -196,6 +196,24 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
hr {
|
||||
@extend .faded-hr-divider-light;
|
||||
border: none;
|
||||
margin: 0px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
&::after {
|
||||
@extend .faded-hr-divider;
|
||||
bottom: 0px;
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.partners {
|
||||
margin: 0 auto;
|
||||
padding: 20px 0px;
|
||||
@@ -206,6 +224,7 @@
|
||||
padding: 0px 30px;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
@extend .faded-vertical-divider;
|
||||
@@ -281,7 +300,7 @@
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 160px;
|
||||
max-width: 190px;
|
||||
position: relative;
|
||||
@include transition(all, 0.25s, ease-in-out);
|
||||
vertical-align: middle;
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
color: $base-font-color;
|
||||
font: normal 1em/1.6em $serif;
|
||||
margin: 0px;
|
||||
|
||||
a {
|
||||
font: 1em $serif;
|
||||
}
|
||||
}
|
||||
|
||||
li + li {
|
||||
|
||||
@@ -13,6 +13,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
.courses-listing {
|
||||
@include clearfix();
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
.courses-listing-item {
|
||||
width: flex-grid(4);
|
||||
margin-right: flex-gutter();
|
||||
float: left;
|
||||
|
||||
&:nth-child(3n+3) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.course {
|
||||
background: rgb(250,250,250);
|
||||
border: 1px solid rgb(180,180,180);
|
||||
@@ -24,6 +41,31 @@
|
||||
width: 100%;
|
||||
@include transition(all, 0.15s, linear);
|
||||
|
||||
.status {
|
||||
background: $blue;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
left: 10px;
|
||||
padding: 2px 10px;
|
||||
@include border-radius(2px);
|
||||
position: absolute;
|
||||
text-transform: uppercase;
|
||||
top: -6px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.status:after {
|
||||
border-bottom: 6px solid shade($blue, 50%);
|
||||
border-right: 6px solid transparent;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
top: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
504
lms/static/scripts/boxsizing.htc
Normal file
@@ -0,0 +1,504 @@
|
||||
/**
|
||||
* box-sizing Polyfill
|
||||
*
|
||||
* A polyfill for box-sizing: border-box for IE6 & IE7.
|
||||
*
|
||||
* JScript
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* See <http://www.gnu.org/licenses/lgpl-3.0.txt>
|
||||
*
|
||||
* @category JScript
|
||||
* @package box-sizing-polyfill
|
||||
* @author Christian Schepp Schaefer <schaepp@gmx.de> <http://twitter.com/derSchepp>
|
||||
* @copyright 2012 Christian Schepp Schaefer
|
||||
* @license http://www.gnu.org/copyleft/lesser.html The GNU LESSER GENERAL PUBLIC LICENSE, Version 3.0
|
||||
* @link http://github.com/Schepp/box-sizing-polyfill
|
||||
*
|
||||
* PREFACE:
|
||||
*
|
||||
* This box-sizing polyfill is based on previous work done by Erik Arvidsson,
|
||||
* which he published in 2002 on http://webfx.eae.net/dhtml/boxsizing/boxsizing.html.
|
||||
*
|
||||
* USAGE:
|
||||
*
|
||||
* Add the behavior/HTC after every `box-sizing: border-box;` that you assign:
|
||||
*
|
||||
* box-sizing: border-box;
|
||||
* *behavior: url(/scripts/boxsizing.htc);`
|
||||
*
|
||||
* Prefix the `behavior` property with a star, like seen above, so it will only be seen by
|
||||
* IE6 & IE7, not by IE8+ who already implement box-sizing.
|
||||
*
|
||||
* The URL to the HTC file must be relative to your HTML(!) document, not relative to your CSS.
|
||||
* That's why I'd advise you to use absolute paths like in the example.
|
||||
*
|
||||
*/
|
||||
<component lightWeight="true">
|
||||
<attach event="onpropertychange" onevent="checkPropertyChange()" />
|
||||
<attach event="ondetach" onevent="restore()" />
|
||||
<attach event="onresize" for="window" onevent="update()" />
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
|
||||
var viewportwidth = (typeof window.innerWidth != 'undefined' ? window.innerWidth : element.document.documentElement.clientWidth);
|
||||
|
||||
// Shortcut for the document object
|
||||
var doc = element.document;
|
||||
|
||||
// Buffer for multiple resize events
|
||||
var resizetimeout = null;
|
||||
|
||||
// Don't apply box-sizing to certain elements
|
||||
var apply = false;
|
||||
switch(element.nodeName){
|
||||
case '#comment':
|
||||
case 'HTML':
|
||||
case 'HEAD':
|
||||
case 'TITLE':
|
||||
case 'SCRIPT':
|
||||
case 'STYLE':
|
||||
case 'LINK':
|
||||
case 'META':
|
||||
break;
|
||||
|
||||
default:
|
||||
apply = true;
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* update gets called during resize events, then waits until there are no further resize events, and finally triggers a recalculation
|
||||
*/
|
||||
function update(){
|
||||
if(resizetimeout !== null){
|
||||
window.clearTimeout(resizetimeout);
|
||||
}
|
||||
resizetimeout = window.setTimeout(function(){
|
||||
restore();
|
||||
try {
|
||||
init();
|
||||
}
|
||||
catch (err) {}
|
||||
resizetimeout = null;
|
||||
},100);
|
||||
}
|
||||
|
||||
/*
|
||||
* restore gets called when the behavior is being detached (see event binding at the top),
|
||||
* resets everything like it was before applying the behavior
|
||||
*/
|
||||
function restore(){
|
||||
if(apply){
|
||||
try{
|
||||
element.runtimeStyle.removeAttribute("width");
|
||||
element.runtimeStyle.removeAttribute("height");
|
||||
}
|
||||
catch(e){}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* init gets called once at the start and then never again,
|
||||
* triggers box-sizing calculations and updates width and height
|
||||
*/
|
||||
function init(){
|
||||
if(apply){
|
||||
updateBorderBoxWidth();
|
||||
updateBorderBoxHeight();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* checkPropertyChange gets called as soon as an element property changes
|
||||
* (see event binding at the top), it then checks if any property influencing its
|
||||
* dimensions was changed and if yes recalculates width and height
|
||||
*/
|
||||
function checkPropertyChange(){
|
||||
if(apply){
|
||||
var pn = event.propertyName;
|
||||
if(pn === "style.boxSizing" && element.style.boxSizing === ""){
|
||||
element.style.removeAttribute("boxSizing");
|
||||
element.runtimeStyle.removeAttribute("boxSizing");
|
||||
element.runtimeStyle.removeAttribute("width");
|
||||
element.runtimeStyle.removeAttribute("height");
|
||||
}
|
||||
switch (pn){
|
||||
case "style.width":
|
||||
case "style.minWidth":
|
||||
case "style.maxWidth":
|
||||
case "style.borderLeftWidth":
|
||||
case "style.borderLeftStyle":
|
||||
case "style.borderRightWidth":
|
||||
case "style.borderRightStyle":
|
||||
case "style.paddingLeft":
|
||||
case "style.paddingRight":
|
||||
updateBorderBoxWidth();
|
||||
break;
|
||||
|
||||
case "style.height":
|
||||
case "style.minHeight":
|
||||
case "style.maxHeight":
|
||||
case "style.borderTopWidth":
|
||||
case "style.borderTopStyle":
|
||||
case "style.borderBottomWidth":
|
||||
case "style.borderBottomStyle":
|
||||
case "style.paddingTop":
|
||||
case "style.paddingBottom":
|
||||
updateBorderBoxHeight();
|
||||
break;
|
||||
|
||||
case "className":
|
||||
case "style.boxSizing":
|
||||
updateBorderBoxWidth();
|
||||
updateBorderBoxHeight();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper function, taken from Dean Edward's IE7 framework,
|
||||
* added by Schepp on 12.06.2010.
|
||||
* http://code.google.com/p/ie7-js/
|
||||
*
|
||||
* Allows us to convert from relative to pixel-values.
|
||||
*/
|
||||
function getPixelValue(value){
|
||||
var PIXEL = /^\d+(px)?$/i;
|
||||
if (PIXEL.test(value)) return parseInt(value);
|
||||
var style = element.style.left;
|
||||
var runtimeStyle = element.runtimeStyle.left;
|
||||
element.runtimeStyle.left = element.currentStyle.left;
|
||||
element.style.left = value || 0;
|
||||
value = parseInt(element.style.pixelLeft);
|
||||
element.style.left = style;
|
||||
element.runtimeStyle.left = runtimeStyle;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function getPixelWidth(object, value){
|
||||
// For Pixel Values
|
||||
var PIXEL = /^\d+(px)?$/i;
|
||||
if (PIXEL.test(value)) return parseInt(value);
|
||||
|
||||
// For Percentage Values
|
||||
var PERCENT = /^[\d\.]+%$/i;
|
||||
if (PERCENT.test(value)){
|
||||
try{
|
||||
var parentPaddingLeft = getPixelWidth(object.parentElement,object.parentElement.currentStyle.paddingLeft);
|
||||
var parentPaddingRight = getPixelWidth(object.parentElement,object.parentElement.currentStyle.paddingRight);
|
||||
var parentBorderLeft = getPixelWidth(object.parentElement,object.parentElement.currentStyle.borderLeft);
|
||||
var parentBorderRight = getPixelWidth(object.parentElement,object.parentElement.currentStyle.borderRight);
|
||||
|
||||
//var parentWidth = getPixelWidth(object.parentElement,(object.parentElement.currentStyle.width != "auto" ? object.parentElement.currentStyle.width : "100%"));
|
||||
var parentWidth = object.parentElement.offsetWidth - parentPaddingLeft - parentPaddingRight - parentBorderLeft - parentBorderRight;
|
||||
var value = (parseFloat(value) / 100) * parentWidth;
|
||||
}
|
||||
catch(e){
|
||||
var value = (parseFloat(value) / 100) * element.document.documentElement.clientWidth;
|
||||
}
|
||||
return parseInt(value);
|
||||
}
|
||||
|
||||
// For EM Values
|
||||
var style = object.style.left;
|
||||
var runtimeStyle = object.runtimeStyle.left;
|
||||
object.runtimeStyle.left = object.currentStyle.left;
|
||||
object.style.left = value || 0;
|
||||
value = parseInt(object.style.pixelLeft);
|
||||
object.style.left = style;
|
||||
object.runtimeStyle.left = runtimeStyle;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function getPixelHeight(object, value){
|
||||
// For Pixel Values
|
||||
var PIXEL = /^\d+(px)?$/i;
|
||||
if (PIXEL.test(value)) return parseInt(value);
|
||||
|
||||
// For Percentage Values
|
||||
var PERCENT = /^[\d\.]+%$/i;
|
||||
if (PERCENT.test(value)){
|
||||
try{
|
||||
if(object.parentElement.currentStyle.height != "auto"){
|
||||
switch(object.parentElement.nodeName){
|
||||
default:
|
||||
if(object.parentElement.currentStyle.height !== "auto"){
|
||||
var parentPaddingTop = getPixelWidth(object.parentElement,object.parentElement.currentStyle.paddingTop);
|
||||
var parentPaddingBottom = getPixelWidth(object.parentElement,object.parentElement.currentStyle.paddingBottom);
|
||||
var parentBorderTop = getPixelWidth(object.parentElement,object.parentElement.currentStyle.borderTop);
|
||||
var parentBorderBottom = getPixelWidth(object.parentElement,object.parentElement.currentStyle.borderBottom);
|
||||
|
||||
var parentHeight = object.parentElement.offsetHeight - parentPaddingTop - parentPaddingBottom - parentBorderTop - parentBorderBottom;
|
||||
//var parentHeight = getPixelHeight(object.parentElement,object.parentElement.currentStyle.height);
|
||||
|
||||
value = (parseFloat(value) / 100) * parentHeight;
|
||||
}
|
||||
else {
|
||||
value = "auto";
|
||||
}
|
||||
break;
|
||||
|
||||
case 'HTML':
|
||||
parentHeight = element.document.documentElement.clientHeight;
|
||||
if(parentHeight !== "auto"){
|
||||
value = (parseFloat(value) / 100) * parentHeight;
|
||||
}
|
||||
else {
|
||||
value = "auto";
|
||||
}
|
||||
break;
|
||||
}
|
||||
if(value !== "auto") value = parseInt(value);
|
||||
}
|
||||
else {
|
||||
value = "auto";
|
||||
}
|
||||
}
|
||||
catch(e){
|
||||
value = "auto";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// For EM Values
|
||||
var style = object.style.left;
|
||||
var runtimeStyle = object.runtimeStyle.left;
|
||||
object.runtimeStyle.left = object.currentStyle.left;
|
||||
object.style.left = value || 0;
|
||||
value = parseInt(object.style.pixelLeft);
|
||||
object.style.left = style;
|
||||
object.runtimeStyle.left = runtimeStyle;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* getBorderWidth & friends
|
||||
* Border width getters
|
||||
*/
|
||||
function getBorderWidth(sSide){
|
||||
if(element.currentStyle["border" + sSide + "Style"] == "none"){
|
||||
return 0;
|
||||
}
|
||||
var n = getPixelValue(element.currentStyle["border" + sSide + "Width"]);
|
||||
return n || 0;
|
||||
}
|
||||
function getBorderLeftWidth() { return getBorderWidth("Left"); }
|
||||
function getBorderRightWidth() { return getBorderWidth("Right"); }
|
||||
function getBorderTopWidth() { return getBorderWidth("Top"); }
|
||||
function getBorderBottomWidth() { return getBorderWidth("Bottom"); }
|
||||
|
||||
|
||||
/*
|
||||
* getPadding & friends
|
||||
* Padding width getters
|
||||
*/
|
||||
function getPadding(sSide) {
|
||||
var n = getPixelValue(element.currentStyle["padding" + sSide]);
|
||||
return n || 0;
|
||||
}
|
||||
function getPaddingLeft() { return getPadding("Left"); }
|
||||
function getPaddingRight() { return getPadding("Right"); }
|
||||
function getPaddingTop() { return getPadding("Top"); }
|
||||
function getPaddingBottom() { return getPadding("Bottom"); }
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* getBoxSizing
|
||||
* Get the box-sizing value for the current element
|
||||
*/
|
||||
function getBoxSizing(){
|
||||
var s = element.style;
|
||||
var cs = element.currentStyle
|
||||
if(typeof s.boxSizing != "undefined" && s.boxSizing != ""){
|
||||
return s.boxSizing;
|
||||
}
|
||||
if(typeof s["box-sizing"] != "undefined" && s["box-sizing"] != ""){
|
||||
return s["box-sizing"];
|
||||
}
|
||||
if(typeof cs.boxSizing != "undefined" && cs.boxSizing != ""){
|
||||
return cs.boxSizing;
|
||||
}
|
||||
if(typeof cs["box-sizing"] != "undefined" && cs["box-sizing"] != ""){
|
||||
return cs["box-sizing"];
|
||||
}
|
||||
return getDocumentBoxSizing();
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* getDocumentBoxSizing
|
||||
* Get the default document box sizing (check for quirks mode)
|
||||
*/
|
||||
function getDocumentBoxSizing(){
|
||||
if(doc.compatMode === null || doc.compatMode === "BackCompat"){
|
||||
return "border-box";
|
||||
}
|
||||
return "content-box"
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* setBorderBoxWidth & friends
|
||||
* Width and height setters
|
||||
*/
|
||||
function setBorderBoxWidth(n){
|
||||
element.runtimeStyle.width = Math.max(0, n - getBorderLeftWidth() -
|
||||
getPaddingLeft() - getPaddingRight() - getBorderRightWidth()) + "px";
|
||||
}
|
||||
function setBorderBoxMinWidth(n){
|
||||
element.runtimeStyle.minWidth = Math.max(0, n - getBorderLeftWidth() -
|
||||
getPaddingLeft() - getPaddingRight() - getBorderRightWidth()) + "px";
|
||||
}
|
||||
function setBorderBoxMaxWidth(n){
|
||||
element.runtimeStyle.maxWidth = Math.max(0, n - getBorderLeftWidth() -
|
||||
getPaddingLeft() - getPaddingRight() - getBorderRightWidth()) + "px";
|
||||
}
|
||||
function setBorderBoxHeight(n){
|
||||
element.runtimeStyle.height = Math.max(0, n - getBorderTopWidth() -
|
||||
getPaddingTop() - getPaddingBottom() - getBorderBottomWidth()) + "px";
|
||||
}
|
||||
function setBorderBoxMinHeight(n){
|
||||
element.runtimeStyle.minHeight = Math.max(0, n - getBorderTopWidth() -
|
||||
getPaddingTop() - getPaddingBottom() - getBorderBottomWidth()) + "px";
|
||||
}
|
||||
function setBorderBoxMaxHeight(n){
|
||||
element.runtimeStyle.maxHeight = Math.max(0, n - getBorderTopWidth() -
|
||||
getPaddingTop() - getPaddingBottom() - getBorderBottomWidth()) + "px";
|
||||
}
|
||||
function setContentBoxWidth(n){
|
||||
element.runtimeStyle.width = Math.max(0, n + getBorderLeftWidth() +
|
||||
getPaddingLeft() + getPaddingRight() + getBorderRightWidth()) + "px";
|
||||
}
|
||||
function setContentBoxMinWidth(n){
|
||||
element.runtimeStyle.minWidth = Math.max(0, n + getBorderLeftWidth() +
|
||||
getPaddingLeft() + getPaddingRight() + getBorderRightWidth()) + "px";
|
||||
}
|
||||
function setContentBoxMaxWidth(n){
|
||||
element.runtimeStyle.maxWidth = Math.max(0, n + getBorderLeftWidth() +
|
||||
getPaddingLeft() + getPaddingRight() + getBorderRightWidth()) + "px";
|
||||
}
|
||||
function setContentBoxHeight(n){
|
||||
element.runtimeStyle.height = Math.max(0, n + getBorderTopWidth() +
|
||||
getPaddingTop() + getPaddingBottom() + getBorderBottomWidth()) + "px";
|
||||
}
|
||||
function setContentBoxMinHeight(n){
|
||||
element.runtimeStyle.minHeight = Math.max(0, n + getBorderTopWidth() +
|
||||
getPaddingTop() + getPaddingBottom() + getBorderBottomWidth()) + "px";
|
||||
}
|
||||
function setContentBoxMaxHeight(n){
|
||||
element.runtimeStyle.maxHeight = Math.max(0, n + getBorderTopWidth() +
|
||||
getPaddingTop() + getPaddingBottom() + getBorderBottomWidth()) + "px";
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* updateBorderBoxWidth & updateBorderBoxHeight
|
||||
*
|
||||
*/
|
||||
function updateBorderBoxWidth() {
|
||||
if(getDocumentBoxSizing() == getBoxSizing()){
|
||||
return;
|
||||
}
|
||||
|
||||
var csw = element.currentStyle.width;
|
||||
if(csw != "auto"){
|
||||
csw = getPixelWidth(element,csw);
|
||||
if(getBoxSizing() == "border-box"){
|
||||
setBorderBoxWidth(parseInt(csw));
|
||||
}
|
||||
else{
|
||||
setContentBoxWidth(parseInt(csw));
|
||||
}
|
||||
}
|
||||
|
||||
csw = element.currentStyle.minWidth;
|
||||
if(csw != "none"){
|
||||
csw = getPixelWidth(element,csw);
|
||||
if(getBoxSizing() == "border-box"){
|
||||
setBorderBoxMinWidth(parseInt(csw));
|
||||
}
|
||||
else{
|
||||
setContentBoxMinWidth(parseInt(csw));
|
||||
}
|
||||
}
|
||||
|
||||
csw = element.currentStyle.maxWidth;
|
||||
if(csw != "none"){
|
||||
csw = getPixelWidth(element,csw);
|
||||
if(getBoxSizing() == "border-box"){
|
||||
setBorderBoxMaxWidth(parseInt(csw));
|
||||
}
|
||||
else{
|
||||
setContentBoxMaxWidth(parseInt(csw));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateBorderBoxHeight() {
|
||||
if(getDocumentBoxSizing() == getBoxSizing()){
|
||||
return;
|
||||
}
|
||||
|
||||
var csh = element.currentStyle.height;
|
||||
if(csh != "auto"){
|
||||
csh = getPixelHeight(element,csh);
|
||||
if(csh !== "auto"){
|
||||
if(getBoxSizing() == "border-box"){
|
||||
setBorderBoxHeight(parseInt(csh));
|
||||
}
|
||||
else{
|
||||
setContentBoxHeight(parseInt(csh));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
csh = element.currentStyle.minHeight;
|
||||
if(csh != "none"){
|
||||
csh = getPixelHeight(element,csh);
|
||||
if(csh !== "none"){
|
||||
if(getBoxSizing() == "border-box"){
|
||||
setBorderBoxMinHeight(parseInt(csh));
|
||||
}
|
||||
else{
|
||||
setContentBoxMinHeight(parseInt(csh));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
csh = element.currentStyle.maxHeight;
|
||||
if(csh != "none"){
|
||||
csh = getPixelHeight(element,csh);
|
||||
if(csh !== "none"){
|
||||
if(getBoxSizing() == "border-box"){
|
||||
setBorderBoxMaxHeight(parseInt(csh));
|
||||
}
|
||||
else{
|
||||
setContentBoxMaxHeight(parseInt(csh));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Run the calculations
|
||||
init();
|
||||
|
||||
//]]>
|
||||
</script>
|
||||
</component>
|
||||
92
lms/templates/accounts_login.html
Normal file
@@ -0,0 +1,92 @@
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%inherit file="main.html" />
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
<%block name="headextra">
|
||||
<style type="text/css">
|
||||
.login-box {
|
||||
display: block;
|
||||
position: relative;
|
||||
left: 0;
|
||||
margin: 100px auto;
|
||||
top: 0;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.login-box input[type=submit] {
|
||||
white-space: normal;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
#lean_overlay {
|
||||
display: block;
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
z-index: 100;
|
||||
width:100%;
|
||||
height:100%;
|
||||
}
|
||||
</style>
|
||||
</%block>
|
||||
|
||||
<section id="login-modal" class="modal login-modal login-box">
|
||||
<div class="inner-wrapper">
|
||||
<header>
|
||||
<h2>Log In</h2>
|
||||
<hr>
|
||||
</header>
|
||||
|
||||
<form id="login_form" class="login_form" method="post" data-remote="true" action="/login">
|
||||
<label>E-mail</label>
|
||||
<input name="email" type="email">
|
||||
<label>Password</label>
|
||||
<input name="password" type="password">
|
||||
<label class="remember-me">
|
||||
<input name="remember" type="checkbox" value="true">
|
||||
Remember me
|
||||
</label>
|
||||
<div class="submit">
|
||||
<input name="submit" type="submit" value="Access My Courses">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<section class="login-extra">
|
||||
<p>
|
||||
<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>
|
||||
</p>
|
||||
% if settings.MITX_FEATURES.get('AUTH_USE_OPENID'):
|
||||
<p>
|
||||
<a href="${MITX_ROOT_URL}/openid/login/">login via openid</a>
|
||||
</p>
|
||||
% endif
|
||||
</section>
|
||||
|
||||
<div class="close-modal">
|
||||
<div class="inner">
|
||||
<p>✕</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
$(document).delegate('#login_form', 'ajax:success', function(data, json, xhr) {
|
||||
if(json.success) {
|
||||
next = getParameterByName('next');
|
||||
if(next) {
|
||||
location.href = next;
|
||||
} else {
|
||||
location.href = "${reverse('dashboard')}";
|
||||
}
|
||||
} else {
|
||||
if($('#login_error').length == 0) {
|
||||
$('#login_form').prepend('<div id="login_error" class="modal-form-error"></div>');
|
||||
}
|
||||
$('#login_error').html(json.value).stop().css("display", "block");
|
||||
}
|
||||
});
|
||||
})(this)
|
||||
</script>
|
||||
@@ -5,6 +5,9 @@
|
||||
%>
|
||||
<%page args="course" />
|
||||
<article id="${course.id}" class="course">
|
||||
%if course.metadata.get('is_new'):
|
||||
<span class="status">New</span>
|
||||
%endif
|
||||
<a href="${reverse('about_course', args=[course.id])}">
|
||||
<div class="inner-wrapper">
|
||||
<header class="course-preview">
|
||||
|
||||
@@ -20,21 +20,13 @@
|
||||
## I'm removing this for now since we aren't using it for the fall.
|
||||
## <%include file="course_filter.html" />
|
||||
<section class="courses">
|
||||
<section class='university-column'>
|
||||
%for course in universities['MITx']:
|
||||
<ul class="courses-listing">
|
||||
%for course in courses:
|
||||
<li class="courses-listing-item">
|
||||
<%include file="../course.html" args="course=course" />
|
||||
</li>
|
||||
%endfor
|
||||
</section>
|
||||
<section class='university-column'>
|
||||
%for course in universities['HarvardX']:
|
||||
<%include file="../course.html" args="course=course" />
|
||||
%endfor
|
||||
</section>
|
||||
<section class='university-column last'>
|
||||
%for course in universities['BerkeleyX']:
|
||||
<%include file="../course.html" args="course=course" />
|
||||
%endfor
|
||||
</section>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -6,7 +6,25 @@
|
||||
<link type="text/html" rel="alternate" href="http://blog.edx.org/"/>
|
||||
##<link type="application/atom+xml" rel="self" href="https://github.com/blog.atom"/>
|
||||
<title>EdX Blog</title>
|
||||
<updated>2012-10-14T14:08:12-07:00</updated>
|
||||
<updated>2012-12-19T14:00:12-07:00</updated>
|
||||
<entry>
|
||||
<id>tag:www.edx.org,2012:Post/10</id>
|
||||
<published>2012-12-19T14:00:00-07:00</published>
|
||||
<updated>2012-12-19T14:00:00-07:00</updated>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press/spring-courses')}"/>
|
||||
<title>edX announces first wave of new courses for Spring 2013</title>
|
||||
<content type="html"><img src="${static.url('images/press/releases/edx-logo_240x180.png')}" />
|
||||
<p></p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>tag:www.edx.org,2012:Post/9</id>
|
||||
<published>2012-12-10T14:00:00-07:00</published>
|
||||
<updated>2012-12-10T14:00:00-07:00</updated>
|
||||
<link type="text/html" rel="alternate" href="${reverse('press/georgetown-joins-edx')}"/>
|
||||
<title>Georgetown University joins edX</title>
|
||||
<content type="html"><img src="${static.url('images/press/releases/georgetown-seal_240x180.png')}" />
|
||||
<p>Sixth institution to join global movement in year one</p></content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>tag:www.edx.org,2012:Post/8</id>
|
||||
<published>2012-12-04T14:00:00-07:00</published>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<h2>Explore free courses from <span class="edx">edX</span> universities</h2>
|
||||
|
||||
<section class="university-partners">
|
||||
<ol class="partners">
|
||||
<ol class="partners partners-primary">
|
||||
<li class="partner mit">
|
||||
<a href="${reverse('university_profile', args=['MITx'])}">
|
||||
<img src="${static.url('images/university/mit/mit.png')}" />
|
||||
@@ -65,7 +65,7 @@
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="partner">
|
||||
<li class="partner last">
|
||||
<a href="${reverse('university_profile', args=['BerkeleyX'])}">
|
||||
<img src="${static.url('images/university/berkeley/berkeley.png')}" />
|
||||
<div class="name">
|
||||
@@ -73,42 +73,46 @@
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<hr />
|
||||
|
||||
<ol class="partners">
|
||||
<li class="partner">
|
||||
<a href="${reverse('university_profile', args=['UTx'])}">
|
||||
<img src="${static.url('images/university/ut/ut-rollover_160x90.png')}" />
|
||||
<img src="${static.url('images/university/ut/ut-rollover_350x150.png')}" />
|
||||
<div class="name">
|
||||
<span>UTx</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="partner last">
|
||||
<li class="partner">
|
||||
<a href="${reverse('university_profile', args=['WellesleyX'])}">
|
||||
<img src="${static.url('images/university/wellesley/wellesley-rollover_160x90.png')}" />
|
||||
<img src="${static.url('images/university/wellesley/wellesley-rollover_350x150.png')}" />
|
||||
<div class="name">
|
||||
<span>WellesleyX</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="partner last">
|
||||
<a href="${reverse('university_profile', args=['GeorgetownX'])}">
|
||||
<img src="${static.url('images/university/georgetown/georgetown-rollover_350x150.png')}" />
|
||||
<div class="name">
|
||||
<span>GeorgetownX</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="courses">
|
||||
<section class='university-column'>
|
||||
%for course in universities['MITx']:
|
||||
<%include file="course.html" args="course=course" />
|
||||
<ul class="courses-listing">
|
||||
%for course in courses:
|
||||
<li class="courses-listing-item">
|
||||
<%include file="course.html" args="course=course" />
|
||||
</li>
|
||||
%endfor
|
||||
</section>
|
||||
<section class='university-column'>
|
||||
%for course in universities['HarvardX']:
|
||||
<%include file="course.html" args="course=course" />
|
||||
%endfor
|
||||
</section>
|
||||
<section class='university-column last'>
|
||||
%for course in universities['BerkeleyX']:
|
||||
<%include file="course.html" args="course=course" />
|
||||
%endfor
|
||||
</section>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
90
lms/templates/instructor/staff_grading.html
Normal file
@@ -0,0 +1,90 @@
|
||||
<%inherit file="/main.html" />
|
||||
<%block name="bodyclass">${course.css_class}</%block>
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
|
||||
<%block name="headextra">
|
||||
<%static:css group='course'/>
|
||||
</%block>
|
||||
|
||||
<%block name="title"><title>${course.number} Staff Grading</title></%block>
|
||||
|
||||
<%include file="/courseware/course_navigation.html" args="active_page='staff_grading'" />
|
||||
|
||||
<%block name="js_extra">
|
||||
<%static:js group='staff_grading'/>
|
||||
</%block>
|
||||
|
||||
<section class="container">
|
||||
|
||||
<div class="staff-grading" data-ajax_url="${ajax_url}">
|
||||
<h1>Staff grading</h1>
|
||||
<div class="breadcrumbs">
|
||||
</div>
|
||||
<div class="error-container">
|
||||
</div>
|
||||
<div class="message-container">
|
||||
</div>
|
||||
<section class="problem-list-container">
|
||||
<h2>Instructions</h2>
|
||||
<div class="instructions">
|
||||
<p>This is the list of problems that current need to be graded in order to train the machine learning models. Each problem needs to be trained separately, and we have indicated the number of student submissions that need to be graded in order for a model to be generated. You can grade more than the minimum required number of submissions--this will improve the accuracy of machine learning, though with diminishing returns. You can see the current accuracy of machine learning while grading.</p>
|
||||
</div>
|
||||
|
||||
<h2>Problem List</h2>
|
||||
<ul class="problem-list">
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="prompt-wrapper">
|
||||
<h2 class="prompt-name"></h2>
|
||||
<div class="meta-info-wrapper">
|
||||
<h3>Problem Information</h3>
|
||||
<div class="problem-meta-info-container">
|
||||
</div>
|
||||
<h3>Maching Learning Information</h3>
|
||||
<div class="ml-error-info-container">
|
||||
</div>
|
||||
</div>
|
||||
<div class="prompt-information-container">
|
||||
<h3>Question</h3>
|
||||
<div class="prompt-container">
|
||||
</div>
|
||||
</div>
|
||||
<div class="rubric-wrapper">
|
||||
<h3>Grading Rubric</h3>
|
||||
<div class="rubric-container">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<div class="action-button">
|
||||
<input type=button value="Submit" class="action-button" name="show" />
|
||||
</div>
|
||||
|
||||
<section class="grading-wrapper">
|
||||
<h2>Grading</h2>
|
||||
|
||||
<div class="grading-container">
|
||||
<div class="submission-wrapper">
|
||||
<h3>Student Submission</h3>
|
||||
<div class="submission-container">
|
||||
</div>
|
||||
</div>
|
||||
<div class="evaluation">
|
||||
<p class="score-selection-container">
|
||||
</p>
|
||||
<textarea name="feedback" placeholder="Feedback for student (optional)"
|
||||
class="feedback-area" cols="70" ></textarea>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="submission">
|
||||
<input type="button" value="Submit" class="submit-button" name="show"/>
|
||||
<input type="button" value="Skip" class="skip-button" name="skip"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
12
lms/templates/open_ended_error.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<section>
|
||||
<div class="shortform">
|
||||
<div class="result-errors">
|
||||
There was an error with your submission. Please contact course staff.
|
||||
</div>
|
||||
</div>
|
||||
<div class="longform">
|
||||
<div class="result-errors">
|
||||
${errors}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
16
lms/templates/open_ended_feedback.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<section>
|
||||
<header>Feedback</header>
|
||||
<div class="shortform">
|
||||
<div class="result-output">
|
||||
<p>Score: ${score}</p>
|
||||
% if grader_type == "ML":
|
||||
<p>Check below for full feedback:</p>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="longform">
|
||||
<div class="result-output">
|
||||
${ feedback | n}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<textarea name="answer" class="answer" cols="70" rows="20">${previous_answer|h}</textarea>
|
||||
<textarea name="answer" class="answer short-form-response" cols="70" rows="20">${previous_answer|h}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="rubric-wrapper">${initial_rubric}</div>
|
||||
|
||||
@@ -12,83 +12,71 @@
|
||||
<a href="${reverse('press')}">Press</a>
|
||||
<a href="${reverse('contact')}">Contact</a>
|
||||
</nav>
|
||||
|
||||
|
||||
<section class="faq">
|
||||
<section class="responses">
|
||||
<section id="the-organization" class="category">
|
||||
<h2>Organization</h2>
|
||||
<article class="response">
|
||||
<h3>What is edX?</h3>
|
||||
<p>edX is a not-for-profit enterprise of its founding partners, the Massachusetts Institute of Technology (MIT) and Harvard University that offers online learning to on-campus students and to millions of people around the world. To do so, edX is building an open-source online learning platform and hosts an online web portal at <a href="http://www.edx.org">www.edx.org</a> for online education.</p>
|
||||
<p>EdX currently offers HarvardX, <em>MITx</em> and BerkeleyX classes online for free. Beginning in Summer 2013, edX will also offer UTx (University of Texas) classes online for free. The University of Texas System includes nine universities and six health institutions. The edX institutions aim to extend their collective reach to build a global community of online students. Along with offering online courses, the three universities undertake research on how students learn and how technology can transform learning – both on-campus and online throughout the world.</p>
|
||||
<h3>What is edX?</h3>
|
||||
<p>edX is a not-for-profit enterprise of its founding partners, the Massachusetts Institute of Technology (MIT) and Harvard University that offers online learning to on-campus students and to millions of people around the world. To do so, edX is building an open-source online learning platform and hosts an online web portal at <a href="http://www.edx.org">www.edx.org</a> for online education.</p>
|
||||
<p>EdX currently offers HarvardX, <em>MITx</em> and BerkeleyX classes online for free. Beginning in fall 2013, edX will offer WellesleyX and GeorgetownX classes online for free. The University of Texas System includes nine universities and six health institutions. The edX institutions aim to extend their collective reach to build a global community of online students. Along with offering online courses, the three universities undertake research on how students learn and how technology can transform learning – both on-campus and online throughout the world.</p>
|
||||
</article>
|
||||
|
||||
<article class="response">
|
||||
<h3>Why is Wellesley College joining edX?</h3>
|
||||
<p>Wellesley College brings a long history, nearly 150 years, of providing liberal arts courses of the highest quality. WellesleyX courses, and the creativity and innovation of the Wellesley faculty, will provide a new perspective from which the hundreds of thousands of edX learners can benefit. </p>
|
||||
<p>Wellesley’s unique, highly personalized, discussion-based learning experience and its commitment to providing pedagogical innovation will mesh with ongoing research into how students learn and how technology can transform learning both on-campus and online. </p>
|
||||
<p>As with all consortium members, the values of Wellesley are aligned with those of edX. Wellesley and edX are both committed to expanding access to education to learners of all ages, means, and backgrounds. Both institutions are also committed to the non-profit model.</p>
|
||||
</article>
|
||||
<article class="response">
|
||||
<h3>Wellesley is the first women’s college to offer courses through a massive open online course (MOOC) platform. What does this mean for the world of online learning?</h3>
|
||||
<p>Wellesley is currently the only women’s college that has announced plans to offer courses through a massive open online course (MOOC) platform. Wellesley’s commitment to educating women to be leaders in their fields, their communities, and the world provides a unique opportunity for edX learners who come from virtually every nation around the world. Women who have had limited access to education, regardless of where they live, will have access to the best courses, taught by the best faculty, from the best women’s college in the world. The potential for a life-changing educational experience for women has never been as great.</p>
|
||||
</article>
|
||||
<article class="response">
|
||||
<h3>How many WellesleyX courses will be offered initially? When?</h3>
|
||||
<p>Initially, WellesleyX will begin offering edX courses in the fall of 2013. The courses, which will offer students the opportunity to explore classic liberal arts and sciences as well as other subjects, will be of the same high quality and rigor as those offered on the Wellesley campus.</p>
|
||||
</article>
|
||||
<article class="response">
|
||||
<h3>Will edX be adding additional X Universities?</h3>
|
||||
<p>More than 200 institutions from around the world have expressed interest in collaborating with edX since Harvard and MIT announced its creation in May. EdX is focused above all on quality and developing the best not-for-profit model for online education. In addition to providing online courses on the edX platform, the “X University” Consortium will be a forum in which members can share experiences around online learning. Harvard, MIT, UC Berkeley, the University of Texas system and the other consortium members will work collaboratively to establish the “X University” Consortium, whose membership will expand to include additional “X Universities”. Each member of the consortium will offer courses on the edX platform as an “X University.” The gathering of many universities’ educational content together on one site will enable learners worldwide to access the offered course content of any participating university from a single website, and to use a set of online educational tools shared by all participating universities.</p>
|
||||
<p>edX will actively explore the addition of other institutions from around the world to the edX platform, and looks forward to adding more “X Universities.”</p>
|
||||
<p>More than 200 institutions from around the world have expressed interest in collaborating with edX since Harvard and MIT announced its creation in May. EdX is focused above all on quality and developing the best not-for-profit model for online education. In addition to providing online courses on the edX platform, the "X University" Consortium will be a forum in which members can share experiences around online learning. Harvard, MIT, UC Berkeley, the University of Texas system and the other consortium members will work collaboratively to establish the "X University" Consortium, whose membership will expand to include additional "X Universities". Each member of the consortium will offer courses on the edX platform as an "X University." The gathering of many universities' educational content together on one site will enable learners worldwide to access the offered course content of any participating university from a single website, and to use a set of online educational tools shared by all participating universities.</p>
|
||||
<p>edX will actively explore the addition of other institutions from around the world to the edX platform, and looks forward to adding more "X Universities."</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section id="students" class="category">
|
||||
<h2>Students</h2>
|
||||
<article class="response">
|
||||
<h3>Who can take edX courses? Will there be an admissions process?</h3>
|
||||
<p>EdX will be available to anyone in the world with an internet connection, and in general, there will not be an admissions process.</p>
|
||||
<h3>Who can take edX courses? Will there be an admissions process?</h3>
|
||||
<p>EdX will be available to anyone in the world with an internet connection, and in general, there will not be an admissions process.</p>
|
||||
</article>
|
||||
<article class="response">
|
||||
<h3>Will certificates be awarded?</h3>
|
||||
<p>Yes. Online learners who demonstrate mastery of subjects can earn a certificate of completion. Certificates will be issued by edX under the name of the underlying "X University" from where the course originated, i.e. HarvardX, <em>MITx</em> or BerkeleyX. For the courses in Fall 2012, those certificates will be free. There is a plan to charge a modest fee for certificates in the future.</p>
|
||||
<h3>Will certificates be awarded?</h3>
|
||||
<p>Yes. Online learners who demonstrate mastery of subjects can earn a certificate of completion. Certificates will be issued by edX under the name of the underlying "X University" from where the course originated, i.e. HarvardX, <em>MITx</em> or BerkeleyX. For the courses in Fall 2012, those certificates will be free. There is a plan to charge a modest fee for certificates in the future.</p>
|
||||
</article>
|
||||
<article class="response">
|
||||
<h3>What will the scope of the online courses be? How many? Which faculty?</h3>
|
||||
<p>Our goal is to offer a wide variety of courses across disciplines. There are currently <a href="${reverse('courses')}">seven courses</a> offered for Fall 2012.</p>
|
||||
<h3>What will the scope of the online courses be? How many? Which faculty?</h3>
|
||||
<p>Our goal is to offer a wide variety of courses across disciplines. There are currently <a href="/courses">nine courses</a> offered for Fall 2012.</p>
|
||||
</article>
|
||||
<article class="response">
|
||||
<h3>Who is the learner? Domestic or international? Age range?</h3>
|
||||
<p>Improving teaching and learning for students on our campuses is one of our primary goals. Beyond that, we don’t have a target group of potential learners, as the goal is to make these courses available to anyone in the world – from any demographic – who has interest in advancing their own knowledge. The only requirement is to have a computer with an internet connection. More than 150,000 students from over 160 countries registered for MITx's first course, 6.002x: Circuits and Electronics. The age range of students certified in this course was from 14 to 74 years-old.</p>
|
||||
<h3>Who is the learner? Domestic or international? Age range?</h3>
|
||||
<p>Improving teaching and learning for students on our campuses is one of our primary goals. Beyond that, we don't have a target group of potential learners, as the goal is to make these courses available to anyone in the world - from any demographic - who has interest in advancing their own knowledge. The only requirement is to have a computer with an internet connection. More than 150,000 students from over 160 countries registered for MITx's first course, 6.002x: Circuits and Electronics. The age range of students certified in this course was from 14 to 74 years-old.</p>
|
||||
</article>
|
||||
<article class="response">
|
||||
<h3>Will participating universities’ standards apply to all courses offered on the edX platform?</h3>
|
||||
<p>Yes: the reach changes exponentially, but the rigor remains the same.</p>
|
||||
<h3>Will participating universities' standards apply to all courses offered on the edX platform?</h3>
|
||||
<p>Yes: the reach changes exponentially, but the rigor remains the same.</p>
|
||||
</article>
|
||||
<article class="response">
|
||||
<h3>How do you intend to test whether this approach is improving learning?</h3>
|
||||
<p>Edx institutions have assembled faculty members who will collect and analyze data to assess results and the impact edX is having on learning.</p>
|
||||
<h3>How do you intend to test whether this approach is improving learning?</h3>
|
||||
<p>Edx institutions have assembled faculty members who will collect and analyze data to assess results and the impact edX is having on learning.</p>
|
||||
</article>
|
||||
<article class="response">
|
||||
<h3>How may I apply to study with edX?</h3>
|
||||
<p>Simply complete the online <a href="#signup-modal" rel="leanModal">signup form</a>. Enrolling will create your unique student record in the edX database, allow you to register for classes, and to receive a certificate on successful completion.</p>
|
||||
<h3>How may I apply to study with edX?</h3>
|
||||
<p>Simply complete the online <a href="#signup-modal" rel="leanModal">signup form</a>. Enrolling will create your unique student record in the edX database, allow you to register for classes, and to receive a certificate on successful completion.</p>
|
||||
</article>
|
||||
<article class="response">
|
||||
<h3>How may another university participate in edX? </h3>
|
||||
<p>If you are from a university interested in discussing edX, please email <a href="mailto:university@edx.org">university@edx.org</a></p>
|
||||
<h3>How may another university participate in edX? </h3>
|
||||
<p>If you are from a university interested in discussing edX, please email <a href="mailto:university@edx.org">university@edx.org</a></p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section id="technology-platform" class="category">
|
||||
<h2>Technology Platform</h2>
|
||||
<article class="response">
|
||||
<h3>What technology will edX use?</h3>
|
||||
<p>The edX open-source online learning platform will feature interactive learning designed specifically for the web. Features will include: self-paced learning, online discussion groups, wiki-based collaborative learning, assessment of learning as a student progresses through a course, and online laboratories and other interactive learning tools. The platform will also serve as a laboratory from which data will be gathered to better understand how students learn. Because it is open source, the platform will be continuously improved by a worldwide community of collaborators, with new features added as needs arise.</p>
|
||||
<p>The first version of the technology was used in the first <em>MITx</em> course, 6.002x Circuits and Electronics, which launched in Spring, 2012.</p>
|
||||
<h3>What technology will edX use?</h3>
|
||||
<p>The edX open-source online learning platform will feature interactive learning designed specifically for the web. Features will include: self-paced learning, online discussion groups, wiki-based collaborative learning, assessment of learning as a student progresses through a course, and online laboratories and other interactive learning tools. The platform will also serve as a laboratory from which data will be gathered to better understand how students learn. Because it is open source, the platform will be continuously improved by a worldwide community of collaborators, with new features added as needs arise.</p>
|
||||
<p>The first version of the technology was used in the first <em>MITx</em> course, 6.002x Circuits and Electronics, which launched in Spring, 2012.</p>
|
||||
</article>
|
||||
<article class="response">
|
||||
<h3>How is this different from what other universities are doing online?</h3>
|
||||
<p>EdX is a not-for-profit enterprise built upon the shared educational missions of its founding partners, Harvard University and MIT. The edX platform will be available as open source. Also, a primary goal of edX is to improve teaching and learning on campus by experimenting with blended models of learning and by supporting faculty in conducting significant research on how students learn.</p>
|
||||
<h3>How is this different from what other universities are doing online?</h3>
|
||||
<p>EdX is a not-for-profit enterprise built upon the shared educational missions of its founding partners, Harvard University and MIT. The edX platform will be available as open source. Also, a primary goal of edX is to improve teaching and learning on campus by experimenting with blended models of learning and by supporting faculty in conducting significant research on how students learn.</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
@@ -96,7 +84,6 @@
|
||||
|
||||
<nav class="categories">
|
||||
<a href="#organization">Organization</a>
|
||||
##<a href="#objectives">Objectives</a>
|
||||
<a href="#students">Students</a>
|
||||
<a href="#technology-platform">Technology Platform</a>
|
||||
</nav>
|
||||
@@ -104,5 +91,5 @@
|
||||
</section>
|
||||
|
||||
%if user.is_authenticated():
|
||||
<%include file="../signup_modal.html" />
|
||||
<%include file="../signup_modal.html" />
|
||||
%endif
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
<section class="container jobs">
|
||||
<h1>Do You Want to Change the Future of Education?</h1>
|
||||
<hr class="horizontal-divider">
|
||||
|
||||
<hr class="horizontal-divider"/>
|
||||
|
||||
<section class="message">
|
||||
<div class="inner-wrapper">
|
||||
@@ -27,23 +28,133 @@
|
||||
</header>
|
||||
</div>
|
||||
</section>
|
||||
<hr class="horizontal-divider">
|
||||
|
||||
<hr class="horizontal-divider"/>
|
||||
|
||||
<section class="jobs-wrapper">
|
||||
<section class="jobs-listing">
|
||||
|
||||
<article id="" class="job">
|
||||
<div class="inner-wrapper">
|
||||
<h3>We're hiring! </h3>
|
||||
<p>Are you passionate? Want to help change the world? Good, you've found the right company! We're growing and our team needs the best and brightest in creating the next evolution in interactive online education.</p>
|
||||
<h4>Want to apply to edX?</h4>
|
||||
<p>Send your resume and cover letter to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
|
||||
<p><em>Note:</em> We'll review each and every resume but please note you may not get a response due to the volume of inquiries.</p>
|
||||
<h3>EdX is looking to add new talent to our team! </h3>
|
||||
<p align="center"><em>Our mission is to give a world-class education to everyone, everywhere, regardless of gender, income or social status</em></p>
|
||||
<p>Today, EdX.org, a not-for-profit provides hundreds of thousands of people from around the globe with access free education. We offer amazing quality classes by the best professors from the best schools. We enable our members to uncover a new passion that will transform their lives and their communities.</p>
|
||||
<p>Around the world-from coast to coast, in over 192 countries, people are making the decision to take one or several of our courses. As we continue to grow our operations, we are looking for talented, passionate people with great ideas to join the edX team. We aim to create an environment that is supportive, diverse, and as fun as our brand. If you're results-oriented, dedicated, and ready to contribute to an unparalleled member experience for our community, we really want you to apply.</p>
|
||||
<p>As part of the edX team, you’ll receive:</p>
|
||||
<ul>
|
||||
<li>Competitive compensation</li>
|
||||
<li>Generous benefits package</li>
|
||||
<li>Free lunch every day</li>
|
||||
<li>A great working experience where everyone cares</li>
|
||||
</ul>
|
||||
<p>While we appreciate every applicant's interest, only those under consideration will be contacted. We regret that phone calls will not be accepted.</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article id="instructional-designer" class="job">
|
||||
<div class="inner-wrapper">
|
||||
<h3><strong>INSTRUCTIONAL DESIGNER</strong> — CONTRACT OPPORTUNITY</h3>
|
||||
<p>The Instructional Designer will work collaboratively with the edX content and engineering teams to plan, develop and deliver highly engaging and media rich online courses. The Instructional Designer will be a flexible thinker, able to determine and apply sound pedagogical strategies to unique situations and a diverse set of academic disciplines.</p>
|
||||
<p><strong>Responsibilities:</strong></p>
|
||||
<ul>
|
||||
<li>Work with the video production team, product managers and course staff on the implementation of instructional design approaches in the development of media and other course materials.</li>
|
||||
<li>Based on course staff and faculty input, articulate learning objectives and align them to design strategies and assessments.</li>
|
||||
<li>Develop flipped classroom instructional strategies in coordination with community college faculty. </li>
|
||||
<li>Produce clear and instructionally effective copy, instructional text, and audio and video scripts</li>
|
||||
<li>Identify and deploy instructional design best practices for edX course staff and faculty as needed.</li>
|
||||
<li>Create course communication style guides. Train and coach teaching staff on best practices for communication and discussion management.</li>
|
||||
<li>Serve as a liaison to instructional design teams based at X universities.</li>
|
||||
<li>Consult on peer review processes to be used by learners in selected courses.</li>
|
||||
<li>Ability to apply game-based learning theory and design into selected courses as appropriate.</li>
|
||||
<li>Use learning analytics and metrics to inform course design and revision process.</li>
|
||||
<li>Collaborate with key research and learning sciences stakeholders at edX and partner institutions for the development of best practices for MOOC teaching and learning and course design.</li>
|
||||
<li>Support the development of pilot courses and modules used for sponsored research initiatives.</li>
|
||||
</ul>
|
||||
<p><strong>Qualifications:</strong></p>
|
||||
<ul>
|
||||
<li>Master's Degree in Educational Technology, Instructional Design or related field. Experience in higher education with additional experience in a start-up or research environment preferable.</li>
|
||||
<li>Excellent interpersonal and communication (written and verbal), project management, problem-solving and time management skills. The ability to be flexible with projects and to work on multiple courses essential. Ability to meet deadlines and manage expectations of constituents.</li>
|
||||
<li>Capacity to develop new and relevant technology skills. Experience using game theory design and learning analytics to inform instructional design decisions and strategy.</li>
|
||||
<li>Technical Skills: Video and screencasting experience. LMS Platform experience, xml, HTML, CSS, Adobe Design Suite, Camtasia or Captivate experience. Experience with web 2.0 collaboration tools.</li>
|
||||
</ul>
|
||||
<p>Eligible candidates will be invited to respond to an Instructional Design task based on current or future edX course development needs.</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="member-services-manager" class="job">
|
||||
<div class="inner-wrapper">
|
||||
<h3><strong>MEMBER SERVICES MANAGER </strong></h3>
|
||||
<p>The edX Member Services Manager is responsible for both defining support best practices and directly supporting edX members by handling or routing issues that come in from our websites, email and social media tools. We are looking for a passionate person to help us define and own this experience. While this is a Manager level position, we see this candidate quickly moving through the ranks, leading a larger team of employees over time. This staff member will be running our fast growth support organization.</p>
|
||||
<p><strong>Responsibilities:</strong></p>
|
||||
<ul>
|
||||
<li>Define and rollout leading technology, best practices and policies to support a growing team of member care representatives.</li>
|
||||
<li>Provide reports and visibility into member care metrics.</li>
|
||||
<li>Identify a staffing plan that mirrors growth and work to grow the team with passionate, member-first focused staff.</li>
|
||||
<li>Manage member services staff to predefined service levels. </li>
|
||||
<li>Resolve issues according to edX policies; escalates non-routine issues.</li>
|
||||
<li>Educate members on edX policies and getting started</li>
|
||||
<li>May assist new members with edX procedures and processing registration issues.</li>
|
||||
<li>Provides timely follow-up and resolution to issues.</li>
|
||||
<li>A passion for doing the right thing - at edX the member is always our top priority<br>
|
||||
</li>
|
||||
</ul>
|
||||
<p><strong>Qualifications:</strong></p>
|
||||
<ul>
|
||||
<li>5-8 years in a call center or support team management</li>
|
||||
<li>Exemplary customer service skills</li>
|
||||
<li>Experience in creating and rolling out support/service best practices </li>
|
||||
<li>Solid computer skills – must be fluent with desktop applications and have a basic understanding of web technologies (i.e. basic HTML)</li>
|
||||
<li>Problem solving - the individual identifies and resolves problems in a timely manner, gathers and analyzes information skillfully and maintains confidentiality.</li>
|
||||
<li>Interpersonal skills - the individual maintains confidentiality, remains open to others' ideas and exhibits willingness to try new things.</li>
|
||||
<li>Oral communication - the individual speaks clearly and persuasively in positive or negative situations and demonstrates group presentation skills.</li>
|
||||
<li>Written communication – the individual edits work for spelling and grammar, presents numerical data effectively and is able to read and interpret written information.</li>
|
||||
<li>Adaptability - the individual adapts to changes in the work environment, manages competing demands and is able to deal with frequent change, delays or unexpected events.</li>
|
||||
<li>Dependability - the individual is consistently at work and on time, follows instructions, responds to management direction and solicits feedback to improve performance.</li>
|
||||
<li>College degree</li>
|
||||
</ul>
|
||||
<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="director_of_pr_and_communications" class="job">
|
||||
<div class="inner-wrapper">
|
||||
<h3><strong>DIRECTOR OF PR AND COMMUNICATIONS</strong></h3>
|
||||
<p>The edX Director of PR & Communications is responsible for creating and executing all PR strategy and providing company-wide leadership to help create and refine the edX core messages and identity as the revolutionary global leader in both on-campus and worldwide education. The Director will design and direct a communications program that conveys cohesive and compelling information about edX's mission, activities, personnel and products while establishing a distinct identity for edX as the leader in online education for both students and learning institutions.</p>
|
||||
<p><strong>Responsibilities:</strong></p>
|
||||
<ul>
|
||||
<li>Develop and execute goals and strategy for a comprehensive external and internal communications program focused on driving student engagement around courses and institutional adoption of the edX learning platform.</li>
|
||||
<li>Work with media, either directly or through our agency of record, to establish edX as the industry leader in global learning.</li>
|
||||
<li>Work with key influencers including government officials on a global scale to ensure the edX mission, content and tools are embraced and supported worldwide.</li>
|
||||
<li>Work with marketing colleagues to co-develop and/or monitor and evaluate the content and delivery of all communications messages and collateral.</li>
|
||||
<li>Initiate and/or plan thought leadership events developed to heighten target-audience awareness; participate in meetings and trade shows</li>
|
||||
<li>Conduct periodic research to determine communications benchmarks</li>
|
||||
<li>Inform employees about edX's vision, values, policies, and strategies to enable them to perform their jobs efficiently and drive morale.</li>
|
||||
<li>Work with and manage existing communications team to effectively meet strategic goals.</li>
|
||||
</ul>
|
||||
<p><strong>Qualifications:</strong></p>
|
||||
<ul>
|
||||
<li>Ten years of experience in PR and communications</li>
|
||||
<li>Ability to work creatively and provide company-wide leadership in a fast-paced, dynamic start-up environment required</li>
|
||||
<li>Adaptability - the individual adapts to changes in the work environment, manages competing demands and is able to deal with frequent change, delays or unexpected events.</li>
|
||||
<li>Experience in working in successful consumer-focused startups preferred</li>
|
||||
<li>PR agency experience in setting strategy for complex multichannel, multinational organizations a plus.</li>
|
||||
<li>Extensive writing experience and simply amazing oral, written, and interpersonal communications skills</li>
|
||||
<li>B.A./B.S. in communications or related field</li>
|
||||
</ul>
|
||||
<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>
|
||||
|
||||
</section>
|
||||
|
||||
<section class="jobs-sidebar">
|
||||
<!-- <h2>Positions</h2> -->
|
||||
<!-- <nav> -->
|
||||
<!-- <a href="#content-engineer">EdX Content Engineer</a> -->
|
||||
<!-- </nav> -->
|
||||
<h2>Positions</h2>
|
||||
<nav>
|
||||
<a href="#instructional-designer">Instructional Designer</a>
|
||||
<a href="#member-services-manager">Member Services Manager</a>
|
||||
<a href="#director_of_pr_and_communications">Director of PR & Communications</a>
|
||||
</nav>
|
||||
<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>
|
||||
<h2>Our Location</h2>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%inherit file="../../main.html" />
|
||||
|
||||
<%namespace name='static' file='../../static_content.html'/>
|
||||
|
||||
<%block name="title"><title>Georgetown University joins edX</title></%block>
|
||||
<div id="fb-root"></div>
|
||||
<script>(function(d, s, id) {
|
||||
var js, fjs = d.getElementsByTagName(s)[0];
|
||||
if (d.getElementById(id)) return;
|
||||
js = d.createElement(s); js.id = id;
|
||||
js.src = "//connect.facebook.net/en_US/all.js#xfbml=1";
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
}(document, 'script', 'facebook-jssdk'));</script>
|
||||
|
||||
<section class="pressrelease">
|
||||
|
||||
<section class="container">
|
||||
<h1>Georgetown University joins edX</h1>
|
||||
<hr class="horizontal-divider"/>
|
||||
<article>
|
||||
<h2>Georgetown becomes sixth institution to join global movement in year one, Broadens course options and brings its unique mission-driven perspective to the world of online learning</h2>
|
||||
|
||||
<p><strong>CAMBRIDGE, MA — December 10, 2012</strong> — EdX, the not-for-profit online learning initiative founded by <a href="http://www.harvard.edu">Harvard University</a> and the <a href="http://www.mit.edu">Massachusetts Institute of Technology</a> (MIT), announced today the addition of <a href="http://www.georgetown.edu">Georgetown University</a> to its group of educational leaders who are focused on providing a category-leading, quality higher education experience to the global online community. </p>
|
||||
|
||||
<p>“It is a privilege to partner with edX and this extraordinary collection of universities,” said Dr. John J. DeGioia, President of Georgetown University. “Our Catholic and Jesuit identity compels us to work at the frontiers of excellence in higher education, and we see in this partnership an exciting opportunity to more fully realize this mission. Not only will it enrich our capacity to serve our global family–beyond our campuses here in Washington, D.C.–but it will also allow us to extend the applications of our research and our scholarship.”</p>
|
||||
|
||||
<p>Georgetown University, the nation’s oldest Catholic and Jesuit university, is one of the world’s leading academic and research institutions, offering a unique educational experience that prepares the next generation of global citizens to lead and make a difference in the world. Students receive a world-class learning experience focused on educating the whole person through exposure to different faiths, cultures and beliefs. Georgetown University will provide a series of GeorgetownX courses to the open source platform and broaden the course offerings available on edx.org.</p>
|
||||
|
||||
<p>“We welcome Georgetown University to edX,” said Anant Agarwal, President of edX. “Georgetown has a long history of research and educational excellence, with a demonstrated commitment to the arts and sciences, foreign service, law, medicine, public policy, business, and nursing and health studies. Georgetown, with its distinguished presence around the world including a School of Foreign Service campus in Qatar, shares with edX a global perspective and a mission to expand educational opportunities.”</p>
|
||||
|
||||
<p>Through edX, the “X Universities” will provide interactive education wherever there is access to the Internet. They will enhance teaching and learning through research about how students learn, and how technologies and game-like experiences can facilitate effective teaching both on-campus and online. The University of California, Berkeley joined edX in July, the University of Texas System joined in October, and Wellesley College joined earlier in December. </p>
|
||||
|
||||
<p>“Georgetown University is an excellent addition to edX,” said MIT President L. Rafael Reif. “It brings important strength in many areas of scholarship and has long had an especially powerful voice in public life and discourse. The edX community stands to benefit greatly from what Georgetown will offer.”</p>
|
||||
|
||||
<p>“EdX is an innovation that will expand access to high-quality educational content for millions around the world while helping us better understand how technology can improve the academic experience for students in classrooms across our campuses,” said Harvard President Drew Faust. “Georgetown’s commitment to technology enhanced learning, its excellence in education, and its long history as an institution dedicated to public service make it a welcome addition to edX.”</p>
|
||||
|
||||
<p>GeorgetownX will offer courses on edX beginning in the fall of 2013. All of the courses will be hosted from edX’s innovative platform at <a href="http://www.edx.org">www.edx.org</a>.</p>
|
||||
|
||||
<h2>About edX</h2>
|
||||
|
||||
<p>edX is a not-for-profit enterprise of its founding partners Harvard University and the Massachusetts Institute of Technology that features learning designed specifically for interactive study via the web. Based on a long history of collaboration and their shared educational missions the founders are creating a new online-learning experience. Anant Agarwal, former Director of MIT’s Computer Science and Artificial Intelligence Laboratory, serves as the first president of edX. Along with offering online courses, the institutions will use edX to research how students learn and how technology can transform learning-both on-campus and worldwide. EdX is based in Cambridge, Massachusetts and is governed by MIT and Harvard.</p>
|
||||
|
||||
<h2>About Georgetown University</h2>
|
||||
|
||||
<p>Georgetown University is the oldest Catholic and Jesuit university in America, founded in 1789 by Archbishop John Carroll. Georgetown today is a major student-centered, international, research university offering respected undergraduate, graduate and professional programs from its home in Washington, D.C. For more information about Georgetown University, visit <a href="http://www.georgetown.edu">www.georgetown.edu</a>.</p>
|
||||
|
||||
<section class="contact">
|
||||
<p><strong>Contact: </strong>Brad Baker</p>
|
||||
<p>BBaker@webershandwick.com</p>
|
||||
<p>617-520-7043</p>
|
||||
<br/>
|
||||
</section>
|
||||
|
||||
<section class="footer">
|
||||
<hr class="horizontal-divider"/>
|
||||
<div class="logo"></div><h3 class="date">12 - 10 - 2012</h3>
|
||||
<div class="social-sharing">
|
||||
<hr class="horizontal-divider"/>
|
||||
<p>Share with friends and family:</p>
|
||||
<a href="http://twitter.com/intent/tweet?text=Georgetown+becomes+sixth+institution+to+edX.+Broadens+course+options+and+brings+its+unique+mission-driven+perspective+to+the+world+of+online+learning:+http://www.edx.org/press/georgetown-joins-edx" class="share">
|
||||
<img src="${static.url('images/social/twitter-sharing.png')}"/>
|
||||
</a>
|
||||
|
||||
<a href="mailto:?subject=Georgetown%20University%20joins%20edX&body=Georgetown%20becomes%20sixth%20institution%20to%20edX.%20Broadens%20course%20options%20and%20brings%20its%20unique%20mission-driven%20perspective%20to%20the%20world%20of%20online%20learning…http://edx.org/press/georgetown-joins-edx" class="share">
|
||||
<img src="${static.url('images/social/email-sharing.png')}"/>
|
||||
</a>
|
||||
<div class="fb-like" data-href="http://edx.org/press/georgetown-joins-edx" data-send="true" data-width="450" data-show-faces="true"></div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</section>
|
||||
</section>
|
||||
@@ -0,0 +1,75 @@
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%inherit file="../../main.html" />
|
||||
|
||||
<%namespace name='static' file='../../static_content.html'/>
|
||||
|
||||
<%block name="title"><title>EdX expands platform, announces first wave of courses for spring 2013</title></%block>
|
||||
<div id="fb-root"></div>
|
||||
<script>(function(d, s, id) {
|
||||
var js, fjs = d.getElementsByTagName(s)[0];
|
||||
if (d.getElementById(id)) return;
|
||||
js = d.createElement(s); js.id = id;
|
||||
js.src = "//connect.facebook.net/en_US/all.js#xfbml=1";
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
}(document, 'script', 'facebook-jssdk'));</script>
|
||||
|
||||
<section class="pressrelease">
|
||||
<section class="container">
|
||||
<h1>EdX expands platform, announces first wave of courses for spring 2013</h1>
|
||||
<hr class="horizontal-divider">
|
||||
|
||||
<article>
|
||||
<h2>Leading minds from top universities to offer world-wide MOOC courses on statistics, history, justice, and poverty</h2>
|
||||
|
||||
<p><strong>CAMBRIDGE, MA – December 19, 2012</strong> —EdX, the not-for-profit online learning initiative founded by <a href="http://www.harvard.edu">Harvard University</a> and the <a href="http://www.mit.edu">Massachusetts Institute of Technology</a> (MIT), announced today its initial spring 2013 schedule including its first set of courses in the humanities and social sciences – introductory courses with wide, global appeal. In its second semester, edX expands its online courses to a variety of subjects ranging from the ancient Greek hero to the riddle of world poverty, all taught by experts at some of the world’s leading universities. EdX is also bringing back several courses from its popular offerings in the fall semester.</p>
|
||||
|
||||
<p>“EdX is both revolutionizing and democratizing education,” said Anant Agarwal, President of edX. “In just eight months we’ve attracted more than half a million unique users from around the world to our learning portal. Now, with these spring courses we are entering a new era – and are poised to touch millions of lives with the best courses from the best faculty at the best institutions in the world.”</p>
|
||||
|
||||
<p>Building on the success of its initial offerings, edX is broadening the courses on its innovative educational platform. In its second semester – now open for registration – edX continues with courses from some of the world’s most esteemed faculty from <a href="http://www.berkeley.edu">UC Berkeley</a>, <a href="http://www.harvard.edu">Harvard</a> and <a href="http://www.mit.edu">MIT</a>. Spring 2013 courses include:</p>
|
||||
|
||||
<ul>
|
||||
<li><a href="http://www.edx.org/courses/HarvardX/ER22x/2013_Spring/about">Justice from Michael Sandel</a>, the Harvard political philosopher whose online lectures have become a global sensation, and inspired millions to think critically about the moral and civic dilemmas facing their societies.</li>
|
||||
<li><a href="http://www.edx.org/courses/BerkeleyX/Stat2.1x/2013_Spring/about">Introduction to Statistics from Ani Adhikari</a>, the UC Berkeley lecturer in statistics and recipient of UC Berkeley’s Distinguished Teaching Award.</li>
|
||||
<li><a href="http://www.edx.org/courses/MITx/14.73x/2013_Spring/about">The Challenges of Global Poverty from Esther Duflo</a>, the MIT economist who has led a comprehensive evaluation of the roots of poverty in developing nations.</li>
|
||||
<li><a href="http://www.edx.org/courses/HarvardX/CB22x/2013_Spring/about">The Ancient Greek Hero from Gregory Nagy</a>, the professor of ancient Greek literature at Harvard who specializes in the linguistic analysis of epic and tragedy as performed in their historical contexts.</li>
|
||||
<li><a href="http://www.edx.org/courses/BerkeleyX/CS191x/2013_Spring/about">Quantum Mechanics and Quantum Computation from Umesh Vazirani</a>, the UC Berkeley computer scientist whose work has helped change our understanding of the relationship between information and quantum physics.</li>
|
||||
<li><a href="http://www.edx.org/courses/HarvardX/PH278x/2013_Spring/about">Human Health and Global Environmental Change</a> from Harvard's Center for Health and the Global Environment and Aaron Bernstein, a physician who studies why climate change, biodiversity loss, and other planetary scale environmental changes matter to our health and what needs to be done to remedy them.</li>
|
||||
</ul>
|
||||
|
||||
<p>“I'm delighted to have my Justice course on edX,” said Michael Sandel, Ann T. and Robert M. Bass Professor of Government at Harvard University, “where students everywhere will be able to engage in a global dialogue about the big moral and civic questions of our time.”</p>
|
||||
|
||||
<p>In addition to these new courses, edX is bringing back several courses from the popular fall 2012 semester: <a href="http://www.edx.org/courses/MITx/6.00x/2013_Spring/about">Introduction to Computer Science and Programming</a>; <a href="http://www.edx.org/courses/MITx/3.091x/2013_Spring/about">Introduction to Solid State Chemistry</a>; <a href="http://www.edx.org/courses/BerkeleyX/CS188.1x/2013_Spring/about">Introduction to Artificial Intelligence</a>; <a href="http://www.edx.org/courses/BerkeleyX/CS169.1x/2013_Spring/about">Software as a Service I</a>; <a href="http://www.edx.org/courses/BerkeleyX/CS169.2x/2013_Spring/about">Software as a Service II</a>; <a href="http://www.edx.org/courses/BerkeleyX/CS184.1x/2013_Spring/about">Foundations of Computer Graphics</a>.</p>
|
||||
|
||||
<p>This spring also features Harvard's <a href="http://www.edx.org/courses/HarvardX/HLS1x/2013_Spring/about">Copyright</a>, taught by Harvard Law School professor William Fisher III, former law clerk to Justice Thurgood Marshall and expert on the hotly debated U.S. copyright system, which will explore the current law of copyright and the ongoing debates concerning how that law should be reformed. <em>Copyright</em> will be offered as an experimental course, taking advantage of different combinations and uses of teaching materials, educational technologies, and the edX platform. 500 learners will be selected through an open application process that will run through January 3rd 2013.</p>
|
||||
|
||||
<p>These new courses would not be possible without the contributions of key edX institutions, including UC Berkeley, which is the inaugural chair of the “X University” consortium and major contributor to the platform. All of the courses will be hosted on edX’s innovative platform at <a href="http://www.edx.org">www.edx.org</a> and are open for registration as of today. EdX expects to announce a second set of spring 2013 courses in the future.</p>
|
||||
|
||||
<h2>About edX</h2>
|
||||
|
||||
<p>EdX is a not-for-profit enterprise of its founding partners Harvard University and the Massachusetts Institute of Technology focused on transforming online and on-campus learning through groundbreaking methodologies, game-like experiences and cutting-edge research. EdX provides inspirational and transformative knowledge to students of all ages, social status, and income who form worldwide communities of learners. EdX uses its open source technology to transcend physical and social borders. We’re focused on people, not profit. EdX is based in Cambridge, Massachusetts in the USA.</p>
|
||||
|
||||
<section class="contact">
|
||||
<p><strong>Contact: </strong>Brad Baker</p>
|
||||
<p>BBaker@webershandwick.com</p>
|
||||
<p>617-520-7260</p>
|
||||
</section>
|
||||
|
||||
<section class="footer">
|
||||
<hr class="horizontal-divider">
|
||||
<div class="logo"></div><h3 class="date">12 - 19 - 2012</h3>
|
||||
<div class="social-sharing">
|
||||
<hr class="horizontal-divider">
|
||||
<p>Share with friends and family:</p>
|
||||
<a href="http://twitter.com/intent/tweet?text=EdX+expands+platform,+announces+first+wave+of+courses+for+spring+2013:+http://www.edx.org/press/spring-courses" class="share">
|
||||
<img src="${static.url('images/social/twitter-sharing.png')}">
|
||||
</a>
|
||||
</a>
|
||||
<a href="mailto:?subject=EdX%20expands%20platform,%20announces%20first%20wave%20of%20courses%20for%20spring%202013&body=Leading%20minds%20from%20top%20universities%20to%20offer%20world-wide%20MOOC%20courses%20on%20statistics,%20history,%20justice,%20and%20poverty…http://edx.org/press/spring-courses" class="share">
|
||||
<img src="${static.url('images/social/email-sharing.png')}">
|
||||
</a>
|
||||
<div class="fb-like" data-href="http://edx.org/press/spring-courses" data-send="true" data-width="450" data-show-faces="true"></div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</section>
|
||||
</section>
|
||||
24
lms/templates/university_profile/georgetownx.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<%inherit file="base.html" />
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<%block name="title"><title>GeorgetownX</title></%block>
|
||||
|
||||
<%block name="university_header">
|
||||
<header class="search" style="background: url('/static/images/university/georgetown/georgetown-cover_2025x550.jpg')">
|
||||
<div class="inner-wrapper university-search">
|
||||
<hgroup>
|
||||
<div class="logo">
|
||||
<img src="${static.url('images/university/georgetown/georgetown.png')}" />
|
||||
</div>
|
||||
<h1>GeorgetownX</h1>
|
||||
</hgroup>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
</%block>
|
||||
|
||||
<%block name="university_description">
|
||||
<p>Georgetown University, the nation’s oldest Catholic and Jesuit university, is one of the world’s leading academic and research institutions, offering a unique educational experience that prepares the next generation of global citizens to lead and make a difference in the world. Students receive a world-class learning experience focused on educating the whole person through exposure to different faiths, cultures and beliefs.</p>
|
||||
</%block>
|
||||
|
||||
${parent.body()}
|
||||
@@ -2,7 +2,9 @@
|
||||
<h2> ${display_name} </h2>
|
||||
% endif
|
||||
|
||||
<div id="video_${id}" class="video" data-streams="${streams}" data-caption-data-dir="${data_dir}" data-caption-asset-path="${caption_asset_path}" data-show-captions="${show_captions}">
|
||||
<div id="video_${id}" class="video" data-streams="${streams}"
|
||||
data-caption-data-dir="${data_dir}" data-show-captions="${show_captions}"
|
||||
data-start="${start}" data-end="${end}" data-caption-asset-path="${caption_asset_path}" >
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
<section class="video-player">
|
||||
|
||||
22
lms/urls.py
@@ -37,6 +37,8 @@ urlpatterns = ('',
|
||||
url(r'^event$', 'track.views.user_track'),
|
||||
url(r'^t/(?P<template>[^/]*)$', 'static_template_view.views.index'), # TODO: Is this used anymore? What is STATIC_GRAB?
|
||||
|
||||
url(r'^accounts/login$', 'student.views.accounts_login', name="accounts_login"),
|
||||
|
||||
url(r'^login$', 'student.views.login_user', name="login"),
|
||||
url(r'^login/(?P<error>[^/]*)$', 'student.views.login_user'),
|
||||
url(r'^logout$', 'student.views.logout_user', name='logout'),
|
||||
@@ -62,6 +64,7 @@ urlpatterns = ('',
|
||||
|
||||
url(r'^university_profile/UTx$', 'courseware.views.static_university_profile', name="static_university_profile", kwargs={'org_id':'UTx'}),
|
||||
url(r'^university_profile/WellesleyX$', 'courseware.views.static_university_profile', name="static_university_profile", kwargs={'org_id':'WellesleyX'}),
|
||||
url(r'^university_profile/GeorgetownX$', 'courseware.views.static_university_profile', name="static_university_profile", kwargs={'org_id':'GeorgetownX'}),
|
||||
url(r'^university_profile/(?P<org_id>[^/]+)$', 'courseware.views.university_profile', name="university_profile"),
|
||||
|
||||
#Semi-static views (these need to be rendered and have the login bar, but don't change)
|
||||
@@ -106,10 +109,13 @@ urlpatterns = ('',
|
||||
{'template': 'press_releases/Gates_Foundation_announcement.html'}, name="press/gates-foundation-announcement"),
|
||||
url(r'^press/wellesley-college-joins-edx$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/Wellesley_College_joins_edX.html'}, name="press/wellesley-college-joins-edx"),
|
||||
|
||||
url(r'^press/georgetown-joins-edx$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/Georgetown_joins_edX.html'}, name="press/georgetown-joins-edx"),
|
||||
url(r'^press/spring-courses$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/Spring_2013_course_announcements.html'}, name="press/spring-courses"),
|
||||
|
||||
# Should this always update to point to the latest press release?
|
||||
(r'^pressrelease$', 'django.views.generic.simple.redirect_to', {'url': '/press/wellesley-college-joins-edx'}),
|
||||
(r'^pressrelease$', 'django.views.generic.simple.redirect_to', {'url': '/press/spring-courses'}),
|
||||
|
||||
|
||||
(r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/static/images/favicon.ico'}),
|
||||
@@ -165,7 +171,7 @@ if settings.COURSEWARE_ENABLED:
|
||||
# input types system so that previews can be context-specific.
|
||||
# Unfortunately, we don't have time to think through the right way to do
|
||||
# that (and implement it), and it's not a terrible thing to provide a
|
||||
# generic chemican-equation rendering service.
|
||||
# generic chemical-equation rendering service.
|
||||
url(r'^preview/chemcalc', 'courseware.module_render.preview_chemcalc',
|
||||
name='preview_chemcalc'),
|
||||
|
||||
@@ -234,6 +240,16 @@ if settings.COURSEWARE_ENABLED:
|
||||
'instructor.views.grade_summary', name='grade_summary'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/enroll_students$',
|
||||
'instructor.views.enroll_students', name='enroll_students'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading$',
|
||||
'instructor.views.staff_grading', name='staff_grading'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading/get_next$',
|
||||
'instructor.staff_grading_service.get_next', name='staff_grading_get_next'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading/save_grade$',
|
||||
'instructor.staff_grading_service.save_grade', name='staff_grading_save_grade'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading/save_grade$',
|
||||
'instructor.staff_grading_service.save_grade', name='staff_grading_save_grade'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading/get_problem_list$',
|
||||
'instructor.staff_grading_service.get_problem_list', name='staff_grading_get_problem_list'),
|
||||
)
|
||||
|
||||
# discussion forums live within courseware, so courseware must be enabled first
|
||||
|
||||
18
rakefile
@@ -268,7 +268,7 @@ Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib|
|
||||
TEST_TASK_DIRS << lib
|
||||
|
||||
desc "Run tests for common lib #{lib} (without coverage)"
|
||||
task "fasttest_#{lib}" do
|
||||
task "fasttest_#{lib}" do
|
||||
sh("nosetests #{lib}")
|
||||
end
|
||||
|
||||
@@ -438,16 +438,22 @@ task :builddocs do
|
||||
end
|
||||
end
|
||||
|
||||
desc "Show doc in browser (mac only for now) TODO add linux support"
|
||||
desc "Show docs in browser (mac and ubuntu)."
|
||||
task :showdocs do
|
||||
Dir.chdir('docs/build/html') do
|
||||
sh('open index.html')
|
||||
if RUBY_PLATFORM.include? 'darwin' # mac os
|
||||
sh('open index.html')
|
||||
elsif RUBY_PLATFORM.include? 'linux' # make more ubuntu specific?
|
||||
sh('sensible-browser index.html') # ubuntu
|
||||
else
|
||||
raise "\nUndefined how to run browser on your machine.
|
||||
Please use 'rake builddocs' and then manually open
|
||||
'mitx/docs/build/html/index.html."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "Build docs and show them in browser"
|
||||
task :doc => :builddocs do
|
||||
Dir.chdir('docs/build/html') do
|
||||
sh('open index.html')
|
||||
end
|
||||
Rake::Task["showdocs"].invoke
|
||||
end
|
||||
|
||||
100
requirements.txt
@@ -1,59 +1,61 @@
|
||||
django>=1.4,<1.5
|
||||
django==1.4.3
|
||||
pip
|
||||
numpy
|
||||
scipy
|
||||
Markdown<2.3.0
|
||||
pygments
|
||||
lxml
|
||||
boto
|
||||
mako
|
||||
python-memcached
|
||||
python-openid
|
||||
numpy==1.6.2
|
||||
scipy==0.11.0
|
||||
Markdown==2.2.1
|
||||
pygments==1.5
|
||||
lxml==3.0.1
|
||||
boto==2.6.0
|
||||
mako==0.7.3
|
||||
python-memcached==1.48
|
||||
python-openid==2.2.5
|
||||
path.py
|
||||
django_debug_toolbar
|
||||
fs
|
||||
beautifulsoup
|
||||
beautifulsoup4
|
||||
feedparser
|
||||
requests
|
||||
fs==0.4.0
|
||||
beautifulsoup==3.2.1
|
||||
beautifulsoup4==4.1.3
|
||||
feedparser==5.1.3
|
||||
requests==0.14.2
|
||||
http://sympy.googlecode.com/files/sympy-0.7.1.tar.gz
|
||||
newrelic
|
||||
glob2
|
||||
pymongo
|
||||
newrelic==1.8.0.13
|
||||
glob2==0.3
|
||||
pymongo==2.4.1
|
||||
django_nose
|
||||
nosexcover
|
||||
rednose
|
||||
GitPython >= 0.3
|
||||
django-override-settings
|
||||
mock>=0.8, <0.9
|
||||
PyYAML
|
||||
South
|
||||
pytz
|
||||
django-celery
|
||||
django-countries
|
||||
django-kombu
|
||||
django-followit
|
||||
django-jasmine
|
||||
django-keyedcache
|
||||
django-mako
|
||||
django-masquerade
|
||||
django-openid-auth
|
||||
django-robots
|
||||
django-ses
|
||||
django-storages
|
||||
django-threaded-multihost
|
||||
django-sekizai<0.7
|
||||
django-mptt>=0.5.3
|
||||
sorl-thumbnail
|
||||
networkx
|
||||
pygraphviz
|
||||
nosexcover==1.0.7
|
||||
rednose==0.3.3
|
||||
GitPython==0.3.2.RC1
|
||||
django-override-settings==1.2
|
||||
mock==0.8.0
|
||||
PyYAML==3.10
|
||||
South==0.7.6
|
||||
pytz==2012h
|
||||
django-celery==3.0.11
|
||||
django-countries==1.5
|
||||
django-kombu==0.9.4
|
||||
django-followit==0.0.3
|
||||
django-jasmine==0.3.2
|
||||
django-keyedcache==1.4-6
|
||||
django-mako==0.1.5pre
|
||||
django-masquerade==0.1.6
|
||||
django-openid-auth==0.4
|
||||
django-robots==0.9.1
|
||||
django-ses==0.4.1
|
||||
django-storages==1.1.5
|
||||
django-threaded-multihost==1.4-1
|
||||
django-sekizai==0.6.1
|
||||
django-mptt==0.5.5
|
||||
sorl-thumbnail==11.12
|
||||
networkx==1.7
|
||||
pygraphviz==1.1
|
||||
-r repo-requirements.txt
|
||||
pil
|
||||
nltk
|
||||
pil==1.1.7
|
||||
nltk==2.0.4
|
||||
django-debug-toolbar-mongo
|
||||
dogstatsd-python
|
||||
dogstatsd-python==0.2.1
|
||||
# Taking out MySQL-python for now because it requires mysql to be installed, so breaks updates on content folks' envs.
|
||||
# MySQL-python
|
||||
sphinx
|
||||
sphinx==1.1.3
|
||||
factory_boy
|
||||
Shapely
|
||||
Shapely==1.2.16
|
||||
ipython==0.13.1
|
||||
|
||||
|
||||