Ejemplo n.º 1
0
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]
Ejemplo n.º 2
0
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)
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
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
    """
    # 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
Ejemplo n.º 5
0
    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
Ejemplo n.º 6
0
    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
Ejemplo n.º 7
0
    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
Ejemplo n.º 8
0
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)
    ]
Ejemplo n.º 9
0
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
Ejemplo n.º 10
0
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})