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.
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.
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 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;
}
}
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});
}
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;
}
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
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