Merge branch 'master' into cale/circular-import-fix

Conflicts:
	common/djangoapps/student/models.py
This commit is contained in:
David Ormsbee
2013-01-15 11:35:19 -05:00
97 changed files with 9210 additions and 1367 deletions

View File

@@ -12,6 +12,8 @@ admin.site.register(UserTestGroup)
admin.site.register(CourseEnrollment)
admin.site.register(CourseEnrollmentAllowed)
admin.site.register(Registration)
admin.site.register(PendingNameChange)

View File

@@ -0,0 +1,155 @@
# -*- 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 'CourseEnrollmentAllowed'
db.create_table('student_courseenrollmentallowed', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('email', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, db_index=True, blank=True)),
))
db.send_create_signal('student', ['CourseEnrollmentAllowed'])
# Adding unique constraint on 'CourseEnrollmentAllowed', fields ['email', 'course_id']
db.create_unique('student_courseenrollmentallowed', ['email', 'course_id'])
def backwards(self, orm):
# Removing unique constraint on 'CourseEnrollmentAllowed', fields ['email', 'course_id']
db.delete_unique('student_courseenrollmentallowed', ['email', 'course_id'])
# Deleting model 'CourseEnrollmentAllowed'
db.delete_table('student_courseenrollmentallowed')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'student.courseenrollment': {
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.courseenrollmentallowed': {
'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'student.pendingemailchange': {
'Meta': {'object_name': 'PendingEmailChange'},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.pendingnamechange': {
'Meta': {'object_name': 'PendingNameChange'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.registration': {
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.testcenteruser': {
'Meta': {'object_name': 'TestCenterUser'},
'address_1': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'address_2': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}),
'address_3': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}),
'candidate_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}),
'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'client_candidate_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'company_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'country': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'extension': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '8', 'blank': 'True'}),
'fax': ('django.db.models.fields.CharField', [], {'max_length': '35', 'blank': 'True'}),
'fax_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'middle_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'phone': ('django.db.models.fields.CharField', [], {'max_length': '35'}),
'phone_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}),
'postal_code': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}),
'salutation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}),
'suffix': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'unique': 'True'}),
'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'})
},
'student.userprofile': {
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}),
'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
},
'student.usertestgroup': {
'Meta': {'object_name': 'UserTestGroup'},
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
}
}
complete_apps = ['student']

View File

@@ -261,6 +261,23 @@ class CourseEnrollment(models.Model):
return "[CourseEnrollment] %s: %s (%s)" % (self.user, self.course_id, self.created)
class CourseEnrollmentAllowed(models.Model):
"""
Table of users (specified by email address strings) who are allowed to enroll in a specified course.
The user may or may not (yet) exist. Enrollment by users listed in this table is allowed
even if the enrollment time window is past.
"""
email = models.CharField(max_length=255, db_index=True)
course_id = models.CharField(max_length=255, db_index=True)
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
class Meta:
unique_together = (('email', 'course_id'), )
def __unicode__(self):
return "[CourseEnrollmentAllowed] %s: %s (%s)" % (self.email, self.course_id, self.created)
#cache_relation(User.profile)
#### Helper methods for use from python manage.py shell.

View File

@@ -78,7 +78,7 @@ def index(request, extra_context={}, user=None):
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']
key = lambda course: course.days_until_start
courses = sorted(courses, key=key, reverse=True)
# Get the 3 most recent news

View File

@@ -186,24 +186,6 @@ class LoncapaProblem(object):
maxscore += responder.get_max_score()
return maxscore
def message_post(self,event_info):
"""
Handle an ajax post that contains feedback on feedback
Returns a boolean success variable
Note: This only allows for feedback to be posted back to the grading controller for the first
open ended response problem on each page. Multiple problems will cause some sync issues.
TODO: Handle multiple problems on one page sync issues.
"""
success=False
message = "Could not find a valid responder."
log.debug("in lcp")
for responder in self.responders.values():
if hasattr(responder, 'handle_message_post'):
success, message = responder.handle_message_post(event_info)
if success:
break
return success, message
def get_score(self):
"""
Compute score for this problem. The score is the number of points awarded.

View File

@@ -735,51 +735,3 @@ class ChemicalEquationInput(InputTypeBase):
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)
#-----------------------------------------------------------------------------

View File

@@ -1815,436 +1815,6 @@ 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'
DEFAULT_MESSAGE_QUEUE = 'open-ended-message'
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)
self.message_queue_name = xml.get('message-queuename', self.DEFAULT_MESSAGE_QUEUE)
# The openendedparam tag encapsulates all grader settings
oeparam = self.xml.find('openendedparam')
prompt = self.xml.find('prompt')
rubric = self.xml.find('openendedrubric')
#This is needed to attach feedback to specific responses later
self.submission_id=None
self.grader_id=None
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)
self.initial_display = find_with_default(oeparam, 'initial_display', '')
self.answer = find_with_default(oeparam, 'answer_display', 'No answer given.')
parsed_grader_payload.update({
'location' : self.system.location,
'course_id' : self.system.course_id,
'prompt' : prompt_string,
'rubric' : rubric_string,
'initial_display' : self.initial_display,
'answer' : self.answer,
})
updated_grader_payload = json.dumps(parsed_grader_payload)
self.payload = {'grader_payload': updated_grader_payload}
try:
self.max_score = int(find_with_default(oeparam, 'max_score', 1))
except ValueError:
self.max_score = 1
def handle_message_post(self,event_info):
"""
Handles a student message post (a reaction to the grade they received from an open ended grader type)
Returns a boolean success/fail and an error message
"""
survey_responses=event_info['survey_responses']
for tag in ['feedback', 'submission_id', 'grader_id', 'score']:
if tag not in survey_responses:
return False, "Could not find needed tag {0}".format(tag)
try:
submission_id=int(survey_responses['submission_id'])
grader_id = int(survey_responses['grader_id'])
feedback = str(survey_responses['feedback'].encode('ascii', 'ignore'))
score = int(survey_responses['score'])
except:
error_message=("Could not parse submission id, grader id, "
"or feedback from message_post ajax call. Here is the message data: {0}".format(survey_responses))
log.exception(error_message)
return False, "There was an error saving your feedback. Please contact course staff."
qinterface = self.system.xqueue['interface']
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
anonymous_student_id = self.system.anonymous_student_id
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.message_queue_name
)
student_info = {'anonymous_student_id': anonymous_student_id,
'submission_time': qtime,
}
contents= {
'feedback' : feedback,
'submission_id' : submission_id,
'grader_id' : grader_id,
'score': score,
'student_info' : json.dumps(student_info),
}
(error, msg) = qinterface.send_to_queue(header=xheader,
body=json.dumps(contents))
#Convert error to a success value
success=True
if error:
success=False
return success, "Successfully submitted your feedback."
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('&nbsp;', '&#160;'), 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 encode_values(feedback_type,value):
feedback_type=str(feedback_type).encode('ascii', 'ignore')
if not isinstance(value,basestring):
value=str(value)
value=value.encode('ascii', 'ignore')
return feedback_type,value
def format_feedback(feedback_type, value):
feedback_type,value=encode_values(feedback_type,value)
feedback= """
<div class="{feedback_type}">
{value}
</div>
""".format(feedback_type=feedback_type, value=value)
return feedback
def format_feedback_hidden(feedback_type , value):
feedback_type,value=encode_values(feedback_type,value)
feedback = """
<div class="{feedback_type}" style="display: none;">
{value}
</div>
""".format(feedback_type=feedback_type, value=value)
return feedback
# 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', 'submission_id', 'grader_id']:
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)
feedback_list_part1 = u"\n".join(format_feedback(k, v) for k, v in feedback_lst)
else:
feedback_list_part1 = format_feedback('errors', response_items['feedback'])
feedback_list_part2=(u"\n".join([format_feedback_hidden(feedback_type,value)
for feedback_type,value in response_items.items()
if feedback_type in ['submission_id', 'grader_id']]))
return u"\n".join([feedback_list_part1,feedback_list_part2])
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': "{0} / {1}".format(response_items['score'], self.max_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', 'grader_id', 'submission_id']:
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)
self.submission_id=score_result['submission_id']
self.grader_id=score_result['grader_id']
# 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']) / float(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
@@ -2261,5 +1831,4 @@ __all__ = [CodeResponse,
ChoiceResponse,
MultipleChoiceResponse,
TrueFalseResponse,
JavascriptResponse,
OpenEndedResponse]
JavascriptResponse]

View File

@@ -1,56 +0,0 @@
<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}
% if status in ['correct','incorrect']:
<div class="collapsible evaluation-response">
<header>
<a href="#">Respond to Feedback</a>
</header>
<section id="evaluation_${id}" class="evaluation">
<p>How accurate do you find this feedback?</p>
<div class="evaluation-scoring">
<ul class="scoring-list">
<li><input type="radio" name="evaluation-score" id="evaluation-score-5" value="5" /> <label for="evaluation-score-5"> Correct</label></li>
<li><input type="radio" name="evaluation-score" id="evaluation-score-4" value="4" /> <label for="evaluation-score-4"> Partially Correct</label></li>
<li><input type="radio" name="evaluation-score" id="evaluation-score-3" value="3" /> <label for="evaluation-score-3"> No Opinion</label></li>
<li><input type="radio" name="evaluation-score" id="evaluation-score-2" value="2" /> <label for="evaluation-score-2"> Partially Incorrect</label></li>
<li><input type="radio" name="evaluation-score" id="evaluation-score-1" value="1" /> <label for="evaluation-score-1"> Incorrect</label></li>
</ul>
</div>
<p>Additional comments:</p>
<textarea rows="${rows}" cols="${cols}" name="feedback_${id}" class="feedback-on-feedback" id="feedback_${id}"></textarea>
<div class="submit-message-container">
<input name="submit-message" class="submit-message" type="button" value="Submit your message"/>
</div>
</section>
</div>
% endif
</div>
</section>

View File

@@ -19,6 +19,7 @@ setup(
"abtest = xmodule.abtest_module:ABTestDescriptor",
"book = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"chapter = xmodule.seq_module:SequenceDescriptor",
"combinedopenended = xmodule.combined_open_ended_module:CombinedOpenEndedDescriptor",
"course = xmodule.course_module:CourseDescriptor",
"customtag = xmodule.template_module:CustomTagDescriptor",
"discuss = xmodule.backcompat_module:TranslateCustomTagDescriptor",
@@ -28,7 +29,6 @@ setup(
"problem = xmodule.capa_module:CapaDescriptor",
"problemset = xmodule.seq_module:SequenceDescriptor",
"section = xmodule.backcompat_module:SemanticSectionDescriptor",
"selfassessment = xmodule.self_assessment_module:SelfAssessmentDescriptor",
"sequential = xmodule.seq_module:SequenceDescriptor",
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"vertical = xmodule.vertical_module:VerticalDescriptor",
@@ -36,6 +36,7 @@ setup(
"videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"videosequence = xmodule.seq_module:SequenceDescriptor",
"discussion = xmodule.discussion_module:DiscussionDescriptor",
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
]
}
)

View File

@@ -380,7 +380,6 @@ class CapaModule(XModule):
'problem_save': self.save_problem,
'problem_show': self.get_answer,
'score_update': self.update_score,
'message_post' : self.message_post,
}
if dispatch not in handlers:
@@ -395,20 +394,6 @@ class CapaModule(XModule):
})
return json.dumps(d, cls=ComplexEncoder)
def message_post(self, get):
"""
Posts a message from a form to an appropriate location
"""
event_info = dict()
event_info['state'] = self.lcp.get_state()
event_info['problem_id'] = self.location.url()
event_info['student_id'] = self.system.anonymous_student_id
event_info['survey_responses']= get
success, message = self.lcp.message_post(event_info)
return {'success' : success, 'message' : message}
def closed(self):
''' Is the student still allowed to submit answers? '''
if self.attempts == self.max_attempts:
@@ -445,6 +430,7 @@ class CapaModule(XModule):
return False
def update_score(self, get):
"""
Delivers grading response (e.g. from asynchronous code checking) to

View File

@@ -0,0 +1,598 @@
import copy
from fs.errors import ResourceNotFoundError
import itertools
import json
import logging
from lxml import etree
from lxml.html import rewrite_links
from path import path
import os
import sys
from pkg_resources import resource_string
from .capa_module import only_one, ComplexEncoder
from .editing_module import EditingDescriptor
from .html_checker import check_html
from progress import Progress
from .stringify import stringify_children
from .x_module import XModule
from .xml_module import XmlDescriptor
from xmodule.modulestore import Location
import self_assessment_module
import open_ended_module
from mitxmako.shortcuts import render_to_string
log = logging.getLogger("mitx.courseware")
# Set the default number of max attempts. Should be 1 for production
# Set higher for debugging/testing
# attempts specified in xml definition overrides this.
MAX_ATTEMPTS = 10000
# Set maximum available number of points.
# Overriden by max_score specified in xml.
MAX_SCORE = 1
class CombinedOpenEndedModule(XModule):
"""
This is a module that encapsulates all open ended grading (self assessment, peer assessment, etc).
It transitions between problems, and support arbitrary ordering.
Each combined open ended module contains one or multiple "child" modules.
Child modules track their own state, and can transition between states. They also implement get_html and
handle_ajax.
The combined open ended module transitions between child modules as appropriate, tracks its own state, and passess
ajax requests from the browser to the child module or handles them itself (in the cases of reset and next problem)
ajax actions implemented by all children are:
'save_answer' -- Saves the student answer
'save_assessment' -- Saves the student assessment (or external grader assessment)
'save_post_assessment' -- saves a post assessment (hint, feedback on feedback, etc)
ajax actions implemented by combined open ended module are:
'reset' -- resets the whole combined open ended module and returns to the first child module
'next_problem' -- moves to the next child module
'get_results' -- gets results from a given child module
Types of children. Task is synonymous with child module, so each combined open ended module
incorporates multiple children (tasks):
openendedmodule
selfassessmentmodule
"""
STATE_VERSION = 1
# states
INITIAL = 'initial'
ASSESSING = 'assessing'
INTERMEDIATE_DONE = 'intermediate_done'
DONE = 'done'
js = {'coffee': [resource_string(__name__, 'js/src/combinedopenended/display.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/javascript_loader.coffee'),
]}
js_module_name = "CombinedOpenEnded"
css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]}
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
"""
Definition file should have one or many task blocks, a rubric block, and a prompt block:
Sample file:
<combinedopenended attempts="10000" max_score="1">
<rubric>
Blah blah rubric.
</rubric>
<prompt>
Some prompt.
</prompt>
<task>
<selfassessment>
<hintprompt>
What hint about this problem would you give to someone?
</hintprompt>
<submitmessage>
Save Succcesful. Thanks for participating!
</submitmessage>
</selfassessment>
</task>
<task>
<openended min_score_to_attempt="1" max_score_to_attempt="1">
<openendedparam>
<initial_display>Enter essay here.</initial_display>
<answer_display>This is the answer.</answer_display>
<grader_payload>{"grader_settings" : "ml_grading.conf",
"problem_id" : "6.002x/Welcome/OETest"}</grader_payload>
</openendedparam>
</openended>
</task>
</combinedopenended>
"""
# Load instance state
if instance_state is not None:
instance_state = json.loads(instance_state)
else:
instance_state = {}
#We need to set the location here so the child modules can use it
system.set('location', location)
#Tells the system which xml definition to load
self.current_task_number = instance_state.get('current_task_number', 0)
#This loads the states of the individual children
self.task_states = instance_state.get('task_states', [])
#Overall state of the combined open ended module
self.state = instance_state.get('state', self.INITIAL)
self.attempts = instance_state.get('attempts', 0)
#Allow reset is true if student has failed the criteria to move to the next child task
self.allow_reset = instance_state.get('ready_to_reset', False)
self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS))
# Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect).
self._max_score = int(self.metadata.get('max_score', MAX_SCORE))
#Static data is passed to the child modules to render
self.static_data = {
'max_score': self._max_score,
'max_attempts': self.max_attempts,
'prompt': definition['prompt'],
'rubric': definition['rubric']
}
self.task_xml = definition['task_xml']
self.setup_next_task()
def get_tag_name(self, xml):
"""
Gets the tag name of a given xml block.
Input: XML string
Output: The name of the root tag
"""
tag = etree.fromstring(xml).tag
return tag
def overwrite_state(self, current_task_state):
"""
Overwrites an instance state and sets the latest response to the current response. This is used
to ensure that the student response is carried over from the first child to the rest.
Input: Task state json string
Output: Task state json string
"""
last_response_data = self.get_last_response(self.current_task_number - 1)
last_response = last_response_data['response']
loaded_task_state = json.loads(current_task_state)
if loaded_task_state['state'] == self.INITIAL:
loaded_task_state['state'] = self.ASSESSING
loaded_task_state['created'] = True
loaded_task_state['history'].append({'answer': last_response})
current_task_state = json.dumps(loaded_task_state)
return current_task_state
def child_modules(self):
"""
Returns the constructors associated with the child modules in a dictionary. This makes writing functions
simpler (saves code duplication)
Input: None
Output: A dictionary of dictionaries containing the descriptor functions and module functions
"""
child_modules = {
'openended': open_ended_module.OpenEndedModule,
'selfassessment': self_assessment_module.SelfAssessmentModule,
}
child_descriptors = {
'openended': open_ended_module.OpenEndedDescriptor,
'selfassessment': self_assessment_module.SelfAssessmentDescriptor,
}
children = {
'modules': child_modules,
'descriptors': child_descriptors,
}
return children
def setup_next_task(self, reset=False):
"""
Sets up the next task for the module. Creates an instance state if none exists, carries over the answer
from the last instance state to the next if needed.
Input: A boolean indicating whether or not the reset function is calling.
Output: Boolean True (not useful right now)
"""
current_task_state = None
if len(self.task_states) > self.current_task_number:
current_task_state = self.task_states[self.current_task_number]
self.current_task_xml = self.task_xml[self.current_task_number]
if self.current_task_number > 0:
self.allow_reset = self.check_allow_reset()
if self.allow_reset:
self.current_task_number = self.current_task_number - 1
current_task_type = self.get_tag_name(self.current_task_xml)
children = self.child_modules()
child_task_module = children['modules'][current_task_type]
self.current_task_descriptor = children['descriptors'][current_task_type](self.system)
#This is the xml object created from the xml definition of the current task
etree_xml = etree.fromstring(self.current_task_xml)
#This sends the etree_xml object through the descriptor module of the current task, and
#returns the xml parsed by the descriptor
self.current_task_parsed_xml = self.current_task_descriptor.definition_from_xml(etree_xml, self.system)
if current_task_state is None and self.current_task_number == 0:
self.current_task = child_task_module(self.system, self.location,
self.current_task_parsed_xml, self.current_task_descriptor, self.static_data)
self.task_states.append(self.current_task.get_instance_state())
self.state = self.ASSESSING
elif current_task_state is None and self.current_task_number > 0:
last_response_data = self.get_last_response(self.current_task_number - 1)
last_response = last_response_data['response']
current_task_state=json.dumps({
'state' : self.ASSESSING,
'version' : self.STATE_VERSION,
'max_score' : self._max_score,
'attempts' : 0,
'created' : True,
'history' : [{'answer' : str(last_response)}],
})
self.current_task = child_task_module(self.system, self.location,
self.current_task_parsed_xml, self.current_task_descriptor, self.static_data,
instance_state=current_task_state)
self.task_states.append(self.current_task.get_instance_state())
self.state = self.ASSESSING
else:
if self.current_task_number > 0 and not reset:
current_task_state = self.overwrite_state(current_task_state)
self.current_task = child_task_module(self.system, self.location,
self.current_task_parsed_xml, self.current_task_descriptor, self.static_data,
instance_state=current_task_state)
log.debug(current_task_state)
return True
def check_allow_reset(self):
"""
Checks to see if the student has passed the criteria to move to the next module. If not, sets
allow_reset to true and halts the student progress through the tasks.
Input: None
Output: the allow_reset attribute of the current module.
"""
if not self.allow_reset:
if self.current_task_number > 0:
last_response_data = self.get_last_response(self.current_task_number - 1)
current_response_data = self.get_current_attributes(self.current_task_number)
if(current_response_data['min_score_to_attempt'] > last_response_data['score']
or current_response_data['max_score_to_attempt'] < last_response_data['score']):
self.state = self.DONE
self.allow_reset = True
return self.allow_reset
def get_context(self):
"""
Generates a context dictionary that is used to render html.
Input: None
Output: A dictionary that can be rendered into the combined open ended template.
"""
task_html = self.get_html_base()
#set context variables and render template
context = {
'items': [{'content': task_html}],
'ajax_url': self.system.ajax_url,
'allow_reset': self.allow_reset,
'state': self.state,
'task_count': len(self.task_xml),
'task_number': self.current_task_number + 1,
'status': self.get_status(),
}
return context
def get_html(self):
"""
Gets HTML for rendering.
Input: None
Output: rendered html
"""
context = self.get_context()
html = self.system.render_template('combined_open_ended.html', context)
return html
def get_html_nonsystem(self):
"""
Gets HTML for rendering via AJAX. Does not use system, because system contains some additional
html, which is not appropriate for returning via ajax calls.
Input: None
Output: HTML rendered directly via Mako
"""
context = self.get_context()
html = render_to_string('combined_open_ended.html', context)
return html
def get_html_base(self):
"""
Gets the HTML associated with the current child task
Input: None
Output: Child task HTML
"""
self.update_task_states()
html = self.current_task.get_html(self.system)
return_html = rewrite_links(html, self.rewrite_content_links)
return return_html
def get_current_attributes(self, task_number):
"""
Gets the min and max score to attempt attributes of the specified task.
Input: The number of the task.
Output: The minimum and maximum scores needed to move on to the specified task.
"""
task_xml = self.task_xml[task_number]
etree_xml = etree.fromstring(task_xml)
min_score_to_attempt = int(etree_xml.attrib.get('min_score_to_attempt', 0))
max_score_to_attempt = int(etree_xml.attrib.get('max_score_to_attempt', self._max_score))
return {'min_score_to_attempt': min_score_to_attempt, 'max_score_to_attempt': max_score_to_attempt}
def get_last_response(self, task_number):
"""
Returns data associated with the specified task number, such as the last response, score, etc.
Input: The number of the task.
Output: A dictionary that contains information about the specified task.
"""
last_response = ""
task_state = self.task_states[task_number]
task_xml = self.task_xml[task_number]
task_type = self.get_tag_name(task_xml)
children = self.child_modules()
task_descriptor = children['descriptors'][task_type](self.system)
etree_xml = etree.fromstring(task_xml)
min_score_to_attempt = int(etree_xml.attrib.get('min_score_to_attempt', 0))
max_score_to_attempt = int(etree_xml.attrib.get('max_score_to_attempt', self._max_score))
task_parsed_xml = task_descriptor.definition_from_xml(etree_xml, self.system)
task = children['modules'][task_type](self.system, self.location, task_parsed_xml, task_descriptor,
self.static_data, instance_state=task_state)
last_response = task.latest_answer()
last_score = task.latest_score()
last_post_assessment = task.latest_post_assessment()
last_post_feedback = ""
if task_type == "openended":
last_post_assessment = task.latest_post_assessment(short_feedback=False, join_feedback=False)
if isinstance(last_post_assessment, list):
eval_list = []
for i in xrange(0, len(last_post_assessment)):
eval_list.append(task.format_feedback_with_evaluation(last_post_assessment[i]))
last_post_evaluation = "".join(eval_list)
else:
last_post_evaluation = task.format_feedback_with_evaluation(last_post_assessment)
last_post_assessment = last_post_evaluation
last_correctness = task.is_last_response_correct()
max_score = task.max_score()
state = task.state
last_response_dict = {
'response': last_response,
'score': last_score,
'post_assessment': last_post_assessment,
'type': task_type,
'max_score': max_score,
'state': state,
'human_state': task.HUMAN_NAMES[state],
'correct': last_correctness,
'min_score_to_attempt': min_score_to_attempt,
'max_score_to_attempt': max_score_to_attempt,
}
return last_response_dict
def update_task_states(self):
"""
Updates the task state of the combined open ended module with the task state of the current child module.
Input: None
Output: boolean indicating whether or not the task state changed.
"""
changed = False
if not self.allow_reset:
self.task_states[self.current_task_number] = self.current_task.get_instance_state()
current_task_state = json.loads(self.task_states[self.current_task_number])
if current_task_state['state'] == self.DONE:
self.current_task_number += 1
if self.current_task_number >= (len(self.task_xml)):
self.state = self.DONE
self.current_task_number = len(self.task_xml) - 1
else:
self.state = self.INITIAL
changed = True
self.setup_next_task()
return changed
def update_task_states_ajax(self, return_html):
"""
Runs the update task states function for ajax calls. Currently the same as update_task_states
Input: The html returned by the handle_ajax function of the child
Output: New html that should be rendered
"""
changed = self.update_task_states()
if changed:
#return_html=self.get_html()
pass
return return_html
def get_results(self, get):
"""
Gets the results of a given grader via ajax.
Input: AJAX get dictionary
Output: Dictionary to be rendered via ajax that contains the result html.
"""
task_number = int(get['task_number'])
self.update_task_states()
response_dict = self.get_last_response(task_number)
context = {'results': response_dict['post_assessment'], 'task_number': task_number + 1}
html = render_to_string('combined_open_ended_results.html', context)
return {'html': html, 'success': True}
def handle_ajax(self, dispatch, get):
"""
This is called by courseware.module_render, to handle an AJAX call.
"get" is request.POST.
Returns a json dictionary:
{ 'progress_changed' : True/False,
'progress': 'none'/'in_progress'/'done',
<other request-specific values here > }
"""
handlers = {
'next_problem': self.next_problem,
'reset': self.reset,
'get_results': self.get_results
}
if dispatch not in handlers:
return_html = self.current_task.handle_ajax(dispatch, get, self.system)
return self.update_task_states_ajax(return_html)
d = handlers[dispatch](get)
return json.dumps(d, cls=ComplexEncoder)
def next_problem(self, get):
"""
Called via ajax to advance to the next problem.
Input: AJAX get request.
Output: Dictionary to be rendered
"""
self.update_task_states()
return {'success': True, 'html': self.get_html_nonsystem(), 'allow_reset': self.allow_reset}
def reset(self, get):
"""
If resetting is allowed, reset the state of the combined open ended module.
Input: AJAX get dictionary
Output: AJAX dictionary to tbe rendered
"""
if self.state != self.DONE:
if not self.allow_reset:
return self.out_of_sync_error(get)
if self.attempts > self.max_attempts:
return {
'success': False,
'error': 'Too many attempts.'
}
self.state = self.INITIAL
self.allow_reset = False
for i in xrange(0, len(self.task_xml)):
self.current_task_number = i
self.setup_next_task(reset=True)
self.current_task.reset(self.system)
self.task_states[self.current_task_number] = self.current_task.get_instance_state()
self.current_task_number = 0
self.allow_reset = False
self.setup_next_task()
return {'success': True, 'html': self.get_html_nonsystem()}
def get_instance_state(self):
"""
Returns the current instance state. The module can be recreated from the instance state.
Input: None
Output: A dictionary containing the instance state.
"""
state = {
'version': self.STATE_VERSION,
'current_task_number': self.current_task_number,
'state': self.state,
'task_states': self.task_states,
'attempts': self.attempts,
'ready_to_reset': self.allow_reset,
}
return json.dumps(state)
def get_status(self):
"""
Gets the status panel to be displayed at the top right.
Input: None
Output: The status html to be rendered
"""
status = []
for i in xrange(0, self.current_task_number + 1):
task_data = self.get_last_response(i)
task_data.update({'task_number': i + 1})
status.append(task_data)
context = {'status_list': status}
status_html = self.system.render_template("combined_open_ended_status.html", context)
return status_html
class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor):
"""
Module for adding combined open ended questions
"""
mako_template = "widgets/html-edit.html"
module_class = CombinedOpenEndedModule
filename_extension = "xml"
stores_state = True
has_score = True
template_dir_name = "combinedopenended"
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
js_module_name = "HTMLEditingDescriptor"
@classmethod
def definition_from_xml(cls, xml_object, system):
"""
Pull out the individual tasks, the rubric, and the prompt, and parse
Returns:
{
'rubric': 'some-html',
'prompt': 'some-html',
'task_xml': dictionary of xml strings,
}
"""
expected_children = ['task', 'rubric', 'prompt']
for child in expected_children:
if len(xml_object.xpath(child)) == 0:
raise ValueError("Combined Open Ended definition must include at least one '{0}' tag".format(child))
def parse_task(k):
"""Assumes that xml_object has child k"""
return [stringify_children(xml_object.xpath(k)[i]) for i in xrange(0, len(xml_object.xpath(k)))]
def parse(k):
"""Assumes that xml_object has child k"""
return xml_object.xpath(k)[0]
return {'task_xml': parse_task('task'), 'prompt': parse('prompt'), 'rubric': parse('rubric')}
def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.'''
elt = etree.Element('combinedopenended')
def add_child(k):
child_str = '<{tag}>{body}</{tag}>'.format(tag=k, body=self.definition[k])
child_node = etree.fromstring(child_str)
elt.append(child_node)
for child in ['task']:
add_child(child)
return elt

View File

@@ -0,0 +1,129 @@
from mitxmako.shortcuts import render_to_string
import logging
from lxml import etree
log=logging.getLogger(__name__)
class CombinedOpenEndedRubric:
@staticmethod
def render_rubric(rubric_xml):
try:
rubric_categories = CombinedOpenEndedRubric.extract_rubric_categories(rubric_xml)
html = render_to_string('open_ended_rubric.html', {'rubric_categories' : rubric_categories})
except:
log.exception("Could not parse the rubric.")
html = rubric_xml
return html
@staticmethod
def extract_rubric_categories(element):
'''
Contstruct a list of categories such that the structure looks like:
[ { category: "Category 1 Name",
options: [{text: "Option 1 Name", points: 0}, {text:"Option 2 Name", points: 5}]
},
{ category: "Category 2 Name",
options: [{text: "Option 1 Name", points: 0},
{text: "Option 2 Name", points: 1},
{text: "Option 3 Name", points: 2]}]
'''
element = etree.fromstring(element)
categories = []
for category in element:
if category.tag != 'category':
raise Exception("[capa.inputtypes.extract_categories] Expected a <category> tag: got {0} instead".format(category.tag))
else:
categories.append(CombinedOpenEndedRubric.extract_category(category))
return categories
@staticmethod
def extract_category(category):
'''
construct an individual category
{category: "Category 1 Name",
options: [{text: "Option 1 text", points: 1},
{text: "Option 2 text", points: 2}]}
all sorting and auto-point generation occurs in this function
'''
has_score=False
descriptionxml = category[0]
scorexml = category[1]
if scorexml.tag == "option":
optionsxml = category[1:]
else:
optionsxml = category[2:]
has_score=True
# parse description
if descriptionxml.tag != 'description':
raise Exception("[extract_category]: expected description tag, got {0} instead".format(descriptionxml.tag))
if has_score:
if scorexml.tag != 'score':
raise Exception("[extract_category]: expected score tag, got {0} instead".format(scorexml.tag))
for option in optionsxml:
if option.tag != "option":
raise Exception("[extract_category]: expected option tag, got {0} instead".format(option.tag))
description = descriptionxml.text
if has_score:
score = int(scorexml.text)
else:
score = 0
cur_points = 0
options = []
autonumbering = True
# parse options
for option in optionsxml:
if option.tag != 'option':
raise Exception("[extract_category]: expected option tag, got {0} instead".format(option.tag))
else:
pointstr = option.get("points")
if pointstr:
autonumbering = False
# try to parse this into an int
try:
points = int(pointstr)
except ValueError:
raise Exception("[extract_category]: expected points to have int, got {0} instead".format(pointstr))
elif autonumbering:
# use the generated one if we're in the right mode
points = cur_points
cur_points = cur_points + 1
else:
raise Exception("[extract_category]: missing points attribute. Cannot continue to auto-create points values after a points value is explicitly dfined.")
optiontext = option.text
selected = False
if has_score:
if points == score:
selected = True
options.append({'text': option.text, 'points': points, 'selected' : selected})
# sort and check for duplicates
options = sorted(options, key=lambda option: option['points'])
CombinedOpenEndedRubric.validate_options(options)
return {'description': description, 'options': options, 'score' : score, 'has_score' : has_score}
@staticmethod
def validate_options(options):
'''
Validates a set of options. This can and should be extended to filter out other bad edge cases
'''
if len(options) == 0:
raise Exception("[extract_category]: no options associated with this category")
if len(options) == 1:
return
prev = options[0]['points']
for option in options[1:]:
if prev == option['points']:
raise Exception("[extract_category]: found duplicate point values between two different options")
else:
prev = option['points']

View File

@@ -1,9 +1,9 @@
from fs.errors import ResourceNotFoundError
import logging
from lxml import etree
from path import path # NOTE (THK): Only used for detecting presence of syllabus
from path import path # NOTE (THK): Only used for detecting presence of syllabus
import requests
import time
from datetime import datetime
from xmodule.util.decorators import lazyproperty
from xmodule.graders import load_grading_policy
@@ -13,6 +13,7 @@ from xmodule.timeparse import parse_time, stringify_time
log = logging.getLogger(__name__)
class CourseDescriptor(SequenceDescriptor):
module_class = SequenceModule
@@ -165,6 +166,38 @@ class CourseDescriptor(SequenceDescriptor):
def show_calculator(self):
return self.metadata.get("show_calculator", None) == "Yes"
@property
def is_new(self):
# The course is "new" if either if the metadata flag is_new is
# true or if the course has not started yet
flag = self.metadata.get('is_new', None)
if flag is None:
return self.days_until_start > 1
elif isinstance(flag, basestring):
return flag.lower() in ['true', 'yes', 'y']
else:
return bool(flag)
@property
def days_until_start(self):
def convert_to_datetime(timestamp):
return datetime.fromtimestamp(time.mktime(timestamp))
start_date = convert_to_datetime(self.start)
# Try to use course advertised date if we can parse it
advertised_start = self.metadata.get('advertised_start', None)
if advertised_start:
try:
start_date = datetime.strptime(advertised_start,
"%Y-%m-%dT%H:%M")
except ValueError:
pass # Invalid date, keep using 'start''
now = convert_to_datetime(time.gmtime())
days_until_start = (start_date - now).days
return days_until_start
@lazyproperty
def grading_context(self):
"""
@@ -244,7 +277,6 @@ class CourseDescriptor(SequenceDescriptor):
raise ValueError("{0} is not a course location".format(loc))
return "/".join([loc.org, loc.course, loc.name])
@property
def id(self):
"""Return the course_id for this course"""
@@ -258,7 +290,7 @@ class CourseDescriptor(SequenceDescriptor):
# form text...
if parsed_advertised_start is None and \
('advertised_start' in self.metadata):
return self.metadata['advertised_start']
return self.metadata['advertised_start']
displayed_start = parsed_advertised_start or self.start
@@ -341,4 +373,3 @@ class CourseDescriptor(SequenceDescriptor):
@property
def org(self):
return self.location.org

View File

@@ -0,0 +1,626 @@
h2 {
margin-top: 0;
margin-bottom: 15px;
&.problem-header {
section.staff {
margin-top: 30px;
font-size: 80%;
}
}
@media print {
display: block;
width: auto;
border-right: 0;
}
}
.inline-error {
color: darken($error-red, 10%);
}
section.combined-open-ended {
@include clearfix;
.status-container
{
float:right;
width:40%;
}
.item-container
{
float:left;
width: 53%;
padding-bottom: 50px;
}
.result-container
{
float:left;
width: 93%;
position:relative;
}
}
section.combined-open-ended-status {
.statusitem {
background-color: #FAFAFA;
color: #2C2C2C;
font-family: monospace;
font-size: 1em;
padding-top: 10px;
}
.statusitem-current {
background-color: #BEBEBE;
color: #2C2C2C;
font-family: monospace;
font-size: 1em;
padding-top: 10px;
}
span {
&.unanswered {
@include inline-block();
background: url('../images/unanswered-icon.png') center center no-repeat;
height: 14px;
position: relative;
width: 14px;
float: right;
}
&.correct {
@include inline-block();
background: url('../images/correct-icon.png') center center no-repeat;
height: 20px;
position: relative;
width: 25px;
float: right;
}
&.incorrect {
@include inline-block();
background: url('../images/incorrect-icon.png') center center no-repeat;
height: 20px;
width: 20px;
position: relative;
float: right;
}
}
}
div.result-container {
.evaluation {
p {
margin-bottom: 1px;
}
}
.feedback-on-feedback {
height: 100px;
margin-right: 0px;
}
.evaluation-response {
header {
text-align: right;
a {
font-size: .85em;
}
}
}
.evaluation-scoring {
.scoring-list {
list-style-type: none;
margin-left: 3px;
li {
&:first-child {
margin-left: 0px;
}
display:inline;
margin-left: 0px;
label {
font-size: .9em;
}
}
}
}
.submit-message-container {
margin: 10px 0px ;
}
.external-grader-message {
section {
padding-left: 20px;
background-color: #FAFAFA;
color: #2C2C2C;
font-family: monospace;
font-size: 1em;
padding-top: 10px;
header {
font-size: 1.4em;
}
.shortform {
font-weight: bold;
}
.longform {
padding: 0px;
margin: 0px;
.result-errors {
margin: 5px;
padding: 10px 10px 10px 40px;
background: url('../images/incorrect-icon.png') center left no-repeat;
li {
color: #B00;
}
}
.result-output {
margin: 5px;
padding: 20px 0px 15px 50px;
border-top: 1px solid #DDD;
border-left: 20px solid #FAFAFA;
h4 {
font-family: monospace;
font-size: 1em;
}
dl {
margin: 0px;
}
dt {
margin-top: 20px;
}
dd {
margin-left: 24pt;
}
}
.result-correct {
background: url('../images/correct-icon.png') left 20px no-repeat;
.result-actual-output {
color: #090;
}
}
.result-incorrect {
background: url('../images/incorrect-icon.png') left 20px no-repeat;
.result-actual-output {
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;
}
}
}
}
}
}
div.result-container, section.open-ended-child {
.rubric {
tr {
margin:10px 0px;
height: 100%;
}
td {
padding: 20px 0px;
margin: 10px 0px;
height: 100%;
}
th {
padding: 5px;
margin: 5px;
}
label,
.view-only {
margin:10px;
position: relative;
padding: 15px;
width: 200px;
height:100%;
display: inline-block;
min-height: 50px;
min-width: 50px;
background-color: #CCC;
font-size: 1em;
}
.grade {
position: absolute;
bottom:0px;
right:0px;
margin:10px;
}
.selected-grade {
background: #666;
color: white;
}
input[type=radio]:checked + label {
background: #666;
color: white; }
input[class='score-selection'] {
display: none;
}
}
}
section.open-ended-child {
@media print {
display: block;
width: auto;
padding: 0;
canvas, img {
page-break-inside: avoid;
}
}
.inline {
display: inline;
}
ol.enumerate {
li {
&:before {
content: " ";
display: block;
height: 0;
visibility: hidden;
}
}
}
.solution-span {
> span {
margin: 20px 0;
display: block;
border: 1px solid #ddd;
padding: 9px 15px 20px;
background: #FFF;
position: relative;
@include box-shadow(inset 0 0 0 1px #eee);
@include border-radius(3px);
&:empty {
display: none;
}
}
}
p {
&.answer {
margin-top: -2px;
}
&.status {
text-indent: -9999px;
margin: 8px 0 0 10px;
}
}
div.unanswered {
p.status {
@include inline-block();
background: url('../images/unanswered-icon.png') center center no-repeat;
height: 14px;
width: 14px;
}
}
div.correct, div.ui-icon-check {
p.status {
@include inline-block();
background: url('../images/correct-icon.png') center center no-repeat;
height: 20px;
width: 25px;
}
input {
border-color: green;
}
}
div.processing {
p.status {
@include inline-block();
background: url('../images/spinner.gif') center center no-repeat;
height: 20px;
width: 20px;
}
input {
border-color: #aaa;
}
}
div.incorrect, div.ui-icon-close {
p.status {
@include inline-block();
background: url('../images/incorrect-icon.png') center center no-repeat;
height: 20px;
width: 20px;
text-indent: -9999px;
}
input {
border-color: red;
}
}
> span {
display: block;
margin-bottom: lh(.5);
}
p.answer {
@include inline-block();
margin-bottom: 0;
margin-left: 10px;
&:before {
content: "Answer: ";
font-weight: bold;
display: inline;
}
&:empty {
&:before {
display: none;
}
}
}
span {
&.unanswered, &.ui-icon-bullet {
@include inline-block();
background: url('../images/unanswered-icon.png') center center no-repeat;
height: 14px;
position: relative;
top: 4px;
width: 14px;
}
&.processing, &.ui-icon-processing {
@include inline-block();
background: url('../images/spinner.gif') center center no-repeat;
height: 20px;
position: relative;
top: 6px;
width: 25px;
}
&.correct, &.ui-icon-check {
@include inline-block();
background: url('../images/correct-icon.png') center center no-repeat;
height: 20px;
position: relative;
top: 6px;
width: 25px;
}
&.incorrect, &.ui-icon-close {
@include inline-block();
background: url('../images/incorrect-icon.png') center center no-repeat;
height: 20px;
width: 20px;
position: relative;
top: 6px;
}
}
.reload
{
float:right;
margin: 10px;
}
.grader-status {
padding: 9px;
background: #F6F6F6;
border: 1px solid #ddd;
border-top: 0;
margin-bottom: 20px;
@include clearfix;
span {
text-indent: -9999px;
overflow: hidden;
display: block;
float: left;
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;
margin-bottom: 0;
float: left;
}
&.file {
background: #FFF;
margin-top: 20px;
padding: 20px 0 0 0;
border: {
top: 1px solid #eee;
right: 0;
bottom: 0;
left: 0;
}
p.debug {
display: none;
}
input {
float: left;
}
}
}
form.option-input {
margin: -10px 0 20px;
padding-bottom: 20px;
select {
margin-right: flex-gutter();
}
}
ul {
list-style: disc outside none;
margin-bottom: lh();
margin-left: .75em;
margin-left: .75rem;
}
ol {
list-style: decimal outside none;
margin-bottom: lh();
margin-left: .75em;
margin-left: .75rem;
}
dl {
line-height: 1.4em;
}
dl dt {
font-weight: bold;
}
dl dd {
margin-bottom: 0;
}
dd {
margin-left: .5em;
margin-left: .5rem;
}
li {
line-height: 1.4em;
margin-bottom: lh(.5);
&:last-child {
margin-bottom: 0;
}
}
p {
margin-bottom: lh();
}
hr {
background: #ddd;
border: none;
clear: both;
color: #ddd;
float: none;
height: 1px;
margin: 0 0 .75rem;
width: 100%;
}
.hidden {
display: none;
visibility: hidden;
}
#{$all-text-inputs} {
display: inline;
width: auto;
}
section.action {
margin-top: 20px;
input.save {
@extend .blue-button;
}
.submission_feedback {
// background: #F3F3F3;
// border: 1px solid #ddd;
// @include border-radius(3px);
// padding: 8px 12px;
// margin-top: 10px;
@include inline-block;
font-style: italic;
margin: 8px 0 0 10px;
color: #777;
-webkit-font-smoothing: antialiased;
}
}
.detailed-solution {
> p:first-child {
font-size: 0.9em;
font-weight: bold;
font-style: normal;
text-transform: uppercase;
color: #AAA;
}
p:last-child {
margin-bottom: 0;
}
}
div.open-ended-alert {
padding: 8px 12px;
border: 1px solid #EBE8BF;
border-radius: 3px;
background: #FFFCDD;
font-size: 0.9em;
margin-top: 10px;
}
div.capa_reset {
padding: 25px;
border: 1px solid $error-red;
background-color: lighten($error-red, 25%);
border-radius: 3px;
font-size: 1em;
margin-top: 10px;
margin-bottom: 10px;
}
.capa_reset>h2 {
color: #AA0000;
}
.capa_reset li {
font-size: 0.9em;
}
}

View File

@@ -0,0 +1,195 @@
"""
Graphical slider tool module is ungraded xmodule used by students to
understand functional dependencies.
"""
import json
import logging
from lxml import etree
from lxml import html
import xmltodict
from xmodule.mako_module import MakoModuleDescriptor
from xmodule.xml_module import XmlDescriptor
from xmodule.x_module import XModule
from xmodule.stringify import stringify_children
from pkg_resources import resource_string
log = logging.getLogger(__name__)
class GraphicalSliderToolModule(XModule):
''' Graphical-Slider-Tool Module
'''
js = {
'js': [
# 3rd party libraries used by graphic slider tool.
# TODO - where to store them - outside xmodule?
resource_string(__name__, 'js/src/graphical_slider_tool/jstat-1.0.0.min.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/gst_main.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/state.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/logme.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/general_methods.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/sliders.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/inputs.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/graph.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/el_output.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/g_label_el_output.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/gst.js')
]
}
js_module_name = "GraphicalSliderTool"
def __init__(self, system, location, definition, descriptor, instance_state=None,
shared_state=None, **kwargs):
"""
For XML file format please look at documentation. TODO - receive
information where to store XML documentation.
"""
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
def get_html(self):
""" Renders parameters to template. """
# these 3 will be used in class methods
self.html_id = self.location.html_id()
self.html_class = self.location.category
self.configuration_json = self.build_configuration_json()
params = {
'gst_html': self.substitute_controls(self.definition['render']),
'element_id': self.html_id,
'element_class': self.html_class,
'configuration_json': self.configuration_json
}
self.content = self.system.render_template(
'graphical_slider_tool.html', params)
return self.content
def substitute_controls(self, html_string):
""" Substitutes control elements (slider, textbox and plot) in
html_string with their divs. Html_string is content of <render> tag
inside <graphical_slider_tool> tag. Documentation on how information in
<render> tag is organized and processed is located in:
mitx/docs/build/html/graphical_slider_tool.html.
Args:
html_string: content of <render> tag, with controls as xml tags,
e.g. <slider var="a"/>.
Returns:
html_string with control tags replaced by proper divs
(<slider var="a"/> -> <div class="....slider" > </div>)
"""
xml = html.fromstring(html_string)
#substitute plot, if presented
plot_div = '<div class="{element_class}_plot" id="{element_id}_plot" \
style="{style}"></div>'
plot_el = xml.xpath('//plot')
if plot_el:
plot_el = plot_el[0]
plot_el.getparent().replace(plot_el, html.fromstring(
plot_div.format(element_class=self.html_class,
element_id=self.html_id,
style=plot_el.get('style', ""))))
#substitute sliders
slider_div = '<div class="{element_class}_slider" \
id="{element_id}_slider_{var}" \
data-var="{var}" \
style="{style}">\
</div>'
slider_els = xml.xpath('//slider')
for slider_el in slider_els:
slider_el.getparent().replace(slider_el, html.fromstring(
slider_div.format(element_class=self.html_class,
element_id=self.html_id,
var=slider_el.get('var', ""),
style=slider_el.get('style', ""))))
# substitute inputs aka textboxes
input_div = '<input class="{element_class}_input" \
id="{element_id}_input_{var}_{input_index}" \
data-var="{var}" style="{style}"/>'
input_els = xml.xpath('//textbox')
for input_index, input_el in enumerate(input_els):
input_el.getparent().replace(input_el, html.fromstring(
input_div.format(element_class=self.html_class,
element_id=self.html_id,
var=input_el.get('var', ""),
style=input_el.get('style', ""),
input_index=input_index)))
return html.tostring(xml)
def build_configuration_json(self):
"""Creates json element from xml element (with aim to transfer later
directly to javascript via hidden field in template). Steps:
1. Convert xml tree to python dict.
2. Dump dict to json.
"""
# <root> added for interface compatibility with xmltodict.parse
# class added for javascript's part purposes
return json.dumps(xmltodict.parse('<root class="' + self.html_class +
'">' + self.definition['configuration'] + '</root>'))
class GraphicalSliderToolDescriptor(MakoModuleDescriptor, XmlDescriptor):
module_class = GraphicalSliderToolModule
template_dir_name = 'graphical_slider_tool'
@classmethod
def definition_from_xml(cls, xml_object, system):
"""
Pull out the data into dictionary.
Args:
xml_object: xml from file.
Returns:
dict
"""
# check for presense of required tags in xml
expected_children_level_0 = ['render', 'configuration']
for child in expected_children_level_0:
if len(xml_object.xpath(child)) != 1:
raise ValueError("Graphical Slider Tool definition must include \
exactly one '{0}' tag".format(child))
expected_children_level_1 = ['functions']
for child in expected_children_level_1:
if len(xml_object.xpath('configuration')[0].xpath(child)) != 1:
raise ValueError("Graphical Slider Tool definition must include \
exactly one '{0}' tag".format(child))
# finished
def parse(k):
"""Assumes that xml_object has child k"""
return stringify_children(xml_object.xpath(k)[0])
return {
'render': parse('render'),
'configuration': parse('configuration')
}
def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.'''
xml_object = etree.Element('graphical_slider_tool')
def add_child(k):
child_str = '<{tag}>{body}</{tag}>'.format(tag=k, body=self.definition[k])
child_node = etree.fromstring(child_str)
xml_object.append(child_node)
for child in ['render', 'configuration']:
add_child(child)
return xml_object

View File

@@ -25,7 +25,6 @@ class @Problem
@$('section.action input.reset').click @reset
@$('section.action input.show').click @show
@$('section.action input.save').click @save
@$('section.evaluation input.submit-message').click @message_post
# Collapsibles
Collapsible.setCollapsibles(@el)
@@ -198,35 +197,6 @@ class @Problem
else
@gentle_alert response.success
message_post: =>
Logger.log 'message_post', @answers
fd = new FormData()
feedback = @$('section.evaluation textarea.feedback-on-feedback')[0].value
submission_id = $('div.external-grader-message div.submission_id')[0].innerHTML
grader_id = $('div.external-grader-message div.grader_id')[0].innerHTML
score = $(".evaluation-scoring input:radio[name='evaluation-score']:checked").val()
fd.append('feedback', feedback)
fd.append('submission_id', submission_id)
fd.append('grader_id', grader_id)
if(!score)
@gentle_alert "You need to pick a rating before you can submit."
return
else
fd.append('score', score)
settings =
type: "POST"
data: fd
processData: false
contentType: false
success: (response) =>
@gentle_alert response.message
@$('section.evaluation').slideToggle()
$.ajaxWithPrefix("#{@url}/message_post", settings)
reset: =>
Logger.log 'problem_reset', @answers
$.postWithPrefix "#{@url}/problem_reset", id: @id, (response) =>

View File

@@ -22,7 +22,7 @@ class @Collapsible
if $(event.target).text() == 'See full output'
new_text = 'Hide output'
else
new_text = 'See full ouput'
new_text = 'See full output'
$(event.target).text(new_text)
@toggleHint: (event) =>

View File

@@ -0,0 +1,282 @@
class @CombinedOpenEnded
constructor: (element) ->
@element=element
@reinitialize(element)
reinitialize: (element) ->
@wrapper=$(element).find('section.xmodule_CombinedOpenEndedModule')
@el = $(element).find('section.combined-open-ended')
@combined_open_ended=$(element).find('section.combined-open-ended')
@id = @el.data('id')
@ajax_url = @el.data('ajax-url')
@state = @el.data('state')
@task_count = @el.data('task-count')
@task_number = @el.data('task-number')
@allow_reset = @el.data('allow_reset')
@reset_button = @$('.reset-button')
@reset_button.click @reset
@next_problem_button = @$('.next-step-button')
@next_problem_button.click @next_problem
@show_results_button=@$('.show-results-button')
@show_results_button.click @show_results
# valid states: 'initial', 'assessing', 'post_assessment', 'done'
Collapsible.setCollapsibles(@el)
@submit_evaluation_button = $('.submit-evaluation-button')
@submit_evaluation_button.click @message_post
@results_container = $('.result-container')
# Where to put the rubric once we load it
@el = $(element).find('section.open-ended-child')
@errors_area = @$('.error')
@answer_area = @$('textarea.answer')
@rubric_wrapper = @$('.rubric-wrapper')
@hint_wrapper = @$('.hint-wrapper')
@message_wrapper = @$('.message-wrapper')
@submit_button = @$('.submit-button')
@child_state = @el.data('state')
@child_type = @el.data('child-type')
if @child_type=="openended"
@skip_button = @$('.skip-button')
@skip_button.click @skip_post_assessment
@open_ended_child= @$('.open-ended-child')
@find_assessment_elements()
@find_hint_elements()
@rebind()
# locally scoped jquery.
$: (selector) ->
$(selector, @el)
show_results: (event) =>
status_item = $(event.target).parent().parent()
status_number = status_item.data('status-number')
data = {'task_number' : status_number}
$.postWithPrefix "#{@ajax_url}/get_results", data, (response) =>
if response.success
@results_container.after(response.html).remove()
@results_container = $('div.result-container')
@submit_evaluation_button = $('.submit-evaluation-button')
@submit_evaluation_button.click @message_post
Collapsible.setCollapsibles(@results_container)
else
@errors_area.html(response.error)
message_post: (event)=>
Logger.log 'message_post', @answers
external_grader_message=$(event.target).parent().parent().parent()
evaluation_scoring = $(event.target).parent()
fd = new FormData()
feedback = evaluation_scoring.find('textarea.feedback-on-feedback')[0].value
submission_id = external_grader_message.find('input.submission_id')[0].value
grader_id = external_grader_message.find('input.grader_id')[0].value
score = evaluation_scoring.find("input:radio[name='evaluation-score']:checked").val()
fd.append('feedback', feedback)
fd.append('submission_id', submission_id)
fd.append('grader_id', grader_id)
if(!score)
@gentle_alert "You need to pick a rating before you can submit."
return
else
fd.append('score', score)
settings =
type: "POST"
data: fd
processData: false
contentType: false
success: (response) =>
@gentle_alert response.msg
$('section.evaluation').slideToggle()
@message_wrapper.html(response.message_html)
$.ajaxWithPrefix("#{@ajax_url}/save_post_assessment", settings)
rebind: () =>
# rebind to the appropriate function for the current state
@submit_button.unbind('click')
@submit_button.show()
@reset_button.hide()
@next_problem_button.hide()
@hint_area.attr('disabled', false)
if @child_type=="openended"
@skip_button.hide()
if @allow_reset=="True"
@reset_button.show()
@submit_button.hide()
@answer_area.attr("disabled", true)
@hint_area.attr('disabled', true)
else if @child_state == 'initial'
@answer_area.attr("disabled", false)
@submit_button.prop('value', 'Submit')
@submit_button.click @save_answer
else if @child_state == 'assessing'
@answer_area.attr("disabled", true)
@submit_button.prop('value', 'Submit assessment')
@submit_button.click @save_assessment
if @child_type == "openended"
@submit_button.hide()
@queueing()
else if @child_state == 'post_assessment'
if @child_type=="openended"
@skip_button.show()
@skip_post_assessment()
@answer_area.attr("disabled", true)
@submit_button.prop('value', 'Submit post-assessment')
if @child_type=="selfassessment"
@submit_button.click @save_hint
else
@submit_button.click @message_post
else if @child_state == 'done'
@answer_area.attr("disabled", true)
@hint_area.attr('disabled', true)
@submit_button.hide()
if @child_type=="openended"
@skip_button.hide()
if @task_number<@task_count
@next_problem()
else
@reset_button.show()
find_assessment_elements: ->
@assessment = @$('select.assessment')
find_hint_elements: ->
@hint_area = @$('textarea.post_assessment')
save_answer: (event) =>
event.preventDefault()
if @child_state == 'initial'
data = {'student_answer' : @answer_area.val()}
$.postWithPrefix "#{@ajax_url}/save_answer", data, (response) =>
if response.success
@rubric_wrapper.html(response.rubric_html)
@child_state = 'assessing'
@find_assessment_elements()
@rebind()
else
@errors_area.html(response.error)
else
@errors_area.html('Problem state got out of sync. Try reloading the page.')
save_assessment: (event) =>
event.preventDefault()
if @child_state == 'assessing'
data = {'assessment' : @assessment.find(':selected').text()}
$.postWithPrefix "#{@ajax_url}/save_assessment", data, (response) =>
if response.success
@child_state = response.state
if @child_state == 'post_assessment'
@hint_wrapper.html(response.hint_html)
@find_hint_elements()
else if @child_state == 'done'
@message_wrapper.html(response.message_html)
@rebind()
else
@errors_area.html(response.error)
else
@errors_area.html('Problem state got out of sync. Try reloading the page.')
save_hint: (event) =>
event.preventDefault()
if @child_state == 'post_assessment'
data = {'hint' : @hint_area.val()}
$.postWithPrefix "#{@ajax_url}/save_post_assessment", data, (response) =>
if response.success
@message_wrapper.html(response.message_html)
@child_state = 'done'
@rebind()
else
@errors_area.html(response.error)
else
@errors_area.html('Problem state got out of sync. Try reloading the page.')
skip_post_assessment: =>
if @child_state == 'post_assessment'
$.postWithPrefix "#{@ajax_url}/skip_post_assessment", {}, (response) =>
if response.success
@child_state = 'done'
@rebind()
else
@errors_area.html(response.error)
else
@errors_area.html('Problem state got out of sync. Try reloading the page.')
reset: (event) =>
event.preventDefault()
if @child_state == 'done' or @allow_reset=="True"
$.postWithPrefix "#{@ajax_url}/reset", {}, (response) =>
if response.success
@answer_area.val('')
@rubric_wrapper.html('')
@hint_wrapper.html('')
@message_wrapper.html('')
@child_state = 'initial'
@combined_open_ended.after(response.html).remove()
@allow_reset="False"
@reinitialize(@element)
@rebind()
@reset_button.hide()
else
@errors_area.html(response.error)
else
@errors_area.html('Problem state got out of sync. Try reloading the page.')
next_problem: =>
if @child_state == 'done'
$.postWithPrefix "#{@ajax_url}/next_problem", {}, (response) =>
if response.success
@answer_area.val('')
@rubric_wrapper.html('')
@hint_wrapper.html('')
@message_wrapper.html('')
@child_state = 'initial'
@combined_open_ended.after(response.html).remove()
@reinitialize(@element)
@rebind()
@next_problem_button.hide()
if !response.allow_reset
@gentle_alert "Moved to next step."
else
@gentle_alert "Your score did not meet the criteria to move to the next step."
else
@errors_area.html(response.error)
else
@errors_area.html('Problem state got out of sync. Try reloading the page.')
gentle_alert: (msg) =>
if @el.find('.open-ended-alert').length
@el.find('.open-ended-alert').remove()
alert_elem = "<div class='open-ended-alert'>" + msg + "</div>"
@el.find('.open-ended-action').after(alert_elem)
@el.find('.open-ended-alert').css(opacity: 0).animate(opacity: 1, 700)
queueing: =>
if @child_state=="assessing" and @child_type=="openended"
if window.queuePollerID # Only one poller 'thread' per Problem
window.clearTimeout(window.queuePollerID)
window.queuePollerID = window.setTimeout(@poll, 10000)
poll: =>
$.postWithPrefix "#{@ajax_url}/check_for_score", (response) =>
if response.state == "done" or response.state=="post_assessment"
delete window.queuePollerID
location.reload()
else
window.queuePollerID = window.setTimeout(@poll, 10000)

View File

@@ -0,0 +1,139 @@
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
(function (requirejs, require, define) {
define('ElOutput', ['logme'], function (logme) {
return ElOutput;
function ElOutput(config, state) {
if ($.isPlainObject(config.functions.function)) {
processFuncObj(config.functions.function);
} else if ($.isArray(config.functions.function)) {
(function (c1) {
while (c1 < config.functions.function.length) {
if ($.isPlainObject(config.functions.function[c1])) {
processFuncObj(config.functions.function[c1]);
}
c1 += 1;
}
}(0));
}
return;
function processFuncObj(obj) {
var paramNames, funcString, func, el, disableAutoReturn, updateOnEvent;
// We are only interested in functions that are meant for output to an
// element.
if (
(typeof obj['@output'] !== 'string') ||
((obj['@output'].toLowerCase() !== 'element') && (obj['@output'].toLowerCase() !== 'none'))
) {
return;
}
if (typeof obj['@el_id'] !== 'string') {
logme('ERROR: You specified "output" as "element", but did not spify "el_id".');
return;
}
if (typeof obj['#text'] !== 'string') {
logme('ERROR: Function body is not defined.');
return;
}
updateOnEvent = 'slide';
if (
(obj.hasOwnProperty('@update_on') === true) &&
(typeof obj['@update_on'] === 'string') &&
((obj['@update_on'].toLowerCase() === 'slide') || (obj['@update_on'].toLowerCase() === 'change'))
) {
updateOnEvent = obj['@update_on'].toLowerCase();
}
disableAutoReturn = obj['@disable_auto_return'];
funcString = obj['#text'];
if (
(disableAutoReturn === undefined) ||
(
(typeof disableAutoReturn === 'string') &&
(disableAutoReturn.toLowerCase() !== 'true')
)
) {
if (funcString.search(/return/i) === -1) {
funcString = 'return ' + funcString;
}
} else {
if (funcString.search(/return/i) === -1) {
logme(
'ERROR: You have specified a JavaScript ' +
'function without a "return" statemnt. Your ' +
'function will return "undefined" by default.'
);
}
}
// Make sure that all HTML entities are converted to their proper
// ASCII text equivalents.
funcString = $('<div>').html(funcString).text();
paramNames = state.getAllParameterNames();
paramNames.push(funcString);
try {
func = Function.apply(null, paramNames);
} catch (err) {
logme(
'ERROR: The function body "' +
funcString +
'" was not converted by the Function constructor.'
);
logme('Error message: "' + err.message + '".');
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from string "' + funcString + '".' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
paramNames.pop();
return;
}
paramNames.pop();
if (obj['@output'].toLowerCase() !== 'none') {
el = $('#' + obj['@el_id']);
if (el.length !== 1) {
logme(
'ERROR: DOM element with ID "' + obj['@el_id'] + '" ' +
'not found. Dynamic element not created.'
);
return;
}
el.html(func.apply(window, state.getAllParameterValues()));
} else {
el = null;
func.apply(window, state.getAllParameterValues());
}
state.addDynamicEl(el, func, obj['@el_id'], updateOnEvent);
}
}
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)

View File

@@ -0,0 +1,113 @@
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
(function (requirejs, require, define) {
define('GLabelElOutput', ['logme'], function (logme) {
return GLabelElOutput;
function GLabelElOutput(config, state) {
if ($.isPlainObject(config.functions.function)) {
processFuncObj(config.functions.function);
} else if ($.isArray(config.functions.function)) {
(function (c1) {
while (c1 < config.functions.function.length) {
if ($.isPlainObject(config.functions.function[c1])) {
processFuncObj(config.functions.function[c1]);
}
c1 += 1;
}
}(0));
}
return;
function processFuncObj(obj) {
var paramNames, funcString, func, disableAutoReturn;
// We are only interested in functions that are meant for output to an
// element.
if (
(typeof obj['@output'] !== 'string') ||
(obj['@output'].toLowerCase() !== 'plot_label')
) {
return;
}
if (typeof obj['@el_id'] !== 'string') {
logme('ERROR: You specified "output" as "plot_label", but did not spify "el_id".');
return;
}
if (typeof obj['#text'] !== 'string') {
logme('ERROR: Function body is not defined.');
return;
}
disableAutoReturn = obj['@disable_auto_return'];
funcString = obj['#text'];
if (
(disableAutoReturn === undefined) ||
(
(typeof disableAutoReturn === 'string') &&
(disableAutoReturn.toLowerCase() !== 'true')
)
) {
if (funcString.search(/return/i) === -1) {
funcString = 'return ' + funcString;
}
} else {
if (funcString.search(/return/i) === -1) {
logme(
'ERROR: You have specified a JavaScript ' +
'function without a "return" statemnt. Your ' +
'function will return "undefined" by default.'
);
}
}
// Make sure that all HTML entities are converted to their proper
// ASCII text equivalents.
funcString = $('<div>').html(funcString).text();
paramNames = state.getAllParameterNames();
paramNames.push(funcString);
try {
func = Function.apply(null, paramNames);
} catch (err) {
logme(
'ERROR: The function body "' +
funcString +
'" was not converted by the Function constructor.'
);
logme('Error message: "' + err.message + '".');
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from string "' + funcString + '".' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
paramNames.pop();
return;
}
paramNames.pop();
state.plde.push({
'elId': obj['@el_id'],
'func': func
});
}
}
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)

View File

@@ -0,0 +1,23 @@
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
(function (requirejs, require, define) {
define('GeneralMethods', [], function () {
if (!String.prototype.trim) {
// http://blog.stevenlevithan.com/archives/faster-trim-javascript
String.prototype.trim = function trim(str) {
return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
};
}
return {
'module_name': 'GeneralMethods',
'module_status': 'OK'
};
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)

View File

@@ -0,0 +1,1496 @@
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
(function (requirejs, require, define) {
define('Graph', ['logme'], function (logme) {
return Graph;
function Graph(gstId, config, state) {
var plotDiv, dataSeries, functions, xaxis, yaxis, numPoints, xrange,
asymptotes, movingLabels, xTicksNames, yTicksNames, graphBarWidth, graphBarAlign;
// We need plot configuration settings. Without them we can't continue.
if ($.isPlainObject(config.plot) === false) {
return;
}
// We must have a graph container DIV element available in order to
// proceed.
plotDiv = $('#' + gstId + '_plot');
if (plotDiv.length === 0) {
logme('ERROR: Could not find the plot DIV with ID "' + gstId + '_plot".');
return;
}
if (plotDiv.width() === 0) {
plotDiv.width(300);
}
// Sometimes, when height is not explicitly set via CSS (or by some
// other means), it is 0 pixels by default. When Flot will try to plot
// a graph in this DIV with 0 height, then it will raise an error. To
// prevent this, we will set it to be equal to the width.
if (plotDiv.height() === 0) {
plotDiv.height(plotDiv.width());
}
plotDiv.css('position', 'relative');
// Configure some settings for the graph.
if (setGraphXRange() === false) {
logme('ERROR: Could not configure the xrange. Will not continue.');
return;
}
if (setGraphAxes() === false) {
logme('ERROR: Could not process configuration for the axes.');
return;
}
graphBarWidth = 1;
graphBarAlign = null;
getBarWidth();
getBarAlign();
// Get the user defined functions. If there aren't any, don't do
// anything else.
createFunctions();
if (functions.length === 0) {
logme('ERROR: No functions were specified, or something went wrong.');
return;
}
if (createMarkingsFunctions() === false) {
return;
}
if (createMovingLabelFunctions() === false) {
return;
}
// Create the initial graph and plot it for the user to see.
if (generateData() === true) {
updatePlot();
}
// Bind an event. Whenever some constant changes, the graph will be
// redrawn
state.bindUpdatePlotEvent(plotDiv, onUpdatePlot);
return;
function getBarWidth() {
if (config.plot.hasOwnProperty('bar_width') === false) {
return;
}
if (typeof config.plot.bar_width !== 'string') {
logme('ERROR: The parameter config.plot.bar_width must be a string.');
return;
}
if (isFinite(graphBarWidth = parseFloat(config.plot.bar_width)) === false) {
logme('ERROR: The parameter config.plot.bar_width is not a valid floating number.');
graphBarWidth = 1;
return;
}
return;
}
function getBarAlign() {
if (config.plot.hasOwnProperty('bar_align') === false) {
return;
}
if (typeof config.plot.bar_align !== 'string') {
logme('ERROR: The parameter config.plot.bar_align must be a string.');
return;
}
if (
(config.plot.bar_align.toLowerCase() !== 'left') &&
(config.plot.bar_align.toLowerCase() !== 'center')
) {
logme('ERROR: Property config.plot.bar_align can be one of "left", or "center".');
return;
}
graphBarAlign = config.plot.bar_align.toLowerCase();
return;
}
function createMovingLabelFunctions() {
var c1, returnStatus;
returnStatus = true;
movingLabels = [];
if (config.plot.hasOwnProperty('moving_label') !== true) {
returnStatus = true;
} else if ($.isPlainObject(config.plot.moving_label) === true) {
if (processMovingLabel(config.plot.moving_label) === false) {
returnStatus = false;
}
} else if ($.isArray(config.plot.moving_label) === true) {
for (c1 = 0; c1 < config.plot.moving_label.length; c1++) {
if (processMovingLabel(config.plot.moving_label[c1]) === false) {
returnStatus = false;
}
}
}
return returnStatus;
}
function processMovingLabel(obj) {
var labelText, funcString, disableAutoReturn, paramNames, func,
fontWeight, fontColor;
if (obj.hasOwnProperty('@text') === false) {
logme('ERROR: You did not define a "text" attribute for the moving_label.');
return false;
}
if (typeof obj['@text'] !== 'string') {
logme('ERROR: "text" attribute is not a string.');
return false;
}
labelText = obj['@text'];
if (obj.hasOwnProperty('#text') === false) {
logme('ERROR: moving_label is missing function declaration.');
return false;
}
if (typeof obj['#text'] !== 'string') {
logme('ERROR: Function declaration is not a string.');
return false;
}
funcString = obj['#text'];
fontColor = 'black';
if (
(obj.hasOwnProperty('@color') === true) &&
(typeof obj['@color'] === 'string')
) {
fontColor = obj['@color'];
}
fontWeight = 'normal';
if (
(obj.hasOwnProperty('@weight') === true) &&
(typeof obj['@weight'] === 'string')
) {
if (
(obj['@weight'].toLowerCase() === 'normal') ||
(obj['@weight'].toLowerCase() === 'bold')
) {
fontWeight = obj['@weight'];
} else {
logme('ERROR: Moving label can have a weight property of "normal" or "bold".');
}
}
disableAutoReturn = obj['@disable_auto_return'];
funcString = $('<div>').html(funcString).text();
if (
(disableAutoReturn === undefined) ||
(
(typeof disableAutoReturn === 'string') &&
(disableAutoReturn.toLowerCase() !== 'true')
)
) {
if (funcString.search(/return/i) === -1) {
funcString = 'return ' + funcString;
}
} else {
if (funcString.search(/return/i) === -1) {
logme(
'ERROR: You have specified a JavaScript ' +
'function without a "return" statemnt. Your ' +
'function will return "undefined" by default.'
);
}
}
paramNames = state.getAllParameterNames();
paramNames.push(funcString);
try {
func = Function.apply(null, paramNames);
} catch (err) {
logme(
'ERROR: The function body "' +
funcString +
'" was not converted by the Function constructor.'
);
logme('Error message: "' + err.message + '"');
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from the string "' + funcString + '".' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
paramNames.pop();
return false;
}
paramNames.pop();
movingLabels.push({
'labelText': labelText,
'func': func,
'el': null,
'fontColor': fontColor,
'fontWeight': fontWeight
});
return true;
}
function createMarkingsFunctions() {
var c1, paramNames, returnStatus;
returnStatus = true;
asymptotes = [];
paramNames = state.getAllParameterNames();
if ($.isPlainObject(config.plot.asymptote)) {
if (processAsymptote(config.plot.asymptote) === false) {
returnStatus = false;
}
} else if ($.isArray(config.plot.asymptote)) {
for (c1 = 0; c1 < config.plot.asymptote.length; c1 += 1) {
if (processAsymptote(config.plot.asymptote[c1]) === false) {
returnStatus = false;
}
}
}
return returnStatus;
// Read configuration options for asymptotes, and store them as
// an array of objects. Each object will have 3 properties:
//
// - color: the color of the asymptote line
// - type: 'x' (vertical), or 'y' (horizontal)
// - func: the function that will generate the value at which
// the asymptote will be plotted; i.e. x = func(), or
// y = func(); for now only horizontal and vertical
// asymptotes are supported
//
// Since each asymptote can have a variable function - function
// that relies on some parameter specified in the config - we will
// generate each asymptote just before we draw the graph. See:
//
// function updatePlot()
// function generateMarkings()
//
// Asymptotes are really thin rectangles implemented via the Flot's
// markings option.
function processAsymptote(asyObj) {
var newAsyObj, funcString, func;
newAsyObj = {};
if (typeof asyObj['@type'] === 'string') {
if (asyObj['@type'].toLowerCase() === 'x') {
newAsyObj.type = 'x';
} else if (asyObj['@type'].toLowerCase() === 'y') {
newAsyObj.type = 'y';
} else {
logme('ERROR: Attribute "type" for asymptote can be "x" or "y".');
return false;
}
} else {
logme('ERROR: Attribute "type" for asymptote is not specified.');
return false;
}
if (typeof asyObj['#text'] === 'string') {
funcString = asyObj['#text'];
} else {
logme('ERROR: Function body for asymptote is not specified.');
return false;
}
newAsyObj.color = '#000';
if (typeof asyObj['@color'] === 'string') {
newAsyObj.color = asyObj['@color'];
}
newAsyObj.label = false;
if (
(asyObj.hasOwnProperty('@label') === true) &&
(typeof asyObj['@label'] === 'string')
) {
newAsyObj.label = asyObj['@label'];
}
funcString = $('<div>').html(funcString).text();
disableAutoReturn = asyObj['@disable_auto_return'];
if (
(disableAutoReturn === undefined) ||
(
(typeof disableAutoReturn === 'string') &&
(disableAutoReturn.toLowerCase() !== 'true')
)
) {
if (funcString.search(/return/i) === -1) {
funcString = 'return ' + funcString;
}
} else {
if (funcString.search(/return/i) === -1) {
logme(
'ERROR: You have specified a JavaScript ' +
'function without a "return" statemnt. Your ' +
'function will return "undefined" by default.'
);
}
}
paramNames.push(funcString);
try {
func = Function.apply(null, paramNames);
} catch (err) {
logme('ERROR: Asymptote function body could not be converted to function object.');
logme('Error message: "".' + err.message);
return false;
}
paramNames.pop();
newAsyObj.func = func;
asymptotes.push(newAsyObj);
return true;
}
}
function setGraphAxes() {
xaxis = {
'tickFormatter': null
};
if (typeof config.plot['xticks'] === 'string') {
if (processTicks(config.plot['xticks'], xaxis, 'xunits') === false) {
logme('ERROR: Could not process the ticks for x-axis.');
return false;
}
} else {
logme('MESSAGE: "xticks" were not specified. Using defaults.');
return false;
}
yaxis = {
'tickFormatter': null
};
if (typeof config.plot['yticks'] === 'string') {
if (processTicks(config.plot['yticks'], yaxis, 'yunits') === false) {
logme('ERROR: Could not process the ticks for y-axis.');
return false;
}
} else {
logme('MESSAGE: "yticks" were not specified. Using defaults.');
return false;
}
xTicksNames = null;
yTicksNames = null;
if (checkForTicksNames('x') === false) {
return false;
}
if (checkForTicksNames('y') === false) {
return false;
}
return true;
//
// function checkForTicksNames(axisName)
//
// The parameter "axisName" can be either "x" or "y" (string). Depending on it, the function
// will set "xTicksNames" or "yTicksNames" private variable.
//
// This function does not return anything. It sets the private variable "xTicksNames" ("yTicksNames")
// to the object converted by JSON.parse from the XML parameter "plot.xticks_names" ("plot.yticks_names").
// If the "plot.xticks_names" ("plot.yticks_names") is missing or it is not a valid JSON string, then
// "xTicksNames" ("yTicksNames") will be set to "null".
//
// Depending on the "xTicksNames" ("yTicksNames") being "null" or an object, the plot will either draw
// number ticks, or use the names specified by the opbject.
//
function checkForTicksNames(axisName) {
var tmpObj;
if ((axisName !== 'x') && (axisName !== 'y')) {
// This is not an error. This funcion should simply stop executing.
return true;
}
if (
(config.plot.hasOwnProperty(axisName + 'ticks_names') === true) ||
(typeof config.plot[axisName + 'ticks_names'] === 'string')
) {
try {
tmpObj = JSON.parse(config.plot[axisName + 'ticks_names']);
} catch (err) {
logme(
'ERROR: plot.' + axisName + 'ticks_names is not a valid JSON string.',
'Error message: "' + err.message + '".'
);
return false;
}
if (axisName === 'x') {
xTicksNames = tmpObj;
xaxis.tickFormatter = xAxisTickFormatter;
}
// At this point, we are certain that axisName = 'y'.
else {
yTicksNames = tmpObj;
yaxis.tickFormatter = yAxisTickFormatter;
}
}
}
function processTicks(ticksStr, ticksObj, unitsType) {
var ticksBlobs, tempFloat, tempTicks, c1, c2;
// The 'ticks' setting is a string containing 3 floating-point
// numbers.
ticksBlobs = ticksStr.split(',');
if (ticksBlobs.length !== 3) {
logme('ERROR: Did not get 3 blobs from ticksStr = "' + ticksStr + '".');
return false;
}
tempFloat = parseFloat(ticksBlobs[0]);
if (isNaN(tempFloat) === false) {
ticksObj.min = tempFloat;
} else {
logme('ERROR: Invalid "min". ticksBlobs[0] = ', ticksBlobs[0]);
return false;
}
tempFloat = parseFloat(ticksBlobs[1]);
if (isNaN(tempFloat) === false) {
ticksObj.tickSize = tempFloat;
} else {
logme('ERROR: Invalid "tickSize". ticksBlobs[1] = ', ticksBlobs[1]);
return false;
}
tempFloat = parseFloat(ticksBlobs[2]);
if (isNaN(tempFloat) === false) {
ticksObj.max = tempFloat;
} else {
logme('ERROR: Invalid "max". ticksBlobs[2] = ', ticksBlobs[2]);
return false;
}
// Is the starting tick to the left of the ending tick (on the
// x-axis)? If not, set default starting and ending tick.
if (ticksObj.min >= ticksObj.max) {
logme('ERROR: Ticks min >= max.');
return false;
}
// Make sure the range makes sense - i.e. that there are at
// least 3 ticks. If not, set a tickSize which will produce
// 11 ticks. tickSize is the spacing between the ticks.
if (ticksObj.tickSize > ticksObj.max - ticksObj.min) {
logme('ERROR: tickSize > max - min.');
return false;
}
// units: change last tick to units
if (typeof config.plot[unitsType] === 'string') {
tempTicks = [];
for (c1 = ticksObj.min; c1 <= ticksObj.max; c1 += ticksObj.tickSize) {
c2 = roundToPrec(c1, ticksObj.tickSize);
tempTicks.push([c2, c2]);
}
tempTicks.pop();
tempTicks.push([
roundToPrec(ticksObj.max, ticksObj.tickSize),
config.plot[unitsType]
]);
ticksObj.tickSize = null;
ticksObj.ticks = tempTicks;
}
return true;
function roundToPrec(num, prec) {
var c1, tn1, tn2, digitsBefore, digitsAfter;
tn1 = Math.abs(num);
tn2 = Math.abs(prec);
// Find out number of digits BEFORE the decimal point.
c1 = 0;
tn1 = Math.abs(num);
while (tn1 >= 1) {
c1 += 1;
tn1 /= 10;
}
digitsBefore = c1;
// Find out number of digits AFTER the decimal point.
c1 = 0;
tn1 = Math.abs(num);
while (Math.round(tn1) !== tn1) {
c1 += 1;
tn1 *= 10;
}
digitsAfter = c1;
// For precision, find out number of digits AFTER the
// decimal point.
c1 = 0;
while (Math.round(tn2) !== tn2) {
c1 += 1;
tn2 *= 10;
}
// If precision is more than 1 (no digits after decimal
// points).
if (c1 === 0) {
return num;
}
// If the precision contains digits after the decimal
// point, we apply special rules.
else {
tn1 = Math.abs(num);
// if (digitsAfter > c1) {
tn1 = tn1.toFixed(c1);
// } else {
// tn1 = tn1.toPrecision(digitsBefore + digitsAfter);
// }
}
if (num < 0) {
return -tn1;
}
return tn1;
}
}
}
function setGraphXRange() {
var xRangeStr, xRangeBlobs, tempNum, allParamNames, funcString,
disableAutoReturn;
xrange = {};
if ($.isPlainObject(config.plot.xrange) === false) {
logme(
'ERROR: Expected config.plot.xrange to be an object. ' +
'It is not.'
);
logme('config.plot.xrange = ', config.plot.xrange);
return false;
}
if (config.plot.xrange.hasOwnProperty('min') === false) {
logme(
'ERROR: Expected config.plot.xrange.min to be ' +
'present. It is not.'
);
return false;
}
disableAutoReturn = false;
if (typeof config.plot.xrange.min === 'string') {
funcString = config.plot.xrange.min;
} else if (
($.isPlainObject(config.plot.xrange.min) === true) &&
(config.plot.xrange.min.hasOwnProperty('#text') === true) &&
(typeof config.plot.xrange.min['#text'] === 'string')
) {
funcString = config.plot.xrange.min['#text'];
disableAutoReturn =
config.plot.xrange.min['@disable_auto_return'];
if (
(disableAutoReturn === undefined) ||
(
(typeof disableAutoReturn === 'string') &&
(disableAutoReturn.toLowerCase() !== 'true')
)
) {
disableAutoReturn = false;
} else {
disableAutoReturn = true;
}
} else {
logme(
'ERROR: Could not get a function definition for ' +
'xrange.min property.'
);
return false;
}
funcString = $('<div>').html(funcString).text();
if (disableAutoReturn === false) {
if (funcString.search(/return/i) === -1) {
funcString = 'return ' + funcString;
}
} else {
if (funcString.search(/return/i) === -1) {
logme(
'ERROR: You have specified a JavaScript ' +
'function without a "return" statemnt. Your ' +
'function will return "undefined" by default.'
);
}
}
allParamNames = state.getAllParameterNames();
allParamNames.push(funcString);
try {
xrange.min = Function.apply(null, allParamNames);
} catch (err) {
logme(
'ERROR: could not create a function from the string "' +
funcString + '" for xrange.min.'
);
logme('Error message: "' + err.message + '"');
$('#' + gstId).html(
'<div style="color: red;">' + 'ERROR IN ' +
'XML: Could not create a function from the string "' +
funcString + '" for xrange.min.' + '</div>'
);
$('#' + gstId).append(
'<div style="color: red;">' + 'Error ' +
'message: "' + err.message + '".' + '</div>'
);
return false;
}
allParamNames.pop();
if (config.plot.xrange.hasOwnProperty('max') === false) {
logme(
'ERROR: Expected config.plot.xrange.max to be ' +
'present. It is not.'
);
return false;
}
disableAutoReturn = false;
if (typeof config.plot.xrange.max === 'string') {
funcString = config.plot.xrange.max;
} else if (
($.isPlainObject(config.plot.xrange.max) === true) &&
(config.plot.xrange.max.hasOwnProperty('#text') === true) &&
(typeof config.plot.xrange.max['#text'] === 'string')
) {
funcString = config.plot.xrange.max['#text'];
disableAutoReturn =
config.plot.xrange.max['@disable_auto_return'];
if (
(disableAutoReturn === undefined) ||
(
(typeof disableAutoReturn === 'string') &&
(disableAutoReturn.toLowerCase() !== 'true')
)
) {
disableAutoReturn = false;
} else {
disableAutoReturn = true;
}
} else {
logme(
'ERROR: Could not get a function definition for ' +
'xrange.max property.'
);
return false;
}
funcString = $('<div>').html(funcString).text();
if (disableAutoReturn === false) {
if (funcString.search(/return/i) === -1) {
funcString = 'return ' + funcString;
}
} else {
if (funcString.search(/return/i) === -1) {
logme(
'ERROR: You have specified a JavaScript ' +
'function without a "return" statemnt. Your ' +
'function will return "undefined" by default.'
);
}
}
allParamNames.push(funcString);
try {
xrange.max = Function.apply(null, allParamNames);
} catch (err) {
logme(
'ERROR: could not create a function from the string "' +
funcString + '" for xrange.max.'
);
logme('Error message: "' + err.message + '"');
$('#' + gstId).html(
'<div style="color: red;">' + 'ERROR IN ' +
'XML: Could not create a function from the string "' +
funcString + '" for xrange.max.' + '</div>'
);
$('#' + gstId).append(
'<div style="color: red;">' + 'Error message: "' +
err.message + '".' + '</div>'
);
return false;
}
allParamNames.pop();
tempNum = parseInt(config.plot.num_points, 10);
if (isFinite(tempNum) === false) {
tempNum = plotDiv.width() / 5.0;
}
if (
(tempNum < 2) &&
(tempNum > 1000)
) {
logme(
'ERROR: Number of points is outside the allowed range ' +
'[2, 1000]'
);
logme('config.plot.num_points = ' + tempNum);
return false;
}
numPoints = tempNum;
return true;
}
function createFunctions() {
var c1;
functions = [];
if (typeof config.functions === 'undefined') {
logme('ERROR: config.functions is undefined.');
return;
}
if (typeof config.functions.function === 'string') {
// If just one function string is present.
addFunction(config.functions.function);
} else if ($.isPlainObject(config.functions.function) === true) {
// If a function is present, but it also has properties
// defined.
callAddFunction(config.functions.function);
} else if ($.isArray(config.functions.function)) {
// If more than one function is defined.
for (c1 = 0; c1 < config.functions.function.length; c1 += 1) {
// For each definition, we must check if it is a simple
// string definition, or a complex one with properties.
if (typeof config.functions.function[c1] === 'string') {
// Simple string.
addFunction(config.functions.function[c1]);
} else if ($.isPlainObject(config.functions.function[c1])) {
// Properties are present.
callAddFunction(config.functions.function[c1]);
}
}
} else {
logme('ERROR: config.functions.function is of an unsupported type.');
return;
}
return;
// This function will reduce code duplication. We have to call
// the function addFunction() several times passing object
// properties as parameters. Rather than writing them out every
// time, we will have a single place where it is done.
function callAddFunction(obj) {
if (
(obj.hasOwnProperty('@output')) &&
(typeof obj['@output'] === 'string')
) {
// If this function is meant to be calculated for an
// element then skip it.
if ((obj['@output'].toLowerCase() === 'element') ||
(obj['@output'].toLowerCase() === 'none')) {
return;
}
// If this function is meant to be calculated for a
// dynamic element in a label then skip it.
else if (obj['@output'].toLowerCase() === 'plot_label') {
return;
}
// It is an error if '@output' is not 'element',
// 'plot_label', or 'graph'. However, if the '@output'
// attribute is omitted, we will not have reached this.
else if (obj['@output'].toLowerCase() !== 'graph') {
logme(
'ERROR: Function "output" attribute can be ' +
'either "element", "plot_label", "none" or "graph".'
);
return;
}
}
// The user did not specify an "output" attribute, or it is
// "graph".
addFunction(
obj['#text'],
obj['@color'],
obj['@line'],
obj['@dot'],
obj['@label'],
obj['@point_size'],
obj['@fill_area'],
obj['@bar'],
obj['@disable_auto_return']
);
}
function addFunction(funcString, color, line, dot, label,
pointSize, fillArea, bar, disableAutoReturn) {
var newFunctionObject, func, paramNames, c1, rgxp;
// The main requirement is function string. Without it we can't
// create a function, and the series cannot be calculated.
if (typeof funcString !== 'string') {
return;
}
// Make sure that any HTML entities that were escaped will be
// unescaped. This is done because if a string with escaped
// HTML entities is passed to the Function() constructor, it
// will break.
funcString = $('<div>').html(funcString).text();
// If the user did not specifically turn off this feature,
// check if the function string contains a 'return', and
// prepend a 'return ' to the string if one, or more, is not
// found.
if (
(disableAutoReturn === undefined) ||
(
(typeof disableAutoReturn === 'string') &&
(disableAutoReturn.toLowerCase() !== 'true')
)
) {
if (funcString.search(/return/i) === -1) {
funcString = 'return ' + funcString;
}
} else {
if (funcString.search(/return/i) === -1) {
logme(
'ERROR: You have specified a JavaScript ' +
'function without a "return" statemnt. Your ' +
'function will return "undefined" by default.'
);
}
}
// Some defaults. If no options are set for the graph, we will
// make sure that at least a line is drawn for a function.
newFunctionObject = {
'line': true,
'dot': false,
'bars': false
};
// Get all of the parameter names defined by the user in the
// XML.
paramNames = state.getAllParameterNames();
// The 'x' is always one of the function parameters.
paramNames.push('x');
// Must make sure that the function body also gets passed to
// the Function constructor.
paramNames.push(funcString);
// Create the function from the function string, and all of the
// available parameters AND the 'x' variable as it's parameters.
// For this we will use the built-in Function object
// constructor.
//
// If something goes wrong during this step, most
// likely the user supplied an invalid JavaScript function body
// string. In this case we will not proceed.
try {
func = Function.apply(null, paramNames);
} catch (err) {
logme(
'ERROR: The function body "' +
funcString +
'" was not converted by the Function constructor.'
);
logme('Error message: "' + err.message + '"');
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from the string "' + funcString + '".' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
paramNames.pop();
paramNames.pop();
return;
}
// Return the array back to original state. Remember that it is
// a pointer to original array which is stored in state object.
paramNames.pop();
paramNames.pop();
newFunctionObject['func'] = func;
if (typeof color === 'string') {
newFunctionObject['color'] = color;
}
if (typeof line === 'string') {
if (line.toLowerCase() === 'true') {
newFunctionObject['line'] = true;
} else if (line.toLowerCase() === 'false') {
newFunctionObject['line'] = false;
}
}
if (typeof dot === 'string') {
if (dot.toLowerCase() === 'true') {
newFunctionObject['dot'] = true;
} else if (dot.toLowerCase() === 'false') {
newFunctionObject['dot'] = false;
}
}
if (typeof pointSize === 'string') {
newFunctionObject['pointSize'] = pointSize;
}
if (typeof bar === 'string') {
if (bar.toLowerCase() === 'true') {
newFunctionObject['bars'] = true;
} else if (bar.toLowerCase() === 'false') {
newFunctionObject['bars'] = false;
}
}
if (newFunctionObject['bars'] === true) {
newFunctionObject['line'] = false;
newFunctionObject['dot'] = false;
// To do: See if need to do anything here.
} else if (
(newFunctionObject['dot'] === false) &&
(newFunctionObject['line'] === false)
) {
newFunctionObject['line'] = true;
}
if (newFunctionObject['line'] === true) {
if (typeof fillArea === 'string') {
if (fillArea.toLowerCase() === 'true') {
newFunctionObject['fillArea'] = true;
} else if (fillArea.toLowerCase() === 'false') {
newFunctionObject['fillArea'] = false;
} else {
logme('ERROR: The attribute fill_area should be either "true" or "false".');
logme('fill_area = "' + fillArea + '".');
return;
}
}
}
if (typeof label === 'string') {
newFunctionObject.specialLabel = false;
newFunctionObject.pldeHash = [];
// Let's check the label against all of the plde objects.
// plde is an abbreviation for Plot Label Dynamic Elements.
for (c1 = 0; c1 < state.plde.length; c1 += 1) {
rgxp = new RegExp(state.plde[c1].elId, 'g');
// If we find a dynamic element in the label, we will
// hash the current plde object, and indicate that this
// is a special label.
if (rgxp.test(label) === true) {
newFunctionObject.specialLabel = true;
newFunctionObject.pldeHash.push(state.plde[c1]);
}
}
newFunctionObject.label = label;
} else {
newFunctionObject.label = false;
}
functions.push(newFunctionObject);
}
}
// The callback that will be called whenever a constant changes (gets
// updated via a slider or a text input).
function onUpdatePlot(event) {
if (generateData() === true) {
updatePlot();
}
}
function generateData() {
var c0, c1, c3, functionObj, seriesObj, dataPoints, paramValues, x, y,
start, end, step, numNotUndefined;
paramValues = state.getAllParameterValues();
dataSeries = [];
for (c0 = 0; c0 < functions.length; c0 += 1) {
functionObj = functions[c0];
try {
start = xrange.min.apply(window, paramValues);
} catch (err) {
logme('ERROR: Could not determine xrange start.');
logme('Error message: "' + err.message + '".');
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not determine xrange start from defined function.' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
return false;
}
try {
end = xrange.max.apply(window, paramValues);
} catch (err) {
logme('ERROR: Could not determine xrange end.');
logme('Error message: "' + err.message + '".');
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not determine xrange end from defined function.' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
return false;
}
seriesObj = {};
dataPoints = [];
// For counting number of points added. In the end we will
// compare this number to 'numPoints' specified in the config
// JSON.
c1 = 0;
step = (end - start) / (numPoints - 1);
// Generate the data points.
for (x = start; x <= end; x += step) {
// Push the 'x' variable to the end of the parameter array.
paramValues.push(x);
// We call the user defined function, passing all of the
// available parameter values. Inside this function they
// will be accessible by their names.
try {
y = functionObj.func.apply(window, paramValues);
} catch (err) {
logme('ERROR: Could not generate data.');
logme('Error message: "' + err.message + '".');
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not generate data from defined function.' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
return false;
}
// Return the paramValues array to how it was before we
// added 'x' variable to the end of it.
paramValues.pop();
// Add the generated point to the data points set.
dataPoints.push([x, y]);
c1 += 1;
}
// If the last point did not get included because of rounding
// of floating-point number addition, then we will include it
// manually.
if (c1 != numPoints) {
x = end;
paramValues.push(x);
try {
y = functionObj.func.apply(window, paramValues);
} catch (err) {
logme('ERROR: Could not generate data.');
logme('Error message: "' + err.message + '".');
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not generate data from function.' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
return false;
}
paramValues.pop();
dataPoints.push([x, y]);
}
// Put the entire data points set into the series object.
seriesObj.data = dataPoints;
// See if user defined a specific color for this function.
if (functionObj.hasOwnProperty('color') === true) {
seriesObj.color = functionObj.color;
}
// See if a user defined a label for this function.
if (functionObj.label !== false) {
if (functionObj.specialLabel === true) {
(function (c1) {
var tempLabel;
tempLabel = functionObj.label;
while (c1 < functionObj.pldeHash.length) {
tempLabel = tempLabel.replace(
functionObj.pldeHash[c1].elId,
functionObj.pldeHash[c1].func.apply(
window,
state.getAllParameterValues()
)
);
c1 += 1;
}
seriesObj.label = tempLabel;
}(0));
} else {
seriesObj.label = functionObj.label;
}
}
// Should the data points be connected by a line?
seriesObj.lines = {
'show': functionObj.line
};
if (functionObj.hasOwnProperty('fillArea') === true) {
seriesObj.lines.fill = functionObj.fillArea;
}
// Should each data point be represented by a point on the
// graph?
seriesObj.points = {
'show': functionObj.dot
};
seriesObj.bars = {
'show': functionObj.bars,
'barWidth': graphBarWidth
};
if (graphBarAlign !== null) {
seriesObj.bars.align = graphBarAlign;
}
if (functionObj.hasOwnProperty('pointSize')) {
seriesObj.points.radius = functionObj.pointSize;
}
// Add the newly created series object to the series set which
// will be plotted by Flot.
dataSeries.push(seriesObj);
}
if (graphBarAlign === null) {
for (c0 = 0; c0 < numPoints; c0 += 1) {
// Number of points that have a value other than 'undefined' (undefined).
numNotUndefined = 0;
for (c1 = 0; c1 < dataSeries.length; c1 += 1) {
if (dataSeries[c1].bars.show === false) {
continue;
}
if (isFinite(parseInt(dataSeries[c1].data[c0][1])) === true) {
numNotUndefined += 1;
}
}
c3 = 0;
for (c1 = 0; c1 < dataSeries.length; c1 += 1) {
if (dataSeries[c1].bars.show === false) {
continue;
}
dataSeries[c1].data[c0][0] -= graphBarWidth * (0.5 * numNotUndefined - c3);
if (isFinite(parseInt(dataSeries[c1].data[c0][1])) === true) {
c3 += 1;
}
}
}
}
for (c0 = 0; c0 < asymptotes.length; c0 += 1) {
// If the user defined a label for this asympote, then the
// property 'label' will be a string (in the other case it is
// a boolean value 'false'). We will create an empty data set,
// and add to it a label. This solution is a bit _wrong_ , but
// it will have to do for now. Flot JS does not provide a way
// to add labels to markings, and we use markings to generate
// asymptotes.
if (asymptotes[c0].label !== false) {
dataSeries.push({
'data': [],
'label': asymptotes[c0].label,
'color': asymptotes[c0].color
});
}
}
return true;
} // End-of: function generateData
function updatePlot() {
var paramValues, plotObj;
paramValues = state.getAllParameterValues();
if (xaxis.tickFormatter !== null) {
xaxis.ticks = null;
}
if (yaxis.tickFormatter !== null) {
yaxis.ticks = null;
}
// Tell Flot to draw the graph to our specification.
plotObj = $.plot(
plotDiv,
dataSeries,
{
'xaxis': xaxis,
'yaxis': yaxis,
'legend': {
// To show the legend or not. Note, even if 'show' is
// 'true', the legend will only show if labels are
// provided for at least one of the series that are
// going to be plotted.
'show': true,
// A floating point number in the range [0, 1]. The
// smaller the number, the more transparent will the
// legend background become.
'backgroundOpacity': 0
},
'grid': {
'markings': generateMarkings()
}
}
);
updateMovingLabels();
// The first time that the graph gets added to the page, the legend
// is created from scratch. When it appears, MathJax works some
// magic, and all of the specially marked TeX gets rendered nicely.
// The next time when we update the graph, no such thing happens.
// We must ask MathJax to typeset the legend again (well, we will
// ask it to look at our entire graph DIV), the next time it's
// worker queue is available.
MathJax.Hub.Queue([
'Typeset',
MathJax.Hub,
plotDiv.attr('id')
]);
return;
function updateMovingLabels() {
var c1, labelCoord, pointOffset;
for (c1 = 0; c1 < movingLabels.length; c1 += 1) {
if (movingLabels[c1].el === null) {
movingLabels[c1].el = $(
'<div>' +
movingLabels[c1].labelText +
'</div>'
);
movingLabels[c1].el.css('position', 'absolute');
movingLabels[c1].el.css('color', movingLabels[c1].fontColor);
movingLabels[c1].el.css('font-weight', movingLabels[c1].fontWeight);
movingLabels[c1].el.appendTo(plotDiv);
movingLabels[c1].elWidth = movingLabels[c1].el.width();
movingLabels[c1].elHeight = movingLabels[c1].el.height();
} else {
movingLabels[c1].el.detach();
movingLabels[c1].el.appendTo(plotDiv);
}
labelCoord = movingLabels[c1].func.apply(window, paramValues);
pointOffset = plotObj.pointOffset({'x': labelCoord.x, 'y': labelCoord.y});
movingLabels[c1].el.css('left', pointOffset.left - 0.5 * movingLabels[c1].elWidth);
movingLabels[c1].el.css('top', pointOffset.top - 0.5 * movingLabels[c1].elHeight);
}
}
// Generate markings to represent asymptotes defined by the user.
// See the following function for more details:
//
// function processAsymptote()
//
function generateMarkings() {
var c1, asymptote, markings, val;
markings = [];
for (c1 = 0; c1 < asymptotes.length; c1 += 1) {
asymptote = asymptotes[c1];
try {
val = asymptote.func.apply(window, paramValues);
} catch (err) {
logme('ERROR: Could not generate value from asymptote function.');
logme('Error message: ', err.message);
continue;
}
if (asymptote.type === 'x') {
markings.push({
'color': asymptote.color,
'lineWidth': 2,
'xaxis': {
'from': val,
'to': val
}
});
} else {
markings.push({
'color': asymptote.color,
'lineWidth': 2,
'yaxis': {
'from': val,
'to': val
}
});
}
}
return markings;
}
}
function xAxisTickFormatter(val, axis) {
if (xTicksNames.hasOwnProperty(val.toFixed(axis.tickDecimals)) === true) {
return xTicksNames[val.toFixed(axis.tickDecimals)];
}
return '';
}
function yAxisTickFormatter(val, axis) {
if (yTicksNames.hasOwnProperty(val.toFixed(axis.tickDecimals)) === true) {
return yTicksNames[val.toFixed(axis.tickDecimals)];
}
return '';
}
}
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)

View File

@@ -0,0 +1,20 @@
/*
* We will add a function that will be called for all GraphicalSliderTool
* xmodule module instances. It must be available globally by design of
* xmodule.
*/
window.GraphicalSliderTool = function (el) {
// All the work will be performed by the GstMain module. We will get access
// to it, and all it's dependencies, via Require JS. Currently Require JS
// is namespaced and is available via a global object RequireJS.
RequireJS.require(['GstMain'], function (GstMain) {
// The GstMain module expects the DOM ID of a Graphical Slider Tool
// element. Since we are given a <section> element which might in
// theory contain multiple graphical_slider_tool <div> elements (each
// with a unique DOM ID), we will iterate over all children, and for
// each match, we will call GstMain module.
$(el).children('.graphical_slider_tool').each(function (index, value) {
GstMain($(value).attr('id'));
});
});
};

View File

@@ -0,0 +1,84 @@
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
(function (requirejs, require, define) {
define(
'GstMain',
// Even though it is not explicitly in this module, we have to specify
// 'GeneralMethods' as a dependency. It expands some of the core JS objects
// with additional useful methods that are used in other modules.
['State', 'GeneralMethods', 'Sliders', 'Inputs', 'Graph', 'ElOutput', 'GLabelElOutput', 'logme'],
function (State, GeneralMethods, Sliders, Inputs, Graph, ElOutput, GLabelElOutput, logme) {
return GstMain;
function GstMain(gstId) {
var config, gstClass, state;
if ($('#' + gstId).attr('data-processed') !== 'processed') {
$('#' + gstId).attr('data-processed', 'processed');
} else {
logme('MESSAGE: Already processed GST with ID ' + gstId + '. Skipping.');
return;
}
// Get the JSON configuration, parse it, and store as an object.
try {
config = JSON.parse($('#' + gstId + '_json').html()).root;
} catch (err) {
logme('ERROR: could not parse config JSON.');
logme('$("#" + gstId + "_json").html() = ', $('#' + gstId + '_json').html());
logme('JSON.parse(...) = ', JSON.parse($('#' + gstId + '_json').html()));
logme('config = ', config);
return;
}
// Get the class name of the GST. All elements are assigned a class
// name that is based on the class name of the GST. For example, inputs
// are assigned a class name '{GST class name}_input'.
if (typeof config['@class'] !== 'string') {
logme('ERROR: Could not get the class name of GST.');
logme('config["@class"] = ', config['@class']);
return;
}
gstClass = config['@class'];
// Parse the configuration settings for parameters, and store them in a
// state object.
state = State(gstId, config);
// It is possible that something goes wrong while extracting parameters
// from the JSON config object. In this case, we will not continue.
if (state === undefined) {
logme('ERROR: The state object was not initialized properly.');
return;
}
// Create the sliders and the text inputs, attaching them to
// appropriate parameters.
Sliders(gstId, state);
Inputs(gstId, gstClass, state);
// Configure functions that output to an element instead of the graph.
ElOutput(config, state);
// Configure functions that output to an element instead of the graph
// label.
GLabelElOutput(config, state);
// Configure and display the graph. Attach event for the graph to be
// updated on any change of a slider or a text input.
Graph(gstId, config, state);
}
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)

View File

@@ -0,0 +1,88 @@
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
(function (requirejs, require, define) {
define('Inputs', ['logme'], function (logme) {
return Inputs;
function Inputs(gstId, gstClass, state) {
var c1, paramName, allParamNames;
allParamNames = state.getAllParameterNames();
for (c1 = 0; c1 < allParamNames.length; c1 += 1) {
$('#' + gstId).find('.' + gstClass + '_input').each(function (index, value) {
var inputDiv, paramName;
paramName = allParamNames[c1];
inputDiv = $(value);
if (paramName === inputDiv.data('var')) {
createInput(inputDiv, paramName);
}
});
}
return;
function createInput(inputDiv, paramName) {
var paramObj;
paramObj = state.getParamObj(paramName);
// Check that the retrieval went OK.
if (paramObj === undefined) {
logme('ERROR: Could not get a paramObj for parameter "' + paramName + '".');
return;
}
// Bind a function to the 'change' event. Whenever the user changes
// the value of this text input, and presses 'enter' (or clicks
// somewhere else on the page), this event will be triggered, and
// our callback will be called.
inputDiv.bind('change', inputOnChange);
inputDiv.val(paramObj.value);
// Lets style the input element nicely. We will use the button()
// widget for this since there is no native widget for the text
// input.
inputDiv.button().css({
'font': 'inherit',
'color': 'inherit',
'text-align': 'left',
'outline': 'none',
'cursor': 'text',
'height': '15px'
});
// Tell the parameter object from state that we are attaching a
// text input to it. Next time the parameter will be updated with
// a new value, tis input will also be updated.
paramObj.inputDivs.push(inputDiv);
return;
// Update the 'state' - i.e. set the value of the parameter this
// input is attached to to a new value.
//
// This will cause the plot to be redrawn each time after the user
// changes the value in the input. Note that he has to either press
// 'Enter', or click somewhere else on the page in order for the
// 'change' event to be tiggered.
function inputOnChange(event) {
var inputDiv;
inputDiv = $(this);
state.setParameterValue(paramName, inputDiv.val(), inputDiv);
}
}
}
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)

View File

@@ -0,0 +1,236 @@
function jstat(){}
j=jstat;(function(){var initializing=false,fnTest=/xyz/.test(function(){xyz;})?/\b_super\b/:/.*/;this.Class=function(){};Class.extend=function(prop){var _super=this.prototype;initializing=true;var prototype=new this();initializing=false;for(var name in prop){prototype[name]=typeof prop[name]=="function"&&typeof _super[name]=="function"&&fnTest.test(prop[name])?(function(name,fn){return function(){var tmp=this._super;this._super=_super[name];var ret=fn.apply(this,arguments);this._super=tmp;return ret;};})(name,prop[name]):prop[name];}
function Class(){if(!initializing&&this.init)
this.init.apply(this,arguments);}
Class.prototype=prototype;Class.constructor=Class;Class.extend=arguments.callee;return Class;};})();jstat.ONE_SQRT_2PI=0.3989422804014327;jstat.LN_SQRT_2PI=0.9189385332046727417803297;jstat.LN_SQRT_PId2=0.225791352644727432363097614947;jstat.DBL_MIN=2.22507e-308;jstat.DBL_EPSILON=2.220446049250313e-16;jstat.SQRT_32=5.656854249492380195206754896838;jstat.TWO_PI=6.283185307179586;jstat.DBL_MIN_EXP=-999;jstat.SQRT_2dPI=0.79788456080287;jstat.LN_SQRT_PI=0.5723649429247;jstat.seq=function(min,max,length){var r=new Range(min,max,length);return r.getPoints();}
jstat.dnorm=function(x,mean,sd,log){if(mean==null)mean=0;if(sd==null)sd=1;if(log==null)log=false;var n=new NormalDistribution(mean,sd);if(!isNaN(x)){return n._pdf(x,log);}else if(x.length){var res=[];for(var i=0;i<x.length;i++){res.push(n._pdf(x[i],log));}
return res;}else{throw"Illegal argument: x";}}
jstat.pnorm=function(q,mean,sd,lower_tail,log){if(mean==null)mean=0;if(sd==null)sd=1;if(lower_tail==null)lower_tail=true;if(log==null)log=false;var n=new NormalDistribution(mean,sd);if(!isNaN(q)){return n._cdf(q,lower_tail,log);}else if(q.length){var res=[];for(var i=0;i<q.length;i++){res.push(n._cdf(q[i],lower_tail,log));}
return res;}else{throw"Illegal argument: x";}}
jstat.dlnorm=function(x,meanlog,sdlog,log){if(meanlog==null)meanlog=0;if(sdlog==null)sdlog=1;if(log==null)log=false;var n=new LogNormalDistribution(meanlog,sdlog);if(!isNaN(x)){return n._pdf(x,log);}else if(x.length){var res=[];for(var i=0;i<x.length;i++){res.push(n._pdf(x[i],log));}
return res;}else{throw"Illegal argument: x";}}
jstat.plnorm=function(q,meanlog,sdlog,lower_tail,log){if(meanlog==null)meanlog=0;if(sdlog==null)sdlog=1;if(lower_tail==null)lower_tail=true;if(log==null)log=false;var n=new LogNormalDistribution(meanlog,sdlog);if(!isNaN(q)){return n._cdf(q,lower_tail,log);}
else if(q.length){var res=[];for(var i=0;i<q.length;i++){res.push(n._cdf(q[i],lower_tail,log));}
return res;}else{throw"Illegal argument: x";}}
jstat.dbeta=function(x,alpha,beta,ncp,log){if(ncp==null)ncp=0;if(log==null)log=false;var b=new BetaDistribution(alpha,beta);if(!isNaN(x)){return b._pdf(x,log);}
else if(x.length){var res=[];for(var i=0;i<x.length;i++){res.push(b._pdf(x[i],log));}
return res;}else{throw"Illegal argument: x";}}
jstat.pbeta=function(q,alpha,beta,ncp,lower_tail,log){if(ncp==null)ncp=0;if(log==null)log=false;if(lower_tail==null)lower_tail=true;var b=new BetaDistribution(alpha,beta);if(!isNaN(q)){return b._cdf(q,lower_tail,log);}else if(q.length){var res=[];for(var i=0;i<q.length;i++){res.push(b._cdf(q[i],lower_tail,log));}
return res;}
else{throw"Illegal argument: x";}}
jstat.dgamma=function(x,shape,rate,scale,log){if(rate==null)rate=1;if(scale==null)scale=1/rate;if(log==null)log=false;var g=new GammaDistribution(shape,scale);if(!isNaN(x)){return g._pdf(x,log);}else if(x.length){var res=[];for(var i=0;i<x.length;i++){res.push(g._pdf(x[i],log));}
return res;}else{throw"Illegal argument: x";}}
jstat.pgamma=function(q,shape,rate,scale,lower_tail,log){if(rate==null)rate=1;if(scale==null)scale=1/rate;if(lower_tail==null)lower_tail=true;if(log==null)log=false;var g=new GammaDistribution(shape,scale);if(!isNaN(q)){return g._cdf(q,lower_tail,log);}else if(q.length){var res=[];for(var i=0;i<q.length;i++){res.push(g._cdf(q[i],lower_tail,log));}
return res;}else{throw"Illegal argument: x";}}
jstat.dt=function(x,df,ncp,log){if(log==null)log=false;var t=new StudentTDistribution(df,ncp);if(!isNaN(x)){return t._pdf(x,log);}else if(x.length){var res=[];for(var i=0;i<x.length;i++){res.push(t._pdf(x[i],log));}
return res;}else{throw"Illegal argument: x";}}
jstat.pt=function(q,df,ncp,lower_tail,log){if(lower_tail==null)lower_tail=true;if(log==null)log=false;var t=new StudentTDistribution(df,ncp);if(!isNaN(q)){return t._cdf(q,lower_tail,log);}else if(q.length){var res=[];for(var i=0;i<q.length;i++){res.push(t._cdf(q[i],lower_tail,log));}
return res;}else{throw"Illegal argument: x";}}
jstat.plot=function(x,y,options){if(x==null){throw"x is undefined in jstat.plot";}
if(y==null){throw"y is undefined in jstat.plot";}
if(x.length!=y.length){throw"x and y lengths differ in jstat.plot";}
var flotOpt={series:{lines:{},points:{}}};var series=[];if(x.length==undefined){series.push([x,y]);flotOpt.series.points.show=true;}else{for(var i=0;i<x.length;i++){series.push([x[i],y[i]]);}}
var title='jstat graph';if(options!=null){if(options.type!=null){if(options.type=='l'){flotOpt.series.lines.show=true;}else if(options.type=='p'){flotOpt.series.lines.show=false;flotOpt.series.points.show=true;}}
if(options.hover!=null){flotOpt.grid={hoverable:options.hover}}
if(options.main!=null){title=options.main;}}
var now=new Date();var hash=now.getMilliseconds()*now.getMinutes()+now.getSeconds();$('body').append('<div title="'+title+'" style="display: none;" id="'+hash+'"><div id="graph-'+hash+'" style="width:95%; height: 95%"></div></div>');$('#'+hash).dialog({modal:false,width:475,height:475,resizable:true,resize:function(){$.plot($('#graph-'+hash),[series],flotOpt);},open:function(event,ui){var id='#graph-'+hash;$.plot($('#graph-'+hash),[series],flotOpt);}})}
jstat.log10=function(arg){return Math.log(arg)/Math.LN10;}
jstat.toSigFig=function(num,n){if(num==0){return 0;}
var d=Math.ceil(jstat.log10(num<0?-num:num));var power=n-parseInt(d);var magnitude=Math.pow(10,power);var shifted=Math.round(num*magnitude);return shifted/magnitude;}
jstat.trunc=function(x){return(x>0)?Math.floor(x):Math.ceil(x);}
jstat.isFinite=function(x){return(!isNaN(x)&&(x!=Number.POSITIVE_INFINITY)&&(x!=Number.NEGATIVE_INFINITY));}
jstat.dopois_raw=function(x,lambda,give_log){if(lambda==0){if(x==0){return(give_log)?0.0:1.0;}
return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
if(!jstat.isFinite(lambda))return(give_log)?Number.NEGATIVE_INFINITY:0.0;if(x<0)return(give_log)?Number.NEGATIVE_INFINITY:0.0;if(x<=lambda*jstat.DBL_MIN){return(give_log)?-lambda:Math.exp(-lambda);}
if(lambda<x*jstat.DBL_MIN){var param=-lambda+x*Math.log(lambda)-jstat.lgamma(x+1);return(give_log)?param:Math.exp(param);}
var param1=jstat.TWO_PI*x;var param2=-jstat.stirlerr(x)-jstat.bd0(x,lambda);return(give_log)?-0.5*Math.log(param1)+param2:Math.exp(param2)/Math.sqrt(param1);}
jstat.bd0=function(x,np){var ej,s,s1,v,j;if(!jstat.isFinite(x)||!jstat.isFinite(np)||np==0.0)throw"illegal parameter in jstat.bd0";if(Math.abs(x-np)>0.1*(x+np)){v=(x-np)/(x+np);s=(x-np)*v;ej=2*x*v;v=v*v;for(j=1;;j++){ej*=v;s1=s+ej/((j<<1)+1);if(s1==s)
return(s1);s=s1;}}
return(x*Math.log(x/np)+np-x);}
jstat.stirlerr=function(n){var S0=0.083333333333333333333;var S1=0.00277777777777777777778;var S2=0.00079365079365079365079365;var S3=0.000595238095238095238095238;var S4=0.0008417508417508417508417508;var sferr_halves=[0.0,0.1534264097200273452913848,0.0810614667953272582196702,0.0548141210519176538961390,0.0413406959554092940938221,0.03316287351993628748511048,0.02767792568499833914878929,0.02374616365629749597132920,0.02079067210376509311152277,0.01848845053267318523077934,0.01664469118982119216319487,0.01513497322191737887351255,0.01387612882307074799874573,0.01281046524292022692424986,0.01189670994589177009505572,0.01110455975820691732662991,0.010411265261972096497478567,0.009799416126158803298389475,0.009255462182712732917728637,0.008768700134139385462952823,0.008330563433362871256469318,0.007934114564314020547248100,0.007573675487951840794972024,0.007244554301320383179543912,0.006942840107209529865664152,0.006665247032707682442354394,0.006408994188004207068439631,0.006171712263039457647532867,0.005951370112758847735624416,0.005746216513010115682023589,0.005554733551962801371038690];var nn;if(n<=15.0){nn=n+n;if(nn==parseInt(nn))return(sferr_halves[parseInt(nn)]);return(jstat.lgamma(n+1.0)-(n+0.5)*Math.log(n)+n-jstat.LN_SQRT_2PI);}
nn=n*n;if(n>500)return((S0-S1/nn)/n);if(n>80)return((S0-(S1-S2/nn)/nn)/n);if(n>35)return((S0-(S1-(S2-S3/nn)/nn)/nn)/n);return((S0-(S1-(S2-(S3-S4/nn)/nn)/nn)/nn)/n);}
jstat.lgamma=function(x){function lgammafn_sign(x,sgn){var ans,y,sinpiy;var xmax=2.5327372760800758e+305;var dxrel=1.490116119384765696e-8;if(sgn!=null)sgn=1;if(isNaN(x))return x;if(x<0&&(Math.floor(-x)%2.0)==0)
if(sgn!=null)sgn=-1;if(x<=0&&x==jstat.trunc(x)){console.warn("Negative integer argument in lgammafn_sign");return Number.POSITIVE_INFINITY;}
y=Math.abs(x);if(y<=10)return Math.log(Math.abs(jstat.gamma(x)));if(y>xmax){console.warn("Illegal arguement passed to lgammafn_sign");return Number.POSITIVE_INFINITY;}
if(x>0){if(x>1e17){return(x*(Math.log(x)-1.0));}else if(x>4934720.0){return(jstat.LN_SQRT_2PI+(x-0.5)*Math.log(x)-x);}else{return jstat.LN_SQRT_2PI+(x-0.5)*Math.log(x)-x+jstat.lgammacor(x);}}
sinpiy=Math.abs(Math.sin(Math.PI*y));if(sinpiy==0){throw"Should never happen!!";}
ans=jstat.LN_SQRT_PId2+(x-0.5)*Math.log(y)-x-Math.log(sinpiy)-jstat.lgammacor(y);if(Math.abs((x-jstat.trunc(x-0.5))*ans/x)<dxrel){throw"The answer is less than half the precision argument too close to a negative integer";}
return ans;}
return lgammafn_sign(x,null);}
jstat.gamma=function(x){var xbig=171.624;var p=[-1.71618513886549492533811,24.7656508055759199108314,-379.804256470945635097577,629.331155312818442661052,866.966202790413211295064,-31451.2729688483675254357,-36144.4134186911729807069,66456.1438202405440627855];var q=[-30.8402300119738975254353,315.350626979604161529144,-1015.15636749021914166146,-3107.77167157231109440444,22538.1184209801510330112,4755.84627752788110767815,-134659.959864969306392456,-115132.259675553483497211];var c=[-.001910444077728,8.4171387781295e-4,-5.952379913043012e-4,7.93650793500350248e-4,-.002777777777777681622553,.08333333333333333331554247,.0057083835261];var i,n,parity,fact,xden,xnum,y,z,yi,res,sum,ysq;parity=(0);fact=1.0;n=0;y=x;if(y<=0.0){y=-x;yi=jstat.trunc(y);res=y-yi;if(res!=0.0){if(yi!=jstat.trunc(yi*0.5)*2.0)
parity=(1);fact=-Math.PI/Math.sin(Math.PI*res);y+=1.0;}else{return(Number.POSITIVE_INFINITY);}}
if(y<jstat.DBL_EPSILON){if(y>=jstat.DBL_MIN){res=1.0/y;}else{return(Number.POSITIVE_INFINITY);}}else if(y<12.0){yi=y;if(y<1.0){z=y;y+=1.0;}else{n=parseInt(y)-1;y-=parseFloat(n);z=y-1.0;}
xnum=0.0;xden=1.0;for(i=0;i<8;++i){xnum=(xnum+p[i])*z;xden=xden*z+q[i];}
res=xnum/xden+1.0;if(yi<y){res/=yi;}else if(yi>y){for(i=0;i<n;++i){res*=y;y+=1.0;}}}else{if(y<=xbig){ysq=y*y;sum=c[6];for(i=0;i<6;++i){sum=sum/ysq+c[i];}
sum=sum/y-y+jstat.LN_SQRT_2PI;sum+=(y-0.5)*Math.log(y);res=Math.exp(sum);}else{return(Number.POSITIVE_INFINITY);}}
if(parity)
res=-res;if(fact!=1.0)
res=fact/res;return res;}
jstat.lgammacor=function(x){var algmcs=[+.1666389480451863247205729650822e+0,-.1384948176067563840732986059135e-4,+.9810825646924729426157171547487e-8,-.1809129475572494194263306266719e-10,+.6221098041892605227126015543416e-13,-.3399615005417721944303330599666e-15,+.2683181998482698748957538846666e-17,-.2868042435334643284144622399999e-19,+.3962837061046434803679306666666e-21,-.6831888753985766870111999999999e-23,+.1429227355942498147573333333333e-24,-.3547598158101070547199999999999e-26,+.1025680058010470912000000000000e-27,-.3401102254316748799999999999999e-29,+.1276642195630062933333333333333e-30];var tmp;var nalgm=5;var xbig=94906265.62425156;var xmax=3.745194030963158e306;if(x<10){return Number.NaN;}else if(x>=xmax){throw"Underflow error in lgammacor";}else if(x<xbig){tmp=10/x;return jstat.chebyshev(tmp*tmp*2-1,algmcs,nalgm)/x;}
return 1/(x*12);}
jstat.incompleteBeta=function(a,b,x){function betacf(a,b,x){var MAXIT=100;var EPS=3.0e-12;var FPMIN=1.0e-30;var m,m2,aa,c,d,del,h,qab,qam,qap;qab=a+b;qap=a+1.0;qam=a-1.0;c=1.0;d=1.0-qab*x/qap;if(Math.abs(d)<FPMIN){d=FPMIN;}
d=1.0/d;h=d;for(m=1;m<=MAXIT;m++){m2=2*m;aa=m*(b-m)*x/((qam+m2)*(a+m2));d=1.0+aa*d;if(Math.abs(d)<FPMIN){d=FPMIN;}
c=1.0+aa/c;if(Math.abs(c)<FPMIN){c=FPMIN;}
d=1.0/d;h*=d*c;aa=-(a+m)*(qab+m)*x/((a+m2)*(qap+m2));d=1.0+aa*d;if(Math.abs(d)<FPMIN){d=FPMIN;}
c=1.0+aa/c;if(Math.abs(c)<FPMIN){c=FPMIN;}
d=1.0/d;del=d*c;h*=del;if(Math.abs(del-1.0)<EPS){break;}}
if(m>MAXIT){console.warn("a or b too big, or MAXIT too small in betacf: "+a+", "+b+", "+x+", "+h);return h;}
if(isNaN(h)){console.warn(a+", "+b+", "+x);}
return h;}
var bt;if(x<0.0||x>1.0){throw"bad x in routine incompleteBeta";}
if(x==0.0||x==1.0){bt=0.0;}else{bt=Math.exp(jstat.lgamma(a+b)-jstat.lgamma(a)-jstat.lgamma(b)+a*Math.log(x)+b*Math.log(1.0-x));}
if(x<(a+1.0)/(a+b+2.0)){return bt*betacf(a,b,x)/a;}else{return 1.0-bt*betacf(b,a,1.0-x)/b;}}
jstat.chebyshev=function(x,a,n){var b0,b1,b2,twox;var i;if(n<1||n>1000)return Number.NaN;if(x<-1.1||x>1.1)return Number.NaN;twox=x*2;b2=b1=0;b0=0;for(i=1;i<=n;i++){b2=b1;b1=b0;b0=twox*b1-b2+a[n-i];}
return(b0-b2)*0.5;}
jstat.fmin2=function(x,y){return(x<y)?x:y;}
jstat.log1p=function(x){var ret=0,n=50;if(x<=-1){return Number.NEGATIVE_INFINITY;}
if(x<0||x>1){return Math.log(1+x);}
for(var i=1;i<n;i++){if((i%2)===0){ret-=Math.pow(x,i)/i;}else{ret+=Math.pow(x,i)/i;}}
return ret;}
jstat.expm1=function(x){var y,a=Math.abs(x);if(a<jstat.DBL_EPSILON)return x;if(a>0.697)return Math.exp(x)-1;if(a>1e-8){y=Math.exp(x)-1;}else{y=(x/2+1)*x;}
y-=(1+y)*(jstat.log1p(y)-x);return y;}
jstat.logBeta=function(a,b){var corr,p,q;p=q=a;if(b<p)p=b;if(b>q)q=b;if(p<0){console.warn('Both arguements must be >= 0');return Number.NaN;}
else if(p==0){return Number.POSITIVE_INFINITY;}
else if(!jstat.isFinite(q)){return Number.NEGATIVE_INFINITY;}
if(p>=10){corr=jstat.lgammacor(p)+jstat.lgammacor(q)-jstat.lgammacor(p+q);return Math.log(q)*-0.5+jstat.LN_SQRT_2PI+corr
+(p-0.5)*Math.log(p/(p+q))+q*jstat.log1p(-p/(p+q));}
else if(q>=10){corr=jstat.lgammacor(q)-jstat.lgammacor(p+q);return jstat.lgamma(p)+corr+p-p*Math.log(p+q)
+(q-0.5)*jstat.log1p(-p/(p+q));}
else
return Math.log(jstat.gamma(p)*(jstat.gamma(q)/jstat.gamma(p+q)));}
jstat.dbinom_raw=function(x,n,p,q,give_log){if(give_log==null)give_log=false;var lf,lc;if(p==0){if(x==0){return(give_log)?0.0:1.0;}else{return(give_log)?Number.NEGATIVE_INFINITY:0.0;}}
if(q==0){if(x==n){return(give_log)?0.0:1.0;}else{return(give_log)?Number.NEGATIVE_INFINITY:0.0;}}
if(x==0){if(n==0)return(give_log)?0.0:1.0;lc=(p<0.1)?-jstat.bd0(n,n*q)-n*p:n*Math.log(q);return(give_log)?lc:Math.exp(lc);}
if(x==n){lc=(q<0.1)?-jstat.bd0(n,n*p)-n*q:n*Math.log(p);return(give_log)?lc:Math.exp(lc);}
if(x<0||x>n)return(give_log)?Number.NEGATIVE_INFINITY:0.0;lc=jstat.stirlerr(n)-jstat.stirlerr(x)-jstat.stirlerr(n-x)-jstat.bd0(x,n*p)-jstat.bd0(n-x,n*q);lf=Math.log(jstat.TWO_PI)+Math.log(x)+jstat.log1p(-x/n);return(give_log)?lc-0.5*lf:Math.exp(lc-0.5*lf);}
jstat.max=function(values){var max=Number.NEGATIVE_INFINITY;for(var i=0;i<values.length;i++){if(values[i]>max){max=values[i];}}
return max;}
var Range=Class.extend({init:function(min,max,numPoints){this._minimum=parseFloat(min);this._maximum=parseFloat(max);this._numPoints=parseFloat(numPoints);},getMinimum:function(){return this._minimum;},getMaximum:function(){return this._maximum;},getNumPoints:function(){return this._numPoints;},getPoints:function(){var results=[];var x=this._minimum;var step=(this._maximum-this._minimum)/(this._numPoints-1);for(var i=0;i<this._numPoints;i++){results[i]=parseFloat(x.toFixed(6));x+=step;}
return results;}});Range.validate=function(range){if(!range instanceof Range){return false;}
if(isNaN(range.getMinimum())||isNaN(range.getMaximum())||isNaN(range.getNumPoints())||range.getMaximum()<range.getMinimum()||range.getNumPoints()<=0){return false;}
return true;}
var ContinuousDistribution=Class.extend({init:function(name){this._name=name;},toString:function(){return this._string;},getName:function(){return this._name;},getClassName:function(){return this._name+'Distribution';},density:function(valueOrRange){if(!isNaN(valueOrRange)){return parseFloat(this._pdf(valueOrRange).toFixed(15));}else if(Range.validate(valueOrRange)){var points=valueOrRange.getPoints();var result=[];for(var i=0;i<points.length;i++){result[i]=parseFloat(this._pdf(points[i]));}
return result;}else{throw"Invalid parameter supplied to "+this.getClassName()+".density()";}},cumulativeDensity:function(valueOrRange){if(!isNaN(valueOrRange)){return parseFloat(this._cdf(valueOrRange).toFixed(15));}else if(Range.validate(valueOrRange)){var points=valueOrRange.getPoints();var result=[];for(var i=0;i<points.length;i++){result[i]=parseFloat(this._cdf(points[i]));}
return result;}else{throw"Invalid parameter supplied to "+this.getClassName()+".cumulativeDensity()";}},getRange:function(standardDeviations,numPoints){if(standardDeviations==null){standardDeviations=5;}
if(numPoints==null){numPoints=100;}
var min=this.getMean()-standardDeviations*Math.sqrt(this.getVariance());var max=this.getMean()+standardDeviations*Math.sqrt(this.getVariance());if(this.getClassName()=='GammaDistribution'||this.getClassName()=='LogNormalDistribution'){min=0.0;max=this.getMean()+standardDeviations*Math.sqrt(this.getVariance());}else if(this.getClassName()=='BetaDistribution'){min=0.0;max=1.0;}
var range=new Range(min,max,numPoints);return range;},getVariance:function(){},getMean:function(){},getQuantile:function(p){var self=this;function findClosestMatch(range,p){var ERR=1.0e-5;var xs=range.getPoints();var closestIndex=0;var closestDistance=999;for(var i=0;i<xs.length;i++){var pp=self.cumulativeDensity(xs[i]);var distance=Math.abs(pp-p);if(distance<closestDistance){closestIndex=i;closestDistance=distance;}}
if(closestDistance<=ERR){return xs[closestIndex];}else{var newRange=new Range(xs[closestIndex-1],xs[closestIndex+1],20);return findClosestMatch(newRange,p);}}
var range=this.getRange(5,20);return findClosestMatch(range,p);}});var NormalDistribution=ContinuousDistribution.extend({init:function(mean,sigma){this._super('Normal');this._mean=parseFloat(mean);this._sigma=parseFloat(sigma);this._string="Normal ("+this._mean.toFixed(2)+", "+this._sigma.toFixed(2)+")";},_pdf:function(x,give_log){if(give_log==null){give_log=false;}
var sigma=this._sigma;var mu=this._mean;if(!jstat.isFinite(sigma)){return(give_log)?Number.NEGATIVE_INFINITY:0.0}
if(!jstat.isFinite(x)&&mu==x){return Number.NaN;}
if(sigma<=0){if(sigma<0){throw"invalid sigma in _pdf";}
return(x==mu)?Number.POSITIVE_INFINITY:(give_log)?Number.NEGATIVE_INFINITY:0.0;}
x=(x-mu)/sigma;if(!jstat.isFinite(x)){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
return(give_log?-(jstat.LN_SQRT_2PI+0.5*x*x+Math.log(sigma)):jstat.ONE_SQRT_2PI*Math.exp(-0.5*x*x)/sigma);},_cdf:function(x,lower_tail,log_p){if(lower_tail==null)lower_tail=true;if(log_p==null)log_p=false;function pnorm_both(x,cum,ccum,i_tail,log_p){var a=[2.2352520354606839287,161.02823106855587881,1067.6894854603709582,18154.981253343561249,0.065682337918207449113];var b=[47.20258190468824187,976.09855173777669322,10260.932208618978205,45507.789335026729956];var c=[0.39894151208813466764,8.8831497943883759412,93.506656132177855979,597.27027639480026226,2494.5375852903726711,6848.1904505362823326,11602.651437647350124,9842.7148383839780218,1.0765576773720192317e-8];var d=[22.266688044328115691,235.38790178262499861,1519.377599407554805,6485.558298266760755,18615.571640885098091,34900.952721145977266,38912.003286093271411,19685.429676859990727];var p=[0.21589853405795699,0.1274011611602473639,0.022235277870649807,0.001421619193227893466,2.9112874951168792e-5,0.02307344176494017303];var q=[1.28426009614491121,0.468238212480865118,0.0659881378689285515,0.00378239633202758244,7.29751555083966205e-5];var xden,xnum,temp,del,eps,xsq,y,i,lower,upper;eps=jstat.DBL_EPSILON*0.5;lower=i_tail!=1;upper=i_tail!=0;y=Math.abs(x);if(y<=0.67448975){if(y>eps){xsq=x*x;xnum=a[4]*xsq;xden=xsq;for(i=0;i<3;++i){xnum=(xnum+a[i])*xsq;xden=(xden+b[i])*xsq;}}else{xnum=xden=0.0;}
temp=x*(xnum+a[3])/(xden+b[3]);if(lower)cum=0.5+temp;if(upper)ccum=0.5-temp;if(log_p){if(lower)cum=Math.log(cum);if(upper)ccum=Math.log(ccum);}}else if(y<=jstat.SQRT_32){xnum=c[8]*y;xden=y;for(i=0;i<7;++i){xnum=(xnum+c[i])*y;xden=(xden+d[i])*y;}
temp=(xnum+c[7])/(xden+d[7]);xsq=jstat.trunc(x*16)/16;del=(x-xsq)*(x+xsq);if(log_p){cum=(-xsq*xsq*0.5)+(-del*0.5)+Math.log(temp);if((lower&&x>0.)||(upper&&x<=0.))
ccum=jstat.log1p(-Math.exp(-xsq*xsq*0.5)*Math.exp(-del*0.5)*temp);}
else{cum=Math.exp(-xsq*xsq*0.5)*Math.exp(-del*0.5)*temp;ccum=1.0-cum;}
if(x>0.0){temp=cum;if(lower){cum=ccum;}
ccum=temp;}}
else if((log_p&&y<1e170)||(lower&&-37.5193<x&&x<8.2924)||(upper&&-8.2924<x&&x<37.5193)){xsq=1.0/(x*x);xnum=p[5]*xsq;xden=xsq;for(i=0;i<4;++i){xnum=(xnum+p[i])*xsq;xden=(xden+q[i])*xsq;}
temp=xsq*(xnum+p[4])/(xden+q[4]);temp=(jstat.ONE_SQRT_2PI-temp)/y;xsq=jstat.trunc(x*16)/16;del=(x-xsq)*(x+xsq);if(log_p){cum=(-xsq*xsq*0.5)+(-del*0.5)+Math.log(temp);if((lower&&x>0.)||(upper&&x<=0.))
ccum=jstat.log1p(-Math.exp(-xsq*xsq*0.5)*Math.exp(-del*0.5)*temp);}
else{cum=Math.exp(-xsq*xsq*0.5)*Math.exp(-del*0.5)*temp;ccum=1.0-cum;}
if(x>0.0){temp=cum;if(lower){cum=ccum;}
ccum=temp;}}else{if(x>0){cum=(log_p)?0.0:1.0;ccum=(log_p)?Number.NEGATIVE_INFINITY:0.0;}else{cum=(log_p)?Number.NEGATIVE_INFINITY:0.0;ccum=(log_p)?0.0:1.0;}}
return[cum,ccum];}
var p,cp;var mu=this._mean;var sigma=this._sigma;var R_DT_0,R_DT_1;if(lower_tail){if(log_p){R_DT_0=Number.NEGATIVE_INFINITY;R_DT_1=0.0;}else{R_DT_0=0.0;R_DT_1=1.0;}}else{if(log_p){R_DT_0=0.0;R_DT_1=Number.NEGATIVE_INFINITY;}else{R_DT_0=1.0;R_DT_1=0.0;}}
if(!jstat.isFinite(x)&&mu==x)return Number.NaN;if(sigma<=0){if(sigma<0){console.warn("Sigma is less than 0");return Number.NaN;}
return(x<mu)?R_DT_0:R_DT_1;}
p=(x-mu)/sigma;if(!jstat.isFinite(p)){return(x<mu)?R_DT_0:R_DT_1;}
x=p;var result=pnorm_both(x,p,cp,(lower_tail?false:true),log_p);return(lower_tail?result[0]:result[1]);},getMean:function(){return this._mean;},getSigma:function(){return this._sigma;},getVariance:function(){return this._sigma*this._sigma;}});var LogNormalDistribution=ContinuousDistribution.extend({init:function(location,scale){this._super('LogNormal')
this._location=parseFloat(location);this._scale=parseFloat(scale);this._string="LogNormal ("+this._location.toFixed(2)+", "+this._scale.toFixed(2)+")";},_pdf:function(x,give_log){var y;var sdlog=this._scale;var meanlog=this._location;if(give_log==null){give_log=false;}
if(sdlog<=0)throw"Illegal parameter in _pdf";if(x<=0){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
y=(Math.log(x)-meanlog)/sdlog;return(give_log?-(jstat.LN_SQRT_2PI+0.5*y*y+Math.log(x*sdlog)):jstat.ONE_SQRT_2PI*Math.exp(-0.5*y*y)/(x*sdlog));},_cdf:function(x,lower_tail,log_p){var sdlog=this._scale;var meanlog=this._location;if(lower_tail==null){lower_tail=true;}
if(log_p==null){log_p=false;}
if(sdlog<=0){throw"illegal std in _cdf";}
if(x>0){var nd=new NormalDistribution(meanlog,sdlog);return nd._cdf(Math.log(x),lower_tail,log_p);}
if(lower_tail){return(log_p)?Number.NEGATIVE_INFINITY:0.0;}else{return(log_p)?0.0:1.0;}},getLocation:function(){return this._location;},getScale:function(){return this._scale;},getMean:function(){return Math.exp((this._location+this._scale)/2);},getVariance:function(){var ans=(Math.exp(this._scale)-1)*Math.exp(2*this._location+this._scale);return ans;}});var GammaDistribution=ContinuousDistribution.extend({init:function(shape,scale){this._super('Gamma');this._shape=parseFloat(shape);this._scale=parseFloat(scale);this._string="Gamma ("+this._shape.toFixed(2)+", "+this._scale.toFixed(2)+")";},_pdf:function(x,give_log){var pr;var shape=this._shape;var scale=this._scale;if(give_log==null){give_log=false;}
if(shape<0||scale<=0){throw"Illegal argument in _pdf";}
if(x<0){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
if(shape==0){return(x==0)?Number.POSITIVE_INFINITY:(give_log)?Number.NEGATIVE_INFINITY:0.0;}
if(x==0){if(shape<1)return Number.POSITIVE_INFINITY;if(shape>1)return(give_log)?Number.NEGATIVE_INFINITY:0.0;return(give_log)?-Math.log(scale):1/scale;}
if(shape<1){pr=jstat.dopois_raw(shape,x/scale,give_log);return give_log?pr+Math.log(shape/x):pr*shape/x;}
pr=jstat.dopois_raw(shape-1,x/scale,give_log);return give_log?pr-Math.log(scale):pr/scale;},_cdf:function(x,lower_tail,log_p){function USE_PNORM(){pn1=Math.sqrt(alph)*3.0*(Math.pow(x/alph,1.0/3.0)+1.0/(9.0*alph)-1.0);var norm_dist=new NormalDistribution(0.0,1.0);return norm_dist._cdf(pn1,lower_tail,log_p);}
if(lower_tail==null)lower_tail=true;if(log_p==null)log_p=false;var alph=this._shape;var scale=this._scale;var xbig=1.0e+8;var xlarge=1.0e+37;var alphlimit=1e5;var pn1,pn2,pn3,pn4,pn5,pn6,arg,a,b,c,an,osum,sum,n,pearson;if(alph<=0.||scale<=0.){console.warn('Invalid gamma params in _cdf');return Number.NaN;}
x/=scale;if(isNaN(x))return x;if(x<=0.0){if(lower_tail){return(log_p)?Number.NEGATIVE_INFINITY:0.0;}else{return(log_p)?0.0:1.0;}}
if(alph>alphlimit){return USE_PNORM();}
if(x>xbig*alph){if(x>jstat.DBL_MAX*alph){if(lower_tail){return(log_p)?0.0:1.0;}else{return(log_p)?Number.NEGATIVE_INFINITY:0.0;}}else{return USE_PNORM();}}
if(x<=1.0||x<alph){pearson=1;arg=alph*Math.log(x)-x-jstat.lgamma(alph+1.0);c=1.0;sum=1.0;a=alph;do{a+=1.0;c*=x/a;sum+=c;}while(c>jstat.DBL_EPSILON*sum);}else{pearson=0;arg=alph*Math.log(x)-x-jstat.lgamma(alph);a=1.-alph;b=a+x+1.;pn1=1.;pn2=x;pn3=x+1.;pn4=x*b;sum=pn3/pn4;for(n=1;;n++){a+=1.;b+=2.;an=a*n;pn5=b*pn3-an*pn1;pn6=b*pn4-an*pn2;if(Math.abs(pn6)>0.){osum=sum;sum=pn5/pn6;if(Math.abs(osum-sum)<=jstat.DBL_EPSILON*jstat.fmin2(1.0,sum))
break;}
pn1=pn3;pn2=pn4;pn3=pn5;pn4=pn6;if(Math.abs(pn5)>=xlarge){pn1/=xlarge;pn2/=xlarge;pn3/=xlarge;pn4/=xlarge;}}}
arg+=Math.log(sum);lower_tail=(lower_tail==pearson);if(log_p&&lower_tail)
return(arg);if(lower_tail){return Math.exp(arg);}else{if(log_p){return(arg>-Math.LN2)?Math.log(-jstat.expm1(arg)):jstat.log1p(-Math.exp(arg));}else{return-jstat.expm1(arg);}}},getShape:function(){return this._shape;},getScale:function(){return this._scale;},getMean:function(){return this._shape*this._scale;},getVariance:function(){return this._shape*Math.pow(this._scale,2);}});var BetaDistribution=ContinuousDistribution.extend({init:function(alpha,beta){this._super('Beta');this._alpha=parseFloat(alpha);this._beta=parseFloat(beta);this._string="Beta ("+this._alpha.toFixed(2)+", "+this._beta.toFixed(2)+")";},_pdf:function(x,give_log){if(give_log==null)give_log=false;var a=this._alpha;var b=this._beta;var lval;if(a<=0||b<=0){console.warn('Illegal arguments in _pdf');return Number.NaN;}
if(x<0||x>1){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
if(x==0){if(a>1){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
if(a<1){return Number.POSITIVE_INFINITY;}
return(give_log)?Math.log(b):b;}
if(x==1){if(b>1){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
if(b<1){return Number.POSITIVE_INFINITY;}
return(give_log)?Math.log(a):a;}
if(a<=2||b<=2){lval=(a-1)*Math.log(x)+(b-1)*jstat.log1p(-x)-jstat.logBeta(a,b);}else{lval=Math.log(a+b-1)+jstat.dbinom_raw(a-1,a+b-2,x,1-x,true);}
return(give_log)?lval:Math.exp(lval);},_cdf:function(x,lower_tail,log_p){if(lower_tail==null)lower_tail=true;if(log_p==null)log_p=false;var pin=this._alpha;var qin=this._beta;if(pin<=0||qin<=0){console.warn('Invalid argument in _cdf');return Number.NaN;}
if(x<=0){if(lower_tail){return(log_p)?Number.NEGATIVE_INFINITY:0.0;}else{return(log_p)?0.1:1.0;}}
if(x>=1){if(lower_tail){return(log_p)?0.1:1.0;}else{return(log_p)?Number.NEGATIVE_INFINITY:0.0;}}
return jstat.incompleteBeta(pin,qin,x);},getAlpha:function(){return this._alpha;},getBeta:function(){return this._beta;},getMean:function(){return this._alpha/(this._alpha+this._beta);},getVariance:function(){var ans=(this._alpha*this._beta)/(Math.pow(this._alpha+this._beta,2)*(this._alpha+this._beta+1));return ans;}});var StudentTDistribution=ContinuousDistribution.extend({init:function(degreesOfFreedom,mu){this._super('StudentT');this._dof=parseFloat(degreesOfFreedom);if(mu!=null){this._mu=parseFloat(mu);this._string="StudentT ("+this._dof.toFixed(2)+", "+this._mu.toFixed(2)+")";}else{this._mu=0.0;this._string="StudentT ("+this._dof.toFixed(2)+")";}},_pdf:function(x,give_log){if(give_log==null)give_log=false;if(this._mu==null){return this._dt(x,give_log);}else{var y=this._dnt(x,give_log);if(y>1){console.warn('x:'+x+', y: '+y);}
return y;}},_cdf:function(x,lower_tail,give_log){if(lower_tail==null)lower_tail=true;if(give_log==null)give_log=false;if(this._mu==null){return this._pt(x,lower_tail,give_log);}else{return this._pnt(x,lower_tail,give_log);}},_dt:function(x,give_log){var t,u;var n=this._dof;if(n<=0){console.warn('Invalid parameters in _dt');return Number.NaN;}
if(!jstat.isFinite(x)){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
if(!jstat.isFinite(n)){var norm=new NormalDistribution(0.0,1.0);return norm.density(x,give_log);}
t=-jstat.bd0(n/2.0,(n+1)/2.0)+jstat.stirlerr((n+1)/2.0)-jstat.stirlerr(n/2.0);if(x*x>0.2*n)
u=Math.log(1+x*x/n)*n/2;else
u=-jstat.bd0(n/2.0,(n+x*x)/2.0)+x*x/2.0;var p1=jstat.TWO_PI*(1+x*x/n);var p2=t-u;return(give_log)?-0.5*Math.log(p1)+p2:Math.exp(p2)/Math.sqrt(p1);},_dnt:function(x,give_log){if(give_log==null)give_log=false;var df=this._dof;var ncp=this._mu;var u;if(df<=0.0){console.warn("Illegal arguments _dnf");return Number.NaN;}
if(ncp==0.0){return this._dt(x,give_log);}
if(!jstat.isFinite(x)){if(give_log){return Number.NEGATIVE_INFINITY;}else{return 0.0;}}
if(!isFinite(df)||df>1e8){var dist=new NormalDistribution(ncp,1.);return dist.density(x,give_log);}
if(Math.abs(x)>Math.sqrt(df*jstat.DBL_EPSILON)){var newT=new StudentTDistribution(df+2,ncp);u=Math.log(df)-Math.log(Math.abs(x))+
Math.log(Math.abs(newT._pnt(x*Math.sqrt((df+2)/df),true,false)-
this._pnt(x,true,false)));}
else{u=jstat.lgamma((df+1)/2)-jstat.lgamma(df/2)
-.5*(Math.log(Math.PI)+Math.log(df)+ncp*ncp);}
return(give_log?u:Math.exp(u));},_pt:function(x,lower_tail,log_p){if(lower_tail==null)lower_tail=true;if(log_p==null)log_p=false;var val,nx;var n=this._dof;var DT_0,DT_1;if(lower_tail){if(log_p){DT_0=Number.NEGATIVE_INFINITY;DT_1=1.;}else{DT_0=0.;DT_1=1.;}}else{if(log_p){DT_0=0.;DT_1=Number.NEGATIVE_INFINITY;}else{DT_0=1.;DT_1=0.;}}
if(n<=0.0){console.warn("Invalid T distribution _pt");return Number.NaN;}
var norm=new NormalDistribution(0,1);if(!jstat.isFinite(x)){return(x<0)?DT_0:DT_1;}
if(!jstat.isFinite(n)){return norm._cdf(x,lower_tail,log_p);}
if(n>4e5){val=1./(4.*n);return norm._cdf(x*(1.-val)/sqrt(1.+x*x*2.*val),lower_tail,log_p);}
nx=1+(x/n)*x;if(nx>1e100){var lval;lval=-0.5*n*(2*Math.log(Math.abs(x))-Math.log(n))
-jstat.logBeta(0.5*n,0.5)-Math.log(0.5*n);val=log_p?lval:Math.exp(lval);}else{if(n>x*x){var beta=new BetaDistribution(0.5,n/2.);return beta._cdf(x*x/(n+x*x),false,log_p);}else{beta=new BetaDistribution(n/2.,0.5);return beta._cdf(1./nx,true,log_p);}}
if(x<=0.)
lower_tail=!lower_tail;if(log_p){if(lower_tail)return jstat.log1p(-0.5*Math.exp(val));else return val-M_LN2;}
else{val/=2.;if(lower_tail){return(0.5-val+0.5);}else{return val;}}},_pnt:function(t,lower_tail,log_p){var dof=this._dof;var ncp=this._mu;var DT_0,DT_1;if(lower_tail){if(log_p){DT_0=Number.NEGATIVE_INFINITY;DT_1=1.;}else{DT_0=0.;DT_1=1.;}}else{if(log_p){DT_0=0.;DT_1=Number.NEGATIVE_INFINITY;}else{DT_0=1.;DT_1=0.;}}
var albeta,a,b,del,errbd,lambda,rxb,tt,x;var geven,godd,p,q,s,tnc,xeven,xodd;var it,negdel;var ITRMAX=1000;var ERRMAX=1.e-7;if(dof<=0.0){return Number.NaN;}else if(dof==0.0){return this._pt(t);}
if(!jstat.isFinite(t)){return(t<0)?DT_0:DT_1;}
if(t>=0.){negdel=false;tt=t;del=ncp;}else{if(ncp>=40&&(!log_p||!lower_tail)){return DT_0;}
negdel=true;tt=-t;del=-ncp;}
if(dof>4e5||del*del>2*Math.LN2*(-(jstat.DBL_MIN_EXP))){s=1./(4.*dof);var norm=new NormalDistribution(del,Math.sqrt(1.+tt*tt*2.*s));var result=norm._cdf(tt*(1.-s),lower_tail!=negdel,log_p);return result;}
x=t*t;rxb=dof/(x+dof);x=x/(x+dof);if(x>0.){lambda=del*del;p=.5*Math.exp(-.5*lambda);if(p==0.){console.warn("underflow in _pnt");return DT_0;}
q=jstat.SQRT_2dPI*p*del;s=.5-p;if(s<1e-7){s=-0.5*jstat.expm1(-0.5*lambda);}
a=.5;b=.5*dof;rxb=Math.pow(rxb,b);albeta=jstat.LN_SQRT_PI+jstat.lgamma(b)-jstat.lgamma(.5+b);xodd=jstat.incompleteBeta(a,b,x);godd=2.*rxb*Math.exp(a*Math.log(x)-albeta);tnc=b*x;xeven=(tnc<jstat.DBL_EPSILON)?tnc:1.-rxb;geven=tnc*rxb;tnc=p*xodd+q*xeven;for(it=1;it<=ITRMAX;it++){a+=1.;xodd-=godd;xeven-=geven;godd*=x*(a+b-1.)/a;geven*=x*(a+b-.5)/(a+.5);p*=lambda/(2*it);q*=lambda/(2*it+1);tnc+=p*xodd+q*xeven;s-=p;if(s<-1.e-10){console.write("precision error _pnt");break;}
if(s<=0&&it>1)break;errbd=2.*s*(xodd-godd);if(Math.abs(errbd)<ERRMAX)break;}
if(it==ITRMAX){throw"Non-convergence _pnt";}}else{tnc=0.;}
norm=new NormalDistribution(0,1);tnc+=norm._cdf(-del,true,false);lower_tail=lower_tail!=negdel;if(tnc>1-1e-10&&lower_tail){console.warn("precision error _pnt");}
var res=jstat.fmin2(tnc,1.);if(lower_tail){if(log_p){return Math.log(res);}else{return res;}}else{if(log_p){return jstat.log1p(-(res));}else{return(0.5-(res)+0.5);}}},getDegreesOfFreedom:function(){return this._dof;},getNonCentralityParameter:function(){return this._mu;},getMean:function(){if(this._dof>1){var ans=(1/2)*Math.log(this._dof/2)+jstat.lgamma((this._dof-1)/2)-jstat.lgamma(this._dof/2)
return Math.exp(ans)*this._mu;}else{return Number.NaN;}},getVariance:function(){if(this._dof>2){var ans=this._dof*(1+this._mu*this._mu)/(this._dof-2)-(((this._mu*this._mu*this._dof)/2)*Math.pow(Math.exp(jstat.lgamma((this._dof-1)/2)-jstat.lgamma(this._dof/2)),2));return ans;}else{return Number.NaN;}}});var Plot=Class.extend({init:function(id,options){this._container='#'+String(id);this._plots=[];this._flotObj=null;this._locked=false;if(options!=null){this._options=options;}else{this._options={};}},getContainer:function(){return this._container;},getGraph:function(){return this._flotObj;},setData:function(data){this._plots=data;},clear:function(){this._plots=[];},showLegend:function(){this._options.legend={show:true}
this.render();},hideLegend:function(){this._options.legend={show:false}
this.render();},render:function(){this._flotObj=null;this._flotObj=$.plot($(this._container),this._plots,this._options);}});var DistributionPlot=Plot.extend({init:function(id,distribution,range,options){this._super(id,options);this._showPDF=true;this._showCDF=false;this._pdfValues=[];this._cdfValues=[];this._maxY=1;this._plotType='line';this._fill=false;this._distribution=distribution;if(range!=null&&Range.validate(range)){this._range=range;}else{this._range=this._distribution.getRange();}
if(this._distribution!=null){this._maxY=this._generateValues();}else{this._options.xaxis={min:range.getMinimum(),max:range.getMaximum()}
this._options.yaxis={max:1}}
this.render();},setHover:function(bool){if(bool){if(this._options.grid==null){this._options.grid={hoverable:true,mouseActiveRadius:25}}else{this._options.grid.hoverable=true,this._options.grid.mouseActiveRadius=25}
function showTooltip(x,y,contents,color){$('<div id="jstat_tooltip">'+contents+'</div>').css({position:'absolute',display:'none',top:y+15,'font-size':'small',left:x+5,border:'1px solid '+color[1],color:color[2],padding:'5px','background-color':color[0],opacity:0.80}).appendTo("body").show();}
var previousPoint=null;$(this._container).bind("plothover",function(event,pos,item){$("#x").text(pos.x.toFixed(2));$("#y").text(pos.y.toFixed(2));if(item){if(previousPoint!=item.datapoint){previousPoint=item.datapoint;$("#jstat_tooltip").remove();var x=jstat.toSigFig(item.datapoint[0],2),y=jstat.toSigFig(item.datapoint[1],2);var text=null;var color=item.series.color;if(item.series.label=='PDF'){text="P("+x+") = "+y;color=["#fee","#fdd","#C05F5F"];}else{text="F("+x+") = "+y;color=["#eef","#ddf","#4A4AC0"];}
showTooltip(item.pageX,item.pageY,text,color);}}
else{$("#jstat_tooltip").remove();previousPoint=null;}});$(this._container).bind("mouseleave",function(){if($('#jstat_tooltip').is(':visible')){$('#jstat_tooltip').remove();previousPoint=null;}});}else{if(this._options.grid==null){this._options.grid={hoverable:false}}else{this._options.grid.hoverable=false}
$(this._container).unbind("plothover");}
this.render();},setType:function(type){this._plotType=type;var lines={};var points={};if(this._plotType=='line'){lines.show=true;points.show=false;}else if(this._plotType=='points'){lines.show=false;points.show=true;}else if(this._plotType=='both'){lines.show=true;points.show=true;}
if(this._options.series==null){this._options.series={lines:lines,points:points}}else{if(this._options.series.lines==null){this._options.series.lines=lines;}else{this._options.series.lines.show=lines.show;}
if(this._options.series.points==null){this._options.series.points=points;}else{this._options.series.points.show=points.show;}}
this.render();},setFill:function(bool){this._fill=bool;if(this._options.series==null){this._options.series={lines:{fill:bool}}}else{if(this._options.series.lines==null){this._options.series.lines={fill:bool}}else{this._options.series.lines.fill=bool;}}
this.render();},clear:function(){this._super();this._distribution=null;this._pdfValues=[];this._cdfValues=[];this.render();},_generateValues:function(){this._cdfValues=[];this._pdfValues=[];var xs=this._range.getPoints();this._options.xaxis={min:xs[0],max:xs[xs.length-1]}
var pdfs=this._distribution.density(this._range);var cdfs=this._distribution.cumulativeDensity(this._range);for(var i=0;i<xs.length;i++){if(pdfs[i]==Number.POSITIVE_INFINITY||pdfs[i]==Number.NEGATIVE_INFINITY){pdfs[i]=null;}
if(cdfs[i]==Number.POSITIVE_INFINITY||cdfs[i]==Number.NEGATIVE_INFINITY){cdfs[i]=null;}
this._pdfValues.push([xs[i],pdfs[i]]);this._cdfValues.push([xs[i],cdfs[i]]);}
return jstat.max(pdfs);},showPDF:function(){this._showPDF=true;this.render();},hidePDF:function(){this._showPDF=false;this.render();},showCDF:function(){this._showCDF=true;this.render();},hideCDF:function(){this._showCDF=false;this.render();},setDistribution:function(distribution,range){this._distribution=distribution;if(range!=null){this._range=range;}else{this._range=distribution.getRange();}
this._maxY=this._generateValues();this._options.yaxis={max:this._maxY*1.1}
this.render();},getDistribution:function(){return this._distribution;},getRange:function(){return this._range;},setRange:function(range){this._range=range;this._generateValues();this.render();},render:function(){if(this._distribution!=null){if(this._showPDF&&this._showCDF){this.setData([{yaxis:1,data:this._pdfValues,color:'rgb(237,194,64)',clickable:false,hoverable:true,label:"PDF"},{yaxis:2,data:this._cdfValues,clickable:false,color:'rgb(175,216,248)',hoverable:true,label:"CDF"}]);this._options.yaxis={max:this._maxY*1.1}}else if(this._showPDF){this.setData([{data:this._pdfValues,hoverable:true,color:'rgb(237,194,64)',clickable:false,label:"PDF"}]);this._options.yaxis={max:this._maxY*1.1}}else if(this._showCDF){this.setData([{data:this._cdfValues,hoverable:true,color:'rgb(175,216,248)',clickable:false,label:"CDF"}]);this._options.yaxis={max:1.1}}}else{this.setData([]);}
this._super();}});var DistributionFactory={};DistributionFactory.build=function(json){if(json.NormalDistribution){if(json.NormalDistribution.mean!=null&&json.NormalDistribution.standardDeviation!=null){return new NormalDistribution(json.NormalDistribution.mean[0],json.NormalDistribution.standardDeviation[0]);}else{throw"Malformed JSON provided to DistributionBuilder "+json;}}else if(json.LogNormalDistribution){if(json.LogNormalDistribution.location!=null&&json.LogNormalDistribution.scale!=null){return new LogNormalDistribution(json.LogNormalDistribution.location[0],json.LogNormalDistribution.scale[0]);}else{throw"Malformed JSON provided to DistributionBuilder "+json;}}else if(json.BetaDistribution){if(json.BetaDistribution.alpha!=null&&json.BetaDistribution.beta!=null){return new BetaDistribution(json.BetaDistribution.alpha[0],json.BetaDistribution.beta[0]);}else{throw"Malformed JSON provided to DistributionBuilder "+json;}}else if(json.GammaDistribution){if(json.GammaDistribution.shape!=null&&json.GammaDistribution.scale!=null){return new GammaDistribution(json.GammaDistribution.shape[0],json.GammaDistribution.scale[0]);}else{throw"Malformed JSON provided to DistributionBuilder "+json;}}else if(json.StudentTDistribution){if(json.StudentTDistribution.degreesOfFreedom!=null&&json.StudentTDistribution.nonCentralityParameter!=null){return new StudentTDistribution(json.StudentTDistribution.degreesOfFreedom[0],json.StudentTDistribution.nonCentralityParameter[0]);}else if(json.StudentTDistribution.degreesOfFreedom!=null){return new StudentTDistribution(json.StudentTDistribution.degreesOfFreedom[0]);}else{throw"Malformed JSON provided to DistributionBuilder "+json;}}else{throw"Malformed JSON provided to DistributionBuilder "+json;}}

View File

@@ -0,0 +1,54 @@
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
(function (requirejs, require, define) {
define('logme', [], function () {
var debugMode;
// debugMode can be one of the following:
//
// true - All messages passed to logme will be written to the internal
// browser console.
// false - Suppress all output to the internal browser console.
//
// Obviously, if anywhere there is a direct console.log() call, we can't do
// anything about it. That's why use logme() - it will allow to turn off
// the output of debug information with a single change to a variable.
debugMode = true;
return logme;
/*
* function: logme
*
* A helper function that provides logging facilities. We don't want
* to call console.log() directly, because sometimes it is not supported
* by the browser. Also when everything is routed through this function.
* the logging output can be easily turned off.
*
* logme() supports multiple parameters. Each parameter will be passed to
* console.log() function separately.
*
*/
function logme() {
var i;
if (
(typeof debugMode === 'undefined') ||
(debugMode !== true) ||
(typeof window.console === 'undefined')
) {
return;
}
for (i = 0; i < arguments.length; i++) {
window.console.log(arguments[i]);
}
} // End-of: function logme
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)

View File

@@ -0,0 +1,89 @@
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
(function (requirejs, require, define) {
define('Sliders', ['logme'], function (logme) {
return Sliders;
function Sliders(gstId, state) {
var c1, paramName, allParamNames, sliderDiv;
allParamNames = state.getAllParameterNames();
for (c1 = 0; c1 < allParamNames.length; c1 += 1) {
paramName = allParamNames[c1];
sliderDiv = $('#' + gstId + '_slider_' + paramName);
if (sliderDiv.length === 1) {
createSlider(sliderDiv, paramName);
} else if (sliderDiv.length > 1) {
logme('ERROR: Found more than one slider for the parameter "' + paramName + '".');
logme('sliderDiv.length = ', sliderDiv.length);
} else {
logme('MESSAGE: Did not find a slider for the parameter "' + paramName + '".');
}
}
function createSlider(sliderDiv, paramName) {
var paramObj;
paramObj = state.getParamObj(paramName);
// Check that the retrieval went OK.
if (paramObj === undefined) {
logme('ERROR: Could not get a paramObj for parameter "' + paramName + '".');
return;
}
// Create a jQuery UI slider from the slider DIV. We will set
// starting parameters, and will also attach a handler to update
// the 'state' on the 'slide' event.
sliderDiv.slider({
'min': paramObj.min,
'max': paramObj.max,
'value': paramObj.value,
'step': paramObj.step
});
// Tell the parameter object stored in state that we have a slider
// that is attached to it. Next time when the parameter changes, it
// will also update the value of this slider.
paramObj.sliderDiv = sliderDiv;
// Atach callbacks to update the slider's parameter.
paramObj.sliderDiv.on('slide', sliderOnSlide);
paramObj.sliderDiv.on('slidechange', sliderOnChange);
return;
// Update the 'state' - i.e. set the value of the parameter this
// slider is attached to to a new value.
//
// This will cause the plot to be redrawn each time after the user
// drags the slider handle and releases it.
function sliderOnSlide(event, ui) {
// Last parameter passed to setParameterValue() will be 'true'
// so that the function knows we are a slider, and it can
// change the our value back in the case when the new value is
// invalid for some reason.
if (state.setParameterValue(paramName, ui.value, sliderDiv, true, 'slide') === undefined) {
logme('ERROR: Could not update the parameter named "' + paramName + '" with the value "' + ui.value + '".');
}
}
function sliderOnChange(event, ui) {
if (state.setParameterValue(paramName, ui.value, sliderDiv, true, 'change') === undefined) {
logme('ERROR: Could not update the parameter named "' + paramName + '" with the value "' + ui.value + '".');
}
}
}
}
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)

View File

@@ -0,0 +1,395 @@
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
(function (requirejs, require, define) {
define('State', ['logme'], function (logme) {
var stateInst;
// Since there will be (can be) multiple GST on a page, and each will have
// a separate state, we will create a factory constructor function. The
// constructor will expect the ID of the DIV with the GST contents, and the
// configuration object (parsed from a JSON string). It will return an
// object containing methods to set and get the private state properties.
stateInst = 0;
// This module defines and returns a factory constructor.
return State;
function State(gstId, config) {
var parameters, allParameterNames, allParameterValues,
plotDiv, dynamicEl, dynamicElByElId;
dynamicEl = [];
dynamicElByElId = {};
stateInst += 1;
logme('MESSAGE: Creating state instance # ' + stateInst + '.');
// Initially, there are no parameters to track. So, we will instantiate
// an empty object.
//
// As we parse the JSON config object, we will add parameters as
// named properties. For example
//
// parameters.a = {...};
//
// will be created for the parameter 'a'.
parameters = {};
// Check that the required parameters config object is available.
if ($.isPlainObject(config.parameters) === false) {
logme('ERROR: Expected config.parameters to be an object. It is not.');
logme('config.parameters = ', config.parameters);
return;
}
// If config.parameters.param is an array, pass it to the processor
// element by element.
if ($.isArray(config.parameters.param) === true) {
(function (c1) {
while (c1 < config.parameters.param.length) {
processParameter(config.parameters.param[c1]);
c1 += 1;
}
}(0));
}
// If config.parameters.param is an object, pass this object to the
// processor directly.
else if ($.isPlainObject(config.parameters.param) === true) {
processParameter(config.parameters.param);
}
// If config.parameters.param is some other type, report an error and
// do not continue.
else {
logme('ERROR: config.parameters.param is of an unsupported type.');
logme('config.parameters.param = ', config.parameters.param);
return;
}
// Instead of building these arrays every time when some component
// requests them, we will create them in the beginning, and then update
// each element individually when some parameter's value changes.
//
// Then we can just return the required array, instead of iterating
// over all of the properties of the 'parameters' object, and
// extracting their names/values one by one.
allParameterNames = [];
allParameterValues = [];
// Populate 'allParameterNames', and 'allParameterValues' with data.
generateHelperArrays();
// The constructor will return an object with methods to operate on
// it's private properties.
return {
'getParameterValue': getParameterValue,
'setParameterValue': setParameterValue,
'getParamObj': getParamObj,
'getAllParameterNames': getAllParameterNames,
'getAllParameterValues': getAllParameterValues,
'bindUpdatePlotEvent': bindUpdatePlotEvent,
'addDynamicEl': addDynamicEl,
// plde is an abbreviation for Plot Label Dynamic Elements.
plde: []
};
function getAllParameterNames() {
return allParameterNames;
}
function getAllParameterValues() {
return allParameterValues;
}
function getParamObj(paramName) {
if (parameters.hasOwnProperty(paramName) === false) {
logme('ERROR: Object parameters does not have a property named "' + paramName + '".');
return;
}
return parameters[paramName];
}
function bindUpdatePlotEvent(newPlotDiv, callback) {
plotDiv = newPlotDiv;
plotDiv.bind('update_plot', callback);
}
function addDynamicEl(el, func, elId, updateOnEvent) {
var newLength;
newLength = dynamicEl.push({
'el': el,
'func': func,
'elId': elId,
'updateOnEvent': updateOnEvent
});
if (typeof dynamicElByElId[elId] !== 'undefined') {
logme(
'ERROR: Duplicate dynamic element ID "' + elId + '" found.'
);
} else {
dynamicElByElId[elId] = dynamicEl[newLength - 1];
}
}
function getParameterValue(paramName) {
// If the name of the constant is not tracked by state, return an
// 'undefined' value.
if (parameters.hasOwnProperty(paramName) === false) {
logme('ERROR: Object parameters does not have a property named "' + paramName + '".');
return;
}
return parameters[paramname].value;
}
// ####################################################################
//
// Function: setParameterValue(paramName, paramValue, element)
// --------------------------------------------------
//
//
// This function can be called from a callback, registered by a slider
// or a text input, when specific events ('slide' or 'change') are
// triggered.
//
// The 'paramName' is the name of the parameter in 'parameters' object
// whose value must be updated to the new value of 'paramValue'.
//
// Before we update the value, we must check that:
//
// 1.) the parameter named as 'paramName' actually exists in the
// 'parameters' object;
// 2.) the value 'paramValue' is a valid floating-point number, and
// it lies within the range specified by the 'min' and 'max'
// properties of the stored parameter object.
//
// If 'paramName' and 'paramValue' turn out to be valid, we will update
// the stored value in the parameter with the new value, and also
// update all of the text inputs and the slider that correspond to this
// parameter (if any), so that they reflect the new parameter's value.
// Finally, the helper array 'allParameterValues' will also be updated
// to reflect the change.
//
// If something went wrong (for example the new value is outside the
// allowed range), then we will reset the 'element' to display the
// original value.
//
// ####################################################################
function setParameterValue(paramName, paramValue, element, slider, updateOnEvent) {
var paramValueNum, c1;
// If a parameter with the name specified by the 'paramName'
// parameter is not tracked by state, do not do anything.
if (parameters.hasOwnProperty(paramName) === false) {
logme('ERROR: Object parameters does not have a property named "' + paramName + '".');
return;
}
// Try to convert the passed value to a valid floating-point
// number.
paramValueNum = parseFloat(paramValue);
// We are interested only in valid float values. NaN, -INF,
// +INF we will disregard.
if (isFinite(paramValueNum) === false) {
logme('ERROR: New parameter value is not a floating-point number.');
logme('paramValue = ', paramValue);
return;
}
if (paramValueNum < parameters[paramName].min) {
paramValueNum = parameters[paramName].min;
} else if (paramValueNum > parameters[paramName].max) {
paramValueNum = parameters[paramName].max;
}
parameters[paramName].value = paramValueNum;
// Update all text inputs with the new parameter's value.
for (c1 = 0; c1 < parameters[paramName].inputDivs.length; c1 += 1) {
parameters[paramName].inputDivs[c1].val(paramValueNum);
}
// Update the single slider with the new parameter's value.
if ((slider === false) && (parameters[paramName].sliderDiv !== null)) {
parameters[paramName].sliderDiv.slider('value', paramValueNum);
}
// Update the helper array with the new parameter's value.
allParameterValues[parameters[paramName].helperArrayIndex] = paramValueNum;
for (c1 = 0; c1 < dynamicEl.length; c1++) {
if (
((updateOnEvent !== undefined) && (dynamicEl[c1].updateOnEvent === updateOnEvent)) ||
(updateOnEvent === undefined)
) {
// If we have a DOM element, call the function "paste" the answer into the DIV.
if (dynamicEl[c1].el !== null) {
dynamicEl[c1].el.html(dynamicEl[c1].func.apply(window, allParameterValues));
}
// If we DO NOT have an element, simply call the function. The function can then
// manipulate all the DOM elements it wants, without the fear of them being overwritten
// by us afterwards.
else {
dynamicEl[c1].func.apply(window, allParameterValues);
}
}
}
// If we have a plot DIV to work with, tell to update.
if (plotDiv !== undefined) {
plotDiv.trigger('update_plot');
}
return true;
} // End-of: function setParameterValue
// ####################################################################
//
// Function: processParameter(obj)
// -------------------------------
//
//
// This function will be run once for each instance of a GST when
// parsing the JSON config object.
//
// 'newParamObj' must be empty from the start for each invocation of
// this function, that's why we will declare it locally.
//
// We will parse the passed object 'obj' and populate the 'newParamObj'
// object with required properties.
//
// Since there will be many properties that are of type floating-point
// number, we will have a separate function for parsing them.
//
// processParameter() will fail right away if 'obj' does not have a
// '@var' property which represents the name of the parameter we want
// to process.
//
// If, after all of the properties have been processed, we reached the
// end of the function successfully, the 'newParamObj' will be added to
// the 'parameters' object (that is defined in the scope of State()
// function) as a property named as the name of the parameter.
//
// If at least one of the properties from 'obj' does not get correctly
// parsed, then the parameter represented by 'obj' will be disregarded.
// It will not be available to user-defined plotting functions, and
// things will most likely break. We will notify the user about this.
//
// ####################################################################
function processParameter(obj) {
var paramName, newParamObj;
if (typeof obj['@var'] !== 'string') {
logme('ERROR: Expected obj["@var"] to be a string. It is not.');
logme('obj["@var"] = ', obj['@var']);
return;
}
paramName = obj['@var'];
newParamObj = {};
if (
(processFloat('@min', 'min') === false) ||
(processFloat('@max', 'max') === false) ||
(processFloat('@step', 'step') === false) ||
(processFloat('@initial', 'value') === false)
) {
logme('ERROR: A required property is missing. Not creating parameter "' + paramName + '"');
return;
}
// Pointers to text input and slider DIV elements that this
// parameter will be attached to. Initially there are none. When we
// will create text inputs and sliders, we will update these
// properties.
newParamObj.inputDivs = [];
newParamObj.sliderDiv = null;
// Everything went well, so save the new parameter object.
parameters[paramName] = newParamObj;
return;
function processFloat(attrName, newAttrName) {
var attrValue;
if (typeof obj[attrName] !== 'string') {
logme('ERROR: Expected obj["' + attrName + '"] to be a string. It is not.');
logme('obj["' + attrName + '"] = ', obj[attrName]);
return false;
} else {
attrValue = parseFloat(obj[attrName]);
if (isFinite(attrValue) === false) {
logme('ERROR: Expected obj["' + attrName + '"] to be a valid floating-point number. It is not.');
logme('obj["' + attrName + '"] = ', obj[attrName]);
return false;
}
}
newParamObj[newAttrName] = attrValue;
return true;
} // End-of: function processFloat
} // End-of: function processParameter
// ####################################################################
//
// Function: generateHelperArrays()
// -------------------------------
//
//
// Populate 'allParameterNames' and 'allParameterValues' with data.
// Link each parameter object with the corresponding helper array via
// an index 'helperArrayIndex'. It will be the same for both of the
// arrays.
//
// NOTE: It is important to remember to update these helper arrays
// whenever a new parameter is added (or one is removed), or when a
// parameter's value changes.
//
// ####################################################################
function generateHelperArrays() {
var paramName, c1;
c1 = 0;
for (paramName in parameters) {
allParameterNames.push(paramName);
allParameterValues.push(parameters[paramName].value);
parameters[paramName].helperArrayIndex = c1;
c1 += 1;
}
}
} // End-of: function State
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)

View File

@@ -1,133 +0,0 @@
class @SelfAssessment
constructor: (element) ->
@el = $(element).find('section.self-assessment')
@id = @el.data('id')
@ajax_url = @el.data('ajax-url')
@state = @el.data('state')
@allow_reset = @el.data('allow_reset')
# valid states: 'initial', 'assessing', 'request_hint', 'done'
# Where to put the rubric once we load it
@errors_area = @$('.error')
@answer_area = @$('textarea.answer')
@rubric_wrapper = @$('.rubric-wrapper')
@hint_wrapper = @$('.hint-wrapper')
@message_wrapper = @$('.message-wrapper')
@submit_button = @$('.submit-button')
@reset_button = @$('.reset-button')
@reset_button.click @reset
@find_assessment_elements()
@find_hint_elements()
@rebind()
# locally scoped jquery.
$: (selector) ->
$(selector, @el)
rebind: () =>
# rebind to the appropriate function for the current state
@submit_button.unbind('click')
@submit_button.show()
@reset_button.hide()
@hint_area.attr('disabled', false)
if @state == 'initial'
@answer_area.attr("disabled", false)
@submit_button.prop('value', 'Submit')
@submit_button.click @save_answer
else if @state == 'assessing'
@answer_area.attr("disabled", true)
@submit_button.prop('value', 'Submit assessment')
@submit_button.click @save_assessment
else if @state == 'request_hint'
@answer_area.attr("disabled", true)
@submit_button.prop('value', 'Submit hint')
@submit_button.click @save_hint
else if @state == 'done'
@answer_area.attr("disabled", true)
@hint_area.attr('disabled', true)
@submit_button.hide()
if @allow_reset
@reset_button.show()
else
@reset_button.hide()
find_assessment_elements: ->
@assessment = @$('select.assessment')
find_hint_elements: ->
@hint_area = @$('textarea.hint')
save_answer: (event) =>
event.preventDefault()
if @state == 'initial'
data = {'student_answer' : @answer_area.val()}
$.postWithPrefix "#{@ajax_url}/save_answer", data, (response) =>
if response.success
@rubric_wrapper.html(response.rubric_html)
@state = 'assessing'
@find_assessment_elements()
@rebind()
else
@errors_area.html(response.error)
else
@errors_area.html('Problem state got out of sync. Try reloading the page.')
save_assessment: (event) =>
event.preventDefault()
if @state == 'assessing'
data = {'assessment' : @assessment.find(':selected').text()}
$.postWithPrefix "#{@ajax_url}/save_assessment", data, (response) =>
if response.success
@state = response.state
if @state == 'request_hint'
@hint_wrapper.html(response.hint_html)
@find_hint_elements()
else if @state == 'done'
@message_wrapper.html(response.message_html)
@allow_reset = response.allow_reset
@rebind()
else
@errors_area.html(response.error)
else
@errors_area.html('Problem state got out of sync. Try reloading the page.')
save_hint: (event) =>
event.preventDefault()
if @state == 'request_hint'
data = {'hint' : @hint_area.val()}
$.postWithPrefix "#{@ajax_url}/save_hint", data, (response) =>
if response.success
@message_wrapper.html(response.message_html)
@state = 'done'
@allow_reset = response.allow_reset
@rebind()
else
@errors_area.html(response.error)
else
@errors_area.html('Problem state got out of sync. Try reloading the page.')
reset: (event) =>
event.preventDefault()
if @state == 'done'
$.postWithPrefix "#{@ajax_url}/reset", {}, (response) =>
if response.success
@answer_area.val('')
@rubric_wrapper.html('')
@hint_wrapper.html('')
@message_wrapper.html('')
@state = 'initial'
@rebind()
@reset_button.hide()
else
@errors_area.html(response.error)
else
@errors_area.html('Problem state got out of sync. Try reloading the page.')

View File

@@ -0,0 +1,660 @@
"""
A Self Assessment module that allows students to write open-ended responses,
submit, then see a rubric and rate themselves. Persists student supplied
hints, answers, and assessment judgment (currently only correct/incorrect).
Parses xml definition file--see below for exact format.
"""
import copy
from fs.errors import ResourceNotFoundError
import itertools
import json
import logging
from lxml import etree
from lxml.html import rewrite_links
from path import path
import os
import sys
import hashlib
import capa.xqueue_interface as xqueue_interface
from pkg_resources import resource_string
from .capa_module import only_one, ComplexEncoder
from .editing_module import EditingDescriptor
from .html_checker import check_html
from progress import Progress
from .stringify import stringify_children
from .xml_module import XmlDescriptor
from xmodule.modulestore import Location
from capa.util import *
import openendedchild
from mitxmako.shortcuts import render_to_string
from numpy import median
from datetime import datetime
from combined_open_ended_rubric import CombinedOpenEndedRubric
log = logging.getLogger("mitx.courseware")
class OpenEndedModule(openendedchild.OpenEndedChild):
"""
The open ended module supports all external open ended grader problems.
Sample XML file:
<openended min_score_to_attempt="1" max_score_to_attempt="1">
<openendedparam>
<initial_display>Enter essay here.</initial_display>
<answer_display>This is the answer.</answer_display>
<grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload>
</openendedparam>
</openended>
"""
def setup_response(self, system, location, definition, descriptor):
"""
Sets up the response type.
@param system: Modulesystem object
@param location: The location of the problem
@param definition: The xml definition of the problem
@param descriptor: The OpenEndedDescriptor associated with this
@return: None
"""
oeparam = definition['oeparam']
self.url = definition.get('url', None)
self.queue_name = definition.get('queuename', self.DEFAULT_QUEUE)
self.message_queue_name = definition.get('message-queuename', self.DEFAULT_MESSAGE_QUEUE)
#This is needed to attach feedback to specific responses later
self.submission_id = None
self.grader_id = None
if oeparam is None:
raise ValueError("No oeparam found in problem xml.")
if self.prompt is None:
raise ValueError("No prompt found in problem xml.")
if self.rubric is None:
raise ValueError("No rubric found in problem xml.")
self._parse(oeparam, self.prompt, self.rubric, system)
if self.created == True and self.state == self.ASSESSING:
self.created = False
self.send_to_grader(self.latest_answer(), system)
self.created = False
def _parse(self, oeparam, prompt, rubric, system):
'''
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 = stringify_children(prompt)
rubric_string = stringify_children(rubric)
self.prompt = prompt_string
self.rubric = rubric_string
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)
self.initial_display = find_with_default(oeparam, 'initial_display', '')
self.answer = find_with_default(oeparam, 'answer_display', 'No answer given.')
parsed_grader_payload.update({
'location': system.location.url(),
'course_id': system.course_id,
'prompt': prompt_string,
'rubric': rubric_string,
'initial_display': self.initial_display,
'answer': self.answer,
})
updated_grader_payload = json.dumps(parsed_grader_payload)
self.payload = {'grader_payload': updated_grader_payload}
def skip_post_assessment(self, get, system):
"""
Ajax function that allows one to skip the post assessment phase
@param get: AJAX dictionary
@param system: ModuleSystem
@return: Success indicator
"""
self.state = self.DONE
return {'success': True}
def message_post(self, get, system):
"""
Handles a student message post (a reaction to the grade they received from an open ended grader type)
Returns a boolean success/fail and an error message
"""
event_info = dict()
event_info['problem_id'] = system.location.url()
event_info['student_id'] = system.anonymous_student_id
event_info['survey_responses'] = get
survey_responses = event_info['survey_responses']
for tag in ['feedback', 'submission_id', 'grader_id', 'score']:
if tag not in survey_responses:
return {'success': False, 'msg': "Could not find needed tag {0}".format(tag)}
try:
submission_id = int(survey_responses['submission_id'])
grader_id = int(survey_responses['grader_id'])
feedback = str(survey_responses['feedback'].encode('ascii', 'ignore'))
score = int(survey_responses['score'])
except:
error_message = ("Could not parse submission id, grader id, "
"or feedback from message_post ajax call. Here is the message data: {0}".format(
survey_responses))
log.exception(error_message)
return {'success': False, 'msg': "There was an error saving your feedback. Please contact course staff."}
qinterface = system.xqueue['interface']
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
anonymous_student_id = system.anonymous_student_id
queuekey = xqueue_interface.make_hashkey(str(system.seed) + qtime +
anonymous_student_id +
str(len(self.history)))
xheader = xqueue_interface.make_xheader(
lms_callback_url=system.xqueue['callback_url'],
lms_key=queuekey,
queue_name=self.message_queue_name
)
student_info = {'anonymous_student_id': anonymous_student_id,
'submission_time': qtime,
}
contents = {
'feedback': feedback,
'submission_id': submission_id,
'grader_id': grader_id,
'score': score,
'student_info': json.dumps(student_info),
}
(error, msg) = qinterface.send_to_queue(header=xheader,
body=json.dumps(contents))
#Convert error to a success value
success = True
if error:
success = False
self.state = self.DONE
return {'success': success, 'msg': "Successfully submitted your feedback."}
def send_to_grader(self, submission, system):
"""
Send a given submission to the grader, via the xqueue
@param submission: The student submission to send to the grader
@param system: Modulesystem
@return: Boolean true (not useful right now)
"""
# Prepare xqueue request
#------------------------------------------------------------
qinterface = system.xqueue['interface']
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
anonymous_student_id = system.anonymous_student_id
# Generate header
queuekey = xqueue_interface.make_hashkey(str(system.seed) + qtime +
anonymous_student_id +
str(len(self.history)))
xheader = xqueue_interface.make_xheader(lms_callback_url=system.xqueue['callback_url'],
lms_key=queuekey,
queue_name=self.queue_name)
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, }
return True
def _update_score(self, score_msg, queuekey, system):
"""
Called by xqueue to update the score
@param score_msg: The message from xqueue
@param queuekey: The key sent by xqueue
@param system: Modulesystem
@return: Boolean True (not useful currently)
"""
new_score_msg = self._parse_score_msg(score_msg)
if not new_score_msg['valid']:
score_msg['feedback'] = 'Invalid grader reply. Please contact the course staff.'
self.record_latest_score(new_score_msg['score'])
self.record_latest_post_assessment(score_msg)
self.state = self.POST_ASSESSMENT
return True
def get_answers(self):
"""
Gets and shows the answer for this problem.
@return: Answer html
"""
anshtml = '<span class="openended-answer"><pre><code>{0}</code></pre></span>'.format(self.answer)
return {self.answer_id: anshtml}
def get_initial_display(self):
"""
Gets and shows the initial display for the input box.
@return: Initial display html
"""
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 displayincorrect-icon.pnged 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 encode_values(feedback_type, value):
feedback_type = str(feedback_type).encode('ascii', 'ignore')
if not isinstance(value, basestring):
value = str(value)
value = value.encode('ascii', 'ignore')
return feedback_type, value
def format_feedback(feedback_type, value):
feedback_type, value = encode_values(feedback_type, value)
feedback = """
<div class="{feedback_type}">
{value}
</div>
""".format(feedback_type=feedback_type, value=value)
return feedback
def format_feedback_hidden(feedback_type, value):
feedback_type, value = encode_values(feedback_type, value)
feedback = """
<input class="{feedback_type}" type="hidden" value="{value}" />
""".format(feedback_type=feedback_type, value=value)
return feedback
# 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', 'submission_id', 'grader_id']:
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)
feedback_list_part1 = u"\n".join(format_feedback(k, v) for k, v in feedback_lst)
else:
feedback_list_part1 = format_feedback('errors', response_items['feedback'])
feedback_list_part2 = (u"\n".join([format_feedback_hidden(feedback_type, value)
for feedback_type, value in response_items.items()
if feedback_type in ['submission_id', 'grader_id']]))
return u"\n".join([feedback_list_part1, feedback_list_part2])
def _format_feedback(self, response_items):
"""
Input:
Dictionary called feedback. Must contain keys seen below.
Output:
Return error message or feedback template
"""
log.debug(response_items)
rubric_feedback=""
feedback = self._convert_longform_feedback_to_html(response_items)
if response_items['rubric_scores_complete']==True:
rubric_feedback = CombinedOpenEndedRubric.render_rubric(response_items['rubric_xml'])
if not response_items['success']:
return system.render_template("open_ended_error.html",
{'errors': feedback})
feedback_template = render_to_string("open_ended_feedback.html", {
'grader_type': response_items['grader_type'],
'score': "{0} / {1}".format(response_items['score'], self.max_score()),
'feedback': feedback,
'rubric_feedback' : rubric_feedback
})
return feedback_template
def _parse_score_msg(self, score_msg, join_feedback=True):
"""
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 = {'valid': False, 'score': 0, 'feedback': ''}
try:
score_result = json.loads(score_msg)
except (TypeError, ValueError):
error_message = ("External grader message should be a JSON-serialized dict."
" Received score_msg = {0}".format(score_msg))
log.error(error_message)
fail['feedback'] = error_message
return fail
if not isinstance(score_result, dict):
error_message = ("External grader message should be a JSON-serialized dict."
" Received score_result = {0}".format(score_result))
log.error(error_message)
fail['feedback'] = error_message
return fail
for tag in ['score', 'feedback', 'grader_type', 'success', 'grader_id', 'submission_id']:
if tag not in score_result:
error_message = ("External grader message is missing required tag: {0}"
.format(tag))
log.error(error_message)
fail['feedback'] = error_message
return fail
#This is to support peer grading
if isinstance(score_result['score'], list):
feedback_items = []
for i in xrange(0, len(score_result['score'])):
new_score_result = {
'score': score_result['score'][i],
'feedback': score_result['feedback'][i],
'grader_type': score_result['grader_type'],
'success': score_result['success'],
'grader_id': score_result['grader_id'][i],
'submission_id': score_result['submission_id'],
'rubric_scores_complete' : score_result['rubric_scores_complete'],
'rubric_xml' : score_result['rubric_xml'],
}
feedback_items.append(self._format_feedback(new_score_result))
if join_feedback:
feedback = "".join(feedback_items)
else:
feedback = feedback_items
score = int(median(score_result['score']))
else:
#This is for instructor and ML grading
feedback = self._format_feedback(score_result)
score = score_result['score']
self.submission_id = score_result['submission_id']
self.grader_id = score_result['grader_id']
return {'valid': True, 'score': score, 'feedback': feedback}
def latest_post_assessment(self, short_feedback=False, join_feedback=True):
"""
Gets the latest feedback, parses, and returns
@param short_feedback: If the long feedback is wanted or not
@return: Returns formatted feedback
"""
if not self.history:
return ""
feedback_dict = self._parse_score_msg(self.history[-1].get('post_assessment', ""), join_feedback=join_feedback)
if not short_feedback:
return feedback_dict['feedback'] if feedback_dict['valid'] else ''
if feedback_dict['valid']:
short_feedback = self._convert_longform_feedback_to_html(
json.loads(self.history[-1].get('post_assessment', "")))
return short_feedback if feedback_dict['valid'] else ''
def format_feedback_with_evaluation(self, feedback):
"""
Renders a given html feedback into an evaluation template
@param feedback: HTML feedback
@return: Rendered html
"""
context = {'msg': feedback, 'id': "1", 'rows': 50, 'cols': 50}
html = render_to_string('open_ended_evaluation.html', context)
return html
def handle_ajax(self, dispatch, get, system):
'''
This is called by courseware.module_render, to handle an AJAX call.
"get" is request.POST.
Returns a json dictionary:
{ 'progress_changed' : True/False,
'progress' : 'none'/'in_progress'/'done',
<other request-specific values here > }
'''
handlers = {
'save_answer': self.save_answer,
'score_update': self.update_score,
'save_post_assessment': self.message_post,
'skip_post_assessment': self.skip_post_assessment,
'check_for_score': self.check_for_score,
}
if dispatch not in handlers:
return 'Error'
before = self.get_progress()
d = handlers[dispatch](get, system)
after = self.get_progress()
d.update({
'progress_changed': after != before,
'progress_status': Progress.to_js_status_str(after),
})
return json.dumps(d, cls=ComplexEncoder)
def check_for_score(self, get, system):
"""
Checks to see if a score has been received yet.
@param get: AJAX get dictionary
@param system: Modulesystem (needed to align with other ajax functions)
@return: Returns the current state
"""
state = self.state
return {'state': state}
def save_answer(self, get, system):
"""
Saves a student answer
@param get: AJAX get dictionary
@param system: modulesystem
@return: Success indicator
"""
if self.attempts > self.max_attempts:
# If too many attempts, prevent student from saving answer and
# seeing rubric. In normal use, students shouldn't see this because
# they won't see the reset button once they're out of attempts.
return {
'success': False,
'error': 'Too many attempts.'
}
if self.state != self.INITIAL:
return self.out_of_sync_error(get)
# add new history element with answer and empty score and hint.
self.new_history_entry(get['student_answer'])
self.send_to_grader(get['student_answer'], system)
self.change_state(self.ASSESSING)
return {'success': True, }
def update_score(self, get, system):
"""
Updates the current score via ajax. Called by xqueue.
Input: AJAX get dictionary, modulesystem
Output: None
"""
queuekey = get['queuekey']
score_msg = get['xqueue_body']
#TODO: Remove need for cmap
self._update_score(score_msg, queuekey, system)
return dict() # No AJAX return is needed
def get_html(self, system):
"""
Gets the HTML for this problem and renders it
Input: Modulesystem object
Output: Rendered HTML
"""
#set context variables and render template
if self.state != self.INITIAL:
latest = self.latest_answer()
previous_answer = latest if latest is not None else self.initial_display
post_assessment = self.latest_post_assessment()
score = self.latest_score()
correct = 'correct' if self.is_submission_correct(score) else 'incorrect'
else:
post_assessment = ""
correct = ""
previous_answer = self.initial_display
context = {
'prompt': self.prompt,
'previous_answer': previous_answer,
'state': self.state,
'allow_reset': self._allow_reset(),
'rows': 30,
'cols': 80,
'id': 'open_ended',
'msg': post_assessment,
'child_type': 'openended',
'correct': correct,
}
log.debug(context)
html = system.render_template('open_ended.html', context)
return html
class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor):
"""
Module for adding open ended response questions to courses
"""
mako_template = "widgets/html-edit.html"
module_class = OpenEndedModule
filename_extension = "xml"
stores_state = True
has_score = True
template_dir_name = "openended"
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
js_module_name = "HTMLEditingDescriptor"
@classmethod
def definition_from_xml(cls, xml_object, system):
"""
Pull out the open ended parameters into a dictionary.
Returns:
{
'oeparam': 'some-html'
}
"""
for child in ['openendedparam']:
if len(xml_object.xpath(child)) != 1:
raise ValueError("Open Ended definition must include exactly one '{0}' tag".format(child))
def parse(k):
"""Assumes that xml_object has child k"""
return xml_object.xpath(k)[0]
return {'oeparam': parse('openendedparam'), }
def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.'''
elt = etree.Element('openended')
def add_child(k):
child_str = '<{tag}>{body}</{tag}>'.format(tag=k, body=self.definition[k])
child_node = etree.fromstring(child_str)
elt.append(child_node)
for child in ['openendedparam']:
add_child(child)
return elt

View File

@@ -0,0 +1,263 @@
import copy
from fs.errors import ResourceNotFoundError
import itertools
import json
import logging
from lxml import etree
from lxml.html import rewrite_links
from path import path
import os
import sys
import hashlib
import capa.xqueue_interface as xqueue_interface
from pkg_resources import resource_string
from .capa_module import only_one, ComplexEncoder
from .editing_module import EditingDescriptor
from .html_checker import check_html
from progress import Progress
from .stringify import stringify_children
from .xml_module import XmlDescriptor
from xmodule.modulestore import Location
from capa.util import *
from datetime import datetime
log = logging.getLogger("mitx.courseware")
# Set the default number of max attempts. Should be 1 for production
# Set higher for debugging/testing
# attempts specified in xml definition overrides this.
MAX_ATTEMPTS = 1
# Set maximum available number of points.
# Overriden by max_score specified in xml.
MAX_SCORE = 1
class OpenEndedChild():
"""
States:
initial (prompt, textbox shown)
|
assessing (read-only textbox, rubric + assessment input shown for self assessment, response queued for open ended)
|
post_assessment (read-only textbox, read-only rubric and assessment, hint input box shown)
|
done (submitted msg, green checkmark, everything else read-only. If attempts < max, shows
a reset button that goes back to initial state. Saves previous
submissions too.)
"""
DEFAULT_QUEUE = 'open-ended'
DEFAULT_MESSAGE_QUEUE = 'open-ended-message'
max_inputfields = 1
STATE_VERSION = 1
# states
INITIAL = 'initial'
ASSESSING = 'assessing'
POST_ASSESSMENT = 'post_assessment'
DONE = 'done'
#This is used to tell students where they are at in the module
HUMAN_NAMES = {
'initial': 'Started',
'assessing': 'Being scored',
'post_assessment': 'Scoring finished',
'done': 'Problem complete',
}
def __init__(self, system, location, definition, descriptor, static_data,
instance_state=None, shared_state=None, **kwargs):
# Load instance state
if instance_state is not None:
instance_state = json.loads(instance_state)
else:
instance_state = {}
# History is a list of tuples of (answer, score, hint), where hint may be
# None for any element, and score and hint can be None for the last (current)
# element.
# Scores are on scale from 0 to max_score
self.history = instance_state.get('history', [])
self.state = instance_state.get('state', self.INITIAL)
self.created = instance_state.get('created', False)
self.attempts = instance_state.get('attempts', 0)
self.max_attempts = static_data['max_attempts']
self.prompt = static_data['prompt']
self.rubric = static_data['rubric']
# Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect).
self._max_score = static_data['max_score']
self.setup_response(system, location, definition, descriptor)
def setup_response(self, system, location, definition, descriptor):
"""
Needs to be implemented by the inheritors of this module. Sets up additional fields used by the child modules.
@param system: Modulesystem
@param location: Module location
@param definition: XML definition
@param descriptor: Descriptor of the module
@return: None
"""
pass
def latest_answer(self):
"""None if not available"""
if not self.history:
return ""
return self.history[-1].get('answer', "")
def latest_score(self):
"""None if not available"""
if not self.history:
return None
return self.history[-1].get('score')
def latest_post_assessment(self):
"""None if not available"""
if not self.history:
return ""
return self.history[-1].get('post_assessment', "")
def new_history_entry(self, answer):
"""
Adds a new entry to the history dictionary
@param answer: The student supplied answer
@return: None
"""
self.history.append({'answer': answer})
def record_latest_score(self, score):
"""Assumes that state is right, so we're adding a score to the latest
history element"""
self.history[-1]['score'] = score
def record_latest_post_assessment(self, post_assessment):
"""Assumes that state is right, so we're adding a score to the latest
history element"""
self.history[-1]['post_assessment'] = post_assessment
def change_state(self, new_state):
"""
A centralized place for state changes--allows for hooks. If the
current state matches the old state, don't run any hooks.
"""
if self.state == new_state:
return
self.state = new_state
if self.state == self.DONE:
self.attempts += 1
def get_instance_state(self):
"""
Get the current score and state
"""
state = {
'version': self.STATE_VERSION,
'history': self.history,
'state': self.state,
'max_score': self._max_score,
'attempts': self.attempts,
'created': False,
}
return json.dumps(state)
def _allow_reset(self):
"""Can the module be reset?"""
return (self.state == self.DONE and self.attempts < self.max_attempts)
def max_score(self):
"""
Return max_score
"""
return self._max_score
def get_score(self):
"""
Returns the last score in the list
"""
score = self.latest_score()
return {'score': score if score is not None else 0,
'total': self._max_score}
def reset(self, system):
"""
If resetting is allowed, reset the state.
Returns {'success': bool, 'error': msg}
(error only present if not success)
"""
self.change_state(self.INITIAL)
return {'success': True}
def get_progress(self):
'''
For now, just return last score / max_score
'''
if self._max_score > 0:
try:
return Progress(self.get_score()['score'], self._max_score)
except Exception as err:
log.exception("Got bad progress")
return None
return None
def out_of_sync_error(self, get, msg=''):
"""
return dict out-of-sync error message, and also log.
"""
log.warning("Assessment module state out sync. state: %r, get: %r. %s",
self.state, get, msg)
return {'success': False,
'error': 'The problem state got out-of-sync'}
def get_html(self):
"""
Needs to be implemented by inheritors. Renders the HTML that students see.
@return:
"""
pass
def handle_ajax(self):
"""
Needs to be implemented by child modules. Handles AJAX events.
@return:
"""
pass
def is_submission_correct(self, score):
"""
Checks to see if a given score makes the answer correct. Very naive right now (>66% is correct)
@param score: Numeric score.
@return: Boolean correct.
"""
correct = False
if(isinstance(score, (int, long, float, complex))):
score_ratio = int(score) / float(self.max_score())
correct = (score_ratio >= 0.66)
return correct
def is_last_response_correct(self):
"""
Checks to see if the last response in the module is correct.
@return: 'correct' if correct, otherwise 'incorrect'
"""
score = self.get_score()['score']
correctness = 'correct' if self.is_submission_correct(score) else 'incorrect'
return correctness

View File

@@ -1,10 +1,3 @@
"""
A Self Assessment module that allows students to write open-ended responses,
submit, then see a rubric and rate themselves. Persists student supplied
hints, answers, and assessment judgment (currently only correct/incorrect).
Parses xml definition file--see below for exact format.
"""
import copy
from fs.errors import ResourceNotFoundError
import itertools
@@ -26,205 +19,50 @@ from .stringify import stringify_children
from .x_module import XModule
from .xml_module import XmlDescriptor
from xmodule.modulestore import Location
import openendedchild
from combined_open_ended_rubric import CombinedOpenEndedRubric
log = logging.getLogger("mitx.courseware")
# Set the default number of max attempts. Should be 1 for production
# Set higher for debugging/testing
# attempts specified in xml definition overrides this.
MAX_ATTEMPTS = 1
# Set maximum available number of points.
# Overriden by max_score specified in xml.
MAX_SCORE = 1
class SelfAssessmentModule(XModule):
class SelfAssessmentModule(openendedchild.OpenEndedChild):
"""
States:
A Self Assessment module that allows students to write open-ended responses,
submit, then see a rubric and rate themselves. Persists student supplied
hints, answers, and assessment judgment (currently only correct/incorrect).
Parses xml definition file--see below for exact format.
initial (prompt, textbox shown)
|
assessing (read-only textbox, rubric + assessment input shown)
|
request_hint (read-only textbox, read-only rubric and assessment, hint input box shown)
|
done (submitted msg, green checkmark, everything else read-only. If attempts < max, shows
a reset button that goes back to initial state. Saves previous
submissions too.)
Sample XML format:
<selfassessment>
<hintprompt>
What hint about this problem would you give to someone?
</hintprompt>
<submitmessage>
Save Succcesful. Thanks for participating!
</submitmessage>
</selfassessment>
"""
STATE_VERSION = 1
# states
INITIAL = 'initial'
ASSESSING = 'assessing'
REQUEST_HINT = 'request_hint'
DONE = 'done'
js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/display.coffee')]}
js_module_name = "SelfAssessment"
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
def setup_response(self, system, location, definition, descriptor):
"""
Definition file should have 4 blocks -- prompt, rubric, submitmessage, hintprompt,
and two optional attributes:
attempts, which should be an integer that defaults to 1.
If it's > 1, the student will be able to re-submit after they see
the rubric.
max_score, which should be an integer that defaults to 1.
It defines the maximum number of points a student can get. Assumed to be integer scale
from 0 to max_score, with an interval of 1.
Note: all the submissions are stored.
Sample file:
<selfassessment attempts="1" max_score="1">
<prompt>
Insert prompt text here. (arbitrary html)
</prompt>
<rubric>
Insert grading rubric here. (arbitrary html)
</rubric>
<hintprompt>
Please enter a hint below: (arbitrary html)
</hintprompt>
<submitmessage>
Thanks for submitting! (arbitrary html)
</submitmessage>
</selfassessment>
Sets up the module
@param system: Modulesystem
@param location: location, to let the module know where it is.
@param definition: XML definition of the module.
@param descriptor: SelfAssessmentDescriptor
@return: None
"""
# Load instance state
if instance_state is not None:
instance_state = json.loads(instance_state)
else:
instance_state = {}
instance_state = self.convert_state_to_current_format(instance_state)
# History is a list of tuples of (answer, score, hint), where hint may be
# None for any element, and score and hint can be None for the last (current)
# element.
# Scores are on scale from 0 to max_score
self.history = instance_state.get('history', [])
self.state = instance_state.get('state', 'initial')
self.attempts = instance_state.get('attempts', 0)
self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS))
# Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect).
self._max_score = int(self.metadata.get('max_score', MAX_SCORE))
self.rubric = definition['rubric']
self.prompt = definition['prompt']
self.submit_message = definition['submitmessage']
self.hint_prompt = definition['hintprompt']
self.prompt = stringify_children(self.prompt)
self.rubric = stringify_children(self.rubric)
def latest_answer(self):
"""None if not available"""
if not self.history:
return None
return self.history[-1].get('answer')
def latest_score(self):
"""None if not available"""
if not self.history:
return None
return self.history[-1].get('score')
def latest_hint(self):
"""None if not available"""
if not self.history:
return None
return self.history[-1].get('hint')
def new_history_entry(self, answer):
self.history.append({'answer': answer})
def record_latest_score(self, score):
"""Assumes that state is right, so we're adding a score to the latest
history element"""
self.history[-1]['score'] = score
def record_latest_hint(self, hint):
"""Assumes that state is right, so we're adding a score to the latest
history element"""
self.history[-1]['hint'] = hint
def change_state(self, new_state):
def get_html(self, system):
"""
A centralized place for state changes--allows for hooks. If the
current state matches the old state, don't run any hooks.
Gets context and renders HTML that represents the module
@param system: Modulesystem
@return: Rendered HTML
"""
if self.state == new_state:
return
self.state = new_state
if self.state == self.DONE:
self.attempts += 1
@staticmethod
def convert_state_to_current_format(old_state):
"""
This module used to use a problematic state representation. This method
converts that into the new format.
Args:
old_state: dict of state, as passed in. May be old.
Returns:
new_state: dict of new state
"""
if old_state.get('version', 0) == SelfAssessmentModule.STATE_VERSION:
# already current
return old_state
# for now, there's only one older format.
new_state = {'version': SelfAssessmentModule.STATE_VERSION}
def copy_if_present(key):
if key in old_state:
new_state[key] = old_state[key]
for to_copy in ['attempts', 'state']:
copy_if_present(to_copy)
# The answers, scores, and hints need to be kept together to avoid them
# getting out of sync.
# NOTE: Since there's only one problem with a few hundred submissions
# in production so far, not trying to be smart about matching up hints
# and submissions in cases where they got out of sync.
student_answers = old_state.get('student_answers', [])
scores = old_state.get('scores', [])
hints = old_state.get('hints', [])
new_state['history'] = [
{'answer': answer,
'score': score,
'hint': hint}
for answer, score, hint in itertools.izip_longest(
student_answers, scores, hints)]
return new_state
def _allow_reset(self):
"""Can the module be reset?"""
return self.state == self.DONE and self.attempts < self.max_attempts
def get_html(self):
#set context variables and render template
if self.state != self.INITIAL:
latest = self.latest_answer()
@@ -235,46 +73,20 @@ class SelfAssessmentModule(XModule):
context = {
'prompt': self.prompt,
'previous_answer': previous_answer,
'ajax_url': self.system.ajax_url,
'initial_rubric': self.get_rubric_html(),
'initial_hint': self.get_hint_html(),
'ajax_url': system.ajax_url,
'initial_rubric': self.get_rubric_html(system),
'initial_hint': self.get_hint_html(system),
'initial_message': self.get_message_html(),
'state': self.state,
'allow_reset': self._allow_reset(),
'child_type': 'selfassessment',
}
html = self.system.render_template('self_assessment_prompt.html', context)
# cdodge: perform link substitutions for any references to course static content (e.g. images)
return rewrite_links(html, self.rewrite_content_links)
def max_score(self):
"""
Return max_score
"""
return self._max_score
def get_score(self):
"""
Returns the last score in the list
"""
score = self.latest_score()
return {'score': score if score is not None else 0,
'total': self._max_score}
def get_progress(self):
'''
For now, just return last score / max_score
'''
if self._max_score > 0:
try:
return Progress(self.get_score()['score'], self._max_score)
except Exception as err:
log.exception("Got bad progress")
return None
return None
html = system.render_template('self_assessment_prompt.html', context)
return html
def handle_ajax(self, dispatch, get):
def handle_ajax(self, dispatch, get, system):
"""
This is called by courseware.module_render, to handle an AJAX call.
"get" is request.POST.
@@ -288,15 +100,14 @@ class SelfAssessmentModule(XModule):
handlers = {
'save_answer': self.save_answer,
'save_assessment': self.save_assessment,
'save_hint': self.save_hint,
'reset': self.reset,
'save_post_assessment': self.save_hint,
}
if dispatch not in handlers:
return 'Error'
before = self.get_progress()
d = handlers[dispatch](get)
d = handlers[dispatch](get, system)
after = self.get_progress()
d.update({
'progress_changed': after != before,
@@ -304,37 +115,30 @@ class SelfAssessmentModule(XModule):
})
return json.dumps(d, cls=ComplexEncoder)
def out_of_sync_error(self, get, msg=''):
"""
return dict out-of-sync error message, and also log.
"""
log.warning("Assessment module state out sync. state: %r, get: %r. %s",
self.state, get, msg)
return {'success': False,
'error': 'The problem state got out-of-sync'}
def get_rubric_html(self):
def get_rubric_html(self, system):
"""
Return the appropriate version of the rubric, based on the state.
"""
if self.state == self.INITIAL:
return ''
rubric_html = CombinedOpenEndedRubric.render_rubric(self.rubric)
# we'll render it
context = {'rubric': self.rubric,
'max_score' : self._max_score,
}
context = {'rubric': rubric_html,
'max_score': self._max_score,
}
if self.state == self.ASSESSING:
context['read_only'] = False
elif self.state in (self.REQUEST_HINT, self.DONE):
elif self.state in (self.POST_ASSESSMENT, self.DONE):
context['read_only'] = True
else:
raise ValueError("Illegal state '%r'" % self.state)
return self.system.render_template('self_assessment_rubric.html', context)
return system.render_template('self_assessment_rubric.html', context)
def get_hint_html(self):
def get_hint_html(self, system):
"""
Return the appropriate version of the hint view, based on state.
"""
@@ -343,7 +147,7 @@ class SelfAssessmentModule(XModule):
if self.state == self.DONE:
# display the previous hint
latest = self.latest_hint()
latest = self.latest_post_assessment()
hint = latest if latest is not None else ''
else:
hint = ''
@@ -351,14 +155,14 @@ class SelfAssessmentModule(XModule):
context = {'hint_prompt': self.hint_prompt,
'hint': hint}
if self.state == self.REQUEST_HINT:
if self.state == self.POST_ASSESSMENT:
context['read_only'] = False
elif self.state == self.DONE:
context['read_only'] = True
else:
raise ValueError("Illegal state '%r'" % self.state)
return self.system.render_template('self_assessment_hint.html', context)
return system.render_template('self_assessment_hint.html', context)
def get_message_html(self):
"""
@@ -370,7 +174,7 @@ class SelfAssessmentModule(XModule):
return """<div class="save_message">{0}</div>""".format(self.submit_message)
def save_answer(self, get):
def save_answer(self, get, system):
"""
After the answer is submitted, show the rubric.
@@ -401,10 +205,10 @@ class SelfAssessmentModule(XModule):
return {
'success': True,
'rubric_html': self.get_rubric_html()
}
'rubric_html': self.get_rubric_html(system)
}
def save_assessment(self, get):
def save_assessment(self, get, system):
"""
Save the assessment. If the student said they're right, don't ask for a
hint, and go straight to the done state. Otherwise, do ask for a hint.
@@ -429,21 +233,20 @@ class SelfAssessmentModule(XModule):
self.record_latest_score(score)
d = {'success': True,}
d = {'success': True, }
if score == self.max_score():
self.change_state(self.DONE)
d['message_html'] = self.get_message_html()
d['allow_reset'] = self._allow_reset()
else:
self.change_state(self.REQUEST_HINT)
d['hint_html'] = self.get_hint_html()
self.change_state(self.POST_ASSESSMENT)
d['hint_html'] = self.get_hint_html(system)
d['state'] = self.state
return d
def save_hint(self, get):
def save_hint(self, get, system):
'''
Save the hint.
Returns a dict { 'success': bool,
@@ -453,63 +256,19 @@ class SelfAssessmentModule(XModule):
with the error key only present if success is False and message_html
only if True.
'''
if self.state != self.REQUEST_HINT:
if self.state != self.POST_ASSESSMENT:
# Note: because we only ask for hints on wrong answers, may not have
# the same number of hints and answers.
return self.out_of_sync_error(get)
self.record_latest_hint(get['hint'])
self.record_latest_post_assessment(get['hint'])
self.change_state(self.DONE)
# To the tracking logs!
event_info = {
'selfassessment_id': self.location.url(),
'state': {
'version': self.STATE_VERSION,
'history': self.history,
}
}
self.system.track_function('save_hint', event_info)
return {'success': True,
'message_html': self.get_message_html(),
'allow_reset': self._allow_reset()}
def reset(self, get):
"""
If resetting is allowed, reset the state.
Returns {'success': bool, 'error': msg}
(error only present if not success)
"""
if self.state != self.DONE:
return self.out_of_sync_error(get)
if self.attempts > self.max_attempts:
return {
'success': False,
'error': 'Too many attempts.'
}
self.change_state(self.INITIAL)
return {'success': True}
def get_instance_state(self):
"""
Get the current score and state
"""
state = {
'version': self.STATE_VERSION,
'history': self.history,
'state': self.state,
'max_score': self._max_score,
'attempts': self.attempts,
}
return json.dumps(state)
class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
"""
Module for adding self assessment questions to courses
@@ -532,13 +291,11 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
Returns:
{
'rubric': 'some-html',
'prompt': 'some-html',
'submitmessage': 'some-html'
'hintprompt': 'some-html'
}
"""
expected_children = ['rubric', 'prompt', 'submitmessage', 'hintprompt']
expected_children = ['submitmessage', 'hintprompt']
for child in expected_children:
if len(xml_object.xpath(child)) != 1:
raise ValueError("Self assessment definition must include exactly one '{0}' tag".format(child))
@@ -547,12 +304,9 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
"""Assumes that xml_object has child k"""
return stringify_children(xml_object.xpath(k)[0])
return {'rubric': parse('rubric'),
'prompt': parse('prompt'),
'submitmessage': parse('submitmessage'),
return {'submitmessage': parse('submitmessage'),
'hintprompt': parse('hintprompt'),
}
}
def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.'''
@@ -563,7 +317,7 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
child_node = etree.fromstring(child_str)
elt.append(child_node)
for child in ['rubric', 'prompt', 'submitmessage', 'hintprompt']:
for child in ['submitmessage', 'hintprompt']:
add_child(child)
return elt

View File

@@ -0,0 +1,90 @@
import unittest
from time import strptime, gmtime
from fs.memoryfs import MemoryFS
from mock import Mock, patch
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
ORG = 'test_org'
COURSE = 'test_course'
NOW = strptime('2013-01-01T01:00:00', '%Y-%m-%dT%H:%M:00')
class DummySystem(ImportSystem):
@patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
def __init__(self, load_error_modules):
xmlstore = XMLModuleStore("data_dir", course_dirs=[],
load_error_modules=load_error_modules)
course_id = "/".join([ORG, COURSE, 'test_run'])
course_dir = "test_dir"
policy = {}
error_tracker = Mock()
parent_tracker = Mock()
super(DummySystem, self).__init__(
xmlstore,
course_id,
course_dir,
policy,
error_tracker,
parent_tracker,
load_error_modules=load_error_modules,
)
class IsNewCourseTestCase(unittest.TestCase):
"""Make sure the property is_new works on courses"""
@staticmethod
def get_dummy_course(start, is_new=None, load_error_modules=True):
"""Get a dummy course"""
system = DummySystem(load_error_modules)
is_new = '' if is_new is None else 'is_new="{0}"'.format(is_new).lower()
start_xml = '''
<course org="{org}" course="{course}"
graceperiod="1 day" url_name="test"
start="{start}"
{is_new}>
<chapter url="hi" url_name="ch" display_name="CH">
<html url_name="h" display_name="H">Two houses, ...</html>
</chapter>
</course>
'''.format(org=ORG, course=COURSE, start=start, is_new=is_new)
return system.process_xml(start_xml)
@patch('xmodule.course_module.time.gmtime')
def test_non_started_yet(self, gmtime_mock):
descriptor = self.get_dummy_course(start='2013-01-05T12:00')
gmtime_mock.return_value = NOW
assert(descriptor.is_new == True)
assert(descriptor.days_until_start == 4)
@patch('xmodule.course_module.time.gmtime')
def test_already_started(self, gmtime_mock):
gmtime_mock.return_value = NOW
descriptor = self.get_dummy_course(start='2012-12-02T12:00')
assert(descriptor.is_new == False)
assert(descriptor.days_until_start < 0)
@patch('xmodule.course_module.time.gmtime')
def test_is_new_set(self, gmtime_mock):
gmtime_mock.return_value = NOW
descriptor = self.get_dummy_course(start='2012-12-02T12:00', is_new=True)
assert(descriptor.is_new == True)
assert(descriptor.days_until_start < 0)
descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=False)
assert(descriptor.is_new == False)
assert(descriptor.days_until_start > 0)
descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=True)
assert(descriptor.is_new == True)
assert(descriptor.days_until_start > 0)

View File

@@ -39,9 +39,12 @@ def strip_filenames(descriptor):
class RoundTripTestCase(unittest.TestCase):
'''Check that our test courses roundtrip properly'''
''' Check that our test courses roundtrip properly.
Same course imported , than exported, then imported again.
And we compare original import with second import (after export).
Thus we make sure that export and import work properly.
'''
def check_export_roundtrip(self, data_dir, course_dir):
root_dir = path(mkdtemp())
print "Copying test course to temp dir {0}".format(root_dir)
@@ -117,3 +120,7 @@ class RoundTripTestCase(unittest.TestCase):
def test_selfassessment_roundtrip(self):
#Test selfassessment xmodule to see if it exports correctly
self.check_export_roundtrip(DATA_DIR,"self_assessment")
def test_graphicslidertool_roundtrip(self):
#Test graphicslidertool xmodule to see if it exports correctly
self.check_export_roundtrip(DATA_DIR,"graphic_slider_tool")

View File

@@ -352,3 +352,19 @@ class ImportTestCase(unittest.TestCase):
sa_sample = modulestore.get_instance(sa_id, location)
#10 attempts is hard coded into SampleQuestion, which is the url_name of a selfassessment xml tag
self.assertEqual(sa_sample.metadata['attempts'], '10')
def test_graphicslidertool_import(self):
'''
Check to see if definition_from_xml in gst_module.py
works properly. Pulls data from the graphic_slider_tool directory
in the test data directory.
'''
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['graphic_slider_tool'])
sa_id = "edX/gst_test/2012_Fall"
location = Location(["i4x", "edX", "gst_test", "graphical_slider_tool", "sample_gst"])
gst_sample = modulestore.get_instance(sa_id, location)
render_string_from_sample_gst_xml = """
<slider var="a" style="width:400px;float:left;"/>\
<plot style="margin-top:15px;margin-bottom:15px;"/>""".strip()
self.assertEqual(gst_sample.definition['render'], render_string_from_sample_gst_xml)

View File

@@ -0,0 +1,2 @@
This is a very very simple course, useful for debugging graphical slider tool
code.

View File

@@ -0,0 +1 @@
roots/2012_Fall.xml

View File

@@ -0,0 +1,5 @@
<course>
<chapter url_name="Overview">
<graphical_slider_tool url_name="sample_gst"/>
</chapter>
</course>

View File

@@ -0,0 +1,30 @@
<graphical_slider_tool>
<render>
<slider var='a' style="width:400px;float:left;"/><plot style="margin-top:15px;margin-bottom:15px;"/>
</render>
<configuration>
<parameters>
<param var="a" min="5" max="25" step="0.5" initial="12.5" />
</parameters>
<functions>
<function color="red">return Math.sqrt(a * a - x * x);</function>
<function color="red">return -Math.sqrt(a * a - x * x);</function>
</functions>
<plot>
<xrange>
<!-- dynamic range -->
<min>
return -a;
</min>
<max>
return a;
</max>
</xrange>
<num_points>1000</num_points>
<xticks>-30, 6, 30</xticks>
<yticks>-30, 6, 30</yticks>
</plot>
</configuration>
</graphical_slider_tool>

View File

@@ -0,0 +1,14 @@
{
"course/2012_Fall": {
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
"start": "2015-07-17T12:00",
"display_name": "GST Test",
"graded": "false"
},
"chapter/Overview": {
"display_name": "Overview"
},
"graphical_slider_tool/sample_gst": {
"display_name": "Sample GST",
},
}

View File

@@ -0,0 +1 @@
<course org="edX" course="gst_test" url_name="2012_Fall"/>

View File

@@ -1 +0,0 @@
roots/2012_Fall.xml

View File

@@ -0,0 +1 @@
<course org="edX" course="sa_test" url_name="2012_Fall"/>

View File

@@ -35,6 +35,43 @@ weights of 30, 10, 10, and 10 to the 4 problems, respectively.
Note that the default weight of a problem **is not 1.** The default weight of a
problem is the module's max_grade.
If weighting is set, each problem is worth the number of points assigned, regardless of the number of responses it contains.
Consider a Homework section that contains two problems.
<problem display_name=”Problem 1”>
<numericalresponse> ... </numericalreponse>
</problem>
and
<problem display_name=”Problem 2”>
<numericalresponse> ... </numericalreponse>
<numericalresponse> ... </numericalreponse>
<numericalresponse> ... </numericalreponse>
</problem>
Without weighting, Problem 1 is worth 25% of the assignment, and Problem 2 is worth 75% of the assignment.
Weighting for the problems can be set in the policy.json file.
"problem/problem1": {
"weight": 2
},
"problem/problem2": {
"weight": 2
},
With the above weighting, Problems 1 and 2 are each worth 50% of the assignment.
Please note: When problems have weight, the point value is automatically included in the display name *except* when “weight”: 1.When “weight”: 1, no visual change occurs in the display name, leaving the point value open to interpretation to the student.
## Section Weighting
Once each section has a percentage score, we must total those sections into a

47
doc/remote_gradebook.md Normal file
View File

@@ -0,0 +1,47 @@
Grades can be pushed to a remote gradebook, and course enrollment membership can be pulled from a remote gradebook. This file documents how to setup such a remote gradebook, and what the API should be for writing new remote gradebook "xservers".
1. Definitions
An "xserver" is a web-based server that is part of the MITx eco system. There are a number of "xserver" programs, including one which does python code grading, an xqueue server, and graders for other coding languages.
"Stellar" is the MIT on-campus gradebook system.
2. Setup
The remote gradebook xserver should be specified in the lms.envs configuration using
MITX_FEATURES[REMOTE_GRADEBOOK_URL]
Each course, in addition, should define the name of the gradebook being used. A class "section" may also be specified. This goes in the policy.json file, eg:
"remote_gradebook": {
"name" : "STELLAR:/project/mitxdemosite",
"section" : "r01"
},
3. The API for the remote gradebook xserver is an almost RESTful service model, which only employs POSTs, to the xserver url, with form data for the fields:
- submit: get-assignments, get-membership, post-grades, or get-sections
- gradebook: name of gradebook
- user: username of staff person initiating the request (for logging)
- section: (optional) name of section
The return body content should be a JSON string, of the format {'msg': message, 'data': data}. The message is displayed in the instructor dashboard.
The data is a list of dicts (associative arrays). Each dict should be key:value.
## For submit=post-grades:
A file is also posted, with the field name "datafile". This file is CSV format, with two columns, one being "External email" and the other being the name of the assignment (that column contains the grades for the assignment).
## For submit=get-assignments
data keys = "AssignmentName"
## For submit=get-membership
data keys = "email", "name", "section"
## For submit=get-sections
data keys = "SectionName"

View File

@@ -0,0 +1,563 @@
*********************************************
Xml format of graphical slider tool [xmodule]
*********************************************
.. module:: xml_format_gst
Format description
==================
Graphical slider tool (GST) main tag is::
<graphical_slider_tool> BODY </graphical_slider_tool>
``graphical_slider_tool`` tag must have two children tags: ``render``
and ``configuration``.
Render tag
----------
Render tag can contain usual html tags mixed with some GST specific tags::
<slider/> - represents jQuery slider for changing a parameter's value
<textbox/> - represents a text input field for changing a parameter's value
<plot/> - represents Flot JS plot element
Also GST will track all elements inside ``<render></render>`` where ``id``
attribute is set, and a corresponding parameter referencing that ``id`` is present
in the configuration section below. These will be referred to as dynamic elements.
The contents of the <render> section will be shown to the user after
all occurrences of::
<slider var="{parameter name}" [style="{CSS statements}"] />
<textbox var="{parameter name}" [style="{CSS statements}"] />
<plot [style="{CSS statements}"] />
have been converted to actual sliders, text inputs, and a plot graph.
Everything in square brackets is optional. After initialization, all
text input fields, sliders, and dynamic elements will be set to the initial
values of the parameters that they are assigned to.
``{parameter name}`` specifies the parameter to which the slider or text
input will be attached to.
[style="{CSS statements}"] specifies valid CSS styling. It will be passed
directly to the browser without any parsing.
There is a one-to-one relationship between a slider and a parameter.
I.e. for one parameter you can put only one ``<slider>`` in the
``<render>`` section. However, you don't have to specify a slider - they
are optional.
There is a many-to-one relationship between text inputs and a
parameter. I.e. for one parameter you can put many '<textbox>' elements in
the ``<render>`` section. However, you don't have to specify a text
input - they are optional.
You can put only one ``<plot>`` in the ``<render>`` section. It is not
required.
Slider tag
..........
Slider tag must have ``var`` attribute and optional ``style`` attribute::
<slider var='a' style="width:400px;float:left;" />
After processing, slider tags will be replaced by jQuery UI sliders with applied
``style`` attribute.
``var`` attribute must correspond to a parameter. Parameters can be used in any
of the ``function`` tags in ``functions`` tag. By moving slider, value of
parameter ``a`` will change, and so result of function, that depends on parameter
``a``, will also change.
Textbox tag
...........
Texbox tag must have ``var`` attribute and optional ``style`` attribute::
<textbox var="b" style="width:50px; float:left; margin-left:10px;" />
After processing, textbox tags will be replaced by html text inputs with applied
``style`` attribute. If you want a readonly text input, then you should use a
dynamic element instead (see section below "HTML tagsd with ID").
``var`` attribute must correspond to a parameter. Parameters can be used in any
of the ``function`` tags in ``functions`` tag. By changing the value on the text input,
value of parameter ``a`` will change, and so result of function, that depends on
parameter ``a``, will also change.
Plot tag
........
Plot tag may have optional ``style`` attribute::
<plot style="width:50px; float:left; margin-left:10px;" />
After processing plot tags will be replaced by Flot JS plot with applied
``style`` attribute.
HTML tags with ID (dynamic elements)
....................................
Any HTML tag with ID, e.g. ``<span id="answer_span_1">`` can be used as a
place where result of function can be inserted. To insert function result to
an element, element ID must be included in ``function`` tag as ``el_id`` attribute
and ``output`` value must be ``"element"``::
<function output="element" el_id="answer_span_1">
function add(a, b, precision) {
var x = Math.pow(10, precision || 2);
return (Math.round(a * x) + Math.round(b * x)) / x;
}
return add(a, b, 5);
</function>
Configuration tag
-----------------
The configuration tag contains parameter settings, graph
settings, and function definitions which are to be plotted on the
graph and that use specified parameters.
Configuration tag contains two mandatory tag ``functions`` and ``parameters`` and
may contain another ``plot`` tag.
Parameters tag
..............
``Parameters`` tag contains ``parameter`` tags. Each ``parameter`` tag must have
``var``, ``max``, ``min``, ``step`` and ``initial`` attributes::
<parameters>
<param var="a" min="-10.0" max="10.0" step="0.1" initial="0" />
<param var="b" min="-10.0" max="10.0" step="0.1" initial="0" />
</parameters>
``var`` attribute links min, max, step and initial values to parameter name.
``min`` attribute is the minimal value that a parameter can take. Slider and input
values can not go below it.
``max`` attribute is the maximal value that a parameter can take. Slider and input
values can not go over it.
``step`` attribute is value of slider step. When a slider increase or decreases
the specified parameter, it will do so by the amount specified with 'step'
``initial`` attribute is the initial value that the specified parameter should be
set to. Sliders and inputs will initially show this value.
The parameter's name is specified by the ``var`` property. All occurrences
of sliders and/or text inputs that specify a ``var`` property, will be
connected to this parameter - i.e. they will reflect the current
value of the parameter, and will be updated when the parameter
changes.
If at lest one of these attributes is not set, then the parameter
will not be used, slider's and/or text input elements that specify
this parameter will not be activated, and the specified functions
which use this parameter will not return a numeric value. This means
that neglecting to specify at least one of the attributes for some
parameter will have the result of the whole GST instance not working
properly.
Functions tag
.............
For the GST to do something, you must defined at least one
function, which can use any of the specified parameter values. The
function expects to take the ``x`` value, do some calculations, and
return the ``y`` value. I.e. this is a 2D plot in Cartesian
coordinates. This is how the default function is meant to be used for
the graph.
There are other special cases of functions. They are used mainly for
outputting to elements, plot labels, or for custom output. Because
the return a single value, and that value is meant for a single element,
these function are invoked only with the set of all of the parameters.
I.e. no ``x`` value is available inside them. They are useful for
showing the current value of a parameter, showing complex static
formulas where some parameter's value must change, and other useful
things.
The different style of function is specified by the ``output`` attribute.
Each function must be defined inside ``function`` tag in ``functions`` tag::
<functions>
<function output="element" el_id="answer_span_1">
function add(a, b, precision) {
var x = Math.pow(10, precision || 2);
return (Math.round(a * x) + Math.round(b * x)) / x;
}
return add(a, b, 5);
</function>
</functions>
The parameter names (along with their values, as provided from text
inputs and/or sliders), will be available inside all defined
functions. A defined function body string will be parsed internally
by the browser's JavaScript engine and converted to a true JS
function.
The function's parameter list will automatically be created and
populated, and will include the ``x`` (when ``output`` is not specified or
is set to ``"graph"``), and all of the specified parameter values (from sliders
and text inputs). This means that each of the defined functions will have
access to all of the parameter values. You don't have to use them, but
they will be there.
Examples::
<function>
return x;
</function>
<function dot="true" label="\(y_2\)">
return (x + a) * Math.sin(x * b);
</function>
<function color="green">
function helperFunc(c1) {
return c1 * c1 - a;
}
return helperFunc(x + 10 * a * b) + Math.sin(a - x);
</function>
Required parameters::
function body:
A string composing a normal JavaScript function
except that there is no function declaration
(along with parameters), and no closing bracket.
So if you normally would have written your
JavaScript function like this:
function myFunc(x, a, b) {
return x * a + b;
}
here you must specify just the function body
(everything that goes between '{' and '}'). So,
you would specify the above function like so (the
bare-bone minimum):
<function>return x * a + b;</function>
VERY IMPORTANT: Because the function will be passed
to the browser as a single string, depending on implementation
specifics, the end-of-line characters can be stripped. This
means that single line JavaScript comments (starting with "//")
can lead to the effect that everything after the first such comment
will be treated as a comment. Therefore, it is absolutely
necessary that such single line comments are not used when
defining functions for GST. You can safely use the alternative
multiple line JavaScript comments (such comments start with "/*"
and end with "*/).
VERY IMPORTANT: If you have a large function body, and decide to
split it into several lines, than you must wrap it in "CDATA" like
so:
<function>
<![CDATA[
var dNew;
dNew = 0.3;
return x * a + b - dNew;
]]>
</function>
Optional parameters::
color: Color name ('red', 'green', etc.) or in the form of
'#FFFF00'. If not specified, a default color (different
one for each graphed function) will be given by Flot JS.
line: A string - 'true' or 'false'. Should the data points be
connected by a line on the graph? Default is 'true'.
dot: A string - 'true' or 'false'. Should points be shown for
each data point on the graph? Default is 'false'.
bar: A string - 'true' or 'false'. When set to 'true', points
will be plotted as bars.
label: A string. If provided, will be shown in the legend, along
with the color that was used to plot the function.
output: 'element', 'none', 'plot_label', or 'graph'. If not defined,
function will be plotted (same as setting 'output' to 'graph').
If defined, and other than 'graph', function will not be
plotted, but it's output will be inserted into the element
with ID specified by 'el_id' attribute.
el_id: Id of HTML element, defined in '<render>' section. Value of
function will be inserted as content of this element.
disable_auto_return: By default, if JavaScript function string is written
without a "return" statement, the "return" will be
prepended to it. Set to "true" to disable this
functionality. This is done so that simple functions
can be defined in an easy fashion (for example, "a",
which will be translated into "return a").
update_on: A string - 'change', or 'slide'. Default (if not set) is
'slide'. This defines the event on which a given function is
called, and its result is inserted into an element. This
setting is relevant only when "output" is other than "graph".
When specifying ``el_id``, it is essential to set "output" to one of
element - GST will invoke the function, and the return of it will be
inserted into a HTML element with id specified by ``el_id``.
none - GST will simply inoke the function. It is left to the instructor
who writes the JavaScript function body to update all necesary
HTML elements inside the function, before it exits. This is done
so that extra steps can be preformed after an HTML element has
been updated with a value. Note, that because the return value
from this function is not actually used, it will be tempting to
omit the "return" statement. However, in this case, the attribute
"disable_auto_return" must be set to "true" in order to prevent
GST from inserting a "return" statement automatically.
plot_label - GST will process all plot labels (which are strings), and
will replace the all instances of substrings specified by
``el_id`` with the returned value of the function. This is
necessary if you want a label in the graph to have some changing
number. Because of the nature of Flot JS, it is impossible to
achieve the same effect by setting the "output" attribute
to "element", and including a HTML element in the label.
The above values for "output" will tell GST that the function is meant for an
HTML element (not for graph), and that it should not get an 'x' parameter (along
with some value).
[Note on MathJax and labels]
............................
Independently of this module, will render all TeX code
within the ``<render>`` section into nice mathematical formulas. Just
remember to wrap it in one of::
\( and \) - for inline formulas (formulas surrounded by
standard text)
\[ and \] - if you want the formula to be a separate line
It is possible to define a label in standard TeX notation. The JS
library MathJax will work on these labels also because they are
inserted on top of the plot as standard HTML (text within a DIV).
If the label is dynamic, i.e. it will contain some text (numeric, or other)
that has to be updated on a parameter's change, then one can define
a special function to handle this. The "output" of such a function must be
set to "none", and the JavaScript code inside this function must update the
MathJax element by itself. Before exiting, MathJax typeset function should
be called so that the new text will be re-rendered by MathJax. For example,
<render>
...
<span id="dynamic_mathjax"></span>
</render>
...
<function output="none" el_id="dynamic_mathjax">
<![CDATA[
var out_text;
out_text = "\\[\\mathrm{Percent \\space of \\space treated \\space with \\space YSS=\\frac{"
+(treated_men*10)+"\\space men *"
+(your_m_tx_yss/100)+"\\space prev. +\\space "
+((100-treated_men)*10)+"\\space women *"
+(your_f_tx_yss/100)+"\\space prev.}"
+"{1000\\space total\\space treated\\space patients}"
+"="+drummond_combined[0][1]+"\\%}\\]";
mathjax_for_prevalence_calcs+="\\[\\mathrm{Percent \\space of \\space untreated \\space with \\space YSS=\\frac{"
+(untreated_men*10)+"\\space men *"
+(your_m_utx_yss/100)+"\\space prev. +\\space "
+((100-untreated_men)*10)+"\\space women *"
+(your_f_utx_yss/100)+"\\space prev.}"
+"{1000\\space total\\space untreated\\space patients}"
+"="+drummond_combined[1][1]+"\\%}\\]";
$("#dynamic_mathjax").html(out_text);
MathJax.Hub.Queue(["Typeset",MathJax.Hub,"dynamic_mathjax"]);
]]>
</function>
...
Plot tag
........
``Plot`` tag inside ``configuration`` tag defines settings for plot output.
Required parameters::
xrange: 2 functions that must return value. Value is constant (3.1415)
or depend on parameter from parameters section:
<xrange>
<min>return 0;</min>
<max>return 30;</max>
</xrange>
or
<xrange>
<min>return -a;</min>
<max>return a;</max>
</xrange>
All functions will be calculated over domain between xrange:min
and xrange:max. Xrange depending on parameter is extremely
useful when domain(s) of your function(s) depends on parameter
(like circle, when parameter is radius and you want to allow
to change it).
Optional parameters::
num_points: Number of data points to generated for the plot. If
this is not set, the number of points will be
calculated as width / 5.
bar_width: If functions are present which are to be plotted as bars,
then this parameter specifies the width of the bars. A
numeric value for this parameter is expected.
bar_align: If functions are present which are to be plotted as bars,
then this parameter specifies how to align the bars relative
to the tick. Available values are "left" and "center".
xticks,
yticks: 3 floating point numbers separated by commas. This
specifies how many ticks are created, what number they
start at, and what number they end at. This is different
from the 'xrange' setting in that it has nothing to do
with the data points - it control what area of the
Cartesian space you will see. The first number is the
first tick's value, the second number is the step
between each tick, the third number is the value of the
last tick. If these configurations are not specified,
Flot will chose them for you based on the data points
set that he is currently plotting. Usually, this results
in a nice graph, however, sometimes you need to fine
grain the controls. For example, when you want to show
a fixed area of the Cartesian space, even when the data
set changes. On it's own, Flot will recalculate the
ticks, which will result in a different graph each time.
By specifying the xticks, yticks configurations, only
the plotted data will change - the axes (ticks) will
remain as you have defined them.
xticks_names, yticks_names:
A JSON string which represents a mapping of xticks, yticks
values to some defined strings. If specified, the graph will
not have any xticks, yticks except those for which a string
value has been defined in the JSON string. Note that the
matching will be string-based and not numeric. I.e. if a tick
value was "3.70" before, then inside the JSON there should be
a mapping like {..., "3.70": "Some string", ...}. Example:
<xticks_names>
<![CDATA[
{
"1": "Treated", "2": "Not Treated",
"4": "Treated", "5": "Not Treated",
"7": "Treated", "8": "Not Treated"
}
]]>
</xticks_names>
<yticks_names>
<![CDATA[
{"0": "0%", "10": "10%", "20": "20%", "30": "30%", "40": "40%", "50": "50%"}
]]>
</yticks_names>
xunits,
yunits: Units values to be set on axes. Use MathJax. Example:
<xunits>\(cm\)</xunits>
<yunits>\(m\)</yunits>
moving_label:
A way to specify a label that should be positioned dynamically,
based on the values of some parameters, or some other factors.
It is similar to a <function>, but it is only valid for a plot
because it is drawn relative to the plot coordinate system.
Multiple "moving_label" configurations can be provided, each one
with a unique text and a unique set of functions that determine
it's dynamic positioning.
Each "moving_label" can have a "color" attribute (CSS color notation),
and a "weight" attribute. "weight" can be one of "normal" or "bold",
and determines the styling of moving label's text.
Each "moving_label" function should return an object with a 'x'
and 'y properties. Within those functions, all of the parameter
names along with their value are available.
Example (note that "return" statement is missing; it will be automatically
inserted by GST):
<moving_label text="Co" weight="bold" color="red>
<![CDATA[ {'x': -50, 'y': c0};]]>
</moving_label>
asymptote:
Add a vertical or horizontal asymptote to the graph which will
be dynamically repositioned based on the specified function.
It is similar to the function in that it provides a JavaScript body function
string. This function will be used to calculate the position of the asymptote
relative to the axis specified by the "type" parameter.
Required parameters:
type:
Which axis should the asymptote be plotted against. Available values
are "x" and "y".
Optional parameters:
color:
The color of the line. A valid CSS color string is expected.
Example
=======
Plotting, sliders and inputs
----------------------------
.. literalinclude:: gst_example_with_documentation.xml
Update of html elements, no plotting
------------------------------------
.. literalinclude:: gst_example_html_element_output.xml
Circle with dynamic radius
--------------------------
.. literalinclude:: gst_example_dynamic_range.xml
Example of a bar graph
----------------------
.. literalinclude:: gst_example_bars.xml
Example of moving labels of graph
---------------------------------
.. literalinclude:: gst_example_dynamic_labels.xml

View File

@@ -0,0 +1,64 @@
<vertical>
<graphical_slider_tool>
<render>
<h2>Graphic slider tool: Bar graph example.</h2>
<p>We can request the API to plot us a bar graph.</p>
<div style="clear:both">
<p style="width:60px;float:left;">a</p>
<slider var='a' style="width:400px;float:left;"/>
<textbox var='a' style="width:50px;float:left;margin-left:15px;"/>
<br /><br /><br />
<p style="width:60px;float:left;">b</p>
<slider var='b' style="width:400px;float:left;"/>
<textbox var='b' style="width:50px;float:left;margin-left:15px;"/>
</div>
<plot style="clear:left;"/>
</render>
<configuration>
<parameters>
<param var="a" min="-100" max="100" step="5" initial="25" />
<param var="b" min="-100" max="100" step="5" initial="50" />
</parameters>
<functions>
<function bar="true" color="blue" label="Men">
<![CDATA[if (((x>0.9) && (x<1.1)) || ((x>4.9) && (x<5.1))) { return Math.sin(a * 0.01 * Math.PI + 2.952 * x); }
else {return undefined;}]]>
</function>
<function bar="true" color="red" label="Women">
<![CDATA[if (((x>1.9) && (x<2.1)) || ((x>3.9) && (x<4.1))) { return Math.cos(b * 0.01 * Math.PI + 3.432 * x); }
else {return undefined;}]]>
</function>
<function bar="true" color="green" label="Other 1">
<![CDATA[if (((x>1.9) && (x<2.1)) || ((x>3.9) && (x<4.1))) { return Math.cos((b - 10 * a) * 0.01 * Math.PI + 3.432 * x); }
else {return undefined;}]]>
</function>
<function bar="true" color="yellow" label="Other 2">
<![CDATA[if (((x>1.9) && (x<2.1)) || ((x>3.9) && (x<4.1))) { return Math.cos((b + 7 * a) * 0.01 * Math.PI + 3.432 * x); }
else {return undefined;}]]>
</function>
</functions>
<plot>
<xrange><min>1</min><max>5</max></xrange>
<num_points>5</num_points>
<xticks>0, 0.5, 6</xticks>
<yticks>-1.5, 0.1, 1.5</yticks>
<xticks_names>
<![CDATA[
{
"1.5": "Single", "4.5": "Married"
}
]]>
</xticks_names>
<yticks_names>
<![CDATA[
{
"-1.0": "-100%", "-0.5": "-50%", "0.0": "0%", "0.5": "50%", "1.0": "100%"
}
]]>
</yticks_names>
<bar_width>0.4</bar_width>
</plot>
</configuration>
</graphical_slider_tool>
</vertical>

View File

@@ -0,0 +1,40 @@
<vertical>
<graphical_slider_tool>
<render>
<h1>Graphic slider tool: Dynamic labels.</h1>
<p>There are two kinds of dynamic lables.
1) Dynamic changing values in graph legends.
2) Dynamic labels, which coordinates depend on parameters </p>
<p>a: <slider var="a"/></p>
<br/>
<p>b: <slider var="b"/></p>
<br/><br/>
<plot style="width:400px; height:400px;"/>
</render>
<configuration>
<parameters>
<param var="a" min="-10" max="10" step="1" initial="0" />
<param var="b" min="0" max="10" step="0.5" initial="5" />
</parameters>
<functions>
<function label="Value of a is: dyn_val_1">a * x + b</function>
<!-- dynamic values in legend -->
<function output="plot_label" el_id="dyn_val_1">a</function>
</functions>
<plot>
<xrange><min>0</min><max>30</max></xrange>
<num_points>10</num_points>
<xticks>0, 6, 30</xticks>
<yticks>-9, 1, 9</yticks>
<!-- custom labels with coordinates as any function of parameter -->
<moving_label text="Dynam_lbl 1" weight="bold">
<![CDATA[ {'x': 10, 'y': a};]]>
</moving_label>
<moving_label text="Dynam lbl 2" weight="bold">
<![CDATA[ {'x': -6, 'y': b};]]>
</moving_label>
</plot>
</configuration>
</graphical_slider_tool>
</vertical>

View File

@@ -0,0 +1,37 @@
<vertical>
<graphical_slider_tool>
<render>
<h2>Graphic slider tool: Dynamic range and implicit functions.</h2>
<p>You can make x range (not ticks of x axis) of functions to depend on
parameter value. This can be useful when function domain depends
on parameter.</p>
<p>Also implicit functons like circle can be plotted as 2 separate
functions of same color.</p>
<div style="height:50px;">
<slider var='a' style="width:400px;float:left;"/>
<textbox var='a' style="float:left;width:60px;margin-left:15px;"/>
</div>
<plot style="margin-top:15px;margin-bottom:15px;"/>
</render>
<configuration>
<parameters>
<param var="a" min="5" max="25" step="0.5" initial="12.5" />
</parameters>
<functions>
<function color="red">Math.sqrt(a * a - x * x)</function>
<function color="red">-Math.sqrt(a * a - x * x)</function>
</functions>
<plot>
<xrange>
<!-- dynamic range -->
<min>-a</min>
<max>a</max>
</xrange>
<num_points>1000</num_points>
<xticks>-30, 6, 30</xticks>
<yticks>-30, 6, 30</yticks>
</plot>
</configuration>
</graphical_slider_tool>
</vertical>

View File

@@ -0,0 +1,40 @@
<vertical>
<graphical_slider_tool>
<render>
<h2>Graphic slider tool: Output to DOM element.</h2>
<p>a + b = <span id="answer_span_1"></span></p>
<div style="clear:both">
<p style="float:left;margin-right:10px;">a</p>
<slider var='a' style="width:400px;float:left;"/>
<textbox var='a' style="width:50px;float:left;margin-left:15px;"/>
</div>
<div style="clear:both">
<p style="float:left;margin-right:10px;">b</p>
<slider var='b' style="width:400px;float:left;"/>
<textbox var='b' style="width:50px;float:left;margin-left:15px;"/>
</div>
<br/><br/><br/>
<plot/>
</render>
<configuration>
<parameters>
<param var="a" min="-10.0" max="10.0" step="0.1" initial="0" />
<param var="b" min="-10.0" max="10.0" step="0.1" initial="0" />
</parameters>
<functions>
<function output="element" el_id="answer_span_1">
function add(a, b, precision) {
var x = Math.pow(10, precision || 2);
return (Math.round(a * x) + Math.round(b * x)) / x;
}
return add(a, b, 5);
</function>
</functions>
</configuration>
</graphical_slider_tool>
</vertical>

View File

@@ -0,0 +1,91 @@
<vertical>
<graphical_slider_tool>
<render>
<h2>Graphic slider tool: full example.</h2>
<p>
A simple equation
\(
y_1 = 10 \times b \times \frac{sin(a \times x) \times sin(b \times x)}{cos(b \times x) + 10}
\)
can be plotted.
</p>
<!-- make text and input or slider at the same line -->
<div>
<p style="float:left;"> Currently \(a\) is</p>
<!-- readonly input for a -->
<span id="a_readonly" style="width:50px; float:left; margin-left:10px;"/>
</div>
<!-- clear:left will make next text to begin from new line -->
<p style="clear:left"> This one
\(
y_2 = sin(a \times x)
\)
will be overlayed on top.
</p>
<div>
<p style="float:left;">Currently \(b\) is </p>
<textbox var="b" style="width:50px; float:left; margin-left:10px;"/>
</div>
<div style="clear:left;">
<p style="float:left;">To change \(a\) use:</p>
<slider var="a" style="width:400px;float:left;margin-left:10px;"/>
</div>
<div style="clear:left;">
<p style="float:left;">To change \(b\) use:</p>
<slider var="b" style="width:400px;float:left;margin-left:10px;"/>
</div>
<plot style='clear:left;width:600px;padding-top:15px;padding-bottom:20px;'/>
<div style="clear:left;height:50px;">
<p style="float:left;">Second input for b:</p>
<!-- editable input for b -->
<textbox var="b" style="color:red;width:60px;float:left;margin-left:10px;"/>
</div >
</render>
<configuration>
<parameters>
<param var="a" min="90" max="120" step="10" initial="100" />
<param var="b" min="120" max="200" step="2.3" initial="120" />
</parameters>
<functions>
<function color="#0000FF" line="false" dot="true" label="\(y_1\)">
return 10.0 * b * Math.sin(a * x) * Math.sin(b * x) / (Math.cos(b * x) + 10);
</function>
<function color="red" line="true" dot="false" label="\(y_2\)">
<!-- works w/o return, if function is single line -->
Math.sin(a * x);
</function>
<function color="#FFFF00" line="false" dot="false" label="unknown">
function helperFunc(c1) {
return c1 * c1 - a;
}
return helperFunc(x + 10 * a * b) + Math.sin(a - x);
</function>
<function output="element" el_id="a_readonly">a</function>
</functions>
<plot>
<xrange>
<min>return 0;</min>
<!-- works w/o return -->
<max>30</max>
</xrange>
<num_points>120</num_points>
<xticks>0, 3, 30</xticks>
<yticks>-1.5, 1.5, 13.5</yticks>
<xunits>\(cm\)</xunits>
<yunits>\(m\)</yunits>
</plot>
</configuration>
</graphical_slider_tool>
</vertical>

View File

@@ -14,7 +14,7 @@ Contents:
overview.rst
common-lib.rst
djangoapps.rst
xml_formats.rst
Indices and tables
==================

View File

@@ -0,0 +1,8 @@
XML formats of Inputtypes and Xmodule
=====================================
Contents:
.. toctree::
:maxdepth: 2
graphical_slider_tool.rst

View File

@@ -3,6 +3,8 @@
set -e
set -x
git remote prune origin
# Reset the submodule, in case it changed
git submodule foreach 'git reset --hard HEAD'

View File

@@ -15,6 +15,8 @@ function github_mark_failed_on_exit {
trap '[ $? == "0" ] || github_status state:failure "failed"' EXIT
}
git remote prune origin
github_mark_failed_on_exit
github_status state:pending "is running"

View File

@@ -13,6 +13,8 @@ from xmodule.modulestore import Location
from xmodule.timeparse import parse_time
from xmodule.x_module import XModule, XModuleDescriptor
from student.models import CourseEnrollmentAllowed
DEBUG_ACCESS = False
log = logging.getLogger(__name__)
@@ -124,6 +126,11 @@ def _has_access_course_desc(user, course, action):
debug("Allow: in enrollment period")
return True
# if user is in CourseEnrollmentAllowed with right course_id then can also enroll
if user is not None and CourseEnrollmentAllowed:
if CourseEnrollmentAllowed.objects.filter(email=user.email, course_id=course.id):
return True
# otherwise, need staff access
return _has_staff_access_to_descriptor(user, course)
@@ -159,13 +166,19 @@ def _has_access_course_desc(user, course, action):
return _dispatch(checkers, action, user, course)
def _get_access_group_name_course_desc(course, action):
'''
Return name of group which gives staff access to course. Only understands action = 'staff'
Return name of group which gives staff access to course. Only understands action = 'staff' and 'instructor'
'''
if not action=='staff':
return []
return _course_staff_group_name(course.location)
if action=='staff':
return _course_staff_group_name(course.location)
elif action=='instructor':
return _course_instructor_group_name(course.location)
return []
def _has_access_error_desc(user, descriptor, action):
"""

View File

@@ -7,3 +7,8 @@ from django.contrib import admin
from django.contrib.auth.models import User
admin.site.register(StudentModule)
admin.site.register(OfflineComputedGrade)
admin.site.register(OfflineComputedGradeLog)

View File

@@ -233,35 +233,5 @@ def get_courses(user, domain=None):
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

View File

@@ -0,0 +1,117 @@
# -*- 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 'OfflineComputedGrade'
db.create_table('courseware_offlinecomputedgrade', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, db_index=True, blank=True)),
('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
('gradeset', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
))
db.send_create_signal('courseware', ['OfflineComputedGrade'])
# Adding unique constraint on 'OfflineComputedGrade', fields ['user', 'course_id']
db.create_unique('courseware_offlinecomputedgrade', ['user_id', 'course_id'])
# Adding model 'OfflineComputedGradeLog'
db.create_table('courseware_offlinecomputedgradelog', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, db_index=True, blank=True)),
('seconds', self.gf('django.db.models.fields.IntegerField')(default=0)),
('nstudents', self.gf('django.db.models.fields.IntegerField')(default=0)),
))
db.send_create_signal('courseware', ['OfflineComputedGradeLog'])
def backwards(self, orm):
# Removing unique constraint on 'OfflineComputedGrade', fields ['user', 'course_id']
db.delete_unique('courseware_offlinecomputedgrade', ['user_id', 'course_id'])
# Deleting model 'OfflineComputedGrade'
db.delete_table('courseware_offlinecomputedgrade')
# Deleting model 'OfflineComputedGradeLog'
db.delete_table('courseware_offlinecomputedgradelog')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'courseware.offlinecomputedgrade': {
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'OfflineComputedGrade'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'gradeset': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'courseware.offlinecomputedgradelog': {
'Meta': {'object_name': 'OfflineComputedGradeLog'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'nstudents': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'seconds': ('django.db.models.fields.IntegerField', [], {'default': '0'})
},
'courseware.studentmodule': {
'Meta': {'unique_together': "(('student', 'module_state_key', 'course_id'),)", 'object_name': 'StudentModule'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'done': ('django.db.models.fields.CharField', [], {'default': "'na'", 'max_length': '8', 'db_index': 'True'}),
'grade': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'max_grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'module_state_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'module_id'", 'db_index': 'True'}),
'module_type': ('django.db.models.fields.CharField', [], {'default': "'problem'", 'max_length': '32', 'db_index': 'True'}),
'state': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
}
}
complete_apps = ['courseware']

View File

@@ -177,3 +177,40 @@ class StudentModuleCache(object):
def append(self, student_module):
self.cache.append(student_module)
class OfflineComputedGrade(models.Model):
"""
Table of grades computed offline for a given user and course.
"""
user = models.ForeignKey(User, db_index=True)
course_id = models.CharField(max_length=255, db_index=True)
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
updated = models.DateTimeField(auto_now=True, db_index=True)
gradeset = models.TextField(null=True, blank=True) # grades, stored as JSON
class Meta:
unique_together = (('user', 'course_id'), )
def __unicode__(self):
return "[OfflineComputedGrade] %s: %s (%s) = %s" % (self.user, self.course_id, self.created, self.gradeset)
class OfflineComputedGradeLog(models.Model):
"""
Log of when offline grades are computed.
Use this to be able to show instructor when the last computed grades were done.
"""
class Meta:
ordering = ["-created"]
get_latest_by = "created"
course_id = models.CharField(max_length=255, db_index=True)
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
seconds = models.IntegerField(default=0) # seconds elapsed for computation
nstudents = models.IntegerField(default=0)
def __unicode__(self):
return "[OCGLog] %s: %s" % (self.course_id, self.created)

View File

@@ -70,7 +70,7 @@ def courses(request):
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']
key = lambda course: course.days_until_start
courses = sorted(courses, key=key, reverse=True)
return render_to_response("courseware/courses.html", {'courses': courses})
@@ -440,7 +440,7 @@ def university_profile(request, org_id):
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']
key = lambda course: course.days_until_start
courses = sorted(courses, key=key, reverse=True)
context = dict(courses=courses, org_id=org_id)

View File

@@ -0,0 +1,52 @@
#!/usr/bin/python
#
# django management command: dump grades to csv files
# for use by batch processes
import os, sys, string
import datetime
import json
#import student.models
from instructor.offline_gradecalc import *
from courseware.courses import get_course_by_id
from xmodule.modulestore.django import modulestore
from django.conf import settings
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Compute grades for all students in a course, and store result in DB.\n"
help += "Usage: compute_grades course_id_or_dir \n"
help += " course_id_or_dir: either course_id or course_dir\n"
help += 'Example course_id: MITx/8.01rq_MW/Classical_Mechanics_Reading_Questions_Fall_2012_MW_Section'
def handle(self, *args, **options):
print "args = ", args
if len(args)>0:
course_id = args[0]
else:
print self.help
return
try:
course = get_course_by_id(course_id)
except Exception as err:
if course_id in modulestore().courses:
course = modulestore().courses[course_id]
else:
print "-----------------------------------------------------------------------------"
print "Sorry, cannot find course %s" % course_id
print "Please provide a course ID or course data directory name, eg content-mit-801rq"
return
print "-----------------------------------------------------------------------------"
print "Computing grades for %s" % (course.id)
offline_grade_calculation(course.id)

View File

@@ -0,0 +1,103 @@
# ======== Offline calculation of grades =============================================================================
#
# Computing grades of a large number of students can take a long time. These routines allow grades to
# be computed offline, by a batch process (eg cronjob).
#
# The grades are stored in the OfflineComputedGrade table of the courseware model.
import json
import logging
import time
import courseware.models
from collections import namedtuple
from json import JSONEncoder
from courseware import grades, models
from courseware.courses import get_course_by_id
from django.contrib.auth.models import User, Group
class MyEncoder(JSONEncoder):
def _iterencode(self, obj, markers=None):
if isinstance(obj, tuple) and hasattr(obj, '_asdict'):
gen = self._iterencode_dict(obj._asdict(), markers)
else:
gen = JSONEncoder._iterencode(self, obj, markers)
for chunk in gen:
yield chunk
def offline_grade_calculation(course_id):
'''
Compute grades for all students for a specified course, and save results to the DB.
'''
tstart = time.time()
enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).prefetch_related("groups").order_by('username')
enc = MyEncoder()
class DummyRequest(object):
META = {}
def __init__(self):
return
def get_host(self):
return 'edx.mit.edu'
def is_secure(self):
return False
request = DummyRequest()
print "%d enrolled students" % len(enrolled_students)
course = get_course_by_id(course_id)
for student in enrolled_students:
gradeset = grades.grade(student, request, course, keep_raw_scores=True)
gs = enc.encode(gradeset)
ocg, created = models.OfflineComputedGrade.objects.get_or_create(user=student, course_id=course_id)
ocg.gradeset = gs
ocg.save()
print "%s done" % student # print statement used because this is run by a management command
tend = time.time()
dt = tend - tstart
ocgl = models.OfflineComputedGradeLog(course_id=course_id, seconds=dt, nstudents=len(enrolled_students))
ocgl.save()
print ocgl
print "All Done!"
def offline_grades_available(course_id):
'''
Returns False if no offline grades available for specified course.
Otherwise returns latest log field entry about the available pre-computed grades.
'''
ocgl = models.OfflineComputedGradeLog.objects.filter(course_id=course_id)
if not ocgl:
return False
return ocgl.latest('created')
def student_grades(student, request, course, keep_raw_scores=False, use_offline=False):
'''
This is the main interface to get grades. It has the same parameters as grades.grade, as well
as use_offline. If use_offline is True then this will look for an offline computed gradeset in the DB.
'''
if not use_offline:
return grades.grade(student, request, course, keep_raw_scores=keep_raw_scores)
try:
ocg = models.OfflineComputedGrade.objects.get(user=student, course_id=course.id)
except models.OfflineComputedGrade.DoesNotExist:
return dict(raw_scores=[], section_breakdown=[],
msg='Error: no offline gradeset available for %s, %s' % (student, course.id))
return json.loads(ocg.gradeset)

View File

@@ -25,7 +25,6 @@ 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
@@ -100,7 +99,6 @@ def action_name(operation, rolename):
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):
@@ -223,94 +221,3 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader):
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'])

View File

@@ -2,10 +2,14 @@
from collections import defaultdict
import csv
import json
import logging
import os
import requests
import urllib
from StringIO import StringIO
from django.conf import settings
from django.contrib.auth.models import User, Group
from django.http import HttpResponse
@@ -20,7 +24,7 @@ from courseware.courses import get_course_with_access
from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA
from django_comment_client.utils import has_forum_access
from psychometrics import psychoanalyze
from student.models import CourseEnrollment
from student.models import CourseEnrollment, CourseEnrollmentAllowed
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
@@ -28,8 +32,7 @@ from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundErr
from xmodule.modulestore.search import path_to_location
import track.views
from .grading import StaffGrading
from .offline_gradecalc import student_grades, offline_grades_available
log = logging.getLogger(__name__)
@@ -76,9 +79,12 @@ def instructor_dashboard(request, course_id):
data.append(['metadata', escape(str(course.metadata))])
datatable['data'] = data
def return_csv(fn, datatable):
response = HttpResponse(mimetype='text/csv')
response['Content-Disposition'] = 'attachment; filename={0}'.format(fn)
def return_csv(fn, datatable, fp=None):
if fp is None:
response = HttpResponse(mimetype='text/csv')
response['Content-Disposition'] = 'attachment; filename={0}'.format(fn)
else:
response = fp
writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL)
writer.writerow(datatable['header'])
for datarow in datatable['data']:
@@ -87,16 +93,23 @@ def instructor_dashboard(request, course_id):
return response
def get_staff_group(course):
staffgrp = get_access_group_name(course, 'staff')
return get_group(course, 'staff')
def get_instructor_group(course):
return get_group(course, 'instructor')
def get_group(course, groupname):
grpname = get_access_group_name(course, groupname)
try:
group = Group.objects.get(name=staffgrp)
group = Group.objects.get(name=grpname)
except Group.DoesNotExist:
group = Group(name=staffgrp) # create the group
group = Group(name=grpname) # create the group
group.save()
return group
# process actions from form POST
action = request.POST.get('action', '')
use_offline = request.POST.get('use_offline_grades',False)
if settings.MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD']:
if 'GIT pull' in action:
@@ -126,39 +139,98 @@ def instructor_dashboard(request, course_id):
except Exception as err:
msg += '<br/><p>Error: {0}</p>'.format(escape(err))
if action == 'Dump list of enrolled students':
if action == 'Dump list of enrolled students' or action=='List enrolled students':
log.debug(action)
datatable = get_student_grade_summary_data(request, course, course_id, get_grades=False)
datatable = get_student_grade_summary_data(request, course, course_id, get_grades=False, use_offline=use_offline)
datatable['title'] = 'List of students enrolled in {0}'.format(course_id)
track.views.server_track(request, 'list-students', {}, page='idashboard')
elif 'Dump Grades' in action:
log.debug(action)
datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True)
datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline)
datatable['title'] = 'Summary Grades of students enrolled in {0}'.format(course_id)
track.views.server_track(request, 'dump-grades', {}, page='idashboard')
elif 'Dump all RAW grades' in action:
log.debug(action)
datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True,
get_raw_scores=True)
get_raw_scores=True, use_offline=use_offline)
datatable['title'] = 'Raw Grades of students enrolled in {0}'.format(course_id)
track.views.server_track(request, 'dump-grades-raw', {}, page='idashboard')
elif 'Download CSV of all student grades' in action:
track.views.server_track(request, 'dump-grades-csv', {}, page='idashboard')
return return_csv('grades_{0}.csv'.format(course_id),
get_student_grade_summary_data(request, course, course_id))
get_student_grade_summary_data(request, course, course_id, use_offline=use_offline))
elif 'Download CSV of all RAW grades' in action:
track.views.server_track(request, 'dump-grades-csv-raw', {}, page='idashboard')
return return_csv('grades_{0}_raw.csv'.format(course_id),
get_student_grade_summary_data(request, course, course_id, get_raw_scores=True))
get_student_grade_summary_data(request, course, course_id, get_raw_scores=True, use_offline=use_offline))
elif 'Download CSV of answer distributions' in action:
track.views.server_track(request, 'dump-answer-dist-csv', {}, page='idashboard')
return return_csv('answer_dist_{0}.csv'.format(course_id), get_answers_distribution(request, course_id))
#----------------------------------------
# export grades to remote gradebook
elif action=='List assignments available in remote gradebook':
msg2, datatable = _do_remote_gradebook(request.user, course, 'get-assignments')
msg += msg2
elif action=='List assignments available for this course':
log.debug(action)
allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline)
assignments = [[x] for x in allgrades['assignments']]
datatable = {'header': ['Assignment Name']}
datatable['data'] = assignments
datatable['title'] = action
msg += 'assignments=<pre>%s</pre>' % assignments
elif action=='List enrolled students matching remote gradebook':
stud_data = get_student_grade_summary_data(request, course, course_id, get_grades=False, use_offline=use_offline)
msg2, rg_stud_data = _do_remote_gradebook(request.user, course, 'get-membership')
datatable = {'header': ['Student email', 'Match?']}
rg_students = [ x['email'] for x in rg_stud_data['retdata'] ]
def domatch(x):
return '<font color="green">yes</font>' if x.email in rg_students else '<font color="red">No</font>'
datatable['data'] = [[x.email, domatch(x)] for x in stud_data['students']]
datatable['title'] = action
elif action in ['Display grades for assignment', 'Export grades for assignment to remote gradebook',
'Export CSV file of grades for assignment']:
log.debug(action)
datatable = {}
aname = request.POST.get('assignment_name','')
if not aname:
msg += "<font color='red'>Please enter an assignment name</font>"
else:
allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline)
if aname not in allgrades['assignments']:
msg += "<font color='red'>Invalid assignment name '%s'</font>" % aname
else:
aidx = allgrades['assignments'].index(aname)
datatable = {'header': ['External email', aname]}
datatable['data'] = [[x.email, x.grades[aidx]] for x in allgrades['students']]
datatable['title'] = 'Grades for assignment "%s"' % aname
if 'Export CSV' in action:
# generate and return CSV file
return return_csv('grades %s.csv' % aname, datatable)
elif 'remote gradebook' in action:
fp = StringIO()
return_csv('', datatable, fp=fp)
fp.seek(0)
files = {'datafile': fp}
msg2, dataset = _do_remote_gradebook(request.user, course, 'post-grades', files=files)
msg += msg2
#----------------------------------------
# Admin
@@ -172,6 +244,16 @@ def instructor_dashboard(request, course_id):
datatable['title'] = 'List of Staff in course {0}'.format(course_id)
track.views.server_track(request, 'list-staff', {}, page='idashboard')
elif 'List course instructors' in action and request.user.is_staff:
group = get_instructor_group(course)
msg += 'Instructor group = {0}'.format(group.name)
log.debug('instructor grp={0}'.format(group.name))
uset = group.user_set.all()
datatable = {'header': ['Username', 'Full name']}
datatable['data'] = [[x.username, x.profile.name] for x in uset]
datatable['title'] = 'List of Instructors in course {0}'.format(course_id)
track.views.server_track(request, 'list-instructors', {}, page='idashboard')
elif action == 'Add course staff':
uname = request.POST['staffuser']
try:
@@ -186,6 +268,20 @@ def instructor_dashboard(request, course_id):
user.groups.add(group)
track.views.server_track(request, 'add-staff {0}'.format(user), {}, page='idashboard')
elif action == 'Add instructor' and request.user.is_staff:
uname = request.POST['instructor']
try:
user = User.objects.get(username=uname)
except User.DoesNotExist:
msg += '<font color="red">Error: unknown username "{0}"</font>'.format(uname)
user = None
if user is not None:
group = get_instructor_group(course)
msg += '<font color="green">Added {0} to instructor group = {1}</font>'.format(user, group.name)
log.debug('staffgrp={0}'.format(group.name))
user.groups.add(group)
track.views.server_track(request, 'add-instructor {0}'.format(user), {}, page='idashboard')
elif action == 'Remove course staff':
uname = request.POST['staffuser']
try:
@@ -200,6 +296,20 @@ def instructor_dashboard(request, course_id):
user.groups.remove(group)
track.views.server_track(request, 'remove-staff {0}'.format(user), {}, page='idashboard')
elif action == 'Remove instructor' and request.user.is_staff:
uname = request.POST['instructor']
try:
user = User.objects.get(username=uname)
except User.DoesNotExist:
msg += '<font color="red">Error: unknown username "{0}"</font>'.format(uname)
user = None
if user is not None:
group = get_instructor_group(course)
msg += '<font color="green">Removed {0} from instructor group = {1}</font>'.format(user, group.name)
log.debug('instructorgrp={0}'.format(group.name))
user.groups.remove(group)
track.views.server_track(request, 'remove-instructor {0}'.format(user), {}, page='idashboard')
#----------------------------------------
# forum administration
@@ -258,6 +368,71 @@ def instructor_dashboard(request, course_id):
track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_ADD, uname, FORUM_ROLE_COMMUNITY_TA, course_id),
{}, page='idashboard')
#----------------------------------------
# enrollment
elif action == 'List students who may enroll but may not have yet signed up':
ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_id)
datatable = {'header': ['StudentEmail']}
datatable['data'] = [[x.email] for x in ceaset]
datatable['title'] = action
elif action == 'Enroll student':
student = request.POST.get('enstudent','')
ret = _do_enroll_students(course, course_id, student)
datatable = ret['datatable']
elif action == 'Un-enroll student':
student = request.POST.get('enstudent','')
datatable = {}
isok = False
cea = CourseEnrollmentAllowed.objects.filter(course_id=course_id, email=student)
if cea:
cea.delete()
msg += "Un-enrolled student with email '%s'" % student
isok = True
try:
nce = CourseEnrollment.objects.get(user=User.objects.get(email=student), course_id=course_id)
nce.delete()
msg += "Un-enrolled student with email '%s'" % student
except Exception as err:
if not isok:
msg += "Error! Failed to un-enroll student with email '%s'\n" % student
msg += str(err) + '\n'
elif action == 'Un-enroll ALL students':
ret = _do_enroll_students(course, course_id, '', overload=True)
datatable = ret['datatable']
elif action == 'Enroll multiple students':
students = request.POST.get('enroll_multiple','')
ret = _do_enroll_students(course, course_id, students)
datatable = ret['datatable']
elif action == 'List sections available in remote gradebook':
msg2, datatable = _do_remote_gradebook(request.user, course, 'get-sections')
msg += msg2
elif action in ['List students in section in remote gradebook',
'Overload enrollment list using remote gradebook',
'Merge enrollment list with remote gradebook']:
section = request.POST.get('gradebook_section','')
msg2, datatable = _do_remote_gradebook(request.user, course, 'get-membership', dict(section=section) )
msg += msg2
if not 'List' in action:
students = ','.join([x['email'] for x in datatable['retdata']])
overload = 'Overload' in action
ret = _do_enroll_students(course, course_id, students, overload=overload)
datatable = ret['datatable']
#----------------------------------------
# psychometrics
@@ -271,9 +446,15 @@ def instructor_dashboard(request, course_id):
problems = psychoanalyze.problems_with_psychometric_data(course_id)
#----------------------------------------
# offline grades?
if use_offline:
msg += "<br/><font color='orange'>Grades from %s</font>" % offline_grades_available(course_id)
#----------------------------------------
# context for rendering
context = {'course': course,
'staff_access': True,
'admin_access': request.user.is_staff,
@@ -286,16 +467,66 @@ def instructor_dashboard(request, course_id):
'plots': plots, # psychometrics
'course_errors': modulestore().get_item_errors(course.location),
'djangopid' : os.getpid(),
'mitx_version' : getattr(settings,'MITX_VERSION_STRING',''),
'offline_grade_log' : offline_grades_available(course_id),
}
return render_to_response('courseware/instructor_dashboard.html', context)
def _do_remote_gradebook(user, course, action, args=None, files=None):
'''
Perform remote gradebook action. Returns msg, datatable.
'''
rg = course.metadata.get('remote_gradebook','')
if not rg:
msg = "No remote gradebook defined in course metadata"
return msg, {}
rgurl = settings.MITX_FEATURES.get('REMOTE_GRADEBOOK_URL','')
if not rgurl:
msg = "No remote gradebook url defined in settings.MITX_FEATURES"
return msg, {}
rgname = rg.get('name','')
if not rgname:
msg = "No gradebook name defined in course remote_gradebook metadata"
return msg, {}
if args is None:
args = {}
data = dict(submit=action, gradebook=rgname, user=user.email)
data.update(args)
try:
resp = requests.post(rgurl, data=data, verify=False, files=files)
retdict = json.loads(resp.content)
except Exception as err:
msg = "Failed to communicate with gradebook server at %s<br/>" % rgurl
msg += "Error: %s" % err
msg += "<br/>resp=%s" % resp.content
msg += "<br/>data=%s" % data
return msg, {}
msg = '<pre>%s</pre>' % retdict['msg'].replace('\n','<br/>')
retdata = retdict['data'] # a list of dicts
if retdata:
datatable = {'header': retdata[0].keys()}
datatable['data'] = [x.values() for x in retdata]
datatable['title'] = 'Remote gradebook response for %s' % action
datatable['retdata'] = retdata
else:
datatable = {}
return msg, datatable
def _list_course_forum_members(course_id, rolename, datatable):
'''
Fills in datatable with forum membership information, for a given role,
so that it will be displayed on instructor dashboard.
course_ID = course's ID string
course_ID = the ID string for a course
rolename = one of "Administrator", "Moderator", "Community TA"
Returns message status string to append to displayed message, if role is unknown.
@@ -360,7 +591,7 @@ def _update_forum_role_membership(uname, course, rolename, add_or_remove):
return msg
def get_student_grade_summary_data(request, course, course_id, get_grades=True, get_raw_scores=False):
def get_student_grade_summary_data(request, course, course_id, get_grades=True, get_raw_scores=False, use_offline=False):
'''
Return data arrays with student identity and grades for specified course.
@@ -381,16 +612,18 @@ 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']
assignments = []
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)
gradeset = student_grades(enrolled_students[0], request, course, keep_raw_scores=get_raw_scores, use_offline=use_offline)
# log.debug('student {0} gradeset {1}'.format(enrolled_students[0], gradeset))
if get_raw_scores:
header += [score.section for score in gradeset['raw_scores']]
assignments += [score.section for score in gradeset['raw_scores']]
else:
header += [x['label'] for x in gradeset['section_breakdown']]
assignments += [x['label'] for x in gradeset['section_breakdown']]
header += assignments
datatable = {'header': header}
datatable = {'header': header, 'assignments': assignments, 'students': enrolled_students}
data = []
for student in enrolled_students:
@@ -401,40 +634,21 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True,
datarow.append('')
if get_grades:
gradeset = grades.grade(student, request, course, keep_raw_scores=get_raw_scores)
# log.debug('student={0}, gradeset={1}'.format(student,gradeset))
gradeset = student_grades(student, request, course, keep_raw_scores=get_raw_scores, use_offline=use_offline)
log.debug('student={0}, gradeset={1}'.format(student,gradeset))
if get_raw_scores:
datarow += [score.earned for score in gradeset['raw_scores']]
# TODO (ichuang) encode Score as dict instead of as list, so score[0] -> score['earned']
sgrades = [(getattr(score,'earned','') or score[0]) for score in gradeset['raw_scores']]
else:
datarow += [x['percent'] for x in gradeset['section_breakdown']]
sgrades = [x['percent'] for x in gradeset['section_breakdown']]
datarow += sgrades
student.grades = sgrades # store in student object
data.append(datarow)
datatable['data'] = data
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):
@@ -453,7 +667,7 @@ def gradebook(request, course_id):
student_info = [{'username': student.username,
'id': student.id,
'email': student.email,
'grade_summary': grades.grade(student, request, course),
'grade_summary': student_grades(student, request, course),
'realname': student.profile.name,
}
for student in enrolled_students]
@@ -476,6 +690,72 @@ def grade_summary(request, course_id):
return render_to_response('courseware/grade_summary.html', context)
#-----------------------------------------------------------------------------
# enrollment
def _do_enroll_students(course, course_id, students, overload=False):
"""Do the actual work of enrolling multiple students, presented as a string
of emails separated by commas or returns"""
ns = [x.split('\n') for x in students.split(',')]
new_students = [item for sublist in ns for item in sublist]
new_students = [str(s.strip()) for s in new_students]
new_students_lc = [x.lower() for x in new_students]
if '' in new_students:
new_students.remove('')
status = dict([x,'unprocessed'] for x in new_students)
if overload: # delete all but staff
todelete = CourseEnrollment.objects.filter(course_id=course_id)
for ce in todelete:
if not has_access(ce.user, course, 'staff') and ce.user.email.lower() not in new_students_lc:
status[ce.user.email] = 'deleted'
ce.delete()
else:
status[ce.user.email] = 'is staff'
ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_id)
for cea in ceaset:
status[cea.email] = 'removed from pending enrollment list'
ceaset.delete()
for student in new_students:
try:
user=User.objects.get(email=student)
except User.DoesNotExist:
# user not signed up yet, put in pending enrollment allowed table
if CourseEnrollmentAllowed.objects.filter(email=student, course_id=course_id):
status[student] = 'user does not exist, enrollment already allowed, pending'
continue
cea = CourseEnrollmentAllowed(email=student, course_id=course_id)
cea.save()
status[student] = 'user does not exist, enrollment allowed, pending'
continue
if CourseEnrollment.objects.filter(user=user, course_id=course_id):
status[student] = 'already enrolled'
continue
try:
nce = CourseEnrollment(user=user, course_id=course_id)
nce.save()
status[student] = 'added'
except:
status[student] = 'rejected'
datatable = {'header': ['StudentEmail', 'action']}
datatable['data'] = [[x, status[x]] for x in status]
datatable['title'] = 'Enrollment of students'
def sf(stat): return [x for x in status if status[x]==stat]
data = dict(added=sf('added'), rejected=sf('rejected')+sf('exists'),
deleted=sf('deleted'), datatable=datatable)
return data
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def enroll_students(request, course_id):
@@ -494,22 +774,10 @@ def enroll_students(request, course_id):
course = get_course_with_access(request.user, course_id, 'staff')
existing_students = [ce.user.email for ce in CourseEnrollment.objects.filter(course_id=course_id)]
if 'new_students' in request.POST:
new_students = request.POST['new_students'].split('\n')
else:
new_students = []
new_students = [s.strip() for s in new_students]
added_students = []
rejected_students = []
for student in new_students:
try:
nce = CourseEnrollment(user=User.objects.get(email=student), course_id=course_id)
nce.save()
added_students.append(student)
except:
rejected_students.append(student)
new_students = request.POST.get('new_students')
ret = _do_enroll_students(course, course_id, new_students)
added_students = ret['added']
rejected_students = ret['rejected']
return render_to_response("enroll_students.html", {'course': course_id,
'existing_students': existing_students,
@@ -518,6 +786,9 @@ def enroll_students(request, course_id):
'debug': new_students})
#-----------------------------------------------------------------------------
# answer distribution
def get_answers_distribution(request, course_id):
"""
Get the distribution of answers for all graded problems in the course.

View File

@@ -0,0 +1,100 @@
# This class gives a common interface for logging into the grading controller
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 GradingService(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.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 post(self, url, data, allow_redirects=False):
"""
Make a post request to the grading controller
"""
try:
op = lambda: self.session.post(url, data=data,
allow_redirects=allow_redirects)
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(self, url, params, allow_redirects=False):
"""
Make a get request to the grading controller
"""
log.debug(params)
op = lambda: self.session.get(url,
allow_redirects=allow_redirects,
params=params)
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 _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

View File

@@ -0,0 +1,355 @@
"""
This module provides an interface on the grading-service backend
for peer grading
Use peer_grading_service() to get the version specified
in settings.PEER_GRADING_INTERFACE
"""
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 grading_service import GradingService
from grading_service import GradingServiceError
from courseware.access import has_access
from util.json_request import expect_json
from xmodule.course_module import CourseDescriptor
from student.models import unique_id_for_user
log = logging.getLogger(__name__)
"""
This is a mock peer grading service that can be used for unit tests
without making actual service calls to the grading controller
"""
class MockPeerGradingService(object):
def get_next_submission(self, problem_location, grader_id):
return json.dumps({'success': True,
'submission_id':1,
'submission_key': "",
'student_response': 'fake student response',
'prompt': 'fake submission prompt',
'rubric': 'fake rubric',
'max_score': 4})
def save_grade(self, location, grader_id, submission_id,
score, feedback, submission_key):
return json.dumps({'success': True})
def is_student_calibrated(self, problem_location, grader_id):
return json.dumps({'success': True, 'calibrated': True})
def show_calibration_essay(self, problem_location, grader_id):
return json.dumps({'success': True,
'submission_id':1,
'submission_key': '',
'student_response': 'fake student response',
'prompt': 'fake submission prompt',
'rubric': 'fake rubric',
'max_score': 4})
def save_calibration_essay(self, problem_location, grader_id,
calibration_essay_id, submission_key, score, feedback):
return {'success': True, 'actual_score': 2}
def get_problem_list(self, course_id, grader_id):
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}),
json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo2',
'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5})
]})
class PeerGradingService(GradingService):
"""
Interface with the grading controller for peer grading
"""
def __init__(self, config):
super(PeerGradingService, self).__init__(config)
self.get_next_submission_url = self.url + '/get_next_submission/'
self.save_grade_url = self.url + '/save_grade/'
self.is_student_calibrated_url = self.url + '/is_student_calibrated/'
self.show_calibration_essay_url = self.url + '/show_calibration_essay/'
self.save_calibration_essay_url = self.url + '/save_calibration_essay/'
self.get_problem_list_url = self.url + '/get_problem_list/'
def get_next_submission(self, problem_location, grader_id):
response = self.get(self.get_next_submission_url,
{'location': problem_location, 'grader_id': grader_id})
return response
def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key):
data = {'grader_id' : grader_id,
'submission_id' : submission_id,
'score' : score,
'feedback' : feedback,
'submission_key': submission_key,
'location': location}
return self.post(self.save_grade_url, data)
def is_student_calibrated(self, problem_location, grader_id):
params = {'problem_id' : problem_location, 'student_id': grader_id}
return self.get(self.is_student_calibrated_url, params)
def show_calibration_essay(self, problem_location, grader_id):
params = {'problem_id' : problem_location, 'student_id': grader_id}
return self.get(self.show_calibration_essay_url, params)
def save_calibration_essay(self, problem_location, grader_id, calibration_essay_id, submission_key, score, feedback):
data = {'location': problem_location,
'student_id': grader_id,
'calibration_essay_id': calibration_essay_id,
'submission_key': submission_key,
'score': score,
'feedback': feedback}
return self.post(self.save_calibration_essay_url, data)
def get_problem_list(self, course_id, grader_id):
params = {'course_id': course_id, 'student_id': grader_id}
response = self.get(self.get_problem_list_url, params)
return response
_service = None
def peer_grading_service():
"""
Return a peer grading service instance--if settings.MOCK_PEER_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_PEER_GRADING:
_service = MockPeerGradingService()
else:
_service = PeerGradingService(settings.PEER_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_required(request, required):
actual = set(request.POST.keys())
missing = required - actual
if len(missing) > 0:
return False, "Missing required keys: {0}".format(', '.join(missing))
else:
return True, ""
def _check_post(request):
if request.method != 'POST':
raise Http404
def get_next_submission(request, course_id):
"""
Makes a call to the grading controller for the next essay that should be graded
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.
'submission_key': a key associated with the submission for validation reasons
'error': if success is False, will have an error message with more info.
"""
_check_post(request)
required = set(['location'])
success, message = _check_required(request, required)
if not success:
return _err_response(message)
grader_id = unique_id_for_user(request.user)
p = request.POST
location = p['location']
try:
response = peer_grading_service().get_next_submission(location, grader_id)
return HttpResponse(response,
mimetype="application/json")
except GradingServiceError:
log.exception("Error getting next submission. server url: {0} location: {1}, grader_id: {2}"
.format(staff_grading_service().url, location, grader_id))
return json.dumps({'success': False,
'error': 'Could not connect to grading service'})
def save_grade(request, course_id):
"""
Saves the grade of a given submission.
Input:
The request should have the following keys:
location - problem location
submission_id - id associated with this submission
submission_key - submission key given for validation purposes
score - the grade that was given to the submission
feedback - the feedback from the student
Returns
A json object with the following keys:
success: bool indicating whether the save was a success
error: if there was an error in the submission, this is the error message
"""
_check_post(request)
required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback'])
success, message = _check_required(request, required)
if not success:
return _err_response(message)
grader_id = unique_id_for_user(request.user)
p = request.POST
location = p['location']
submission_id = p['submission_id']
score = p['score']
feedback = p['feedback']
submission_key = p['submission_key']
try:
response = peer_grading_service().save_grade(location, grader_id, submission_id,
score, feedback, submission_key)
return HttpResponse(response, mimetype="application/json")
except GradingServiceError:
log.exception("""Error saving grade. server url: {0}, location: {1}, submission_id:{2},
submission_key: {3}, score: {4}"""
.format(staff_grading_service().url,
location, submission_id, submission_key, score)
)
return json.dumps({'success': False,
'error': 'Could not connect to grading service'})
def is_student_calibrated(request, course_id):
"""
Calls the grading controller to see if the given student is calibrated
on the given problem
Input:
In the request, we need the following arguments:
location - problem location
Returns:
Json object with the following keys
success - bool indicating whether or not the call was successful
calibrated - true if the grader has fully calibrated and can now move on to grading
- false if the grader is still working on calibration problems
total_calibrated_on_so_far - the number of calibration essays for this problem
that this grader has graded
"""
_check_post(request)
required = set(['location'])
success, message = _check_required(request, required)
if not success:
return _err_response(message)
grader_id = unique_id_for_user(request.user)
p = request.POST
location = p['location']
try:
response = peer_grading_service().is_student_calibrated(location, grader_id)
return HttpResponse(response, mimetype="application/json")
except GradingServiceError:
log.exception("Error from grading service. server url: {0}, grader_id: {0}, location: {1}"
.format(staff_grading_service().url, grader_id, location))
return json.dumps({'success': False,
'error': 'Could not connect to grading service'})
def show_calibration_essay(request, course_id):
"""
Fetch the next calibration essay from the grading controller and return it
Inputs:
In the request
location - problem location
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.
'submission_key': a key associated with the submission for validation reasons
'error': if success is False, will have an error message with more info.
"""
_check_post(request)
required = set(['location'])
success, message = _check_required(request, required)
if not success:
return _err_response(message)
grader_id = unique_id_for_user(request.user)
p = request.POST
location = p['location']
try:
response = peer_grading_service().show_calibration_essay(location, grader_id)
return HttpResponse(response, mimetype="application/json")
except GradingServiceError:
log.exception("Error from grading service. server url: {0}, location: {0}"
.format(staff_grading_service().url, location))
return json.dumps({'success': False,
'error': 'Could not connect to grading service'})
def save_calibration_essay(request, course_id):
"""
Saves the grader's grade of a given calibration.
Input:
The request should have the following keys:
location - problem location
submission_id - id associated with this submission
submission_key - submission key given for validation purposes
score - the grade that was given to the submission
feedback - the feedback from the student
Returns
A json object with the following keys:
success: bool indicating whether the save was a success
error: if there was an error in the submission, this is the error message
actual_score: the score that the instructor gave to this calibration essay
"""
_check_post(request)
required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback'])
success, message = _check_required(request, required)
if not success:
return _err_response(message)
grader_id = unique_id_for_user(request.user)
p = request.POST
location = p['location']
calibration_essay_id = p['submission_id']
submission_key = p['submission_key']
score = p['score']
feedback = p['feedback']
try:
response = peer_grading_service().save_calibration_essay(location, grader_id, calibration_essay_id, submission_key, score, feedback)
return HttpResponse(response, mimetype="application/json")
except GradingServiceError:
log.exception("Error saving calibration grade, location: {0}, submission_id: {1}, submission_key: {2}, grader_id: {3}".format(location, submission_id, submission_key, grader_id))
return _err_response('Could not connect to grading service')

View File

@@ -7,6 +7,8 @@ import logging
import requests
from requests.exceptions import RequestException, ConnectionError, HTTPError
import sys
from grading_service import GradingService
from grading_service import GradingServiceError
from django.conf import settings
from django.http import HttpResponse, Http404
@@ -14,13 +16,11 @@ 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
from student.models import unique_id_for_user
log = logging.getLogger(__name__)
class GradingServiceError(Exception):
pass
class MockStaffGradingService(object):
"""
@@ -57,62 +57,16 @@ class MockStaffGradingService(object):
return self.get_next(course_id, 'fake location', grader_id)
class StaffGradingService(object):
class StaffGradingService(GradingService):
"""
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/'
super(StaffGradingService, self).__init__(config)
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):
"""
@@ -130,17 +84,8 @@ class StaffGradingService(object):
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
params = {'course_id': course_id,'grader_id': grader_id}
return self.get(self.get_problem_list_url, params)
def get_next(self, course_id, location, grader_id):
@@ -161,17 +106,9 @@ class StaffGradingService(object):
Raises:
GradingServiceError: something went wrong with the connection.
"""
op = lambda: self.session.get(self.get_next_url,
allow_redirects=False,
return self.get(self.get_next_url,
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):
@@ -186,28 +123,20 @@ class StaffGradingService(object):
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}
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 self.post(self.save_grade_url, data=data)
return r.text
# don't initialize until grading_service() is called--means that just
# don't initialize until staff_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():
def staff_grading_service():
"""
Return a staff grading service instance--if settings.MOCK_STAFF_GRADING is True,
returns a mock one, otherwise a real one.
@@ -248,7 +177,7 @@ def _check_access(user, course_id):
def get_next(request, course_id):
"""
Get the next thing to grade for course_id and with the location specified
in the .
in the request.
Returns a json dict with the following keys:
@@ -276,11 +205,11 @@ def get_next(request, course_id):
if len(missing) > 0:
return _err_response('Missing required keys {0}'.format(
', '.join(missing)))
grader_id = request.user.id
grader_id = unique_id_for_user(request.user)
p = request.POST
location = p['location']
return HttpResponse(_get_next(course_id, request.user.id, location),
return HttpResponse(_get_next(course_id, grader_id, location),
mimetype="application/json")
@@ -308,12 +237,12 @@ def get_problem_list(request, course_id):
"""
_check_access(request.user, course_id)
try:
response = grading_service().get_problem_list(course_id, request.user.id)
response = staff_grading_service().get_problem_list(course_id, unique_id_for_user(request.user))
return HttpResponse(response,
mimetype="application/json")
except GradingServiceError:
log.exception("Error from grading service. server url: {0}"
.format(grading_service().url))
.format(staff_grading_service().url))
return HttpResponse(json.dumps({'success': False,
'error': 'Could not connect to grading service'}))
@@ -323,10 +252,10 @@ 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)
return staff_grading_service().get_next(course_id, location, grader_id)
except GradingServiceError:
log.exception("Error from grading service. server url: {0}"
.format(grading_service().url))
.format(staff_grading_service().url))
return json.dumps({'success': False,
'error': 'Could not connect to grading service'})
@@ -357,14 +286,14 @@ def save_grade(request, course_id):
return _err_response('Missing required keys {0}'.format(
', '.join(missing)))
grader_id = request.user.id
grader_id = unique_id_for_user(request.user)
p = request.POST
location = p['location']
skipped = 'skipped' in p
try:
result_json = grading_service().save_grade(course_id,
result_json = staff_grading_service().save_grade(course_id,
grader_id,
p['submission_id'],
p['score'],

View File

@@ -0,0 +1,112 @@
"""
Tests for open ended grading interfaces
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/open_ended_grading
"""
from django.test import TestCase
from open_ended_grading import staff_grading_service
from django.core.urlresolvers import reverse
from django.contrib.auth.models import Group
from courseware.access import _course_staff_group_name
import courseware.tests.tests as ct
from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django
from nose import SkipTest
from mock import patch, Mock
import json
from override_settings import override_settings
_mock_service = staff_grading_service.MockStaffGradingService()
@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.staff_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'])

View File

@@ -0,0 +1,118 @@
# Grading Views
import logging
import urllib
from django.conf import settings
from django.views.decorators.cache import cache_control
from mitxmako.shortcuts import render_to_response
from django.core.urlresolvers import reverse
from student.models import unique_id_for_user
from courseware.courses import get_course_with_access
from peer_grading_service import PeerGradingService
from peer_grading_service import MockPeerGradingService
from grading_service import GradingServiceError
import json
from .staff_grading import StaffGrading
log = logging.getLogger(__name__)
template_imports = {'urllib': urllib}
if settings.MOCK_PEER_GRADING:
peer_gs = MockPeerGradingService()
else:
peer_gs = PeerGradingService(settings.PEER_GRADING_INTERFACE)
"""
Reverses the URL from the name and the course id, and then adds a trailing slash if
it does not exist yet
"""
def _reverse_with_slash(url_name, course_id):
ajax_url = reverse(url_name, kwargs={'course_id': course_id})
if not ajax_url.endswith('/'):
ajax_url += '/'
return ajax_url
@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')
ajax_url = _reverse_with_slash('staff_grading', course_id)
return render_to_response('instructor/staff_grading.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 peer_grading(request, course_id):
'''
Show a peer grading interface
'''
course = get_course_with_access(request.user, course_id, 'load')
# call problem list service
success = False
error_text = ""
problem_list = []
try:
problem_list_json = peer_gs.get_problem_list(course_id, unique_id_for_user(request.user))
problem_list_dict = json.loads(problem_list_json)
success = problem_list_dict['success']
if 'error' in problem_list_dict:
error_text = problem_list_dict['error']
problem_list = problem_list_dict['problem_list']
except GradingServiceError:
error_text = "Error occured while contacting the grading service"
success = False
# catch error if if the json loads fails
except ValueError:
error_text = "Could not get problem list"
success = False
ajax_url = _reverse_with_slash('peer_grading', course_id)
return render_to_response('peer_grading/peer_grading.html', {
'course': course,
'course_id': course_id,
'ajax_url': ajax_url,
'success': success,
'problem_list': problem_list,
'error_text': error_text,
# Checked above
'staff_access': False, })
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def peer_grading_problem(request, course_id):
'''
Show individual problem interface
'''
course = get_course_with_access(request.user, course_id, 'load')
problem_location = request.GET.get("location")
ajax_url = _reverse_with_slash('peer_grading', course_id)
return render_to_response('peer_grading/peer_grading_problem.html', {
'view_html': '',
'course': course,
'problem_location': problem_location,
'course_id': course_id,
'ajax_url': ajax_url,
# Checked above
'staff_access': False, })

View File

@@ -76,8 +76,8 @@ DATABASES = AUTH_TOKENS['DATABASES']
XQUEUE_INTERFACE = AUTH_TOKENS['XQUEUE_INTERFACE']
STAFF_GRADING_INTERFACE = AUTH_TOKENS.get('STAFF_GRADING_INTERFACE')
STAFF_GRADING_INTERFACE = AUTH_TOKENS.get('STAFF_GRADING_INTERFACE', STAFF_GRADING_INTERFACE)
PEER_GRADING_INTERFACE = AUTH_TOKENS.get('PEER_GRADING_INTERFACE', PEER_GRADING_INTERFACE)
PEARSON_TEST_USER = "pearsontest"
PEARSON_TEST_PASSWORD = AUTH_TOKENS.get("PEARSON_TEST_PASSWORD")

View File

@@ -329,10 +329,29 @@ WIKI_LINK_DEFAULT_LEVEL = 2
################################# Staff grading config #####################
STAFF_GRADING_INTERFACE = None
#By setting up the default settings with an incorrect user name and password,
# will get an error when attempting to connect
STAFF_GRADING_INTERFACE = {
'url': 'http://sandbox-grader-001.m.edx.org/staff_grading',
'username': 'incorrect_user',
'password': 'incorrect_pass',
}
# Used for testing, debugging
MOCK_STAFF_GRADING = False
################################# Peer grading config #####################
#By setting up the default settings with an incorrect user name and password,
# will get an error when attempting to connect
PEER_GRADING_INTERFACE = {
'url': 'http://sandbox-grader-001.m.edx.org/peer_grading',
'username': 'incorrect_user',
'password': 'incorrect_pass',
}
# Used for testing, debugging
MOCK_PEER_GRADING = False
################################# Jasmine ###################################
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
@@ -411,6 +430,7 @@ courseware_only_js += [
main_vendor_js = [
'js/vendor/RequireJS.js',
'js/vendor/json2.js',
'js/vendor/RequireJS.js',
'js/vendor/jquery.min.js',
'js/vendor/jquery-ui.min.js',
'js/vendor/jquery.cookie.js',
@@ -421,6 +441,7 @@ main_vendor_js = [
discussion_js = sorted(glob2.glob(PROJECT_ROOT / 'static/coffee/src/discussion/**/*.coffee'))
staff_grading_js = sorted(glob2.glob(PROJECT_ROOT / 'static/coffee/src/staff_grading/**/*.coffee'))
peer_grading_js = sorted(glob2.glob(PROJECT_ROOT / 'static/coffee/src/peer_grading/**/*.coffee'))
# Load javascript from all of the available xmodules, and
@@ -496,6 +517,7 @@ PIPELINE_JS = {
for pth in sorted(glob2.glob(PROJECT_ROOT / 'static/coffee/src/**/*.coffee'))\
if (pth not in courseware_only_js and
pth not in discussion_js and
pth not in peer_grading_js and
pth not in staff_grading_js)
] + [
'js/form.ext.js',
@@ -529,8 +551,11 @@ PIPELINE_JS = {
'staff_grading' : {
'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in staff_grading_js],
'output_filename': 'js/staff_grading.js'
},
'peer_grading' : {
'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in peer_grading_js],
'output_filename': 'js/peer_grading.js'
}
}
PIPELINE_DISABLE_WRAPPER = True
@@ -603,6 +628,7 @@ INSTALLED_APPS = (
'util',
'certificates',
'instructor',
'open_ended_grading',
'psychometrics',
'licenses',

View File

@@ -102,6 +102,10 @@ SUBDOMAIN_BRANDING = {
COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE"
################################# mitx revision string #####################
MITX_VERSION_STRING = os.popen('cd %s; git describe' % REPO_ROOT).read().strip()
################################# Staff grading config #####################
STAFF_GRADING_INTERFACE = {
@@ -110,6 +114,13 @@ STAFF_GRADING_INTERFACE = {
'password': 'abcd',
}
################################# Peer grading config #####################
PEER_GRADING_INTERFACE = {
'url': 'http://127.0.0.1:3033/peer_grading',
'username': 'lms',
'password': 'abcd',
}
################################ LMS Migration #################################
MITX_FEATURES['ENABLE_LMS_MIGRATION'] = True
MITX_FEATURES['ACCESS_REQUIRE_STAFF_FOR_COURSE'] = False # require that user be in the staff_* group to be able to enroll

View File

@@ -62,6 +62,7 @@ XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds
# Don't rely on a real staff grading backend
MOCK_STAFF_GRADING = True
MOCK_PEER_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

View File

@@ -0,0 +1,13 @@
# This is a simple class that just hides the error container
# and message container when they are empty
# Can (and should be) expanded upon when our problem list
# becomes more sophisticated
class PeerGrading
constructor: () ->
@error_container = $('.error-container')
@error_container.toggle(not @error_container.is(':empty'))
@message_container = $('.message-container')
@message_container.toggle(not @message_container.is(':empty'))
$(document).ready(() -> new PeerGrading())

View File

@@ -0,0 +1,390 @@
##################################
#
# This is the JS that renders the peer grading problem page.
# Fetches the correct problem and/or calibration essay
# and sends back the grades
#
# Should not be run when we don't have a location to send back
# to the server
#
# PeerGradingProblemBackend -
# makes all the ajax requests and provides a mock interface
# for testing purposes
#
# PeerGradingProblem -
# handles the rendering and user interactions with the interface
#
##################################
class PeerGradingProblemBackend
constructor: (ajax_url, mock_backend) ->
@mock_backend = mock_backend
@ajax_url = ajax_url
@mock_cnt = 0
post: (cmd, data, callback) ->
if @mock_backend
callback(@mock(cmd, data))
else
# if this post request fails, the error callback will catch it
$.post(@ajax_url + cmd, data, callback)
.error => callback({success: false, error: "Error occured while performing this operation"})
mock: (cmd, data) ->
if cmd == 'is_student_calibrated'
# change to test each version
response =
success: true
calibrated: @mock_cnt >= 2
else if cmd == 'show_calibration_essay'
#response =
# success: false
# error: "There was an error"
@mock_cnt++
response =
success: true
submission_id: 1
submission_key: 'abcd'
student_response: '''
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.
'''
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>
<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>
'''
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>
'''
max_score: 4
else if cmd == 'get_next_submission'
response =
success: true
submission_id: 1
submission_key: 'abcd'
student_response: '''Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nec tristique ante. Proin at mauris sapien, quis varius leo. Morbi laoreet leo nisi. Morbi aliquam lacus ante. Cras iaculis velit sed diam mattis a fermentum urna luctus. Duis consectetur nunc vitae felis facilisis eget vulputate risus viverra. Cras consectetur ullamcorper lobortis. Nam eu gravida lorem. Nulla facilisi. Nullam quis felis enim. Mauris orci lectus, dictum id cursus in, vulputate in massa.
Phasellus non varius sem. Nullam commodo lacinia odio sit amet egestas. Donec ullamcorper sapien sagittis arcu volutpat placerat. Phasellus ut pretium ante. Nam dictum pulvinar nibh dapibus tristique. Sed at tellus mi, fringilla convallis justo. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus tristique rutrum nulla sed eleifend. Praesent at nunc arcu. Mauris condimentum faucibus nibh, eget commodo quam viverra sed. Morbi in tincidunt dolor. Morbi sed augue et augue interdum fermentum.
Curabitur tristique purus ac arcu consequat cursus. Cras diam felis, dignissim quis placerat at, aliquet ac metus. Mauris vulputate est eu nibh imperdiet varius. Cras aliquet rhoncus elit a laoreet. Mauris consectetur erat et erat scelerisque eu faucibus dolor consequat. Nam adipiscing sagittis nisl, eu mollis massa tempor ac. Nulla scelerisque tempus blandit. Phasellus ac ipsum eros, id posuere arcu. Nullam non sapien arcu. Vivamus sit amet lorem justo, ac tempus turpis. Suspendisse pharetra gravida imperdiet. Pellentesque lacinia mi eu elit luctus pellentesque. Sed accumsan libero a magna elementum varius. Nunc eget pellentesque metus. '''
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>
<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>
'''
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>
'''
max_score: 4
else if cmd == 'save_calibration_essay'
response =
success: true
actual_score: 2
else if cmd == 'save_grade'
response =
success: true
return response
class PeerGradingProblem
constructor: (backend) ->
@prompt_wrapper = $('.prompt-wrapper')
@backend = backend
# get the location of the problem
@location = $('.peer-grading').data('location')
# prevent this code from trying to run
# when we don't have a location
if(!@location)
return
# get the other elements we want to fill in
@submission_container = $('.submission-container')
@prompt_container = $('.prompt-container')
@rubric_container = $('.rubric-container')
@calibration_panel = $('.calibration-panel')
@grading_panel = $('.grading-panel')
@content_panel = $('.content-panel')
@grading_message = $('.grading-message')
@grading_message.hide()
@grading_wrapper =$('.grading-wrapper')
@calibration_feedback_panel = $('.calibration-feedback')
@interstitial_page = $('.interstitial-page')
@interstitial_page.hide()
@error_container = $('.error-container')
@submission_key_input = $("input[name='submission-key']")
@essay_id_input = $("input[name='essay-id']")
@feedback_area = $('.feedback-area')
@score_selection_container = $('.score-selection-container')
@score = null
@calibration = null
@submit_button = $('.submit-button')
@action_button = $('.action-button')
@calibration_feedback_button = $('.calibration-feedback-button')
@interstitial_page_button = $('.interstitial-page-button')
Collapsible.setCollapsibles(@content_panel)
# Set up the click event handlers
@action_button.click -> history.back()
@calibration_feedback_button.click =>
@calibration_feedback_panel.hide()
@grading_wrapper.show()
@is_calibrated_check()
@interstitial_page_button.click =>
@interstitial_page.hide()
@is_calibrated_check()
@is_calibrated_check()
##########
#
# Ajax calls to the backend
#
##########
is_calibrated_check: () =>
@backend.post('is_student_calibrated', {location: @location}, @calibration_check_callback)
fetch_calibration_essay: () =>
@backend.post('show_calibration_essay', {location: @location}, @render_calibration)
fetch_submission_essay: () =>
@backend.post('get_next_submission', {location: @location}, @render_submission)
construct_data: () ->
data =
score: @score
location: @location
submission_id: @essay_id_input.val()
submission_key: @submission_key_input.val()
feedback: @feedback_area.val()
return data
submit_calibration_essay: ()=>
data = @construct_data()
@backend.post('save_calibration_essay', data, @calibration_callback)
submit_grade: () =>
data = @construct_data()
@backend.post('save_grade', data, @submission_callback)
##########
#
# Callbacks for various events
#
##########
# called after we perform an is_student_calibrated check
calibration_check_callback: (response) =>
if response.success
# if we haven't been calibrating before
if response.calibrated and (@calibration == null or @calibration == false)
@calibration = false
@fetch_submission_essay()
# If we were calibrating before and no longer need to,
# show the interstitial page
else if response.calibrated and @calibration == true
@calibration = false
@render_interstitial_page()
else
@calibration = true
@fetch_calibration_essay()
else if response.error
@render_error(response.error)
else
@render_error("Error contacting the grading service")
# called after we submit a calibration score
calibration_callback: (response) =>
if response.success
@render_calibration_feedback(response)
else if response.error
@render_error(response.error)
else
@render_error("Error saving calibration score")
# called after we submit a submission score
submission_callback: (response) =>
if response.success
@is_calibrated_check()
@grading_message.fadeIn()
@grading_message.html("<p>Grade sent successfully.</p>")
else
if response.error
@render_error(response.error)
else
@render_error("Error occurred while submitting grade")
# called after a grade is selected on the interface
graded_callback: (event) =>
@grading_message.hide()
@score = event.target.value
@show_submit_button()
##########
#
# Rendering methods and helpers
#
##########
# renders a calibration essay
render_calibration: (response) =>
if response.success
# load in all the data
@submission_container.html("<h3>Training Essay</h3>")
@render_submission_data(response)
# TODO: indicate that we're in calibration mode
@calibration_panel.addClass('current-state')
@grading_panel.removeClass('current-state')
# Display the right text
# both versions of the text are written into the template itself
# we only need to show/hide the correct ones at the correct time
@calibration_panel.find('.calibration-text').show()
@grading_panel.find('.calibration-text').show()
@calibration_panel.find('.grading-text').hide()
@grading_panel.find('.grading-text').hide()
@submit_button.unbind('click')
@submit_button.click @submit_calibration_essay
else if response.error
@render_error(response.error)
else
@render_error("An error occurred while retrieving the next calibration essay")
# Renders a student submission to be graded
render_submission: (response) =>
if response.success
@submit_button.hide()
@submission_container.html("<h3>Submitted Essay</h3>")
@render_submission_data(response)
@calibration_panel.removeClass('current-state')
@grading_panel.addClass('current-state')
# Display the correct text
# both versions of the text are written into the template itself
# we only need to show/hide the correct ones at the correct time
@calibration_panel.find('.calibration-text').hide()
@grading_panel.find('.calibration-text').hide()
@calibration_panel.find('.grading-text').show()
@grading_panel.find('.grading-text').show()
@submit_button.unbind('click')
@submit_button.click @submit_grade
else if response.error
@render_error(response.error)
else
@render_error("An error occured when retrieving the next submission.")
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 common information between calibration and grading
render_submission_data: (response) =>
@content_panel.show()
@submission_container.append(@make_paragraphs(response.student_response))
@prompt_container.html(response.prompt)
@rubric_container.html(response.rubric)
@submission_key_input.val(response.submission_key)
@essay_id_input.val(response.submission_id)
@setup_score_selection(response.max_score)
@submit_button.hide()
@action_button.hide()
@calibration_feedback_panel.hide()
render_calibration_feedback: (response) =>
# display correct grade
@calibration_feedback_panel.slideDown()
calibration_wrapper = $('.calibration-feedback-wrapper')
calibration_wrapper.html("<p>The score you gave was: #{@score}. The actual score is: #{response.actual_score}</p>")
score = parseInt(@score)
actual_score = parseInt(response.actual_score)
if score == actual_score
calibration_wrapper.append("<p>Congratulations! Your score matches the actual score!</p>")
else
calibration_wrapper.append("<p>Please try to understand the grading critera better to be more accurate next time.</p>")
# disable score selection and submission from the grading interface
$("input[name='score-selection']").attr('disabled', true)
@submit_button.hide()
render_interstitial_page: () =>
@content_panel.hide()
@interstitial_page.show()
render_error: (error_message) =>
@error_container.show()
@calibration_feedback_panel.hide()
@error_container.html(error_message)
@content_panel.hide()
@action_button.show()
show_submit_button: () =>
@submit_button.show()
setup_score_selection: (max_score) =>
# 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
mock_backend = false
ajax_url = $('.peer-grading').data('ajax_url')
backend = new PeerGradingProblemBackend(ajax_url, mock_backend)
$(document).ready(() -> new PeerGradingProblem(backend))

View File

@@ -1,4 +1,5 @@
div.staff-grading {
div.staff-grading,
div.peer-grading{
textarea.feedback-area {
height: 75px;
margin: 20px;
@@ -36,8 +37,8 @@ div.staff-grading {
}
.prompt-information-container,
.submission-wrapper,
.rubric-wrapper,
.calibration-feedback-wrapper,
.grading-container
{
border: 1px solid gray;
@@ -49,6 +50,18 @@ div.staff-grading {
padding: 15px;
margin-left: 0px;
}
.submission-wrapper
{
h3
{
margin-bottom: 15px;
}
p
{
margin-left:10px;
}
padding: 15px;
}
.meta-info-wrapper
{
background-color: #eee;
@@ -67,7 +80,8 @@ div.staff-grading {
}
}
}
.message-container
.message-container,
.grading-message
{
background-color: $yellow;
padding: 10px;
@@ -81,6 +95,69 @@ div.staff-grading {
margin-bottom:5px;
font-size: .8em;
}
.instructions-panel
{
margin-right:20px;
> div
{
padding: 10px;
margin: 0px;
background: #eee;
height: 10em;
h3
{
text-align:center;
text-transform:uppercase;
color: #777;
}
p
{
color: #777;
}
}
.calibration-panel
{
float:left;
width:48%;
}
.grading-panel
{
float:right;
width: 48%;
}
.current-state
{
background: #1D9DD9;
h3, p
{
color: white;
}
}
@include clearfix;
}
.collapsible
{
margin-left: 0px;
header
{
margin-top:20px;
margin-bottom:20px;
font-size: 1.2em;
}
}
.interstitial-page
{
text-align: center;
input[type=button]
{
margin-top: 20px;
}
}
padding: 40px;
}

View File

@@ -0,0 +1,22 @@
<section id="combined-open-ended" class="combined-open-ended" data-ajax-url="${ajax_url}" data-allow_reset="${allow_reset}" data-state="${state}" data-task-count="${task_count}" data-task-number="${task_number}">
<div class="status-container">
<h4>Status</h4><br/>
${status | n}
</div>
<div class="item-container">
<h4>Problem</h4><br/>
% for item in items:
<div class="item">${item['content'] | n}</div>
% endfor
<input type="button" value="Reset" class="reset-button" name="reset"/>
<input type="button" value="Next Step" class="next-step-button" name="reset"/>
</div>
<div class="result-container">
</div>
</section>

View File

@@ -0,0 +1,4 @@
<div class="result-container">
<h4>Results from Step ${task_number}</h4><br/>
${results | n}
</div>

View File

@@ -0,0 +1,28 @@
<section id="combined-open-ended-status" class="combined-open-ended-status">
%for i in xrange(0,len(status_list)):
<%status=status_list[i]%>
%if i==len(status_list)-1:
<div class="statusitem-current" data-status-number="${i}">
%else:
<div class="statusitem" data-status-number="${i}">
%endif
Step ${status['task_number']} (${status['human_state']}) : ${status['score']} / ${status['max_score']}
% if status['state'] == 'initial':
<span class="unanswered" id="status"></span>
% elif status['state'] in ['done', 'post_assessment'] and status['correct'] == 'correct':
<span class="correct" id="status"></span>
% elif status['state'] in ['done', 'post_assessment'] and status['correct'] == 'incorrect':
<span class="incorrect" id="status"></span>
% elif status['state'] == 'assessing':
<span class="grading" id="status"></span>
% endif
%if status['type']=="openended" and status['state'] in ['done', 'post_assessment']:
<div class="show-results">
<a href="#" class="show-results-button">Show results from step ${status['task_number']}</a>
</div>
%endif
</div>
%endfor
</section>

View File

@@ -5,7 +5,7 @@
%>
<%page args="course" />
<article id="${course.id}" class="course">
%if course.metadata.get('is_new'):
%if course.is_new:
<span class="status">New</span>
%endif
<a href="${reverse('about_course', args=[course.id])}">

View File

@@ -57,10 +57,13 @@ function goto( mode)
<a href="#" onclick="goto('Psychometrics');" class="${modeflag.get('Psychometrics')}">Psychometrics</a> |
%endif
<a href="#" onclick="goto('Admin');" class="${modeflag.get('Admin')}">Admin</a> |
<a href="#" onclick="goto('Forum Admin');" class="${modeflag.get('Forum Admin')}">Forum Admin</a> ]
<a href="#" onclick="goto('Forum Admin');" class="${modeflag.get('Forum Admin')}">Forum Admin</a> |
<a href="#" onclick="goto('Enrollment');" class="${modeflag.get('Enrollment')}">Enrollment</a>
]
</h2>
<div style="text-align:right" id="djangopid">${djangopid}</div>
<div style="text-align:right"><span id="djangopid">${djangopid}</span>
| <span id="mitxver">${mitx_version}</span></div>
<form name="idashform" method="POST">
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
@@ -68,6 +71,12 @@ function goto( mode)
##-----------------------------------------------------------------------------
%if modeflag.get('Grades'):
%if offline_grade_log:
<p><font color='orange'>Pre-computed grades ${offline_grade_log} available: Use?
<input type='checkbox' name='use_offline_grades' value='yes'></font> </p>
%endif
<p>
<a href="${reverse('gradebook', kwargs=dict(course_id=course.id))}">Gradebook</a>
</p>
@@ -93,6 +102,42 @@ function goto( mode)
<p>
<input type="submit" name="action" value="Download CSV of answer distributions">
</p>
<hr width="40%" style="align:left">
%if settings.MITX_FEATURES.get('REMOTE_GRADEBOOK_URL','') and instructor_access:
<%
rg = course.metadata.get('remote_gradebook',{})
%>
<h3>Export grades to remote gradebook</h3>
<p>The assignments defined for this course should match the ones
stored in the gradebook, for this to work properly!</p>
<ul>
<li>Gradebook name: <font color="green">${rg.get('name','None defined!')}</font>
<br/>
<br/>
<input type="submit" name="action" value="List assignments available in remote gradebook">
<input type="submit" name="action" value="List enrolled students matching remote gradebook">
<br/>
<br/>
</li>
<li><input type="submit" name="action" value="List assignments available for this course">
<br/>
<br/>
</li>
<li>Assignment name: <input type="text" name="assignment_name" size=40 >
<br/>
<br/>
<input type="submit" name="action" value="Display grades for assignment">
<input type="submit" name="action" value="Export grades for assignment to remote gradebook">
<input type="submit" name="action" value="Export CSV file of grades for assignment">
</li>
</ul>
%endif
%endif
##-----------------------------------------------------------------------------
@@ -128,6 +173,16 @@ function goto( mode)
<hr width="40%" style="align:left">
%endif
%if admin_access:
<hr width="40%" style="align:left">
<p>
<input type="submit" name="action" value="List course instructors">
<p>
<input type="text" name="instructor"> <input type="submit" name="action" value="Remove instructor">
<input type="submit" name="action" value="Add instructor">
<hr width="40%" style="align:left">
%endif
%if settings.MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] and admin_access:
<p>
<input type="submit" name="action" value="Reload course from XML files">
@@ -163,10 +218,52 @@ function goto( mode)
%endif
%endif
##-----------------------------------------------------------------------------
%if modeflag.get('Enrollment'):
<hr width="40%" style="align:left">
<p>
<input type="submit" name="action" value="List enrolled students">
<input type="submit" name="action" value="List students who may enroll but may not have yet signed up">
<p>
Student Email: <input type="text" name="enstudent"> <input type="submit" name="action" value="Un-enroll student">
<input type="submit" name="action" value="Enroll student">
<input type="submit" name="action" value="Un-enroll ALL students">
<hr width="40%" style="align:left">
%if settings.MITX_FEATURES.get('REMOTE_GRADEBOOK_URL','') and instructor_access:
<%
rg = course.metadata.get('remote_gradebook',{})
%>
<p>Pull enrollment from remote gradebook</p>
<ul>
<li>Gradebook name: <font color="green">${rg.get('name','None defined!')}</font>
<li>Section: <input type="text" name="gradebook_section" size=40 value="${rg.get('section','')}"></li>
</ul>
<input type="submit" name="action" value="List sections available in remote gradebook">
<input type="submit" name="action" value="List students in section in remote gradebook">
<input type="submit" name="action" value="Overload enrollment list using remote gradebook">
<input type="submit" name="action" value="Merge enrollment list with remote gradebook">
<hr width="40%" style="align:left">
%endif
<p>Add students: enter emails, separated by returns or commas;</p>
<textarea rows="6" cols="70" name="enroll_multiple"></textarea>
<input type="submit" name="action" value="Enroll multiple students">
%endif
##-----------------------------------------------------------------------------
</form>
##-----------------------------------------------------------------------------
%if modeflag.get('Psychometrics') is None:
##-----------------------------------------------------------------------------
%if datatable and modeflag.get('Psychometrics') is None:
<br/>
<br/>

View File

@@ -0,0 +1,9 @@
<div align="center" id="${element_id}" class="${element_class}">
<!-- hidden field to read configuration json from -->
<div class="${element_class}" id="${element_id}_json" style="display: none;">
${configuration_json}
</div>
<!-- main xml with marked places for sliders, numbers, and plot -->
${gst_html}
</div>

View File

@@ -24,6 +24,8 @@
</div>
<div class="message-container">
</div>
<! -- Problem List View -->
<section class="problem-list-container">
<h2>Instructions</h2>
<div class="instructions">
@@ -35,6 +37,8 @@
</ul>
</section>
<!-- Grading View -->
<section class="prompt-wrapper">
<h2 class="prompt-name"></h2>
<div class="meta-info-wrapper">

View File

@@ -0,0 +1,31 @@
<section id="openended_${id}" class="open-ended-child" data-state="${state}" data-child-type="${child_type}">
<div class="error"></div>
<div class="prompt">
${prompt|n}
</div>
<textarea rows="${rows}" cols="${cols}" name="answer" class="answer short-form-response" id="input_${id}">${previous_answer|h}</textarea>
<div class="message-wrapper"></div>
<div class="grader-status">
% if state == 'initial':
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span>
% elif state in ['done', 'post_assessment'] and correct == 'correct':
<span class="correct" id="status_${id}">Correct</span>
% elif state in ['done', 'post_assessment'] and correct == 'incorrect':
<span class="incorrect" id="status_${id}">Incorrect</span>
% elif state == 'assessing':
<span class="grading" id="status_${id}">Submitted for grading</span>
% endif
% if hidden:
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif
</div>
<input type="button" value="Submit" class="submit-button" name="show"/>
<input name="skip" class="skip-button" type="button" value="Skip Post-Assessment"/>
<div class="open-ended-action"></div>
<span id="answer_${id}"></span>
</section>

View File

@@ -0,0 +1,23 @@
<div class="external-grader-message">
${msg|n}
<div class="collapsible evaluation-response">
<header>
<a href="#">Respond to Feedback</a>
</header>
<section id="evaluation_${id}" class="evaluation">
<p>How accurate do you find this feedback?</p>
<div class="evaluation-scoring">
<ul class="scoring-list">
<li><input type="radio" name="evaluation-score" id="evaluation-score-5" value="5" /> <label for="evaluation-score-5"> Correct</label></li>
<li><input type="radio" name="evaluation-score" id="evaluation-score-4" value="4" /> <label for="evaluation-score-4"> Partially Correct</label></li>
<li><input type="radio" name="evaluation-score" id="evaluation-score-3" value="3" /> <label for="evaluation-score-3"> No Opinion</label></li>
<li><input type="radio" name="evaluation-score" id="evaluation-score-2" value="2" /> <label for="evaluation-score-2"> Partially Incorrect</label></li>
<li><input type="radio" name="evaluation-score" id="evaluation-score-1" value="1" /> <label for="evaluation-score-1"> Incorrect</label></li>
</ul>
</div>
<p>Additional comments:</p>
<textarea rows="${rows}" cols="${cols}" name="feedback_${id}" class="feedback-on-feedback" id="feedback_${id}"></textarea>
<input type="button" value="Submit Feedback" class="submit-evaluation-button" name="reset"/>
</section>
</div>
</div>

View File

@@ -12,5 +12,6 @@
<div class="result-output">
${ feedback | n}
</div>
${rubric_feedback | n}
</div>
</section>

View File

@@ -0,0 +1,30 @@
<table class="rubric">
% for i in range(len(rubric_categories)):
<% category = rubric_categories[i] %>
<tr>
<th>
${category['description']}
% if category['has_score'] == True:
(Your score: ${category['score']})
% endif
</th>
% for j in range(len(category['options'])):
<% option = category['options'][j] %>
<td>
<div class="view-only">
${option['text']}
% if option.has_key('selected'):
% if option['selected'] == True:
<div class="selected-grade">[${option['points']} points]</div>
%else:
<div class="grade">[${option['points']} points]</div>
% endif
% else:
<div class="grade">[${option['points']} points]</div>
%endif
</div>
</td>
% endfor
</tr>
% endfor
</table>

View File

@@ -0,0 +1,39 @@
<%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} Peer Grading</title></%block>
<%include file="/courseware/course_navigation.html" args="active_page='staff_grading'" />
<%block name="js_extra">
<%static:js group='peer_grading'/>
</%block>
<section class="container">
<div class="peer-grading" data-ajax_url="${ajax_url}">
<div class="error-container">${error_text}</div>
<h1>Peer Grading</h1>
<h2>Instructions</h2>
<p>Here are a list of problems that need to be peer graded for this course.</p>
% if success:
% if len(problem_list) == 0:
<div class="message-container">
Nothing to grade!
</div>
%else:
<ul class="problem-list">
%for problem in problem_list:
<li>
<a href="${ajax_url}problem?location=${problem['location']}">${problem['problem_name']} (${problem['num_graded']} graded, ${problem['num_pending']} pending)</a>
</li>
%endfor
</ul>
%endif
%endif
</div>
</section>

View File

@@ -0,0 +1,112 @@
<%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} Peer Grading.</title></%block>
<%include file="/courseware/course_navigation.html" args="active_page='staff_grading'" />
<%block name="js_extra">
<%static:js group='peer_grading'/>
</%block>
<section class="container">
<div class="peer-grading" data-ajax_url="${ajax_url}" data-location="${problem_location}">
<div class="error-container"></div>
<section class="content-panel">
<h1>Peer Grading </h1>
<div class="instructions-panel">
<div class="calibration-panel">
<h3>Learning to Grade</h3>
<div class="calibration-text">
<p>Before you can do any proper peer grading, you first need to understand how your own grading compares to that of the instrutor. Once your grades begin to match the instructor's, you will move on to grading your peers!</p>
</div>
<div class="grading-text">
<p>You have successfully managed to calibrate your answers to that of the instructors and have moved onto the next step in the peer grading process.</p>
</div>
</div>
<div class="grading-panel">
<h3>Grading</h3>
<div class="calibration-text">
<p>You cannot start grading until you have graded a sufficient number of training problems and have been able to demonstrate that your scores closely match that of the instructor.</p>
</div>
<div class="grading-text">
<p>Now that you have finished your training, you are now allowed to grade your peers. Please keep in mind that students are allowed to respond to the grades and feedback they receive.</p>
</div>
</div>
</div>
<div class="prompt-wrapper">
<div class="prompt-information-container collapsible">
<header><a href="javascript:void(0)">Question</a></header>
<section>
<div class="prompt-container">
</div>
</section>
</div>
<div class="rubric-wrapper collapsible">
<header><a href="javascript:void(0)">Rubric</a></header>
<section>
<div class="rubric-container">
</div>
</section>
</div>
</div>
<section class="grading-wrapper">
<h2>Grading</h2>
<div class="grading-container">
<div class="submission-wrapper">
<h3></h3>
<div class="submission-container">
</div>
<input type="hidden" name="submission-key" value="" />
<input type="hidden" name="essay-id" value="" />
</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"/>
</div>
</div>
<div class="grading-message">
</div>
</section>
</section>
<!-- Calibration feedback: Shown after a calibration is sent -->
<section class="calibration-feedback">
<h2>How did I do?</h2>
<div class="calibration-feedback-wrapper">
</div>
<input type="button" class="calibration-feedback-button" value="Continue" name="calibration-feedback-button" />
</section>
<!-- Interstitial Page: Shown between calibration and grading steps -->
<section class="interstitial-page">
<h1>Congratulations!</h1>
<p> You have now completed the calibration step. You are now ready to start grading.</p>
<input type="button" class="interstitial-page-button" value="Start Grading!" name="interstitial-page-button" />
</section>
<input type="button" value="Go Back" class="action-button" name="back" />
</div>
</section>

View File

@@ -2,6 +2,6 @@
<div class="hint-prompt">
${hint_prompt}
</div>
<textarea name="hint" class="hint" cols="70" rows="5"
<textarea name="post_assessment" class="post_assessment" cols="70" rows="5"
${'readonly="true"' if read_only else ''}>${hint}</textarea>
</div>

View File

@@ -1,5 +1,5 @@
<section id="self_assessment_${id}" class="self-assessment" data-ajax-url="${ajax_url}"
data-id="${id}" data-state="${state}" data-allow_reset="${allow_reset}">
<section id="self_assessment_${id}" class="open-ended-child" data-ajax-url="${ajax_url}"
data-id="${id}" data-state="${state}" data-allow_reset="${allow_reset}" data-child-type="${child_type}">
<div class="error"></div>
<div class="prompt">
${prompt}
@@ -9,6 +9,8 @@
<textarea name="answer" class="answer short-form-response" cols="70" rows="20">${previous_answer|h}</textarea>
</div>
<div class="open-ended-action"></div>
<div class="rubric-wrapper">${initial_rubric}</div>
<div class="hint-wrapper">${initial_hint}</div>
@@ -16,5 +18,4 @@
<div class="message-wrapper">${initial_message}</div>
<input type="button" value="Submit" class="submit-button" name="show"/>
<input type="button" value="Reset" class="reset-button" name="reset"/>
</section>

View File

@@ -1,7 +1,7 @@
<div class="assessment">
<div class="rubric">
<h3>Self-assess your answer with this rubric:</h3>
${rubric}
${rubric | n }
</div>
% if not read_only:

View File

@@ -243,15 +243,32 @@ if settings.COURSEWARE_ENABLED:
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'),
'open_ended_grading.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'),
'open_ended_grading.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'),
'open_ended_grading.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'),
'open_ended_grading.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'),
'open_ended_grading.staff_grading_service.get_problem_list', name='staff_grading_get_problem_list'),
# Peer Grading
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading$',
'open_ended_grading.views.peer_grading', name='peer_grading'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/problem$',
'open_ended_grading.views.peer_grading_problem', name='peer_grading_problem'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/get_next_submission$',
'open_ended_grading.peer_grading_service.get_next_submission', name='peer_grading_get_next_submission'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/show_calibration_essay$',
'open_ended_grading.peer_grading_service.show_calibration_essay', name='peer_grading_show_calibration_essay'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/is_student_calibrated$',
'open_ended_grading.peer_grading_service.is_student_calibrated', name='peer_grading_is_student_calibrated'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/save_grade$',
'open_ended_grading.peer_grading_service.save_grade', name='peer_grading_save_grade'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/save_calibration_essay$',
'open_ended_grading.peer_grading_service.save_calibration_essay', name='peer_grading_save_calibration_essay'),
)
# discussion forums live within courseware, so courseware must be enabled first

View File

@@ -56,3 +56,4 @@ dogstatsd-python==0.2.1
sphinx==1.1.3
Shapely==1.2.16
ipython==0.13.1
xmltodict==0.4.1