Commit 3f5924a3 by Will Daly

Course staff can see counts

parent c2509762
...@@ -64,6 +64,25 @@ ...@@ -64,6 +64,25 @@
</li> </li>
{% endfor %} {% endfor %}
</ol> </ol>
{% if is_course_staff %}
<div>
<h2>Course Staff Debug Information</h2>
<p><b>Total number of submissions</b>: {{ num_submissions }}</p>
<table border="1">
<tr>
<th>Step</th>
<th>Number of students</th>
</tr>
{% for item in status_counts %}
<tr>
<td>{{ item.status }}</td>
<td>{{ item.count }}</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
......
...@@ -292,6 +292,36 @@ def update_from_assessments(submission_uuid, assessment_requirements): ...@@ -292,6 +292,36 @@ def update_from_assessments(submission_uuid, assessment_requirements):
return _serialized_with_details(workflow, assessment_requirements) return _serialized_with_details(workflow, assessment_requirements)
def get_status_counts(**kwargs):
"""
Count how many workflows have each status, for a given item in a course.
Kwargs:
course_id (unicode): The ID of the course.
item_id (unicode): The ID of the item in the course.
item_type (unicode): The type of the item.
Returns:
list of dictionaries with keys "status" (str) and "count" (int)
Example usage:
>>> get_status_counts("ora2/1/1", "peer-assessment-problem")
[
{"status": "peer", "count": 5},
{"status": "self", "count": 10},
{"status": "waiting", "count": 43},
{"status": "done", "count": 12},
]
"""
submission_uuids = sub_api.get_submission_uuids(**kwargs)
workflows = AssessmentWorkflow.objects.filter(submission_uuid__in=submission_uuids)
return [
{"status": status, "count": workflows.filter(status=status).count()}
for status in AssessmentWorkflow.STATUS_VALUES
]
def _get_workflow_model(submission_uuid): def _get_workflow_model(submission_uuid):
"""Return the `AssessmentWorkflow` model for a given `submission_uuid`. """Return the `AssessmentWorkflow` model for a given `submission_uuid`.
......
...@@ -30,14 +30,16 @@ class AssessmentWorkflow(TimeStampedModel, StatusModel): ...@@ -30,14 +30,16 @@ class AssessmentWorkflow(TimeStampedModel, StatusModel):
an after the fact recording of the last known state of that information so an after the fact recording of the last known state of that information so
we can search easily. we can search easily.
""" """
STATUS = Choices( # implicit "status" field STATUS_VALUES = [
"peer", # User needs to assess peer submissions "peer", # User needs to assess peer submissions
"self", # User needs to assess themselves "self", # User needs to assess themselves
"waiting", # User has done all necessary assessment but hasn't been "waiting", # User has done all necessary assessment but hasn't been
# graded yet -- we're waiting for assessments of their # graded yet -- we're waiting for assessments of their
# submission by others. # submission by others.
"done", # Complete "done", # Complete
) ]
STATUS = Choices(*STATUS_VALUES) # implicit "status" field
submission_uuid = models.CharField(max_length=36, db_index=True, unique=True) submission_uuid = models.CharField(max_length=36, db_index=True, unique=True)
uuid = UUIDField(version=1, db_index=True, unique=True) uuid = UUIDField(version=1, db_index=True, unique=True)
......
...@@ -81,3 +81,68 @@ class TestAssessmentWorkflowApi(TestCase): ...@@ -81,3 +81,68 @@ class TestAssessmentWorkflowApi(TestCase):
submission = sub_api.create_submission(ITEM_1, "We talk TV!") submission = sub_api.create_submission(ITEM_1, "We talk TV!")
workflow = workflow_api.create_workflow(submission["uuid"]) workflow = workflow_api.create_workflow(submission["uuid"])
workflow_api.get_workflow_for_submission(workflow["uuid"], REQUIREMENTS) workflow_api.get_workflow_for_submission(workflow["uuid"], REQUIREMENTS)
def test_get_status_counts(self):
# Initially, the counts should all be zero
counts = workflow_api.get_status_counts()
self.assertEqual(counts, [
{"status": "peer", "count": 0},
{"status": "self", "count": 0},
{"status": "waiting", "count": 0},
{"status": "done", "count": 0},
])
# Create assessments with each status
# We're going to cheat a little bit by using the model objects
# directly, since the API does not provide access to the status directly.
self._create_workflow_with_status("user 1", "test/1/1", "peer-problem", "peer")
self._create_workflow_with_status("user 2", "test/1/1", "peer-problem", "self")
self._create_workflow_with_status("user 3", "test/1/1", "peer-problem", "self")
self._create_workflow_with_status("user 4", "test/1/1", "peer-problem", "waiting")
self._create_workflow_with_status("user 5", "test/1/1", "peer-problem", "waiting")
self._create_workflow_with_status("user 6", "test/1/1", "peer-problem", "waiting")
self._create_workflow_with_status("user 7", "test/1/1", "peer-problem", "done")
self._create_workflow_with_status("user 8", "test/1/1", "peer-problem", "done")
self._create_workflow_with_status("user 9", "test/1/1", "peer-problem", "done")
self._create_workflow_with_status("user 10", "test/1/1", "peer-problem", "done")
# Now the counts should be updated
counts = workflow_api.get_status_counts()
self.assertEqual(counts, [
{"status": "peer", "count": 1},
{"status": "self", "count": 2},
{"status": "waiting", "count": 3},
{"status": "done", "count": 4},
])
def _create_workflow_with_status(
self, student_id, course_id, item_id, status,
item_type="openassessment", answer="answer"
):
"""
Create a submission and workflow with a given status.
Args:
student_id (unicode): Student ID for the submission.
course_id (unicode): Course ID for the submission.
item_id (unicode): Item ID for the submission
status (unicode): One of acceptable status values (e.g. "peer", "self", "waiting", "done")
Kwargs:
item_type (unicode): Type of item for the submission.
answer (unicode): Submission answer.
Returns:
None
"""
submission = sub_api.create_submission({
"student_id": student_id,
"course_id": course_id,
"item_id": item_id,
"item_type": item_type
}, answer)
AssessmentWorkflow.objects.create(
submission_uuid=submission['uuid'],
status=status
)
...@@ -290,8 +290,15 @@ class OpenAssessmentBlock( ...@@ -290,8 +290,15 @@ class OpenAssessmentBlock(
"question": self.prompt, "question": self.prompt,
"rubric_criteria": self.rubric_criteria, "rubric_criteria": self.rubric_criteria,
"rubric_assessments": ui_models, "rubric_assessments": ui_models,
"is_course_staff": False,
} }
if self.is_course_staff:
status_counts, num_submissions = self.get_workflow_status_counts()
context_dict['is_course_staff'] = True
context_dict['status_counts'] = status_counts
context_dict['num_submissions'] = num_submissions
template = get_template("openassessmentblock/oa_base.html") template = get_template("openassessmentblock/oa_base.html")
context = Context(context_dict) context = Context(context_dict)
frag = Fragment(template.render(context)) frag = Fragment(template.render(context))
...@@ -300,6 +307,19 @@ class OpenAssessmentBlock( ...@@ -300,6 +307,19 @@ class OpenAssessmentBlock(
frag.initialize_js('OpenAssessmentBlock') frag.initialize_js('OpenAssessmentBlock')
return frag return frag
@property
def is_course_staff(self):
"""
Check whether the user has course staff permissions for this XBlock.
Returns:
bool
"""
if hasattr(self, 'xmodule_runtime'):
return getattr(self.xmodule_runtime, 'user_is_staff', False)
else:
return False
def _create_ui_models(self): def _create_ui_models(self):
"""Combine UI attributes and XBlock configuration into a UI model. """Combine UI attributes and XBlock configuration into a UI model.
......
...@@ -333,6 +333,37 @@ class TestDates(XBlockHandlerTestCase): ...@@ -333,6 +333,37 @@ class TestDates(XBlockHandlerTestCase):
# If the runtime doesn't provide a published_date field, assume we've been published # If the runtime doesn't provide a published_date field, assume we've been published
self.assertTrue(xblock.is_released()) self.assertTrue(xblock.is_released())
@scenario('data/basic_scenario.xml')
def test_is_course_staff(self, xblock):
# By default, we shouldn't be course staff
self.assertFalse(xblock.is_course_staff)
# If the LMS runtime tells us we're not course staff,
# we shouldn't be course staff.
xblock.xmodule_runtime = Mock(user_is_staff=False)
self.assertFalse(xblock.is_course_staff)
# If the LMS runtime tells us that we ARE course staff,
# then we're course staff.
xblock.xmodule_runtime.user_is_staff = True
self.assertTrue(xblock.is_course_staff)
@scenario('data/basic_scenario.xml')
def test_course_staff_debug_info(self, xblock):
# If we're not course staff, we shouldn't see the debug info
xblock.xmodule_runtime = Mock(
course_id='test_course',
anonymous_student_id='test_student',
user_is_staff=False
)
xblock_fragment = self.runtime.render(xblock, "student_view")
self.assertNotIn("course staff debug", xblock_fragment.body_html().lower())
# If we ARE course staff, then we should see the debug info
xblock.xmodule_runtime.user_is_staff = True
xblock_fragment = self.runtime.render(xblock, "student_view")
self.assertIn("course staff debug", xblock_fragment.body_html().lower())
def assert_is_open(self, xblock, now, step, expected_is_open, expected_reason, released=None): def assert_is_open(self, xblock, now, step, expected_is_open, expected_reason, released=None):
""" """
Assert whether the XBlock step is open/closed. Assert whether the XBlock step is open/closed.
......
...@@ -67,3 +67,33 @@ class WorkflowMixin(object): ...@@ -67,3 +67,33 @@ class WorkflowMixin(object):
return workflow_api.get_workflow_for_submission( return workflow_api.get_workflow_for_submission(
self.submission_uuid, self.workflow_requirements() self.submission_uuid, self.workflow_requirements()
) )
def get_workflow_status_counts(self):
"""
Retrieve the counts of students in each step of the workflow.
Returns:
tuple of (list, int), where the list contains dicts with keys
"status" (unicode value) and "count" (int value), and the
integer represents the total number of submissions.
Example Usage:
>>> status_counts, num_submissions = xblock.get_workflow_status_counts()
>>> num_submissions
12
>>> status_counts
[
{"status": "peer", "count": 2},
{"status": "self", "count": 1},
{"status": "waiting": "count": 4},
{"status": "done", "count": 5}
]
"""
student_item = self.get_student_item_dict()
status_counts = workflow_api.get_status_counts(
course_id=student_item['course_id'],
item_id=student_item['item_id'],
item_type=student_item['item_type'],
)
num_submissions = sum(item['count'] for item in status_counts)
return status_counts, num_submissions
...@@ -4,6 +4,7 @@ Public interface for the submissions app. ...@@ -4,6 +4,7 @@ Public interface for the submissions app.
""" """
import copy import copy
import logging import logging
from collections import defaultdict
from django.core.cache import cache from django.core.cache import cache
from django.db import IntegrityError, DatabaseError from django.db import IntegrityError, DatabaseError
...@@ -465,6 +466,60 @@ def set_score(submission_uuid, score, points_possible): ...@@ -465,6 +466,60 @@ def set_score(submission_uuid, score, points_possible):
pass pass
def get_submission_uuids(course_id=None, item_id=None, item_type=None):
"""
Return a list of submission UUIDs that have a particular course_id, item_id, and/or item_type.
Kwargs:
course_id (unicode): The ID of the course.
item_id (unicode): The ID of the item in the course.
item_type (unicode): The type of the item.
Returns:
list of submission UUIDs
Example Usage:
>>> get_submissions_for_course_item(course_id="ora2/1/1", item_id="peer-assessment-problem")
[
'de97b11169ab4761b80b56cc42f6cb4a',
'bcb9fcae309a4628859c72f1d33eb89c',
...
]
"""
kwargs = {}
if course_id is not None:
kwargs['student_item__course_id'] = course_id
if item_id is not None:
kwargs['student_item__item_id'] = item_id
if item_type is not None:
kwargs['student_item__item_type'] = item_type
return Submission.objects.filter(**kwargs).values_list('uuid', flat=True)
def get_course_items():
"""
Retrieve the IDs of all items with workflows, grouped by course.
Returns:
dict
Example Usage:
>>> get_course_items()
{
"test/course/1": ["item-1", "item-2", "item-3"],
"test/course/2": ["item-4", "item-5", "item-6"],
...
}
"""
courses_dict = defaultdict(list)
items = StudentItem.objects.values_list('course_id', 'item_id').distinct()
for course_id, item_id in items:
courses_dict[course_id].append(item_id)
return courses_dict
def _get_or_create_student_item(student_item_dict): def _get_or_create_student_item(student_item_dict):
"""Gets or creates a Student Item that matches the values specified. """Gets or creates a Student Item that matches the values specified.
......
...@@ -177,6 +177,61 @@ class TestSubmissionsApi(TestCase): ...@@ -177,6 +177,61 @@ class TestSubmissionsApi(TestCase):
submissions = api.get_submissions(STUDENT_ITEM, 1) submissions = api.get_submissions(STUDENT_ITEM, 1)
self.assertEqual(u"Testing unicode answers.", submissions[0]["answer"]) self.assertEqual(u"Testing unicode answers.", submissions[0]["answer"])
def test_get_course_items(self):
# Initially, no course items available
self.assertEqual(api.get_course_items(), dict())
# Same course, same item
api.create_submission({
'student_id': 'Tim',
'course_id': 'Foo',
'item_id': 'Bar',
'item_type': 'openassessment'
}, "answer 1")
api.create_submission({
'student_id': 'Alice',
'course_id': 'Foo',
'item_id': 'Bar',
'item_type': 'openassessment'
}, "answer 2")
self.assertItemsEqual(api.get_course_items(), {"Foo": ["Bar"]})
# Same course, different items
api.create_submission({
'student_id': 'Alice',
'course_id': 'Foo',
'item_id': 'Different',
'item_type': 'openassessment'
}, "answer 3")
course_items = api.get_course_items()
self.assertItemsEqual(course_items.keys(), ["Foo"])
self.assertItemsEqual(api.get_course_items()["Foo"], ["Bar", "Different"])
# Different course, same item
api.create_submission({
'student_id': 'Alice',
'course_id': 'Lorem',
'item_id': 'Bar',
'item_type': 'openassessment'
}, "answer 3")
course_items = api.get_course_items()
self.assertItemsEqual(course_items.keys(), ["Foo", "Lorem"])
self.assertItemsEqual(api.get_course_items()["Foo"], ["Bar", "Different"])
self.assertItemsEqual(api.get_course_items()["Lorem"], ["Bar"])
# Different course, different item
api.create_submission({
'student_id': 'Alice',
'course_id': 'Ipsum',
'item_id': 'Dolar',
'item_type': 'openassessment'
}, "answer 3")
course_items = api.get_course_items()
self.assertItemsEqual(course_items.keys(), ["Foo", "Lorem", "Ipsum"])
self.assertItemsEqual(api.get_course_items()["Foo"], ["Bar", "Different"])
self.assertItemsEqual(api.get_course_items()["Lorem"], ["Bar"])
self.assertItemsEqual(api.get_course_items()["Ipsum"], ["Dolar"])
def _assert_submission(self, submission, expected_answer, expected_item, def _assert_submission(self, submission, expected_answer, expected_item,
expected_attempt): expected_attempt):
self.assertIsNotNone(submission) self.assertIsNotNone(submission)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment