HTML Logo by World Wide Web Consortium (www.w3.org). Click to learn more about our commitment to accessibility and standards.

Moving forward with Composr

ocPortal has been relaunched as Composr CMS, which is now in beta. ocPortal 9 will be superseded by Composr 10.

Head over to compo.sr for our new site, and to our migration roadmap. Existing ocPortal member accounts have been mirrored.


ocPortal Tutorial: Making an addon (part 3)

Written by Chris Graham, ocProducts
Welcome to the third of our series of addon making tutorials. If you haven't yet read the prior tutorials then it's advisable that you do so before reading this one.


Showing an expanded users-online display in OCF

A similar feature to this was added to ocPortal 4, so this example is no longer usable. But it is still a good tool for learning.

Today we're going to make a modification to how OCF shows the online users at the bottom of the forum-view.
We're going to colour-code each user there against usergroup, and give usergroup colour-keys beneath.

This example is much more involved than the ones given in the previous tutorial. The previous addons were straight-forward as we were just adding new modular functionality into the ocPortal framework – now we are extending and changing existing behaviour. Right now I don't know how I'll do this, and my explanations will follow my train of logic. I've intentionally done this to help you to learn to think like a programmer like me thinks.

In order to get an idea of what to do we need to consider what we're influencing. To find this out I'll need to go and find the ocPortal code that handles the user-online functionality that we're changing. Right now I've just opened up my web browser to our ocportal.com forums and scrolled to the bottom. I see we have the text 'Users online:' there, near the list that we need to change. This doesn't help us find the code because this is a language string and thus not stored where the actual code is stored. If I can find some unique HTML I'll be able to find the template and then use the name of that template to find the PHP code (there are other ways to do it, but this way is what I am most comfortable doing). I'm opening up the HTML page source and searching for that 'Users online:' phrase, and I've found '<td class="ocf_stats_usersonline_2">'.

Next I do a file contents search in Windows Explorer, in the themes/default/templates directory for the string I found. The search gives me 'OCF_STATS.tpl'. I open up this in my editor as I'll probably need it. Now I want to know where this template is used in the PHP code, so I do another file search, this time in the ocPortal root directory, for all PHP files containing do_template('OCF_STATS'. This search reveals a match in sources/ocf_general.php, so I open that up in my editor and I use the editor search feature to go to the exact line, which turns out to be line 418.

We now have PHP code and a template, and these are probably all we need to be modifying. Because this is an addon, we should be overriding files rather than editing them directly, otherwise we wouldn't be able to revert, and upgrading ocPortal would be awkward because we wouldn't know what we had changed (everything old and new would be lumped together). Therefore I copy themes/default/templates/OCF_STATS.tpl to themes/default/templates_custom/OCF_STATS.tpl, and I copy the PHP function in sources/ocf_general.php that uses that template ('ocf_wrapper') into a new PHP file, sources_custom/ocf_general.php, between new <?php and ?> markers. Our sources_custom/ocf_general.php uses function-level overriding, which is much more effective because it reduces the chance of bug fixes to the original copy being non-effective. This is because less is overridden, and thus less verbatim-ocProducts-code is masked.

I remove the old non-overridden files from my editor, and open up my copies instead.

Now all we need to do is to read over ocPortal's 'ocf_wrapper' function and 'OCF_STATS' template to get an understanding of how they work. I can't explain all that here – you need to have built up your own code understanding so you can figure it out yourself. In fact, after reading the code I've found I'm also going to need to override the OCF_USER_MEMBER.tpl template, so I do for that what I did for OCF_STATS.tpl.

Knowing how to program, I now have written my code, with my modified files saved as following:

OCF_USER_MEMBER.tpl

Code

{+START,IF_ADJACENT,OCF_USER_MEMBER}, {+END}{+START,IF_PASSED,AT}<a {+START,IF_PASSED,COLOUR}style="color: {COLOUR}" {+END}title="{AT#}" href="{PROFILE_URL*}">{USERNAME*}</a>{+END}{+START,IF_NON_PASSED,AT}<a {+START,IF_PASSED,COLOUR}style="color: {COLOUR}" {+END}title="{!MEMBER}" href="{PROFILE_URL*}">{USERNAME*}</a>{+END}<span class="accessibility_hidden">, </span>

OCF_STATS.tpl

Code

<br />

<div class="box guid_{_GUID}">
   <div class="box_inner">
      <h4>{!_STATISTICS}</h4>

      <div class="ocf_stats_1">
         <table summary="{!MAP_TABLE}" cellpadding="0" cellspacing="0" class="map_table ocf_stats_2">
            <colgroup span="2">
               <col class="ocf_bottom_bar_left_column" />
               <col class="ocf_bottom_bar_right_column" />
            </colgroup>

            <tr>
               <th class="de_th ocf_column1 ocf_stats_usersonline_1">
                  <span class="field_name">{!USERS_ONLINE}:</span><br />
                  <span class="associated_link">[ <a href="{USERS_ONLINE_URL*}">{!DETAILS}</a> ]</span>
               </th>
               <td class="ocf_stats_usersonline_2">
                  {USERS_ONLINE}

                  <p>
                     {!USERGROUPS}:
                     {$SET,doing_first_group,1}
                     {+START,LOOP,GROUPS}{+START,IF,{$NOT,{$GET,doing_first_group}}}, {+END}<a style="color: {GCOLOUR*}" href="{$PAGE_LINK*,_SEARCH:groups:view:{GID}}">{GTITLE*}</a>{$SET,doing_first_group,0}{+END}
                  </p>
               </td>
            </tr>
            <tr>
               <th class="de_th ocf_column1 ocf_stats_main_1">
                  {!FORUM_STATISTICS}:
               </th>
               <td class="ocf_stats_main_2">
               {!FORUM_NUM_TOPICS,{NUM_TOPICS*}}, {!FORUM_NUM_POSTS,{NUM_POSTS*}}, {!FORUM_NUM_MEMBERS,{NUM_MEMBERS*}}<br />
               {!NEWEST_MEMBER,<a href="{NEWEST_MEMBER_PROFILE_URL*}">{NEWEST_MEMBER_USERNAME*}</a>}<br />
               {BIRTHDAYS}
               </td>
            </tr>
         </table>
      </div>
   </div>
</div>

<br />

ocf_general.php

Code

<?php

/**
 * Do the wrapper that fits around OCF module output.
 *
 * @param  tempcode   The title for the module output that we are wrapping.
 * @param  tempcode   The module output that we are wrapping.
 * @param  boolean    Whether to include the personal bar in the wrap.
 * @param  boolean    Whether to include statistics in the wrap.
 * @param  ?AUTO_LINK The forum to make the search link search under (NULL: Users own PT forum/unknown).
 * @return tempcode   The wrapped output.
 */
function ocf_wrapper($title,$content,$show_personal_bar=true,$show_stats=true,$forum_id=NULL)
{
   //removed-assert

   global $ZONE;
   $wide=(($ZONE['zone_wide']==1) || (get_param_integer('wide',get_param_integer('wide_high',0))==1));
   if (!$wide)
   {
      $show_personal_bar=false;
      $show_stats=false;
   }

   // Notifications
   if ((get_member()!=$GLOBALS['OCF_DRIVER']->get_guest_id()) && ((get_page_name()=='forumview') || (get_page_name()=='topicview')))
   {
      $cache_identifier=serialize(array(get_member()));
      $_notifications=NULL;
      if (get_option('is_on_block_cache')=='1') $_notifications=get_cache_entry('_new_pp',$cache_identifier);
      if (is_null($_notifications))
      {
         $unread_pps=ocf_get_pp_rows();
         $notifications=new ocp_tempcode();
         $num_unread_pps=0;
         foreach ($unread_pps as $unread_pp)
         {
            $by=$GLOBALS['OCF_DRIVER']->get_username($unread_pp['p_poster']);
            if (is_null($by)) $by=do_lang('UNKNOWN');
            $u_title=$unread_pp['t_cache_first_title'];
            if (is_null($unread_pp['t_forum_id']))
            {
               $type=do_lang(($unread_pp['t_cache_first_post_id']==$unread_pp['id'])?'NEW_PT_NOTIFICATION':'NEW_PP_NOTIFICATION');
               $num_unread_pps++;
               $reply_url=build_url(array('page'=>'topics','type'=>'new_post','id'=>$unread_pp['p_topic_id'],'quote'=>$unread_pp['id']),get_module_zone('topics'));
            } else
            {
               $type=do_lang('ADD_INLINE_PERSONAL_POST');
               if ($unread_pp['p_title']!='') $u_title=$unread_pp['p_title'];
               $reply_url=build_url(array('page'=>'topics','type'=>'new_post','id'=>$unread_pp['p_topic_id'],'quote'=>$unread_pp['id'],'intended_solely_for'=>$unread_pp['p_poster']),get_module_zone('topics'));
            }
            $time_raw=$unread_pp['p_time'];
            $time=do_timezoned_date($unread_pp['p_time']);
            $topic=$GLOBALS['OCF_DRIVER']->post_url($unread_pp['id'],NULL);
            $post=ocf_clean_post_for_tooltip(text_lookup_comcode($unread_pp['p_post'],$GLOBALS['FORUM_DB']));
            $description=$unread_pp['t_description'];
            if ($description!='') $description=' ('.$description.')';
            $profile_link=$GLOBALS['OCF_DRIVER']->member_profile_url($unread_pp['p_poster']);
            $redirect_url=get_self_url(true,true);
            $ignore_url=build_url(array('page'=>'topics','type'=>'mark_read_topic','id'=>$unread_pp['p_topic_id'],'redirect'=>$redirect_url),get_module_zone('topics'));
            $notifications->attach(do_template('OCF_NOTIFICATION',array('_GUID'=>'3b224ea3f4da2f8f869a505b9756970a','ID'=>strval($unread_pp['id']),'U_TITLE'=>$u_title,'IGNORE_URL'=>$ignore_url,'REPLY_URL'=>$reply_url,'TOPIC_URL'=>$topic,'POST'=>$post,'DESCRIPTION'=>$description,'TIME'=>$time,'TIME_RAW'=>strval($time_raw),'BY'=>$by,'PROFILE_LINK'=>$profile_link,'TYPE'=>$type)));
         }

         put_into_cache('_new_pp',60*60*24,$cache_identifier,array($notifications,$num_unread_pps));
      } else
      {
         list($notifications,$num_unread_pps)=$_notifications;
      }
   } else
   {
      $notifications=new ocp_tempcode();
      $num_unread_pps=0;
   }

   if ($show_personal_bar)
   {
      if (get_member()!=$GLOBALS['OCF_DRIVER']->get_guest_id()) // Logged in user
      {
         $member_info=ocf_read_in_member_profile(get_member(),true);

         $profile_url=$GLOBALS['OCF_DRIVER']->member_profile_url(get_member());

         $zone_chooser=get_zone_chooser(true);

         $_new_topics=$GLOBALS['FORUM_DB']->query('SELECT COUNT(*) AS mycnt FROM '.$GLOBALS['FORUM_DB']->get_table_prefix().'f_topics WHERE t_forum_id IS NOT NULL AND t_cache_first_time>'.(string)intval($member_info['last_visit_time']));
         $new_topics=$_new_topics[0]['mycnt'];
         $_new_posts=$GLOBALS['FORUM_DB']->query('SELECT COUNT(*) AS mycnt FROM '.$GLOBALS['FORUM_DB']->get_table_prefix().'f_posts WHERE p_cache_forum_id IS NOT NULL AND p_time>'.(string)intval($member_info['last_visit_time']));
         $new_posts=$_new_posts[0]['mycnt'];

         // Any unread PT-PPs?
         $pt_extra=($num_unread_pps==0)?'':do_lang('NUM_UNREAD',number_format($num_unread_pps));

         $private_topic_url=build_url(array('page'=>'members','type'=>'view','id'=>get_member()),get_module_zone('members'),NULL,true,false,false,'tab__pts')

         $head=do_template('OCF_MEMBER_BAR',array(
               '_GUID'=>'s3kdsadf0p3wsjlcfksdj',
               'AVATAR'=>array_key_exists('avatar',$member_info)?$member_info['avatar']:'',
               'PROFILE_URL'=>$profile_url,
               'USERNAME'=>$member_info['username'],
               'LOGOUT_URL'=>build_url(array('page'=>'login','type'=>'logout'),get_module_zone('login')),
               'NUM_POINTS_ADVANCE'=>array_key_exists('num_points_advance',$member_info)?number_format($member_info['num_points_advance']):do_lang('NA'),
               'NUM_POINTS'=>number_format($member_info['points']),
               'NUM_POSTS'=>number_format($member_info['posts']),
               'PRIMARY_GROUP'=>$member_info['primary_group_name'],
               'LAST_VISIT_DATE_RAW'=>strval($member_info['last_visit_time']),
               'LAST_VISIT_DATE'=>$member_info['last_visit_time_string'],
               'PRIVATE_TOPIC_URL'=>$private_topic_url,
               'NEW_POSTS_URL'=>build_url(array('page'=>'vforums','type'=>'misc'),get_module_zone('vforums')),
               'UNREAD_TOPICS_URL'=>build_url(array('page'=>'vforums','type'=>'unread'),get_module_zone('vforums')),
               'PT_EXTRA'=>$pt_extra,
               'NEW_TOPICS'=>number_format($new_topics),
               'NEW_POSTS'=>number_format($new_posts)
         ));

      } else // Guest
      {
         $_this_url=build_url(array('page'=>'_SELF'),'_SELF',NULL,true);
         $this_url=$_this_url->evaluate();
         $login_url=build_url(array('page'=>'login','type'=>'login','redirect'=>$this_url),get_module_zone('login'));
         $full_link=build_url(array('page'=>'login','type'=>'misc','redirect_passon'=>$this_url),get_module_zone('login'));
         $join_url=build_url(array('page'=>'join'),get_module_zone('join'));
         $head=do_template('OCF_GUEST_BAR',array('NAVIGATION'=>get_zone_chooser(true),'LOGIN_URL'=>$login_url,'JOIN_LINK'=>$join_url,'FULL_LINK'=>$full_link));
      }
   } else $head=new ocp_tempcode();

   if ($show_stats)
   {
      $stats=ocf_get_forums_stats();

      // Colours for various usergroup IDs -- we'll do 16, but we'll repeat 4 times in case we have up to 64 usergroups
      $all_colours=array('#FAA500','#FA0C00','#FA00F1','#5800FA','#0099FA','#00FAA5','#2BAB03','#7D7E04','#966038','#96384A','#963895','#4E3896','#386296','#389596','#389653','#859638');
      $all_colours=array_merge($all_colours,$all_colours,$all_colours,$all_colours);

      // Users online
      $users_online=new ocp_tempcode();
      $members=get_online_members();
      $members=collapse_2d_complexity('the_user','cache_username',$members);
      $guests=0;
      foreach ($members as $member=>$username)
      {
         if ($member==$GLOBALS['OCF_DRIVER']->get_guest_id())
         {
            $guests++;
            continue;
         }
         if (is_null($username)) continue;
         $url=$GLOBALS['OCF_DRIVER']->member_profile_url($member);
         $pgid=$GLOBALS['FORUM_DRIVER']->get_member_row_field($member,'m_primary_group');
         $users_online->attach(do_template('OCF_USER_MEMBER',array('_GUID'=>'a9cb1af2a04b14edd70749c944495bff','COLOUR'=>$all_colours[$pgid],'PROFILE_URL'=>$url,'USERNAME'=>$username)));
      }
      if ($guests!=0)
      {
         if (!$users_online->is_blank()) $users_online->attach(', ');
         $users_online->attach(do_lang('NUM_GUESTS',number_format($guests)));
      }

      // Birthdays
      $_birthdays=ocf_find_birthdays();
      $birthdays=new ocp_tempcode();
      foreach ($_birthdays as $_birthday)
      {
         $birthday=do_template('OCF_USER_MEMBER',array('_GUID'=>'a98959187d37d80e134d47db7e3a52fa','PROFILE_URL'=>$GLOBALS['OCF_DRIVER']->member_profile_url($_birthday['id']),'USERNAME'=>$_birthday['username']));
         if (array_key_exists('age',$_birthday)) $birthday->attach(' ('.$_birthday['age'].')');
         $birthdays->attach($birthday);
      }
      if (!$birthdays->is_blank()) $birthdays=do_template('OCF_BIRTHDAYS',array('_GUID'=>'03da2c0d46e76407d63bff22aac354bd','BIRTHDAYS'=>$birthdays));

      // Usergroup keys
      $groups=array();
      $all_groups=$GLOBALS['FORUM_DRIVER']->get_usergroup_list();
      foreach ($all_groups as $gid=>$gtitle)
      {
         if ($gid==db_get_first_id()) continue; // Throw out the first, guest usergroup
         $groups[]=array('GCOLOUR'=>$all_colours[$gid],'GID'=>strval($gid),'GTITLE'=>$gtitle);
      }

      $foot=do_template('OCF_STATS',array(
         '_GUID'=>'sdflkdlfd303frksdf',
         'NEWEST_MEMBER_PROFILE_URL'=>$GLOBALS['OCF_DRIVER']->member_profile_url($stats['newest_member_id']),
         'NEWEST_MEMBER_USERNAME'=>$stats['newest_member_username'],
         'NUM_MEMBERS'=>number_format($stats['num_members']),
         'NUM_TOPICS'=>number_format($stats['num_topics']),
         'NUM_POSTS'=>number_format($stats['num_posts']),
         'BIRTHDAYS'=>$birthdays,
         'USERS_ONLINE'=>$users_online,
         'USERS_ONLINE_URL'=>build_url(array('page'=>'onlinemembers'),get_module_zone('onlinemembers')),
         'GROUPS'=>$groups
      ));
   } else $foot=new ocp_tempcode();

   $wrap=do_template('OCF_WRAPPER',array('_GUID'=>'456c21db6c09ae260accfa4c2a59fce7','TITLE'=>$title,'NOTIFICATIONS'=>$notifications,'HEAD'=>$head,'FOOT'=>$foot,'CONTENT'=>$content));

   return $wrap;
}

?>

Thumbnail: Our functionality modification in action.

Our functionality modification in action.

As usual I won't explain all the code, but I will mention a few things:
  • The main PHP code doesn't have to do much more than define some colours and pass in a usergroup list. Almost all the code in my file is just code from the original ocf_wrapper function
  • I passed a new parameter into OCF_USER_MEMBER.tpl and used that to indicate the colour to display a member link with; however, I wrapped a Tempcode 'IF_PASSED' directive around the use of the parameter because the template is reused and won't always be given a colour in that way
  • I passed in an array into OCF_STATS.tpl, containing all the usergroup names and colours. I then processed the array using a Tempcode 'LOOP' directive that spat out comma-separated links. I used a common GET/SET trick so I could show commas between each usergroup without also suffering a leading/trailing comma
Don't worry that I keep pulling out clever things from a proverbial hat. There are a finite number of standard ocPortal programming 'tricks' so once you know them all you can stop learning.

Exercises

Gambling hook (line and sinker)

Create a new Point Store product based around gambling. Members pay a certain set price to do a gamble (e.g. 3 points), and then they receive a random number of points within a range (e.g. -20 to 20). Therefore the gambling odds are fair, but there's an 'administration fee' for it.

Point-store hooks are just files in sources/hooks/modules/pointstore. Use flagrant.php as an example, but yours should be much simpler. There are actually only three functions (methods, technically) that need to be in a Point Store hook:
  • 'init', which performs common initialisation for all steps (you can probably leave this empty)
  • 'info', which contains Tempcoded HTML which describes the product and provides a link
  • a function that has the same name as whatever 'type' parameter you used in the aforementioned link; this either is the first in a chain of input and confirmation and delivery, or for a simple case like this, you can just do the actual gamble delivery in this single function.

Be imaginative

Make a feature change of your own design, and release it as an addon.

Points challenge

125 ocPortal.com points will be given to any user that releases a working ocPortal addon (which may be based off pre-existing Open Source code, made by anybody) so long as the addon contains at least 1000 lines of PHP code.

To claim your prize, post in the Addons forum and 'report post' with the phrase '125 points please' in your report.

See also