CiviCRM with Drupal

Various aspects of integrating drupal with CiviCRM on a real life project. I'm getting more and more involved with the development of Civi so I may need to post a few links to pages on civicrm.org. Please bear with me as things develop.

CiviCRM - Ideas for future development

There is an official page on civicrm's wiki site to document many of the possible changes for civicrm v3.0. As a result, this page is simple a brainstorm I'm working on and not an indication of any comfirmed features for civicrm.

Disclaimer out of the way, time for some ideas. I have been working closest with the events system which will probably skew the focus of this page. I'll start with the list I've slowly been adding to.

Design based on use cases.
A lot of my changes have been direct functionality requests from my clients. This is a key indicator of the needs of the crm
Events statuses snaphots.
So it is possible to look back at event state before it began.
Modular framework - easily extendable.
By having modules, the system can be as small or as large as needed in terms of functions
Only load what is needed for any page request.
I think this is more or less the case but adding requires all over the place is getting a pain
Modules that have prerequisites to 'borrow' functionality from other code
Simply the same way that drupal modules work
Menus exposed to the host cms (Drupal/Joomla)
I'd love to be able to edit menu item names etc from within the cms
Directly expand on a cms user.
This is basically profiles with extra refining.
Ability to create a cms user if one doesn't exist for a contact.
Batch mode for the above too
Price sets for memberships
I had to add this but I borrowed a fair amount of code from events, surely we can do it for memberships as standard
Ability to moderate memberships
Simply put, not all people who sign up will qualify for membership.
Image uploads
Built in support for uploads using fckeditor + image_assist etc.
Combine the editors in cms with that of civicrm's.
I combined fckeditor with reasonable success but for drupal but in future there is wysiwyg api to do this job. This should mean easier maintenance of the editor, it only requires a config file then.
Events sub pages
e.g. testimonials and associated media etc. Use the host cms to provide these? It should extend the life of an event page's usefulness and keep things together.
RSS of events.
Maybe using views but would be nice out-of-the-box This already exists and has done for a while. Silly me
Full calendar system to locate/browse events.
I think this may already exist but who knows how much the system will change for v3.0
Integrate with social networking systems.
Twitter, Facebook and Flickr etc. Build an output conduit system, modules provide the links to the actual destination system.
Usability is KEY to front and back end.
We know this already and steps are being taken
Memberships/Relationships are ACLs too.
I think this is a critical feature. One which will bring civicrm on a par with role purchasing in ubercart etc. Memberships have a lot more granularity than just a role. Grace periods etc.
Free places based on membership/relationship
Basically, what do you get as a perk for being a member? How about free entrance to events. Simple, or rather not quite. I've added a membership for organisations that cascades to individuals and on top of that, the admin gets to choose how many places they get.

I will add to this list as and when I can and please add your 2 pence in the comments.

CiviCRM - Placeholder Participant Names

After finishing a chunk of changes for a client to make changing registration names after submitting your form, I then had to tweak things further.

As It stands I haven't yet solved the problems that arose. I posted my original article on civicrm's website but I have now included it below. After the article I will provide my current problems, questions and solutions.

Article

I recently finished writing an addition to the events system that will allow pa's etc to book places for people to events and then return at a later date to confirm the names. As far as I know, this is in progress for v2.3 but basically I need it now. Apart from some testing the system should work without too much trouble.

There is only one issue that will perhaps need addressing and that is anonymous signups. As far as I know, CiviCRM will create a new contact record for the person booking if need be and the existing system is preserved so things should work fine. There is an extra button which will make this system work on additional participant pages. Over the next few days I'll test this with v2.2 and see if it is worth getting into the core.

How did I do it I hear you say. Well make yourself comfy and I'll tell.

At the heart of it is an extra button on the additional participant pages. It says something like 'Just reserve place' and I check for this in postProcess() for the form. I also make sure the amount field IS required rather than ignoring it like you do for a skipped one. Then in AdditionalParticipant->postProcess() I set the email of the skipped participant to the same as the booking contact.

I made a small change to CRM_Event_Form_Registration->addParticipant so that the booker's contact_id can be added multiple times to an event. After that it was a case of providing a new link on the EventInfo page to the register update page I created which allowed you to only change the email addresses of your additional participants. Any new emails have a contact created for them and the record in the civicrm_participant table is updated to reflect the new id.

That about sums it up really. I think there may be a little checking I need to do with the change I made in addParticipant to make sure a user can't force extra participants in by revisiting the same page. Other than that, it seems to work.

I have created a patch so this can be tested. The patch should work against the 2.1 branch from the subversion repository but there's a good chance it will work with the tarball too. Obviously this will change files so when patching, please make sure you BACKUP FIRST. You can see the files altered from a quick scan of the file.

I made the patch against the core files so if you have tailored them to suit in a custom directory you might want to apply the changes manually.

note
This patch will NOT make the menu item it needs. You'll have to do that manually I'm afraid, in the database. The path in use is: civicrm/event/updateregister with the usual query string e.g. id=2&reset=1&action=preview will get you event 2 in test mode.

File civi_2_1_delayed_participants.patch

Current Status

I need to check the form when submitted. Rather than adding the new email into the system as a contact if it doesn't exist, I need to ask for a name in these cases. I thought it wouldn't be that hard but it is proving rather tricky.

I've tried working with the elements in a group and failed miserably after several hours of trying. I now have the items as individual elements. I can set errors on these using a formRule. I would have thought that I could get the error by just using element->getElementError() but this draws a blank and so is the _errors array for the form.

I now have it that an error is set using setElementError() which may not be needed soon. I also store the names of the failed elements in a session variable. Then when rebuilding the form, I check whether it has been submitted or not and if so I can check for the error each time I put an element into the form.

Wish it was easier.

Update

The situation as it stands now is; it's working. It's not pretty but it does the job. I don't know why it took me so long to work out what I was doing wrong but to clarify I'll go through the form lifecycle for a sec.

When a form is built methods get called in order:
preProcess -> buildQuickform -> setDefaultValues -> addRules -> display()

The display method is that of the QuickForm class.

This works for any form and when a form is submitted the form is rebuilt, just the same as it is in drupal. Here's the sequence:
preProcess -> buildQuickForm -> setDefaultValues -> addRules -> validate -> postProcess

With this in mind, if a form does not validate things go like this:
preProcess -> buildQuickForm -> setDefaultValues -> addRules -> validate -> display()

At the moment there is no easy way of doing something to the form AFTER it has failed validation because you build the form 3 steps earlier and errors are caught and highlighted later on.

My solution is ugly and I'm not proud of it but I pass in the form to the validation method and make changes there if I need to. Very hacky and if I can come up with a more elegant solution then I certainly will document it here or somewhere on this site.

I think perhaps a hook would be nice, especially if it was optional. Similar to the #after_build or #pre_render hooks from drupal.

In the meantime, there are other things I need to build/fix.

CiviCRM Drupal extended API

I'm currently working on a project that is heavily involved with the inner workings of drupal and CiviCRM. In the course of this I am increasingly needing to write my own hooks and calls to CiviCRM from within a drupal module. I agree that the api could get bloated and full of unnecessary functions but the fact remains, I need the data. So begins my quest to extend the tentacles of a drupal module into CiviCRM.

Features that are currently either in progress or at a point where they do the job they should and are waiting for refinement. None of this code is security tested although I do my best to keep things safe from the script kiddies.

Membership signup / Contribution block - displays a link to the contribution page.
Upcoming events block - displays a simple list of upcoming events

Events additions:

  • Ability to add extra income and expenditure to an event.
  • General reports pages - tabs link to events reports and CiviCRM's Reports pages

To come are:

  • Augmented delegate lists - with printable badges.
  • Event register - like delegate list but allows you to keep track of attendees at the event - printable

I was hoping for a module or set of modules I could release but things are getting a little too bespoke at the moment. On the other hand, I can certainly provide the code for reference if anyone requests it. Just drop me an email or contact me from here and I'll send you a copy.

CiviCRM and Sage

I'm afraid this is a blog about a blog. I have a bit of a task ahead of me and part of which is integrating CiviCRM with Sage. I have a few questions I'd like to put to the community so the best place for it is to ask directly on the CiviCRM site. You can have a good read here

UPDATE:
It seems I have more things to write about so here is another blog about possible future development ideas for CiviCRM here

CiviCRM hook_page for drupal

While I'm not overly competent with CiviCRM I recently needed to work on the content of a page from within a drupal module. The hook system seems to be in active development but as of v2.1.2 it hasn't got to the point of exposing the template to the hook api. I added a few lines of code to enable this. I also files a feature request for version 2.2 so hopefully it'll be added for that.

During the meanwhilst here's my effort:

edit CRM/Core/Page.php

first add

<?php
require_once 'CRM/Utils/Hook.php';
?>
to the top of the doc.
next alter the run() method.

<?php
function run( )
    {
        if (
$this->_embedded ) {
            return;
        }

       
self::$_template->assign( 'mode'   , $this->_mode );
       
self::$_template->assign( 'tplFile', $this->getTemplateFileName() );

        if (
$this->_print ) {
            if (
$this->_print == CRM_Core_Smarty::PRINT_SNIPPET ||
                
$this->_print == CRM_Core_Smarty::PRINT_PDF ) {
               
$content = self::$_template->fetch( 'CRM/common/snippet.tpl' );
            } else {
               
$content = self::$_template->fetch( 'CRM/common/print.tpl' );
            }
            if (
$this->_print == CRM_Core_Smarty::PRINT_PDF ) {
                require_once
'CRM/Utils/PDF/Utils.php';
               
CRM_Utils_PDF_Utils::domlib( $content, "{$this->_name}.pdf" );
            } else {
                echo
$content;
            }
            exit( );
        }
       
$config =& CRM_Core_Config::singleton();
       
       
// Adding hook to drupal
       
CRM_Utils_Hook::pageProcess(self::$_template);
       
       
$content = self::$_template->fetch( 'CRM/common/'. strtolower($config->userFramework) .'.tpl' );
        echo
CRM_Utils_System::theme( 'page', $content, true, $this->_print );
        return;
    }
?>

Now we add a corresponding hook function in CRM/Utils/Hook.php
Add this before the end of the class.

<?php
static function pageProcess( &$page ) {
       
$config =& CRM_Core_Config::singleton( );
        require_once(
str_replace( '_', DIRECTORY_SEPARATOR, $config->userHookClass ) . '.php' );
       
$null =& CRM_Core_DAO::$_nullObject;
        return  
            eval(
'return ' .
                 
$config->userHookClass .
                 
'::invoke( 1, $page, $null, $null, $null, $null, \'civicrm_page\' );' );
    }
?>

And now in your module you can write your hook.

<?php
function MODULE_NAME_civicrm_page(&$page) {
   
$someVar = "Hello World";
   
$page->assign("someVar", $someValue);
}
?>

The only thing remaining is to add this to the template which will look something like {$someVar}

IMPORTANT:
At this point we are not checking what page we are using. I suggest looking at the smartyDebug=1 info to find out what makes your page unique and check for it OR just use the arg() from drupal to get your url. This method will add the var to all pages of your site although it will not be displayed unless it is added to you template.

Using FCKeditor and Image Assist from Drupal

Firstly, congratulations all the book sprinters. You did well. I'll be having a good read of it this weekend.

Before I delve into some code I'd like to give you my motivation for doing what I'm about to describe.

I need image uploading, ease of use and consistency. I also don't want to bother with maintaining 2 versions of fckeditor. I knew it shouldn't be too hard to get it working across to 2 systems and to be honest it wasn't. It took a little more to find out what the Image assist module needed though.

A nice feature of fckeditor is that we get a nice button in the editor toolbar instead of the link below the textarea as it would normally be. To get fckeditor and image assist working in drupal together you will need to copy img_assist_fckeditor.js from fckeditor module folder into img_assist folder. If you get a white empty popup screen then you have missed this step out.

On to some actual coding. Please make sure your drupal modules are all working before trying this for your own sanity's sake.

Rather than giving you chunks to piece together, here are the highlights and you can see the rest in the patch below.

I check for the Drupal userFramework and that it has fckeditor installed. We then alter the location of the fckeditor javascript files.
Later on I add the location to the drupal fckeditor's config file: fckeditor.config.js I altered this config file a bit to have some control over the buttons when used in a CiviCRM context.

Apart from setting a few more paths, that's all you need for fckeditor alone.

To add the Image Assist capability I had to trick it a bit. The standard thing to do for civicrm and fckeditor is to have a hidden input in the form but I needed a hidden (display:none) textarea as part of the javascript checking makes sure the element exists and is hidden by the sylesheet. The only other thing is that cunningly hidden <a> tag. I had to do this because the href is read by the javscript of fckeditor's toolbar button and it also points the image assist's output to the correct editor window.

Although I only did this today, I'm not sure why I added: $this->Config['TextareaID'] = $name; If someone finds out if it does anything in this situation then feel free to comment and I'll update if needed.

Now all you need is the patch and my additions to the drupal config file.

Patch for civicrm/packages/HTML/QuickForm/fckeditor.php

Index: fckeditor.php
===================================================================
--- fckeditor.php (revision 990)
+++ fckeditor.php (revision 1494)
@@ -187,33 +188,75 @@
             if (!defined('HTML_QUICKFORM_FCKEDITOR_LOADED')) {               
                 // load FCKeditor
                 $config = CRM_Core_Config::singleton( );
-                $html  = sprintf(
-                    '<script type="text/javascript" src="%s"></script>',
-                    $config->resourceBase . $this->BasePath . 'fckeditor.js'
-                );               
+                if($config->userFramework == 'Drupal' && module_exists('fckeditor')) {
+                   $html  = sprintf(
+                     '<script type="text/javascript" src="%s"></script>',
+                     drupal_get_path('module', 'fckeditor') . '/fckeditor/fckeditor.js');
+                } else {
+                 $html  = sprintf(
+                     '<script type="text/javascript" src="%s"></script>',
+                     $config->resourceBase . $this->BasePath . 'fckeditor.js'
+                 );     
+                }         
                 define('HTML_QUICKFORM_FCKEDITOR_LOADED', true);
             }
-
-            $config =& CRM_Core_Config::singleton();
-
-            // make link for iframe src
-            $link = $config->resourceBase . $this->BasePath . 'editor/fckeditor.html?InstanceName=' . $name;
-
-            if (strlen($this->ToolbarSet)) {
-                $link .= '&amp;Toolbar=' . $this->ToolbarSet;
+ $config =& CRM_Core_Config::singleton();
+
+ if($config->userFramework == 'Drupal' && module_exists('fckeditor')) {
+ global $base_url;
+ $fckPath = drupal_get_path('module', 'fckeditor');
+ // make link for iframe src
+             $link = '/' . $fckPath . '/fckeditor/editor/fckeditor.html?InstanceName=' . $name;
+            
+             if(module_exists('img_assist')) {
+             drupal_add_js(drupal_get_path('module', 'img_assist') .'/img_assist.js');
+            
+             drupal_add_js($fckPath .'/fckeditor/fckeditor.js');
+             drupal_add_js($fckPath .'/fckeditor.utils.js');
+             $html .= sprintf('<a href="%s" id="%s" style="display: none;"
+             />Add Image</a>',
+                              htmlspecialchars($base_url . '/img_assist/load/fckeditor?textarea=' . $name),
+                              'img_assist-link-' . $name, $name);
+                  $this->Config['TextareaID'] = $name;
+             }
+            
+             $link .= '&amp;Toolbar=' . 'DrupalCiviCRM';
+            
+             // render the linked hidden field
+             $html .= sprintf('<input type="textarea" id="%s" name="%s" value="%s" style="display: none;"/>',
+                              $name, $name, htmlspecialchars($this->getValue()));
+            
+             $this->Config['CustomConfigurationsPath'] = '/' . $fckPath . '/fckeditor.config.js';
+             // render the config hidden field
+             $html .= sprintf('<input type="hidden" id="%s" name="%s" value="%s" />',
+                              $cname, $cname, $this->_getConfigFieldString());
+              // render the editor iframe
+             $html .= sprintf(
+                 '<iframe id="%s" src="%s" width="%s" height="%s" framborder="no" scrolling="no"></iframe>',
+                 $name . '___Frame', $link, $this->Width, $this->Height
+             );
+
+ } else {
+
+             // make link for iframe src
+             $link = $config->resourceBase . $this->BasePath . 'editor/fckeditor.html?InstanceName=' . $name;
+
+             if (strlen($this->ToolbarSet)) {
+                 $link .= '&amp;Toolbar=' . $this->ToolbarSet;
+             }
+
+             // render the linked hidden field
+             $html .= sprintf('<input type="hidden" id="%s" name="%s" value="%s" />',
+                              $name, $name, htmlspecialchars($this->getValue()));
+             // render the config hidden field
+             $html .= sprintf('<input type="hidden" id="%s" name="%s" value="%s" />',
+                              $cname, $cname, $this->_getConfigFieldString());
+              // render the editor iframe
+             $html .= sprintf(
+                 '<iframe id="%s" src="%s" width="%s" height="%s" framborder="no" scrolling="no"></iframe>',
+                 $name . '___Frame', $link, $this->Width, $this->Height
+             );
             }
-
-            // render the linked hidden field
-            $html .= sprintf('<input type="hidden" id="%s" name="%s" value="%s" />',
-                             $name, $name, htmlspecialchars($this->getValue()));
-            // render the config hidden field
-            $html .= sprintf('<input type="hidden" id="%s" name="%s" value="%s" />',
-                             $cname, $cname, $this->_getConfigFieldString());
-             // render the editor iframe
-            $html .= sprintf(
-                '<iframe id="%s" src="%s" width="%s" height="%s" framborder="no" scrolling="no"></iframe>',
-                $name . '___Frame', $link, $this->Width, $this->Height
-            );
             return $html;
         }
     }

Additions to drupal's modules/fckeditor/fckeditor.config.js

//This toolbar should work fine with &quot;Filtered HTML&quot; filter
FCKConfig.ToolbarSets[&quot;DrupalCiviCRM&quot;] = [
['Source'],
['Cut','Copy','Paste','PasteText','PasteWord'],
['Undo','Redo','-','Find','Replace','-','SelectAll','RemoveFormat'],
/*
* EXPERIMENTAL
* Uncomment the line below to enable linktonode and linktomenu buttons
* ATTENTION: Link to Content module must be installed first!
* Remember to load appropriate plugins with FCKConfig.Plugins.Add command a couple of lines above
*/
//['Link','Unlink','LinkToNode','LinkToMenu','Anchor'],
['Link','Unlink','Anchor'],
['Image','Flash','Table','Rule','Smiley','SpecialChar'],
'/',
['FontFormat'],
['Bold','Italic','Underline','StrikeThrough','-','Subscript','Superscript'],
['OrderedList','UnorderedList','-','Outdent','Indent'],
//as of FCKeditor 2.5 you can use also 'Blockquote' button
//['OrderedList','UnorderedList','-','Outdent','Indent','Blockquote'],
['JustifyLeft','JustifyCenter','JustifyRight'],
//uncomment this line to enable the page break button
//remember to load appropriate plugin with FCKConfig.Plugins.Add command a couple of lines above
//['JustifyLeft','JustifyCenter','JustifyRight','DrupalBreak','DrupalPageBreak'],
] ;

And that's it. Please let me know what you think. The diff was run directly against the file in question and not from the civicrm installation root.

Various CiviCRM posts

I think some of the work I am doing for CiviCRM is getting lost amongst the myriad pages in the civicrm.org's site. I'll try and define a few links here to important things I have been working on or are essential for Civi development.

http://forum.civicrm.org/index.php?topic=7239 A new way of getting and using user data
http://civicrm.org/node/503 Things I think the event system is missing
http://civicrm.org/node/511 Placeholder participant names - for PA's to use etc.
http://civicrm.org/node/524 Waiting Lists - this is making it into core.

Membership Price Sets

I had to add some price sets to our recent project and I think I managed to get both the existing system and the price set system to work side by side. To give you an idea of why I had to do it, think of this; you have a specific membership open to organisations, the amount that the organisation needs to pay to join is based on its size. A company of 2-4 people will pay less than a 300+ employee company.

How did I do it? I added a system similar (and borrowed code) from the events system. I used the custom fields and not the price sets as defined in events. When I had the prices available I had to implement a complicated radio button system (complicated in code not usability) to get the price chosen. This means that if a price set is available, it will be shown and can be selected as a sub set of the main membership options.

I guess the best thing to illustrate this may be a picture. We haven't finished the theme yet so here is a VERY small glimpse of what we have set up.