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): ...@@ -444,7 +444,7 @@ def _has_access_descriptor(user, action, descriptor, course_key=None):
checkers = { checkers = {
'load': can_load, 'load': can_load,
'staff': lambda: _has_staff_access_to_descriptor(user, descriptor, course_key), '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) return _dispatch(checkers, action, user, descriptor)
......
...@@ -10,6 +10,9 @@ import mimetypes ...@@ -10,6 +10,9 @@ import mimetypes
import static_replace import static_replace
from datetime import datetime
from django.utils.timezone import UTC
from collections import OrderedDict from collections import OrderedDict
from functools import partial from functools import partial
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
...@@ -430,6 +433,14 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl ...@@ -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 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) user_id = event.get('user_id', user.id)
grade = event.get('value') grade = event.get('value')
...@@ -472,6 +483,14 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl ...@@ -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 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) user_id = event.get('user_id', user.id)
if not user_id: if not user_id:
return return
......
...@@ -3,16 +3,21 @@ ...@@ -3,16 +3,21 @@
Run these tests @ Devstack: Run these tests @ Devstack:
paver test_system -s lms --test_id=lms/djangoapps/gradebook/tests.py paver test_system -s lms --test_id=lms/djangoapps/gradebook/tests.py
""" """
from mock import MagicMock from mock import MagicMock, patch
import uuid 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 django.test.utils import override_settings
from capa.tests.response_xml_factory import StringResponseXMLFactory from capa.tests.response_xml_factory import StringResponseXMLFactory
from courseware import module_render from courseware import module_render
from courseware.model_data import FieldDataCache from courseware.model_data import FieldDataCache
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase 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 xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from gradebook.models import StudentGradebook, StudentGradebookHistory from gradebook.models import StudentGradebook, StudentGradebookHistory
...@@ -42,7 +47,11 @@ class GradebookTests(ModuleStoreTestCase): ...@@ -42,7 +47,11 @@ class GradebookTests(ModuleStoreTestCase):
self.user = UserFactory() self.user = UserFactory()
self.score = 0.75 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 self.course.always_recalculate_grades = True
test_data = '<html>{}</html>'.format(str(uuid.uuid4())) test_data = '<html>{}</html>'.format(str(uuid.uuid4()))
chapter1 = ItemFactory.create( chapter1 = ItemFactory.create(
...@@ -114,6 +123,7 @@ class GradebookTests(ModuleStoreTestCase): ...@@ -114,6 +123,7 @@ class GradebookTests(ModuleStoreTestCase):
) )
def test_receiver_on_score_changed(self): def test_receiver_on_score_changed(self):
self._create_course()
module = self.get_module_for_user(self.user, self.course, self.problem) 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} grade_dict = {'value': 0.75, 'max_value': 1, 'user_id': self.user.id}
module.system.publish(module, 'grade', grade_dict) module.system.publish(module, 'grade', grade_dict)
...@@ -139,3 +149,101 @@ class GradebookTests(ModuleStoreTestCase): ...@@ -139,3 +149,101 @@ class GradebookTests(ModuleStoreTestCase):
history = StudentGradebookHistory.objects.all() history = StudentGradebookHistory.objects.all()
self.assertEqual(len(history), 5) 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 @@ ...@@ -2,13 +2,19 @@
Django database models supporting the progress app Django database models supporting the progress app
""" """
from datetime import datetime
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.db.models import Sum, Q from django.db.models import Sum, Q
from django.utils.timezone import UTC
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
from xmodule_django.models import CourseKeyField from xmodule_django.models import CourseKeyField
from xmodule.modulestore.django import modulestore
from opaque_keys.edx.keys import CourseKey
class StudentProgress(TimeStampedModel): 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 ...@@ -74,6 +74,7 @@ FEATURES['EMBARGO'] = True
# Toggles API on for testing # Toggles API on for testing
FEATURES['API'] = True FEATURES['API'] = True
FEATURES['ALLOW_STUDENT_STATE_UPDATES_ON_CLOSED_COURSE'] = False
FEATURES['ENABLE_COMBINED_LOGIN_REGISTRATION'] = True 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