Skip navigation
All Places > Developer > Blog
1 2 3 Previous Next

Developer

206 posts

Hello Sugar Developers!

 

It's that time again - we have completed a new quarterly release. Sugar 9.3 (Winter '20) is a cloud release but the updates in this version will be available to on-premise customers in the upcoming Sugar 9.0 release this Spring.

 

In December, we conducted a webinar highlighting the changes of Sugar 9.3. If you missed it, you can watch the recording or browse through the slides.

 

Here's a quick list of some of the features in this Winter '20 release:

  • A new REST endpoint has been added to return related activity records for a specified record
  • New fields were added to the Cases module
  • No-touch renewal pipeline automation has been added to SugarSell
  • There's also a new Renewals console
  • New Active Subscriptions Dashlet
  • We've added First Response SLA Reporting
  • 2 new SugarBean functions for business center calculations

 

Check out the below resources that have the rest of the details.

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 Developer 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.

Back in September of this year (2019), we held the Developer's Webinar for the 9.2 release of Sugar. During that presentation, I touched on how the Portal can be customized. Among the things illustrated in the webinar was adding a module to the Portal. This is apparently something that you all wanted to know more about. So, let's use this space to focus on just that.

 

There isn't really much to it - just a few things to remember.

 

First, you need to create a Module Loadable Package. The package can do pretty much anything you'd like it to do. In the example from the webinar, we had the module display announcements.

 

Once you have the package ready, add the following vardef to make the new module visible in the portal:

 

$dictionary['sugar_NAME_OF_YOUR_MODULE']['portal_visibility'] = [
    'class' => 'Visible',
];

 

If your module will have record or list views, you will need a portal directory under the clients directory. Within that portal folder, be sure to add the appropriate directories and files like so:

 

<basepath>/sugar_PortalAnnouncements/clients/portal/views/list/list.php

 

Now upload and install the package in module loader.

 

To allow this module to be seen/used on the Customer Portal, we must update the permissions for the role that is assigned to our Portal users. In Admin > Role Management find and select the Customer Self Service Portal role.

 

 

The next screen will show all of the modules and their permission levels for this role.

 

 

Find your new module in the list, and set the permissions that it requires. If you are unsure, base the permissions off of a pre-existing module on the Portal like Bugs. These are the basic settings to make the module appear on the Portal:

  • Access - Enabled
  • Access Type - Normal
  • List - All
  • View - All

For more details on role permissions, refer to the documentation for Setting Module-Level Permissions.

 

Finally, go to Admin > Sugar Portal > Configure Portal. Your new module will be listed in the Hidden column. To see it in the Portal, simply move it into the Displayed Modules list. And, that's it! Your new custom module has been added to the Customer Portal.

It's that time again! We have held another quarterly webinar where we discussed all the big changes that Sugar Developers need to know about in our upcoming Winter '20 releases.

 

In addition to details about the latest release, we also looked at topics that have been requested frequently since our last webinar.

 

What we covered:

Some of the big changes that are likely to impact you including the following:

  • Sugar Sell Renewals Console
  • Enhanced Sugar Serve SLA reporting
  • New Change Timer module

 

We also spent some time on:

  • Sugar Market integrations


In case you missed it:

If you missed the live sessions, don't worry. You can watch the recorded session here. The slide deck is also available for download or viewing here.

Note: This content was originally posted on Upsert's blog on October 31, 2019.

 

Have you ever wanted to create a custom route in Sugar that allows you to create, display, and edit a subset of a module's fields? Well, you're in luck!

 

In this post we will cover:

  • Creating views to handle record creation, viewing, and editing
  • Creating layouts to display those views
  • Creating routes to beautify your URLs

 

For our example, we will create a set of Account views that allow us to show a limited set of fields. This will work separately from the stock record view but behave the same.

  • To access the limited account record view, a user can directly navigate to <sugar_url>/#Accounts/<account_id>/limited
  • To access the limited account record edit view, a user can directly navigate to <sugar_url>/#Accounts/<account_id>/limited/edit
  • To access the limited account record create view, a user can directly navigate to <sugar_url>/#Accounts/limited/create

 

Why would you want to do this you ask? This can be beneficial in situations where you don't want to leverage Sugar's Role-Based Record View Layouts or if you have a data entry team that needs to populate fields without the noise that may come from a crowded record view.

 

Record View

To handle viewing and editing existing records, we will create a record-limited view that will be located in ./custom/modules/Accounts/clients/base/views/record-limited/record-limited.js.

 

./custom/modules/Accounts/clients/base/views/record-limited/record-limited.js

({
    extendsFrom: 'AccountsRecordView',

    /**
     * @inheritdoc
     */

    _loadTemplate: function (options) {
        this.tplName = 'record';
        this.template = app.template.getView(this.tplName);
    },

    /**
     * @inheritdoc
     */

    setRoute: function (action) {
        if (!this.meta.hashSync) {
            return;
        }

        if (action == 'edit') {
            action = 'limited/' + action;
        } else if (action == 'detail' || _.isEmpty(action)) {
            action = 'limited';
        }

        app.router.navigate(app.router.buildRoute(this.module, this.model.id, action), { trigger: false });
    },
})

 

Let's break down the record-limited.js file:

 

extendsFrom: 'AccountsRecordView',
The extendsFrom property allows us to specify the component we want to extend our view from. Normally, you would see extendsFrom: 'RecordView' however, we want to ensure that we extend the base accounts record view found in ./modules/Accounts/clients/base/views/record/record.js so that the existing core functionality isn't lost and that the historical summary button continues to work.

 

/**
* @inheritdoc
*/

_loadTemplate: function (options) {
    this.tplName = 'record';
    this.template = app.template.getView(this.tplName);
},

 

The _loadTemplate function allows us to load a template by another name. By default, Sugar will look for a template matching our view name of record-limited.hbs in ./custom/modules/Accounts/clients/base/views/record-limited/. As we want to extend and reuse the core record view, we will set this.tplName to record.

 

/**
* @inheritdoc
*/

setRoute: function (action) {
    if (!this.meta.hashSync) {
        return;
    }

    if (action == 'edit' || action == 'create') {
        action = 'limited/' + action;
    } else if (action == 'detail' || _.isEmpty(action)) {
        action = 'limited';
    }

    app.router.navigate(app.router.buildRoute(this.module, this.model.id, action), { trigger: false });
},

 

The setRoute function allows us to make sure the URL routes are set correctly when returning from our view. This is mainly for aesthetic purposes but ensures that the user does not get confused by the URL or have any copy & paste issues.

 

Record View Metadata

Next, we will create the record-limited metadata that will be located in ./custom/modules/Accounts/clients/base/views/record-limited/record-limited.php. This will define the buttons and fields that are displayed in our view.

 

./custom/modules/Accounts/clients/base/views/record-limited/record-limited.php

<?php

$viewdefs['Accounts'] = array(
  'base' => array(
    'view' => array(
      'record-limited' => array(
        'buttons' => array(
          0 => array(
            'type' => 'button',
            'name' => 'cancel_button',
            'label' => 'LBL_CANCEL_BUTTON_LABEL',
            'css_class' => 'btn-invisible btn-link',
            'showOn' => 'edit',
            'events' => array(
              'click' => 'button:cancel_button:click',
            ),
          ),
          1 => array(
            'type' => 'rowaction',
            'event' => 'button:save_button:click',
            'name' => 'save_button',
            'label' => 'LBL_SAVE_BUTTON_LABEL',
            'css_class' => 'btn btn-primary',
            'showOn' => 'edit',
            'acl_action' => 'edit',
          ),
          2 => array(
            'type' => 'actiondropdown',
            'name' => 'main_dropdown',
            'primary' => true,
            'showOn' => 'view',
            'buttons' => array(
              0 => array(
                'type' => 'rowaction',
                'event' => 'button:edit_button:click',
                'name' => 'edit_button',
                'label' => 'LBL_EDIT_BUTTON_LABEL',
                'acl_action' => 'edit',
              ),
              1 => array(
                'type' => 'shareaction',
                'name' => 'share',
                'label' => 'LBL_RECORD_SHARE_BUTTON',
                'acl_action' => 'view',
              ),
              2 => array(
                'type' => 'pdfaction',
                'name' => 'download-pdf',
                'label' => 'LBL_PDF_VIEW',
                'action' => 'download',
                'acl_action' => 'view',
              ),
              3 => array(
                'type' => 'pdfaction',
                'name' => 'email-pdf',
                'label' => 'LBL_PDF_EMAIL',
                'action' => 'email',
                'acl_action' => 'view',
              ),
              4 => array(
                'type' => 'divider',
              ),
              5 => array(
                'type' => 'rowaction',
                'event' => 'button:find_duplicates_button:click',
                'name' => 'find_duplicates_button',
                'label' => 'LBL_DUP_MERGE',
                'acl_action' => 'edit',
              ),
              6 => array(
                'type' => 'rowaction',
                'event' => 'button:duplicate_button:click',
                'name' => 'duplicate_button',
                'label' => 'LBL_DUPLICATE_BUTTON_LABEL',
                'acl_module' => 'Accounts',
                'acl_action' => 'create',
              ),
              7 => array(
                'type' => 'rowaction',
                'event' => 'button:historical_summary_button:click',
                'name' => 'historical_summary_button',
                'label' => 'LBL_HISTORICAL_SUMMARY',
                'acl_action' => 'view',
              ),
              8 => array(
                'type' => 'rowaction',
                'event' => 'button:audit_button:click',
                'name' => 'audit_button',
                'label' => 'LNK_VIEW_CHANGE_LOG',
                'acl_action' => 'view',
              ),
              9 => array(
                'type' => 'divider',
              ),
              10 => array(
                'type' => 'rowaction',
                'event' => 'button:delete_button:click',
                'name' => 'delete_button',
                'label' => 'LBL_DELETE_BUTTON_LABEL',
                'acl_action' => 'delete',
              ),
            ),
          ),
          3 => array(
            'name' => 'sidebar_toggle',
            'type' => 'sidebartoggle',
          ),
        ),
        'panels' => array(
          0 => array(
            'name' => 'panel_header',
            'label' => 'LBL_PANEL_HEADER',
            'header' => true,
            'fields' => array(
              0 => array(
                'name' => 'picture',
                'type' => 'avatar',
                'size' => 'large',
                'dismiss_label' => true,
                'readonly' => true,
              ),
              1 => array(
                'name' => 'name',
              ),
              2 => array(
                'name' => 'favorite',
                'label' => 'LBL_FAVORITE',
                'type' => 'favorite',
                'dismiss_label' => true,
              ),
              3 => array(
                'name' => 'follow',
                'label' => 'LBL_FOLLOW',
                'type' => 'follow',
                'readonly' => true,
                'dismiss_label' => true,
              ),
            ),
          ),
          1 => array(
            'name' => 'panel_body',
            'label' => 'LBL_RECORD_BODY',
            'columns' => 2,
            'labelsOnTop' => true,
            'placeholders' => true,
            'newTab' => false,
            'panelDefault' => 'expanded',
            'fields' => array(
              0 => 'industry',
              1 => 'website',
              2 => 'parent_name',
              3 => 'account_type',
              4 => 'service_level',
            ),
          ),
        ),
        'templateMeta' => array(
          'useTabs' => false,
        ),
      ),
    ),
  ),
);

This file is largely a duplicate of the core Accounts record view metadata, originally located in ./modules/Accounts/clients/base/views/record/record.php, with a limited set of fields. More information on view metadata can be found in the Sugar Developer Guide.


Record Layout

To display our new record-limited view, we will need to create a record-limited layout that will be located in ./custom/modules/Accounts/clients/base/layouts/record-limited/record-limited.php.

 

./custom/modules/Accounts/clients/base/layouts/record-limited/record-limited.php

<?php

$viewdefs['Accounts']['base']['layout']['record-limited'] = array(
    'components' => array(
        array(
            'layout' => array(
                'type' => 'default',
                'name' => 'sidebar',
                'components' => array(
                    array(
                        'layout' => array(
                            'type' => 'base',
                            'name' => 'main-pane',
                            'css_class' => 'main-pane span8',
                            'components' => array(
                                array(
                                    'view' => 'record-limited',
                                    'primary' => true,
                                ),
                                array(
                                    'layout' => 'extra-info',
                                ),
                                array(
                                    'layout' => array(
                                        'type' => 'filterpanel',
                                        'last_state' => array(
                                            'id' => 'record-filterpanel',
                                            'defaults' => array(
                                                'toggle-view' => 'subpanels',
                                            ),
                                        ),
                                        'refresh_button' => true,
                                        'availableToggles' => array(
                                            array(
                                                'name' => 'subpanels',
                                                'icon' => 'fa-table',
                                                'label' => 'LBL_DATA_VIEW',
                                            ),
                                            array(
                                                'name' => 'list',
                                                'icon' => 'fa-table',
                                                'label' => 'LBL_LISTVIEW',
                                            ),
                                            array(
                                                'name' => 'activitystream',
                                                'icon' => 'fa-clock-o',
                                                'label' => 'LBL_ACTIVITY_STREAM',
                                            ),
                                        ),
                                        'components' => array(
                                            array(
                                                'layout' => 'filter',
                                                'xmeta' => array(
                                                    'layoutType' => '',
                                                ),
                                                'loadModule' => 'Filters',
                                            ),
                                            array(
                                                'view' => 'filter-rows',
                                            ),
                                            array(
                                                'view' => 'filter-actions',
                                            ),
                                            array(
                                                'layout' => 'activitystream',
                                                'context' => array(
                                                    'module' => 'Activities',
                                                ),
                                            ),
                                            array(
                                                'layout' => 'subpanels',
                                            ),
                                        ),
                                    ),
                                ),
                            ),
                        ),
                    ),
                    array(
                        'layout' => array(
                            'type' => 'base',
                            'name' => 'dashboard-pane',
                            'css_class' => 'dashboard-pane',
                            'components' => array(
                                array(
                                    'layout' => array(
                                        'type' => 'dashboard',
                                        'last_state' => array(
                                            'id' => 'last-visit',
                                        ),
                                    ),
                                    'context' => array(
                                        'forceNew' => true,
                                        'module' => 'Home',
                                    ),
                                    'loadModule' => 'Dashboards',
                                ),
                            ),
                        ),
                    ),
                    array(
                        'layout' => array(
                            'type' => 'base',
                            'name' => 'preview-pane',
                            'css_class' => 'preview-pane',
                            'components' => array(
                                array(
                                    'layout' => 'preview',
                                ),
                            ),
                        ),
                    ),
                ),
            ),
        ),
    ),
);

 

This file is largely a duplicate of the core record layout metadata, originally located in ./clients/base/layouts/record/record.php, with the exception of our metadata index being $viewdefs['Accounts']['base']['layout']['record-limited'] and the view pointing to record-limited instead of record. More information on layouts can be found in the Sugar Developer Guide.


Creation View

To handle creating new records, we will create a create-limited view that will be located in ./custom/modules/Accounts/clients/base/views/create-limited/create-limited.js.

 

./custom/modules/Accounts/clients/base/views/create-limited/create-limited.js

({
    extendsFrom: 'CreateView',

    /**
     * @inheritdoc
     */

    initialize: function (options) {
        options.meta = options.meta || {};
        options.meta = _.extend(app.metadata.getView(null, 'create'), options.meta);
        options.meta = _.extend(app.metadata.getView(options.module, 'record-limited'), options.meta);
        this._super('initialize', [options]);
    },

    /**
     * @inheritdoc
     */

    saveAndClose: function () {
        this.initiateSave(_.bind(function () {
            if (this.closestComponent('drawer')) {
                app.drawer.close(this.context, this.model);
            } else {
                app.navigate(this.context, this.model, 'limited');
            }
        }, this));
    },
})

 

Let's break down the create-limited.js file:

 

extendsFrom: 'CreateView',

 

The extendsFrom property allows us to specify the component we want to extend our view from. In the record-limited view example above, we extended from AccountsRecordView. As we do not have a ./modules/Accounts/clients/base/views/create/create.js in the Sugar core product, we won't be able to extend from AccountsCreateView and can default to using CreateView.

 

/**
* @inheritdoc
*/

initialize: function (options) {
    options.meta = options.meta || {};
    options.meta = _.extend(app.metadata.getView(null, 'create'), options.meta);
    options.meta = _.extend(app.metadata.getView(options.module, 'record-limited'), options.meta);
    this._super('initialize', [options]);
},

 

The initialize function allows us to override and populate custom metadata into the view before it's loaded. Due to how Sugar view inheritance works, and because we want our create metadata to match what's defined in ./custom/modules/Accounts/clients/base/views/record-limited/record-limited.php, we can tell the controller to load the default create buttons from ./clients/base/views/create/create.php with the code snippet options.meta = _.extend(app.metadata.getView(null, 'create'), options.meta) and then to fill in the rest of the metadata from ./custom/modules/Accounts/clients/base/views/record-limited/record-limited.php with the code snippet options.meta = _.extend(app.metadata.getView(options.module, 'record-limited'), options.meta). You could opt to not use this approach and create a ./custom/modules/Accounts/clients/base/views/create-limited/create-limited.php file with your field definitions.

 

/**
* @inheritdoc
*/

saveAndClose: function () {
    this.initiateSave(_.bind(function () {
        if (this.closestComponent('drawer')) {
            app.drawer.close(this.context, this.model);
        } else {
            app.navigate(this.context, this.model, 'limited');
        }
    }, this));
},

 

The saveAndClose function is what gets called upon save. The key change here is that app.navigate(this.context, this.model, 'limited') directs users to the record-limited layout instead of the stock record layout.


Create Layout

To display our new create-limited view, we will need to create a create-limited layout that will be located in ./custom/modules/Accounts/clients/base/layouts/create-limited/create-limited.php.

 

./custom/modules/Accounts/clients/base/layouts/create-limited/create-limited.php

<?php

$viewdefs['Accounts']['base']['layout']['create-limited'] = array(
    'components' => array(
        array(
            'layout' => array(
                'type' => 'default',
                'name' => 'sidebar',
                'last_state' => array(
                    'id' => 'create-default',
                ),
                'components' => array(
                    array(
                        'layout' => array(
                            'type' => 'base',
                            'name' => 'main-pane',
                            'css_class' => 'main-pane span8',
                            'components' => array(
                                array(
                                    'view' => 'create-limited',
                                ),
                            ),
                        ),
                    ),
                    array(
                        'layout' => array(
                            'type' => 'base',
                            'name' => 'preview-pane',
                            'css_class' => 'preview-pane',
                            'components' => array(
                                array(
                                    'layout' => 'preview',
                                ),
                            ),
                        ),
                    ),
                ),
            ),
        ),
    ),
);

 

This file is largely a duplicate of the core create layout metadata, originally located in ./clients/base/layouts/create/create.php, with the exception of our metadata index being $viewdefs['Accounts']['base']['layout']['create-limited'] and the view pointing to create-limited instead of record.

 

Layout Routing

Now that we have our views and layouts in place, we can define our routes. To accomplish this, we must first define a javascript file containing our routes. This file can exist anywhere you like, though we recommend ./custom/include/JavaScript/.

 

./custom/include/JavaScript/myCustomRoutes.js

(function (app) {
    app.events.on("router:init", function () {
        var routes = [
            {
                route: 'Accounts/:id/limited',
                name: 'AccountsRecordLimited',
                callback: function () {
                    App.controller.loadView({
                        module: 'Accounts',
                        layout: 'record-limited',
                        modelId: arguments[0],
                        action: 'detail',
                    });
                }
            },
            {
                route: 'Accounts/:id/limited/edit',
                name: 'AccountsRecordLimitedEdit',
                callback: function () {
                    App.controller.loadView({
                        module: 'Accounts',
                        layout: 'record-limited',
                        modelId: arguments[0],
                        action: 'edit',
                    });
                }
            },
            {
                route: 'Accounts/limited/create',
                name: 'AccountsCreateLimited',
                callback: function () {
                    App.controller.loadView({
                        module: 'Accounts',
                        layout: 'create-limited',
                        create: true,
                        action: 'create',
                    });
                }
            }
        ];
        app.router.addRoutes(routes);
    })
})(SUGAR.App);

 

This file contains the 3 routes we will use for creating, viewing, and editing. The important thing to note here is that if you are accepting variables (i.e. ":id") from the route path, they will be available as arguments in the route's callback. More detailed information on routing can be found in the Developer Guide.

 

Next, we need to add the routes file to our JSGroupings. To accomplish this we will create ./custom/Extension/application/Ext/JSGroupings/CustomRecordViews.php and append our JavaScript file to the ./include/javascript/sugar_grp7.min.js file.

 

./custom/Extension/application/Ext/JSGroupings/CustomRecordViews.php

<?php

foreach ($js_groupings as $key => $groupings) {
    $target = current(array_values($groupings));
    if ($target == 'include/javascript/sugar_grp7.min.js') {
        $js_groupings[$key]['custom/include/JavaScript/myCustomRoutes.js'] = 'include/javascript/sugar_grp7.min.js';
    }
}

 

More information on using JSGroupings with routes can be found in the Developer Guide.

Finally, navigate to Admin > Repair > Quick Repair & Rebuild. Once complete, navigate to any of the following URLs to work with your new view:

  • Create - <sugar_url>/#Accounts/limited/create
  • View - <sugar_url>/#Accounts/<account_id>/limited
  • Edit - <sugar_url>/#Accounts/<account_id>/limited/edit

 

It is important to note that if you need to make additional changes to your routes, you will need to rebuild the js grouping files by navigating to Admin > Repair > Rebuild JS Grouping Files.

 

Note: This code example was written against Sugar 9.0.0 Professional. You can view the Sugar code for the example above here or download the module loadable package attached to this post.

In case you missed it, here's the recording!

Recording: Webinar: How to deploy code to SugarCloud using Module Loader 

The Module Loader is one of the most useful tools available to a SugarCloud developer when customizing Sugar. It is the safest (and only) method for manipulating files within the SugarCloud. Join us for a talk on how to use the Module Loader effectively for your customizations.

 

What we will be covering:

We will demonstrate and discuss the following topics:

  • Module Loadable Package definition and structure
  • The manifest
  • Package scanner
  • Versioning
  • Important rules to keep in mind
  • How to avoid common problems and when to reach out for help


Webinar Information:

Join us for the live webinar:

Monday, October 28th 7:00 - 8:00 AM PDT (10am EDT)

 

Can't make it? Don't worry! We will be posting the webinar recording to this Sugar Developers Community for those who are unable to attend the live sessions.

 

If you have further input or questions, you can reach out to us directly at developers@sugarcrm.com.

Sugar Fall '19 release is officially live! There's a huge buzz around our offices for this release - it's very exciting! Fall '19 brings an updated Portal experience as well as a few other features and fixes.

 

We recently held a webinar with an overview of what is in the Fall '19 release. If you missed it, watch the recording here or view the slides from this post. Note: Sugar 9.2 (Fall '19) is a cloud-only Sugar release. On-premise customers must wait until Sugar 10.0 (Spring '20) to obtain the feature enhancements provided in this release.

 

Here's the TL;DR for those of you looking for a quick list:

  • Portal changes:
    • Header and footer UI changes
    • Mega menu configurability
    • Metadata customization
    • New fields in Contacts module
    • Portal users are now able to use Preview to view records
    • Cases, Notes, and Bugs now show in Portal by default
    • Ability to designate/identify the source of a case has been added
    • Knowledge Base dashlet added to Portal home
    • Administrators can now choose to enable "case deflection"
    • Portal FilterView and PortalListTop UI components were deprecated
    • Portal users can now search for answers to their own questions
    • Field display changes to Portal views
    • It is no longer possible to view PII from the Portal User Profile screen
    • 3 new Portal dashlets have been added
    • Improved Portal user signup
    • Admins can configure password reset options (phone, email, url)
  • Library updates
    • Jquery
    • jQuery Migrate
    • moment.js
    • Bootstrap library components
  • Business Centers Module now added to Sugar Sell

 

For details on everything in this release, check out our other Summer '19 resources:

Hello Sugar Developers!

 

Fall is upon us and we have lots of things to share. We want to make sure you are ready for the Sugar Fall '19 release, so we're hosting two webinars just for you!

 

What we will be covering:

Some big changes that are likely to impact you this fall are:

  • Portal updates and improvements
  • Business Centers module has been added to Sugar Sell
    • Associates Leads with a business center to keep track of business hours per location. Using SugarBPM, lead SLAs can be met by automatically following up within a time measured in business hours
  • Javascript Library and Bootstrap component updates

 

We will also spend some time on:

  • Customizing the new Portal
  • Addressing common questions about new SugarCRM product lines
  • More best practices for developing for the Cloud

 

Webinar Information:

We are holding two sessions to accommodate various geographical locations.On the registration page, you will have the choice of ONE of the following times.

 

Monday, September 23rd 4:30 - 5:30 PM PT

OR

Tuesday, September 24th 7:00 - 8:00 AM PT

 

Register Now!


We will be posting the webinar recordings to the Sugar Community for those who are unable to attend the live sessions.

Hi, my name is Julian Haresco and I am a Software Engineering Intern at SugarCRM. I am going to be a rising senior at Purdue University majoring in Computer Science with a focus in Machine Learning and Software Engineering. This summer, I was given the opportunity to integrate Amazon’s Beta EventBridge API with Sugar for SugarCRM to become one of Amazon's ten launch partners for the EventBridge.

 

What is Amazon EventBridge?

In case you haven’t heard, Amazon released a new product for its cloud services called Amazon EventBridge. This service is a new addition to the vast array of services offered within Amazon Web Services (AWS) but comes with a very special feature that I believe makes it stand out from other products offered by Amazon’s cloud service. EventBridge is unique as it allows you to utilize a third-party SaaS system to send CloudWatch Events to AWS. The biggest benefit of this product is the freedom it offers to customers. Since the Event Buses and sources are customizable based on region, they can view the events in their desired location and then have them interact with other AWS resources easily. Furthermore, prior to EventBridge, all AWS CloudWatch Events could only be triggered by another resource within AWS. So allowing them to connect to an external SaaS system simply allows for AWS to be more usable than ever before with data external to AWS.

 

Amazon has partnered with various SaaS companies to set-up what they are calling Partner Event Sources. These are SaaS systems with EventBridge Partner Event Sources integrated into their systems already for customer use. SugarCRM Inc. is one of the ten initial AWS EventBridge launch partners including Symantec, Zendesk, and SignalFx. For Sugar, this pre-installed integration builds a timeline within AWS based on the modules and logic hooks the developer desires. Empowering customers with a real-time log about how the records and relationships within their system are changing with every event sent.

 

Here’s how I did it

To meet these needs, a new administration module that is installed via a module loadable package was created named EventBridge Logic Hooks. Within this module, Sugar will automatically connect, or create, the internal “Partner event source” based on how the customer fills out the fields when creating a new record for this module. However, the values within the fields will need to meet the requirements outlined in our support documentation for Amazon EventBridge. This module then automatically creates a Sugar logic hook that will send an event to EventBridge using the Partner Event Source to the AWS account they provided once the specified module triggers the logic hook.

 

 

On the Amazon side, the EventBridge Console will automatically display the “Partner event source” with the same name displayed on the record created with Sugar. These event sources can then be attached to an Event Bridge by following Amazon’s instructions. Now our customers can attach the event source to the Event Bus. This allows them to target resources that best suit their needs.

 

 

How do developers get involved?

Developers who want to experiment with Sugar’s integration of EventBridge can download the module loadable package from SugarExchange and follow the instructions outlined with our support documentation for Amazon EventBridge to install the module. From there, developers should be able to effectively create and set-up Partner Event Sources and connect them to an EventBus within their AWS account. 

 

NOTE: This is a beta version so there are improvements that could be made. For example: The operations to Event Bridge are synchronous which may cause additional latency in save operations for enabled modules.

 

What has been our experience?

Amazon EventBridge has been incredibly easy to use in terms of creating, editing, and setting up the Partner event source within Sugar. As a developer, the documentation is clearly laid out and extremely helpful in finding the answers to any questions about configuring EventBridge within a SaaS system like Sugar. Furthermore, the EventBridge Console makes a clear distinction between a Partner event source and one I create on my own. This set-up is incredibly  intuitive and easy to use, providing a seamless set-up process from within Sugar to the AWS EventBridge Console on the customer’s side.

 

This project was just the beginning. AWS EventBridge opens up great possibilities for integration. I'll continue using it and looking for new ways to leverage this system. I hope you will too. So, please, take a look at AWS EventBridge. Connect it with Sugar. Then, tell us about your experiences in the comments.

We recently held a webinar on How to write code for SugarCloud. At the end, we gave a summary of some of the Dos and Don'ts for working with SugarCloud.

 

With more and more customers utilizing SugarCloud products, I thought it would be a good idea to expand on some of the basic best practices when developing for SugarCloud. As Sugar's cloud-based product line evolves, I will add more items to this list.

 

When developing for SugarCloud:
Don't

use custom code when configuration will do just fine.

The ability to write custom code for Sugar is a huge benefit. It isn't, however, the best solution for all situations. Very often your problem can be alleviated by simply using the configuration tools that Sugar provides in its admin console. Manipulating a configuration in the system is typically a safer choice as there is no concern with upgrade compatibility.

Don't

have direct filesystem or DB access.

SugarCloud is a shared environment. Any changes made to the filesystem could impact other customers.

Don't

use blacklisted classes, functions, or file types.

In order to maintain the integrity of the standard Sugar functionality when we upgrade a customer instance and limit any negative impact our upgrade has on the customer's modifications, all instances hosted on Sugar's cloud service have package scanner enabled. Here is a blacklist of cases that will cause the package scanner to fail.

Don't

perform load or pen testing without permission of Sugar Support.

SugarCloud is a shared environment. An unscheduled load test may cause performance issues with other customers' instances. You must obtain Sugar Support's permission so that they may make the proper adjustments to ensure no other instances are affected by your tests.

Don't

introduce performance or security issues with your code.

For the safety and security of your users, it is never wise to introduce performance or security issues into your code. This is especially true when working in a shared environment so as not to affect other customers' user experiences.

Don't

disable or circumvent package scanner.

Package scanner is enabled on all cloud instances to ensure no security violations are introduced.

Don't

allow an outbound HTTP connection to last longer than 1 second.

SugarCloud is a shared environment. Long connections can have a performance impact on your users as well as the users of other customers.

Don't

abuse the job queue with a multitude of long running jobs.

SugarCloud is a shared environment. Long running can have a performance impact on your users as well as the users of other customers. If you load the queue with too many long running jobs, the rest of the jobs awaiting their turn will be affected

Don't

abuse the REST API with more than 20 requests per second.

SugarCloud is a shared environment. Too many requests can have a performance impact on your users as well as the users of other customers.

Do

upgrade to every new release.

Sugar Sell and Sugar Serve operate on a quarterly update cycle while Sugar Market is updated approximately every two weeks. Each update will include new improvements or fixes from the previous version. It is important to keep up-to-date on these upgrades to minimize the number of things that will need to be tested. 

Do

test before you deploy!

It is always better to find any issues in a test environment prior to deploying live. If there are issues or incompatibilities after a change, these should be caught and addressed before a user runs into a problem.

 

Want to learn more? Don't miss the webinar recording.

In response to the recent evolution of the SugarCRM product line, we’ve compiled a list of answers to some common questions that we have received from the developer community about our new products. This FAQ will be a living document, so please post any additional questions in the comments section and we will do our best to address them here.

Sugar Professional and Enterprise customers: 

If you are an existing customer of Sugar Professional or Sugar Enterprise then nothing has changed for you. If you are in our cloud, you will still get new features on a quarterly basis. If you are on-premise, you will still get new features on an annual basis.

    
QuestionSugar MarketSugar SellSugar Serve
Will it be available On-Premise?No. Sugar Market, Sugar Sell, and Sugar Serve are available via cloud only and are not available for on-site deployment.
Can we write code customizations for it?The Sugar Market platform does not support direct code customizations. It does, however, have REST APIs and other tools to enhance your development.

Yes, Sugar Sell and Sugar Serve are based on the Sugar Enterprise platform which supports code customizations. You can use Module Loader to install code customizations for Sugar Serve and Sell. Since Serve and Sell are built on Sugar Enterprise, use the ENT flavor in your package manifests.

How can we download instance backups for local development and test?No, Sugar Market is a multi-tenant application. There is no concept of local development.Yes using Backups module.
Can we get data backup?Yes, by exporting a report.Yes using Backups module.
Can we access development builds?We are working toward a solution for this.There is a Developer Builds space in the SugarCRM Developers community. We will post development builds here for each release.
Will Sell/Serve/Market use the same platform as Sugar Enterprise?Sugar Market is a unique platform.Sugar Sell and Sugar Serve are built on the Sugar Enterprise platform.
Will Sell/Serve/Market be connected to the same database, or will they be separate instances connected via API?At this time, Sugar Market utilizes an independent database. Sugar Market integrates with Sugar Sell/Ent/Pro out of the box.A single SugarCloud instance and database can have both users of Sugar Sell and Sugar Serve using it at the same time.
Where can I find more specific info about the divergence between Sugar products?The differences between the SugarCRM License Types are outlined in the User Management section of the Sugar Enterprise 9.1 Administration Guide.
What resources are available if I have more questions?Sugar Market DocumentationSugar Sell DocumentationSugar Serve Documentation
How often will Sugar products be updated?

Sugar Market operates on a continuous update cycle, with releases approximately every two weeks.

Sugar Sell and Sugar Serve operate on a quarterly release cycle (every three months).

Sugar Summer '19 release is officially live! There's a huge buzz around our offices for this release - it's very exciting! Summer '19 introduces brand new Sugar products as well as many enhancements for existing Sugar Professional and Sugar Enterprise customers.

 

We recently held a webinar with an overview of what is in the Summer '19 release. If you missed it, watch the recording here or view the slides from this post. Note: Sugar 9.1 (Summer '19) is a cloud-only Sugar release. On-premise customers must wait until Sugar 10.0 (Spring '20) to obtain the feature enhancements provided in this release.

 

Here's the TL;DR for those of you looking for a quick list:

 

  • SugarIdentity service is out of beta. SugarIdentity is a set of user authentication and access management microservices that will improve how we manage Sugar cloud users today. It offers improved OAuth 2.0 support, leverages OpenID Connect, and supports SAML Web Single Sign On (SSO) with the MS Outlook Plug-In. Read more about SugarIdentity in our post called What you need to know about the new SugarIdentity service! All new SugarCloud customers in the Americas, including those customers of our new Sugar Sell and Sugar Serve products, are using SugarIdentity today.
  • Sugar products now support TLS encryption for LDAP single sign-on.
  • A new field on user records, License Type, has been added to allow administrators to grant each user access to one or more products including SugarCRM's newest offerings, Sugar Sell and Sugar Serve.
  • NEW PRODUCTS!! We are very excited to announce 3 new products:
    • Sugar Market is a rebranding of Sugar's recently-acquired marketing automation solution, SalesFusion.
    • Sugar Sell is our award-wining sales automation solution.
    • Sugar Serve is Sugar's new customer engagement center solution.
  • The new SugarCloud Insights page allows administrators to easily monitor their instance's database and file system storage usage, license usage, as well as gain access to PHP error logs and access logs.
  • Shareable dashboards now include custom user-created filters.
  • Tile View : For cases, tasks, and opportunities, a new view has been added that displays records as tiles in a familiar interactive, drag-and-drop interface.
  • Bug and Case bean classes now extend \Issue instead of \Basic class.
  • A new direction field has been added to the Emails module. The possible values are inbound, outbound, internal, unknown.

 

For details on everything in this release, check out our other Summer '19 resources:

We intend to disable support for TLS v1.1 and older in the SugarCloud on November 8, 2019 February  1, 2020. This action is consistent with the rest of the industry. It may impact some Sugar integrations that connect to the SugarCloud. If you are hosting Sugar on-site, you should consider taking similar steps to disable TLS v1.1 and earlier on your web servers.

 

Read on to learn more.

TLS/SSL Vulnerabilities

The SSL (“Secure Sockets Layer”) protocol was initially invented by Netscape back in the mid-1990s as a method for securing communications over a computer network. This protocol provides the “S” in HTTPS which is used to secure all HTTP traffic to Sugar web servers. As you might expect with 25 year old security technology, there’s been quite a few revisions and improvements to the original concept over time. In fact, SSL v3.0 came out in 1996 which was only a couple years after SSL itself was first invented. SSL was later succeeded by TLS (“Transport Layer Security”) which itself has seen several iterations.

 

Protocol

Published

Status

SSL 1.0

Unpublished

Unpublished

SSL 2.0

1995

Deprecated in 2011 (RFC 6176)

SSL 3.0

1996

Deprecated in 2015 (RFC 7568)

TLS 1.0

1999

Deprecation planned in 2020

TLS 1.1

2006

Deprecation planned in 2020

TLS 1.2

2008

TLS 1.3

2018

Courtesy of Wikipedia

 

With most technology, the penalty for not adopting the latest and greatest is mostly FOMO (“fear of missing out”). But cryptographic protocols are used for target practice by white and black hat wearing security researchers the world over. This means that using out of date cryptographic protocol compounds FOMO with FOLE (“fear of losing everything”).

 

The value of a TLS/SSL protocol is inversely proportional to the number of holes that have been punched into it. Some of these holes are exploits that go by the name of POODLE and BEAST. At the same time, the industry has been continuously adding better and stronger encryption protocols in response.

 

The industry is dropping support of old TLS versions

SSL is REALLY old, so hopefully nobody is still using this. However, there is still plenty of code out there using older versions of TLS. The PCI Data Security Standard requires all connections to use TLS v1.1 or higher while strongly recommending TLS v1.2 or higher. Even the browser vendors who are loathe to drop features that could impact website compatibility (and market share) have agreed to drop support for TLS v1.0 and v1.1 in 2020.

 

As a result, we are considering the right time to disable support for TLS v1.1 and older for connections to the SugarCloud. This may impact some Sugar integrations that connect to the SugarCloud as we look to stay in step with the rest of the industry.

 

Make sure your REST API integrations are using TLS v1.2+

If you are using a modern web browser, then it is unlikely that you will run into any problems connecting to Sugar instances. However, some REST API integrations that are using old client libraries or runtimes are liable to use these older protocols. Basically, if you are running 10+ year old software in your integration then you will likely have some of these problems below.

 

In particular, please take extra care if you are using any of the following technology with your Sugar integration.

 

Client

Preferred Runtime

Apache HttpComponents

Use latest Java 8 or greater

RestSharp

Use latest .NET 4.7 or greater

cURL and OpenSSL (PHP)

Use OpenSSL 1.0.x or greater (PHP 7.1 or greater)

 

If you aren’t sure, you can use a network analyzer to verify the version of TLS that is in use. For example, you can use tcpdump or Wireshark.

 

Take the following steps if you believe your integration is affected.

  • If applicable, upgrade to newer runtime environments for your integrations
    • Ex. Upgrade to Java 8 or newer or to .NET 4.6 or newer
  • Upgrade to latest HTTP client library versions
    • Ex. HttpComponents v4.4.11+ is compatible with TLS v1.3 implementation found in Java 11
  • Configure your HTTP clients to require use of TLS v1.2

How to disable TLS v1.1 and earlier for Sugar on-site installations

You will typically configure the web server with the versions of TLS/SSL that will be allowed by your Sugar instance.

 

For Apache, the allowed versions of TLS can be configured using mod_ssl’s SSLProtocol directive

 

For IIS, the allowed versions of TLS can be configured using TLS Registry Settings.

 

Connections using TLS v1.1 or earlier will break

Only 6% of web traffic in SugarCloud is using an out of date version of TLS. So we are moving aggressively to ensure SugarCloud will only support TLS v1.2+ in the future. 

 

 

Support for TLS versions 1.1 and older in SugarCloud will be disabled on November 8, 2019.

Updated Support for TLS versions 1.1 and older in SugarCloud will be disabled on February 1, 2020THIS IS A FIRM DEADLINE AND WILL NOT BE EXTENDED.

Hello Sugar Developers!

 

We often get questions about building code customizations for SugarCloud. Even experienced developers who have developed customizations for on-site Sugar installations do not know what is possible in SugarCloud. In short, Sugar Cloud does support custom code though there are some rules that need to be followed. We will dig into how write code customizations for SugarCloud in this webinar.

 

What we will be covering:

We will demonstrate by example (with the help of Professor M) the following topics:

  • Accessing SugarCloud Developer Builds
  • How to configure local dev environment to be similar to SugarCloud
  • Methods for managing and deploying custom code in SugarCloud
  • How to debug problems in a Cloud instance
  • Important rules to keep in mind

 

Webinar Information:

Join us for the live webinar:

Tuesday, July 16th 7:00 - 8:00 AM PT

Register Now!

 

Can’t make it? Don’t worry! We will be posting the webinar recording to this community for those who are unable to attend the live session.

SugarCRM strives to be the "No-Touch" CRMOne of the most important steps in making this vision a reality is to integrate Sugar with systems that customers uses to engage with organizations. For example, if a customer has been recently complaining about your product on your blog, wouldn't you want to know about that before talking with them?

 

Building a Wordpress integration

 

This post will explain how to import a comment from your blog (using Wordpress for this example) into Sugar as a note record related to the lead or contact who posted it. The goal of this exercise was to illustrate how to communicate from a common external content management system and Sugar. It turns out that getting the data into Sugar using REST APIs is pretty simple. It was everything else that took some thought and time.

 

The project started out as "send a Wordpress comment to Sugar as a note." It turned into much more than that (and has the potential to go so much further). So, here's a high level of our steps:

 

  • Created a Wordpress plugin
    • Plugin will record the Sugar login credentials and instance path
    • Plugin will call the Sugar OAuth REST endpoint to validate the user's credentials before allowing a save 
  • Upon submitting a comment to any Wordpress blog post, the comment data is sent to our instance of Sugar as a Note
    • Get the current logged-in Wordpress user
    • Authenticate to the Sugar API
    • Check if Wordpress user is in Sugar already (by e-mail address)
      • If so, add this comment as a Note linked to their [Account] record
      • If not, create a Lead record then create and link a Note to that record

 

Every CMS has its own special methods for connecting to external APIs. I chose to not use those functions in Wordpress for this exercise and simply used PHP's cURL functions. This should allow our example to be reused more easily with other applications.

 

The first step was to create the Wordpress plugin with a Settings page to allow a Wordpress admin to configure the integration. Our Settings page asks for Sugar user credentials, the URL to your instance of Sugar, and which version of the API your instance is using. All of this information gets stored in the Wordpress database for easy retrieval by our plugin.

 

The Wordpress settings page for our custom plugin

 

Now that we have our credentials, we will do something with them. We will create some functions that kick off when a Wordpress user adds a comment on any blog post. Wordpress has an action called "comment_post" that gets triggered after a comment is added by a user and subsequently stored in Wordpress database. We can assign a custom function to fire when that event is triggered by writing:

add_action( 'comment_post', 'send_comment_to_sugar_function', 10, 2 );

 

Basically, this line says any time a comment is saved to the Wordpress database, call the function called 'send_comment_to_sugar_function' with a priority of 10 and 2 possible parameters. For more information on Wordpress actions and hooks, check out the Wordpress Codex.

 

Now, let's write the guts for that custom function. The first thing we must do is authenticate to Sugar API. To do that, we will build the URL to the REST endpoint by concatenating the values that we stored in the plugin's settings page. This looks something like:

$api_path_auth = "/oauth2/token";
$auth_url = get_option('sugarcrm_input_url') . '/rest/v' . get_option("sugarcrm_input_api_version") . $api_path_auth;

 

On the settings page, I saved the values in the DB as "sugarcrm_input_url", "sugarcrm_input_api_version", "sugarcrm_input_username", and "sugarcrm_input_password". So, we can access those values with get_option(OPTION_NAME).

 

The oauth2/token endpoint needs parameters to return a successful authentication. Here is what we are sending:

$oauth2_token_arguments = array(
        'grant_type' => 'password',
        'client_id' => 'sugar',
        'client_secret' => '',
        'username' => get_option( 'sugarcrm_input_username' ),
        'password' => get_option( 'sugarcrm_input_password' ),
        'platform' => 'wordpress_api'
);

 

Note the "platform" parameter. This is the name you are giving to your Wordpress instance. You must go into Sugar's Admin settings to allow this platform to communicate with the Sugar REST API. The name could be anything that you like - as long as you add that same name to the Sugar API Platforms list.

Configure API Platforms page in the Sugar Admin

Now that we have our parameters to send, we need to set the cURL options. For a more detailed explanation of cURL in PHP, check out the PHP cURL manual

 

These are our options (pretty typical):

$auth_request = curl_init($auth_url);
curl_setopt($auth_request, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
curl_setopt($auth_request, CURLOPT_SSL_VERIFYPEER, 1);
curl_setopt($auth_request, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($auth_request, CURLOPT_FOLLOWLOCATION, 0);
curl_setopt($auth_request, CURLOPT_HTTPHEADER, array(
    "Content-Type: application/json"
));

//convert arguments to json
$json_arguments = json_encode($oauth2_token_arguments);
curl_setopt($auth_request, CURLOPT_POSTFIELDS, $json_arguments);

 

That last line is where we are ensuring that we are POSTing to the endpoint and that we are sending a JSON encoded array of parameters.

 

Finally, we can execute the request and parse out the data that we need from the response:

$oauth2_token_response = curl_exec($auth_request);
$oauth2_token_response_obj = json_decode($oauth2_token_response);
$sugar_oauth_token = $oauth2_token_response_obj->access_token;

 

I originally had the function return the auth_token but I decided to also store the auth_token and the refresh_token values in the Wordpress cache so that I can grab them whenever I need them. This will ensures we aren't needlessly logging into Sugar over and over again.

wp_cache_set('sugar_oauth_token_access', $sugar_oauth_token);
wp_cache_set('sugar_oauth_token_refresh', $oauth2_token_response_obj->refresh_token);

 

Now that we have authenticated, we can start sending data through the Sugar REST API. Let's see if the current wordpress user is in our instance of Sugar by utilizing the global search API (/search). Since we are making a GET request, we will format our URL to include our parameters in the URL query string. I used the http_build_query function to loop through an array of parameters and construct a query string to be appended to the end of our endpoint URL. This could have been done manually.

 

Either way works as long as the final result resembles:

http://localhost:8080/sugar/rest/v11_3/search?q=mshaheen@sugarcrm.com&module_list=Contacts,Leads

With the URL setup, we need to use cURL again. Since the settings are essentially the same for each of my requests, I wrote a single function to initialize and execute our REST calls.

function curl_it($sugar_oauth_token, $curl_url, $data = null, $ispost = false) {
     $the_response = null;
     $the_request = curl_init($curl_url);
     curl_setopt($the_request, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
     curl_setopt($the_request, CURLOPT_SSL_VERIFYPEER,1);
     curl_setopt($the_request, CURLOPT_RETURNTRANSFER, 1);
     curl_setopt($the_request, CURLOPT_FOLLOWLOCATION, 0);
     curl_setopt($the_request, CURLOPT_HTTPHEADER, array(
         "Content-Type: application/json",
         "oauth-token: {$sugar_oauth_token}"
     ));
     if ($ispost) {
          curl_setopt($the_request, CURLOPT_POSTFIELDS, $data);
     }
     $the_response = curl_exec($the_request) ;
     curl_close($the_request);
     return $the_response;
}

 

The notable part of this is where we set the OAuth-Token or a standard bearer token. This wasn't necessary in the initial auth call.

curl_setopt($the_request, CURLOPT_HTTPHEADER, array(
    "Content-Type: application/json",
    "OAuth-Token: {$sugar_oauth_token}"
));

 

Now we should have a response back from Sugar. This response, if successful, will contain a record that has an e-mail address matching the one we sent in. From the returned record, we can grab the IDs that we need to be able to associate this comment with the returned user. We will add the comment as a note linked to the account record. In order to appropriately associate the new note, we'll send the Sugar User ID as the parent_id for the note and ensure that we are using the appropriate parent_type.

 

What if the e-mail address of the current Wordpress commenter can't be found in Sugar? Well then let's add them as a Lead and THEN create the new note and attach it to the newly created lead record.

 

What if the e-mail address is found but it is a Lead and not a Contact? Then we need to set the parent_type to "Leads" and the parent_id to the id of the Lead record. For anything else, we use the account_id from sugar as the parent_id.

 

Now that the plugin is doing essentially what we want it to do, there’s many enhancements we could add. The first one I threw in there was a verify button on the admin form to check if the user settings are valid. I adjusted the auth function to take the form values as parameters. That way, when we expose the function for REST calls we can pass in the values from the form BEFORE they are saved to the Wordpress database.

 

This button is essentially making the same authentication REST call that we are making when sending data to Sugar. I could have written the javascript to call the Sugar endpoint directly but that opens up a few issues. Firstly we would likely (I did) run into a Same-Origin policy conflict where the Sugar client doesn’t recognize and/or trust the external Wordpress domain. The other issue I saw was that I’d have to rewrite exactly what I did in the PHP for the front-end. I prefer reusable code. There were also some security issues to consider but these were the glaring problems from a strictly front-end implementation. SO, I simply researched how to turn my Wordpress functions within the plugin into additional endpoints in the Wordpress API. All it took was registering the route for the endpoint and telling it which function to call.

add_action( 'rest_api_init', 'init_rest_api' );

function init_rest_api () {
     register_rest_route( 'sugarcrm-api/v1', '/auth', array(
          'methods' => \WP_REST_Server::EDITABLE,
          'callback' => 'sugarcrm_rest_auth_validate',
          'args' => ['username', 'password', 'url', 'version']
     ));
}

Other tips & tricks

Much of this exercise was new to me (or I needed some refreshers). What I found was that sending the data to Sugar was not particularly difficult. The work came from deciding what to do with the information and how to handle different scenarios - like the user already being in the system as a Lead not a Contact. I spent a lot of time using the browser console to look at objects returned in the Sugar instance. I think I used this command in the console more than any other:

SUGAR.App.controller.context.get("model")

 

I would navigate to a Lead or Note record in Sugar and then run that command in the developer console to see all of the properties and values for the current module. It really helped me decide on what data to send or grab.

 

But not everything could be traced out on the front-end. So, Stack Overflow came to my rescue with a simple logging function I implemented in the PHP. With this function, I could spit out whatever I wanted to a local log file for debugging. If I wanted to see the contents of a response object, for example, I would write the line:

log_it($my_response, 'The object my_response contains: ');

 

Try it out yourself!

 

If you would like to use the code that was written for this article for some hands-on practice, you will need an instance of Sugar and an instance of Wordpress. I did it all locally with a Vagrant box for Sugar and MAMP for the Wordpress instance.

 

You don't need to install any custom code into Sugar. It uses the standard REST API and a custom API platform which can be configured easily via Sugar Admin panel.

 

The Wordpress integration code has been added to the SugarCRM Building Blocks git repository in the api-examples directory. Simply download and unpack the plugin files and drop them into your Wordpress plugins directory (typically at /wp-content/plugins). Now you should be able to activate the plugin in Wordpress and add your customized settings via the "Sugar API Plugin" settings menu option on the left navigation (like that custom logo icon? hehe).

 

Wordpress admin side navigation hi-lighting "Sugar API Plugin" option

 

Remember to register the platform in Sugar's admin section using the name "wordpress_api" or whatever you change it to in the code. That's it!

 

To try it out, you should be able to add a comment to a Wordpress post and see it attached to a record in Sugar.

 

Now go play! Here are some ideas for changes you can make to my plug-in example.

 

  • Try to add more detail to the Note created in Sugar.
  • When a comment is deleted in Wordpress, find the note in Sugar and update it.
  • Include a link to the record in Sugar that points back to the comment in Wordpress.
  • And please... share with the community what you did - no matter how minor or major.

 

I hope you enjoyed this article. I’ve never really thought of myself as a writer. In fact, my first job was working in an orange juice factory. But I got canned. I couldn’t concentrate.