Example #1
0
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))))
Example #2
0
def evaluate_action(
    action: Action,
    extra_string: str = None,
    column_name: str = None,
    exclude_values: List[str] = None,
) -> List[List]:
    """Evaluate the content in an action based on the values in the columns.

    Given an action object and an optional string:
    1) Access the attached workflow
    2) Obtain the data from the appropriate data frame
    3) Loop over each data row and
      3.1) Evaluate the conditions with respect to the values in the row
      3.2) Create a context with the result of evaluating the conditions,
           attributes and column names to values
      3.3) Run the template with the context
      3.4) Run the optional string argument with the template and the context
      3.5) Select the optional column_name
    4) Return the resulting objects:
       List of (HTMLs body, extra string, column name value)
        or an error message

    :param action: Action object with pointers to conditions, filter,
                   workflow, etc.
    :param extra_string: An extra string to process (something like the email
           subject line) with the same dictionary as the text in the action.
    :param column_name: Column from where to extract the special value (
           typically the email address) and include it in the result.
    :param exclude_values: List of values in the column to exclude
    :return: list of lists resulting from the evaluation of the action. Each
             element in the list contains the HTML body, the extra string (if
             provided) and the column value.
    """
    # Get the table data
    rows = get_rows(
        action.workflow.get_data_frame_table_name(),
        filter_formula=action.get_filter_formula())
    list_of_renders = []
    for row in rows:
        if exclude_values and str(row[column_name]) in exclude_values:
            # Skip the row with the col_name in exclude values
            continue

        # Step 4: Create the context with the attributes, the evaluation of the
        # conditions and the values of the columns.
        context = get_action_evaluation_context(action, row)

        # Append result
        list_of_renders.append(
            _render_tuple_result(action, context, extra_string, column_name),
        )

    if settings.DEBUG:
        # Check that n_rows_selected is equal to rows.rowcount
        action_filter = action.get_filter()
        if action_filter and action_filter.n_rows_selected != rows.rowcount:
            raise ontask.OnTaskException('Inconsisten n_rows_selected')

    return list_of_renders
Example #3
0
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 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 = Log.objects.register(
            req.user, Log.SCHEDULE_JSON_EXECUTE, action.workflow, {
                'action': action.name,
                'action_id': action.id,
                'exported_workflow': action_info['export_wf'],
                'status': 'Preparing to execute',
                'target_url': action.target_url
            })

        # Update the last_execution_log
        action.last_executed_log = log_item
        action.save()

        # 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()
        })
Example #5
0
def run_json_action(
    req: HttpRequest,
    workflow: Workflow,
    action: Action,
) -> HttpResponse:
    """Request data to send JSON objects.

    Form asking for token, item_column and if an item confirmation step is
    needed

    :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 = get_or_set_action_info(
        req.session,
        JSONPayload,
        initial_values={
            'action_id': action.id,
            'prev_url': reverse('action:run', kwargs={'pk': action.id}),
            'post_url': reverse('action:json_done')
        },
    )

    # Create the form to ask for the email subject and other information
    form = JSONActionForm(req.POST or None,
                          column_names=[
                              col.name
                              for col in workflow.columns.filter(is_key=True)
                          ],
                          form_info=action_info)

    if req.method == 'POST' and form.is_valid():
        if action_info['confirm_items']:
            # Add information to the session object to execute the next pages
            action_info['button_label'] = ugettext('Send')
            action_info['valuerange'] = 2
            action_info['step'] = 2
            set_action_payload(req.session, action_info.get_store())

            return redirect('action:item_filter')

        # Go straight to the final step.
        return run_json_done(req, action_info=action_info, workflow=workflow)

    # Render the form
    return render(
        req, 'action/request_json_data.html', {
            'action': action,
            'num_msgs': action.get_rows_selected(),
            'form': form,
            'valuerange': range(2),
            'rows_all_false': action.get_row_all_false_count()
        })
Example #6
0
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 = Log.objects.register(
            req.user, Log.SCHEDULE_EMAIL_EXECUTE, action.workflow, {
                'action': action.name,
                'action_id': action.id,
                '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'],
                'status': 'Preparing to execute',
            })

        # Update the last_execution_log
        action.last_executed_log = log_item
        action.save()

        # 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)
    })
Example #7
0
    def get_render_context(
        action: models.Action,
        form: Optional[Type[forms.ModelForm]] = None,
        form_filter: Optional[condition_forms.FilterForm] = None,
    ) -> Dict:
        """Get the initial context to render the response."""
        filter_condition = action.get_filter()
        return {
            # Workflow elements
            'attribute_names':
            [attr for attr in list(action.workflow.attributes.keys())],
            'columns':
            action.workflow.columns.all(),
            'has_data':
            action.workflow.has_table(),
            'total_rows':
            action.workflow.nrows,

            # Action Elements
            'action':
            action,
            'form':
            form,
            'form_filter':
            form_filter,
            'filter_condition':
            filter_condition,
            'selected_rows':
            filter_condition.n_rows_selected if filter_condition else -1,
            'is_email_report':
            action.action_type == models.Action.EMAIL_REPORT,
            'is_report': (action.action_type == models.Action.EMAIL_REPORT
                          or action.action_type == models.Action.JSON_REPORT),
            'is_personalized_text':
            (action.action_type == models.Action.PERSONALIZED_TEXT),
            'is_rubric':
            action.action_type == models.Action.RUBRIC_TEXT,
            'is_survey':
            action.action_type == models.Action.SURVEY,
            'all_false_conditions':
            any(cond.n_rows_selected == 0 for cond in action.conditions.all()),
            'rows_all_false':
            action.get_row_all_false_count(),

            # Page elements
            'load_summernote':
            (action.action_type == models.Action.PERSONALIZED_TEXT
             or action.action_type == models.Action.EMAIL_REPORT
             or action.action_type == models.Action.RUBRIC_TEXT),
            'query_builder_ops':
            action.workflow.get_query_builder_ops_as_str(),
            'vis_scripts':
            PlotlyHandler.get_engine_scripts()
        }
Example #8
0
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)
Example #9
0
def do_clone_action(
    action: Action,
    new_workflow: Workflow = None,
    new_name: str = None,
):
    """Clone an action.

    Function that given an action clones it and changes workflow and name

    :param action: Object to clone

    :param new_workflow: New workflow object to point

    :param new_name: New name

    :return: Cloned object
    """
    if new_name is None:
        new_name = action.name
    if new_workflow is None:
        new_workflow = action.workflow

    new_action = 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
            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 conditions
        for condition in action.conditions.all():
            do_clone_condition(condition, new_action)

        # Update
        new_action.save()
    except Exception as exc:
        new_action.delete()
        raise exc

    return new_action
Example #10
0
    def process_run_request(
        self,
        operation_type: str,
        request: http.HttpRequest,
        action: models.Action,
        prev_url: str,
    ) -> http.HttpResponse:
        """Process a request (GET or POST)."""
        payload = SessionPayload(request.session,
                                 initial_values={
                                     'action_id': action.id,
                                     'operation_type': operation_type,
                                     'prev_url': prev_url,
                                     'post_url': reverse('action:run_done')
                                 })

        form = self.run_form_class(
            request.POST or None,
            columns=action.workflow.columns.filter(is_key=True),
            action=action,
            form_info=payload)

        if request.method == 'POST' and form.is_valid():
            return self.process_run_post(request, action, payload)

        # Render the form
        return render(
            request, self.run_template, {
                'action': action,
                'num_msgs': action.get_rows_selected(),
                'form': form,
                'valuerange': range(2)
            })
Example #11
0
def get_row_values(
    action: models.Action,
    row_idx: Union[int, Tuple[str, str]],
) -> Dict[str, Union[str, int, float, datetime]]:
    """Get the values in a row either by index or by key.

    Given an action and a row index, obtain the appropriate row of values
    from the data frame.

    :param action: Action object
    :param row_idx: Row index to use for evaluation
    :return Dictionary with the data row
    """
    # Step 1: Get the row of data from the DB
    filter_formula = action.get_filter_formula()

    # If row_idx is an integer, get the data by index, otherwise, by key
    if isinstance(row_idx, int):
        row = pandas.get_table_row_by_index(
            action.workflow,
            filter_formula,
            row_idx,
        )
    else:

        row = sql.get_row(
            action.workflow.get_data_frame_table_name(),
            row_idx[0],
            row_idx[1],
            column_names=action.workflow.get_column_names(),
            filter_formula=filter_formula,
        )
    return row
Example #12
0
def save_condition_form(
    request: HttpRequest,
    form,
    action: models.Action,
    is_filter: Optional[bool] = False,
) -> JsonResponse:
    """Process the AJAX form POST to create and update conditions and filters.

    :param request: HTTP request
    :param form: Form being used to ask for the fields
    :param action: The action to which the condition is attached to
    :param is_filter: The condition is a filter
    :return: JSON response
    """
    if is_filter and form.instance.id is None and action.get_filter():
        # Should not happen. Go back to editing the action
        return JsonResponse({'html_redirect': ''})

    is_new = form.instance.id is None

    # Update fields and save the condition
    condition = form.save(commit=False)
    condition.formula_text = None
    condition.action = action
    condition.is_filter = is_filter
    condition.save()
    condition.columns.set(
        action.workflow.columns.filter(name__in=formula.get_variables(
            condition.formula), ))

    # If the request has the 'action_content' field, update the action
    action_content = request.POST.get('action_content')
    if action_content:
        action.set_text_content(action_content)

    _propagate_changes(condition, form.changed_data, form.old_name, is_new)

    # Store the type of event to log
    if is_new:
        log_type = models.Log.CONDITION_CREATE
    else:
        log_type = models.Log.CONDITION_UPDATE
    condition.log(request.user, log_type)
    return JsonResponse({'html_redirect': ''})
Example #13
0
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'])
Example #14
0
def send_json(
    user,
    action: Action,
    log_item: Log,
    action_info: Mapping,
):
    """Send json objects to target URL.

    Sends the json objects evaluated per row to the URL in the action

    :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

    :return: Nothing
    """
    # Evaluate the action string and obtain the list of list of JSON objects
    action_evals = evaluate_action(
        action,
        column_name=action_info['item_column'],
        exclude_values=action_info['exclude_values'],
    )

    # Create the headers to use for all requests
    headers = {
        'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
        'Authorization': 'Bearer {0}'.format(action_info['token']),
    }

    # Create the context for the log events
    context = {
        'user': user.id,
        'action': action.id,
    }

    # Iterate over all json objects to create the strings and check for
    # correctness
    for json_string in action_evals:
        _send_and_log_json(
            user,
            action,
            json.loads(json_string[0]),
            headers,
            context,
        )

        # Update data in the overall log item
    log_item.payload['objects_sent'] = len(action_evals)
    log_item.payload['filter_present'] = action.get_filter() is not None
    log_item.payload['datetime'] = str(
        datetime.datetime.now(pytz.timezone(settings.TIME_ZONE)))
    log_item.save()
Example #15
0
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)})
Example #16
0
    def create(self, validated_data, **kwargs):
        """Create the action.

        :param validated_data: Validated data
        :param kwargs: Extra material
        :return: Create the action in the DB
        """
        action_obj = None
        try:
            action_type = validated_data.get('action_type')
            if not action_type:
                if validated_data['is_out']:
                    action_type = Action.PERSONALIZED_TEXT
                else:
                    action_type = Action.SURVEY

            action_obj = Action(
                workflow=self.context['workflow'],
                name=validated_data['name'],
                description_text=validated_data['description_text'],
                action_type=action_type,
                serve_enabled=validated_data['serve_enabled'],
                active_from=validated_data['active_from'],
                active_to=validated_data['active_to'],
                text_content=validated_data.get(
                    'content',
                    validated_data.get('text_content'),  # Legacy
                ),
                target_url=validated_data.get('target_url', ''),
                shuffle=validated_data.get('shuffle', False),
            )
            action_obj.save()

            # Load the conditions pointing to the action
            condition_data = ConditionSerializer(
                data=validated_data.get('conditions', []),
                many=True,
                context={'action': action_obj})
            if condition_data.is_valid():
                condition_data.save()
            else:
                raise Exception(_('Invalid condition data'))

            # Process the fields columns (legacy) and column_condition_pairs
            self.create_column_condition_pairs(
                validated_data,
                action_obj,
                self.context['workflow'].columns.all(),
            )
        except Exception:
            if action_obj and action_obj.id:
                ActionColumnConditionTuple.objects.filter(
                    action=action_obj,
                ).delete()
                action_obj.delete()
            raise

        return action_obj
Example #17
0
    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)
Example #18
0
    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)
Example #19
0
def send_json_list(
    user,
    action: Action,
    log_item: Log,
    action_info: Mapping,
):
    """Send single json object to target URL.

    Sends a single json object to the URL in the action

    :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: Object with the additional parameters

    :return: Nothing
    """
    # Evaluate the action string and obtain the list of list of JSON objects
    action_text = evaluate_row_action_out(
        action,
        get_action_evaluation_context(action, {}))

    _send_and_log_json(
        user,
        action,
        json.loads(action_text),
        {
            'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
            'Authorization': 'Bearer {0}'.format(action_info['token']),
        },
        {'user': user.id, 'action': action.id})

    # Update data in the overall log item
    log_item.payload['filter_present'] = action.get_filter() is not None
    log_item.payload['datetime'] = str(
        datetime.datetime.now(pytz.timezone(settings.TIME_ZONE)))
    log_item.save()
Example #20
0
def create_survey_table(
    workflow: models.Workflow,
    action: models.Action,
    dt_page: DataTablesServerSidePaging,
) -> http.JsonResponse:
    """Create the table with the survey entries for instructor.

    :param workflow: Workflow being processed
    :param action: Action representing the survey
    :param dt_page: Data tables server side paging object
    :return : JSon respnse
    """
    columns = [ccpair.column for ccpair in action.column_condition_pair.all()]
    query_set = _create_initial_qs(
        workflow.get_data_frame_table_name(),
        action.get_filter_formula(),
        columns,
        dt_page,
    )

    filtered = len(query_set)

    # Get the subset of the qs to show in the table
    query_set = _create_table_qsdata(
        action.id,
        query_set,
        dt_page,
        columns,
        next(idx for idx, col in enumerate(columns) if col.is_key),
    )

    return http.JsonResponse({
        'draw': dt_page.draw,
        'recordsTotal': workflow.nrows,
        'recordsFiltered': filtered,
        'data': query_set,
    })
Example #21
0
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,
        },
    )
Example #22
0
def save_condition_form(
    request: HttpRequest,
    form,
    template_name: str,
    action: Action,
    is_filter: Optional[bool] = False,
) -> JsonResponse:
    """
    Process the AJAX form to create and update conditions and filters.

    :param request: HTTP request

    :param form: Form being used to ask for the fields

    :param template_name: Template being used to render the form

    :param action: The action to which the condition is attached to

    :param is_filter: The condition is a filter

    :return: JSON response
    """
    if request.method == 'POST' and form.is_valid():

        if not form.has_changed():
            return JsonResponse({'html_redirect': None})

        if is_filter and form.instance.id is None and action.get_filter():
            # Should not happen. Go back to editing the action
            return JsonResponse({'html_redirect': ''})

        is_new = form.instance.id is None

        # Update fields and save the condition
        condition = form.save(commit=False)
        condition.formula_text = None
        condition.action = action
        condition.is_filter = is_filter
        condition.save()
        condition.columns.set(action.workflow.columns.filter(
            name__in=get_variables(condition.formula),
        ))

        # If the request has the 'action_content' field, update the action
        action_content = request.POST.get('action_content')
        if action_content:
            action.set_text_content(action_content)

        propagate_changes(condition, form.changed_data, form.old_name, is_new)

        # Store the type of event to log
        if is_new:
            log_type = Log.CONDITION_CREATE
        else:
            log_type = Log.CONDITION_UPDATE
        condition.log(request.user, log_type)
        return JsonResponse({'html_redirect': ''})

    # GET request or invalid form
    return JsonResponse({
        'html_form': render_to_string(
            template_name,
            {
                'form': form,
                'action_id': action.id,
                'condition': form.instance},
            request=request),
    })
Example #23
0
def create_row_preview_context(
    action: models.Action,
    idx: int,
    context: Dict,
    prelude: Optional[str] = None,
):
    """Create the elements to render a single row preview.

    :param action: Action being previewed.
    :param idx:
    :param context:
    :param prelude: Optional additional text to include in the preview.
    :return: context is modified to include the appropriate items
    """
    # Get the total number of items
    filter_obj = action.get_filter()
    if filter_obj:
        n_items = filter_obj.n_rows_selected
    else:
        n_items = action.workflow.nrows

    # Set the correct values to the indeces
    prv, idx, nxt = _get_navigation_index(idx, n_items)

    row_values = get_row_values(action, idx)

    # Obtain the dictionary with the condition evaluation
    condition_evaluation = action_condition_evaluation(action, row_values)
    # Get the dictionary containing column names, attributes and condition
    # valuations:
    eval_context = get_action_evaluation_context(action, row_values,
                                                 condition_evaluation)

    all_false = False
    if action.conditions.filter(is_filter=False).count():
        # If there are conditions, check if they are all false
        all_false = all(not bool_val
                        for __, bool_val in condition_evaluation.items())

    # Evaluate the action content.
    show_values = ''
    incorrect_json = False
    if action.is_out:
        action_content = evaluate_row_action_out(action, eval_context)
        if action.action_type == models.Action.PERSONALIZED_JSON:
            incorrect_json = not _check_json_is_correct(action_content)
    else:
        action_content = _evaluate_row_action_in(action, eval_context)
    if action_content is None:
        action_content = _(
            'Error while retrieving content (index: {0})', ).format(idx)
    else:
        # Get the conditions used in the action content
        act_cond = action.get_used_conditions()
        # Get the variables/columns from the conditions
        act_vars = set().union(*[
            cond.columns.all()
            for cond in action.conditions.filter(name__in=act_cond)
        ])

        act_vars = act_vars.union(
            {triplet.column
             for triplet in action.column_condition_pair.all()})

        # Sort the variables/columns  by position and get the name
        show_values = ', '.join([
            '"{0}" = {1}'.format(col.name, row_values[col.name])
            for col in act_vars
        ])

    uses_plain_text = (
        action.action_type == models.Action.PERSONALIZED_CANVAS_EMAIL
        or action.action_type == models.Action.PERSONALIZED_JSON)
    if uses_plain_text:
        action_content = escape(action_content)

    if prelude:
        prelude = evaluate_row_action_out(action, eval_context, prelude)

    # Update the context
    context.update({
        'n_items':
        n_items,
        'nxt':
        nxt,
        'prv':
        prv,
        'incorrect_json':
        incorrect_json,
        'show_values':
        show_values,
        'show_conditions':
        ', '.join([
            '"{0}" = {1}'.format(cond_name, str(cond_value))
            for cond_name, cond_value in condition_evaluation.items()
        ]),
        'all_false':
        all_false,
        'prelude':
        prelude,
        'action_content':
        action_content,
        'show_navigation':
        True
    })
Example #24
0
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 []
Example #25
0
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
Example #26
0
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,
    log_item: Log,
    action_info: Mapping,
):
    """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: Send the 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 = {
        'user': user.id,
        '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)
    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,
        }

        # 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)),
        )
        Log.objects.register(
            user,
            Log.ACTION_CANVAS_EMAIL_SENT,
            action.workflow,
            context)

    # Update data in the overall log item
    log_item.payload['objects_sent'] = len(action_evals)
    log_item.payload['filter_present'] = action.get_filter() is not None
    log_item.payload['datetime'] = str(datetime.datetime.now(pytz.timezone(
        settings.TIME_ZONE)))
    log_item.save()

    return None
def edit_action_in(
    request: HttpRequest,
    workflow: Workflow,
    action: Action,
) -> HttpResponse:
    """Edit an action in.

    :param request: Request object
    :param workflow: workflow
    :param action: Action
    :return: HTTP response
    """
    # All tuples (action, column, condition) to consider
    tuples = action.column_condition_pair.all()

    # Columns
    all_columns = workflow.columns

    # Conditions
    filter_condition = action.get_filter()
    all_conditions = action.conditions.filter(is_filter=False)

    # Create the context info.
    context = {
        'action': action,
        # Workflow elements
        'total_rows': workflow.nrows,
        'query_builder_ops': workflow.get_query_builder_ops_as_str(),
        'has_data': workflow.has_table(),
        'selected_rows':
            filter_condition.n_rows_selected if filter_condition else -1,
        'all_false_conditions': any(
            cond.n_rows_selected == 0 for cond in all_conditions
        ),
        # Column elements
        'key_columns': all_columns.filter(is_key=True),
        'stat_columns': all_columns.filter(is_key=False),
        'key_selected': tuples.filter(column__is_key=True).first(),
        'has_no_key': tuples.filter(column__is_key=False).exists(),
        'any_empty_description': tuples.filter(
            column__description_text='',
            column__is_key=False,
        ).exists(),
        'columns_to_insert': all_columns.exclude(
            column_condition_pair__action=action,
        ).exclude(
            is_key=True,
        ).distinct().order_by('position'),
        'column_selected_table': ColumnSelectedTable(
            tuples.filter(column__is_key=False).values(
                'id',
                'column__id',
                'column__name',
                'column__description_text',
                'condition__name',
            ),
            orderable=False,
            extra_columns=[(
                'operations',
                OperationsColumn(
                    verbose_name='',
                    template_file=ColumnSelectedTable.ops_template,
                    template_context=lambda record: {
                        'id': record['column__id'],
                        'aid': action.id}),
            )],
            condition_list=all_conditions,
        ),
        # Conditions
        'filter_condition': filter_condition,
        'conditions': all_conditions,
        'vis_scripts': PlotlyHandler.get_engine_scripts(),
        'other_conditions': Condition.objects.filter(
            action__workflow=workflow, is_filter=False,
        ).exclude(action=action),
    }

    return render(request, 'action/edit_in.html', context)
Example #29
0
def _create_row_preview_response(
    action: Action,
    idx: int,
    page_context: Dict,
    prelude: str = None,
):
    """Create the elements to render a sigle row preview.

    :param action: Action being previewed.
    :param idx:
    :param page_context:
    :return: page_context is modified to include the appropriate items
    """

    # Get the total number of items
    filter_obj = action.get_filter()
    if filter_obj:
        n_items = filter_obj.n_rows_selected
    else:
        n_items = action.workflow.nrows

    # Set the correct values to the indeces
    prv, idx, nxt = _get_navigation_index(idx, n_items)

    row_values = get_row_values(action, idx)

    # Obtain the dictionary with the condition evaluation
    condition_evaluation = action_condition_evaluation(action, row_values)
    # Get the dictionary containing column names, attributes and condition
    # valuations:
    eval_context = get_action_evaluation_context(action, row_values,
                                                 condition_evaluation)

    all_false = False
    if action.conditions.filter(is_filter=False).count():
        # If there are conditions, check if they are all false
        all_false = all(not bool_val
                        for __, bool_val in condition_evaluation.items())

    # Evaluate the action content.
    show_values = ''
    incorrect_json = False
    if action.is_out:
        action_content = evaluate_row_action_out(action, eval_context)
        if action.action_type == Action.personalized_json:
            incorrect_json = not _check_json_is_correct(action_content)
    else:
        action_content = evaluate_row_action_in(action, eval_context)
    if action_content is None:
        action_content = _(
            'Error while retrieving content for student {0}', ).format(idx)
    else:
        # Get the conditions used in the action content
        act_cond = action.get_used_conditions()
        # Get the variables/columns from the conditions
        act_vars = set().union(
            *[
                cond.columns.all()
                for cond in action.conditions.filter(name__in=act_cond)
            ], )
        # Sort the variables/columns  by position and get the name
        show_values = ', '.join([
            '{0} = {1}'.format(col.name, row_values[col.name])
            for col in act_vars
        ], )

    uses_plain_text = (action.action_type == Action.personalized_canvas_email
                       or action.action_type == Action.personalized_json)
    if uses_plain_text:
        action_content = escape(action_content)

    if prelude:
        prelude = evaluate_row_action_out(action, eval_context, prelude)

    # Update the context
    page_context.update({
        'n_items': n_items,
        'nxt': nxt,
        'prv': prv,
        'incorrect_json': incorrect_json,
        'show_values': show_values,
        'all_false': all_false,
        'prelude': prelude,
        'action_content': action_content,
        'show_navigation': True
    })

    return
Example #30
0
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