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
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
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
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
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