Magento – Inchoo http://inchoo.net Magento Design and Magento Development Professionals - Inchoo Fri, 27 May 2016 13:14:00 +0000 en-US hourly 1 https://wordpress.org/?v=4.4.2 A simple frontend workflow for Gulp http://inchoo.net/magento/magento-frontend/a-simple-frontend-workflow-for-gulp/ http://inchoo.net/magento/magento-frontend/a-simple-frontend-workflow-for-gulp/#respond Wed, 18 May 2016 11:56:37 +0000 http://inchoo.net/?p=26802 What is Gulp? Gulp is a task/build runner which uses Node.js for web development. In this tutorial, I will not explain how to create your Gulp plugin but I will show you how you can create a simple, basic, customized build process for your frontend workflow to ease your job. Gulp is often used for automated front...

The post A simple frontend workflow for Gulp appeared first on Inchoo.

]]>
What is Gulp?

Gulp is a task/build runner which uses Node.js for web development. In this tutorial, I will not explain how to create your Gulp plugin but I will show you how you can create a simple, basic, customized build process for your frontend workflow to ease your job.

Gulp is often used for automated front end tasks like:

  • Compiling preprocessors like Sass or Less
  • Optimizing assets like images, CSS and Javascripts
  • Including browsers prefixes with Autoprefixer
  • Injecting CSS in browser without reloading page whenever file is saved
  • Spinning up a web server

This is not, of course, a comprehensive list of things Gulp can do. There are thousands of plugins which you can use or you can build your own.

Step 1: Install Node.js

Because Gulp is built on Node.js (Javascript runtime built on Chrome’s V8 Javascript engine), first we need to install Node.js.

Node.js can be downloaded for Windows, Mac and Linux at nodejs.org/download/. You can also install it from the command line using the package manager.

Once installed, open a command prompt and enter:

$ node-v

This will show a Node.js version number if it is a correctly installed.

Step 2: Install Gulp

Now we can install Gulp using by using the following command in command line:

$ sudo npm install gulp -g

Note: Mac users need the sudo keyword. “$” in the code above just symbolize the command prompt. You don’t need to enter it.

The “-g” option in the command line tells to install Gulp globally, not just for the specific project. It allows you to use Gulp command anywhere on your system.

Now, we can make a project that uses Gulp.

Step 3: Set Up a Gulp Project

Go to

root/skin/frontend/design_package/default/

In this folder, we will create a Gulp config file and install all Gulps dependencies. Enter the command below in command line:

$ npm init

The “npm init” command creates a package.json file in which are stored information like dependencies, project name etc.

Once the package.json file is created, we can install Gulp and other plugins used in this tutorial.

Step 4: Installing Gulp

Install Gulp locally in the same folder. This will create “node_modules” folder where are all our dependencies files located. Enter the command in command line:

$ npm install gulp --save-dev

Great, now we can move on to install Gulp-Sass plugin.

Step 5: Installing Gulp-sass

Gulp Sass is a libsass version of Gulp. Libsass is a much faster in compiling sass files than Ruby.

Enter the command in command line:

$ npm install gulp-sass --save-dev

Great, move on to the next dependency, Gulp Source Maps.

Step 6: Install Gulp Source Maps

Gulp Source Maps is useful for debugging. You can see in which sass file are styles located for the inspected element in the browsers Inspector tool.

Enter the command in command line:

$ npm install gulp-sourcemaps --save-dev

Step 7: Install Autoprefixer

Autoprefixer for Gulp helps us to write CSS without vendor prefixes. It will automatically search if there is a need to add vendor prefix for some newly CSS properties. Autoprefixer is pulling data from caniuse.com site and adds prefixes if there is a need for it.

In the command line tool, enter command:

$ npm install gulp-autoprefixer --save-dev

Step 8: Install Browser Sync

BrowserSync is one of my favorites. It can reload a browser page after file saving, inject CSS without reloading page, sync browser page screen and your browsing actions through devices. Very handy.

Write in the command line:

$ npm install gulp-autoprefixer --save-dev

Step 9: Create a Gulp config file

Create a new file gulpfile.js in the same folder. This will be the main configuration file for the Gulp where we write our tasks.

Open up gulpfile.js and write:

var gulp = require('gulp');
var sass = require('gulp-sass');
var sourcemaps = require('gulp-sourcemaps');
var autoprefixer = require('gulp-autoprefixer');
var browserSync = require('browser-sync').create();

We need to require all dependencies we have installed. The requirement statement tells Node to look into the node_modules folder for a package. Once the package is found, we assign its contents to the variable.

Next step is to create two variables used by GulpSass plugin for compiling sass files and we will assign to them a path for our Sass files and path for our compiled CSS file.

Write:

var input = './scss/**/*.scss';
var output = './css';

We can now begin to write a Gulp sass task. Add a code below:

gulp.task('sass', function () {
	return gulp.src(input)
	.pipe(sourcemaps.init())
	.pipe(sass(errLogToConsole: true,outputStyle: 'compressed'))
	.pipe(autoprefixer(browsers: ['last 2 versions', '> 5%', 'Firefox ESR']))
	.pipe(sourcemaps.write())
	.pipe(gulp.dest(output))
	.pipe(browserSync.stream());
});

Add a watcher task for watching Scss file changes and BrowserSync settings. Add a code below:

// Watch files for change and set Browser Sync
gulp.task('watch', function() {
	// BrowserSync settings
	browserSync.init({
	proxy: "mydomain.loc",
	files: "./css/styles.css"
});
 
// Scss file watcher
gulp.watch(input, ['sass'])
	.on('change', function(event){
		console.log('File' + event.path + ' was ' + event.type + ', running tasks...')
	});
});

Finally we create our default/main task which will run all above task. Add a code below:

// Default task
gulp.task('default', ['sass', 'watch']);

Our final gulpfile.js should look like this:

var gulp = require('gulp');
var sass = require('gulp-sass');
var sourcemaps = require('gulp-sourcemaps');
var autoprefixer = require('gulp-autoprefixer');
var browserSync = require('browser-sync').create();
 
var input = './scss/**/*.scss';
var output = './css';
 
gulp.task('sass', function () {
	return gulp.src(input)
	.pipe(sourcemaps.init())
	.pipe(sass(errLogToConsole: true,outputStyle: 'compressed'))
	.pipe(autoprefixer(browsers: ['last 2 versions', '> 5%', 'Firefox ESR']))
	.pipe(sourcemaps.write())
	.pipe(gulp.dest(output))
	.pipe(browserSync.stream());
});
 
// Watch files for change and set Browser Sync
gulp.task('watch', function() {
	// BrowserSync settings
	browserSync.init({
	proxy: "mydomain.loc",
	files: "./css/styles.css"
});
 
// Scss file watcher
gulp.watch(input, ['sass'])
	.on('change', function(event){
		console.log('File' + event.path + ' was ' + event.type + ', running tasks...')
	});
});
 
// Default task
gulp.task('default', ['sass', 'watch']);

To run above tasks, enter

$ gulp default

command in the command line.

Hope this will help someone to automate frontend workflow with Gulp. Thanks for reading!

The post A simple frontend workflow for Gulp appeared first on Inchoo.

]]>
http://inchoo.net/magento/magento-frontend/a-simple-frontend-workflow-for-gulp/feed/ 0
Enabling Multi-part MIME Emails in Magento http://inchoo.net/magento/enabling-multi-part-mime-emails-in-magento/ http://inchoo.net/magento/enabling-multi-part-mime-emails-in-magento/#comments Wed, 04 May 2016 11:47:23 +0000 http://inchoo.net/?p=26670 This article will explain steps needed for altering Magento transactional emails in order to support Multi-part mime emails. Which means that those emails will support both Text and HTMl versions of email. Core for handling multipart data is already implemented in Zend_Mime module, so there is no need for writing code that will handle necessary...

The post Enabling Multi-part MIME Emails in Magento appeared first on Inchoo.

]]>
This article will explain steps needed for altering Magento transactional emails in order to support Multi-part mime emails. Which means that those emails will support both Text and HTMl versions of email. Core for handling multipart data is already implemented in Zend_Mime module, so there is no need for writing code that will handle necessary headers and boundary for multipart implementation.

So let’s get to it!

Main part that is needed for setting up of multipart emails is located in Mage_Core_Model_Email_Queue->send() function in line 225:

if ($parameters->getIsPlain()) {
                    $mailer->setBodyText($message->getMessageBody());
                } else {
                    $mailer->setBodyHTML($message->getMessageBody());
                }

If we add body text along with body html, Zend_Mime will handle the rest and add needed headers and boundary for multipart support. So the rewrite would go like this:

public function send()
    {
        /** @var $collection Mage_Core_Model_Resource_Email_Queue_Collection */
        $collection = Mage::getModel('core/email_queue')->getCollection()
            ->addOnlyForSendingFilter()
            ->setPageSize(self::MESSAGES_LIMIT_PER_CRON_RUN)
            ->setCurPage(1)
            ->load();
 
        ini_set('SMTP', Mage::getStoreConfig('system/smtp/host'));
        ini_set('smtp_port', Mage::getStoreConfig('system/smtp/port'));
 
        /** @var $message Mage_Core_Model_Email_Queue */
        foreach ($collection as $message) {
            if ($message->getId()) {
                $parameters = new Varien_Object($message->getMessageParameters());
                if ($parameters->getReturnPathEmail() !== null) {
                    $mailTransport = new Zend_Mail_Transport_Sendmail("-f" . $parameters->getReturnPathEmail());
                    Zend_Mail::setDefaultTransport($mailTransport);
                }
 
                $mailer = new Zend_Mail('utf-8');
                foreach ($message->getRecipients() as $recipient) {
                    list($email, $name, $type) = $recipient;
                    switch ($type) {
                        case self::EMAIL_TYPE_BCC:
                            $mailer->addBcc($email, '=?utf-8?B?' . base64_encode($name) . '?=');
                            break;
                        case self::EMAIL_TYPE_TO:
                        case self::EMAIL_TYPE_CC:
                        default:
                            $mailer->addTo($email, '=?utf-8?B?' . base64_encode($name) . '?=');
                            break;
                    }
                }
 
                if ($parameters->getIsPlain()) {
                    $mailer->setBodyText($message->getMessageBody());
                } else {
                    /** INCHOO EDIT START */
                    $mailer->setBodyText($message->getMessageBodyPlain());
                    /** INCHOO EDIT END */
                    $mailer->setBodyHTML($message->getMessageBody());
                }
 
                $mailer->setSubject('=?utf-8?B?' . base64_encode($parameters->getSubject()) . '?=');
                $mailer->setFrom($parameters->getFromEmail(), $parameters->getFromName());
                if ($parameters->getReplyTo() !== null) {
                    $mailer->setReplyTo($parameters->getReplyTo());
                }
                if ($parameters->getReturnTo() !== null) {
                    $mailer->setReturnPath($parameters->getReturnTo());
                }
 
                try {
                    $mailer->send();
                } catch (Exception $e) {
                    Mage::logException($e);
                }
 
                unset($mailer);
                $message->setProcessedAt(Varien_Date::formatDate(true));
                $message->save();
            }
        }
 
        return $this;
    }

Since we don’t have support for message_body_plain attribute added here, we will need to add it to tables and implement the logic for handling this new attribute.

First, lets extend needed classes to add text area for our new attribute when viewing transactional email:
Extend Mage_Adminhtml_Block_System_Email_Template_Edit_Form by overwriting _prepareForm() function to look like this:

protected function _prepareForm()
    {
 
        parent::_prepareForm();
 
        $form = $this->getForm();
 
        $fieldset = $form->getElement('base_fieldset');
 
        $templateId = $this->getEmailTemplate()->getId();
 
        $fieldset->addField('template_text_plain', 'textarea', array(
            'name'=>'template_text_plain',
            'label' => Mage::helper('adminhtml')->__('Template Content - Plain'),
            'title' => Mage::helper('adminhtml')->__('Template Content - Plain'),
            'required' => false,
            'style' => 'height:24em;',
        ));
 
        if ($templateId) {
            $form->addValues($this->getEmailTemplate()->getData());
        }
 
        if ($values = Mage::getSingleton('adminhtml/session')->getData('email_template_form_data', true)) {
            $form->setValues($values);
        }
 
        $this->setForm($form);
 
        return $this;
    }

Now we have added the new field for our plaintext version of email. Next we will add controller support for saving new attribute in database, by extending
Mage_Adminhtml_System_Email_TemplateController class and rewriting saveAction():

public function saveAction()
    {
        $request = $this->getRequest();
        $id = $this->getRequest()->getParam('id');
 
        $template = $this->_initTemplate('id');
        if (!$template->getId() && $id) {
            Mage::getSingleton('adminhtml/session')->addError(
                Mage::helper('adminhtml')->__('This Email template no longer exists.')
            );
            $this->_redirect('*/*/');
            return;
        }
 
        try {
            /** INCHOO EDIT */
            $template->setTemplateSubject($request->getParam('template_subject'))
                ->setTemplateCode($request->getParam('template_code'))
                ->setTemplateText($request->getParam('template_text'))
                ->setTemplateTextPlain($request->getParam('template_text_plain'))
                ->setTemplateStyles($request->getParam('template_styles'))
                ->setModifiedAt(Mage::getSingleton('core/date')->gmtDate())
                ->setOrigTemplateCode($request->getParam('orig_template_code'))
                ->setOrigTemplateVariables($request->getParam('orig_template_variables'));
            /** INCHOO EDIT */
 
            if (!$template->getId()) {
                $template->setAddedAt(Mage::getSingleton('core/date')->gmtDate());
                $template->setTemplateType(Mage_Core_Model_Email_Template::TYPE_HTML);
            }
 
            if ($request->getParam('_change_type_flag')) {
                $template->setTemplateType(Mage_Core_Model_Email_Template::TYPE_TEXT);
                $template->setTemplateStyles('');
            }
 
            $template->save();
            Mage::getSingleton('adminhtml/session')->setFormData(false);
            Mage::getSingleton('adminhtml/session')->addSuccess(
                Mage::helper('adminhtml')->__('The email template has been saved.')
            );
            $this->_redirect('*/*');
        }
        catch (Exception $e) {
            Mage::getSingleton('adminhtml/session')->setData(
                'email_template_form_data',
                $this->getRequest()->getParams()
            );
            Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
            $this->_forward('new');
        }
    }

 After adding controller support for saving the attribute, we need to add additional database columns that will contain plaintext email version along with original HTML/plaintext version of email. We will add template_text_plain column to core_email_template table to contain plaintext skeleton for generating customer templates, and message_body_plain column to core_email_queue table to contain plaintext generated emails. To add necessary columns we can use this script:

/** @var $installer Mage_Core_Model_Resource_Setup*/
$installer = $this;
 
$installer->startSetup();
 
$connection = $installer->getConnection();
 
$emailTemplateTable = Mage::getSingleton('core/resource')->getTableName('core/email_template');
$emailQueueTable = Mage::getSingleton('core/resource')->getTableName('core/email_queue');
 
$connection->addColumn($emailTemplateTable, 'template_text_plain', array(
    'type' => Varien_Db_Ddl_Table::TYPE_TEXT,
    'nullable' => false,
    'comment' => 'Template Text Plain'
));
 
$connection->addColumn($emailQueueTable, 'message_body_plain', array(
    'type' => Varien_Db_Ddl_Table::TYPE_TEXT,
    'nullable' => false,
    'comment' => 'Message Body Plain'
));
 
$installer->endSetup();

Now that we have added our attribute, we need to implement the logic for generating customer emails after placing the order. To do that we will need to extend class Mage_Core_Model_Email_Template and rewrite send() and getProcessedTemplate() functions. In send function we will add call for processing template in plaintext along with normal processing:

public function send($email, $name = null, array $variables = array())
    {
        if (!$this->isValidForSend()) {
            Mage::logException(new Exception('This letter cannot be sent.')); // translation is intentionally omitted
            return false;
        }
 
        $emails = array_values((array)$email);
        $names = is_array($name) ? $name : (array)$name;
        $names = array_values($names);
        foreach ($emails as $key => $email) {
            if (!isset($names[$key])) {
                $names[$key] = substr($email, 0, strpos($email, '@'));
            }
        }
 
        $variables['email'] = reset($emails);
        $variables['name'] = reset($names);
 
        $this->setUseAbsoluteLinks(true);
        /** INCHOO EDIT START */
        $text = $this->getProcessedTemplate($variables);
        $textPlain = $this->getProcessedTemplate($variables, true);
        /** INCHOO EDIT END */
        $subject = $this->getProcessedTemplateSubject($variables);
 
        $setReturnPath = Mage::getStoreConfig(self::XML_PATH_SENDING_SET_RETURN_PATH);
        switch ($setReturnPath) {
            case 1:
                $returnPathEmail = $this->getSenderEmail();
                break;
            case 2:
                $returnPathEmail = Mage::getStoreConfig(self::XML_PATH_SENDING_RETURN_PATH_EMAIL);
                break;
            default:
                $returnPathEmail = null;
                break;
        }
 
        if ($this->hasQueue() && $this->getQueue() instanceof Mage_Core_Model_Email_Queue) {
            /** @var $emailQueue Mage_Core_Model_Email_Queue */
            $emailQueue = $this->getQueue();
            $emailQueue->setMessageBody($text);
            /** INCHOO EDIT START */
            $emailQueue->setMessageBodyPlain($textPlain);
            /** INCHOO EDIT END */
            $emailQueue->setMessageParameters(array(
                'subject'           => $subject,
                'return_path_email' => $returnPathEmail,
                'is_plain'          => $this->isPlain(),
                'from_email'        => $this->getSenderEmail(),
                'from_name'         => $this->getSenderName(),
                'reply_to'          => $this->getMail()->getReplyTo(),
                'return_to'         => $this->getMail()->getReturnPath(),
            ))
                ->addRecipients($emails, $names, Mage_Core_Model_Email_Queue::EMAIL_TYPE_TO)
                ->addRecipients($this->_bccEmails, array(), Mage_Core_Model_Email_Queue::EMAIL_TYPE_BCC);
            $emailQueue->addMessageToQueue();
 
            return true;
        }
 
        ini_set('SMTP', Mage::getStoreConfig('system/smtp/host'));
        ini_set('smtp_port', Mage::getStoreConfig('system/smtp/port'));
 
        $mail = $this->getMail();
 
        if ($returnPathEmail !== null) {
            $mailTransport = new Zend_Mail_Transport_Sendmail("-f".$returnPathEmail);
            Zend_Mail::setDefaultTransport($mailTransport);
        }
 
        foreach ($emails as $key => $email) {
            $mail->addTo($email, '=?utf-8?B?' . base64_encode($names[$key]) . '?=');
        }
 
        if ($this->isPlain()) {
            $mail->setBodyText($text);
        } else {
            $mail->setBodyHTML($text);
        }
 
        $mail->setSubject('=?utf-8?B?' . base64_encode($subject) . '?=');
        $mail->setFrom($this->getSenderEmail(), $this->getSenderName());
 
        try {
            $mail->send();
            $this->_mail = null;
        }
        catch (Exception $e) {
            $this->_mail = null;
            Mage::logException($e);
            return false;
        }
 
        return true;
    }

In getProcessedTemplate function we will alter the way plain and html templates are processed so that when we use html mode, both html and plaintext processing in enabled. Notice we added new $forcePlain argument for controlling how templates are processed:

public function getProcessedTemplate(array $variables = array(), $forcePlain = false)
    {
        $processor = $this->getTemplateFilter();
        /** INCHOO EDIT START */
        if($forcePlain) {
            $processor->setUseSessionInUrl(false)
                ->setPlainTemplateMode(true);
        } else {
            $processor->setUseSessionInUrl(false)
                ->setPlainTemplateMode($this->isPlain());
        }
        /** INCHOO EDIT END */
 
        if (!$this->_preprocessFlag) {
            $variables['this'] = $this;
        }
 
        if (isset($variables['subscriber']) && ($variables['subscriber'] instanceof Mage_Newsletter_Model_Subscriber)) {
            $processor->setStoreId($variables['subscriber']->getStoreId());
        }
 
        // Apply design config so that all subsequent code will run within the context of the correct store
        $this->_applyDesignConfig();
 
        // Populate the variables array with store, store info, logo, etc. variables
        $variables = $this->_addEmailVariables($variables, $processor->getStoreId());
 
        $processor
            ->setTemplateProcessor(array($this, 'getTemplateByConfigPath'))
            ->setIncludeProcessor(array($this, 'getInclude'))
            ->setVariables($variables);
 
        try {
            // Filter the template text so that all HTML content will be present
            /** INCHOO EDIT START */
            if($forcePlain) {
                $result = $processor->filter($this->getTemplateTextPlain());
            } else {
                $result = $processor->filter($this->getTemplateText());
            }
            /** INCHOO EDIT END */
            // If the {{inlinecss file=""}} directive was included in the template, grab filename to use for inlining
            $this->setInlineCssFile($processor->getInlineCssFile());
            // Now that all HTML has been assembled, run email through CSS inlining process
            $processedResult = $this->getPreparedTemplateText($result);
        }
        catch (Exception $e)   {
            $this->_cancelDesignConfig();
            throw $e;
        }
        $this->_cancelDesignConfig();
        return $processedResult;
    }

Lastly we need to add small adjustment to Mage_Adminhtml_Block_System_Email_Template_Preview class in _toHtml() function, which is adjustment for previewing template in admin, and it is needed because of getProcessedTemplate rewrite we did earlier. It is possible to rewrite getProcessedTemplate differently in order to bypass this last rewrite, but I chose this because of easier read:

protected function _toHtml()
    {
        // Start store emulation process
        // Since the Transactional Email preview process has no mechanism for selecting a store view to use for
        // previewing, use the default store view
        $defaultStoreId = Mage::app()->getDefaultStoreView()->getId();
        $appEmulation = Mage::getSingleton('core/app_emulation');
        $initialEnvironmentInfo = $appEmulation->startEnvironmentEmulation($defaultStoreId);
 
        /** @var $template Mage_Core_Model_Email_Template */
        $template = Mage::getModel('core/email_template');
        $id = (int)$this->getRequest()->getParam('id');
        if ($id) {
            $template->load($id);
        } else {
            $template->setTemplateType($this->getRequest()->getParam('type'));
            $template->setTemplateText($this->getRequest()->getParam('text'));
            $template->setTemplateStyles($this->getRequest()->getParam('styles'));
        }
 
        /* @var $filter Mage_Core_Model_Input_Filter_MaliciousCode */
        $filter = Mage::getSingleton('core/input_filter_maliciousCode');
 
        $template->setTemplateText(
            $filter->filter($template->getTemplateText())
        );
 
        Varien_Profiler::start("email_template_proccessing");
        $vars = array();
 
        /** INCHOO EDIT START */
        $templateProcessed = $template->getProcessedTemplate($vars);
        /** INCHOO EDIT END */
 
        if ($template->isPlain()) {
            $templateProcessed = "<pre>" . htmlspecialchars($templateProcessed) . "</pre>";
        }
 
        Varien_Profiler::stop("email_template_proccessing");
 
        // Stop store emulation process
        $appEmulation->stopEnvironmentEmulation($initialEnvironmentInfo);
 
        return $templateProcessed;
    }

That’s it! 😉

The post Enabling Multi-part MIME Emails in Magento appeared first on Inchoo.

]]>
http://inchoo.net/magento/enabling-multi-part-mime-emails-in-magento/feed/ 4
The Struggle For Ideal Design http://inchoo.net/magento/design/the-struggle-for-ideal-design/ http://inchoo.net/magento/design/the-struggle-for-ideal-design/#comments Tue, 03 May 2016 12:34:46 +0000 http://inchoo.net/?p=26673 Many already attempted to define ideal design – and many have already confused design with aestheticism. That’s why we’re bringing you an overview of what design stands for and its primary role, problem solving. We also went over the fact that design is supposed to please the customer visually and trigger an emotion in him, so...

The post The Struggle For Ideal Design appeared first on Inchoo.

]]>
Many already attempted to define ideal design – and many have already confused design with aestheticism. That’s why we’re bringing you an overview of what design stands for and its primary role, problem solving. We also went over the fact that design is supposed to please the customer visually and trigger an emotion in him, so here’s how you combine all of that.

The design and the way problems are solved

Every one of us is a designer in a way. We are all trying to find the quickest and easiest way to solve problems that we encounter, whether it be a trivial thing like opening a yoghurt (as successfully as possible) or something much more demanding, like building life-sustaining structures on Mars. Realizing that something is not working like it should be and as well as it should is, in fact, in human nature. We observe and analyze, and upon identifying the problem, we iterate the product for as long as it takes for us to bypass the said problem. Some will probably find a completely different use for the same product than the other, or perhaps a different method of solving the problem. That is the essence of design: enabling anyone and everyone to use the product or service, any time, any place, without delays and disruptions, without the need to wonder about the inner workings.

Everyone designs who devises courses of action aimed at changing existing situations into preferred ones
– Herbert Simon

Design, contrary to popular belief, is not an art. Design is not a matter of style or taste. When talking about design, we talk about the decision-making process around the functions of a product or a service. What product or service would that be? The one that brings a definite solution to a particular problem. We talk about enhancing the quality of life and shortening the time it takes to carry out a certain task. We talk about breaking down the problem into its smallest segments so as to understand what causes it and how to avoid it. Design that does not work the problem out, therefore, is not design. If we think about design as only something aesthetically pleasing to a certain group of individuals, then we are not, in fact, talking about design. We are talking about taste and [i]n matters of taste, there can be no disputes“.

unusable
“The Uncomfortable” — Katerina Kamprani

Design fulfills the function. It solves the problem. Still, is finding the solution to a problem sufficient enough to create an emotional connection?

The taste & trends and why you should not go gentle into that good night

It is a well-known fact that everyone is entitled to their own opinion, and everyone can state if and what they like about something. Likewise, everyone can follow trends to “stay up to date”, to earn bragging rights and become popular on social networks, among friends and acquaintances. But the question remains: Are trends really for everyone? Do all trends agree with everyone? Are the current, contemporary trends applicable to every kind of business, to every product?

Subjective opinions of stakeholders’ random family members on the color, positioning or shape of an element that they may or may not like do not matter. Someone’s personal taste does not matter. What matters is the kind of visual presentation that will arouse emotions.

Trends come and go. They depend on a multitude of factors. Some trends evolve and adapt, but most are replaced with new ones, which are at that moment presented as the way things should be done, as an example to be followed. Some trends are utilized to such extent that they become a standard, which is not necessarily a bad thing.

Through standardisation of user interface elements we help create the platform that provides a consistent and cohesive experience, which then enables users to find their way around more effectively. This, in turn, saves time and allows the development of a visual language. Android Material Design and Apple Human Interface Guidelines offer exactly that: standardisation for their platforms in order to habituate their users with them. They offer designers a visual solution with which it is easier to shape the information architecture and tailor the user experience. However, is letting something that has become a standard only for certain platforms turn into a widespread trend a good thing? Especially if we consider the fact that the elements of that ‘standard’ are being implemented without careful thought and consideration of their peculiarities and effects?

The perfect example can be found on the web, where standardisation of elements may have quite the opposite result. If we consider online stores designed through the use of  templates, we can notice the pattern; they all look the same. Remove the logo and one cannot be sure what web-page one is on. Remove the cover image and all the emotion is gone. Again, the user has no idea what he or she may find there. Trends come in handy with systems that enable quicker and simpler creation and presentation of products and services, but not with the users themselves. Those kinds of systems use the information only to analyze and possibly alter the behaviour of users. They observe only one side of the users and miss the bigger picture. Those kind of systems lose the connection with their users and do not offer the emotional experience of product use. Establishing that kind of bond is very important, and it cannot be done via the interface built on the current trends.

uniformity_inchooUniformity of todays interfaces

Templates of that sort may be attractive, but they fail to realize the emotional connection. If the template is created to solve all the potential problems, to be of broad functionality, it is most likely to fail. The presentation might seem satisfactory, but the connections between the product and the customer will not be realized.

Trends are not here so they can be blindly followed and exercised. Trends are, especially nowadays, a result of the lack of time, and the means for the quickest launch of MVPs onto the market possible. Unfortunately, many remain on that level, never growing, never evolving.

“The Competition Does It This Way” Problem

One of the more frequent issues is also one of “borrowing” from the competition. If the competition follows the latest trends when presenting its products and is doing well, some may start believing that the same mechanisms might also be adequate for them. That is, most often than not, a mistake.

Although the decisions made during the process of problem-solving (e.g. on a user-interface of an online store) are the result of an extensive research and thorough methodology (customer journey maps, user stories, personas, analytics/data, brainstorm sessions, user flows, storyboards, use cases and scenarios, usability testing, card sorting, a/b testing, sketches, etc), many still rely solely on the visual aspect that they find appealing.

That brings us back to personal taste, which again brings us back to trends, to imitation. Imitation, uniformity, lack of personality, lack of emotions and finally, an assembly line type of approach.

All that is a part of a problem that could easily be solved by people who are ready to invest time and effort into a process of both problem-solving and creating a user-product relationship.

The emotional connection between data and aesthetics

30% of all human decisions are rational and 70% are emotionally driven.

With that in mind, two Japanese researchers, Masaaki Kurosu and Kaori Kashimura conducted an experiment which confirmed the people’s preference for products that are more visually appealing than those that are simply functional. They modified ATM’s interface that was now more pleasant to use than the one before. All ATM’s were identical in function, number of buttons and the way they worked; the only difference was how their interfaces looked. And people loved it.

When talking about aesthetics, many still think about trends and styles, without examining how those trends would affect the presentation of their products to the targeted group of users.

When designing interfaces, we get to know where our users come from, we get to know their demographic, their interests, when and where they are using the service or product, and what they are using it for. We get to know their internet connection speed, screen resolution and how they use the product. We get to see firsthand what users perceive more and we get to learn how we can improve it.

In order to design an ideal experience we use our findings to help create emotional responses from the ones using the product or service. Through the use of Gestalt principles combined with color theory, shapes, typography, photography, iconography, we are given the tools to create experiences that will keep users happy, engaged and allow them to finish their tasks in no time. When designing products with such care not only will emotional connection be established, but users might also become so engaged as to start promoting the product for you. Apple achieved just that. Their products are aesthetically pleasing and easy to use, which, together with carefully devised marketing strategy, created a strong emotional relationship between the users and their, no longer products, but companions.

moods
Mood Lines

The researches have shown that the aesthetic quality is necessary during the user experience design because of the way people think, perceive the world around them and the way they interact with it. Aesthetics is deeply ingrained in human emotions and feelings, and therefore affects the way people receive information.

The common belief is that emotions are animalistic, irrational, while the cognition is cold, logical… human. (Emotional Design, Donald A. Norman). However, emotions are an indispensable part of people’s lives (however animalistic they might be). Everything we think about and everything we do is connected to our emotions and our subconscious. Emotions alter the way we think and often serve as guides to proper behavior. Product design needs to incite emotions because those are more memorable than anything experienced through the senses (sight, hearing, etc.). Most will not remember how something looked like, but how it made them feel.

Designing products that solve problems in an instant and connecting that product with the users on an emotional level lifts the experience users have. Experience that provides engagement and creates relationship stands out in the sea of monotonous products that looks like they were crafted just to be sold.

Because of that it is imperative to connect visually appealing with practical, to unite form and function.

How To Make It All Work?

Visuals and graphics do not make the design, but they do play a significant role in the design process. Design solves the problem, aesthetic quality shapes and connects the cold function with the user. The emotional reaction that stems from that could be further nurtured into a relationship.

Designing interfaces by function which does not follow the form will yield undependable results. Only by investing in quality research and time needed to create a deeper bond between the user and the interface will a business build a recognisable brand and create a relationship. In consequence, the user might spread the word about how he felt while using the product, reaching a greater number of people who might become interested to try it out for themselves.

After all, if something is visually appealing and impractical or dysfunctional, but still makes people smile and makes them happy, is it by definition still functional? Product itself may not meet the purpose it was created for, but it definitely meets another purpose – the emotional one, which is the hardest to come by.

Good design is making something intelligible and memorable. Great design is making something memorable and meaningful. – Dieter Rams

The post The Struggle For Ideal Design appeared first on Inchoo.

]]>
http://inchoo.net/magento/design/the-struggle-for-ideal-design/feed/ 1
Pimcore Portlets http://inchoo.net/magento/pimcore-portlets/ http://inchoo.net/magento/pimcore-portlets/#comments Mon, 02 May 2016 11:54:23 +0000 http://inchoo.net/?p=26644 After long time, we are back on Pimcore development. This time, we are working on Pimcore 4, which is in development (RC version), and we will share some guides with you. There are many changes in new Pimcore version, but most important are; improved UI, new ext-js (version 6) and under the hood code (like...

The post Pimcore Portlets appeared first on Inchoo.

]]>
After long time, we are back on Pimcore development. This time, we are working on Pimcore 4, which is in development (RC version), and we will share some guides with you. There are many changes in new Pimcore version, but most important are; improved UI, new ext-js (version 6) and under the hood code (like new Zend version, classification store, etc).

Portlets in Pimcore are dynamic widgets that can be placed on dashboard panels. As you can add multiple dashboards, you also can add multiple portlets. By default Pimcore 4 comes with Modified Documents, Modified Objects, Modified Assets, Changes made in last 31 days, Feed, Google Analytics and Custom report portlets. They can be great because you can add them to dashboard panel in any way you want, and you can have dynamic information for users as well.

For example, good portlet is “Modified Objects”, where you can see every change made in Objects like log feed, which can be very useful in big teams for easy user work tracking. Portlets can be added as many times as you want in any dashboard panel. They are stored in psf file in dashboard configuration – configurations are located in website/var/config/portal/ folder, and they are named like dashboards_DASHBOARD_ID.psf. They are fully rendered and loaded by extjs. They can be created custom for any purpose you want. All you need is one custom extension and portlet class that will be added to the layout portlet – and, of course, some knowledge of ext-js 6. Please note that if dynamic data is required you should have some knowledge about php too.

How to add portlet to dashboard

This is very simple to do. First, you should open any dashboard panel by selecting File->Dashboards->DashboardName on the left menu, and then on the top right side of panel you can find “Add Portlet” button (you can see that on the image below).

add-portlet

How to remove portlet from dashboard

You can simply remove portlet by clicking on the “X” button on portlet box, but remember this will remove portlet for current user.

Want custom portlet? Sure, let’s create one!

For demonstration purposes we will create one custom portlet. Let’s create portlet that will display users last 20 records in recycle bin. We will create custom extension, set all configuration, create portlet class and custom controller that will return data for our portlet. We have all planned out, now we should start with creating custom extension for Pimcore. In Pimcore, you can do that automatically by opening “Manage extensions” panel (select Tools->Extensionson on the left menu) and clicking on “Create new plugin skeleton”button. For now let’s call extension Inchooportlet.

pimcore-manage-extensions

You can see in plugins folder a new plugin with main folder named Inchooportlet. In plugin skeleton most important files are; plugin.xml, which contains all important settings for extension to work, and Plugin.php (located in lib/Inchooportlet) that is entry class loaded by pimcore autoloader and contains all important code for extension (like hooks, install/uninstall, etc). Now, we will create inchoo-portlet.js script which will contain all js logic for our portlet. Here is complete code for portlet class:

pimcore.registerNS("pimcore.layout.portlets.inchooexample");
pimcore.layout.portlets.inchooexample = Class.create(pimcore.layout.portlets.abstract, {
 
    getType: function () {
        return "pimcore.layout.portlets.inchooexample";
    },
 
    getName: function () {
        return 'Example portlet';
    },
 
    getIcon: function () {
        return "pimcore_icon_recyclebin";
    },
 
    getLayout: function (portletId) {
 
        var store = new Ext.data.Store({
            autoDestroy: true,
            proxy: {
                type: 'ajax',
                url: '/plugin/Inchooportlet/portlet/data',
                reader: {
                    type: 'json',
                    rootProperty: 'data'
                }
            },
            fields: ['desc','info']
        });
 
        store.load();
 
        var grid = Ext.create('Ext.grid.Panel', {
            store: store,
            columns: [
                {header: 'Id', sortable: false, dataIndex: 'id', flex: 1},
                {header: 'Path', sortable: false, dataIndex: 'path', flex: 1},
            ],
            stripeRows: true,
        });
 
        this.layout = Ext.create('Portal.view.Portlet', Object.extend(this.getDefaultConfig(), {
            title: this.getName(),
            iconCls: this.getIcon(),
            height: 275,
            layout: "fit",
            items: [grid]
        }));
 
        this.layout.portletId = portletId;
        return this.layout;
    }
});

Now we will analyze code and explain every part. First, we need to register new namespace (pimcore.layout.portlets.inchooexample), after that we will extend pimcore.layout.portlets with our unique identifier and assing our class pimcore.layout.portlets.abstract. Our class needs to contain next methods: getType (returns class type), getName (returns portlet title), getIcon (returns icon class for portlet icons displayed on add button and portlet box) and getLayout (contains all logic to render portlet content). As you can see, our most important method is getLayout so let’s explain that in detail.

As we have planned, we will have data returned from custom controller. For that we need to create store which will get response from controller in json format, and we will use that data for portlet content grid. After store load, we need to create grid panel which is standard format in ext js – it should contain our store to have data, and columns configuration so it can name table columns and display data by dataIndex name. Now we have all prepared to create portlet layout which has to contain title, icon class, height and items (our grid). We only need to set portlet id and to return all layout data.

Next step is to create simple controllers and for that we need to create PortletController.php script in controllers folder. Let’s create Inchooportlet_PortletController class which extends \Pimcore\Controller\Action\Admin class. In class we will create our logic in method dataAction(). Let’s prepare data that will be returned and encode that in json format with helper that returns json string. Here you can see our controller code:

<?php
/**
 *  Example Portlet Controller
 */
use \Pimcore\Controller\Action;
use Pimcore\Db;
use Pimcore\Model\Element\Recyclebin;
use Pimcore\Model\Element;
 
class Inchooportlet_PortletController extends \Pimcore\Controller\Action\Admin {
 
    /**
     * Pimcore\Db
     *
     * @var
     */
    protected $db;
 
    /**
     * Set db connection
     *
     * @throws Exception
     */
    public function init() {
        $this->db = Db::getConnection();
        parent::init();
    }
 
    /**
     * Get data for test portlet
     */
    public function dataAction() {
 
        $list = new Recyclebin\Item\Listing();
        $list->setLimit(20);
        $list->setOffset(0);
 
        $list->setOrderKey("date");
        $list->setOrder("DESC");
 
        $items = $list->load();
 
        $this->_helper->json(array(
            'data' => $items,
            "success" => true
        ));
    }
}

And here you can see how new portlet look’s like:

example-portlet-screenshot

More Pimcore?

I hope this will help you to create custom portlets and give you the motivation for deeper exploration on this great platform for Pim, CMS and Commerce. If you are interested in more Pimcore articles let us know. All suggestions and comments are welcome.

The post Pimcore Portlets appeared first on Inchoo.

]]>
http://inchoo.net/magento/pimcore-portlets/feed/ 1
When can you deliver? (FedEx) http://inchoo.net/magento/shipping-details-magento-fedex/ http://inchoo.net/magento/shipping-details-magento-fedex/#comments Thu, 14 Apr 2016 12:04:17 +0000 http://inchoo.net/?p=26596 As a customer, there are two things prioritizing choices of shipping methods. First one, of course, is shipping price and other is delivery date. Since Magento is already showing us shipping prices on the checkout we don’t need to worry about that. Magento offers us a lot of different shipping possibilities and each one has...

The post When can you deliver? (FedEx) appeared first on Inchoo.

]]>
As a customer, there are two things prioritizing choices of shipping methods. First one, of course, is shipping price and other is delivery date. Since Magento is already showing us shipping prices on the checkout we don’t need to worry about that.

Magento offers us a lot of different shipping possibilities and each one has its own way of communicating and returning shipping details so there is not one single solution for all of them, but for now, we can show you how to do this for FedEx.

2016-04-14 11_18_53-Settings

Changing FedEx API

Biggest problem we encounter with getting delivery date is that shipping provider as FedEx is not returning that date back to us. To resolve this, we need to request this data from Fedex and to do so, we need to change our API request.

That is why we want to overwrite _formRateRequest method in Mage_Usa_Model_Shipping_Carrier_Fedex class and add “ReturnTransitAndCommit” in API call.

protected function _formRateRequest($purpose)
{
   $ratesRequest = parent::_formRateRequest($purpose);
   $ratesRequest['ReturnTransitAndCommit'] = true;
   return $ratesRequest;
}

Adding this attribute in call will request that Transit Time information is included in the reply from Fedex.

Fedex will return to us delivery timestamp for each method, but to make this more readable and accessible to shipping rate we will overwrite _prepareRateResponse method in same class and split it into date and time.

protected function _prepareRateResponse($response)
{
   $costArr = array();
   $priceArr = array();
   $errorTitle = 'Unable to retrieve tracking';
 
   if (is_object($response)) {
       if ($response->HighestSeverity == 'FAILURE' || $response->HighestSeverity == 'ERROR') {
           if (is_array($response->Notifications)) {
               $notification = array_pop($response->Notifications);
               $errorTitle = (string)$notification->Message;
           } else {
               $errorTitle = (string)$response->Notifications->Message;
           }
       } elseif (isset($response->RateReplyDetails)) {
           $allowedMethods = explode(",", $this->getConfigData('allowed_methods'));
 
           if (is_array($response->RateReplyDetails)) {
               foreach ($response->RateReplyDetails as $rate) {
                   $serviceName = (string)$rate->ServiceType;
                   if (in_array($serviceName, $allowedMethods)) {
                       $amount = $this->_getRateAmountOriginBased($rate);
                       $costArr[$serviceName]  = $amount;
                       $priceArr[$serviceName] = $this->getMethodPrice($amount, $serviceName);
                       $deliveryTimestamp[$serviceName] = $rate->DeliveryTimestamp;
                   }
               }
               asort($priceArr);
           } else {
               $rate = $response->RateReplyDetails;
               $serviceName = (string)$rate->ServiceType;
               if (in_array($serviceName, $allowedMethods)) {
                   $amount = $this->_getRateAmountOriginBased($rate);
                   $costArr[$serviceName]  = $amount;
                   $priceArr[$serviceName] = $this->getMethodPrice($amount, $serviceName);
               }
           }
       }
   }
 
   $result = Mage::getModel('shipping/rate_result');
   if (empty($priceArr)) {
       $error = Mage::getModel('shipping/rate_result_error');
       $error->setCarrier($this->_code);
       $error->setCarrierTitle($this->getConfigData('title'));
       $error->setErrorMessage($errorTitle);
       $error->setErrorMessage($this->getConfigData('specificerrmsg'));
       $result->append($error);
   } else {
       foreach ($priceArr as $method=>$price) {
           $rate = Mage::getModel('shipping/rate_result_method');
           $rate->setCarrier($this->_code);
           $rate->setCarrierTitle($this->getConfigData('title'));
           $rate->setMethod($method);
           $rate->setMethodTitle($this->getCode('method', $method));
           $rate->setDeliveryDate(date("l, F d",strtotime($deliveryTimestamp[$method])));
           $rate->setDeliveryTime(date("h:i A", strtotime($deliveryTimestamp[$method])));
           $rate->setCost($costArr[$method]);
           $rate->setPrice($price);
           $result->append($rate);
       }
   }
   return $result;
}

Now we made sure that Fedex is returning transit data.

Displaying delivery data

Before we can use this on a template we need to add it to shipping rate. To do this we will overwrite importShippingRate method in Mage_Sales_Model_Quote_Address_Rate class and append our new data to shipping.

public function importShippingRate(Mage_Shipping_Model_Rate_Result_Abstract $rate)
{
   $shippingRate = parent::importShippingRate($rate);
   if($rate->getDeliveryDate()){
       $shippingRate->setDeliveryDate($rate->getDeliveryDate());
       $shippingRate->setDeliveryTime($rate->getDeliveryTime());
   }
   return $shippingRate;
}

Now we can simply access this in checkout\onepage\shipping_method\available.phtml template as $_rate attribute and create our delivery message.

<label for="s_method_<?php echo $_rate->getCode() ?>"><?php echo $this->escapeHtml($_rate->getMethodTitle()) ?>
<?php $_excl = $this->getShippingPrice($_rate->getPrice(), $this->helper('tax')->displayShippingPriceIncludingTax()); ?>
<?php $_incl = $this->getShippingPrice($_rate->getPrice(), true); ?>
<?php echo $_excl; ?>
<?php if ($this->helper('tax')->displayShippingBothPrices() && $_incl != $_excl): ?>
   (<?php echo $this->__('Incl. Tax'); ?> <?php echo $_incl; ?>)
<?php endif; ?>
   <?php if($_rate->getDeliveryDate()): ?>
   <?php echo "Delivery on " . $_rate->getDeliveryDate(). " in " . $_rate->getDeliveryTime(); ?>
   <?php endif ?>
</label>

This was a simple modification on an existing call and it gave us one useful feature that makes it easier for a customer to decide what shipping method to use. But there are many cool things that Fedex Api offers that are not used in Magento. I invite you to play with it and give a little more flexibility to your shipping methods.

If you find this useful and would like this functionality on some other shipping method please comment below.

 

The post When can you deliver? (FedEx) appeared first on Inchoo.

]]>
http://inchoo.net/magento/shipping-details-magento-fedex/feed/ 1
Developers Paradise – a safe haven for true heroes of Magento community http://inchoo.net/magento/developers-paradise-safe-haven-heroes-magento-community/ http://inchoo.net/magento/developers-paradise-safe-haven-heroes-magento-community/#respond Wed, 30 Mar 2016 11:38:14 +0000 http://inchoo.net/?p=26516 We’re less than a month away from an amazing Croatian experience that will be Developers Paradise 2016. We’re thrilled to be able to welcome more than 200 community members from over 30 countries for an event packed with everything you want from a dev conference – amazing speakers, laid back atmosphere, workshops, hackathon – wrapping it all...

The post Developers Paradise – a safe haven for true heroes of Magento community appeared first on Inchoo.

]]>
We’re less than a month away from an amazing Croatian experience that will be Developers Paradise 2016.

We’re thrilled to be able to welcome more than 200 community members from over 30 countries for an event packed with everything you want from a dev conference – amazing speakers, laid back atmosphere, workshops, hackathon – wrapping it all up in a tagline that’s become much more than just that – lots of code and fun in the sun!

Here are some cool insights into #DevParadise and a reminder of why this event is as close as it gets to a real-life paradise for developers – read on!

Who you gonna meet? Developers!

We’re often saying how you can’t really feel and truly understand the power of Magento community until you attend one of the many events in a packed calendar that’s getting fuller every year (hey, Meet Magento went to Vietnam and Japan last year!).

Now, take that power, add some steroids to it and you get Developers Paradise – this event is different from any other because we pay special tribute to the true heroes of eCommerce, those making it all happen (don’t make me go all Steve Ballmer on you) – developers, developers, developers!

Unlike any other event that features many business and merchant-oriented topics (and you can always feel someone is looking for ways to upsell you something), this is a safe haven for these unsung heroes where they will feel like home, share war stories, make fun of Magento and Magento 2 in the way only those heavily involved and living off of these products can, with a lot of heart and soul put into it :)

You see, many people still think of developers as primarily introverts, but I’ve come to understand that they’re in fact very vocal (extremely at times) and can socialize as much as anyone else would, but this will happen in two scenarios:

  • Online (Forums, Twitter, Stack Overflow)
  • At a specialized conference surrounded by their peers (and beers)

And that is where the magic happens…

paradise-blog-post

So, who’s coming and what’s on the agenda?

I mentioned over 30 countries earlier, and the majority of the people coming will be from Europe (no Eastern vs Western Europe, we’re all in the same Magento 2 pickle here ;)) with a handful of delegates crossing the ocean(s) to get here – people are coming from Canada, USA, Australia, New Zealand.

There are some that come in wolf packs, some lone developers, but we’ll make sure everyone feels relaxed and makes the most of the event.

We have an amazing speakers roster, but not only that, the audience will be packed with people who know their stuff when it comes to Magento (2), so the pressure will be on the speakers to deliver something good.

We’ll also have the core of Magento 2 dev team in the house, but some of the most interesting names you will get to meet and learn from in Opatija are:

It actually seems a bit unfair to single our only these guys, so check out the entire roster and an agenda that’s really packed with quality Magento 2 content.

Oh, did I forget to mention that we’re kicking things off with a Monday morning Magento 2 hackathon? I did? Sorry, but we are – and we have just the right person to MC it – it’s Damian Luszczymak assuming the role of our Hackathon Shaman – needless to say, everyone’s in really good hands.

Ok, so you have the “code” part covered. What about the “fun in the sun”?

Glad you asked – Opatija is an amazing resort and the hotel complex is situated right at the beach and on the long scenic seaside promenade – plenty of opportunity to soak up some sun.

developers-paradise-hotel-opatija

But, we won’t leave the fun part to chance – we have three great parties and an adrenalin park adventure prepared, in addition to many more suprises coming your way.

So, if you’re reading all of this and you’re thinking:

  • Can’t wait! So excited and happy to have my ticket ready!

Then good for you and see you soon :)

But, if by any chance you read through all of this and all you can think of is:

  • Well, this all sounds great! Where do I sign up?

Then better hurry up and get your ticket now!

See you soon in Opatija! And if you’re attending Imagine in Vegas, why not have it both? Is there a better way to reminisce about everything that will be going on there than on the Adriatic coast with this amazing group of people?

Croatia awaits!

The post Developers Paradise – a safe haven for true heroes of Magento community appeared first on Inchoo.

]]>
http://inchoo.net/magento/developers-paradise-safe-haven-heroes-magento-community/feed/ 0
Google Shopping Feed and Magento – Basic Product Information http://inchoo.net/magento/magento-marketing/google-shopping-basic-product-information/ http://inchoo.net/magento/magento-marketing/google-shopping-basic-product-information/#comments Tue, 29 Mar 2016 11:05:49 +0000 http://inchoo.net/?p=26498 Google Shopping Feed is definitely the most important part for the success of your Google Shopping campaigns. Although the strategy and bids play a big role for your Google Shopping campaign success, mostly everything rests on the quality of your Google Shopping Feed. So, what’s a Google Shopping Feed? It’s basically a list of products...

The post Google Shopping Feed and Magento – Basic Product Information appeared first on Inchoo.

]]>
Google Shopping Feed is definitely the most important part for the success of your Google Shopping campaigns. Although the strategy and bids play a big role for your Google Shopping campaign success, mostly everything rests on the quality of your Google Shopping Feed.

So, what’s a Google Shopping Feed? It’s basically a list of products you want to advertise on Google Shopping. But, it’s not just a regular list, it’s a really detailed list with a lot of attributes that describe your products. The feed you send to Google, via Merchant Center, Google uses to match consumer’s search terms to your products, so the better the quality of your feed, the more visibility you’ll get.

And you must know Google is really strict when it comes to the data you send to them via Merchant Center. If the Feed fails to meet Google’s Product Feed Specifications, any products carrying incorrect information may be removed from the Google Shopping campaigns. Depending on the frequency, an amount of products or the severity of the errors, your account may even be suspended. (But don’t worry, if your account gets suspended because you can always fix the problems and request a review. If you fixed all the issues you had, your account will be reactivated again.)

Basic Product Information

Product ID [id]

The same as you have Product IDs in your Magento store, Google Shopping Feed requires a completely unique product identifier, which means there can not be a product with the identical item ID to some other item. Since in Magento, except the ID itself, SKU is also a unique attribute, I recommend you to use it instead because you will be able to manage your Item ID bidding in AdWords more easily. (but only if your SKUs are strings with less than 50 and without spaces and special characters).

Also, note that if you have more than one store with the same items you want to advertise, you should add store ID or store name to your product ID or SKU, to have a completely unique ID in your Feed.

Product Name [title]

The title is probably the most important attribute you are sending to Google. If you didn’t yet do a keyword research for the SEO purposes, you will want to do it for Shopping. If you don’t have enough resources to do keyword research for all of your products, you should write down yourself the most important attributes buyers may look for: like a brand, model, age group, gender, material, pattern, size, color, etc. and include them in your products’ title. It is also important to not include any promotional text like “Free Shipping”, special characters, all capital letters or HTML tags in your product titles because those products may be removed from the Google Shopping campaigns.

Product Description [description]

This attribute is probably self-explanatory. Although shoppers will rarely actually see your product description, it also needs to be an accurate representation of what you are selling. This is because Google crawls the description to get the relevant information about your product. It’s not as important attribute as your title, so if you don’t have enough time to manage both title and description, spend your time on the titles rather. The description is limited to 5,000 characters so if your Magento description attributes are longer than that you can send short description instead.

Google Product Taxonomy [google_product_category]

The Google Product Category indicates the category in which the product falls based on the Google Product Taxonomy – make sure you find the Google Product Taxonomy for your targeted country because they might slightly vary from country to country.

Your Product Taxonomy [product_type]

The Product Type is your own category taxonomy and Google left the possibility to name the Product Type however you want. For Product Type, you can use your product categories, but if you overcomplicated category structure in your store, you should strive to make it as simple as possible so you can more easily manage Product Groups in your AdWords account.

Although Product Type is a recommended and not required attribute, it is really important for determining your rankings and I can even say that it’s more important than having a correct Google product category because it seems like Google is using Product Type as a factor in search relevance.

The Product Type is also more flexible than the Google Product Category so you can include up to 5 attribute values. (I am not sure if Google is using the rest of the Product Types as a factor in search relevance, I still need to test this. If you have already tested this, please leave me a comment.) When adding more than 1 Product Type, make sure to put your main Product Type in first place because that’s the Product Group where your product will appear in your AdWords account.

Other Basic Information: [link], [mobile_link], [image_link], [additional_image_link], [condition]

These attributes are all pretty straightforward, so I will not go into details about them. If you have any questions about them, please leave me a comment.

The post Google Shopping Feed and Magento – Basic Product Information appeared first on Inchoo.

]]>
http://inchoo.net/magento/magento-marketing/google-shopping-basic-product-information/feed/ 1
Push notifications in Magento http://inchoo.net/magento/push-notifications-magento/ http://inchoo.net/magento/push-notifications-magento/#comments Mon, 21 Mar 2016 13:03:21 +0000 http://inchoo.net/?p=26296 When running a web shop there is always need to somehow keep your customers posted with the new information. It can be done in different ways, but using push notifications seems like a pretty neat and convenient way to do it. This technology has been around for some time now but unlike mobile apps, there...

The post Push notifications in Magento appeared first on Inchoo.

]]>
When running a web shop there is always need to somehow keep your customers posted with the new information. It can be done in different ways, but using push notifications seems like a pretty neat and convenient way to do it. This technology has been around for some time now but unlike mobile apps, there aren’t many websites that actually utilize it. There’s probably not enough buzz about it out there and to be honest, for certain channels, the technology is not quite ready yet in terms of development. Nevertheless, let’s check it out.

How it works?

Implementation of push notification feature differs depending on a mobile platform and a web browser we are targeting, but the core principle of the technology is the same in all cases.

In order to start receiving push notifications user first must opt-in to receive messages. In other words we must give a permission to an app or a website to send notifications to our device. This usually happens on applications first launch after installation or on first visit to a website, and looks similar to this:

push_notifications_opt_in_web push_notifications_opt_in_app

If we choose to receive push notifications then mobile app or a web browser makes a request to notification service provider which generates a unique token for our device and sends it back to the app/browser. Token is then sent to the web server of our choice which stores it in the database for later use.

push_notifications_token_gen_and_save

To send push notifications we use tokens which were previously stored in the database. Think of a token as an address of a device which should receive a message. Notifications are not sent directly to devices but instead to notification service providers which are responsible for its delivery to the end users.

push_notifications_send

Which notification service provider we are referencing depends on the platform we wish to target.

There are a few most “important” ones:

  • GCM (Google Cloud Messaging) for Chrome browser and Android apps,
  • APNS (Apple Push Notification Service) for OSX and iOS apps,
  • and from up until recently MWPS (Mozilla Web Push Service) for Firefox browser.

There is no universal standard which defines how notification providers should implement their service and how it should work. Therefore, every provider has its own way of implementation with different requirements and limitations. From developers point of view that kind of makes things more complicated.

Our experience

That’s all well and nice but you are probably wondering how do I implement this in my Magento store? Well, one of our clients requested of us to implement push notifications system on his Magento web shop and to cover two platforms – Apple (OSX and iOS mobile application) and Google (Chrome browser and Android mobile app). In the following text I’ll try to briefly cover the way we implemented it and the problems we faced along the way. I won’t go into details of our module or it’s structure. Instead, I’ll focus on parts which are specific for push notifications system and necessary in order to utilize it.

Before we started our work on the extension we knew it had to be robust. We needed to cover several things:

  • implement javascript opt-in logic
  • create an API through which different channels can import generated tokens to our database,
  • make all imported tokens easily visible in magento admin,
  • make an admin interface which enables creation of notification messages and their scheduling for sending,
  • make all created notification messages easily visible in magento admin along with their current status (new, scheduled, sent, failed),
  • implement the actual notifications sending process for different channels,
  • implement javascript for notification display,
  • make it easily expandable with additional new channels in the future (Firefox, Edge…).

Before we go over these steps I would like to give you links to the documentations since I’m gonna be referring to them later on. So, Google and Apple.

› Opt-in

Implementation of opt-in and subscription creation process is different depending on a platform. I won’t go into mobile app side of the story since I’m not competent to talk about it. Instead, I’ll give you an example of how it can be done for web platforms – Chrome and Safari.

• Chrome

Before displaying opt-in pop up we need to make a few checks to make sure that the given browser supports all that is needed for push notifications to work properly. First thing we need to check is whether service workers are supported in your browser. In short, service worker is a script that runs in the background in browser, and enables features that don’t require web page or user interaction. For now, lets just say that Chrome uses service worker to display a push notification. We’ll see what the actual file looks like later on. Here’s a code sample which does the check:

<?php   $workerFile = Mage::helper('inchoo_notification')->getChromeWorkerFile(); ?>
<script>
   window.addEventListener('load', function() {
       // Check that service workers are supported, if so, progressively
       // enhance and add push messaging support, otherwise continue without it.
       if ('serviceWorker' in navigator) {
           navigator.serviceWorker.register('<?php echo $workerFile; ?>')
               .then(initialiseState);
       } else {
           console.warn('Service workers aren\'t supported in this browser.');
       }
   });
</script>

Important thing to note is that service workers function on secure connections only. In other words, if your site is not on https Chrome push notifications simply can’t work since service worker can’t be registered on insecure connections.

If service workers are supported and Chrome manages to internally register the provided file, we call initializeState() function which does the rest of the necessary checks:

// Once the service worker is registered set the initial state
   function initialiseState() {
       // Are Notifications supported in the service worker?
       if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
           console.warn('Notifications aren\'t supported.');
           return;
       }
 
       // Check the current Notification permission.
       // If its denied, it's a permanent block until the user changes the permission
       if (Notification.permission === 'denied') {
           console.warn('The user has blocked notifications.');
           return;
       }
 
       // Check if push messaging is supported
       if (!('PushManager' in window)) {
           console.warn('Push messaging isn\'t supported.');
           return;
       }
 
       // We need the service worker registration to check for a subscription
       navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
           // Do we already have a push message subscription?
           serviceWorkerRegistration.pushManager.getSubscription()
               .then(function(subscription) {
                   if (!subscription) {
                       return;
                   }
 
                   // Keep your server in sync with the latest subscriptionId
                   sendSubscriptionIdToServer(subscription);
               })
               .catch(function(err) {
                   console.warn('Error during getSubscription()', err);
               });
       });
   }

If the given browser version supports all push requirements Chrome will display opt-in pop. However, opt-in pop up will be displayed only if Chrome has no data about notification permissions for the given website. It serves as initial push notification setting for a given page which Chrome saves internally under its configuration settings. If we forbid Chrome to show push notifications the only way to enable them back is via settings menu. More precisely under: Settings – Content Settings(under Privacy section) – Notifications – Manage Exceptions.

If we allow to receive push notifications Chrome tries to get a token for our device from GCM. In order for subscription process to succeed we need to include web app manifest file to our website. Manifest is a simple json file and looks something like this:

{
  "name": "Push Demo",
  "short_name": "Push Demo",
  "icons": [{
        "src": "images/icon-192x192.png",
        "sizes": "192x192",
        "type": "image/png"
      }],
  "start_url": "/index.html?homescreen=1",
  "display": "standalone",
  "gcm_sender_id": "<Your Project ID Without the Hash>"
}

You’ll notice the parameter called gcm_sender_id or project id. In order to obtain it you need to create a new project for your website through google developers console. If you are not familiar with how to do it just follow the google documentation, section “Make a Project on the Google Developer Console”.

Once we have created the manifest file we need to include it in the head of our website:

<link rel="manifest" href="path_to_manifest.json">

If manifest file is valid and everything goes well we should have a subscription object holding our device’s token. We can take the token and send it to our server in order to be saved to database:

function sendSubscriptionIdToServer(subscription)
 {
      subscription.endpoint.substring('https://android.googleapis.com/gcm/send/'.length);
 
       new Ajax.Request('<?php echo $this->getUrl('your_api_url', array('_secure'=> (Mage::app()->getStore()->isCurrentlySecure()) ? true : false))?>', {
           parameters: {subscriptionId: subscriptionId},
           onSuccess: function(response) {
               var status = JSON.parse(response.responseText).status;
               if(status) {
                   console.log('Subscription ID imported successfully.');
               } else {
                   console.log('Error while subscribing.');
               }
           },
           onFailure: function(response)
           {
               console.log('Error while subscribing.');
           }
       });
 }

At this point we have a new Chrome browser associated token in our database.

• Safari

Similar to Chrome, before taking any other action, we need to check whether push notifications are supported in a given browser version:

window.onload = function(){
   if ('safari' in window && 'pushNotification' in window.safari) {
       var permissionData = window.safari.pushNotification.permission(WEB_SITE_PUSH_ID);
       checkRemotePermission(permissionData);
   }
};

You’ll notice the parameter web_site_push_id. It identifies your website with Apple push service. You need to create one from your apple developers account. If your are not familiar with how to do it you can follow these instructions.

If push is supported, we call checkRemotePermission() function which checks permission type for a given website and takes an appropriate action:

var WEB_SITE_PUSH_ID = '<?php echo $this->getWebSitePushId();?>';
var packageUrl = '<?php echo Mage::app()->getDefaultStoreView()->getBaseUrl(Mage_Core_Model_Store::URL_TYPE_LINK, true) . 'safari_package_action_url'?>';
 
var checkRemotePermission = function (permissionData) {
    if (permissionData.permission === 'default') {
        window.safari.pushNotification.requestPermission(
            packageUrl,
            WEB_SITE_PUSH_ID,
            {},
            checkRemotePermission
        );
    } else if (permissionData.permission === 'denied') {
        console.log('denied');
    } else if (permissionData.permission === 'granted') {
             sendSubscriptionIdToServer(permissionData)
 	}
};

If there is no notification permission data for the given website (permission equals ‘default’), Safari will ask for a permission i.e. try to display an opt-in pop up. But things are not that simple as with Chrome. In order to display opt-in pop up Safari first makes an ajax request to an url of our choice and expects a valid zip package to be returned. In order for a zip package to be valid it needs to be signed with a valid .p12 certificate and have very specific content and structure. For more information on the package itself and how to generate it check out the documentation under section “Building the Push Package” section.

If returned zip package is valid then opt-in pop up will get displayed, otherwise we’ll get an error in our console log.

If we allow to receive push notifications then unique token for our device is generated. We can grab token from permissionData object and send it to our server in order to be saved to database:

function sendSubscriptionIdToServer(permissionData)
{
 new Ajax.Request('<?php echo $this->getUrl('your_api_url', array('_secure'=> (Mage::app()->getStore()->isCurrentlySecure()) ? true : false))?>', {
                parameters: {
                     token_id: permissionData.deviceToken,
                     website_id: '<?php echo Mage::app()->getStore()->getWebsiteId()?>',
                     store_id: '<?php echo Mage::app()->getStore()->getId()?>'},
                onSuccess: function(response) {
                     console.log('Subscription ID imported successfully.');
                }
            });
        }
 
 }

At this point we have a new Safari browser associated token in our database.

› Token management

All tokens that were imported to Magento can easily be viewed through Magento admin.

push_notifications_token_grid

Every token has certain information associated with it like:

  • customer email and id – if customer was logged while subscribing for push,
  • website and store id customer subscribed from,
  • channel type (chrome, safari, ios, android or some other in the future) – which is necessary to differentiate tokens from different channels.

All this information is useful if we want to do some kind of segmentation of our customers when it comes to push notifications or to create different messages for different websites and languages (store views in Magento).

› Message creation and management

To create a new notification message admin has an appropriate interface at his disposal. It looks like this:

push_notifications_message_create

Two message parameters are obligatory:

  • message text – which is a text that user sees when he receives a message and
  • channel type – which defines a platform for which certain message is intended for.

In addition to those parameters, admin can define a resource which notification will display once user clicks/taps on it. This can really be anything, some product page, category page, cms page, search link, some external link or something else.

Once message is created it needs to be scheduled for sending in order to get picked up by a sending script.

Similar to tokens, all created messages and their current status can easily be viewed via admin grid:

push_notifications_messages_grid

› Sending messages

Messages are being sent periodically by cron. Since channels differ in a way messages are sent, every channel has a php class associated with it that handles the sending process and potential errors. This way we can add a new channels in the future without much trouble.

• Google

To send Android and Chrome push notifications we need to communicate with the GCM. In order for the GCM to accept our request we need a valid API key which authenticates our server with the GCM. API key is obtained through google developers console for the particular project. Just follow the google documentation if you are not familiar on how to do it.

Once we have an API key sending process is pretty straightforward. We need to make a curl request to:

https://android.googleapis.com/gcm/send

and include device tokens and the message data as a request parameters in the following format:

curl --header "Authorization: key=<YOUR_API_KEY>" --header
"Content-Type: application/json" https://android.googleapis.com/gcm/send -d
"{\"registration_ids\":[\"<YOUR_REGISTRATION_ID>\"], \"data\":[\"<MESSAGE_PAYLOAD>\"]}"

Just a small digression, in the documentation, message data is also referred to as a payload. I may use this word in the following text so you know what I mean.

Here’s the php code example which can be used for sending:

$curlParams = array(
   'registration_ids' => array('token_1', 'token_2', 'token_3'),
   'data' => array(
       'title'    => 'your_website.com',
       'text'   => 'message_text',
       'icon'  => Mage::getBaseUrl('skin') . 'path_to_notification_image',
       'url'      => 'your_website.com/product.html',
);
);
 
$apiKey = 'YOUR_API_KEY';
 
$this->_curlAdapter->write(
     Zend_Http_Client::POST,
     'https://android.googleapis.com/gcm/send',
     CURL_HTTP_VERSION_1_1,
     array(
        'Content-Type: application/json',
        'Authorization: key=' . $apiKey
     ),
     json_encode($curlParams)
);

Once we successfully deliver message data to GCM our work is done. It is up to GCM itself to deliver messages to the end users.

• Apple

With Apple things are a bit more complicated than with Google. According to the documentation all communication with the APNS has to go over the secure TCP socket connection and data needs to be sent in a specific binary format.

Address of the socket that we need to connect to is:

ssl://gateway.push.apple.com:2195

To create a socket connection in php we can use something like this:

$certDir =  'PATH_TO_PEM_CERTIFICATE';
$address =  'ssl://gateway.push.apple.com:2195';
 
$socketClient = stream_socket_client(
   $address,
   null,
   null,
   5,
   STREAM_CLIENT_CONNECT,
   stream_context_create(array('ssl'=>array('local_cert'=> $certDir)))
);

As you’ve probably noticed, to encrypt a communication with the APNS we need to pass a local certificate to the socket client as an option of stream context. Funny thing is, obtaining of the certificate actually gave us lots of trouble since apple documentation really lacks any useful information about the certificate or on how to obtain it. After lots of searching, trials and errors we found this tutorial, which turned out to be very useful. As you’ll see for yourself, process is quite extensive and somewhat complicated, especially if you are not an Apple user and not familiar with the system.
Once we have a valid certificate we can focus on sending our messages to APNS. As I mentioned before, all data needs to be written to the socket in a binary format. According to the documentation APNS supports three binary notification formats: legacy format, enhanced format and new binary interface. We ended up formating our messages in legacy notification format which has the following structure:

apple_push_notifications_legacy_format

If you are just starting your work on this matter you should definitely go with the new binary interface and format your messages that way. You can find all details about it in the documentation. We’ll eventually have to refactor our extension to adopt this new standard, but the legacy one works just fine for now.

Following php code can be used to do the formating:

$payload = array(
     'aps'    => array(
     'alert'    => array(
          'title'    => 'your_website.com',
          'body'   => 'message_text',
     ),
     'url-args'   => array(
          'some_url'
     )
  )
);
 
$encodedPayload = json_encode($payload);
 
$binaryMessage = chr(0).
      chr(0).
      chr(32).
      pack('H*', $deviceToken).
      chr(0).chr(strlen($encodedPayload)).
      $encodedPayload;

Once message is converted to binary format we can send it to APNS by writing to the socket:

fwrite($socketClient,  $binaryMessage);

At this point it is up to APNS to deliver message to the end user.

› Message delivery

Once push notification reaches a device it somehow needs to be displayed to the user.

On Apple’s OS X devices this process is handled automatically since the whole push notification system is integrated in the operating system itself. That basically means that if you format your notification message correctly you don’t have to do anything to display it to user, it all happens automatically. For mobile applications on iOS and Android there are certain things that need to be implemented on the app side but I won’t go into it since I’m not competent to talk about it. But what I will show you is how to display a notification in Chrome.

As mentioned before, Chrome uses service workers to display a notification message.

When service worker is registered with Chrome, it is registered for a specific domain. This way, when GCM pings our browser it checks whether there is a service worker associated with the domain from which the notification is coming. If so, Chrome dispatches a push event which gets picked up by a service worker which does the rest of the work.

In order for a service worker to actually show notification we need to fill it in with the necessary javascript code. Here’s a code sample from our worker file:

self.addEventListener('push', function(event) {
     event.waitUntil(
 
     self.registration.pushManager.getSubscription().then(function(subscription) {
          var fetchUrl = "https://magento.store.com/get_chrome_push_data_from_file";
          fetch(fetchUrl).then(function (response) {
 
               if (response.status !== 200) {
                   console.log('Looks like there was a problem. Status Code: ' + response.status);
                   throw new Error();
               }
 
               // Examine the text in the response
               return response.json().then(function (data) {
                    if (data.error || !data.notification) {
                         console.error('The API returned an error.', data.error);
                         throw new Error();
                    }
 
                    if(data.status == 1) {
                         var title = data.notification.title;
                         var text = data.notification.text;
                         var icon = data.notification.icon;
                         var notificationTag = data.notification.tag;
                         var url = data.notification.url;
 
                         return self.registration.showNotification(title, {
                              body: text,
                              icon: icon,
                              tag: notificationTag,
                              data: {
                                   url : url
                              }
                         });
                    }
               });
          }).catch(function (err) {
               console.error('Unable to retrieve data', err);
          })
     })
);
});

If you take a closer look at the code you’ll notice something strange. We are grabbing notification data from remote url instead of getting it from the event object. Why are we doing that? Well, as I said in the first few lines of this article, for some channels push technology if not quite ready it terms of development. Chrome is exactly what I had in mind by saying that. At the moment GCM is not delivering message payload to Chrome. The reason for this is that in a future implementation, payload data will have to be encrypted before it is sent to a GCM. So basically, you can send payload in your curl request but it won’t be delivered to the browser. This kind of makes Chrome push notifications useless, or at least very limited in terms of functionality.

To overcome this problem and ensure at least some kind of Chrome push functionality for our client, we decided to grab notification data from a file on our server. File is populated with the data of the last Chrome message that gets scheduled from Magento admin.

The most important piece of code is obviously the one that shows the notification:

return self.registration.showNotification(title, {
     body: text,
     icon: icon,
     tag: notificationTag,
     data: {
          url : url
     }
});

If we want to open an url when user clicks on the notification we need to specify it with a bit of code, also in the service worker file:

self.addEventListener('notificationclick', function(event) {
     var url = event.notification.data.url;
     clients.openWindow(url);
});

Unsubscribe?

Yes, it’s possible.

I didn’t cover it in this post since I wanted to focus on initial subscription process and show you how to get up and running with this whole push notification thing.

Obviously, you can unsubscribe by going into your browser settings and block push notifications for a particular site. That’s one way of doing it. Aggressive, if you wish.

If you want more elegant way then you can add an interface to your site which customer can use to subscribe or unsubscribe from notifications. In that case you would need some more javascript code which would manipulate with the subscription (remove token from server or change its status) and update user interface (a button of some sort) depending on whether user is subscribed or not.

Documentations have some nice examples on how it can be done.

 
Phew…that’s it guys.

This one was a beast to write. I know, it’s a long one, but there are just so many things to cover. Hope you managed to follow along. I tried my best to be as concise as possible. If you find any mistakes or think that something is missing please let me know.

Also, here are some useful links for better understanding of some things that were mentioned in this post:

Happy coding :) !

The post Push notifications in Magento appeared first on Inchoo.

]]>
http://inchoo.net/magento/push-notifications-magento/feed/ 5
Our process of designing a Magento web store http://inchoo.net/life-at-inchoo/our-process-of-designing-a-magento-web-store/ http://inchoo.net/life-at-inchoo/our-process-of-designing-a-magento-web-store/#comments Fri, 18 Mar 2016 12:04:22 +0000 http://inchoo.net/?p=26441 Do you know in how many steps designing a Magento web store is divided? There is so much “invisible” work behind the final design of a Magento web store and in the following text I will try to explain how and why we do what we do. Visual aspects are, of course, a very important part,...

The post Our process of designing a Magento web store appeared first on Inchoo.

]]>
Do you know in how many steps designing a Magento web store is divided? There is so much “invisible” work behind the final design of a Magento web store and in the following text I will try to explain how and why we do what we do.

Visual aspects are, of course, a very important part, but our design lies behind a big number of data, user research and understanding how users interact with a web store.  

We divide process of designing a Magento web store in 4 steps: research and planning (1), sketching (2), wireframing (3) and design phase (4).

1. Research and planning

As mentioned, we start every process of web store design with an extensive research. We believe that a good plan is a prerequisite for a successful project, and in order to achieve that, we have to gather the required data.

In order to do that, we usually implement Hotjar (or some other similar service) if a client already has an existing web store. With a help of heat maps, recordings and scrolling maps, Hotjar became a very useful tool that gives us specific information about users’ behaviour. This way, we can truly understand how users engage with a web store and what bothers them.

Hotjar1

A great thing about Hotjar is the possibility of recordings, where we can literally see where users click and encounter problems. It takes time to watch all those recordings, but in the end, this is the only way we can really see whether users have issues using or navigating specific parts of the web store.

In addition to Hotjar, we use Google Analytics. With its help we can get to know our users, see which pages are mostly used as starting pages and which pages are the biggest drop-offs.

Besides Hotjar and Google Analytics, we do one more part of the research. This one is aimed at getting more familiar with the client, their business culture, industry, internal processes and competitors as well as their short and long term business goals. By understanding this part, we can start developing a creative strategy for our client’s online presence.

This entire process help us to make data driven decisions about pages and elements on web stores that are the most important for our client’s business. Collected data influences the layout of the homepage, category, and product page, the main menu items, and general internal linking structure of the website.

2. Sketching

After we establish conclusions which are the result of our previous research, we start with a quick sketching on the paper. This way we shape our ideas into a form and create a few variations of the basic layout. With a help of the sketches we can quickly think about element positioning, their relationship and functioning without wasting too much time.

After choosing the best layout, the next step is creating a wireframe.

Wireframe is a visual guide that presents the website’s structure and informational architecture, avoiding the emotional deviations caused by styled or colorful elements. Using the data from the previously conducted research, we continue the process of building the web store while the main focus is on element positioning and functioning.

3. Why are wireframes so important?

It’s very important that we create wireframes while constantly thinking about Magento and its limitations. We communicate daily with our frontend and backend developers trying to achieve the right balance between technical performance and the best possible user experience.

In this phase the biggest highlights are communicating with the client, resolving any issues he might have and understanding their needs. We always listen to our client’s demands, but if we find any usability problems in those, we do our best to explain why that might not be the best solution for them, and suggest a better option.

This approach is time-saving and cost-efficient, where new inputs and ideas can be effortlessly implemented and assessed with minor time requirements.

Wireframe-4-min

4. Design phase

Once the final wireframes are approved, we delve into details to develop a unique design solution that will help our clients’ business grow.

We always create visual elements based on the current visual identity of the brand. It means using the same colors which are already in the logo and adding some neutral ones with which we create a color palette. Furthermore, we add a contrast color that will be used as the “call to action” color.

It’s the same with typography. We always reference on the logo and usually define 2 fonts that will be used for headings and body text. Note that typography and colors are just a small part of the style guide.

To show our clients the progress with our work and to help them feel how store will function, we use interactive prototyping tools that transform static screens into clickable prototypes. We typically do this for every major interface of their store. After designs are approved, they are ready for the development phase. But our work is not completely finished. Throughout the development phase we spend quite a big amount of time on quality assurance while communicating with our developers on a daily basis and if there are some issues, we work on resolving them immediately.

After the launch…?

No, our work does not stop even after the web store launch. Why? Because a web store is a living organism – it grows and changes very often, so it needs constant care. We like to continue our research using Hotjar and Google Analytics to see if there is something that we can improve or create some new visuals.

We are very passionate about our work and dedicated to our projects. Just as our clients, we want for their businesses to grow.

Design as a team work

Our design team has 4 designers and we all have different views and special skills. Some of us are better in complex web stores, some in detail analysis or in coding and some in illustrations.

When one of us is assigned to a project, that person works exclusively through all the planning phases until the final design is approved and ready for the development. But, since we work as a team, our work is always seen by more than one pair of eyes. We are constantly looking for feedback or different approach ideas from the other team members.

What is your experience with the design? If this process is a little bit overwhelming for you – no worries, you can start with something smaller – usability audit. In our article you can check out how our usability audits can effectively improve usability of your Magento store. Make sure to get back to us with. We’d like to hear your opinion on design processes or get back to you if you request a quote.

 

The post Our process of designing a Magento web store appeared first on Inchoo.

]]>
http://inchoo.net/life-at-inchoo/our-process-of-designing-a-magento-web-store/feed/ 2
Changing default category sort direction in Magento http://inchoo.net/magento/changing-default-category-sort-direction-in-magento/ http://inchoo.net/magento/changing-default-category-sort-direction-in-magento/#comments Wed, 16 Mar 2016 12:25:46 +0000 http://inchoo.net/?p=26272 While Magento offers a lot of power when you want to customize category pages and their impressions on your users, there are still some options that are not provided but would come in handy. One common desire is to promote expensive products and ability to reverse sort direction of products in category is a simple way to...

The post Changing default category sort direction in Magento appeared first on Inchoo.

]]>
While Magento offers a lot of power when you want to customize category pages and their impressions on your users, there are still some options that are not provided but would come in handy. One common desire is to promote expensive products and ability to reverse sort direction of products in category is a simple way to achieve that.

Functionality like that will require some custom coding, but no fear – it is rather straightforward. :)
Prerequisites: Magento module creation, setup scripts, block rewrites.

First step is adding attribute to categories which we will later use to “simulate” user clicking on descending arrow in frontend with setup(install) script like:

$installer = $this;
 
$installer->startSetup();
 
$installer->addAttribute(Mage_Catalog_Model_Category::ENTITY, 'reverse_sort', [
    'group'         => 'Display Settings',
    'input'         => 'select',
    'type'          => 'int',
    'label'         => 'Reverse sort',
    'source'        => 'eav/entity_attribute_source_boolean',
    'global'        => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_STORE,
    'visible'       => true,
    'required'      => false,
    'user_defined'  => false,
    'sort_order'    => 50
]);
 
$installer->endSetup();

Which will add attribute that can be changed in magento admin interface, under Catalog -> Manage Categories, on tab “Display settings”.

Reverse sort screenshot

 

Second step is rewriting toolbar block used on category page. It controls by which criteria we sort products in listing (reacts to user changing “Sort By” in frontend or clicking direction change arrows “ASC/DESC”). We are going to extend that functionality and make it react also to reverse_sort attribute we just made, if it is set to “Yes“.

Class/method that we are extending is:

Mage_Catalog_Block_Product_List_Toolbar->getCurrentDirection()

In your config.xml, inside <frontend> or <global> node, declare block rewrite so Magento knows we are doing something funky with product_list_toolbar block.

For example:

<global>
    <blocks>
        <inchoo_reversesort>
            <class>Inchoo_ReverseSort_Block</class>
        </inchoo_reversesort>
        <catalog>
            <rewrite>
                <product_list_toolbar>Inchoo_ReverseSort_Block_Product_List_Toolbar</product_list_toolbar>
            </rewrite>
        </catalog>
    </blocks>
</global>

And final piece of functionality that we need to help us with market penetration, alter how getCurrentDirection() works:

class Inchoo_ReverseSort_Block_Product_List_Toolbar extends  Mage_Catalog_Block_Product_List_Toolbar {
    /**
     * Retrieve current direction
     *
     * @return string
     */
    public function getCurrentDirection()
    {
        $dir = $this->_getData('_current_grid_direction');
        if ($dir) {
            return $dir;
        }
 
        $directions = array('asc', 'desc');
        $dir = strtolower($this->getRequest()->getParam($this->getDirectionVarName()));
        if ($dir && in_array($dir, $directions)) {
            if ($dir == $this->_direction) {
                Mage::getSingleton('catalog/session')->unsSortDirection();
            } else {
                $this->_memorizeParam('sort_direction', $dir);
            }
        //MOD
        } elseif($cat = Mage::registry('current_category')) {
            $direction = $cat->getReverseSort();
            if($direction) {
                $dir = 'desc';
            } else {
                $dir = Mage::getSingleton('catalog/session')->getSortDirection();
            }
        //ENDMOD
        } else {
            $dir = Mage::getSingleton('catalog/session')->getSortDirection();
        }
        // validate direction
        if (!$dir || !in_array($dir, $directions)) {
            $dir = $this->_direction;
        }
        $this->setData('_current_grid_direction', $dir);
        return $dir;
    }
}

And we are done.

Thought exercise for a bored reader: how would you change getCurrentDirection() to reduce cyclomatic complexity and improve readability? :)

The post Changing default category sort direction in Magento appeared first on Inchoo.

]]>
http://inchoo.net/magento/changing-default-category-sort-direction-in-magento/feed/ 1
Taking control over module upgrade process http://inchoo.net/magento/taking-control-over-module-upgrade-process/ http://inchoo.net/magento/taking-control-over-module-upgrade-process/#comments Thu, 03 Mar 2016 12:51:48 +0000 http://inchoo.net/?p=26095 Every Magento developer has at least once encountered situation where long lasting module upgrade script caused issue for end customers. Or even worse, fail over and over, due to being executed multiple times. Of course, there is a very simple solution for this. And since we have encountered many sites which do not use this approach,...

The post Taking control over module upgrade process appeared first on Inchoo.

]]>
Every Magento developer has at least once encountered situation where long lasting module upgrade script caused issue for end customers. Or even worse, fail over and over, due to being executed multiple times.

Of course, there is a very simple solution for this. And since we have encountered many sites which do not use this approach, I have decided to share it with you.

Analyzing the problem

Consider following scenario: You are performing an module upgrade of an outdated extension. During this upgrade, it is required to create few extra tables, as well as insert one or two new attributes. You have successfully deployed your code, which means that next page request will trigger those upgrades.

Before you manage to refresh page in your browser, there is already a customer waiting for those scripts to be executed. And before you know it, there is another one waiting as well. In this scenario, it is most likely that only one will get desired result, while others will most likely end up with maintenance page.

Let’s see what happened here: When script starts, Magento does not know that module is currently being processed (no in progress state). So, as far as system is concerned, module is outdated and should be update. This is true for each request, until one of them finishes all module upgrade scripts, and increments its version.

Therefore, it is possible (and most likely) for multiple requests to attempt creating same attribute, causing all but one to fail.

Finding the solution

So how can we prevent this? The answer is simple: disable automatic processing of module upgrade scripts by adding this snippet into local.xml in global node:

<skip_process_modules_updates>1</skip_process_modules_updates>

This line will instruct Magento to skip module upgrades during application initialization. However, keep in mind that this will be ignored if your site is in develop mode (dev server, local environment, etc.). To preserve same behavior on those environments as well, add one more line to the same place:

<skip_process_modules_updates_ignore_dev_mode>1</skip_process_modules_updates_ignore_dev_mode>

And there you go, Magento will no longer automatically trigger module updates. Which, of course, is another problem because now you need to find another way to do it.

Manually trigger module upgrade

Luckily for you, someone already wrote you a script for addressing this issue. You can find several variations over the internet, but first one I have encountered was one developed by Colin Mollenhour:

https://gist.github.com/colinmollenhour/2715268

Copy and paste it to your shell folder, and execute it after each deployment. It will trigger upgrades for you, and recreate config cache for you. This way, your module upgrade would have minimal effect on fronted. Also you will benefit from triggering upgrades from shell when it comes to timeouts and memory allowed for PHP process (different config file, different settings).

Just one last note here, keep in mind that from now on, your changes need to be backwards compatible. You can not deploy code that depends on attribute that is yet to be created. Either place site under maintenance while install is finished, or deploy code in two stages.

I hope that you find this helpful.

The post Taking control over module upgrade process appeared first on Inchoo.

]]>
http://inchoo.net/magento/taking-control-over-module-upgrade-process/feed/ 4
Extending “Contact us” functionality in Magento http://inchoo.net/magento/extending-contact-us-functionality/ http://inchoo.net/magento/extending-contact-us-functionality/#comments Wed, 24 Feb 2016 10:07:40 +0000 http://inchoo.net/?p=26084 One of the most important aspects of any business is customer support. It is important to enable your customers to easily reach to you if they need to contact you. Out of the box Magento offers nice form on “Contact us” page where customers can enter their info and submit support request without opening their email...

The post Extending “Contact us” functionality in Magento appeared first on Inchoo.

]]>
One of the most important aspects of any business is customer support. It is important to enable your customers to easily reach to you if they need to contact you.

Out of the box Magento offers nice form on “Contact us” page where customers can enter their info and submit support request without opening their email client and searching for your customer service email.

However, sometimes that’s not enough. Depending of your business type and size, you may want to split your customer support into departments to improve your efficiency of solving support requests.

Luckily, it’s not complicated so let’s start!

Registering our module

Note: I’m working with fresh install of latest Magento CE 1.9.2.3 with sample data

We will override core Magento module called Contacts so we’ll also name our module Contacts and add it to the Inchoo vendor map in local code pool. First step is to register our module in Magento by creating Inchoo_Contacts.xml in app/etc/modules folder of our project. Our file should look something like this:

<?xml version="1.0"?>
<config>
    <modules>
        <Inchoo_Contacts>
            <active>true</active>
            <codePool>local</codePool>
        </Inchoo_Contacts>
    </modules>
</config>

Now, let’s make our modules directory structure in app/code/local directory. First we create Inchoo directory and then add child directory named Contacts. Now we have directory structure like this: app/code/local/Inchoo/Contacts. Next step is to make our modules configuration file, where we define all models, helpers, events etc. Make etc directory in app/code/local/Inchoo/Contacts and create config.xml file. We will add some basic information in that file, our module name and version, so config.xml should look like this:

<?xml version="1.0"?>
<config>
    <modules>
        <Inchoo_Contacts>
            <version>1.0.0</version>
        </Inchoo_Contacts>
    </modules>
</config>

Now our extension should be registered in Magento and listed in administration in Configuration→Advanced under “Disable Modules Output” group, but it doesn’t do anything, so let’s change that.

Creating backend logic

Next step is to enable administration users to define new emails that should be available for “Contact us” page, and add “Available on contact us page” setting for existing emails. Unfortunately, in order to administrators dynamically add new emails we should create slightly complicated logic and implement grid in admin so we won’t do that now. Instead we will add new email fields in our system.xml file where we can modify our Configuration page in administration panel. So, in etc directory of our module create system.xml file and copy following code:

<?xml version="1.0" encoding="UTF-8"?>
<config>
    <sections>
        <trans_email>
            <groups>
                <ident_sales>
                    <fields>
                        <contact_us>
                            <label>Available on contact us page</label>
                            <frontend_type>select</frontend_type>
                            <sort_order>30</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                            <source_model>adminhtml/system_config_source_yesno</source_model>
                        </contact_us>
                    </fields>
                </ident_sales>
                <ident_support>
                    <fields>
                        <contact_us>
                            <label>Available on contact us page</label>
                            <frontend_type>select</frontend_type>
                            <sort_order>30</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                            <source_model>adminhtml/system_config_source_yesno</source_model>
                        </contact_us>
                    </fields>
                </ident_support>
                <ident_repair>
                    <label>Repair</label>
                    <frontend_type>text</frontend_type>
                    <sort_order>9</sort_order>
                    <show_in_default>1</show_in_default>
                    <show_in_website>1</show_in_website>
                    <show_in_store>1</show_in_store>
                    <fields>
                        <name translate="label">
                            <label>Name (Label)</label>
                            <frontend_type>text</frontend_type>
                            <backend_model>adminhtml/system_config_backend_email_sender</backend_model>
                            <validate>validate-emailSender</validate>
                            <sort_order>10</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                        </name>
                        <email translate="label">
                            <label>Email</label>
                            <frontend_type>text</frontend_type>
                            <validate>validate-email</validate>
                            <backend_model>adminhtml/system_config_backend_email_address</backend_model>
                            <sort_order>20</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                        </email>
                        <contact_us>
                            <label>Available on contact us page</label>
                            <frontend_type>select</frontend_type>
                            <sort_order>30</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                            <source_model>adminhtml/system_config_source_yesno</source_model>
                        </contact_us>
                    </fields>
                </ident_repair>
                <ident_returns>
                    <label>Returns</label>
                    <frontend_type>text</frontend_type>
                    <sort_order>10</sort_order>
                    <show_in_default>1</show_in_default>
                    <show_in_website>1</show_in_website>
                    <show_in_store>1</show_in_store>
                    <fields>
                        <name translate="label">
                            <label>Name (Label)</label>
                            <frontend_type>text</frontend_type>
                            <backend_model>adminhtml/system_config_backend_email_sender</backend_model>
                            <validate>validate-emailSender</validate>
                            <sort_order>1</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                        </name>
                        <email translate="label">
                            <label>Email</label>
                            <frontend_type>text</frontend_type>
                            <validate>validate-email</validate>
                            <backend_model>adminhtml/system_config_backend_email_address</backend_model>
                            <sort_order>20</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                        </email>
                        <contact_us>
                            <label>Available on contact us page</label>
                            <frontend_type>select</frontend_type>
                            <sort_order>30</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                            <source_model>adminhtml/system_config_source_yesno</source_model>
                        </contact_us>
                    </fields>
                </ident_returns>
            </groups>
        </trans_email>
    </sections>
</config>

In this code we added several groups to section <trans_emails> which references to “Store Email Addresses” page in Admin→Configuration. Every group represents information about single email address that our site uses. Out of the box there are several groups already added in Magento, for instance there are <ident_support>, <ident_sales> etc.

They represent customer support email and sales representative email, respectively. Let’s say you want those emails to be listed on “Contact us” page as departments so we need additional field where we can set availability of those emails on “Contact us” page. We added new field to every email group and called it <contact us> and set it’s label to “Available on contact us page”. It’s a simple “Yes/No” select field with several other options, If you’re little confused you can read more about fields and system.xml here. As you can see, we added two new groups called <ident_repair> and <ident_returns> which represents our Repair and Returns departments. Each of them has three fields, “Name (Label)”, “Email” and “Available on contacts us page” which we can modify in administration.

Now if we go to administration configuration and open “Store Email Addresses” section we can see that there are two new groups of emails. However, their fields are empty so let’s set default values in our config.xml file. Add these lines to your config.xml:

<?xml version="1.0"?>
<config>
    <!-- This comment represents other config nodes we set earlier -->
    <default>
        <trans_email>
            <ident_sales>
                <contact_us>1</contact_us>
            </ident_sales>
            <ident_support>
                <contact_us>1</contact_us>
            </ident_support>
            <ident_repair>
                <name>Repair</name>
                <email>repair@example.com</email>
                <contact_us>1</contact_us>
            </ident_repair>
            <ident_returns>
                <name>Returns</name>
                <email>returns@example.com</email>
                <contact_us>1</contact_us>
            </ident_returns>
        </trans_email>
    </default>
    <!-- This comment represents other config nodes we set earlier -->
</config>

As you can recognize, these nodes that we added follow section/group/field structure and values we add under the field nodes (in this case “name”, “email” and “contact_us” are field nodes) will be displayed in configuration settings.

Next thing is to write some code where we fetch emails that are available on “Contact us” page. Since “Contact us” page is core Magento functionality, we should override some of the Magento classes. To be more specific, we will override Data helper of Contacts core module. You can read more about overriding helpers in this article but basically we add following lines in our config.xml:

<?xml version="1.0"?>
<config>
    <!-- This comment represents other config nodes we set earlier -->
    <global>
        <helpers>
            <contacts>
                <rewrite>
                    <data>Inchoo_Contacts_Helper_Data</data>
                </rewrite>
            </contacts>
        </helpers>
    </global>
    <!-- This comment represents other config nodes we set earlier -->
</config>

Now we must create Helper directory in our modules directory and create file Data.php. Content of that file should be like this:

<?php
 
class Inchoo_Contacts_Helper_Data extends Mage_Contacts_Helper_Data
{
    /**
     * Get emails for "Contact us" page
     *
     * @return mixed
     */
    public function getContactEmails()
    {
        $allowedEmails = array();
        $emails = Mage::getStoreConfig('trans_email');
 
        foreach ($emails as $email) {
            if (isset($email['contact_us']) && $email['contact_us'] == 1) {
                $allowedEmails[] = $email;
            }
        }
 
        return $allowedEmails;
    }
}

We extended core Contacts helper class and just added one method which returns emails that will be listed on “Contact us” page. First we initialize an empty array, secondly we fetch all of the email groups that are located in <trans_email> section. Then in foreach loop we are checking whether every email has set “contact_us” field value to 1 (that is “Yes” in configuration). If it’s set we are storing it in our array and finally return that array.

Displaying departments on “Contact us” page

Now that we made logic for getting emails, we should display them on “Contact us” page. First we need to find template file that we’ll override.

To do so, we must look under <contacts_index_index> layout handle in app/design/frontend/rwd/default/layout/contacts.xml layout file. There we can see that template file we need is “contacts/form.phtml”. I presume you are using custom theme, but if not, you should always make your custom theme if you’re planning to modify and override Magento templates. You can read more about creating custom themes here.

Now add these lines to your custom theme “contacts/form.phtml” file:

<?php
    $emails = Mage::helper('contacts')->getContactEmails();
?>
<!-- Other markup →
 
<!-- Other form fields -->
 
<div class="field">
    <label for="department"><?php echo Mage::helper('contacts')->__('Department') ?></label>
    <div class="input-box">
        <select name="department" id="department" class="input-select validate-select">
            <option value="">Select Department</option>
            <?php foreach($emails as $email): ?>
                <option value="<?php echo $email['email'] ?>"><?php echo $email['name'] ?></option>
            <?php endforeach ?>
        </select>
    </div>
</div>
<!-- Other form fields →

If we refresh our “Contact us” page we should see nice new Department dropdown with labels that can be changed in our admin configuration.

Final step is to modify controller which is handling this request. As you can guess, we must override core Magento controller and that is IndexController of core Contacts module. Add following lines to config.xml file of our module:

<?xml version="1.0"?>
<config>
    <!-- This comment represents other config nodes we set earlier -->
    <frontend>
        <routers>
            <contacts>
                <args>
                    <modules>
                        <Inchoo_Contacts before="Mage_Contacts">Inchoo_Contacts</Inchoo_Contacts>
                    </modules>
                </args>
            </contacts>
        </routers>
    </frontend>
    <!-- This comment represents other config nodes we set earlier -->
</config>

Now in app/code/local/Inchoo/Contacts/controllers directory create file IndexController.php and add the following content:

<?php
require_once(Mage::getModuleDir('controllers','Mage_Contacts').DS.'IndexController.php');
 
class Inchoo_Contacts_IndexController extends Mage_Contacts_IndexController
{
    public function postAction()
    {
        $post = $this->getRequest()->getPost();
        if ( $post ) {
            $translate = Mage::getSingleton('core/translate');
            /* @var $translate Mage_Core_Model_Translate */
            $translate->setTranslateInline(false);
            try {
                $postObject = new Varien_Object();
                $postObject->setData($post);
                // get emails enabled for Contact us page
                $allowedEmails = Mage::helper('contacts')->getContactEmails();
 
                $error = false;
 
                if (!Zend_Validate::is(trim($post['name']) , 'NotEmpty')) {
                    $error = true;
                }
 
                if (!Zend_Validate::is(trim($post['comment']) , 'NotEmpty')) {
                    $error = true;
                }
 
                if (!Zend_Validate::is(trim($post['email']), 'EmailAddress')) {
                    $error = true;
                }
 
                if (Zend_Validate::is(trim($post['hideit']), 'NotEmpty')) {
                    $error = true;
                }
 
                // validate department email
                if (!Zend_Validate::is(trim($post['department']), 'EmailAddress')) {
                    $error = true;
                }
 
                // check whether submitted emails is enabled in configuration
                $foundEmail = false;
                foreach($allowedEmails as $email)
                {
                    if($email['email'] == $post['department']){
                        $foundEmail = true;
                    }
                }
                // if email is in allowed emails set error to false, if it's not
                // in allowed emails set error to true
                $error = ! $foundEmail;
 
                if ($error) {
                    throw new Exception();
                }
                $mailTemplate = Mage::getModel('core/email_template');
                /* @var $mailTemplate Mage_Core_Model_Email_Template */
                $mailTemplate->setDesignConfig(array('area' => 'frontend'))
                    ->setReplyTo($post['email'])
                    ->sendTransactional(
                        Mage::getStoreConfig(self::XML_PATH_EMAIL_TEMPLATE),
                        Mage::getStoreConfig(self::XML_PATH_EMAIL_SENDER),
                        // send to selected department email
                        $post['department'],
                        null,
                        array('data' => $postObject)
                    );
 
                if (!$mailTemplate->getSentSuccess()) {
                    throw new Exception();
                }
 
                $translate->setTranslateInline(true);
 
                Mage::getSingleton('customer/session')
                    ->addSuccess(Mage::helper('contacts')->__('Your inquiry was submitted and will be responded to as soon as possible. Thank you for contacting us.'));
                $this->_redirect('home');
 
                return;
            } catch (Exception $e) {
                $translate->setTranslateInline(true);
 
                Mage::getSingleton('customer/session')->addError(Mage::helper('contacts')->__('Unable to submit your request. Please, try again later'));
                $this->_redirect('*/*/');
                return;
            }
 
        } else {
            $this->_redirect('*/*/');
        }
    }
}

We’ve overridden postAction() method and actually copied most of the code from parent class. We just added additional validation for our department email and checked whether it’s enabled in our configuration. I added some comments above lines I added because it’s easier to follow everything in code.

Conclusion

And that’s it folks, with just a little effort you can extend your “Contact us” page by splitting your customer service into departments which should speed up handling of support request.

And of course, it’s much easier for your customers to send you support requests because there’s no need for turning on email clients and searching for your emails.

Thanks for reading, let me know in the comments if you have any questions. Enjoy coding!

The post Extending “Contact us” functionality in Magento appeared first on Inchoo.

]]>
http://inchoo.net/magento/extending-contact-us-functionality/feed/ 4
Sorry, we can’t ship there http://inchoo.net/magento/sorry-we-cant-ship-there/ http://inchoo.net/magento/sorry-we-cant-ship-there/#comments Tue, 23 Feb 2016 12:26:42 +0000 http://inchoo.net/?p=26072 Magento offers a fair amount of shipping options out of the box, but we often find ourselves customizing this particular core functionality due to specific needs of our clients. In this post we will show you how to disallow shipping to specific countries and we will do it on a product level. Let’s get started!...

The post Sorry, we can’t ship there appeared first on Inchoo.

]]>
Magento offers a fair amount of shipping options out of the box, but we often find ourselves customizing this particular core functionality due to specific needs of our clients.

In this post we will show you how to disallow shipping to specific countries and we will do it on a product level. Let’s get started!

Adding product attribute

Since the configuration will be done per product, first thing we need to do is programmatically create new product attribute. In our custom module we will add following setup script:

<?php
 
/* @var $installer Mage_Eav_Model_Entity_Setup */
$installer = $this;
$installer->startSetup();
 
$installer->addAttribute(
    Mage_Catalog_Model_Product::ENTITY,
    'shipping_restriction',
    array(
        'type'      => 'varchar',
        'group'     => 'General',
        'input'     => 'multiselect',
        'label'     => 'Disallow Shipping to',
        'source'    => 'inchoo_shippingrestriction/attribute_source_country',
        'backend'   => 'inchoo_shippingrestriction/attribute_backend_country',
        'global'    => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_WEBSITE,
        'apply_to'  => 'simple,configurable,bundle,grouped',
        'required'  => false
    )
);
 
$installer->endSetup();

You will notice that our attribute has custom backend and source models and that we apply it only to product types which require shipping method.

First let’s take a look at our source model. For those that are not familiar with source models, their purpose is to fetch data for attribute. In our case it is a list of countries.

<?php
 
class Inchoo_ShippingRestriction_Model_Attribute_Source_Country
    extends Mage_Eav_Model_Entity_Attribute_Source_Abstract
    implements Mage_Eav_Model_Entity_Attribute_Source_Interface
{
    /**
     * Get list of all available countries
     *
     * @return array|mixed
     */
    public function getAllOptions()
    {
        $cacheKey = 'DIRECTORY_COUNTRY_SELECT_STORE_' . Mage::app()->getStore()->getCode();
        if (Mage::app()->useCache('config') && $cache = Mage::app()->loadCache($cacheKey)) {
            $options = unserialize($cache);
        } else {
            $collection = Mage::getModel('directory/country')->getResourceCollection();
            $options = $collection->toOptionArray();
            if (Mage::app()->useCache('config')) {
                Mage::app()->saveCache(serialize($options), $cacheKey, array('config'));
            }
        }
        return $options;
    }
}

Next we will look at our backend model. Purpose of backend model is to perform various actions on data that is stored in our attribute. For example we can validate data, modify before saving/after loading, etc. In our case we will use it to convert data from array (because we get array on input) to string before it is saved and other way around after our data is loaded.

<?php
 
class Inchoo_ShippingRestriction_Model_Attribute_Backend_Country
    extends Mage_Eav_Model_Entity_Attribute_Backend_Abstract
    implements Mage_Eav_Model_Entity_Attribute_Backend_Interface
{
    public function beforeSave($object)
    {
        $attrCode = $this->getAttribute()->getAttributeCode();
        $object->setData($attrCode, implode(',', $object->getData($attrCode)));
 
        return $this;
    }
 
    public function afterLoad($object)
    {
        $attrCode = $this->getAttribute()->getAttributeCode();
        $object->setData($attrCode, explode(',', $object->getData($attrCode)));
 
        return $this;
    }
}

Once the attribute has been created, it will appear on the product edit page under General tab.
Here is what it looks like:

shipping_restriction

Validating shipping address

Now that we can configure our products to block shipping for specific countries, we have to implement logic which takes customer’s cart items, compares them against entered shipping address and decides if customer is eligible to select shipping method and complete the checkout process.

We cannot run our code immediately after the product was added to cart because there is no guarantee that we will have shipping address at this point. Instead, we have to wait for customer to start the checkout process.

With that in mind, we will implement our code by class rewrite method. We will rewrite Mage_Shipping_Model_Shipping class and modify its collectRates method. It seems like a good place since we have access to customer’s shipping address, all the cart items and we can control which shipping methods are returned (in our case it will be either all or none).

 

<?php
 
class Inchoo_ShippingRestriction_Model_Shipping extends Mage_Shipping_Model_Shipping
{
    public function collectRates(Mage_Shipping_Model_Rate_Request $request)
    {
        if ($this->isShippingRestriction($request)) {
            return $this;
        }
 
        return parent::collectRates($request);
    }
 
    /**
     * Check if shipping restrictions apply
     *
     * @param Mage_Shipping_Model_Rate_Request $request
     * @return bool
     */
    public function isShippingRestriction(Mage_Shipping_Model_Rate_Request $request)
    {
        $options = array();
 
        /** @var Mage_Sales_Model_Quote_Item $quoteItem */
        foreach ($request->getAllItems() as $quoteItem) {
            $options = array_merge(
                $options,
                explode(',', $quoteItem->getProduct()->getShippingRestriction())
            );
        }
 
        return in_array($request->getDestCountryId(), $options);
    }
}

You may notice that following line

$quoteItem->getProduct()->getShippingRestriction()

does not return any data at first. This is because Magento does not copy all the attribute data from product once it converts it to quote (cart) item. Luckily for us, Magento provides a mechanism to include additional attributes, all we need to do is write a few lines of XML.

The following code should be placed in your config.xml file inside <global> node:

<sales>
    <quote>
        <item>
            <product_attributes>
                <shipping_restriction/>
            </product_attributes>
        </item>
    </quote>
</sales>

Conclusion

Adding the config lines was the last piece of puzzle. Basic functionality is now implemented, but obviously there is always room for improvement, especially on frontend, in terms of notifications and error messages for customer. This is something we will leave as an exercise for reader since main focus of this article was on backend implementation.

Happy coding! :)

The post Sorry, we can’t ship there appeared first on Inchoo.

]]>
http://inchoo.net/magento/sorry-we-cant-ship-there/feed/ 4
Behind the scenes of Inchoo Design team: a talk with Katarina Dijakovic http://inchoo.net/life-at-inchoo/inchoo-design-katarina/ http://inchoo.net/life-at-inchoo/inchoo-design-katarina/#respond Thu, 18 Feb 2016 13:15:24 +0000 http://inchoo.net/?p=25981 Have you noticed the fresh look on our blog posts? Recently our Inchoo design team has been putting a lot of effort into our featured images, but also working on a great portfolio through various projects along with evolving Inchoo’s identity. Our usability audit is now just one of their services because with the newest addition to the...

The post Behind the scenes of Inchoo Design team: a talk with Katarina Dijakovic appeared first on Inchoo.

]]>
Have you noticed the fresh look on our blog posts? Recently our Inchoo design team has been putting a lot of effort into our featured images, but also working on a great portfolio through various projects along with evolving Inchoo’s identity. Our usability audit is now just one of their services because with the newest addition to the team, Katarina Dijakovic, they’re more versatile than ever.

That’s why we decided to talk to Katarina and learn a bit about what inspires her, what keeps her going and how she found herself in eCommerce.

Here you have an interview with her – about technicalities, design hacks and about what she thinks of design in eCommerce now when there’s a certain mileage in her Inchoo shoes.

Hi Katarina! For starters, tell us something about yourself. You can share something about the early beginnings – when did you start with design – and about the most recent adventure – working at Inchoo?

Katarina: Hi all! My name is Katarina. I began designing back when I started college and my Inchoo adventure started in October of last year. I’ve been drawing for a long time now, way before I started to think about working in graphic design. I knew that was what I eventually wanted to do – ever since I was enjoying commercials and opening and closing credits of the movies more than the movies themselves.

I always found it interesting how different formats can communicate information through details and almost inconspicuous things, how certain elements can make a whole and “round it up” in a certain context and atmosphere. Whether we’re talking about user interfaces, print materials, animations or illustrations, they can connect a person with the world around them. That’s why it’s important to mention the advantage of every member of the Inchoo design team having a different skill set, work approach and understanding of the design process. That’s also one of the reasons why we initiated Design Talks, a gathering of local designers – to stimulate the exchange of knowledge, skills and experience in the Osijek area.

katarina

Now that you’re mentioning differences… I can’t help but wonder – are there any differences now, between this job, when you compare it to the projects you worked on so far?

Katarina: I mean, sure there are differences. I worked in a marketing agency, print production, won several contests, studied graphic design… I really had so many opportunities to be in various environments, dynamics and among so many different people. Every person I worked with had their own take on design and approach to work. You can learn so much about different design methods, the way it affects people’s behaviour and its foundations once you get to see things from perspectives which are not alike. I’m still learning, but so far, in my experience, that core meaning of the word design and its foundations are always true. Primary principles such as symmetry, contrast, rhythm and proportion can always be used, no matter the figuration. The human eye and brain will always react in a certain frame of those principles, that’s very simple, but there’s always a question of using it right while keeping in mind the technical requirements and user behaviour you as a designer are trying to encourage.

When it comes to user interface, or to be more specific, eCommerce, it is so interesting because there are so many types of tools we use to show us which parts of the interface are a success, or to put it bluntly, are they doing the job we intended for them – stimulated someone to click, swipe or scroll. They also tell us what can be changed and improved and we gladly allow that functionality to naturally define the design.

Still, it’s important not to follow the same patterns of design blindly. It is crucial not to make design decisions presuming that the users are used to a certain look; for example, that a certain icon represents specific content or that the search bar necessarily needs to be aligned a certain way. Sometimes research and numbers show failure of a design, but they can also be perceived as pointers which show that the users need more time to get used to something what is, in the long run, better in terms of optimal usability. At our team we’re always thinking about something being created more intuitively and not just accepting default solutions because, not so long ago, they weren’t default themselves. They became such because users learned what they represent, they learned to use interfaces which are now accepted as default because it made their experience better. As a consequence of that, our clients business is a success and so is our own. We must remember that there’s always room for teaching users about innovative elements, if they contribute to better usability and functionality. The biggest goal is to make these digital interfaces as natural as possible, or to explain it differently, to create as little emotional distance as we can between the user and the act of online shopping. In doing that, we as designers are educated on the technical implications and demands of our work, especially working on a platform such as Magento.

So, what can inspire all of that? How does your day look like?

Katarina: Normally I always know what my focus for the day is in advance, so I immediately start on that when I get in. But it’s good to prepare yourself or think about the visual solutions outside of work, too. You tend to bring your design home with you, it’s kind of a way of thinking and living.

One of the issues we might have is reaching that certain limit within one day where you feel like you’re not taking as big visual steps as you did when you started. That’s rarely the phase where you feel like you’ve created something visually complete. Often we need to come back to a design after a while and finish it up with fresh eyes. That’s also why I break down all tasks into smaller ones. If there’s something I can do right away or finish in a short amount of time, I simply do it. We’re big believers in design sprints and making things happen without hesitating. It saves us a lot of time later with things in the future.

Personally, I love doing hand sketches. That’s just my own preference, something like my own personal brainstorm session. I’m used to coming across visual solutions which are the results of natural hand movements. From defining shadows in illustrations, planning storyboards for animations or defining wireframes for interfaces – it’s a great way to think about your design and start building it from there.

Can you tell us about what inspires all of that?

Katarina: Mostly things which are personally interesting to me – visually or conceptually. They inspire me to create and work in design in general. Still, I never force my own style and preferences onto my work. It just wouldn’t be appropriate for so many projects and would be very unprofessional because your own style is just one of many filters that you create through. As a designer, I have to think about the client, the end user and their needs. In that scenario, there’s not a lot of room for things which I like or I find beautiful because we’re often not the end user or target group to that project. What is of significance to me, might not be to them. Every project has its own aesthetic demands and you should always try to understand your users objectively and create from that point on. And when I say that, I’m not talking about replicating things which are trendy, no, not at all. Because every project is unique you get to have space, if you approach it in the right way, to create something original and push your own limits as a designer.

I must admit that even though I had certain projects I liked more than others, I still think I learned the most from those which were most risky or my biggest failures. You can’t make big steps towards progress if you don’t learn from your own mistakes, they can be a great motivator and a way of looking at the glass half full. :)

In the end, can you tell us why eCommerce, why Inchoo?

Katarina: I’ve been asked that before and I always say why not? At Inchoo we get to be involved with shaping and influencing an important aspect of online behaviour while delivering great user experiences and that as a result has a real effect on the world around us.

We often forget that no matter the seriousness of our work, expectations, technical inquiries, short deadlines and budgets, what makes these positive numbers and high rates of success is the human interaction we aim to create. Behind every number, chart, statistic or a well done audit is none other than a human being. Here at Inchoo, every team works on achieving these goals equally and I think that’s what makes this unique environment a place where everyone would want to learn and thrive.

So, there you have it, guys. Insiders look into artwork our design team has been creating. Let us know what you think! :)

The post Behind the scenes of Inchoo Design team: a talk with Katarina Dijakovic appeared first on Inchoo.

]]>
http://inchoo.net/life-at-inchoo/inchoo-design-katarina/feed/ 0
How to effectively improve usability of your Magento store? http://inchoo.net/magento/improve-usability-magento-store/ http://inchoo.net/magento/improve-usability-magento-store/#comments Tue, 09 Feb 2016 12:42:41 +0000 http://inchoo.net/?p=25903 As a merchant, sometimes you may find high bounce rates, lower conversion rates than you’d like, or any other signal warning you of troubles in your online store. Online merchants are leaving plenty of money on the table by missing a very important component of an eCommerce business: online store usability. Many of them fail...

The post How to effectively improve usability of your Magento store? appeared first on Inchoo.

]]>
As a merchant, sometimes you may find high bounce rates, lower conversion rates than you’d like, or any other signal warning you of troubles in your online store.

Online merchants are leaving plenty of money on the table by missing a very important component of an eCommerce business: online store usability. Many of them fail in their primary goal of making purchasing easy for their customers.

So, why do customers leave and how to change that?

Customers could leave due to numerous factors, some will find a navigation too complicated or find themselves staring at a mobile page icons attempting to figure out what to do next. There are plenty of other sites which sell the same products and your customers aren‘t going to take time to learn how to use your site; they will just walk away and see if the shopping would be easier with your competitors.

There are many eCommerce usability guidelines online with actionable instructions which should improve your store usability and try to help you get the lower abandonment rates and boost conversion. However, there is one problem with all those guidelines, they’ve tested many eCommerce sites, but not yours.

It is true that most online stores face similar usability problems, but each business is unique with different type of customers, specific industry and business strategy. For this reason it is not possible to create feasible list of guidelines which will work for all online stores. What works for one online store may not work for another. Nevertheless, there is a way to find and resolve usability problems and make users less likely to abandon your site.

Auditing Usability Of Your Store To Detect Possible Flaws

Our Magento usability audit is a detailed analysis of store’s usability, created without any automation software. Main goal of this audit is to assist merchants with overcoming obstacles standing on the path to realize full potential of online store.

Comparing your online store against usability best practices is only small part of our usability audit. What’s different about this audit is web store accessibility analysis and analysis of micro interactions; areas that many tend to ignore or overlook even though they can have great influence on success of the store.

Another important difference is user research. Our goal is to better understand how customers are using your store and develop better experience.

It is possible to measure two types of data: quantitative and qualitative. Most agencies will use Google Analytics to collect numerical data that answer questions who, what, and when, but they fail answering more important questions: why and how? To answer these questions we need to use usability testing tools and methods. At Inchoo we use HotJar which allows us to collect heatmaps, visitors recordings, conversion funnels, and help us understand how visitors engage with a store.

To conclude, it is crucial to couple numerical data with qualitative data measurements to have a complete picture of users and their needs, to be able to detect possible flaws and compile a usability audit report.

With our Magento eCommerce usability report you get a list of recommendations based on both quantitative and qualitative data, that could help solve the issues that affect your store and improve usability and user experience.

Implementing suggested changes

With concrete design recommendations in-hand, we discuss the next steps together with a client. Sometimes, if there are too many issues to resolve, it is more efficient to create everything from scratch, but usually we recommend incremental redesign strategy to minimize risk, maximize ROI and improve your web store continually.

Let’s say one of recommendations on your list is to consider switching from carousel to alternate way of showcasing important information on the homepage because your users aren’t clicking on it. Many merchants will probably suggest to copy the layout from their more successful competitors and many designers will do that without many questions. At Inchoo we prefer to use data and power of cognitive psychology to understand customers needs and optimize your online store based on their behaviour.

A/B test – validating new design changes

Once we’ve collected enough data, we can set up conversion goals and create design iterations. However, we can’t be sure that the new design will perform better than the current version without testing. We need to discover which of the two versions will be more successful – should we go with the version A or version B? To find an answer to that question we should run an A/B or split test. This is the simplest testing method available. It is an experiment between different design versions of the same page used to test potential improvements. Once we find out which version is more effective at driving the behaviour we prefer, we are ready to implement the winning version.

So, are you facing usability issues on your store?

To conclude, if you fail in successfully converting visitors into customers, your visitors are probably facing usability issues on your store.

Recommended solutions are:

  1. Identify pages on your site suffering from usability issues by auditing usability of your store
  2. Gather quantitative and qualitative data to understand how visitors engage with a store
  3. With design recommendations in-hand set up conversion goals
  4. Create design variations for A/B testing and run a test
  5. Implement more effective design version

Whether you are running a successful Magento store or you are aware of usability issues, there’s always room for improvements so get in touch to get a detailed report with recommendations and guidelines to improve your users’ experience and increase conversion rates.

The post How to effectively improve usability of your Magento store? appeared first on Inchoo.

]]>
http://inchoo.net/magento/improve-usability-magento-store/feed/ 2
State of Magento Solution Specialist Certification – January 2016 http://inchoo.net/ecommerce/state-of-magento-solution-specialist-certification-january-2016/ http://inchoo.net/ecommerce/state-of-magento-solution-specialist-certification-january-2016/#comments Mon, 08 Feb 2016 13:58:33 +0000 http://inchoo.net/?p=25841 The latest figures on the Magento Certified Solution Specialist program and the updated wall of fame with 31 people holding all 4 available Magento certificates. This report features some interesting stats and trends around Magento certification program and the data comes from the official Magento’s certification directory (end of January 2016). As Magento 2 rolled out since my last...

The post State of Magento Solution Specialist Certification – January 2016 appeared first on Inchoo.

]]>
The latest figures on the Magento Certified Solution Specialist program and the updated wall of fame with 31 people holding all 4 available Magento certificates.

This report features some interesting stats and trends around Magento certification program and the data comes from the official Magento’s certification directory (end of January 2016).

As Magento 2 rolled out since my last report, I thought we might get into a much slower mode as many people would wait until the M2 certification.

But, it was clear that new certifications are quite some time away and many people want to prove their Magento proficiency, so we’ve seen pretty similar growth in absolute numbers compared to previous reports. We now have the total of 540 people worldwide making the proud group of Magento Certified Solution Specialists.

Overall growth continues to slow down with 35% new certificates obtained globally since August 2015 as opposed to almost 50% growth in a previously observed period (March to August 2015).

Magento 2 is certainly a factor, and some trends from previous reports have continued – Europe is still the strongest continent way ahead of all others. Asia is again among the highest growing continents with the only one surpassing a 50% growth mark (55%), and North America’s growth has declined.

Great news for Magento as an international ecosystem is that top 5 countries combined make up for just about half of all certificates – and there are 46 countries on the MCSS map at the moment.

mcss-continents-2016-01

Top fives – countries, cities and solution partners

There are no significant changes in the countries’ list – USA still holds reign with 122 MCSS badges which gives them a comfortable lead in front of its European counterparts.

United Kingdom continues its growth and will probably stay second for quite some time. Netherlands, Germany and Sweden are all there as well, but India is fast approaching with the total of 21.

TOP 5 COUNTRIES

  1. USA – 122
  2. UK – 64
  3. Netherlands – 36
  4. Germany – 33
  5. Sweden – 29

mcss-countries-2016-01

TOP CITIES

Now, this is exciting – even with Vaimo’s huge growth, we’re thrilled we were able to put Osijek right there to the top, where we used to be for quite some time. And with Stockholm and London just behind us, it continues to be a fun race. Let’s see what happens in a couple of months :)

And finally, Kiev makes the list – with Ukraine being the backbone of Magento, it only makes sense – good to have you on board, guys. And just like that, there are no US cities in Top 5 any more.

  1. Osijek, Croatia – 19
  2. Stockholm, Sweden – 18
  3. London, UK – 17
  4. Groningen, Netherlands – 12
  5. Kiev, Ukraine12

TOP 5 SOLUTION PARTNERS / COMPANIES

Scandiweb is making their way higher on the list so we’ll need to be careful if we want to hold on to our 2nd place :)

  1. Vaimo, Sweden – 34
  2. Inchoo, Croatia – 16
  3. Scandiweb, Latvia – 10
  4. Classy Llama, USA – 10
  5. Smile, France – 9

Now, one question need to be raised here – how come almost none of the top US Gold Solution Partner companies have significant numbers of certified solution specialists? I checked out some of their official partner profiles and it seems like even the biggest ones have only a handful of solution specialists on board.

They may not perceive this certificate as something valuable. This may be the case if they’re already established and have different marketing and sales channels where this may not be perceived as something contributing to their value proposition. Or it can be something else – we can ask them before the next report :)

Magento certification wall of fame – 34 and counting

It was great to see how people responded positively to shout-outs to those #RealMagento individuals who have collected all four Magento certificates (MCD, MCD+, MCFD and MCSS). Hey, we even had two guys take the MCD exam just to make the cut, so a special shout-out this time goes to Michael Türk and Nicolai Essig.

We now have 34 people in the wall of fame, and counting. If you know of anyone who should also be included but isn’t (this would most likely be due to their profiles not being searchable in the directory), let me know and I’ll make sure to include them to the full list available here.

Here they are, listed alphabetically:

  • Adriano Aguiar (Brazil)
  • James Anelay (UK)
  • Jitze Bakker (Netherlands)
  • Fabrizio Balliano (Italy)
  • Jacques Bodin-Hullin (France)
  • Kris Brown (USA)
  • Ashoka de Wit (Netherlands)
  • Nicolai Essig (Germany)
  • Bartosz Gorski (Poland)
  • Gabriel Guarino (Argentina)
  • Phillip Jackson (USA)
  • Vladimir Kerkhoff (Netherlands)
  • Francis Kim (Australia)
  • Filip Krejči (Czech Republic)
  • Dave Macaulay (UK)
  • Chris Manger (USA)
  • Ben Marks (USA)
  • Paul Masson (France)
  • Pierre Masson (France)
  • Daniel Navarro (Spain)
  • Derrick Nyomo (USA)
  • Vladislav Osmianskij (Lithuania)
  • Rutger Rademaker (Netherlands)
  • Martijn Riemersma (Netherlands)
  • Bas Rozema (Netherlands)
  • Sergii Shymko (USA)
  • Henry Tran (Australia)
  • Nhu Tran (Vietnam)
  • Michael Türk (Germany)
  • Toon Van Dooren (Belgium)
  • Anthony Van Zandycke (Belgium)
  • Danny Verkade (Netherlands)
  • Jakub Winkler (Poland)
  • Tobias Zander (Germany)

All hail the certification kings! :)

Magento 2 is out – what does it mean for you?

If your organization is combining developer and solution specialist certificates with a Magento 2 Trained Partner badge (like we did), chances are you are in a better position for new potential clients. Even with so many differences between M1 and M2, merchants still need to have top quality support and people they can rely on to help them grow, not merely develop an eCommerce website.

This is why we’re still investing in getting our people certified and we’re now at 22 developer and 16 solution specialist badges (several more pending in February), and we won’t be stopping any time soon.

What are your plans for MCSS certification in light of Magento 2?


Disclaimer:

The data for this article is taken from the official public Magento certification directory as well as some of the partners listings and the “heavy crunching” required for this article was completed on January 31st 2016 – if you got certified and had your profile published in the meantime, you may want to wait for this post to be updated :)

There’s also a possibility that not all people who got certified made their profiles publicly available and searchable (I know of several such cases), so the actual number of certified solution specialists is probably a bit higher.

The post State of Magento Solution Specialist Certification – January 2016 appeared first on Inchoo.

]]>
http://inchoo.net/ecommerce/state-of-magento-solution-specialist-certification-january-2016/feed/ 6
How to add an external javascript/css file to Magento? http://inchoo.net/magento/how-to-add-an-external-javascriptcss-file-to-magento/ http://inchoo.net/magento/how-to-add-an-external-javascriptcss-file-to-magento/#comments Wed, 03 Feb 2016 12:35:42 +0000 http://inchoo.net/?p=25775 Few years ago, we had an article describing how to add external files into Magento. It was quite popular, so we decided to come back to it today. Knowing that there is no way to add or remove external files via Magento layout using action/methods, this extension was introduced. Idea was quite simple actually. All...

The post How to add an external javascript/css file to Magento? appeared first on Inchoo.

]]>
Few years ago, we had an article describing how to add external files into Magento.
It was quite popular, so we decided to come back to it today.
Knowing that there is no way to add or remove external files via Magento layout using action/methods, this extension was introduced.

Idea was quite simple actually. All you had to do is to install this extension:
Inchoo_Xternal extension

Starting from there, it was possible to use two new methods in your layout.

<!-- Along with these 3 methods -->
<action method="addJs">
<action method="addCss">
<action method="addItem">
<!-- you can use additional two methods -->
<action method="addExternalItem">
<action method="removeExternalItem">

addExternalItem and removeExternalItem were just aliases of addItem and removeItem added to Inchoo_Xternal_Block_Html_Head which is the class this extension is rewriting.

Of course, nobody would rewrite class just to add methods aliases so the main thing was adding two new types there: external_css and external_js.

So, here is how it looks like if you want to add for example 

https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js

in all pages except product view page.

<layout version="0.1.0">
    <default>
        <reference name="head">
            <action method="addExternalItem"><type>external_js</type><name>https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js</name><params/></action>
        </reference>
    </default>
    <catalog_product_view>
        <reference name="head">
            <action method="removeExternalItem"><type>external_js</type><name>https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js</name><params/></action>
        </reference>
    </catalog_product_view>
</layout>

You could use the code below and you would get the same result, but for readability’s sake, use aliases so you know which part of code depends on the 3rd party extension.

<action method="addItem"><type>external_js</type><name>https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js</name><params/></action>

Depends” is a keyword here and one of the main reason why we got back to this article again.

Even though this extensions works now (5 years later) and even though it found it’s home in some of our projects, very important point that wasn’t made when the article was originally published.
This is the third party code. If it’s clean and nice and if you are adding many external files into your installation it can really help you speed things up, but average site doesn’t need that many external files.

So why would you want to have 3rd party extension just to be able to support your theme?

There are many developers around the globe that are looking for quick solution no matter is that the best tool for the job. Result is a website which is made of 100+ installed extensions and majority of those extensions are rarely used. Most of them are installed, I would guess, because developer was looking for an easy and dirty solution.

After some time, that becomes a burden. New developers need more time to get familiar with the project, site gets slow, maintenance becomes horror – there is just more point of failures than you actually need on an average site.

When you publish a Magento related code on the blog, that’s mostly the result of playing with Magento, but many people will use it in real life situations which isn’t always the best idea.

I am not saying that this extension is wrong, I would just like to point out that using it on an average site is not needed. Many people ended up installing the extension just to add 3 lines of text to their shop.

Unless you are already developing similar set of tools that you plan to use as a starting point on most of your sites, there are more simple solutions.

So, even though you are still free to use the extension as much as you like, we recommend that you do that in case you know why exactly you need it, otherwise, do the following:

Add this to your layout, and sleep well :)

<reference name="head">
    <block type="core/text" name="your_external_file">
        <action method="setText"><text><![CDATA[<script src="//ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>]]></text></action>
    </block>
</reference>

That’s it!

Enjoy responsible coding!

The post How to add an external javascript/css file to Magento? appeared first on Inchoo.

]]>
http://inchoo.net/magento/how-to-add-an-external-javascriptcss-file-to-magento/feed/ 1
Magento’s transactional eMails do-s and don’t-s http://inchoo.net/magento/magentos-transactional-emails-do-s-and-dont-s/ http://inchoo.net/magento/magentos-transactional-emails-do-s-and-dont-s/#comments Tue, 02 Feb 2016 13:17:06 +0000 http://inchoo.net/?p=25617 Editing and styling Magento’s transactional eMails and eMails in general can be a real headache. There are numerous reasons for that, for example the number of eMail clients available that have their own quirks and the complicated workflow of the testing process. This article will cover some of the general advices and tricks that will,...

The post Magento’s transactional eMails do-s and don’t-s appeared first on Inchoo.

]]>
Editing and styling Magento’s transactional eMails and eMails in general can be a real headache. There are numerous reasons for that, for example the number of eMail clients available that have their own quirks and the complicated workflow of the testing process. This article will cover some of the general advices and tricks that will, hopefully, help you in development. These aren’t some general rules for the most part, but advices and guidelines that you may or may not use, which I discovered during my work on various projects.

Choosing the eMail clients for testing

Before you start working on your eMails, first thing that you should do is set up your testing environment. Usually, I choose around 9-12 eMail clients (alongside with a tool for local testing) to do the tests on, based on usage statistics from Litmus Labs. The way I see it, there are 3 general categories of eMail clients:

  1. Default applications which come with the operating system or device (for example, default Android eMail app, default iPad eMail app, Windows 10 eMail app, etc.). These are most accessible applications for the users.
  2. Web clients which are accessible from Internet browser (for example, Gmail, Outlook.com, Yahoo mail, etc.). These are second most accessible applications for the users.
  3. Applications that require setup which may require purchase, download and installation (for example, Mozilla Thunderbird, Microsoft Office Outlook, Windows Live Mail, etc.).

This is also the priority I usually take when choosing eMail clients on which I do the tests. This way, we can cover most of the eMail clients and make sure that eMails display correctly across majority of devices and clients. Also, it is a good idea to take a look at the statistics of most used eMail clients before making decisions.

Defining the structure

When writing HTML and CSS for eMails, we have to use outdated and deprecated workflow and code, due to the lack of support in various clients. We cannot use HTML tags we would usually use today to define the layout; we have to resort to using tables for that. Magento also allows us to create custom header and footer HTML for the eMails that can be included and used across all Magento eMails. It is also important to consult and work with the designers to define the look and structure of the transactional eMails which can be fully supported by most, if not all eMail clients, without resorting to the fallback mechanism and eMail client hacks.

Look out for Outlook and Windows 10 Mail App

Microsoft Office Outlook and Windows 10 Mail App share the similar poor support of some HTML and CSS features. But luckily, there is a way to target those two particular eMail clients with the following simple lines of code.

If you want to add some specific CSS code for Outlook and Windows 10 Mail App, add your CSS between the style tag in Magento’s eMail Header.

<head>
	<!-- ... -->
	<!--[if mso]>
	<style type="text/css">
		/* CSS styles for Microsoft Office Outlook and Windows 10 Mail App */
	</style>
	<![endif]-->
</head>

 If you want to show or hide HTML chunks of code for Outlook and Windows 10 Mail App (for example, if you want to inform the recipient to view the eMail in browser), add the HTML code between the following tags.

<!--[if gte mso 9]>
<!-- HTML code for Microsoft Office Outlook and Windows 10 Mail App -->
<![endif]-->

For reference, you can check out the official documentation for the CSS rendering capabilities for these eMail clients.

The problematic HTML paragraph <p> tag

The paragraph tag is one of the few block elements which are supported in eMails, but there are a few problems with it:

  1. Outlook desktop application ignores any padding CSS applied to it
  2. Outlook web client strips all margin CSS (without any exceptions) and replaces with their own custom bottom margins which cannot be avoided

Those are two specific reasons for me to avoid the paragraph tag. Any padding applied to the tag will only be ignored in outlook, but in outlook.com client the forced margins will just add more empty space along with the padding.

In Magento, the default eMail templates contain paragraph tags by default. The best solution I’ve found for this issue is to replace all paragraph tags with the following tags:

<span> <!-- Content of the block element --> </span><br/>

We only used inline span tag and line break tag to create our custom block element. This way we have a lot more control over it and we can be sure that there won’t be any forced styles applied to it. Using multiple line breaks before and after the block element you can control the vertical space between block elements. This may not be the best solution, but it did make the eMails look consistent across all eMail clients and it gave me a proper control over the vertical spacing.

Issues with <ul> and <ol>  tags

One of the (many) common issues with Outlook and Windows 10 Mail App eMail rendering  is bug in rendering bullets in list elements. This issue causes list bullets not being displayed in eMail. There are two ways of fixing this issue – by using CSS (more universal across the eMails) and by using HTML (requires changing code and layout for each instance of list elements).

CSS solution consists of applying left margin to the <ul> and <ol> elements in your eMails. To prevent really large left margins on other eMail clients, combine this CSS code with Outlook targeting code.

<head>
	<!-- ... -->
	<!--[if mso]>
	<style type="text/css">
		ul{
		margin:0 0 0 50px; /* Horizontal margins only */
		}
		li{
		margin:0 0 12px 0; /* Vertical margins only */
		}
	</style>
	<![endif]-->
</head>

 Another solution is to replace your <ul> and <ol> tags with tables and use UNICODE bullet point characters (like “&bull;”) to represent lists.

<table width="100%" border="0" cellspacing="0" cellpadding="0">
	<tr>
		<td valign="top">&bull;</td>
		<td valign="top">Text</td>
	</tr>
	<tr>
		<td valign="top">&bull;</td>
		<td valign="top">Text</td>
	</tr>
	<tr>
		<td valign="top">&bull;</td>
		<td valign="top">Text</td>
	</tr>
</table>

Web fonts and proper font fallback

Web fonts are supported by only some eMail clients, which makes the proper font fallback very important if you don’t want to see a default eMail client font in your eMails. Web fonts are supported by the following eMail clients:

  •         Outlook 2000
  •         iOS Mail
  •         Apple Mail
  •         Android default eMail app
  •         Mozilla Thunderbird

Even if you provide a proper fallback, like you would when you’re writing CSS for websites, Outlook and Windows 10 Mail app (surprise, surprise) can cause issues by not respecting the fallback if the web font is used. In case of Web font, Outlook and Windows 10 Mail app set their font to “Times New Roman” or other default font without respecting the provided fallback. To avoid trouble with Outlook and Windows 10 Mail app, create a standard font fallback wrapped in tags that only target those eMail clients.

Add the following line in the Magento’s Header template.

<link href='url-to-external-font' type='text/css'>

Add the following code to Magento’s noninline eMail CSS file or to the CSS text input in Magento’s eMail editor.

@import url(url-to-external-font);
	@media screen {
		body,
		td {
			font-family: "External font", fallback-fonts !important;
		}
}

And add a special font fallback code in Magento’s Header template for Outlook and Windows 10 Mail app (we know for sure that external fonts aren’t supported):

<head>
	<!-- ... -->
	<!--[if mso]>
	<style type="text/css">
		body, table, td, span, a, b {
		font-family: fallback-fonts !important;
		}
	</style>
	<![endif]-->
</head>

Bulletproof background images and buttons

Adding Image backgrounds and buttons to Magento transactional eMails and Newsletters can certainly make your eMail stand out and make it interesting and engaging to the users, but background image support and buttons support is very poor on some browsers like Outlook (both web and desktop clients) and Windows 10 where it’s difficult to set a background image and even then, it is limited to a single image.

Luckily, there are a few great online tools that will help you create, preview and edit your container with background image and buttons with universal support.

Campaign monitor’s bulletproof backgrounds generator

Campaign monitor’s bulletproof buttons generator

While creating container with background image for Magento eMails using these online generators, remember to change the image links to your media folders after you added the generated code to your Magento image.

Hiding content in eMail

Even simple styles like hiding content in eMail can be difficult due to the poor support of the “display” CSS. But luckily, there is a bulletproof method that ensures that content remains hidden in eMail method. Some of the code relies of setting the height and width of the element 0 and hiding the overflow, and some of the code relies on using Microsoft Office style “mso-hide” to hide the element on Microsoft’s eMail clients.

.hidden {
	display: none !important;
	mso-hide: all !important;
	overflow: hidden;
	line-height: 0px;
	font-size: 0px;
	width: 0px;
	height: 0px;
	margin-top: 0;
	margin-bottom: 0;
	margin-right: 0;
	margin-left: 0;
	padding-top: 0;
	padding-bottom: 0;
	padding-right: 0;
	padding-left: 0;
}

Adjusting maximum width and centering eMail in client window

Similar to the previous section of this article, even adding maximum width and centering the eMail layout can be a real headache, but with the following code and following default Magento eMail structure, you can center the fixed-width table. It’s also important to note that eMail width shouldn’t be wider than 600px, this is a general rule and most frequent advice I got. Use the following HTML attributes in your Magento Header template on table and table cell tags to add the maximum width and center the eMail.

<table align="center" cellpadding="0" cellspacing="0" border="0" width="600" style="max-width:600px; width=600px;">
<tr>
	<td align="center" width="600" style="max-width:600px; width=600px;">
		<!-- ... -->

List of widely supported CSS attributes

Best way to avoid additional fixes and special fallback code is to check out and try to memorize the CSS attributes which are widely supported and plan for various fallbacks if there is a required, but not widely supported, CSS attribute which must be used in eMail CSS. Based on the current list on Campaign Monitor’s website, the widely supported CSS attributes are:

Text & Fonts
direction font font-family
font-style font-variant font-size
font-weight letter-spacing line-height

(except on <td> element)

text-align text-decoration text-indent
text-transform vertical-align
Color & Background
color background

(limited support for images)

background-color
HSL colors
Box model
border padding

(not supported for <p>, <div> and <a> )

width

(not supported for <p> and <div>)

Lists & Tables
list-style-type border-collapse table-layout

Taking a peek into transactional eMails in Magento 2

Transactional eMails editor in Magento 2 isn’t any different from transactional eMails editor in Magento 1.9, apart from the new, modern look. It has the same barebones text input for HTML structure and CSS. The default templates have been changed, like the eMail Header which now has added meta tags for modern browsers like Microsoft Edge and automatically adds <style> tag in <head> for noninline CSS, or the new email.css in Magento 2 to be more specific.

Magento 1 Default eMail Header
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="initial-scale=1.0, width=device-width" />
</head>
<body>
{{var non_inline_styles}}
<!-- Begin wrapper table -->
<table width="100%" cellpadding="0" cellspacing="0" border="0" id="background-table">
    <tr>
        <td valign="top" class="container-td" align="center">
            <table cellpadding="0" cellspacing="0" border="0" align="center" class="container-table">
                <tr>
                    <td>
                        <table cellpadding="0" cellspacing="0" border="0" class="logo-container">
                            <tr>
                                <td class="logo">
                                    <a href="{{store url=""}}">
                                        <img
                                            {{if logo_width}}
                                            width="{{var logo_width}}"
                                            {{else}}
                                            width="120"
                                            {{/if}}
 
                                            {{if logo_height}}
                                            height="{{var logo_height}}"
                                            {{else}}
                                            height="67"
                                            {{/if}}
 
                                            src="{{var logo_url}}"
                                            alt="{{var logo_alt}}"
                                            border="0"/>
                                    </a>
                                </td>
                            </tr>
                        </table>
                    </td>
                </tr>
                <tr>
                    <td valign="top" class="top-content">
                    <!-- Begin Content -->
Magento 2 Default eMail Header
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
		<meta name="viewport" content="initial-scale=1.0, width=device-width" />
		<meta http-equiv="X-UA-Compatible" content="IE=edge" />
		<style type="text/css">
			{{var template_styles|raw}}
			{{css file="css/email.css"}}
		</style>
	</head>
	<body>
		{{inlinecss file="css/email-inline.css"}}
		<!-- Begin wrapper table -->
		<table class="wrapper" width="100%">
		<tr>
			<td class="wrapper-inner" align="center">
				<table class="main" align="center">
		<tr>
			<td class="header">
				<a class="logo" href="{{store url=""}}">
				<img
				{{if logo_width}}
				width="{{var logo_width}}"
				{{else}}
				width="180"
				{{/if}}
				{{if logo_height}}
				height="{{var logo_height}}"
				{{else}}
				height="52"
				{{/if}}
				src="{{var logo_url}}"
				alt="{{var logo_alt}}"
				border="0"
				/>
				</a>
			</td>
		</tr>
		<tr>
			<td class="main-content">
				<!-- Begin Content -->

Final steps

After you finish editing, styling and testing Magento’s transactional eMails, it’s a good idea to do an overall test and final check on all eMail clients at the same time and compare the results just in case to see if there are any minor issues with styles and layout that you might have missed.

After that, you can do a spam test. Spam test determines the chance of your eMail being caught in a spam filter by analyzing the contents of the eMail (like Subject of the eMail and plain text) and eMail code. There are many free online tools which can be used for spam testing. Even though spam analyzers cannot ensure that your email won’t be stuck in every existing spam filter due to their quirkiness, they can help you minimize the chances of that happening by ensuring that your eMail is up to standards.

These have been all the helpful little tricks that I have learned over the few months of working on transactional eMails and I hope that they will help you out too. If you have any more helpful and cool little tricks and code snippets for eMails, feel free to add your comment below this article.

The post Magento’s transactional eMails do-s and don’t-s appeared first on Inchoo.

]]>
http://inchoo.net/magento/magentos-transactional-emails-do-s-and-dont-s/feed/ 7
Magento 2 Wallpapers http://inchoo.net/magento/magento-2-wallpapers/ http://inchoo.net/magento/magento-2-wallpapers/#comments Thu, 21 Jan 2016 13:00:56 +0000 http://inchoo.net/?p=25555 It’s been a while since we’ve posted a pack of Magento 1 wallpapers so the recent Magento 2 release was the perfect opportunity to put together a couple of brand new ones. Check them out and feel free to download as many as you’d like. Climb the Magento 2 mountains and show off what a true...

The post Magento 2 Wallpapers appeared first on Inchoo.

]]>
It’s been a while since we’ve posted a pack of Magento 1 wallpapers so the recent Magento 2 release was the perfect opportunity to put together a couple of brand new ones. Check them out and feel free to download as many as you’d like.

Climb the Magento 2 mountains and show off what a true Magento fan you really are! Feel free to share with us and the rest of the community how you’re using them, we’d love to see!

If there are any screen resolutions we missed, let us know and we’ll make sure to deliver.

750×1334   1440×2560   1366×768

1920×1080   2560×1440   2960×1050

The post Magento 2 Wallpapers appeared first on Inchoo.

]]>
http://inchoo.net/magento/magento-2-wallpapers/feed/ 7
Thou Shalt Not Do Inserts in a Foreach … Unless You Know The Trick http://inchoo.net/dev-talk/thou-shalt-not-do-inserts-in-a-foreach-unless-you-know-the-trick/ http://inchoo.net/dev-talk/thou-shalt-not-do-inserts-in-a-foreach-unless-you-know-the-trick/#comments Tue, 19 Jan 2016 11:21:18 +0000 http://inchoo.net/?p=25559 TL;DR; Don’t do it. Just, simply, never write a database query inside a loop. Ever. Put in a little bit of effort and write code that will insert or read all data in one big batch, or at least group big chunks of data in the smallest possible number of queries. That’s it. Move on....

The post Thou Shalt Not Do Inserts in a Foreach … Unless You Know The Trick appeared first on Inchoo.

]]>
TL;DR;

Don’t do it. Just, simply, never write a database query inside a loop. Ever. Put in a little bit of effort and write code that will insert or read all data in one big batch, or at least group big chunks of data in the smallest possible number of queries. That’s it. Move on. These aren’t the droids you’re looking for.

But I’ve been blessed with the virtue of laziness!

Ah. Ok then, read on.

Being blessed with the same virtue myself, I’ve taken a shortcut here or there too, when i was sure no one knew it’s in a non-performance-critical piece of code.

But, that really clashed with my other virtues of impatience and hubris. What if that foreach gets a lot of data, and queries take forever, strangling the server? What if someone saw that code and got sick? Those questions kept me awake in the night and woke me, sweaty, in the wee hours. I could not look at my image in the mirror without a sense of shame.

Things could not go on like that any more.

So I learned of this little trick.

The Trick

Take a good, hard look at this piece of code:

$model = Mage::getModel('your_model/dbtable');
$data = [/* Array with lots of data */];
foreach ($data as $row){
    $model->setData($row)->save();
}

We have all seen it numerous times. Let’s admit it, we wrote code like this at some (bad) point in our life. We’ll call this one “stupid foreach”. What makes it so slow? Transactions and disk writes. For each iteration, database will make the lock, write data to it, unlock it, make a network request, etc.… And that takes time.
That’s why you really should do this instead, somewhere in your model resource:

$this->_getWriteAdapter()->insertMultiple($this->getMainTable(), $data);

That writes the data in 1 transaction, no matter how big it is. And it’s even less code than the stupid foreach! Yet, stupid foreach creeps into code again and again.

Enter the trick:

$model = Mage::getModel('your_model/dbtable');
$resource = $model->getResource();
$data = [/* Array with lots of data */];
 
$resource->beginTransaction();
foreach ($data as $row){
$model->setData($row)->save();
}
$resource->commit();

Yep, it still looks stupid, it’s the most code of the three, but this also gets the data into that database in one commit. And, it still works if you have a really messy foreach with database reads and writes combined.

Talk is cheap, show me the numbers

Here’s what the speed tests say (time in seconds):

100 iterations 1000 iterations 10000 iterations
Insert multiple 0.017 0.079 0.379
Foreach in a transaction 0.074 0.501 4.701
Stupid foreach 1.097 11.729 117.410

What’s the point?

What if you have a huge CSV that needs to be imported, and it just doesn’t fit in memory in one big batch? Reading and inserting line by line would work. What do you do when you come across badly written foreach inserts, and don’t have time or energy to refactor that? Or whatever. You know, stuff in the real world. Daily survival in backend development trenches.

The answer is simple. Yes, it’s always possible to do it properly and you really should, but if you can’t for whatever reason, at least do the trick. It’s just two lines of code around the foreach, and the performance is an order of magnitude better than the stupid foreach. Yeah, it’s still an order of magnitude worse than doing it properly, but it’s a horrible world out there.

The post Thou Shalt Not Do Inserts in a Foreach … Unless You Know The Trick appeared first on Inchoo.

]]>
http://inchoo.net/dev-talk/thou-shalt-not-do-inserts-in-a-foreach-unless-you-know-the-trick/feed/ 2