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:
  1. 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.
  2. Add a link to this delegate page from one or more of the existing pages in MolProbity.
  3. 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.
  4. 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:
  1. 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.
  2. 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:
  1. 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.
  2. 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.
  3. 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:

The End