def _send_and_log_json( user, action: models.Action, json_obj: str, headers: Mapping, ): """Send a JSON object to the action URL and LOG event.""" if settings.EXECUTE_ACTION_JSON_TRANSFER: http_resp = requests.post(url=action.target_url, data=json_obj, headers=headers) status_val = http_resp.status_code else: payload = { 'target': action.target_url, 'text': json.dumps(json_obj), 'auth': headers['Authorization'] } LOGGER.info('SEND JSON(%s): %s', action.target_url, payload['text']) if getattr(OnTaskSharedState, 'json_outbox', None): OnTaskSharedState.json_outbox.append(payload) else: OnTaskSharedState.json_outbox = [payload] status_val = 200 # Log seng object action.log(user, models.Log.ACTION_JSON_SENT, action=action.id, object=json.dumps(json_obj), status=status_val, json_sent_datetime=str( datetime.datetime.now(pytz.timezone(settings.TIME_ZONE))))
def _send_confirmation_message( user, action: models.Action, nmsgs: int, ) -> None: """Send the confirmation message. :param user: Destination email :param action: Action being considered :param nmsgs: Number of messages being sent :return: """ # Creating the context for the confirmation email now = datetime.datetime.now(pytz.timezone(settings.TIME_ZONE)) cfilter = action.get_filter() context = { 'user': user, 'action': action, 'num_messages': nmsgs, 'email_sent_datetime': now, 'filter_present': cfilter is not None, 'num_rows': action.workflow.nrows, 'num_selected': action.get_rows_selected(), } # Create template and render with context try: html_content = Template( str(getattr(ontask_settings, 'NOTIFICATION_TEMPLATE')), ).render(Context(context)) text_content = strip_tags(html_content) except TemplateSyntaxError as exc: raise Exception( _('Syntax error in notification template ({0})').format( str(exc)), ) # Log the event context = { 'num_messages': nmsgs, 'email_sent_datetime': str(now), 'filter_present': cfilter is not None, 'num_rows': action.workflow.nrows, 'subject': str(ontask_settings.NOTIFICATION_SUBJECT), 'body': text_content, 'from_email': str(ontask_settings.NOTIFICATION_SENDER), 'to_email': [user.email] } action.log(user, models.Log.ACTION_EMAIL_NOTIFY, **context) # Send email out try: send_mail(str(ontask_settings.NOTIFICATION_SUBJECT), text_content, str(ontask_settings.NOTIFICATION_SENDER), [user.email], html_message=html_content) except Exception as exc: raise Exception( _('Error when sending the notification: {0}').format(str(exc)), )
def serve_action_out( user, action: Action, user_attribute_name: str, ): """Serve request for an action out. Function that given a user and an Action Out searches for the appropriate data in the table with the given attribute name equal to the user email and returns the HTTP response. :param user: User object making the request :param action: Action to execute (action out) :param user_attribute_name: Column to check for email :return: """ # For the response payload = {'action': action.name, 'action_id': action.id} # User_instance has the record used for verification row_values = get_row_values(action, (user_attribute_name, user.email)) # Get the dictionary containing column names, attributes and condition # valuations: context = get_action_evaluation_context(action, row_values) error = '' if context is None: # Log the event action.log( user, Log.ACTION_SERVED_EXECUTE, error=_('Error when evaluating conditions for user {0}').format( user.email)) return HttpResponse( render_to_string('action/action_unavailable.html', {})) # Evaluate the action content. action_content = evaluate_row_action_out(action, context) # If the action content is empty, forget about it response = action_content if action_content is None: response = render_to_string('action/action_unavailable.html', {}) error = _('Action not enabled for user {0}').format(user.email) # Log the event action.log(user, Log.ACTION_SERVED_EXECUTE, error=_('Error when evaluating conditions for user {0}').format( user.email)) # Respond the whole thing return HttpResponse(response)
def process_edit_request( self, request: http.HttpRequest, workflow: models.Workflow, action: models.Action ) -> http.HttpResponse: """Process the action edit request.""" form = self.edit_form_class(request.POST or None, instance=action) form_filter = condition_forms.FilterForm( request.POST or None, instance=action.get_filter(), action=action ) # Processing the request after receiving the text from the editor if ( request.method == 'POST' and form.is_valid() and form_filter.is_valid() ): # Log the event action.log(request.user, models.Log.ACTION_UPDATE) # Text is good. Update the content of the action action.set_text_content(form.cleaned_data['text_content']) if 'target_url' in form.cleaned_data: action.target_url = form.cleaned_data['target_url'] action.save(update_fields=['target_url']) if request.POST['Submit'] == 'Submit': return redirect(request.get_full_path()) return redirect('action:index') # This is a GET request or a faulty POST request context = self.get_render_context(action, form, form_filter) try: self.extend_edit_context(workflow, action, context) except Exception as exc: messages.error(request, str(exc)) return redirect(reverse('action:index')) # Return the same form in the same page return render(request, self.edit_template, context=context)
def run_send_list_action( req: HttpRequest, workflow: Workflow, action: Action, ) -> HttpResponse: """Request data to send a list. Form asking for subject line and target email. :param req: HTTP request (GET) :param workflow: workflow being processed :param action: Action being run :return: HTTP response """ # Create the payload object with the required information action_info = SendListPayload({'action_id': action.id}) # Create the form to ask for the email subject and other information form = SendListActionForm( req.POST or None, action=action, form_info=action_info) # Request is a POST and is valid if req.method == 'POST' and form.is_valid(): # Log the event log_item = action.log( req.user, Log.ACTION_RUN_SEND_LIST, from_email=req.user.email, recipient_email=action_info['email_to'] , subject=action_info['subject'], cc_email=action_info['cc_email'], bcc_email=action_info['bcc_email']) # Send the emails! run_task.delay(req.user.id, log_item.id, action_info.get_store()) # Successful processing. return render( req, 'action/action_done.html', {'log_id': log_item.id, 'download': action_info['export_wf']}) # Render the form return render( req, 'action/request_send_list_data.html', {'action': action, 'form': form, 'valuerange': range(2)})
def _create_log_event( self, user, action: models.Action, payload: Optional[Dict] = None, log_item: Optional[models.Log] = None, ): """Create an ACTION RUN log if needed.""" if log_item or not self.log_event: return log_item log_payload = dict(payload) log_payload['operation_type'] = self.log_event log_payload['action'] = action.name return action.log(user, **log_payload)
def run_json_list_action( req: HttpRequest, workflow: Workflow, action: Action, ) -> HttpResponse: """Request data to send JSON objects. Form asking for token and export wf :param req: HTTP request (GET) :param workflow: workflow being processed :param action: Action begin run :return: HTTP response """ # Get the payload from the session, and if not, use the given one action_info = JSONListPayload({'action_id': action.id}) # Create the form to ask for the email subject and other information form = JSONListActionForm(req.POST or None, form_info=action_info) if req.method == 'POST' and form.is_valid(): # Log the event log_item = action.log(req.user, Log.ACTION_RUN_JSON_LIST, exported_workflow=action_info['export_wf']) # Send the objects run_task.delay(req.user.id, log_item.id, action_info.get_store()) # Reset object to carry action info throughout dialogs set_action_payload(req.session) req.session.save() # Successful processing. return render(req, 'action/action_done.html', { 'log_id': log_item.id, 'download': action_info['export_wf'] }) # Render the form return render( req, 'action/request_json_list_data.html', { 'action': action, 'num_msgs': action.get_rows_selected(), 'form': form, 'valuerange': range(2), 'rows_all_false': action.get_row_all_false_count() })
def update_row_values( request: http.HttpRequest, action: models.Action, row_data: Tuple[List, List, str, Any], ): """Serve a request for action in. Function that given a request, and an action IN, it performs the lookup and data input of values. :param request: HTTP request :param action: Action In :param row_data: Tuple containing keys, values, where_field, where_value. Keys and values are the values in the row. Where field, and where value is pair find the given row :return: """ keys, values, where_field, where_value = row_data # Execute the query sql.update_row( action.workflow.get_data_frame_table_name(), keys, values, filter_dict={where_field: where_value}, ) # Recompute all the values of the conditions in each of the actions # TODO: Explore how to do this asynchronously (or lazy) for act in action.workflow.actions.all(): act.update_n_rows_selected() # Log the event and update its content in the action log_item = action.log( request.user, models.Log.ACTION_SURVEY_INPUT, action_id=action.id, action=action.name, new_values=json.dumps(dict(zip(keys, values)))) # Modify the time of execution for the action action.last_executed_log = log_item action.save(update_fields=['last_executed_log'])
def serve_survey_row( request: HttpRequest, action: Action, user_attribute_name: str, ) -> HttpResponse: """Serve a request for action in. Function that given a request, and an action IN, it performs the lookup and data input of values. :param request: HTTP request :param action: Action In :param user_attribute_name: The column name used to check for email :return: """ # Get the attribute value depending if the user is managing the workflow # User is instructor, and either owns the workflow or is allowed to access # it as shared manager = has_access(request.user, action.workflow) user_attribute_value = None if manager: user_attribute_value = request.GET.get('uatv') if not user_attribute_value: user_attribute_value = request.user.email # Get the dictionary containing column names, attributes and condition # valuations: context = get_action_evaluation_context( action, get_row_values( action, (user_attribute_name, user_attribute_value), ), ) if not context: # If the data has not been found, flag if not manager: return ontask_handler404(request, None) messages.error(request, _('Data not found in the table')) return redirect(reverse('action:run', kwargs={'pk': action.id})) # Get the active columns attached to the action colcon_items = extract_survey_questions(action, request.user) # Bind the form with the existing data form = EnterActionIn( request.POST or None, tuples=colcon_items, context=context, values=[context[colcon.column.name] for colcon in colcon_items], show_key=manager) keep_processing = (request.method == 'POST' and form.is_valid() and not request.POST.get('lti_version')) if keep_processing: # Update the content in the DB row_keys, row_values = survey_update_row_values( action, colcon_items, manager, form.cleaned_data, 'email', request.user.email, context) # Log the event and update its content in the action log_item = action.log(request.user, Log.ACTION_SURVEY_INPUT, new_values=json.dumps( dict(zip(row_keys, row_values)))) # Modify the time of execution for the action action.last_executed_log = log_item action.save() # If not instructor, just thank the user! if not manager: return render(request, 'thanks.html', {}) # Back to running the action return redirect(reverse('action:run', kwargs={'pk': action.id})) return render( request, 'action/run_survey_row.html', { 'form': form, 'action': action, 'cancel_url': reverse( 'action:run', kwargs={'pk': action.id}, ) if manager else None, }, )
def do_clone_action( user, action: models.Action, new_workflow: models.Workflow = None, new_name: str = None, ): """Clone an action. Function that given an action clones it and changes workflow and name :param user: User executing the operation :param action: Object to clone :param new_workflow: New workflow object to point :param new_name: New name :return: Cloned object """ old_id = action.id old_name = action.name if new_name is None: new_name = action.name if new_workflow is None: new_workflow = action.workflow new_action = models.Action( name=new_name, description_text=action.description_text, workflow=new_workflow, last_executed_log=None, action_type=action.action_type, serve_enabled=action.serve_enabled, active_from=action.active_from, active_to=action.active_to, rows_all_false=copy.deepcopy(action.rows_all_false), text_content=action.text_content, target_url=action.target_url, shuffle=action.shuffle, ) new_action.save() try: # Clone the column/condition pairs field. for acc_tuple in action.column_condition_pair.all(): cname = acc_tuple.condition.name if acc_tuple.condition else None models.ActionColumnConditionTuple.objects.get_or_create( action=new_action, column=new_action.workflow.columns.get( name=acc_tuple.column.name), condition=new_action.conditions.filter(name=cname).first(), ) # Clone the rubric cells if any for rubric_cell in action.rubric_cells.all(): models.RubricCell.objects.create( action=new_action, column=new_action.workflow.columns.get( name=rubric_cell.column.name), loa_position=rubric_cell.loa_position, description_text=rubric_cell.description_text, feedback_text=rubric_cell.feedback_text) # Clone the conditions for condition in action.conditions.all(): services.do_clone_condition(user, condition, new_action) # Update new_action.save() except Exception as exc: new_action.delete() raise exc # Log event action.log( user, models.Log.ACTION_CLONE, id_old=old_id, name_old=old_name) return new_action
def _create_messages( user, action: models.Action, action_evals: List, track_col_name: str, payload: Dict, ) -> List[Union[EmailMessage, EmailMultiAlternatives]]: """Create the email messages to send and the tracking ids. :param user: User that sends the message (encoded in the track-id) :param action: Action to process :param action_evals: Action content already evaluated :param track_col_name: column name to track :param payload: Dictionary with the required fields :return: """ # Context to log the events (one per email) context = {'action': action.id} cc_email = _check_email_list(payload['cc_email']) bcc_email = _check_email_list(payload['bcc_email']) # Everything seemed to work to create the messages. msgs = [] column_to = action.workflow.columns.get(pk=payload['item_column']).name # for msg_body, msg_subject, msg_to in action_evals: for msg_body_sbj_to in action_evals: # If read tracking is on, add suffix for message (or empty) track_str = '' if payload['track_read']: # The track id must identify: action & user track_str = ( '<img src="https://{0}{1}{2}?v={3}" alt=""' + ' style="position:absolute; visibility:hidden"/>').format( Site.objects.get_current().domain, settings.BASE_URL, reverse('trck'), signing.dumps( { 'action': action.id, 'sender': user.email, 'to': msg_body_sbj_to[2], 'column_to': column_to, 'column_dst': track_col_name, }, ), ) msg = _create_single_message(msg_body_sbj_to, track_str, user.email, cc_email, bcc_email) msgs.append(msg) # Log the event context['subject'] = msg.subject context['body'] = msg.body context['from_email'] = msg.from_email context['to_email'] = msg.to[0] context['email_sent_datetime'] = str( datetime.datetime.now(pytz.timezone(settings.TIME_ZONE))) if track_str: context['track_id'] = track_str action.log(user, models.Log.ACTION_EMAIL_SENT, **context) return msgs
def edit_action_rubric( request: HttpRequest, workflow: Workflow, action: Action, ) -> HttpResponse: """Edit action out. :param request: Request object :param workflow: The workflow with the action :param action: Action :return: HTML response """ # Create the form form = EditActionOutForm(request.POST or None, instance=action) form_filter = FilterForm(request.POST or None, instance=action.get_filter(), action=action) # Processing the request after receiving the text from the editor if request.method == 'POST' and form.is_valid() and form_filter.is_valid(): # Get content text_content = form.cleaned_data.get('text_content') # Render the content as a template and catch potential problems. if text_renders_correctly(text_content, action, form): # Log the event action.log(request.user, Log.ACTION_UPDATE) # Text is good. Update the content of the action action.set_text_content(text_content) action.save() if request.POST['Submit'] == 'Submit': return redirect(request.get_full_path()) return redirect('action:index') # Get the filter or None filter_condition = action.get_filter() criteria = action.column_condition_pair.all() if not _verify_criteria_loas(criteria): messages.error(request, _('Inconsistent LOA in rubric criteria')) return redirect(reverse('action:index')) columns_to_insert_qs = action.workflow.columns.exclude( column_condition_pair__action=action, ).exclude( is_key=True, ).distinct().order_by('position') if criteria: columns_to_insert = [ column for column in columns_to_insert_qs if set(column.categories) == set(criteria[0].column.categories) ] else: columns_to_insert = [ column for column in columns_to_insert_qs if column.categories ] # This is a GET request or a faulty POST request context = { 'form': form, 'form_filter': form_filter, 'filter_condition': filter_condition, 'action': action, 'load_summernote': Action.LOAD_SUMMERNOTE[action.action_type], 'query_builder_ops': action.workflow.get_query_builder_ops_as_str(), 'attribute_names': [attr for attr in list(action.workflow.attributes.keys())], 'columns': action.workflow.columns.all(), 'selected_rows': filter_condition.n_rows_selected if filter_condition else -1, 'has_data': action.workflow.has_table(), 'is_send_list': (action.action_type == Action.SEND_LIST or action.action_type == Action.SEND_LIST_JSON), 'is_personalized_text': action.action_type == Action.PERSONALIZED_TEXT, 'is_rubric_cell': action.action_type == Action.RUBRIC_TEXT, 'rows_all_false': action.get_row_all_false_count(), 'total_rows': action.workflow.nrows, 'all_false_conditions': False, 'columns_to_insert': columns_to_insert, 'vis_scripts': PlotlyHandler.get_engine_scripts() } # Get additional context to render the page depending on the action type if criteria: _create_rubric_table(request, action, criteria, context) # Return the same form in the same page return render(request, 'action/edit_rubric.html', context=context)
def send_canvas_emails( user, action: Action, action_info: Mapping, log_item: Optional[Log] = None, ): """Send CANVAS emails with the action content evaluated for each row. Performs the submission of the emails for the given action and with the given subject. The subject will be evaluated also with respect to the rows, attributes, and conditions. :param user: User object that executed the action :param action: Action from where to take the messages :param log_item: Log object to store results :param action_info: Mapping key, value as defined in CanvasEmailPayload :return: List of field values used as "to" in emails """ # Evaluate the action string, evaluate the subject, and get the value of # the email column. action_evals = evaluate_action( action, extra_string=action_info['subject'], column_name=action_info['item_column'], exclude_values=action_info['exclude_values']) # Get the oauth info target_url = action_info['target_url'] oauth_info = settings.CANVAS_INFO_DICT.get(target_url) if not oauth_info: raise Exception(_('Unable to find OAuth Information Record')) # Get the token user_token = OAuthUserToken.objects.filter( user=user, instance_name=target_url, ).first() if not user_token: # There is no token, execution cannot proceed raise Exception(_('Incorrect execution due to absence of token')) # Create the headers to use for all requests headers = { 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', 'Authorization': 'Bearer {0}'.format(user_token.access_token), } # Create the context for the log events context = { 'action': action.id, } # Send the objects to the given URL idx = 1 burst = oauth_info['aux_params'].get('burst') burst_pause = oauth_info['aux_params'].get('pause', 0) domain = oauth_info['domain_port'] conversation_url = oauth_info['conversation_url'].format(domain) to_emails = [] for msg_body, msg_subject, msg_to in action_evals: # # JSON object to send. Taken from method.conversations.create in # https://canvas.instructure.com/doc/api/conversations.html # canvas_email_payload = { 'recipients[]': int(msg_to), 'body': msg_body, 'subject': msg_subject, 'force_new': True, } # Manage the bursts do_burst_pause(burst, burst_pause, idx) # Index to detect bursts idx += 1 # Send the email if settings.EXECUTE_ACTION_JSON_TRANSFER: result_msg, response_status = send_single_canvas_message( target_url, conversation_url, canvas_email_payload, headers, oauth_info, ) else: # Print the JSON that would be sent through the logger logger.info('SEND JSON(%s): %s', target_url, json.dumps(canvas_email_payload)) result_msg = 'SENT TO LOGGER' response_status = 200 # Log message sent context['object'] = json.dumps(canvas_email_payload) context['status'] = response_status context['result'] = result_msg context['email_sent_datetime'] = str( datetime.datetime.now(pytz.timezone(settings.TIME_ZONE)), ) action.log(user, Log.ACTION_CANVAS_EMAIL_SENT, **context) to_emails.append(msg_to) return to_emails
def send_list_email( user, action: Action, action_info: Dict, log_item: Optional[Log] = None, ) -> List[str]: """Send action content evaluated once to include lists. Sends a single email for the given action with the lists expanded and with the given subject evaluated also with respect to the attributes. :param user: User object that executed the action :param action: Action from where to take the messages :param log_item: Log object to store results :param action_info: Dictionary key, value as defined in EmailPayload :return: Empty list (because it is a single email sent) """ # Evaluate the action string, evaluate the subject, and get the value of # the email column. action_text = evaluate_row_action_out( action, get_action_evaluation_context(action, {})) # Turn cc_email and bcc email into lists action_info['cc_email'] = action_info['cc_email'].split() action_info['bcc_email'] = action_info['bcc_email'].split() _check_cc_lists(action_info['cc_email'], action_info['bcc_email']) # Context to log the events msg = _create_single_message( [action_text, action_info['subject'], action_info['email_to']], '', user.email, action_info['cc_email'], action_info['bcc_email'], ) try: # Send email out mail.get_connection().send_messages([msg]) except Exception as exc: raise Exception( _('Error when sending the list email: {0}').format(str(exc)), ) # Log the event context = { 'email_sent_datetime': str(datetime.datetime.now(pytz.timezone(settings.TIME_ZONE)), ), 'subject': msg.subject, 'body': msg.body, 'from_email': msg.from_email, 'to_email': msg.to[0] } action.log(user, Log.ACTION_EMAIL_SENT, **context) return []
def edit_action_out( request: HttpRequest, workflow: Workflow, action: Action, ) -> HttpResponse: """Edit action out. :param request: Request object :param workflow: The workflow with the action :param action: Action :return: HTML response """ # Create the form form = EditActionOutForm(request.POST or None, instance=action) form_filter = FilterForm(request.POST or None, instance=action.get_filter(), action=action) # Processing the request after receiving the text from the editor if request.method == 'POST' and form.is_valid() and form_filter.is_valid(): # Get content text_content = form.cleaned_data.get('text_content') # Render the content as a template and catch potential problems. if text_renders_correctly(text_content, action, form): # Log the event action.log(request.user, Log.ACTION_UPDATE) # Text is good. Update the content of the action action.set_text_content(text_content) # If it is a JSON action, store the target_url if (action.action_type == Action.PERSONALIZED_JSON or action.action_type == Action.SEND_LIST_JSON): # Update the target_url field action.target_url = form.cleaned_data['target_url'] action.save() if request.POST['Submit'] == 'Submit': return redirect(request.get_full_path()) return redirect('action:index') # This is a GET request or a faulty POST request # Get the filter or None filter_condition = action.get_filter() # Context to render the form context = { 'filter_condition': filter_condition, 'action': action, 'load_summernote': (action.action_type == Action.PERSONALIZED_TEXT or action.action_type == Action.SEND_LIST), 'conditions': action.conditions.filter(is_filter=False), 'conditions_to_clone': Condition.objects.filter( action__workflow=workflow, is_filter=False, ).exclude(action=action), 'query_builder_ops': workflow.get_query_builder_ops_as_str(), 'attribute_names': [attr for attr in list(workflow.attributes.keys())], 'columns': workflow.columns.all(), 'columns_show_stat': workflow.columns.filter(is_key=False), 'selected_rows': filter_condition.n_rows_selected if filter_condition else -1, 'has_data': action.workflow.has_table(), 'is_send_list': (action.action_type == Action.SEND_LIST or action.action_type == Action.SEND_LIST_JSON), 'all_false_conditions': any(cond.n_rows_selected == 0 for cond in action.conditions.all()), 'rows_all_false': action.get_row_all_false_count(), 'total_rows': workflow.nrows, 'form': form, 'form_filter': form_filter, 'vis_scripts': PlotlyHandler.get_engine_scripts(), } # Return the same form in the same page return render(request, 'action/edit_out.html', context=context)