1. Introduction

GR8 CRM is a set of Grails Web Application Framework plugins that makes it easy to develop web applications with CRM functionality.

You can find more information about GR8 CRM on the main documentation site http://gr8crm.github.io.

1.1. Customer Relationship Management

Customer relationship management (CRM) is a system for managing a company’s interactions with current and future customers. It involves using technology to organize, automate and synchronize sales, marketing, customer service, and technical support. Wikipedia

The GR8 CRM "Ecosystem" currently contains over 40 Grails plugins. For a complete list of plugins see http://gr8crm.github.io.

Each GR8 CRM plugin defines a Bounded Context that focus on one specific domain, for example contact, project or document.

2. Content Management UI Plugin

This plugin provide a Twitter Bootstrap user interface for authoring content in GR8 CRM applications.

3. Groovy Server Pages

This section describes some useful Groovy Server Pages (GSP) included in this plugin, pages used for Content Management.

3.1. Attach documents to domain instances

crmContent/_embedded.gsp

If you have a page that shows information in tabs you can add an extra tab that display attachments. In this tab you can also let users upload files and even create new files attached to a domain instance.

The tab looks like this:

The content tab

As you can see the Files (5) tab displays existing attachments and also has buttons for creating and uploading new content.

To insert the extra tab in a .gsp page without modifying the .gsp source you include the following code in BootStrap.groovy.

BootStrap.groovy
class BootStrap {

    def crmCoreService
    def crmPluginService
    def crmContentService

    def init = { servletContext ->

        crmPluginService.registerView('crmContact', 'show', 'tabs',   (1)
            [id: "documents",
                index: 500,     (2)
                label: "crmContact.tab.documents.label",
                template: '/crmContent/embedded',
                plugin: "crm-content-ui",
                model: {
                    def id = crmCoreService.getReferenceIdentifier(crmContact)  (3)
                    def result = crmContentService.findResourcesByReference(crmContact) (4)
                    return [bean: crmContact, list: result, totalCount: result.size(),
                            reference: id, openAction: 'show']
                } (5)
            ]
        )
    }
}
1 Add the tab to crmContact/show.gsp
2 Tab order, lower number tabs are inserted to the left of higher number tabs.
3 The reference identifier is used to create a dynamic relation between the attachment and the domain instance.
4 Find existing files attached to the domain instance so the inserted view/tab can list them.
5 The model Closure have access to page scope in crmContact/show.gsp and can therefore reference the domain instance crmContact.

When content is uploaded the status is set to 'shared' by default. This can be changed with the config parameter 'crm.content.upload.opts'.

3.2. Working with folders and files

crmFolder/list.gsp

Included in this plugin is a simple File Manager. The list action in CrmFolderController displays folders and files. Users can create new folders and upload files. It is also possible to create new TEXT and HTML files using an embedded Rich Text editor.

The file manager

3.3. Create a new HTML file

crmContent/create.gsp

The create action in CrmContentController displays an embedded Rich Text editor (ckEditor) that lets the user create a new HTML document.

Rich Text Editor

The create action takes a parameter ref that should contain a Reference Identifier to the domain instance that the new content should be attached to. For example crmContent/create?ref=crmResourceFolder@42 would attach the newly created file to a folder with id 42.

4. Configuration

Pre-defined list of file names

When saving a text or html document it’s sometimes nessecary to enter a file name that matches some functionality. For example index.html for web pages. You can help the user by supplying a list of file names to use when saving the document.

crm.content.editor.filenames.html = ['index.html': 'Home page in default language', 'index_sv.html': 'Home page in Swedish']
crm.content.editor.filenames.plain = ['data.csv': 'Comma separated data']
editor filenames
Figure 1. File name suggestions
Default content when creating new documents

You can initialize new files with some text.

crm.content.editor.text.plain = 'Enter your plain text here...'
crm.content.editor.text.html = '<p>Enter your rich text here...</p>'
crm.content.editor.text.default = 'Lorem ipsum...'
Extra CSS in rich text editor
crm.content.editor.css = 'path to css file'

5. Extensions

Like most GR8 CRM plugins the crm-content plugin trigger events when important things happens in the system, when content is created, updated, deleted, etc. These events can be used to extend functionality. By listening to events in your application service and take actions. The following example shows how you can scale and crop images automatically when uploaded to a folder tagged with a dimension tag. The program Imagemagick is used on the server to scale images.

Example: If you add a tag with the name 1024x768 to a folder. Then all images uploaded to that folder will be resized to 1024x768 pixels in a "smart way", always trying to keep as much as possible of the image and focusing on the middle. The Imagemagick command convert is used on the server with the following options:

-resize <width>x<height>^
-gravity center
-crop <width>x<height>+0+0
+repage
-quality 50

Here is the complete source for the service that listens for crmContent.created events and perform the resizing.

CrmContentResizingService
package my.company

import grails.events.Listener
import grails.plugins.crm.core.Pair

import java.util.concurrent.TimeUnit
import java.util.regex.Pattern

/**
 * Scale uploaded images automatically.
 */
class CrmContentResizingService {

    def grailsApplication
    def crmContentService
    def crmTagService

    private static final DIMENSION_PATTERN = Pattern.compile(/(\d+)x(\d+)/)

    @Listener(namespace = 'crmContent', topic = 'created')
    def contentCreated(data) {
        filter(data)
    }

    @Listener(namespace = 'crmResourceRef', topic = 'updated')
    def contentUpdated(data) {
        filter(data)
    }

    private void filter(data) {
        // [tenant: ref.tenantId, id: ref.id, user: username, name: ref.name]

        String name = data.name ? data.name.toLowerCase() : ''
        def image = name.endsWith('.png') || name.endsWith('.jpg') || name.endsWith('.gif')

        Thread.sleep(2000) // Wait for the transaction to complete.

        def file = crmContentService.getResourceRef(data.id) (1)
        if (!file) {
            log.error("No content found with id: ${data.id}")
            return
        }

        if (!image) {
            // The file name did not tell us it was an image, what about the mime type?
            image = file.metadata.contentType.startsWith('image/')
        }

        if (image) {
            def (width, height) = getDimensions(file)
            if (width && height) {
                resize(file, width, height)
                crmTagService.setTagValue(file, "resized") (4)
            }
        }
    }

    /**
     * Find a tag with the format <width>x<height> on the resource or the resource's owner.
     *
     * @param file CrmResourceRef instance
     * @return width and height as a Pair, or 0x0 if no tags was found
     */
    private Pair<Integer, Integer> getDimensions(file) {
        def tags = crmTagService.getTagValue(file, null)
        if (tags) {
            if (tags.contains("resized")) {
                log.debug("File $file is already resized") (2)
                return new Pair<>(0, 0)
            }
        } else {
            def reference = file.reference
            if (!reference) {
                log.error("No instance found with reference: ${file.ref}")
                return new Pair<>(0, 0)
            }
            tags = crmTagService.getTagValue(reference, null)
        }

        def width = 0
        def height = 0
        for (value in tags) {
            def m = DIMENSION_PATTERN.matcher(value)
            if (m.find()) { (3)
                width = Integer.valueOf(m.group(1))
                height = Integer.valueOf(m.group(2))
                break
            }
        }

        return new Pair<>(width, height)
    }

    /**
     * convert in.jpg -resize "1920x1080^" -gravity center -crop 1920x1080+0+0 +repage -quality 50 out.jpg
     * @param file file to resize
     * @param width wanted width in pixels
     * @param height wanted height in pixels
     */
    private void resize(file, int width, int height) {
        final String command = grailsApplication.config.crm.content.convert.executable ?: "/usr/bin/convert"
        final File infile = File.createTempFile("crm", '.' + file.ext)
        final File outfile = File.createTempFile("crm", '.' + file.ext)

        infile.deleteOnExit()
        outfile.deleteOnExit()

        try {
            infile.withOutputStream { os ->
                file.writeTo(os)
            }
            Process p = new ProcessBuilder().inheritIO().command(command, infile.absolutePath,
                    "-resize", "${width}x${height}^",
                    "-gravity", "center",
                    "-crop", "${width}x${height}+0+0",
                    "+repage",
                    "-quality", "50",
                    outfile.absolutePath).start()
            p.waitFor(30, TimeUnit.SECONDS)
            int exitValue = p.exitValue()
            if (exitValue == 0) {
                outfile.withInputStream { is ->
                    crmContentService.updateResource(file, is) (5)
                }
                log.debug "File $file resized to ${width}x${height}"
            } else {
                log.error "Could not resize file $file, exit code: $exitValue"
            }
        } catch (Exception e) {
            log.error("Failed to resize file", e)
        } finally {
            infile.delete()
            outfile.delete()
        }
    }
}
1 Grab the uploaded/created image
2 If image is already resized we do nothing
3 If we find a tag with the form <width>x<height> we use the dimensions to scale the image
4 Add a tag to tell that this image was resized
5 Update the image, overwriting original content

6. Changes

2.4.5

Content-Disposition filename is now URL encoded. Adds support for CKEditor templates plugin.

2.4.4

Fixed problem with file edit icon not rendered correct.

2.4.3

Fixed wrong paths in crm-ckeditor-config.js.

2.4.2

UI tweaks. Edit button (pencil) is now hidden by default, visible by hovering the file icon in left column.

2.4.1

Default options for uploaded content can now be configured with 'crm.content.upload.opts'. crmContent/open now redirect to public endpoint if content is public.

2.4.0

Grails 2.4.x compatibility.

2.0.2

Action attachDocument on CrmContentController can now tag uploaded files with the tags parameter

2.0.1

Files can now be tagged (under content settings).
You can now update status on files directly from the embedded file list

2.0.0

First public release

7. License

This plugin is licensed with Apache License version 2.0

8. Source Code

The source code for this plugin is available at https://github.com/technipelago/grails-crm-content-ui

9. Contributing

Please report issues or suggestions.

Want to improve the plugin: Fork the repository and send a pull request.