Recent Changes - Search:

Administrators

Developers

Articles

Cookbook

edit

RecipeManagerTutorial

developers (intermediate)

Download the Recipe Manager Porcupine package file

Table of contents

  1. Introduction
  2. Creating the content classes
  3. Creating the initial content
  4. Designing a new recipe form
  5. The Recipe Manager application
  6. Packaging your application

1. Introduction

In this tutorial we will create a simple Porcupine application from scratch. This application is a simple recipe manager, used for storing and retrieving cooking recipes. The purpose of this tutorial is to teach you the steps required for building a Porcupine – QuiX application. The skills required to complete this tutorial are good knowledge of Python and JavaScript. The code sections that begin with three dots ("...") contain existing code, and they are mainly included for explanatory purposes. All the paths referred in this tutorial are relative to the Porcupine installation folder.

Before proceeding further, please install the project as described in Installation page. Linux users can take a look to the InstallationLinux page.

2. Creating the content classes

Create a new folder named "recipemanager" inside the "org/innoscript" folder. Add an empty "__init__.py" file to make it a Python package. Inside this folder create a new python script named "schema.py". This file will host our custom content classes. In the case of our sample application, the recipe manager, we will initially need two new content classes. The first content class is a container capable of keeping recipes and the second content class is the recipe itself. Open the blank "schema.py" with your favourite Pyhton editor. First, we need to import the primary Porcupine content classes and the primary data types:

from porcupine import systemObjects
from porcupine import datatypes

Next, we create the recipes' container content class:

class RecipeContainer(systemObjects.Container):
    containment = (
      'org.innoscript.recipemanager.schema.RecipeContainer',
      'org.innoscript.recipemanager.schema.CookingRecipe')

To define a new container type we need to subclass the system container (the "porcupine.systemObjects.Container" class). The "containment" class attribute is a tuple of strings, containing all the types of the Porcupine objects that this container type can accept. In this case, the specified container can either accept new containers of the same type or new recipes.

The recipe object must have the following attributes:

AttributeTypeMandatory
Display name(title)StringYes
DescriptionStringNo
CategoriesRelatorNNo
RatingIntegerNo
Preparation time (min)IntegerNo
ServingsIntegerYes
IngredientsTextYes
InstructionsTextYes

The "categories" attribute is of type "RelatorN" since each recipe can be attributed to more than one category (French cuisine, desert etc.). For reasons of simplicity, we will not create a new content class for the categorization of the recipes. Instead, we will use the existing "Category" content class.

The "Categories" data type class is already defined; it is used for the categorization of the documents. Thus, we just need to import the following module:

from org.innoscript.desktop.schema import properties

In order to define the relation both ways, we have to somehow add the "CookingRecipe" object type to the list of object types that can be categorized. This is accomplished by editing the "properties.py" file inside the "org/innoscript/desktop/schema" folder. The highlighted line should be added:

...
  class CategoryObjects(RelatorN):
    ...
    relCc = (
      'org.innoscript.desktop.schema.common.Document',
      'org.innoscript.desktop.schema.collab.Contact',
      'org.innoscript.recipemanager.schema.CookingRecipe',
    )
    ...
...

By default the primary data types are not mandatory. So we must create three new data type classes which subclass the primary data types.

class Servings(datatypes.Integer):
    isRequired = True


class Ingredients(datatypes.Text):
    isRequired = True


class Instructions(datatypes.Text):
    isRequired = True

Having defined all of our required data types, we proceed to the "CookingRecipe" content class definition:

class CookingRecipe(systemObjects.Item):
    "The recipe object"

    def __init__(self):
        systemObjects.Item.__init__(self)
        self.categories = properties.Categories()
        self.rating = datatypes.Integer()
        self.rating.value = 5
        self.preparationTime = datatypes.Integer()
        self.preparationTime.value = 30
        self.servings = Servings()
        self.servings.value = 4
        self.ingredients = Ingredients()
        self.instructions = Instructions()

The name of the "categories" attribute is obligatory. This is because we have a two-way many-to-many relationship between the "CookingRecipe" and "Category" objects. To clarify this, we need to examine the "Category" content type defined in the "org/innoscript/desktop/schema/common.py" file.

...
class Category(system.Container):
  ...
  def __init__(self):
    ...
    self.category_objects = properties.CategoryObjects()

The objects' references that belong in a specified category are stored inside the "category_objects" attribute. The data type of this attribute is the "CategoryObjects" class.

Now let's take a closer look at the "CategoryObjects" data type:

class CategoryObjects(RelatorN):
  ...
  relCc = (
    'org.innoscript.desktop.schema.common.Document',
    'org.innoscript.desktop.schema.collab.Contact',
    'org.innoscript.recipemanager.schema.CookingRecipe',
  )
  relAttr = 'categories'

The highlighted section simply indicates that each Porcupine object that needs to be categorized should have an attribute named "categories". If the "categories" attribute is a subclass of "Relator1", then we have established an one-to-many relationship. On the other hand, if the "categories" attribute is a subclass of "RelatorN", then the relationship is considered to be many-to-many.

3. Creating the initial content

The next step outlines the creation of the initial content required for our application to start. First, we will need a container where we can append our recipes. In order to achieve this, we need to temporarily edit the containment of the "RootFolder" content class, located inside "org/innoscript/desktop/schema/common.py".

class RootFolder(system.Container):
  ...
  containment = (
    'schemas.org.innoscript.common.Folder',
    'schemas.org.innoscript.collab.ContactsFolder',
    'org.innoscript.recipemanager.schema.RecipeContainer',
  )

Restart the server and login using an administrative account (admin/admin). Open the root folder (marked as Porcupine Server), right click on the list view and select "Create". Notice that the "RecipeContainer" type is added to the list of the allowed types:

Select the "RecipeContainer" type and create a new object named "Recipes".

Finally, we also need to create the recipe categories container. Double click the "Categories" folder and create a new category folder named "Recipe categories". Please note that in case your session has expired, you just have to login again.

Before restarting the server, you can safely delete the "RecipeContainer" entry from the containment list of the "RootFolder" content type.

4. Designing a new recipe form

The current implementation of the "new" web method bound to the base container "porcupine.systemObjects.Container" content class has some limitations that force us to design a new form for the "CookingRecipe" content type. Looking at the form that is automatically generated by the specific web method as defined in the "org/innoscript/desktop/webmethods/basecontainer.py" file, we notice the absence of the integer data types. This is happening because the web method, in its current state, ignores the numeric data types (a QuiX numeric input control, other than the spin button, is in the wish list).

Therefore, we have to create a new recipe form using a new Web method. We start by creating the Python package that will host our application's web methods. Create a new folder named "webmethods" inside the "org/innoscript/recipemanager" folder and then add an empty "__init__.py" file in order to make it a Python package. It is recommended to maintain a separate Python file for the Web methods of each content type.

Consequently, we create and edit a new Python file named "recipecontainer.py" inside the "webmethods" folder. The following imports are required:

from porcupine import webmethods
from porcupine import HttpContext

from org.innoscript.recipemanager.schema import CookingRecipe
from org.innoscript.recipemanager.schema import RecipeContainer

In order to overwrite the default form generated, when creating a new recipe, we must create a new web method as shown below:

@webmethods.quixui(of_type=RecipeContainer,
                   qs="cc=org.innoscript.recipemanager.schema.CookingRecipe",
                   template='ui.RecipeForm.quix',
                   max_age=1200)
def new(self):
    oRecipe = CookingRecipe()
    return {
        'URI': HttpContext.current().request.SCRIPT_NAME + '/' + self.id,
        'METHOD': 'create',
        'TITLE': 'Create new recipe',
        'HIDDEN': '<field name="CC" type="hidden" value="org.innoscript.recipemanager.schema.CookingRecipe" />',
        'DISPLAYNAME': oRecipe.displayName.value,
        'DESCRIPTION': oRecipe.description.value,
        'RATING': oRecipe.rating.value,
        'PREPARATION_TIME': oRecipe.preparationTime.value,
        'SERVINGS': oRecipe.servings.value,
        'INGREDIENTS': oRecipe.ingredients.value,
        'INSTRUCTIONS': oRecipe.instructions.value,
        'CATEGORIES': '',
        'ICON': oRecipe.__image__,
        'ACTION': 'Create',
    }

As you probably already have guessed, the server formats the contents of the quix file defined in the "template" argument using the dictionary returned along with the Python's built-in "string.Template" module.

This Web method is invoked only when the user needs to create a new recipe. This is achieved by the use of the "qs" argument. This argument ensures that this web method is called only when the query string of the request contains the string "cc=org.innoscript.recipemanager.schema.CookingRecipe". If not then the framework invokes the aforementioned base container's "new" Web method.

The QuiX XML definition of the form is a new file named "ui.RecipeForm.quix" inside the "org/innoscript/recipemanager/webmethods" folder.

<?xml version="1.0" ?>
<dialog xmlns="http://www.innoscript.org/quix" title="$TITLE" img="$ICON"
	close="true" width="480" height="380" left="30%" top="10%">
  <script name="Desktop Generic Functions" src="desktop/generic.js" /> 
  <module name="Desktop Widgets" src="desktop/widgets.js" depends="10,14,15" />
  <wbody>
    <form action="%(URI)s" method="%(METHOD)s" padding="4,4,4,4">
      $HIDDEN 
      <label caption="Recipe name:" />
      <field left="80" width="380" name="displayName" value="$DISPLAYNAME" />
      <label caption="Description:" top="25" />
      <field left="80" top="24" width="380" name="description" value="$DESCRIPTION" />
      <hr top="50" width="100%" />
      <label top="58" caption="Preparation time (min):" />
      <spinbutton name="preparationTime" top="55" left="110" width="50" max="240"
        value="$PREPARATION_TIME" editable="true" />
      <label top="58" left="200" caption="Servings:" />
      <spinbutton name="servings" top="55" left="250" width="40" max="12"
        editable="true" value="$SERVINGS" />
      <label top="58" left="320" caption="Rating:" />
      <spinbutton name="rating" top="55" left="360" width="40" max="10" editable="true"
        value="$RATING" />
      <tabpane top="90" width="100%" height="220">
        <tab caption="Ingredients">
          <field type="textarea" name="ingredients" width="100%" height="100%">$INGREDIENTS</field>
        </tab>
        <tab caption="Instructions">
          <field type="textarea" name="instructions" width="100%" height="100%">$INSTRUCTIONS</field>
        </tab>
        <tab caption="Categories">
          <custom classname="ReferenceN" width="100%" height="100%"
            root="Categories/Recipe categories"
            cc="org.innoscript.desktop.schema.common.Category"
            name="categories"
            value="$CATEGORIES"/>
        </tab>
      </tabpane>
    </form>
  </wbody>
    <dlgbutton onclick="generic.submitForm" width="70" height="22"
      caption="$ACTION" default="true" />
    <dlgbutton onclick="__closeDialog__" width="70" height="22"
      caption="Cancel" />
</dialog>

Initially, this form imports "generic.js"; one of the desktop's JavaScript files. From this script ("org/innoscript/desktop/generic.js") we reuse the "generic.submitForm" function. This function simply submits the first QuiX form found inside the current dialog.

...
generic.submitForm = function(evt, w) {
  var oDialog = w.getParentByType(Dialog);
  var oForm = oDialog.getWidgetsByType(Form)[0];
  oForm.submit(__closeDialog__);
}
...

The "submit" method of a QuiX form takes one optional argument; the function to call asynchronously, once the browser has the submission response. In this case the dialog is simply closed.

This form also uses a custom widget used for editing "RelatorN" data types. This custom widget is contained inside the "org/innoscript/desktop/widgets.js" JavaScript file. The "depends" attribute defines the dependencies required by the custom widgets contained in this file. This value contains the indices of the QuiX's modules as these are defined in the QuiX.modules array in the "quix/compat.js" file.

The form's "action" parameter is the URL where the form data get posted. This is usually the HTTP address of a Porcupine object whose type has a remote Web method with the same name as this defined in the "method" form attribute (visit http://wiki.innoscript.org/index.php/Articles/RequestProcessingPipeline to see how each object is accessible over HTTP).

Since this template will be used both for creating and editing recipes the name of the method is diferrent in each aspect. When creating a new recipe we call the "create" web method bound the "porcupine.systemObjects.Container" base class. This method can be found in the "org/innoscript/desktop/webmethods/basecontainer.py" file:

...
@webmethods.remotemethod(of_type=Container) 
def create(self, data):
    "Creates a new item"
    context = HttpContext.current()
    oNewItem = misc.getClassByName(data.pop('CC'))()
    # get user role
    iUserRole = objectAccess.getAccess(self, context.session.user)
    if data.has_key('__rolesinherited') and iUserRole == objectAccess.COORDINATOR:
        oNewItem.inheritRoles = data.pop('__rolesinherited')
        if not oNewItem.inheritRoles:
            acl = data.pop('__acl')
            if acl:
                security = {}
                for descriptor in acl:
                    security[descriptor['id']] = int(descriptor['role'])
                oNewItem.security = security

    # set props
    for prop in data:
        oAttr = getattr(oNewItem, prop)
        if isinstance(oAttr, datatypes.File):
            if data[prop]['tempfile']:
                oAttr.filename = data[prop]['filename']
                sPath = context.server.temp_folder + '/' + data[prop]['tempfile']
                oAttr.loadFromFile(sPath)
                os.remove(sPath)
        elif isinstance(oAttr, datatypes.Date):
            oAttr.value = data[prop].value
        elif isinstance(oAttr, datatypes.Integer):
            oAttr.value = int(data[prop])
        else:
            oAttr.value = data[prop]

    txn = db.getTransaction()
    oNewItem.appendTo(self, txn)
    txn.commit()
    return oNewItem.id
...

The "data" argument contains the form data. It is a Python dictionary containing the field names and values.

This method requires an additional field posted named "CC". This field contains the type of the object that is going to be appended to the calling container. Each XML-RPC method should always return a value. This remote method returns the ID of the newly created object.

Apart from creating recipes we should also be able to edit them. In order to override the default edit form, we need to override the "properties" web method of the "CookingRecipe" content class. Create a new Python module named "cookingrecipe.py" inside the "org/innoscript/recipemanager/webmethods" folder and add the following code:

from porcupine import webmethods
from porcupine import HttpContext

from org.innoscript.recipemanager.schema import CookingRecipe

@webmethods.quixui(of_type=CookingRecipe,
                   template='ui.RecipeForm.quix')
def properties(self):
    categories = []
    # build the categories
    for category in self.categories.getItems():
        categories.extend([category.__image__, category.id, category.displayName.value])
    return {
      'URI': HttpContext.current().request.SCRIPT_NAME + '/' + self.id,
      'METHOD': 'update',
      'TITLE': 'Recipe properties',
      'HIDDEN': '',
      'DISPLAYNAME': self.displayName.value,
      'DESCRIPTION': self.description.value,
      'RATING': self.rating.value,
      'PREPARATION_TIME': self.preparationTime.value,
      'SERVINGS': self.servings.value,
      'INGREDIENTS': self.ingredients.value,
      'INSTRUCTIONS': self.instructions.value,
      'CATEGORIES': ';'.join(categories),
      'ICON': self.__image__,
      'ACTION': 'Update',
    }

Respectively, when editing an existing recipe, we call the "update" Web method of the "Item" content type as defined in the "org/innoscript/desktop/webmethods/baseitem.py".

The "CATEGORIES" key of the dictionary returned contains a semicolon delimited string of "image;id;name" triplets; this format is required by the "RelatorN" editor custom widget.

Before restarting the server we need to import our newly created web methods for these to take effect. First edit the "org/innoscript/recipemanager/__init__.py" file by adding the following import:

from org.innoscript.recipemanager.webmethods import *

Then edit the "org/innoscript/recipemanager/webmethods/__init__.py" file:

__all__ = ['cookingrecipe', 'recipecontainer']

In this section we explored two different kinds of web methods:

  1. The porcupine.webmethods.quixui decorator is used for serving QuiX markup files defined using the "template" argument. This kind of web method is invoked over HTTP using the URL "http://SERVER_NAME/porcupine.py/OBJECT_ID?cmd=METHOD_NAME"
  2. The porcupine.webmethods.remotemethod decorator is used for serving RPC requests. The method is incorporated in the request body and these kind of methods are invoked by POSTing to "http://SERVER_NAME/porcupine.py/OBJECT_ID".

Restart the server. If everything is in place, you should see the following form when choosing to create a new recipe:

5. The Recipe Manager application

Apart from the custom forms and the custom content types, we will also need a custom UI for our application tailored to the given requirements. This is accomplished by creating a new application object. The Porcupine desktop provides generic interfaces and almost in every case, will not meet the requirements analysis.

Before creating our application object we must publish the "org/innoscript/recipemanager" direcotry. This directory will contain our applications XML UI definition and the required JavaScript functions. Edit the "conf/pubdir.xml" configuration file by adding the highlighted node:

<config>
	<dirs>
		<dir name="__quix" path="quix"/>
		<dir name="desktop" path="org/innoscript/desktop"/>
		<dir name="hypersearch" path="org/innoscript/hypersearch"/>
		<dir name="usermgmnt" path="org/innoscript/usermgmnt"/>
		<dir name="queryperformer" path="org/innoscript/queryperformer"/>
		<dir name="recipemanager" path="org/innoscript/recipemanager"/>
	</dirs>
</config>

Each published directory must contain a file called "config.xml". This file keeps track of the files accesible over HTTP. This file should be:

<config>
	<context path="recipemanager.quix"
		method="GET"
		client=".*"
		lang=".*"
		action="recipemanager.quix"/>
	<context path="recipemanager.js"
		method="GET"
		client=".*"
		lang=".*"
		action="recipemanager.js"/>
</config>

To create a new application, login to Porcupine as an administrator and navigate to the "Administrative Tools/Applications" folder. Inside the "Launch URL" input box type "recipemanager/recipemanager.quix". Press the "Create" button to create the application object.

Next, create a new text file named "recipemanager.quix" inside the "recipemanager" folder we just published.

Do not forget to replace the tree node's ID with the ID of the "Recipes" folder given by the system. The contents of this file are:

<?xml version="1.0" encoding="UTF-8"?>
<window xmlns="http://www.innoscript.org/quix" title="Recipe manager" resizable="true"
    close="true" minimize="true" maximize="true" width="640" height="480"
    left="center" top="center">
  <script name="Recipe manager Script" src="recipemanager/recipemanager.js"/>
  <wbody>
    <box width="100%" height="100%" orientation="v" spacing="0">
      <toolbar id="toolbar" height="28">
        <tbbutton caption="Create recipe folder" width="120" disabled="true">
          <prop name="cc" value="org.innoscript.recipemanager.schema.RecipeContainer"/>
        </tbbutton>
        <tbbutton caption="Create recipe" width="80" disabled="true">
          <prop name="cc" value="org.innoscript.recipemanager.schema.CookingRecipe"/>
        </tbbutton>
        <tbsep/>
        <tbbutton caption="Refresh" width="60"/>
      </toolbar>
      <splitter>
        <outlookbar width="200">
          <tool caption="Recipes" bgcolor="white">
            <foldertree id="tree" method="getSubtree" padding="4,4,4,4">
              <treenode id="[Put here the ID of the Recipes Porcupine folder]"
                haschildren="true" img="desktop/images/folder.gif" caption="Recipes"/>
            </foldertree>
          </tool>
          <tool caption="Search" bgcolor="white">
            <label caption="Recipe title contains:"/>
            <field id="title" left="5" top="20" width="160"/>

            <label top="50" caption="Preparation time is less than"/>
            <field id="preparationTime" left="5" top="70" width="30"/>
            <label top="72" left="40" caption="min."/>

            <label top="100" caption="The recipe's rating is greater than"/>
            <spinbutton top="120" left="5" width="40" max="10" value="0" id="rating"/>

            <label top="150" caption="Recipe ingredients contain:"/>
            <field id="ingredients" top="170" left="5" width="160"/>
            <label top="190" caption="(comma separated list)" style="font-style:italic"/>

            <button top="220" left="center" width="60" height="28" caption="Search"/>
          </tool>
        </outlookbar>
        <listview id="list">
          <listheader>
            <column width="140" caption="Recipe title" name="displayName"
              bgcolor="#EFEFEF" sortable="true"/>
            <column width="60" caption="Servings" type="int" name="servings"
              sortable="true"/>
            <column width="100" caption="Preparation time" type="int"
              name="preparationTime" sortable="true"/>
            <column width="40" caption="Rating" type="int" name="rating" sortable="true"/>
            <column width="160" caption="Date modified" type="date"
              name="modified" sortable="true"/>
          </listheader>
        </listview>
      </splitter>
    </box>
  </wbody>
</window>

The folder tree widget and the list view have been given an ID for them to become easily accessible from our scripts. The "method" attribute is the XML-RPC method to call whenever the user expands a tree node. Notice that the interface includes a JavaScript file with a "script" node right after the window's opening tag. Thus, create an empty JavaScript file named "recipemanager.js" inside the application folder.

Let's start by adding some event handlers for the toolbar's buttons:

...
<toolbar id="toolbar" height="28">
  <tbbutton caption="Create recipe folder" width="120" disabled="true"
    onclick="recipemanager.createItem">
    ...
  </tbbutton>
  <tbbutton caption="Create recipe" width="80" disabled="true"
    onclick="recipemanager.createItem">
    ...
  </tbbutton>
  <tbsep />
  <tbbutton caption="Refresh" width="60"
    onclick="recipemanager.refresh_onclick" />
</toolbar>

Insert the following functions inside the empty "recipemanager.js" file:

function recipemanager() {}

recipemanager.createItem = function(evt, w) {
  var oWin = w.getParentByType(Window);
  var oTree = oWin.getWidgetById('tree');
  var sCC = w.attributes.cc;
  var id = oTree.getSelection().getId();
  oWin.showWindow(id + '?cmd=new&cc=' + sCC,
    function(dlg) {
      dlg.attachEvent("onclose", recipemanager.dialogClose);
    }
  );
}

recipemanager.dialogClose = function(dlg) {
  if (dlg.buttonIndex == 0) {
    recipemanager.refreshList(dlg.opener);
  }
}

recipemanager.refresh_onclick = function(evt, w) {
  recipemanager.loadRecipes(w);
}
recipemanager.loadRecipes = function(w) {
  var appWin = w.getParentByType(Window);
  recipemanager.refreshList(appWin);
}

recipemanager.refreshList = function(appWin) {
  var oTree = appWin.getWidgetById('tree');
  var selection = oTree.getSelection();
  if (selection) {
    var id = selection.getId();
    var listView = appWin.getWidgetById('list');
    var sOql = "select id, displayName, servings, preparationTime, rating, " +
               "modified from '" + id + "' where contentclass = " +
               "'org.innoscript.recipemanager.schema.Recipe' order by displayName asc";
    var xmlrpc = new XMLRPCRequest(QuiX.root);
    xmlrpc.oncomplete = recipemanager.updateList;
    xmlrpc.callback_info = listView;
    xmlrpc.callmethod('executeOqlCommand', sOql);
  }
}

recipemanager.updateList = function(req) {
  req.callback_info.dataSet = req.response;
  req.callback_info.refresh();
}

The "recipemanager.createItem" handler displays the appropriate form based on the type of the object selected by the user. This is detected by the "cc" custom attribute assigned to the toolbar buttons using the "prop" nodes. The location of the new object is determined by the current tree selection. The "showWindow" method loads an XML UI definition for a new window or dialog from a specified URL, and opens it as a child window. The second argument of this method is a function called when the interface is loaded. The first argument of this function is the dialog created. In this event, the root widget - the form dialog - will be added to the child windows of our application's main window.

The "recipemanager.loadrecipes" handler is called once whenever the user presses the "Refresh" button. This handler loads the recipes from the selected container and displays them in the list view. The "getParentByType" QuiX widget method returns the first parent widget of the specified type. The only argument of this method is the constructor function of the parent we are searching for. The "Window" class is defined inside the "quix/windows.js" QuiX module.

The "recipemanager.refreshList" function executes the OQL query that returns the recipes contained inside the selected tree container. Inside this function we send a new XML-RPC request to the root folder. The "oncomplete" attribute of the request is the callback function to call when the query has completed. This callback function is always called having the request object as the first argument. The "callback_info" attribute is a placeholder for anything we would like to access inside the callback function. In this case, the "callback_info" is a reference to the list view widget.

Finally, the "recipemanager.updateList" callback function updates the "dataSet" attribute of the list view and then refreshes it. The "dataSet" attribute is an array of arbitrary objects. Such an array is directly returned from the XML-RPC call.

Afterwards, we must synchronize the tree with the list view. For each new tree selection we should update the list view accordingly. This is accomplished by adding the following event handler:

...
<foldertree id="tree" method="getSubtree" padding="4,4,4,4"
  onselect="recipemanager.loadRecipes">
  ...
</foldertree>
...

Next, we implement the search functionality. We start by adding a new "onclick" event handler on the search button:

...
<button top="220" left="center" width="60" height="28" caption="Search"
  onclick="recipemanager.search" />
...

Add the following function inside the "recipemanager.js" file:

recipemanager.search = function(evt, w) {
  var displayName = w.parent.getWidgetById('title').getValue();
  var preparationTime = w.parent.getWidgetById('preparationTime').getValue();
  var rating = w.parent.getWidgetById('rating').getValue
  var ingredients = w.parent.getWidgetById('ingredients').getValue();
  var sOql = "select id, displayName, servings, preparationTime, rating, " +
             "modified from deep('/Recipes')";
  var listView = w.getParentByType(Window).getWidgetById('list');
  var conditions = ["contentclass='schemas.org.innoscript.recipemanager.Recipe'"];
  if (displayName!='') {
    conditions.push("'" + displayName + "' in displayName");
  }
  if (preparationTime!='') {
    conditions.push("preparationTime < " + preparationTime);
  }
  if (rating>0) {
    conditions.push("rating > " + rating);
  }
  if (ingredients!='') {
    var arrIng = ingredients.split(',');
    for (var i=0; i<arrIng.length; i++) {
      conditions.push("'" + arrIng[i] + "' in ingredients");
    }
  }
  if (conditions.length > 0) {
    sOql += ' where ' + conditions.join(' and ');
    var xmlrpc = new XMLRPCRequest(QuiX.root);
    xmlrpc.oncomplete = recipemanager.updateList;
    xmlrpc.callback_info = listView;
    xmlrpc.callmethod('executeOqlCommand', sOql);
  }
}

The above function forms the OQL query according to the criteria set by the user on the search form. Notice that we search all the recipes, by using the "deep" operator at the select scope. Also when the user double clicks on the list view, the selected recipe dialog should open:

<listview id="list"
  ondblclick="recipemanager.loadItem">

The following event handler should be added to the script of the application:

recipemanager.loadItem = function(evt, w, recipe) {
  var oWin = w.getParentByType(Window);
  oWin.showWindow(recipe.id + "?cmd=properties",
    function(dlg) {
      dlg.attachEvent("onclose", recipemanager.dialogClose);
    }
  );
}

This handler opens the recipe form when a recipe item is double clicked on the list. It also attaches an "onclose" handler to the recipe form. In case of the recipe being updated this handler reloads the list items.

The final enhancement is the addition of a context menu. Add the following lines to the interface definition inside the list view widget:

<listview id="list" ondblclick="recipemanager.loadItem">
  <contextmenu onshow="recipemanager.contextmenu_onshow">
    <menuoption caption="Open" onclick="recipemanager.openRecipe" />
    <menuoption caption="Delete" onclick="recipemanager.deleteRecipe" />
  </contextmenu>
   ...
 </listview>

Add the following handlers to the application’s script:

recipemanager.contextmenu_onshow = function(menu) {
  var oList = menu.owner;
  menu.options[0].disabled = (oList.selection.length == 0); //open
  menu.options[1].disabled = (oList.selection.length == 0); //properties
}

recipemanager.openRecipe = function(evt, w) {
  var oList = w.parent.owner;
  var oWin = oList.getParentByType(Window);
  var selectedRecipe = oList.getSelection();
  oWin.showWindow(selectedRecipe.id + "?cmd=properties",
    function(dlg) {
      dlg.attachEvent("onclose", recipemanager.dialogClose);
    }
  );
}

recipemanager.deleteRecipe = function(evt, w) {
  var oList = w.parent.owner;
  var selectedRecipe = oList.getSelection();
  var desktop = document.desktop;
  var _deleteItem = function(evt, w) {
    var oMsgBox = w.getParentByType(Dialog);
    var xmlrpc = new XMLRPCRequest(QuiX.root + selectedRecipe.id);
    xmlrpc.oncomplete = function(req) {
      oMsgBox.close();
      recipemanager.loadRecipes(oList);
    }
    xmlrpc.onerror = function(req) { oMsgBox.close(); }
    xmlrpc.callmethod('delete');
  }
  desktop.msgbox(w.getCaption(),
    "Are you sure you want to delete this recipe?",
    [
      [desktop.attributes['YES'], 60, _deleteItem],
      [desktop.attributes['NO'], 60]
    ],
    'desktop/images/messagebox_warning.gif', 'center', 'center', 260, 112);
}

The first event handler is called whenever the context menu is displayed. The parent widget of every context menu is the desktop. Thus, there must be a way to get the menu's enclosing widget. The enclosing widget is accessible through the "owner" menu property.

The "document.desktop.msgbox" is used for displaying message boxes. The arguments accepted by this method are:

  1. The title of the message box.
  2. The message displayed in the message box.
  3. An array of the buttons. Each button is also an array of three elements. The first element is the caption of the button, the second is the width of the button, and the third is the function to call once the button is clicked. If omitted the button closes the message box.
  4. The image displayed inside the message box.
  5. The x-coordinate of the message box.
  6. The y-coordinate of the message box.
  7. The width of the message box.
  8. The height of the message box.

Next, log off and log on again. Launch the recipe manager application. If you see the following window, then congratulations, you have just created your first Porcupine application!

6. Packaging your application

Using the "pakager" utility we will consolidate all of the application’s pieces into a single distributable file. This procedure requires at least one package definition file. Create a new folder named "PKG" inside the porcupine installation directory. Inside this folder create a new file named "recipemanager.ini". Edit the contents of this file and add the following sections:

[package]
name=RecipeManager
version=0.0.5

The first section of the package definition file has information about the package (its name and its version).

[files]

The second section contains the relative paths to any files that need to be included in the package. Leave this section blank.

[dirs]

This section adds directories to the package.

[pubdir]
1=recipemanager

The fourth section contains the published directories that are usually contain all the files required by our application. In this case, we published the "org/innoscript/recipemanager" directory. This directory also contains all of our applications files. So, by including this section, the package will contain all the files that need to be distributed.

[items]
application=[Put here the ID of the Recipe Manager application]
recipes_folder=[Put here the ID of the "/Recipes" folder]
recipe_categories_folder=[Put here the ID of the "/Categories/Recipe categories" folder]

In this section we include all the Porcupine objects to export in the package, including the application object itself. Replace the highlighted portions with the object IDs printed on their properties dialog.

[scripts]
postinstall=PKG/postinstall.py
uninstall=PKG/uninstall.py

The "scripts" section contains Python script files included in the package. The "postinstall" executes immediately after the installation finishes. The "uninstall" script executes when the application is uninstalled.

Let’s see the contents of these scripts. Create a new Python script file inside the "PKG" directory named "postinstall.py". Open the file and add the following lines:

# RecipeManager post installation script
from porcupine.administration import codegen
ce = codegen.DatatypeEditor('org.innoscript.desktop.schema.properties.CategoryObjects')
ce.relCc.append('org.innoscript.recipemanager.schema.CookingRecipe')
ce.commitChanges()

The "codegen" module provides an essential API for runtime manipulation of the Porcupine objects and data types. This script modifies an existing data type (the "CategoryObjects" class). To be precise, it adds the "org.innoscript.recipemanager.schema.CookingRecipe" content class to the list of types that can be categorized (i.e. to the "relCc" class attribute of the data type). Similarly, inside the "PKG" folder create a new Python script named "uninstall.py". Open the file and add the following lines:

# RecipeManager uninstallation script
from porcupine.administration import codegen
ce = codegen.DatatypeEditor('org.innoscript.desktop.schema.properties.CategoryObjects')
ce.relCc.remove('org.innoscript.recipemanager.schema.CookingRecipe')
ce.commitChanges()

Before proceeding to the creation of the package, stop the Porcupine service. Afterwards, run the "pakager" utility as follows:

 $ python pakager.py –c –d PKG/recipemanager.ini

The Porcupine package file (RecipeManager-0.0.5.ppf) is created inside the Porcupine installation directory. The application can be installed on another Porcupine installation using the following command:

 $ python pakager.py –i –p RecipeManager-0.0.5.ppf
Page last modified on April 20, 2010, at 03:38 AM