To get you started, here's a basic explanation of the structure of the created module.
The language files
In the lang/ folder, you will find one file for each supported language. Inside each file, you should have several lines that look like this:
$lang["postinstall"] = "Module successfully added.";
This line defines a string referred to as "postinstall". In every language file, there should be a string for this key. When the modules wishes to display some text, it will look for the string corresponding to the key in the language file corresponding to the user's language. You will most likely wish to change these files, but be careful. Each string is delimited with quotes (either ' or "), and obviously you can't use these delimiters inside the string. If you wish to do so, you'll have to use an escape sign : \" will display a quote without being considered as a delimiter.
The templates
The
files in the templates/ folder contain no php - just plain html and
smarty tags. They are used for the layout of most actions.
adminpanel.tpl is the layout for the list of elements of each level in the admin panel (to modify this, see this topic)
noresult.tpl is used when there are no item to display. Typically, this is just a message saying that nothing was found.
search.tpl
is the template for the simple search form (when calling the search
action with searchmode="simple"), which searches in all levels.
search_generalresults.tpl
is the template for the search results when you are searching in more
than one level (otherwise, the default list template for that level is
used).
browsefiles.tpl is the template used for the file selection page (when using table display).
For each level, you should have a editLEVELPREFIX.tpl, a search_LEVELNAME.tpl and possibly a frontend_add_LEVELPREFIX.tpl.
The first is the template for the admin form, when an item of this
level is added or edited. The second is the template for the advanced
search form. The last is the template for the frontend add action for
this level (if you chose to create one).
The action files
In the root of the module folders, you will notice that files have a prefix (either "action" or "method"), except for the module file (modulename.module.php), which contains the most important parts of the code. Methods are called when the module is installed, uninstalled, or upgraded - and at no other moment. Actions, on the other hand, are called everytime the cms asks the module to do something - either in the frontend or the admin. But the module has more actions than the files you see there. Open the module file (modulename.module.php), and look for the function DoAction (should be around the 400th line). You should see something like this:
function DoAction($action, $id, $params, $returnid=-1){
global $gCms;
switch($action){
case "link":
echo $this->CreateLink($id,"default",$returnid,"",$params,"",true);
break;
case "changedeftemplates":
foreach($params as $key=>$value){
if($key != "submit") $this->setPreference($key, $value);
}
$this->Redirect($id, "defaultadmin", $returnid, array("active_tab"=>"templates", "module_message"=>"message_modified"));
break;
case "deletetpl":
$newparams = array("active_tab"=>"templates");
$deftemplates = $this->getDefaultTemplates();
if(isset($params["tplname"]) && !in_array($params["tplname"], $deftemplates)){
if($this->DeleteTemplate($params["tplname"])) $newparams["module_message>"] = $this->lang("message_modified");
}
$this->Redirect($id, "defaultadmin", $returnid, $newparams);
break;
and so on...
Some actions (mostly because they are small) don't have their own file.
Each "case", here, is such an action. When asked for an action that is
not in the list, the module will look for the according file
(action.ACTIONNAME.php).
So here's a short description of the actions you might notice in your module:
The functions
Aside from DoAction, the module file contains a lot of functions that are used in any action. Most of them are simply procedures that the module needs to do quite often. I've tried to comment them at least to say what they're there for, but here I'll try to describe some of the most important ones.
The get_level functions
Each get_level_NAMEOFLEVEL()
function queries elements from the database, creates an object for
each, and return an array of these objects (basically, the $itemlist
you're using in templates).
The function accepts several parameters, which I'll exlain here:
$where=array("active"=>1)
would mean that only the items were the "active" field is equal to 1 are queried.Hope this gets you going. If you have any question, or wish to share the modifications you've done, feel to contact me, either at the forge or on the forum.
Yes.
Let's say you have products which have a description field, and in that description you would like to put some smarty code. If you try and put, say, {title} in an attempt to display the page title, you should notice that "{title}" will be displayed exactly like that in the product's description. That is because items' fields aren't evaluated.
However, in your template, you may choose to evaluate the fields. For example, in your template, if instead of the simple {$item->description}
you write:
{eval var=$item->description}
You will then notice that {title} is evaluated, and the page's title is displayed.
You put {$item->imagefield} in your template, and the image doesn't show up...
That's pretty much normal. $item->imagefield is itself an object, that has several attributes:
To
display the image, you can use {$item->imagefield->image}, or if
you wish more options, you can create the image tag yourself:
<img src="uploads/{$item->imagefield->filepath}" alt=""/>
If you have a list field that allows multiple choices (checkboxes or select list) and simply use {$item->fieldname} or {$item->fieldname_namevalue} in your template, you'll probably end up with this being displayed:
Array
This is normal. As the field contains (or can contain) multiple values, it is indeed an array of values. To get the values themselves, you have to iterate through the values in a way like this:
<ul>
{foreach from=$item->fieldname_namevalue item="onevalue"}
<li>{$onevalue}</li>
{/foreach}
</ul>
Posted By: Solutic on Date: 2009-05-07 10:31
Use: {$item->yourVar|date_format:"%H:%M"}
See the smarty entry about date format modifier or the most useful smarty list of variable modifiers.
The function that generates the pagemenu is split_into_pages (in module file), but you should be able to format it the way you like without modifying the function.
Classes and CSS
The
pagemenu is in a div with class "pagination". Each page number is an
anchor(<a>) with class "pagenumber", and the current page also
has the class "current". The "..." shown when there are too many pages
are spans with the class "pagemenuoverflow", and the left and right
arrows are anchors respectively with classes "previouslink" and
"nextlink".
You may use these classes to style your page menu. You can find examples of styles here (I tried to use the same classes, but for uniformity reasons I didn't use spans, so double-check the css).
Strings
To change the page delimiter ("|") or the "...", look at the first
lines language files, respectively at the entries "pagemenudelimiter"
and "pagemenuoverflow".
As for the < and >, they are hard-coded in the function split_into_pages, but it shouldn't be too difficult to change.
We will look at how to create a list of sorted elements in the following way:
The basic idea will be to order elements by the sorting field, and notice when there's a change.
Order by parents
Create the following template (which I'll call "order_by_parents"):
<ul>
{assign var="parentflag" value=false}
{foreach from=$itemlist item="item"}
{if $item->parent_name != $parentflag}
{if $parentflag}</ul></li>{/if}
<li>{$item->parent_name}<ul>
{assign var="parentflag" value=$item->parent_name}
{/if}
<li>{$item->name}</li>
{/foreach}
</ul>
and call it using {cms_module module="mymodule" what="item" listtemplate="order_by_parents" orderby="parent"}
Order by field
Let's say your level has a dropdown field named "myfield", and you wish
to list elements sorted on that field. Create the following template
(which I'll call "order_by_myfield"):
<ul>
{assign var="fieldflag" value=false}
{foreach from=$itemlist item="item"}
{if $item->myfield != $fieldflag}
{if $fieldflag}</ul></li>{/if}
<li>{$item->myfield}<ul>
{assign var="fieldflag" value=$item->myfield}
{/if}
<li>{$item->name}</li>
{/foreach}
</ul>
and call it using {cms_module module="mymodule" what="item" listtemplate="order_by_myfield" orderby="myfield"}
If you're on the final level and there's only one item to show, the module won't display the list view but will immediately show the details of this item, so that the user doesn't have to click twice for no reason.
If you want to display the list view anyway, there is now a "forcelist" parameter that will do the job (see the help of your module). Your tag should become:
{cms_module module="products" what="product" forcelist="1"}
To set forcelist to true by default, look here.
Yes. In fact, the Comments module makes this very easy for any other module.
Open the detail template for your last level elements, and simply add this:
{cms_module module="comments" modulename="NAMEOFYOURMODULE" pageid=$item->id}
Where NAMEOFYOURMODULE is, of course, the name of your module.
[*** Thanks to ikulis, this was definitely fixed in 1.8.4.2 and shouldn't be a problem anymore ***]
The advanced search form would need to register a param for each field of each level, which I find rather inelegant. For that reason the RestrictUnknownParams option has been disabled.
If you are not using the advanced search form, you can safely activate the RestrictUnknownParams() option. Edit the module file (modulename.module.php), find the SetParameters() function, and edit the following line :
$this->RestrictUnknownParams(false);to:
$this->RestrictUnknownParams();
If you do use the advanced search, I would suggest disabling the "Allow parameter checks to create warning messages" in the CMS Global Settings.
In any module template, you can retrieve a whole item object using the following smarty tag: {MODULENAME_get_levelitem}
.
The
tag requires 3 parameters : what (retrieve an item from which level),
alias (the item's alias) and assign (in which smarty variable this
should be stored).
So a typical example would be:
{mymodule_get_levelitem what="category" alias="myfirstcategory" assign="catobject"}
And you can then use the assigned object to display information:
{$catobject->name}
Yes.
From CTLModuleMaker 1.8.3, it is possible to define which fields will be shown in the advanced search form upon field creation. If you have created your module with an earlier version or do not want to create it again, there's another way around.
The search form for the module's search action are generated from templates that can be found in the "templates" folder of your module.
The "search.tpl" file is the template for the search form when the "simple" searchmode is chosen.
The "search_generalresults.tpl" file is the template for the search results when we are searching in more than one level at the same time. Otherwise, the level's default list module is used.
For each level of your module, there should be a file named "search_NAMEOFTHELEVEL.tpl". This contains the template for the advanced search form for this level. If you want to remove a field input from the search form, you can simply delete the appropriate section of the template. (I would recommand to make a backup of the original template, if you ever change your mind!)
Yes.
The edit forms for each level are generated from templates that can be found in the "templates" folder of your module. For each level of your module, the template for the edit formre should be in a file named "editPREFIXOFTHELEVEL.tpl".
You can modify the template, but keep two things in mind:
- If you remove an input, the form might not work anymore.
- If you wish to change the field labels, you should preferably do so in the language file.
One of your level has, for example, a numeric field (which we'll call "myfield"), and you wish to make sure it's between, say, 0 and 100? Of course it's possible, but you'll need to edit the code.
Open the edit file associated with the level (action.editLEVELPREFIX.php). Somewhere not so far from the beginning of the file, you will find the following code :
// CHECK IF THE NEEDED VALUES ARE THERE
#1 if( !isset($params["itemname"]) || $params["itemname"] == "" ){
#2 echo $this->ShowErrors($this->Lang("error_missginvalue"));
#3 }elseif(false == $this->checkalias("module_modulename_item", $item->alias, isset($params["itemid"])?$params["itemid"]:false)){
#4 echo $this->ShowErrors($this->Lang("error_alreadyexists"));
#5 }else{
############ DOING THE UPDATE
Here two conditions are checked before updating/adding the element: first, on line #1, we check if a name was given (and any other mandatory value, if any), and if it wasn't, we display an error (line #2). On line #3, we check if another element already has the same alias and display an error message (line #4) if it is the case. If no problem is met, we get to line #5 and go on with the update.
Basically, you can add any condition you like to these. So with our example, it could look like this:
// CHECK IF THE NEEDED VALUES ARE THERE
if( !isset($params["itemname"]) || $params["itemname"] == "" ){
echo $this->ShowErrors($this->Lang("error_missginvalue"));
}elseif(false == $this->checkalias("module_modulename_item", $item->alias, isset($params["itemid"])?$params["itemid"]:false)){
echo $this->ShowErrors($this->Lang("error_alreadyexists"));
}elseif( false == ($item->myfield > 0 && $item->myfield <= 100) ){
echo $this->ShowErrors($this->Lang("my_custom_error_message"));
}else{
############ DOING THE UPDATE
We just check if the field is over 0 and below/equal to 100. If it is, we go on with the submission. Otherwise we display an error message (which you'd need to add to the language files, otherwise you can hardcode the error message...)
If
you open the /templates/ folder, you will notice that there's only one
template for the defaultadmin : adminpanel.tpl. This is used for all
levels. So the difference is the information passed to the template.
Before taking a look at it, let's see that information.
Open the action.defaultadmin.php of your module. You will notice that it is divided into tabs (delimited by echo $this->StartTab("nameoflevel");
and echo $this->EndTab();
). For each level tab, the list of its items is fetched and stored into $itemlist
, which is then passed to smarty. $itemlist is an array of objects having your fields as attributes ($item->field).
Just below that, you will notice lines that look like this:
$adminshow = array(
array($this->Lang("name"),"editlink",false),
array($this->Lang("alias"),"alias",false),
array($this->Lang("active"),"toggleactive",true),
array($this->Lang("nbchildren"),"nbchildren",false),
array($this->Lang("reorder"),"movelinks",true),
array($this->Lang("Actions"),"deletelink",true)
);
$this->smarty->assign("adminshow", $adminshow);
$this->smarty->assign("tableid", "levelname_table");
echo $this->ProcessTemplate("adminpanel.tpl");
The $adminshow
variable contains what should be shown in the adminpanel of that level.
We then pass this variable to smarty (and also a name for the table),
and the last line tells smarty to display the template "adminpanel.tpl".
$adminshow is an array, where each element represents a column of the
adminpanel. Each column, in turn, is an array containing three values:
the first (position 0) is the title of the column, and the second
(position 1) is the name of the object's field that should be shown in
this column, and the third (position 2) is whether the instant search
(the searchbox just at the top of the adminpanel) should skip this
column.
For example, array($this->Lang("name"),"editlink",false)
means, here, that we take $this->Lang("name") ("name" in the
language that is currently displayed) to be the title of the column,
and that for each row of the table, whatever is in
$oneitem->editlink will be displayed (this particular value is not
strictly speaking a field of the level, but an attribute created by the
addadminlinks() function). It also means that the instant search will not skip this column, so the text displayed will be used to see if the row matches the search words.
Now, let's take a look at the adminpanel.tpl file:
#1 <div>
#2 <table {if $tableid}id="{$tableid}" {/if}cellspacing="0" class="pagetable">
#3 <thead><tr>
#4 {foreach from=$adminshow item=column}
#5 <th>{$column[0]}</th>
#6 {/foreach}
#7 </tr></thead>
#8 <tbody>
#9 {cycle values="row2,row1" assign=rowclass reset=true}
#10 {foreach from=$itemlist item=oneitem}
#11 {cycle values="row2,row1" assign=rowclass}
#12 <tr class="{$rowclass}" onmouseover="this.className='{$rowclass}hover';" onmouseout="this.className='{$rowclass}';">
#13 {foreach from=$adminshow item=column}
#14 {assign var=oneval value=$column[1]}
#15 <td{if $column[2]} class="ctlmm_nosearch"{/if}>{$oneitem->$oneval}</td>
#16 {/foreach}
#17 </tr>
#18 {/foreach}
#19 </tbody>
#20 </table>
#21 </div>
Now, I will assume that everyone understands the html tags, and will leave alone the code concerning the classes. I will explain lines #4 to #6, which write the table header (the titles of the columns), and lines #9 to #18, which write the table rows.
Line #5 is executed for each element of the $adminshow variable; in other words, for each column of the table. Now, remember that each element of the $adminshow variable is an array of two elements, so what line #5 does is display the first (position [0]) of these two elements as the column header.
Lines #11 to #17 are repeated for each item in $itemlist (for each row of the table). For each row, for each element of the $adminshow variable, lines #14 and #15 are repeated. Line #14 fetch the name of the object's attribute that should be displayed (position [1] of the adminshow element), and line #15 displays this attribute of the current object.
As of the ctlmm_nosearch
class, it only means that the instant search will not check into this column.
Alright... how can I change this?
If
you want to make major changes, the best thing to do would be to create
an individual template for your level. In the templates/ folder, create
a file (like "adminpanel_nameoflevel.tpl"), put the template you wish
to use in it, and in the according tab of the action.defaultadmin.php
file, change
echo $this->ProcessTemplate("adminpanel.tpl");
to
echo $this->ProcessTemplate("adminpanel_nameoflevel.tpl");
Now,
let's say you wish to make a very simple modification. For example, you
are displaying a file field (which we'll call "myfield") on the
adminpanel, but instead of showing the filepath you would like to show
the picture. What you could do is this:
In the adminpanel.tpl template file, you could change line #15 to something like this:
<td{if $column[2]} class="ctlmm_nosearch"{/if}>
{if $oneval == 'myfield'}
{if $oneitem->$oneval != ""}<img src="{root_url}/uploads/{$oneitem->$oneval}" />{/if}
{else}
{$oneitem->$oneval}
{/if}</td>
This means that smarty checks if the field we're displaying is 'myfield', if it is, and if the value isn't empty, it displays the image. If it isn't, it just displays the field value normally.
[*** See update below... ***]
The short answer: no.
The long answer: yes, most of the time.
The name of the selected item or category is retrieved only when the {cms_module module="yourmodule"}
tag is encountered in the template - which is quite always way after
the page's head. However, in most cases in could be retrieved by a
user-defined tag (UDT).
Note that this will no work when the tag is directly called on the page, and will only work when we are following a module action (when you have clicked on a link)
Create the following UDT, replacing NAME_OF_YOUR_MODULE with your module's name:
$modulename = 'NAME_OF_YOUR_MODULE';
global $gCms;
if( isset($gCms->modules[$modulename]) &&
$gCms->modules[$modulename]['active'] &&
isset($params['assign']) ){
global $smarty;
$instance = $gCms->modules[$modulename]['object'];
$glob = $instance->get_moduleGetVars();
$wantedlevel = false;
$modlevels = $instance->get_levelarray();
if(isset($glob['alias'])){
$wantedlevel = isset($glob['what'])?$glob['what']:$modlevels[count($modlevels)-1];
$alias = $glob['alias'];
}elseif(isset($glob['parent'])){
if(isset($glob['what'])){
$wantedlevel = $instance->get_nextlevel($glob['what'], false);
}else{
$wantedlevel =$modlevels[count($modlevels)-2];
}
$alias = $glob['parent'];
}
if($wantedlevel){
$getfunction = 'get_level_'.$wantedlevel;
$item = $instance->$getfunction(array('alias'=>$alias));
if(isset($item[0])) $smarty->assign($params['assign'],$item[0]);
}
}
Let's
say you've named your tag "retrievemodinfo". Then, at the beginning of
your page's template, you retrieve the module informations by using the
tag {retrievemodinfo assign="currentelement"}
. This means
that the information about the current item are stored in the variable
$currentelement (which could have been anything). So in the
<title> tag of your template, you could write: {if $currentelement}{$currentelement->name}{/if}
, which would display the name of the current element if it could have been retrieved.
UPDATE: since version 1.8, you can do the same thing much more easily just by using the "breadcrumbs" action and its parameters.
This will help you include the module entries to a google sitemap, provided that your module elements are all displayed on the same page.
If you are using the google sitemap generator module:
Open your sitemap generator (gsitemap.php), and just before the closure of the urlset (echo ' </urlset>'. "\n";
), add the following lines:
$modulename = "NAMEOFMODULE"; // CHANGE THIS TO THE NAME OF YOUR MODULE
if( isset($gCms->modules[$modulename])
&& $gCms->modules[$modulename]["active"]
&& $themodule = $gCms->modules[$modulename]["object"]
){
// HERE YOU MUST PUT THE ID OR ALIAS OF THE PAGE USED TO DISPLAY THE MODULE ELEMENTS:
$detailpage = "MYPAGE";
$params = array("mode"=>"google", "detailpage"=>$detailpage);
$themodule->DoAction("sitemap", "", $params);
}
Of course, replace NAMEOFMODULE in the first line with the name of your module, and enter a detailpage, be it an id or page alias.
Note that you could also use the "what" parameter to specify which levels you wish to see displayed (you can specify several levels with "level1|level2" and so on). Otherwise, all levels will be displayed.
If you are using the SiteMap Made Simple module:
Simply put the code above in a UDT (user-defined-tag), and call the UDT at the end of your sitemap's template, just before </urlset>
.
You wish to add a date field that will decide when the item will be displayed or not (like in the news module). It shouldn't be very hard.
To do this, you will need two fields (or three):
For simplicity, I will assume we need only one date (you should figure out the part for the second date).
In your list template, use the following code:
{foreach from=$itemlist item="item"}
{if $item->use_expiration && ($item->expiration_date|date_format:"%s" < $smarty.now)}
the code to display the item...
{/if}
{/foreach}
Basically, we only display the item when a condition is met, namely that use_expiration is on, and that the expiration date is lower (before) the current date. To do the opposite, you can just change the < to a >.
This is done in two parts. The first part is to display the edit link or edit form only to selected users (or logged in users). This is to be done with the FEU module (and possibly Custom Content), and as such will not be covered here. This is only about hiding the edition from other users. The second part, which will be covered here, is about the real protection. Sadly, this will require a little programming, but everyone should be able to get through this easily.
If you're wondering how to change the look of the form, see the frontend_add_*.tpl templates in the templates folder.
Before
editing an item through the frontend, the function feadd_permcheck
(whose content is in the file function.feadd_permcheck.php) is called.
If it returns true (if the value of the $return variable is set to
true), the user has permission to edit the item, otherwise (if it
returns false), the access is denied.
If you open the file
function.feadd_permcheck.php, you will notice that aside from comments,
the function is empty. We will have to fill it out depending on what we
wish to do.
The variables at our disposition are the following:
You might need the following functions of the FEU module:
Now, here are four basic examples of how to use this.
Allowing only logged in users
Here's how to allow only logged in users to edit items:
Failsafe:
place the below code in a page with WYSIWYG turned off:
{assign var=userids value=$gCms->modules.FrontEndUsers.object->LoggedinId()}
{assign var=username value=$gCms->modules.FrontEndUsers.object->GetUserName($userids)}
{cms_module module=FrontEndUsers}
{if $userids!=''}
{cms_module module="youtubeplayer" action="frontend_edit" what="videos"}
{/if}
Or:
$return = false;
$FEU = $this->GetModuleInstance('FrontEndUsers');
if($FEU && $FEU->LoggedIn()){
$return = true;
}
Allowing only members of a group
Here's how to allow only members of the group "mygroup" to edit items:
$return = false;
$FEU = $this->GetModuleInstance('FrontEndUsers');
if($FEU){
if($FEU->LoggedIn()){
$userid = $FEU->LoggedInId();
$groupid = $FEU->GetGroupID("mygroup");
if($FEU->MemberOfGroup($userid, $groupid)){
$return = true;
}
}
}
Allowing members to edit only a single element
Suppose you have a FEU member corresponding to each of your item, and
that the username of that member is the alias of the item (see below on
how to do this). You could restrict access in the following way:
$return = false;
$FEU = $this->GetModuleInstance('FrontEndUsers');
if($FEU){
if($FEU->LoggedIn()){
$username = $FEU->LoggedInName();
if($username == $alias){
$return = true;
}
}
}
Using $FEU->GetUserProperty($propertyname), you could use something else than the username.
If permissions depend on the alias of the item, you should make sure (see Settings) that your frontend users can't change it.
Should you need more informations about the item, you can use the following snippet to load the full item object:
$getfunction = "get_level_".$what;
$itemlist = $this->$getfunction(array("id"=>$itemid));
$item = isset($itemlist[0])?$itemlist[0]:false;
Finally, you may use $what variable to have different permission management for each level. A switch would be the typical solution:
switch($what){
case "level1":
// code
break;
case "level2":
//code
break;
...(and so on)
}
Triggering the creation of a FEU member for each item
If you're using something similar to the last example, you need to have
a FEU user for each item. This can be done using events.
Everytime an item is added, modified or deleted, the modules sends an event
to the cms. If you go into Extensions -> Event Manager, you should
find the events sent by your module in the list. Click, for example, on
the modulename_added event. You will notice that here, you can select a
User Defined Tag (UDT) that will be called when the event is triggered.
So we simply need to design an UDT that will add a member when a new
item is created.
We will assume your item has a field named "password", which will be used as the FEU password, that your module is named "NAMEOFMODULE", and the level is named "MYLEVEL". Look at the following UDT:
// first, we check if an item has been added on the level we're concerned with
if($params["what"] != "MYLEVEL") return false;
// we retrieve the two modules
$FEU = $this->GetModuleInstance('FrontEndUsers');
$mymod = $this->GetModuleInstance('NAMEOFMODULE');
if(!$FEU || !$mymod) return false;
// we then retrieve the item
$itemlist = $mymod->get_MYLEVEL(array("id"=>$params["itemid"]));
if(!isset($itemlist[0])) return false;
$item = $itemlist[0];
// we then add the FEU user
$expires_in = 365; // sets the number of days until expiration
$expires = strtotime(sprintf("+%d days",$expires_in), time());
$FEU->AddUser($item->name, $item->password, $expires);
Yes, to some extent.
I'll try to make this very comprehensive. Before talking of modying it, let's see how this works.
Basically, the module has two kinds of urls:
Type A: http://www.mydomain.com/nameofmodule/detail/nameofobject/5/
and
Type B: http://www.mydomain.com/nameofmodule/nameoflevel/nameofparent/5/
(if you are separating elements into pages, you will notice that type B has some variations... we will discuss this later.)
Type A is used when we are looking at the details of a last-level object. Type B is used when we display a list of items. Now, remember that the url has to contain all the information about what should be displayed. So let's take a look about what's common to both types of urls:
The nameofmodule/ tells us that the module should be called.
The 5/
(that annoying number) is the returnid of the page. In other words, it
tells us on what page of the cms the informations of the module should
be shown. Even though the page content is replaced by what the module
has to display, this is very important as it determines, for instance,
what page template should be used.
When the /detail/ is in the url, the module knows that we're displaying the details of a last level item. All we need to specify is which item to display, and thats the nameofobject part.
When the /detail/ isn't
in the url, the module concludes that we are displaying a list of
something*. The elements of what level are displayed is determined by
whatever is in the place of "/detail/": in our case, nameoflevel/. A url like http://www.mydomain.com/nameofmodule/product/5/
would be valid, and would tell the module to display all elements of the "product" level.
The /nameofparent/ part tells the module to display only the items of the chosen level that have "nameofparent" as a parent.
The pretty urls are registered in the SetParameters() function of the main module file (which should be something like NAMEOFYOURMODULE.module.php). Open this file, and do a search to find the function. What matters in there is the lines that look like this (without the numbers on the left) :
#1 $this->RegisterRoute("/[nN]ameofmodule\/([Dd]etail)\/(?P[^\/]+)\/(?P [0-9]+)$/");
#2 $this->RegisterRoute("/[nN]ameofmodule\/(?P[^\/]+)\/(?P [0-9]+)$/");
#3 $this->RegisterRoute("/[nN]ameofmodule\/(?P[^\/]+)\/(?P [^\/]+)\/(?P [0-9]+)$/");
#4 $this->RegisterRoute("/[nN]ameofmodule\/(?P[^\/]+)\/(?P [0-9]+)\/(?P [0-9]+)\/(?P [0-9]+)$/");
#5 $this->RegisterRoute("/[nN]ameofmodule\/(?P[^\/]+)\/(?P [^\/]+)\/(?P [0-9]+)\/(?P [0-9]+)\/(?P [0-9]+)$/");
Each line here register a different url structure.
Line #1 registers the Type A urls.
All the other lines register Type B urls:
Line #2 is for when no parent is specified, like in http://www.mydomain.com/nameofmodule/nameoflevel/5
Line #3 is used when a parent is specified, like in http://www.mydomain.com/nameofmodule/nameoflevel/nameofparent/5
Line #4 is a variation of line #2 that handles separation into pages
(when we separate into pages, we need to know which page we're looking
at). For example, http://www.mydomain.com/nameofmodule/product/2/10/5
would mean that we are looking at the list of all items of the
"product" level, that we are separating in pages with "10" elements per
page, and that we are looking at the page "2".
In pretty much the same way, line #5 is a variation of line #3.
The <paramname> thing means that whatever is in that place in the url will be interpreted as the "paramname" parameter. In other words, each url structure output different parameters that will tell the module what to do.
Modifying all this...
There are some things that you can change, some that you can't, and some that you can at a price.
Let's say you don't like the word "detail"
in the url, and would like to change it to something else**. First,
let's not that the "[Dd]" part in the url structure says that there can
be either a "D" or a "d" here. In other words, http://www.mydomain.com/nameofmodule/detail/nameofobject/5/
is the same thing as http://www.mydomain.com/nameofmodule/Detail/nameofobject/5/
.
You can change the word "detail" for something else, but you have to be
careful and remember that the url carry all necessary information about
what should be shown. For example, if one of your level is named
"product", you couldn't replace the word "detail" by the word
"product". If you did, the module couldn't know if the word "product"
it encounters means that we're watching the details of an item, or if
it means that we are watching a list of the items in the "product"
level.
So
let's say we want to change "detail" for "view" (provided that you
don't have a level called "view"). In line #1, we would replace ([Dd]etail)
with (view)
(or ([Vv]iew)
, if you want it to accept both "View" and "view").
But this is not enough. You've changed the way the urls are interpreted, but not the way they are created. In the same file, search for the BuildPrettyURLs function. It should contain something like this:
#1 $prettyurl = "nameofmodule/";
#2 if(isset($params["alias"])){
#3 $prettyurl .= "detail/".$params["alias"];
#4 }elseif(isset($params["parent"])){
#5 $prettyurl .= $params["what"]."/".$params["parent"];
#6 }else{
#7 $prettyurl .= $params["what"];
#8 }
#9 if(!isset($params["alias"]) && isset($params["pageindex"]) && isset($params["nbperpage"])) $prettyurl .= "/".$params["pageindex"]."/".$params["nbperpage"];
#10 $prettyurl .= "/".$returnid;
#11 return $prettyurl;
This function transforms the parameters into a url.
Line #1 starts with the module's name.
In lines #2-#3, if we want to watch the details of an item, we add the "detail/" block to the url.
If this isn't the case, we add the level of the items we want to show
and, if specified, the parent (lines #4 to #8). In line #9, if we're
separating into pages we add the page we wish to look and the number of
elements per page. Finally, in line #10, we add the id of the page in
which it should be displayed.
If
we changed "[Dd]etail" to "[Vv]iew" in the SetParameters() function, we
must also change the "detail/" in line #3 for "view/".
Now, this should do the trick.
Can I get rid of the returnid?
No, not really. However, if you the module is always displayed in the same page (i.e., if it's always the same number), we could remove it from the url and provide it in another way. Here's how to do it:
Let's say the id of the page in which the module is always called (the number we're trying to get rid of) is 5.
1. in the SetParameters() function of your module, remove all \/(?P
//
).
2. in the BuildPrettyURLs() function of your module, comment (add //
before it) the line #10 (// $prettyurl .= "/".$returnid;
).
3. we now need to provide the id. Go back to the SetParameters() function, and for each line, just after the $"/
part (and before the closure of the parenthesis), add , array("returnid"=>5)
(with the comma), where "5" is your id. For example, this:
$this->RegisterRoute("/[nN]ameofmodule\/([Dd]etail)\/(?P
should become:
$this->RegisterRoute("/[nN]ameofmodule\/([Dd]etail)\/(?P
(Remember not just to copy and paste lines from this help, because your module is probably not named "nameofmodule")
This should do the trick, but the module will never use another page to display its content.
Final notice: If you change the pretty urls and make use of the "is_selected" attributes in your templates, you might also have to change the get_moduleGetVars() function (take a look here).
*
if there is only one item to display, and if you aren't using the
"forcelist" parameter, the details will be displayed instead of a list.
** no, you can't just get rid of it, unless you change all registered
urls... for example, instead of having a "/detail/", all other urls
could have a "/list/" block.
Since 1.8.6, this has become a simple option to turn on in the "Settings" tab...
Yes.
Open the action.default.php file. Around the line #16 you should see something like this:
$forcelist = isset($params["forcelist"])?$params["forcelist"]:false;
What this says is that if the forcelist parameter has been set, we use this value, and if it hasn't, we use the value "false".
Changing this false
to a true
would mean that the forcelist mode is always enabled, unless specified otherwise.
But that's not exactly what we want, for this would mean that we always
display a list, even when we're trying to display an item's details. So
the proper code would be:
$forcelist = isset($params["forcelist"])?$params["forcelist"]:(isset($params["alias"])?false:true);
This way, $forcelist will be false unless specified otherwise, and unless an alias (used in detail links) is specified.
(Two lines below, you could do the same kind of thing for the "inline" parameter...)
What is it for? And why is it such a mess?
In
the main module file, there is a function called get_moduleGetVars().
Basically, the function retrieve the module action parameters from the
url.
Imagine that in your template, in a column on the left, you
call the module to display a category list. When you click on an
category, the list stays there, but the main content block displays the
children within that category. Now, in the category list, you might
want to show which category is selected (using the
$item->is_selected attribute). But the problem is that the category
list is called before the main module action (the list of children), and thus the action parameters aren't yet parsed.
This subject has been discussed in the forums (here),
and I chose the solution that is the easiest to implement for the user:
retrieve the parameters from the url even before they are parsed by the
cms. It's a rather complicated function because we need to deal with
the possibility of prettyurls and rewritten urls...
Of course, it
means that if you wish to change the way prettyurls are made and still
use the is_selected attribute, you're gonna have to play in this
inelegant code. But hey, I'm writing this just to make it clearer...
How does it work, and how to change it?
Before we go, here's a look at the function:
function get_moduleGetVars(){
#1 // unorthodox hack so that different calls of the module speak with each other
#2 // basically, we retrieve parameters in the url that were meant for other instances of the module
#3 global $_GET;
#4 global $gCms;
#5 $globalmodulevars = array();
#6 if(isset($_GET["mact"])){
#7 // if we aren't using pretty urls...
#8 $modinfo = explode(",",$_GET["mact"]);
#9 if(isset($modinfo) && $modinfo[0] == $this->GetName()){
#10 if(isset($_GET[$modinfo[1]."parent"]))
#11 $globalmodulevars["parent"]=$_GET[$modinfo[1]."parent"];
#12 if(isset($_GET[$modinfo[1]."what"]))
#13 $globalmodulevars["what"]=$_GET[$modinfo[1]."what"];
#14 if(isset($_GET[$modinfo[1]."alias"]))
#15 $globalmodulevars["alias"]=$_GET[$modinfo[1]."alias"];
#16 if(isset($_GET[$modinfo[1]."pageindex"]))
#17 $globalmodulevars["pageindex"]=$_GET[$modinfo[1]."pageindex"];
#18 }
#19 }elseif($gCms->config["internal_pretty_urls"] || $gCms->config["assume_mod_rewrite"]){
#20 $params = array();
#21 if($gCms->config["assume_mod_rewrite"] && isset($_GET["page"])){
#22 // if we are using an external mod_rewrite, assuming you are using the very
#23 // basic rewrite which puts the module informations inside the page variable
#24 $parts = explode("/",$_GET["page"]);
#25 foreach($parts as $part){
#26 if($part != "") $params[] = $part;
#27 }
#28 }elseif(!$gCms->config["assume_mod_rewrite"] && $gCms->config["internal_pretty_urls"] && isset($_SERVER["REQUEST_URI"])){
#29 // if we are using the internal pretty urls
#30 $parts = explode("/",$_SERVER["REQUEST_URI"]);
#31 $started = false;
#32 foreach($parts as $part){
#33 if($started && $part != "") $params[] = $part;
#34 if(strtolower($part) == "index.php") $started = true;
#35 }
#36 }
#37 if(isset($params[0]) && strtolower($params[0]) == strtolower($this->GetName())){
#38 // we are in a module action
#39 if(!isset($params[1]) || strtolower($params[1]) == "query"){
#40
#41 }elseif(isset($params[1]) && strtolower($params[1]) == "detail"){
#42 $globalmodulevars["what"] = "tsuba";
#43 $globalmodulevars["alias"] = $params[2];
#44 }else{
#45 $globalmodulevars["what"] = $params[1];
#46 switch(count($params)){
#47 case 6:
#48 $globalmodulevars["pageindex"] = $params[3];
#49 $globalmodulevars["nbperpage"] = $params[4];
#50 case 4:
#51 $globalmodulevars["parent"] = $params[2];
#52 break;
#53 case 5:
#54 $globalmodulevars["pageindex"] = $params[2];
#55 $globalmodulevars["nbperpage"] = $params[3];
#56 break;
#57 }
#58 }
#59 }
#60 }
#61 return $globalmodulevars;
}
Now there are four possible scenarios:
1. There is no rewrite or prettyurls at all (that's the easiest... lines 7 to 18 deal with this)
2. We are using internal prettyurls without mod_rewrite (lines 29 to 35, and 37 to 59)
3. We are using internal prettyurls along with the standard assumed mod_rewrite (lines 22 to 37, and 37 to 59)
4. We are using a non-standard mod_rewrite... in that case, you'll
either have to forget about the is_selected attribute or to write the
code yourself.
Case 1: good ol' urls
If we find the "mact" in the GET parameters, then we are dealing with
standard urls. We just have to break the mact value into pieces, make
sure it's an action of our module, and retrieve the different
parameters by their name (along with the $id).
Case 2: internal pretty urls
Lines 29 to 35 retrieve the url, separate it into parts (each part
being a fake folder in the url), and only keep the parts that are after
the "index.php/".
Lines 37 to 59 try to figure just what parameters these parts could be. Remember that we had the following routes registered:
#1 $this->RegisterRoute("/[nN]ameofmodule\/([Dd]etail)\/(?P[^\/]+)\/(?P [0-9]+)$/");
#2 $this->RegisterRoute("/[nN]ameofmodule\/(?P[^\/]+)\/(?P [0-9]+)$/");
#3 $this->RegisterRoute("/[nN]ameofmodule\/(?P[^\/]+)\/(?P [^\/]+)\/(?P [0-9]+)$/");
#4 $this->RegisterRoute("/[nN]ameofmodule\/(?P[^\/]+)\/(?P [0-9]+)\/(?P [0-9]+)\/(?P [0-9]+)$/");
#5 $this->RegisterRoute("/[nN]ameofmodule\/(?P[^\/]+)\/(?P [^\/]+)\/(?P [0-9]+)\/(?P [0-9]+)\/(?P [0-9]+)$/");
Route
#1 is handled by the lines 41-43. Otherwise, it will depend on the
number of parameters. If there are 6, we know we're using route #5 :
$params[0] is the name of the module, $params[1] the name of the level
(what), $params[2] the name of the parent, $params[3] the pageindex,
and so on...
Likewise, if we have 5 params, we know we're using
route #4, and with 4 params, we know we're using route #3. If we were
using route #2, there wouldn't be anything to retrieve anyway.
If the module action is a query (lines 39-40), we don't retrieve anything...
So basically, if you change the routes, you've got to change lines 37 to 59.
Case 3: internal pretty urls with standard mod_rewrite
In this case, the path that's after "index.php/" in case 2 is simply
passed through the "page" GET parameter. So once we've retrieved it,
the task is just like case 2.
Pierre-Luc Germain