- added the abstract and concrete layers of enrollment report provider - created a celery task. -added the button in the e-commerce reports section added the enrollment data backend added the payment data and start writing the test cases. updated the code with the feedback suggestions and wrote some test cases. - all the downloadable reports are now visible in the ecommerce download section. Pending instructor tasks is also visible in the ecommerce section added the fields in the user profile information changed the report store configuration key added the new http endpoint for financial reports to add more permissions for finance_admin to access. fix quality issues added test cases to check csv content data rebased with master and resolved conflicts changed the log messages added the changes as per code clintonb suggestions during code review updated the test cases for the finance_admin decorator changes suggested by clinton. Created and moved Table level filters to the Custom Manager for the CourseEnrollment model. ecommerce.js file was loaded twice in the instructor_dashboard.js fixed the issues added the registration code column in the csv added the full gender in the csv file Update data sources and add display name translations for the report columns fix meta name Make sure the reports section does not appear on non whitelabel courses pylint fixes expand out enumerated values
390 lines
17 KiB
Python
390 lines
17 KiB
Python
"""
|
|
API for submitting background tasks by an instructor for a course.
|
|
|
|
Also includes methods for getting information about tasks that have
|
|
already been submitted, filtered either by running state or input
|
|
arguments.
|
|
|
|
"""
|
|
import hashlib
|
|
|
|
from celery.states import READY_STATES
|
|
|
|
from xmodule.modulestore.django import modulestore
|
|
|
|
from instructor_task.models import InstructorTask
|
|
from instructor_task.tasks import (
|
|
rescore_problem,
|
|
reset_problem_attempts,
|
|
delete_problem_state,
|
|
send_bulk_course_email,
|
|
calculate_grades_csv,
|
|
calculate_problem_grade_report,
|
|
calculate_students_features_csv,
|
|
cohort_students,
|
|
enrollment_report_features_csv)
|
|
|
|
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
|
|
|
|
|
|
def get_running_instructor_tasks(course_id):
|
|
"""
|
|
Returns a query of InstructorTask objects of running tasks for a given course.
|
|
|
|
Used to generate a list of tasks to display on the instructor dashboard.
|
|
"""
|
|
instructor_tasks = InstructorTask.objects.filter(course_id=course_id)
|
|
# exclude states that are "ready" (i.e. not "running", e.g. failure, success, revoked):
|
|
for state in READY_STATES:
|
|
instructor_tasks = instructor_tasks.exclude(task_state=state)
|
|
return instructor_tasks.order_by('-id')
|
|
|
|
|
|
def get_instructor_task_history(course_id, usage_key=None, student=None, task_type=None):
|
|
"""
|
|
Returns a query of InstructorTask objects of historical tasks for a given course,
|
|
that optionally match a particular problem, a student, and/or a task type.
|
|
"""
|
|
instructor_tasks = InstructorTask.objects.filter(course_id=course_id)
|
|
if usage_key is not None or student is not None:
|
|
_, task_key = encode_problem_and_student_input(usage_key, student)
|
|
instructor_tasks = instructor_tasks.filter(task_key=task_key)
|
|
if task_type is not None:
|
|
instructor_tasks = instructor_tasks.filter(task_type=task_type)
|
|
|
|
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
|
|
"""
|
|
Request a problem to be rescored as a background task.
|
|
|
|
The problem will be rescored for the specified student only. Parameters are the `course_id`,
|
|
the `problem_url`, and the `student` as a User object.
|
|
The url must specify the location of the problem, using i4x-type notation.
|
|
|
|
ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError
|
|
if the problem is already being rescored for this student, 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 arguments: let exceptions return up to the caller.
|
|
check_arguments_for_rescoring(usage_key)
|
|
|
|
task_type = 'rescore_problem'
|
|
task_class = rescore_problem
|
|
task_input, task_key = encode_problem_and_student_input(usage_key, student)
|
|
return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key)
|
|
|
|
|
|
def submit_rescore_problem_for_all_students(request, usage_key): # pylint: disable=invalid-name
|
|
"""
|
|
Request a problem to be rescored as a background task.
|
|
|
|
The problem will be rescored for all students who have accessed the
|
|
particular problem in a course and have provided and checked an answer.
|
|
Parameters are the `course_id` and the `problem_url`.
|
|
The url must specify the location of the problem, using i4x-type notation.
|
|
|
|
ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError
|
|
if the problem is already being rescored, 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 arguments: let exceptions return up to the caller.
|
|
check_arguments_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_problem_and_student_input(usage_key)
|
|
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.
|
|
|
|
The problem's attempts will be reset for all students who have accessed the
|
|
particular problem in a course. Parameters are the `course_id` and
|
|
the `usage_key`, which must be a :class:`Location`.
|
|
|
|
ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError
|
|
if the problem 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 that the usage_key is defined
|
|
# (since that's currently typed in). If the corresponding module descriptor doesn't exist,
|
|
# an exception will be raised. Let it pass up to the caller.
|
|
modulestore().get_item(usage_key)
|
|
|
|
task_type = 'reset_problem_attempts'
|
|
task_class = reset_problem_attempts
|
|
task_input, task_key = encode_problem_and_student_input(usage_key)
|
|
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.
|
|
|
|
The problem's state will be deleted for all students who have accessed the
|
|
particular problem in a course. Parameters are the `course_id` and
|
|
the `usage_key`, which must be a :class:`Location`.
|
|
|
|
ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError
|
|
if the particular problem's state is already being deleted.
|
|
|
|
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 that the usage_key is defined
|
|
# (since that's currently typed in). If the corresponding module descriptor doesn't exist,
|
|
# an exception will be raised. Let it pass up to the caller.
|
|
modulestore().get_item(usage_key)
|
|
|
|
task_type = 'delete_problem_state'
|
|
task_class = delete_problem_state
|
|
task_input, task_key = encode_problem_and_student_input(usage_key)
|
|
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.
|
|
|
|
The specified CourseEmail object will be sent be updated for all students who have enrolled
|
|
in a course. Parameters are the `course_key` and the `email_id`, the id of the CourseEmail object.
|
|
|
|
AlreadyRunningError is raised if the same recipients are already being emailed with the same
|
|
CourseEmail object.
|
|
|
|
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.
|
|
"""
|
|
# Assume that the course is defined, and that the user has already been verified to have
|
|
# appropriate access to the course. But make sure that the email exists.
|
|
# We also pull out the To argument here, so that is displayed in
|
|
# the InstructorTask status.
|
|
email_obj = CourseEmail.objects.get(id=email_id)
|
|
to_option = email_obj.to_option
|
|
|
|
task_type = 'bulk_course_email'
|
|
task_class = send_bulk_course_email
|
|
# Pass in the to_option as a separate argument, even though it's (currently)
|
|
# in the CourseEmail. That way it's visible in the progress status.
|
|
# (At some point in the future, we might take the recipient out of the CourseEmail,
|
|
# so that the same saved email can be sent to different recipients, as it is tested.)
|
|
task_input = {'email_id': email_id, 'to_option': to_option}
|
|
task_key_stub = "{email_id}_{to_option}".format(email_id=email_id, to_option=to_option)
|
|
# create the key value by using MD5 hash:
|
|
task_key = hashlib.md5(task_key_stub).hexdigest()
|
|
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
|
|
|
|
|
|
def submit_calculate_grades_csv(request, course_key):
|
|
"""
|
|
AlreadyRunningError is raised if the course's grades are already being updated.
|
|
"""
|
|
task_type = 'grade_course'
|
|
task_class = calculate_grades_csv
|
|
task_input = {}
|
|
task_key = ""
|
|
|
|
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
|
|
|
|
|
|
def submit_problem_grade_report(request, course_key):
|
|
"""
|
|
Submits a task to generate a CSV grade report containing problem
|
|
values.
|
|
"""
|
|
task_type = 'grade_problems'
|
|
task_class = calculate_problem_grade_report
|
|
task_input = {}
|
|
task_key = ""
|
|
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
|
|
|
|
|
|
def submit_calculate_students_features_csv(request, course_key, features):
|
|
"""
|
|
Submits a task to generate a CSV containing student profile info.
|
|
|
|
Raises AlreadyRunningError if said CSV is already being updated.
|
|
"""
|
|
task_type = 'profile_info_csv'
|
|
task_class = calculate_students_features_csv
|
|
task_input = {'features': features}
|
|
task_key = ""
|
|
|
|
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
|
|
|
|
|
|
def submit_detailed_enrollment_features_csv(request, course_key): # pylint: disable=invalid-name
|
|
"""
|
|
Submits a task to generate a CSV containing detailed enrollment info.
|
|
|
|
Raises AlreadyRunningError if said CSV is already being updated.
|
|
"""
|
|
task_type = 'detailed_enrollment_report'
|
|
task_class = enrollment_report_features_csv
|
|
task_input = {}
|
|
task_key = ""
|
|
|
|
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
|
|
|
|
|
|
def submit_cohort_students(request, course_key, file_name):
|
|
"""
|
|
Request to have students cohorted in bulk.
|
|
|
|
Raises AlreadyRunningError if students are currently being cohorted.
|
|
"""
|
|
task_type = 'cohort_students'
|
|
task_class = cohort_students
|
|
task_input = {'file_name': file_name}
|
|
task_key = ""
|
|
|
|
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
|