Merge pull request #1516 from MITx/hack/dave/submission_history
Record and report submission history for a problem
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
# -*- 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 'StudentModuleHistory'
|
||||
db.create_table('courseware_studentmodulehistory', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('student_module', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['courseware.StudentModule'])),
|
||||
('version', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
|
||||
('created', self.gf('django.db.models.fields.DateTimeField')(db_index=True)),
|
||||
('state', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
|
||||
('grade', self.gf('django.db.models.fields.FloatField')(null=True, blank=True)),
|
||||
('max_grade', self.gf('django.db.models.fields.FloatField')(null=True, blank=True)),
|
||||
))
|
||||
db.send_create_signal('courseware', ['StudentModuleHistory'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'StudentModuleHistory'
|
||||
db.delete_table('courseware_studentmodulehistory')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'courseware.offlinecomputedgrade': {
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'OfflineComputedGrade'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'gradeset': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'courseware.offlinecomputedgradelog': {
|
||||
'Meta': {'ordering': "['-created']", 'object_name': 'OfflineComputedGradeLog'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'nstudents': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'seconds': ('django.db.models.fields.IntegerField', [], {'default': '0'})
|
||||
},
|
||||
'courseware.studentmodule': {
|
||||
'Meta': {'unique_together': "(('student', 'module_state_key', 'course_id'),)", 'object_name': 'StudentModule'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'done': ('django.db.models.fields.CharField', [], {'default': "'na'", 'max_length': '8', 'db_index': 'True'}),
|
||||
'grade': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'max_grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'module_state_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'module_id'", 'db_index': 'True'}),
|
||||
'module_type': ('django.db.models.fields.CharField', [], {'default': "'problem'", 'max_length': '32', 'db_index': 'True'}),
|
||||
'state': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'courseware.studentmodulehistory': {
|
||||
'Meta': {'object_name': 'StudentModuleHistory'},
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
|
||||
'grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'max_grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'state': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'student_module': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['courseware.StudentModule']"}),
|
||||
'version': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['courseware']
|
||||
@@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
# Changing field 'StudentModuleHistory.version'
|
||||
db.alter_column('courseware_studentmodulehistory', 'version', self.gf('django.db.models.fields.CharField')(max_length=255, null=True))
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
# User chose to not deal with backwards NULL issues for 'StudentModuleHistory.version'
|
||||
raise RuntimeError("Cannot reverse this migration. 'StudentModuleHistory.version' and its values cannot be restored.")
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'courseware.offlinecomputedgrade': {
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'OfflineComputedGrade'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'gradeset': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'courseware.offlinecomputedgradelog': {
|
||||
'Meta': {'ordering': "['-created']", 'object_name': 'OfflineComputedGradeLog'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'nstudents': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'seconds': ('django.db.models.fields.IntegerField', [], {'default': '0'})
|
||||
},
|
||||
'courseware.studentmodule': {
|
||||
'Meta': {'unique_together': "(('student', 'module_state_key', 'course_id'),)", 'object_name': 'StudentModule'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'done': ('django.db.models.fields.CharField', [], {'default': "'na'", 'max_length': '8', 'db_index': 'True'}),
|
||||
'grade': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'max_grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'module_state_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'module_id'", 'db_index': 'True'}),
|
||||
'module_type': ('django.db.models.fields.CharField', [], {'default': "'problem'", 'max_length': '32', 'db_index': 'True'}),
|
||||
'state': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'courseware.studentmodulehistory': {
|
||||
'Meta': {'object_name': 'StudentModuleHistory'},
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
|
||||
'grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'max_grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'state': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'student_module': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['courseware.StudentModule']"}),
|
||||
'version': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'db_index': 'True'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['courseware']
|
||||
@@ -12,8 +12,10 @@ file and check it in at the same time as your model changes. To do that,
|
||||
ASSUMPTIONS: modules have unique IDs, even across different module_types
|
||||
|
||||
"""
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
class StudentModule(models.Model):
|
||||
"""
|
||||
@@ -60,6 +62,37 @@ class StudentModule(models.Model):
|
||||
self.student.username, self.module_state_key, str(self.state)[:20]])
|
||||
|
||||
|
||||
class StudentModuleHistory(models.Model):
|
||||
"""Keeps a complete history of state changes for a given XModule for a given
|
||||
Student. Right now, we restrict this to problems so that the table doesn't
|
||||
explode in size."""
|
||||
|
||||
HISTORY_SAVING_TYPES = {'problem'}
|
||||
|
||||
class Meta:
|
||||
get_latest_by = "created"
|
||||
|
||||
student_module = models.ForeignKey(StudentModule, db_index=True)
|
||||
version = models.CharField(max_length=255, null=True, blank=True, db_index=True)
|
||||
|
||||
# This should be populated from the modified field in StudentModule
|
||||
created = models.DateTimeField(db_index=True)
|
||||
state = models.TextField(null=True, blank=True)
|
||||
grade = models.FloatField(null=True, blank=True)
|
||||
max_grade = models.FloatField(null=True, blank=True)
|
||||
|
||||
@receiver(post_save, sender=StudentModule)
|
||||
def save_history(sender, instance, **kwargs):
|
||||
if instance.module_type in StudentModuleHistory.HISTORY_SAVING_TYPES:
|
||||
history_entry = StudentModuleHistory(student_module=instance,
|
||||
version=None,
|
||||
created=instance.modified,
|
||||
state=instance.state,
|
||||
grade=instance.grade,
|
||||
max_grade=instance.max_grade)
|
||||
history_entry.save()
|
||||
|
||||
|
||||
# TODO (cpennington): Remove these once the LMS switches to using XModuleDescriptors
|
||||
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@ from functools import partial
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.context_processors import csrf
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import redirect
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
#from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
@@ -20,7 +21,7 @@ from courseware.access import has_access
|
||||
from courseware.courses import (get_courses, get_course_with_access,
|
||||
get_courses_by_university, sort_by_announcement)
|
||||
import courseware.tabs as tabs
|
||||
from courseware.models import StudentModule, StudentModuleCache
|
||||
from courseware.models import StudentModule, StudentModuleCache, StudentModuleHistory
|
||||
from module_render import toc_for_course, get_module, get_instance_module, get_module_for_descriptor
|
||||
|
||||
from django_comment_client.utils import get_discussion_title
|
||||
@@ -608,3 +609,48 @@ def progress(request, course_id, student_id=None):
|
||||
context.update()
|
||||
|
||||
return render_to_response('courseware/progress.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def submission_history(request, course_id, student_username, location):
|
||||
"""Render an HTML fragment (meant for inclusion elsewhere) that renders a
|
||||
history of all state changes made by this user for this problem location.
|
||||
Right now this only works for problems because that's all
|
||||
StudentModuleHistory records.
|
||||
"""
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
staff_access = has_access(request.user, course, 'staff')
|
||||
|
||||
# Permission Denied if they don't have staff access and are trying to see
|
||||
# somebody else's submission history.
|
||||
if (student_username != request.user.username) and (not staff_access):
|
||||
raise PermissionDenied
|
||||
|
||||
try:
|
||||
student = User.objects.get(username=student_username)
|
||||
student_module = StudentModule.objects.get(course_id=course_id,
|
||||
module_state_key=location,
|
||||
student_id=student.id)
|
||||
except User.DoesNotExist:
|
||||
return HttpResponse("User {0} does not exist.".format(student_username))
|
||||
except StudentModule.DoesNotExist:
|
||||
return HttpResponse("{0} has never accessed problem {1}"
|
||||
.format(student_username, location))
|
||||
|
||||
history_entries = StudentModuleHistory.objects \
|
||||
.filter(student_module=student_module).order_by('-created')
|
||||
|
||||
# If no history records exist, let's force a save to get history started.
|
||||
if not history_entries:
|
||||
student_module.save()
|
||||
history_entries = StudentModuleHistory.objects \
|
||||
.filter(student_module=student_module).order_by('-created')
|
||||
|
||||
context = {
|
||||
'history_entries': history_entries,
|
||||
'username': student.username,
|
||||
'location': location,
|
||||
'course_id': course_id
|
||||
}
|
||||
|
||||
return render_to_response('courseware/submission_history.html', context)
|
||||
|
||||
@@ -83,6 +83,10 @@ MITX_FEATURES = {
|
||||
|
||||
# Flip to True when the YouTube iframe API breaks (again)
|
||||
'USE_YOUTUBE_OBJECT_API': False,
|
||||
|
||||
# Give a UI to show a student's submission history in a problem by the
|
||||
# Staff Debug tool.
|
||||
'ENABLE_STUDENT_HISTORY_VIEW': True
|
||||
}
|
||||
|
||||
# Used for A/B testing
|
||||
|
||||
@@ -57,6 +57,8 @@
|
||||
border: 1px solid rgba(0, 0, 0, 0.9);
|
||||
@include box-shadow(inset 0 1px 0 0 rgba(255, 255, 255, 0.7));
|
||||
overflow: hidden;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-bottom: 30px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
13
lms/templates/courseware/submission_history.html
Normal file
13
lms/templates/courseware/submission_history.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<% import json %>
|
||||
<h3>${username} > ${course_id} > ${location}</h3>
|
||||
|
||||
% for i, entry in enumerate(history_entries):
|
||||
<hr/>
|
||||
<div>
|
||||
<b>#${len(history_entries) - i}</b>: ${entry.created} UTC</br>
|
||||
Score: ${entry.grade} / ${entry.max_grade}
|
||||
<pre>
|
||||
${json.dumps(json.loads(entry.state), indent=2, sort_keys=True) | h}
|
||||
</pre>
|
||||
</div>
|
||||
% endfor
|
||||
@@ -5,6 +5,27 @@ function setup_debug(element_id, edit_link, staff_context){
|
||||
$('#' + element_id + '_trig').leanModal();
|
||||
$('#' + element_id + '_xqa_log').leanModal();
|
||||
$('#' + element_id + '_xqa_form').submit(function () {sendlog(element_id, edit_link, staff_context);});
|
||||
|
||||
$("#" + element_id + "_history_trig").leanModal();
|
||||
|
||||
$('#' + element_id + '_history_form').submit(
|
||||
function () {
|
||||
var username = $("#" + element_id + "_history_student_username").val();
|
||||
var location = $("#" + element_id + "_history_location").val();
|
||||
|
||||
// This is a ridiculous way to get the course_id, but I'm not sure
|
||||
// how to do it sensibly from within the staff debug code.
|
||||
// staff_problem_info.html is rendered through a wrapper to get_html
|
||||
// that's injected by the code that adds the histogram -- it's all
|
||||
// kinda bizarre, and it remains awkward to simply ask "what course
|
||||
// is this problem being shown in the context of."
|
||||
var path_parts = window.location.pathname.split('/');
|
||||
var course_id = path_parts[2] + "/" + path_parts[3] + "/" + path_parts[4];
|
||||
$("#" + element_id + "_history_text").load('/courses/' + course_id +
|
||||
"/submission_history/" + username + "/" + location);
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function sendlog(element_id, edit_link, staff_context){
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
## The JS for this is defined in xqa_interface.html
|
||||
${module_content}
|
||||
%if location.category in ['problem','video','html']:
|
||||
% if edit_link:
|
||||
@@ -13,6 +14,11 @@ ${module_content}
|
||||
% endif
|
||||
<div><a href="#${element_id}_debug" id="${element_id}_trig">Staff Debug Info</a></div>
|
||||
|
||||
% if settings.MITX_FEATURES.get('ENABLE_STUDENT_HISTORY_VIEW') and \
|
||||
location.category == 'problem':
|
||||
<div><a href="#${element_id}_history" id="${element_id}_history_trig">Submission history</a></div>
|
||||
% endif
|
||||
|
||||
<section id="${element_id}_xqa-modal" class="modal xqa-modal" style="width:80%; left:20%; height:80%; overflow:auto" >
|
||||
<div class="inner-wrapper">
|
||||
<header>
|
||||
@@ -57,8 +63,26 @@ category = ${category | h}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="${element_id}_setup"></div>
|
||||
<section class="modal history-modal" id="${element_id}_history" style="width:80%; left:20%; height:80%; overflow:auto;" >
|
||||
<div class="inner-wrapper" style="color:black">
|
||||
<header>
|
||||
<h2>Submission History Viewer</h2>
|
||||
</header>
|
||||
<form id="${element_id}_history_form">
|
||||
<label for="${element_id}_history_student_username">User:</label>
|
||||
<input id="${element_id}_history_student_username" type="text" placeholder=""/>
|
||||
<input type="hidden" id="${element_id}_history_location" value="${location}"/>
|
||||
<div class="submit">
|
||||
<button name="submit" type="submit">View History</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="${element_id}_history_text" class="staff_info" style="display:block">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="${element_id}_setup"></div>
|
||||
|
||||
<script type="text/javascript">
|
||||
// assumes courseware.html's loaded this method.
|
||||
|
||||
@@ -360,7 +360,6 @@ if settings.COURSEWARE_ENABLED:
|
||||
|
||||
# discussion forums live within courseware, so courseware must be enabled first
|
||||
if settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
|
||||
|
||||
urlpatterns += (
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/news$',
|
||||
'courseware.views.news', name="news"),
|
||||
@@ -373,6 +372,14 @@ if settings.COURSEWARE_ENABLED:
|
||||
'courseware.views.static_tab', name="static_tab"),
|
||||
)
|
||||
|
||||
if settings.MITX_FEATURES.get('ENABLE_STUDENT_HISTORY_VIEW'):
|
||||
urlpatterns += (
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/submission_history/(?P<student_username>[^/]*)/(?P<location>.*?)$',
|
||||
'courseware.views.submission_history',
|
||||
name='submission_history'),
|
||||
)
|
||||
|
||||
|
||||
if settings.ENABLE_JASMINE:
|
||||
urlpatterns += (url(r'^_jasmine/', include('django_jasmine.urls')),)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user