Skip to main content
Liaison

CAS API Table/Fixed Table Instructions

Overview

The CAS API and Expert integration automatically builds a table in Expert based on data from the CAS API. This prevents the need for manual updates.

CSDCAS Program Table

Integration Steps

Review the integration steps below. More information for select steps is below.

Step #

Task

Owner

1

Identify which tables will have the integration, who the SME is for the content (e.g., account team), and when the integration is needed by (e.g., cycle rollover, etc.).

E&K team

2

Identify the data needed for the table, including:

  • What data to include.
  • How to present the data.
  • Timeline for updating data (e.g., data refresh at Applicant Portal launch, ongoing feed, etc.).

E&K team

3

Create the table "stub" (i.e., table header and column names).

E&K team

4

Create the AWS S3 folder, CAS API subscription, retrieve the backlog (if any), and place it in the AWS S3 folder.

Implementation team

5

Configure the BuildTable() function for the table.

E&K team

6

Test the BuildTable() function (e.g., test against backlog, test automated delivery, etc.).

E&K team

7

Publish the new table.

E&K team

Step 3: Create the Table Stub

You can use the existing table header for the new table stub. Be sure to edit the table properties and select First row for headings.

Step 4: Configure the CAS API Subscription

The CAS API subscription must be set before the file can be sent to the AWS S3 bucket. On Amazon, a script that combines the individual JSON entries into a combined file when a new file appears in the S3 bucket.

All files are delivered to subdirectories under the “CAS” directory; the file naming conventions are:

  • CAS/atcas_organizations_<instanceId>/<instanceId>_<organizationId>.json
  • CAS/csdcas_programs_<instanceId>/<instanceId>_<organizationId>_<programId>.json
AWS S3 Bucket Details

Bucket: services-liaison
Region: us-east-1
Access key: AKIARGNW5KGAKUEUCHHH
Secret key: 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

CAS API Account

Username: HelpCenter
Password (Prelaunch and Production): p1NbG9B6n
API key: rawsu40KkOUm77HDYOeCadparjOYtqddDaLW4og0
User level: Instance

CAS API Subscriptions

Endpoint: POST/v1/applicationForms/{applicationFormId}/subscriptions

Request Bodies:
Be sure to replace "CASNAMEHERE" with the name of the relevant CAS in lowercase letters (e.g. "atcas_...")
The program payloads feed pages called "Participating Programs", while the organization payloads mostly feed pages called "GRE Codes"

PROGRAMS

01{
02    "notificationEmailAddress": "training@liaisonedu.com",
03    "subscriptionDetails": [
04        {
05            "event": "program.created",
06            "responseType": "fullResponse",
07            "destinationType": "AWSS3",
08            "apiVersion": "v1",
09            "awsS3Destination": {
10                "awsS3PathTemplate": "CAS/CASNAMEHERE_programs_<instanceId>/<instanceId>_<organizationId>_<programId>.json",
11                "awsS3BucketName": "services-liaison",
12                "awsS3Region": "us-east-1",
13                "awsS3AccessKey": "AKIARGNW5KGAKUEUCHHH",
14                "awsS3SecretKey": "CeJ5PJkbleGtrWyZeAD7u1Daf5J45+gdEUeL3zfq"
15            }
16        },
17        {
18            "event": "program.updated",
19            "responseType": "fullResponse",
20            "destinationType": "AWSS3",
21            "apiVersion": "v1",
22            "awsS3Destination": {
23                "awsS3PathTemplate": "CAS/CASNAMEHERE_programs_<instanceId>/<instanceId>_<organizationId>_<programId>.json",
24                "awsS3BucketName": "services-liaison",
25                "awsS3Region": "us-east-1",
26                "awsS3AccessKey": "AKIARGNW5KGAKUEUCHHH",
27                "awsS3SecretKey": "CeJ5PJkbleGtrWyZeAD7u1Daf5J45+gdEUeL3zfq"
28            }
29        }
30    ]
31}

ORGANIZATIONS

01{
02    "notificationEmailAddress": "training@liaisonedu.com",
03    "subscriptionDetails": [
04        {
05            "event": "organization.created",
06            "responseType": "fullResponse",
07            "destinationType": "AWSS3",
08            "apiVersion": "v1",
09            "awsS3Destination": {
10                "awsS3PathTemplate": "CAS/CASNAMEHERE_organizations_<instanceId>/<instanceId>_<organizationId>.json",
11                "awsS3BucketName": "services-liaison",
12                "awsS3Region": "us-east-1",
13                "awsS3AccessKey": "AKIARGNW5KGAKUEUCHHH",
14                "awsS3SecretKey": "CeJ5PJkbleGtrWyZeAD7u1Daf5J45+gdEUeL3zfq"
15            }
16        },
17        {
18            "event": "organization.updated",
19            "responseType": "fullResponse",
20            "destinationType": "AWSS3",
21            "apiVersion": "v1",
22            "awsS3Destination": {
23                "awsS3PathTemplate": "CAS/CASNAMEHERE_organizations_<instanceId>/<instanceId>_<organizationId>.json",
24                "awsS3BucketName": "services-liaison",
25                "awsS3Region": "us-east-1",
26                "awsS3AccessKey": "AKIARGNW5KGAKUEUCHHH",
27                "awsS3SecretKey": "CeJ5PJkbleGtrWyZeAD7u1Daf5J45+gdEUeL3zfq"
28            }
29        }
30    ]
31}

Step 4 and a 1/2: Batch Retrieve Payloads (if necessary)

As of now, subscriptions to the organization.created and organization.updated events will not fire upon cycle rollover. Organizations, in fact, are not bound by cycles, but a table associates organizations with application forms (unicas_config.dbo.associationOrg). For CAS API-Help Center integrations that use organization payloads, it will be necessary to manually retrieve the organizations and upload them to the appropriate folder in the S3 bucket.

Steps

  1. Retrieve the relevant payloads in bulk using the appropriate script below
    1. Don't run any other API calls with the HelpCenter user account while the script is running!!!
  2. Drop the created folder into the S3 bucket (see above for credentials)
    1. Load the newly created folder into the /services-liaison/CAS/ folder on the S3 bucket

Tools

Python https://www.python.org/downloads/
PyCharm https://www.jetbrains.com/pycharm/do...ection=windows
CyberDuck (log into S3 bucket) https://cyberduck.io/download/

Scripts

Organizations

001import json
002import os
003import pandas as pd
004import requests
005from datetime import datetime
006import time  # pause in between file generation status checks
007 
008# credentials, root url, application form id, organization id
009print("credentials, root url, application form id, organization id")
010casName = "csdcas"
011applicationFormId = 6801
012username = 'HelpCenter'
013password = 'p1NbG9B6n'
014apiKey = 'rawsu40KkOUm77HDYOeCadparjOYtqddDaLW4og0'
016saveDir = f'C:\\Users\\gmartin\\OneDrive - Liaison International\\001_Projects\\Client_Projects\\MindTouch\\{casName}_organizations_{applicationFormId}\\'
017print(casName)
018print(applicationFormId)
019print(username)
020print(password)
021print(apiKey)
022print(rootUrl)
023print(saveDir)
024 
025runtime = datetime.now()
026runtimestamp = runtime.strftime("%Y-%m-%dT%H%M")
027print(runtimestamp)
028 
029# Define a function for authorization
030print("Define a function for authorization")
031def auth(apiKey, UserName, Password, baseUrl):
032    """
033        Authorize session for CAS API
034        :param apiKey: user account's CAS API key
035        :param UserName: user account's username
036        :param Password: user account's password
037        :param baseUrl: the base url for the target environment
038        :return: Authorization Token to be used with all requests
039    """
040    endpoint = "/v1/auth/token"
041    url = baseUrl + endpoint
042    payload = "{\"UserName\": \"" + UserName + "\",\"Password\": \"" + Password + "\"}"
043    headers = {
044        'Content-Type': "application/json",
045        'x-api-key': apiKey
046    }
047    response = requests.request("POST", url, data=payload, headers=headers)
048    responseJson = response.json()
049    token = responseJson["Token"]
050    # refreshToken = responseJson["RefreshToken"]
051    return token
052 
053 
054# Define a function for interacting with the CAS API
055print("Define a function for interacting with the CAS API")
056def casapi(endpoint, apiKey, UserName, Password, baseUrl="https://api.liaisonedu.com", requestType="GET", payload="", contentJson=False, extraHeaders={}, urlParams={}):
057    """
058        Interact with various CAS API endpoints
059        :param endpoint: the URL indicating the desired endpoint (see https://api.liaisonedu.com/reference/index.htm)
060        :param apiKey: the user's API key
061        :param UserName: the user's username
062        :param Password: the user's password
063        :param baseUrl: the root URL for the environment you want to interact with (Defaults to prod: baseUrl="https://api.liaisonedu.com")
064        :param requestType: the HTTP request to send e.g. "GET" (see https://api.liaisonedu.com/reference/index.htm)
065        :param payload: OPTIONAL the body of the HTTP request
066        :param contentJson: OPTIONAL adds a header indicating that the body of the request is "application/json"
067        :param extraHeaders: OPTIONAL additional headers for the request; send key-values as dict
068        :param urlParams: OPTIONAL additional URL string query parameters; send key-values as dict
069        :return: raw response from endpoint
070    """
071    if len(urlParams) == 0:
072        url = baseUrl + endpoint
073    else:
074        i = 1
075        urlQueryStr = ""
076        for urlParam, urlParamValue in urlParams.items():
077            if i == 1:
078                urlQueryPart = "?" + urlParam + "=" + str(urlParamValue)
079            else:
080                urlQueryPart = "&" + urlParam + "=" + str(urlParamValue)
081            urlQueryStr += urlQueryPart
082            i += 1
083        url = baseUrl + endpoint + urlQueryStr
084    token = auth(apiKey=apiKey, UserName=UserName, Password=Password, baseUrl=baseUrl)
085    if contentJson:
086        headers = {
087            'Authorization': token,
088            'x-api-key': apiKey,
089            'Content-Type': "application/json"
090        }
091        headers.update(extraHeaders)
092    else:
093        headers = {
094            'Authorization': token,
095            'x-api-key': apiKey,
096        }
097        headers.update(extraHeaders)
098    response = requests.request(requestType, url, data=payload, headers=headers)
099    return response
100 
101# make directory if not exist
102print('# make directory if not exist')
103print('directory already exists?\t\t' + str(os.path.isdir(saveDir)))
104if not os.path.isdir(saveDir):
105    os.mkdir(saveDir)
106    print('directory created?\t\t' + str(os.path.isdir(saveDir)))
107 
108# retrieve organization list
109print('# retrieve organization list')
110endpoint = f"/v1/applicationForms/{applicationFormId}/organizations"
111organizationList = casapi(endpoint, apiKey, username, password, rootUrl, "GET").json()
112print(len(organizationList))
113 
114# loop through organizations, retrieving details and saving as JSON
115print("# loop through organizations, retrieving details and saving as JSON")
116i = 0
117now = datetime.now()
118nowtimestamp = now.strftime("%Y-%m-%dT%H%M")
119print(nowtimestamp)
120for organization in organizationList:
121    time.sleep(2)
122    organizationId = organization["id"]
123    endpoint = f"/v1/applicationForms/{applicationFormId}/organizations/{organizationId}"
124    i += 1
125    now = datetime.now()
126    nowtimestamp = now.strftime("%Y-%m-%dT%H%M")
127    response = casapi(endpoint, apiKey, username, password, rootUrl, "GET")
128    organizationDetails = response.json()
129    print(nowtimestamp + "\t\t" + str(i) + "\t" + str(len(organizationList) - i) + " left\t\t" + str(applicationFormId) + "\t"
130          + str(organizationId) + "\t\t" + str(response.status_code))
131    if response.status_code == 200:
132        with open(saveDir+f"{applicationFormId}_{organizationId}.json", 'w') as file:
133            json.dump(organizationDetails, file)
134print("\r\rretrieval complete")
135now = datetime.now()
136runduration = (now-runtime).total_seconds()/60
137print("run duration: " + str(runduration) + " minutes")

Programs

001import json
002import os
003import pandas as pd
004import requests
005from datetime import datetime
006import time  # pause in between file generation status checks
007 
008# credentials, root url, application form id, organization id
009print("credentials, root url, application form id, organization id")
010casName = "unmc"
011applicationFormId = 6783
012username = 'HelpCenter'
013password = 'p1NbG9B6n'
014apiKey = 'rawsu40KkOUm77HDYOeCadparjOYtqddDaLW4og0'
016saveDir = f'C:\\Users\\gmartin\\OneDrive - Liaison International\\001_Projects\\Client_Projects\\MindTouch\\{casName}_programs_{applicationFormId}\\'
017print(casName)
018print(applicationFormId)
019print(username)
020print(password)
021print(apiKey)
022print(rootUrl)
023print(saveDir)
024 
025runtime = datetime.now()
026runtimestamp = runtime.strftime("%Y-%m-%dT%H%M")
027print(runtimestamp)
028 
029# Define a function for authorization
030print("Define a function for authorization")
031def auth(apiKey, UserName, Password, baseUrl):
032    """
033        Authorize session for CAS API
034        :param apiKey: user account's CAS API key
035        :param UserName: user account's username
036        :param Password: user account's password
037        :param baseUrl: the base url for the target environment
038        :return: Authorization Token to be used with all requests
039    """
040    endpoint = "/v1/auth/token"
041    url = baseUrl + endpoint
042    payload = "{\"UserName\": \"" + UserName + "\",\"Password\": \"" + Password + "\"}"
043    headers = {
044        'Content-Type': "application/json",
045        'x-api-key': apiKey
046    }
047    response = requests.request("POST", url, data=payload, headers=headers)
048    responseJson = response.json()
049    token = responseJson["Token"]
050    # refreshToken = responseJson["RefreshToken"]
051    return token
052 
053 
054# Define a function for interacting with the CAS API
055print("Define a function for interacting with the CAS API")
056def casapi(endpoint, apiKey, UserName, Password, baseUrl="https://api.liaisonedu.com", requestType="GET", payload="", contentJson=False, extraHeaders={}, urlParams={}):
057    """
058        Interact with various CAS API endpoints
059        :param endpoint: the URL indicating the desired endpoint (see https://api.liaisonedu.com/reference/index.htm)
060        :param apiKey: the user's API key
061        :param UserName: the user's username
062        :param Password: the user's password
063        :param baseUrl: the root URL for the environment you want to interact with (Defaults to prod: baseUrl="https://api.liaisonedu.com")
064        :param requestType: the HTTP request to send e.g. "GET" (see https://api.liaisonedu.com/reference/index.htm)
065        :param payload: OPTIONAL the body of the HTTP request
066        :param contentJson: OPTIONAL adds a header indicating that the body of the request is "application/json"
067        :param extraHeaders: OPTIONAL additional headers for the request; send key-values as dict
068        :param urlParams: OPTIONAL additional URL string query parameters; send key-values as dict
069        :return: raw response from endpoint
070    """
071    if len(urlParams) == 0:
072        url = baseUrl + endpoint
073    else:
074        i = 1
075        urlQueryStr = ""
076        for urlParam, urlParamValue in urlParams.items():
077            if i == 1:
078                urlQueryPart = "?" + urlParam + "=" + str(urlParamValue)
079            else:
080                urlQueryPart = "&" + urlParam + "=" + str(urlParamValue)
081            urlQueryStr += urlQueryPart
082            i += 1
083        url = baseUrl + endpoint + urlQueryStr
084    token = auth(apiKey=apiKey, UserName=UserName, Password=Password, baseUrl=baseUrl)
085    if contentJson:
086        headers = {
087            'Authorization': token,
088            'x-api-key': apiKey,
089            'Content-Type': "application/json"
090        }
091        headers.update(extraHeaders)
092    else:
093        headers = {
094            'Authorization': token,
095            'x-api-key': apiKey,
096        }
097        headers.update(extraHeaders)
098    response = requests.request(requestType, url, data=payload, headers=headers)
099    return response
100 
101# make directory if not exist
102print('# make directory if not exist')
103print('directory already exists?\t\t' + str(os.path.isdir(saveDir)))
104if not os.path.isdir(saveDir):
105    os.mkdir(saveDir)
106    print('directory created?\t\t' + str(os.path.isdir(saveDir)))
107 
108# retrieve program list
109print('# retrieve program list')
110endpoint = f"/v1/applicationForms/{applicationFormId}/programs"
111programList = casapi(endpoint, apiKey, username, password, rootUrl, "GET").json()
112print(len(programList))
113 
114# loop through programs, retrieving details and saving as JSON
115print("# loop through programs, retrieving details and saving as JSON")
116i = 0
117now = datetime.now()
118nowtimestamp = now.strftime("%Y-%m-%dT%H%M")
119print(nowtimestamp)
120for program in programList:
121    # if (i == 50):
122    #     continue
123    time.sleep(2)
124    organizationId = program["orgId"]
125    programId = program["id"]
126    endpoint = f"/v1/applicationForms/{applicationFormId}/organizations/{organizationId}/programs/{programId}"
127    i += 1
128    now = datetime.now()
129    nowtimestamp = now.strftime("%Y-%m-%dT%H%M")
130    response = casapi(endpoint,apiKey,username,password,rootUrl,"GET")
131    programDetails = response.json()
132    print(nowtimestamp + "\t\t" + str(i) + "\t" + str(len(programList) - i) + " left\t\t" + str(applicationFormId) + "\t"
133        + str(organizationId) + "\t" + str(programId) + "\t\t" + str(response.status_code))
134    if response.status_code==200:
135        with open(saveDir+f"{applicationFormId}_{organizationId}_{programId}.json", 'w') as file:
136            json.dump(programDetails, file)
137print("\r\rretrieval complete")
138now = datetime.now()
139runduration = (now-runtime).total_seconds()/60
140print("run duration: " + str(runduration) + " minutes")

Step 5: Configure the BuildTable Function

  1. Edit the page and insert the BuildTable script (https://help.liaisonedu.com/Template...tom/BuildTable), which downloads a combined JSON file and renders it based on the configuration options provided.
  2. On each table page, there are 3 components:
    1. Reference to the BuildTable script (usually collapsed).
    2. Configuration settings for the table (usually collapsed).
    3. Table with headers (these can be styled, resized, etc. as needed). The rows are filled in by the script.

      Table page components
Step B: Configure Table Settings

Sample options:

02 
03var SELECTOR = 'table tbody';
04var SORT_ORDER = ["orgName", "name", "type", "deadlineDisplay"];
05var DISPLAY_ORDER = ["state", "orgName", "name", "type", "deadlineDisplay",
06                     "programAttributes.supplementalApplicationRequired",
07                     "programAttributes.supplementalFeeRequired",
08                     "programAttributes.greRequired"];
09var IGNORE = function(item) {
10    return item.status == "draft" || item.status == "review";  
11};
12 
13var STYLE = function(row) {
14    var styles = {};
15     
16    if (row.concentration == "Application submitted by the deadline") {
17        styles["deadlineDisplay"] = "background: #3498db; font-weight: bold;";    // blue
18    }
19    if (row.concentration == "Application and transcripts received by deadline") {
20        styles["deadlineDisplay"] = "background: #f39c12; font-weight: bold;";    // orange
21    }
22    if (row.concentration == "Application verified by deadline") {
23        styles["deadlineDisplay"] = "background: #27ae60; font-weight: bold;";    // green
24    }
25     
26    styles.default = "";
27    return styles;
28}
29 
30buildTable(data_url, {SELECTOR: SELECTOR, SORT_ORDER: SORT_ORDER, DISPLAY_ORDER: DISPLAY_ORDER, IGNORE: IGNORE, STYLE: STYLE});

The options include:

  • data_url: the data file to load, in the following format. Note that the ID is unique to a CAS and cycle and is stored in the Master Delivery Schedule.
    1https://services-liaison.s3.amazonaws.com/data_csdcas_programs_ID.json
    2https://services-liaison.s3.amazonaws.com/data_atcas_programs_ID.json
  • SELECTOR: CSS selector for the table to use. If there are multiple tables on the page, you can target a specific one. Otherwise, just leave the default.
  • SORT_ORDER: an array of fields to sort by (such as "orgName" first, then "name," etc.).
  • DISPLAY_ORDER: an array of fields to display in the table columns, from left to right. Field names may be standard attributes ("state") or custom attributes ("programAttributes.attributeName"). In custom attributes, the programAttributes[].displayName should be used for filtering (corresponding to the value entered by the user in the “AttributeName” parameter - var DISPLAY_ORDER = ["name", "etsCode", "programAttributes.AttributeName"]; ). The value to be displayed in the row in the table would be programAttributes.attributeValues.attributeValue.
  • IGNORE: a function that determines which rows to ignore (e.g., ignore items with a status). The incoming item is an object based on the JSON entries.
  • STYLE: a function that determines the inline CSS style to apply to each cell. The incoming item is an object based on the JSON entries. The return value should be an object with an inline CSS style for each field name. For example: styles["fieldName"] = "background: #aa0000;". You may optionally set a value for styles.default: this inline CSS will be added to cells where an exact field match isn't already there.
Step C: Set Up Table

Be sure that the table stub is already created with header rows labeled. Columns should match the DISPLAY_ORDER configuration settings, both in name and order.

Sample table:

State

Institution

Program

Program Type

Program Deadline

Supplemental Application Required Supplemental Fee Required GRE Required

Sample table HTML:

01<table border="1" cellpadding="1" cellspacing="1" class="mt-responsive-table mt-table-big" id="program-table">
02    <thead>
03        <tr>
04            <td class="mt-noheading mt-column-width-10" style="background-color:rgb(30, 87, 160);vertical-align:middle;">
05            <p><span class="mt-font-size-12"><strong><span class="mt-color-ffffff">State</span></strong></span></p>
06            </td>
07            <td class="mt-noheading mt-column-width-10" style="background-color:rgb(30, 87, 160);vertical-align:middle;"><span class="mt-font-size-12"><font color="#ffffff"><b>Institution</b></font></span></td>
08            <td class="mt-noheading mt-column-width-15" style="background-color:rgb(30, 87, 160);vertical-align:middle;">
09            <p><span class="mt-font-size-12"><strong><span class="mt-color-ffffff">Program</span></strong></span></p>
10            </td>
11            <td class="mt-noheading mt-column-width-15" style="background-color:rgb(30, 87, 160);vertical-align:middle;">
12            <p><span class="mt-font-size-12"><strong><span class="mt-color-ffffff">Program Type</span></strong></span></p>
13            </td>
14            <td class="mt-noheading mt-column-width-10" style="background-color:rgb(30, 87, 160);vertical-align:middle;">
15            <p><span class="mt-font-size-12"><strong><span class="mt-color-ffffff">Program Deadline</span></strong></span></p>
16            </td>
17            <td class="mt-noheading mt-column-width-10" style="background-color:rgb(30, 87, 160);vertical-align:middle;"><span class="mt-font-size-12"><font color="#ffffff"><b>Supplemental Application Required</b></font></span></td>
18            <td class="mt-noheading mt-column-width-10" style="background-color:rgb(30, 87, 160);vertical-align:middle;"><span class="mt-font-size-12"><strong><span class="mt-color-ffffff">Supplemental Fee Required</span></strong></span></td>
19            <td class="mt-noheading mt-column-width-15" style="background-color:rgb(30, 87, 160);vertical-align:middle;"><span class="mt-font-size-12"><font color="#ffffff"><b>GRE Required</b></font></span></td>
20        </tr>
21    </thead>
22    <tbody>
23    </tbody>
24</table>
Additional Style Code

If needed, you can use the codes below to further customize program data.

Make text invisible via CSS styling if there's a value you don't want users to see (NAFCAS example)

01var STYLE = function(row) {
02var styles = {};
03 
04if (typeof row.campus == 'undefined') {
05styles["campus"] = "opacity: 0"; // hide the text - using opacity means users can't highlight the invisible text with the cursor
06}
07 
08styles.default = "";
09return styles;
10}

Apply snippets of default answer values when the answer values themselves aren't being picked up (CASPA example)

01var STYLE = function(row) {
02    var styles = {};
03     
04    if (row.track.includes("Blue:")) {
05        styles["deadlineDisplay"] = "background: #3498db; font-weight: bold;";    // blue
06    }
07    if (row.track.includes("Orange:")) {
08        styles["deadlineDisplay"] = "background: #f39c12; font-weight: bold;";    // orange
09    }
10    if (row.track.includes("Green:")) {
11        styles["deadlineDisplay"] = "background: #27ae60; font-weight: bold;";    // green
12    }
13     
14    styles.default = "";
15    return styles;
16}

Fixed table headers

To add the fixed table headers functionality, you must add a specific class name to the <table> tag. Note that the fixed table header doesn't work in mobile. Instead, the default behavior was selected for ease of reading.

  1. Edit the page and click HTML in the top right of the editor.
  2. Enter the following tag in the <table> script:
    • For a single-header table, add class="stick-one". If the table already has class attributes, then you can add stick-one to the existing class list. You must also edit the table properties and select First row for headings.
    • For a double-header table, add class="stick-two". If the table already has class attributes, then you can add stick-two to the existing class list.

Table script