Adding pages to the MolProbity3 web interface
This document is a tutorial on adding new web pages to the graphical
side of MolProbity, that is, the MolProbity that you use via a web
browser. Before reading this, it would be a VERY good idea to read
through the document on MolProbity's UI
framework. You don't have to understand all of it, but it really is
important to see the overview. After you finish this tutorial, you
should go back and read it again, and it should be clearer.
Overview
As an example, this tutorial describes adding the "simple kinemages"
feature to MolProbity3, the one that allows users to choose one of the
basic Prekin scripts to make a kinemage, and then allows them to view
it in KiNG or to download it.
Here are the steps:
- Create a class that displays a web page for the user input, so
the user can choose a Prekin script to run. This class is called a delegate and will live in the pages/ folder.
- Add a link to this delegate page from one or more of the existing
pages in MolProbity.
- Create a PHP script that runs Prekin and produces the kinemage,
based on the user input. This script will run as a background job and
lives in the jobs/ folder.
- Create a class that displays a web page that presents the
finished kinemage to the user, with links for downloading and viewing.
This class is also a delegate and will also live in the pages/ folder.
User input page
Copy the file template-page.php
from doc/extending/ into pages/, and give it a new name
(like makekin_setup.php).
The file makekin_setup.php
in fact already exists, so that you can follow along in it and see the
actual code that is summarized below. Every script in pages/ declares a class, and
those classes must all have unique names that exactly match their
filenames (with ".php" removed and and "_delegate" added). So, change
"template_delegate"
to "makekin_setup_delegate".
The template contains a fair bit of junk that we don't need, but there
are a lot of other xxx_setup.php scripts that
can be plundered for example code. It also inherits some functionality
from the BasicDelegate
class, which is definted in event_page.php.
We're going to start in the display()
function, which is
responsible for outputing the HTML web page that the user sees when
they get here. (We'll discuss how
they get here later on.)
Display should start with $this->pageHeader()
and end with $this->pageFooter().
These two functions take care of creating the <HTML> and
<BODY> tags, including stylesheets, etc. The header can
optionally generate a sidebar of links, timed commands to refresh the
page, and other things. By default, these functions are just wrappers
for mpPageHeader() and mpPageFooter() in core.php.
The working content of the
page is quite simple: a list of radio buttons
for selecting the model (PDB file) to work with, a similar list of
buttons for the Prekin scripts, and a <FORM> to hold them. In
order for event handling to work properly, the form must be created
with the makeEventForm()
function, although it needs to be ended by a normal </form> tag.
The Prekin-script radio buttons are arranged in a table for easier
formatting, with
alternating colors for the rows. All the radio buttons have the same name='scriptName', which is
what makes them act as a group (only one member of the group can be
selected at any given time). Each radio button has a different value, which we will use later
to decide which Prekin script to run.
The buttons for selecting a model are very similar, and were in fact
stolen from one of the other xxx_setup.php scripts. All
these buttons have name='modelID'.
Many things in MolProbity do not need to be reinvented -- there is
either a reuseable function available, or there is existing code that
can form a good starting point. In this case, we choose to copy and
modify because our requirements are subtly different from those of the
other pages.
There are two submit buttons for this form, which both have name='cmd'. The text in their value property is what appears
as the button text, and it's what we'll use in a minute to decide which
one the user pressed, and whether they want to run Prekin or cancel
this operation.
The other thing we need to declare is a function to handle the
form-submission event. We call it onRunPrekin(),
because that's the name we passed to makeEventForm() back in display(). It could take one or
more
arguments as well; this feature
exists to allow one function to handle many related <FORM>s, but
we only have one form that needs to be handled. All the data submitted
by the user in the form is available through the PHP superglobal $_REQUEST. The keys of $_REQUEST are the names of things in the form, so
our keys are scriptName
(from the radio buttons) and cmd
(from the submit buttons). The values in $_REQUEST are the values specified in the form.
Thus, we first use $_REQUEST['cmd']
to decide if the user pressed the Cancel button or the Make Kinemage
button. Pressing Cancel leads us to return to the previous page using pageReturn(). (This requires
that the previous page got here by calling pageCall() rather than pageGoto(), which we'll ensure
it does in just a minute.) For now, the other button should just do
nothing. Doing nothing will just cause our form to be displayed again.
In a while, we'll add code to actually run Prekin when the button is
pressed.
Linking to the input page
Now that we have an input page, we need to be able to see it. Because
it's not a typical, simple PHP web page, you can't just go to its URL.
This is actually deliberate and a huge advantage for a web application,
as described in the UI Framework document. However, that means that we
need to connect it to the real site in order to try it out. There are
two places where we want to link it: from the site map, and from the
quick-links sidebar.
The site map is generated by pages/sitemap.php.
There are lots of links here, so it's easy to copy & paste a good
example. The key call is this one:
makeEventURL("onCall",
"makekin_setup.php")
Unlike other uses of makeEventURL()
we've seen, this one has two
arguments. The first is still the name of the function that will be
called when the user clicks on the link. The second one will be passed
to that function as it's $arg.
This allows one function (onCall())
to handle jumping off in any number of directions. Incidentally, onCall() is defined in
the BasicDelegate class,
which is declared in lib/event_page.php.
If you go look at it, you'll see it just uses pageCall() to transfer control
to the named delegate page:
function
onCall($arg)
{
pageCall($arg);
}
The very similar onGoto()
event handling function uses pageGoto()
instead, but is otherwise identical. It's not appropriate here, because
we want to be able to "call" the Prekin page from anywhere and always
return to where we started from when it finishes. The trade-off we make
is that the Prekin pages must not display the sidebar, lest the user
jump out of our nicely constructed "loop" of Prekin pages.
We add our page to the sidebar in a similar way, by editing lib/core.php. (Not really
ideal, is it? This function should live somewhere else, but it's stayed
in the core as a historical artifact.) In the mpNavigationBar() function,
duplicated one of the existing entries and modify it to look like this:
$s .=
mpNavBar_call('makekin_setup.php', 'Make simple kins');
Now we've succeeded in linking our page into the main site. Try
accessing your local development copy of MolProbity, and on the Site
Map page you should see a link to the kinemage-creation page, as well
as a link in the sidebar.
Running Prekin
Now that we've got an interface, it would be nice if it actually did
something when the user pushed the button. For that, we'll need a
command-line PHP script that actually does the work in the background
while pages/job_progress.php
is displayed in the web browser to keep the user entertained. Create a
new background job by copying doc/extending/template-job.php
to jobs/makekin.php.
Here we don't have to worry about delegates and all that mess -- this
script runs in a much more straightforward manner. However, you'll
notice that it has to explicitly load the session data and initialize
the MolProbity environment in the first few lines of the script. These
chores are done for the delegate pages by public_html/index.php, so they
never have to bother with it.
Within the main part of the script, we retrieve form values from the $_SESSION['bgjob'] array, which
was filled with data from the form by our onRunPrekin() function. We also
make sure the kinemage file directory exists so we have somewhere to
put the output file -- notice that files are stored in directories
based on their type (PDB, kinemage, tabular ASCII data, etc) and are
named with a prefix that comes from their original PDB file.
Next, we'll use a switch
statement to make sure that the specified script is a valid one. It's
very easy for someone to submit arbitrary data to a web form, so we
want to make sure the input is valid before running it on the command
line. This sort of precaution is very important -- it would be bad if
someone embedded a "; rm -rf / ;"
in the middle of your command! PHP has several built-in functions that
can also help protect you (e.g.
escapeshellcmd()); use
these when appropriate.
Even though our script will run quickly, in just a few seconds, we
should display a status message for the user. Remember, the job_progress.php page is being
displayed in the user's browser while this
script is running in the background, and it gets refreshed every few
seconds with the latest status. The steps to be checked off are listed
in order in an array (call it $tasks),
and then you call setProgress()
to update which step is the active one. In our example, we have just
one step:
$tasks['kin']
= "Make kinemage using <code>Prekin $flag</code>";
setProgress($tasks, 'kin');
Now were ready to execute the Prekin command. When it returns, we'll
call setProgress($tasks, null)
to mark all steps as complete, just before returning from the script.
There are two ways to approach getting output to the user. We could
store some data in the $_SESSION['bgjob']
array (e.g. the name of the
kinemage file) and pass that to our "results" delegate page, which
would then use it to construct a bit of explanatory HTML with a link to
open the kinemage in KiNG. There's nothing wrong with that per se, and that's the way a lot of
the old MolProbity was written. The alternative scheme is to construct
a result message or mini-page in HTML and store it as a lab notebook
entry. This is done as the last processing step of the background
script. Using a notebook entry has two benefits:
- At any time, the user can go to the lab notebook page and see a
chronological, stream-of-consciousness record of all the results
they've produced during the MolProbity session.
- The user can edit the HTML of these entries in order to annotate
them, creating a rich record of the refitting process that can be
archived as part of a "real" lab notebook.
The disadvantage of a notebook entry is that it can only contain links
to "data" -- things like kinemages and charts. Trying to link to an
"active" page in the site via makeEventURL()
creates a link that will be stale and unusable by the time anyone
actually sees it. It's fairly rare that you need to do this, although
you can imagine (for example) the results from running Reduce having a
link that suggests running SSWING too. This sort of thing has to be
done from the delegate page that displays results (see the next
section). Most of the time, though, the results page can just display
the lab notebook entry that was created by the background job, which is
the approach we'll take in the next section.
First, though, we have to create a notebook entry. It's quite simple:
just build up a string containing data and HTML formatting, like you
would embed in a <BODY> or <DIV> tag somewhere in a page.
Notice the linkKinemage()
convenience function for generating a kin-viewing link. Then call addLabbookEntry() with a title,
body, etc. and save the resulting entry ID so that the results page can
look it up and display it. Here we save the ID as $_SESSION['bgjob']['labbookEntry'].
MolProbity's machinery for background jobs will automatically make this
available as $context['labbookEntry']
in the display() function
of the results page. Notice that all these machine-generated notebook
entries should be marked with the "auto" keyword, to distinguish them
from entries created de novo
by the user.
The last few lines of boilerplate code are very important, but don't
need any modification. They mark the background job as complete and
record the total running time. Nothing should come after them -- they
should be the last lines in the script. Once the script exits, job_progress.php will notice
the background job is finished and control will be transferred to the
results page delegate.
Results page
The results page is very, very simple, because there are lots of
jobs already written that just need to display a lab notebook entry as
their results. All those jobs use generic_done.php
as the results page.
This introduces the $context
variable for delegate pages. $context is a place to put data
that configures how a page looks, or to save data between views of this
same page -- sort of like member variable in an object. In fact,
that's how I should have implemented it, but I didn't, and it's too
scary to change it now, because they could collide with other member
variables. Anyway, the setContext()
and getContext()
functions can be used to retrieve and replace this data. The setContext() function can
either be passed the whole context as an array, or a key and value to
be updated. That is,
setContext("foo", "bar");
is a shortcut for
$ctx =
getContext();
$ctx["foo"] =
"bar";
setContext($ctx);
Context persists while a delegate is on the stack. That is, it
will persist through a pageCall()
to a subroutine and the corresponding pageReturn(), but it will
disappear when this delegate
does a pageReturn() or a pageGoto(). Context gets
used for things like flags that determine whether the "advanced
options" are visible or hidden, or for recording which folders are open
and which are closed in the file browser page.
Notice that the data from $_SESSION['bgjob']
has been transferred to this page's $context, so that it's easy to
retrieve the lab notebook ID number. That number can be used with openLabbbook() and formatLabbookEntry() to easily
retrieve and display the entry.
Two event handlers are set up on this page: a button (form) to pageReturn() out of the Prekin
loop, back to wherever we came from; and a link that will pageCall() the notebook
editting page.
Launching the background job
Now it's time to go back to pages/makekin_setup.php,
and make sure that when the user presses the "Make kinemage" button,
something actually happens. This involves three steps:
- Pass all the data the user submitted in the HTML form (in $_REQUEST) to the background
job, by
storing that data in the $_SESSION['bgjob']
variable. In this case, there are only two pieces of data (which model
to act on, and which script to run) but a complicated input page might
provide tens of input fields that need to be made available to the
background job.
- Log a message about what's happening to the system log using mpLog(). While the lab notebook
serves as a log of sorts for the user, mpLog() records short messages
in the system log. The system log can then later be parsed to provide
usage statistics for the MolProbity administrators. The log file has
one record per line with colon-delimited fields, so the proper message
format is a short unique identifier (in this case, "makekin") followed
by a colon and a longer, human-readable explanation of what's
happening, possibly with additional information about which options or
files were used.
- Use pageGoto() to
transfer control to job_progress.php,
which will display the progress messages while the background job is
running. This doesn't take affect until we exit onRunPrekin(), so you still
have time to launch the background job with launchBackground(). It's very
important that pageGoto()
be called before launchBackground(),
because the later freezes the session data. In fact, as a rule of thumb
nothing should be done after calling launchBackground(); it should
be the last call in the event handler.
That's it. Now when the user pushes the button on the set-up page, the
background job script will be started and the progress page displayed.
When the background job finishes, control will pass to the results
page, which will display the HTML notebook entry created by the
background job.
Denouement
Your script is finished -- now all that remains is to test and debug
it. Here are some hints:
- PHP errors in the background script are particularly nasty
because the error messages don't show up anywhere obvious. Use the
"View & download file" page to view system/errors, which should
have (most) the errors generated by any programs run in the background.
- Use php -l ("L" for
"lint") to check your scripts for syntax errors before trying to run
them. This is especially useful for background job scripts.
- You can also use the download page to view the contents of
kinemage files, which sometimes contain error messages instead of data
when something goes wrong.
- Make sure your variable names are spelled correctly. I'm always
mistyping variable names and then wondering why my variable doesn't
have any data in it. This can happen when you copy and paste, too.
- Ditto for making sure the name
fields in your HTML forms actually match up with the variable names
you're looking for in $_REQUEST
or in $_SESSION['bgjob'].
- Make sure you've included the library/libraries you need using require_once(). Missing library
function definitions will crash your script.
- Document the inputs and outputs of your own library functions. In
detail. You won't remember later.
- Read the existing documentation, both in the code (comments) and
in the doc/ folder.
- When in doubt, look for something that already exists that's
similar to what you want. I've done the best I can with MolProbity's
architecture, but it's still complicated and difficult to remember. I
am constantly using the existing code as a reference manual.
The End