RecordActionButton

Record action buttons are individual elements or modules that make up the profile and managed through a Django Admin model. Each button in the profile serves a specific function, enabling users to perform various actions on records within the profile.


RecordActionButton Database Table Structure

The RecordActionButton in Django Admin provides a structured way to manage and perform actions on profile records. Each button is meticulously defined with attributes that ensure it functions correctly and provides clear feedback to users.

These actions can include creating, updating, submitting or deleting records among other functions. Understanding the fields and their purposes can significantly enhance the management and usability of profiles in a Django application

The PostgreSQL table RecordActionButton consists of the following fields:

  • id (Integer):

    The unique identifier for the action button. It is auto-incremented by the database.

  • name (String):

    The internal name of the action button, used in the back-end code to reference the button.

  • title (String):

    The display title of the action button, shown to users in the UI. It describes the action the button performs in a concise manner.

  • label (String):

    The short label shown on the button, providing a brief indication was the button does.

  • type (String):

    The type of action the button performs, such as single (acting on a single record) or multi (acting on multiple records).

  • error_message (String):

    The error message displayed to users if the action cannot be completed. This helps in providing feedback to users about why an action failed.

  • icon_class (String):

    The CSS class for the icon associated with the button, providing a visual representation of the button’s action and help improve user interface design.

  • action (String):

    The specific action performed by the button, often mapped to a function or a URL ndpoint that the action will call.

  • icon_colour (String):

    The colour of the icon used for UI consistency and visual cues thereby helping users to quickly identify the type of action.

Hint

Click the collapsible-item-arrow button below to view the contents

Example records for the ProfileActionButton model, detailing the various actions available within a profile
 id |              name               |                   title                    |          label           |  type  |                                     error_message                                     |      icon_class       |          action          | icon_colour
----+---------------------------------+--------------------------------------------+--------------------------+--------+---------------------------------------------------------------------------------------+-----------------------+--------------------------+-------------
  1 | add_record_all                  | Add new record                             | Add                      |        |                                                                                       | fa fa-plus            | add                      | blue
  2 | edit_record_single              | Edit record                                | Edit                     | single | Please select a record to edit                                                        | fa fa-pencil-square-o | edit                     | green
  3 | delete_record_multi             | Delete records                             | Delete                   | multi  | Please select one or more records to delete                                           | fa fa-trash-can       | validate_and_delete      | red
  4 | submit_assembly_multi           | Submit Assembly                            | Submit                   | multi  | Please select one or more record to submit                                            | fa fa-info            | submit_assembly          |
  5 | submit_annotation_multi         | Submit Annotation                          | Submit                   | multi  | Please select one or more record to submit                                            | fa fa-info            | submit_annotation        | teal
  6 | submit_read_multi               | Submit Read                                | Submit                   | multi  | Please select one or more record to submit                                            | fa fa-info            | submit_read              | teal
  7 | add_local_all                   | Add new file by browsing local file system | Add                      |        | Add new file by browsing local file system                                            | fa fa-desktop         | add_files_locally        | blue
  8 | add_terminal_all                | Add new file by terminal                   | Add                      |        |                                                                                       | fa fa-terminal        | add_files_by_terminal    | blue
  9 | submit_tagged_seq_multi         | Submit Tagged Sequence                     | Submit                   | multi  | Please select one or more record to submit                                            | fa fa-info            | submit_tagged_seq        | teal
 10 | download_sample_manifest_single | Download Sample Manifest                   | Download sample manifest | single | Please select one of samples in the manifest to download                              | fa fa-download        | download-sample-manifest | blue
 11 | view_images_multiple            | View Images                                | View images              | multi  | Please select one or more sample records from the table shown to view images for      | fa fa-eye             | view-images              | teal
 12 | download_permits_multiple       | Download Permits                           | Download permits         | multi  | Please select one or more sample records from the table shown to download permits for | fa fa-download        | download-permits         | orange
 13 | releasestudy                    | Release Study                              | Release Study            | single |                                                                                       | fa fa-globe           | release_study            | blue


Description of some RecordActionButton records


Referencing Created RecordActionButton in Project

Hint

Click the collapsible-item-arrow button below to view the contents

  • In the views.py, define the views to render the template containing the buttons

RecordActionButton example views.py
# myapp/views.py
from django.shortcuts import render
from django.views import View
from .models import TitleButton
import pandas as pd
from .utils import get_resolver

class TitleButtonView(View):
    def get(self, request):
        my_models = TitleButton.objects.all()
        return render(request, 'myapp/index.html', {'title_button_def': my_models})
class BrokerVisuals:
    def __init__(self, **kwargs):
        self.param_dict = kwargs
        self.component = self.param_dict.get("component", str())
        self.profile_id = self.param_dict.get("profile_id", str())
        self.user_id = self.param_dict.get("user_id", str())
        self.context = self.param_dict.get("context", dict())
        self.request_dict = self.param_dict.get("request_dict", dict())
        self.da_object = self.param_dict.get("da_object", DAComponent(self.profile_id, self.component))

    def do_server_side_table_data(self):
        self.context["component"] = self.component
        request_dict = self.param_dict.get("request_dict", dict())

        data = generate_server_side_table_records(self.profile_id, component=self.component, request=request_dict)
        self.context["draw"] = data["draw"]
        self.context["records_total"] = data["records_total"]
        self.context["records_filtered"] = data["records_filtered"]
        self.context["data_set"] = data["data_set"]

        return self.context

class DAComponent:
    def __init__(self, profile_id=None, component=str()):
        self.profile_id = profile_id
        self.component = component
def visualize(request):
    context = dict()

    task = request.POST.get("task", str())
    profile_id = request.session.get("profile_id", str())

    component = request.POST.get("component", str())
    da_object = DAComponent(profile_id=profile_id, component=request.POST.get("component", str()))

    if component in da_dict:
        da_object = da_dict[component](profile_id=profile_id)

    target_id = request.POST.get("target_id", None)

    if component == "read" and target_id:
        target_id = target_id.split("_")[0]

    broker_visuals = BrokerVisuals(context=context,
                                   profile_id=profile_id,
                                   request=request,
                                   user_id=request.user.id,
                                   component=request.POST.get("component", str()),
                                   target_id=target_id,
                                   da_object=da_object)

    task_dict = dict(server_side_table_data=broker_visuals.do_server_side_table_data)

    if task in task_dict:
        context = task_dict[task]()

    out = jsonpickle.encode(context, unpicklable=False)
    return HttpResponse(out, content_type='application/json')

def get_not_deleted_flag():
    """
    provides a consistent way of setting records as not deleted
    :return:
    """
    return "0"

def resolve_control_output_apply(data, args):
    if args.get("type", str()) == "array":  # resolve array data types
        resolved_value = list()
        for d in data:
            resolved_value.append(get_resolver(d, args))
    else:  # non-array types
        resolved_value = get_resolver(data, args)

    return resolved_value

def generate_server_side_table_records(profile_id=str(), da_object=None, request=dict()):
    # function generates component records for building an UI table using server-side processing
    # - please note that for effective data display,
    # all array and object-type fields (e.g., characteristics) are deferred to sub-table display.
    # please define such in the schema as "show_in_table": false and "show_as_attribute": true

    data_set = list()

    # assumes 10 records per page if length not set
    n_size = int(request.get("length", 10))
    draw = int(request.get("draw", 1))
    start = int(request.get("start", 0))

    return_dict = dict()

    records_total = da_object.get_collection_handle().count_documents(
        {'profile_id': profile_id, 'deleted': get_not_deleted_flag()})

    # retrieve and process records
    filter_by = dict()

    # get and filter schema elements based on displayable columns
    schema = [x for x in da_object.get_schema().get(
        "schema_dict") if x.get("show_in_table", True)]

    # build db column projection
    projection = [(x["id"].split(".")[-1], 1) for x in schema]

    # order by
    sort_by = request.get('order[0][column]', '0')
    sort_by = request.get('columns[' + sort_by + '][data]', '')
    sort_direction = request.get('order[0][dir]', 'asc')

    sort_by = '_id' if not sort_by else sort_by
    sort_direction = 1 if sort_direction == 'asc' else -1

    # search
    search_term = request.get('search[value]', '').strip()

    records = da_object.get_all_records_columns_server(sort_by=sort_by,
                                                       sort_direction=sort_direction,
                                                       search_term=search_term,
                                                       projection=dict(projection),
                                                       limit=n_size,
                                                       skip=start,
                                                       filter_by=filter_by)
    records_filtered = records_total

    if search_term:
        records_filtered = da_object.get_collection_handle().count_documents(
            {'profile_id': profile_id, 'deleted': get_not_deleted_flag(),
             'name': {'$regex': search_term, "$options": 'i'}})

    if records:
        df = pd.DataFrame(records)

        df['record_id'] = df._id.astype(str)
        df["DT_RowId"] = df.record_id
        df.DT_RowId = 'row_' + df.DT_RowId
        df = df.drop('_id', axis='columns')

        for x in schema:
            x["id"] = x["id"].split(".")[-1]
            df[x["id"]] = df[x["id"]].apply(
                resolve_control_output_apply, args=(x,)).astype(str)

        data_set = df.to_dict('records')

    return_dict["records_total"] = records_total
    return_dict["records_filtered"] = records_filtered
    return_dict["data_set"] = data_set
    return_dict["draw"] = draw

    return return_dict
  • In the template HTML file (myapp.html), reference each element from the RecordActionButton table.

RecordActionButton example template
<!-- myapp/templates/myapp/component.html -->
{% load static %}
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=10">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>My App</title>

        <link rel="stylesheet" href="{% static 'myapp/css/myapp.css' %}">

        <script src="{% static 'myapp/js/myapp.js' %}"></script>
        <!-- jQuery -->
        <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
        <!-- DataTables CSS -->
        <link rel="stylesheet" href="https://cdn.datatables.net/1.11.3/css/jquery.dataTables.min.css">
        <!-- DataTables JS -->
        <script src="https://cdn.datatables.net/1.11.3/js/jquery.dataTables.min.js"></script>

        <script>
            const component_def = {
              {% for component in component_def %}
                {{ component.name|lower }}:  {
                  component: '{{ component.name }}',
                  title: '{{ component.title }}',
                  {% if component.subtitle %}
                  subtitle: '{{ component.subtitle }}',
                  {% endif %}
                  iconClass: '{{ component.widget_icon_class }}',
                  semanticIcon: '{{ component.widget_icon }}',
                  buttons: [ {% for button in component.title_buttons.all %} '{{ button.name }}', {% endfor %} ],
                  sidebarPanels: ['copo-sidebar-info'],
                  colorClass: '{{ component.widget_colour_class }}',
                  color: '{{ component.widget_colour }}',
                  tableID: '{{ component.table_id }}',
                  recordActions: [ {% for button in component.recordaction_buttons.all %} '{{ button.name }}', {% endfor %} ],
                  url: "{% if component.reverse_url %}{% url component.reverse_url profile_id='999' %}{% endif %}",
                },
              {% endfor %}
            }

        const title_button_def = {
          {% autoescape off %}
              {% for button in title_button_def %}
                {{ button.name  }} : {
                  template: `{{ button.template }}`,
                  additional_attr: '{{ button.additional_attr }}',
                  },
              {% endfor  %}
          {% endautoescape %}
        }

        const profile_type_def = {
          {% for profile_type in profile_type_def %}
            {{ profile_type.type|lower }}:  {
              title: '{{ profile_type.type | upper }}',
              widget_colour: '{{ profile_type.widget_colour }}',
              components: [ {% for component in profile_type.components.all  %} '{{ component.name }}', {% endfor %} ]
            },
          {% endfor %}
        }
        function get_profile_components(profile_type) {
            return profile_type_def[profile_type.toLowerCase()].components.map(component => component_def[component]);
        }
        </script>
    </head>
    <body>
        <div class="container">
            <div class="row">
                <div class="col-sm-9 col-md-9 col-lg-9">
                    <div hidden id="hidden_attrs">
                       <!-- hidden attributes  -->
                       <input type="hidden" id="nav_component_name" value="component_name"/>
                    </div>
                    {% block content %}
                        <!-- component_table -->
                        <div id="component_table_loader" class="col-sm-5 col-md-5 col-lg-5 col-xs-offset-5"
                             data-copo-tour-id="component_name_table">
                        </div> <!-- div used by the quick tour agent -->
                        <div class="table-parent-div">
                            <table id="component_name_table" class="ui celled table hover"
                                   cellspacing="0" width="100%">
                            </table>
                        </div>
                    {% endblock content %}
                </div>
            </div>
         </div>
    </body>
</html>

  • Handle any JavaScript functionality needed for the buttons in the JS file (myapp.js)

RecordActionButton example javascript
// record_action_button.js
var visualizeURL = '/myapp/visualize/';
var server_side_select = {}; //holds selected ids for table data - needed in server-side processing

$(document).ready(function () {
    var componentName = $('#nav_component_name').val();

    // 'component_def' is defined in the 'record_action_button.html' file
    var componentMeta = null;
    componentMeta = component_def[componentName]

    do_render_server_side_table(componentMeta);
});

function place_task_buttons(componentMeta) {
  //place custom buttons on table
  var is_custom_buttons_needed = false;

  var customButtons = $('<span/>', {
    style: 'padding-left: 15px;',
    class: 'copo-table-cbuttons',
  });

  if (componentMeta.recordActions.length) {
    componentMeta.recordActions.forEach(function (item) {
      button_str = record_action_button_def[item].template
      var actionBTN = $(button_str);
      /*
      var actionBTN = $('.record-action-templates')
        .find('.' + item)
        .clone();
      */
      actionBTN.removeClass(item);
      actionBTN.attr('data-table', componentMeta.tableID);
      customButtons.append(actionBTN);
    });
    is_custom_buttons_needed = true;
  }



 $('.components_custom_templates').find('.record-action-custom-template').each(function () {
    var actionBTN = $(this).clone();
    actionBTN.removeClass('record-action-custom-template');
    customButtons.append(actionBTN);
    is_custom_buttons_needed = true;
 }) ;

 if (is_custom_buttons_needed) {
  var table = $('#' + componentMeta.tableID).DataTable();
  $(table.buttons().container()).append(customButtons);
  refresh_tool_tips();
  //table action buttons
  do_table_buttons_events();
 }
}

function do_table_buttons_events_server_side(component) {
  //attaches events to table buttons - server-side processing version to function with similar name

  $(document)
    .off('click', '.copo-dt')
    .on('click', '.copo-dt', function (event) {
      event.preventDefault();

      $('.copo-dt').webuiPopover('destroy');

      var elem = $(this);
      var task = elem.attr('data-action').toLowerCase(); //action to be performed e.g., 'Edit', 'Delete'
      var tableID = elem.attr('data-table'); //get target table
      var btntype = elem.attr('data-btntype'); //type of button: single, multi, all
      var title = elem.find('.action-label').html();
      var message = elem.attr('data-error-message');

      if (!title) {
        title = 'Record action';
      }

      if (!message) {
        message = 'No records selected for ' + title + ' action';
      }

      //validate event before passing to handler
      var selectedRows = server_side_select[component].length;

      var triggerEvent = true;

      //do button type validation based on the number of records selected
      if (btntype == 'single' || btntype == 'multi') {
        if (selectedRows == 0) {
          triggerEvent = false;
        } else if (selectedRows > 1 && btntype == 'single') {
          //sort out 'single record buttons'
          triggerEvent = false;
        }
      }

      if (triggerEvent) {
        //trigger button event, else deal with error
        var event = jQuery.Event('addbuttonevents');
        event.tableID = tableID;
        event.task = task;
        event.title = title;
        $('body').trigger(event);
      } else {
        //alert user
        button_event_alert(title, message);
      }
    });
}

function do_render_server_side_table(componentMeta) {
  // Use this function for server-side processing of large tables
  var csrftoken = $.cookie('csrftoken');

  try {
    componentMeta.table_columns = JSON.parse($('#table_columns').val());
  } catch (err) {}

  var tableID = componentMeta.tableID;
  var component = componentMeta.component;

  var columnDefs = [];
  var table = null;

  if ($.fn.dataTable.isDataTable('#' + tableID)) {
    // get table instance
    table = $('#' + tableID).DataTable();
  }

  if (table) {
    //if table instance already exists, refresh
    table.draw();
  }
  else {
    server_side_select[component] = [];

    table = $('#' + tableID).DataTable({
      paging: true,
      processing: true,
      serverSide: true,
      searchDelay: 850,
      columns: componentMeta.table_columns,
      ajax: {
        url: visualizeURL,
        type: 'POST',
        headers: {
          'X-CSRFToken': csrftoken,
        },
        data: {
          task: 'server_side_table_data',
          component: component,
        },
        dataFilter: function (data) {
          var json = jQuery.parseJSON(data);
          json.recordsTotal = json.records_total;
          json.recordsFiltered = json.records_filtered;
          json.data = json.data_set;

          return JSON.stringify(json); // return JSON string
        },
      },
      rowCallback: function (row, data) {
        if ($.inArray(data.DT_RowId, server_side_select[component]) !== -1) {
          $(row).addClass('selected');
        }
      },
      createdRow: function (row, data, index) {
      },
      fnDrawCallback: function () {
      },
      buttons: [
        {
          text: 'Select visible records',
          action: function (e, dt, node, config) {
            //remove custom select info
            $('#' + tableID + '_info')
              .find('.select-item-1')
              .remove();

            dt.rows().select();
            var selectedRows = table.rows('.selected').ids().toArray();

            for (var i = 0; i < selectedRows.length; ++i) {
              var index = $.inArray(
                selectedRows[i],
                server_side_select[component]
              );

              if (index === -1) {
                server_side_select[component].push(selectedRows[i]);
              }
            }

            $('#' + tableID + '_info')
              .find('.select-row-message')
              .html(server_side_select[component].length + ' records selected');
          },
        },
        {
          text: 'Clear selection',
          action: function (e, dt, node, config) {
            dt.rows().deselect();
            server_side_select[component] = [];
            $('#' + tableID + '_info')
              .find('.select-item-1')
              .remove();
          },
        },
      ],
      columnDefs: columnDefs,
      language: {
        select: {
          rows: {
            _: "<span class='select-row-message'>%d records selected</span>",
            0: '',
            1: '%d record selected',
          },
        },
        processing: "<div class='copo-i-loader'></div>",
      },
      dom: 'Bfr<"row"><"row info-rw" i>tlp',
    });

    table
      .buttons()
      .nodes()
      .each(function (value) {
        $(this).removeClass('btn btn-default').addClass('tiny ui button');
      });

    place_task_buttons(componentMeta); //this will place custom buttons on the table for executing tasks on records

    do_table_buttons_events_server_side(component);

     table.on('click', 'tr >td', function () {
      var classList = [
        'annotate-datafile',
        'summary-details-control',
        'detail-hover-message',
      ]; //don't select on columns with these classes
      var foundClass = false;

      var tdList = this.className.split(' ');

      for (var i = 0; i < tdList.length; ++i) {
        if ($.inArray(tdList[i], classList) > -1) {
          foundClass = true;
          break;
        }
      }

      if (foundClass) {
        return false;
      }

      var elem = $(this).closest('tr');

      var id = elem.attr('id');
      var index = $.inArray(id, server_side_select[component]);

      if (index === -1) {
        server_side_select[component].push(id);
      } else {
        server_side_select[component].splice(index, 1);
      }

      elem.toggleClass('selected');

      //selected message
      $('#' + tableID + '_info')
        .find('.select-item-1')
        .remove();
      var message = '';

      if ($('#' + tableID + '_info').find('.select-row-message').length) {
        if (server_side_select[component].length > 0) {
          message = server_side_select[component].length + ' records selected';
          if (server_side_select[component].length == 1) {
            message = server_side_select[component].length + ' record selected';
          }

          $('#' + tableID + '_info')
            .find('.select-row-message')
            .html(message);
        } else {
          $('#' + tableID + '_info')
            .find('.select-row-message')
            .html('');
        }
      } else {
        if (server_side_select[component].length > 0) {
          message = server_side_select[component].length + ' records selected';
          if (server_side_select[component].length == 1) {
            message = server_side_select[component].length + ' record selected';
          }
          $('#' + tableID + '_info').append(
            "<span class='select-item select-item-1'>" + message + '</span>'
          );
        }
      }
    });
  }

  let table_wrapper = $('#' + tableID + '_wrapper');

  table_wrapper.find('.dt-buttons').css({ float: 'right' });

  table_wrapper
    .find('.dataTables_filter')
    .find('label')
    .css({ padding: '10px 0' })
    .find('input')
    .removeClass('input-sm')
    .attr('placeholder', 'Search ' + componentMeta.title)
    .attr('size', 30);

  $('<br><br>').insertAfter(table_wrapper.find('.dt-buttons'));

  //handle event for table details

  //handle event for annotation of datafile

} //end of func

Visualisation of RecordActionButton in Project

Visualisation of the add, edit, delete and submit record action buttons on the Assembly web page

Assembly web page: Visualisation of the add, edit, delete record and submit action buttons

  • add_record_all button is labelled Add and uses a add-icon icon. It is indicated by the blue arrow.

  • edit_record_single button is labelled Edit and uses edit-icon icon. It is indicated by the green arrow.

  • delete_record_multi button is labelled Delete and uses a delete-icon icon. It is indicated by the red arrow. The icon and colour of this button is used on multiple web pages with different actions.

  • submit_assembly_multi button is labelled Submit and uses a info-icon icon. The icon and colour used in for this button, is also used for the submit_annotation_multi, submit_read_multi and submit_tagged_seq_multi buttons.

    The difference is in the label assigned and the action performed by the button. The button is indicated by the teal arrow in the image above.


Visualisation of the 'download sample manifest' button, 'view images' button and 'download permits' buttons on the Samples web page

Samples web page: Visualisation of the download sample manifest action button, view images action button and download permits action button

  • add_local_all button is labelled Add new file by browsing local file system and uses a computer-icon icon. It is indicated by the blue arrow on the right in the image above.

  • add_terminal_all button is labelled Add new file by terminal and uses a terminal-icon icon. It is indicated by the blue arrow on the left in the image above.


Visualisation of the 'add file by browser' record action button and 'add file via terminal' record action button on the Samples web page

Samples web page: Visualisation of the add file via browser record action button and add file via terminal record action button

  • download_sample_manifest_single button is labelled Download Sample Manifest and uses a download-icon1 icon. It is indicated by the blue arrow in the image above.

  • view_images_multiple button is labelled View Images and uses a eye-icon icon. It is indicated by the teal arrow.

  • download_permits_multiple button is labelled Download Permits and uses a download-icon2 icon. It is indicated by the orange arrow.


Visualisation of the release study record action button on the Work Profiles web page

Work Profiles web page: Visualisation of the release study record action button on a profile

  • releasestudy button is labelled Release Study and uses a globe-icon icon. It is indicated by the blue arrow in the image above.