Instructor Dashboard management tools for Entrance Exams
Conflicts: lms/djangoapps/courseware/courses.py lms/djangoapps/instructor_task/api.py refined entrance exam student attempts reset added rescore, delete state and task history functionality
This commit is contained in:
@@ -23,9 +23,13 @@ from instructor_task.tasks import (
|
||||
cohort_students,
|
||||
)
|
||||
|
||||
from instructor_task.api_helper import (check_arguments_for_rescoring,
|
||||
encode_problem_and_student_input,
|
||||
submit_task)
|
||||
from instructor_task.api_helper import (
|
||||
check_arguments_for_rescoring,
|
||||
encode_problem_and_student_input,
|
||||
encode_entrance_exam_and_student_input,
|
||||
check_entrance_exam_problems_for_rescoring,
|
||||
submit_task,
|
||||
)
|
||||
from bulk_email.models import CourseEmail
|
||||
|
||||
|
||||
@@ -57,6 +61,19 @@ def get_instructor_task_history(course_id, usage_key=None, student=None, task_ty
|
||||
return instructor_tasks.order_by('-id')
|
||||
|
||||
|
||||
def get_entrance_exam_instructor_task_history(course_id, usage_key=None, student=None): # pylint: disable=invalid-name
|
||||
"""
|
||||
Returns a query of InstructorTask objects of historical tasks for a given course,
|
||||
that optionally match an entrance exam and student if present.
|
||||
"""
|
||||
instructor_tasks = InstructorTask.objects.filter(course_id=course_id)
|
||||
if usage_key is not None or student is not None:
|
||||
_, task_key = encode_entrance_exam_and_student_input(usage_key, student)
|
||||
instructor_tasks = instructor_tasks.filter(task_key=task_key)
|
||||
|
||||
return instructor_tasks.order_by('-id')
|
||||
|
||||
|
||||
# Disabling invalid-name because this fn name is longer than 30 chars.
|
||||
def submit_rescore_problem_for_student(request, usage_key, student): # pylint: disable=invalid-name
|
||||
"""
|
||||
@@ -117,6 +134,38 @@ def submit_rescore_problem_for_all_students(request, usage_key): # pylint: disa
|
||||
return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key)
|
||||
|
||||
|
||||
def submit_rescore_entrance_exam_for_student(request, usage_key, student=None): # pylint: disable=invalid-name
|
||||
"""
|
||||
Request entrance exam problems to be re-scored as a background task.
|
||||
|
||||
The entrance exam problems will be re-scored for given student or if student
|
||||
is None problems for all students who have accessed the entrance exam.
|
||||
|
||||
Parameters are `usage_key`, which must be a :class:`Location`
|
||||
representing entrance exam section and the `student` as a User object.
|
||||
|
||||
ItemNotFoundError is raised if entrance exam does not exists for given
|
||||
usage_key, AlreadyRunningError is raised if the entrance exam
|
||||
is already being re-scored, or NotImplementedError if the problem doesn't
|
||||
support rescoring.
|
||||
|
||||
This method makes sure the InstructorTask entry is committed.
|
||||
When called from any view that is wrapped by TransactionMiddleware,
|
||||
and thus in a "commit-on-success" transaction, an autocommit buried within here
|
||||
will cause any pending transaction to be committed by a successful
|
||||
save here. Any future database operations will take place in a
|
||||
separate transaction.
|
||||
"""
|
||||
# check problems for rescoring: let exceptions return up to the caller.
|
||||
check_entrance_exam_problems_for_rescoring(usage_key)
|
||||
|
||||
# check to see if task is already running, and reserve it otherwise
|
||||
task_type = 'rescore_problem'
|
||||
task_class = rescore_problem
|
||||
task_input, task_key = encode_entrance_exam_and_student_input(usage_key, student)
|
||||
return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key)
|
||||
|
||||
|
||||
def submit_reset_problem_attempts_for_all_students(request, usage_key): # pylint: disable=invalid-name
|
||||
"""
|
||||
Request to have attempts reset for a problem as a background task.
|
||||
@@ -146,6 +195,37 @@ def submit_reset_problem_attempts_for_all_students(request, usage_key): # pylin
|
||||
return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key)
|
||||
|
||||
|
||||
def submit_reset_problem_attempts_in_entrance_exam(request, usage_key, student): # pylint: disable=invalid-name
|
||||
"""
|
||||
Request to have attempts reset for a entrance exam as a background task.
|
||||
|
||||
Problem attempts for all problems in entrance exam will be reset
|
||||
for specified student. If student is None problem attempts will be
|
||||
reset for all students.
|
||||
|
||||
Parameters are `usage_key`, which must be a :class:`Location`
|
||||
representing entrance exam section and the `student` as a User object.
|
||||
|
||||
ItemNotFoundError is raised if entrance exam does not exists for given
|
||||
usage_key, AlreadyRunningError is raised if the entrance exam
|
||||
is already being reset.
|
||||
|
||||
This method makes sure the InstructorTask entry is committed.
|
||||
When called from any view that is wrapped by TransactionMiddleware,
|
||||
and thus in a "commit-on-success" transaction, an autocommit buried within here
|
||||
will cause any pending transaction to be committed by a successful
|
||||
save here. Any future database operations will take place in a
|
||||
separate transaction.
|
||||
"""
|
||||
# check arguments: make sure entrance exam(section) exists for given usage_key
|
||||
modulestore().get_item(usage_key)
|
||||
|
||||
task_type = 'reset_problem_attempts'
|
||||
task_class = reset_problem_attempts
|
||||
task_input, task_key = encode_entrance_exam_and_student_input(usage_key, student)
|
||||
return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key)
|
||||
|
||||
|
||||
def submit_delete_problem_state_for_all_students(request, usage_key): # pylint: disable=invalid-name
|
||||
"""
|
||||
Request to have state deleted for a problem as a background task.
|
||||
@@ -175,6 +255,36 @@ def submit_delete_problem_state_for_all_students(request, usage_key): # pylint:
|
||||
return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key)
|
||||
|
||||
|
||||
def submit_delete_entrance_exam_state_for_student(request, usage_key, student): # pylint: disable=invalid-name
|
||||
"""
|
||||
Requests reset of state for entrance exam as a background task.
|
||||
|
||||
Module state for all problems in entrance exam will be deleted
|
||||
for specified student.
|
||||
|
||||
Parameters are `usage_key`, which must be a :class:`Location`
|
||||
representing entrance exam section and the `student` as a User object.
|
||||
|
||||
ItemNotFoundError is raised if entrance exam does not exists for given
|
||||
usage_key, AlreadyRunningError is raised if the entrance exam
|
||||
is already being reset.
|
||||
|
||||
This method makes sure the InstructorTask entry is committed.
|
||||
When called from any view that is wrapped by TransactionMiddleware,
|
||||
and thus in a "commit-on-success" transaction, an autocommit buried within here
|
||||
will cause any pending transaction to be committed by a successful
|
||||
save here. Any future database operations will take place in a
|
||||
separate transaction.
|
||||
"""
|
||||
# check arguments: make sure entrance exam(section) exists for given usage_key
|
||||
modulestore().get_item(usage_key)
|
||||
|
||||
task_type = 'delete_problem_state'
|
||||
task_class = delete_problem_state
|
||||
task_input, task_key = encode_entrance_exam_and_student_input(usage_key, student)
|
||||
return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key)
|
||||
|
||||
|
||||
def submit_bulk_course_email(request, course_key, email_id):
|
||||
"""
|
||||
Request to have bulk email sent as a background task.
|
||||
|
||||
@@ -8,10 +8,13 @@ import hashlib
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from celery.result import AsyncResult
|
||||
from celery.states import READY_STATES, SUCCESS, FAILURE, REVOKED
|
||||
|
||||
from courseware.module_render import get_xqueue_callback_url_prefix
|
||||
from courseware.courses import get_problems_in_section
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
@@ -253,6 +256,22 @@ def check_arguments_for_rescoring(usage_key):
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
|
||||
def check_entrance_exam_problems_for_rescoring(exam_key): # pylint: disable=invalid-name
|
||||
"""
|
||||
Grabs all problem descriptors in exam and checks each descriptor to
|
||||
confirm that it supports re-scoring.
|
||||
|
||||
An ItemNotFoundException is raised if the corresponding module
|
||||
descriptor doesn't exist for exam_key. NotImplementedError is raised if
|
||||
any of the problem in entrance exam doesn't support re-scoring calls.
|
||||
"""
|
||||
problems = get_problems_in_section(exam_key).values()
|
||||
if any(not hasattr(problem, 'module_class') or not hasattr(problem.module_class, 'rescore_problem')
|
||||
for problem in problems):
|
||||
msg = _("Not all problems in entrance exam support re-scoring.")
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
|
||||
def encode_problem_and_student_input(usage_key, student=None): # pylint: disable=invalid-name
|
||||
"""
|
||||
Encode optional usage_key and optional student into task_key and task_input values.
|
||||
@@ -276,6 +295,28 @@ def encode_problem_and_student_input(usage_key, student=None): # pylint: disabl
|
||||
return task_input, task_key
|
||||
|
||||
|
||||
def encode_entrance_exam_and_student_input(usage_key, student=None): # pylint: disable=invalid-name
|
||||
"""
|
||||
Encode usage_key and optional student into task_key and task_input values.
|
||||
|
||||
Args:
|
||||
usage_key (Location): The usage_key identifying the entrance exam.
|
||||
student (User): the student affected
|
||||
"""
|
||||
assert isinstance(usage_key, UsageKey)
|
||||
if student is not None:
|
||||
task_input = {'entrance_exam_url': unicode(usage_key), 'student': student.username}
|
||||
task_key_stub = "{student}_{entranceexam}".format(student=student.id, entranceexam=unicode(usage_key))
|
||||
else:
|
||||
task_input = {'entrance_exam_url': unicode(usage_key)}
|
||||
task_key_stub = "_{entranceexam}".format(entranceexam=unicode(usage_key))
|
||||
|
||||
# create the key value by using MD5 hash:
|
||||
task_key = hashlib.md5(task_key_stub).hexdigest()
|
||||
|
||||
return task_input, task_key
|
||||
|
||||
|
||||
def submit_task(request, task_type, task_class, course_key, task_input, task_key):
|
||||
"""
|
||||
Helper method to submit a task.
|
||||
|
||||
@@ -22,7 +22,7 @@ from util.file import course_filename_prefix_generator, UniversalNewlineIterator
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.split_test_module import get_split_user_partitions
|
||||
|
||||
from courseware.courses import get_course_by_id
|
||||
from courseware.courses import get_course_by_id, get_problems_in_section
|
||||
from courseware.grades import iterate_grades_for
|
||||
from courseware.models import StudentModule
|
||||
from courseware.model_data import FieldDataCache
|
||||
@@ -33,6 +33,7 @@ from instructor_task.models import ReportStore, InstructorTask, PROGRESS
|
||||
from lms.djangoapps.lms_xblock.runtime import LmsPartitionService
|
||||
from openedx.core.djangoapps.course_groups.cohorts import get_cohort
|
||||
from openedx.core.djangoapps.course_groups.models import CourseUserGroup
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
@@ -296,14 +297,28 @@ def perform_module_state_update(update_fcn, filter_fcn, _entry_id, course_id, ta
|
||||
|
||||
"""
|
||||
start_time = time()
|
||||
usage_key = course_id.make_usage_key_from_deprecated_string(task_input.get('problem_url'))
|
||||
usage_keys = []
|
||||
problem_url = task_input.get('problem_url')
|
||||
entrance_exam_url = task_input.get('entrance_exam_url')
|
||||
student_identifier = task_input.get('student')
|
||||
problems = {}
|
||||
|
||||
# find the problem descriptor:
|
||||
module_descriptor = modulestore().get_item(usage_key)
|
||||
# if problem_url is present make a usage key from it
|
||||
if problem_url:
|
||||
usage_key = course_id.make_usage_key_from_deprecated_string(problem_url)
|
||||
usage_keys.append(usage_key)
|
||||
|
||||
# find the module in question
|
||||
modules_to_update = StudentModule.objects.filter(course_id=course_id, module_state_key=usage_key)
|
||||
# find the problem descriptor:
|
||||
problem_descriptor = modulestore().get_item(usage_key)
|
||||
problems[unicode(usage_key)] = problem_descriptor
|
||||
|
||||
# if entrance_exam is present grab all problems in it
|
||||
if entrance_exam_url:
|
||||
problems = get_problems_in_section(entrance_exam_url)
|
||||
usage_keys = [UsageKey.from_string(location) for location in problems.keys()]
|
||||
|
||||
# find the modules in question
|
||||
modules_to_update = StudentModule.objects.filter(course_id=course_id, module_state_key__in=usage_keys)
|
||||
|
||||
# give the option of updating an individual student. If not specified,
|
||||
# then updates all students who have responded to a problem so far
|
||||
@@ -327,6 +342,7 @@ def perform_module_state_update(update_fcn, filter_fcn, _entry_id, course_id, ta
|
||||
|
||||
for module_to_update in modules_to_update:
|
||||
task_progress.attempted += 1
|
||||
module_descriptor = problems[unicode(module_to_update.module_state_key)]
|
||||
# There is no try here: if there's an error, we let it throw, and the task will
|
||||
# be marked as FAILED, with a stack trace.
|
||||
with dog_stats_api.timer('instructor_tasks.module.time.step', tags=[u'action:{name}'.format(name=action_name)]):
|
||||
|
||||
Reference in New Issue
Block a user