plainblack.com
Username Password
search
Bookmark and Share
Subscribe

Wobject Development Tutorial

Wobject Tutorial

The following is a tutorial on how to build your own Wobjects. Note that the same techniques go into building regular assets. Before building a Wobject you should be familiar with WebGUI's available API's, the Wobjects that already exist and their code. If you don't familiarize yourself with all of these you'll likely spend a lot of time reinventing the wheel, and ultimately you may even fail in building your own Wobject.


NOTE: Feel free to install and debug your Wobject at any point during this tutorial. Nobody codes perfectly the first time, and since you're new to Wobjects, you'll likely need more debugging time. We won't mention debugging again, as we'll assume you're doing it after each step.

Before You Begin

Before you begin developing a new Wobject, you need to make some design considerations about your new Wobject, and make sure you have everything you need for development.

Design Considerations

There are all sorts of questions you should ask yourself before you begin writing the code for your new Wobject.  Here are some you should definitely consider, and you should probably come up with a few of your own.

  • Is it necessary?
    • Should I build an asset rather than a wobject?
    • Is there already another Wobject or asset that can do what I need?
    • Could I do this with a macro instead?
  • What do I want it to do?
    • What does my client or audience need from this Wobject?
      • What are they asking for?
      • What does their request really mean?
    • What features should it have in the first version?
      • Do all these features need to go in right away or should I focus on getting it done fast?
    • What features would I like to add in the future?
  • Where can I borrow from?
    • Is there an existing Wobject that I can copy and modify?
    • Is there another program that has similar functionality that I can borrow ideas from?
  • What data do I need to store?
    • Where do I get my data from?
      • External databases?
      • Data entry from users?
      • A web service?
      • Another service?
    • Where do I put my data?
      • On the filesystem?
      • In the database?
      •  
        • What does the database need to look like?
        • What tables do I need?
        • Should I store it in WebGUI's database or an external database?
      • In WebGUI's cache?
  • What should I call my wobject?
    • Does my name (and more importantly namespace) conflict with existing Wobjects?
    • Does my name tell the user what the Wobject does?

NOTE: All Wobjects must have a unique namespace. A namespace is a unique name for a Wobject that WebGUI will use to keep your Wobject separated from all the other Wobjects in the system. It will be used in database table names, package names, internationalization, and more.

What You'll Need

There are a number of things you'll need in order to create your new Wobject. Make sure you have them all available before you start coding.

  • A good text editor. (See the WebGUI install instructions for a list of text editors for your platform.)
  • A working installation of WebGUI.
    • Be sure that you're developing on a system that doesn't need to be live. You'll likely have syntax errors and other problems with your code throughout the development cycle. Your users won't appreciate it if you keep killing the production server during your development.
    • Be sure to use the version of WebGUI that you'll be deploying to as there are differences in every version.  Though we try to make the changes in API compatible from version to version, if you develop for WebGUI 8.2 and want to deploy on WebGUI 6.0 then you'll certainly run into problems.
    • Use the WebGUI Runtime Environment in developer mode.
    • You'll need access to the filesystem of your development server as well as access to the database so you can create and drop tables.
  • A copy of this document and the API for reference.

Start With A Skeleton

In lib/WebGUI/Asset/_NewAsset.skeleton and lib/WebGUI/Asset/Wobject/_NewWobject.skeleton we've provided you with baseline files to begin with. Use these! Don't start from scratch.

Now we need to modify the skeleton to use the namespace of our Wobject, rather than "NewWobject". Go through each of the templates, replacing "NewWobject" with the namespace of your Wobject. Also, don't forget to rename each file to represent your namespace.

For the purposes of this tutorial we've decided to create a Trivia wobject, so our namespace will be "Trivia". We've also decided not to internationalize the Wobject during the development process because it would add too much complexity to the tutorial. When you become a proficient Wobject developer you'll save yourself a lot of time by internationalizing the Wobject as you develop it.

NOTE: After you've modified the skeleton, you've already created your Wobject. It obviously wouldn't do much, but you could install it at this point and have a functioning Wobject.

definition()

We first need to modify the definition method to match our needs. The definition method allows us to add properties to the Wobject, as well as adjust other settings for this wobject.

sub definition {
my $class = shift;
my $session = shift;
my $definition = shift;
my %properties;
tie %properties, 'Tie::IxHash';
%properties = (
templateId =>{
fieldType=>"template",
defaultValue=>'TriviaTmpl000000000001',
tab=>"display",
namespace=>"Trivia",
hoverHelp=>"Choose a template for your trivia wobject.",
label=>"Trivia Template"
}
);
push(@{$definition}, {
assetName=>"Trivia",
icon=>'trivia.gif', #we need to create an icon and put it in www/extras/assets/
autoGenerateForms=>1,
tableName=>'Trivia',
className=>'WebGUI::Asset::Wobject::Trivia',
properties=>\%properties
});
return $class->SUPER::definition($session, $definition);
}

 

Add Data Management

Add the data management methods for your Wobject. This is where your Wobject really starts to set itself apart from any other wobject out there. Because the functionality of the Wobject that you're developing could literally be anything you can dream of, we can't provide much guidance for this section other than the examples of the data management for our Trivia Wobject.

#-------------------------------------------------------------------
sub www_deleteAnswer {
        my $self = shift;
        my ($privilege, $form) = $self->session->quick("privilege", "form");
        return $privilege->insufficient() unless ($self->canEdit);
        $self->deleteCollateral("Trivia_answer","answerId",$form->param("answerId"));
        return $self->www_editQuestion;
}

#-------------------------------------------------------------------
sub www_deleteQuestion {
        my $self = shift;
        my ($privilege, $form, $db) = $self->session->quick("privilege", "form", "db");
        return $privilege->insufficient() unless ($self->canEdit);
        $db->write("delete from Trivia_answer where questionId=?", $form->param("questionId"));
        $self->deleteCollateral("Trivia_question","questionId", $form->param("questionId"));
        return $self->www_view;
}

#-------------------------------------------------------------------
sub www_editAnswer {
        my $self = shift;
        my ($privilege, $form) = $self->session->quick("privilege", "form");
        my $questionId = shift || $form->param("questionId");
        my $answerId = shift || $form->param("answerId");
        return $privilege->insufficient() unless ($self->canEdit);
        my $answer = $self->getCollateral("Trivia_answer","answerId", $answerId);
        my $f = WebGUI::HTMLForm->new($self->session, action=>$self->getUrl);
        $f->hidden(
                name=>"func",
                value=>"editAnswerSave"
                );
        $f->hidden(
                name=>"questionId",
                value=>$questionId
                );
        $f->hidden(
                name=>"answerId",
                value=>$answerId
                );
        $f->text(
                name=>"answer",
                value=>$answer->{answer},
                label=>"Answer",
                hoverHelp=>"Fill in a possible answer to the question."
                );
        $f->yesNo(
                name=>"isCorrect",
                value=>$answer->{isCorrect},
                label=>"Is this answer correct?",
                hoverHelp=>"If this answer is the correct answer for the question, select 'yes'"
                );
        if ($answerId eq "new") {
                $f->whatNext(
                        value=>"addAnswer",
                        options=>{
                                addAnswer=>"Add another answer.",
                                backToQuestion=>"Go back to the question.",
                                addQuestion=>"Add another question.",
                                backToPage=>"Go back to the page."
                                }
                        );
        }
        $f->submit;
        return $self->getAdminConsole->render($f->print, "Edit Trivia Answer");
}

#-------------------------------------------------------------------
sub www_editAnswerSave {
        my $self = shift;
        my ($privilege, $form) = $self->session->quick("privilege", "form");
        return $privilege->insufficient() unless ($self->canEdit);
        $self->setCollateral("Trivia_answer","answerId",{
                questionId=>$form->param("questionId"),
                answer=>$form->process("answer", "text"),
                isCorrect=>$form->process("isCorrect","yesNo"),
                answerId=>$form->param("answerId")
                },0);
        my $proceed = $form->param("proceed");
        if ($proceed eq "backToPage") {
                return $self->www_view;
        } elsif ($proceed eq "addAnswer") {
                return $self->www_editAnswer;
        } elsif ($proceed eq "addQuestion") {
                return $self->www_editQuestion("new");
        } else {
                return $self->www_editQuestion;
        }
}

#-------------------------------------------------------------------
sub www_editQuestion {
        my $self = shift;
        my ($privilege, $form, $icon, $db) = $self->session->quick("privilege", "form", "icon", "db");
        my $questionId = shift || $form->param("questionId");
        return $self->session->privilege->insufficient() unless ($self->canEdit);
        my $answerList;
        my $question = $self->getCollateral("Trivia_question","questionId",$questionId);
        my $f = WebGUI::HTMLForm->new($self->session, action=>$self->getUrl);
        $f->hidden(
                name=>"func",
                value=>"editQuestionSave"
                );
        $f->hidden(
                name=>"questionId",
                value=>$question->{questionId}
                );
        $f->text(
                name=>"question",
                value=>$question->{question},
                label=>"Question"
                );
        my $ac = $self->getAdminConsole;
        if ($question->{questionId} eq "new") {
                $f->whatNext(
                        value=>"addAnswer",
                        options=>{
                                addAnswer=>"Add an answer.",
                                backToPage=>"Go back to the page."
                                }
                        );
        } else {
                $ac->addSubmenuItem($self->getUrl("func=editAnswer;questionId=".$question->{questionId}
.";answerId=new"), "Add a new answer.");
                my $sth = $db->read("select answerId,answer from Trivia_answer where questionId=?", 
[$question->{questionId}]);
                while (my ($id, $answer) = $sth->array) {
                        $answerList .= $icon->delete("func=deleteAnswer;answerId=".$id.";questionId="
.$question->{questionId},
                                        $self->get("url"), "Are you certain you wish to delete this answer?")
                                .$icon->edit("func=editAnswer;answerId=".$id.";questionId=".$question->{questionId}, 
$self->get("url"))
                                ." ".$answer."<br />";
                }
        }
        $f->submit;
        return $ac->render($f->print.'<p>'.$answerList.'</p>', "Edit Trivia Question");
}

#-------------------------------------------------------------------
sub www_editQuestionSave {
        my $self = shift;
        my ($privilege, $form) = $self->session->quick("privilege", "form");
        return $privilege->insufficient() unless ($self->canEdit);
        my $id = $self->setCollateral("Trivia_question","questionId",{
                questionId=>$form->param("questionId"),
                question=>$form->process("question","text")
                },0);
        if ($form->param("proceed") eq "addAnswer") {
                return $self->www_editAnswer($id,"new");
        } else {
                return $self->www_view;
        }
}

 

Create The Default View

Now that we've got data management, we need to start working on the user interface for the site visitors. This all starts with the default view and is handled through the www_view(), view(), and prepareView() methods. The Wobject superclass has a www_view method, so we'll just inherit that one. But we'll create the other two:

#-------------------------------------------------------------------
sub prepareView {
my $self = shift;
my $session = $self->session;
my $db = $session->db;
$self->SUPER::prepareView();
my %var = ();
my @questions = $db->buildArray("select questionId from Trivia_question where assetId=?",[$self->getId]);
my @answer_loop = ();
if ($#questions >= 0) {
# we preprocess the form, because the form controls may have
# css, javascript or other stuff to go in the html head block
my $question = $self->getCollateral("Trivia_question","questionId",$questions[rand($#questions+1)]);
$var{question} = $question->{question};
$var{"form.start"} = WebGUI::Form::formHeader($session, {action => $self->getUrl});
$var{"form.start"} .= WebGUI::Form::hidden($session,{
name=>"func",
value=>"view"
});
$var{"form.start"} .= WebGUI::Form::hidden($session,{
name=>"questionId",
value=>$question->{questionId}
});
$var{"form.submit"} = WebGUI::Form::submit($session,{value=>"Guess!"});
$var{"form.end"} = WebGUI::Form::formFooter($session);
my $sth = $db->read("select answerId,answer from Trivia_answer where questionId=?",
[$question->{questionId}]);
while (my ($id, $answer) = $sth->array) {
push (@answer_loop,{
"answer.id"=>$id,
"answer.form"=>WebGUI::Form::radio($session,{
name=>"guess",
value=>$id
}),
"answer.label"=>$answer
});
}
randomizeArray(\@answer_loop);
$var{answer_loop} = \@answer_loop;
}
my $template = WebGUI::Asset::Template->new($session, $self->get("templateId"));
$template->prepare;
$self->{_var} = \%var;
$self->{_viewTemplate} = $template;
}
#-------------------------------------------------------------------
sub view {
my $self = shift;
my ($form, $db, $icon) = $self->session->quick(qw(form db icon));
my $var = $self->{_var};
my $guess = $form->process("guess","radioList");
if ($guess ne "") {
my $questionId = $form->param("questionId");
$var->{"hasGuessed"} = 1;
my $question = $self->getCollateral("Trivia_question","questionId", $questionId);
$var->{"guess.question"} = $question->{question};
my $answer = $self->getCollateral("Trivia_answer","answerId",$guess);
$var->{"guess.response"} = $answer->{answer};
$var->{"guess.isCorrect"} = 0;
if ($answer->{isCorrect}) {
$var->{"guess.isCorrect"} = 1;
} else {
my ($answer) = $db->quickArray("select answer from Trivia_answer where questionId=?
and isCorrect=1",[$questionId]);
$var->{"guess.answer"} = $answer;
}
}
$var->{"add.url"} = $self->getUrl("func=editQuestion;questionId=new");
$var->{"add.label"} = "Add a new trivia question.";
my $sth = $db->read("select questionId,question from Trivia_question where assetId=?",[$self->getId]);
while (my ($id, $question) = $sth->array) {
$var->{"question.list"} .= $icon->delete("func=deleteQuestion;questionId=".$id, $self->get("url"),
"Are you sure you want to delete this question and it's answers?")
.$icon->edit("func=editQuestion;questionId=$id", $self->get("url"))
." ".$question."<br />";
}
return $self->processTemplate($var, undef, $self->{_viewTemplate});
}

 

Add Other UI Components

If you have other user-level functionality and UI components, you should add them at this point. Our trivia example is pretty simple andhas no additional functionality to add.

Extend Utility Methods

Now that you've got all your functionality, it's time to round out your Wobject by extending any utility methods that you'll need. Two common utility methods are duplicate and purge. The duplicate method is used by users when they click on the "Copy" button, as well as by the package management system. The purge method is used by the trash system when it's cleaning up old content. We need to extend these two methods to deal with our collateral data (the questions and answers).

#-------------------------------------------------------------------
sub duplicate {
my $self = shift;
my $newAsset = $self->SUPER::duplicate(shift);
my $db = $self->session->db;
my $stha = $db->read("select * from Trivia_question where assetId=?",[$self->getId]);
while (my $question = $stha->hashRef) {
my $sthb = $db->read("select * from Trivia_answer where questionId=?",[$question->{questionId}]);
$question->{questionId} = "new";
$question->{questionId} = $newAsset->setCollateral("Trivia_question","questionId",$question,0);
while (my $answer = $sthb->hashRef) {
$answer->{answerId} = "new";
$answer->{questionId} = $question->{questionId};
$newAsset->setCollateral("Trivia_answer","answerId",$answer,0);
}
}
return $newAsset;
}
#-------------------------------------------------------------------
sub purge {
my $self = shift;
my $db = $self->session->db;
$db->write("delete from Trivia_question where assetId=?",[$self->getId]);
$db->write("delete from Trivia_answer where assetId=?",[$self->getId]);
return $self->SUPER::purge;
}

 

Finishing It Off

We're finally done building our Wobject. However, we need to create a mechanism to create the database tables, add the entry to the config file, and import the template. We could write seperate scripts for these things, but it may be better to just include them in the Wobject itself. Below is an example that can be added to the bottom of the script to give install/uninstall functionality.

use base 'Exporter';
our @EXPORT = qw(install uninstall);
use WebGUI::Session;

#-------------------------------------------------------------------
sub install {
        my $config = $ARGV[0];
        my $home = $ARGV[1] || "/data/WebGUI";
        die "usage: perl -MWebGUI::Asset::Wobject::Trivia -e install www.example.com.conf\n" 
unless ($home && $config);
        print "Installing asset.\n";
        my $session = WebGUI::Session->open($home, $config);
        $session->config->addToArray("assets","WebGUI::Asset::Wobject::Trivia");
        $session->db->write("create table Trivia (
                assetId varchar(22) binary not null,
                revisionDate bigint not null,
                templateId varchar(22) binary not null default 'TriviaTmpl000000000001',
                primary key (assetId, revisionDate)
                )");
        $session->db->write("create table Trivia_question (
                assetId varchar(22) binary not null,
                questionId varchar(22) binary not null primary key,
                question varchar(255)
                )");
        $session->db->write("create table Trivia_answer (
                assetId varchar(22) binary not null,
                questionId varchar(22) binary not null,
                answerId varchar(22) binary not null primary key,
                answer varchar(255),
                isCorrect int not null default 0
                )");
        my $import = WebGUI::Asset->getImportNode($session);
        $import->addChild({
                className=>"WebGUI::Asset::Template",
                template=>q|
<div><a name="id<tmpl_var assetId>" id="id<tmpl_var assetId>"></a></div>

<tmpl_if session.var.adminOn>
        <p><tmpl_var controls></p>
</tmpl_if>
<tmpl_if displayTitle>
    <h1><tmpl_var title></h1>
</tmpl_if>

<tmpl_if description>
    <tmpl_var description><p />
</tmpl_if>

<table class="content" width="100%">
<tr>

<td valign="top">
   <tmpl_var form.start>
   <b><tmpl_var question></b><p/>
    <tmpl_loop answer_loop>
        <tmpl_var answer.form> <tmpl_var answer.label> <br />
   </tmpl_loop>
    <p />
   <tmpl_var form.submit>
   <tmpl_var form.end>
</td>

<tmpl_if hasGuessed>
   <td valign="top" width="50%">
     <b>Previous Guess:</b><br />
   <tmpl_var guess.question><p />
     <b>You guessed:</b> <tmpl_var guess.response> <p />
    <tmpl_if guess.isCorrect>
         <span style="color: green; font-weight: bold;">That is correct!</span>
    <tmpl_else>
          <span style="color: red; font-weight: bold;">Wrong!</span><p />
           <b>The correct answer is:</b> <tmpl_var guess.answer>
    </tmpl_if>
   </td>
</tmpl_if>

</tr>
</table>
<tmpl_if session.var.adminOn>
<p />
<a href="<tmpl_var add.url>"><tmpl_var add.label></a><p />

<tmpl_var question.list>
</tmpl_if>
|,
                ownerUserId=>'3',
                groupIdView=>'7',
                groupIdEdit=>'12',
                title=>"Trivia",
                menuTitle=>"Trivia",
                url=>"templates/trivia",
                namespace=>"Trivia"
                },'TriviaTmpl000000000001');
        my $versionTag = WebGUI::VersionTag->getWorking($session);
        $versionTag->set({name=>"Install Trivia Wobject Template"});
        $versionTag->commit;
        $session->var->end;
        $session->close;
        print "Done. Please restart Apache.\n";
}

#-------------------------------------------------------------------
sub uninstall {
        my $config = $ARGV[0];
        my $home = $ARGV[1] || "/data/WebGUI";
        die "usage: perl -MWebGUI::Asset::Wobject::Trivia -e uninstall www.example.com.conf\n" 
unless ($home && $config);
        print "Uninstalling asset.\n";
        my $session = WebGUI::Session->open($home, $config);
        $session->config->deleteFromArray("assets","WebGUI::Asset::Wobject::Trivia");
        # delete Trivia asset instances
        my $rs = $session->db->read("select assetId from asset where className=
'WebGUI::Asset::Wobject::Trivia'");
        while (my ($id) = $rs->array) {
                my $asset = WebGUI::Asset->new($session, $id, "WebGUI::Asset::Wobject::Trivia");
                $asset->purge if defined $asset;
        }
        # delete templates
        my $rs = $session->db->read("select distinct(assetId) from template where namespace='Trivia'");
        while (my ($id) = $rs->array) {
                my $asset = WebGUI::Asset->new($session, $id, "WebGUI::Asset::Template");
                $asset->purge if defined $asset;
        }
        $session->db->write("drop table Trivia_answer");
        $session->db->write("drop table Trivia_question");
        $session->db->write("drop table Trivia");
        $session->var->end;
        $session->close;
        print "Done. Please restart Apache.\n";
}

 

To use these functions, we do the following:

cp /path/to/Trivia.pm /data/WebGUI/lib/WebGUI/Asset/Wobject/
cd /data/WebGUI/lib
perl -MWebGUI::Asset::Wobject::Trivia -e install www.example.com.conf

And if we ever want to uninstall it:

cd /data/WebGUI/lib
perl -MWebGUI::Asset::Wobject::Trivia -e uninstall www.example.com.conf
rm /data/WebGUI/lib/WebGUI/Asset/Wobject/Trivia.pm

 

Additional Considerations

There are a few other important things to consider when building your Wobject:

Consider contributing your Wobject back to the WebGUI community. WebGUI was made available free to anyone who wanted to use it. That's likely how you found out about WebGUI, and one of the reasons you decided to use it. Give back to the community that has given you so much.

If you're developing a Wobject that you're planning to publish on the internet, you should consider Internationalizing it. If you do not, your wobject will only be useful to the group of people who speak your language. If you internationalize it, you can make the Wobject available to billions more people.

If you're developing a Wobject for production use, you'll likely need to patch it from time to time to add new features, or to fix bugs. Be sure to keep all of your patches in an organized fashion so that people using the older versions of your Wobject can upgrade. We recommend putting your upgrades in docs/upgrades and naming them using the WebGUI patch convention like this: upgrade_NewWobject-1.0.0-1.0.1.sql

Keywords: api Assets dev tutorial

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