Example: Task List, Part 1
Let's build a simple task list application with the following features:
- Home page with basic site info, and links for users to login or register
- Simple user registration with name, email, and password
- Profile page to edit their info
- Control access based on login to pages on the site and page contents.
- Ability for users to designate other users as "friends" by entering their email address.
- Ability to create task lists, and share those lists with friends
- Ability to add/update/remove/mark as "complete" individual tasks on a task list
- Do all of this either on a desktop or a web-enabled phone
That sounds pretty ambitious, but let's give it a try.
Create Application
One way to create a Knickers application is to create your database tables first (using whatever database tools you use), then run the create_appskel utility. However, as that was covered in another example, we'll try a different method here, which is to use the Knickers table builder.
To use the table builder, we still need the application skeleton to get us started. Let's call our application "tasklist", and we want to name our application "myapp" and we want it to reside in /home/yourname.
(Note: If you have not already created the database itself, you need to do that before continuing.)
cd /home/yourname [KNICKERS_ROOT]/scripts/create_appskel.php tasklist...where [KNICKERS_ROOT] is the path to your Knickers installation.
You will be prompted for database connection information; fill that in. Then, assuming you started without a users table in your database, you will be given the opportunity to setup a basic "users" table; just follow the defaults.
After the users table (and associated classes) are created, it will ask if you want to setup login management; again, just follow the defaults.
Once this is complete, point your web server Document Root at /home/yourname/tasklist/common directory (if this step is a mystery, see Setting Up A Web Server). When you view that directory in your web browser, you should see something that looks like this:
Users and Login
Nice to see there is a place to login, but we need to create a user first!
One way to get to the user creation page is to click on the Admin link, then Users, then create new. However, it would be better to have this link readily accessible from the front page, so crack open common/tpl/html/outer.tpl and add (to the "nav-links" section):
<li><a href='{SYS_REL_WEB_PATH}UserAdmin'>Register</a></li>
(That SYS_REL_WEB_PATH tag helps with relative paths when your site is in a subdirectory.)
Once you add this link and click on it, you'll notice there is only one Password field; normally when you want to create or update a user account, you want to provide a "confirm password" field, because the user can't see what they are typing. Fortunately, the Knickers password component understands how to deal with this situation -- you just have to add the field. Open up common/tpl/html/Panel_UserAdmin.tpl and add this after the div containing the main Password field:
<div> <label for='{USER.PASSWORD.DOMID}'>Confirm Password:</label> {USER.PASSWORD.ERROR} {USER.PASSWORD.CONFIRM} </div>
You may want to switch the order of the Password and Email divs in the form so than the Confirm Password input shows up under the Password input.
Now use this form to create a user for yourself. Once completed, use inputs at the bottom of the page to login to your site; if all goes well, you should see "Logged in as " at the bottom of the page rather than the login inputs. Success!
Add Lists and Tasks
Now let's create the tables and files for lists and tasks. There are lots of properties lists and tasks could have, but we're going to try to keep this as simple as possible for the moment. We'll add other functionality later as we need it.
- Lists have a name, and know who created them
- Tasks have a description and know which list they belong to
First, let's set up our lists table and then run the class builder.
cd /home/yourname/tasklist [KNICKERS_ROOT]/scripts/create_table.php lists name:Text creator_id:Integer [KNICKERS_ROOT]/scripts/class_builder.php lists
When the class builder runs into creator_id, it will (correctly) think that this field is to store an ID of a relative, but as we do not have any models ("Things") named "creator", it will pause to ask for help.
We tried to automatically set up a relationship for the field 'CREATOR_ID', but it appears that the Thing (model) for that field does not exist.
We need to tell it that creator is a "user", so answer "n" to the prompt, and then enter Thing_User at the following prompt when it asks for the name of the related Thing.
When class builder is done, it will create several files for you that we will look at shortly. First, however, we will add the files for "tasks". An option to create_table is to run class_builder after the table has been created, so we'll save ourselves a step:
cd /home/yourname/tasklist [KNICKERS_ROOT]/scripts/create_table.php --class-builder tasks list_id:Integer description:Text
...and just accept all of the defaults.
Now that we have the basic files set up for lists and tasks, lets use them to create a basic list and task. On your site, go to Admin, then Lists, and click create new. Fill in the name field with "My First List", select a creator, and click Create List.
Then, go to Admin, then Tasks, and click create new. Now select "My First List" from the list drop down, add a description of "Get some milk", and click Create Task. Woohoo! You now have a list with a task!
Obviously there are a number of issues to address at this point, like making some of the fields required, and setting up an interface that has list and task management on the same page, but its a good start!
Who can see the list?
Right now, these lists are wide open; anyone in the world can see them. We only want specific users to be able to see a given list.
More specifically, we really only want logged-in users to have access to most of the site, and when we are looking at lists, we want those users to only be able to see the lists they created.
Site-wide permissions
So first, lets limit access across the site to logged-in users. The "Perm" class is our friend here, because every Panel passes through Perm. Create a file called /home/yourname/tasklist/common/cls/Perm.class.php that looks like this:
<?php /** * Extension of Perm specific to our app * *@author Your Name <you@you.com> */ define('PERM_CLASS_DEFINED', TRUE); //@const boolean Since Perm is one of Knickers' // special core classes, we need to tell it that we are extending Knickers_Perm, // so the base class doesn't create its own Perm extension. require_once KNICKERS_ROOT.'/common/cls/Perm.class.php'; // some constants we will use in our Panels to tell Perm // what kind of permissions they should have define('PANEL_VIEWABLE_BY_ALL', 1); //@const int Panel is viewable by anyone define('PANEL_VIEWABLE_BY_LOGGED_IN', 2); //@const int Panel is viewable by anyone logged in class Perm extends Knickers_Perm { var $panelViewableBy = PANEL_VIEWABLE_BY_LOGGED_IN; /** * The Perm object may need to be configured based on individual Panels * so that it can make appropriate decisions. * Normally a Panel calls this for each of its child panels in isAvailable(). * *@access public *@param Panel *@return void *@throws Error_CDMJ */ function configureForPanel(&$panel) { // if our panel has specified who can view it, // use that instead of our default if($panel->viewableBy) $this->panelViewableBy = $panel->viewableBy; } /** * Typically a Perm object has many individual permissions * that can be checked, but this is a general "am I allowed to be here" * call that can be made that might be based on a Panel configuration * -- typically called from Panel's isAvailable() * *@access public *@return boolean *@throws Error_CDMJ */ function isAvailable() { if($this->panelViewableBy < PANEL_VIEWABLE_BY_LOGGED_IN) return TRUE; if($this->lm->isViewerLoggedIn()) return TRUE; return FALSE; } } ?>
Oops, now I can't see anything!
As soon as you do this, if you are not logged in, you will not be able to access any page on the site (if you are still logged in from the earlier steps, click the "Logout" link at the bottom of your site to see the effect). Since we want anyone to be able to see the home page, we need to designate that in common/cls/Controller/Panel_Index.class.php by adding the following member variable within the class definition:
var $viewableBy = PANEL_VIEWABLE_BY_ALL; //@field public Who can see us?
... and now if you return to your site, you should be able to see the home page again. To view any other page on the site, you will need to login again (if you had logged out).
Note! You should also add the same viewableBy member variable to common/cls/Controller/Panel_UserAdmin.class.php, or else unregistered users will not be able to sign up!
Only show me my lists
First, when a list gets created through the interface, we want the appropriate user set in as the list's creator by default. To do that, open up common/cls/Component/ComponentFinder_List.class.php and add the following:
/** * Need to set in the current user as the "creator" * *@access public *@return void */ function postSetCFDPAction() { if($this->CFDP->isA('Storable') && $this->CFDP->isNew()) $this->CFDP->set('CREATOR', $this->user); }
You might ask, "why are we adding this function to the ComponentFinder instead of the Thing?". The reason is that the ComponentFinder, which is an interface-level wrapper for Thing, knows the current context (user, locale, etc), whereas Thing does not.
At this point, you should remove the creator selector div from common/tpl/html/Panel_ListAdmin.tpl since it will be set automatically.
Now, when viewing the list of lists at this point, we want users to only be able to see the lists that they created.
Search pages in Knickers -- those Panels that descend from Panel_SFS (Search, Filter, Sort) -- have the ability to set in "fixed" filters that the user can not modify from the interface. In this case, we want to modify common/cls/Controller/Panel_Lists.class.php to add this function:
/** * Filter the list by creator is the viewer * *@access protected *@return void */ function init() { parent::init(); $this->fixedFilterSet = new FilterSet(new Filter('CREATOR_ID', '=', $this->user->getID())); }
As we are forcing the creator to be the current user, we should remove the creator selector div from common/tpl/html/Panel_Lists.tpl.
Another thing we might want to do is make sure that tasks can only be seen by the owner of the list...but that's for another day.
What about our registration page?
The registration page also doubles as our user profile page; there are a couple permissions-related issues to consider here;
- You have to be able to see the page if you are not logged in to be able to register, so add the same line to cls/Controller/Panel_UserAdmin.class.php that you added to Panel_Index.class.php above.
- If you're not creating a new user, we want to make sure you are only able to look at your own account. Panels that descend from Panel_VEC (View, Edit, Create) know the ID of the record being looked at (it is determined in Panel_VEC's init() function if you want to see how it works); so open cls/Controller/Panel_UserAdmin.class.php, and add:
/** * Make sure we are only looking at our own profile * *@access protected *@return void */ function isAvailable() { if($this->id && $this->id != $this->user->getID()) return FALSE; return TRUE; }
Modify the navigation links
Since we've made most of the site not accessible if you are not logged in, it doesn't make sense to show that "Admin" link to viewers that are not logged in. Additionally, it doesn't really make sense to show our Register link to someone that is logged in, as they already have an account. One way to deal with this is to conditionally vanish these links. In common/tpl/html/outer.tpl, modify your Admin and Register links to look like this:
{LOGINMANAGER.VIEWER_LOGGED_IN?:|VANISH|} <a class='admin' href='{SYS_REL_WEB_PATH}Admin'><span>{|LABEL|ADMIN}</span></a> {/LOGINMANAGER.VIEWER_LOGGED_IN?:|VANISH|}
and
{LOGINMANAGER.VIEWER_LOGGED_IN?|VANISH|} <a href='{SYS_REL_WEB_PATH}UserAdmin'>Register</a> {/LOGINMANAGER.VIEWER_LOGGED_IN?|VANISH|}
What's that {|LABEL|ADMIN} tag? Knickers provides a translation mechanism; for more information on that, check out Text Handling.
What's next?
That's enough for this lesson; in the next chapter we'll talk about making a better List/Task interface.