plainblack.com
Username Password
search
Bookmark and Share

Writing Wobjects

Wobject is short for web object and serves as the plugin type for creating custom applications for your site in WebGUI. Because wobjects are both pluggable and true objects, the possibilities of what you can do are nearly limitless. Applications as simple as a guest book and as complex as complete Human Resources Information Systems have been created for WebGUI. The only limiting factor when it comes to what you can accomplish with wobjects is you.

 

History

WebGUI has had a pluggable application interface since its inception in 2001. To better understand wobjects and what they are, it's helpful to take a look back at how wobjects evolved into what they are today.

 

Up until version 5 of WebGUI, Wobjects were called Widgets. If you have been using WebGUI since before version 5, it's possible that you might still have the widget table in your database. To create a new widget for WebGUI, you basically needed to create a new WebGUI::Widget package, add a www_view and www_edit method, and then add the widget to WebGUI's config file, similar to what is still done today.

 

While they were extremely simple to write (no extended API's existed way back in the beginning), there was a lot of necessary repetition. Each time you created a widget you had to basically re-write the edit screen. If you wanted to add a feature to an existing widget, you had to copy it and all of its database tables before you could do so. There was nothing you could do to control the style. Every widget, including the edit page, was wrapped in the style of the site.

 

Widgets were not flexible enough to accommodate advanced applications, and their repetitiveness made the development cycle long and tedious with a good deal of code copying. WebGUI 5 introduced the wobject, which replaced widgets and introduced an API for developing applications within WebGUI. Wobjects solved the problem of repetition by including some convenience methods for adding the edit form and handling data; however, it was still limiting in that Wobjects in WebGUI 5 weren't true objects, so you still needed to fully copy wobjects to make changes. Style handlers were added which allowed some flexibility when it came to styles; however, wobjects were still limited in that they had to have a style. You couldn't return raw data, such as RSS feeds.

 

WebGUI 6 introduced the modern wobject as you know them today. Wobjects became true objects which allowed for inheritance. Style became completely flexible, allowing raw data to be returned by an application, and the API was enhanced to further limit the development cycle by introducing auto generating forms based off a definition. Wobjects became truly powerful, capable of performing any task regardless of how complex.

 

What is a Wobject?

Now that you have a little history about the evolution of wobjects, let's talk a bit more about what the modern wobject is and what it provides to you, the developer. Wobjects in WebGUI are simply assets. A full chapter in this book is dedicated to developing assets. If you have read and understand that chapter then there is relatively nothing new to learn to develop a wobject.

 

So what's the difference then? Why have a whole chapter on writing wobjects if they are simply assets?

 

The main difference between assets and wobjects is that wobjects give you API with convenience methods for handling collateral data and have a built in style wrapper. What this means is that you don't have to manually look up the current style of the page and wrap your asset inside it. This is done automatically for you. So how do you know whether to write an “Asset” or a “Wobject”? Ask yourself a few questions:

 

  1. Does your asset take on its own “focus”?

 

Let's digress for a moment and discuss a concept called “Wobject focus” in WebGUI. Start by imagining a discussion forum on a page of your WebGUI website called “Discussion” with URL /discussion. When you go to the discussion page, you see a form within the current style of your site (you likely have a top banner, maybe left hand navigation, and a footer) along with any other assets you may have dropped on to this page. For the sake of argument, let's say you have a second discussion forum on this page.

 

Now, imagine you want to view all of the threads within the “General” topic of the first discussion forum. This is going to take you to a new url: /discussion/forum1?func=viewThreads;threadId=xxxx. This is the URL of the wobject. The page which contained both discussion forums loses focus and the wobject, which displays the threads within the discussion forum you clicked, gains focus. What this means is that everything else on the page (the second discussion forum) is no longer displayed. Only the wobject you've clicked on is displayed. This is most visible when you return to main view of the wobject. /discussion/forum1?func=view. This will return you to the main view of the forum; however, the second discussion forum will not show up on the page. This is because the first discussion forum still has focus. To see them both, you need to go back to the page where both of the discussion forums reside, /discussion.

 

If your wobject gains focus, you will need to wrap it in a style so that it looks like the rest of the site. Otherwise, users will be confused when your wobject gains focus as it will look completely different than the page they were just on. As stated before, wobjects have a built in style wrapper which automatically inherits from the parent when users add it to the site.

 

If you anticipate situations in which your wobject will need to gain “focus”, you probably want to create a wobject instead of an asset.

 

  1. Is there collateral data associated with your asset?

 

Let's say you want to build a recipe book asset which allows users to enter and search for their favorite recipes. As you learned in the chapter on writing assets, each asset needs to store metadata associated with it so WebGUI knows how to display various aspects of it. You would start building this asset much in the same way by creating a table that had the assetId and revisionDate fields. This asset, however, needs to have multiple recipes associated with each instance. In order to do that you are going to need at least one more database table that stores information about each recipe. This is what is called collateral data.

 

If you anticipate needing collateral data, you probably want to create a wobject instead of an asset.

 

  1. Are there multiple UI components of your asset?

 

In the recipe asset example above, you are going to need more than just the asset add/edit and view screens. You are going to need a screen which allows users to add, edit, view, and remove recipes from the system. These are separate UI components which you will expose to your users, and therefore require their own templates.

 

If you anticipate needing multiple UI components, you probably want to create a wobject instead of an asset.

 

Getting Started

Since you already know how to develop assets from reading the Assets chapter, this discussion will dive into a little more of the functional and technical planning that is necessary when developing a Wobject. For all intents and purposes, a wobject is a full featured application within WebGUI which requires planning before you dive into the code. The planning stage is divided into two parts: the Functional Specification and the Technical Specification.

 

Writing a Functional Specification

One of the most important, if not the most important part of designing an application (especially an application for the web) is to try to anticipate how your users are going to attempt to use it. Users have some preconceived notions of how software works. This is what is called the “User Model,” and it is what you should attempt to identify before writing any piece of software. The document that describes your vision of the “User Model” is called a Functional Specification for the software.

 

Writing a specification is a lot like flossing: you know you should be doing it, but you never do. Most people will tell you they don't write specifications because it saves time. That may be true initially, but a bigger problem is that most developers get an idea and want to run with it before organizing all of their thoughts.

 

Let's clear one thing up before going any further. Writing a functional specification may not be the most enjoyable thing to do, but for any project that takes longer than a week to code, not writing one will almost certainly lead to an even less enjoyable practice. The practice of patching up software that is full of holes.

 

A functional specification describes how your application will work entirely from the user's perspective. It doesn't care how it is implemented. It talks about features and it shows screen shots of every page trying to visually describe the application and the user's experience. This doesn't need to be a long document, but it should be long enough to fully express your idea.

 

You also don't need to create flashy graphics. In fact, simply drawing out the screen on a piece of paper and scanning it would suffice. If you are so inclined, applications like Microsoft Visio (Windows)® or Omnigraffle (Mac)® have tools that let you lay out simple forms. Defining the flow of the user's experience is what is important.

 

A functional specification should contain the following:

 

  • Description: a brief description of the application, no longer than one or two small paragraphs. Include the goal of the project and a high level overview of what the software does.

  • User Roles: bullet the various user roles (admins, managers, approvers) that exist in the application, describing each briefly in one or two sentences.

  • Rules: bullet any rules that must be followed at all times by the software. For example, “An administrator must approve a document before it's published to the site.”

  • External Processes: briefly describe all interactions with any third party software. For example, “Data is retrieved from XYZ RSS feed on yahoo.com.”

  • Future Considerations: detail all functionality that will NOT be included in this release of the application, but may be considered in the future. This will force you to think about possible stubs you may want to add for future development.

  • Workflow Diagram: provide a workflow diagram of how data moves through the system.

  • Screen Specifications: provide a screen shot of each screen of the application as you envision it, and briefly describe what actions can occur. This will not only make it easier to figure out how your application should be implemented, but it ensures that you don't leave anything out that you might need.

 

Writing a Technical Specification

The technical specification describes the internal implementation of the application. It talks about data structures, relational database models, algorithms, etc. It sets the parameters for building this piece of software.

 

A Technical Specification should include:

 

  • Entity Relationship Diagram (ERD): provide a graphical representation of the different database tables and their relationships. This will help you identify that various components that need to be created.

  • Flow Charts: provide a diagram that shows the different components of the application and how they fit together. This will allow you to see a visual representation of your application, making it easier to find any holes that may need to be filled.

  • Method Library: describe any methods you need to create along with their signatures. This helps you better visualize the entire application and what needs to be done.

  • High Level Pseudocode: for complex ideas, it may be a good idea to provide some pseudocode that describes how you anticipate things working. This is useful as it allows you to easily share your ideas with others and identify shortcomings and pitfalls.

 

Building Your Wobject

Before beginning, take a look at the following Hello World example. This illustrates building a wobject at its most basic level, and is a helpful reference before moving on to a more advanced, step by step, example.

 

Hello World

package WebGUI::Asset::Wobject::HelloWorld;



$VERSION = "1.0.0";



use strict;

use Tie::IxHash;

use base 'WebGUI::Asset::Wobject';



#-------------------------------------------------------------------

sub definition {

my $class = shift;

my $session = shift;

my $definition = shift;

 

tie my %properties, 'Tie::IxHash';

%properties = ();

 

push(@{$definition}, {

assetName=>"Hello World",

autoGenerateForms=>1,

tableName=>'HelloWorld',

className=>'WebGUI::Asset::Wobject::HelloWorld',

properties=>\%properties

});

return $class->SUPER::definition($session, $definition);

}



#-------------------------------------------------------------------

sub view {

my $self = shift;

my $session = $self->session;

 

return "Hello World!";

}



#-------------------------------------------------------------------

# INSTALL / UNINSTALL

#-------------------------------------------------------------------



use base 'Exporter';

our @EXPORT = qw(install uninstall);

use WebGUI::Session;



#-------------------------------------------------------------------

sub install {

my $config = $ARGV[0];

my $home = $ARGV[1] || "/data/WebGUI";

unless ($home && $config) {

die "usage: perl -MWebGUI::Asset::Wobject::HelloWorld -e install www.example.com.conf\n";

}

print "Installing asset.\n";

my $session = WebGUI::Session->open($home, $config);

$session->config->addToArray("assets","WebGUI::Asset::Wobject::HelloWorld");

$session->db->write("create table HelloWorld (

assetId varchar(22) binary not null,

revisionDate bigint not null,

primary key (assetId, revisionDate)

)");

$session->var->end;

$session->close;

print "Done. Please restart Apache.\n";

}



#-------------------------------------------------------------------

sub uninstall {

my $config = $ARGV[0];

my $home = $ARGV[1] || "/data/WebGUI";

unless ($home && $config) {

die "usage: perl -MWebGUI::Asset::Wobject::HelloWorld -e uninstall www.example.com.conf\n";

}

print "Uninstalling asset.\n";

my $session = WebGUI::Session->open($home, $config);

$session->config->deleteFromArray("assets","WebGUI::Asset::Wobject::HelloWorld");

my $rs = $session->db->read("select assetId from asset where className='WebGUI::Asset::Wobject::HelloWorld'");

while (my ($id) = $rs->array) {

my $asset = WebGUI::Asset->new($session, $id, "WebGUI::Asset::Wobject::HelloWorld");

$asset->purge if defined $asset;

}

$session->db->write("drop table HelloWorld");

$session->var->end;

$session->close;

print "Done. Please restart Apache.\n";

}





1;

 

Example: Recipe Catalog

Similar to creating a new asset, when creating a new wobject you should begin with the wobject skeleton available at /data/WebGUI/lib/WebGUI/Asset/Wobject/_NewWobject.skeleton. The wobject skeleton contains all of the methods you'll need to get your base wobject up and running.

 

Copy the wobject skeleton into a new Perl module that will be your new wobject. For the purposes of this chapter, create your recipe application allowing users to search for, add, edit, and delete recipes.

 

cd /data/WebGUI/lib/WebGUI/Asset/Wobject

cp _NewWobject.skeleton RecipeCatalog.pm

 

Update the first line of code to reflect the name of your wobject package WebGUI::Asset::Wobject::RecipeCatalog.

 

As with assets, you now need to build your definition based on the table structure for your RecipeCatalog asset. This is the exactly same process as creating the definition for assets as described in the chapter on creating assets. The database table looks like:

 

CREATE TABLE RecipeCatalog (

assetId VARCHAR(22) BINARY NOT NULL,

revisionDate BIGINT NOT NULL,

viewTemplateId VARCHAR(22) BINARY NOT NULL,

editRecipeTemplateId VARCHAR(22) BINARY NOT NULL,

viewRecipeTemplateId VARCHAR(22) BINARY NOT NULL,

groupToPost VARCHAR(22) BINARY NOT NULL,

paginateAfter INTEGER DEFAULT '25',

PRIMARY KEY (assetId,revisionDate)

);

 

After adding the assetId / revisionDate composite key, create three new templates. The first template (viewTemplateId) will be used to display a paginated list of all of the recipes in your database. The second (editRecipeTemplateId) will be used to create the form for entering new recipes. Finally, the third (viewRecipeTemplateId) will be used to view individual recipes.

 

Additionally, you will notice a groupToPost field. With this field you will let your administrators choose to differentiate between those who can view the recipes and those who can post them. You will also notice a paginateAfter field in which you will let your administrator designate how many recipes will be displayed before they are paginated.

 

From this table you can create your definition which looks like this ( assume for this example that no internationalization is necessary):

 

sub definition {

my $class = shift;

my $session = shift;

my $definition = shift;

tie my %properties, 'Tie::IxHash';

%properties = (

paginateAfter => {

fieldType =>"integer",

defaultValue =>25,

tab =>"properties",

hoverHelp =>"Enter the number of recipes per page",

label =>"Paginate Recipes After",

},

viewTemplateId => {

fieldType =>"template",

defaultValue =>'RecipeCatalog000000001',

tab =>"display",

namespace =>"RecipeCatalog/view",

hoverHelp =>"View template for the recipe catalog",

label =>"View Recipe Catalog Template",

},

editRecipeTemplateId => {

fieldType =>"template",

defaultValue =>'RecipeCatalog000000002',

tab =>"display",

namespace =>"RecipeCatalog/edit",

hoverHelp =>"View template for the recipe catalog",

label =>"Edit Recipe Template",

},

viewRecipeTemplateId => {

fieldType =>"template",

defaultValue =>'RecipeCatalog000000003',

tab =>"display",

namespace =>"RecipeCatalog/viewRecipe",

hoverHelp =>"Template for viewing recipes",



label =>"View Recipe Template",

},

groupToPost => {

fieldType =>"group",

defaultValue =>3,

tab =>"security",

hoverHelp =>"Choose the group to post recipes",

label =>"Group To Post",

}

);


push(@{$definition}, {

assetName =>"Recipe Catalog",

autoGenerateForms =>1,

tableName =>'RecipeCatalog',

className =>'WebGUI::Asset::Wobject::RecipeCatalog',

properties =>\%properties

});

return $class->SUPER::definition($session, $definition);

}

 

Notice a few things. All of the default template variables are hardcoded. This is because the default templates will be included with the installation method of the asset. Each template also has a separate namespace. This ensures that the right templates will always be chosen and you won't wind up with a page that has the wrong template for the variables returned.

 

Installing Your Wobject

Once the definition is created, it is useful to get your wobject on your WebGUI instance so you can see progress as you develop. It is unrealistic to think you can build an entire application without ever looking at it, so getting it on the site is a crucial early step.

 

As with developing assets, _NewWobject.skeleton comes with an install method which performs all of the installation necessary for your asset, such as adding the wobject to the config file and creating all of the wobject database tables.

 

There is one additional step you should add to your install method that is not included in the _NewWobject.skeleton class, which is importing all of the default templates for your asset. This not only saves the user the headache of having to manually update your templates, but it also ensures that you are able to hard code the default template values into your definition and database tables as you saw earlier in the chapter.

 

To get a visual working environment for building your application, update the install method of your asset and run the script. Once this is done, you should be able to turn admin on in WebGUI and see your asset in the New Content menu of the Admin Bar.

 

This is a good time to note a few things. First, the installation method is error prone. It is wise to also update the uninstall method in case you make mistakes. That way you can uninstall and start over. Second, installing this early in the application process typically means that you are going to have to make changes. Even though you did some preliminary design, there is usually something that comes up during the development phase that you didn't think of and requires changes to templates and database tables.

 

There are two ways to approach this problem. The first is simply to embed everything in your install scripts and any time you have to make a change to a database table or template, uninstall the application and reinstall it. This will ensure that your scripts continue to work and that you haven't made any syntax errors that might cause it to fail. The drawback is that every time you make a mistake that affects a template or the database you lose any test data you had. Given that during the development process you will be changing templates on a regular basis, this solution can be tedious and time consuming.

 

A better way to manage the installation process is to move all of the templates outside of the script and create an additional executable method which simply modifies all of the templates. This way, after the initial install you can simply run your new method if you make a change to the template. Additionally, your templates (which are mostly HTML) now exist in separate files making them easier to edit and able to be part of any versioning system you use. A cookbook chapter at the end of this book further describes how to do this. For the rest of this chapter, simply assume that your install script was generated through the development process and is now complete, thus it will not be demonstrated here.

 

sub install {

my $config = $ARGV[0];

my $home = $ARGV[1] || "/data/WebGUI";

my $className = "WebGUI::Asset::Wobject::RecipeCatalog";

unless ($home && $config) {

die "usage: Perl -M$className -e install yoursite.conf\n";

}

print "Installing asset.\n";

my $session = WebGUI::Session->open($home, $config);

#Add wobject to config file

$session->config->addToArray("assets",$className);

#Create database tables

$session->db->write("CREATE TABLE RecipeCatalog (

assetId VARCHAR(22) BINARY NOT NULL,

revisionDate BIGINT NOT NULL,

viewTemplateId VARCHAR(22) BINARY NOT NULL,

editRecipeTemplateId VARCHAR(22) BINARY NOT NULL,

viewRecipeTemplateId VARCHAR(22) BINARY NOT NULL,

groupToPost VARCHAR(22) BINARY NOT NULL,

paginateAfter INTEGER DEFAULT '25',

PRIMARY KEY (assetId,revisionDate)

)");

$session->db->write("CREATE TABLE RecipeCatalog_recipe (

recipeId VARCHAR(22) BINARY NOT NULL,

assetId VARCHAR(22) BINARY NOT NULL,

title VARCHAR(100) NOT NULL,

description TEXT NOT NULL,

servings VARCHAR(30) NOT NULL,

ingredients TEXT NOT NULL,

directions TEXT NOT NULL,

PRIMARY KEY (recipeId)

)");

### Create a folder asset to store the default templates

my $importNode = WebGUI::Asset->getImportNode($session);

my $newFolder = $importNode->addChild({

className => "WebGUI::Asset::Wobject::Folder",

title => "Recipe Catalog",

menuTitle => "Recipe Catalog",

url => "recipe_catalog_folder",

groupIdView => "3"

},"RecipeCatalogFolder001");

#Create the templates

#Recipe Catalog View Template

my $recipeCatalogTmpl = q|

<tmpl_if session.var.adminOn>

<p><tmpl_var controls></p>

</tmpl_if>

<tmpl_if displayTitle>

<h2><tmpl_var title></h2>

</tmpl_if>

<tmpl_if error_msg>

<div class="error"><tmpl_var error_msg></div>

</tmpl_if>

<tmpl_if canPost>

<a href="<tmpl_var add_recipe>">add a recipe</a>

</tmpl_if>

<tmpl_if has_recipes>

<table border="1" cellpadding="3" cellspacing="0">

<tbody>

<tr>

<th>Recipe Name</th>

<tmpl_if canPost>

<th>&nbsp;</th>

<th>&nbsp;</th>

</tmpl_if>

</tr>

<tmpl_loop recipe_loop>

<tr>

<td>

<a href="<tmpl_var view_recipe>">

<tmpl_var recipeName>

</a>

</td>

<tmpl_if canPost>

<td>

<a href="<tmpl_var edit_recipe>">edit</a>

</td>

<td>

<a href="<tmpl_var delete_recipe>">delete</a>

</td>

</tmpl_if>

</tr>

</tmpl_loop>

</tbody>

</table>

<tmpl_if pagination.pageCount.isMultiple>

<tmpl_unless pagination.isFirstPage>

<tmpl_var pagination.firstPage>

</tmpl_unless>

<tmpl_var pagination.previousPage>

<tmpl_var pagination.nextPage>

<tmpl_unless pagination.isLastPage>

<tmpl_var pagination.lastPage>

</tmpl_unless>

</tmpl_if>

<tmpl_else>

<table border="1" cellpadding="3" cellspacing="0">

<tbody>

<tr>

<td>No Recipes Have Been Entered.</td>

</tr>

</tbody>

</table>

</tmpl_if>

|;

#Recipe Edit Template Code

my $recipeEditTmpl = q|

<tmpl_if error_msg>

<div class="error"><tmpl_var error_msg></div>

</tmpl_if>

<h3>Edit Recipe</h3>

<tmpl_var form_header>

<tmpl_var form_hidden>

<table border="0" cellpadding="3" cellspacing="0">

<tbody>

<tr>

<td>Recipe Name</td>

<td><tmpl_var form_title></td>

</tr>

<tr>

<td>Description</td>

<td><tmpl_var form_description></td>

</tr>

<tr>

<td>Servings</td>

<td><tmpl_var form_servings></td>

</tr>

<tr>

<td>Ingredients</td>

<td><tmpl_var form_ingredients></td>

</tr>

<tr>

<td>Directions</td>

<td><tmpl_var form_directions></td>

</tr>

<tr>

<td colspan="2" align="right"><tmpl_var form_submit></td>

</tr>

</tbody>

</table>

<tmpl_var form_footer>

|;

#Recipe View Template Code

my $recipeViewTmpl = q|

<h3><tmpl_var recipe_name></h3>

<br /><br />

<table border="0" cellpadding="3" cellspacing="0">

<tbody>

<tr>

<td>Description</td>

<td><tmpl_var recipe_description></td>

</tr>

<tr>

<td>Servings</td>

<td><tmpl_var recipe_servings></td>

</tr>

<tr>

<td>Ingredients</td>

<td><tmpl_var recipe_ingredients></td>

</tr>

<tr>

<td>Directions</td>

<td><tmpl_var recipe_directions></td>

</tr>

</tbody>

</table>

<br /> <br />

<a href="<tmpl_var home_url>">back to recipe catalog</a>

|;

#Add the templates to the folder

$newFolder->addChild({

className =>"WebGUI::Asset::Template",

ownerUserId =>'3',

groupIdView =>'7',

groupIdEdit =>'12',

title =>"Default Recipe Catalog Template",

menuTitle =>"Default Recipe Catalog Template",

url =>"default_recipe_catalog_template",

namespace =>"RecipeCatalog/view",

template =>$recipeCatalogTmpl,

},'RecipeCatalog000000001'

);

$newFolder->addChild({

className=>"WebGUI::Asset::Template",

ownerUserId=>'3',

groupIdView=>'7',

groupIdEdit=>'12',

title=>"Default Edit Recipe Template",

menuTitle=>"Default Edit Recipe Template",

url=>"default_edit_recipe_template",

namespace=>"RecipeCatalog/edit",

template=>$recipeEditTmpl,

},'RecipeCatalog000000002'

);

$newFolder->addChild({

className=>"WebGUI::Asset::Template",

ownerUserId=>'3',

groupIdView=>'7',

groupIdEdit=>'12',

title=>"Default View Recipe Template",

menuTitle=>"Default View Recipe Template",

url=>"default_view_recipe_template",

namespace=>"RecipeCatalog/viewRecipe",

template=>$recipeViewTmpl,

},'RecipeCatalog000000003'

);

#Commit the working version tag

my $workingTag = WebGUI::VersionTag->getWorking($session);

my $workingTagId = $workingTag->getId;

my $tag = WebGUI::VersionTag->new($session,$workingTagId);

if (defined $tag) {

print "Committing tag\n";

$tag->set({comments=>"Folder created by Asset Install Process"});

$tag->requestCommit;

}

$session->var->end;

$session->close;

print "Done. Please restart Apache.\n";

}

 

The following collateral table has been added for storing recipes (will discuss this more a bit later in the chapter).

 

CREATE TABLE RecipeCatalog_recipe (

recipeId VARCHAR(22) BINARY NOT NULL,

assetId VARCHAR(22) BINARY NOT NULL,

title VARCHAR(100) NOT NULL,

description TEXT NOT NULL,

servings VARCHAR(30) NOT NULL,

ingredients TEXT NOT NULL,

directions TEXT NOT NULL,

PRIMARY KEY (recipeId)

);

 

Additionally, a folder has been created and added to the import node. WebGUI's import node can easily become cluttered with templates if you are not responsible about where you put things. For that reason, it's important to create a folder to store the content related to your asset. Not only does it reduce clutter, but it makes it easier to find asset related material.

 

A few changes have also been made to the uninstall script:

 

sub uninstall {

my $config = $ARGV[0];

my $home = $ARGV[1] || "/data/WebGUI";

my $className = "WebGUI::Asset::Wobject::RecipeCatalog";

unless ($home && $config) {

die "usage: Perl -M$className -e uninstall yoursite.conf\n";

}

print "Uninstalling asset.\n";

my $session = WebGUI::Session->open($home, $config);

#Delete wobject from config file

$session->config->deleteFromArray("assets",$className);

#Delete all assets and default templates

my $rs = $session->db->read(qq{

select

assetId

from

asset

where

className='$className'

or assetId like 'RecipeCatalog%'

});

while (my ($id) = $rs->array) {

my $asset = WebGUI::Asset->new($session, $id, $className);

$asset->purge if defined $asset;

}

#Drop asset related tables

$session->db->write("drop table if exists RecipeCatalog");

$session->db->write("drop table if exists RecipeCatalog_recipe");

$session->var->end;

$session->close;

print "Done. Please restart Apache.\n";

}

 

Notice that you are additionally getting rid of your default templates by adding the “or assetId like 'RecipeCatalog%'” clause to the query retrieving all of the assets to purge. Additionally, you are dropping the collateral table as well as the main wobject table.

 

One additional note. In order to display the wobject properly, you need to update the prepareView method to prepare the correct default template. In the wobject skeleton, it assumes a template variable of tempalteId.

 

sub prepareView {

my $self = shift;

$self->SUPER::prepareView();

my $template = WebGUI::Asset::Template->new(

$self->session,

$self->get("viewTemplateId")

);

$template->prepare;

$self->{_viewTemplate} = $template;

}

 

If you fail to do this, WebGUI will complain that it can't instantiate your template.

 

Building Your Wobject

Once the base wobject is installed you should be able to see it in the New Content menu of the Admin Bar and add it to the site. The edit screen should work correctly, and you should see whatever default template you have for the view when you save. Since this is exactly the same process that is described in the chapter on building assets, you are simply going to point out a few additional things in the view method.

 

sub view {

my $self = shift;

my $session = $self->session;

my $db = $session->db;

my $user = $session->user;

my $var = $self->get;

my $canPost = $user->isInGroup($self->get("groupToPost"));

#Create a new instance of a paginator

my $p = WebGUI::Paginator->new(

$session,

$self->getUrl,

$self->get("paginateAfter"),

);

#Create a query to retrieve all of the recipes

my $sql = q|

SELECT

*

FROM

RecipeCatalog_recipe

WHERE

assetId=?

ORDER BY

title ASC

|;

#Pass the query and arguments to the paginator

$p->setDataByQuery($sql,undef,undef,[$self->getId]);

#Use the Paginator to get back an array ref of the current page of data

my $pageData = $p->getPageData;

#Create an array to store the recipes

my @recipeLoop = ();

foreach my $row (@{$pageData}) {

#Create a hashref to store individual loop data

my $recipeHash = {};

#Store all relevant recipe data

$recipeHash->{'recipeName' } = $row->{title};

$recipeHash->{'recipeDesc' } = $row->{description};

$recipeHash->{'servings' } = $row->{servings};

$recipeHash->{'ingredients'} = $row->{ingredients};

$recipeHash->{'direction' } = $row->{directions};

#Create a link to view the recipe

$recipeHash->{'view_recipe'} = $self->getUrl(

"func=viewRecipe;recipeId=".$row->{recipeId}

);

#Create edit and delete links

if($canPost) {

$recipeHash->{'edit_recipe' } = $self->getUrl(

"func=editRecipe;recipeId=".$row->{recipeId}

);

$recipeHash->{'delete_recipe'} = $self->getUrl(

"func=deleteRecipe;recipeId=".$row->{recipeId}

);

}

#Push the hashref onto the recipe loop

push(@recipeLoop,$recipeHash);

}

#Create a template variable for the recipe loop

$var->{'recipe_loop'} = \@recipeLoop;

#Append pagination template variables

$p->appendTemplateVars($var);

#Create a template variable in case there is no data

$var->{'has_recipes'} = (scalar(@recipeLoop) > 0);

#Show a link to add a recipe if the user can post new recipes

if($canPost) {

$var->{'canPost' } = "true";

$var->{'add_recipe'} = $self->getUrl("func=editRecipe");

}

#Return the template to the www_view method

return $self->processTemplate($var, undef, $self->{_viewTemplate});

}

 

In the first few lines set up some helper variables for things contained in session like the database and user object. Then, create a hash reference, $var, in which to put all of your template variables. Calling $self->get initially populates the hash reference with all of the superclass asset data.

 

Then, instantiate a Paginator object to automate pagination of your asset. The Paginator API is extremely useful when building assets that list data. Not only does it provide you a simple means to allow users to page through large data sets, but it does so efficiently.

 

Create a new Paginator instance by calling the constructor and passing it the current session object, the URL that you wish to have paginated (the link to your data set), and the number of data items you wish to have per page.

 

Once you've created a Paginator instance, you can then pass either a query (most efficient) or an array of data (least efficient) to paginate. The Paginator will calculate the number of items to return, as well as the current page, and return you only the relevant data.

 

Additionally, the Paginator has methods for easily appending template variables to your template for displaying pagination. For more information about the Paginator see the WebGUI::Paginator API.

 

Calling $p->getPageData returns a full page of data which you then need to export to your HTML Template. Because you don't know exactly how many rows of data will be returned at any given time, you need to return it as a loop which can be iterated over inside the template. Do this by creating an array reference of hash references. Each hash reference represents a single iteration in the loop and contains the same keys as the previous iteration.



When you assign the array reference to a template variable you create a loop which can be referenced from within your template using the <tmpl_loop> structure.

 

Finally, return the template to the www_view method for the wobject. The www_view method contains the logic to wrap your template in the page style and does this for you automatically.

 

www_ methods

By prefacing a method with www_, you tell WebGUI that this method should be able to be called from the website. You've seen two such methods so far, www_view and www_edit, in the chapter on writing assets. WebGUI calls these methods automatically, so it might not be clear how this functionality works. To call any www_ method in WebGUI, you simply add func=methodName to the end of a URL where methodName is whatever follows www_. For instance, if a method called www_showDetails was created in the wobject, it would be called by simply appending func=showDetails to the end of the asset's URL: http://www.mysite.com/myasset?func=showDetails. This tells WebGUI that you don't wish to see the default view of the wobject, but rather the showDetails page.



Whenever you create a wobject, you will likely need to create your own www_ methods for various tasks you would like your users to do. For instance, in the recipe application, in order to create a new recipe, you are going to need to provide your users a form for adding new recipes. You can accomplish this task by creating a www_editRecipe method.

 

sub www_editRecipe {

my $self = shift;

my $session = $self->session;

my $db = $session->db;

my $user = $session->user;

my $form = $session->form;

my $privilege = $session->privilege;

my $var = $self->get;

my $canPost = $user->isInGroup($self->get("groupToPost"));

#Create an error template variable if an error happened

$var->{'error_msg'} = shift;

#Only allow those who can post recipes to post.

return $privilege->insufficient() unless ($canPost);

#Get the collateral row if one exists

my $recipe = $self->getCollateral(

"RecipeCatalog_recipe",

"recipeId",

$form->get("recipeId")

);

#Create the form elements

$var->{'form_header'} = WebGUI::Form::formHeader($session,{

action=>$self->getUrl()

});

$var->{'form_hidden'} = WebGUI::Form::Hidden($session,{

name =>"func",

value =>"editRecipeSave"

});

$var->{'form_hidden'} .= WebGUI::Form::Hidden($session,{

name =>"recipeId",

value =>$recipe->{recipeId}

});

$var->{'form_title'} = WebGUI::Form::Text($session,{

name =>"title",

value =>$form->get("title") || $recipe->{title},

maxLength => 100

});

my $description

= $form->process("description","HTMLArea") || $recipe->{description};

$var->{'form_description'} = WebGUI::Form::HTMLArea($session,{

name =>"description",

value =>$description,

});

$var->{'form_servings'} = WebGUI::Form::Text($session,{

name =>"servings",

value =>$form->get("servings") || $recipe->{servings}

});

my $ingredients

= $form->process("ingredients","HTMLArea") || $recipe->{ingredients};

$var->{'form_ingredients'} = WebGUI::Form::HTMLArea($session,{

name =>"ingredients",

value =>$ingredients,

});

my $directions

= $form->process("directions","HTMLArea") || $recipe->{directions};

$var->{'form_directions'} = WebGUI::Form::HTMLArea($session,{

name =>"directions",

value =>$directions,

});

$var->{'form_submit'} = WebGUI::Form::submit($session);

$var->{'form_footer'} = WebGUI::Form::formFooter($session);

#return the template wrapped in the page style.

my $templateId = $self->getValue("editRecipeTemplateId");

my $template = $self->processTemplate($var,$templateId);

return $self->processStyle($template);

}

 

Notice that this looks very similar to your view method. There are a few differences that are important to note. First of all, it is important to understand that you are fully responsible for security on your own pages. WebGUI takes care of security when it comes to viewing the wobject, but if you are going to create your own web accessible methods, you need to make sure you set security accordingly. You'll notice that the example uses WebGUI's privilege API to block anyone that isn't in the groupToPost group set in the wobject properties from seeing the page.

 

The other major difference is that you cannot simply return $self->processTemplate() if you want the contents of your template to be wrapped in the site's style. Remember earlier in the chapter it was said that one of the drawbacks to legacy widgets and wobjects was that they didn't enable you to send raw content back to the browser. This is where you have that power. WebGUI's wobject API has a method which wraps the style around the template for you called processStyle():

 

$wobject->processStyle($template)

 

Keep in mind that processStyle will not process the template for you. You must do this prior to calling this method. If you do want to return raw content (such as an RSS feed), you simply omit the style wrapper and return the results of $self->processTemplate (which comes from the Asset API).

 

Another wobject API method used in the www_editRecipe method is the getCollateral method. getCollateral is the first of several methods which make it easy to deal with collateral data in your wobjects:

 

my $row = $wobject->getCollateral(collateralTable,keyName,keyValue)

 

This method returns one row of data from the collateral table you specify by looking for the key which has the value passed in. Some of the other collateral methods will be covered later in this chapter. Here are a few that will not be discussed in detail.

 

  • $wobject->moveCollateralDown(collateralTable, keyName, keyValue); This method reorders content that is sequenced in the database switching its position with the next row.

  • $wobject->moveCollateralUp(collateralTable,keyName,keyValue) This method reorders content that is sequenced in the database switching it's position with the previous row.

  • $wobject->reorderCollateral(collateralTable,keyName) When working with sequential data, it is sometimes necessary to fill gaps in the sequencing that might occur from removing data. This method does this for you automatically.

 

Finally, notice that in the form you have hidden a field called func which has a value of editRecipeSave. Since this is a form post, you need another method which processes your data and puts it into the database. Do this by creating another www_ method and including a hidden func variable which tells WebGUI where to send the post content.

 

sub www_editRecipeSave {

my $self = shift;

my $session = $self->session;

my $db = $session->db;

my $user = $session->user;

my $form = $session->form;

my $privilege = $session->privilege;

my $var = $self->get;

my $canPost = $user->isInGroup($self->get("groupToPost"));

#Only allow those who can post recipes to post.

return $privilege->insufficient() unless ($canPost);

#Build a list of properties based on form elements passed in

my $props = {};

$props->{'recipeId' } = $form->process("recipeId","hidden");

$props->{'title' } = $form->process("title","text");

$props->{'description'} = $form->process("description","HTMLArea");

$props->{'servings' } = $form->process("servings","text");

$props->{'ingredients'} = $form->process("ingredients","HTMLArea");

$props->{'directions' } = $form->process("directions","HTMLArea");

#Do some error checking. Return the user to

if($props->{'title'} eq "") {

return $self->www_editRecipe(

"All recipes must have a title"

);

}

if($props->{'description'} eq "") {

return $self->www_editRecipe(

"All recipes must have a description"

);

}

if($props->{'servings'} eq "") {

return $self->www_editRecipe(

"All recipes must have the number of servings"

);

}

if($props->{'ingredients'} eq "") {

return $self->www_editRecipe(

"All recipes must list ingredients"

);

}

if($props->{'directions'} eq "") {

return $self->www_editRecipe(

"All recipes must have cooking directions"

);

}

 

#Add or update the database with the recipe data

$self->setCollateral(

"RecipeCatalog_recipe",

"recipeId",

$props,

0,

1

);

#Return the default view of the wobject.

return "";

}

 

Like before, this method begins by checking privileges. A common mistake for new developers is to add privilege checks to the view pages but not to the submit pages, making it possible for users to post content to pages they don't have access to.

 

Also note that another collateral method, setCollateral, is used. This method is invaluable when it comes to writing wobjects because it saves you a lot of steps. Normally, when you write software that has to insert and update to a database table, you wind up with code that first has to check to make sure that the content doesn't already exist in the database and then decide whether to perform an insert or an update. The setCollateral method of WebGUI's wobject API does this for you. Additionally, it automates adding assetIds and sequence numbers to your collateral tables:

 

$wobject->setCollateral( collateralTable, keyName, properties, sequenceNumber, assetId)

 

This method accepts a hash reference of properties to be inserted or updated in a database table. If a value is found in the properties hash reference for keyName, it updates all columns in the database matching the key of the properties hash reference with the associated value. If no keyValue is found, or the keyValue is the string “new,” it will insert all of the data in the properties hash reference. This is a huge benefit as any developer who has dealt with this before can attest.

 

Additionally, setCollateral will automatically sequence tables with the column name sequenceNumber. You must be careful to exactly name your column this to take advantage of auto sequencing. Finally, setCollateral will automatically update an assetId column if you specify. The column name must be “assetId” for this to work properly.

 

One final thing to note about this method is that it returns an empty string. This tells WebGUI to return the default view for the asset.

 

Container Wobjects

This is a good opportunity to digress and talk briefly again about Wobject design. As you've seen, WebGUI's wobject API provides you with methods for dealing with collateral data. However, there are many times when your collateral data needs to be indexed for searching, versioned, or maintained in a tree like structure. When you run into these roadblocks, it is a good idea to think about creating a container wobject.

 

A container wobject does not contain any collateral data. Instead, it is a parent for other assets for which it is responsible for displaying. WebGUI is full of container assets. The most notable ones being the Collaboration System, which is responsible for displaying Thread and Post assets; the Calendar asset, which is responsible for displaying Event assets; and the Page Layout asset, which is responsible for displaying all of its children.

 

This works well in situations described above because WebGUI automatically indexes every asset. What this means is that if you are writing a wobject in which you need to search for collateral data, by creating a new asset type for that asset and a container wobject, you won't have to create a search. You will simply use WebGUI's default search to get the job done.



Also, you may need to version your data. This can be tedious and time consuming if you attempt to do this manually within the wobject. Instead, if you create a new asset type and container wobject, any changes made to your sub-assets will automatically be versioned by WebGUI.

 

This is a very powerful method employed for developing wobjects of this type. The recipe asset could very well have taken a different route in the design phase and become a container wobject which displays Recipe Assets.

 

Finishing Up

Now that your users can view, create, and edit recipes, you need to polish off the wobject by adding methods to let the users view and delete recipes. Finally, you need to update the purge and duplicate methods to account for your collateral data.

 

Start by creating a www_veiwRecipes method which allows users to view the recipes that have been entered:

 

sub www_viewRecipe {

my $self = shift;

my $session = $self->session;

my $db = $session->db;

my $user = $session->user;

my $form = $session->form;

my $privilege = $session->privilege;

my $var = $self->get;

#Only allow those who can view the wobject to view the recipes.

return $privilege->insufficient() unless ($self->canView);

my $recipeId = $form->get("recipeId");

#Get the collateral row if one exists.

my $recipe = $self->getCollateral(

"RecipeCatalog_recipe",

"recipeId",

$form->get("recipeId")

);

#Don't let users try to view recipes that don't exist

return "" if($recipe->{recipeId} eq "new");

#Create template variables for the recipe data

$var->{'recipe_name' } = $recipe->{title};

$var->{'recipe_description'} = $recipe->{description};

$var->{'recipe_servings' } = $recipe->{servings};

$var->{'recipe_ingredients'} = $recipe->{ingredients};

$var->{'recipe_directions' } = $recipe->{directions};

#Create a url for a "back" button

$var->{'home_url'} = $self->getUrl();

#return the template wrapped in the page style.

my $templateId = $self->getValue("viewRecipeTemplateId");

my $template = $self->processTemplate($var,$templateId);

return $self->processStyle($template);

}

 

This method looks very similar to the others. You'll note that one slight change has been made in that you are checking the view privileges of the wobject to determine whether or not a user can view the recipe. This differs from the the previous methods which were looking to see if a user could post. Also note that you use the getCollateral and processStyle methods from the wobject superclass again.

 

Next, add a way for users who can post to delete recipes. You could certainly extend this to allow only admins or users who own the posts to delete the records, but for the purposes of this example simply allow anyone who can post to delete any recipe.

 

sub www_deleteRecipe {

my $self = shift;

my $session = $self->session;

my $user = $session->user;

my $form = $session->form;

my $privilege = $session->privilege;

my $var = $self->get;

my $canPost = $user->isInGroup($self->get("groupToPost"));

#Only allow those who can post recipes to post.

return $privilege->insufficient() unless ($canPost);

my $recipeId = $form->get("recipeId");

#Don't let users try to delete recipes that don't exist

return "" unless ($recipeId);

 

my $message = "Are you sure you want to delete this recipe?";

my $yesUrl = $self->getUrl(

"func=deleteRecipeConfirm;recipeId=$recipeId"

);

return $self->processStyle($self->confirm($message,$yesUrl));

}

 

This method uses another wobject API method used for displaying a confirmation page to the user.

 

$html = $wobject->confirm($message,$yesUrl,$noUrl)

 

This method returns a standard, pre-configured confirmation screen which allows users to change their minds if they accidentally delete the wrong item. Passing in a message, along with a URL to go to if the user would like to continue deleting, and a URL to go to if a mistake was made returns standard HTML used throughout WebGUI for confirming user intentions. If you do not supply a noUrl, the users will be taken back to the main view of the application if they make a mistake.

 

The example above indicates that if the user is purposely deleting the content, he/she should be directed to the www_deleteRecipeConfirm page to which you pass the unique key of the table in which recipes are stored.

 

sub www_deleteRecipeConfirm {

my $self = shift;

my $session = $self->session;

my $user = $session->user;

my $form = $session->form;

my $privilege = $session->privilege;

my $var = $self->get;

my $canPost = $user->isInGroup($self->get("groupToPost"));

#Only allow those who can post recipes to post.

return $privilege->insufficient() unless ($canPost);

my $recipeId = $form->get("recipeId");

#Don't let users try to delete recipes that don't exist

return "" unless ($recipeId);

$self->deleteCollateral(

"RecipeCatalog_recipe",

"recipeId",

$recipeId

);

return "";

}

 

This method uses the final wobject API method discussed in this chapter.

 

$wobject->deleteCollateral(collateralTable,keyName,keyValue);

 

This method deletes a single row from the collateral table where keyName matches keyValue.

 

The last thing you need to do for this wobject to be complete is to update the purge and duplicate methods that were discussed in the chapter on writing assets. These methods are slightly different for wobjects as you need to account for collateral data.

 

sub purge {

my $self = shift;

my $db = $self->session->db;

my $sql = q{

DELETE

FROM

RecipeCatalog_recipe

WHERE

assetId=?

};]

$db->write($sql,[$self->getId]);

return $self->SUPER::purge;

}

 

When purge is called on an instance of a wobject, it needs to remove all collateral data associated with the wobject. This goes back to your design. A common mistake is to forget that multiple instances of wobjects can exist across the site. It is important to make sure that all collateral is related to its asset index so you can properly purge as well as duplicate.

 

sub duplicate {

my $self = shift;

my $db = $self->session->db;

my $newAsset = $self->SUPER::duplicate(@_);

my $sql = q{

SELECT

*

FROM

RecipeCatalog_recipe

WHERE

assetId=?

};

my $sth = $db->read($sql,[$self->getId]);

while (my $row = $sth->hashRef) {

$row->{recipeId} = "new";

$row->{assetId } = $newAsset->getId;

$newAsset->setCollateral(

"RecipeCatalog_recipe",

"recipeId",

$row,

0,

1

);

}

return $newAsset;

}

 

The duplicate method selects all of the data related to the original asset, and copies it using the setCollateral method of the new asset. This ensures that all the recipes, as well as the asset itself, are copied.

 

Wobject Inheritance

Finally, it's worth some time to talk about wobject inheritance. As said at the beginning of the chapter, there are times where you want an existing asset to work just a bit differently. In cases like this, you can inherit all of the properties of the existing wobject and simply override the parts you want to change.

 

Say, for example, that you would like to export a few more template variables in the view of your recipe wobject. Rather than starting from scratch or copying the recipe wobject exactly, you can easily extend the wobject by using it as your base instead of the wobject class.

 

use base 'WebGUI::Asset::Wobject::RecipeCatalog';

 

Since the RecipeCatalog wobject is a wobject, your new wobject is one also. Like every wobject, you will need to create a database table that has the assetId / revisionDate composite key as well as a definition. The good news is, if you aren't changing any of the properties, your definition is going to be rather short.

 

Finally, to change the view, simply copy the view from the RecipeCatalog wobject into the new one and make the changes that are necessary. No collateral tables would be necessary as all of the recipes would be stored in the original RecipeCatalog tables. Instead of all the work that went into creating the new asset, overriding two methods allows you to create a brand new asset with its own functionality.

Keywords: API application asset plugin specification web object

Search | Most Popular | Recent Changes | Wiki Home
© 2023 Plain Black Corporation | All Rights Reserved