How to build an External API Dashlet

On October 28, 2019, we conducted a webinar on the topic of How to deploy code to SugarCloud using Module Loader. In that presentation, we demonstrated a Module Loadable Package that would do 3 things:

  1. Modify the UI via custom.less
  2. Add a post-install script
  3. Add global javascript

I'd like to take some time to look a bit deeper into making and using Module Loadable Packages in Sugar.

Use Case

So, let's start with a new example. This package will add a custom dashlet to the record view of Accounts and Leads modules. The dashlet will pull data from an external API based on a field value from the current module. That's it.

Could it do more? Sure. It is my opinion, however, that each package should be singular in action. In other words, each package that *I* create will do one thing. If I also want to add a new script library that changes all telephone numbers into clickable links, for example, I would create a separate package with those files and instructions - even though it might be faster for me at this moment to just throw that in with the package I am working on.

Creating and using small, singularly-focused packages allows me to keep things simple. So, if something fails, I have a reasonable amount of code and files to look through that are all working toward the same end. Sure, arguments can be made for creating one large package that can be installed/uninstalled once. In fact, that’s the way we approached all of Professor M. It is a single package that loads absolutely every customization that we wanted for the project. I am of the opinion that smaller is better (think microservices). There is a plan in the works to revamp Professor M a bit. At that time, we will likely split things up into more manageable pieces.

Planning

OK, back to this project. The first thing I want to do is determine the scope and functionality of this dashlet. I know it should display on the Accounts and Leads modules. I know that it should only show for the record view. Let's set it for the most current major version of Sugar (as of now that is 9.x). But what will the dashlet actually do?

Since my Sugar instance is using Professor M, I think it makes sense to display to the user a list of statistics of OTHER schools in the same area as the record which they are viewing. That way, a user can show a potential student what other schools around them are charging for tuition (or what the job placement rate is, etc). For this, I needed to find a data source. I created a free account with api.data.gov so that I could get an API key to call the REST endpoint at api.data.gov/ed/collegescorecard/v1/schools. The last thing I want to do is ensure that what I put into the dashlet is pleasing to the eye. For that, I will add some custom CSS.

The Package

Now that I have all of the high-level details ironed out, it is time to start creating the module loadable package. To start, let's create the manifest. From the details in the last paragraph, my manifest variable will look like this:

$manifest = array(
        'acceptable_sugar_flavors' => array('PRO','ENT','ULT'),
        'acceptable_sugar_versions' => array(
            'regex_matches' => array('9.*.*'),
        ),
        'author' => 'Michael Shaheen',
        'description' => 'Adds College Stats dashlet that pulls data from api.data.gov',
        'is_uninstallable' => true,
        'name' => 'College Stats dashlet',
        'published_date' => date("Y-m-d H:i:s"),
        'type' => 'module',
        'version' => '1.0',
    );

The next thing I like to do is create the file structure of the package that closely mirrors where the files will live in my Sugar instance. For this dashlet, I will place files at /custom/clients/base/views/college-stats (my new directory for this dashlet). Inside of that directory, we need 3 files:

  • college-stats.js: the javascript controller 
  • college-stats.php: a metadata php file that essentially defines the dashlet and makes it available for use in my Sugar instance 
  • college-stats.hbs: a handlebars template to define the layout and display the data

The Code - "Hello World"

What specifically goes into those files? Let's start with the metadata file. Within /custom/clients/base/views/college-stats/college-stats.php we will add our dashlet definition like so:

$viewdefs['base']['view']['college-stats'] = array(
     'dashlets' => array(
          array(
               //Display label for this dashlet
               'label' => 'College Stats',

               //Description label for this Dashlet
               'description' => 'Lists tuition statistics for colleges in the same state as the current record. Will show all states if none is specified',

               'config' => array(),
               'preview' => array(),

               //Filter array decides where this dashlet is allowed to appear
               'filter' => array(
                    //Modules where this dashlet can appear
                    'module' => array(
                         'Contacts',
                         'Accounts',
                         'Leads',
                    ),
                    
                    //Views where this dashlet can appear
                    'view' => array(
                         'record',
                    )
               )
          ),
     ),
);

This assignment statement is saying that we would like to add a dashlet that can be displayed in the Contacts, Accounts, or Leads modules and only for the record view of those modules.

Now we can start populating the javascript controller. To /custom/clients/base/views/college-stats/college-stats.js, we will add the following:

({
     plugins: ['Dashlet'],

     _retrieveData: function() {
          this.schools = "hello";
     },
     
     initialize: function(options) {
          this.schools = [];
          // call the parent's (View's) initialize function
          // passing options as an array
          this._super('initialize', [options]);
          this._retrieveData();
     },
})

In this very basic code, we are simply declaring that we are using the dashlet plugin, initializing a variable that will hold our data called schools, calling the View's initialize function, and calling a function that we will use to grab our data from the external source. For now, that function _retrieveData is setting the value of schools = "hello". This is a functioning controller for our dashlet. All that's left is to write the handlebars template to display the data that is being retrieved. So, inside of /custom/clients/base/views/college-stats/college-stats.hbs, we will add HTML and a placeholder for our data like so:

<div class="control-group dashlet-options">
    <div class="controls controls-two btn-group-fit">
        <div class="row-fluid">
            <div class="">
                <p>This is the internal header of our dashlet</p>
            </div>
        </div>       
    </div>
</div>
<div class="ext_schools">
{{schools}}
</div>

I put in some HTML that I copied from other dashlets so that the look would be somewhat consistent. The important piece in this template is the {{schools}} line. This is going to look at the data in the controller and display whatever is contained in the variable called schools.

These are the files that will make our dashlet accessible in the Contacts, Accounts, and Leads modules' record views. Once installed and added to a dashboard, we will see a dashlet with a header and the word "Hello". Not very useful in the long run but it is a first step. Unfortunately, this isn't enough to get our dashlet into our Sugar instance.

I approached this project from an entirely cloud-based position. I had no local environment for testing - just a cloud sandbox instance. This is NOT a practice that I recommend. At all. Ugh it took a very long time to work on because to test each change I made, I had to go into Admin, then to module loader where I would uninstall the old package, upload and install the new package, then navigate to the Accounts module, go into an account record and add the dashboard. Just explaining the process is tedious and I left out all of the clicks and the waiting for the processes to complete! So, if you can, do your development work in a local build (see the Developer Builds space in the Dev Club Community) where you can work directly in the files that you are manipulating. Then, when happy, create the package and use Module Loader to install it into your cloud instance. Since I did not do that, this project took a lot longer than I had hoped.

Nevertheless, I persisted. In order for me to get my dashlet into my cloud instance, I had to update the manifest to describe what to do with each of my files. It should look something like this:

    $installdefs = array(
        'id' => 'college-stats-dashlet',
        'copy' => array(
            0 => array(
                'from' => '<basepath>/Files/custom/clients/base/views/college-stats/college-stats.js',
                'to' => 'custom/clients/base/views/college-stats/college-stats.js',
            ),
            1 => array(
                'from' => '<basepath>/Files/custom/clients/base/views/college-stats/college-stats.hbs',
                'to' => 'custom/clients/base/views/college-stats/college-stats.hbs',
            ),
            2 => array(
                'from' => '<basepath>/Files/custom/clients/base/views/college-stats/college-stats.php',
                'to' => 'custom/clients/base/views/college-stats/college-stats.php',
            ),
        ),
    );

NOW all that's left to do is zip up the package and upload/install it into my cloud instance. Remember, your archived zip cannot contain any extraneous files. So, on a mac, be sure to zip it with the CLI command:

zip -r --filesync ../college-stats.zip * -x "*.DS_Store" -x "*.git*" -x "__MAC*"

This will zip up the current directory and all of its children excluding any files that match *.DS_Store, *.git, or __MAC*. It will place that archive in the directory above the current one and call it "college-stats.zip".

After uploading and installing the package, I can see it is now accessible as a dashlet on the Leads module:

I can add it to my dashboard and I can see my "hello" statement in the dashlet.

Hello world dashlet example

The Code - With Data

The next step is to use actual data. I used Postman to do some test calls to the REST API. By doing so I was able to fiddle around until I found the proper query for my purposes. And, for now, I am going to copy/paste some results from that call into my controller so that I can have some data to work with. To do this, I will change the _retrieveData function to return my copied data. Like so:

_retrieveData: function() {
    this.schools = [
        {
            "latest.cost.tuition.out_of_state": 16836,
            "school.school_url": "www.dli.pa.gov",
            "latest.cost.tuition.in_state": 16836,
            "school.name": "Commonwealth Technical Institute",
            "school.state": "PA",
            "id": 212975,
            "latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings": 14600,
            "school.city": "Johnstown",
            "latest.aid.median_debt.completers.overall": null,
            "latest.repayment.1_yr_repayment.completers": null
        },
        {
            "school.name": "Susquehanna County Career and Technology Center",
            "school.state": "PA",
            "id": 441672,
            "school.school_url": "www.scctc-school.org",
            "school.city": "Springville",
            "latest.aid.median_debt.completers.overall": null,
            "latest.repayment.1_yr_repayment.completers": null,
            "latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings": null,
            "latest.cost.tuition.out_of_state": null,
            "latest.cost.tuition.in_state": null
        },
        {
            "latest.repayment.1_yr_repayment.completers": 30,
            "latest.cost.tuition.out_of_state": 21126,
            "school.school_url": "www.brynathyn.edu",
            "latest.cost.tuition.in_state": 21126,
            "school.name": "Bryn Athyn College of the New Church",
            "school.state": "PA",
            "id": 210492,
            "school.city": "Bryn Athyn",
            "latest.aid.median_debt.completers.overall": 26571.5,
            "latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings": null
        },
        {
            "latest.repayment.1_yr_repayment.completers": 55,
            "latest.cost.tuition.out_of_state": 15662,
            "school.school_url": "www.goPMI.org",
            "latest.cost.tuition.in_state": 15662,
            "school.name": "Precision Manufacturing Institute",
            "school.state": "PA",
            "id": 446455,
            "school.city": "Meadville",
            "latest.aid.median_debt.completers.overall": null,
            "latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings": null
        },
        {
            "latest.repayment.1_yr_repayment.completers": 528,
            "school.school_url": "www.empire.edu",
            "school.name": "Empire Beauty School-North Hills",
            "school.state": "PA",
            "id": 450605,
            "latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings": 19900,
            "school.city": "Pittsburgh",
            "latest.aid.median_debt.completers.overall": 10666.5,
            "latest.cost.tuition.out_of_state": null,
            "latest.cost.tuition.in_state": null
        },
        {
            "latest.repayment.1_yr_repayment.completers": 34,
            "school.school_url": "www.cde.edu",
            "school.name": "CDE Career Institute",
            "school.state": "PA",
            "id": 451495,
            "school.city": "Tannersville",
            "latest.aid.median_debt.completers.overall": 6480.0,
            "latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings": null,
            "latest.cost.tuition.out_of_state": null,
            "latest.cost.tuition.in_state": null
        },
        {
            "latest.repayment.1_yr_repayment.completers": 2593,
            "latest.cost.tuition.out_of_state": 11005,
            "school.school_url": "www.mccann.edu",
            "latest.cost.tuition.in_state": 11005,
            "school.name": "McCann School of Business & Technology",
            "school.state": "PA",
            "id": 438212,
            "latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings": 27100,
            "school.city": "Pottsville",
            "latest.aid.median_debt.completers.overall": 24549.5
        },
        {
            "latest.repayment.1_yr_repayment.completers": 125,
            "school.school_url": "www.lcctc.edu",
            "school.name": "Lebanon County Area Vocational Technical School",
            "school.state": "PA",
            "id": 418542,
            "latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings": 38400,
            "school.city": "Lebanon",
            "latest.aid.median_debt.completers.overall": 15485.0,
            "latest.cost.tuition.out_of_state": null,
            "latest.cost.tuition.in_state": null
        },
        {
            "school.name": "Geisinger Commonwealth School of Medicine",
            "school.state": "PA",
            "id": 456542,
            "school.school_url": "https://www.geisinger.edu/education",
            "school.city": "Scranton",
            "latest.aid.median_debt.completers.overall": null,
            "latest.repayment.1_yr_repayment.completers": null,
            "latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings": null,
            "latest.cost.tuition.out_of_state": null,
            "latest.cost.tuition.in_state": null
        },
        {
            "latest.repayment.1_yr_repayment.completers": 4120,
            "school.school_url": "www.strayer.edu/pennsylvania/warrendale",
            "latest.cost.tuition.in_state": 13857,
            "school.name": "Strayer University-Warrendale Campus",
            "school.state": "PA",
            "id": 44378405,
            "latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings": 53500,
            "school.city": "Warrendale",
            "latest.aid.median_debt.completers.overall": 34239.5,
            "latest.cost.tuition.out_of_state": null
        }
    ];
}

Now that I know the data structure, I can write the Handlebars template to display this data properly. Take a look at the data keys in that sample set above. Each field uses dot notation. So, if in the Handlebars template I try to place one of those fields into the HTML by simply using {{ school.school_url }}, nothing will display. There is no field called schools[0].school.school_url. There is, however, a field called schools[0]["school.school_url"]. To represent that in Handlebars, we simply add the square brackets inside of the double-curly brackets. Like this: {{ [ school.school_url ] }}. With that in mind, I set up my template like this:

<div class="control-group dashlet-options">
    <div class="controls controls-two btn-group-fit">
        <div class="row-fluid">
            <div class="">
                <p>This is the internal header of our dashlet</p>
            </div>
        </div>       
    </div>
</div>
<div class="ext_schools">
{{#each schools}}
<div class="row-fluid ext_school">

{{#if [id]}}
<div class="dta_block ext_school_id"><span class="dta_lbl">ID:</span><span class="dta_val">{{[id]}}</span></div>
{{/if}}

{{#if [school.name]}}
<div class="dta_block ext_school_name"><span class="dta_lbl">Name:</span><span class="dta_val">{{[school.name]}}</span></div>
{{/if}}

{{#if [school.city]}}
<div class="dta_block ext_school_city"><span class="dta_lbl">City:</span><span class="dta_val">{{[school.city]}}</span></div>
{{/if}}

{{#if [school.state]}}
<div class="dta_block ext_school_state"><span class="dta_lbl">State:</span><span class="dta_val">{{[school.state]}}</span></div>
{{/if}}

{{#if [school.school_url]}}
<div class="dta_block ext_school_url"><span class="dta_lbl">Website:</span><span class="dta_val"><a href="{{[school.school_url]}}" target="_blank">{{[school.school_url]}}</a></span></div>
{{/if}}

{{#if [latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings]}}
<div class="dta_block ext_school_earnings10"><span class="dta_lbl">Mean Earnings 10yr After Entry:</span><span class="dta_val">{{[latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings]}}</span></div>
{{/if}}

{{#if [latest.repayment.1_yr_repayment.completers]}}
<div class="dta_block ext_school_repay1yr"><span class="dta_lbl">Loan Repayment After 1yr:</span><span class="dta_val">{{[latest.repayment.1_yr_repayment.completers]}}</span></div>
{{/if}}

{{#if [latest.cost.tuition.in_state]}}
<div class="dta_block ext_school_tuition_instate"><span class="dta_lbl">In-state Tuition:</span><span class="dta_val">{{[latest.cost.tuition.in_state]}}</span></div>
{{/if}}

{{#if [latest.cost.tuition.out_of_state]}}
<div class="dta_block ext_school_tuition_outstate"><span class="dta_lbl">Out-of-state Tuition:</span><span class="dta_val">{{[latest.cost.tuition.out_of_state]}}</span></div>
{{/if}}

</div>
{{/each}}
</div>

There's some simple HTML with classes on each element. The Handlebars-specific code consists of a loop for the schools variable {{#each schools}} and a bunch of conditionals checking to see if the variable exists before displaying it {{#if [school.school_url]}}.

With that, we should check on how these changes affect our dashlet. So, I will uninstall the current package and upload/install the new package. Then we can view the changes.

The Code - Styling

I think I'd like my next step to be styling. It will make it easier to see my changes going forward if the data isn't all jumbled in the dashlet. In previous examples, we have added a custom.less file with our styling changes for the site's UI. This is a very viable method. But, what if another package also adds a custom.less file? That file (if installed after ours) will replace the one from this package. That's not ideal. In our building blocks git repo, there is a plugin called CssLoader. It will take CSS files and add them to the loaded CSS for the site. There is no overwriting involved. The tradeoff is that we must use straight CSS instead of LessJS. If you already have Less written I like converters like this one https://www.webtoolkitonline.com/less-to-css.html. The CssLoader plugin happens to now be already installed in the core application. So, all we need to do is reference it in our Javascript controller by updating our plugins array to include CssLoader and adding the path to our CSS file(s):

    plugins: ['Dashlet','CssLoader'],
    css: ["/custom/include/css/college-stats.css"],

Now, the CSS file does not exist yet. I will create that file within my package at /Files/custom/include/css/college-stats.css and then add an entry to the copy array of my manifest that defines where to put the CSS file in my Sugar instance. That value will be the same as the path I put into the controller /custom/include/css/college-stats.css.

Finally, here is the content of our CSS file:

.ext_school { padding: 10px; width: 90% !important; margin: 16px auto; border-top: solid 1px #e8e8e8; border-bottom: solid 1px #cacaca; background: #f9f9f9; font-size: 14px; }
.ext_school .dta_block .dta_lbl { display:inline-block; font-weight: 700; color: #003865; margin-right: 4px; }
.ext_school .dta_block .dta_val { display: inline-block; }
.ext_school a { color: #ff8200; text-decoration: none; }
.ext_school a:hover { color: #009cde; text-decoration: underline; }

When we look at our new changes in the application, we will now see a styled dashlet:

Pretty sweet!

The Code - External Data Request

Let's keep going by pulling in live data from the external REST API. Because of cross-site-reference rules, we cannot call the external API directly from our Javascript controller. The approach we'll take, then, will be to make our own custom API endpoint in our instance of Sugar to act as a proxy to the api.data.gov endpoint.

This means adding a file for our endpoint at /Files/custom/clients/base/api/ExternalCollegeStatsAPI.php. Remember to add a copy directive to the manifest for this new file. For the content of the file, we will need to register the new endpoint and add the functions to return the college stats data.

The first thing we will add to our file is the endpoint registration code:

<?php

class ExternalCollegeStatsAPI extends SugarApi
{
public function registerApiRest()
    {
        return array(
            //GET & POST
            'ExternalCollegeStats' => array(
                //request type
                'reqType' => array('GET'),

                //set authentication
                'noLoginRequired' => false,

                //endpoint path will equal External/CollegeStats/{variable}
                'path' => array('External', 'CollegeStats', '?'),

                //endpoint variables. to access the last value in the url, we will use "state"
                'pathVars' => array('', '', 'state'),

                //method to call
                'method' => 'CallExternal',
            ),
        );
    }

}

What does all of that code do? We start by creating a new class for the endpoint that extends the SugarApi class. Then in the registerApiRest function, we must define a few values. We want this to be just a GET request so we can set 'reqType' => array('GET'). I'd like to be able to call the endpoint by going to <base_url>/rest/v11_6/External/CollegeStats/PA where "PA" is the state for which I'd like to see results. To define that path, we need to set 'path' => array('External', 'CollegeStats', '?'). With the path defined, we need a way of getting to the state variable at the end of the URL path. If we set 'pathVars' => array('', '', 'state'), we can access the value through a variable that we are calling state. Lastly, in our registration function, we need to designate what method will be called to return the data. So we set 'method' => 'CallExternal', and then add a function to this file called CallExternal($api, $args).

Within that new CallExternal function, we can add the code and logic to return the data. When I first built this package, I did an intermediary step before calling the external API. I moved the dummy data from the Javascript controller into the new endpoint. I simply told the endpoint to return that array. This was really to test that my API endpoint was registered properly and that I could get data from it. For this article, however, I'm jumping right into the actual call.

Firstly, I added the API key and the URL of the external endpoint:

    $api_key = "XXXXXXXyour-real-key-goes-hereXXXXXXX";
    $base_url = "https://api.data.gov/ed/collegescorecard/v1/schools";

remember to update the $api_key value in your own package to use the api key from registering with api.data.gov

Now let's add an array of the fields to send with the call:

$data = array(
'api_key' => $api_key,
'per_page' => 50,
'fields' => 'school.name,school.school_url,school.city,school.state,id,latest.aid.median_debt.completers.overall,latest.repayment.1_yr_repayment.completers,latest.earnings.10_yrs_after_entry.working_not_enrolled.mean_earnings,latest.cost.tuition.out_of_state,latest.cost.tuition.in_state'
);

You can see from here that I am sending my API key, how many items I wish to see per page, and the fields from the full dataset that I would like to retrieve. Note for this endpoint, the field's value must be a comma-delimited string with no whitespace.

I'd like to have the data retrieved reflect the current Sugar record. So, I'll try to grab a "state" field from the current record and send it to this call. We'll get to the front-end logic around this in a minute. For now, we just want to ensure that our custom endpoint can handle that parameter.

if ( isset($args['state']) && !empty($args['state']) && $args['state'] != "all" ) {
$data['school.state'] = $args['state'];
}

I added a condition for state != "all" because if all states are requested, the endpoint just needs to omit that value.

I feel like we have our setup complete. Now we need to make the actual external call. Some examples in the Developer Community and the Documentation use file_get_contents to make the call. Unfortunately, this is a blacklisted function for Module Loadable Packages in the cloud. Package Scanner will reject installing the package when it finds this function used. So, I used cURL. The code below should be familiar to anyone who has used cURL before.

$ch = curl_init();
$query = http_build_query($data);
$url = $base_url . '?' . $query;
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$result = curl_exec($ch);

if ( $result !== false ) {
        $response->success = true;
        $response->body = $result;
} else {
        $response->success = false;
        $response->body = curl_error($ch);
        $response->error = curl_errno($ch);
}
curl_close($ch);
return $response;

That's all the code that we need for our custom endpoint that will proxy our call to the external API. Now we have to write the Javascript code in our controller to consume this endpoint. Let's replace the dummy data in the _retrieveData method with the following:

var cntrlr = this;

App.api.call('GET', App.api.buildURL('External/CollegeStats/' + "PA"), null, {
     success: function (data) {
          if (data.success == true) {
               var results = JSON.parse(data.body).results;
               if (results.length > 0) {
                    cntrlr.schools = results;

                    // the data needs to be added and rendered
                    _.extend(cntrlr, cntrlr.schools);
                    cntrlr.render();
               }
          }
     },

     error: function (e) {
          throw e;
     }
});

This is a pretty basic Sugar API call. I specified the URL path and added "PA" as the state for now. In the success callback, I parse the data and grab the node from it that I need - results - and assign it to our schools variable. Remember scope. I had to set a variable called cntrlr = this so that I could still reference the current controller from within the API callback. One other thing to note here is that since we are inside of the callback and render has (likely) already happened, we will need to re-call the controller's render function.

Install this new version of the dashlet package and we should see a similar outcome as before. Our data is all from the state of Pennsylvania for now because we hard-coded that in. How do we make this dashlet grab and use the state associated with the current module record that we are viewing? With just a bit more Javascript:

var currentState;

var currentModule = this.module;

// ensure we are using the correct STATE field for the current module (e.g. Accounts doesn't have a primary_address_state field)

switch(currentModule) {

     case "Accounts":
          currentState = this.model.get('billing_address_state');
          break;
     default:
          currentState = this.model.get('primary_address_state');
}

var stateAbbrev = cntrlr._getStateAbbreviation(currentState);

First, we grab the current module with this.module. Remember, each module in Sugar has different fields. I know that if we are viewing this dashlet alongside an Account record, we should use the billing_address_state field and our other modules both use primary_address_state. So, my switch statement reflects that. Unfortunately, the data from the module comes across as a full state name (like "Pennsylvania") and the external endpoint requires a state abbreviation. So, I made a quick helper function that takes a state name and returns the proper abbreviation:

_getStateAbbreviation: function(stateName) {
     var states = [{"name": "Alabama","abbreviation": "AL"},{"name": "Alaska","abbreviation": "AK"},{"name": "American Samoa","abbreviation": "AS"},{"name": "Arizona","abbreviation": "AZ"},{"name": "Arkansas","abbreviation": "AR"},{"name": "California","abbreviation": "CA"},{"name": "Colorado","abbreviation": "CO"},{"name": "Connecticut","abbreviation": "CT"},{"name": "Delaware","abbreviation": "DE"},{"name": "District Of Columbia","abbreviation": "DC"},{"name": "Federated States Of Micronesia","abbreviation": "FM"},{"name": "Florida","abbreviation": "FL"},{"name": "Georgia","abbreviation": "GA"},{"name": "Guam","abbreviation": "GU"},{"name": "Hawaii","abbreviation": "HI"},{"name": "Idaho","abbreviation": "ID"},{"name": "Illinois","abbreviation": "IL"},{"name": "Indiana","abbreviation": "IN"},{"name": "Iowa","abbreviation": "IA"},{"name": "Kansas","abbreviation": "KS"},{"name": "Kentucky","abbreviation": "KY"},{"name": "Louisiana","abbreviation": "LA"},{"name": "Maine","abbreviation": "ME"},{"name": "Marshall Islands","abbreviation": "MH"},{"name": "Maryland","abbreviation": "MD"},{"name": "Massachusetts","abbreviation": "MA"},{"name": "Michigan","abbreviation": "MI"},{"name": "Minnesota","abbreviation": "MN"},{"name": "Mississippi","abbreviation": "MS"},{"name": "Missouri","abbreviation": "MO"},{"name": "Montana","abbreviation": "MT"},{"name": "Nebraska","abbreviation": "NE"},{"name": "Nevada","abbreviation": "NV"},{"name": "New Hampshire","abbreviation": "NH"},{"name": "New Jersey","abbreviation": "NJ"},{"name": "New Mexico","abbreviation": "NM"},{"name": "New York","abbreviation": "NY"},{"name": "North Carolina","abbreviation": "NC"},{"name": "North Dakota","abbreviation": "ND"},{"name": "Northern Mariana Islands","abbreviation": "MP"},{"name": "Ohio","abbreviation": "OH"},{"name": "Oklahoma","abbreviation": "OK"},{"name": "Oregon","abbreviation": "OR"},{"name": "Palau","abbreviation": "PW"},{"name": "Pennsylvania","abbreviation": "PA"},{"name": "Puerto Rico","abbreviation": "PR"},{"name": "Rhode Island","abbreviation": "RI"},{"name": "South Carolina","abbreviation": "SC"},{"name": "South Dakota","abbreviation": "SD"},{"name": "Tennessee","abbreviation": "TN"},{"name": "Texas","abbreviation": "TX"},{"name": "Utah","abbreviation": "UT"},{"name": "Vermont","abbreviation": "VT"},{"name": "Virgin Islands","abbreviation": "VI"},{"name": "Virginia","abbreviation": "VA"},{"name": "Washington","abbreviation": "WA"},{"name": "West Virginia","abbreviation": "WV"},{"name": "Wisconsin","abbreviation": "WI"},{"name": "Wyoming","abbreviation": "WY"}];
     if (!_.isUndefined(stateName) && stateName.length != 2) {
          var obj = _.find(states, function (obj) { return obj.name.toLowerCase() === stateName.toLowerCase(); });
          if (!_.isUndefined(obj) && !_.isUndefined(obj.abbreviation)) {
               return obj.abbreviation;
          }
     }
     return "all";
},

If the function cannot make a match for the stateName parameter, it will return the text "all". You hopefully recall that in our endpoint definition, I added a condition for "all". This was necessary because our path requires a last value as a parameter.

In the final controller, you will see a few other helper functions that I added just to format the data to be more readable. I also added a help file for the API endpoint and language definitions.

I hope this is helpful to some of you out there who are adding dashlets or modules that need to bring related data in from an external source. If you see room for improvement, please take the attached package and run with your changes. Then, share them with us here. Or, as always, you can reach us at developers@sugarcrm.com.

19176_college-stats.zip