Skip navigation
All Places > Developer > Blog > 2015 > July
2015
Do you write unit tests for your Sugar customizations?

 

Do you performance test your Sugar on-site deployments?

 

Every developer knows they should be building unit tests and performance tests but sometimes it is just too hard to get started from nothing.  Well now SugarCRM Engineering is going to make it easy for you!

 

The Sugar 7 unit test suite and performance test frameworks that we use to develop Sugar 7 are now available to current customers and partners via Github!

 

This is the first release in a new effort to provide enhanced standardized development tooling and automation to the Sugar Developer community.

 

At this time, we are only releasing these tools to existing Sugar Customers and Sugar Partners.  So these private Github repositories require that you are logged in using your Github account in order to access them.  You may also need to request access from SugarCRM if you cannot access these repositories currently.

In the coming weeks, we will be posting step-by-step tutorials for creating Sugar 7 unit tests and custom JMeter scenarios. So stay tuned!

 

More details below.

 

Sugar 7.x Unit Test repository

 

Our JavaScript unit tests are built using Jasmine.  Our PHP unit tests were built using PHPUnit.https://github.com/sugarcrm/unit-tests

 

Check out the README for getting tests installed and running in your development environment.  We also have a short NEW_TESTS guide that will help guide you in creating your own unit tests.

 

Sugar 7.x Performance Test repository

 

Additionally, we are making available the Apache JMeter based performance test framework for load testing your Sugar 7 on-site deployments.https://github.com/sugarcrm/performance

 

Check out the README for getting your development environment setup and for running the existing JMeter scenarios.  You can adapt our stock JMeter scenarios to create your own test scenarios.

 

 

Requesting Access

 

Some partner developers will find that they already have access to the above repositories.  However, if you get a 404 screen when you click the links above then you do not yet have access.If you are a current Sugar Customer or Sugar Partner, please request access here.

 

Again, you can only get access to these repositories at this time if you work for a current Sugar Customer or Sugar Partner.

 

Any questions/concerns/accolades can be e-mailed to developers@sugarcrm.com.

If you've used Sugar 7.5 or earlier then you have probably noticed that it take a few moments after a record appears for all the subpanels to finish being populated with data.

 



 

This was because each subpanel generated a separate round-trip HTTP transaction from the browser to Sugar web server in order to fetch the related records for each represented relationship.

 

However, in Sugar 7.6 this behavior has been improved due to the introduction of the Bulk API.

 

Sugar 7 Bulk API

 

The Bulk API was introduced in Sugar 7.5, however the Sugar 7 client code did not start fully taking advantage of it for subpanels until the Sugar 7.6 release.  It exists at the /rest/v10/bulk endpoint.

 

For convenience, I'm including some of the in-app documentation for the Bulk API here but you can find more detailed API docs by visiting the /rest/v10/help endpoint on a Sugar 7.6 instance.

 

POST /bulk

 

Summary

 

This request will run a sequence of API requests within one query. The requests are executed sequentially and their results are returned as one response. Some requests may return failure code, that does not interrupt the execution of the batch, and the overall request will still be considered successful.

 

Request Parameters

NameTypeDescriptionRequired
requestsArrayThe list of requestsTrue

 

Each of the requests can have the following fields:

NameTypeDescriptionRequired
urlStringThe request URL, starting with version.True
dataJSON StringThe data for the POST/PUT body. Must be a JSON-encoded string.False
headersArrayThe request headersFalse
methodStringThe HTTP method (default is GET)False

 

Response

 

The response will contain an array of response objects, each of them will correspond to the individual request. The following fields are in the response objects:

NameTypeDescription
contentsArray or StringThe response contents, can be JSON object or string depending on what the individual request is supposed to return.
headersArrayThe response headers
statusIntegerHTTP status code of the response. Will be 2XX for successful requests and 4XX or 5XX for errors.

 

A Short Example

 

Imagine building an integration where you need to keep track of Accounts and Contacts that exist within the state of North Carolina.  This can be done with the v10 REST API today but it requires a separate Filter API request for each the Accounts and Contacts modules.  With the Bulk API, it is possible to combine the two requests into a single HTTP transaction.

 

POST /v10/rest/bulk

Request Body
{

 

  "requests": [

 

    {

 

      "url": "/v10/Accounts/filter/count",

 

      "method": "POST",

 

      "data": "{\"filter\":[{\"billing_address_state\":\"NC\"}]}"

 

    },

 

    {

 

      "url": "/v10/Contacts/filter/count",

 

      "method": "POST",

 

      "data": "{\"filter\":[{\"primary_address_state\":\"NC\"}]}"

 

    }

 

  ]

 

}

 

An important thing that should be pointed out for Bulk API requests.  When using the data property, the contents needs to be a JSON encoded string.  This means that JSON request bodies appear to be double encoded when used as part of a Bulk API sequence which explains the escaped quote marks in the example above.

When passing JSON data via Bulk API, this JSON data will need to contained within a JSON encoded string.
Response
[

 

  {

 

    "contents": {

 

      "record_count": "1"

 

    },

 

    "headers": [],

 

    "status": 200,

 

    "status_text": "OK"

 

  },

 

  {

 

    "contents": {

 

      "record_count": "1"

 

    },

 

    "headers": [],

 

    "status": 200,

 

    "status_text": "OK"

 

  }

 

]

 

When parsing result array from a Bulk API request, the actual response bodies are stored under the contents properties.  Also keep in mind the order requests are listed since each request is executed in turn.  If you make an update in the first request, later requests in the sequence will be affected by that update.

The Bulk API executes requests in array order sequence but returns all responses at once.

 

Taking advantage of Bulk API to improve performance

 

Using the Bulk API will have immediate beneficial impact for any long sequence of API calls.  It eliminates a lot of network roundtrip overhead that can be a real bottleneck for higher latency connections.  This benefit should be evident by the improved responsiveness of subpanels in Sugar 7.6.  But really where it shines is in clients or interfaces that need to move CRM data around in real time.

The Bulk API is the ideal solution when you need to move a lot of data in real time.

 

It is also easy to adopt as a standard gateway for the Sugar API since it can be used to execute a single request at a time.  So even if you don't have a need for bulk transactions all the time, you can allow yourself flexibility to support them as needed.

Matt Marum

An Easter egg in Sugar 7.6

Posted by Matt Marum Employee Jul 13, 2015
In Sugar 7.6, we added an awesome little undocumented feature that we're calling Sweet Spot.

 

Try the matching shortcut key sequence below within a Sugar 7.6 window.

OSShortcut
Mac OS Xcmd+shift+space
Windowsctrl+shift+space
Linuxctrl+shift+space

 



 

Sweet Spot will appear.

 



 

Then start typing!

 



 

All you Sugar Developers and Sugar Administrators will like the Actions feature which allows you to quickly run many actions from anywhere in the application.  Administrators additionally get easy access to many administrative functions through Sweet Spot.

 

There are actually several more features that we haven't shown here.  But we will leave the rest for you guys to discover on your own!

 

Plans for this feature are still evolving but we think it will ultimately grow into an exciting addition to the Sugar user experience.  Try it out today in any Sugar 7.6 instance!

Post originally written by tshubbard.

 

In our previous "Hello World" dashlet post, we established what a minimal dashlet entailed.  In these next post, we'll be building on those skills to create a more useful dashlet that takes advantage of Sugar 7 List Views.  We will be creating a dashlet for Cases that binds to the list's Collection and sums the number of Cases by their status.  So if the Cases list contains 5 records, and 3 of those are in "New" state and 2 are in "Closed" state then we want our dashlet to display "New: 3" and "Closed: 2".  To the code!

 

File Structure

 

Again, using what we learned in the previous post, we're going to create a folder in custom/clients/base/views/ called "case-count-by-status". Inside that folder you should create 3 files:

  • case-count-by-status.php
  • case-count-by-status.js
  • case-count-by-status.hbs

 

You should have something that looks like the following screenshot:

 

While technically optional, we will also utilize the Language extension in order to provide multilingual support for our example dashlet.  This extension file will be located at custom/Extension/application/Ext/Language/en_us.case-count-by-status.php.

 

Dashlet Metadata (.php file)

 

Dashlet metadata is going to look almost identical to our previous "Hello World" dashlet. We're not doing anything too fancy here, so everything should look basically the same.

 

case-count-by-status.php

<?php

/**
* Metadata for the Case Count by Status example dashlet view
*
* This dashlet is only allowed to appear on the Case module's list view
* which is also known as the 'records' layout.
*/
$viewdefs['base']['view']['case-count-by-status'] = array(
'dashlets' => array(
array(
//Display label for this dashlet
'label' => 'LBL_CASE_COUNT_BY_STATUS',
//Description label for this Dashlet
'description' => 'LBL_CASE_COUNT_BY_STATUS_DESCRIPTION',
'config' => array(
            ),
'preview' => array(
            ),
//Filter array decides where this dashlet is allowed to appear
'filter' => array(
//Modules where this dashlet can appear
'module' => array(
'Cases',
                ),
//Views where this dashlet can appear
'view' => array(
'records',
                )
            )
        ),
    ),
);

 

Dashlet Metadata Filter Options

 

Currently there are two main dashlet filter keys that you'll see in the codebase; "module" and "view".  Across of these filter keys, the main thing to remember is that not specifying a filter at all means that your dashlet will be available in all views of all modules. You only need to add filters if you desire to restrict your dashlet to a specific module or view.  Let's look at the filter keys in more detail.

Specifying a filter means your dashlet will be restricted to specified modules and views.  Not specifying a filter means your dashlet will be available in all modules and views.

 

"module"

 

The module filter lets you add an array of modules where your dashlet can appear. If you wanted your dashlet to appear in the list of available dashlets for only the Accounts, Cases, and Contacts modules then your module filter would look like the following.

 'filter' => array(

 

     'module' => array(

 

         'Cases',

 

         'Accounts',

 

         'Contacts',

 

     ),

 

)

 

"view"

 

The view filter lets you add an array of views to limit on which views your dashlet can appear. If you wanted your dashlet to appear only on the Record view, your view filter would look like the following.

 'filter' => array(

 

     'view' => array(

 

         'record',

 

     ),

 

)

 

Currently, there are two possible values for the view filter. The List View is indicated by using "records".  The Record View is indicated by using "record".

 

 

Dashlet Controller (.js file)

 

Enough metadata nonsense, now for the fun stuff!  Here is the JavaScript controller for a Case Count By Status dashlet.

 

case-count-by-status.js

/**
* Case Count by Status example dashlet controller
*
* Controller logic watches the current collection on display and updates the
* dashlet automatically whenever the current collection changes.
*
* This is a simple example of a dashlet for List Views in Sugar 7.x.
*
**/
({
//This view uses the essential Dashlet plug-in
    plugins: ['Dashlet'],

/**
     * Values is used by the template to display the statuses and counts.  Backs our Handlebars template.
*/
    values: undefined,

/**
     * Keeps track of how many cases in total we're displaying.  Also used in our Handlebars template.
*/
    totalCases: undefined,

/**
     * Keeps a map of status types by model ID as the key.
*/
    modelsMap: undefined,

/**
     * @inheritdoc
*/
initialize: function(options) {
// call the parent's (View's) initialize function
// passing options as an array
this._super('initialize', [options]);

// initialize vars
this.modelsMap = {};
this.totalCases = 0;
this.values = {};
    },

/**
     * @inheritdoc
*/
bindDataChange: function() {
var ctx = this.context,
            collection = ctx.get("collection");
if(_.isEmpty(collection)){  //Collection will be empty in "preview" mode
return;
        }

//Listening to 'reset' events on the collection
collection.on('reset', function(collection) {
// Ensure that collection exists, has models, then parse out models for display
if(collection && collection.length) {
this._parseModels(collection.models, false);
            }
        }, this);

//Listening to 'add' and 'remove' events on the collection
collection.on('add remove', function(model, collection, options)  {
// The Backbone's options argument for 'add' and 'remove' events are different
// if options.removed doesn't exist, then we will know this is a 'remove' event
if (_.isUndefined(options.remove)) {
options.remove = true;
            }

// Backbone passes add/remove options as an event param, so we can tell
// if this was the add or remove event and pass it to parseModels
this._parseModels([model], options.remove);
        }, this);

if(collection.models && _.isEmpty(this.modelsMap)) {
// manually cause a parsing of the models
// this covers the scenario when a user creates a new record
this._parseModels(collection.models, true);
        }
    },

/**
     * Recalculates values used in the template from modelsMap
     *
     * @private
*/
_recalcValues: function() {
// reset values
this.values = {};
this.totalCases = 0;

_.each(_.values(this.modelsMap), function(status) {
this.totalCases++;
// check to see if we've already set a value
// for this status
if (this.values[status]) {
// status is already set so just increment
this.values[status].count++;
            } else {
// add a new entry on the values object
// with status as the key and an Object
// with name and count for our template
this.values[status] = {
                    name: status,
                    count: 1
                };
            }
        }, this);
    },

/**
     * Takes an array of models and parses them into modelsMap then (re)counts the values in this map
     *
     * @param {Array} models The Array of models to parse
     * @param {Boolean} remove If the models passed in should be removed or not
     * @private
*/
_parseModels: function(models, remove) {
var id,
            status;

_.each(models, function(model) {
// get the case id & status
            id = model.get('id');
            status = model.get('status');

if (remove && this.modelsMap[id]) {
// if the function was called
// to remove models, delete the id/value
delete this.modelsMap[id];
            } else {
// otherwise, add the id and status
// to modelsMap
this.modelsMap[id] = status;
            }
        }, this);

// now that we've updated the modelsMap,
// recalculate the this.values object for rendering
this._recalcValues();

// double-check that the view has not been disposed
// if not, then re-render the dashlet
if (!this.disposed) {
this.render();
        }
    }
})

On a List View, this.context points to the BeanCollection.  this.context.parent points to a parent model (when it exists, such as on a dashlet preview).

 

Dashlet Template (.hbs file)

 

Again, with a Dashlet you are free to format the display any way you like.  However, we do recommend leveraging Sugar's Styleguide so that your dashlet appears like a seamless extension of the Sugar user interface.  It is a great reference for you to leverage that allows your dashlets to appear as just another seamless part of the Sugar 7 application.

 

In this example, we leveraged some dashlet design patterns and CSS pulled directly from the Sugar 7 Styleguide.

 



The Sugar Styleguide describes how Sugar 7's CSS works and the design patterns used in common components such as Dashlets.  The Styleguide is how you build seamless UI for Sugar 7.x.

 

As a Sugar Admin, navigate to Styleguide > Core Elements > Dashboards > Dashlets to view the Dashlets style guide.  Specifically, we borrowed CSS from the "Summary" dashlet example listed in the Styleguide.

 



 

Here is our Handlebars template we put together leveraging this "Summary" pattern.

 

case-count-by-status.hbs

{{!
Case Count by Status example dashlet Handlebars template

We are reusing styling from the Sugar 7 Styleguide for our example dashlet.

Here we are borrowing CSS used in our Forecast Details dashlet which is suitable for display any set of name value pairs.
}}
<div class="forecast-details">
{{#each values}}
        <div class="row-fluid">
            <span class="span6">
{{name}}
            </span>
            <span class="span6 tright">
{{count}}
            </span>
        </div>
{{/each}}
    <div class="row-fluid">
        <strong class="span6">
{{! 'str' is the Sugar 7 Handlebars helper that translates a label into a localized language string}}
{{str "LBL_CASE_COUNT_BY_STATUS_TOTAL"}}
        </strong>
        <strong class="span6 tright">
{{totalCases}}
        </strong>
    </div>
</div>

 

Language Extension (.php file)

 

Finally, in the above Handlebars template and in the dashlet metadata file we have defined some display labels that need to be translated into human readable strings.  To accomplish this, we have added a language extension for these new labels that we have introduced.

It is a development best practice to use labels for strings so that your user interface can be translated and supported in multiple languages

 

en_us.case-count-by-status.php

<?php
// This file will provide English strings for our new labels.
// Additional extensions could be created so that our dashlet supports other languages.
$app_strings['LBL_CASE_COUNT_BY_STATUS'] = 'Case Count By Status';
$app_strings['LBL_CASE_COUNT_BY_STATUS_DESCRIPTION'] = 'Shows the number of Cases on the Cases List view by status.';
$app_strings['LBL_CASE_COUNT_BY_STATUS_TOTAL'] = 'Total Cases:';

 

The "en_us" at the beginning of this filename is significant.  It represents the user locale where these strings are used.  If you wanted to created translations for other locales, then this value would be different.  For example, French language extensions must start with "fr_FR".

 

Wrapping Up

 

So now we've got a working example dashlet that takes advantage of a module's List View.  Remember to run a Quick Repair and Rebuild and then navigate to your Cases module and add the new dashlet.  It should look something like what you see below.