179 lines
7.4 KiB
Python
179 lines
7.4 KiB
Python
"""
|
|
WE'RE USING MIGRATIONS!
|
|
|
|
If you make changes to this model, be sure to create an appropriate migration
|
|
file and check it in at the same time as your model changes. To do that,
|
|
|
|
1. Go to the edx-platform dir
|
|
2. ./manage.py schemamigration instructor_task --auto description_of_your_change
|
|
3. Add the migration file created in edx-platform/lms/djangoapps/instructor_task/migrations/
|
|
|
|
|
|
ASSUMPTIONS: modules have unique IDs, even across different module_types
|
|
|
|
"""
|
|
from uuid import uuid4
|
|
import json
|
|
|
|
from django.contrib.auth.models import User
|
|
from django.db import models, transaction
|
|
|
|
|
|
# define custom states used by InstructorTask
|
|
QUEUING = 'QUEUING'
|
|
PROGRESS = 'PROGRESS'
|
|
|
|
|
|
class InstructorTask(models.Model):
|
|
"""
|
|
Stores information about background tasks that have been submitted to
|
|
perform work by an instructor (or course staff).
|
|
Examples include grading and rescoring.
|
|
|
|
`task_type` identifies the kind of task being performed, e.g. rescoring.
|
|
`course_id` uses the course run's unique id to identify the course.
|
|
`task_key` stores relevant input arguments encoded into key value for testing to see
|
|
if the task is already running (together with task_type and course_id).
|
|
`task_input` stores input arguments as JSON-serialized dict, for reporting purposes.
|
|
Examples include url of problem being rescored, id of student if only one student being rescored.
|
|
|
|
`task_id` stores the id used by celery for the background task.
|
|
`task_state` stores the last known state of the celery task
|
|
`task_output` stores the output of the celery task.
|
|
Format is a JSON-serialized dict. Content varies by task_type and task_state.
|
|
|
|
`requester` stores id of user who submitted the task
|
|
`created` stores date that entry was first created
|
|
`updated` stores date that entry was last modified
|
|
"""
|
|
task_type = models.CharField(max_length=50, db_index=True)
|
|
course_id = models.CharField(max_length=255, db_index=True)
|
|
task_key = models.CharField(max_length=255, db_index=True)
|
|
task_input = models.CharField(max_length=255)
|
|
task_id = models.CharField(max_length=255, db_index=True) # max_length from celery_taskmeta
|
|
task_state = models.CharField(max_length=50, null=True, db_index=True) # max_length from celery_taskmeta
|
|
task_output = models.CharField(max_length=1024, null=True)
|
|
requester = models.ForeignKey(User, db_index=True)
|
|
created = models.DateTimeField(auto_now_add=True, null=True)
|
|
updated = models.DateTimeField(auto_now=True)
|
|
subtasks = models.TextField(blank=True) # JSON dictionary
|
|
|
|
def __repr__(self):
|
|
return 'InstructorTask<%r>' % ({
|
|
'task_type': self.task_type,
|
|
'course_id': self.course_id,
|
|
'task_input': self.task_input,
|
|
'task_id': self.task_id,
|
|
'task_state': self.task_state,
|
|
'task_output': self.task_output,
|
|
},)
|
|
|
|
def __unicode__(self):
|
|
return unicode(repr(self))
|
|
|
|
@classmethod
|
|
def create(cls, course_id, task_type, task_key, task_input, requester):
|
|
"""
|
|
Create an instance of InstructorTask.
|
|
|
|
The InstructorTask.save_now 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.
|
|
"""
|
|
# create the task_id here, and pass it into celery:
|
|
task_id = str(uuid4())
|
|
|
|
json_task_input = json.dumps(task_input)
|
|
|
|
# check length of task_input, and return an exception if it's too long:
|
|
if len(json_task_input) > 255:
|
|
fmt = 'Task input longer than 255: "{input}" for "{task}" of "{course}"'
|
|
msg = fmt.format(input=json_task_input, task=task_type, course=course_id)
|
|
raise ValueError(msg)
|
|
|
|
# create the task, then save it:
|
|
instructor_task = cls(
|
|
course_id=course_id,
|
|
task_type=task_type,
|
|
task_id=task_id,
|
|
task_key=task_key,
|
|
task_input=json_task_input,
|
|
task_state=QUEUING,
|
|
requester=requester
|
|
)
|
|
instructor_task.save_now()
|
|
|
|
return instructor_task
|
|
|
|
@transaction.autocommit
|
|
def save_now(self):
|
|
"""
|
|
Writes InstructorTask immediately, ensuring the transaction is committed.
|
|
|
|
Autocommit annotation makes sure the database entry is committed.
|
|
When called from any view that is wrapped by TransactionMiddleware,
|
|
and thus in a "commit-on-success" transaction, this autocommit 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.
|
|
"""
|
|
self.save()
|
|
|
|
@staticmethod
|
|
def create_output_for_success(returned_result):
|
|
"""
|
|
Converts successful result to output format.
|
|
|
|
Raises a ValueError exception if the output is too long.
|
|
"""
|
|
# In future, there should be a check here that the resulting JSON
|
|
# will fit in the column. In the meantime, just return an exception.
|
|
json_output = json.dumps(returned_result)
|
|
if len(json_output) > 1023:
|
|
raise ValueError("Length of task output is too long: {0}".format(json_output))
|
|
return json_output
|
|
|
|
@staticmethod
|
|
def create_output_for_failure(exception, traceback_string):
|
|
"""
|
|
Converts failed result information to output format.
|
|
|
|
Traceback information is truncated or not included if it would result in an output string
|
|
that would not fit in the database. If the output is still too long, then the
|
|
exception message is also truncated.
|
|
|
|
Truncation is indicated by adding "..." to the end of the value.
|
|
"""
|
|
tag = '...'
|
|
task_progress = {'exception': type(exception).__name__, 'message': str(exception.message)}
|
|
if traceback_string is not None:
|
|
# truncate any traceback that goes into the InstructorTask model:
|
|
task_progress['traceback'] = traceback_string
|
|
json_output = json.dumps(task_progress)
|
|
# if the resulting output is too long, then first shorten the
|
|
# traceback, and then the message, until it fits.
|
|
too_long = len(json_output) - 1023
|
|
if too_long > 0:
|
|
if traceback_string is not None:
|
|
if too_long >= len(traceback_string) - len(tag):
|
|
# remove the traceback entry entirely (so no key or value)
|
|
del task_progress['traceback']
|
|
too_long -= (len(traceback_string) + len('traceback'))
|
|
else:
|
|
# truncate the traceback:
|
|
task_progress['traceback'] = traceback_string[:-(too_long + len(tag))] + tag
|
|
too_long = 0
|
|
if too_long > 0:
|
|
# we need to shorten the message:
|
|
task_progress['message'] = task_progress['message'][:-(too_long + len(tag))] + tag
|
|
json_output = json.dumps(task_progress)
|
|
return json_output
|
|
|
|
@staticmethod
|
|
def create_output_for_revoked():
|
|
"""Creates standard message to store in output format for revoked tasks."""
|
|
return json.dumps({'message': 'Task revoked before running'})
|