This book is intended for the lasqueti.ca webmaster, and only contains information relevant to the maintenance of the site. Before you begin, there are two things you should do:
Once you've got a basic idea about what Drupal is and how the site is structured, this document will fill in some of the niggly little details.
Some known issues we should try to resolve:
One of Drupal's primary powers is the seperation of the data processig from the presentation. Lasqueti.ca takes full advantage of these abilities by defining a custom theme. The "lasqueti theme" is documented here, along with some sub-pages that describe some of the custom theme programming.
The Lasqueti theme is a "hard-coded" version of the Denver theme. Denver is a very flexible theme and specifies vitually everything with configurable parameters. That is great for proto-typing, but slow for the production site.
The Denver theme was set-up to look "correct", and them copied to the Lasqueti theme. The Lasqueti theme was then editted to "hard code" the various parmater values set in Denver.
Several additional changes were made to Lasqueti theme, for example:
All of this is documented in the sites/all/themes/lasqueti readme.
The Lasqueti theme could have a set of sub-themes, which simply change the banner for special parts of the site (e.g., like the marketplace) or provide style to hide images or other bandwidth intensive content (a low-bandwidth style).
Important - from the Denver Theme readme file:
"to prevent rendering errors in IE you must enable the CSS Aggregator in admin/settings/performance... Enable "aggregate and compress CSS files" to make your production drupal site run faster, and to prevent IE rendering errors."
The following functions were added to PHPtemplate in the lasqueti theme to obtain the required functionality. Some of these should probably be made into a custom module.
see: HowTo: Replace node title with a related image | drupal.org
<code>
// This function is used to substitue the node title with an image of the same name.
// Really good candidate to turn into a small module.
function _phptemplate_variables($hook, $vars = array()) {
switch ($hook) {
case 'page':
// substitute node title with an image, if a suitable replacement can be found.
// save original title text for use in head and breadcrumb.
$vars['breadcrumb_title'] = $vars['title'];
$vars['head_title'] = $vars['title'];
// Substitute title only for nodes...
if (arg(0) == 'node' && is_numeric(arg(1))) {
$node = node_load(arg(1)); // (expensive, unless arg(1) is already loaded, which is should be at this point.)
// ... of one of the listed types - add node types to process to the array.
if (in_array($node->type, array('page', 'page_layout_b'))) {
// convert title to suitable filename and derive both file system path and URL.
$titleFile = clean_title($vars['title']) . '.gif';
$titleImage = base_path() . "files/images/titles/" . $titleFile;
$titleURL = "http://" . $_SERVER['SERVER_NAME'] . $titleImage;
$titlePath = $_SERVER['DOCUMENT_ROOT'] . $titleImage;
// $vars['title'] = $titleURL; // DEBUG - see what's being produced
// Determine if a suitable replacement image for title can be found, and make substitution.
if ( file_exists($titlePath) ) {
$newTitle = '<img alt=\'' . $vars['title'] .'\' src=\'' . $titleURL .'\' />';
$vars['title'] = $newTitle;
}
}
}
break;
}
return $vars;
}
// Prepare some general title text for use as a file name.
// Remove special HTML characters, trim whitespace, convert to lower-case
// repace spaces with underscores.
function clean_title($string)
{
$cleanString = htmlspecialchars_decode( strtolower(trim($string)), ENT_QUOTES );
$cleanString = str_replace(array("& ", "'"), '', $cleanString);
// $string = strtolower(preg_replace("/[^a-zA-Z0-9 ]/", "", $string)); // slower, but more general purpose.
return str_replace(' ', '_', $cleanString );;
}
</code>
<code>
// Create a mock-menu from a view's summary (usually for a block view)
// See Example: How to Display a Summary View as a Mock Menu http://drupal.org/node/42605
function _summary_menu($view, $level, $nodes) {
foreach ($nodes as $node) {
$list .= '<li class="leaf">' . views_get_summary_link($view->argument[$level]['type'], $node, $view->real_url) . "</li>\n";
}
if ($list) {
return "<div class='menu'><ul>$list</ul></div>";
}
}
// Photo albums are per-user, so agrument is user ID UID
function phptemplate_views_summary_photo_albums($view, $type, $level, $nodes, $args) {
if ($type == 'block') {
return _summary_menu($view, $level, $nodes);
}
else {
drupal_add_css(drupal_get_path('theme', 'lasqueti') .'/css/image_gallery.css', 'theme');
// Need generic way to get image file info? - must use name of CCK field.
// Replace this with the name of your image field.
// Uncomment the print_r below to figure out what it is.
$cck_fid_field = 'node_data_field_image_field_image_fid';
$content .= '<ul class="galleries">';
$title = "'s Personal Photo Album";
$baseUrl = $view->url;
// Build a small, single node teaser for each query in the summary and
// use it as a link to the actual view page with that argument.
foreach ($nodes as $query) {
// Get the argument for this view from the query.
$uid = $query->uid; // argument is user id;
$user = $query->name; // query also has user's name.
// Build a 1 item query to grab the first node in this view
$result = views_build_view('items', $view, array($uid), false, 1);
// Grab the image from that node to display in our summary.
// The file id comes from the node info. we got back from views.
// print_r($result['items'][0]);
$fid = $result['items'][0]->$cck_fid_field;
$modDate = $result['items'][0]->node_changed;
// Query the database to get the file object
$file = _imagefield_file_load($fid);
$imgTag = theme('imagecache', 'Thumbnail', $file['filepath'], 'Latest image from gallery', $user . $title);
$url= $baseUrl .'/'. $uid;
$linkText = $imgTag .' '. $user . $title;
$content .= _theme_gallery_summary($imgTag, $url, $user.$title, $query->num_nodes, $modDate, NULL);
}
$content .= "</ul>\n";
return $content;
}
}
// Produce the themed output for the image gallery summary.
// This code was ripped from the image gallery module.
function _theme_gallery_summary($imgTag, $url, $title, $count, $modDate, $description=NULL)
{
$content = "";
$content .= '<li class="clear-block">';
if ($count>0) {
$content .= l($imgTag, $url, array(), NULL, NULL, FALSE, TRUE);
}
$content .= "<h3>". l($title, $url) ."</h3>\n";
if ($description) {
$content .= '<div class="description">'. check_markup($description) ."</div>\n";
}
$content .= '<p class="count">'. format_plural($count, 'There is 1 photo in this gallery', 'There are @count photos in this gallery') ."</p>\n";
if ($modDate) {
$content .= '<p class="last">'. t('Last updated: %date', array('%date' => format_date($modDate))) ."</p>\n";
}
$content .= "</li>\n";
return $content;
}
// Photo galleries are per-term, so argument is taxonomy term.
function phptemplate_views_summary_photo_gallery($view, $type, $level, $nodes, $args) {
if ($type == 'block') {
return _summary_menu($view, $level, $nodes);
}
else {
drupal_add_css(drupal_get_path('theme', 'lasqueti') .'/css/image_gallery.css', 'theme');
// Need generic way to get image file info? - must use name of CCK field.
// Replace this with the name of your image field.
// Uncomment the print_r below to figure out what it is.
$cck_fid_field = 'node_data_field_image_field_image_fid';
$content .= '<ul class="galleries">';
$title = " Photo Gallery";
$baseUrl = $view->url;
// Build a small, single node teaser for each query in the summary and
// use it as a link to the actual view page with that argument.
foreach ($nodes as $query) {
// Get the gallery description from the term's vocab.
$term = $query->letter; // argument for taxonomy terms;
$tid = $query->tid; // term IS the argument!
$termObj = taxonomy_get_term($tid);
$description = $termObj->description;
// Build a 1 item query to grab the first node in this view
$result = views_build_view('items', $view, array($term), false, 1);
// Grab the image from that node to display in our summary.
// The file id comes from the node info. we got back from views.
// print_r($result['items'][0]);
$fid = $result['items'][0]->$cck_fid_field;
$modDate = $result['items'][0]->node_changed;
$file = _imagefield_file_load($fid);
$imgTag = theme('imagecache', 'Thumbnail', $file['filepath'], 'Latest image from gallery', $term . $title);
$url= $baseUrl .'/'. $term;
$linkText = $imgTag .' '. $term . $title;
$content .= _theme_gallery_summary($imgTag, $url, $term.$title, $query->num_nodes, $modDate, $description);
}
$content .= "</ul>\n";
return $content;
}
}
This simply addes a collapsible fieldset for the CCK fields in the "Market Page" type, and hides the Categories fieldset from non-admins, to try to improve useability. See http://drupal.org/node/101092#comment-748013 for details.
function phptemplate_page_layout_b_node_form($form) {
// Collapse the taxonomoy fieldset by default (as this rarely changes).
$form['taxonomy']['#collapsed'] = true;
// enforce crude access rights on the taxonomy terms.
global $user;
if (!($user->uid==1 || in_array('contributor',$user->roles)) ) {
unset($form['taxonomy']);
}
// These are the fields that should be in the fieldset
$fields = array('field_teaser_image', 'field_bio_image', 'field_bio');
// Add fieldsets to the page_layout_b input form & render it.
_add_fieldset($form, 'Images', $fields);
return drupal_render($form);
}
/**
* Add the given fields to a new fieldset on the form.
* @form - the form to add the fieldset to
* @name - a string name of the fieldset
* @fieldsInSet - field ids (names) for the fields in
* the form that should go into the fieldset.
*/
function _add_fieldset(&$form, $name, $fieldsInSet) {
// Add a fieldset to the form to hold related fields.
$form[$name] = array (
'#type' => 'fieldset',
'#title' => $name,
'#collapsible' => 1,
'#collapsed' => 0,
'#weight' => -2,
'#tree' => 1,
);
// Move all the related fields into the new fieldset.
foreach ($fieldsInSet as $field) {
if ($form[$field]) {
$form[$name][$field] = $form[$field];
$form[$name][$field]['#parents'] = array ($name, $field);
unset($form[$field]);
}
}
}
When a node's page is viewed in a tab, we don't want to show any book navigation.
function phptemplate_book_navigation($node) {
if ($node->inTab)
return NULL;
else
return theme_book_navigation($node);
}
Exposed filters allow the user to have control over what nodes are viewed by selecting from a widget. The default layout is to have the widget displayed above the view, in the page - yuck - very ugly and poor usability. This "snippet" hides the default exposed filter for a view with the name "test" - it can be copied for any View where you want to do this. Then a custom block is created to display the exposed filter widget off to the side, or "rolled-up"! Much nicer.
See http://www.angrydonuts.com/displaying_views_exposed_filters
function phptemplate_views_display_filters_test() {
// Do absolutely nothing.
}
Anywhere the user can enter data is a form.
Mostly, these forms are generated by Drupal and the sequence the fields are presented is controlled using the field weights (manage fields under Content Types). However, there were a few cases where input forms were customised to improve their usability (mostly to make them less daunting to the community).
Any page, node, or block can have a custom template that defines its look. This is a very easy way to subtly alter the look of different parts of the site, or to customize the display of different types of data.
Heavily modified the Denver Theme version to remove almost all conditional theming and unused divs. Made a few other customizations to:
Basically, this is straight from the Denver theme, but with:
Defines the footer portion of the page that includes the "Our Community" and "MarketPlace" random image blocks.
Defines a custom layout for the "Market Page". Basically, this just adds the code to pull in the "mini-gallery" at the bottom of the page.
Added this to customize the look and feel of comments.
See Theming Drupal Comments, Exemplifying with Garland | All Drupal Themes
Some things just can't be developed as a full-fledged module...
Sometimes a hard-coded argument is required (e.g., specify vocabulary for a view), sometimes the code just needs further development to create a nice, general-purpose module. In any case, these small "modulettes" are less than a full-fledged module, but more than simply a snippet. They all perform some custom theming, often for a summary view of some sort.
This module creates a view of nodes by-category, to form a simple, 2-column directory.
Requires:
Options:
All code snippets, modules, and modulettes on this site are covered by GPL - see license.txt
The source code is attached below...
Full instructions for installing and using this module are included as comments in the source file.
Attachment | Size |
---|---|
directory_view.zip | 15.28 KB |
This module themes a views summary for a set of image galleries. The image galleries are defined with a view using an argument (usually Term or User). The summary view shows a thumbnail of the latest image from each gallery, the page title, last modified date, and number of images in the gallery. This is nearly identical to the gallery summary provided by the Image module's Image Gallery contrib module. This can be used to theme per-user gallery summaries or per-term gallery summaries. I can't think of (and haven't tested) other possibilities, but it should work with any argument type and any node type that defines at least one image where you want a nice looking summary.
Requires:
Options:
All code snippets, modules, and modulettes on this site are covered by GPL - see license.txt
The source code is attached below...
Full instructions for installing and using this module are included as comments in the source file.
Attachment | Size |
---|---|
summary_view_image_gallery.zip | 15.01 KB |
This module "automatically" replaces text titles with an "associated:" image, if a suitable replacement can be found. It simply converts the title into a filename and looks for that file - if it finds it, it formats the image tag to replace the title.
Requires:
All code snippets, modules, and modulettes on this site are covered by GPL - see license.txt
The source code is attached below...
Full instructions for installing and using this module are included as comments in the source file.
Attachment | Size |
---|---|
replace_title.zip | 13.35 KB |
This very tiny module creates a "mock menu" from a views summary - a menu item is created for each query, or potential argument value, in the views summary. This is handy for providing a menu that links to the various pages provided when a view has an argument.
All code snippets, modules, and modulettes on this site are covered by GPL - see license.txt
The source code is attached below...
Full instructions for installing and using this module are included as comments in the source file.
Attachment | Size |
---|---|
summary_view_mock_menu.zip | 13.03 KB |
This module creates a "mock menu" from a vocabulary, with links to the terms at a given base-path. This is very handy for providing a menu that links to a view with a Taxonomy:Term ID argument.
Requires:
Options:
All code snippets, modules, and modulettes on this site are covered by GPL - see license.txt
The source code is attached below...
Full instructions for installing and using this module are included as comments in the source file.
Attachment | Size |
---|---|
category_mock_menu.zip | 13.95 KB |
Here are some of the "tricky" ways I've manipulated Drupal to get the job done, without resorting to custom modules or theming.
The menus module in Drupal core allows you to manipulate the system menus through the admin GUI interface (admin >> build >> menus)
However, it does not give you control over at least two key aspects of the menu:
These can be controlled if the menu is built programmatically. For these reasons, I created a custom lasqueti menu module (lasqueti_menu). This module does the following:
See the following pages for more information about working with and maintaining these tabbed menus.
The idea behind tabbed navigation is to provide the user with a spatial orientation to the context of the page they are viewing. The tabbed layout creates its own, implicit "breadcrumb" by always showing the user exactly where they are in the structure. Thus, you'll notice that the breadcrumb has been removed from these pages.
The tabbed menus only present information that is available elsewhere on the site. It is meant as a seperate navigational structure, primarily aimed at users who are new to the site, and simply want to browse around. Thus, the tabs are organized into "categories of interest" - so users can view all content on the site related to their interests.
To this end, tabs "pull-in" two distinct kinds of data:
The teaser lists are almost all generated by a single view named "by_category". This view simply lists teasers for all nodes, using a "Taxonomy:term id" as an argument. The tabbed menu code supplies the relevant taxonomy terms for the Views argument when the view is built, to pull-in all the related nodes. In this way, whenever a new node is added or a the categories of a node are modified, it will automatically show up on the right tabbed page.
When a list of teasers is presented, there is always a "heading" node placed at the top of the list (simply by tagging it with the right categories and making it "sticky"). Although this "heading node" is just a normal node with the title set to the tab title and the body containing an overview of the tab's contents, it is not used as a normal node, and should not appear in any other displays (I hope!). Some special logic was added to template.php and node.tpl.php to handle this node, but ultimately, these may need to be stored in a special node type so their display in other lists can be contolled more easily. An alternate implementation would be to display the heading node explicitly during _display_view in the custom tabs module. This way those nodes could be controlled very precisely.
Pages in the Farmer's Market, with their associated produce lists, represent a fairly complex set of inter-relations. Understanding how the various node types, cck fields, categories, and views fit together will take some time. However, setting up and maintaining a Farmer's Market page is not hard, but you need to be quite careful to get the relationships right:
Thus, a "Famer's Market page" is a Market Page, tagged with Topic "Farmer's Market, with the same URL path as a term in the Lasqueti Market category. These rules are used to identify these pages in the code - so pages that don't adhere will not be considered part of the Farmer's Market, and won't work correctly. This is a little sketchy, perhaps even a bit hacky, and limits the re-use of the Product node type ouside the Farmer's Market - but it was expedient, and seemed cleaner than creating a new node type or yet another taxonomy. Although the Topic category is used to identify Farmer's Market pages, the Lasquti Market term and URL path still need to be identical for Taxonomy Redirect to work.
The Author of a correctly configured Farmer's Market page will see two new tabs on their page:
These two tabs are created by the custom Lasqueti Menu module - they are "dynamic" local menu items. It is important to note that only the page author will see these tabs, so once the admin gives authorship over to another user, they won't see these tabs anymore. I could not figure out how to make this work so admin always sees all.
There are three distinct "Product List" views:
The upshot?
When user's create a new product, they need to select both the correct Lasqueti Market taxonomy (so the product is listed correctly in the Farmer's Market) and the correct Market Page node reference (so the product is added to their mini-gallery). The node reference selector uses the user_pages view to restict them to selecting references to only pages they own. This is good because it avoids confusion and mistakes, but it is bad because it means the admin cannot create these node reference links without first "masquarading" as the author of the page. The Lasqueti Market category terms would be more difficult to restrict in this way, so they are open, and thus prone to error.
This reliance on authorship for a number of the functions (e.g., edit tab availablitity, node editing access, node reference list, etc.) also implies that only one person can manage a given market page. It is certainly possible to set-up group access and do things based on groups rather than author, but there would be a lot of overhead (and extra work!). An easy workaround would be to create a group ID for the website that all members of the group could use to update the shared resource.
This page should document the reasons why each contrib module has been installed.
Note that some modules are actually packages, and not all individual modules in the package are enabled.
These modules are used only for site administration and site development - they do not affect the "user" view of the site. In general, these modules can be removed from the site (e.g., to improve performance if that is an issue) without affecting anyone except the site admin.
These modules provide access control functions to restrict who sees and edits what. Used in conjunction with the built-in access control panel, both to secure the site from anonymous users and to simplify the view of the site for novice users. These module could be removed if the access control function they provide is no longer required.
These modules are used for automating some of the site building tasks, and creating specific behaviours. Users do interact with these at some level, but not directly - mostly they act in the background. Be careful to understand how these modules are used before removing them - the behaviour of the site depends on them, but they tend to act in the background, so its not always obvious they are at work.
These modules are installer to allow creation of custom content types (e.g., CCK). The site could not function without these modules as CCK is used extensively throughout.
These modules are installed primarily to provide extra functionality to the user.
Almost everything I ever thought of has been done in Drupal by someone else - it's a beautiful thing. But there have been a few things I've imagined would be really useful that I couldn't find anywhere...
I'd consider writing these myself one day, if time ever permits...
This module would define a new CCK date type (using Date API) to define an expiry date for a node. The node owner would get an e-mail reminding them that their node was about to expire, and if no action was taken, the node would simply be removed. This would be very handy for announcements and to create a CCK classified ads module. Code could largely be ripped from ed_classified module.
This module would look for an image in a specific directory with a name "similar" to the node title. If a suitable image is found, then replace the node title with the image. Use hook_node_?
DONE: But code in template.php - need to move it to a custom module.
This small module uses a hook_form_alter to create per-role access rights to elements of the Publishing Options fieldset on the node-edit form. This would allow admin to grant rights to user-roles to, for example, promote to front page.
I have already made a good start on this module and will look to contributing it.
A View with an argument creates a "summary view" when the argument is missing. The default summary view is simply a list of potentail argument values, linked to their repsepective view "pages". I could only find one example of a customized summary view (Example: How to Display a Summary View as a Mock Menu | drupal.org ). I used this to create the "pop-open" menus when browsing the photo gallery, for example.
I heard a rumour that Views Theming is going to get MUCH easier in Drupal 6, so this may be a wait-and-see.
But there are some other summary views I'd find a great use for:
Module attached below - install as-per-usual.
This small module uses a hook_form_alter to create per-role access rights to all 4 elements of the Publishing Options fieldset on the node-edit form. This would allow admin to grant rights to user-roles to, for example, promote to front page.
I have already made a good start on this module and will look to contributing it.
I have contacted RobRoy, author of a similar module: http://drupal.org/project/override_node_options
about integrating these two similar modules.
Attachment | Size |
---|---|
publish_access.zip | 7.73 KB |
Forgive me Drupal, for I have sinned...
One of the key benefits of Drupal is that you DON'T (usually) need to "hack" away at existing code to customize it - you can use the hook_ and theme_ systems to add your custom code, thus making upgrading much easier. Nonetheless, there were a couple modules that I needed to "hack" a little, just to suit my finicky tastes... These changes now need to be "re-implemented" any time these modules are updated (unless you want the old behaviour to revert)...
I have tried to install only modules that I needed to meet the immediate designs I was trying to achieve. In my travels, though, I thought these modules looked very interesting and worth checking out:
- Hovertips and Clicktips | drupal.org - uses JQuery (AJAX) to create nice pop-up or roll-down tips. Really useful for displaying complex information or hiding stuff that's not used much (e.g., hide all comments current user has already seen.
- Panels | drupal.org - provides API and UI for creating sophisticated panelled layouts. With related "mini-panel" modules to add AJAX tabbed panels and carousel panels Mini Panels demo | Wim Leers
- Node Queue | drupal.org - allows you to display items from a "queue" that automatically drops last item when a new one is added. Can be used manually for creating "top 5" type lists, or integrated with actions module to automate.
- module_builder - I install this locally to make custom module development easier. It simple creates a template for a new module based on a few settings. Only useful for developing a new custom module from scratch.
- form_inspect - a companion for the devel module, this is useful if you are working with forms (say implementing a hook_form_alter), and want an easy way to see view the form data structure.
- chipin - for collecting money for a cause ** NOT AVAILABLE in 5.0 YET **
- nodewords - meta tag (description, keywords) for each page.
- rsvp - enables rsvp for events (probably won't work with the CCK event types I defined :-( Perhaps re-develop for CCK date field?)
- volunteer_timeslots - for organising events where volunteers sign up for a timeslot - ditto!
- spam - spam blocking tools.
- taxonomy_access - access control for users based on taxonomy categories.
-taxonomy_role - access control to taxonomy field on node-edit forms. Useful to allow admin to place "hidden" taxonomy term in a node, probably for organisational purposes.
- cck_role - same thing for a CCK field. Could be handy.
- Notifications - a potential replacement / upgrade for Notify and/or Subscriptions?
- Custom Breadcrumb - format breadcrumbs for custom views.
Here's what I've learned about maintaining a Drupal site. There are probably other (better) ways to do this, but its how I do it...
Installation, Migration, and Backups: http://drupal.org/node/206647
Best Practices: http://drupal.org/best-practices
I use MyPHPadmin to create a dump of the database. This can be imported back in case of a problem. This should be done on a regular basis - remember, all of the data, configuration settings, etc. are all stored in the database.
Useful article: http://drupal.org/node/22281
The Files area is the only one that is changed by non-admins. It is where all file uploads (images, attachements, etc.) are stored. The rest of the Drupal install is "replaceable" by re-installing Drupal and the contrib. modules. The only exceptions are: (1) custom modules; and (2) custom themes; - these should be backed up whenever you make a change to them. (Keep an archive of old versions in case you ever need to go back to a previous version!)
I run Apache/PHP/MySQL/Drupal locally on my own machine. I keep the version on my machine in sync with the production system on a regular basis by simply importing my database backup. I use this version as a "sandbox" to try things out thoroughly before implementing them on the production site. I NEVER export the database from my sandbox and upload to the producton site, because (1) the sandbox is never clean; and (2) users may have added data to the production site since I grabbed the database.
Here are some useful articles on this topic: