Write a dashlet that uses an external data source: the Football World Cup dashlet

During the Sugar 7 Developer training sessions, the attendees often asked for more code samples to get up to speed writing dashlets. This blog provides a good template that could be used when writing a dashlet that will pull data from an external source.Quick start

The dashlet is available for free on Github https://github.com/tortugacrm/sugar7_football_wc2014_dashlet

This code is published under the GPL 3 license.For a quick installation in your Sugar 7 instance, please follow the guidances in §Installation.

The dashlet comes in English, French, German, Swedish and Russian.

Introduction

This blog post is a straightforward example how to consume an external service and render the data into a dashlet. The service will be the World Cup in JSON! http://worldcup.sfg.io

This is a free REST API that returns the scores for the games played the current day or the scores for your favorite team.

JSON example:http://worldcup.sfg.io/matches/today

Remark: if the response is empty, there is no match scheduled for the day. Please try the other call: http://worldcup.sfg.io/matches/country?fifa_code=USA

[{

    "match_number": 45,

    "location": "Arena Pernambuco",

    "datetime": "2014-06-26T13:00:00.000-03:00",

    "status": "in progress",

    "home_team": {

        "country": "USA",

        "code": "USA",

        "goals": 0

    },

    "away_team": {

        "country": "Germany",

        "code": "GER",

        "goals": 0

    },

    "winner": null,

},

ETC...

It looks like another News dashlet? Not at all! If you ever tweak the out of the box News dashlet, you probably noticed that the data source was tough to change. Why? The reason is browser security: a default behavior prevents JavaScript cross-scripting. In short, JavaScript can only call services located on the same server unless they are over https.

Assuming you start with a clone of the News dashlet, the JQuery call to the World Cup service will return a 401 error (unauthorized). How to fix this? All you have is to make the call with a PHP script located on the same web server. The best practice will be extending Sugar REST services by adding a custom endpoint. Then, your JavaScript controller will call the service that will call the external World Cup service and then will receive the data in a JSON format and will send it back to the JS controller.The codeEdit: 30th June 12:45AM CET. blog & github had been updated. The web service call is made through a custom endpoint.

Now let’s have a look to the code.

The PHP script that calls the service. We implement two calls. The first to retrieve the games for the day, the other one to return all the games for a specific team. If the first call does not return any data, it is because no game is scheduled on that day.

You might already be familiar with the function call_service (it was already published on this blog). I added one parameter at the end that lets the user decide if he wants to receive a JSON string or the data in a structured PHP array.

And a last important remark: the name of the class and the php file has to be the same. I recommend to end this name with "Api"../custom/clients/base/api/WorldCup14Api.php

WorldCup14Api.php

<?php
if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');


class WorldCup14Api extends SugarApi
{
public function registerApiRest()
    {
return array(
'GetScoresEndpoint' => array(
//request type
'reqType' => 'GET',
//endpoint path
'path' => array('wc14', 'GetScores'),
//endpoint variables
'pathVars' => array(''),
//method to call
'method' => 'GetScores',
//short help string to be displayed in the help documentation
'shortHelp' => 'Get today\'s matches live scores',
//long help to be displayed in the help documentation
'longHelp' => '',
            ),

'GetMyTeamScoresEndpoint' => array(
//request type
'reqType' => 'GET',
//endpoint path
'path' => array('wc14', 'GetMyTeamScores'),
//endpoint variables
'pathVars' => array('', '', 'data'),
//method to call
'method' => 'GetMyTeamScores',
//short help string to be displayed in the help documentation
'shortHelp' => 'See my favorite team\'s scores',
//long help to be displayed in the help documentation
'longHelp' => '',
            ),
        );
    }

/**
     * Method to be used for my MyEndpoint/GetExample endpoint
*/
public function GetScores($api, $args)
    {
$service = '/matches/today';
return self::call_service('http://worldcup.sfg.io/'.$service, '', 'GET','', true, false, true);
    }

public function GetMyTeamScores($api, $args)
    {
if (!isset($args['fifa_code'])) {
return '[]';
exit;
  }
$service = '/matches/country';
return self::call_service('http://worldcup.sfg.io/'.$service, '', 'GET',$args, true, false, true);
    }

/**
  * Generic function to make cURL request.
  * @param $url - The URL route to use.
  * @param string $oauthtoken - The oauth token.
  * @param string $type - GET, POST, PUT, DELETE. Defaults to GET.
  * @param array $arguments - Endpoint arguments.
  * @param array $encodeData - Whether or not to JSON encode the data.
  * @param array $returnHeaders - Whether or not to return the headers.
  * @return mixed
*/
private function call_service(
$url,
$oauthtoken='',
$type='GET',
$arguments=array(),
$encodeData=true,
$returnHeaders=false,
$decode=true
)
{
$type = strtoupper($type);

if ($type == 'GET')
  {
$url .= "?" . http_build_query($arguments);
  }

$curl_request = curl_init($url);

if ($type == 'POST')
  {
curl_setopt($curl_request, CURLOPT_POST, 1);
  }
elseif ($type == 'PUT')
  {
curl_setopt($curl_request, CURLOPT_CUSTOMREQUEST, "PUT");
  }
elseif ($type == 'DELETE')
  {
curl_setopt($curl_request, CURLOPT_CUSTOMREQUEST, "DELETE");
  }

curl_setopt($curl_request, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
curl_setopt($curl_request, CURLOPT_HEADER, $returnHeaders);
curl_setopt($curl_request, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($curl_request, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl_request, CURLOPT_FOLLOWLOCATION, 0);

if (!empty($oauthtoken))
  {
$token = array("oauth-token: {$oauthtoken}");
curl_setopt($curl_request, CURLOPT_HTTPHEADER, $token);
  }

if (!empty($arguments) && $type !== 'GET')
  {
if ($encodeData)
   {
//encode the arguments as JSON
$arguments = json_encode($arguments);
   }
curl_setopt($curl_request, CURLOPT_POSTFIELDS, $arguments);
  }

$result = curl_exec($curl_request);

if ($returnHeaders)
  {
//set headers from response
list($headers, $content) = explode("\r\n\r\n", $result ,2);
foreach (explode("\r\n",$headers) as $header)
   {
header($header);
   }

//return the nonheader data
return trim($content);
  }

curl_close($curl_request);

//decode the response from JSON
$response = $result;
if ($decode) {
$response = json_decode($result);
  }
return $response;
}

}

?>

Now the dashlet:

1) The metadata./worldcup14/worldcup14.php

[gist https://gist.github.com/dd021284af9bf8378bb1 /]

2) The controller./worldcup14/worldcup14.js

Easy to understand. the loadData function builds the parameters to provide to the PHP script depending the settings (shall we show today's matches live scores or my favorite team's scores?)

Then, this mysterious loop modifies on the fly the date value in order to be shown according to the user's timezone, based on his settings in Sugar:

$.each(data, function (idx, obj) {

    obj.datetime = formatDateUser(obj.datetime);

});

Note these two functions:

- app.api.buildURL will build the url to call our custom web service.

- app.api.call will call our custom web service.

worldcup14.js

({
    plugins: ['Dashlet'],
initDashlet: function () {
if (this.meta.config) {
var team = this.settings.get("team") || "FRA";
this.settings.set("team", team);
var todayorteam = this.settings.get("todayorteam") || "team";
this.settings.set("todayorteam", todayorteam);
        }
//this.model.on("change:name", this.loadData, this);
    },

loadData: function (options) {
var team;
if (_.isUndefined(this.model)) {
return;
        }
        team = this.settings.get('team') || 'FRA';
        todayorteam = this.settings.get('todayorteam') || 'team';
        arg='';
        purl = parent.location.href;
  baseurl = purl.substring(0,purl.indexOf('#'));
if (todayorteam=='team') {
if ((typeof team != 'undefined')) arg='/wc14/GetMyTeamScores?fifa_code='+team;
  } else arg='/wc14/GetScores';

var self = this;
app.api.call('GET', app.api.buildURL(arg), null,
  {
success: function (data) {
if (this.disposed) {
return;
                }

formatDateUser = function(dstr){
if ((typeof(dstr)=='undefined') || (dstr=='')) return '';
var year  = dstr.substr(0,4);
var month = dstr.substr(5,2)-1;
var day   = dstr.substr(8,2);
var hour  = dstr.substr(11,2);
var min   = dstr.substr(14,2);
var tz    = parseInt(dstr.substr(23,3)); // hours
var date_game = Date.UTC(year, month, day, hour, min, 0, 0);
var current_tz = app.user.attributes.preferences.tz_offset_sec; // secs
var date_game_local = new Date();
date_game_local.setTime(date_game + current_tz*1000 - tz*3600*1000);
return(
//dstr + ', tz=' + tz + ', current_tz=' + current_tz + ', ' +
app.date(date_game_local).formatUser(true)+' '
+formatTimeUser(date_game_local.getUTCHours(),date_game_local.getUTCMinutes(),app.user.attributes.preferences.timepref)
     );
    },

formatTimeUser = function(h,m,pref){
var ampm = '';
if (pref.charAt(0)=='h') {//12
if (h>11) ampm='pm'; else ampm='am';
if (h>12) h-=12;
     }
     m = '' + m;
if (m.length==1) m = '0' + m;
return(h + ':' + m + ampm);
    }                    

$.each(data, function(idx, obj) {
obj.datetime = formatDateUser(obj.datetime);
obj.url = baseurl;
    });
_.extend(self, data);
self.render();
            },
  }
  );
    },

})

3) The Handlebar template./worldcup14/worldcup14.hbs

Remark: as the JSON data do not come with item names, the loop is built on "this" (good to know).

worldcup14.hbs

<div style="background-image: url('custom/clients/base/views/worldcup14/media/wc2014.png'); background-repeat: no-repeat; background-position: right top;">
{{#if this}}
{{#each this}}
{{#if this.home_team.country}}
    <div class="news-article">
        <p><img src="{{{this.url}}}custom/clients/base/views/worldcup14/media/flags/{{{this.home_team.code}}}.png" border="0" />
        <b>{{{this.home_team.country}}}</b> vs <b>{{{this.away_team.country}}}</b>
        <img src="{{{this.url}}}custom/clients/base/views/worldcup14/media/flags/{{{this.away_team.code}}}.png" border="0" />
        <br/>
{{{this.home_team.goals}}} - {{{this.away_team.goals}}} 
{{#if this.winner}}
        Winner: {{{this.winner}}}
{{/if}}
        <br/>
        match #{{{this.match_number}}} @ {{{this.location}}} - {{{this.datetime}}} ({{str "LBL_DASHLET_WC14_LOCALTIME"}})</p>
    </div>
{{/if}}
{{/each}}
{{else}}
    <div class="block-footer">{{str "LBL_NO_DATA_AVAILABLE"}}</div>
{{/if}}
</div>

4) The language files./language.worldcup14/en_us.lang.php

en_us.lang.php

<?php
$app_strings['LBL_DASHLET_WORLDCUP14'] = 'Football World Cup 2014';
$app_strings['LBL_DASHLET_WC14_DESC'] = 'Football World Cup results per day or team';
$app_strings['LBL_DASHLET_WC14_TODAYTEAM'] = 'What do you want to see?';
$app_strings['LBL_DASHLET_WC14_FAVTEAM'] = 'My Favorite team is';
$app_strings['LBL_DASHLET_WC14_LOCALTIME'] = 'locale time';

$app_list_strings['DASHLET_WC14_list']=array (
'today' => 'Today\'s matches live scores',
'team' => 'See my favorite team\'s scores',
);

5) And finally the manifest.php./manifest.php

manifest.php

<?php
$manifest = array (

'acceptable_sugar_versions' =>
array(
'regex_matches' => array(
'7\\.[0-9]\\.[0-9]$'
    ),
   ),  

'acceptable_sugar_flavors' =>
array(
0 => 'PRO',
1 => 'CORP',
2 => 'ENT',
3 => 'ULT',
    ),
'readme'=>'README.txt',
'key'=>'WC14',
'author' => 'Olivier Nepomiachty',
'description' => 'Football World Cup 2014 Dashlet',
'icon' => '',
'is_uninstallable' => true,
'name' => 'Football World Cup 2014',
'published_date' => '2014-06-29 08:00',
'type' => 'module',
'version' => '1.0.1.1',
'remove_tables' => false,
    ); 

$installdefs = array (
'id' => 'FWC2014',

'copy' =>
array (
array (
'from' => '<basepath>/worldcup14/',
'to' => 'custom/clients/base/views/worldcup14',
    ),
array (
'from' => '<basepath>/api.worldcup14/WorldCup14Api.php',
'to' => 'custom/clients/base/api/WorldCup14Api.php',
    ),
   ),  

'language' =>
array (
array (
'from' => '<basepath>/language.worldcup14/en_us.lang.php',
'to_module' => 'application',
'language' => 'en_us',
    ),   
array (
'from' => '<basepath>/language.worldcup14/fr_FR.lang.php',
'to_module' => 'application',
'language' => 'fr_FR',
    ),   
array (
'from' => '<basepath>/language.worldcup14/de_DE.lang.php',
'to_module' => 'application',
'language' => 'de_DE',
    ),   
array (
'from' => '<basepath>/language.worldcup14/ru_RU.lang.php',
'to_module' => 'application',
'language' => 'ru_RU',
    ),   
array (
'from' => '<basepath>/language.worldcup14/ru_RU.lang.php',
'to_module' => 'application',
'language' => 'sv_SE',
    ),   
   ) 

);

Installation

This section describes how to install the dashlet. It does not required any technical knowledge. However, please be aware that you will follow the exact steps.

1) Download the module (Zip file) from the Github repositoryhttps://github.com/tortugacrm/sugar7_football_wc2014_dashletClick on the 'Download ZIP' button located on the bottom right.

2) Log in your Sugar 7 instance as the admin.

3) Go to the menu located next to the profile picture on the top right. Click on ‘Admin’.

4) Scroll down to the 5th section called ‘Developer Tools’. Click on ’Module Loader’.

5) In the lower section of the page, click on the ‘Choose file’ button, select the wc14.zip archive included in the file you downloaded during step 1. Click on ‘Upload’.

6) Now the module is in the list below. Click on the install button.

7) Accept the license.

8) Wait for the browser to complete the installation. This step might take a couple of minutes, be patient. For some unknown reasons, some instances might finish the installation with a blank screen. It should not, but it's ok. The installation is finished when your browser stops loading the page.

9) On the top bar, click on ‘Administration

10) Scroll down to the 3rd section called ‘System’. Click on ’Repair’.

11) Click on ‘Quick Repair and Rebuild

12) Wait for the browser to complete the repair. The repair is finished when your browser stops loading the page.

13) Log out

14) Clear your browser cache

15) Log in as Jim

16) Edit the Dashboard

17) Select the area where you want to see you dashlet. Click on the Plus image

18) Chose the ‘Football’ dashlet in the list. You might want to preview it clicking on the preview button (with the eye).

19) Configure the dashlet: chose to view the games for the current day or your fav team’s games

20) Save the dashboard

21) Be a happy user of the Football dashlet!Acknowledgements

I would like to thank my colleagues: Abhijeet who found out the World Cup Json web site, Harald for reviewing the code, Alena, Anki, Evi for the translations.

Special thank to Jeff Bickart who suggested that I added a custom endpoint in order to provide the best practices in this tutorial.