Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
b0afbba5
Commit
b0afbba5
authored
Mar 04, 2013
by
David Ormsbee
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1516 from MITx/hack/dave/submission_history
Record and report submission history for a problem
parents
c0bb4ee2
88a30cb7
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
366 additions
and
5 deletions
+366
-5
lms/djangoapps/courseware/migrations/0006_create_student_module_history.py
+110
-0
lms/djangoapps/courseware/migrations/0007_allow_null_version_in_history.py
+101
-0
lms/djangoapps/courseware/models.py
+34
-1
lms/djangoapps/courseware/views.py
+48
-2
lms/envs/common.py
+4
-0
lms/static/sass/shared/_modal.scss
+2
-0
lms/templates/courseware/submission_history.html
+13
-0
lms/templates/courseware/xqa_interface.html
+21
-0
lms/templates/staff_problem_info.html
+25
-1
lms/urls.py
+8
-1
No files found.
lms/djangoapps/courseware/migrations/0006_create_student_module_history.py
0 → 100644
View file @
b0afbba5
# -*- 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'
]
\ No newline at end of file
lms/djangoapps/courseware/migrations/0007_allow_null_version_in_history.py
0 → 100644
View file @
b0afbba5
# -*- 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'
]
\ No newline at end of file
lms/djangoapps/courseware/models.py
View file @
b0afbba5
...
...
@@ -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
...
...
lms/djangoapps/courseware/views.py
View file @
b0afbba5
...
...
@@ -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
,
HttpResponse
Redirect
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
)
lms/envs/common.py
View file @
b0afbba5
...
...
@@ -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
...
...
lms/static/sass/shared/_modal.scss
View file @
b0afbba5
...
...
@@ -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
;
...
...
lms/templates/courseware/submission_history.html
0 → 100644
View file @
b0afbba5
<
%
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
lms/templates/courseware/xqa_interface.html
View file @
b0afbba5
...
...
@@ -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
){
...
...
lms/templates/staff_problem_info.html
View file @
b0afbba5
## 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.
...
...
lms/urls.py
View file @
b0afbba5
...
...
@@ -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'
)),)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment