def get_table_row_by_index( workflow, filter_formula, idx: int, ): """Select the set of elements in the row with the given index. :param workflow: Workflow object storing the data :param filter_formula: Condition object to filter the data (or None) :param idx: Row number to get (first row is idx = 1) :return: A dictionary with the (column_name, value) data or None if the index is out of bounds """ # Get the data df_data = get_rows( workflow.get_data_frame_table_name(), column_names=workflow.get_column_names(), filter_formula=filter_formula) # If the data is not there, return None if idx > df_data.rowcount: return None return df_data.fetchall()[idx - 1]
def get_subframe( table_name: str, filter_formula, column_names: List[str], ) -> pd.DataFrame: """Load the subframe using the filter and column names. Execute a select query to extract a subset of the dataframe and turn the resulting query set into a data frame. :param table_name: Table :param filter_formula: Formula to filter the data (or None) :param column_names: [list of column names], QuerySet with the data rows :return: DataFrame """ # Create the DataFrame and set the column names return pd.DataFrame.from_records( get_rows( table_name, column_names=column_names, filter_formula=filter_formula, ).fetchall(), columns=column_names, coerce_float=True)
def clean(self): """Detect uniques values in one column, and different column names.""" form_data = super().clean() # Move data to the payload so that is ready to be used self.action_info['item_column'] = form_data['participant_column'] self.action_info['user_fname_column'] = form_data['user_fname_column'] self.action_info['file_suffix'] = form_data['file_suffix'] self.action_info['zip_for_moodle'] = form_data['zip_for_moodle'] self.action_info['confirm_items'] = form_data['confirm_items'] # Participant column must be unique pcolumn = form_data['participant_column'] ufname_column = form_data['user_fname_column'] # The given column must have unique values if not is_column_unique( self.action.workflow.get_data_frame_table_name(), pcolumn, ): self.add_error( 'participant_column', _('Column needs to have all unique values (no empty cells)'), ) return form_data # If both values are given and they are identical, return with error if pcolumn and ufname_column and pcolumn == ufname_column: self.add_error( None, _('The two columns must be different'), ) return form_data # If a moodle zip has been requested if form_data.get('zip_for_moodle'): if not pcolumn or not ufname_column: self.add_error( None, _('A Moodle ZIP requires two column names'), ) return form_data # Participant columns must match the pattern 'Participant [0-9]+' pcolumn_data = get_rows( self.action.workflow.get_data_frame_table_name(), column_names=[pcolumn]) participant_error = any( not participant_re.search(str(row[pcolumn])) for row in pcolumn_data) if participant_error: self.add_error( 'participant_column', _('Values in column must have format ' + '"Participant [number]"'), ) return form_data
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 """ # 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), ) # Check field n_rows_selected (table may have been modified) action_filter = action.get_filter() if action_filter and action_filter.n_rows_selected != rows.rowcount: # Filter now returns different number of rows. Action conditions need # to be refreshed action_filter.n_rows_selected = rows.rowcount action.update_n_rows_selected(filter_formula=action_filter.formula) return list_of_renders
def clean(self): """Verify email values.""" form_data = super().clean() # Move data to the payload so that is ready to be used self.action_info['subject'] = form_data['subject'] self.action_info['item_column'] = form_data['email_column'] self.action_info['cc_email'] = [ email.strip() for email in form_data['cc_email'].split(',') if email ] self.action_info['bcc_email'] = [ email.strip() for email in form_data['bcc_email'].split(',') if email ] self.action_info['confirm_items'] = form_data['confirm_items'] self.action_info['send_confirmation'] = form_data['send_confirmation'] self.action_info['track_read'] = form_data['track_read'] self.action_info['export_wf'] = form_data['export_wf'] # Check if the values in the email column are correct emails try: column_data = get_rows( self.action.workflow.get_data_frame_table_name(), column_names=[self.action_info['item_column']]) if not all(is_correct_email(iname[0]) for iname in column_data): # column has incorrect email addresses self.add_error( 'email_column', _('The column with email addresses has incorrect values.'), ) except TypeError: self.add_error( 'email_column', _('The column with email addresses has incorrect values.'), ) all_correct = all( is_correct_email(email) for email in self.action_info['cc_email']) if not all_correct: self.add_error( 'cc_email', _('Field needs a comma-separated list of emails.'), ) all_correct = all( is_correct_email(email) for email in self.action_info['bcc_email']) if not all_correct: self.add_error( 'bcc_email', _('Field needs a comma-separated list of emails.'), ) return form_data
def extra_validation(self, validated_data): """Validate the presence of certain fields.""" act, execute, column, exclude, payload = super().extra_validation( validated_data) if act.action_type != Action.personalized_text: raise APIException(_('Incorrect type of action to schedule.')) subject = payload.get('subject') if not subject: raise APIException(_('Personalized text needs a subject.')) if not column: raise APIException(_('Personalized text needs an item_column')) # Check if the values in the email column are correct emails try: column_data = get_rows( act.workflow.get_data_frame_table_name(), column_names=[column.name]) if not all( is_correct_email(row[column.name]) for row in column_data ): # column has incorrect email addresses raise APIException( _('The column with email addresses has incorrect values.')) except TypeError: raise APIException( _('The column with email addresses has incorrect values.')) try: if not all( is_correct_email(email_val) for email_val in payload.get('cc_email', []) if email_val ): raise APIException( _('cc_email must be a comma-separated list of emails.')) except TypeError: raise APIException( _('cc_email must be a comma-separated list of emails.')) try: if not all( is_correct_email(email) for email in payload.get('bcc_email', []) if email ): raise APIException( _('bcc_email must be a comma-separated list of emails.')) except TypeError: raise APIException( _('bcc_email must be a comma-separated list of emails.')) return act, execute, column, exclude, payload
def __init__(self, form_data, *args, **kwargs): """Store action, column name and exclude init, adjust fields.""" self.action: Action = kwargs.pop('action', None) self.column_name: str = kwargs.pop('column_name', None) self.exclude_init: List[str] = kwargs.pop('exclude_values', list) super().__init__(form_data, *args, **kwargs) self.fields['exclude_values'].choices = get_rows( self.action.workflow.get_data_frame_table_name(), column_names=[self.column_name, self.column_name], filter_formula=self.action.get_filter_formula()).fetchall() self.fields['exclude_values'].initial = self.exclude_init
def create_eval_data_tuple( action: Action, participant_column: str, exclude_values: List, user_fname_column: Optional[str], ) -> List[Tuple[str, str, str]]: """Evaluate text and create list of tuples [filename, part id, text]. Evaluate the conditions in the actions based on the given participant_column excluding the values in exclude_values. This returns a list with tuples [action text, None, participant column value]. Process that list to insert as second element of the tuple the corresponding value in user_fname_column (if given). The final result is a list of triplets with: - Filename - part id as extracted from the participation column - HTML body text :param action: Action being processed :param participant_column: The :param exclude_values: List of values to exclude from evaluation :param user_fname_column: Column name to use for filename creation :return: List[Tuple[text, text, text]] """ # Obtain the personalised text action_evals = evaluate_action( action, column_name=participant_column, exclude_values=exclude_values) if user_fname_column: # Get the user_fname_column values user_fname_data = [row[user_fname_column] for row in get_rows( action.workflow.get_data_frame_table_name(), column_names=[user_fname_column], filter_formula=None).fetchall()] else: # Array of empty strings to concatenate user_fname_data = [''] * len(action_evals) return [ (user_fname, part_id, html_body.format(msg_body)) for (msg_body, part_id), user_fname in zip(action_evals, user_fname_data) ]
def scheduled_email_action_data_is_correct(action, cleaned_data): """Verify email action data. Verify the integrity of a ScheduledAction object with a Personalised_text type. The function returns a list of pairs (field name, message) with the errors, or [] if no error has been found. :param action: Action object to use for scheduling :param cleaned_data: :return: List of pairs (field name, error) or [] if correct """ pair_list = [] item_column = cleaned_data['item_column'] # Verify the correct time pair_list.extend(scheduled_action_data_is_correct(cleaned_data)) # Check if the values in the email column are correct emails try: column_data = get_rows(action.workflow.get_data_frame_table_name(), column_names=[item_column.name]) if not all(is_correct_email(row['email']) for row in column_data): # column has incorrect email addresses pair_list.append( ('item_column', _('The column with email addresses has incorrect values.')), ) except TypeError: pair_list.append( ('item_column', _('The column with email addresses has incorrect values.')), ) if not all( is_correct_email(email) for email in cleaned_data['cc_email'].split(',') if email): pair_list.append( ('cc_email', _('This field must be a comma-separated list of emails.')), ) if not all( is_correct_email(email) for email in cleaned_data['bcc_email'].split(',') if email): pair_list.append( ('bcc_email', _('This field must be a comma-separated list of emails.')), ) return pair_list
def stat_row_view( request: HttpRequest, pk: Optional[int] = None, workflow: Optional[Workflow] = None, ) -> HttpResponse: """Render stats for a row. Render the page with stats and visualizations for a row in the table and a view (subset of columns). The request must include key and value to get the right row. In principle, there is a visualisation for each row. :param request: HTTP request :param pk: View id to use :return: Render the page """ # If there is no workflow object, go back to the index # Get the pair key,value to fetch the row from the table update_key = request.GET.get('key') update_val = request.GET.get('val') if not update_key or not update_val: # Malformed request return render( request, 'error.html', {'message': _('Unable to visualize table row')}) # If a view is given, filter the columns columns_to_view = workflow.columns.all() column_names = workflow.get_column_names() if pk: view = workflow.views.filter(pk=pk).first() if not view: # View not found. Redirect to home return redirect('home') columns_to_view = view.columns.all() column_names = [col.name for col in columns_to_view] df = load_table( workflow.get_data_frame_table_name(), column_names, view.formula) else: # No view given, fetch the entire data frame df = load_table(workflow.get_data_frame_table_name()) # Get the row from the table row = get_rows( workflow.get_data_frame_table_name(), column_names=column_names, filter_pairs={update_key: update_val}, ).fetchone() vis_scripts = [] visualizations = [] context = {'style': 'width:400px; height:225px;'} for idx, column in enumerate(columns_to_view): # Skip primary keys (no point to represent any of them) if column.is_key: continue # Add the title and surrounding container visualizations.append('<h4>' + column.name + '</h4>') # If all values are empty, no need to proceed if all(not col for col in df[column.name]): visualizations.append( '<p>' + _('No values in this column') + '</p><hr/>') continue if row[column.name] is None or row[column.name] == '': visualizations.append( '<p class="alert-warning">' + _('No value for this student in this column') + '</p>', ) visualizations.append('<div style="display: inline-flex;">') col_viz = _get_column_visualisations( column, df[[column.name]], vis_scripts=vis_scripts, viz_id='column_{0}'.format(idx), single_val=row[column.name], context=context) visualizations.extend([viz.html_content for viz in col_viz]) visualizations.append('</div><hr/>') return render( request, 'table/stat_row.html', { 'value': update_val, 'vis_scripts': vis_scripts, 'visualizations': visualizations})