Lost Flirts – a Joomla 1.5 component from scratch
Update: Paceville.com is now online to check out the interface go here.
Although I got a pretty good picture of how a Joomla component works when I did the extended user registration form it wasn’t really from scratch.
This time though it was time to try and create something from scratch, in a “proper” fashion. We need a Lost Flirts section which will basically be a simple web board where members can post short messages. Other members can then answer with direct messages.
Administrators need to be able to remove any post and the person who created a post needs to be able to remove it. There will however not be any admin interface for this one, the admin needs to be logged in to the front end to be able to remove flirts.
The whole interface will be contained on one page, first the posts at the top, then pagination controls and at the bottom the form. If a person is allowed to remove a post we will show a delete link to the right in each post. Note that the picture to the right is the designer’s take on what the interface should look like. Currently unregistered people won’t be able to use Lost Flirts, that will simplify the form a lot compared to what you see in the image.
Lets’ talk structure, we will have an entry point called lostflirts.php in components/com_lostflirts, this script is currently only loading the controller and will have the controller call a default task if none has been specified in the GET global array.
The controller and table is where everything is happening, the controller will in turn instantiate a view which renders the template, take a look at image to the right to see how everything is structured at the moment. This is kind of the default structure, very little configuration is needed to make stuff happen.
The table script tables/lostflirts.php is used as much as possible to separate concerns and to make as much use of the JTable class as possible.
Note that this tutorial will not go through any kind of packaging, prior to creating all the folders/scripts manually in the components directory I simply ran the following SQL:
CREATE TABLE IF NOT EXISTS `jos_lostflirts` (
`id` int(21) NOT NULL auto_increment,
`userid` int(21) NOT NULL,
`message` text NOT NULL,
`date` timestamp NOT NULL default CURRENT_TIMESTAMP,
`title` varchar(255) NOT NULL,
`flirt_date` date NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8
INSERT INTO `jos_components` (`name`, `link`, `menuid`, `parent`, `admin_menu_link`, `admin_menu_alt`, `option`, `ordering`, `admin_menu_img`, `iscore`, `params`, `enabled`) VALUES
('Lost Flirts', 'option=com_lostflirts', 0, 0, 'option=com_lostflirts', 'Lost Flirts', 'com_lostflirts', 0, 'js/ThemeOffice/component.png', 0, '', 1);
For more on packaging see here.
Let’s begin with the entry point lostflirts.php:
defined('_JEXEC') or die('Restricted access');
require_once (JPATH_COMPONENT.DS.'controller.php');
$controller = new LostFlirtsController();
$controller->registerDefaultTask('listFlirts');
$controller->execute( JRequest::getCmd('task'));
$controller->redirect();
Not much to add here in addition to what I’ve already stated above, we will render the task listFlirts if the $_GET[‘task’] variable is empty.
Let’s move on to controller.php’s complete listing, discussion of each function is below:
defined('_JEXEC') or die();
jimport('joomla.application.component.controller');
class LostFlirtsController extends JController{
function __construct(){
parent::__construct();
$this->user =& JFactory::getUser();
JTable::addIncludePath(JPATH_COMPONENT . DS . 'tables');
}
function redirListAll(){ $this->setRedirect(JRoute::_('index.php?option=com_lostflirts')); }
function getTable(){ return JTable::getInstance('LostFlirts', 'Table'); }
function create(){
$post = JRequest::get('post');
$row =& $this->getTable();
if (!$row->bind($post))
return JError::raiseWarning(500, $row->getError());
$row->userid = $this->user->id;
$row->flirt_date = "{$post['year']}-{$post['month']}-{$post['day']}";
if (!$row->store())
return JError::raiseWarning(500, $row->getError());
$this->redirListAll();
}
function getLostFlirt($id){
$tbl = $this->getTable();
$tbl->load($id);
return $tbl;
}
function delete(){
$id = JRequest::getVar( 'id', 0, 'get', 'int' );
if($this->isAdmin() || $this->user->id == $this->getLostFlirt($id)->userid)
$this->getTable()->delete($id);
$this->redirListAll();
}
function isAdmin(){
return in_array($this->user->usertype, array('Super Administrator', 'Administrator', 'Manager', 'Author'));
}
function isRegistered(){ return $this->user->usertype == 'Registered' || $this->isAdmin(); }
function listFlirts(){
$doc = JFactory::getDocument();
$doc->addScript("components/com_community/assets/window-1.0.pack.js");
$doc->addScript("components/com_community/assets/script-1.2.pack.js");
$get = JRequest::get('get');
$view =& $this->getView('listflirts', 'html');
$limit = 10;
$offset = empty($get['offset']) ? 0 : $get['offset'];
$view->assign("rows", $this->getTable()->getLostFlirtsList($offset, $limit));
$view->assign("admin", $this->isAdmin());
$view->assign("registered", $this->isRegistered());
$view->assign("cur_usr_id", $this->user->id);
$view->assign('pagination', $this->getPagination( $limit ));
$view->display();
}
function getPagination($per_page){
$get = JRequest::get('get');
$total = $this->getTable()->getFlirtCount();
if($total < $per_page)
return array();
$page_num = ceil($total / $per_page);
$result = array();
for($i = 0; $i < $page_num; $i++){
$cur = array();
$offset = round($i * $per_page);
$cur['offset'] = $offset;
$cur['limit'] = $per_page;
$cur['current'] = $offset == $get['offset'];
$result[] = $cur;
}
return $result;
}
}
Let’s discuss each function from top to bottom:
__construct: The constructor, we assign the currently logged in user to a member variable because it will be used a lot, we also include our table/model whose code we will get to further down.
redirListAll: Convenience function to redirect to the default task/function which is listFlirts.
getTable: Will instantiate our table.
create: Is called everytime the form is submitted, we do the customary bind() call but we will also set the userid field to the currently logged in user’s id. We will also set the posted flirt date to a normal MySQL date, with the help of the contents of the date dropdowns.
getLostFlirt: Will simply get a lost flirt row with the help of the passed argument.
delete: Will delete a lost flirt with the help of an id, will redirect to the full lost flirts listing. Note that only the owner of the flirt, or someone more powerful than just registered can do this.
isAdmin: Simple ACL function to check if we have something above Guest and Registered.
isRegistered: Simple check to see if the currently logged in user is Registered or higher.
listFlirts: The main function that always renders the GUI. First we get the current document, we then add some JomSocial JavaScripts that are needed in order for the direct messaging to work. We get the first 10 flirts and assign them to “rows” in the view, assign a boolean to the “admin” variable, if we’re above Registered it will be true, assign true to “registered” if we are have registered member. Finally we assign the pagination controls.
getPagination: Will get the pagination controls with the help of the total flirt count, current offset and limit/per page count (10). The current pagination is very simple, there are no page groups, only pages. Hopefully this will be enough since the content displayed is all about freshness, older flirts should be deleted by for instance a cron job, hence there will not be any need for page groups and such.
Let’s move over to the tables/lostflirts.php script:
defined( '_JEXEC' ) or die( 'Restricted access' );
class TableLostFlirts extends JTable{
var $id = null;
var $message = null;
var $date = null;
var $userid = null;
var $flirt_date = null;
var $title = null;
function __construct(&$db){
parent::__construct( '#__lostflirts', 'id', $db );
}
function getFlirtCount(){
$sql = "SELECT DISTINCT COUNT(*) FROM #__users u, #__lostflirts f, #__community_users cu
WHERE f.userid = u.id AND f.userid = cu.userid AND u.id = cu.userid";
$this->_db->setQuery($sql);
return $this->_db->loadResult();
}
function getLostFlirtsList($offset, $limit){
$sql = "SELECT DISTINCT * FROM #__users u, #__lostflirts f, #__community_users cu
WHERE f.userid = u.id AND f.userid = cu.userid AND u.id = cu.userid
GROUP BY f.id ORDER by f.date LIMIT $offset, $limit";
$this->_db->setQuery($sql);
return $this->_db->loadObjectList();
}
}
Not much to add here, the only additional info we’ve added to what we already have in JTable is getFlirtCount and getLostFlirtsList which is doing exactly what their names imply.
That was quick, let’s take a look at the view.html.php script:
defined( '_JEXEC' ) or die( 'Restricted access' );
jimport( 'joomla.application.component.view');
class lostflirtsViewlistFlirts extends JView{
function display($tpl = null){
parent::display($tpl);
}
function prettyDate($date){
$stamp = strtotime($date);
return date("j M, Y", $stamp);
}
function printArrToOptions(&$arr, $selected){
foreach($arr as $item){
$item = is_int($item) ? str_pad($item, 2, "0", STR_PAD_LEFT) : $item;
if($item == $selected)
echo "<option value=\"$item\" selected=\"selected\">$item</option>";
else
echo "<option value=\"$item\">$item</option>";
}
}
function yearOptions($selected){
$this_year = date('Y');
$years = range($this_year - 2, $this_year);
$selected = empty($selected) ? $this_year : $selected;
$this->printArrToOptions($years, $selected);
}
function monthOptions($selected){
$this_month = date('m');
$selected = empty($selected) ? $this_month : $selected;
$months = range('01', '12');
$this->printArrToOptions($months, $selected);
}
function dayOptions($selected){
$this_day = date('d');
$selected = empty($selected) ? $this_day : $selected;
$days = range('01', '31');
$this->printArrToOptions($days, $selected);
}
}
We’ve got some helper functions here, this stuff should really be in some library instead of directly in the view. Better than in the template anyway 😉
You might also recognize this stuff from the extended user form. Anyway, the above functions are used to print the date drop downs where we have year, month and day of month.
Over to the template in tmpl/default.php:
<?php // no direct access
defined('_JEXEC') or die('Restricted access');
?>
<?php foreach($this->rows as $usr): ?>
<div class="flirt_cont">
<img class="float_left" src="<?php echo $usr->avatar ?>" />
<h1><?php echo $usr->title ?></h1>
<div class="float_right">
<?php if($this->registered && $this->cur_usr_id != $usr->userid): ?>
<a href="javascript:void(0);" onclick="joms.messaging.loadComposeWindow('<?php echo $usr->userid ?>')">Reply</a>
<?php endif ?>
<?php if($this->admin || $usr->userid == $this->cur_usr_id): ?>
<p>
<a href="<?php echo JRoute::_("index.php?option=com_lostflirts&task=delete&id={$usr->id}") ?>">Delete</a>
</p>
<?php endif ?>
</div>
<p class="grey">We got together on <?php echo $this->prettyDate($usr->flirt_date) ?></p>
<p><?php echo $usr->message ?></p>
<div class="flirt_line"></div>
</div>
<?php endforeach; ?>
<div class="flirt_cont">
<div class="pagination_ctrl">
<?php foreach($this->pagination as $num => $page): ?>
<div class="pagination_<?php echo $page['current'] ? 'current' : 'normal' ?>">
<a href="<?php echo JRoute::_("index.php?option=com_lostflirts&offset={$page['offset']}&limit={$page['limit']}") ?>">
<?php echo $num + 1 ?>
</a>
</div>
<?php endforeach ?>
</div>
<br/>
<br/>
<br/>
<span class="super_headline">Are you missing that special one?</span>
<br/>
<?php if($this->registered): ?>
<script>
function postLostFlirt(){
if(jQuery("#lostflirt_message").val().length < 5 || jQuery("#lostflirt_title").val().length < 5)
jQuery("#lostflirt_errors").show("slow");
else
jQuery("#lostflirts_form").submit();
return false;
}
</script>
<div id="lostflirt_errors" class="hide pink">
The title and/or message field is empty, or almost empty, write some more please.
</div>
<br/>
<form id="lostflirts_form" method="post" action="<?php echo JRoute::_("index.php?option=com_lostflirts&task=create") ?>">
<table>
<tr>
<td style="vertical-align: bottom;">
<span class="pink">*</span> Date:
</td>
<td>
Day:
<select name="day" style="width:100px;">
<?php echo $this->dayOptions() ?>
</select>
<span style="margin-left: 40px;">Month:</span>
<select name="month" style="width:100px;">
<?php echo $this->monthOptions() ?>
</select>
<span style="margin-left: 40px;">Year:</span>
<select name="year" style="width:100px;">
<?php echo $this->yearOptions() ?>
</select>
</td>
</tr>
<tr>
<td style="vertical-align: bottom;">
<span class="pink">*</span> Title:
</td>
<td>
<input id="lostflirt_title" type="text" class="inputbox" name="title" value="" style="width:500px;"/>
</td>
</tr>
<tr>
<td style="vertical-align: top;">
<br/>
<span class="pink">*</span> Message:
</td>
<td>
<textarea id="lostflirt_message" name="message" rows="40" style="width:500px;"></textarea>
</td>
</tr>
<tr>
<td></td>
<td>
<button class="btn_purple" onclick="postLostFlirt(); return false;">Submit</button>
</td>
</tr>
</table>
</form>
<?php else: ?>
<p>
<a href="<?php echo CRoute::_( 'index.php?option=com_community&view=register' , false ); ?>">Sign up</a> for an account and find your lost flirt!
</p>
<?php endif ?>
<br/>
<br/>
<br/>
</div>
We loop through all the flirts and output them, we render the pagination controls and finally we render the create form. Note all the tests for displaying various controls, tests that are also repeated in the controller.
Note also the minimal JavaScript validation we perform with the help of jQuery, simply to avoid empty, or near empty, values.