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).
For each level, you should have a editLEVELPREFIX.tpl, a search_LEVELNAME.tpl and possibly a frontendadd_LEVELNAME.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 everything 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);
}
$params = array("active_tab"=>"templates", "module_message"=>$this->lang("message_modified"));
// no break, natural redirect to defaultadmin
case "defaultadmin":
require "action.defaultadmin.php";
break;
case "movesomething":
require "action.movesomething.php";
break;
case "toggle":
require "action.toggle.php";
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...
Each "case", here, is an action. You will notice that most of them call to a file (require "action.movesomething.php";), but some don't (just because they're small) and are coded right on the spot. 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->image} in your template, and the image doesn't show up, it just displays /folder/nameofimage.jpg ...
...
That's pretty much normal.
Not all people want to simply "display" the image in the old fashioned way. For this reason, what is given is the path to the image, starting from your upload dir. If you want to display the image, you'd have to put something like this in your template:
<>img src="uploads/{$item->image}" 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>
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"}
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.
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"),
array($this->Lang("alias"),"alias"),
array($this->Lang("active"),"toggleactive"),
array($this->Lang("nbchildren"),"nbchildren"),
array($this->Lang("reorder"),"movelinks"),
array($this->Lang("Actions"),"deletelink")
);
$this->smarty->assign("adminshow", $adminshow);
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 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 two 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.
For example, array($this->Lang("name"),"editlink") 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).
Now, let's take a look at the adminpanel.tpl file:
#1 <>div>
#2 <>table 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>{$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.
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, the adminpanel shows either a "1" or a "0" for some field (which we'll call "myfield"), and you would like to display "Yes" and "No" instead. What you could do is this:
In the adminpanel.tpl template file, you could change line #15 to something like this:
<>td>{if $oneval == 'myfield'}
{if $oneitem->$oneval == 1}Yes{else}No{/if}
{else}
{$oneitem->$oneval}
{/if}<>/td>
This means that smarty checks if the field we're displaying is 'myfield', and if it is, instead of simply displaying it, it will check if it is equal to "1", display "Yes" if it is, and "No" if it isn't.
[*** 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.
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 parts. (it would be a good idea to copy and paste the original lines just in case... you can comment lines using //).
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 (in the main module file).
* 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.
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.
Notice that two lines below, you could do the same thing for the "inline" parameter...
Pierre-Luc Germain