Source code for ievv_opensource.ievv_batchframework.models

import json

from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone


[docs]class BatchOperationManager(models.Manager): """ Manager for :class:`.BatchOperation`. """ def __create(self, input_data, **kwargs): batchoperation = BatchOperation(**kwargs) if input_data: batchoperation.input_data = input_data batchoperation.clean() batchoperation.save() return batchoperation
[docs] def create_synchronous(self, input_data=None, **kwargs): """ Create a synchronous :class:`.BatchOperation`. An synchronous batch operation starts with :obj:`.BatchOperation.status` set to :obj:`.BatchOperation.STATUS_RUNNING` and ``started_running_datetime`` set just as if :meth:`.BatchOperation.mark_as_running` was called. So calling this would have the same result as calling :meth:`.create_asynchronous` and then calling :meth:`.BatchOperation.mark_as_running`, but this will just use one database query instead of two. The :class:`.BatchOperation` is cleaned before it is saved. Args: input_data: The input data. A python object to set as the input data using the :meth:`.BatchOperation.input_data` property. **kwargs: Forwarded to the constructor for :class:`.BatchOperation`. Returns: BatchOperation: The created BatchOperation object. """ return self.__create(input_data=input_data, status=BatchOperation.STATUS_RUNNING, started_running_datetime=timezone.now(), **kwargs)
[docs] def create_asynchronous(self, input_data=None, **kwargs): """ Create an asynchronous :class:`.BatchOperation`. An asynchronous batch operation starts with :obj:`.BatchOperation.status` set to :obj:`.BatchOperation.STATUS_UNPROCESSED`. The :class:`.BatchOperation` is cleaned before it is saved. Args: input_data: The input data. A python object to set as the input data using the :meth:`.BatchOperation.input_data` property. **kwargs: Forwarded to the constructor for :class:`.BatchOperation`. Returns: BatchOperation: The created BatchOperation object. """ return self.__create(input_data=input_data, **kwargs)
[docs]class BatchOperation(models.Model): """ Defines a batch operation. """ objects = BatchOperationManager() #: One of the possible values for :obj:`~.BatchOperation.status`. #: Defines the BatchOperation as uprocessed (not yet started). #: This only makes sense for background tasks. They will typically #: be created with the unprocessed status, and then set to #: :obj:`~.BatchOperation.STATUS_RUNNING` when the batching service #: starts running the operation. STATUS_UNPROCESSED = 'unprocessed' #: One of the possible values for :obj:`~.BatchOperation.status`. #: Defines the BatchOperation as running (in progress). STATUS_RUNNING = 'running' #: One of the possible values for :obj:`~.BatchOperation.status`. #: Defines the BatchOperation as finished. STATUS_FINISHED = 'finished' #: Allowed values for :obj:`~.BatchOperation.status`. #: Possible values are: #: #: - :obj:`~.BatchOperation.STATUS_UNPROCESSED`. #: - :obj:`~.BatchOperation.STATUS_RUNNING`. #: - :obj:`~.BatchOperation.STATUS_FINISHED`. STATUS_CHOICES = [ (STATUS_UNPROCESSED, 'unprocessed'), (STATUS_RUNNING, 'running'), (STATUS_FINISHED, 'finished'), ] #: One of the possible values for :obj:`~.BatchOperation.result`. #: This is used when we have no result yet (the operation is not finished). RESULT_NOT_AVAILABLE = 'not-available' #: One of the possible values for :obj:`~.BatchOperation.result`. #: Defines the BatchOperation as failed. This is set if the operation #: could not be completed because of an error. Any details about the #: result of the operation can be stored in :obj:`~.BatchOperation.output_data_json`. RESULT_SUCCESSFUL = 'successful' #: One of the possible values for :obj:`~.BatchOperation.result`. #: Defines the BatchOperation as failed. This is set if the operation #: could not be completed because of an error. Any error message(s) #: should be stored in :obj:`~.BatchOperation.output_data_json`. RESULT_FAILED = 'failed' #: Allowed values for :obj:`~.BatchOperation.result`. #: Possible values are: #: #: - :obj:`~.BatchOperation.RESULT_NOT_AVAILABLE`. #: - :obj:`~.BatchOperation.RESULT_SUCCESSFUL`. #: - :obj:`~.BatchOperation.RESULT_FAILED`. RESULT_CHOICES = [ (RESULT_NOT_AVAILABLE, 'not available yet (processing not finished)'), (RESULT_SUCCESSFUL, 'successful'), (RESULT_FAILED, 'failed'), ] #: The user that started this batch operation. #: Optional, but it is good metadata to add for debugging. started_by = models.ForeignKey( to=settings.AUTH_USER_MODEL, null=True, blank=True) #: The datetime when this batch operation was created. #: Defaults to ``timezone.now()``. created_datetime = models.DateTimeField( default=timezone.now) #: The datetime when this batch operation started running. #: This is not the same as :obj:`~.BatchOperation.created_datetime`, #: this is the time when the operation started processing. started_running_datetime = models.DateTimeField( null=True, blank=True) #: The datetime when this batch operation was finished. finished_datetime = models.DateTimeField( null=True, blank=True) # #: Is this an asynchronous operation? Set this to ``True`` for # #: background tasks (such as Celery tasks). # asynchronous = models.BooleanField( # default=False) #: The content type for :obj:`~.BatchOperation.context_object`. context_content_type = models.ForeignKey( to=ContentType, on_delete=models.CASCADE, null=True, blank=True) #: The id field for :obj:`~.BatchOperation.context_object`. context_object_id = models.PositiveIntegerField( null=True, blank=True) #: Generic foreign key that identifies the context this operation #: runs in. This is optional. context_object = GenericForeignKey('context_content_type', 'context_object_id') #: The type of operation. This is application specific - you #: typically use this if you allow multiple different batch operations #: on the same :obj:`~BatchOperation.context_object`. #: This is not required, and defaults to empty string. operationtype = models.CharField( max_length=255, db_index=True, null=False, blank=True, default='') #: The status of the operation. #: The allowed values for this field is documented in #: :obj:`~.BatchOperation.STATUS_CHOICES`. #: Defaults to :obj:`~.BatchOperation.STATUS_UNPROCESSED`. status = models.CharField( max_length=12, choices=STATUS_CHOICES, default=STATUS_UNPROCESSED) #: The result of the operation. #: The allowed values for this field is documented in #: :obj:`~.BatchOperation.RESULT_CHOICES`. #: Defaults to :obj:`~.BatchOperation.RESULT_NOT_AVAILABLE`. result = models.CharField( max_length=13, choices=RESULT_CHOICES, default=RESULT_NOT_AVAILABLE) #: Input data for the BatchOperation. #: You should not use this directly, use the :obj:`~.BatchOperation.input_data` #: property instead. input_data_json = models.TextField( null=False, blank=True, default='') #: Output data for the BatchOperation. #: You should not use this directly, use the :obj:`~.BatchOperation.output_data` #: property instead. output_data_json = models.TextField( null=False, blank=True, default='') class Meta: ordering = ['-created_datetime'] @property def input_data(self): """ Decode :obj:`.BatchOperation.input_data_json` and return the result. Return `None` if input_data_json is empty. """ if self.input_data_json: if not hasattr(self, '_input_data'): # Store the decoded input_data to avoid re-decoding the json for # each access. We invalidate this cache in the setter. self._input_data = json.loads(self.input_data_json) return self._input_data else: return None @input_data.setter def input_data(self, input_data): """ Set :obj:`.BatchOperation.input_data_json`. Encodes the given input_data using `json.dumps`. """ self.input_data_json = json.dumps(input_data) if hasattr(self, '_input_data'): delattr(self, '_input_data') @property def output_data(self): """ Decode :obj:`.BatchOperation.output_data_json` and return the result. Returns: object: `None` if output_data_json is empty, or the decoded json data if the ``output_data`` is not empty. """ if self.output_data_json: if not hasattr(self, '_output_data'): # Store the decoded output_data to avoid re-decoding the json for # each access. We invalidate this cache in the setter. self._output_data = json.loads(self.output_data_json) return self._output_data else: return None @output_data.setter def output_data(self, output_data): """ Set :obj:`.BatchOperation.output_data_json`. Encodes the given output_data using `json.dumps`. Returns: object: `None` if output_data_json is empty, or the decoded json data if the ``output_data`` is not empty. """ self.output_data_json = json.dumps(output_data) if hasattr(self, '_output_data'): delattr(self, '_output_data')
[docs] def mark_as_running(self): """ Mark the batch operation as running. Sets the :obj:`.status` to :obj:`.STATUS_RUNNING`, :obj:`.started_running_datetime` to the current datetime, clean and save. """ self.status = self.STATUS_RUNNING self.started_running_datetime = timezone.now() self.clean() self.save()
[docs] def finish(self, failed=False, output_data=None): """ Mark the bulk operation as finished. Sets :obj:`.result` as documented in the ``failed`` parameter below. Sets :obj:`.finished_datetime` to the current datetime. Sets :obj:`.output_data_json` as documented in the ``output_data`` parameter below. Args: failed (boolean): Set this to ``False`` to set :obj:`.result` to :obj:`.RESULT_FAILED`. The default is ``True``, which means that :obj:`.result` is set to :obj:`.RESULT_SUCCESSFUL` output_data: The output data. A python object to set as the output data using the :meth:`.BatchOperation.output_data` property. """ if failed: self.result = self.RESULT_FAILED else: self.result = self.RESULT_SUCCESSFUL if output_data: self.output_data = output_data self.status = self.STATUS_FINISHED self.finished_datetime = timezone.now() self.clean() self.save()
[docs] def clean(self): if self.status == self.STATUS_FINISHED and self.result == self.RESULT_NOT_AVAILABLE: raise ValidationError({ 'result': 'Must be "successful" or "failed" when status is "finished".' }) if self.status != self.STATUS_UNPROCESSED and self.started_running_datetime is None: raise ValidationError({ 'started_running_datetime': 'Can not be None when status is "running" or "finished".' })
def __str__(self): return \ '#{id}-{operationtype}({created_datetime})[{status}, {result}]' \ ' - {context_content_type}#{context_object_id}'.format( id=self.id, operationtype=self.operationtype, created_datetime=self.created_datetime, status=self.status, result=self.result, context_content_type=self.context_content_type, context_object_id=self.context_object_id )