diff --git a/lms/templates/graphical_slider_tool.html b/lms/templates/graphical_slider_tool.html new file mode 100644 index 0000000000..93a6761784 --- /dev/null +++ b/lms/templates/graphical_slider_tool.html @@ -0,0 +1,9 @@ +
diff --git a/common/djangoapps/student/admin.py b/common/djangoapps/student/admin.py
index ec3b708ca7..64fe844801 100644
--- a/common/djangoapps/student/admin.py
+++ b/common/djangoapps/student/admin.py
@@ -12,6 +12,8 @@ admin.site.register(UserTestGroup)
admin.site.register(CourseEnrollment)
+admin.site.register(CourseEnrollmentAllowed)
+
admin.site.register(Registration)
admin.site.register(PendingNameChange)
diff --git a/common/djangoapps/student/migrations/0021_auto__add_courseenrollmentallowed__add_unique_courseenrollmentallowed_.py b/common/djangoapps/student/migrations/0021_auto__add_courseenrollmentallowed__add_unique_courseenrollmentallowed_.py
new file mode 100644
index 0000000000..f7e2571685
--- /dev/null
+++ b/common/djangoapps/student/migrations/0021_auto__add_courseenrollmentallowed__add_unique_courseenrollmentallowed_.py
@@ -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']
\ No newline at end of file
diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index cf7bc7696a..5311e49844 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -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.
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 06c59d7937..39805fd85f 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -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
diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py
index efc96fc717..2eaa0e4286 100644
--- a/common/lib/capa/capa/capa_problem.py
+++ b/common/lib/capa/capa/capa_problem.py
@@ -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.
diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py
index e3eb47acc5..1d3646fefc 100644
--- a/common/lib/capa/capa/inputtypes.py
+++ b/common/lib/capa/capa/inputtypes.py
@@ -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)
-
-#-----------------------------------------------------------------------------
diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py
index 1bc34b70a3..3d97cb0bea 100644
--- a/common/lib/capa/capa/responsetypes.py
+++ b/common/lib/capa/capa/responsetypes.py
@@ -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(' ', ' '), 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 = '
'.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= """
- {0}
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 We can request the API to plot us a bar graph. a b There are two kinds of dynamic lables.
+ 1) Dynamic changing values in graph legends.
+ 2) Dynamic labels, which coordinates depend on parameters a: b: 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. Also implicit functons like circle can be plotted as 2 separate
+ functions of same color. a + b = a b
+ 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.
+ Currently \(a\) is This one
+ \(
+ y_2 = sin(a \times x)
+ \)
+ will be overlayed on top.
+ Currently \(b\) is To change \(a\) use: To change \(b\) use: Second input for b: Error: {0} Shown below are schematic band diagrams for two different metals. Both diagrams appear different, yet both of the elements are undisputably metallic in nature. * Why is it that both sodium and magnesium behave as metals, even though the s-band of magnesium is filled? This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question. Please score your response according to how many of the above components you identified: Shown below are schematic band diagrams for two different metals. Both diagrams appear different, yet both of the elements are undisputably metallic in nature. * Why is it that both sodium and magnesium behave as metals, even though the s-band of magnesium is filled? This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question. Please score your response according to how many of the above components you identified: Grade sent successfully. #{paragraph} The score you gave was: #{@score}. The actual score is: #{response.actual_score} Congratulations! Your score matches the actual score! Please try to understand the grading critera better to be more accurate next time. Here are a list of problems that need to be peer graded for this course. 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! 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. 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. 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.
+ You have now completed the calibration step. You are now ready to start grading.
'.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 = """
+ {0}Graphic slider tool: Bar graph example.
+
+
+ Graphic slider tool: Dynamic labels.
+
+
+ Graphic slider tool: Dynamic range and implicit functions.
+
+ Graphic slider tool: Output to DOM element.
+
+
+ Graphic slider tool: full example.
+ {0} '.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'])
-
-
diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py
index 79cf0caaf3..2d58799efe 100644
--- a/lms/djangoapps/instructor/views.py
+++ b/lms/djangoapps/instructor/views.py
@@ -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 += '%s
' % 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 'yes' if x.email in rg_students else 'No'
+ 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 += "Please enter an assignment name"
+ 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 += "Invalid assignment name '%s'" % 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 += 'Error: unknown username "{0}"'.format(uname)
+ user = None
+ if user is not None:
+ group = get_instructor_group(course)
+ msg += 'Added {0} to instructor group = {1}'.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 += 'Error: unknown username "{0}"'.format(uname)
+ user = None
+ if user is not None:
+ group = get_instructor_group(course)
+ msg += 'Removed {0} from instructor group = {1}'.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 += "
Grades from %s" % 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
" % rgurl
+ msg += "Error: %s" % err
+ msg += "
resp=%s" % resp.content
+ msg += "
data=%s" % data
+ return msg, {}
+
+ msg = '%s
' % retdict['msg'].replace('\n','
')
+ 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.
diff --git a/lms/djangoapps/open_ended_grading/__init__.py b/lms/djangoapps/open_ended_grading/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/lms/djangoapps/open_ended_grading/grading_service.py b/lms/djangoapps/open_ended_grading/grading_service.py
new file mode 100644
index 0000000000..7362411daa
--- /dev/null
+++ b/lms/djangoapps/open_ended_grading/grading_service.py
@@ -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
+
diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py
new file mode 100644
index 0000000000..9ef0383fb5
--- /dev/null
+++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py
@@ -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')
diff --git a/lms/djangoapps/instructor/grading.py b/lms/djangoapps/open_ended_grading/staff_grading.py
similarity index 100%
rename from lms/djangoapps/instructor/grading.py
rename to lms/djangoapps/open_ended_grading/staff_grading.py
diff --git a/lms/djangoapps/instructor/staff_grading_service.py b/lms/djangoapps/open_ended_grading/staff_grading_service.py
similarity index 71%
rename from lms/djangoapps/instructor/staff_grading_service.py
rename to lms/djangoapps/open_ended_grading/staff_grading_service.py
index ea8f0de074..5c6cec17eb 100644
--- a/lms/djangoapps/instructor/staff_grading_service.py
+++ b/lms/djangoapps/open_ended_grading/staff_grading_service.py
@@ -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'],
diff --git a/lms/djangoapps/open_ended_grading/tests.py b/lms/djangoapps/open_ended_grading/tests.py
new file mode 100644
index 0000000000..0c4376a44b
--- /dev/null
+++ b/lms/djangoapps/open_ended_grading/tests.py
@@ -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'])
diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py
new file mode 100644
index 0000000000..858c9a4fd5
--- /dev/null
+++ b/lms/djangoapps/open_ended_grading/views.py
@@ -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, })
+
+
diff --git a/lms/envs/aws.py b/lms/envs/aws.py
index 0516bddc56..7b8c48f4af 100644
--- a/lms/envs/aws.py
+++ b/lms/envs/aws.py
@@ -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")
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 1607e5e36c..6790e5b714 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -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',
diff --git a/lms/envs/dev.py b/lms/envs/dev.py
index 0ad42f67d3..f5999bf52e 100644
--- a/lms/envs/dev.py
+++ b/lms/envs/dev.py
@@ -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
diff --git a/lms/envs/test.py b/lms/envs/test.py
index c72c8b98bf..e9e4a43c6f 100644
--- a/lms/envs/test.py
+++ b/lms/envs/test.py
@@ -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
diff --git a/lms/static/coffee/src/peer_grading/peer_grading.coffee b/lms/static/coffee/src/peer_grading/peer_grading.coffee
new file mode 100644
index 0000000000..0736057df8
--- /dev/null
+++ b/lms/static/coffee/src/peer_grading/peer_grading.coffee
@@ -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())
diff --git a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee
new file mode 100644
index 0000000000..e294c50f7c
--- /dev/null
+++ b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee
@@ -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: '''
+ S11E3: Metal Bands
+
+
+
+S11E3: Metal Bands
+
+
+
+Training Essay
")
+ @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("Submitted Essay
")
+ @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 += "Status
+ ${status | n}
+ Problem
+ % for item in items:
+ Results from Step ${task_number}
+ ${results | n}
+
diff --git a/lms/templates/graphical_slider_tool.html b/lms/templates/graphical_slider_tool.html
new file mode 100644
index 0000000000..93a6761784
--- /dev/null
+++ b/lms/templates/graphical_slider_tool.html
@@ -0,0 +1,9 @@
+Instructions
+ % for i in range(len(rubric_categories)):
+ <% category = rubric_categories[i] %>
+
\ No newline at end of file
diff --git a/lms/templates/peer_grading/peer_grading.html b/lms/templates/peer_grading/peer_grading.html
new file mode 100644
index 0000000000..484bb94182
--- /dev/null
+++ b/lms/templates/peer_grading/peer_grading.html
@@ -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">
+
+ % endfor
+
+ ${category['description']}
+ % if category['has_score'] == True:
+ (Your score: ${category['score']})
+ % endif
+
+ % for j in range(len(category['options'])):
+ <% option = category['options'][j] %>
+
+
+ % endfor
+ Peer Grading
+ Instructions
+
+ %for problem in problem_list:
+
+ %endif
+ %endif
+ Peer Grading
+ Learning to Grade
+ Grading
+ Grading
+
+ How did I do?
+ Congratulations!
+ Self-assess your answer with this rubric:
- ${rubric}
+ ${rubric | n }