Commit 259242f0 by Chris Dodge Committed by Jonathan Piacenti

tie into the xblock publish API point for progress and grading events

parent db0e36eb
......@@ -444,7 +444,7 @@ def _has_access_descriptor(user, action, descriptor, course_key=None):
checkers = {
'load': can_load,
'staff': lambda: _has_staff_access_to_descriptor(user, descriptor, course_key),
'instructor': lambda: _has_instructor_access_to_descriptor(user, descriptor, course_key)
'instructor': lambda: _has_instructor_access_to_descriptor(user, descriptor, course_key),
}
return _dispatch(checkers, action, user, descriptor)
......
......@@ -10,6 +10,9 @@ import mimetypes
import static_replace
from datetime import datetime
from django.utils.timezone import UTC
from collections import OrderedDict
from functools import partial
from requests.auth import HTTPBasicAuth
......@@ -430,6 +433,14 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl
"""
Manages the workflow for recording and updating of student module grade state
"""
if not settings.FEATURES.get("ALLOW_STUDENT_STATE_UPDATES_ON_CLOSED_COURSE", True):
# if a course has ended, don't register grading events
course = modulestore().get_course(course_id, depth=0)
now = datetime.now(UTC())
if course.end is not None and now > course.end:
return
user_id = event.get('user_id', user.id)
grade = event.get('value')
......@@ -472,6 +483,14 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl
"""
tie into the CourseCompletions datamodels that are exposed in the api_manager djangoapp
"""
if not settings.FEATURES.get("ALLOW_STUDENT_STATE_UPDATES_ON_CLOSED_COURSE", True):
# if a course has ended, don't register progress events
course = modulestore().get_course(course_id, depth=0)
now = datetime.now(UTC())
if course.end is not None and now > course.end:
return
user_id = event.get('user_id', user.id)
if not user_id:
return
......
......@@ -3,16 +3,21 @@
Run these tests @ Devstack:
paver test_system -s lms --test_id=lms/djangoapps/gradebook/tests.py
"""
from mock import MagicMock
from mock import MagicMock, patch
import uuid
from datetime import datetime
from django.utils.timezone import UTC
from django.conf import settings
from django.test.utils import override_settings
from capa.tests.response_xml_factory import StringResponseXMLFactory
from courseware import module_render
from courseware.model_data import FieldDataCache
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from student.tests.factories import UserFactory, AdminFactory
from courseware.tests.factories import StaffFactory
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from gradebook.models import StudentGradebook, StudentGradebookHistory
......@@ -42,7 +47,11 @@ class GradebookTests(ModuleStoreTestCase):
self.user = UserFactory()
self.score = 0.75
self.course = CourseFactory.create()
def _create_course(self, start=None, end=None):
self.course = CourseFactory.create(
start=start,
end=end
)
self.course.always_recalculate_grades = True
test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
chapter1 = ItemFactory.create(
......@@ -114,6 +123,7 @@ class GradebookTests(ModuleStoreTestCase):
)
def test_receiver_on_score_changed(self):
self._create_course()
module = self.get_module_for_user(self.user, self.course, self.problem)
grade_dict = {'value': 0.75, 'max_value': 1, 'user_id': self.user.id}
module.system.publish(module, 'grade', grade_dict)
......@@ -139,3 +149,101 @@ class GradebookTests(ModuleStoreTestCase):
history = StudentGradebookHistory.objects.all()
self.assertEqual(len(history), 5)
@patch.dict(settings.FEATURES, {'ALLOW_STUDENT_STATE_UPDATES_ON_CLOSED_COURSE': False})
def test_open_course(self):
self._create_course(start=datetime(2010,1,1, tzinfo=UTC()), end=datetime(3000, 1, 1, tzinfo=UTC()))
module = self.get_module_for_user(self.user, self.course, self.problem)
grade_dict = {'value': 0.75, 'max_value': 1, 'user_id': self.user.id}
module.system.publish(module, 'grade', grade_dict)
module = self.get_module_for_user(self.user, self.course, self.problem2)
grade_dict = {'value': 0.95, 'max_value': 1, 'user_id': self.user.id}
module.system.publish(module, 'grade', grade_dict)
gradebook = StudentGradebook.objects.all()
self.assertEqual(len(gradebook), 1)
history = StudentGradebookHistory.objects.all()
self.assertEqual(len(history), 2)
@patch.dict(settings.FEATURES, {'ALLOW_STUDENT_STATE_UPDATES_ON_CLOSED_COURSE': False})
def test_not_yet_started_course(self):
self._create_course(start=datetime(3000,1,1, tzinfo=UTC()), end=datetime(3000, 1, 1, tzinfo=UTC()))
module = self.get_module_for_user(self.user, self.course, self.problem)
grade_dict = {'value': 0.75, 'max_value': 1, 'user_id': self.user.id}
module.system.publish(module, 'grade', grade_dict)
module = self.get_module_for_user(self.user, self.course, self.problem2)
grade_dict = {'value': 0.95, 'max_value': 1, 'user_id': self.user.id}
module.system.publish(module, 'grade', grade_dict)
gradebook = StudentGradebook.objects.all()
self.assertEqual(len(gradebook), 1)
history = StudentGradebookHistory.objects.all()
self.assertEqual(len(history), 2)
@patch.dict(settings.FEATURES, {'ALLOW_STUDENT_STATE_UPDATES_ON_CLOSED_COURSE': False})
def test_closed_course_student(self):
self._create_course(start=datetime(2010,1,1, tzinfo=UTC()), end=datetime(2011, 1, 1, tzinfo=UTC()))
module = self.get_module_for_user(self.user, self.course, self.problem)
grade_dict = {'value': 0.75, 'max_value': 1, 'user_id': self.user.id}
module.system.publish(module, 'grade', grade_dict)
module = self.get_module_for_user(self.user, self.course, self.problem2)
grade_dict = {'value': 0.95, 'max_value': 1, 'user_id': self.user.id}
module.system.publish(module, 'grade', grade_dict)
gradebook = StudentGradebook.objects.all()
self.assertEqual(len(gradebook), 0)
history = StudentGradebookHistory.objects.all()
self.assertEqual(len(history), 0)
@patch.dict(settings.FEATURES, {'ALLOW_STUDENT_STATE_UPDATES_ON_CLOSED_COURSE': False})
def test_closed_course_admin(self):
"""
Users marked as Admin should be able to submit grade events to a closed course
"""
self.user = AdminFactory()
self._create_course(start=datetime(2010,1,1, tzinfo=UTC()), end=datetime(2011, 1, 1, tzinfo=UTC()))
module = self.get_module_for_user(self.user, self.course, self.problem)
grade_dict = {'value': 0.75, 'max_value': 1, 'user_id': self.user.id}
module.system.publish(module, 'grade', grade_dict)
module = self.get_module_for_user(self.user, self.course, self.problem2)
grade_dict = {'value': 0.95, 'max_value': 1, 'user_id': self.user.id}
module.system.publish(module, 'grade', grade_dict)
gradebook = StudentGradebook.objects.all()
self.assertEqual(len(gradebook), 0)
history = StudentGradebookHistory.objects.all()
self.assertEqual(len(history), 0)
@patch.dict(settings.FEATURES, {'ALLOW_STUDENT_STATE_UPDATES_ON_CLOSED_COURSE': False})
def test_closed_course_staff(self):
"""
Users marked as course staff should be able to submit grade events to a closed course
"""
self._create_course(start=datetime(2010,1,1, tzinfo=UTC()), end=datetime(2011, 1, 1, tzinfo=UTC()))
self.user = StaffFactory(course_key=self.course.id)
module = self.get_module_for_user(self.user, self.course, self.problem)
grade_dict = {'value': 0.75, 'max_value': 1, 'user_id': self.user.id}
module.system.publish(module, 'grade', grade_dict)
module = self.get_module_for_user(self.user, self.course, self.problem2)
grade_dict = {'value': 0.95, 'max_value': 1, 'user_id': self.user.id}
module.system.publish(module, 'grade', grade_dict)
gradebook = StudentGradebook.objects.all()
self.assertEqual(len(gradebook), 0)
history = StudentGradebookHistory.objects.all()
self.assertEqual(len(history), 0)
......@@ -2,13 +2,19 @@
Django database models supporting the progress app
"""
from datetime import datetime
from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
from django.db.models import Sum, Q
from django.utils.timezone import UTC
from model_utils.models import TimeStampedModel
from xmodule_django.models import CourseKeyField
from xmodule.modulestore.django import modulestore
from opaque_keys.edx.keys import CourseKey
class StudentProgress(TimeStampedModel):
......
# pylint: disable=E1101
"""
Run these tests @ Devstack:
paver test_system -s lms --test_id=lms/djangoapps/progress/tests.py
- or -
python -m coverage run --rcfile=lms/.coveragerc ./manage.py lms test --verbosity=1 lms/djangoapps/progress/tests.py --traceback --settings=test
"""
import uuid
from mock import MagicMock, patch
from datetime import datetime
from django.utils.timezone import UTC
from django.test.utils import override_settings
from django.conf import settings
from capa.tests.response_xml_factory import StringResponseXMLFactory
from student.tests.factories import UserFactory, AdminFactory
from courseware.tests.factories import StaffFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from progress.models import CourseModuleCompletion
from courseware.model_data import FieldDataCache
from courseware import module_render
@override_settings(STUDENT_GRADEBOOK=True)
class CourseModuleCompletionTests(ModuleStoreTestCase):
""" Test suite for CourseModuleCompletion """
def get_module_for_user(self, user, course, problem):
"""Helper function to get useful module at self.location in self.course_id for user"""
mock_request = MagicMock()
mock_request.user = user
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course.id, user, course, depth=2)
return module_render.get_module( # pylint: disable=protected-access
user,
mock_request,
problem.location,
field_data_cache,
course.id
)._xmodule
def setUp(self):
self.user = UserFactory()
self._create_course()
def _create_course(self, start=None, end=None):
self.course = CourseFactory.create(
start=start,
end=end
)
self.course.always_recalculate_grades = True
test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
chapter1 = ItemFactory.create(
category="chapter",
parent_location=self.course.location,
data=test_data,
display_name="Chapter 1"
)
chapter2 = ItemFactory.create(
category="chapter",
parent_location=self.course.location,
data=test_data,
display_name="Chapter 2"
)
ItemFactory.create(
category="sequential",
parent_location=chapter1.location,
data=test_data,
display_name="Sequence 1",
)
ItemFactory.create(
category="sequential",
parent_location=chapter2.location,
data=test_data,
display_name="Sequence 2",
)
ItemFactory.create(
parent_location=chapter2.location,
category='problem',
data=StringResponseXMLFactory().build_xml(answer='foo'),
metadata={'rerandomize': 'always'},
display_name="test problem 1",
max_grade=45
)
self.problem = ItemFactory.create(
parent_location=chapter1.location,
category='problem',
data=StringResponseXMLFactory().build_xml(answer='bar'),
display_name="homework problem 1",
metadata={'rerandomize': 'always', 'graded': True, 'format': "Homework"}
)
self.problem2 = ItemFactory.create(
parent_location=chapter2.location,
category='problem',
data=StringResponseXMLFactory().build_xml(answer='bar'),
display_name="homework problem 2",
metadata={'rerandomize': 'always', 'graded': True, 'format': "Homework"}
)
self.problem3 = ItemFactory.create(
parent_location=chapter2.location,
category='problem',
data=StringResponseXMLFactory().build_xml(answer='bar'),
display_name="lab problem 1",
metadata={'rerandomize': 'always', 'graded': True, 'format': "Lab"}
)
self.problem4 = ItemFactory.create(
parent_location=chapter2.location,
category='problem',
data=StringResponseXMLFactory().build_xml(answer='bar'),
display_name="midterm problem 2",
metadata={'rerandomize': 'always', 'graded': True, 'format': "Midterm Exam"}
)
self.problem5 = ItemFactory.create(
parent_location=chapter2.location,
category='problem',
data=StringResponseXMLFactory().build_xml(answer='bar'),
display_name="final problem 2",
metadata={'rerandomize': 'always', 'graded': True, 'format': "Final Exam"}
)
def test_save_completion(self):
"""
Save a CourseModuleCompletion and fetch it again
"""
module = self.get_module_for_user(self.user, self.course, self.problem4)
module.system.publish(module, 'progress', {})
completion_fetch = CourseModuleCompletion.objects.get(
user=self.user.id,
course_id=self.course.id,
content_id=self.problem4.location
)
self.assertIsNotNone(completion_fetch)
@patch.dict(settings.FEATURES, {'ALLOW_STUDENT_STATE_UPDATES_ON_CLOSED_COURSE': False})
def test_save_completion_with_feature_flag(self):
"""
Save a CourseModuleCompletion with the feature flag, but the course is still open
"""
module = self.get_module_for_user(self.user, self.course, self.problem4)
module.system.publish(module, 'progress', {})
completion_fetch = CourseModuleCompletion.objects.get(
user=self.user.id,
course_id=self.course.id,
content_id=self.problem4.location
)
self.assertIsNotNone(completion_fetch)
@patch.dict(settings.FEATURES, {'ALLOW_STUDENT_STATE_UPDATES_ON_CLOSED_COURSE': False})
def test_save_completion_admin_not_started(self):
"""
Save a CourseModuleCompletion with the feature flag on a course that has not yet started
but Admins should be able to write
"""
self._create_course(start=datetime(3000, 1, 1, tzinfo=UTC()))
self.user = AdminFactory()
module = self.get_module_for_user(self.user, self.course, self.problem4)
module.system.publish(module, 'progress', {})
completion_fetch = CourseModuleCompletion.objects.get(
user=self.user.id,
course_id=self.course.id,
content_id=self.problem4.location
)
self.assertIsNotNone(completion_fetch)
@patch.dict(settings.FEATURES, {'ALLOW_STUDENT_STATE_UPDATES_ON_CLOSED_COURSE': False})
def test_save_completion_staff_not_started(self):
"""
Save a CourseModuleCompletion with the feature flag on a course that has not yet started
but Staff should be able to write
"""
self._create_course(start=datetime(3000, 1, 1, tzinfo=UTC()))
self.user = StaffFactory(course_key=self.course.id)
module = self.get_module_for_user(self.user, self.course, self.problem4)
module.system.publish(module, 'progress', {})
completion_fetch = CourseModuleCompletion.objects.get(
user=self.user.id,
course_id=self.course.id,
content_id=self.problem4.location
)
self.assertIsNotNone(completion_fetch)
@patch.dict(settings.FEATURES, {'ALLOW_STUDENT_STATE_UPDATES_ON_CLOSED_COURSE': False})
def test_save_completion_admin_ended(self):
"""
Save a CourseModuleCompletion with the feature flag on a course that has not yet started
but Admins should be able to write
"""
self._create_course(end=datetime(1999, 1, 1, tzinfo=UTC()))
self.user = AdminFactory()
module = self.get_module_for_user(self.user, self.course, self.problem4)
module.system.publish(module, 'progress', {})
with self.assertRaises(CourseModuleCompletion.DoesNotExist):
CourseModuleCompletion.objects.get(
user=self.user.id,
course_id=self.course.id,
content_id=self.problem4.location
)
@patch.dict(settings.FEATURES, {'ALLOW_STUDENT_STATE_UPDATES_ON_CLOSED_COURSE': False})
def test_save_completion_staff_ended(self):
"""
Save a CourseModuleCompletion with the feature flag on a course that has not yet started
but Staff should be able to write
"""
self._create_course(end=datetime(1999, 1, 1, tzinfo=UTC()))
self.user = StaffFactory(course_key=self.course.id)
module = self.get_module_for_user(self.user, self.course, self.problem4)
module.system.publish(module, 'progress', {})
with self.assertRaises(CourseModuleCompletion.DoesNotExist):
CourseModuleCompletion.objects.get(
user=self.user.id,
course_id=self.course.id,
content_id=self.problem4.location
)
@patch.dict(settings.FEATURES, {'ALLOW_STUDENT_STATE_UPDATES_ON_CLOSED_COURSE': False})
def test_save_completion_with_course_not_started(self):
"""
Save a CourseModuleCompletion with the feature flag, but the course has not yet started
"""
self._create_course(start=datetime(3000, 1, 1, tzinfo=UTC()))
module = self.get_module_for_user(self.user, self.course, self.problem4)
module.system.publish(module, 'progress', {})
entry = CourseModuleCompletion.objects.get(
user=self.user.id,
course_id=self.course.id,
content_id=self.problem4.location
)
self.assertIsNotNone(entry)
@patch.dict(settings.FEATURES, {'ALLOW_STUDENT_STATE_UPDATES_ON_CLOSED_COURSE': False})
def test_save_completion_with_course_already_ended(self):
"""
Save a CourseModuleCompletion with the feature flag, but the course has already ended
"""
self._create_course(
start=datetime.now(UTC()),
end=datetime(2000, 1, 1, tzinfo=UTC())
)
module = self.get_module_for_user(self.user, self.course, self.problem4)
module.system.publish(module, 'progress', {})
with self.assertRaises(CourseModuleCompletion.DoesNotExist):
CourseModuleCompletion.objects.get(
user=self.user.id,
course_id=self.course.id,
content_id=self.problem4.location
)
......@@ -74,6 +74,7 @@ FEATURES['EMBARGO'] = True
# Toggles API on for testing
FEATURES['API'] = True
FEATURES['ALLOW_STUDENT_STATE_UPDATES_ON_CLOSED_COURSE'] = False
FEATURES['ENABLE_COMBINED_LOGIN_REGISTRATION'] = True
......
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