def _update_item_status( s_item: ScheduledOperation, run_result: List[str], debug: bool, ): """Update the status of the scheduled item. :param s_item: Scheduled item :return: Nothing """ now = datetime.now(pytz.timezone(settings.TIME_ZONE)) if run_result is None: s_item.status = ScheduledOperation.STATUS_DONE_ERROR else: if s_item.execute_until and s_item.execute_until > now: # There is a second date/time and is not passed yet! s_item.status = ScheduledOperation.STATUS_PENDING # Update exclude values s_item.exclude_values.extend(run_result) else: s_item.status = ScheduledOperation.STATUS_DONE # Save the new status in the DB s_item.save() if debug: logger.info('Status set to %s', s_item.status)
def schedule_task(s_item: models.ScheduledOperation): """Create the task corresponding to the given scheduled item. :param s_item: Scheduled operation item being processed. :return: Creates a new task in the PeriodicTask table If the s_item already contains a pointer to a task in PeriodicTasks, it is canceled. If the s_item status is not PENDING, no new task is created. """ enabled = True if s_item.task: # Preserve the enabled flag in the old task, otherwise is reset enabled = s_item.task.enabled s_item.delete_task() msg = s_item.are_times_valid() if msg: raise errors.OnTaskScheduleIncorrectTimes(msg) # Case of a single execution in the future if s_item.execute and not s_item.frequency and not s_item.execute_until: # Case 5 clocked_item, __ = ClockedSchedule.objects.get_or_create( clocked_time=s_item.execute) task_id = PeriodicTask.objects.create( clocked=clocked_item, one_off=True, name=ONTASK_SCHEDULED_TASK_NAME_TEMPLATE.format(s_item.id), task='ontask.tasks.scheduled_ops.execute_scheduled_operation', args=json.dumps([s_item.id]), enabled = enabled) else: # Cases 3, 4, 7 and 8: crontab execution crontab_items = s_item.frequency.split() crontab_item, __ = CrontabSchedule.objects.get_or_create( minute=crontab_items[0], hour=crontab_items[1], day_of_week=crontab_items[2], day_of_month=crontab_items[3], month_of_year=crontab_items[4]) task_id = PeriodicTask.objects.create( crontab=crontab_item, name=ONTASK_SCHEDULED_TASK_NAME_TEMPLATE.format(s_item.id), task='ontask.tasks.scheduled_ops.execute_scheduled_operation', args=json.dumps([s_item.id]), enabled = enabled) models.ScheduledOperation.objects.filter(pk=s_item.id).update(task=task_id) s_item.refresh_from_db(fields=['task'])
def _update_item_status(s_item: models.ScheduledOperation): """Update the status of the scheduled item. :param s_item: Scheduled item :return: Nothing """ now = datetime.now(pytz.timezone(settings.TIME_ZONE)) if s_item.frequency and (not s_item.execute_until or now < s_item.execute_until): new_status = models.scheduler.STATUS_PENDING else: new_status = models.scheduler.STATUS_DONE # Save the new status in the DB models.ScheduledOperation.objects.filter(pk=s_item.id).update( status=new_status) s_item.refresh_from_db(fields=['status']) if settings.DEBUG: CELERY_LOGGER.info('Status set to %s', s_item.status)
def test_scheduled_json_action(self): """Create a scheduled send list action and execute it.""" token = 'fake token' OnTaskSharedState.json_outbox = [] settings.EXECUTE_ACTION_JSON_TRANSFER = False user = get_user_model().objects.get(email='*****@*****.**') # User must exist self.assertIsNotNone(user, 'User [email protected] not found') action = Action.objects.get(name='send json') scheduled_item = ScheduledOperation( user=user, name='JSON scheduled action', action=action, execute=datetime.now(pytz.timezone(settings.TIME_ZONE)).replace( second=0), status=ScheduledOperation.STATUS_PENDING, item_column=action.workflow.columns.get(name='email'), payload={'token': token}) scheduled_item.save() # Execute the scheduler tasks.execute_scheduled_actions_task(True) scheduled_item.refresh_from_db() json_outbox = OnTaskSharedState.json_outbox assert scheduled_item.status == ScheduledOperation.STATUS_DONE assert len(json_outbox) == 3 assert all(item['target'] == action.target_url for item in json_outbox) assert all(token in item['auth'] for item in json_outbox)
def test_scheduled_email_action(self): """Create a scheduled send email action and execute it.""" user = get_user_model().objects.get(email='*****@*****.**') # User must exist self.assertIsNotNone(user, 'User [email protected] not found') action = Action.objects.get(name='send email') scheduled_item = ScheduledOperation( user=user, name='send email action', action=action, execute=datetime.now(pytz.timezone(settings.TIME_ZONE)).replace( second=0), status=ScheduledOperation.STATUS_PENDING, item_column=action.workflow.columns.get(name='email'), payload={ 'subject': 'Email subject', 'cc_email': '', 'bcc_email': '', 'send_confirmation': False, 'track_read': False}) scheduled_item.save() # Execute the scheduler tasks.execute_scheduled_actions_task(True) scheduled_item.refresh_from_db() assert scheduled_item.status == ScheduledOperation.STATUS_DONE assert len(mail.outbox) == 2 assert 'Hi Student Two' in mail.outbox[0].body assert 'Hi Student Three' in mail.outbox[1].body
def test_scheduled_send_list_action(self): """Create a scheduled send list action and execute it.""" user = get_user_model().objects.get(email='*****@*****.**') # User must exist self.assertIsNotNone(user, 'User [email protected] not found') action = Action.objects.get(name='send list') scheduled_item = ScheduledOperation( user=user, name='send list scheduled action', action=action, execute=datetime.now(pytz.timezone(settings.TIME_ZONE)).replace( second=0), status=ScheduledOperation.STATUS_PENDING, payload={ 'email_to': '*****@*****.**', 'subject': 'Action subject', 'cc_email': '', 'bcc_email': ''}) scheduled_item.save() # Execute the scheduler tasks.execute_scheduled_actions_task(True) scheduled_item.refresh_from_db() assert scheduled_item.status == ScheduledOperation.STATUS_DONE assert len(mail.outbox) == 1 assert ( '[email protected], [email protected]' in mail.outbox[0].body)
def finish( self, request: http.HttpRequest, payload: SessionPayload, schedule_item: models.ScheduledOperation = None, ) -> Optional[http.HttpResponse]: """Finalize the creation of a scheduled operation. All required data is passed through the payload. :param request: Request object received :param schedule_item: ScheduledOperation item being processed. If None, it has to be extracted from the information in the payload. :param payload: Dictionary with all the required data coming from previous requests. :return: Http Response """ s_item_id = payload.pop('schedule_id', None) schedule_item = None if s_item_id: # Get the item being processed if not schedule_item: schedule_item = models.ScheduledOperation.objects.filter( id=s_item_id).first() if not schedule_item: messages.error(request, _('Incorrect request for operation scheduling')) return redirect('action:index') else: action = models.Action.objects.get(pk=payload.pop('action_id')) payload['workflow'] = action.workflow payload['action'] = action # Remove some parameters from the payload for key in [ 'button_label', 'valuerange', 'step', 'prev_url', 'post_url', 'confirm_items', 'action_id', 'page_title', ]: payload.pop(key, None) try: schedule_item = self.create_or_update(request.user, payload.get_store(), schedule_item) except Exception as exc: messages.error( request, str(_('Unable to create scheduled operation: {0}')).format( str(exc))) return redirect('action:index') schedule_item.log(models.Log.SCHEDULE_EDIT) # Reset object to carry action info throughout dialogs SessionPayload.flush(request.session) # Successful processing. tdelta = create_timedelta_string(schedule_item.execute, schedule_item.frequency, schedule_item.execute_until) return render(request, 'scheduler/schedule_done.html', { 'tdelta': tdelta, 's_item': schedule_item })
def test_scheduled_incremental_email_action(self): """Test an incremental scheduled action.""" # Modify the data table so that initially all records have registered # equal to alse workflow = Workflow.objects.all().first() with connection.cursor() as cursor: query = sql.SQL('UPDATE {0} SET {1} = false').format( sql.Identifier(workflow.get_data_frame_table_name()), sql.Identifier('registered')) cursor.execute(query) user = get_user_model().objects.get(email='*****@*****.**') # User must exist self.assertIsNotNone(user, 'User [email protected] not found') action = Action.objects.get(name='send email incrementally') now = datetime.now(pytz.timezone(settings.TIME_ZONE)).replace(second=0) scheduled_item = ScheduledOperation( user=user, name='send email action incrementally', action=action, execute=now, execute_until=now + timedelta(hours=1), status=ScheduledOperation.STATUS_PENDING, item_column=action.workflow.columns.get(name='email'), payload={ 'subject': 'Email subject', 'cc_email': '', 'bcc_email': '', 'send_confirmation': False, 'track_read': False}) scheduled_item.save() # Execute the scheduler for the first time tasks.execute_scheduled_actions_task(True) # Event stil pending, with no values in exclude values scheduled_item.refresh_from_db() assert scheduled_item.status == ScheduledOperation.STATUS_PENDING assert scheduled_item.exclude_values == [] # Modify one of the values in the matrix with connection.cursor() as cursor: query = sql.SQL( 'UPDATE {0} SET {1} = true WHERE {2} = {3}').format( sql.Identifier(workflow.get_data_frame_table_name()), sql.Identifier('registered'), sql.Identifier('email'), sql.Placeholder()) cursor.execute(query, ['*****@*****.**']) # Execute the scheduler for the first time tasks.execute_scheduled_actions_task(True) # Event stil pending, with no values in exclude values scheduled_item.refresh_from_db() assert scheduled_item.status == ScheduledOperation.STATUS_PENDING assert scheduled_item.exclude_values == ['*****@*****.**'] # Modify one of the values in the matrix with connection.cursor() as cursor: query = sql.SQL( 'UPDATE {0} SET {1} = true WHERE {2} = {3}').format( sql.Identifier(workflow.get_data_frame_table_name()), sql.Identifier('registered'), sql.Identifier('email'), sql.Placeholder()) cursor.execute(query, ['*****@*****.**']) # Execute the scheduler for the first time tasks.execute_scheduled_actions_task(True) # Event stil pending, with no values in exclude values scheduled_item.refresh_from_db() assert scheduled_item.status == ScheduledOperation.STATUS_PENDING assert scheduled_item.exclude_values == [ '*****@*****.**', '*****@*****.**'] # Modify one of the values in the matrix with connection.cursor() as cursor: query = sql.SQL( 'UPDATE {0} SET {1} = true WHERE {2} = {3}').format( sql.Identifier(workflow.get_data_frame_table_name()), sql.Identifier('registered'), sql.Identifier('email'), sql.Placeholder()) cursor.execute(query, ['*****@*****.**']) # Execute the scheduler for the first time tasks.execute_scheduled_actions_task(True) # Event stil pending, with no values in exclude values scheduled_item.refresh_from_db() assert scheduled_item.status == ScheduledOperation.STATUS_PENDING assert scheduled_item.exclude_values == [ '*****@*****.**', '*****@*****.**', '*****@*****.**'] # Execute the scheduler for the first time tasks.execute_scheduled_actions_task(True) # Event stil pending, with no values in exclude values scheduled_item.refresh_from_db() assert scheduled_item.status == ScheduledOperation.STATUS_PENDING assert scheduled_item.exclude_values == [ '*****@*****.**', '*****@*****.**', '*****@*****.**']
def delete_item(s_item: models.ScheduledOperation): """Delete a scheduled operation and log the event.""" s_item.log(models.Log.SCHEDULE_DELETE) s_item.delete()
def instantiate_or_update( self, validated_data, action, execute, item_column, exclude_values, payload, scheduled_obj=None, ): """Instantiate or update the object of class ScheduledOperation. Given the validated data and a set of parameters that have been validated, instantiate or update the object of class ScheduledOperation. :param validated_data: Data obtained by the serializer :param action: Action object :param execute: Execution date/time :param item_column: Item column object (if given) :param exclude_values: List of values from item_column to exluce :param payload: JSON object :param scheduled_obj: Object to instantiate or update :return: instantiated object """ if not scheduled_obj: scheduled_obj = ScheduledOperation() scheduled_obj.user = self.context['request'].user scheduled_obj.name = validated_data['name'] scheduled_obj.description_text = validated_data.get('description_text') scheduled_obj.action = action scheduled_obj.execute = execute scheduled_obj.item_column = item_column scheduled_obj.exclude_values = exclude_values scheduled_obj.payload = payload scheduled_obj.status = ScheduledOperation.STATUS_PENDING scheduled_obj.save() return scheduled_obj
def finish_scheduling( request: HttpRequest, schedule_item: ScheduledOperation = None, payload: Dict = None, ): """Finalize the creation of a scheduled action. All required data is passed through the payload. :param request: Request object received :param schedule_item: ScheduledOperation item being processed. If None, it has to be extracted from the information in the payload. :param payload: Dictionary with all the required data coming from previous requests. :return: """ # Get the payload from the session if not given if payload is None: payload = request.session.get(action_session_dictionary) # If there is no payload, something went wrong. if payload is None: # Something is wrong with this execution. Return to action table. messages.error( request, _('Incorrect action scheduling invocation.')) return redirect('action:index') # Get the scheduled item if needed s_item_id = payload.pop('schedule_id', None) action = Action.objects.get(pk=payload.pop('action_id')) column_name = payload.pop('item_column', None) column = None if column_name: column = action.workflow.columns.get(name=column_name) # Clean up some parameters from the payload payload = { key: payload[key] for key in payload if key not in [ 'button_label', 'valuerange', 'step', 'prev_url', 'post_url', 'confirm_items']} # Create the payload to record the event in the log log_payload = payload.copy() if s_item_id: # Get the item being processed if not schedule_item: schedule_item = ScheduledOperation.objects.filter( id=s_item_id).first() if not schedule_item: messages.error( None, _('Incorrect request in action scheduling')) return redirect('action:index') schedule_item.name = payload.pop('name') schedule_item.description_text = payload.pop('description_text') schedule_item.item_column = column schedule_item.execute = parse_datetime(payload.pop('execute')) schedule_item.execute_until = parse_datetime( payload.pop('execute_until')) schedule_item.exclude_values = payload.pop('exclude_values', []) else: schedule_item = ScheduledOperation( user=request.user, action=action, name=payload.pop('name'), description_text=payload.pop('description_text'), operation_type=ScheduledOperation.ACTION_RUN, item_column=column, execute=parse_datetime(payload.pop('execute')), execute_until=parse_datetime(payload.pop('execute_until')), exclude_values=payload.pop('exclude_values', [])) # Check for exclude schedule_item.status = ScheduledOperation.STATUS_PENDING schedule_item.payload = payload schedule_item.save() # Create the payload to record the event in the log log_type = LOG_TYPE_DICT.get(schedule_item.action.action_type) if not log_type: messages.error( request, _('This type of actions cannot be scheduled')) return redirect('action:index') schedule_item.log(log_type) # Reset object to carry action info throughout dialogs set_action_payload(request.session) request.session.save() # Successful processing. is_executing, tdelta = create_timedelta_string( schedule_item.execute, schedule_item.execute_until) return render( request, 'scheduler/schedule_done.html', { 'is_executing': is_executing, 'tdelta': tdelta, 's_item': schedule_item})