コード例 #1
0
ファイル: views_out.py プロジェクト: HexaCubist/ontask_b
def action_zip_export(request):
    """
    Create a zip with the personalised text and return it as response

    :param request: Request object with a Dictionary with all the required
    information
    :return: Response (download)
    """

    # Get the payload from the session if not given
    payload = request.session.get(session_dictionary_name, None)

    # If there is no payload, something went wrong.
    if not payload:
        # Something is wrong with this execution. Return to action table.
        messages.error(request, _('Incorrect ZIP action invocation.'))
        return redirect('action:index')

    # Get the information from the payload
    action = Action.objects.get(pk=payload['action_id'])
    user_fname_column = payload['user_fname_column']
    participant_column = payload['item_column']
    file_suffix = payload['file_suffix']
    if not file_suffix:
        file_suffix = 'feedback.html'
    zip_for_moodle = payload['zip_for_moodle']
    exclude_values = payload['exclude_values']

    # Obtain the personalised text
    # Invoke evaluate_action
    # Returns: [ (HTML, None, column name value) ] or String error!
    result = evaluate_action(action,
                             column_name=participant_column,
                             exclude_values=exclude_values)

    # Check the type of the result to see if it was successful
    if not isinstance(result, list):
        # Something went wrong. The result contains a message
        messages.error(request, _('Unable to generate zip:') + result)
        return redirect('action:index')

    if not result:
        # Result is an empty list. There is nothing to include in the ZIP
        messages.error(request, _('The resulting ZIP is empty'))
        return redirect('action:index')

    if user_fname_column:
        # Get the user_fname_column values
        user_fname_data = get_table_cursor(
            action.workflow.pk,
            None,
            column_names=[user_fname_column]
        ).fetchall()

        # Data list combining messages, full name and participant (assuming
        # participant columns has format "Participant [number]"
        data_list = [(x[0], str(y[0]), str(x[1]))
                     for x, y in zip(result, user_fname_data)]
    else:
        # No user_fname_column given
        data_list = [(x[0], None, str(x[1])) for x in result]

    # Loop over the result
    files = []
    for msg_body, user_fname, part_id in data_list:
        html_text = html_body.format(msg_body)
        files.append((user_fname, part_id, html_text))

    # Create the file name template
    if zip_for_moodle:
        file_name_template = \
            '{user_fname}_{part_id}_assignsubmission_file_{file_suffix}'
    else:
        if user_fname_column:
            file_name_template = '{part_id}_{user_fname}_{file_suffix}'
        else:
            file_name_template = '{part_id}_{file_suffix}'

    # Create the ZIP and return it for download
    sbuf = BytesIO()
    zf = zipfile.ZipFile(sbuf, 'w')
    for user_fname, part_id, msg_body in files:
        if zip_for_moodle:
            # If a zip for moodle, field is Participant [number]. Take the
            # number
            part_id = part_id.split()[1]

        fdict = {'user_fname': user_fname,
                 'part_id': part_id,
                 'file_suffix': file_suffix}
        zf.writestr(file_name_template.format(**fdict), str(msg_body))
    zf.close()

    suffix = datetime.now().strftime('%y%m%d_%H%M%S')
    # Attach the compressed value to the response and send
    compressed_content = sbuf.getvalue()
    response = HttpResponse(compressed_content)
    response['Content-Type'] = 'application/x-zip-compressed'
    response['Content-Transfer-Encoding'] = 'binary'
    response['Content-Disposition'] = \
        'attachment; filename="ontask_zip_action_{0}.zip"'.format(suffix)
    response['Content-Length'] = str(len(compressed_content))

    # Reset object to carry action info throughout dialogs
    request.session[session_dictionary_name] = {}
    request.session.save()

    return response
コード例 #2
0
def send_messages(user, action, subject, email_column, canvas_id_column,
                  from_email, send_to_canvas, send_confirmation, track_read):
    """
    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 subject: Email subject
    :param email_column: Name of the column from which to extract emails
    :param from_email: Email of the sender
    :param send_confirmation: Boolean to send confirmation to sender
    :param track_read: Should read tracking be included?
    :return: Send the emails
    """

    # Evaluate the action string, evaluate the subject, and get the value of
    # the email colummn.
    result = evaluate_action(action,
                             extra_string=subject,
                             column_name=email_column)
    # this is for canvas inbox
    recipients_list = []
    result_2nd = evaluate_action_canvas(action,
                                        extra_string=subject,
                                        column_name=canvas_id_column)

    # Check the type of the result to see if it was successful
    if not isinstance(result, list):
        # Something went wrong. The result contains a message
        return result

    track_col_name = ''
    data_frame = None
    if track_read:
        data_frame = pandas_db.load_from_db(action.workflow.id)
        # Make sure the column name does not collide with an existing one
        i = 0  # Suffix to rename
        while True:
            i += 1
            track_col_name = 'EmailRead_{0}'.format(i)
            if track_col_name not in data_frame.columns:
                break

    # CC the message to canvas conversation Api if the box send_to_canvas is ticked
    if send_to_canvas:
        msgs = []
        for msg_body, msg_subject, msg_to in result_2nd:

            # Get the plain text content and bundle it together with the HTML in
            # a message to be added to the list.
            text_content = strip_tags(msg_body)
            recipients_list.append(str(msg_to))
            #msg.attach_alternative(msg_body + track_str, "text/html")
            #msgs.append(msg)

        try:
            # send a plain text copy to canvas inbox
            p = post_to_canvas_api()
            p.set_payload(True, '', '', recipients_list, msg_subject,
                          html2text.html2text(msg_body))
            r = p.post_to_conversation(p.access_token, p.payload)
            # extracting response text
            pastebin_url = r.text
            print(r.status_code)
            print(pastebin_url)

        except Exception as e:
            # Something went wrong, notify above
            return str(e)

    # Update the number of filtered rows if the action has a filter (table
    # might have changed)
    filter = action.conditions.filter(is_filter=True).first()
    if filter and filter.n_rows_selected != len(result):
        filter.n_rows_selected = len(result)
        filter.save()

    # Everything seemed to work to create the messages.
    msgs = []
    for msg_body, msg_subject, msg_to in result:

        # If read tracking is on, add suffix for message (or empty)
        if track_read:
            # The track id must identify: action & user
            track_id = {
                'action': action.id,
                'sender': user.email,
                'to': msg_to,
                'column_to': email_column,
                'column_dst': track_col_name
            }

            track_str = \
                """<img src="https://{0}{1}{2}?v={3}" alt="" 
                    style="position:absolute; visibility:hidden"/>""".format(
                    Site.objects.get_current().domain,
                    ontask_settings.BASE_URL,
                    reverse('trck'),
                    signing.dumps(track_id)
                )
        else:
            track_str = ''

        # Get the plain text content and bundle it together with the HTML in
        # a message to be added to the list.
        text_content = strip_tags(msg_body)
        msg = EmailMultiAlternatives(msg_subject, text_content, from_email,
                                     [msg_to])
        msg.attach_alternative(msg_body + track_str, "text/html")
        msgs.append(msg)

    # Mass mail!
    try:
        connection = mail.get_connection()
        connection.send_messages(msgs)
    except Exception as e:
        # Something went wrong, notify above
        return str(e)

    # Add the column if needed
    if track_read:
        # Create the new column and store
        column = Column(name=track_col_name,
                        workflow=action.workflow,
                        data_type='integer',
                        is_key=False,
                        position=action.workflow.ncols + 1)
        column.save()

        # Increase the number of columns in the workflow
        action.workflow.ncols += 1
        action.workflow.save()

        # Initial value in the data frame and store the table
        data_frame[track_col_name] = 0
        ops.store_dataframe_in_db(data_frame, action.workflow.id)

    # Log the events (one per email)
    now = datetime.datetime.now(pytz.timezone(ontask_settings.TIME_ZONE))
    context = {
        'user': user.id,
        'action': action.id,
        'email_sent_datetime': str(now),
    }
    for msg in msgs:
        context['subject'] = msg.subject
        context['body'] = msg.body
        context['from_email'] = msg.from_email
        context['to_email'] = msg.to[0]
        logs.ops.put(user, 'action_email_sent', action.workflow, context)

    # Log the event
    logs.ops.put(
        user, 'action_email_sent', action.workflow, {
            'user': user.id,
            'action': action.name,
            'num_messages': len(msgs),
            'email_sent_datetime': str(now),
            'filter_present': filter is not None,
            'num_rows': action.workflow.nrows,
            'subject': subject,
            'from_email': user.email
        })

    # If no confirmation email is required, done
    if not send_confirmation:
        return None

    # Creating the context for the personal email
    context = {
        'user': user,
        'action': action,
        'num_messages': len(msgs),
        'email_sent_datetime': now,
        'filter_present': filter is not None,
        'num_rows': action.workflow.nrows,
        'num_selected': filter.n_rows_selected if filter else -1
    }

    # Create template and render with context
    try:
        html_content = Template(str(getattr(settings,
                                            'NOTIFICATION_TEMPLATE'))).render(
                                                Context(context))
        text_content = strip_tags(html_content)
    except TemplateSyntaxError as e:
        return 'Syntax error detected in OnTask notification template (' + \
               e.message + ')'

    # Log the event
    logs.ops.put(
        user, 'action_email_notify', action.workflow, {
            'user': user.id,
            'action': action.id,
            'num_messages': len(msgs),
            'email_sent_datetime': str(now),
            'filter_present': filter is not None,
            'num_rows': action.workflow.nrows,
            'subject': str(getattr(settings, 'NOTIFICATION_SUBJECT')),
            'body': text_content,
            'from_email': str(getattr(settings, 'NOTIFICATION_SENDER')),
            'to_email': [user.email]
        })

    # Send email out
    try:
        send_mail(str(getattr(settings, 'NOTIFICATION_SUBJECT')),
                  text_content,
                  str(getattr(settings, 'NOTIFICATION_SENDER')), [user.email],
                  html_message=html_content)
    except Exception as e:
        return 'An error occurred when sending your notification: ' + e.message

    return None
コード例 #3
0
def send_canvas_messages(user, action, subject, canvas_id_column,
                         exclude_values, target_url, log_item):
    """
    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 subject: Email subject
    :param canvas_id_column: Name of the column from which to extract canvas ID
    :param exclude_values: List of values to exclude from the mailing
    :param target_url: Server name to use to send the emails
    :param log_item: Log object to store results
    :return: Send the emails
    """

    # Evaluate the action string, evaluate the subject, and get the value of
    # the email column.
    result = evaluate_action(action,
                             extra_string=subject,
                             column_name=canvas_id_column,
                             exclude_values=exclude_values)

    # Check the type of the result to see if it was successful
    if not isinstance(result, list):
        # Something went wrong. The result contains a message
        return result

    # Update the number of filtered rows if the action has a filter (table
    # might have changed)
    cfilter = action.get_filter()
    if cfilter and cfilter.n_rows_selected != len(result):
        cfilter.n_rows_selected = len(result)
        cfilter.save()

    # Get the oauth info
    oauth_info = ontask_settings.CANVAS_INFO_DICT.get(target_url)
    if not oauth_info:
        return _('Unable to find OAuth Information Record')

    # Get the token
    user_token = OnTaskOAuthUserTokens.objects.filter(
        user=user, instance_name=target_url).first()
    if not user_token:
        # There is no token, execution cannot proceed
        return _('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),
    }

    # Send the objects to the given URL
    status_vals = []
    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 result:
        #
        # JSON object to send. Taken from method.conversations.create in
        # https://canvas.instructure.com/doc/api/conversations.html
        #
        canvas_email_payload = {
            'recipients[]': msg_to,  # Required
            'body': msg_body,  # Required
            'subject': msg_subject,  # Optional, but we will require it
            # 'group_conversation': '',
            # 'attachment_ids[]': '',
            # 'media_comment_id': '',
            # 'media_comment_type': '',
            # 'user_note': '0',
            # 'mode': 'sync',
            # 'scope': 'unread',
            # 'filter[]': '',
            # 'filter_mode': '',
            # 'context_code': '',
        }

        if burst and idx % burst == 0:
            # Burst exists and the limit has been reached
            logger.info('Burst ({0}) reached. Waiting for {1} secs'.format(
                burst, burst_pause))
            sleep(burst_pause)
        # Index to detect bursts
        idx += 1

        #
        # Send the email
        #
        result_msg = ugettext('Message successfuly sent')
        if ontask_settings.EXECUTE_ACTION_JSON_TRANSFER:
            # Send the email through the API call
            # First attempt
            response = requests.post(url=conversation_url,
                                     data=canvas_email_payload,
                                     headers=headers)
            response_status = response.status_code

            if response_status == status.HTTP_401_UNAUTHORIZED and \
                    response.headers.get('WWW-Authenticate'):
                # Request rejected due to token expiration. Refresh the
                # token
                user_token = None
                result_msg = ugettext('OAuth token refreshed')
                try:
                    user_token = refresh_token(user_token, target_url,
                                               oauth_info)
                except Exception as e:
                    result_msg = str(e)

                if user_token:
                    # Update the header with the new token
                    headers = {
                        'content-type':
                        'application/x-www-form-urlencoded; charset=UTF-8',
                        'Authorization':
                        'Bearer {0}'.format(user_token.access_token),
                    }

                    # Second attempt at executing the API call
                    response = requests.post(url=conversation_url,
                                             data=canvas_email_payload,
                                             headers=headers)
                    response_status = response.status_code
            elif response_status != status.HTTP_201_CREATED:
                result_msg = \
                    ugettext('Unable to deliver message (code {0})').format(
                        response_status
                    )
        else:
            # Print the JSON that would be sent through the logger
            logger.info('SEND JSON({0}): {1}'.format(
                target_url, json.dumps(canvas_email_payload)))
            response_status = 200

        # Append the response status
        status_vals.append(
            (response_status, result_msg,
             datetime.datetime.now(pytz.timezone(ontask_settings.TIME_ZONE)),
             canvas_email_payload))

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

    # Log all OBJ sent
    for st_val, result_msg, dt, json_obj in status_vals:
        context['object'] = json.dumps(json_obj)
        context['status'] = st_val
        context['result'] = result_msg
        context['email_sent_datetime'] = str(dt)
        Log.objects.register(user, Log.ACTION_CANVAS_EMAIL_SENT,
                             action.workflow, context)

    # Update data in the log item
    log_item.payload['objects_sent'] = len(result)
    log_item.payload['filter_present'] = cfilter is not None
    log_item.payload['datetime'] = str(
        datetime.datetime.now(pytz.timezone(ontask_settings.TIME_ZONE)))
    log_item.save()

    return None
コード例 #4
0
def send_json(user, action, token, key_column, exclude_values, log_item):
    """
    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 token: String to include as authorisation token
    :param key_column: Key column name to use to exclude elements (if needed)
    :param exclude_values: List of values to exclude from the mailing
    :param log_item: Log object to store results
    :return: Send the json objects
    """

    # Evaluate the action string and obtain the list of list of JSON objects
    result = evaluate_action(action,
                             column_name=key_column,
                             exclude_values=exclude_values)

    # Check the type of the result to see if it was successful
    if not isinstance(result, list):
        # Something went wrong. The result contains a message
        return result

    # Update the number of filtered rows if the action has a filter (table
    # might have changed)
    cfilter = action.get_filter()
    if cfilter and cfilter.n_rows_selected != len(result):
        cfilter.n_rows_selected = len(result)
        cfilter.save()

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

    # Iterate over all json objects to create the strings and check for
    # correctness
    json_objects = []
    idx = 0
    for json_string in result:
        idx += 1
        try:
            json_obj = json.loads(json_string[0])
        except:
            return _('Incorrect JSON string in element number {0}').format(idx)
        json_objects.append(json_obj)

    # Send the objects to the given URL
    status_vals = []
    for json_obj in json_objects:
        if ontask_settings.EXECUTE_ACTION_JSON_TRANSFER:
            response = requests.post(url=action.target_url,
                                     data=json_obj,
                                     headers=headers)
            status = response.status_code
        else:
            logger.info('SEND JSON({0}): {1}'.format(action.target_url,
                                                     json.dumps(json_obj)))
            status = 200

        status_vals.append(
            (status,
             datetime.datetime.now(pytz.timezone(ontask_settings.TIME_ZONE)),
             json_obj))

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

    # Log all OBJ sent
    for status, dt, json_obj in status_vals:
        context['object'] = json.dumps(json_obj)
        context['status'] = status
        context['json_sent_datetime'] = str(dt)
        Log.objects.register(user, Log.ACTION_JSON_SENT, action.workflow,
                             context)

    # Update data in the log item
    log_item.payload['objects_sent'] = len(result)
    log_item.payload['filter_present'] = cfilter is not None
    log_item.payload['datetime'] = str(
        datetime.datetime.now(pytz.timezone(ontask_settings.TIME_ZONE)))
    log_item.save()

    return None
コード例 #5
0
def send_messages(user, action, subject, email_column, from_email,
                  cc_email_list, bcc_email_list, send_confirmation, track_read,
                  exclude_values, log_item):
    """
    Sends 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.

    The messages are sent in bursts with a pause in seconds as specified by the
    configuration variables EMAIL_BURST  and EMAIL_BURST_PAUSE

    :param user: User object that executed the action
    :param action: Action from where to take the messages
    :param subject: Email subject
    :param email_column: Name of the column from which to extract emails
    :param from_email: Email of the sender
    :param cc_email_list: List of emails to include in the CC
    :param bcc_email_list: List of emails to include in the BCC
    :param send_confirmation: Boolean to send confirmation to sender
    :param track_read: Should read tracking be included?
    :param exclude_values: List of values to exclude from the mailing
    :param log_item: Log object to store results
    :return: Send the emails
    """

    # Evaluate the action string, evaluate the subject, and get the value of
    # the email column.
    result = evaluate_action(action,
                             extra_string=subject,
                             column_name=email_column,
                             exclude_values=exclude_values)

    # Check the type of the result to see if it was successful
    if not isinstance(result, list):
        # Something went wrong. The result contains a message
        return result

    track_col_name = ''
    data_frame = None
    if track_read:
        data_frame = pandas_db.load_from_db(action.workflow.id)
        # Make sure the column name does not collide with an existing one
        i = 0  # Suffix to rename
        while True:
            i += 1
            track_col_name = 'EmailRead_{0}'.format(i)
            if track_col_name not in data_frame.columns:
                break

        # Get the log item payload to store the tracking column
        log_item.payload['track_column'] = track_col_name
        log_item.save()

    # Update the number of filtered rows if the action has a filter (table
    # might have changed)
    cfilter = action.get_filter()
    if cfilter and cfilter.n_rows_selected != len(result):
        cfilter.n_rows_selected = len(result)
        cfilter.save()

    # Set the cc_email_list and bcc_email_list to the right values
    if not cc_email_list:
        cc_email_list = []
    if not bcc_email_list:
        bcc_email_list = []

    # Check that cc and bcc contain list of valid email addresses
    if not all([validate_email(x) for x in cc_email_list]):
        return _('Invalid email address in cc email')
    if not all([validate_email(x) for x in bcc_email_list]):
        return _('Invalid email address in bcc email')

    # Everything seemed to work to create the messages.
    msgs = []
    track_ids = []
    for msg_body, msg_subject, msg_to in result:

        # If read tracking is on, add suffix for message (or empty)
        if track_read:
            # The track id must identify: action & user
            track_id = {
                'action': action.id,
                'sender': user.email,
                'to': msg_to,
                'column_to': email_column,
                'column_dst': track_col_name
            }

            track_str = \
                """<img src="https://{0}{1}{2}?v={3}" alt="" 
                    style="position:absolute; visibility:hidden"/>""".format(
                    Site.objects.get_current().domain,
                    ontask_settings.BASE_URL,
                    reverse('trck'),
                    signing.dumps(track_id)
                )
        else:
            track_str = ''

        # Get the plain text content and bundle it together with the HTML in
        # a message to be added to the list.
        text_content = html2text.html2text(msg_body)
        msg = EmailMultiAlternatives(msg_subject,
                                     text_content,
                                     from_email, [msg_to],
                                     bcc=bcc_email_list,
                                     cc=cc_email_list)
        msg.attach_alternative(msg_body + track_str, "text/html")
        msgs.append(msg)
        track_ids.append(track_str)

    # Add the column if needed (before the mass email to avoid overload
    if track_read:
        # Create the new column and store
        column = Column(
            name=track_col_name,
            description_text='Emails sent with action {0} on {1}'.format(
                action.name, str(timezone.now())),
            workflow=action.workflow,
            data_type='integer',
            is_key=False,
            position=action.workflow.ncols + 1)
        column.save()

        # Increase the number of columns in the workflow
        action.workflow.ncols += 1
        action.workflow.save()

        # Initial value in the data frame and store the table
        data_frame[track_col_name] = 0
        ops.store_dataframe_in_db(data_frame, action.workflow.id)

    # Partition the list of emails into chunks as per the value of EMAIL_BURST
    chunk_size = len(msgs)
    wait_time = 0
    if ontask_settings.EMAIL_BURST:
        chunk_size = ontask_settings.EMAIL_BURST
        wait_time = ontask_settings.EMAIL_BURST_PAUSE
    msg_chunks = [
        msgs[i:i + chunk_size] for i in range(0, len(msgs), chunk_size)
    ]
    for idx, msg_chunk in enumerate(msg_chunks):
        # Mass mail!
        try:
            connection = mail.get_connection()
            connection.send_messages(msg_chunk)
        except Exception as e:
            # Something went wrong, notify above
            return str(e)

        if idx != len(msg_chunks) - 1:
            logger.info(
                'Email Burst ({0}) reached. Waiting for {1} secs'.format(
                    len(msg_chunk), wait_time))
            sleep(wait_time)

    # Log the events (one per email)
    now = datetime.datetime.now(pytz.timezone(ontask_settings.TIME_ZONE))
    context = {
        'user': user.id,
        'action': action.id,
        'email_sent_datetime': str(now),
    }
    for msg, track_id in zip(msgs, track_ids):
        context['subject'] = msg.subject
        context['body'] = msg.body
        context['from_email'] = msg.from_email
        context['to_email'] = msg.to[0]
        if track_id:
            context['track_id'] = track_id
        Log.objects.register(user, Log.ACTION_EMAIL_SENT, action.workflow,
                             context)

    # Update data in the log item
    log_item.payload['objects_sent'] = len(result)
    log_item.payload['filter_present'] = cfilter is not None
    log_item.payload['datetime'] = str(
        datetime.datetime.now(pytz.timezone(ontask_settings.TIME_ZONE)))
    log_item.save()

    # If no confirmation email is required, done
    if not send_confirmation:
        return None

    # Creating the context for the confirmation email
    context = {
        'user': user,
        'action': action,
        'num_messages': len(msgs),
        'email_sent_datetime': now,
        'filter_present': cfilter is not None,
        'num_rows': action.workflow.nrows,
        'num_selected': cfilter.n_rows_selected if cfilter else -1
    }

    # Create template and render with context
    try:
        html_content = Template(str(getattr(settings,
                                            'NOTIFICATION_TEMPLATE'))).render(
                                                Context(context))
        text_content = strip_tags(html_content)
    except TemplateSyntaxError as e:
        return _('Syntax error detected in OnTask notification template '
                 '({0})').format(e)

    # Log the event
    Log.objects.register(
        user, Log.ACTION_EMAIL_NOTIFY, action.workflow, {
            'user': user.id,
            'action': action.id,
            'num_messages': len(msgs),
            'email_sent_datetime': str(now),
            'filter_present': cfilter is not None,
            'num_rows': action.workflow.nrows,
            'subject': str(getattr(settings, 'NOTIFICATION_SUBJECT')),
            'body': text_content,
            'from_email': str(getattr(settings, 'NOTIFICATION_SENDER')),
            'to_email': [user.email]
        })

    # Send email out
    try:
        send_mail(str(getattr(settings, 'NOTIFICATION_SUBJECT')),
                  text_content,
                  str(getattr(settings, 'NOTIFICATION_SENDER')), [user.email],
                  html_message=html_content)
    except Exception as e:
        return _('An error occurred when sending your notification: '
                 '{0}').format(e)

    return None