Daylite Interactive Reports & Layouts

This document focuses on how the interactive web reports work.

How It Works

The mechanism is pretty simple. We take in a web report, look at the Info.plist to determine the initial action, parse the html file for that action according to the template language, execute, inline, the embedded fscript and template commands. Then we recombine the content into an html file, place it in a temporary folder and ask a WebKit view to render that html file.

The generated content can be of any language that WebKit supports, notably Javascript, CSS and HTML 5 (including SVG). For interactivity, you can employ any DOM manipulation technique you like. We include jQuery, but if you prefer a different Javascript library, you can simply include it in the report wrapper and reference it accordingly.

Contents of a Web Report Wrapper

A report wrapper is simply a folder with a .dl4wreport extension. The extension hides the contents of the folder from the end user. To view the contents if any report wrapper, simply right-click or control-click the wrapper in Finder and select "Show package contents".

During development, you store and work with reports in Home -> Library -> Application Support -> com.marketcircle.Daylite4/Templates/. Once you are happy with the results, you can store them in the database and share them in Preferences -> Report Templates.

A report wrapper can be flat with all files at the top level or you can organize your files into folders within. The minimum number of files in a wrapper is 2. The Info.plist and one html file. There is no maximum number of files or folders. A minimum example would look like this:

  • MyWebReport.dl4wreport
    • Info.plist
    • Main.html (or whatever you named it the Info.plist)
    • resources (any number of resources files such as CSS or Javascript if needed)

Info.plist The Info.plist file within the wrapper is mandatory. It describes to Daylite what kind of report this is and provides critical information such as the initial action, spec version, etc...

<?xml version="1.0" encoding="UTF-8"?>
        <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
        <plist version="1.0">
        <dict>
            <key>Behavior</key>
            <string>MCReportWebReportBehavior</string>
            <key>DisplayName</key>
            <string>Contacts by Top 5 Keywords</string>
            <key>classTypeName</key>
            <string>Contact</string>
            <key>SpecVersion</key>
            <integer>4</integer>
            <key>UniqueURLName</key>
            <string>mc-dl4-contactsbytop5keywords</string>
            <key>WebReportProperties</key>
            <dict>
              <key>InitialAction</key>
              <string>chart</string>
              <key>Actions</key>
              <dict>
                <key>chart</key>
                <dict>
                  <key>Filename</key>
                  <string>Main.html</string>
                </dict>
                <key>people</key>
                <dict>
                  <key>Filename</key>
                  <string>People.html</string>
                </dict>
                <key>companies</key>
                <dict>
                  <key>Filename</key>
                  <string>Companies.html</string>
                </dict>
              </dict>
            </dict>
        </dict>
        </plist>
        

Behaviour: The value of this key must be either MCReportWebReportBehavior for reports or MCReportWebPrintLayoutBehavior for print layouts. DisplayName: This is a name that the user will see in the reports list or in the print layout sheet. classTypeName: This is the name of the section the report appears in in the reports list or, for print layouts, which object type the print layout is valid for. SpecVersion: This value must be the integer 4 for wrappers to be read by Daylite 4. UniqueURLName: This value is a string and must be unique with no spaces or special characters or punctuation. Use unique prefixes to identify your or your company. For example: mc-dl4 (which is reserved). WebReportProperties: This is a dictionary that defines the initial action and for each action, specifies the filename. InitialAction: This is one item from the list of actions as specified in Actions. Actions: This value is a dictionary once again. Each item in the dictionary is another dictionary describing each action. Filename: This is the actual filename with extension in your wrapper. Note that when navigation from one action to another, you must use the action name and not the filename.

Main.html You need a least one file which, using the template language (described below), specifies what will be displayed. You can have multiple actions, with each action equating to an html file. You can pass arguments from one action to another using standard URL notation (described later).

The following in an example html file in which we show how FScript is embedded, how variables are passed from the FScript environment to the templating environment using globals.

<!DOCTYPE html>
        <html lang="en">
        {{fscript
            "We query the db directly instead of the options cache here because the options cache doesn't have such a combination"
            keywords := objectContext objectsForEntityNamed:'Keyword'
                qualifierLocum:'active == 1 and forContact == 1 and forOrganization == 1'
                bindings:nil.
        
            all := {}.
            others := {}.
        
            "query the db to see how many we actually have"
            keywords do:[:keyword |
                v1 := objectContext resultCountForEntityNamed:'ContactKeywordJoin'
                    qualifierLocum:'keywordID == $kID'
                    bindings:#{'kID' -> (keyword keywordID)}.
                v2 := objectContext resultCountForEntityNamed:'KeywordOrganizationJoin'
                    qualifierLocum:'keywordID == $kID'
                    bindings:#{'kID' -> (keyword keywordID)}.
        
                dict := #{'contacts' -> v1, 'organizations' -> v2,
                                'total' -> (v1 + v2), 'name' -> (keyword name),
                                'kID' -> (keyword keywordID)}.
        
                all add:dict.
            ].
        
            "Sort so we can figure out the 5 if we have more than 5. We have to use NSSortDescriptor instead of the less verbose FScript method because we are working with dictionaries at this point."
            all := all sortedArrayUsingDescriptors:{(NSSortDescriptor sortDescriptorWithKey:'total'
                                                                        ascending:false)}.
        
            (keywords count > 5) ifTrue: [
                top := all at:{0,1,2,3,4}.
                others := all difference:top.
        
                "we have to resort others..."
                others:= (others sortedArrayUsingDescriptors:{(NSSortDescriptor
                                        sortDescriptorWithKey:'total' ascending:false)}).
        
            ]
            ifFalse: [
                top := all.
            ].
        
            "Make it available to other fscripts within this template and to the template itself"
            globals setTop:top.
            globals setOthers:others.
        
            "now we iterate the top and morph it to work with the chart"
            labels := {}.
            values := {}.
        
            top do:[:dict |
                values add:{(dict valueForKey:'contacts'), (dict valueForKey:'organizations')}.
                labels add:(dict valueForKey:'name').
            ].
        
            "If we have others, it would be nice to see the total as compared to the rest"
            (others count > 0) ifTrue: [
                t1 := others valueForKeyPath:'@sum.contacts'.
                t2 := others valueForKeyPath:'@sum.organizations'.
        
                values add:{t1, t2}.
                labels add:'Others'.
            ].
        
            "The chart needs JSON, so we make the JSON available"
            globals setLabels:labels JSONRepresentation.
            globals setValues:values JSONRepresentation.
        
        }}
        <head>
            <meta charset="utf-8">
            <link rel="stylesheet" href="{{framework}}/default.css" type="text/css" charset="utf-8">
            <link rel="stylesheet" href="{{framework}}/screen.css" type="text/css" charset="utf-8" media="screen">
            <link rel="stylesheet" href="{{framework}}/print.css" type="text/css" charset="utf-8" media="print">
            <script src="{{framework}}/raphael.js"></script>
            <script src="{{framework}}/ico.js"></script>
            <script type="text/javascript">
                function $(id) {
                  return document.getElementById(id);
                }
            </script>
        
            <style type="text/css" media="screen">
            .linegraph { width: 720px; height: 400px; background-color: #fff; margin-left: 10px; margin-bottom: 30px;}
            .contacts {border: 5px #3399ff solid;}
            .organizations {border: 5px #6666cc solid;}
            </style>
        </head>
        <body>
            <h1>Contacts by Top 5 Keywords</h1>
            <p>Compare how people and companies have the same keywords. Only keywords that are set for both People and Companies are displayed here. Sorted by total.</p>
            <p><span class="contacts"></span>&nbsp&nbspPeople&nbsp&nbsp
                    <span class="organizations"></span>&nbsp&nbspCompanies</p>
        
            <div id="bargraph_5" class="linegraph"></div>
            <script type="text/javascript">
        
            new Ico.BarGraph($('bargraph_5'),
                {{values}}, {
                    grid: true,
                    grid_colour: '#eee',
                    colours: ['#3399ff', '#6666cc'],
                    bar_labels: true,
                    labels: {{labels}} });
            </script>
        
            <h3>Top 5 Keywords</h3>
            <table>
                <thead>
                        <td>Keyword</td>
                        <td width="15%">People</td>
                        <td width="15%">Companies</td>
                        <td width="15%">Total</td>
                </thead>
                <tbody>{{foreach dict top do}}
                    <tr>
                    <td>
                        {{dict.name}}
                    </td>
                    <td>
                        <a href="{{baseURL}}/people?keywordID={{dict.kID}}">{{dict.contacts}}</a>
                    </td>
                    <td>
                        <a href="{{baseURL}}/companies?keywordID={{dict.kID}}">{{dict.organizations}}</a>
                    </td>
                    <td>
                        {{dict.total}}
                    </td>
                    </tr>{{endforeach do}}
                </tbody>
            </table>
        
            {{if others.@count}}
            <h3>Others</h3>
            <table>
                <thead>
                        <td>Keyword</td>
                        <td width="15%">People</td>
                        <td width="15%">Companies</td>
                        <td width="15%">Total</td>
                </thead>
                <tbody>{{foreach dict others do}}
                    <tr>
                    <td>
                        {{dict.name}}
                    </td>
                    <td>
                        <a href="{{baseURL}}/people?keywordID={{dict.kID}}">{{dict.contacts}}</a>
                    </td>
                    <td>
                        <a href="{{baseURL}}/companies?keywordID={{dict.kID}}">{{dict.organizations}}</a>
                    </td>
                    <td>
                        {{dict.total}}
                    </td>
                    </tr>{{endforeach do}}
                </tbody>
            </table>
            {{endif}}
        
        
            <div id="reportfooter">
            Generated by {{objectContext.user.contact.cachedName}} on {{date}}.
            </div>
        
        </body>
        </html>
        

Template Language

If you are familiar with template languages such as Smarty or the MiscMerge, you'll be right at home. While the tokens and rules are different, the concept is the same with a start delimiter of {{ and an end delimiter of }}.

Field

A field and how you use it correlates directly with KVC. {{objectContext.className}} will return the class name of the objectContext as a string and insert it at that specific location in the template.

Foreach

Loop through an array of objects.

{{foreach entity entities do}}
        <a href="#{{entity.name}}">{{entity.displayName}}</a><br>
        {{endforeach do}}
        

The "foreach" command requires three parameters. The first parameter is a variable name to be used as an iterator. Each time through the loop, this variable will take on a new value. The second parameter is the name of a variable which contains an instance of the NSArray class (or one of its subclasses). The final parameter is a special tag. The "endforeach" command requires a single parameter, again a tag. The tags for each set of matching "foreach"/"endforeach" commands are expected to match. If they do not, then the template is assumed to be incorrectly constructed and the merge is aborted with an error message. Note: Loops set up with this command will always be passed through at least one time. If this is undesirable, then you should surround the loop construct with an if/then that tests for the case when the loop should be entirely skipped.

Conditional (if, else, endif)

These three commands allow conditional text output with a merge. Here would be a way to print out a different string of text based upon the value of the key "salary":


        Template:
        Congratulations!  You qualify for our offer for a free
        Visa {{if salary > 35000}}Gold{{else}}Classic{{endif}}
        card!
        
        Dictionary (Object):
        salary = "20000"
        
        Output:
        Congratulations!  You qualify for our offer for a free
        Visa Classic card!
        
        Dictionary (Object):
        salary = "40000"
        
        Output:
        Congratulations!  You qualify for our offer for a free
        Visa Gold card!
        

The conditionals accepted by the if command match the C operators: <, >, >=, <=, ==, !=. Also accepted are: =, <>, ><, =>, =<. The value on either side of the operator is tried as a merge dictionary key first. If no key exists, then its literal value is used. The comparison will be numeric if the strings begin with a number (such as the 35000, 20000, and 40000 in the above example). Complex "and", "or", and "not" expressions are not allowed. Similar effects can be obtained by nesting if statements, however, and this is the recommended procedure. Case types of statements may also be simulated this way.

Local Variable (identify)

This allows a value for a key to be determined. For example, "{{identify name = f0}}" will make the key "name" return "f0". As part of the field resolution performed by the merge engine, the key "f0" will then be searched for. If not found, the text "f0" would be returned, otherwise the value of the key "f0" would be returned. This allows aliases for key names to be created as well as simple setting the values of keys. Note that the statement requires an "=" or "==" operator for it to be parsed correctly.

Location in Array (index)

If one of the dictionary values is actually an instance of NSArray or one of its subclasses, then this command can be used to access a single string from the list. Two parameters are required. The first is the variable name and the second is the index (starting with one) of the string to use. As an example:


        Please hand me that {{index theList 2}}.
        
        Dictionary (Object) value is an NSArray:
        theList = ("apple", "banana", "orange")
        
        Output:
        Please hand me that banana.
        

Loop (loop, endloop)

These commands implement a loop. The "loop" command requires five parameters. The first parameter is a variable name to be used as an iterator. Each time through the loop, this variable will take on a new value. The second parameter is a start value, while the third parameter is an end value. The fourth parameter is a step value. The second, third, and fourth parameters are all integers; no floating point math is supported. The final parameter is a special tag. The "endloop" command requires a single parameter, again a tag. The tags for each set of matching "loop"/"endloop" commands are expected to match. If they do not, then the template is assumed to be incorrectly constructed and the merge is aborted with an error message.

He ate {{loop value 10 50 10 loop1}}{{value}}
        {{endloop loop1 }}times.
        
        Dictionary (Object):
        Output:
        He ate 10 20 30 40 50 times.
        

Note: Loops set up with this command will always be passed through at least one time. If this is undesirable, then you should surround the loop construct with an if/then that tests for the case when the loop should be entirely skipped.

FScript (fscript)

This command allows you to execute FScript. An FScript command can be placed anywhere in the template. You can see a number of examples of the recipes page. Whatever is returned from FScript is injected into the template, as such, you should be careful to return an NSString or an object that responds to description.


        {{fscript
            start := (NSDate date) mcDateByAddingDays:-100.
            end := NSDate date.
            dates := MCReportDateEnumerator allDailyDatesBetween:start end:end.
        
            vals := {}.
            dates do:[:date |
                begin := date mcDateAsBeginningOfDay.
                endDay := date mcDateAsEndOfDay.
        
                c := objectContext resultCountForEntityNamed:'Contact'
                    qualifierLocum:'createDate >= $start and createDate <= $end'
                    bindings:#{'start' -> begin, 'end' -> endDay}.
        
                vals add:#{'date' -> date mcLongDate, 'count' -> c}.
            ].
        
            globals setStats: (vals JSONRepresentation).
        }}
        

Order of Execution

The order of execution is linear. Starting at the top and ending at the bottom and going from left to right. A variable you share at the bottom will not be available at the top.

Passing Variables/Data Around

There are three environments you have to think about. The first is the template environment itself, the second is FScript execution contexts and the third is Javascript. Variables declared in the template environment are automatically inherited by a subsequent FScript context. The reverse is not true however. While in FScript, you must explicitly share a variable using globals.

Template Variables Variables declared in the template environment are automatically inherited by a subsequent FScript context. The example below illustrates how the key variable is made available to the FScript context. In the example below you see that key is declared as part of the foreach loop and both key and keys are automatically available to the subsequent fscript.

{{foreach key keys do1}}
        <tr>
        <td width="150px">{{key.displayName}}</td>
        <td width="200px">{{key.name}}</td>
        <td width="150px"><a href="{{fscript
            type := key objectForKey:'typeString'.
            val := '#' ++ type.
            applbase := 'http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/'.
        
            (type isEqual:'NSString') ifTrue: [
                val := (applbase ++ 'Foundation/Classes/NSString_Class/Reference/NSString.html').
            ].
        
            (type isEqual:'NSDate') ifTrue: [
                val := (applbase ++ 'Foundation/Classes/nsdate_Class/Reference/Reference.html').
            ].
        
            (type isEqual:'NSNumber') ifTrue: [
                val := (applbase ++ 'foundation/classes/nsnumber_class/Reference/Reference.html').
            ].
        
            (type isEqual:'NSImage') ifTrue: [
                val := (applbase ++ 'ApplicationKit/Classes/NSImage_Class/Reference/Reference.html').
            ].
        
            val.
        }}">{{key.typeString}}</a></td>
        <td width="70px">{{fscript
            val := key objectForKey:'isToMany'.
            str := ''.
        
            (val > 0) ifTrue: [
                str := 'Yes'.
            ].
        
            str.
        }}</td>
        <td>{{key.discussion}}</td>
        </tr>
        {{endforeach do1}}
        

FScript Variables Variables declared in FScript are local to that FScript execution context. If you want to share a variable between FScript contexts or with the template or with Javascript, you must declare the variable as a global by setting it on globals.

  globals setStats: (vals JSONRepresentation).
        

Declaring the variable follows the Objective-C getter/setter pattern. The variable is stats, the setter is setStats: and the getter is simply stats. You can add any number of variables. When setting a variable, it is retained and then released. In the rare event you have to alloc an object, remember to autorelease it, otherwise you will cause a memory leak.

Passing Data from FScript to Javascript To accomplish this, you have to use globals and the value has to be something that Javascript understands such as JSON.

globals setLabels:labels JSONRepresentation.
        globals setValues:values JSONRepresentation.
        
        
        <script type="text/javascript">
        new Ico.BarGraph($('bargraph_5'),
            {{values}}, {
                grid: true,
                grid_colour: '#eee',
                colours: ['#3399ff', '#6666cc'],
                bar_labels: true,
                labels: {{labels}} });
        </script>
        

Note how values and labels are passed to the graph. First set as JSON on globals and then placed as arguments using {{ and }}.

To produce JSON from FScript, you have to make sure you are using basic foundation object types such as NSString, NSNumber, NSArray, NSDictionary. You cannot get JSON directly out of MCPObjects (our persistent objects) at this point, so you will have to assemble the needed dictionaries if needed.

JSON does not define how to handle dates, so you might have to be a bit creative if you need to pass dates to a Javascript library.

Drilling Down and Passing Data from one Action to another

To drill down from one report page to another, you'll typically need to pass some arguments to the next page (action). You do this using standard URL notation. The URL prefix has to follow some rules however. The {{baseURL}} variable is critical in this case as it tells the system that you are move from page to page. After the base URL, you must specify the action name. In the example below we specify people which maps to the People.html as specified in the Info.plist. The variable are declared after the ?. In this case we have keywordID=12000. You separate arguments using the & character.

<a href="{{baseURL}}/people?keywordID={{dict.kID}}">The Hyperlink</a>
        

On the receiving page, the variables will be part of the globals, which you can then use to query the database.


        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="utf-8">
            <link rel="stylesheet" href="{{framework}}/default.css" type="text/css" charset="utf-8">
            <link rel="stylesheet" href="{{framework}}/screen.css" type="text/css" charset="utf-8" media="screen">
            <link rel="stylesheet" href="{{framework}}/print.css" type="text/css" charset="utf-8" media="print">
        </head>
        
        {{fscript
            "Using the keywordID, get the keyword from the database"
            keyword := objectContext objectForEntityNamed:'Keyword' wherePrimaryKeyValueIs:keywordID.
            globals setKeyword:keyword.
        
            "Get all the people that have this keyword using the keywordID"
            objs := objectContext objectsForEntityNamed:'Contact'
                qualifierLocum:'contactKeywords.keywordID == $kID'
                bindings:#{'kID' -> keywordID}.
        
            "Sort by cachedName"
            objs := (objs at:(objs cachedName sort)).
        
            "Make it available"
            globals setContacts:objs.
        }}
        
        <body>
            <h2>People with {{keyword.name}} keyword</h2>
            <table width="100%">
                <tr>
                    <td width="40%">Name</td>
                    <td width="40%">Email</td>
                    <td width="30%">City</td>
                </tr>
                {{foreach contact contacts do}}
                    <tr>
                        <td width="40%"><a href="daylite4://ShowObject/Contact/{{contact.contactID}}">{{contact.cachedName}}</a></td>
                        <td width="40%">{{contact.defaultEmailAddress.url}}</td>
                        <td width="30%">{{contact.defaultGeoAddress.city}}</td>
                    </tr>
                {{endforeach do}}
            </table>
        
            <div id="reportfooter">
            Generated by {{objectContext.user.contact.cachedName}} on {{date}}.
            </div>
        
        </body>
        </html>
        

Going from a report to a Specific Object in the UI

As you drill down from page to page, you may want to go to the actual object the report references. You can do so using a hyperlink. The format of the URL is daylite4://ShowObject//. This will open a new tab in Daylite and go to that object.

<a href="daylite4://ShowObject/Contact/{{contact.contactID}}">{{contact.cachedName}}</a>
        

Printing

The printing rules for interactive report or print layouts are the same as web page printing in Safari on Mac OS X. WebKit implements a subset of the Paged Media CSS spec. As a result, specifying headers and footers can be tricky. If pagination is important for a specific report or print layout, please consider using the PDF Reports mechanism.

You will notice that the examples provided include a print style sheet. At this time, we change some dotted lines to solid lines and we unhide any content within the reportfooter div.

<div id="reportfooter">
        Generated by {{objectContext.user.contact.cachedName}} on {{date}}.
        </div>
        

Included Libraries and Resources

We've included a few Javascript libraries and default, print and screen style sheets. To include the built in resources, simply prefix the resource name with {{framework}}.

<link rel="stylesheet" href="{{framework}}/default.css" type="text/css" charset="utf-8">
        

Keep in mind that you can include your own style sheets and javascripts. Simply add them to your report wrapper and references them using the {{resources}} prefix.

<link rel="stylesheet" href="{{resources}}/mystyle.css" type="text/css" charset="utf-8">
        

Development Tips

You can use a number of editors to edit an interactive report. Some commond examples follow that include syntax highlighting, but there are more. All you have to do with these editors is drop the .dl4wreport file on the dock icon of these apps. Remember to keep your reports in Home/Library/Application Support/com.marketcircle.Daylite4/Templates. If you add a new report while Daylite is running, control-click or right-click on the reports list and select Reload to reload the list of available reports.

Chocolat

Coda 2

TextMate

TextWrangler TextWrangler is a great text editor and best of all, it is free. Its big brother BBEdit is even better.

Debugging Tips

Looking at the HTML that is generated can be very helpful when tracking down problems. You can enable a "show in browser" button that allows you to open the generated code in a browser and use the debugger within the browser. In Terminal, type the following line, then restart Daylite.

defaults write com.marketcircle.Daylite4 MCReportsWebReportsDebug YES
        

With that defaults entry active, you will see the button.

The same defaults entry also generates contents in the console that may be handy.