RecipeManagerTutorialDevelopers.RecipeManagerTutorial HistoryHide minor edits - Show changes to markup April 19, 2010, at 11:38 PM
by - Initial update of the tutorial.
Changed lines 40-44 from:
__slots__ = ()
containment = (
'org.innoscript.recipemanager.schema.RecipeContainer?',
'org.innoscript.recipemanager.schema.CookingRecipe?'
)
to:
containment = (
'org.innoscript.recipemanager.schema.RecipeContainer?',
'org.innoscript.recipemanager.schema.CookingRecipe?')
Changed lines 46-47 from:
"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. It is worth mentioning that the special class attribute "__slots__" should not be omitted from any custom content class or data type, since instances of such types consume considerably less memory. to:
"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. Changed lines 99-100 from:
__slots__ = () isRequired = True to:
isRequired = True Changed lines 103-104 from:
__slots__ = () isRequired = True to:
isRequired = True Changed lines 107-108 from:
__slots__ = () isRequired = True to:
isRequired = True Changed lines 115-119 from:
"The recipe object"
__slots__ = (
'categories','rating','preparationTime',
'servings','ingredients','instructions'
)
to:
"The recipe object"
def __init__(self):
systemObjects.Item.__init__(self)
Changed line 122 from:
__props__ = systemObjects.Item.__props__ + __slots__ to:
self.categories = properties.Categories() Changed lines 126-127 from:
def __init__(self):
systemObjects.Item.__init__(self)
to:
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()
Changed lines 135-138 from:
self.categories = properties.Categories()
to:
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. Added lines 139-150:
...
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:
Changed lines 152-159 from:
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()
to:
class CategoryObjects?(RelatorN?): ...
relCc = (
'org.innoscript.desktop.schema.common.Document',
'org.innoscript.desktop.schema.collab.Contact',
'org.innoscript.recipemanager.schema.CookingRecipe?',
)
Changed lines 160-177 from:
The "__props__" class attribute is a special attribute used by the framework. It should be defined only in each new content class that has additional data type attributes. This attribute is a tuple of strings containing the names of all such attributes, including those of the super classes. 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:
to:
Changed lines 162-168 from:
class CategoryObjects?(RelatorN?): ...
relCc = (
'org.innoscript.desktop.schema.common.Document',
'org.innoscript.desktop.schema.collab.Contact',
'org.innoscript.recipemanager.schema.CookingRecipe?',
)
to:
relAttr = 'categories' Changed lines 164-168 from:
relAttr = 'categories' to:
Changed lines 173-174 from:
temporarily edit the containment of the "RootFolder" content class, located inside "common.py". to:
temporarily edit the containment of the "RootFolder" content class, located inside "org/innoscript/desktop/schema/common.py". Changed lines 192-195 from:
Restart the server and login using an administrative account. Open the root folder, right click on the list view and select "New". Notice that the "RecipeContainer" type is added to the list of the allowed types: to:
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: April 19, 2010, at 11:08 PM
by - Links to installation page have been added.
Added lines 21-22:
Before proceeding further, please install the project as described in Installation page. Linux users can take a look to the InstallationLinux page. January 07, 2009, at 04:07 PM
by -
Changed lines 3-4 from:
Download the Recipe Manager Porcupine package file to:
Download the Recipe Manager Porcupine package file August 10, 2008, at 04:55 PM
by - Updated to reflect the 0.5 API changes
Changed lines 3-8 from:
This tutorial is outdated. Please check back again for a new updated version.Download the Recipe Manager Porcupine package file to:
Download the Recipe Manager Porcupine package file August 10, 2008, at 10:20 AM
by -
Changed lines 50-51 from:
"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. It is worth mentioning that the special class attribute "__slots__" should not be omitted from any custom content class or data type, since instances of such types consume considerably less memory. to:
"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. It is worth mentioning that the special class attribute "__slots__" should not be omitted from any custom content class or data type, since instances of such types consume considerably less memory. Changed lines 69-71 from:
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: to:
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: Changed lines 151-152 from:
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. to:
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. Changed lines 161-164 from:
The objects references that belong in a specified category are stored inside the "category_objects" attribute. The data type of this attribute is the "org.innoscript.desktop.schema.properties.CategoryObjects" class. It is not considered a good practice to assign the same name to the attribute name and the data type class. Now lets take a closer look at the CategoryObjects data type: to:
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: Changed line 181 from:
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 to:
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 Changed lines 188-189 from:
temporarily edit the containment of the "RootFolder" content class, located inside "common.py". to:
temporarily edit the containment of the "RootFolder" content class, located inside "common.py". Changed line 225 from:
("porcupine.systemObjects.Container") content class has some to:
"porcupine.systemObjects.Container" content class has some Changed lines 233-235 from:
![]() Therefore, we have to create a new recipe form using a new web method. We start by creating the to:
Therefore, we have to create a new recipe form using a new Web method. We start by creating the Changed lines 237-238 from:
It is recommended to maintain a separate Python file for the web methods of each content type. to:
It is recommended to maintain a separate Python file for the Web methods of each content type. Changed lines 240-241 from:
package. The following imports are required: to:
folder. The following imports are required: Changed line 280 from:
As you propably already have guessed, the server formats the contents of the quix file defined in the to:
As you probably already have guessed, the server formats the contents of the quix file defined in the Changed line 284 from:
This web method is invoked only when the user needs to create a new recipe. This is achieved by the use to:
This Web method is invoked only when the user needs to create a new recipe. This is achieved by the use Changed lines 287-288 from:
If not then the framework invokes the aforementioned base container's "new" web method. to:
If not then the framework invokes the aforementioned base container's "new" Web method. Changed lines 373-376 from:
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 for 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). to:
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). Changed lines 478-479 from:
Respectively, when editing an existing recipe, we call the "update" method of the "porcupine.systemObjects.Item" content type as defined in the "org/innoscript/desktop/webmethods/baseitem.py". to:
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". Changed line 557 from:
Do not forget to replace the tree nodes ID with the ID of the Recipes folder given by the system. to:
Do not forget to replace the tree node's ID with the ID of the "Recipes" folder given by the system. Deleted line 562:
<?xml version="1.0" encoding="UTF-8"?> Changed lines 763-764 from:
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. to:
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. Changed lines 958-959 from:
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 menus enclosing widget. The enclosing widget is accessible through the "owner" menu property. to:
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. Changed lines 1033-1034 from:
Lets 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: to:
Lets 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: August 10, 2008, at 09:10 AM
by -
Changed lines 162-163 from:
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. It is not considered a good practice to assign the same name to the attribute name and the data type class. to:
The objects references that belong in a specified category are stored inside the "category_objects" attribute. The data type of this attribute is the "org.innoscript.desktop.schema.properties.CategoryObjects" class. It is not considered a good practice to assign the same name to the attribute name and the data type class. Changed lines 317-318 from:
<spinbutton name="preparationTime" top="55" left="110" width="50" max="240" value="$PREPARATION_TIME" editable="true" /> to:
<spinbutton name="preparationTime" top="55" left="110" width="50" max="240"
value="$PREPARATION_TIME" editable="true" />
Changed lines 320-321 from:
<spinbutton name="servings" top="55" left="250" width="40" max="12" editable="true" value="$SERVINGS" /> to:
<spinbutton name="servings" top="55" left="250" width="40" max="12"
editable="true" value="$SERVINGS" />
Changed lines 323-324 from:
<spinbutton name="rating" top="55" left="360" width="40" max="10" editable="true" value="$RATING" /> to:
<spinbutton name="rating" top="55" left="360" width="40" max="10" editable="true"
value="$RATING" />
Changed lines 590-591 from:
<treenode id="[Put here the ID of the Recipes Porcupine folder]" haschildren="true" img="desktop/images/folder.gif" caption="Recipes"/> to:
<treenode id="[Put here the ID of the Recipes Porcupine folder]"
haschildren="true" img="desktop/images/folder.gif" caption="Recipes"/>
Changed lines 623-625 from:
<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"/>
to:
<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"/>
Changed lines 630-631 from:
<column width="160" caption="Date modified" type="date" name="modified" sortable="true"/> to:
<column width="160" caption="Date modified" type="date"
name="modified" sortable="true"/>
August 10, 2008, at 08:48 AM
by -
Changed line 348 from:
<a:dlgbutton onclick="generic.submitForm" width="70" height="22" to:
<dlgbutton onclick="generic.submitForm" width="70" height="22" Changed line 353 from:
<a:dlgbutton onclick="__closeDialog__" width="70" height="22" to:
<dlgbutton onclick="__closeDialog__" width="70" height="22" Changed line 355 from:
</a:dialog> to:
</dialog> Changed lines 759-760 from:
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. to:
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. Changed line 1046 from:
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). to:
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). August 10, 2008, at 07:40 AM
by -
Changed line 982 from:
version=0.0.4 to:
version=0.0.5 Changed lines 985-986 from:
The first section of the package definition file has information about the package (its name and its version). Please notice that the name of the package and the value of the name attribute of the package node in the "store.xml" configuration file must be identical. to:
The first section of the package definition file has information about the package (its name and its version). Changed lines 1007-1008 from:
The fourth section contains the published directories (usually containing external JavaScript files and images) that are required by the 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. to:
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. Changed lines 1014-1015 from:
recipe_categories_folder=[Put here the ID of the "/Categories/Recipe categories" folder] to:
recipe_categories_folder=[Put here the ID of the "/Categories/Recipe categories" folder] Changed line 1039 from:
ce.relCc.append('org.innoscript.recipemanager.schema.Recipe') to:
ce.relCc.append('org.innoscript.recipemanager.schema.CookingRecipe?') Changed line 1046 from:
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.Recipe" content class to the list of types that can be categorized (i.e. to the "relCc" class attribute of the data type). to:
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). Changed line 1057 from:
ce.relCc.remove('org.innoscript.recipemanager.schema.Recipe') to:
ce.relCc.remove('org.innoscript.recipemanager.schema.CookingRecipe?') Changed lines 1068-1070 from:
The Porcupine package file (RecipeManager-0.0.3.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.3.ppf to:
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 August 10, 2008, at 07:30 AM
by -
Changed lines 498-500 from:
to:
August 10, 2008, at 07:25 AM
by -
Changed line 289 from:
of the request contains the string "cc=org.innoscript.recipemanager.schema.CookingRecipe?". to:
of the request contains the string "cc=org.innoscript.recipemanager.schema.CookingRecipe". Changed lines 373-374 from:
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 for the custom widgets contained in this file. This value contains the indexes of the QuiX's modules as these are defined in the QuiX.modules array in the "quix/compat.js" file. to:
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 for 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. Changed lines 480-481 from:
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. to:
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. Changed lines 489-490 from:
The edit the "org/innoscript/recipemanager/webmethods/__init__.py" file: to:
Then edit the "org/innoscript/recipemanager/webmethods/__init__.py" file: Added lines 496-500:
In this section we explored two different kinds of web methods:
Changed lines 563-582 from:
<a:window xmlns:a="http://www.innoscript.org/quix" title="Recipe manager" resizable="true" close="true" minimize="true" maximize="true" img="" width="640" height="480" left="center" top="center"> <a:script name="Recipe manager Script" src="recipemanager/recipemanager.js"/>
<a:wbody>
<a:box width="100%" height="100%" orientation="v" spacing="0">
<a:toolbar height="28">
<a:tbbutton caption="Create recipe folder" width="120">
<a:prop name="cc" value="org.innoscript.recipemanager.schema.RecipeContainer?"/>
</a:tbbutton>
<a:tbbutton caption="Create recipe" width="80">
<a:prop name="cc" value="org.innoscript.recipemanager.schema.Recipe"/>
</a:tbbutton>
<a:tbsep/>
<a:tbbutton caption="Refresh" width="60"/>
</a:toolbar>
<a:splitter height="-1" orientation="v" interactive="true">
<a:pane length="200">
<a:outlookbar width="100%" height="100%">
<a:tool caption="Recipes">
to:
<?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">
Changed lines 586-588 from:
<a:foldertree id="tree" method="getSubtree" padding="4,4,4,4">
<a:treenode id="[Put here the ID of the Recipes Porcupine folder]" haschildren="true"
img="desktop/images/folder.gif" caption="Recipes"/>
to:
<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>
Changed lines 592-613 from:
</a:foldertree>
</a:tool>
<a:tool caption="Search" bgcolor="gray">
<a:label caption="Recipe title contains:"/>
<a:field id="title" left="5" top="20" width="160"/>
<a:label top="50" caption="Preparation time is less than"/>
<a:field id="preparationTime" left="5" top="70" width="30"/>
<a:label top="72" left="40" caption="min."/>
<a:label top="100" caption="The recipe's rating is greater than"/>
<a:spinbutton top="120" left="5" width="40" max="10" value="0" id="rating"/>
<a:label top="150" caption="Recipe ingredients contain:"/>
<a:field id="ingredients" top="170" left="5" width="160"/>
<a:label top="190" caption="(comma separated list)" style="font-style:italic"/>
<a:button top="220" left="center" width="60" height="28" caption="Search"/>
</a:tool>
</a:outlookbar>
</a:pane>
<a:pane length="-1">
to:
</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>
Changed line 614 from:
<a:listview width="100%" height="100%" id="list"> to:
<listview id="list"> Changed lines 618-630 from:
<a:listheader>
<a:column width="140" caption="Recipe title" name="displayName" bgcolor="#EFEFEF" sortable="true"/>
<a:column width="60" caption="Servings" type="int" name="servings" sortable="true"/>
<a:column width="100" caption="Preparation time" type="int" name="preparationTime" sortable="true"/>
<a:column width="40" caption="Rating" type="int" name="rating" sortable="true"/>
<a:column width="160" caption="Date modified" type="date" name="modified" sortable="true"/>
</a:listheader>
</a:listview>
</a:pane>
</a:splitter>
</a:box>
</a:wbody>
</a:window> to:
<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> Changed lines 635-636 from:
Let's start by adding some event handlers. to:
Let's start by adding some event handlers for the toolbar's buttons: Changed lines 640-641 from:
<a:toolbar width="100" height="100"> <a:tbbutton caption="Create recipe folder" width="120" to:
<toolbar id="toolbar" height="28"> <tbbutton caption="Create recipe folder" width="120" disabled="true" Changed lines 650-651 from:
</a:tbbutton> <a:tbbutton caption="Create recipe" width="80" to:
</tbbutton> <tbbutton caption="Create recipe" width="80" disabled="true" Changed lines 660-662 from:
</a:tbbutton> <a:tbsep /> <a:tbbutton caption="Refresh" width="60" to:
</tbbutton> <tbsep /> <tbbutton caption="Refresh" width="60" Changed lines 670-672 from:
</a:toolbar> ... <a:listview id="list" multiple="true" width="100" height="100" to:
</toolbar> Changed lines 672-675 from:
to:
Insert the following functions inside the empty "recipemanager.js" file:
Changed lines 677-681 from:
onload="recipemanager.loadRecipes"> to:
function recipemanager() {} recipemanager.createItem = function(evt, w) { var oWin = w.getParentByType(Window);
var oTree = oWin.getWidgetById('tree');
Changed line 683 from:
to:
Changed line 685 from:
... to:
var sCC = w.attributes.cc; Deleted lines 686-688:
Insert the following functions inside the empty "recipemanager.js" file: Changed lines 689-693 from:
function recipemanager() {} recipemanager.createItem = function(evt, w) { var oWin = w.getParentByType(Window);
var oTree = oWin.getWidgetById('tree');
to:
var id = oTree.getSelection().getId(); Changed lines 693-697 from:
var sCC = w.attributes.cc; to:
oWin.showWindow(id + '?cmd=new&cc=' + sCC,
function(dlg) {
dlg.attachEvent("onclose", recipemanager.dialogClose);
}
);
Deleted lines 700-711:
var id = oTree.getSelection().getId(); @]
oWin.showWindow(id + '?cmd=new&cc=' + sCC,
function(dlg) {
dlg.attachEvent("onclose", recipemanager.dialogClose);
}
);
[@ Changed lines 725-729 from:
var id = oTree.getSelection().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";
to:
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";
Changed lines 735-737 from:
var xmlrpc = new XMLRPCRequest?(QuiX?.root); xmlrpc.oncomplete = recipemanager.updateList; xmlrpc.callback_info = listView; to:
var xmlrpc = new XMLRPCRequest?(QuiX?.root); xmlrpc.oncomplete = recipemanager.updateList; xmlrpc.callback_info = listView; Changed lines 741-742 from:
xmlrpc.callmethod('executeOqlCommand', sOql);
to:
xmlrpc.callmethod('executeOqlCommand', sOql);
}
Changed lines 757-760 from:
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 "a: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 when the list view is loaded. This handler loads the recipes from the selected container and displays them in the list view. The "onload" event handler, unlike the other events, does not accept the two common event handler arguments, the event object and the widget, but instead accepts only the widget that fires the event. 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. to:
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. Changed lines 762-763 from:
contained inside the selected tree container. Inside this function we send a new XMLRPC request to the root folder. The "oncomplete" attribute of the request is the callback function to call when the query has completed. This function is always called having the request object as the first argument. to:
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. Changed lines 767-769 from:
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 XMLRPC call. Afterwards, we must synchronize the tree with the list view. This means that for each new tree to:
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 Changed line 776 from:
<a:foldertree id="tree" method="getSubtree" padding="4,4,4,4" to:
<foldertree id="tree" method="getSubtree" padding="4,4,4,4" Changed line 785 from:
</a:foldertree> to:
</foldertree> Changed line 794 from:
<a:button top="220" left="center" width="60" height="28" caption="Search" to:
<button top="220" left="center" width="60" height="28" caption="Search" Changed lines 854-855 from:
<a:listview multiple="true" width="100" height="100" id="list" onload="recipemanager.loadRecipes" to:
<listview id="list" Changed lines 877-879 from:
The final enhancement is the addition of a context menu. Open the application object and add the following lines to the interface definition: to:
The final enhancement is the addition of a context menu. Add the following lines to the interface definition inside the list view widget: Changed line 882 from:
<a:pane length="-1"> to:
<listview id="list" ondblclick="recipemanager.loadItem"> Changed lines 886-889 from:
<a:contextmenu onshow="recipemanager.contextmenu_onshow">
<a:menuoption caption="Open" onclick="recipemanager.openRecipe" />
<a:menuoption caption="Delete" onclick="recipemanager.deleteRecipe" />
</a:contextmenu>
to:
<contextmenu onshow="recipemanager.contextmenu_onshow">
<menuoption caption="Open" onclick="recipemanager.openRecipe" />
<menuoption caption="Delete" onclick="recipemanager.deleteRecipe" />
</contextmenu>
Changed lines 893-897 from:
<a:listview width="100" height="100" id="list"
onload="recipemanager.loadRecipes" ondblclick="recipemanager.loadItem">
...
</a:listview>
</a:pane> to:
... </listview> Changed line 905 from:
var oList = menu.owner.getWidgetById('list');
to:
var oList = menu.owner; Changed line 914 from:
var oList = w.parent.owner.getWidgetById('list');
to:
var oList = w.parent.owner; Changed line 925 from:
var oList = w.parent.owner.getWidgetById('list');
to:
var oList = w.parent.owner; Changed line 956 from:
The "desktop.msgbox" is used for displaying message boxes. The arguments accepted by this to:
The "document.desktop.msgbox" is used for displaying message boxes. The arguments accepted by this August 09, 2008, at 08:20 PM
by -
Deleted lines 71-74:
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: Added lines 74-80:
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: [@ August 09, 2008, at 08:17 PM
by -
Changed lines 1074-1075 from:
python pakager.py c d PKG/recipemanager.ini to:
$ python pakager.py c d PKG/recipemanager.ini Changed line 1078 from:
python pakager.py i p RecipeManager?-0.0.3.ppf to:
$ python pakager.py i p RecipeManager-0.0.3.ppf August 09, 2008, at 08:16 PM
by -
Changed line 1078 from:
to:
python pakager.py i p RecipeManager?-0.0.3.ppf August 09, 2008, at 08:11 PM
by -
Changed line 45 from:
'org.innoscript.recipemanager.schema.Recipe' to:
'org.innoscript.recipemanager.schema.CookingRecipe?' Changed lines 74-75 from:
In order to define the relation both ways, we have to somehow add the "Recipe" object to the list of objects 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: to:
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: Changed line 87 from:
'org.innoscript.recipemanager.schema.Recipe', to:
'org.innoscript.recipemanager.schema.CookingRecipe?', Changed lines 111-112 from:
Having defined all of our required data types, we proceed to the "Recipe" content class definition: to:
Having defined all of our required data types, we proceed to the "CookingRecipe" content class definition: Changed line 115 from:
class Recipe(systemObjects.Item): to:
class CookingRecipe?(systemObjects.Item): Changed lines 149-150 from:
The name of the "categories" attribute is obligatory. This is because we have a two-way many-to-many relationship between the "Recipe" and "Category" objects. To clarify this, we need to examine the "Category" content type defined in the "common.py" file. to:
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. Changed line 170 from:
'org.innoscript.recipemanager.schema.Recipe', to:
'org.innoscript.recipemanager.schema.CookingRecipe?', Changed lines 211-221 from:
Select the "RecipeContainer" type and create a new object named "Recipes". If you double click on the new folder you should get the following error: ![]() This is because we have not yet defined a valid registration (servlet binding) for the "RecipeContainer" content class. Hence, we have to edit the "store.xml" file inside the "conf" folder. Since these are the first servlet registrations we create for our application, we must enclose it inside a new package node. Locate the "packages" node at the top of the afore-mentioned file and append the following nodes: to:
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 formThe 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" package. The following imports are required: Changed lines 244-247 from:
<package name="RecipeManager?"> <reg cc="org.innoscript.recipemanager.schema.RecipeContainer?" method="POST" param="" to:
from porcupine import webmethods from porcupine import HttpContext? from org.innoscript.recipemanager.schema import CookingRecipe? from org.innoscript.recipemanager.schema import RecipeContainer? Changed lines 250-254 from:
to:
In order to overwrite the default form generated, when creating a new recipe, we must create a new web method as shown below:
Changed lines 256-277 from:
client="vcXMLRPC" to:
@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', } Added lines 279-291:
As you propably 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. Changed lines 294-299 from:
lang=".*" to:
<?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> Changed line 303 from:
action="org.innoscript.desktop.XMLRPC.ContainerGeneric?"/> to:
<form action="%(URI)s" method="%(METHOD)s" padding="4,4,4,4"> Changed lines 307-309 from:
<reg cc="org.innoscript.recipemanager.schema.RecipeContainer?" method="GET" param="new" to:
$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">
Changed lines 330-334 from:
client="(MSIE [6-7])|(Mozilla/5.0.+rv:1.[7-9])" to:
<custom classname="ReferenceN?" width="100%" height="100%" root="Categories/Recipe categories" cc="org.innoscript.desktop.schema.common.Category" name="categories" value="$CATEGORIES"/> Changed lines 338-341 from:
lang=".*" to:
</tab>
</tabpane>
</form>
</wbody>
Changed line 345 from:
action="org.innoscript.desktop.ui.Frm_AutoNew"/> to:
<a:dlgbutton onclick="generic.submitForm" width="70" height="22" Changed lines 349-352 from:
</package> to:
caption="$ACTION" default="true" />
<a:dlgbutton onclick="__closeDialog__" width="70" height="22"
caption="Cancel" />
</a:dialog> Changed lines 355-387 from:
As you can see, the first registration takes care of the XMLRPC methods exposed by the "RecipeContainer" content type. For the time being, we need no special behavior; the "RecipeContainer" type exposes the same methods as any other container. Also, notice that the client parameter is set to "vcXMLRPC". This should be true for every XMLRPC servlet we publish. The second registration defines the servlet to be executed when the user chooses to create a new object inside a "RecipeContainer" folder. Because the "org.innoscript.desktop.ui.Frm_AutoNew" is a QuiX? servlet (instance of XULServlet), we should always check for browser compatibility by allowing this servlet to run only on the supported browsers. What this servlet actually does is to render a generic form based on the attributes of the new Porcupine object we are about to create. 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 of the "RootFolder" content type. 4. Designing a new recipe formThe current implementation of the "org.innoscript.desktop.ui.Frm_AutoNew" servlet has some limitations that force us to design a new form for the "Recipe" content type. Looking at the form that is automatically generated by the servlet, we notice the absence of the integer data types. This is happening because the servlet, 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 QuiX servlet. We start by creating the Python module that will host our application servlets. It is recommended to maintain the QuiX servlets and the XMLRPC servlets in separate Python files named "ui.py" and "XMLRPC.py" respectively. Consequently, we create and edit a new Python file named "ui.py" inside the "recipemanager" module. First, we import the "XULServlet" class and the content classes module we just created: to:
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. Changed lines 359-361 from:
from porcupine.core.servlet import XULServlet? from org.innoscript.recipemanager import schema to:
... generic.submitForm = function(evt, w) { var oDialog = w.getParentByType(Dialog); var oForm = oDialog.getWidgetsByType(Form)[0]; oForm.submit(__closeDialog__); } ... Changed lines 368-376 from:
Now comes the tricky part. In order to overwrite the default form generated, when creating a new recipe, we must create two new registrations. One for each type accepted by the "RecipeContainer" type (recipe containers and recipes). For the recipe container, we will use the desktop's "Frm_Auto" servlet, but for the recipe items we will use our own servlet which outputs our custom QuiX form. This is shown below: Before proceeding to the recipe form itself, we must modify the "store.xml" configuration file by adding modifying the highlighted lines: to:
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 for the custom widgets contained in this file. This value contains the indexes 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: Changed lines 378-384 from:
<package name="RecipeManager?"> <reg cc="org.innoscript.recipemanager.schema.RecipeContainer?" method="POST" param="" client="vcXMLRPC" lang=".*" action="org.innoscript.desktop.XMLRPC.ContainerGeneric?"/> to:
... Changed lines 382-404 from:
<reg cc="org.innoscript.recipemanager.schema.RecipeContainer?" qs="org.innoscript.recipemanager.schema.RecipeContainer?" client="(MSIE [6-7])|(Mozilla/5.0.+rv:1.[7-9])" lang=".*" method="GET" param="new" action="org.innoscript.desktop.ui.Frm_AutoNew"> <filter type="porcupine.filters.postProcessing.multilingual.Multilingual" using="org.innoscript.desktop.strings.resources"/> </reg> <reg cc="org.innoscript.recipemanager.schema.RecipeContainer?" qs="org.innoscript.recipemanager.schema.Recipe" client="(MSIE [6-7])|(Mozilla/5.0.+rv:1.[7-9])" lang=".*" method="GET" param="new" action="org.innoscript.recipemanager.ui.RecipeForm?"/> <reg cc="org.innoscript.recipemanager.schema.Recipe$" method="GET" param="properties" client="(MSIE [6-7])|(Mozilla/5.0.+rv:1.[7-9])" lang=".*" action="org.innoscript.recipemanager.ui.RecipeForm?"/> to:
@webmethods.remotemethod(of_type=Container) def create(self, data): Changed lines 387-388 from:
<package> to:
"Creates a new item"
context = HttpContext?.current()
Changed lines 390-412 from:
The first highlighted registration assigns the "Frm_AutoNew" as the servlet to be executed when the browser GETs on a "RecipeContainer" object with the "cmd" query parameter set to "new" and the query string contains the string "org.innoscript.recipemanager.schema.RecipeContainer" (this is the new "qs" parameter). The second highlighted registration assigns the "RecipeForm" as the servlet to be executed when the browser GETs on a "RecipeContainer" object with the "cmd" query parameter set to "new" but this time when the query string contains the string "org.innoscript.recipemanager.schema.Recipe". The last registration is for for the "Recipe" type, and renders the recipe form whenever a compatible browser GETs? on an object of this type with the parameter set to "properties". Remember that every QuiX servlet (an instance of the XULServlet class) must always be accompanied with a quix file in the same directory. This file usually contains the XML definition of the interface, which abides to the Python string formatting rules. The file name of this file is of the following form: [Name of Python file].[Name of the servlet class name].quix Now let's write the "RecipeForm" servlet:
to:
Changed lines 392-432 from:
class RecipeForm?(XULServlet?): def setParams(self):
sCC = self.item.contentclass
sCategories = ''
sHiddenField = ''
if sCC == 'org.innoscript.recipemanager.schema.RecipeContainer?':
# we create a new recipe
sTitle = 'Create new recipe'
oRecipe = schema.Recipe()
# in this case we need an extra hidden field with the type of
# object we are about to create
sHiddenField = '<a:field name="CC" type="hidden" ' + 'value="schemas.org.innoscript.recipemanager.Recipe" />'
sMethod = 'create'
sAction = 'Create'
elif sCC == 'org.innoscript.recipemanager.schema.Recipe':
# we editing an existing one
oRecipe = self.item
sMethod = 'update'
sTitle = 'Recipe properties'
sAction = 'Update'
# build the categories options list
for category in oRecipe.categories.getItems():
sCategories += '<a:option caption="s" img="%s" />' %
(category.displayName.value, category.id, category.__image__)
self.params = {
'URI': self.request.serverVariables['SCRIPT_NAME'] + '/' + self.item.id,
'METHOD': sMethod,
'TITLE': sTitle,
'HIDDEN': sHiddenField,
'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': sCategories,
'ICON': oRecipe.__image__,
'ACTION': sAction,
}
to:
oNewItem = misc.getClassByName(data.pop('CC'))()
Deleted lines 393-403:
As you propably already have guessed, the server formats the contents of the quix file using the "params" attribute of the servlet. This servlet is called either when the user needs to create a new recipe, or when the user wants to display the properties of an existing recipe. In the first case, the type of the object "called" is the "RecipeContainer" unlike the second case, in which the type of the object is the "Recipe" type. The QuiX XML definition of the form is a new file named "ui.RecipeForm.quix" inside the "org/innoscript/recipemanager" folder. Changed lines 396-400 from:
<?xml version="1.0" ?> <a:dialog xmlns:a="http://www.innoscript.org/quix" title="%(TITLE)s" img="%(ICON)s" close="true" width="480" height="380" left="30" top="10"> <a:script name="Generic Functions" src="desktop/generic.js" />
<a:wbody>
to:
# 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()
Changed line 430 from:
<a:form action="%(URI)s" method="%(METHOD)s" padding="4,4,4,4"> to:
return oNewItem.id Changed lines 434-468 from:
%(HIDDEN)s
<a:label caption="Recipe name:"/>
<a:field left="80" width="380" name="displayName"
value="%(DISPLAYNAME)s" />
<a:label caption="Description:" top="25" />
<a:field left="80" top="24" width="380" name="description"
value="%(DESCRIPTION)s" />
<a:hr top="50" width="100" />
<a:label top="58" caption="Preparation time (min):" />
<a:spinbutton name="preparationTime" top="55" left="110" width="50"
max="240" value="%(PREPARATION_TIME)d" editable="true" />
<a:label top="58" left="200" caption="Servings:" />
<a:spinbutton name="servings" top="55" left="250" width="40" max="12"
editable="true" value="%(SERVINGS)d" />
<a:label top="58" left="320" caption="Rating:" />
<a:spinbutton name="rating" top="55" left="360" width="40" max="10"
editable="true" value="%(RATING)d" />
<a:tabpane top="90" width="100" height="220">
<a:tab caption="Ingredients">
<a:field type="textarea" name="ingredients" width="100"
height="100">%(INGREDIENTS)s</a:field>
</a:tab>
<a:tab caption="Instructions">
<a:field type="textarea" name="instructions" width="100"
height="100">%(INSTRUCTIONS)s</a:field>
</a:tab>
<a:tab caption="Categories">
<a:box width="100" height="100" orientation="v">
<a:selectlist name="categories" multiple="true" posts="all" height="-1">
<a:prop name="SelectFrom?" value="Categories/Recipe categories"></a:prop>
<a:prop name="RelatedCC?" value="org.innoscript.desktop.schema.common.Category"/>
%(CATEGORIES)s
</a:selectlist>
<a:rect height="24">
<a:flatbutton width="70" height="22" caption="Add"
to:
... Changed lines 436-443 from:
to:
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:
Changed lines 445-472 from:
onclick="generic.selectItems"></a:flatbutton> to:
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',
}
Added lines 474-480:
Respectively, when editing an existing recipe, we call the "update" method of the "porcupine.systemObjects.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: Changed line 483 from:
<a:flatbutton left="80" width="70" height="22" caption="Remove" to:
from org.innoscript.recipemanager.webmethods import * Changed lines 485-488 from:
to:
The edit the "org/innoscript/recipemanager/webmethods/__init__.py" file:
Changed line 490 from:
onclick="generic.removeSelectedItems"></a:flatbutton> to:
__all__ = ['cookingrecipe', 'recipecontainer'] Added lines 492-504:
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 applicationApart 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: Changed lines 507-512 from:
</a:rect>
</a:box>
</a:tab>
</a:tabpane>
</a:form>
</a:wbody>
to:
<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"/> Changed line 517 from:
<a:dlgbutton onclick="generic.submitForm" width="70" height="22" to:
<dir name="recipemanager" path="org/innoscript/recipemanager"/> Changed lines 521-524 from:
caption="%(ACTION)s" default="true" />
<a:dlgbutton onclick="__closeDialog__" width="70" height="22"
caption="Cancel" />
</a:dialog> to:
</dirs> </config> Changed lines 525-526 from:
Initially, this form imports "generic.js"; one of the QuiX 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. to:
Each published directory must contain a file called "config.xml". This file keeps track of the files accesible over HTTP. This file should be: Changed lines 529-535 from:
... generic.submitForm = function(evt, w) { var oDialog = w.getParentByType(Dialog); var oForm = oDialog.getWidgetsByType(Form)[0]; oForm.submit(__closeDialog__); } ... to:
<config> <context path="recipemanager.quix" method="GET" client=".*" lang=".*" action="recipemanager.quix"/> <context path="recipemanager.js" method="GET" client=".*" lang=".*" action="recipemanager.js"/> </config> Changed lines 543-549 from:
The "submit" method of a QuiX form takes one argument; the function to call asynchronously, once the browser has the submission response. In this case the dialog is simply closed. 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 an XMLRPC servlet registered in the "store.xml" configuration file (visit http://wiki.innoscript.org/index.php/Articles/RequestProcessingPipeline to see how each object is accessible over HTTP). The form's "method" parameter is the name of the method of the XMLRPC servlet to call. Since this form is used for both creating and editing recipes the name of the method is diferrent in each aspect. When creating a new recipe we call the "create" method of the "org.innoscript.desktop.XMLRPC.ContainerGeneric" XMLRPC servlet. to:
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 nodes ID with the ID of the Recipes folder given by the system. The contents of this file are: Changed lines 554-574 from:
... to:
<?xml version="1.0" encoding="UTF-8"?> <a:window xmlns:a="http://www.innoscript.org/quix" title="Recipe manager" resizable="true" close="true" minimize="true" maximize="true" img="" width="640" height="480" left="center" top="center"> <a:script name="Recipe manager Script" src="recipemanager/recipemanager.js"/>
<a:wbody>
<a:box width="100%" height="100%" orientation="v" spacing="0">
<a:toolbar height="28">
<a:tbbutton caption="Create recipe folder" width="120">
<a:prop name="cc" value="org.innoscript.recipemanager.schema.RecipeContainer?"/>
</a:tbbutton>
<a:tbbutton caption="Create recipe" width="80">
<a:prop name="cc" value="org.innoscript.recipemanager.schema.Recipe"/>
</a:tbbutton>
<a:tbsep/>
<a:tbbutton caption="Refresh" width="60"/>
</a:toolbar>
<a:splitter height="-1" orientation="v" interactive="true">
<a:pane length="200">
<a:outlookbar width="100%" height="100%">
<a:tool caption="Recipes">
Changed lines 578-579 from:
class ContainerGeneric?(ItemGeneric?): def create(self, data): to:
<a:foldertree id="tree" method="getSubtree" padding="4,4,4,4">
<a:treenode id="[Put here the ID of the Recipes Porcupine folder]" haschildren="true"
img="desktop/images/folder.gif" caption="Recipes"/>
Changed lines 584-605 from:
# create new item to:
</a:foldertree>
</a:tool>
<a:tool caption="Search" bgcolor="gray">
<a:label caption="Recipe title contains:"/>
<a:field id="title" left="5" top="20" width="160"/>
<a:label top="50" caption="Preparation time is less than"/>
<a:field id="preparationTime" left="5" top="70" width="30"/>
<a:label top="72" left="40" caption="min."/>
<a:label top="100" caption="The recipe's rating is greater than"/>
<a:spinbutton top="120" left="5" width="40" max="10" value="0" id="rating"/>
<a:label top="150" caption="Recipe ingredients contain:"/>
<a:field id="ingredients" top="170" left="5" width="160"/>
<a:label top="190" caption="(comma separated list)" style="font-style:italic"/>
<a:button top="220" left="center" width="60" height="28" caption="Search"/>
</a:tool>
</a:outlookbar>
</a:pane>
<a:pane length="-1">
Changed line 609 from:
oNewItem = misc.getClassByName(data.pop('CC'))()
to:
<a:listview width="100%" height="100%" id="list"> Changed lines 613-641 from:
# get user role
iUserRole = objectAccess.getAccess(self.item, self.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 = self.server.temp_folder + '/' + data[prop]['tempfile']
oAttr.loadFromFile(sPath)
os.remove(sPath)
elif isinstance(oAttr, datatypes.Date):
oAttr.value = data[prop].value
else:
oAttr.value = data[prop]
txn = self.server.store.getTransaction()
oNewItem.appendTo(self.item.id, txn)
txn.commit()
to:
<a:listheader>
<a:column width="140" caption="Recipe title" name="displayName" bgcolor="#EFEFEF" sortable="true"/>
<a:column width="60" caption="Servings" type="int" name="servings" sortable="true"/>
<a:column width="100" caption="Preparation time" type="int" name="preparationTime" sortable="true"/>
<a:column width="40" caption="Rating" type="int" name="rating" sortable="true"/>
<a:column width="160" caption="Date modified" type="date" name="modified" sortable="true"/>
</a:listheader>
</a:listview>
</a:pane>
</a:splitter>
</a:box>
</a:wbody>
</a:window> Changed lines 627-633 from:
to:
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.
Deleted lines 634-637:
return True @] [@ Added lines 636-637:
<a:toolbar width="100" height="100"> <a:tbbutton caption="Create recipe folder" width="120" Changed lines 639-650 from:
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 method simply returns "True" only if the object is created and appended to the container successfully. This way, the browser knows that the request has been succefully completed. Also notice, that the "ContainerGeneric" servlet is a subclass of the "ItemGeneric" servlet. Besides the container specific methods defined, a container can also be updated, copied or renamed just like any other Porcupine object. Respectively, when editing an existing recipe, we call the "update" method of the "org.innoscript.desktop.XMLRPC.ItemGeneric" XMLRPC servlet. From the "generic.js" file we also reuse two JavaScript functions named "generic.selectItems" and "generic.removeSelectedItems".
to:
Changed lines 641-646 from:
... generic.selectItems = function(evt, w) { var oDialog = w.getParentByType(Dialog); var oTarget = w.parent.parent.getWidgetsByType(SelectList?)[0]; var sFolderURI = oTarget.attributes.SelectFrom?; var sCC = oTarget.attributes.RelatedCC?; to:
onclick="recipemanager.createItem"> Changed line 643 from:
to:
Changed lines 645-646 from:
generic.selectObjects(oDialog, oTarget, generic.addSelectionToList,
sFolderURI, sCC, 'true');
to:
... </a:tbbutton> <a:tbbutton caption="Create recipe" width="80" Changed line 649 from:
to:
Changed lines 651-662 from:
} ... generic.selectObjects = function(win, target, select_func, startFrom, contentclass, multiple) { ... } ... generic.removeSelectedItems = function(evt, w) { var oSelectList = w.parent.parent.getWidgetsByType(SelectList?)[0]; oSelectList.removeSelected(); } ... to:
onclick="recipemanager.createItem"> Deleted lines 652-672:
The "generic.selectItems" function displays the object select dialog by calling the "generic.selectObjects" function. The auto-generated form servlet uses this function for "ReferenceN" or "RelatorN" data types. The arguments given to the "generic.selectObjects" function are:
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 applicationApart 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: Changed lines 655-661 from:
<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"/> to:
... </a:tbbutton> <a:tbsep /> <a:tbbutton caption="Refresh" width="60" Changed line 662 from:
<dir name="recipemanager" path="org/innoscript/recipemanager"/> to:
onclick="recipemanager.refresh_onclick" /> Changed lines 666-667 from:
</dirs> </config> to:
</a:toolbar> ... <a:listview id="list" multiple="true" width="100" height="100" Changed lines 670-673 from:
Each published directory must contain a file called "config.xml". This file keeps track of the files accesible over HTTP. This file should be:
to:
Changed lines 672-684 from:
<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:
onload="recipemanager.loadRecipes"> Deleted lines 673-682:
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 nodes ID with the ID of the Recipes folder given by the system. The contents of this file are: Changed lines 676-696 from:
<?xml version="1.0" encoding="UTF-8"?> <a:window xmlns:a="http://www.innoscript.org/quix" title="Recipe manager" resizable="true" close="true" minimize="true" maximize="true" img="" width="640" height="480" left="center" top="center"> <a:script name="Recipe manager Script" src="recipemanager/recipemanager.js"/>
<a:wbody>
<a:box width="100%" height="100%" orientation="v" spacing="0">
<a:toolbar height="28">
<a:tbbutton caption="Create recipe folder" width="120">
<a:prop name="cc" value="org.innoscript.recipemanager.schema.RecipeContainer?"/>
</a:tbbutton>
<a:tbbutton caption="Create recipe" width="80">
<a:prop name="cc" value="org.innoscript.recipemanager.schema.Recipe"/>
</a:tbbutton>
<a:tbsep/>
<a:tbbutton caption="Refresh" width="60"/>
</a:toolbar>
<a:splitter height="-1" orientation="v" interactive="true">
<a:pane length="200">
<a:outlookbar width="100%" height="100%">
<a:tool caption="Recipes">
to:
... Changed lines 678-681 from:
to:
Insert the following functions inside the empty "recipemanager.js" file:
Changed lines 683-685 from:
<a:foldertree id="tree" method="getSubtree" padding="4,4,4,4">
<a:treenode id="[Put here the ID of the Recipes Porcupine folder]" haschildren="true"
img="desktop/images/folder.gif" caption="Recipes"/>
to:
function recipemanager() {} recipemanager.createItem = function(evt, w) { var oWin = w.getParentByType(Window);
var oTree = oWin.getWidgetById('tree');
Changed line 689 from:
to:
Changed lines 691-712 from:
</a:foldertree>
</a:tool>
<a:tool caption="Search" bgcolor="gray">
<a:label caption="Recipe title contains:"/>
<a:field id="title" left="5" top="20" width="160"/>
<a:label top="50" caption="Preparation time is less than"/>
<a:field id="preparationTime" left="5" top="70" width="30"/>
<a:label top="72" left="40" caption="min."/>
<a:label top="100" caption="The recipe's rating is greater than"/>
<a:spinbutton top="120" left="5" width="40" max="10" value="0" id="rating"/>
<a:label top="150" caption="Recipe ingredients contain:"/>
<a:field id="ingredients" top="170" left="5" width="160"/>
<a:label top="190" caption="(comma separated list)" style="font-style:italic"/>
<a:button top="220" left="center" width="60" height="28" caption="Search"/>
</a:tool>
</a:outlookbar>
</a:pane>
<a:pane length="-1">
to:
var sCC = w.attributes.cc; Changed line 693 from:
to:
Changed line 695 from:
<a:listview width="100%" height="100%" id="list"> to:
var id = oTree.getSelection().getId(); Changed line 697 from:
to:
Changed lines 699-711 from:
<a:listheader>
<a:column width="140" caption="Recipe title" name="displayName" bgcolor="#EFEFEF" sortable="true"/>
<a:column width="60" caption="Servings" type="int" name="servings" sortable="true"/>
<a:column width="100" caption="Preparation time" type="int" name="preparationTime" sortable="true"/>
<a:column width="40" caption="Rating" type="int" name="rating" sortable="true"/>
<a:column width="160" caption="Date modified" type="date" name="modified" sortable="true"/>
</a:listheader>
</a:listview>
</a:pane>
</a:splitter>
</a:box>
</a:wbody>
</a:window> to:
oWin.showWindow(id + '?cmd=new&cc=' + sCC,
function(dlg) {
dlg.attachEvent("onclose", recipemanager.dialogClose);
}
);
Deleted lines 704-709:
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. Changed lines 707-709 from:
... <a:toolbar width="100" height="100"> <a:tbbutton caption="Create recipe folder" width="120" to:
} recipemanager.dialogClose = function(dlg) { if (dlg.buttonIndex == 0) {
recipemanager.refreshList(dlg.opener);
}
} recipemanager.refresh_onclick = function(evt, w) { recipemanager.loadRecipes(w); } Changed line 721 from:
onclick="recipemanager.createItem"> to:
recipemanager.loadRecipes = function(w) { Changed lines 725-727 from:
... </a:tbbutton> <a:tbbutton caption="Create recipe" width="80" to:
var appWin = w.getParentByType(Window); recipemanager.refreshList(appWin); } recipemanager.refreshList = function(appWin) { var oTree = appWin.getWidgetById('tree');
var id = oTree.getSelection().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";
Changed lines 739-741 from:
onclick="recipemanager.createItem"> to:
var xmlrpc = new XMLRPCRequest?(QuiX?.root); xmlrpc.oncomplete = recipemanager.updateList; xmlrpc.callback_info = listView; Changed lines 745-748 from:
... </a:tbbutton> <a:tbsep /> <a:tbbutton caption="Refresh" width="60" to:
xmlrpc.callmethod('executeOqlCommand', sOql);
} recipemanager.updateList = function(req) { Changed lines 752-753 from:
onclick="recipemanager.refresh_onclick" /> to:
req.callback_info.dataSet = req.response; req.callback_info.refresh(); Changed lines 757-759 from:
</a:toolbar> ... <a:listview id="list" multiple="true" width="100" height="100" to:
} Changed lines 759-776 from:
to:
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 "a: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 when the list view is loaded. This handler loads the recipes from the selected container and displays them in the list view. The "onload" event handler, unlike the other events, does not accept the two common event handler arguments, the event object and the widget, but instead accepts only the widget that fires the event. 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 XMLRPC request to the root folder. The "oncomplete" attribute of the request is the callback function to call when the query has completed. This 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 XMLRPC call. Afterwards, we must synchronize the tree with the list view. This means that for each new tree selection we should update the list view accordingly. This is accomplished by adding the following event handler:
Changed lines 778-779 from:
onload="recipemanager.loadRecipes"> to:
... <a:foldertree id="tree" method="getSubtree" padding="4,4,4,4" Changed line 781 from:
to:
Changed line 783 from:
... to:
onselect="recipemanager.loadRecipes"> Deleted lines 784-786:
Insert the following functions inside the empty "recipemanager.js" file: Changed lines 787-791 from:
function recipemanager() {} recipemanager.createItem = function(evt, w) { var oWin = w.getParentByType(Window);
var oTree = oWin.getWidgetById('tree');
to:
... </a:foldertree> ... Changed lines 791-794 from:
to:
Next, we implement the search functionality. We start by adding a new "onclick" event handler on the search button:
Changed lines 796-797 from:
var sCC = w.attributes.cc; to:
... <a:button top="220" left="center" width="60" height="28" caption="Search" Changed line 799 from:
to:
Changed line 801 from:
var id = oTree.getSelection().getId(); to:
onclick="recipemanager.search" /> Changed line 803 from:
to:
Changed lines 805-809 from:
oWin.showWindow(id + '?cmd=new&cc=' + sCC,
function(dlg) {
dlg.attachEvent("onclose", recipemanager.dialogClose);
}
);
to:
... Added lines 807-809:
Add the following function inside the "recipemanager.js" file: Changed lines 812-822 from:
} recipemanager.dialogClose = function(dlg) { if (dlg.buttonIndex == 0) {
recipemanager.refreshList(dlg.opener);
}
} recipemanager.refresh_onclick = function(evt, w) { recipemanager.loadRecipes(w); } to:
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, " +
Changed line 821 from:
recipemanager.loadRecipes = function(w) { to:
"modified from deep('/Recipes')";
Changed lines 825-826 from:
var appWin = w.getParentByType(Window); recipemanager.refreshList(appWin); to:
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);
}
Deleted lines 849-856:
recipemanager.refreshList = function(appWin) { var oTree = appWin.getWidgetById('tree');
var id = oTree.getSelection().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";
Changed lines 851-855 from:
to:
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:
Changed lines 857-859 from:
var xmlrpc = new XMLRPCRequest?(QuiX?.root); xmlrpc.oncomplete = recipemanager.updateList; xmlrpc.callback_info = listView; to:
<a:listview multiple="true" width="100" height="100" id="list" onload="recipemanager.loadRecipes" Changed line 860 from:
to:
Changed lines 862-865 from:
xmlrpc.callmethod('executeOqlCommand', sOql);
} recipemanager.updateList = function(req) { to:
ondblclick="recipemanager.loadItem"> Changed lines 864-867 from:
to:
The following event handler should be added to the script of the application:
Changed lines 869-870 from:
req.callback_info.dataSet = req.response; req.callback_info.refresh(); to:
recipemanager.loadItem = function(evt, w, recipe) { var oWin = w.getParentByType(Window);
oWin.showWindow(recipe.id + "?cmd=properties",
function(dlg) {
dlg.attachEvent("onclose", recipemanager.dialogClose);
}
);
} Added lines 878-883:
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. Open the application object and add the following lines to the interface definition: Changed line 886 from:
} to:
<a:pane length="-1"> Changed lines 888-905 from:
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 "a: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 when the list view is loaded. This handler loads the recipes from the selected container and displays them in the list view. The "onload" event handler, unlike the other events, does not accept the two common event handler arguments, the event object and the widget, but instead accepts only the widget that fires the event. 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 XMLRPC request to the root folder. The "oncomplete" attribute of the request is the callback function to call when the query has completed. This 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 XMLRPC call. Afterwards, we must synchronize the tree with the list view. This means that for each new tree selection we should update the list view accordingly. This is accomplished by adding the following event handler:
to:
Changed lines 890-891 from:
... <a:foldertree id="tree" method="getSubtree" padding="4,4,4,4" to:
<a:contextmenu onshow="recipemanager.contextmenu_onshow">
<a:menuoption caption="Open" onclick="recipemanager.openRecipe" />
<a:menuoption caption="Delete" onclick="recipemanager.deleteRecipe" />
</a:contextmenu>
Changed line 895 from:
to:
Changed lines 897-901 from:
onselect="recipemanager.loadRecipes"> to:
<a:listview width="100" height="100" id="list"
onload="recipemanager.loadRecipes" ondblclick="recipemanager.loadItem">
...
</a:listview>
</a:pane> Added lines 903-905:
Add the following handlers to the applications script: Changed lines 908-910 from:
... </a:foldertree> ... to:
recipemanager.contextmenu_onshow = function(menu) { Changed lines 910-913 from:
Next, we implement the search functionality. We start by adding a new "onclick" event handler on the search button:
to:
Changed lines 912-913 from:
... <a:button top="220" left="center" width="60" height="28" caption="Search" to:
var oList = menu.owner.getWidgetById('list');
Changed line 914 from:
to:
Changed lines 916-944 from:
onclick="recipemanager.search" /> to:
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.getWidgetById('list');
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.getWidgetById('list');
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');
}
Changed line 946 from:
to:
Changed line 948 from:
... to:
desktop.msgbox(w.getCaption(), Deleted lines 949-951:
Add the following function inside the "recipemanager.js" file: Changed lines 952-957 from:
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, " +
to:
"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);
} Changed lines 960-985 from:
to:
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 menus enclosing widget. The enclosing widget is accessible through the "owner" menu property. The "desktop.msgbox" is used for displaying message boxes. The arguments accepted by this method are:
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 applicationUsing the "pakager" utility we will consolidate all of the applications 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:
Changed lines 987-989 from:
"modified from deep('/Recipes')";
to:
[package] name=RecipeManager? version=0.0.4 Added lines 991-993:
The first section of the package definition file has information about the package (its name and its version). Please notice that the name of the package and the value of the name attribute of the package node in the "store.xml" configuration file must be identical. Changed lines 996-1020 from:
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);
}
} to:
[files] Changed lines 999-1001 from:
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: to:
The second section contains the relative paths to any files that need to be included in the package. Leave this section blank. Changed lines 1003-1004 from:
<a:listview multiple="true" width="100" height="100" id="list" onload="recipemanager.loadRecipes" to:
[dirs] Changed lines 1005-1008 from:
to:
This section adds directories to the package.
Changed lines 1010-1011 from:
ondblclick="recipemanager.loadItem"> to:
[pubdir] 1=recipemanager Changed lines 1014-1015 from:
The following event handler should be added to the script of the application: to:
The fourth section contains the published directories (usually containing external JavaScript files and images) that are required by the 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. Changed lines 1018-1025 from:
recipemanager.loadItem = function(evt, w, recipe) { var oWin = w.getParentByType(Window);
oWin.showWindow(recipe.id + "?cmd=properties",
function(dlg) {
dlg.attachEvent("onclose", recipemanager.dialogClose);
}
);
} to:
[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] Changed lines 1025-1029 from:
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. Open the application object and add the following lines to the interface definition: to:
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. Changed lines 1030-1032 from:
<a:pane length="-1"> to:
[scripts] postinstall=PKG/postinstall.py uninstall=PKG/uninstall.py Changed lines 1034-1039 from:
to:
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. Lets 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:
Changed lines 1041-1044 from:
<a:contextmenu onshow="recipemanager.contextmenu_onshow">
<a:menuoption caption="Open" onclick="recipemanager.openRecipe" />
<a:menuoption caption="Delete" onclick="recipemanager.deleteRecipe" />
</a:contextmenu>
to:
from porcupine.administration import codegen ce = codegen.DatatypeEditor?('org.innoscript.desktop.schema.properties.CategoryObjects?') Changed line 1045 from:
to:
Changed lines 1047-1051 from:
<a:listview width="100" height="100" id="list"
onload="recipemanager.loadRecipes" ondblclick="recipemanager.loadItem">
...
</a:listview>
</a:pane> to:
ce.relCc.append('org.innoscript.recipemanager.schema.Recipe') Deleted lines 1048-1050:
Add the following handlers to the applications script: Changed line 1051 from:
recipemanager.contextmenu_onshow = function(menu) { to:
ce.commitChanges() Changed lines 1053-1057 from:
to:
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.Recipe" 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:
Changed lines 1059-1061 from:
var oList = menu.owner.getWidgetById('list');
to:
from porcupine.administration import codegen ce = codegen.DatatypeEditor?('org.innoscript.desktop.schema.properties.CategoryObjects?') Changed line 1063 from:
to:
Changed lines 1065-1093 from:
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.getWidgetById('list');
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.getWidgetById('list');
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');
}
to:
ce.relCc.remove('org.innoscript.recipemanager.schema.Recipe') Changed line 1067 from:
to:
Changed line 1069 from:
desktop.msgbox(w.getCaption(), to:
ce.commitChanges() Changed lines 1071-1192 from:
"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 menus enclosing widget. The enclosing widget is accessible through the "owner" menu property. The "desktop.msgbox" is used for displaying message boxes. The arguments accepted by this method are:
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 applicationUsing the "pakager" utility we will consolidate all of the applications 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.4 The first section of the package definition file has information about the package (its name and its version). Please notice that the name of the package and the value of the name attribute of the package node in the "store.xml" configuration file must be identical. [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 (usually containing external JavaScript files and images) that are required by the 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. Lets 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.Recipe')
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.Recipe" 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.Recipe')
ce.commitChanges() to:
May 02, 2008, at 10:53 PM
by -
Added lines 3-6:
This tutorial is outdated. Please check back again for a new updated version.May 29, 2007, at 07:53 PM
by -
Changed lines 509-510 from:
Initially, this form imports one of the QuiX desktop JavaScript files. One of the functions we reuse from this script (org/innoscript/desktop/generic.js) is the "generic.submitForm" function. This function simply submits the first QuiX form found inside the current dialog. to:
Initially, this form imports "generic.js"; one of the QuiX 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. May 29, 2007, at 07:51 PM
by -
Changed lines 157-158 from:
Now lets take a closer look at the CategoryObjects? data type: to:
Now lets take a closer look at the CategoryObjects data type: Changed line 1197 from:
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.Recipe" content class to the list of types that can be categorized (i.e. to the "relCc" class attribute of the data type). to:
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.Recipe" content class to the list of types that can be categorized (i.e. to the "relCc" class attribute of the data type). May 29, 2007, at 07:50 PM
by -
Changed lines 351-352 from:
browser GETs? on a "RecipeContainer" object with the "cmd" query parameter set to "new" and the query string contains the string "org.innoscript.recipemanager.schema.RecipeContainer?" (this is the to:
browser GETs on a "RecipeContainer" object with the "cmd" query parameter set to "new" and the query string contains the string "org.innoscript.recipemanager.schema.RecipeContainer" (this is the Changed line 356 from:
to:
browser GETs on a "RecipeContainer" object with the "cmd" query parameter set to "new" but this May 29, 2007, at 07:47 PM
by -
Changed lines 291-293 from:
module. First, we import the "XULServlet" class, the servers desktop "ui" module and the content classes module we just created: to:
module. First, we import the "XULServlet" class and the content classes module we just created: Deleted line 296:
from org.innoscript.desktop import ui Changed lines 301-306 from:
recipe, we must register a new QuiX servlet that acts as a splitter. This is because the "RecipeContainer" type accepts both new recipe containers and recipes. When creating a new recipe container, the splitter must get the XML definition output from the "Frm_Auto" servlet, whereas when creating a new recipe the splitter should return the XML definition output from our custom servlet. This is shown below: to:
recipe, we must create two new registrations. One for each type accepted by the "RecipeContainer" type (recipe containers and recipes). For the recipe container, we will use the desktop's "Frm_Auto" servlet, but for the recipe items we will use our own servlet which outputs our custom QuiX form. This is shown below: Before proceeding to the recipe form itself, we must modify the "store.xml" configuration file by adding modifying the highlighted lines: Changed lines 311-313 from:
class RecipeContainerSplitter?(XULServlet?): def setParams(self):
sCC = self.request.queryString['cc'][0]
to:
<package name="RecipeManager?"> <reg cc="org.innoscript.recipemanager.schema.RecipeContainer?" method="POST" param="" client="vcXMLRPC" lang=".*" action="org.innoscript.desktop.XMLRPC.ContainerGeneric?"/> Changed lines 321-343 from:
self.params['FORM'] = '' to:
<reg cc="org.innoscript.recipemanager.schema.RecipeContainer?" qs="org.innoscript.recipemanager.schema.RecipeContainer?" client="(MSIE [6-7])|(Mozilla/5.0.+rv:1.[7-9])" lang=".*" method="GET" param="new" action="org.innoscript.desktop.ui.Frm_AutoNew"> <filter type="porcupine.filters.postProcessing.multilingual.Multilingual" using="org.innoscript.desktop.strings.resources"/> </reg> <reg cc="org.innoscript.recipemanager.schema.RecipeContainer?" qs="org.innoscript.recipemanager.schema.Recipe" client="(MSIE [6-7])|(Mozilla/5.0.+rv:1.[7-9])" lang=".*" method="GET" param="new" action="org.innoscript.recipemanager.ui.RecipeForm?"/> <reg cc="org.innoscript.recipemanager.schema.Recipe$" method="GET" param="properties" client="(MSIE [6-7])|(Mozilla/5.0.+rv:1.[7-9])" lang=".*" action="org.innoscript.recipemanager.ui.RecipeForm?"/> Changed lines 347-352 from:
if sCC == 'org.innoscript.recipemanager.schema.RecipeContainer?': servlet = ui.Frm_AutoNew(self.server, self.session, self.request) self.params['FORM'] = servlet.execute() elif sCC == 'org.innoscript.recipemanager.schema.Recipe': servlet = RecipeForm?(self.server, self.session, self.request) self.params['FORM'] = servlet.execute() to:
<package> Added lines 350-362:
The first highlighted registration assigns the "Frm_AutoNew" as the servlet to be executed when the browser GETs? on a "RecipeContainer" object with the "cmd" query parameter set to "new" and the query string contains the string "org.innoscript.recipemanager.schema.RecipeContainer?" (this is the new "qs" parameter). The second highlighted registration assigns the "RecipeForm" as the servlet to be executed when the browser GETs? on a "RecipeContainer" object with the "cmd" query parameter set to "new" but this time when the query string contains the string "org.innoscript.recipemanager.schema.Recipe". The last registration is for for the "Recipe" type, and renders the recipe form whenever a compatible browser GETs? on an object of this type with the parameter set to "properties". Changed lines 369-372 from:
In order for the servlet to execute successfully, a file named "ui.RecipeContainerSplitter.quix" must be created in the same directory. The specific servlet acts as a wrapper; the content of the quix file is simply: to:
Now let's write the "RecipeForm" servlet: Changed lines 373-413 from:
%(FORM)s to:
class RecipeForm?(XULServlet?): def setParams(self):
sCC = self.item.contentclass
sCategories = ''
sHiddenField = ''
if sCC == 'org.innoscript.recipemanager.schema.RecipeContainer?':
# we create a new recipe
sTitle = 'Create new recipe'
oRecipe = schema.Recipe()
# in this case we need an extra hidden field with the type of
# object we are about to create
sHiddenField = '<a:field name="CC" type="hidden" ' + 'value="schemas.org.innoscript.recipemanager.Recipe" />'
sMethod = 'create'
sAction = 'Create'
elif sCC == 'org.innoscript.recipemanager.schema.Recipe':
# we editing an existing one
oRecipe = self.item
sMethod = 'update'
sTitle = 'Recipe properties'
sAction = 'Update'
# build the categories options list
for category in oRecipe.categories.getItems():
sCategories += '<a:option caption="s" img="%s" />' %
(category.displayName.value, category.id, category.__image__)
self.params = {
'URI': self.request.serverVariables['SCRIPT_NAME'] + '/' + self.item.id,
'METHOD': sMethod,
'TITLE': sTitle,
'HIDDEN': sHiddenField,
'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': sCategories,
'ICON': oRecipe.__image__,
'ACTION': sAction,
}
Changed lines 418-420 from:
Before proceeding to the recipe form itself, we must modify the "store.xml" configuration file by adding modifying the highlighted lines: to:
This servlet is called either when the user needs to create a new recipe, or when the user wants to display the properties of an existing recipe. In the first case, the type of the object "called" is the "RecipeContainer" unlike the second case, in which the type of the object is the "Recipe" type. The QuiX XML definition of the form is a new file named "ui.RecipeForm.quix" inside the "org/innoscript/recipemanager" folder. Changed lines 428-439 from:
<package name="RecipeManager?"> <reg cc="org.innoscript.recipemanager.schema.RecipeContainer?" method="POST" param="" client="vcXMLRPC" lang=".*" action="org.innoscript.desktop.XMLRPC.ContainerGeneric?"/> <reg cc="org.innoscript.recipemanager.schema.RecipeContainer?" method="GET" param="new" client="(MSIE [6-7])|(Mozilla/5.0.+rv:1.[7-9])" lang=".*" to:
<?xml version="1.0" ?> <a:dialog xmlns:a="http://www.innoscript.org/quix" title="%(TITLE)s" img="%(ICON)s" close="true" width="480" height="380" left="30" top="10"> <a:script name="Generic Functions" src="desktop/generic.js" />
<a:wbody>
Changed lines 436-442 from:
action="org.innoscript.recipemanager.ui.RecipeContainerSplitter?"/> <reg cc="org.innoscript.recipemanager.schema.Recipe$" method="GET" param="properties" client="(MSIE [6-7])|(Mozilla/5.0.+rv:1.[7-9])" lang=".*" action="org.innoscript.recipemanager.ui.RecipeForm?"/> to:
<a:form action="%(URI)s" method="%(METHOD)s" padding="4,4,4,4"> Changed lines 440-474 from:
<package> to:
%(HIDDEN)s
<a:label caption="Recipe name:"/>
<a:field left="80" width="380" name="displayName"
value="%(DISPLAYNAME)s" />
<a:label caption="Description:" top="25" />
<a:field left="80" top="24" width="380" name="description"
value="%(DESCRIPTION)s" />
<a:hr top="50" width="100" />
<a:label top="58" caption="Preparation time (min):" />
<a:spinbutton name="preparationTime" top="55" left="110" width="50"
max="240" value="%(PREPARATION_TIME)d" editable="true" />
<a:label top="58" left="200" caption="Servings:" />
<a:spinbutton name="servings" top="55" left="250" width="40" max="12"
editable="true" value="%(SERVINGS)d" />
<a:label top="58" left="320" caption="Rating:" />
<a:spinbutton name="rating" top="55" left="360" width="40" max="10"
editable="true" value="%(RATING)d" />
<a:tabpane top="90" width="100" height="220">
<a:tab caption="Ingredients">
<a:field type="textarea" name="ingredients" width="100"
height="100">%(INGREDIENTS)s</a:field>
</a:tab>
<a:tab caption="Instructions">
<a:field type="textarea" name="instructions" width="100"
height="100">%(INSTRUCTIONS)s</a:field>
</a:tab>
<a:tab caption="Categories">
<a:box width="100" height="100" orientation="v">
<a:selectlist name="categories" multiple="true" posts="all" height="-1">
<a:prop name="SelectFrom?" value="Categories/Recipe categories"></a:prop>
<a:prop name="RelatedCC?" value="org.innoscript.desktop.schema.common.Category"/>
%(CATEGORIES)s
</a:selectlist>
<a:rect height="24">
<a:flatbutton width="70" height="22" caption="Add"
Changed lines 476-484 from:
The first modification assigns the "RecipeContainerSplitter" as the servlet to be executed when the browser GETs? on a "RecipeContainer" object with the "cmd" query parameter set to "new". The second modification is simply a new servlet registration for the "Recipe" type, which renders the recipe form whenever a compatible browser GETs? on an object of this type with the parameter set to "properties". Now its time to write the "RecipeForm" servlet:
to:
Changed lines 478-518 from:
class RecipeForm?(XULServlet?): def setParams(self):
sCC = self.item.contentclass
sCategories = ''
sHiddenField = ''
if sCC == 'org.innoscript.recipemanager.schema.RecipeContainer?':
# we create a new recipe
sTitle = 'Create new recipe'
oRecipe = schema.Recipe()
# in this case we need an extra hidden field with the type of
# object we are about to create
sHiddenField = '<a:field name="CC" type="hidden" ' + 'value="schemas.org.innoscript.recipemanager.Recipe" />'
sMethod = 'create'
sAction = 'Create'
elif sCC == 'org.innoscript.recipemanager.schema.Recipe':
# we editing an existing one
oRecipe = self.item
sMethod = 'update'
sTitle = 'Recipe properties'
sAction = 'Update'
# build the categories options list
for category in oRecipe.categories.getItems():
sCategories += '<a:option caption="s" img="%s" />' %
(category.displayName.value, category.id, category.__image__)
self.params = {
'URI': self.request.serverVariables['SCRIPT_NAME'] + '/' + self.item.id,
'METHOD': sMethod,
'TITLE': sTitle,
'HIDDEN': sHiddenField,
'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': sCategories,
'ICON': oRecipe.__image__,
'ACTION': sAction,
}
to:
onclick="generic.selectItems"></a:flatbutton> Deleted lines 479-486:
This servlet is called either when the user needs to create a new recipe, or when the user wants to display the properties of an existing recipe. In the first case, the type of the object "called" is the "RecipeContainer" unlike the second case, in which the type of the object is the "Recipe" type. The QuiX XML definition of the form is a new file named "ui.RecipeForm.quix" inside the "org/innoscript/recipemanager" folder. Changed lines 482-487 from:
<?xml version="1.0" ?> <a:dialog xmlns:a="http://www.innoscript.org/quix" title="%(TITLE)s" img="%(ICON)s" close="true" width="480" height="380" left="30" top="10"> <a:script name="Generic Form Script" src="desktop/ui.Frm_Auto.js" />
<a:script name="Generic Functions" src="desktop/generic.js" />
<a:wbody>
to:
<a:flatbutton left="80" width="70" height="22" caption="Remove" Changed line 486 from:
<a:form action="%(URI)s" method="%(METHOD)s" padding="4,4,4,4"> to:
onclick="generic.removeSelectedItems"></a:flatbutton> Changed lines 490-510 from:
%(HIDDEN)s
<a:label caption="Recipe name:"/>
<a:field left="80" width="380" name="displayName"
value="%(DISPLAYNAME)s" />
<a:label caption="Description:" top="25" />
<a:field left="80" top="24" width="380" name="description"
value="%(DESCRIPTION)s" />
<a:hr top="50" width="100" />
<a:label top="58" caption="Preparation time (min):" />
<a:spinbutton name="preparationTime" top="55" left="110" width="50"
max="240" value="%(PREPARATION_TIME)d" editable="true" />
<a:label top="58" left="200" caption="Servings:" />
<a:spinbutton name="servings" top="55" left="250" width="40" max="12"
editable="true" value="%(SERVINGS)d" />
<a:label top="58" left="320" caption="Rating:" />
<a:spinbutton name="rating" top="55" left="360" width="40" max="10"
editable="true" value="%(RATING)d" />
<a:tabpane top="90" width="100" height="220">
<a:tab caption="Ingredients">
<a:field type="textarea" name="ingredients" width="100"
height="100">%(INGREDIENTS)s</a:field>
to:
</a:rect>
</a:box>
Changed lines 493-505 from:
<a:tab caption="Instructions">
<a:field type="textarea" name="instructions" width="100"
height="100">%(INSTRUCTIONS)s</a:field>
</a:tab>
<a:tab caption="Categories">
<a:box width="100" height="100" orientation="v">
<a:selectlist name="categories" multiple="true" posts="all" height="-1">
<a:prop name="SelectFrom?" value="Categories/Recipe categories"></a:prop>
<a:prop name="RelatedCC?" value="org.innoscript.desktop.schema.common.Category"/>
%(CATEGORIES)s
</a:selectlist>
<a:rect height="24">
<a:flatbutton width="70" height="22" caption="Add"
to:
</a:tabpane>
</a:form>
</a:wbody>
Changed line 499 from:
onclick="generic.selectItems"></a:flatbutton> to:
<a:dlgbutton onclick="generic.submitForm" width="70" height="22" Changed lines 503-506 from:
<a:flatbutton left="80" width="70" height="22" caption="Remove" to:
caption="%(ACTION)s" default="true" />
<a:dlgbutton onclick="__closeDialog__" width="70" height="22"
caption="Cancel" />
</a:dialog> Changed lines 508-511 from:
to:
Initially, this form imports one of the QuiX desktop JavaScript files. One of the functions we reuse from this script (org/innoscript/desktop/generic.js) is the "generic.submitForm" function. This function simply submits the first QuiX form found inside the current dialog.
Changed lines 513-519 from:
onclick="generic.removeSelectedItems"></a:flatbutton> to:
... generic.submitForm = function(evt, w) { var oDialog = w.getParentByType(Dialog); var oForm = oDialog.getWidgetsByType(Form)[0]; oForm.submit(__closeDialog__); } ... Added lines 521-528:
The "submit" method of a QuiX form takes one argument; the function to call asynchronously, once the browser has the submission response. In this case the dialog is simply closed. 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 an XMLRPC servlet registered in the "store.xml" configuration file (visit http://wiki.innoscript.org/index.php/Articles/RequestProcessingPipeline to see how each object is accessible over HTTP). The form's "method" parameter is the name of the method of the XMLRPC servlet to call. Since this form is used for both creating and editing recipes the name of the method is diferrent in each aspect. When creating a new recipe we call the "create" method of the "org.innoscript.desktop.XMLRPC.ContainerGeneric" XMLRPC servlet. Changed lines 531-536 from:
</a:rect>
</a:box>
</a:tab>
</a:tabpane>
</a:form>
</a:wbody>
to:
... Changed lines 535-536 from:
<a:dlgbutton onclick="autoform.submit" width="70" height="22" to:
class ContainerGeneric?(ItemGeneric?): def create(self, data): Changed lines 540-543 from:
caption="%(ACTION)s" default="true" />
<a:dlgbutton onclick="__closeDialog__" width="70" height="22"
caption="Cancel" />
</a:dialog> to:
# create new item Changed lines 542-545 from:
Initially, this form imports two QuiX desktop JavaScript files. The first script is the one used by the forms generated automatically by the "Frm_Auto" servlet. The function we reuse from this script (org/innoscript/desktop/ui.Frm_Auto.js) is the "autoform.submit" function. This function simply submits the first QuiX form found inside the current dialog.
to:
Changed lines 544-546 from:
... autoform.submit = function(evt, w) { var oForm = w.getParentByType(Dialog).getWidgetsByType(Form)[0]; to:
oNewItem = misc.getClassByName(data.pop('CC'))()
Changed line 546 from:
to:
Changed lines 548-576 from:
oForm.submit(autoform.update); to:
# get user role
iUserRole = objectAccess.getAccess(self.item, self.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 = self.server.temp_folder + '/' + data[prop]['tempfile']
oAttr.loadFromFile(sPath)
os.remove(sPath)
elif isinstance(oAttr, datatypes.Date):
oAttr.value = data[prop].value
else:
oAttr.value = data[prop]
txn = self.server.store.getTransaction()
oNewItem.appendTo(self.item.id, txn)
txn.commit()
Changed line 578 from:
to:
Changed lines 580-582 from:
} autoform.update = function(response, form) { var dlg = form.getParentByType(Dialog); to:
return True Changed line 582 from:
to:
Changed line 584 from:
dlg.attributes.refreshlist(); to:
... Added lines 586-596:
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 method simply returns "True" only if the object is created and appended to the container successfully. This way, the browser knows that the request has been succefully completed. Also notice, that the "ContainerGeneric" servlet is a subclass of the "ItemGeneric" servlet. Besides the container specific methods defined, a container can also be updated, copied or renamed just like any other Porcupine object. Respectively, when editing an existing recipe, we call the "update" method of the "org.innoscript.desktop.XMLRPC.ItemGeneric" XMLRPC servlet. From the "generic.js" file we also reuse two JavaScript functions named "generic.selectItems" and "generic.removeSelectedItems". Deleted lines 598-599:
dlg.close(); } Added lines 600-604:
generic.selectItems = function(evt, w) { var oDialog = w.getParentByType(Dialog); var oTarget = w.parent.parent.getWidgetsByType(SelectList?)[0]; var sFolderURI = oTarget.attributes.SelectFrom?; var sCC = oTarget.attributes.RelatedCC?; Changed lines 606-614 from:
The "submit" method of a QuiX form takes one argument; the function to call asynchronously, once the browser has the submission response. In this case the dialog has a custom attribute which is actually the function to call for refreshing the openers list view. After calling this function, the dialog is closed. 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 an XMLRPC servlet registered in the "store.xml" configuration file (visit http://wiki.innoscript.org/index.php/Articles/RequestProcessingPipeline to see how each object is accessible over HTTP). The form's "method" parameter is the name of the method of the XMLRPC servlet to call. Since this form is used for both creating and editing recipes the name of the method is diferrent in each aspect. When creating a new recipe we call the "create" method of the "org.innoscript.desktop.XMLRPC.ContainerGeneric" XMLRPC servlet.
to:
Changed lines 608-609 from:
... to:
generic.selectObjects(oDialog, oTarget, generic.addSelectionToList,
sFolderURI, sCC, 'true');
Changed line 611 from:
to:
Changed lines 613-614 from:
class ContainerGeneric?(ItemGeneric?): def create(self, data): to:
} ... generic.selectObjects = function(win, target, select_func, startFrom, contentclass, multiple) { ... } ... generic.removeSelectedItems = function(evt, w) { var oSelectList = w.parent.parent.getWidgetsByType(SelectList?)[0]; oSelectList.removeSelected(); } ... Added lines 626-646:
The "generic.selectItems" function displays the object select dialog by calling the "generic.selectObjects" function. The auto-generated form servlet uses this function for "ReferenceN" or "RelatorN" data types. The arguments given to the "generic.selectObjects" function are:
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 applicationApart 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: Changed lines 649-655 from:
# create new item to:
<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"/> Changed line 659 from:
oNewItem = misc.getClassByName(data.pop('CC'))()
to:
<dir name="recipemanager" path="org/innoscript/recipemanager"/> Changed lines 663-691 from:
# get user role
iUserRole = objectAccess.getAccess(self.item, self.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 = self.server.temp_folder + '/' + data[prop]['tempfile']
oAttr.loadFromFile(sPath)
os.remove(sPath)
elif isinstance(oAttr, datatypes.Date):
oAttr.value = data[prop].value
else:
oAttr.value = data[prop]
txn = self.server.store.getTransaction()
oNewItem.appendTo(self.item.id, txn)
txn.commit()
to:
</dirs> </config> Changed lines 666-669 from:
to:
Each published directory must contain a file called "config.xml". This file keeps track of the files accesible over HTTP. This file should be:
Changed lines 671-683 from:
return True to:
<config> <context path="recipemanager.quix" method="GET" client=".*" lang=".*" action="recipemanager.quix"/> <context path="recipemanager.js" method="GET" client=".*" lang=".*" action="recipemanager.js"/> </config> Added lines 685-694:
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 nodes ID with the ID of the Recipes folder given by the system. The contents of this file are: Changed lines 697-717 from:
... to:
<?xml version="1.0" encoding="UTF-8"?> <a:window xmlns:a="http://www.innoscript.org/quix" title="Recipe manager" resizable="true" close="true" minimize="true" maximize="true" img="" width="640" height="480" left="center" top="center"> <a:script name="Recipe manager Script" src="recipemanager/recipemanager.js"/>
<a:wbody>
<a:box width="100%" height="100%" orientation="v" spacing="0">
<a:toolbar height="28">
<a:tbbutton caption="Create recipe folder" width="120">
<a:prop name="cc" value="org.innoscript.recipemanager.schema.RecipeContainer?"/>
</a:tbbutton>
<a:tbbutton caption="Create recipe" width="80">
<a:prop name="cc" value="org.innoscript.recipemanager.schema.Recipe"/>
</a:tbbutton>
<a:tbsep/>
<a:tbbutton caption="Refresh" width="60"/>
</a:toolbar>
<a:splitter height="-1" orientation="v" interactive="true">
<a:pane length="200">
<a:outlookbar width="100%" height="100%">
<a:tool caption="Recipes">
Changed lines 719-730 from:
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 method simply returns "True" only if the object is created and appended to the container successfully. This way, the browser knows that the request has been succefully completed. Also notice, that the "ContainerGeneric" servlet is a subclass of the "ItemGeneric" servlet. Besides the container specific methods defined, a container can also be updated, copied or renamed just like any other Porcupine object. Respectively, when editing an existing recipe, we call the "update" method of the "org.innoscript.desktop.XMLRPC.ItemGeneric" XMLRPC servlet. The recipe form also includes the "org/innoscript/desktop/generic.js" JavaScript file. From this file we reuse two JavaScript functions named "generic.selectItems" and "generic.removeSelectedItems".
to:
Changed lines 721-726 from:
... generic.selectItems = function(evt, w) { var oDialog = w.getParentByType(Dialog); var oTarget = w.parent.parent.getWidgetsByType(SelectList?)[0]; var sFolderURI = oTarget.attributes.SelectFrom?; var sCC = oTarget.attributes.RelatedCC?; to:
<a:foldertree id="tree" method="getSubtree" padding="4,4,4,4">
<a:treenode id="[Put here the ID of the Recipes Porcupine folder]" haschildren="true"
img="desktop/images/folder.gif" caption="Recipes"/>
Changed line 725 from:
to:
Changed lines 727-728 from:
generic.selectObjects(oDialog, oTarget, generic.addSelectionToList,
sFolderURI, sCC, 'true');
to:
</a:foldertree>
</a:tool>
<a:tool caption="Search" bgcolor="gray">
<a:label caption="Recipe title contains:"/>
<a:field id="title" left="5" top="20" width="160"/>
<a:label top="50" caption="Preparation time is less than"/>
<a:field id="preparationTime" left="5" top="70" width="30"/>
<a:label top="72" left="40" caption="min."/>
<a:label top="100" caption="The recipe's rating is greater than"/>
<a:spinbutton top="120" left="5" width="40" max="10" value="0" id="rating"/>
<a:label top="150" caption="Recipe ingredients contain:"/>
<a:field id="ingredients" top="170" left="5" width="160"/>
<a:label top="190" caption="(comma separated list)" style="font-style:italic"/>
<a:button top="220" left="center" width="60" height="28" caption="Search"/>
</a:tool>
</a:outlookbar>
</a:pane>
<a:pane length="-1">
Changed line 750 from:
to:
Changed lines 752-763 from:
} ... generic.selectObjects = function(win, target, select_func, startFrom, contentclass, multiple) { ... } ... generic.removeSelectedItems = function(evt, w) { var oSelectList = w.parent.parent.getWidgetsByType(SelectList?)[0]; oSelectList.removeSelected(); } ... to:
<a:listview width="100%" height="100%" id="list"> Deleted lines 753-773:
The "generic.selectItems" function displays the object select dialog by calling the "generic.selectObjects" function. The auto-generated form servlet uses this function for "ReferenceN" or "RelatorN" data types. The arguments given to the "generic.selectObjects" function are:
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 applicationApart 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: Changed lines 756-762 from:
<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"/> to:
<a:listheader>
<a:column width="140" caption="Recipe title" name="displayName" bgcolor="#EFEFEF" sortable="true"/>
<a:column width="60" caption="Servings" type="int" name="servings" sortable="true"/>
<a:column width="100" caption="Preparation time" type="int" name="preparationTime" sortable="true"/>
<a:column width="40" caption="Rating" type="int" name="rating" sortable="true"/>
<a:column width="160" caption="Date modified" type="date" name="modified" sortable="true"/>
</a:listheader>
</a:listview>
</a:pane>
</a:splitter>
</a:box>
</a:wbody>
</a:window> Changed lines 770-776 from:
to:
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.
Changed lines 778-780 from:
<dir name="recipemanager" path="org/innoscript/recipemanager"/> to:
... <a:toolbar width="100" height="100"> <a:tbbutton caption="Create recipe folder" width="120" Changed line 782 from:
to:
Changed lines 784-785 from:
</dirs> </config> to:
onclick="recipemanager.createItem"> Deleted lines 785-787:
Each published directory must contain a file called "config.xml". This file keeps track of the files accesible over HTTP. This file should be: Changed lines 788-800 from:
<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:
... </a:tbbutton> <a:tbbutton caption="Create recipe" width="80" Changed lines 792-802 from:
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 nodes ID with the ID of the Recipes folder given by the system. The contents of this file are:
to:
Changed lines 794-814 from:
<?xml version="1.0" encoding="UTF-8"?> <a:window xmlns:a="http://www.innoscript.org/quix" title="Recipe manager" resizable="true" close="true" minimize="true" maximize="true" img="" width="640" height="480" left="center" top="center"> <a:script name="Recipe manager Script" src="recipemanager/recipemanager.js"/>
<a:wbody>
<a:box width="100%" height="100%" orientation="v" spacing="0">
<a:toolbar height="28">
<a:tbbutton caption="Create recipe folder" width="120">
<a:prop name="cc" value="org.innoscript.recipemanager.schema.RecipeContainer?"/>
</a:tbbutton>
<a:tbbutton caption="Create recipe" width="80">
<a:prop name="cc" value="org.innoscript.recipemanager.schema.Recipe"/>
</a:tbbutton>
<a:tbsep/>
<a:tbbutton caption="Refresh" width="60"/>
</a:toolbar>
<a:splitter height="-1" orientation="v" interactive="true">
<a:pane length="200">
<a:outlookbar width="100%" height="100%">
<a:tool caption="Recipes">
to:
onclick="recipemanager.createItem"> Changed line 796 from:
to:
Changed lines 798-800 from:
<a:foldertree id="tree" method="getSubtree" padding="4,4,4,4">
<a:treenode id="[Put here the ID of the Recipes Porcupine folder]" haschildren="true"
img="desktop/images/folder.gif" caption="Recipes"/>
to:
... </a:tbbutton> <a:tbsep /> <a:tbbutton caption="Refresh" width="60" Changed line 803 from:
to:
Changed lines 805-826 from:
</a:foldertree>
</a:tool>
<a:tool caption="Search" bgcolor="gray">
<a:label caption="Recipe title contains:"/>
<a:field id="title" left="5" top="20" width="160"/>
<a:label top="50" caption="Preparation time is less than"/>
<a:field id="preparationTime" left="5" top="70" width="30"/>
<a:label top="72" left="40" caption="min."/>
<a:label top="100" caption="The recipe's rating is greater than"/>
<a:spinbutton top="120" left="5" width="40" max="10" value="0" id="rating"/>
<a:label top="150" caption="Recipe ingredients contain:"/>
<a:field id="ingredients" top="170" left="5" width="160"/>
<a:label top="190" caption="(comma separated list)" style="font-style:italic"/>
<a:button top="220" left="center" width="60" height="28" caption="Search"/>
</a:tool>
</a:outlookbar>
</a:pane>
<a:pane length="-1">
to:
onclick="recipemanager.refresh_onclick" /> Changed line 807 from:
to:
Changed lines 809-811 from:
<a:listview width="100%" height="100%" id="list"> to:
</a:toolbar> ... <a:listview id="list" multiple="true" width="100" height="100" Changed line 813 from:
to:
Changed lines 815-827 from:
<a:listheader>
<a:column width="140" caption="Recipe title" name="displayName" bgcolor="#EFEFEF" sortable="true"/>
<a:column width="60" caption="Servings" type="int" name="servings" sortable="true"/>
<a:column width="100" caption="Preparation time" type="int" name="preparationTime" sortable="true"/>
<a:column width="40" caption="Rating" type="int" name="rating" sortable="true"/>
<a:column width="160" caption="Date modified" type="date" name="modified" sortable="true"/>
</a:listheader>
</a:listview>
</a:pane>
</a:splitter>
</a:box>
</a:wbody>
</a:window> to:
onload="recipemanager.loadRecipes"> Deleted lines 816-821:
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. Changed lines 819-821 from:
... <a:toolbar width="100" height="100"> <a:tbbutton caption="Create recipe folder" width="120" to:
... Changed lines 821-824 from:
to:
Insert the following functions inside the empty "recipemanager.js" file:
Changed lines 826-830 from:
onclick="recipemanager.createItem"> to:
function recipemanager() {} recipemanager.createItem = function(evt, w) { var oWin = w.getParentByType(Window);
var oTree = oWin.getWidgetById('tree');
Changed line 832 from:
to:
Changed lines 834-836 from:
... </a:tbbutton> <a:tbbutton caption="Create recipe" width="80" to:
var sCC = w.attributes.cc; Changed line 836 from:
to:
Changed line 838 from:
onclick="recipemanager.createItem"> to:
var id = oTree.getSelection().getId(); Changed line 840 from:
to:
Changed lines 842-845 from:
... </a:tbbutton> <a:tbsep /> <a:tbbutton caption="Refresh" width="60" to:
oWin.showWindow(id + '?cmd=new&cc=' + sCC,
function(dlg) {
dlg.attachEvent("onclose", recipemanager.dialogClose);
}
);
Changed line 848 from:
to:
Changed lines 850-860 from:
onclick="recipemanager.refresh_onclick" /> to:
} recipemanager.dialogClose = function(dlg) { if (dlg.buttonIndex == 0) {
recipemanager.refreshList(dlg.opener);
}
} recipemanager.refresh_onclick = function(evt, w) { recipemanager.loadRecipes(w); } Changed line 862 from:
to:
Changed lines 864-866 from:
</a:toolbar> ... <a:listview id="list" multiple="true" width="100" height="100" to:
recipemanager.loadRecipes = function(w) { Changed line 866 from:
to:
Changed lines 868-878 from:
onload="recipemanager.loadRecipes"> to:
var appWin = w.getParentByType(Window); recipemanager.refreshList(appWin); } recipemanager.refreshList = function(appWin) { var oTree = appWin.getWidgetById('tree');
var id = oTree.getSelection().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";
Changed line 880 from:
to:
Changed lines 882-884 from:
... to:
var xmlrpc = new XMLRPCRequest?(QuiX?.root); xmlrpc.oncomplete = recipemanager.updateList; xmlrpc.callback_info = listView; Deleted lines 885-887:
Insert the following functions inside the empty "recipemanager.js" file: Changed lines 888-892 from:
function recipemanager() {} recipemanager.createItem = function(evt, w) { var oWin = w.getParentByType(Window);
var oTree = oWin.getWidgetById('tree');
to:
xmlrpc.callmethod('executeOqlCommand', sOql);
} recipemanager.updateList = function(req) { Changed lines 895-896 from:
var sCC = w.attributes.cc; to:
req.callback_info.dataSet = req.response; req.callback_info.refresh(); Changed line 900 from:
var id = oTree.getSelection().getId(); to:
} Changed lines 902-919 from:
to:
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 "a: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 when the list view is loaded. This handler loads the recipes from the selected container and displays them in the list view. The "onload" event handler, unlike the other events, does not accept the two common event handler arguments, the event object and the widget, but instead accepts only the widget that fires the event. 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 XMLRPC request to the root folder. The "oncomplete" attribute of the request is the callback function to call when the query has completed. This 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 XMLRPC call. Afterwards, we must synchronize the tree with the list view. This means that for each new tree selection we should update the list view accordingly. This is accomplished by adding the following event handler:
Changed lines 921-928 from:
document.desktop.parseFromUrl(id + '?cmd=new&cc=' + sCC,
function(w) {
w.attributes.refreshlist = function() {
if (sCC=='org.innoscript.recipemanager.schema.Recipe')
recipemanager.refreshList(oWin);
}
}
);
to:
... <a:foldertree id="tree" method="getSubtree" padding="4,4,4,4" Changed line 924 from:
to:
Changed lines 926-930 from:
} recipemanager.refresh_onclick = function(evt, w) { recipemanager.loadRecipes(w); } to:
onselect="recipemanager.loadRecipes"> Changed line 928 from:
to:
Changed lines 930-932 from:
recipemanager.loadRecipes = function(w) { to:
... </a:foldertree> ... Added lines 934-936:
Next, we implement the search functionality. We start by adding a new "onclick" event handler on the search button: Changed lines 939-949 from:
var appWin = w.getParentByType(Window); recipemanager.refreshList(appWin); } recipemanager.refreshList = function(appWin) { var oTree = appWin.getWidgetById('tree');
var id = oTree.getSelection().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";
to:
... <a:button top="220" left="center" width="60" height="28" caption="Search" Changed lines 944-946 from:
var xmlrpc = new XMLRPCRequest?(QuiX?.root); xmlrpc.oncomplete = recipemanager.updateList; xmlrpc.callback_info = listView; to:
onclick="recipemanager.search" /> Changed lines 948-951 from:
xmlrpc.callmethod('executeOqlCommand', sOql);
} recipemanager.updateList = function(req) { to:
... Changed lines 950-953 from:
to:
Add the following function inside the "recipemanager.js" file:
Changed lines 955-956 from:
req.callback_info.dataSet = req.response; req.callback_info.refresh(); to:
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, " +
Changed line 962 from:
to:
Changed line 964 from:
} to:
"modified from deep('/Recipes')";
Deleted lines 965-981:
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 "a:prop" nodes. The location of the new object is determined by the current tree selection. The "parseFromUrl" QuiX widget method loads an XML UI definition from a specified URL, inside the referring widget. The second argument of this method is a function called when the interface is loaded. The first argument of this function is the root widget created. In this event, the root widget - the form dialog - will be appended to the desktop ("document.desktop" is the desktop widget). The "recipemanager.loadrecipes" handler is called once when the list view is loaded. This handler loads the recipes from the selected container and displays them in the list view. The "onload" event handler, unlike the other events, does not accept the two common event handler arguments, the event object and the widget, but instead accepts only the widget that fires the event. 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 XMLRPC request to the root folder. The "oncomplete" attribute of the request is the callback function to call when the query has completed. This 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 XMLRPC call. Afterwards, we must synchronize the tree with the list view. This means that for each new tree selection we should update the list view accordingly. This is accomplished by adding the following event handler: Changed lines 968-969 from:
... <a:foldertree id="tree" method="getSubtree" padding="4,4,4,4" to:
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);
}
} Changed lines 994-998 from:
to:
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:
Changed lines 1000-1001 from:
onselect="recipemanager.loadRecipes"> to:
<a:listview multiple="true" width="100" height="100" id="list" onload="recipemanager.loadRecipes" Changed line 1003 from:
to:
Changed lines 1005-1007 from:
... </a:foldertree> ... to:
ondblclick="recipemanager.loadItem"> Changed lines 1008-1009 from:
Next, we implement the search functionality. We start by adding a new "onclick" event handler on the search button: to:
The following event handler should be added to the script of the application: Changed lines 1012-1013 from:
... <a:button top="220" left="center" width="60" height="28" caption="Search" to:
recipemanager.loadItem = function(evt, w, recipe) { var oWin = w.getParentByType(Window);
oWin.showWindow(recipe.id + "?cmd=properties",
function(dlg) {
dlg.attachEvent("onclose", recipemanager.dialogClose);
}
);
} Changed lines 1021-1027 from:
to:
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. Open the application object and add the following lines to the interface definition:
Changed line 1029 from:
onclick="recipemanager.search" /> to:
<a:pane length="-1"> Changed line 1031 from:
to:
Changed lines 1033-1036 from:
... to:
<a:contextmenu onshow="recipemanager.contextmenu_onshow">
<a:menuoption caption="Open" onclick="recipemanager.openRecipe" />
<a:menuoption caption="Delete" onclick="recipemanager.deleteRecipe" />
</a:contextmenu>
Deleted lines 1037-1039:
Add the following function inside the "recipemanager.js" file: Changed lines 1040-1045 from:
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, " +
to:
<a:listview width="100" height="100" id="list"
onload="recipemanager.loadRecipes" ondblclick="recipemanager.loadItem">
...
</a:listview>
</a:pane> Changed lines 1046-1049 from:
to:
Add the following handlers to the applications script:
Changed line 1051 from:
"modified from deep('/Recipes')";
to:
recipemanager.contextmenu_onshow = function(menu) { Changed line 1053 from:
to:
Changed lines 1055-1079 from:
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);
}
} to:
var oList = menu.owner.getWidgetById('list');
Deleted lines 1056-1059:
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: Changed lines 1059-1083 from:
<a:listview multiple="true" width="100" height="100" id="list" onload="recipemanager.loadRecipes" @] 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);
generic.showObjectProperties(null, null, recipe,
function() {
recipemanager.refreshList(oWin);
}
[@ ); to:
menu.options[0].disabled = (oList.selection.length == 0); //open menu.options[1].disabled = (oList.selection.length == 0); //properties Added lines 1062-1087:
recipemanager.openRecipe = function(evt, w) { var oList = w.parent.owner.getWidgetById('list');
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.getWidgetById('list');
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');
}
Changed lines 1089-1095 from:
This event handler reuses the "generic.showObjectProperties" function. The function given as an argument is called when the user updates the item and the list view of the opener must be updated. The final enhancement is the addition of a context menu. Open the application object and add the following lines to the interface definition:
to:
Changed line 1091 from:
<a:pane length="-1"> to:
desktop.msgbox(w.getCaption(), Changed line 1093 from:
to:
Changed lines 1095-1098 from:
<a:contextmenu onshow="recipemanager.contextmenu_onshow">
<a:menuoption caption="Open" onclick="recipemanager.openRecipe" />
<a:menuoption caption="Delete" onclick="recipemanager.deleteRecipe" />
</a:contextmenu>
to:
"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);
} Added lines 1103-1127:
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 menus enclosing widget. The enclosing widget is accessible through the "owner" menu property. The "desktop.msgbox" is used for displaying message boxes. The arguments accepted by this method are:
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 applicationUsing the "pakager" utility we will consolidate all of the applications 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: Changed lines 1130-1134 from:
<a:listview width="100" height="100" id="list"
onload="recipemanager.loadRecipes" ondblclick="recipemanager.loadItem">
...
</a:listview>
</a:pane> to:
[package] name=RecipeManager? version=0.0.4 Changed lines 1135-1136 from:
Add the following handlers to the applications script: to:
The first section of the package definition file has information about the package (its name and its version). Please notice that the name of the package and the value of the name attribute of the package node in the "store.xml" configuration file must be identical. Changed line 1139 from:
recipemanager.contextmenu_onshow = function(menu) { to:
[files] Changed lines 1141-1144 from:
to:
The second section contains the relative paths to any files that need to be included in the package. Leave this section blank.
Changed line 1146 from:
var oList = menu.owner.getWidgetById('list');
to:
[dirs] Added lines 1148-1150:
This section adds directories to the package. Changed lines 1153-1176 from:
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.getWidgetById('list');
var selectedRecipe = oList.getSelection();
recipemanager.loadItem(null, oList, selectedRecipe);
} recipemanager.deleteRecipe = function(evt, w) { var oList = w.parent.owner.getWidgetById('list');
var selectedRecipe = oList.getSelection();
var desktop = document.desktop;
_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');
}
to:
[pubdir] 1=recipemanager Changed lines 1156-1159 from:
to:
The fourth section contains the published directories (usually containing external JavaScript files and images) that are required by the 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.
Changed lines 1161-1165 from:
desktop.msgbox(w.getCaption(), to:
[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] Added lines 1167-1170:
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. Changed lines 1173-1179 from:
"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);
} to:
[scripts] postinstall=PKG/postinstall.py uninstall=PKG/uninstall.py Changed lines 1178-1201 from:
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 menus enclosing widget. The enclosing widget is accessible through the "owner" menu property. The "desktop.msgbox" is used for displaying message boxes. The arguments accepted by this method are:
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 applicationUsing the "pakager" utility we will consolidate all of the applications 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: to:
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. Lets 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: Changed lines 1184-1186 from:
[package] name=RecipeManager? version=0.0.3 to:
from porcupine.administration import codegen ce = codegen.DatatypeEditor?('org.innoscript.desktop.schema.properties.CategoryObjects?') Changed lines 1188-1191 from:
The first section of the package definition file has information about the package (its name and its version). Please notice that the name of the package and the value of the name attribute of the package node in the "store.xml" configuration file must be identical.
to:
Changed line 1190 from:
[files] to:
ce.relCc.append('org.innoscript.recipemanager.schema.Recipe') Deleted lines 1191-1193:
The second section contains the relative paths to any files that need to be included in the package. Leave this section blank. Changed line 1194 from:
[dirs] to:
ce.commitChanges() Changed lines 1197-1198 from:
This section adds directories to the package. to:
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.Recipe" 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: Changed lines 1202-1203 from:
[pubdir] 1=recipemanager to:
from porcupine.administration import codegen ce = codegen.DatatypeEditor?('org.innoscript.desktop.schema.properties.CategoryObjects?') Changed lines 1206-1209 from:
The fourth section contains the published directories (usually containing external JavaScript files and images) that are required by the 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.
to:
Changed lines 1208-1212 from:
[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] to:
ce.relCc.remove('org.innoscript.recipemanager.schema.Recipe') Deleted lines 1209-1212:
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. Changed lines 1212-1214 from:
[scripts] postinstall=PKG/postinstall.py uninstall=PKG/uninstall.py to:
ce.commitChanges() Deleted lines 1214-1250:
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. Lets 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.Recipe')
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.Recipe" 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.Recipe')
ce.commitChanges() November 11, 2006, at 07:32 PM
by -
Changed lines 1246-1248 from:
The Porcupine package file (RecipeManager-0.0.1.ppf) is created inside the Porcupine installation directory. The application can be installed on another Porcupine installation using the following command:
to:
The Porcupine package file (RecipeManager-0.0.3.ppf) is created inside the Porcupine installation directory. The application can be installed on another Porcupine installation using the following command:
November 11, 2006, at 07:29 PM
by -
Deleted lines 2-3:
This tutorial is valid only for Porcupine 0.0.7. We are currently in the process of updating this tutorial to comply with the new 0.0.8 release. November 11, 2006, at 07:13 PM
by -
Changed line 1161 from:
version=0.0.1 to:
version=0.0.3 November 11, 2006, at 06:40 PM
by -
Changed line 20 from:
The code sections that begin with three dots (...) contain existing code, and they are mainly to:
The code sections that begin with three dots ("...") contain existing code, and they are mainly Changed line 485 from:
to:
November 11, 2006, at 04:54 PM
by -
Changed lines 443-444 from:
<a:dialog xmlns:a="http://www.innoscript.org/quix" title="%(TITLE)s"
img="%(ICON)s" close="true" width="480" height="380" left="30" top="10">
to:
<a:dialog xmlns:a="http://www.innoscript.org/quix" title="%(TITLE)s" img="%(ICON)s" close="true" width="480" height="380" left="30" top="10"> Changed line 451 from:
<a:form action="%(URI)s" method="%(METHOD)s" padding="4,4,4,4"> to:
<a:form action="%(URI)s" method="%(METHOD)s" padding="4,4,4,4"> Changed lines 455-494 from:
%(HIDDEN)s
<a:label caption="Recipe name:"/>
<a:field left="80" width="380" name="displayName"
value="%(DISPLAYNAME)s" />
<a:label caption="Description:" top="25" />
<a:field left="80" top="24" width="380" name="description"
value="%(DESCRIPTION)s" />
<a:hr top="50" width="100" />
<a:label top="58" caption="Preparation time (min):" />
<a:spinbutton name="preparationTime" top="55" left="110" width="50"
max="240" value="%(PREPARATION_TIME)d" editable="true" />
<a:label top="58" left="200" caption="Servings:" />
<a:spinbutton name="servings" top="55" left="250" width="40" max="12"
editable="true" value="%(SERVINGS)d" />
<a:label top="58" left="320" caption="Rating:" />
<a:spinbutton name="rating" top="55" left="360" width="40" max="10"
editable="true" value="%(RATING)d" />
<a:tabpane top="90" width="100" height="220">
<a:tab caption="Ingredients">
<a:field type="textarea" name="ingredients" width="100"
height="100">%(INGREDIENTS)s</a:field>
</a:tab>
<a:tab caption="Instructions">
<a:field type="textarea" name="instructions" width="100"
height="100">%(INSTRUCTIONS)s</a:field>
</a:tab>
<a:tab caption="Categories">
<a:splitter width="100" height="100" orientation="h">
<a:pane length="-1">
<a:selectlist name="categories" multiple="true" posts="all"
width="100" height="100">
<a:prop name="SelectFrom?"
value="Categories/Recipe categories"></a:prop>
<a:prop name="RelatedCC?"
value="schemas.org.innoscript.common.Category"></a:prop>
%(CATEGORIES)s
</a:selectlist>
</a:pane>
<a:pane length="24">
<a:flatbutton width="70" height="22" caption="Add"
to:
%(HIDDEN)s
<a:label caption="Recipe name:"/>
<a:field left="80" width="380" name="displayName"
value="%(DISPLAYNAME)s" />
<a:label caption="Description:" top="25" />
<a:field left="80" top="24" width="380" name="description"
value="%(DESCRIPTION)s" />
<a:hr top="50" width="100" />
<a:label top="58" caption="Preparation time (min):" />
<a:spinbutton name="preparationTime" top="55" left="110" width="50"
max="240" value="%(PREPARATION_TIME)d" editable="true" />
<a:label top="58" left="200" caption="Servings:" />
<a:spinbutton name="servings" top="55" left="250" width="40" max="12"
editable="true" value="%(SERVINGS)d" />
<a:label top="58" left="320" caption="Rating:" />
<a:spinbutton name="rating" top="55" left="360" width="40" max="10"
editable="true" value="%(RATING)d" />
<a:tabpane top="90" width="100" height="220">
<a:tab caption="Ingredients">
<a:field type="textarea" name="ingredients" width="100"
height="100">%(INGREDIENTS)s</a:field>
</a:tab>
<a:tab caption="Instructions">
<a:field type="textarea" name="instructions" width="100"
height="100">%(INSTRUCTIONS)s</a:field>
</a:tab>
<a:tab caption="Categories">
<a:box width="100" height="100" orientation="v">
<a:selectlist name="categories" multiple="true" posts="all" height="-1">
<a:prop name="SelectFrom?" value="Categories/Recipe categories"></a:prop>
<a:prop name="RelatedCC?" value="schemas.org.innoscript.common.Category"/>
%(CATEGORIES)s
</a:selectlist>
<a:rect height="24">
<a:flatbutton width="70" height="22" caption="Add"
Changed line 493 from:
onclick="generic.selectItems"></a:flatbutton> to:
onclick="generic.selectItems"></a:flatbutton> Changed line 497 from:
<a:flatbutton left="80" width="70" height="22" caption="Remove" to:
<a:flatbutton left="80" width="70" height="22" caption="Remove" Changed line 501 from:
onclick="generic.removeSelectedItems"></a:flatbutton> to:
onclick="generic.removeSelectedItems"></a:flatbutton> Changed lines 505-509 from:
</a:pane>
</a:splitter>
</a:tab>
</a:tabpane>
</a:form>
to:
</a:rect>
</a:box>
</a:tab>
</a:tabpane>
</a:form>
Changed line 521 from:
</a:dialog> to:
</a:dialog> November 11, 2006, at 04:40 PM
by -
Changed line 1134 from:
'images/messagebox_warning.gif', 'center', 'center', 260, 112); to:
'desktop/images/messagebox_warning.gif', 'center', 'center', 260, 112); November 10, 2006, at 09:37 PM
by -
Changed line 1125 from:
desktop.msgbox(w.caption, to:
desktop.msgbox(w.getCaption(), November 10, 2006, at 09:35 PM
by -
Changed line 881 from:
if (sCC=='schemas.org.innoscript.recipemanager.Recipe') to:
if (sCC=='org.innoscript.recipemanager.schema.Recipe') Changed line 911 from:
"'schemas.org.innoscript.recipemanager.Recipe' order by displayName asc"; to:
"'org.innoscript.recipemanager.schema.Recipe' order by displayName asc"; November 10, 2006, at 09:26 PM
by -
Changed line 398 from:
oRecipe = recipemanager.Recipe() to:
oRecipe = schema.Recipe() November 10, 2006, at 08:59 PM
by -
Changed line 247 from:
client="(MSIE 6)|(Mozilla/5.0.+rv:1.[7-9])" to:
client="(MSIE [6-7])|(Mozilla/5.0.+rv:1.[7-9])" Changed line 363 from:
client="(MSIE 6)|(Mozilla/5.0.+rv:1.[7-9])" to:
client="(MSIE [6-7])|(Mozilla/5.0.+rv:1.[7-9])" Changed line 372 from:
client="(MSIE 6)|(Mozilla/5.0.+rv:1.[7-9])" to:
client="(MSIE [6-7])|(Mozilla/5.0.+rv:1.[7-9])" November 10, 2006, at 08:47 PM
by -
Changed line 739 from:
<a:box width="100%" height="100%" orientation="h"> to:
<a:box width="100%" height="100%" orientation="v" spacing="0"> November 10, 2006, at 08:42 PM
by -
Changed line 759 from:
img="images/folder.gif" caption="Recipes"/> to:
img="desktop/images/folder.gif" caption="Recipes"/> November 10, 2006, at 08:31 PM
by -
Changed lines 65-66 from:
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 "[[http://www.innoscript.org/api/org.innoscript.desktop.schema.common.Category-class.html | Category" content class. to:
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. November 10, 2006, at 08:30 PM
by -
Changed line 25 from:
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. to:
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. Changed lines 27-28 from:
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: to:
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: Changed lines 35-36 from:
Next, we create the recipes container: to:
Next, we create the recipes' container content class: Changed lines 48-49 from:
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. It is worth mentioning that the special class attribute __slots__ should not be omitted from any custom content class or data type, since instances of such types consume considerably less memory. to:
"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. It is worth mentioning that the special class attribute "__slots__" should not be omitted from any custom content class or data type, since instances of such types consume considerably less memory. Changed lines 65-67 from:
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 org.innoscript.desktop.schema.common.Category content class. The Categories data type class is already defined; it is used for the categorization of the to:
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 "[[http://www.innoscript.org/api/org.innoscript.desktop.schema.common.Category-class.html | Category" content class. The "Categories" data type class is already defined; it is used for the categorization of the Changed lines 72-73 from:
In order to define the relation both ways, we have to somehow add the Recipe object to the list of objects 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: to:
In order to define the relation both ways, we have to somehow add the "Recipe" object to the list of objects 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: Changed lines 109-110 from:
Having defined all of our required data types, we proceed to the Recipe content class definition: to:
Having defined all of our required data types, we proceed to the "Recipe" content class definition: Changed lines 145-148 from:
The '_props_' class attribute is a special attribute used by the framework. It should be defined only in each new content class that has additional data type attributes. This attribute is a tuple of strings containing the names of all such attributes, including those of the super classes. The name of the categories attribute is obligatory. This is because we have a two-way many-to-many relationship between the Recipe and Category objects. To clarify this, we need to examine the Category content type defined in the common.py file. to:
The "__props__" class attribute is a special attribute used by the framework. It should be defined only in each new content class that has additional data type attributes. This attribute is a tuple of strings containing the names of all such attributes, including those of the super classes. The name of the "categories" attribute is obligatory. This is because we have a two-way many-to-many relationship between the "Recipe" and "Category" objects. To clarify this, we need to examine the "Category" content type defined in the "common.py" file. Changed lines 157-158 from:
The objects references that belong in a specified category are stored inside the category_objects attribute. The data type of this attribute is the category_objects class. It is not considered a good practice to assign the same name to the attribute name and the data type class. In this case this is happening because of a limitation that has been waived since the release of the 0.0.4 version. to:
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. It is not considered a good practice to assign the same name to the attribute name and the data type class. Changed lines 177-179 from:
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. to:
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. Changed lines 184-185 from:
temporarily edit the containment of the RootFolder content class, located inside common.py. to:
temporarily edit the containment of the "RootFolder" content class, located inside "common.py". Changed line 204 from:
list view and select New. Notice that the RecipeContainer type is added to the list of the to:
list view and select "New". Notice that the "RecipeContainer" type is added to the list of the Changed line 209 from:
Select the RecipeContainer type and create a new object named Recipes. If you double click on to:
Select the "RecipeContainer" type and create a new object named "Recipes". If you double click on Changed line 215 from:
RecipeContainer content class. Hence, we have to edit the store.xml file inside the conf to:
"RecipeContainer" content class. Hence, we have to edit the "store.xml" file inside the "conf" Changed line 217 from:
inside a new package node. Locate the packages node at the top of the afore-mentioned file and to:
inside a new package node. Locate the "packages" node at the top of the afore-mentioned file and Changed lines 262-265 from:
As you can see, the first registration takes care of the XML-RPC methods exposed by the RecipeContainer content type. For the time being, we need no special behavior; the RecipeContainer type exposes the same methods as any other container. Also, notice that the client parameter is set to vcXMLRPC. This should be true for every XML-RPC servlet we publish. to:
As you can see, the first registration takes care of the XMLRPC methods exposed by the "RecipeContainer" content type. For the time being, we need no special behavior; the "RecipeContainer" type exposes the same methods as any other container. Also, notice that the client parameter is set to "vcXMLRPC". This should be true for every XMLRPC servlet we publish. Changed line 267 from:
object inside a RecipeContainer folder. Because the org.innoscript.desktop.ui.Frm_AutoNew is a to:
object inside a "RecipeContainer" folder. Because the "org.innoscript.desktop.ui.Frm_AutoNew" is a Changed lines 271-272 from:
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 to:
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 Changed lines 274-276 from:
Before restarting the server, you can safely delete the RecipeContainer entry from the containment of the RootFolder content type. to:
Before restarting the server, you can safely delete the "RecipeContainer" entry from the containment of the "RootFolder" content type. Changed lines 279-280 from:
The current implementation of the org.innoscript.desktop.ui.Frm_AutoNew servlet has some limitations that force us to design a new form for the Recipe content type. Looking at the form to:
The current implementation of the "org.innoscript.desktop.ui.Frm_AutoNew" servlet has some limitations that force us to design a new form for the "Recipe" content type. Looking at the form Changed lines 289-293 from:
It is recommended to maintain the QuiX servlets and the XML-RPC servlets in separate Python files named ui.py and XMLRPC.py respectively. Consequently, we create and edit a new Python file named ui.py inside the recipemanager module. First, we import the XULServlet class, the servers desktop ui module and the content classes to:
It is recommended to maintain the QuiX servlets and the XMLRPC servlets in separate Python files named "ui.py" and "XMLRPC.py" respectively. Consequently, we create and edit a new Python file named "ui.py" inside the "recipemanager" module. First, we import the "XULServlet" class, the servers desktop "ui" module and the content classes Changed lines 306-307 from:
RecipeContainer type accepts both new recipe containers and recipes. When creating a new recipe container, the splitter must get the XML definition output from the Frm_Auto servlet, whereas to:
"RecipeContainer" type accepts both new recipe containers and recipes. When creating a new recipe container, the splitter must get the XML definition output from the "Frm_Auto" servlet, whereas Changed line 337 from:
In order for the servlet to execute successfully, a file named ui.RecipeContainerSplitter.quix must to:
In order for the servlet to execute successfully, a file named "ui.RecipeContainerSplitter.quix" must Changed lines 347-348 from:
params attribute of the servlet. Before proceeding to the recipe form itself, we must modify the store.xml configuration file by to:
"params" attribute of the servlet. Before proceeding to the recipe form itself, we must modify the "store.xml" configuration file by Changed lines 381-387 from:
The first modification assigns the RecipeContainerSplitter as the servlet to be executed when the browser GETs? a RecipeContainer object with the cmd parameter set to new. The second modification is simply a new servlet registration for the Recipe type, which renders the recipe form whenever a compatible browser GETs? an object of this type with the parameter set to properties. Now its time to write the RecipeForm servlet: to:
The first modification assigns the "RecipeContainerSplitter" as the servlet to be executed when the browser GETs? on a "RecipeContainer" object with the "cmd" query parameter set to "new". The second modification is simply a new servlet registration for the "Recipe" type, which renders the recipe form whenever a compatible browser GETs? on an object of this type with the parameter set to "properties". Now its time to write the "RecipeForm" servlet: Changed lines 434-439 from:
display the properties of an existing recipe. In the first case, the type of the object called is the RecipeContainer unlike the second case, in which the type of the object is the Recipe type. The QuiX XML definition of the form is a new file named ui.RecipeForm.quix inside the org/innoscript/recipemanager folder. to:
display the properties of an existing recipe. In the first case, the type of the object "called" is the "RecipeContainer" unlike the second case, in which the type of the object is the "Recipe" type. The QuiX XML definition of the form is a new file named "ui.RecipeForm.quix" inside the "org/innoscript/recipemanager" folder. Changed lines 529-530 from:
Initially, this form imports two QuiX desktop JavaScript files. The first script is the one used by the forms generated automatically by the Frm_Auto servlet. The function we reuse from this script (org/innoscript/desktop/ui.Frm_Auto.js) is the autoform.submit function. This function simply submits the first QuiX form found inside the current dialog. to:
Initially, this form imports two QuiX desktop JavaScript files. The first script is the one used by the forms generated automatically by the "Frm_Auto" servlet. The function we reuse from this script (org/innoscript/desktop/ui.Frm_Auto.js) is the "autoform.submit" function. This function simply submits the first QuiX form found inside the current dialog. Changed lines 558-564 from:
The submit method of a QuiX form takes one argument; the function to call asynchronously, once the browser has the submission response. In this case the dialog has a custom attribute which is actually the function to call for refreshing the openers list view. After calling this function, the dialog is closed. 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 an XML-RPC servlet registered in the store.xml configuration file (visit http://wiki.innoscript.org/index.php/Articles/RequestProcessingPipeline to see how each object is accessible over HTTP). The form's method parameter is the name of the method of the XML-RPC servlet to call. Since this form is used for both creating and editing recipes the name of the method is diferrent in each aspect. When creating a new recipe we call the create method of the org.innoscript.desktop.XMLRPC.ContainerGeneric XML-RPC servlet. to:
The "submit" method of a QuiX form takes one argument; the function to call asynchronously, once the browser has the submission response. In this case the dialog has a custom attribute which is actually the function to call for refreshing the openers list view. After calling this function, the dialog is closed. 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 an XMLRPC servlet registered in the "store.xml" configuration file (visit http://wiki.innoscript.org/index.php/Articles/RequestProcessingPipeline to see how each object is accessible over HTTP). The form's "method" parameter is the name of the method of the XMLRPC servlet to call. Since this form is used for both creating and editing recipes the name of the method is diferrent in each aspect. When creating a new recipe we call the "create" method of the "org.innoscript.desktop.XMLRPC.ContainerGeneric" XMLRPC servlet. Changed lines 623-632 from:
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 method simply returns True only if the object is created and appended to the container successfully. This way, the browser knows that the request has been succefully completed. Also notice, that the ContainerGeneric servlet is a subclass of the ItemGeneric servlet. Besides the container specific methods defined, a container can also be updated, copied or renamed just like any other Porcupine object. Respectively, when editing an existing recipe, we call the update method of the org.innoscript.desktop.XMLRPC.ItemGeneric XML-RPC servlet. The recipe form also includes the org/innoscript/desktop/generic.js JavaScript file. From this file we reuse two JavaScript functions named generic.selectItems and generic.removeSelectedItems. to:
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 method simply returns "True" only if the object is created and appended to the container successfully. This way, the browser knows that the request has been succefully completed. Also notice, that the "ContainerGeneric" servlet is a subclass of the "ItemGeneric" servlet. Besides the container specific methods defined, a container can also be updated, copied or renamed just like any other Porcupine object. Respectively, when editing an existing recipe, we call the "update" method of the "org.innoscript.desktop.XMLRPC.ItemGeneric" XMLRPC servlet. The recipe form also includes the "org/innoscript/desktop/generic.js" JavaScript file. From this file we reuse two JavaScript functions named "generic.selectItems" and "generic.removeSelectedItems". Changed line 663 from:
The generic.selectItems function displays the object select dialog by calling the generic.selectObjects function. The auto-generated form servlet uses this function for ReferenceN or RelatorN data types. The arguments given to the generic.selectObjects function are: to:
The "generic.selectItems" function displays the object select dialog by calling the "generic.selectObjects" function. The auto-generated form servlet uses this function for "ReferenceN" or "RelatorN" data types. The arguments given to the "generic.selectObjects" function are: Changed lines 666-667 from:
to:
Changed lines 669-670 from:
to:
Changed lines 681-682 from:
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: to:
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: Changed lines 703-704 from:
Each published directory must contain a file called "config.xml". This file keeps track of the files accesible over HTTP. This file should be: to:
Each published directory must contain a file called "config.xml". This file keeps track of the files accesible over HTTP. This file should be: Changed lines 723-727 from:
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. to:
"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. Changed lines 807-809 from:
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. to:
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. Changed lines 858-859 from:
Insert the following functions inside the empty ""recipemanager.js"" file: to:
Insert the following functions inside the empty "recipemanager.js" file: Changed lines 936-940 from:
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 a:prop nodes. The location of the new object is determined by the current tree selection. The parseFromUrl QuiX? widget method loads an XML UI definition from a specified URL, inside the referring widget. The second argument of this method is a function called when the interface is loaded. The first argument of this function is the root widget created. In this event, the root widget - the form dialog - will be appended to the desktop (document.desktop is the desktop widget). The recipemanager.loadrecipes handler is called once when the list view is loaded. This handler loads the recipes from the selected container and displays them in the list view. The onload event handler, unlike the other events, does not accept the two common event handler arguments, the event object and the widget, but instead accepts only the widget that fires the event. 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 to:
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 "a:prop" nodes. The location of the new object is determined by the current tree selection. The "parseFromUrl" QuiX widget method loads an XML UI definition from a specified URL, inside the referring widget. The second argument of this method is a function called when the interface is loaded. The first argument of this function is the root widget created. In this event, the root widget - the form dialog - will be appended to the desktop ("document.desktop" is the desktop widget). The "recipemanager.loadrecipes" handler is called once when the list view is loaded. This handler loads the recipes from the selected container and displays them in the list view. The "onload" event handler, unlike the other events, does not accept the two common event handler arguments, the event object and the widget, but instead accepts only the widget that fires the event. 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 Changed lines 942-947 from:
the root folder. The oncomplete attribute of the request is the callback function to call when the query has completed. This 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 XMLRPC call. to:
the root folder. The "oncomplete" attribute of the request is the callback function to call when the query has completed. This 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 XMLRPC call. Changed lines 968-969 from:
Next, we implement the search functionality. We start by adding a new onclick event handler on the search button: to:
Next, we implement the search functionality. We start by adding a new "onclick" event handler on the search button: Changed lines 984-985 from:
Add the following function inside the recipemanager.js" file: to:
Add the following function inside the "recipemanager.js" file: Changed line 1028 from:
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. to:
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. Changed lines 1061-1062 from:
This event handler reuses the generic.showObjectProperties function. The function given as an argument is called when the user updates the item and the list view of the opener must be updated. to:
This event handler reuses the "generic.showObjectProperties" function. The function given as an argument is called when the user updates the item and the list view of the opener must be updated. Changed lines 1138-1140 from:
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 menus enclosing widget. The enclosing widget is accessible through the owner menu property. The desktop.msgbox is used for displaying message boxes. The arguments accepted by this to:
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 menus enclosing widget. The enclosing widget is accessible through the "owner" menu property. The "desktop.msgbox" is used for displaying message boxes. The arguments accepted by this Changed lines 1159-1161 from:
Using the pakager utility we will consolidate all of the applications 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: to:
Using the "pakager" utility we will consolidate all of the applications 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: Changed lines 1169-1170 from:
The first section of the package definition file has information about the package (its name and its version). Please notice that the name of the package and the value of the name attribute of the package node in the store.xml configuration file must be identical. to:
The first section of the package definition file has information about the package (its name and its version). Please notice that the name of the package and the value of the name attribute of the package node in the "store.xml" configuration file must be identical. Changed lines 1191-1192 from:
The fourth section contains the published directories (usually containing external JavaScript files and images) that are required by the 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 destributed. to:
The fourth section contains the published directories (usually containing external JavaScript files and images) that are required by the 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. Changed lines 1197-1199 from:
recipes_folder=[Put here the ID of the /Recipes folder] recipe_categories_folder=[Put here the ID of the /Categories/Recipe categories folder] to:
recipes_folder=[Put here the ID of the "/Recipes" folder] recipe_categories_folder=[Put here the ID of the "/Categories/Recipe categories" folder] Changed lines 1203-1204 from:
application object itself. Replace the highlighted portions with the object IDs? printed on their properties dialog. to:
application object itself. Replace the highlighted portions with the object IDs printed on their properties dialog. Changed lines 1212-1215 from:
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. Lets 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: to:
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. Lets 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: Changed lines 1231-1233 from:
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 category_objects class). To be precise, it adds the schemas.org.innoscript.recipemanager.Recipe 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: to:
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.Recipe" 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: Changed lines 1249-1250 from:
Before proceeding to the creation of the package, stop the Porcupine service. Afterwards, run the pakager utility as follows: to:
Before proceeding to the creation of the package, stop the Porcupine service. Afterwards, run the "pakager" utility as follows: November 10, 2006, at 08:04 PM
by -
Added line 725:
Changed line 741 from:
<a:tbbutton caption="Create recipe folder" width="120" onclick="recipemanager.createItem"> to:
<a:tbbutton caption="Create recipe folder" width="120"> Changed line 744 from:
<a:tbbutton caption="Create recipe" width="80" onclick="recipemanager.createItem"> to:
<a:tbbutton caption="Create recipe" width="80"> Changed line 748 from:
<a:tbbutton caption="Refresh" width="60" onclick="recipemanager.refresh_onclick"/> to:
<a:tbbutton caption="Refresh" width="60"/> Changed lines 757-758 from:
<a:foldertree id="tree" method="getSubtree" padding="4,4,4,4" onselect="recipemanager.loadRecipes">
<a:treenode id="[Put here the ID of the Recipes Porcupine Object]" haschildren="true"
to:
<a:foldertree id="tree" method="getSubtree" padding="4,4,4,4">
<a:treenode id="[Put here the ID of the Recipes Porcupine folder]" haschildren="true"
Changed line 780 from:
<a:button top="220" left="center" width="60" height="28" caption="Search" onclick="recipemanager.search"/> to:
<a:button top="220" left="center" width="60" height="28" caption="Search"/> Deleted lines 784-787:
<a:contextmenu onshow="recipemanager.contextmenu_onshow">
<a:menuoption caption="Open" onclick="recipemanager.openRecipe"/>
<a:menuoption caption="Delete" onclick="recipemanager.deleteRecipe"/>
</a:contextmenu>
Changed lines 788-789 from:
<a:listview width="100%" height="100%" id="list" onload="recipemanager.loadRecipes"
ondblclick="recipemanager.loadItem">
to:
<a:listview width="100%" height="100%" id="list"> Changed lines 808-809 from:
to:
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. Changed lines 858-859 from:
In the application's script tab insert the following code: to:
Insert the following functions inside the empty ""recipemanager.js"" file: Changed lines 938-939 from:
The recipemanager.loadrecipes handler is called once when the list view is loaded. This handler loads the recipes from the selected container and displays them in the list view. The onload event handler, unlike the other events, does not accept the two common event handler arguments, the event object and the widget, but instead accepts only the widget that fires the event. 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 pubdir/__xul/windows.js QuiX? module. to:
The recipemanager.loadrecipes handler is called once when the list view is loaded. This handler loads the recipes from the selected container and displays them in the list view. The onload event handler, unlike the other events, does not accept the two common event handler arguments, the event object and the widget, but instead accepts only the widget that fires the event. 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. Changed lines 946-947 from:
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. to:
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 XMLRPC call. Changed lines 984-985 from:
Add the following function to the Script tab of the application dialog: to:
Add the following function inside the recipemanager.js" file: Changed lines 1160-1161 from:
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: to:
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: Deleted line 1173:
1=schemas/org/innoscript/recipemanager.py Changed lines 1176-1177 from:
The second section has the relative paths of the files required by the application. Particularly, this section adds to the package the applications content classes. to:
The second section contains the relative paths to any files that need to be included in the package. Leave this section blank. Deleted line 1180:
1=resources/recipemanager Changed lines 1183-1184 from:
The third section adds directories to the package. This directory is the one that contains the servlets. to:
This section adds directories to the package. Added line 1188:
1=recipemanager Changed lines 1191-1192 from:
The fourth section contains the published directories (usually containing external JavaScript? files and images) that are required by the application. In this case, we did not create a new published directory; all of the applications JavaScript? handlers are embedded inside the application object and we did not use any new images. to:
The fourth section contains the published directories (usually containing external JavaScript files and images) that are required by the 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 destributed. Changed line 1220 from:
ce = codegen.DatatypeEditor?('schemas.org.innoscript.properties.category_objects') to:
ce = codegen.DatatypeEditor?('org.innoscript.desktop.schema.properties.CategoryObjects?') Changed line 1224 from:
ce.relCc.append('schemas.org.innoscript.recipemanager.Recipe') to:
ce.relCc.append('org.innoscript.recipemanager.schema.Recipe') Changed line 1238 from:
ce = codegen.DatatypeEditor?('schemas.org.innoscript.properties.category_objects') to:
ce = codegen.DatatypeEditor?('org.innoscript.desktop.schema.properties.CategoryObjects?') Changed line 1242 from:
ce.relCc.remove('schemas.org.innoscript.recipemanager.Recipe') to:
ce.relCc.remove('org.innoscript.recipemanager.schema.Recipe') November 10, 2006, at 07:31 PM
by -
Changed lines 724-726 from:
Create a new text file named "recipemanager.quix" inside the "recipemanager" folder we just published. to:
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. Changed lines 732-750 from:
<a:splitter width="100" height="100" orientation="h" spacing="0"> <a:pane length="28">
<a:toolbar width="100" height="100">
<a:tbbutton caption="Create recipe folder" width="120">
<a:prop name="cc" value="schemas.org.innoscript.recipemanager.RecipeContainer?"/>
</a:tbbutton>
<a:tbbutton caption="Create recipe" width="80">
<a:prop name="cc" value="schemas.org.innoscript.recipemanager.Recipe"/>
</a:tbbutton>
<a:tbsep/>
<a:tbbutton caption="Refresh" width="60" />
</a:toolbar>
</a:pane>
<a:pane length="-1" bgcolor="white">
<a:splitter width="100" height="100" orientation="v"
interactive="true">
<a:pane length="200">
<a:outlookbar width="100" height="100">
<a:tool caption="Recipes">
to:
<?xml version="1.0" encoding="UTF-8"?> <a:window xmlns:a="http://www.innoscript.org/quix" title="Recipe manager" resizable="true" close="true" minimize="true" maximize="true" img="" width="640" height="480" left="center" top="center"> <a:script name="Recipe manager Script" src="recipemanager/recipemanager.js"/>
<a:wbody>
<a:box width="100%" height="100%" orientation="h">
<a:toolbar height="28">
<a:tbbutton caption="Create recipe folder" width="120" onclick="recipemanager.createItem">
<a:prop name="cc" value="org.innoscript.recipemanager.schema.RecipeContainer?"/>
</a:tbbutton>
<a:tbbutton caption="Create recipe" width="80" onclick="recipemanager.createItem">
<a:prop name="cc" value="org.innoscript.recipemanager.schema.Recipe"/>
</a:tbbutton>
<a:tbsep/>
<a:tbbutton caption="Refresh" width="60" onclick="recipemanager.refresh_onclick"/>
</a:toolbar>
<a:splitter height="-1" orientation="v" interactive="true">
<a:pane length="200">
<a:outlookbar width="100%" height="100%">
<a:tool caption="Recipes">
Changed lines 756-757 from:
<a:foldertree id="tree" method="getSubtree" padding="4,4,4,4">
<a:treenode id="[Put here the ID of the Recipes folder]" haschildren="true"
to:
<a:foldertree id="tree" method="getSubtree" padding="4,4,4,4" onselect="recipemanager.loadRecipes">
<a:treenode id="[Put here the ID of the Recipes Porcupine Object]" haschildren="true"
img="images/folder.gif" caption="Recipes"/>
Changed lines 762-784 from:
img="images/folder.gif" caption="Recipes">
</a:treenode>
</a:foldertree>
</a:tool>
<a:tool caption="Search" bgcolor="gray">
<a:label caption="Recipe title contains:" />
<a:field id="title" left="5" top="20" width="160" />
<a:label top="50" caption="Preparation time is less than" />
<a:field id="preparationTime" left="5" top="70" width="30" />
<a:label top="72" left="40" caption="min." />
<a:label top="100" caption="The recipe's rating is greater than" />
<a:spinbutton top="120" left="5" width="40" max="10" value="0"
id="rating"/>
<a:label top="150" caption="Recipe ingredients contain:" />
<a:field id="ingredients" top="170" left="5" width="160" />
<a:label top="190" caption="(comma separated list)"
style="font-style:italic" />
<a:button top="220" left="center" width="60" height="28"
caption="Search" />
</a:tool>
</a:outlookbar>
</a:pane>
<a:pane length="-1">
to:
</a:foldertree>
</a:tool>
<a:tool caption="Search" bgcolor="gray">
<a:label caption="Recipe title contains:"/>
<a:field id="title" left="5" top="20" width="160"/>
<a:label top="50" caption="Preparation time is less than"/>
<a:field id="preparationTime" left="5" top="70" width="30"/>
<a:label top="72" left="40" caption="min."/>
<a:label top="100" caption="The recipe's rating is greater than"/>
<a:spinbutton top="120" left="5" width="40" max="10" value="0" id="rating"/>
<a:label top="150" caption="Recipe ingredients contain:"/>
<a:field id="ingredients" top="170" left="5" width="160"/>
<a:label top="190" caption="(comma separated list)" style="font-style:italic"/>
<a:button top="220" left="center" width="60" height="28" caption="Search" onclick="recipemanager.search"/>
</a:tool>
</a:outlookbar>
</a:pane>
<a:pane length="-1">
<a:contextmenu onshow="recipemanager.contextmenu_onshow">
<a:menuoption caption="Open" onclick="recipemanager.openRecipe"/>
<a:menuoption caption="Delete" onclick="recipemanager.deleteRecipe"/>
</a:contextmenu>
Changed lines 791-792 from:
<a:listview id="list" width="100" height="100"> to:
<a:listview width="100%" height="100%" id="list" onload="recipemanager.loadRecipes"
ondblclick="recipemanager.loadItem">
Changed lines 796-812 from:
<a:listheader>
<a:column width="140" caption="Recipe title" name="displayName"
bgcolor="#EFEFEF" sortable="true"></a:column>
<a:column width="60" caption="Servings" type="int" name="servings"
sortable="true"></a:column>
<a:column width="100" caption="Preparation time" type="int"
name="preparationTime" sortable="true"></a:column>
<a:column width="40" caption="Rating" type="int" name="rating"
sortable="true"></a:column>
<a:column width="160" caption="Date modified" type="date"
name="modified" sortable="true"></a:column>
</a:listheader>
</a:listview>
</a:pane>
</a:splitter>
</a:pane>
</a:splitter> to:
<a:listheader>
<a:column width="140" caption="Recipe title" name="displayName" bgcolor="#EFEFEF" sortable="true"/>
<a:column width="60" caption="Servings" type="int" name="servings" sortable="true"/>
<a:column width="100" caption="Preparation time" type="int" name="preparationTime" sortable="true"/>
<a:column width="40" caption="Rating" type="int" name="rating" sortable="true"/>
<a:column width="160" caption="Date modified" type="date" name="modified" sortable="true"/>
</a:listheader>
</a:listview>
</a:pane>
</a:splitter>
</a:box>
</a:wbody>
</a:window> Deleted lines 810-812:
Press the Create button to create the application object. The above interface contains no functionality. We have just created a dummy application. November 10, 2006, at 07:20 PM
by -
Changed line 287 from:
to:
Therefore, we have to create a new recipe form using a new QuiX servlet. We start by creating the Changed line 289 from:
to:
It is recommended to maintain the QuiX servlets and the XML-RPC servlets in separate Python files Changed lines 331-332 from:
Remember that every QuiX servlet (an instance of the XULServlet class) must always be accompanied with a xul file in the same directory. This file usually contains the XML definition of the interface, to:
Remember that every QuiX servlet (an instance of the XULServlet class) must always be accompanied with a quix file in the same directory. This file usually contains the XML definition of the interface, Changed line 437 from:
to:
The QuiX XML definition of the form is a new file named ui.RecipeForm.quix inside the Changed lines 529-530 from:
Initially, this form imports two QuiX desktop JavaScript? files. The first script is the one used by the forms generated automatically by the Frm_Auto servlet. The function we reuse from this script(org/innoscript/desktop/ui.Frm_Auto.js) is the autoform.submit function. This function simply submits the first QuiX form found inside the current dialog. to:
Initially, this form imports two QuiX desktop JavaScript files. The first script is the one used by the forms generated automatically by the Frm_Auto servlet. The function we reuse from this script (org/innoscript/desktop/ui.Frm_Auto.js) is the autoform.submit function. This function simply submits the first QuiX form found inside the current dialog. Changed lines 631-632 from:
The recipe form also includes the org/innoscript/desktop/generic.js JavaScript file. From this file we reuse two JavaScript? functions named generic.selectItems and generic.removeSelectedItems. to:
The recipe form also includes the org/innoscript/desktop/generic.js JavaScript file. From this file we reuse two JavaScript functions named generic.selectItems and generic.removeSelectedItems. Changed line 663 from:
The generic.selectItems function displays the object select dialog by calling the generic.selectObjects function. The auto-generated form servlet uses this function for ReferenceN? or RelatorN? data types. The arguments given to the generic.selectObjects function are: to:
The generic.selectItems function displays the object select dialog by calling the generic.selectObjects function. The auto-generated form servlet uses this function for ReferenceN or RelatorN data types. The arguments given to the generic.selectObjects function are: Changed line 678 from:
Apart from the custom forms and the custom content types, we will also need a custom UI tailored to:
Apart from the custom forms and the custom content types, we will also need a custom UI for our application tailored Changed lines 681-684 from:
To create a new application, login to Porcupine as an administrator and navigate to the Administrative Tools/Applications folder. Create a new application named Recipe manager. Set the dimensions of the application window to 640x480 (width and height). In the interface tab, paste the following QuiX? definition. Do not forget to replace the tree nodes ID with the ID of the Recipes folder given by the system. to:
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: Changed lines 685-703 from:
<a:splitter width="100" height="100" orientation="h" spacing="0"> <a:pane length="28">
<a:toolbar width="100" height="100">
<a:tbbutton caption="Create recipe folder" width="120">
<a:prop name="cc" value="schemas.org.innoscript.recipemanager.RecipeContainer?"/>
</a:tbbutton>
<a:tbbutton caption="Create recipe" width="80">
<a:prop name="cc" value="schemas.org.innoscript.recipemanager.Recipe"/>
</a:tbbutton>
<a:tbsep/>
<a:tbbutton caption="Refresh" width="60" />
</a:toolbar>
</a:pane>
<a:pane length="-1" bgcolor="white">
<a:splitter width="100" height="100" orientation="v"
interactive="true">
<a:pane length="200">
<a:outlookbar width="100" height="100">
<a:tool caption="Recipes">
to:
<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"/> Changed lines 695-696 from:
<a:foldertree id="tree" method="getSubtree" padding="4,4,4,4">
<a:treenode id="[Put here the ID of the Recipes folder]" haschildren="true"
to:
<dir name="recipemanager" path="org/innoscript/recipemanager"/> Changed lines 699-721 from:
img="images/folder.gif" caption="Recipes">
</a:treenode>
</a:foldertree>
</a:tool>
<a:tool caption="Search" bgcolor="gray">
<a:label caption="Recipe title contains:" />
<a:field id="title" left="5" top="20" width="160" />
<a:label top="50" caption="Preparation time is less than" />
<a:field id="preparationTime" left="5" top="70" width="30" />
<a:label top="72" left="40" caption="min." />
<a:label top="100" caption="The recipe's rating is greater than" />
<a:spinbutton top="120" left="5" width="40" max="10" value="0"
id="rating"/>
<a:label top="150" caption="Recipe ingredients contain:" />
<a:field id="ingredients" top="170" left="5" width="160" />
<a:label top="190" caption="(comma separated list)"
style="font-style:italic" />
<a:button top="220" left="center" width="60" height="28"
caption="Search" />
</a:tool>
</a:outlookbar>
</a:pane>
<a:pane length="-1">
to:
</dirs> </config> Changed lines 702-705 from:
to:
Each published directory must contain a file called "config.xml". This file keeps track of the files accesible over HTTP. This file should be:
Changed lines 707-719 from:
<a:listview id="list" width="100" height="100"> to:
<config> <context path="recipemanager.quix" method="GET" client=".*" lang=".*" action="recipemanager.quix"/> <context path="recipemanager.js" method="GET" client=".*" lang=".*" action="recipemanager.js"/> </config> Added lines 721-727:
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". Create a new text file named "recipemanager.quix" inside the "recipemanager" folder we just published. Do not forget to replace the tree nodes ID with the ID of the Recipes folder given by the system. The contents of this file are: Changed lines 730-744 from:
<a:listheader>
<a:column width="140" caption="Recipe title" name="displayName"
bgcolor="#EFEFEF" sortable="true"></a:column>
<a:column width="60" caption="Servings" type="int" name="servings"
sortable="true"></a:column>
<a:column width="100" caption="Preparation time" type="int"
name="preparationTime" sortable="true"></a:column>
<a:column width="40" caption="Rating" type="int" name="rating"
sortable="true"></a:column>
<a:column width="160" caption="Date modified" type="date"
name="modified" sortable="true"></a:column>
</a:listheader>
</a:listview>
</a:pane>
</a:splitter>
to:
<a:splitter width="100" height="100" orientation="h" spacing="0"> <a:pane length="28">
<a:toolbar width="100" height="100">
<a:tbbutton caption="Create recipe folder" width="120">
<a:prop name="cc" value="schemas.org.innoscript.recipemanager.RecipeContainer?"/>
</a:tbbutton>
<a:tbbutton caption="Create recipe" width="80">
<a:prop name="cc" value="schemas.org.innoscript.recipemanager.Recipe"/>
</a:tbbutton>
<a:tbsep/>
<a:tbbutton caption="Refresh" width="60" />
</a:toolbar>
Changed lines 743-748 from:
</a:splitter> to:
<a:pane length="-1" bgcolor="white">
<a:splitter width="100" height="100" orientation="v"
interactive="true">
<a:pane length="200">
<a:outlookbar width="100" height="100">
<a:tool caption="Recipes">
Changed lines 750-758 from:
Press the Create button to create the application object. The above interface contains no functionality. We have just created a dummy application. 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. Let's start by adding some event handlers.
to:
Changed lines 752-754 from:
... <a:toolbar width="100" height="100"> <a:tbbutton caption="Create recipe folder" width="120" to:
<a:foldertree id="tree" method="getSubtree" padding="4,4,4,4">
<a:treenode id="[Put here the ID of the Recipes folder]" haschildren="true"
Changed line 755 from:
to:
Changed lines 757-779 from:
onclick="recipemanager.createItem"> to:
img="images/folder.gif" caption="Recipes">
</a:treenode>
</a:foldertree>
</a:tool>
<a:tool caption="Search" bgcolor="gray">
<a:label caption="Recipe title contains:" />
<a:field id="title" left="5" top="20" width="160" />
<a:label top="50" caption="Preparation time is less than" />
<a:field id="preparationTime" left="5" top="70" width="30" />
<a:label top="72" left="40" caption="min." />
<a:label top="100" caption="The recipe's rating is greater than" />
<a:spinbutton top="120" left="5" width="40" max="10" value="0"
id="rating"/>
<a:label top="150" caption="Recipe ingredients contain:" />
<a:field id="ingredients" top="170" left="5" width="160" />
<a:label top="190" caption="(comma separated list)"
style="font-style:italic" />
<a:button top="220" left="center" width="60" height="28"
caption="Search" />
</a:tool>
</a:outlookbar>
</a:pane>
<a:pane length="-1">
Changed line 781 from:
to:
Changed lines 783-785 from:
... </a:tbbutton> <a:tbbutton caption="Create recipe" width="80" to:
<a:listview id="list" width="100" height="100"> Changed line 785 from:
to:
Changed lines 787-803 from:
onclick="recipemanager.createItem"> to:
<a:listheader>
<a:column width="140" caption="Recipe title" name="displayName"
bgcolor="#EFEFEF" sortable="true"></a:column>
<a:column width="60" caption="Servings" type="int" name="servings"
sortable="true"></a:column>
<a:column width="100" caption="Preparation time" type="int"
name="preparationTime" sortable="true"></a:column>
<a:column width="40" caption="Rating" type="int" name="rating"
sortable="true"></a:column>
<a:column width="160" caption="Date modified" type="date"
name="modified" sortable="true"></a:column>
</a:listheader>
</a:listview>
</a:pane>
</a:splitter>
</a:pane>
</a:splitter> Added lines 805-812:
Press the Create button to create the application object. The above interface contains no functionality. We have just created a dummy application. 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. Let's start by adding some event handlers. Changed lines 815-818 from:
... </a:tbbutton> <a:tbsep /> <a:tbbutton caption="Refresh" width="60" to:
... <a:toolbar width="100" height="100"> <a:tbbutton caption="Create recipe folder" width="120" Changed line 821 from:
onclick="recipemanager.refresh_onclick" /> to:
onclick="recipemanager.createItem"> Changed lines 825-827 from:
</a:toolbar> ... <a:listview id="list" multiple="true" width="100" height="100" to:
... </a:tbbutton> <a:tbbutton caption="Create recipe" width="80" Changed line 831 from:
onload="recipemanager.loadRecipes"> to:
onclick="recipemanager.createItem"> Changed lines 835-838 from:
... to:
... </a:tbbutton> <a:tbsep /> <a:tbbutton caption="Refresh" width="60" Changed lines 840-843 from:
In the application's script tab insert the following code:
to:
Changed lines 842-846 from:
function recipemanager() {} recipemanager.createItem = function(evt, w) { var oWin = w.getParentByType(Window);
var oTree = oWin.getWidgetById('tree');
to:
onclick="recipemanager.refresh_onclick" /> Changed line 844 from:
to:
Changed lines 846-848 from:
var sCC = w.attributes.cc; to:
</a:toolbar> ... <a:listview id="list" multiple="true" width="100" height="100" Changed line 850 from:
to:
Changed line 852 from:
var id = oTree.getSelection().getId(); to:
onload="recipemanager.loadRecipes"> Changed line 854 from:
to:
Changed lines 856-863 from:
document.desktop.parseFromUrl(id + '?cmd=new&cc=' + sCC,
function(w) {
w.attributes.refreshlist = function() {
if (sCC=='schemas.org.innoscript.recipemanager.Recipe')
recipemanager.refreshList(oWin);
}
}
);
to:
... Added lines 858-860:
In the application's script tab insert the following code: Changed lines 863-867 from:
} recipemanager.refresh_onclick = function(evt, w) { recipemanager.loadRecipes(w); } to:
function recipemanager() {} recipemanager.createItem = function(evt, w) { var oWin = w.getParentByType(Window);
var oTree = oWin.getWidgetById('tree');
Changed line 871 from:
recipemanager.loadRecipes = function(w) { to:
var sCC = w.attributes.cc; Deleted lines 874-879:
var appWin = w.getParentByType(Window); recipemanager.refreshList(appWin); } recipemanager.refreshList = function(appWin) { var oTree = appWin.getWidgetById('tree');
Deleted lines 875-878:
var listView = appWin.getWidgetById('list');
var sOql = "select id, displayName, servings, preparationTime, rating, " +
"modified from '" + id + "' where contentclass = " +
"'schemas.org.innoscript.recipemanager.Recipe' order by displayName asc";
Changed lines 879-881 from:
var xmlrpc = new XMLRPCRequest?(QuiX?.root); xmlrpc.oncomplete = recipemanager.updateList; xmlrpc.callback_info = listView; to:
document.desktop.parseFromUrl(id + '?cmd=new&cc=' + sCC,
function(w) {
w.attributes.refreshlist = function() {
if (sCC=='schemas.org.innoscript.recipemanager.Recipe')
recipemanager.refreshList(oWin);
}
}
);
Deleted line 889:
xmlrpc.callmethod('executeOqlCommand', sOql);
Changed lines 892-894 from:
recipemanager.updateList = function(req) { to:
recipemanager.refresh_onclick = function(evt, w) { recipemanager.loadRecipes(w); } Changed lines 898-899 from:
req.callback_info.dataSet = req.response; req.callback_info.refresh(); to:
recipemanager.loadRecipes = function(w) { Added lines 902-903:
var appWin = w.getParentByType(Window); recipemanager.refreshList(appWin); Added lines 905-912:
recipemanager.refreshList = function(appWin) { var oTree = appWin.getWidgetById('tree');
var id = oTree.getSelection().getId();
var listView = appWin.getWidgetById('list');
var sOql = "select id, displayName, servings, preparationTime, rating, " +
"modified from '" + id + "' where contentclass = " +
"'schemas.org.innoscript.recipemanager.Recipe' order by displayName asc";
Changed lines 914-931 from:
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 a:prop nodes. The location of the new object is determined by the current tree selection. The parseFromUrl QuiX? widget method loads an XML UI definition from a specified URL, inside the referring widget. The second argument of this method is a function called when the interface is loaded. The first argument of this function is the root widget created. In this event, the root widget - the form dialog - will be appended to the desktop (document.desktop is the desktop widget). The recipemanager.loadrecipes handler is called once when the list view is loaded. This handler loads the recipes from the selected container and displays them in the list view. The onload event handler, unlike the other events, does not accept the two common event handler arguments, the event object and the widget, but instead accepts only the widget that fires the event. 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 pubdir/__xul/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 XMLRPC request to the root folder. The oncomplete attribute of the request is the callback function to call when the query has completed. This 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. This means that for each new tree selection we should update the list view accordingly. This is accomplished by adding the following event handler:
to:
Changed lines 916-917 from:
... <a:foldertree id="tree" method="getSubtree" padding="4,4,4,4" to:
var xmlrpc = new XMLRPCRequest?(QuiX?.root); xmlrpc.oncomplete = recipemanager.updateList; xmlrpc.callback_info = listView; Changed line 920 from:
to:
Changed lines 922-925 from:
onselect="recipemanager.loadRecipes"> to:
xmlrpc.callmethod('executeOqlCommand', sOql);
} recipemanager.updateList = function(req) { Changed line 927 from:
to:
Changed lines 929-931 from:
... </a:foldertree> ... to:
req.callback_info.dataSet = req.response; req.callback_info.refresh(); Deleted lines 931-933:
Next, we implement the search functionality. We start by adding a new onclick event handler on the search button: Changed lines 934-935 from:
... <a:button top="220" left="center" width="60" height="28" caption="Search" to:
} Changed lines 936-953 from:
to:
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 a:prop nodes. The location of the new object is determined by the current tree selection. The parseFromUrl QuiX? widget method loads an XML UI definition from a specified URL, inside the referring widget. The second argument of this method is a function called when the interface is loaded. The first argument of this function is the root widget created. In this event, the root widget - the form dialog - will be appended to the desktop (document.desktop is the desktop widget). The recipemanager.loadrecipes handler is called once when the list view is loaded. This handler loads the recipes from the selected container and displays them in the list view. The onload event handler, unlike the other events, does not accept the two common event handler arguments, the event object and the widget, but instead accepts only the widget that fires the event. 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 pubdir/__xul/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 XMLRPC request to the root folder. The oncomplete attribute of the request is the callback function to call when the query has completed. This 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. This means that for each new tree selection we should update the list view accordingly. This is accomplished by adding the following event handler:
Changed lines 955-956 from:
onclick="recipemanager.search" /> to:
... <a:foldertree id="tree" method="getSubtree" padding="4,4,4,4" Changed line 958 from:
to:
Changed line 960 from:
... to:
onselect="recipemanager.loadRecipes"> Deleted lines 961-963:
Add the following function to the Script tab of the application dialog: Changed lines 964-969 from:
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, " +
to:
... </a:foldertree> ... Changed lines 968-971 from:
to:
Next, we implement the search functionality. We start by adding a new onclick event handler on the search button:
Changed lines 973-974 from:
"modified from deep('/Recipes')";
to:
... <a:button top="220" left="center" width="60" height="28" caption="Search" Changed line 976 from:
to:
Added lines 978-1001:
onclick="recipemanager.search" /> @] ... Add the following function to the Script tab of the application dialog:
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')";
[@ November 09, 2006, at 05:05 PM
by -
Deleted line 4:
Download this tutorial in PDF format\\ November 09, 2006, at 05:03 PM
by -
Changed line 665 from:
The generic.selectItems function displays the object select dialog by calling the generic.selectObjects function. The auto-generated form uses this function for ReferenceN? or RelatorN? data types. The arguments given to the generic.selectObjects function are: to:
The generic.selectItems function displays the object select dialog by calling the generic.selectObjects function. The auto-generated form servlet uses this function for ReferenceN? or RelatorN? data types. The arguments given to the generic.selectObjects function are: November 09, 2006, at 04:59 PM
by -
Changed line 280 from:
The current implementation of the resources.system.ui.Frm_AutoNew servlet has some to:
The current implementation of the org.innoscript.desktop.ui.Frm_AutoNew servlet has some Changed line 283 from:
happening because the servlet, in its current state, ignores the numeric data types (a QuiX? numeric to:
happening because the servlet, in its current state, ignores the numeric data types (a QuiX numeric Changed lines 289-290 from:
Python module that will host our application servlets. Create a new folder named recipemanager inside the resources folder. To make it a Python module, add an empty __init__.py file. to:
Python module that will host our application servlets. Changed line 294 from:
module. First, we import the XULServlet class, the servers ui module and the content classes to:
module. First, we import the XULServlet class, the servers desktop ui module and the content classes Changed lines 300-301 from:
from resources.system import ui from schemas.org.innoscript import recipemanager to:
from org.innoscript.desktop import ui from org.innoscript.recipemanager import schema Changed line 306 from:
to:
recipe, we must register a new QuiX servlet that acts as a splitter. This is because the Changed line 324 from:
if sCC == 'schemas.org.innoscript.recipemanager.RecipeContainer?': to:
if sCC == 'org.innoscript.recipemanager.schema.RecipeContainer?': Changed line 327 from:
elif sCC == 'schemas.org.innoscript.recipemanager.Recipe': to:
elif sCC == 'org.innoscript.recipemanager.schema.Recipe': Changed line 332 from:
to:
Remember that every QuiX servlet (an instance of the XULServlet class) must always be accompanied Changed lines 337-340 from:
[Name of Python file].[Name of the servlet class name].xul In order for the servlet to execute successfully, a file named ui.RecipeContainerSplitter.xul must be created in the same directory. The specific servlet acts as a wrapper; the content of the xul file is to:
[Name of Python file].[Name of the servlet class name].quix In order for the servlet to execute successfully, a file named ui.RecipeContainerSplitter.quix must be created in the same directory. The specific servlet acts as a wrapper; the content of the quix file is Changed line 348 from:
As you propably already have guessed, the server formats the contents of the xul file using the to:
As you propably already have guessed, the server formats the contents of the quix file using the Changed line 356 from:
<reg cc="schemas.org.innoscript.recipemanager.RecipeContainer?" to:
<reg cc="org.innoscript.recipemanager.schema.RecipeContainer?" Changed lines 361-362 from:
action="resources.system.XMLRPC.ContainerGeneric?"/> <reg cc="schemas.org.innoscript.recipemanager.RecipeContainer?" to:
action="org.innoscript.desktop.XMLRPC.ContainerGeneric?"/> <reg cc="org.innoscript.recipemanager.schema.RecipeContainer?" Changed lines 370-371 from:
action="resources.recipemanager.ui.RecipeContainerSplitter?"/> <reg cc="schemas.org.innoscript.recipemanager.Recipe$" to:
action="org.innoscript.recipemanager.ui.RecipeContainerSplitter?"/> <reg cc="org.innoscript.recipemanager.schema.Recipe$" Changed line 376 from:
action="resources.recipemanager.ui.RecipeForm?"/> to:
action="org.innoscript.recipemanager.ui.RecipeForm?"/> Changed line 397 from:
if sCC == 'schemas.org.innoscript.recipemanager.RecipeContainer?': to:
if sCC == 'org.innoscript.recipemanager.schema.RecipeContainer?': Changed line 407 from:
elif sCC == 'schemas.org.innoscript.recipemanager.Recipe': to:
elif sCC == 'org.innoscript.recipemanager.schema.Recipe': Changed line 418 from:
'URI': self.request.serverVariables['SCRIPT_NAME'] + '/' + self.item. to:
'URI': self.request.serverVariables['SCRIPT_NAME'] + '/' + self.item.id, Changed lines 439-441 from:
The QuiX? XML definition of the form is a new file named ui.RecipeForm.xul inside the resources/recipemanager folder. to:
The QuiX? XML definition of the form is a new file named ui.RecipeForm.quix inside the org/innoscript/recipemanager folder. Changed lines 447-448 from:
<a:script name="Generic Form Script" src="scripts/form_auto.js" />
<a:script name="Generic Functions" src="scripts/generic.js" />
to:
<a:script name="Generic Form Script" src="desktop/ui.Frm_Auto.js" />
<a:script name="Generic Functions" src="desktop/generic.js" />
Changed lines 531-532 from:
Initially, this form imports two QuiX? desktop JavaScript? files. The first script is the one used by the forms generated automatically by the Frm_Auto servlet. The function we reuse from this script(pubdir/scripts/form_auto.js) is the autoform.submit function. This function simply submits the first QuiX? form found inside the current dialog. to:
Initially, this form imports two QuiX desktop JavaScript? files. The first script is the one used by the forms generated automatically by the Frm_Auto servlet. The function we reuse from this script(org/innoscript/desktop/ui.Frm_Auto.js) is the autoform.submit function. This function simply submits the first QuiX form found inside the current dialog. Changed lines 560-561 from:
The submit method of a QuiX? form takes one argument; the function to call asynchronously, once the browser has the submission response. In this case the dialog has a custom attribute which is actually the function to call for refreshing the openers list view. After calling this function, the dialog is closed. to:
The submit method of a QuiX form takes one argument; the function to call asynchronously, once the browser has the submission response. In this case the dialog has a custom attribute which is actually the function to call for refreshing the openers list view. After calling this function, the dialog is closed. Changed lines 565-566 from:
this form is used for both creating and editing recipes the name of the method is diferrent in each aspect. When creating a new recipe we call the create method of the resources.system.XMLRPC.ContainerGeneric XML-RPC servlet. to:
this form is used for both creating and editing recipes the name of the method is diferrent in each aspect. When creating a new recipe we call the create method of the org.innoscript.desktop.XMLRPC.ContainerGeneric XML-RPC servlet. Changed lines 631-634 from:
Respectively, when editing an existing recipe, we call the update method of the resources.system.XMLRPC.ItemGeneric XML-RPC servlet. The recipe form also includes the pubdir/scripts/generic.js JavaScript? file. From this file we reuse two JavaScript? functions named generic.selectItems and generic.removeSelectedItems. to:
Respectively, when editing an existing recipe, we call the update method of the org.innoscript.desktop.XMLRPC.ItemGeneric XML-RPC servlet. The recipe form also includes the org/innoscript/desktop/generic.js JavaScript file. From this file we reuse two JavaScript? functions named generic.selectItems and generic.removeSelectedItems. November 09, 2006, at 04:17 PM
by -
Changed line 268 from:
object inside a RecipeContainer folder. Because the resources.system.ui.Frm_AutoNew is a to:
object inside a RecipeContainer folder. Because the org.innoscript.desktop.ui.Frm_AutoNew is a November 09, 2006, at 03:19 PM
by -
Changed line 197 from:
'schemas.org.innoscript.recipemanager.RecipeContainer?', to:
'org.innoscript.recipemanager.schema.RecipeContainer?', Changed line 224 from:
<reg cc="schemas.org.innoscript.recipemanager.RecipeContainer?" to:
<reg cc="org.innoscript.recipemanager.schema.RecipeContainer?" Changed line 238 from:
action="resources.system.XMLRPC.ContainerGeneric?"/> to:
action="org.innoscript.desktop.XMLRPC.ContainerGeneric?"/> Changed line 242 from:
<reg cc="schemas.org.innoscript.recipemanager.RecipeContainer?" to:
<reg cc="org.innoscript.recipemanager.schema.RecipeContainer?" Changed line 256 from:
action="resources.system.ui.Frm_AutoNew"/> to:
action="org.innoscript.desktop.ui.Frm_AutoNew"/> November 07, 2006, at 06:23 PM
by -
Changed line 26 from:
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. to:
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. Changed lines 66-68 from:
The categories attribute is of type RelatorN since each recipe can be attributed to more than onencategory (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 schemas.org.innoscript.common.Category content class. The categories data type class is already defined; it is used for the categorization of the to:
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 org.innoscript.desktop.schema.common.Category content class. The Categories data type class is already defined; it is used for the categorization of the Changed lines 71-74 from:
In order to define the relation both ways, we have to somehow add the Recipe object to the list of objects that can be categorized. This is accomplished by editing the properties.py file inside the schemas/org/innoscript folder. The highlighted line should be added: to:
In order to define the relation both ways, we have to somehow add the Recipe object to the list of objects 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: Changed line 78 from:
to:
class CategoryObjects?(RelatorN?): Changed lines 81-82 from:
'schemas.org.innoscript.common.Document',
'schemas.org.innoscript.collab.Contact',
to:
'org.innoscript.desktop.schema.common.Document',
'org.innoscript.desktop.schema.collab.Contact',
Changed line 86 from:
'schemas.org.innoscript.recipemanager.Recipe', to:
'org.innoscript.recipemanager.schema.Recipe', Changed line 132 from:
self.categories = properties.categories() to:
self.categories = properties.Categories() Changed lines 156-157 from:
self.category_objects = properties.category_objects()@] to:
self.category_objects = properties.CategoryObjects?()@] Changed lines 160-161 from:
Now lets take a closer look at the category_objects data type: to:
Now lets take a closer look at the CategoryObjects? data type: Changed line 164 from:
to:
class CategoryObjects?(RelatorN?): Changed lines 167-169 from:
'schemas.org.innoscript.common.Document',
'schemas.org.innoscript.collab.Contact',
'schemas.org.innoscript.recipemanager.Recipe',
to:
'org.innoscript.desktop.schema.common.Document',
'org.innoscript.desktop.schema.collab.Contact',
'org.innoscript.recipemanager.schema.Recipe',
November 07, 2006, at 06:09 PM
by -
Changed lines 26-29 from:
The first step is to design your business objects. 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. The content classes are Python classes defined inside the schemas package. Therefore, we create a new Python file named recipemanager.py inside the schemas/org/innoscript folder. First, we need to import the primary Porcupine content classes and the primary data types: to:
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: Changed lines 43-44 from:
'schemas.org.innoscript.recipemanager.RecipeContainer?', 'schemas.org.innoscript.recipemanager.Recipe' to:
'org.innoscript.recipemanager.schema.RecipeContainer?', 'org.innoscript.recipemanager.schema.Recipe' October 29, 2006, at 05:15 PM
by -
Added lines 2-4:
This tutorial is valid only for Porcupine 0.0.7. We are currently in the process of updating this tutorial to comply with the new 0.0.8 release. February 24, 2006, at 06:37 PM
by -
Changed lines 3-4 from:
Download the Recipe Manager Porcupine package file to:
Download the Recipe Manager Porcupine package file February 24, 2006, at 06:15 PM
by -
Changed line 831 from:
var id = oTree.getSelection().id; to:
var id = oTree.getSelection().getId(); Changed line 864 from:
var id = oTree.getSelection().id; to:
var id = oTree.getSelection().getId(); February 24, 2006, at 06:06 PM
by -
Changed line 287 from:
inside the resources/servlets folder. To make it a Python module, add an empty __init__.py file. to:
inside the resources folder. To make it a Python module, add an empty __init__.py file. Changed lines 437-438 from:
resources/servlets/recipemanager folder. to:
resources/recipemanager folder. Changed line 1139 from:
1=resources/servlets/recipemanager to:
1=resources/recipemanager February 24, 2006, at 06:00 PM
by -
Changed line 235 from:
action="resources.servlets.XMLRPC.ContainerGeneric?"/> to:
action="resources.system.XMLRPC.ContainerGeneric?"/> Changed line 253 from:
action="resources.servlets.ui.Frm_AutoNew"/> to:
action="resources.system.ui.Frm_AutoNew"/> Changed line 265 from:
object inside a RecipeContainer folder. Because the resources.servlets.ui.Frm_AutoNew is a to:
object inside a RecipeContainer folder. Because the resources.system.ui.Frm_AutoNew is a Changed line 277 from:
The current implementation of the resources.servlets.ui.Frm_AutoNew servlet has some to:
The current implementation of the resources.system.ui.Frm_AutoNew servlet has some Changed line 298 from:
from resources.servlets import ui to:
from resources.system import ui Changed line 358 from:
action="resources.servlets.XMLRPC.ContainerGeneric?"/> to:
action="resources.system.XMLRPC.ContainerGeneric?"/> Changed line 367 from:
action="resources.servlets.recipemanager.ui.RecipeContainerSplitter?"/> to:
action="resources.recipemanager.ui.RecipeContainerSplitter?"/> Changed line 373 from:
action="resources.servlets.recipemanager.ui.RecipeForm?"/> to:
action="resources.recipemanager.ui.RecipeForm?"/> Changed lines 562-563 from:
this form is used for both creating and editing recipes the name of the method is diferrent in each aspect. When creating a new recipe we call the create method of the resources.servlets.XMLRPC.ContainerGeneric XML-RPC servlet. to:
this form is used for both creating and editing recipes the name of the method is diferrent in each aspect. When creating a new recipe we call the create method of the resources.system.XMLRPC.ContainerGeneric XML-RPC servlet. Changed lines 628-629 from:
Respectively, when editing an existing recipe, we call the update method of the resources.servlets.XMLRPC.ItemGeneric XML-RPC servlet. to:
Respectively, when editing an existing recipe, we call the update method of the resources.system.XMLRPC.ItemGeneric XML-RPC servlet. November 03, 2005, at 07:13 PM
by -
Changed lines 559-560 from:
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 an XML-RPC servlet registered in the store.xml configuration file (visit http://www.innoscript.org/content/view/35/41/ to see how each object is accessible over HTTP). to:
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 an XML-RPC servlet registered in the store.xml configuration file (visit http://wiki.innoscript.org/index.php/Articles/RequestProcessingPipeline to see how each object is accessible over HTTP). November 03, 2005, at 06:07 PM
by -
Changed line 2 from:
Download this tutorial in PDF format to:
Download this tutorial in PDF format\\ November 03, 2005, at 06:05 PM
by -
Changed lines 3-4 from:
to:
Download the Recipe Manager Porcupine package file Changed line 17 from:
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. to:
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. November 01, 2005, at 09:45 AM
by -
Changed line 16 from:
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?. to:
November 01, 2005, at 09:43 AM
by -
Changed line 291 from:
module. First, we import the XULServlet? class, the servers ui module and the content classes to:
module. First, we import the XULServlet class, the servers ui module and the content classes November 01, 2005, at 09:40 AM
by -
Changed lines 265-268 from:
<<<<<<< QuiX? servlet (instance of XULServlet), we should always check for browser compatibility by allowing this servlet to run only on the supported browsers. What this servlet actually does is to ======= QuiX? servlet (instance of XULServlet), we should always check for browser compatibility by to:
Deleted line 266:
>>>>>>> Changed line 291 from:
module. First, we import the XULServlet class, the servers ui module and the content classes to:
module. First, we import the XULServlet? class, the servers ui module and the content classes Changed line 1212 from:
to:
November 01, 2005, at 09:36 AM
by -
Added lines 265-267:
<<<<<<< QuiX? servlet (instance of XULServlet), we should always check for browser compatibility by allowing this servlet to run only on the supported browsers. What this servlet actually does is to ======= Added line 270:
>>>>>>> Changed line 275 from:
Before restarting the server, you can safely delete the RecipeContainer? entry from the containment to:
Before restarting the server, you can safely delete the RecipeContainer entry from the containment Changed line 295 from:
module. First, we import the XULServlet? class, the servers ui module and the content classes to:
module. First, we import the XULServlet class, the servers ui module and the content classes Changed line 332 from:
Remember that every QuiX? servlet (instance of the XULServlet? class) must always be accompanied to:
Changed line 1216 from:
to:
November 01, 2005, at 09:31 AM
by -
Changed line 265 from:
QuiX? servlet (instance of XULServlet?), we should always check for browser compatibility by to:
November 01, 2005, at 08:56 AM
by -
Changed line 259 from:
As you can see, the first registration takes care of the XML-RPC methods exposed by the to:
As you can see, the first registration takes care of the XML-RPC methods exposed by the November 01, 2005, at 08:54 AM
by -
Changed line 259 from:
As you can see, the first registration takes care of the XML-RPC methods exposed by the to:
As you can see, the first registration takes care of the XML-RPC methods exposed by the November 01, 2005, at 07:43 AM
by -
Changed lines 1-5 from:
Download PDF to:
developers (intermediate) Download this tutorial in PDF format November 01, 2005, at 07:33 AM
by -
Changed lines 1-3 from:
to:
November 01, 2005, at 07:29 AM
by -
Changed lines 1-3 from:
Developers (intermediate) Download tutorial in PDF to:
Download PDF November 01, 2005, at 07:21 AM
by -
Added line 1:
Developers (intermediate) November 01, 2005, at 07:15 AM
by -
Added lines 1-2:
Download tutorial in PDF November 01, 2005, at 07:06 AM
by -
Deleted line 0:
November 01, 2005, at 05:27 AM
by -
Changed lines 43-44 from:
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. It is worth mentioning that the special class attribute '_slots_' should not be omitted from any custom content class or data type, since instances of such types consume considerably less memory. to:
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. It is worth mentioning that the special class attribute __slots__ should not be omitted from any custom content class or data type, since instances of such types consume considerably less memory. November 01, 2005, at 05:22 AM
by -
Added lines 1-1210:
Table of contents
1. IntroductionIn 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. 2. Creating the content classesThe first step is to design your business objects. 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. The content classes are Python classes defined inside the schemas package. Therefore, we create a new Python file named recipemanager.py inside the schemas/org/innoscript folder. 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:
class RecipeContainer(systemObjects.Container):
__slots__ = ()
containment = (
'schemas.org.innoscript.recipemanager.RecipeContainer',
'schemas.org.innoscript.recipemanager.Recipe'
)
To define a new container type we need to subclass the system container (the 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. It is worth mentioning that the special class attribute '_slots_' should not be omitted from any custom content class or data type, since instances of such types consume considerably less memory. The recipe object must have the following attributes:
The categories attribute is of type RelatorN since each recipe can be attributed to more than onencategory (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 schemas.org.innoscript.common.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: In order to define the relation both ways, we have to somehow add the Recipe object to the list of objects that can be categorized. This is accomplished by editing the properties.py file inside the schemas/org/innoscript folder. The highlighted line should be added:
...
class category_objects(RelatorN):
...
relCc = (
'schemas.org.innoscript.common.Document',
'schemas.org.innoscript.collab.Contact',
'schemas.org.innoscript.recipemanager.Recipe',
)
...
...
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): __slots__ = () isRequired = True class Ingredients(datatypes.Text): __slots__ = () isRequired = True class Instructions(datatypes.Text): __slots__ = () isRequired = True Having defined all of our required data types, we proceed to the Recipe content class definition:
class Recipe(systemObjects.Item):
"The recipe object"
__slots__ = (
'categories','rating','preparationTime',
'servings','ingredients','instructions'
)
__props__ = systemObjects.Item.__props__ + __slots__
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 '_props_' class attribute is a special attribute used by the framework. It should be defined only in each new content class that has additional data type attributes. This attribute is a tuple of strings containing the names of all such attributes, including those of the super classes. The name of the categories attribute is obligatory. This is because we have a two-way many-to-many relationship between the Recipe and Category objects. To clarify this, we need to examine the Category content type defined in the common.py file. ...
class Category(system.Container):
...
def __init__(self):
...
self.category_objects = properties.category_objects()
The objects references that belong in a specified category are stored inside the category_objects attribute. The data type of this attribute is the category_objects class. It is not considered a good practice to assign the same name to the attribute name and the data type class. In this case this is happening because of a limitation that has been waived since the release of the 0.0.4 version. Now lets take a closer look at the category_objects data type:
class category_objects(RelatorN):
...
relCc = (
'schemas.org.innoscript.common.Document',
'schemas.org.innoscript.collab.Contact',
'schemas.org.innoscript.recipemanager.Recipe',
)
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 contentThe 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 common.py.
class RootFolder(system.Container):
...
containment = (
'schemas.org.innoscript.common.Folder',
'schemas.org.innoscript.collab.ContactsFolder',
'schemas.org.innoscript.recipemanager.RecipeContainer',
) Restart the server and login using an administrative account. Open the root folder, right click on the list view and select New. 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. If you double click on the new folder you should get the following error: ![]() This is because we have not yet defined a valid registration (servlet binding) for the RecipeContainer content class. Hence, we have to edit the store.xml file inside the conf folder. Since these are the first servlet registrations we create for our application, we must enclose it inside a new package node. Locate the packages node at the top of the afore-mentioned file and append the following nodes:
<package name="RecipeManager">
<reg cc="schemas.org.innoscript.recipemanager.RecipeContainer"
method="POST"
param=""
client="vcXMLRPC"
lang=".*"
action="resources.servlets.XMLRPC.ContainerGeneric"/>
<reg cc="schemas.org.innoscript.recipemanager.RecipeContainer"
method="GET"
param="new"
client="(MSIE 6)|(Mozilla/5.0.+rv:1.[7-9])"
lang=".*"
action="resources.servlets.ui.Frm_AutoNew"/>
</package> As you can see, the first registration takes care of the XML-RPC methods exposed by the RecipeContainer content type. For the time being, we need no special behavior; the RecipeContainer type exposes the same methods as any other container. Also, notice that the client parameter is set to vcXMLRPC. This should be true for every XML-RPC servlet we publish. The second registration defines the servlet to be executed when the user chooses to create a new object inside a RecipeContainer folder. Because the resources.servlets.ui.Frm_AutoNew is a QuiX? servlet (instance of XULServlet?), we should always check for browser compatibility by allowing this servlet to run only on the supported browsers. What this servlet actually does is to render a generic form based on the attributes of the new Porcupine object we are about to create. 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 of the RootFolder content type. 4. Designing a new recipe formThe current implementation of the resources.servlets.ui.Frm_AutoNew servlet has some limitations that force us to design a new form for the Recipe content type. Looking at the form that is automatically generated by the servlet, we notice the absence of the integer data types. This is happening because the servlet, 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 QuiX? servlet. We start by creating the Python module that will host our application servlets. Create a new folder named recipemanager inside the resources/servlets folder. To make it a Python module, add an empty __init__.py file. It is recommended to maintain the QuiX? servlets and the XML-RPC servlets in separate Python files named ui.py and XMLRPC.py respectively. Consequently, we create and edit a new Python file named ui.py inside the recipemanager module. First, we import the XULServlet? class, the servers ui module and the content classes module we just created: from porcupine.core.servlet import XULServlet from resources.servlets import ui from schemas.org.innoscript import recipemanager Now comes the tricky part. In order to overwrite the default form generated, when creating a new recipe, we must register a new QuiX? servlet that acts as a splitter. This is because the RecipeContainer type accepts both new recipe containers and recipes. When creating a new recipe container, the splitter must get the XML definition output from the Frm_Auto servlet, whereas when creating a new recipe the splitter should return the XML definition output from our custom servlet. This is shown below:
class RecipeContainerSplitter(XULServlet):
def setParams(self):
sCC = self.request.queryString['cc'][0]
self.params['FORM'] = ''
if sCC == 'schemas.org.innoscript.recipemanager.RecipeContainer':
servlet = ui.Frm_AutoNew(self.server, self.session, self.request)
self.params['FORM'] = servlet.execute()
elif sCC == 'schemas.org.innoscript.recipemanager.Recipe':
servlet = RecipeForm(self.server, self.session, self.request)
self.params['FORM'] = servlet.execute()
Remember that every QuiX? servlet (instance of the XULServlet? class) must always be accompanied with a xul file in the same directory. This file usually contains the XML definition of the interface, which abides to the Python string formatting rules. The file name of this file is of the following form: [Name of Python file].[Name of the servlet class name].xul In order for the servlet to execute successfully, a file named ui.RecipeContainerSplitter.xul must be created in the same directory. The specific servlet acts as a wrapper; the content of the xul file is simply: %(FORM)s As you propably already have guessed, the server formats the contents of the xul file using the params attribute of the servlet. Before proceeding to the recipe form itself, we must modify the store.xml configuration file by adding modifying the highlighted lines:
<package name="RecipeManager">
<reg cc="schemas.org.innoscript.recipemanager.RecipeContainer"
method="POST"
param=""
client="vcXMLRPC"
lang=".*"
action="resources.servlets.XMLRPC.ContainerGeneric"/>
<reg cc="schemas.org.innoscript.recipemanager.RecipeContainer"
method="GET"
param="new"
client="(MSIE 6)|(Mozilla/5.0.+rv:1.[7-9])"
lang=".*"
action="resources.servlets.recipemanager.ui.RecipeContainerSplitter"/>
<reg cc="schemas.org.innoscript.recipemanager.Recipe$"
method="GET"
param="properties"
client="(MSIE 6)|(Mozilla/5.0.+rv:1.[7-9])"
lang=".*"
action="resources.servlets.recipemanager.ui.RecipeForm"/>
<package> The first modification assigns the RecipeContainerSplitter as the servlet to be executed when the browser GETs? a RecipeContainer object with the cmd parameter set to new. The second modification is simply a new servlet registration for the Recipe type, which renders the recipe form whenever a compatible browser GETs? an object of this type with the parameter set to properties. Now its time to write the RecipeForm servlet:
class RecipeForm(XULServlet):
def setParams(self):
sCC = self.item.contentclass
sCategories = ''
sHiddenField = ''
if sCC == 'schemas.org.innoscript.recipemanager.RecipeContainer':
# we create a new recipe
sTitle = 'Create new recipe'
oRecipe = recipemanager.Recipe()
# in this case we need an extra hidden field with the type of
# object we are about to create
sHiddenField = '<a:field name="CC" type="hidden" ' + \
'value="schemas.org.innoscript.recipemanager.Recipe" />'
sMethod = 'create'
sAction = 'Create'
elif sCC == 'schemas.org.innoscript.recipemanager.Recipe':
# we editing an existing one
oRecipe = self.item
sMethod = 'update'
sTitle = 'Recipe properties'
sAction = 'Update'
# build the categories options list
for category in oRecipe.categories.getItems():
sCategories += '<a:option caption="%s" value="%s" img="%s" />' %
(category.displayName.value, category.id, category.__image__)
self.params = {
'URI': self.request.serverVariables['SCRIPT_NAME'] + '/' + self.item.
'METHOD': sMethod,
'TITLE': sTitle,
'HIDDEN': sHiddenField,
'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': sCategories,
'ICON': oRecipe.__image__,
'ACTION': sAction,
}
This servlet is called either when the user needs to create a new recipe, or when the user wants to display the properties of an existing recipe. In the first case, the type of the object called is the RecipeContainer unlike the second case, in which the type of the object is the Recipe type. The QuiX? XML definition of the form is a new file named ui.RecipeForm.xul inside the resources/servlets/recipemanager folder.
<?xml version="1.0" ?>
<a:dialog xmlns:a="http://www.innoscript.org/quix" title="%(TITLE)s"
img="%(ICON)s" close="true" width="480" height="380" left="30%%" top="10%%">
<a:script name="Generic Form Script" src="scripts/form_auto.js" />
<a:script name="Generic Functions" src="scripts/generic.js" />
<a:wbody>
<a:form action="%(URI)s" method="%(METHOD)s" padding="4,4,4,4">
%(HIDDEN)s
<a:label caption="Recipe name:"/>
<a:field left="80" width="380" name="displayName"
value="%(DISPLAYNAME)s" />
<a:label caption="Description:" top="25" />
<a:field left="80" top="24" width="380" name="description"
value="%(DESCRIPTION)s" />
<a:hr top="50" width="100%%" />
<a:label top="58" caption="Preparation time (min):" />
<a:spinbutton name="preparationTime" top="55" left="110" width="50"
max="240" value="%(PREPARATION_TIME)d" editable="true" />
<a:label top="58" left="200" caption="Servings:" />
<a:spinbutton name="servings" top="55" left="250" width="40" max="12"
editable="true" value="%(SERVINGS)d" />
<a:label top="58" left="320" caption="Rating:" />
<a:spinbutton name="rating" top="55" left="360" width="40" max="10"
editable="true" value="%(RATING)d" />
<a:tabpane top="90" width="100%%" height="220">
<a:tab caption="Ingredients">
<a:field type="textarea" name="ingredients" width="100%%"
height="100%%">%(INGREDIENTS)s</a:field>
</a:tab>
<a:tab caption="Instructions">
<a:field type="textarea" name="instructions" width="100%%"
height="100%%">%(INSTRUCTIONS)s</a:field>
</a:tab>
<a:tab caption="Categories">
<a:splitter width="100%%" height="100%%" orientation="h">
<a:pane length="-1">
<a:selectlist name="categories" multiple="true" posts="all"
width="100%%" height="100%%">
<a:prop name="SelectFrom"
value="Categories/Recipe categories"></a:prop>
<a:prop name="RelatedCC"
value="schemas.org.innoscript.common.Category"></a:prop>
%(CATEGORIES)s
</a:selectlist>
</a:pane>
<a:pane length="24">
<a:flatbutton width="70" height="22" caption="Add"
onclick="generic.selectItems"></a:flatbutton>
<a:flatbutton left="80" width="70" height="22" caption="Remove"
onclick="generic.removeSelectedItems"></a:flatbutton>
</a:pane>
</a:splitter>
</a:tab>
</a:tabpane>
</a:form>
</a:wbody>
<a:dlgbutton onclick="autoform.submit" width="70" height="22"
caption="%(ACTION)s" default="true" />
<a:dlgbutton onclick="__closeDialog__" width="70" height="22"
caption="Cancel" />
</a:dialog>
Initially, this form imports two QuiX? desktop JavaScript? files. The first script is the one used by the forms generated automatically by the Frm_Auto servlet. The function we reuse from this script(pubdir/scripts/form_auto.js) is the autoform.submit function. This function simply submits the first QuiX? form found inside the current dialog.
...
autoform.submit = function(evt, w) {
var oForm = w.getParentByType(Dialog).getWidgetsByType(Form)[0];
oForm.submit(autoform.update);
}
autoform.update = function(response, form) {
var dlg = form.getParentByType(Dialog);
dlg.attributes.refreshlist(); dlg.close(); } ... The submit method of a QuiX? form takes one argument; the function to call asynchronously, once the browser has the submission response. In this case the dialog has a custom attribute which is actually the function to call for refreshing the openers list view. After calling this function, the dialog is closed. 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 an XML-RPC servlet registered in the store.xml configuration file (visit http://www.innoscript.org/content/view/35/41/ to see how each object is accessible over HTTP). The form's method parameter is the name of the method of the XML-RPC servlet to call. Since this form is used for both creating and editing recipes the name of the method is diferrent in each aspect. When creating a new recipe we call the create method of the resources.servlets.XMLRPC.ContainerGeneric XML-RPC servlet. ... class ContainerGeneric(ItemGeneric): def create(self, data):
# create new item
oNewItem = misc.getClassByName(data.pop('CC'))()
# get user role
iUserRole = objectAccess.getAccess(self.item, self.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 = self.server.temp_folder + '/' + data[prop]['tempfile']
oAttr.loadFromFile(sPath)
os.remove(sPath)
elif isinstance(oAttr, datatypes.Date):
oAttr.value = data[prop].value
else:
oAttr.value = data[prop]
txn = self.server.store.getTransaction()
oNewItem.appendTo(self.item.id, txn)
txn.commit()
return True
... 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 method simply returns True only if the object is created and appended to the container successfully. This way, the browser knows that the request has been succefully completed. Also notice, that the ContainerGeneric servlet is a subclass of the ItemGeneric servlet. Besides the container specific methods defined, a container can also be updated, copied or renamed just like any other Porcupine object. Respectively, when editing an existing recipe, we call the update method of the resources.servlets.XMLRPC.ItemGeneric XML-RPC servlet. The recipe form also includes the pubdir/scripts/generic.js JavaScript? file. From this file we reuse two JavaScript? functions named generic.selectItems and generic.removeSelectedItems.
...
generic.selectItems = function(evt, w) {
var oDialog = w.getParentByType(Dialog);
var oTarget = w.parent.parent.getWidgetsByType(SelectList)[0];
var sFolderURI = oTarget.attributes.SelectFrom;
var sCC = oTarget.attributes.RelatedCC;
generic.selectObjects(oDialog, oTarget, generic.addSelectionToList,
sFolderURI, sCC, 'true');
}
...
generic.selectObjects =
function(win, target, select_func, startFrom, contentclass, multiple) {
...
}
...
generic.removeSelectedItems = function(evt, w) {
var oSelectList = w.parent.parent.getWidgetsByType(SelectList)[0];
oSelectList.removeSelected();
}
...
The generic.selectItems function displays the object select dialog by calling the generic.selectObjects function. The auto-generated form uses this function for ReferenceN? or RelatorN? data types. The arguments given to the generic.selectObjects function are:
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 applicationApart from the custom forms and the custom content types, we will also need a custom UI 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. To create a new application, login to Porcupine as an administrator and navigate to the Administrative Tools/Applications folder. Create a new application named Recipe manager. Set the dimensions of the application window to 640x480 (width and height). In the interface tab, paste the following QuiX? definition. Do not forget to replace the tree nodes ID with the ID of the Recipes folder given by the system.
<a:splitter width="100%%" height="100%%" orientation="h" spacing="0">
<a:pane length="28">
<a:toolbar width="100%%" height="100%%">
<a:tbbutton caption="Create recipe folder" width="120">
<a:prop name="cc" value="schemas.org.innoscript.recipemanager.RecipeContainer"/>
</a:tbbutton>
<a:tbbutton caption="Create recipe" width="80">
<a:prop name="cc" value="schemas.org.innoscript.recipemanager.Recipe"/>
</a:tbbutton>
<a:tbsep/>
<a:tbbutton caption="Refresh" width="60" />
</a:toolbar>
</a:pane>
<a:pane length="-1" bgcolor="white">
<a:splitter width="100%%" height="100%%" orientation="v"
interactive="true">
<a:pane length="200">
<a:outlookbar width="100%%" height="100%%">
<a:tool caption="Recipes">
<a:foldertree id="tree" method="getSubtree" padding="4,4,4,4">
<a:treenode id="[Put here the ID of the Recipes folder]" haschildren="true"
img="images/folder.gif" caption="Recipes">
</a:treenode>
</a:foldertree>
</a:tool>
<a:tool caption="Search" bgcolor="gray">
<a:label caption="Recipe title contains:" />
<a:field id="title" left="5" top="20" width="160" />
<a:label top="50" caption="Preparation time is less than" />
<a:field id="preparationTime" left="5" top="70" width="30" />
<a:label top="72" left="40" caption="min." />
<a:label top="100" caption="The recipe's rating is greater than" />
<a:spinbutton top="120" left="5" width="40" max="10" value="0"
id="rating"/>
<a:label top="150" caption="Recipe ingredients contain:" />
<a:field id="ingredients" top="170" left="5" width="160" />
<a:label top="190" caption="(comma separated list)"
style="font-style:italic" />
<a:button top="220" left="center" width="60" height="28"
caption="Search" />
</a:tool>
</a:outlookbar>
</a:pane>
<a:pane length="-1">
<a:listview id="list" width="100%%" height="100%%">
<a:listheader>
<a:column width="140" caption="Recipe title" name="displayName"
bgcolor="#EFEFEF" sortable="true"></a:column>
<a:column width="60" caption="Servings" type="int" name="servings"
sortable="true"></a:column>
<a:column width="100" caption="Preparation time" type="int"
name="preparationTime" sortable="true"></a:column>
<a:column width="40" caption="Rating" type="int" name="rating"
sortable="true"></a:column>
<a:column width="160" caption="Date modified" type="date"
name="modified" sortable="true"></a:column>
</a:listheader>
</a:listview>
</a:pane>
</a:splitter>
</a:pane>
</a:splitter>
Press the Create button to create the application object. The above interface contains no functionality. We have just created a dummy application. 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. Let's start by adding some event handlers. ... <a:toolbar width="100%%" height="100%%"> <a:tbbutton caption="Create recipe folder" width="120"
onclick="recipemanager.createItem">
...
</a:tbbutton>
<a:tbbutton caption="Create recipe" width="80"
onclick="recipemanager.createItem">
...
</a:tbbutton>
<a:tbsep />
<a:tbbutton caption="Refresh" width="60"
onclick="recipemanager.refresh_onclick" />
</a:toolbar> ... <a:listview id="list" multiple="true" width="100%%" height="100%%" onload="recipemanager.loadRecipes"> ... In the application's script tab insert the following code:
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().id;
document.desktop.parseFromUrl(id + '?cmd=new&cc=' + sCC,
function(w) {
w.attributes.refreshlist = function() {
if (sCC=='schemas.org.innoscript.recipemanager.Recipe')
recipemanager.refreshList(oWin);
}
}
);
}
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 id = oTree.getSelection().id;
var listView = appWin.getWidgetById('list');
var sOql = "select id, displayName, servings, preparationTime, rating, " +
"modified from '" + id + "' where contentclass = " +
"'schemas.org.innoscript.recipemanager.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 a:prop nodes. The location of the new object is determined by the current tree selection. The parseFromUrl QuiX? widget method loads an XML UI definition from a specified URL, inside the referring widget. The second argument of this method is a function called when the interface is loaded. The first argument of this function is the root widget created. In this event, the root widget - the form dialog - will be appended to the desktop (document.desktop is the desktop widget). The recipemanager.loadrecipes handler is called once when the list view is loaded. This handler loads the recipes from the selected container and displays them in the list view. The onload event handler, unlike the other events, does not accept the two common event handler arguments, the event object and the widget, but instead accepts only the widget that fires the event. 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 pubdir/__xul/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 XMLRPC request to the root folder. The oncomplete attribute of the request is the callback function to call when the query has completed. This 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. This means that for each new tree selection we should update the list view accordingly. This is accomplished by adding the following event handler: ... <a:foldertree id="tree" method="getSubtree" padding="4,4,4,4" onselect="recipemanager.loadRecipes"> ... </a:foldertree> ... Next, we implement the search functionality. We start by adding a new onclick event handler on the search button: ... <a:button top="220" left="center" width="60" height="28" caption="Search" onclick="recipemanager.search" /> ... Add the following function to the Script tab of the application dialog:
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: <a:listview multiple="true" width="100%%" height="100%%" id="list" onload="recipemanager.loadRecipes" 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);
generic.showObjectProperties(null, null, recipe,
function() {
recipemanager.refreshList(oWin);
}
); } This event handler reuses the generic.showObjectProperties function. The function given as an argument is called when the user updates the item and the list view of the opener must be updated. The final enhancement is the addition of a context menu. Open the application object and add the following lines to the interface definition: <a:pane length="-1">
<a:contextmenu onshow="recipemanager.contextmenu_onshow">
<a:menuoption caption="Open" onclick="recipemanager.openRecipe" />
<a:menuoption caption="Delete" onclick="recipemanager.deleteRecipe" />
</a:contextmenu>
<a:listview width="100%%" height="100%%" id="list"
onload="recipemanager.loadRecipes" ondblclick="recipemanager.loadItem">
...
</a:listview>
</a:pane>
Add the following handlers to the applications script:
recipemanager.contextmenu_onshow = function(menu) {
var oList = menu.owner.getWidgetById('list');
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.getWidgetById('list');
var selectedRecipe = oList.getSelection();
recipemanager.loadItem(null, oList, selectedRecipe);
}
recipemanager.deleteRecipe = function(evt, w) {
var oList = w.parent.owner.getWidgetById('list');
var selectedRecipe = oList.getSelection();
var desktop = document.desktop;
_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.caption,
"Are you sure you want to delete this recipe?",
[
[desktop.attributes['YES'], 60, _deleteItem],
[desktop.attributes['NO'], 60]
],
'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 menus enclosing widget. The enclosing widget is accessible through the owner menu property. The desktop.msgbox is used for displaying message boxes. The arguments accepted by this method are:
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 applicationUsing the pakager utility we will consolidate all of the applications 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: [package] name=RecipeManager version=0.0.1 The first section of the package definition file has information about the package (its name and its version). Please notice that the name of the package and the value of the name attribute of the package node in the store.xml configuration file must be identical. [files] 1=schemas/org/innoscript/recipemanager.py The second section has the relative paths of the files required by the application. Particularly, this section adds to the package the applications content classes. [dirs] 1=resources/servlets/recipemanager The third section adds directories to the package. This directory is the one that contains the servlets. [pubdir] The fourth section contains the published directories (usually containing external JavaScript? files and images) that are required by the application. In this case, we did not create a new published directory; all of the applications JavaScript? handlers are embedded inside the application object and we did not use any new images. [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. Lets 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('schemas.org.innoscript.properties.category_objects')
ce.relCc.append('schemas.org.innoscript.recipemanager.Recipe')
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 category_objects class). To be precise, it adds the schemas.org.innoscript.recipemanager.Recipe 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('schemas.org.innoscript.properties.category_objects')
ce.relCc.remove('schemas.org.innoscript.recipemanager.Recipe')
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.1.ppf) is created inside the Porcupine installation directory. The application can be installed on another Porcupine installation using the following command:
|
||||||||||||||||||||||||||||
|
Page last modified on April 19, 2010, at 11:38 PM
|
||||||||||||||||||||||||||||