Skip navigation
All Places > Developer > Blog > 2016 > August
2016

You may have heard that SugarCRM will be presenting a series of webinars on solution architecture for the Sugar platform, throughout this September and October.

Beginning on September 6th, Sugar University’s Solution Architect Webinar Series will present the following topics:

  • Introduction to Solution Architecture for Sugar
  • CRM Project Fundamentals
  • The Sugar Platform
  • Design
  • Integration
  • Sugar Implementation
  • Performance and Quality Assurance
  • Deployment
  • Security

Each webinar will be guided by a panel of experts from across SugarCRM, including some familiar faces from the Architecting Successful Projects panel at this year's UnCon

 

 

https://youtu.be/pxWSKXwn_KY

Just as at UnCon, you’ll have an opportunity to engage Sugar’s experts around your topics of interest.

Customizations that developers implement on the Sugar platform must operate within the unique configurations of the Sugar implementations they are deployed to. 

Attendees will gain deeper insight into the the workings and capabilities of the Sugar platform as the webinars delve into the tasks that Sugar solution architects undertake in order to design and deploy Sugar successfully.

This series covers the same concepts that are tested by the Sugar Solution Architect certification exam. So do not pass up this opportunity to broaden your horizons and reignite your Sugar developer journey!

Enroll in Solution Architect Webinar Series today!

Here is a guest post from Ivica Nedeljkovic from Intelestream which is one of our new SugarCRM Partners. In this post, Ivica explains how you can use Intelestream created Docker containers hosted on DockerHub to easily deploy Sugar.If I was asked to list the top five technologies in the last 5 years, Docker would certainly be on the list.The real advantage that Docker containers have over other server virtualization software is that Docker uses shared operating systems instead of trying to virtualize hardware. This takes less resources, is easier to boot, and faster to spin up instances.

 

Docker 1.12

Some performance issues were experienced with Docker containers on OS X and Windows and some security issues were also experienced because up until Docker version 1.12. Until Docker 1.12, the native OS X/Windows virtualization options were not used and 3rd party Linux virtual machines such as VirtualBox were used instead. For virtualization, Docker 1.12 for Windows uses Alpine Linux on top of a Hyper-V VM and Docker 1.12 for Mac OSX uses Alpine Linux on top of a xhyve Virtual Machine.

 

Docker 1.12  was released in July 2016. In addition to boosting performance it also simplifies the whole process of creating new containers, as you no longer need to use a docker-machine anymore to create virtual machines.

 

DockerHub

For this project, we store Docker container images on DockerHub. DockerHub allows developers to use pre-built images that are stored in the web repository. Additionally, every time a developer makes a change to a Docker config file, these changes can be pushed back to DockerHub to generate new automated builds. This simplifies the process if you want to deploy your container image on a number of servers. For example, you can set triggers and hooks. DockerHub allows you to have a number of public repositories and one private repository for free. If further private repositories are required then additional packages can be purchased.

 

We are using docker-compose to create different containers for web app (php 5.5, Apache), db server (Mysql 5.6), Elastic Search (required for the professional version). As a bonus we added a container with phpmyadmin that you might need to manage a mysql databases.

 

Launching Sugar using DockerHub Containers

 

Follow along to start using Sugar via DockerHub containers.

 

Step 1: Install Docker 1.12 or later

 

This is a recommendation and not a requirement. An older version of Docker or Docker Tools can be used, but in such a case you must use a docker-machine command to create a virtual machine for Docker to run inside and set environment variables. Should you choose to run one of these options instead of Docker 1.12 you will notice that the remaining steps are similar to those listed below.

 

Step 2: Clone intelestream/sugarcrm-docker.git repository

git clone git@github.com:intelestream/sugarcrm-docker.git

 

In case you already have some service listening on ports 80 (Apache), 3307 (MySql - custom port), 9200, 9300 (Elasticsearch) or 8181 (PhpMyAdmin) please disable them. Alternatively, you can also modify these ports in the docker-compose.yml file for all of these services, with the exception of the web server port. Sugar uses the file_get_contents command which does not work with ports (just for checking installation requirements).

 

Step 3: Run docker-compose to start images

Using the console, navigate to the folder that has been created and type:
docker-compose up
This command will download all images from the DockerHub and start building containers. Spinning up containers for the first time will be fairly slow but as soon as all images are downloaded then the process will speed up considerably.Once everything is completed, you should have containers with Apache, MySql, and Elasticsearch running. To stop/pause them, use “CTRL+C”

 

At this point all services should be running. Our public web folder is www. To check if everything is running as expected you can test by creating a test.php file under there and accessing it within your browser:http://localhost/test.php

 

Step 4: Place Sugar code and run Sugar installer

 

Copy Sugar application code to the www directory and place it in a directory named sugarcrm. The system is designed for developers to be able to have more than one instance of Sugar. All you need is multiple individual databases.

 

Open http://localhost/sugarcrm (or whatever name you decided to assign to the above folder) and enter the following in the Sugar installation wizard:Host: mysql_crm (The host needs to be mysql_crm, it will be resolved by docker.  See docker-compose.yml)DB Name: sugarcrmDB User: sugarcrmDB Password: sugarcrmroot user: rootroot password: sugarcrmElasticsearch Host: elasticsearch_crm (See host again in docker-compose.yml)Elasticsearch Port: 9200

 

Finished!

 

Sugar is now up and running! Happy coding!

 

Below are some tips for working with this server set-up.

 

Accessing the MySQL database

phpMyAdmin is installed if you need to administer your MySQL database. You can access phpMyAdmin by opening the following url:http://localhost:8181

 

Accessing a command line for a container

 

In case you need to access any containers:

docker ps 
This lists all of the containers and their IDs.

 

In the command below replace container_id with the container id you want to connect to based on the results of the previous step:

docker exec -i -t container_id /bin/bash

 

Setting up Cron from command line

 

First, retrieve a command line for your Apache server instance using the previous steps. Then to set up a cronjob, run the following line:

cd /app/www/crm; /usr/local/bin/php -f cron.php > /dev/null 2>&1
And start Cron service:
service cron start
If you shutdown and remove container, you will need to set up the cronjob again for new instances.

 

Additional Considerations

  • When you repair and rebuild your caches, you will notice a decrease of around 40-50% in terms of speed in comparison with an instance of Sugar running directly on the host, because many files need to be written and synced between systems. If you do not do this action too often you will not notice a big difference between performance
  • If you do not need all containers, you can comment those you do not need in the docker-compose.yml file. For example, if you do not want to use both the web server and MySQL server because you can use them from your local host, then you can simply have only Elasticsearch running in a container so you do not need to install it. Even simpler, you could just download the Elasticsearch image from the official repository on DockerHub and start it!
At SugarCRM, we have been accelerating the rate at which we share technology with the Sugar Developer community. Back in June at UnCon, we shared more open source code examples and tools than ever before. In April, we announced Sucrose Charts and the Sugar REST Harness. I am pleased to announce that we have open sourced three more projects under the Apache-2 license!

 

SugarCRM XHProf Viewer

 

The SugarCRM XHProf viewer is a visual performance analysis tool designed specifically for Sugar 7.7.0 and later. This project was created by the Sugar Engineering's Performance team. This project extends the original xhprof viewer created by Facebook. Our Viewer tracks PHP memory usage and execution time (just like the original) but we add an improved UI and detailed query logging. The Viewer logs both SQL and Elasticsearch queries including timing and stack traces.

 

Sugar Repairs Module

 

The Sugar Repairs Module is a ML package created by the Sugar Support team. Sugar Support commonly uses this module to help assist with repairing common issues that the Support team encounters in Sugar instances. This package can only be deployed on Sugar On-Site (or local development) instances because it does not pass package scanner. We recommend that you test repairs on a clone of your production instance before trying to use it in production. Ultimately, use of this tool is at your own risk!

 

Sugar REST PHP Client

 

The Sugar REST PHP client is another tool created by the Sugar Support team. It is a simple and intuitive PHP library for accessing a Sugar 7's REST v10 API. It provides an object oriented model for accessing data from a Sugar system. This is a great tool for accelerating any Sugar integration project.

 

Learn more in coming weeks!

 

I look forward to sharing more details about each of these new projects in coming weeks. Watch this space for blog posts from the developers behind these open source projects.

Here is a guest post from Sugar Developer Francesca Shiekh who works for our customer Wolfram Research. She also happens to be one of the most active developers in the Sugar Developer Community! In this post, Francesca and Wolfram Research Systems Administrator Trey Mitchell share how you can customize Sugar to use an alternative PDF generator for the creation of PDF documents in Sugar.

 

Creating better PDFs with CSS


 

We wanted a custom Contracts module that allowed specific styling to be applied to all Contract PDFs.  We determined that we could use CSS to apply a consistent style to any and all PDF documents associated with this new custom Contracts module. But we could not achieve this with SugarPDF due to the complexity of our CSS. Also overriding sugarpdf.pfdmanager.php was not an option as it is an extension of TCPDF and TCPDF has limited CSS support.

 

It was therefore necessary for use to look at alternatives that could better handle CSS.  We found a promising open source tool called wkhtmltopdf that uses WebKit.  WebKit should be familiar because it is the same layout engine used in Safari and Google Chrome, and is therefore very accurate in rendering CSS.

 

We still wanted to leverage the PDF Manager module to create and maintain our PDFs. We planned to develop our Contract templates in HTML with our own custom CSS and then copy them into the HTML editor that was already available in PDF Manager.

 

Read more below to find out how we made it work.

 

Step 1: Create a custom PDF rowaction

 

A custom field type called wpdfaction provides the rowaction needed for the dropdown that contains all the active PDF Manager templates for the current module. This new rowaction is dynamically included in the buttons section of your module's record view much like the out of the box pdfaction.custom/modules/*/clients/base/views/record/record.php

 

 


              array (

                'type' => 'wpdfaction',

                'name' => 'download-pdf',

                'label' => 'LBL_PDF_VIEW',

                'action' => 'download',

                'acl_action' => 'view',

              ),

 

The field type wpdfaction is a slight modification of the clients/base/fields/pdfaction field where the template is unchanged and the JavaScript is modified to call a custom API that in turn generates the PDF to be downloaded.custom/clients/base/fields/wpdfaction/wpdfaction.js

 

[code language="js"]

 

/**

* @extends View.Fields.Base.RowactionField

*/


({

  extendsFrom: 'RowactionField',

  events: {

    'click [data-action=link]': 'linkClicked',

    'click [data-action=download]': 'downloadClicked',

  },

  /**

   * PDF Template collection.

   *

   * @type {Data.BeanCollection}

   */


   templateCollection: null,

   /**

   * Visibility property for available template links.

   *

   * @property {Boolean}

   */


  fetchCalled: false,

  /**

   * {@inheritDoc}

   * Create PDF Template collection in order to get available template list.

   */


  initialize: function(options) {

    this._super('initialize', [options]);

    this.templateCollection = app.data.createBeanCollection('PdfManager');

    this._fetchTemplate();

  },

  /**

   * {@inheritDoc}

   * @private

   */


  _render: function() {

    this._super('_render');

  },

  /**

   * Define proper filter for PDF template list.

   * Fetch the collection to get available template list.

   * @private

   */


  _fetchTemplate: function() {

    this.fetchCalled = true;

    var collection = this.templateCollection;

    collection.filterDef = {'$and': [{

      'base_module': this.module

    }, {

      'published': 'yes'

    }]};

      collection.fetch();

  },

  /**

   * Build download link url.

   *

   * @param {String} templateId PDF Template id.

   * @return {string} Link url.

   * @private

   */


  _buildDownloadLink: function(templateId) {

      var url = app.api.buildURL(this.module+'/'+this.model.id+'/'+'generateWPDF/'+templateId);

      return url;

  },

  /**

   * Handle the button click event.

   * Stop event propagation in order to keep the dropdown box.

   *

   * @param {Event} evt Mouse event.

   */


  linkClicked: function(evt) {

    evt.preventDefault();

    evt.stopPropagation();

    if (this.templateCollection.dataFetched) {

      this.fetchCalled = !this.fetchCalled;

    } else {

      this._fetchTemplate();

    }

    this.render();

  },

  /**

   * Handles download pdf link.

   * @param {Event} evt The `click` event.

   */


  downloadClicked: function(evt) {

    var templateId = this.$(evt.currentTarget).data('id');

    this._triggerDownload(this._buildDownloadLink(templateId));

  },

  /**

   * do the actual downloading

   * @param {String} uri The location of the cached file.

   **/


  startDownload: function(uri) {

    app.api.fileDownload(uri, {

      error: function(e) {

        app.error.handleHttpError(e, {});

      }

    }, {iframe: this.$el});

  },

  /**

   * Download the file.

   *

   * @param {String} url The url for the file download API.

   * @protected

   */


  _triggerDownload: function(url) {

    var self = this;

    app.api.call('GET', url, null, {

      success: function(o){

        self.startDownload(o);

      },

      error: function (e){

        app.error.handleHttpError(e, {});

      },

    });

  },

  /**

   * {@inheritDoc}

   * Bind listener for template collection.

   */


  bindDataChange: function() {

    this.templateCollection.on('reset', this.render, this);

    this._super('bindDataChange');

  },

  /**

   * {@inheritDoc}

   * Dispose safe for templateCollection listeners.

   */


  unbindData: function() {

    this.templateCollection.off(null, null, this);

    this.templateCollection = null;

    this._super('unbindData');

  },

  /**

   * {@inheritDoc}

   * Check additional access for PdfManager Module.

   */


  hasAccess: function() {

    var pdfAccess = app.acl.hasAccess('view', 'PdfManager');

    return pdfAccess && this._super('hasAccess');

  }

})

 

 

Step 2: Create a custom API that generates the new PDFs

 

The custom API is what generates the new PDFs from HTML stored in the PDF Manager module.

 

You can put the following REST API implementation in custom/clients/base/api/generateWPDFApi.php for use in multiple modules or custom/modules/<module>/clients/base/api/generateWPDFApi.php for use in a specific module only.

 

We have to include SugarpdfHelper because it is required by PdfManagerHelper to parse the SugarBean fields needed for SugarSmarty to replace our field variables with data from the record.

 

The outputWPdf function can be changed to use your favorite PDF generator. We were able to try a few different implementations before we settled on the ease of use of wkhtmltopdf.

 

The final PDF is stored in Sugar's cache directory and ultimately downloaded using app.api.fileDownload() in the client.

If testing in Google Chrome, you need to disable the PDF Viewer to ensure that it is downloaded correctly.  To do this go to chrome://plugins and find and disable "Chrome PDF Viewer" in the list.

 

 


<php class generateWPDFApi extends SugarApi {   public function registerApiRest() {     return array(       'generateWPDF' => array(

        'reqType' => 'GET',

        'path' => array('<module>','?','generateWPDF','?'),

        'pathVars' => array('module','record','','pdf_template_id'),

        'method' => 'generateWPDF',

        'shortHelp' => 'CustomWR: Generate PDF file from PdfManager template with specified ID',

        'longHelp' => '',

      ),

    );

  } 

  function generateWPDF($api, $args)

  {

    require_once 'include/Sugarpdf/SugarpdfHelper.php'; //needed by PdfManagerHelper

    require_once 'modules/PdfManager/PdfManagerHelper.php';

    global $sugar_config;

    $this->requireArgs($args,array('module','record','pdf_template_id'));

    $this->_initSmartyInstance();

    // settings for disable smarty php tags

    $this->ss->security_settings['PHP_TAGS'] = false;

    $this->ss->security = true;

    if (defined('SUGAR_SHADOW_PATH')) {

      $this->ss->secure_dir[] = SUGAR_SHADOW_PATH;

    } 

    $pdfTemplate = BeanFactory::retrieveBean('PdfManager' , $args['pdf_template_id']);

    $this->bean = BeanFactory::retrieveBean($args['module'],$args['record']);

    $this->pdfFilename = $pdfTemplate->name . '_' . $this->bean->name;

    $this->templateLocation = $this->buildTemplateFile($pdfTemplate);

    $fields = PdfManagerHelper::parseBeanFields($this->bean, true);

    //assign values to merge fields

    $this->ss->assign('fields', $fields);

    //write the pdf from html into a cached file for downloading

    return $this->outputWPdf();

  }

  private function outputWPdf()

  {

    //using wkthmltopdf as our converter of choice.

    global $sugar_config;

    $host = 'https://'.parse_url($sugar_config['site_url'], PHP_URL_HOST);

    if(!empty($this->templateLocation)){

      //get the HTML

      $html = $this->ss->fetch($this->templateLocation);

      //add the css to the HTML and any other headers you wish to add

      $css_url = '<YOUR CSS URL HERE>';

      $html = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"><head><link rel="stylesheet" type="text/css" href="'.$css_url.'"/></head>'.$html.'</html>';

      // Run wkhtmltopdf

      $descriptorspec = array(

        0 => array('pipe', 'r'), // stdin

        1 => array('pipe', 'w'), // stdout

        2 => array('pipe', 'w'), // stderr

      );

      $page_size = 'Letter';

      // print-media-type option is essential to proper pagination during conversion so page-break-xxxx css property is respected

      // -q = quiet

      // - -  allows for the pipes to be used

      $options =  " --print-media-type "; //gets proper pagination

      $options .= " -q "; //quiet

      $options .= " --footer-spacing 5 "; //space between footer and text

      $options .= " --header-spacing 5 "; //space between header and text

      $options .= " --page-size ". $page_size;

      $cmd = '/usr/local/bin/wkhtmltopdf ' . $options . ' - - ';

      $process = proc_open( $cmd  , $descriptorspec, $pipes);

      if(is_resource($process)){

        // Send the HTML on stdin

        fwrite($pipes[0], $html);

        fclose($pipes[0]);

        // Read the outputs

        $this->pdf = stream_get_contents($pipes[1]);

        // Any errors that may arise

        $errors = stream_get_contents($pipes[2]);

        // Close all pipes and close the process

        fclose($pipes[1]);

        $return_value = proc_close($process);

        // Check for errors and Output the results

        if ($errors) {

          throw new SugarApiException('PDF generation failed:' . $errors);

        }else {

          //stamp filename with user and date stamp

          $filenamestamp = '';

          if(isset($current_user)){

            $filenamestamp .= '_'.$current_user->user_name;

          }

          $filenamestamp .= '_'.time();

          //avoid special characters in the filename

          $cr = array(' ',"\r", "\n","/","-");

          $filename = str_replace($cr, '_', $this->pdfFilename. $filenamestamp.'.pdf');

          //create the file in sugar cache

          $cachefile = sugar_cached('tmp/').$filename;

          $fp = sugar_fopen($cachefile, 'w');

          fwrite($fp, $this->pdf);

          fclose($fp);

          return($cachefile);

        }

      }else{

        throw new SugarApiException('PDF generation failed: wkhtmlpdf resource error ');

      }

    }else{

      throw new SugarApiException('PDF generation failed: No Template Location ');

    }

  }

  private function buildTemplateFile($pdfTemplate, $previewMode = FALSE)

  {

    if (!empty($pdfTemplate)) {

      if ( ! file_exists(sugar_cached('modules/PdfManager/tpls')) ) {

        mkdir_recursive(sugar_cached('modules/PdfManager/tpls'));

      }

      $tpl_filename = sugar_cached('modules/PdfManager/tpls/' . $pdfTemplate->id . '.tpl');

      $pdfTemplate->body_html = from_html($pdfTemplate->body_html);

      sugar_file_put_contents($tpl_filename, $pdfTemplate->body_html);

      return $tpl_filename;

    }

    return '';

  }

  /**

   * Init the Sugar_Smarty object.

   */


  private function _initSmartyInstance()

  {

    if ( !($this->ss instanceof Sugar_Smarty) ) {

      $this->ss = new Sugar_Smarty();

      $this->ss->security = true;

      if (defined('SUGAR_SHADOW_PATH')) {

        $this->ss->secure_dir[] = SUGAR_SHADOW_PATH;

      }

      $this->ss->assign('MOD', $GLOBALS['mod_strings']);

      $this->ss->assign('APP', $GLOBALS['app_strings']);

    }

  }

}

 

Wkhtmltopdf installation and setup

 

As stated above, we needed an HTML to PDF solution that would allow us to compose documents using a CSS and would render them correctly in PDF and chose wkhtmltopdf for its precision and ease of use.

 

We provide here a walk through for a simple manual installation of wkhtmltopdf on CentOS. Steps on other Red Hat based distributions should be identical.

 

On Debian derivatives dependency package names may differ, and you will use apt-get instead of yum to install them.

 

You will need root access or sudo to do this.

 

In the following writing ,commands requiring root access will be preceded with an asterisk (*).

 

First you need to ensure you have the proper wkhtmltopdf dependencies:

*[root@sugarcrm-test ~]# yum -y install zlib fontconfig freetype libX11 libXext libXrender

Download the latest copy of wkhtmltopdf from the project's download page.

 

We will be using the 64-bit version here:

[root@sugarcrm-test ~]# wget http://download.gna.org/wkhtmltopdf/0.12/0.12.3/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz

It's also advisable to verify the integrity of the file you downloaded:

[root@sugarcrm-test ~]# wget http://download.gna.org/wkhtmltopdf/0.12/0.12.3/SHA256SUMS

[root@sugarcrm-test ~]# sha256sum -c SHA256SUMS

You will see a lot of errors because we didn't download all the files listed in SHA256SUMS. What you are looking to verify is an OK next to the file you downloaded like this:

wkhtmltox-0.12.3_linux-generic-amd64.tar.xz: OK

Next we want to extract the archive and place the files in the proper locations.

 

This next command will do a few things

 

- Extract the archive's contents into /usr/local

 

- Print all the files that were created.

 

- write a file called wkhtmltox-files with a list of files that were extracted.

 

This is useful if you wish to remove or otherwise cleanup the install later.

*[root@sugarcrm-test ~]# tar xvJf wkhtmltox-0.12.3_linux-generic-amd64.tar.xz --strip-components=1 -C /usr/local|grep -v \/$|sed -e 's/^wkhtmltox/\/usr\/local/'|tee wkhtmtox-files

All permissions should be correct out of the box but if you wish to verify you can run the following command and check against the output below.

[root@sugarcrm-test ~]# for file in `cat wkhtmtox-files`;do ls -l $file;done

-rwxr-xr-x 1 root root 45072792 Jan 20 02:32 /usr/local/lib/libwkhtmltox.so.0.12.3

lrwxrwxrwx 1 root root 22 Jul 15 15:56 /usr/local/lib/libwkhtmltox.so.0 -&amp;amp;gt; libwkhtmltox.so.0.12.3

lrwxrwxrwx 1 root root 22 Jul 15 15:56 /usr/local/lib/libwkhtmltox.so.0.12 -&amp;amp;gt; libwkhtmltox.so.0.12.3

lrwxrwxrwx 1 root root 22 Jul 15 15:56 /usr/local/lib/libwkhtmltox.so -&amp;amp;gt; libwkhtmltox.so.0.12.3

-rw-r--r-- 1 root root 2546 Jan 20 02:34 /usr/local/share/man/man1/wkhtmltoimage.1.gz

-rw-r--r-- 1 root root 6774 Jan 20 02:33 /usr/local/share/man/man1/wkhtmltopdf.1.gz

-rw-r--r-- 1 root root 3980 Jul  7  2015 /usr/local/include/wkhtmltox/pdf.h

-rw-r--r-- 1 root root 911 Jul  7  2015 /usr/local/include/wkhtmltox/dllend.inc

-rw-r--r-- 1 root root 1475 Jul  7  2015 /usr/local/include/wkhtmltox/dllbegin.inc

-rw-r--r-- 1 root root 3407 Jul  7  2015 /usr/local/include/wkhtmltox/image.h

-rwxr-xr-x 1 root root 39666344 Jan 20 02:34 /usr/local/bin/wkhtmltoimage

-rwxr-xr-x 1 root root 39745960 Jan 20 02:33 /usr/local/bin/wkhtmltopdf

That should be it. You can verify the binary is working from command line:

[root@sugarcrm-test ~]# wkhtmltopdf http://google.com google.pdf

If you ever wish to remove the application you can do so using the file we generated upon extraction:

*[root@sugarcrm-test ~]# for file in `cat wkhtmtox-files`;do rm -I $file;done

 

There will be a stray empty directory to remove as well:

*[root@sugarcrm-test ~]# rmdir /usr/local/include/wkhtmltox/

Three UnCon General Sessions recorded and now available!

 

The recordings for the UnCon general sessions are now available in the UnCon space of the Sugar Community. Visit the UnCon Archive to get access to the below videos as well as slides, pictures, and links to code examples for these general sessions as well as our breakout sessions.

 

Tuesday, June 14th 2016

Sugar Developer Ecosystem Overview (26 minutes)Sugar Platform Update for Developers (26 minutes)

 

Wednesday, June 15th 2016

Architecting Successful Projects Panel (49 minutes)