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
8f17e6ae
Commit
8f17e6ae
authored
Feb 15, 2013
by
David Ormsbee
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
First pass at implementing problem history.
parent
f9664284
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
362 additions
and
5 deletions
+362
-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
+32
-1
lms/djangoapps/courseware/views.py
+47
-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
+24
-1
lms/urls.py
+8
-1
No files found.
lms/djangoapps/courseware/migrations/0006_create_student_module_history.py
0 → 100644
View file @
8f17e6ae
# -*- 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 @
8f17e6ae
# -*- 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 @
8f17e6ae
...
...
@@ -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,35 @@ 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."""
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
==
'problem'
:
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 @
8f17e6ae
...
...
@@ -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
...
...
@@ -606,3 +607,47 @@ 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.
"""
# Make sure our has_access check uses the course_id, eh? or is ourself
course
=
get_course_with_access
(
request
.
user
,
course_id
,
'load'
)
staff_access
=
has_access
(
request
.
user
,
course
,
'staff'
)
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 @
8f17e6ae
...
...
@@ -84,6 +84,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 @
8f17e6ae
...
...
@@ -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 @
8f17e6ae
<
%
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} EST
</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 @
8f17e6ae
...
...
@@ -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 @
8f17e6ae
## 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,10 @@ ${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'):
<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 +62,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 @
8f17e6ae
...
...
@@ -307,7 +307,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"
),
...
...
@@ -320,6 +319,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