Aug 06 2013
Aug 06

Traditionally the open source Drupal CMS has been the mainstay of the corporate / NGO and charity market since it lends itself perfectly to the rapid development of websites in those sectors. It also has traction for social media sites, being possibly the best equipped CMS using contributed modules for that functionality; and in addition for news portals and blog networks too.

TottenhamTurfies
Today sees the launch of the Tottenham Hotspur http://www.tottenhamtufies.com gaming and gamification platform aimed at children aged between 7 and 10 years old. The site's underlying architecture is Drupal 7 which may not on initial consideration be the most obvious choice. The site operates in essence on a single url and presents overlays to the users to extend the experience and to play the games.

In fact Drupal met the requirements perfectly. Drupal out-of-the-box comes with multi-user, multi-role capability – an essential requirement for a site that needs different levels of membership for gated content, along with back-end administration to create the content. Drupal's built-in concept of content types is ideal for these administrators to create the competitions, quizzes, quick polls and football predictions for the children, administered using a dashboard created with the Panels module.

Connectivity to external API web services is easily constructed with supporting helper modules and configuration credentials can be set by extending the superadmin menu scheme making the flip between sandbox and production services a breeze.

The Drupal database schema is extensible and this is the bedrock of the gamification logic. The gamification needed to cater for leaderboards, experience points, coins, medals (default blue and also a hierarchy of bronze, silver, gold), trophies, levelling up and asset unlocks upon achievement. The site also introduces the concept of 'promocodes' – unique alpha-numeric combinations of the format A2B-C3D-EF4 (with O0LI1 removed from the set of valid characters / numbers to avoid confusing the children) which can be used in an offline or online context to redeem against points, coins, trophies and medals. So, a child awarded a unique promocode for being a ball-boy at a game at White Hart Lane can type in the code on the site and have a trophy added to his trophy cabinet. The entire gamification business logic was complex but using object oriented PHP (PHP being Drupal's native language) reduced the chore of the activity.

Drupal's session management enables logged in users to have their progress restored automatically when re-opening a browser. Its internal API allowed the construction of the Ajax front-end back-end handshaking mechanism so for instance a call from the front-end to the back-end to delete a user's email will trigger a whole sequence of events including gamification checks (has the user been awarded points for keeping their inbox tidy? And as a consequence of that have they won a medal or levelled up?) and subsequence database updates that would be reflected and reported back to the front-end.

Throughout the lifespan of the project I was adamant that, whist Drupal was a somewhat unconventional choice in the first instance, it has proven itself and my belief that choice was correct has never wavered. Hopefully other industry professionals will have the courage to select Drupal as their choice of architecture for their avant-garde project too!

Badzilla is Hangar Seven's Consultant Technical Director

Jun 08 2013
Jun 08

Subsequent to my earlier blog on installing and setting up the commercial InnoCompany Drupal 7 theme, it is now time to do something useful - set up the Piecemaker 3D slider which is based on Flash animation, with an XML configuration file. Thankfully a lot of the work has already been bundled into the InnoCompany theme and there isn't that much to do. Principally the slider images, captions and links are included in theme configuration, and there are 10 available placeholders which means the carousel may have up to 10 images. Once this has been saved as theme settings it is retrieved in template.php->icomp_get_slider_html()->icomp_prepare_piecemaker() and the XML file is written out for the Flash file to process.

Config1
Go to admin/appearance/settings/icompany and scroll down to the Slider setting. Change the selection to Piecemaker Slider

Config2
Click on the next tab down and start populating each image in the carousel. Note well that you need to make your images 1050px by 400px and not the 1170x450px as prompted. The dimensions suggested are for the other slider types included in the theme that span the entire web page width. Piecemaker 3D needs to sit inside its own div containers. Furthermore, the ratios are slightly different as I discovered to my cost Sad Now the fields - I copied my images to a directory below the standard image area - /sites/default/images/slider. The target link for the image should be a valid landing page, and ideally pertinent to the image you are showing. In my case, it's the portfolio narrative item for the experience the image is showing.

Complete
If you now save your carousel settings then navigate to your homepage (remember to flush your browser's cache also) you should see your completed slider. However, there is a bug we still need to deal with...

Bug
Clicking on Info shows the heading and caption associated with the image. However, there is a bug in the previously mentioned template.php file - when it writes out the XML file it uses the H2 tag for the heading text and not H1. This can be fixed by either editing the template file or the css file. I elected to edit the css file...

Change the /sites/all/themes/icompany/sliders/piecemaker/piecemaker.css file

/* CSS Document */

H2 {
        font-family: Verdana;
        font-style: bold;
        font-weight: normal;
        color: #222222;
        display: block;
        font-size: 20px;
        margin-bottom: 10px;
        line-height: 30;
        text-align: left;
        letter-spacing: 0px;
}


Note how I've changed the H1 to H2 so the XML finds the correct tag. You can take this opportunity to edit the tag to how you want it output in the info box.

That's all there is to it Smile

Jun 02 2013
Jun 02

This is a tutorial for installing and setting up the commercial InnoCompany Theme that retails for around $45 in the US (£32 or so in the UK). The theme offers a multi-purpose corporate solution, but my own use is to create a portfolio site. I consider myself a back-end Drupal dev first and foremost, and my feeling is it would take me too long to create a neat portfolio site (which must of course be Drupal) from scratch. That effort would detract from my paid work for my clients and the pro bono weekend work I undertake for charities.

Despite the theme being constructed by WorthAPost, payment is I actually made to themeforest. Once the theme is bought it can be downloaded as a zip file.

To follow this tutorial, the following assumptions are made:

  • You have already set up a LAMP stack on your rig with a working Apache server and PHP and MySQL
  • You have Drush installed
  • You have an understanding of Linux command line
  • You have at least a passing knowledge of Drupal 7

Ok, so firstly navigate to the innocompany download which under most circumstances will by either in your home downloads directory, or in my case /tmp.

[email protected]:~> cd /tmp
[email protected]:/tmp> ls -las *.zip
29452 -rw-r--r-- 1 badzilla users 30158778 May 31 21:40 themeforest-3177223-innocompany-multipurpose-corporate-drupal-theme.zip
[email protected]:/tmp>


The zip file expands into the current directory (nasty and messy) so create a subdirectory and move the zip file into it, then unzip

[email protected]:/tmp> mkdir innocompany
[email protected]:/tmp> cd innocompany/
[email protected]:/tmp/innocompany> cp ../themeforest-* .
[email protected]:/tmp/innocompany> unzip themeforest-*


The zip file expands to quite a complicated file structure. It ships with features and views to minimise the effort of setting up the theme since these are dependencies. The entire directory tree is shown below.

[email protected]:/tmp/innocompany> find ./ -type d | sed -e 's/[^-][^\/]*\//--/g;s/--/ |-/'
|-
|-theme_with_demo_installation
|---compressed_database
|-standalone_theme
|---icompany
|-----forum
|-----js
|-----sliders
|-------iview
|---------js
|---------img
|---------css
|-----------skin1
|-----------skin4
|-------piecemaker
|---------scripts
|-----------swfobject
|-------flex
|---------images
|-------nivo
|---------themes
|-----------bar
|---------images
|-------elastic
|---------js
|---------css
|---------images
|-----simplenews
|-----img
|-------slides
|-------icons
|---------48
|---------picons
|-------bg
|-----views
|-----css
|-------images
|---------ie6
|-----cache
|-----fonts
|-----includes
|-licensing
|-Documentation
|---img
|---bootstrap
|-----js
|-----img
|-----css
|-psd
|-modules
|---features
|-----tests
|-----includes
|-----theme
|---jquery_update
|-----replace
|-------ui
|---------ui
|-----------minified
|-------------i18n
|-----------i18n
|---------themes
|-----------base
|-------------minified
|---------------images
|-------------images
|---------external
|-------jquery
|---------1.5
|---------1.7
|---------1.8
|-------misc
|---------1.7
|---token
|-----tests
|---icompany_config
|---views
|-----views_export
|-----tests
|-------test_plugins
|-------taxonomy
|-------user
|-------node
|-------plugins
|-------templates
|-------styles
|-------field
|-------handlers
|-------comment
|-----js
|-----plugins
|-------export_ui
|-------views_wizard
|-----css
|-------ie
|-----help
|-------images
|-----handlers
|-----includes
|-----drush
|-----modules
|-------translation
|-------taxonomy
|-------user
|-------filter
|-------node
|-------locale
|-------aggregator
|-------search
|-------system
|-------profile
|-------tracker
|-------field
|-------statistics
|-------book
|-------contact
|-------comment
|-----theme
|-----images
|---icompany_module
|-----.settings
|---ctools
|-----page_manager
|-------js
|-------plugins
|---------task_handlers
|---------tasks
|---------cache
|-------css
|-------help
|-------theme
|-------images
|-----ctools_ajax_sample
|-------js
|-------css
|-------images
|-----tests
|-------ctools_export_test
|-------plugins
|---------not_cached
|---------cached
|-----bulk_export
|-----js
|-----ctools_plugin_example
|-------plugins
|---------content_types
|---------access
|---------relationships
|---------arguments
|---------contexts
|-------help
|-----stylizer
|-------plugins
|---------export_ui
|-----plugins
|-------content_types
|---------entity_context
|---------term_context
|---------block
|---------node_form
|---------node_context
|---------token
|---------page
|---------custom
|---------node
|---------search
|---------form
|---------user_context
|---------vocabulary_context
|---------contact
|---------comment
|-------access
|-------relationships
|-------export_ui
|-------arguments
|-------cache
|-------contexts
|-----ctools_custom_content
|-------plugins
|---------export_ui
|-----views_content
|-------plugins
|---------content_types
|---------views
|---------relationships
|---------contexts
|-----css
|-----ctools_access_ruleset
|-------plugins
|---------access
|---------export_ui
|-----help
|-----includes
|-----drush
|-----images
|---jcarousel
|-----js
|-----skins
|-------default
|-------tango
|-----includes


Next you will need to create a MySQL database for your site. I tend to do this on the command line - a habit I've got into.
[email protected]:/tmp/innocompany> mysql -u root
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 181
Server version: 5.5.28-log Source distribution

Copyright (c) 2000, 2012, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> CREATE DATABASE portfolio;
database created
mysql> GRANT ALL PRIVILEGES ON portfolio.* TO 'database_username'@'localhost' WITH GRANT OPTION;
Query OK, 0 rows affected (0.77 sec)
mysql> quit
Bye


You will need to change database_username to whatever value you are using.
Next we need to create the document root for the website - and this is where the convention varies between Linux distros. Since I'm using openSUSE I placed my website under /srv/www/htdocs/ but I know other distros such as the immensely popular Ubuntu uses /var/www/html/. Pick whichever is appropriate for your rig. Whichever you choose, ensure that:
  • Apache user and group has write access to the document root
  • The user you use at the command line is a member of the Apache group and the directory has group write permissions
[email protected]:/tmp/innocompany> cd /srv/www/htdocs
[email protected]:/srv/www/htdocs> drush dl drupal
Project drupal (7.22) downloaded to /srv/www/htdocs/drupal-7.22.                                                 [success]
Project drupal contains:                                                                                         [success]
- 3 profiles: minimal, testing, standard
- 4 themes: seven, garland, bartik, stark
- 47 modules: comment, syslog, menu, shortcut, simpletest, contact, trigger, book, statistics, field_ui, update,
dblog, contextual, list, options, field_sql_storage, text, number, field, openid, path, image, tracker, help,
profile, system, search, aggregator, rdf, locale, node, filter, poll, dashboard, color, block, blog, user,
overlay, taxonomy, forum, translation, file, php, toolbar, drupal_system_listing_compatible_test,
drupal_system_listing_incompatible_test

[email protected]:/srv/www/htdocs> mv drupal-7.22 portfolio
[email protected]:/srv/www/htdocs>


Note how I use drush to download the latest version of Drupal and then since it creates the directory under the name of the latest version of Drupal, I rename it to portfolio since that's what I want my document root to be called.

You can now install the Drupal download, and this can be effected by using Drush. Change directory to inside portfolio and execute the following command below. It is unimportant what you specify for the Drupal user 1 values since they will be overwritten shortly when we create the InnoCompany demo data. However, you will need to change your database username, password and db name. Also I've had problems in the past with Drupal understanding localhost so I always use 127.0.0.1 instead.

[email protected]:/srv/www/htdocs> cd portfolio
[email protected]:/srv/www/htdocs/portfolio> drush site-install standard --account-name=admin --account-pass=admin --db-url=mysql://your_username:[email protected]/your_db_name
You are about to create a sites/default/files directory and create a sites/default/settings.php file and DROP all tables in your 'portfolio' database. Do you want to continue? (y/n): y
Starting Drupal installation. This takes a few seconds ...                                                       [ok]
Installation complete.  User name: admin  User password: admin                                                   [ok]
[email protected]:/srv/www/htdocs/portfolio>


Now it's almost time to check you have a functioning website - but that will need a change to the Apache configuration first. Here your mileage will vary dependent upon your setup. I am adding to the httpd.conf file (not necessarily a good idea since changes could be lost in a system upgrade), and your configuration could be different.
/etc/apache2/httpd.conf
<VirtualHost *:80>
DocumentRoot "/srv/www/htdocs/portfolio"
ServerName portfolio.localhost
        ServerAlias portfolio.localhost
RewriteEngine On
RewriteOptions Inherit
</VirtualHost>

<Directory /srv/www/htdocs/portfolio>
Options -Indexes +FollowSymLinks
AllowOverride All
Order allow,deny
Allow from all
</directory>


Next an entry to the hosts file is required to ensure you can actually point a browser at your site.
/etc/hosts

127.0.0.1 portfolio.localhost


Now it's time for an Apache reboot

[email protected]:/srv/www/htdocs/portfolio> sudo /etc/init.d/apache2 restart
redirecting to systemctl
[email protected]:/srv/www/htdocs/portfolio>

Site-Install
By pointing a browser to portfolio.localhost you should be able to see the website, albeit without (as yet) the InnoCompany theme and functionality

I noticed that the Webform module is not listed in the directory tree above, yet the site does use it for the contact form. So this needs to be added.

[email protected]:/srv/www/htdocs/portfolio> drush dl webform
Project webform (7.x-3.19) downloaded to /srv/www/htdocs/portfolio/sites/all/modules/webform.                    [success]
[email protected]:/srv/www/htdocs/portfolio> drush en webform -y
The following extensions will be enabled: webform
Do you really want to continue? (y/n): y
webform was enabled successfully.                                                                                [ok]
[email protected]:/srv/www/htdocs/portfolio>


Now let's copy over the modules from the InnoCompany directory tree.

[email protected]:/srv/www/htdocs/portfolio> cd sites/all/modules/
[email protected]:/srv/www/htdocs/portfolio/sites/all/modules> ls
README.txt  webform
[email protected]:/srv/www/htdocs/portfolio/sites/all/modules> cp -R /tmp/innocompany/modules/* .
[email protected]:/srv/www/htdocs/portfolio/sites/all/modules> ls
ctools  features  icompany_config  icompany_module  jcarousel  jquery_update  README.txt  token  views  webform
[email protected]:/srv/www/htdocs/portfolio/sites/all/modules>


The theme is situated in the standalone_theme directory in the tree, in a zip file. So unzip this into sites/all/themes as per the Drupal convention.

[email protected]:/srv/www/htdocs/portfolio/sites/all/modules/icompany_config> cd ../../themes/
[email protected]:/srv/www/htdocs/portfolio/sites/all/themes> unzip /tmp/innocompany/standalone_theme/icompany.zip
Archive:  /tmp/innocompany/standalone_theme/icompany.zip
   creating: icompany/
  inflating: icompany/block--footer1.tpl.php 
  inflating: icompany/block--footer2.tpl.php 
etc.....


In the Documentation directory in the tree there is a file called docs.html that unsurprisingly contains useful configuration information. So copy this to the document root along with its supporting img directory files BUT REMEMBER TO DELETE THIS FILE BEFORE HOSTING YOUR WEBSITE!!

[email protected]:/srv/www/htdocs/portfolio/sites/all/themes> cd ../../..
[email protected]:/srv/www/htdocs/portfolio> cp /tmp/innocompany/Documentation/docs.html .
[email protected]:/srv/www/htdocs/portfolio> cp -R /tmp/innocompany/Documentation/img .


Ok so now we need to add the sql statements provided by WorthAPost - this will provide all the startup data to pre-populate our Drupal database. I've elected for the strategy of using the test data and amending to fit my circumstances rather than building from scratch. The test data db commands can be found in the directory tree at theme_with_demo_installation in a zipped file with a filename commencing InnoCompany-
[email protected]:/tmp/innocompany/theme_with_demo_installation/compressed_database> ls -lash
total 160K
4.0K drwxr-xr-x 2 badzilla users 4.0K Apr 20 23:16 .
4.0K drwxr-xr-x 3 badzilla users 4.0K Apr 20 23:19 ..
152K -rw-r--r-- 1 badzilla users 152K Apr 20 23:16 InnoCompany-2013-04-20T23-16-41.mysql.gz
[email protected]:/tmp/innocompany/theme_with_demo_installation/compressed_database> gunzip InnoCompany-2013-04-20T23-16-41.mysql.gz
[email protected]:/tmp/innocompany/theme_with_demo_installation/compressed_database> mysql -u your_db_username -p portfolio
Enter password:
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 193
Server version: 5.5.28-log Source distribution

Copyright (c) 2000, 2012, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> source InnoCompany-2013-04-20T23-16-41.mysql;
...etc...
Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

mysql>


There are a number of test images which belong in sites/default/files. These are buried quite deep in the InnoCompany filesystem and first need expanding out of an entire Drupal build and then copying to the correct place.

[email protected]:/srv/www/htdocs/portfolio> cd /tmp/innocompany/theme_with_demo_installation/
[email protected]:/tmp/innocompany/theme_with_demo_installation> unzip themeforest-3177223-innocompany-multipurpose-corporate-drupal-theme.zip
[email protected]:/srv/www/htdocs/portfolio> cp -R /tmp/innocompany/theme_with_demo_installation/sites/default/files/* sites/default/files/.
[email protected]:/srv/www/htdocs/portfolio>


Under Construction
Now refresh your browser and you will see the site appear, albeit the site in maintenance mode screen.

User
Navigate to /user and login with the credentials of Username: worthapost and password of worthapost. Once you are in, immediately change the username and the password by clicking on your username (top right) and then the Edit tab. You will also probably want to change the timezone setting at the same time.

docs
Take a look at /docs.html and you'll see the config page that came with the installation.

It is now worthwhile to perform some tidy-up activities, such as ensure that the permissions are set correctly on the filesystem, and update.php has been run.

[email protected]:/srv/www/htdocs/portfolio> sudo chown -R wwwrun:www *
root's password:
[email protected]:/srv/www/htdocs/portfolio> sudo chown -R wwwrun:www .*
[email protected]:/srv/www/htdocs/portfolio> sudo chmod -R ag+w *
[email protected]:/srv/www/htdocs/portfolio> drush updatedb
Operations on Unicode strings are emulated on a best-effort basis. Install the PHP mbstring extension for        [warning]
improved Unicode support. (Currently using Unicode library Standard PHP)
The following updates are pending:

webform module :
  7321 -   Remove files left over from deleted submissions. Such files are now deleted  automatically.

Do you wish to run all pending updates? (y/n): y
Operations on Unicode strings are emulated on a best-effort basis. Install the PHP mbstring extension for        [warning]
improved Unicode support. (Currently using Unicode library Standard PHP)
Performed update: webform_update_7321                                                                            [ok]
Finished performing updates.                                                                                     [ok]
[email protected]:/srv/www/htdocs/portfolio>


You should now have a fully functioning website. There is now plenty to do since you need to replace all the test data with your own, and your basic site details (such as name, email address etc) should be set by going to admin/config/system/site-information. All these activities can be worked on in the fullness of time, and I will be providing more tutorials to aid that process as I build my own portfolio site.

Complete

Mar 04 2013
Mar 04

This module extends the existing user_restrictions module by providing a mechanism for the bulk import of prohibited words in the screen name during user registration. This is useful to ensure that on a community site no users attempt to create accounts with offensive names. Without this module, any admin wishing to add a few hundred words to the user_restrictions list would face quite an onerous chore to type them all in. With the module, they can be cut and pasted from an existing source into a textarea in one go. The textarea allows for carriage return / linefeeds and will also remove commas between words.

The module was very easy to put together, so I don't believe there is anything requiring further expansion. If you disagree, drop me a line and I'll answer your questions. Smile

user_resitrictions_batch.module

<?php
/*
* File         : user_restrictions_batch.module
* Title        : Batch add of user restrictions
* Sponsor      : Hangar Seven Digital
* Author       : Badzilla <a href="http://www.badzilla.co.uk" title="www.badzilla.co.uk">www.badzilla.co.uk</a> @badzillacouk
*
* This work is copyright Badzilla under the GPL licence terms and conditions
*
*/

/**
* Implements hook_menu().
*/

function user_restrictions_batch_menu() { $edit_restrictions = array('edit user restriction rules');
   
$items = array(); $items['admin/config/people/user-restrictions/batch-input'] = array(
           
'title' => 'Batch input ',
           
'page callback' => 'drupal_get_form',
           
'page arguments' => array('user_restrictions_batch_input_form', 4),
           
'access arguments' => $edit_restrictions,
           
'type' => MENU_LOCAL_ACTION,
    );

    return

$items;
}

function

user_restrictions_batch_input_form($form, &$form_state) { $form = array(); $form['banned'] = array(
       
'#type' => 'textarea',
       
'#title' => t('Enter prohibited words'),
       
'#required' => TRUE,
       
'#weight' => 0
   
);
   
   
$form['submit'] = array(
       
'#type' => 'submit',
       
'#value' => t('Save rule'),
    );

    return

$form;   
}

function

user_restrictions_batch_input_form_submit($form, &$form_state) { $lines = explode("\r\n", $form_state['values']['banned']);
    if (
count($lines))
        foreach(
$lines as $line) {
           
$phrases = explode(",", $line);

            if (

count($phrases))
                foreach(
$phrases as $phrase)
                    if (
$phrase = trim($phrase))
                       
db_insert('user_restrictions')
                            ->
fields(array('urid' => NULL, 'mask' => $phrase, 'type' => 'name', 'subtype' => '', 'status' => 0, 'expire' => 0))
                            ->
execute();
        }   
}
?>

user_resitrictions_batch.info

name = User restrictions_batch
description = Brute force batch insertion of restricted words.
core = 7.x
project = "user_restrictions"


Attachment Size user_restrictions_batch.tar.gz 1.06 KB

Feb 03 2013
Feb 03

My magcat module for cataloguing magazines, which compliments my bookcat module (which is a dependency) has been promoted to alpha 2. To get the best use of the module you will need a copy of the MS Windows FNProgramvare's BookCAT product and create your MS Access databases there before importing into MySQL.

Follow the link above to download the latest version.

Jan 04 2013
Jan 04

This module adds a configurable limit to how many nodes of any content type can be published at any one time. This is useful in circumstances such as when a website has created content types of polls or competitions or quizzes but only one of each type can be live and published at any one time. Of course the module allows limits other than 1 in case there is such a circumstance. The default is 0 which means unlimited. A web author may create content and exceed the limits providing the content remains unpublished. This allows for content to be created in advance of publication.

If a web author attempts to publish some content which would exceed the limit, the content is saved as unpublished.

The admin should go to admin/config/content/node_limit_publish to set the limits for each content type known to the system.

node_limit_publish.info

name = Node Limit Publish
description = Limit the number of published nodes per type
core = 7.x
configure = admin/config/content/node_limit_publish

node_limit_publish.module

<?php
/*
* File         : node_limit_publish.module
* Title        : Limits the number of concurrently published node types dependent upon admin configurable limits
* Sponsor      : Hangar Seven Digital
* Author       : Badzilla <a href="http://www.badzilla.co.uk" title="www.badzilla.co.uk">www.badzilla.co.uk</a> @badzillacouk
*
* This work is copyright Badzilla under the GPL licence terms and conditions
*
*/

/**
* Implementation of hook_menu().
*
*/

function node_limit_publish_menu() {
   
   
$items = array(); $items['admin/config/content/node_limit_publish'] = array(
       
'title' => 'Limit Number of Published Nodes per Node Type',
       
'description' => t('Zero represents an unlimited amount of published nodes'),
       
'page callback' => 'drupal_get_form',
       
'page arguments' => array('node_limit_publish_admin_settings'),
       
'access arguments' => array('administer node_limit_publish'),
       
'type' => MENU_NORMAL_ITEM,
    );
   
    return
$items;
}

function

node_limit_publish_admin_settings() {
   
   
$form = array();

    if (

is_array($types = node_type_get_types())) { $form['title'] = array(
           
'#markup' => t('Zero represents an unlimited amount of published nodes'),
        );

        foreach(

$types as $key => $value)
           
$form['node_limit_publish_'.$key] = array(
               
'#type' => 'textfield',
               
'#description' => $key,
               
'#size' => 4,
               
'#maxlength' => 10,
               
'#element_validate' => array('node_limit_publish_is_numeric'),
               
'#default_value' => variable_get('node_limit_publish_'.$key, 0),               
            );
    }

    return

system_settings_form($form);
}

function

node_limit_publish_is_numeric($element, &$form_state, $form) {
   
    if (!
is_numeric($element['#value']))
       
form_error($element, t('This field must be numeric'));
}
/**
* Implementation of hook_presave().
*
*/
function node_limit_publish_node_presave($node) {
   
   
// Get the limit on this type
   
if (($limit = variable_get('node_limit_publish_'.$node->type, 0)) and $node->status == 1) {
       
// now check whether we have reached our maximum
       
$query = db_select('node')
            ->
condition('type', $node->type)
            ->
condition('status', 1);
        if (isset(
$node->nid))
           
$query->condition('nid', $node->nid, '!=');
       
$count = $query->countQuery()
            ->
execute()
            ->
fetchField();

        if (

$count >= $limit) {
           
$node->status = 0;
           
drupal_set_message(t('Sorry, the maximum amount of published content for %type has already been reached.', array('%type' => $node->type)), 'warning');
        }
    }
}
?>

Attachment Size node_limit_publish_02.tar.gz 1.41 KB

Dec 24 2012
Dec 24
;;;;;;;;;;;;;;;;;;;;;
; FPM Configuration ;
;;;;;;;;;;;;;;;;;;;;;

; All relative paths in this configuration file are relative to PHP's install
; prefix (/usr). This prefix can be dynamicaly changed by using the
; '-p' argument from the command line.

; Include one or more files. If glob(3) exists, it is used to include a bunch of
; files from a glob(3) pattern. This directive can be used everywhere in the
; file.
; Relative path can also be used. They will be prefixed by:
;  - the global prefix if it's been set (-p arguement)
;  - /usr otherwise
;include=etc/fpm.d/*.conf

;;;;;;;;;;;;;;;;;;
; Global Options ;
;;;;;;;;;;;;;;;;;;

[global]
; Pid file
; Note: the default prefix is /usr/var
; Default Value: none
pid = /var/run/php-fpm.pid

; Error log file
; Note: the default prefix is /usr/var
; Default Value: log/php-fpm.log
error_log = /var/log/php-fpm/php-fpm.log

; Log level
; Possible Values: alert, error, warning, notice, debug
; Default Value: notice
log_level = error

; If this number of child processes exit with SIGSEGV or SIGBUS within the time
; interval set by emergency_restart_interval then FPM will restart. A value
; of '0' means 'Off'.
; Default Value: 0
;emergency_restart_threshold = 0

; Interval of time used by emergency_restart_interval to determine when
; a graceful restart will be initiated.  This can be useful to work around
; accidental corruptions in an accelerator's shared memory.
; Available Units: s(econds), m(inutes), h(ours), or d(ays)
; Default Unit: seconds
; Default Value: 0
;emergency_restart_interval = 0

; Time limit for child processes to wait for a reaction on signals from master.
; Available units: s(econds), m(inutes), h(ours), or d(ays)
; Default Unit: seconds
; Default Value: 0
;process_control_timeout = 0

; Send FPM to background. Set to 'no' to keep FPM in foreground for debugging.
; Default Value: yes
;daemonize = yes

; Set open file descriptor rlimit for the master process.
; Default Value: system defined value
;rlimit_files = 1024

; Set max core size rlimit for the master process.
; Possible Values: 'unlimited' or an integer greater or equal to 0
; Default Value: system defined value
;rlimit_core = 0

;;;;;;;;;;;;;;;;;;;;
; Pool Definitions ;
;;;;;;;;;;;;;;;;;;;;

; Multiple pools of child processes may be started with different listening
; ports and different management options.  The name of the pool will be
; used in logs and stats. There is no limitation on the number of pools which
; FPM can handle. Your system will tell you anyway :)

; Start a new pool named 'www'.
; the variable $pool can we used in any directive and will be replaced by the
; pool name ('www' here)
[www]

; Per pool prefix
; It only applies on the following directives:
; - 'slowlog'
; - 'listen' (unixsocket)
; - 'chroot'
; - 'chdir'
; - 'php_values'
; - 'php_admin_values'
; When not set, the global prefix (or /usr) applies instead.
; Note: This directive can also be relative to the global prefix.
; Default Value: none
;prefix = /path/to/pools/$pool

; The address on which to accept FastCGI requests.
; Valid syntaxes are:
;   'ip.add.re.ss:port'    - to listen on a TCP socket to a specific address on
;                            a specific port;
;   'port'                 - to listen on a TCP socket to all addresses on a
;                            specific port;
;   '/path/to/unix/socket' - to listen on a unix socket.
; Note: This value is mandatory.
listen = /tmp/phpfpm.sock

; Set listen(2) backlog. A value of '-1' means unlimited.
; Default Value: 128 (-1 on FreeBSD and OpenBSD)
;listen.backlog = -1

; List of ipv4 addresses of FastCGI clients which are allowed to connect.
; Equivalent to the FCGI_WEB_SERVER_ADDRS environment variable in the original
; PHP FCGI (5.2.2+). Makes sense only with a tcp listening socket. Each address
; must be separated by a comma. If this value is left blank, connections will be
; accepted from any ip address.
; Default Value: any
;listen.allowed_clients = 127.0.0.1

; Set permissions for unix socket, if one is used. In Linux, read/write
; permissions must be set in order to allow connections from a web server. Many
; BSD-derived systems allow connections regardless of permissions.
; Default Values: user and group are set as the running user
;                 mode is set to 0666
;listen.owner = nobody
;listen.group = nobody
;listen.mode = 0666

; Unix user/group of processes
; Note: The user is mandatory. If the group is not set, the default user's group
;       will be used.
user = nobody
group = nobody

; Choose how the process manager will control the number of child processes.
; Possible Values:
;   static  - a fixed number (pm.max_children) of child processes;
;   dynamic - the number of child processes are set dynamically based on the
;             following directives:
;             pm.max_children      - the maximum number of children that can
;                                    be alive at the same time.
;             pm.start_servers     - the number of children created on startup.
;             pm.min_spare_servers - the minimum number of children in 'idle'
;                                    state (waiting to process). If the number
;                                    of 'idle' processes is less than this
;                                    number then some children will be created.
;             pm.max_spare_servers - the maximum number of children in 'idle'
;                                    state (waiting to process). If the number
;                                    of 'idle' processes is greater than this
;                                    number then some children will be killed.
; Note: This value is mandatory.
pm = dynamic

; The number of child processes to be created when pm is set to 'static' and the
; maximum number of child processes to be created when pm is set to 'dynamic'.
; This value sets the limit on the number of simultaneous requests that will be
; served. Equivalent to the ApacheMaxClients directive with mpm_prefork.
; Equivalent to the PHP_FCGI_CHILDREN environment variable in the original PHP
; CGI.
; Note: Used when pm is set to either 'static' or 'dynamic'
; Note: This value is mandatory.
pm.max_children = 50

; The number of child processes created on startup.
; Note: Used only when pm is set to 'dynamic'
; Default Value: min_spare_servers + (max_spare_servers - min_spare_servers) / 2
pm.start_servers = 20

; The desired minimum number of idle server processes.
; Note: Used only when pm is set to 'dynamic'
; Note: Mandatory when pm is set to 'dynamic'
pm.min_spare_servers = 5

; The desired maximum number of idle server processes.
; Note: Used only when pm is set to 'dynamic'
; Note: Mandatory when pm is set to 'dynamic'
pm.max_spare_servers = 35

; The number of requests each child process should execute before respawning.
; This can be useful to work around memory leaks in 3rd party libraries. For
; endless request processing specify '0'. Equivalent to PHP_FCGI_MAX_REQUESTS.
; Default Value: 0
pm.max_requests = 500

; The URI to view the FPM status page. If this value is not set, no URI will be
; recognized as a status page. By default, the status page shows the following
; information:
;   accepted conn        - the number of request accepted by the pool;
;   pool                 - the name of the pool;
;   process manager      - static or dynamic;
;   idle processes       - the number of idle processes;
;   active processes     - the number of active processes;
;   total processes      - the number of idle + active processes.
;   max children reached - number of times, the process limit has been reached,
;                          when pm tries to start more children (works only for
;                          pm 'dynamic')
; The values of 'idle processes', 'active processes' and 'total processes' are
; updated each second. The value of 'accepted conn' is updated in real time.
; Example output:
;   accepted conn:        12073
;   pool:                 www
;   process manager:      static
;   idle processes:       35
;   active processes:     65
;   total processes:      100
;   max children reached: 1
; By default the status page output is formatted as text/plain. Passing either
; 'html', 'xml' or 'json' as a query string will return the corresponding output
; syntax. Example:
;   http://www.foo.bar/status
;
   http://www.foo.bar/status?json
;
   http://www.foo.bar/status?html
;
   http://www.foo.bar/status?xml
;
Note: The value must start with a leading slash (/). The value can be
;       anything, but it may not be a good idea to use the .php extension or it
;       may conflict with a real PHP file.
; Default Value: not set
;pm.status_path = /status

; The ping URI to call the monitoring page of FPM. If this value is not set, no
; URI will be recognized as a ping page. This could be used to test from outside
; that FPM is alive and responding, or to
; - create a graph of FPM availability (rrd or such);
; - remove a server from a group if it is not responding (load balancing);
; - trigger alerts for the operating team (24/7).
; Note: The value must start with a leading slash (/). The value can be
;       anything, but it may not be a good idea to use the .php extension or it
;       may conflict with a real PHP file.
; Default Value: not set
;ping.path = /ping

; This directive may be used to customize the response of a ping request. The
; response is formatted as text/plain with a 200 response code.
; Default Value: pong
;ping.response = pong

; The access log file
; Default: not set
;access.log = log/$pool.access.log

; The access log format.
; The following syntax is allowed
;  %%: the '%' character
;  %C: %CPU used by the request
;      it can accept the following format:
;      - %{user}C for user CPU only
;      - %{system}C for system CPU only
;      - %{total}C  for user + system CPU (default)
;  %d: time taken to serve the request
;      it can accept the following format:
;      - %{seconds}d (default)
;      - %{miliseconds}d
;      - %{mili}d
;      - %{microseconds}d
;      - %{micro}d
;  %e: an environment variable (same as $_ENV or $_SERVER)
;      it must be associated with embraces to specify the name of the env
;      variable. Some exemples:
;      - server specifics like: %{REQUEST_METHOD}e or %{SERVER_PROTOCOL}e
;      - HTTP headers like: %{HTTP_HOST}e or %{HTTP_USER_AGENT}e
;  %f: script filename
;  %l: content-length of the request (for POST request only)
;  %m: request method
;  %M: peak of memory allocated by PHP
;      it can accept the following format:
;      - %{bytes}M (default)
;      - %{kilobytes}M
;      - %{kilo}M
;      - %{megabytes}M
;      - %{mega}M
;  %n: pool name
;  %o: ouput header
;      it must be associated with embraces to specify the name of the header:
;      - %{Content-Type}o
;      - %{X-Powered-By}o
;      - %{Transfert-Encoding}o
;      - ....
;  %p: PID of the child that serviced the request
;  %P: PID of the parent of the child that serviced the request
;  %q: the query string
;  %Q: the '?' character if query string exists
;  %r: the request URI (without the query string, see %q and %Q)
;  %R: remote IP address
;  %s: status (response code)
;  %t: server time the request was received
;      it can accept a strftime(3) format:
;      %d/%b/%Y:%H:%M:%S %z (default)
;  %T: time the log has been written (the request has finished)
;      it can accept a strftime(3) format:
;      %d/%b/%Y:%H:%M:%S %z (default)
;  %u: remote user
;
; Default: "%R - %u %t \"%m %r\" %s"
;access.format = %R - %u %t "%m %r%Q%q" %s %f %{mili}d %{kilo}M %C%%

; The timeout for serving a single request after which the worker process will
; be killed. This option should be used when the 'max_execution_time' ini option
; does not stop script execution for some reason. A value of '0' means 'off'.
; Available units: s(econds)(default), m(inutes), h(ours), or d(ays)
; Default Value: 0
;request_terminate_timeout = 0

; The timeout for serving a single request after which a PHP backtrace will be
; dumped to the 'slowlog' file. A value of '0s' means 'off'.
; Available units: s(econds)(default), m(inutes), h(ours), or d(ays)
; Default Value: 0
;request_slowlog_timeout = 0

; The log file for slow requests
; Default Value: not set
; Note: slowlog is mandatory if request_slowlog_timeout is set
;slowlog = log/$pool.log.slow

; Set open file descriptor rlimit.
; Default Value: system defined value
;rlimit_files = 1024

; Set max core size rlimit.
; Possible Values: 'unlimited' or an integer greater or equal to 0
; Default Value: system defined value
;rlimit_core = 0

; Chroot to this directory at the start. This value must be defined as an
; absolute path. When this value is not set, chroot is not used.
; Note: you can prefix with '$prefix' to chroot to the pool prefix or one
; of its subdirectories. If the pool prefix is not set, the global prefix
; will be used instead.
; Note: chrooting is a great security feature and should be used whenever
;       possible. However, all PHP paths will be relative to the chroot
;       (error_log, sessions.save_path, ...).
; Default Value: not set
;chroot =

; Chdir to this directory at the start.
; Note: relative path can be used.
; Default Value: current directory or / when chroot
;chdir = /var/www

; Redirect worker stdout and stderr into main error log. If not set, stdout and
; stderr will be redirected to /dev/null according to FastCGI specs.
; Note: on highloaded environement, this can cause some delay in the page
; process time (several ms).
; Default Value: no
catch_workers_output = yes

; Pass environment variables like LD_LIBRARY_PATH. All $VARIABLEs are taken from
; the current environment.
; Default Value: clean env
;env[HOSTNAME] = $HOSTNAME
;env[PATH] = /usr/local/bin:/usr/bin:/bin
;env[TMP] = /tmp
;env[TMPDIR] = /tmp
;env[TEMP] = /tmp

; Additional php.ini defines, specific to this pool of workers. These settings
; overwrite the values previously defined in the php.ini. The directives are the
; same as the PHP SAPI:
;   php_value/php_flag             - you can set classic ini defines which can
;                                    be overwritten from PHP call 'ini_set'.
;   php_admin_value/php_admin_flag - these directives won't be overwritten by
;                                     PHP call 'ini_set'
; For php_*flag, valid values are on, off, 1, 0, true, false, yes or no.

; Defining 'extension' will load the corresponding shared extension from
; extension_dir. Defining 'disable_functions' or 'disable_classes' will not
; overwrite previously defined php.ini values, but will append the new value
; instead.

; Note: path INI options can be relative and will be expanded with the prefix
; (pool, global or /usr)

; Default Value: nothing is defined by default except the values in php.ini and
;                specified at startup with the -d argument
;php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f [email protected]
;php_flag[display_errors] = off
;php_admin_value[error_log] = /var/log/fpm-php.www.log
;php_admin_flag[log_errors] = on
;php_admin_value[memory_limit] = 32M

Oct 24 2012
Oct 24

I have been coming up against the same Drupal 7 requirement over and over recently - the need to have a form in a block with a separate template file I can pass over to the front-end guy to weave his magic. This is one of my easiest ever tutorials, and is really only here as an aide memoire for when I can't quite remember the exact syntax in say hook_theme or I can't remember which rendering functions I call in the template.

By using the module below, you have an excellent boilerplate to start your FAPI coding since this pattern repeats (as I have noted) over and over! Too easy!

fb_pattern.info

name = Form / Block Pattern
description = Boilerplate form in a block pattern code with template file
core = 7.x

fb_pattern.module

<?php
/*
* File         : fb_pattern.module
* Title        : Drupal 7 Boilerplate form in a block pattern with template file
* Author       : <a href="http://www.badzilla.co.uk" title="www.badzilla.co.uk">www.badzilla.co.uk</a> @badzillacouk
*
* This work is copyright BadZilla under the GPL licence terms and conditions
*
*/

/*
* Implements fb_pattern_info
*/

function fb_pattern_block_info() {
   
   
$block = array(); // Sign up block
   
$block['fb_pattern'] = array(
               
'info' =>   t('Form / Block Pattern Example'),
               
'weight' => 0); 

    return

$block;
}
/*
* Implements hook_block_view
*/
function fb_pattern_block_view($delta) {
   
   
$block = array();

    switch(

$delta) {
        case
'fb_pattern':
           
$block['title'] = t('Sample Form in a Block Pattern');
           
$block['content'] = drupal_get_form('fb_pattern_form');
            break;
    }

    return

$block;
}

function

fb_pattern_form($form, &$form_state) {
   
   
$form['age'] = array(
       
'#title' => t('Age'),
       
'#type' => 'textfield',
       
'#required' => TRUE,
       
'#maxlength' => 3,
       
'#size' => 3,
       
'#weight' => 0,
    );
$form['submit'] = array(
       
'#type' => 'submit',
       
'#value' => t('Submit'),
       
'#weight' => 10,
       
'#validate' => array('fb_pattern_form_validate'),
       
'#submit' => array('fb_pattern_form_submit'),
    );
$form['#theme'][] = 'fb_pattern_form';
  
    return
$form;
}

function

fb_pattern_form_validate($form, &$form_state) {
   
    if (!
is_numeric($form_state['values']['age']))
       
form_set_error('age', t('That doesn\'t look like a valid age to me!'));

}

function

fb_pattern_form_submit($form, &$form_state) {
   
   
drupal_set_message(t('Wow! You don\'t look :age - have you had work done?', array(':age' => $form_state['values']['age'])));
}
/*
* Implements hook_theme
*/
function fb_pattern_theme() {
   
    return array(
       
'fb_pattern_form' => array(
           
'arguments' => array('form' => NULL),
           
'render element' => 'form',
           
'template' => 'fb_pattern_form',
        ),
    );
}
?>

fb_pattern_form.tpl.php

<?php/*
* File         : fb_pattern_form.tpl.php
* Title        : Drupal 7 Boilerplate form in a block pattern with template file
* Author       : <a href="http://www.badzilla.co.uk" title="www.badzilla.co.uk">www.badzilla.co.uk</a> @badzillacouk
*
* This work is copyright BadZilla under the GPL licence terms and conditions
*
*/
?>

       

    <?php print render($form['age']); ?>

    <?php print render($form['submit']); ?>

    <?php print drupal_render_children($form); ?>


Attachment Size fb_pattern.tar.gz 1.06 KB

Jul 05 2012
Jul 05

This module provides integration between the Drupal Webform module and Paypal donation buttons. This includes the facility to receive payment status responses from Paypal and record these in the webforms as a hidden field for audit / inspection later to ascertain whether payments were successful or refused. The module should be useful for charities and NGOs as a mechanism to collect donations from their followers.

The module was developed as a commission from a well-known NGO. The NGO was already utilising a SECPAY gateway on their 'Donate Now' webform, and required a radio button in the donation process for the user to select either the SECPAY credit card / debit card direct payment, or a Paypal payment. As a consequence, this module presupposes a SECPAY gateway is being used. It should be relatively trivial for even a low experience PHP / Drupal developer to remove this constraint if necessary from my code so the only option is Paypal, or I can be contacted directly and undertake the work at a small cost.

Usage

To ensure the module works correctly, the steps identified below should be followed closely, paying particular attention when requested to do so.

Paypal Donation Button Creation

You will need a donation button from the Paypal site. It is also recommended you create a Paypal sandbox account for test purposes before you 'go live'. By doing this you can satisfy yourself everything is working fine before you commence your campaign.

The navigation around the Paypal site is inordinately complex. To create a button in the UK (may vary depending upon your territory), do:
1. Log into your Paypal account;
2. Click on "Merchant Services";
3. Click on "Website Payments Standard";
4. Click on "Create Button Now";
This will take you to the button factory. Once you have changed the button type to 'Donations' your Step 1 screen should be filled in like below. Enter reasonable text for the Organisation name/service and for the Donation ID values. This will be very useful in the future when you have a whole bunch of buttons and you are having trouble distinguishing between them all.



Paypal1

Paypal Donation Button Step 1
Step 3 of the process will look like the screen below. This is where you need to add you own site address to the two URL fields I have partially pixelated out. The first URL is the address the user will be returned to once payment from Paypal has been collected. The second URL is the 'secret' URL that Paypal will use to send the payment status information to your site.



Paypal1

Paypal Donation Button Step 3

At the end of the process you will get a link to the button in the format:
https: //www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=xxxxxxxxxxxxxx
if you are using Paypal, and a URL prefixing the word 'sandbox' before 'paypal' in the address if you are using the test Sandbox system. You should copy these into your clipboard for later.

Installation of the paypal_webform Module

I'm assuming you know how to install a Drupal module. Smile

paypal_webform.info

; $Id: $
name = Paypal Webform
description = Adds the Paypal donation colleciton method to the donations page
core = 6.x
dependencies[] = webform
php = 5.2

paypal_webform.module

<?php
// Copyright  <a href="http://www.badzilla.co.uk
//" title="www.badzilla.co.uk
//">www.badzilla.co.uk
//</a> Author: <a href="http://www.badzilla.co.uk" title="www.badzilla.co.uk">www.badzilla.co.uk</a> @badzillacouk
// All Drupal work undertaken - competitive hourly and daily rates
// 13/11/2011
// paypal_webform - Adds Paypal functionality to the single donation webform
define('PAYPAL_PRODUCTION', 0);
define('PAYPAL_SANDBOX', 1);

global

$paypal_webform;$paypal_webform = array(array('url' => 'https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=xxxxxxxxxxxxx',
                                                   
'listener' => 'ssl://www.paypal.com'),
                        array(
'url' => 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=xxxxxxxxxxxxx',
                                                   
'listener' => 'ssl://www.sandbox.paypal.com'),
                        );
// Change this value dependent upon whether production, sandbox, or running in test mode on badzilla
define('PAYPAL_SYSTEM', PAYPAL_PRODUCTION);/**
* Implementation of hook_form_alter().
*/
function paypal_webform_form_alter($form, &$form_state, $form_id) { // locate the webform id and override
   
if (strpos($form_id, 'webform_client_form_') === FALSE)
        if (!
$form_state['webform']['component_tree']['children'])
            return;
$is_pay = FALSE;
    foreach(
$form_state['webform']['component_tree']['children'] as $child)
        if (
$child['type'] == 'pay') {
           
$is_pay = TRUE;
            break;
        }

    if (!

$is_pay)
        return;
// check whether there is a Paypal widget on this form
   
if (!preg_match("#webform_client_form_(\d+)$#", $form_id, $match))
        return;

    if (!

db_result(db_query("SELECT COUNT(*) FROM {webform_component} WHERE nid = %d AND form_key = 'payment_method'", $match[1])))
        return;
// add theme
   
$form['submitted']['payment_method']['#theme'] = 'paypal_webform_radio'; // Knock out the title and use our own
   
$form['submitted']['payment_method']['#title'] = ''; // after build so we can see which radio button was selected
   
$form['#after_build'][] = 'paypal_webform_form_after_build'; // add paypal submission
   
array_push($form['#submit'], 'paypal_webform_submit_redirect'); //dsm($form, __FUNCTION__);}

function

paypal_webform_form_after_build($form, &$form_state) { //dsm($_SESSION); if (isset($form['#post']['submitted']['payment_method']) and $form['#post']['submitted']['payment_method'] == 'paypal') {
       
// remove the secpay submit
       
if (($ind = array_search('webform_simple_payments_submit_redirect', $form['#submit'])) !== FALSE) {
            unset(
$form['#submit'][$ind]);
        }
    } elseif (isset(
$form['#post']['submitted']['payment_method']) and $form['#post']['submitted']['payment_method'] == 'cc_dd') {
        if ((
$ind = array_search('paypal_webform_submit_redirect', $form['#submit'])) !== FALSE) {
            unset(
$form['#submit'][$ind]);
        }
    }

    return

$form;
}
/**
* Implementation of hook_menu().
*/
function paypal_webform_menu() { $items['webform/submissions/paypal'] = array(
       
'title' => 'Pay',
       
'page callback' => 'drupal_get_form',
       
'page arguments' => array('paypal_webform_submit_redirect'),
       
'access callback' => TRUE,
       
'type' => MENU_LOCAL_TASK,
    );
$items['webform/submissions/paypal-response'] = array(
       
'title' => 'Response',
       
'page callback' => 'paypal_webform_response',
       
'access callback' => TRUE,
       
'type' => MENU_LOCAL_TASK,
    );
$items['webform/submissions/paypal-listener'] = array(
       
'title' => 'Listener',
       
'page callback' => 'paypal_webform_listener',
       
'access callback' => TRUE,
       
'type' => MENU_LOCAL_TASK,
    );

    return

$items;

}

function

paypal_webform_submit_redirect($form, &$form_state) {
    global
$paypal_webform;
  
   
$_SESSION['webform_paypal_payment'] = array(
       
'nid' => $form_state['values']['details']['nid'],
       
'sid' => $form_state['values']['details']['sid'],
    );
drupal_goto($paypal_webform[PAYPAL_SYSTEM]['url']);

}

function

paypal_webform_listener() {
    global
$paypal_webform; $header = "";
   
$emailtext = ""; // Read the post from PayPal and add 'cmd'
   
$req = 'cmd=_notify-validate';
    if(
function_exists('get_magic_quotes_gpc'))
       
$get_magic_quotes_exists = true;

    foreach (

$_POST as $key => $value) {
        if(
$get_magic_quotes_exists == true && get_magic_quotes_gpc() == 1) {
           
$value = urlencode(stripslashes($value));
        } else {
           
$value = urlencode($value);
        }
       
$req .= "&$key=$value";
    }
// Post back to PayPal to validate
   
$header .= "POST /cgi-bin/webscr HTTP/1.0\r\n";
   
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
   
$header .= "Content-Length: " . strlen($req) . "\r\n\r\n";
   
$fp = fsockopen ($paypal_webform[PAYPAL_SYSTEM]['listener'], 443, $errno, $errstr, 30);

    if (!

$fp) { // HTTP ERROR
   
} else {
       
// NO HTTP ERROR
       
fputs($fp, $header . $req);
       
//while (!feof($fp)) {
       
$res = fgets($fp);
       
fclose ($fp);
       
       
// create the output record
       
$out = sprintf("Amount %s %s %s", $_POST['mc_currency'], $_POST['mc_gross'], $_POST['payment_status']); // now try and find the correct record in the database by using the email address from Paypal
        // It would kill the server to try and do this in one sql statement so it has been chunked for performance reasons
       
$found = FALSE;
       
$res = db_query("SELECT sid FROM {webform_submitted_data}
                         INNER JOIN {webform_component} ON {webform_submitted_data}.nid = {webform_component}.nid
                         INNER JOIN {webform_submissions} USING (sid)
                         WHERE data = '%s' AND form_key = 'email_address'
                         ORDER BY submitted DESC"
, $_POST['payer_email']);

        while(

$ret = db_fetch_object($res)) {
           
$res2 = db_query("SELECT cid, nid FROM {webform_component} INNER JOIN {webform_submissions} USING (nid)
                              WHERE sid = %d AND form_key = 'payment_response'"
, $ret->sid);
            while(
$ret2 = db_fetch_object($res2)) {
               
$found = TRUE;
                break
2;
            }
        }
// HAve we found a record to write?
       
if ($found == TRUE and isset($ret2)) {
           
db_query("UPDATE {webform_submitted_data} SET `data` = '%s' WHERE nid = %d AND sid = %d and cid = %d",
                     
$out, $ret2->nid, $ret->sid, $ret2->cid);
        } else {
           
watchdog("paypal_webform",
                    
"Unreconciled Paypal payment from @email with data @data",
                     array(
'@email' => $_POST['payer_email'], '@data' => $out),
                    
WATCHDOG_NOTICE);
        }

    }
}

function

paypal_webform_response() {

    if (isset(

$_SESSION['webform_paypal_payment']['nid'])
        and
$_SESSION['webform_paypal_payment']['nid']
        and isset(
$_SESSION['webform_paypal_payment']['sid'])
        and
$_SESSION['webform_paypal_payment']['sid']) { // redirect to the thank you screen
       
drupal_goto('node/' . $_SESSION['webform_paypal_payment']['nid'] . '/done', 'sid=' . $_SESSION['webform_paypal_payment']['sid']);

    } else {
       

watchdog('paypal_webform', t('Cannot reconcile response from Paypal with database entries'), NULL, WATCHDOG_ERROR);
        die;
    }

}

/**
* Implementation of hook_theme().
*/
function paypal_webform_theme() {

    return array(

'paypal_webform_radio' => array('arguments' => array('element' => NULL)));
}

function

theme_paypal_webform_radio($element) { drupal_add_css(drupal_get_path('module', 'paypal_webform') . "/css/webform-cc-paypal.css");
   
   
$output = drupal_render($element); // get the images directory
   
$image_dir = drupal_get_path('module', 'paypal_webform') . "/images/";
   
$credit_img = "<img class=\"webform-credit-card-image\" src=\"/" . $image_dir . "credit-cards.gif\" />";
   
$paypal_img = "<img class=\"webform-paypal-image\" src=\"/" . $image_dir . "paypal.gif\" />"; $output = ''; $output .= "<table class=\"payment-method-paypal-widget\"><tbody>\n";
   
$output .= "<tr>\n"; $output .= "<td>\n";
   
$output .= "<label class=\"paypal-option\" for=\"edit-submitted-payment-method-1\">\n";
   
$output .= "<input type=\"radio\" id=\"edit-submitted-payment-method-1\" name=\"submitted[payment_method]\" value=\"cc_dd\"  checked=\"checked\"  class=\"form-radio\" /> Credit / Debit cards</label>\n";
   
$output .= "</td>\n";
   
$output .= "<td class=\"payment-method-paypal-widget-image\">\n";
   
$output .= $credit_img;
   
$output .= "</td>\n";
   
$output .= "</tr>\n";
   
$output .= "<tr>\n";
   
$output .= "<td>\n";
   
$output .= "<label class=\"paypal-option\" for=\"edit-submitted-payment-method-2\"><input type=\"radio\" id=\"edit-submitted-payment-method-2\" name=\"submitted[payment_method]\" value=\"paypal\"   class=\"form-radio\" /> Paypal</label>\n";
   
$output .= "</td>\n";
   
$output .= "<td class=\"payment-method-paypal-widget-image\">\n";
   
$output .= $paypal_img;
   
$output .= "</td>\n";
   
$output .= "</tr>\n";
   
$output .= "</tbody></table>\n";

    return

$output;
}
?>

css/webform-cc-paypal.css

#edit-submitted-payment-method-1-wrapper, #edit-submitted-payment-method-2-wrapper {
    position: relative;
    vertical-align: center;
    padding-bottom: 0px;
    padding-top: 14px;
}

.payment-method-paypal-widget tbody {
    border: none;
}

table.payment-method-paypal-widget {
    width: auto;
}

td.payment-method-paypal-widget-image {
    padding-left: 30px;
}

images/credit-cards.gif
Credit Cards

images/paypal.gif
Paypal

.module file Configuration

You must now copy the button URLs you created in Paypal (and perhaps Sandbox) at the top of the paypal_webform.module code. It should be obvious where they go - they are replacing the two URLS in the $paypal_webform array. In addition, you need to set the enumerator type to either use the Paypal or the Sandbox URL by changing the

define('PAYPAL_SYSTEM', PAYPAL_PRODUCTION);


accordingly.

Webform creation

You can now make your first webform with Paypal donations available. Below is a sample webform - note the "Select payment method" selection drop down, and the "Payment Response" hidden field.




Paypal3

Sample Webform

The "Select payment method" component should be filled in as below, paying particular attention to the ringed values.




Paypal4

Select payment method
The "Payment Response" component stores the returned values from Paypal and must be as ringed below.



Paypal5

Payment Response

The Finished Webform

If everything has gone well, you should see your completed webform looking something like the image below. If the user selects a Paypal payment type, they will be whisked off to Paypal to effect the payment Smile
Select payment method
The "Payment Response" component stores the returned values from Paypal and must be as ringed below.



Paypal6

Completed Webform

Next Steps

Due to severe budget constraints, there are a number of activities that weren't completed to my satisfaction, but really aught to be.

  • As mentioned previously, there is a small dependency on there being another gateway in use (in this case SECPAY) and a radio button is added to provide a choice. It would be good not to have that requirement
  • The configuration is messy. At the moment, Paypal button URLs need to be pasted into the PHP code
  • Linked to the above concern, there is no convenient way of switching between buttons (i.e. production and sandbox)

These problem areas, as mentioned, were forced due to the lack of complete sponsorship. If you or your organisation would like to pay to have this tidied up, drop me a line Smile


Attachment Size paypal_webform.tar.gz 10.29 KB

Jun 26 2012
Jun 26

This module (which requires the Silverpop XML API module to be installed) provides the facility to add recipients to a Silverpop database. By default this is through a 'Subscribe now' block or node, both of which can be themed. In addition, there is functionality to delete a recipient from a Silverpop database. User permissions (for the nodes) and block permissions (for the blocks) can control the visibility of the forms.

Installation

Installation is by the usual Drupal mechanism. Once the module is installed, permissions should be set by going to admin/user/permissions. It is important to ensure that these are set correctly to prevent unauthorised access through to the Silverpop API.

Usage

To create a form to capture an email address, go to node/add/silverpop-subscribe-now. You will need to provide a title and a database id. This will provide both a unique block and a node. To make the block visible you will need to go to admin/build/block/list and configure as appropriate. The blocks can be identified by their nid or by the titles they were created with.

Those uses with the correct permissions will see a link to silverpop/remove_recipient in the navigation menu (providing this itself is visible on your system). Use the form from this link to delete email addresses from a database.

Limitations

This system captures email addresses which can be useful for newsletters and campaigns. It does not capture other information such as names / titles etc. It would be possible to re-factor the code to do this as an exercise at a later date.

The RemoveRecipient API call assumes there is only one key, of Email, in the database and will not work should this not be the case.

silverpop_addrecipient.info

version = "6.x-1.00"
name = "Silverpop AddRecipient"
description = "Silverpop Add Recipient through a subscribe now form on a block"
core = 6.x
php = 5.2
package = "silverpop"
dependencies[] = "silverpop"

silverpop_addrecipient.install

<?php
/**
* Implementation of hook_install().
*/
function silverpop_addrecipient_install() { drupal_install_schema('silverpop_addrecipient');

}

/**
* Implementation of hook_uninstall().
*/
function silverpop_addrecipient_uninstall() { drupal_uninstall_schema('silverpop_addrecipient');
}
/**
* Implementation of hook_schema().
*/
function silverpop_addrecipient_schema() { $schema['silverpop_subscribe_now'] = array(
       
'fields' => array(
           
'vid' =>            array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
           
'nid' =>            array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
           
'sp_db' =>          array('type' => 'varchar', 'length' => 255, 'not null' => FALSE),
            ),
       
'unique keys' => array(
           
'nid_vid' =>        array('nid', 'vid'),
            ),
       
'indexes' => array(
           
'nid' =>            array('nid'),
        ),
       
'primary key' => array('vid'),
    );

    return

$schema;
}
?>

silverpop_addrecipient.module

<?php
// Copyright @badzillacouk <a href="http://www.badzilla.co.uk
//" title="http://www.badzilla.co.uk
//">http://www.badzilla.co.uk
//</a> Licence GPL. This program may be distributed as per the terms of GPL and all credits
// must be retained
//
// If you find this script useful, please consider a donation to help me fund my web presence
// and encourage me to develop more products to be placed under the terms of GPL
// To donate, go to <a href="http://www.badzilla.co.uk" title="http://www.badzilla.co.uk">http://www.badzilla.co.uk</a> and click on the donation button
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

/**
* Implements hook_node_info().
*/

function silverpop_addrecipient_node_info() {

    return array(
       

'silverpop_subscribe_now' => array(
           
'name' => t('Silverpop Subscribe Now Block'),
           
'module' => 'silverpop_addrecipient',
           
'description' => t("Silverpop Subscribe Now Block to capture new email addresses and add to Silverpop"),
           
'has_title' => TRUE,
           
'title_label' => t('Silverpop Subscribe Now Block Title'),
           
'has_body' => FALSE,
            ),
        );
}
/**
* implements hook_insert().
*/
function silverpop_addrecipient_insert($node) { db_query(
       
'INSERT INTO {silverpop_subscribe_now} (vid, nid, sp_db) '
       
."VALUES (%d, %d, '%s')",
           
$node->vid,
           
$node->nid,
           
$node->sp_db);
}
/**
* Implements hook_delete().
*/
function silverpop_addrecipient_delete($node) { db_query('DELETE FROM {silverpop_subscribe_now} WHERE nid = %d', $node->nid);
}
/**
* This implementation just handles deleting node revisions.
* Implements hook_nodeapi().
*/
function silverpop_addrecipient_nodeapi(&$node, $op, $teaser, $page) {

    if (

$op == 'delete revision' and $type->type == 'silverpop_addrecipient')
       
db_query('DELETE FROM {silverpop_subscribe_now} WHERE vid = %d', $node->vid);
}
/**
* implements hook_update().
*/
function silverpop_addrecipient_update($node) { db_query("UPDATE {silverpop_subscribe_now} SET sp_db = '%s'WHERE vid = '%d'",
               
$node->sp_db);
}
/**
* Implementation of hook_load().
*/
function silverpop_addrecipient_load($node) {

    return

db_fetch_object(db_query("SELECT * FROM {silverpop_subscribe_now} WHERE vid = %d", $node->vid));
}
/**
* Implements hook_perm()
*/
function silverpop_addrecipient_perm() {

    return array(
       

'create silverpop_subscribe_now',
       
'edit silverpop_subscribe_now',
       
'delete silverpop_subscribe_now',
       
'view silverpop_subscribe_now',
    );
}
/**
* Implements hook_access()
*/
function silverpop_addrecipient_access($op, $node, $account) {

    switch (

$op) {
        case
'create':
            return
user_access('create silverpop_subscribe_now', $account);
        case
'update':
            return
user_access('edit silverpop_subscribe_now', $account);
        case
'delete':
            return
user_access('delete silverpop_subscribe_now', $account);
        case
'view':
            return
user_access('view silverpop_subscribe_now', $account);

    }
}

/**
* Implementation of hook_view().
*/
function silverpop_addrecipient_view($node, $teaser = FALSE, $page = FALSE) {

    if (!

$teaser) {
       
$node = node_prepare($node, $teaser);
       
$node->content['form'] = array('#value' => drupal_get_form('silverpop_subscribe_now_form', $node));
    }

    return

$node;
}
/**
* Implementation of hook_form().
*/
function silverpop_addrecipient_form(&$node, $form_state) { $form['title'] = array(
       
'#type' => 'textfield',
       
'#maxlength' => 250,
       
'#title' => $type->title_label,
       
'#required' => TRUE,
       
'#default_value' => $node->title,
       
'#weight' => -10,
    );
$form['sp_db'] = array(
       
'#type' => 'textfield',
       
'#maxlength' => 255,
       
'#title' => t('Silverpop database id'),
       
'#required' => TRUE,
       
'#description' => t('Silverpop database id'),
       
'#default_value' => $node->sp_db ? $node->sp_db : '',
       
'#weight' => 0,
    );

    return

$form;
}
/**
* Implementation of hook_block().
*/
function silverpop_addrecipient_block($op = 'list', $delta = 0, $edit = array()) {

    switch(

$op) {
        case
'list':
           
$res = db_query("SELECT n.nid, r.title FROM {node} AS n INNER JOIN {node_revisions} AS r
                             USING (vid) WHERE type = 'silverpop_subscribe_now' AND status = 1"
);

            while(

$ret = db_fetch_object($res))
               
$block[$ret->nid]['info'] = t('Silverpop Subscribe Now: @title [nid:@nid]',
                                    array(
'@title' => $ret->title, '@nid' => $ret->nid));
           
            break;

        case

'view':
            if (
$node = node_load($delta)) {
               
$block['content'] = drupal_get_form('silverpop_subscribe_now_form', $node);
               
$block['subject'] = $node->title;
            }

            break;

        default:
            break;
    }

    return

$block;
}
// Forms for user input
function silverpop_subscribe_now_form($form_state, $node_params) { $form['email_address'] = array(
       
'#type' => 'textfield',
       
'#maxlength' => 255,
       
'#title' => t('Email address'),
       
'#size' => 15,
       
'#required' => TRUE,
    );
$form['submit'] = array(
       
'#type' => 'submit',
       
'#value' => t('Subscribe'),
    );
$form['#validate'][] = 'silverpop_subscribe_now_form_validate';
   
$form['#submit'][] = 'silverpop_subscribe_now_form_submit';

    return

$form;
}

function

silverpop_subscribe_now_form_validate($form, &$form_state) {

    if (!

valid_email_address($form_state['values']['email_address']))
       
form_set_error('email_address', t('Please enter a valid Email address'));
}

function

silverpop_subscribe_now_form_submit($form, &$form_state) { // Construct the XML for the Silverpop API call
   
$dom = new DOMDocument;
   
$list_id = $dom->createTextNode($form['#parameters'][2]->sp_db);
   
$list = $dom->createElement('LIST_ID');
   
$cfrom_text = $dom->createTextNode('1');
   
$cfrom = $dom->createElement('CREATED_FROM');
   
$column = $dom->createElement('COLUMN');
   
$name_text = $dom->createTextNode('EMAIL');
   
$name = $dom->createElement('NAME');
   
$value_text = $dom->createTextNode($form_state['values']['email_address']);
   
$value = $dom->createElement('VALUE');
   
$method = $dom->createElement('AddRecipient'); $list->appendChild($list_id);
   
$cfrom->appendChild($cfrom_text);
   
$name->appendChild($name_text);
   
$value->appendChild($value_text);
   
$method->appendChild($list);
   
$method->appendChild($cfrom);
   
$method->appendChild($column);
   
$column->appendChild($name);
   
$column->appendChild($value);
   
$dom->appendChild($method); module_load_include('inc', 'silverpop', 'silverpop_xml');
   
$addrecipient = new silverpop_xml($dom, FALSE);
    list(
$retcode, $response) = $addrecipient->apicall(); $msg = '';
    if (
$retcode)
       
$msg = t('Thanks! You have now been suscribed to the mailing list');
    else if (
is_object($response) and
             isset(
$response->getElementsByTagName('errorid')->item(0)->nodeValue) and
            
$response->getElementsByTagName('errorid')->item(0)->nodeValue == 122)
       
$msg = t('Thanks, but you are already subscribed');
    else
       
$msg = t('Sorry, unable to subscribe you at this present time'); drupal_set_message($msg);
}
/**
* Implementation of hook_theme().
*/
function silverpop_addrecipient_theme() { $ret['silverpop_subscribe_now_form'] = array(
           
'arguments' => array('form' => NULL)); $ret['silverpop_remove_recipient_form'] = array(
           
'arguments' => array('form' => NULL));

    return

$ret;
}

function

theme_silverpop_subscribe_now_form($form) { $output = ''; $output .= "To subscribe to our newsletter, please enter your Email address below";
   
$output .= drupal_render($form['email_address']);
   
$output .= drupal_render($form['submit']); $output .= drupal_render($form);

    return

$output;
}
/**
* Implementation of hook_menu().
*/
function silverpop_addrecipient_menu() { $items['silverpop/remove_recipient'] = array(
       
'title' => t('Silverpop Remove Recipient'),
       
'description' => t('Removal of recipients from Silverpop'),
       
'page callback' => 'drupal_get_form',
       
'page arguments' => array('silverpop_remove_recipient_form'),
       
'access arguments' => array('delete silverpop_subscribe_now'),
       
'type' => MENU_NORMAL_ITEM,
    );

    return

$items;
}

function

silverpop_remove_recipient_form() { $form['sp_db'] = array(
       
'#type' => 'textfield',
       
'#maxlength' => 250,
       
'#title' => t('Silverpop database id'),
       
'#required' => TRUE,
       
'#description' => t('Silverpop database id'),
       
'#default_value' => $node->sp_db ? $node->sp_db : '',
       
'#weight' => 0,
    );
$form['email'] = array(
       
'#type' => 'textfield',
       
'#maxlength' => 250,
       
'#title' => t('Email address'),
       
'#description' => t('Email address to remove from the Silverpop database'),
       
'#required' => TRUE,
       
'#weight' => 10,
    );
$form['submit'] = array(
       
'#type' => 'submit',
       
'#value' => t('Submit'),
       
'#weight' => 20,
    );
$form['#submit'][] = 'silverpop_remove_recipient_form_submit';
   
$form['#validate'][] = 'silverpop_remove_recipient_form_validate';

    return

$form;
}

function

silverpop_remove_recipient_form_validate($form, &$form_state) {

    if (!

valid_email_address($form_state['values']['email']))
       
form_set_error('email', t('Please enter a valid Email address'));
}

function

silverpop_remove_recipient_form_submit($form, &$form_state) { // Construct the XML for the Silverpop API call
   
$dom = new DOMDocument;
   
$list_id = $dom->createTextNode($form_state['values']['sp_db']);
   
$list = $dom->createElement('LIST_ID'); $email_text = $dom->createTextNode($form_state['values']['email']);
   
$email = $dom->createElement('EMAIL'); $method = $dom->createElement('RemoveRecipient'); $list->appendChild($list_id);
   
$email->appendChild($email_text);
   
$method->appendChild($list);
   
$method->appendChild($email); $dom->appendChild($method); module_load_include('inc', 'silverpop', 'silverpop_xml');
   
$removerecipient = new silverpop_xml($dom, TRUE);
    list(
$retcode, $response) = $removerecipient->apicall(); $msg = '';
    if (
$retcode)
       
$msg = t('Email address succesfully deleted from the specified database');
    else if (
is_object($response) and
             isset(
$response->getElementsByTagName('errorid')->item(0)->nodeValue) and
            
$response->getElementsByTagName('errorid')->item(0)->nodeValue == 138)
       
$msg = t('The email address you specified does not exist in the Silverpop database');
    else
       
$msg = t('Sorry, the removal of the email address failed. Please contact your administrator'); drupal_set_message($msg);
}

function

theme_silverpop_remove_recipient_form($form) { $output = ''; $output .= "To remove an email address from a Silverpop database, please use the form below";
   
$output .= drupal_render($form['sp_db']);
   
$output .= drupal_render($form['email']);
   
$output .= drupal_render($form['submit']); $output .= drupal_render($form);

    return

$output;
}
?>

Attachment Size silverpop_addrecipient-6.x-1.1.tar.gz 3.3 KB silverpop_addrecipient-6.x-1.0.tar.gz 3.3 KB

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web