When creating an instructor task total_num_items may change between the time it and the number of subtasks is calculated and the time the subtasks are actually queued (all of this cannot happen in one transaction). In such a case the extra items are distributed among the subtasks of the last query. LMS-2090
220 lines
9.7 KiB
Python
220 lines
9.7 KiB
Python
"""
|
|
Base test classes for LMS instructor-initiated background tasks
|
|
|
|
"""
|
|
import json
|
|
from uuid import uuid4
|
|
from mock import Mock
|
|
|
|
from celery.states import SUCCESS, FAILURE
|
|
|
|
from django.test.testcases import TestCase
|
|
from django.contrib.auth.models import User
|
|
from django.test.utils import override_settings
|
|
|
|
from capa.tests.response_xml_factory import OptionResponseXMLFactory
|
|
from xmodule.modulestore.django import editable_modulestore
|
|
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
|
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
|
|
|
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
|
from courseware.model_data import StudentModule
|
|
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_MIXED_MODULESTORE
|
|
|
|
from instructor_task.api_helper import encode_problem_and_student_input
|
|
from instructor_task.models import PROGRESS, QUEUING
|
|
from instructor_task.tests.factories import InstructorTaskFactory
|
|
from instructor_task.views import instructor_task_status
|
|
|
|
|
|
TEST_COURSE_ORG = 'edx'
|
|
TEST_COURSE_NAME = 'test course'
|
|
TEST_COURSE_NUMBER = '1.23x'
|
|
TEST_SECTION_NAME = "Problem"
|
|
TEST_COURSE_ID = 'edx/1.23x/test_course'
|
|
|
|
TEST_FAILURE_MESSAGE = 'task failed horribly'
|
|
TEST_FAILURE_EXCEPTION = 'RandomCauseError'
|
|
|
|
OPTION_1 = 'Option 1'
|
|
OPTION_2 = 'Option 2'
|
|
|
|
|
|
class InstructorTaskTestCase(TestCase):
|
|
"""
|
|
Tests API and view methods that involve the reporting of status for background tasks.
|
|
"""
|
|
def setUp(self):
|
|
self.student = UserFactory.create(username="student", email="student@edx.org")
|
|
self.instructor = UserFactory.create(username="instructor", email="instructor@edx.org")
|
|
self.problem_url = InstructorTaskTestCase.problem_location("test_urlname")
|
|
|
|
@staticmethod
|
|
def problem_location(problem_url_name):
|
|
"""
|
|
Create an internal location for a test problem.
|
|
"""
|
|
return "i4x://{org}/{number}/problem/{problem_url_name}".format(org='edx',
|
|
number='1.23x',
|
|
problem_url_name=problem_url_name)
|
|
|
|
def _create_entry(self, task_state=QUEUING, task_output=None, student=None):
|
|
"""Creates a InstructorTask entry for testing."""
|
|
task_id = str(uuid4())
|
|
progress_json = json.dumps(task_output) if task_output is not None else None
|
|
task_input, task_key = encode_problem_and_student_input(self.problem_url, student)
|
|
|
|
instructor_task = InstructorTaskFactory.create(course_id=TEST_COURSE_ID,
|
|
requester=self.instructor,
|
|
task_input=json.dumps(task_input),
|
|
task_key=task_key,
|
|
task_id=task_id,
|
|
task_state=task_state,
|
|
task_output=progress_json)
|
|
return instructor_task
|
|
|
|
def _create_failure_entry(self):
|
|
"""Creates a InstructorTask entry representing a failed task."""
|
|
# view task entry for task failure
|
|
progress = {'message': TEST_FAILURE_MESSAGE,
|
|
'exception': TEST_FAILURE_EXCEPTION,
|
|
}
|
|
return self._create_entry(task_state=FAILURE, task_output=progress)
|
|
|
|
def _create_success_entry(self, student=None):
|
|
"""Creates a InstructorTask entry representing a successful task."""
|
|
return self._create_progress_entry(student, task_state=SUCCESS)
|
|
|
|
def _create_progress_entry(self, student=None, task_state=PROGRESS):
|
|
"""Creates a InstructorTask entry representing a task in progress."""
|
|
progress = {'attempted': 3,
|
|
'succeeded': 2,
|
|
'total': 5,
|
|
'action_name': 'rescored',
|
|
}
|
|
return self._create_entry(task_state=task_state, task_output=progress, student=student)
|
|
|
|
|
|
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
|
class InstructorTaskCourseTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
|
"""
|
|
Base test class for InstructorTask-related tests that require
|
|
the setup of a course.
|
|
"""
|
|
course = None
|
|
current_user = None
|
|
|
|
def initialize_course(self):
|
|
"""Create a course in the store, with a chapter and section."""
|
|
self.module_store = editable_modulestore()
|
|
|
|
# Create the course
|
|
self.course = CourseFactory.create(org=TEST_COURSE_ORG,
|
|
number=TEST_COURSE_NUMBER,
|
|
display_name=TEST_COURSE_NAME)
|
|
|
|
# Add a chapter to the course
|
|
chapter = ItemFactory.create(parent_location=self.course.location,
|
|
display_name=TEST_SECTION_NAME)
|
|
|
|
# add a sequence to the course to which the problems can be added
|
|
self.problem_section = ItemFactory.create(parent_location=chapter.location,
|
|
category='sequential',
|
|
metadata={'graded': True, 'format': 'Homework'},
|
|
display_name=TEST_SECTION_NAME)
|
|
|
|
@staticmethod
|
|
def get_user_email(username):
|
|
"""Generate email address based on username"""
|
|
return '{0}@test.com'.format(username)
|
|
|
|
def login_username(self, username):
|
|
"""Login the user, given the `username`."""
|
|
if self.current_user != username:
|
|
self.login(InstructorTaskCourseTestCase.get_user_email(username), "test")
|
|
self.current_user = username
|
|
|
|
def _create_user(self, username, is_staff=False):
|
|
"""Creates a user and enrolls them in the test course."""
|
|
email = InstructorTaskCourseTestCase.get_user_email(username)
|
|
thisuser = UserFactory.create(username=username, email=email, is_staff=is_staff)
|
|
CourseEnrollmentFactory.create(user=thisuser, course_id=self.course.id)
|
|
return thisuser
|
|
|
|
def create_instructor(self, username):
|
|
"""Creates an instructor for the test course."""
|
|
return self._create_user(username, is_staff=True)
|
|
|
|
def create_student(self, username):
|
|
"""Creates a student for the test course."""
|
|
return self._create_user(username, is_staff=False)
|
|
|
|
@staticmethod
|
|
def get_task_status(task_id):
|
|
"""Use api method to fetch task status, using mock request."""
|
|
mock_request = Mock()
|
|
mock_request.REQUEST = {'task_id': task_id}
|
|
response = instructor_task_status(mock_request)
|
|
status = json.loads(response.content)
|
|
return status
|
|
|
|
def create_task_request(self, requester_username):
|
|
"""Generate request that can be used for submitting tasks"""
|
|
request = Mock()
|
|
request.user = User.objects.get(username=requester_username)
|
|
request.get_host = Mock(return_value="testhost")
|
|
request.META = {'REMOTE_ADDR': '0:0:0:0', 'SERVER_NAME': 'testhost'}
|
|
request.is_secure = Mock(return_value=False)
|
|
return request
|
|
|
|
|
|
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
|
class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase):
|
|
"""
|
|
Base test class for InstructorTask-related tests that require
|
|
the setup of a course and problem in order to access StudentModule state.
|
|
"""
|
|
@staticmethod
|
|
def problem_location(problem_url_name):
|
|
"""
|
|
Create an internal location for a test problem.
|
|
"""
|
|
if "i4x:" in problem_url_name:
|
|
return problem_url_name
|
|
else:
|
|
return "i4x://{org}/{number}/problem/{problem_url_name}".format(org=TEST_COURSE_ORG,
|
|
number=TEST_COURSE_NUMBER,
|
|
problem_url_name=problem_url_name)
|
|
|
|
def define_option_problem(self, problem_url_name):
|
|
"""Create the problem definition so the answer is Option 1"""
|
|
factory = OptionResponseXMLFactory()
|
|
factory_args = {'question_text': 'The correct answer is {0}'.format(OPTION_1),
|
|
'options': [OPTION_1, OPTION_2],
|
|
'correct_option': OPTION_1,
|
|
'num_responses': 2}
|
|
problem_xml = factory.build_xml(**factory_args)
|
|
ItemFactory.create(parent_location=self.problem_section.location,
|
|
category="problem",
|
|
display_name=str(problem_url_name),
|
|
data=problem_xml)
|
|
|
|
def redefine_option_problem(self, problem_url_name):
|
|
"""Change the problem definition so the answer is Option 2"""
|
|
factory = OptionResponseXMLFactory()
|
|
factory_args = {'question_text': 'The correct answer is {0}'.format(OPTION_2),
|
|
'options': [OPTION_1, OPTION_2],
|
|
'correct_option': OPTION_2,
|
|
'num_responses': 2}
|
|
problem_xml = factory.build_xml(**factory_args)
|
|
location = InstructorTaskTestCase.problem_location(problem_url_name)
|
|
self.module_store.update_item(location, problem_xml)
|
|
|
|
def get_student_module(self, username, descriptor):
|
|
"""Get StudentModule object for test course, given the `username` and the problem's `descriptor`."""
|
|
return StudentModule.objects.get(course_id=self.course.id,
|
|
student=User.objects.get(username=username),
|
|
module_type=descriptor.location.category,
|
|
module_state_key=descriptor.location.url(),
|
|
)
|