
I'm going to be working with a partner to create a Drupal site for my school. We have a full month to work on the main details of the site, and will work on improving it over the course of the whole year.
The idea is that we want to build a site which will serve as a marketing front-end for the school, and as an way to distribute information to parents and students from the school, and allow teachers to collaborate with each other and their students.
I know that Drupal can do the job. I plan on using this blog to document my process.
Today we started by setting up 2 domains for us to use, one as a back-up to the other, and installing a mirrored Drupal installation on both of them. Plan is to start with a huge bundle of modules (most of which will never be enabled) to remind ourselves of all of the features available to us through the active community at Drupal.org. We don't want to build anything from scratch that exists already.
The major difficulty with the site will be in the intended upgrades. We want to eventually have parents able to monitor their child's progress through their account on the website. So we have a lot of sensitive information that we need to protect, which means access control is going to have to be carefully checked.
As you know, a colleague and I are working on our new school website. This part of a series of blog posts explaining what we have done.
The very first task we did was to look at the information we were provided on the needs of the school in terms of menu layout and functionality. This was actually fairly easily done since we had spent the previous year working in a committee. From this, we decided on a menu structure and overall plan for our site.
Our idea is to segment the site using Organic Groups, largely because it's easy to create access control (just assign users to the groups they are allowed to view) and easy to maintain relationships between the content of the site and it's location within the site. We are also planning on using the Panels module to provide the display of the group nodes and the front page.
We plan on using the Views module to expose our site information, and using custom content types to make it easier for users to submit information for display within the correct view. We may end up using CCK to add some fields to our nodes, particularly for our Alumni users to submit user profiles and for our school bulletin items.
The site is already starting to come together a bit, which is exciting.
David
Some major developments have happened since my last post, and the site is coming together nicely.
One is the development of a theme for our site. We've basically taken the Zen theme and using some CSS and some modification of the .tpl.php files, we have created a decent unique theme for the site. Much of the initial work can be attributed to my partner, but I managed some of the details, especially the modification of the template files. The design for our theme can from in-house, from a graphic designer who created the layout and presentation of the design using Adobe Illustrator. I have to say, .ai files are a pain to work with, glad she was able to export it to a .svg so at least I could open it and view it using Gimp.
One of the less fun things about theme development of course is checking for browser compatibility. We used the conditional commenting technique to add css just for IE (and yech! just for IE 6!). We decided against making our theme work for IE 5, and IE on the Mac is a waste of time. Safari 3 was easy to check for, since it has a Windows install, as was checking Firefox on Ubuntu. However I don't have a Mac around so testing for Safari 2 will be interesting (although necessary given Safari increasing market share).
Another development is the import of the old galleries, a sub-project I was happy my colleague handled. We had to manually find each folder (which were scattered all over the place because of the sloppiness of the old site management) and then import them using the image import module. I tried to use the nodeorder module to arrange the order of one of the galleries but the JS script they use seems to have bugs in it. I ended up having to keep the nodeorder module installed, and then manually arrange the order in the database. Bleh.
We have also started adding the old content from the original site, which has required some formatting. One of the problems we encountered here was that much of the old content is out of date, and uses very poor HTML. I've found myself editing the content quite frequently, and in many cases not importing the content at all. Moving from flat HTML files to a content management system is a tedious business.
We've also gone through and created many, many blocks, to use in our group panels, and almost as many views. We created the blocks using the admin/block/build menu because of the greater flexibility in the input format (including the ability to relatively easily use TinyMCE to create the HTML) and then all of the site blocks are located on the same menu. Our views include some specialized views of OG content (things like Og_single_post and Og_edit_table, etc...) and a few 'Views Bulk Operations', which is a terrific module. We use this so that the process of approving and managing specific site content is a lot easier. For example, in order for images to be used in a Views Slideshow on the front page, we have a form which lists all of the images. Users can toggle between 'promoted' and 'unpromoted' pictures and choose a selection of pictures to be used in the slideshow.
The vast majority of the people who will be using our site will not have publishing privileges, at least initially. The plan is to gradually allow more and more of the staff to be able to publish to the site. To this end we have developed a very simple workflow. Any time a user without the permission to 'publish directly' either creates or edits a post, the post is set to 'unpublished'. We then have a bulk operation where a user can see all of the unpublished content, and choose which of it to publish, and which of it to edit.
We have added a number of different types of content, this simplified the creation of content. Our other alternative was to use Taxonomy to separate some of the different content types (for example Elementary school news, Middle school news, and High school news) but felt this might not be totally intuitive to some users. After watching one of our teachers trying to fill in a form online for almost 20 minutes, I realized that many users do not read the form they are presented, and the taxonomy select might be easy to miss. Rather than spend time tracking down errant posts and assigning them the proper vocabulary term, we decided to go with a different content type, allowing us to use the Content Access module to fix the user permissions to their section of the school.
We are of course, using Taxonomy for managing the 'Image Galleries' on the site, and for restricting access to content. Our content is split into 'Public', 'Private' and 'Restricted'. Content which only meant to be viewed by logged in users is marked 'Private' and content only meant to be viewed by users with the administrator role is marked 'Restricted'.
Today and tomorrow we will be presenting our site, and for the first time getting feedback from some people on the current state of the website. We'll see what modifications we have to make, hopefully it won't be too onerous (like: redo the theme, or we don't like the current menu structure, redo it).
This is a simple module I created which has a very simple premise. You've created a bunch of accounts for some people, and you want them to be able to easily access their account, without you having to know their passwords. You didn't want the users to be notified right away on account creation, for many possible reasons. Maybe it was a development site that you needed to test your user imports, maybe you needed to finish up some quick stuff first, who knows.
So what this module does is allows you to customize a 'welcome to your account' type email in the settings page. You can then go to the user/user menu, where this module has a hook into the user operations drop down. Select which users you want to include, and send them an email, it's that easy.
It doesn't do anything automatically, it requires action on your part. However it does greatly simplify the user experience for finding the site, and logging on for the first time.
Only thing it lacks is a 'send an introduction to every user' which is probably a good thing since you may click it by accident!
Anyway, you are welcome to check it out. I'm not including it on Drupal.org since I just won't have the time to maintain it. If someone else wants to take it over, that's fine with me.
| Attachment | Size |
|---|---|
| account_intro.zip | 2.32 KB |
There is a module by this name up on Drupal.org, but a quick check of the issue queue makes it obvious that it is in need of some love.
What my module does is create a Menus tab for each Organic group which only a user with the appropriate permissions can use. Then, each group manager can be in charge of a menu block which only shows up in the context of their group (so on the group home page, and every content item created in the context of the group). The module requires that you give all appropriate users the permissions 'administer menu', but then defines a new permission to prevent user access to the base menu system.
The users can either define a new menu block, or reuse an existing part of the menu structure (which probably needs to be part of a permission but that's a kink to iron out).
The interface is clean and easy to use, I think. Check it out.
| Attachment | Size |
|---|---|
| og_menu.zip | 8.19 KB |
This is a simple module that allows users to post responses/reviews of other people's posts. It also includes a simple 5 star voting widget, which degrades nicely when JavaScript is disabled. The posts are limited to the default filter input and are carefully screened for possible xss attacks since it is likely this kind of module could be used for allowing anonymous users to post.
Another feature of this module is that you can use one of your site vocabularies to define in what categories people can vote on. The voting unfortunately does not currently use the Voting Api module. It really should, because then one could define Views of the votes much more easily, and it would allow for a greater generalization of the voting process.
This module has actually already seen use in a couple of places, one a public library (they are using it to review books) and another site is using it to review cars.
Both of the sites have given me permission to release the code. Again, I'm too busy to maintain a module like this, but if someone wants to take up the torch...
| Attachment | Size |
|---|---|
| review.zip | 65.43 KB |
This is yet another WYSIWYG module. It has a very simple configuration page, and will currently only show up on node/add or node/edit pages, of the content types one wants. It has a couple of advantages I see over TinyMCE.
1. The code for the editor is open-source.
2. It is a lot, lot smaller in size than many of the other editors out there, which makes it load faster than other editors.
3. The JS only loads on pages that have the editor present, rather than on all pages.
The major disadvantage of course is a much smaller developer team working on it, so bugs and feature requests will presumably take a long time to get implemented.
Anyway, I don't want to maintain this module, so I'm releasing it here, feature frozen.
| Attachment | Size |
|---|---|
| openwysiwyg.zip | 90.63 KB |
So my wife and I have decided that we are going to try and be partners in a new business. We'll see how well we work together, we really haven't tried very hard to work on any serious projects (except of course for our son) together before this.
My wife knows a lot about graphic design and what looks good as a result of her training as a landscape arctitect, but knows very little about building websites. I know a fair bit about building websites, but my design sense is horrible. So we combine together these two talents, and we should make a pretty good team.
Instead of trying to build entire websites right now, our plan is to focus on building themes for Drupal 5 and Drupal 6. We'll create a showcase site for our themes, and charge people either a license to use the theme, or the complete copyright to a theme at a much steeper price.
So yesterday we started our collaboration by building a simple theme for our theme shop.
My wife and I each brainstormed a few ideas, and we settled on one of my ideas, with some touch-ups from my wife. I went to work and started installing and setting up the modules I thought we would need.
Installed modules:
The website portion isn't yet totally complete, but my wife created a layout, which I have turned into a theme. One of the fun aspects of this theme is the front page rotating image preview, done by customizing the html displayed by the view (essentially minimizing it as much as possible) and some jQuery fun.
Check out the site at:
So I thought I should give more detail on how to create a theme for Drupal 5.
What I basically did was copy page.tpl.php, node.tpl.php, block.tpl.php, box.tpl.php, comment.tpl.php and template.php from the Garland theme. I then also created a file called page-front.tpl.php. This gave me a basic framework with which to work.
For the creation of the theme for the Skin Drupal site, the first thing I did was rip out all of the html between the body tags in the page.tpl.php file. I then looked at the picture my wife gave me, and mentally divided it into regions.

From this picture, you can see it could be easily divided into 4 blocks, which is essentially how I created the html.

First I inserted a wrapper div, to make it easy to center the display of the site for people with browsers with resolution greater than 1024x768. We decided, by doing some research online, that a theme that was 960px wide by about 500px tall was ideal for display in most people's monitors. We'd be leaving those poor 800x600 people out of the loop, but that's only 10% of the market, and not likely to be the people looking for themes at our site (developers tend to try and use the best tools at their disposal).
Inside the wrapper div, I inserted two sibling divs to divide the site into the header and main regions. Since the content of the header was so simple, I just inserted 2 divs for the right and left portions of the header and the same for the main portion.
The 5 rotating image blocks, I quickly realized I would have to position absolutely on the page in order to make the fade in and fade out work. This presented problems later and led us to eventually scrap this theme, but more about that in a future post. Anyway, to allow for this, I opened up my blank template.php file and created a themename_regions function, and created 5 more regions for my site, calling them easy to remember things like box 1, box 2, box 3, box 4 and box 5. Before I went any further, I created 5 views to match these blocks and used the block administration page to match the blocks to their position on the front page. I went ahead and created some filler content, so we'd be able to see these blocks at work.
It was pretty simple to set some default blocks for the right column of the main page, and then I went ahead and worked on the css for the theme, which is remarkably simple since our layout is so clean and easy.
Fix the width of all of the blocks, fix their height, add float left to most of them, and float right to the right column, and we were set. Then I used absolute positioning on the blocks and wrote the JS script to fade them in and out. jQuery is amazing, I have to say, this script was a breeze to write. If you examine the source of the page, or use JSview, you can see the name of the JS file, which you can safely view in your browser. You'll see that it's pretty short.
I kept the default html for the box, block, comment, and node files, but used a views theming function inside my template.php file to greatly simplify the html presented by the views (since all I really wanted to see was the thumbnails). Then Tada! I had a really simple plain theme. The idea of the theme is that it looks essentially unthemed, and that our themes will be ways to spruce up the site.
So our new site already has a new theme, which is just a modified version of the old theme. It looks pretty good I think.

The idea is, instead of fading out the backgrounds of the preview images, we leave them in place, and fade out the images. We also reduced the number of images previewed to 3, and arranged them in a column. Finally, my wife had a great idea of moving the menu to beneath the images, so now our menu block is at the bottom of the page.
This is an extremely easy theme, and it even works in IE 5.5 (with a minor adjustment for the logo, which I haven't done yet).
One column, centered horizontally, with header, main, and footer sections divided vertically. No sidebars at all, and extremely easy to retheme later. The JS had to be rewritten, nearly from scratch, but I think it works a bit smoother now, less jerkiness because of the fewer number of elements involved.
Check it out at http://unitorganizer.com/skindrupal
Dave
So I love the Panels module. It gives me much, much greater control over where I want to place my content, and how I create my blocks. It's stupendous.
It has a serious problem though, the actual construction of content is not meant to be done by a beginner. The form includes many unnecessary elements for a novice user and the process is confusing. Worse still, you can't use either the IMCE module or any WYSIWYG editor when creating custom panes. This is a serious issue for many of my users, most of whom will be publishing to the web for the first time.
So I wrote a module which takes the process of custom pane construction, and tries to simplify it as much as possible. The basic idea is, people navigate to a Panes tab, where they see all of the displays associated with this group (or one display in the case of a panels node). They can then choose to add panes to the display, and be sent to a separate form, or edit the custom panes that have already been built. Since this is a separate form, the users have access to a WYSIWYG editor and the nice IMCE pop-up for adding images.
I think this will make managing custom pane content much easier for most of my users. We'll see, I'm going to let my partner know about it, and see what he thinks.
Screen shots:

Main pane edit window

Add pane window

Edit pane window
Notice the use of a WYSIWYG editor in both the 'Add pane' and 'Edit pane' forms (and the link to the IMCE image uploader). This is what really lowers the difficulty of adding nice custom panes to a panels node or an Og panels page.
| Attachment | Size |
|---|---|
| simple_panes.zip | 6.55 KB |
There are 8 different basic usable website themes I can think of. I've listed them below. My intention is to find minimal mark-up that will produce these themes, with as wide a range of browsers as possible support.
![]() |
![]() |
| One column | Three fixed columns |
![]() |
![]() |
| Three column fluid center | Three columns - fluid left |
![]() |
![]() |
| Three columns fluid right | Two fixed columns |
![]() |
![]() |
| Two columns - fluid left | Two columns - fluid right |
Does anyone have any suggestions of an Open Source solution to these themes? I'm sure they have all been created before. Why re-invent the wheel?
Check this out!
This is a script written by John Bezanis which takes 6 images, specified by GET parameters (his own words) and turns them into a spinning cube. You can click on a face of the cube and it will spin around into focus. Click again and the cube continues spinning.
It's currently displaying 6 images from a course in my online MA where we have to analyze an e-Learning platform using these 6 facets.
Someone mentioned in one of the Drupal groups that they had created a very simple script to check how popular search terms, unique for IP address. In other words, how many different unique visitors have searched for a particular word. I just took their script and converted it to a simple module. Can't take much credit for this one.
| Attachment | Size |
|---|---|
| search_statistics.zip | 2.09 KB |
This is my BookGUI module, including the most recent updates (which are actually almost a year old). I don't consider it stable for a except for a development environment, but maybe someone can iron out a couple of the kinks I gave up on.
This allows you to edit the structure of a book (AKA the Book module) as well as add/delete pages to the book on the fly, all via an Ajax management page.
It's huge flaw is that it doesn't check for the number of pages that it's going to load. I really need to build some kind of paging system or something similar into this structure. Maybe only load the structure of the book, and none of the content? Content could be loaded on the fly via Ajax...
Anyway, check it out if you like, it's released under the GPL.
| Attachment | Size |
|---|---|
| bookgui.zip | 45.57 KB |
One problem with Drupal is that the UI sucks for beginner users. They have great difficulty finding all of the most important administrative menu items they want to use. Since Drupal tends to group administrative items by general functionality and by module, it can tricky working where something is located for a beginner. Not to mention the fact, many beginning CMS users are terrified to death of 'clicking the auto-destruct link' by mistake and screwing up their work. This fear is not ungrounded, as many Desktop applications have a strange red X in the top right corner of the page that is terribly destructive when accidentally clicked (assuming one is not used to reading dialogs carefully before clicking Yes).
So the solution in Drupal is simple. Create a page where all of the most useful administrative items are linked, and make sure that each type of functionality is in blocks which are displayed only to the user who has access to do that item.

This is a module to help non-coders add help text to the top of any path, much like the core Help module does. One problem with the core Help module is that it relies on using hook_help to add the help text to the top of the page. This means that if you want to add help to a page which does not already have it, you need to create a custom module. This means that only the developers of the site can easily add help text, although once the text is added, a user with sufficient privileges could 'translate' the help text string and reword it however they like.
This is a barrier of use for non-coders. My wife wanted to be able to add help text to pages, so I created a module which I call 'Add Help' which allows her to search for a path, and then add help text to that path.
Here is a screen-shot of the "Search for Path" form:

Here is a screen-shot of the "Add Help to Path" form:

There are some minor problems with the implementation, for one there is no way currently to keep track of which pages to which you have added help text so you have to keep track of this yourself. A second problem is that I haven't figured out how to execute php code properly in case the user has chosen the php filter.
Anyway I'm happy to release my code under the GPL, hopefully I'll find time to upload it to Drupal.org as well.
| Attachment | Size |
|---|---|
| add_help.tar.gz | 2.96 KB |
This module let's you take a Podcast view you have created (or probably ANY audio podcast) and allow the JW Media Player 4 module (from the SWF tools module ) to play the audio feed as a playlist, automatically embedding the player as a block. Note that this is built for Drupal 6.
It does not have a lot of configuration options yet, you pretty much need to open up swf_playlist.tpl.php and modify it to suit the location of your feed.
swf_playlist.tpl.php
<?php
// get the location of the JW Media Player
// this doesn't work, I'm not sure why
// $player = url(drupal_get_path('module', 'swftools'). '/shared/flash_media_player/player.swf', array('absolute', TRUE));
// you have to set this manually for now. yech.
$player = "http://localhost/skywyatt/sites/all/modules/swftools/shared/flash_media_player/player.swf";
// get the default settings
$flashvars = _wijering4_flashvars($settings['name']);
// set the flashvars
$flashvars['file'] = url($settings['path'], array('absolute', TRUE));
$flashvars['autostart'] = $settings['autostart'] ? 'true' : 'false';
$flashvars['repeat'] = $settings['repeats'] ? 'true' : 'false';
$flashvars['playlist'] = 'over';
$flashvars['playlistsize'] = '40';
// add the fancy random image stuff while the audio file is playing
$flashvars['plugins'] = 'revolt-1';
// unset the name of the playlist
// we don't want to confuse the JW Media player
$name = $settings['name'];
unset($settings['name']);
unset($settings['path']);
// change some default options
$settings['base'] = base_path();
$options = array(
'params' => $settings,
'flashvars' => $flashvars,
);
// requires SWF tools module and JW Media Player 4 modules
print swf($player, $options);
?>| Attachment | Size |
|---|---|
| swf_playlist.tar.gz | 3.59 KB |
I am working on a website called Pedagogle.com which is intended to be a file sharing website for educators. The plan is, upload and categorize resources, and if enough people do this, and we have enough resources, then we end up with a website where teachers can go to find resources easily. The problem is that it takes a long time to upload and categorize each file, which leads to frustration from users. So far hardly any files have been uploaded, mostly because I think the process is a bit too difficult for the average teachers, and is definitely too time-consuming.
So I'm developing a module which will cut down the time it takes to share resources on my site. The basic work-flow I envisioned is, a teacher creates a ZIP archive for a folder of digital resources they have created, and uploads the ZIP. They then are presented with a form where they categorize all of the resources and save them to the database, with the option of deleting any files from our server right then that were included in the ZIP archive by error.
The first step in writing this module was to decide on how this was going to work conceptually for the user. It occurred to me that some users might not want to complete the categorization all at the same time, so that the files should be unarchived and stored in a temporary folder on the computer while the user is in the middle of the categorization, and that the process should be one that the user can come back to later if they like. This meant that the URL for the uploading of the ZIP should be different than the URL for managing the ZIP files to make the UI a bit easier to manage.
I implemented hook_perm() and hook_menu() in my module first like so:
<?php
/**
* Implementation of hook_perm().
*/
function zip_upload_perm() {
return array('upload zip files');
}
?><?php
/**
* Implementation of hook_menu().
*/
function zip_upload_menu() {
$items = array();
$items['zip_upload'] = array(
'title' => 'Upload ZIP',
'description' => 'Allows you to upload a zip of your resources and then categorize and save the resources',
'page callback' => 'drupal_get_form',
'page arguments' => array('zip_upload'),
'access callback' => 'user_access',
'access arguments' => array('upload zip files'),
);
$items['zip_upload/my'] = array(
'title' => 'My ZIP Uploads',
'description' => 'Categorize your uploaded files here and save them permanently',
'page callback' => 'drupal_get_form',
'page arguments' => array('zip_upload_recent'),
'access callback' => 'user_access',
'access arguments' => array('upload zip files'),
);
$items['admin/settings/zip_upload'] = array(
'title' => 'Zip Uploads',
'description' => 'Configure zip uploads for your site.',
'page callback' => 'drupal_get_form',
'page arguments' => array('zip_upload_settings_form'),
'access callback' => 'user_access',
'access arguments' => array('administer site configuration'),
'type' => MENU_NORMAL_ITEM,
);
return $items;
}
?>I decided that even though my module was for a specific website, I should build it with some generalization in mind, just to make it easier to use the module for a different site. This is why I have a menu entry in my hook_menu() for a settings page.
Once I had my menu entries sorted out, the next step was to build both the forms for each path, but also the submit functions for these entries, which are the most difficult part of this module. The form for the settings page was extremely easy, especially as I could let Drupal handle the submission of the form by running it through system_settings_form.
<?php
/**
* Custom settings form for zip uploads
*/
function zip_upload_settings_form() {
$form['zip_upload_import_path'] = array(
'#type' => 'textfield',
'#title' => t('Zip Uploads path'),
'#description' => t('Choose to which directory inside your default files directory your zip uploads will be saved temporarily. Note that once the user finishes categorizing their resources, the zip files and the extracted files will be deleted automatically.'),
'#default_value' => variable_get('zip_upload_import_path', 'zip_upload'),
);
$form['zip_upload_allowed_file_types'] = array(
'#type' => 'textfield',
'#title' => t('Allowed extensions'),
'#description' => t('Choose which extensions of files are allowed to be contained within the zip archives. Separate each extension with a space.'),
'#default_value' => variable_get('zip_upload_allowed_file_types', 'jpeg jpg png gif'),
);
$types = node_get_types();
foreach ($types as $name => $type) {
$options[$name] = $name;
}
$form['zip_upload_node_type'] = array(
'#type' => 'select',
'#title' => t('Choose node type'),
'#description' => t('You must associate each file uploaded with a node type that has a either an upload field or a filefield attached to it.'),
'#options' => $options,
'#default_value' => variable_get('zip_upload_node_type', 'page'),
);
return system_settings_form($form);
}
?>The upload ZIP form was also very easy, I just had to remember to make the form have the right information initially, so hence $form['#attributes'] = array("enctype" => "multipart/form-data");.
<?php
/**
* Zip Upload form
*/
function zip_upload() {
$form = array();
$form['#attributes'] = array("enctype" => "multipart/form-data");
$form['zipfile'] = array(
'#type' => 'file',
'#title' => t('Zip'),
'#size' => 40,
'#description' => t('Click "Browse..." to select a Zip file to upload.'),
'#weight' => -3,
);
$form['hash_directory'] = array(
'#type' => 'value',
'#value' => md5(mktime()),
);
$form['file'] = array(
'#type' => 'value',
'#value' => '',
);
$form['buttons']['submit'] = array(
'#type' => 'submit',
'#value' => t('Import zip'),
);
return $form;
}
?>The hard part about the ZIP upload form was the validating function and the submit function. I decided the best way of handling this section would be to extract the ZIP during validation (after verifying it was a legitimate ZIP archive) into a temporary folder and then go through each file and check the extension, and remove all files with improper file extensions immediately. In the submit handler, I save the information about the directory for later, so that the user can come back to the next step, and then redirect the user to the next form.
<?php
/**
* Validate upload form
*
* @function
* Confirm the user has uploaded a zipped file first
* then we extract the file to a temporary folder and remove any undesired files.
*/
function zip_upload_validate($form, &$form_state) {
$dirpath = file_create_path(variable_get('zip_upload_import_path', 'zip_upload'));
if (file_check_directory($dirpath)) {
if ($file = file_save_upload('zipfile', array('file_validate_extensions' => array('zip')),$dirpath . $file->filename)) {
if (!$file) {
form_set_error('zipfile', t('Zip file not uploaded'));
}
else {
// try to avoid php's script timeout with large files or a slow machine
if (!ini_get('safe_mode')) {
set_time_limit(0);
}
// extract the zip file into the subdirectory
$zip = new ZipArchive;
if ($zip->open($dirpath . '/' . $file->filename) === TRUE) {
$directory = file_create_path($dirpath . '/' . $form_state['values']['hash_directory']);
$zip->extractTo($directory);
$zip->close();
file_delete($dirpath .'/'. $file->filename);
// check here for bad files and remove them?
$allowed_extensions = drupal_strtolower(variable_get('zip_upload_allowed_file_types', 'jpeg jpg png gif'));
$files = file_scan_directory($directory, '.*');
// This is crucial. We need to remove disallowed files.
$invalid = 0;
foreach ($files as $filename => $file) {
$errors = file_validate_extensions($files[$filename], $allowed_extensions);
if ($errors) {
$name = array_pop(explode("/", $filename));
// warn the user
drupal_set_message("Skipping " . check_plain($name) ." because extension isn't allowed", 'error');
// remove the file
file_delete($filename);
unset($files[$filename]);
$invalid++;
}
}
if ($invalid) {
drupal_set_message("There were $invalid errors while extracting your archive.", 'error');
}
elseif (!empty($files)) {
drupal_set_message(t('Zip file uploaded and extracted.'), 'success');
}
if (empty($files)) {
// remove empty subdirectories
_zip_upload_rmdir_recurse($directory);
// remove the now empty top directory
rmdir($directory);
form_set_error('zipfile', t('Your archive has no files with valid extensions in it. Please upload a different archive and try again.'));
}
}
else {
drupal_set_message(t('There was a problem trying to extract the zip archive to the temporary folder. Report this issue to your system administrator.'), 'error');
}
}
}
}
}
/**
* Submit upload form
*
* @function
* Here we store the information about the temporary folder and send the user to a
* form to categorize and give a description to each file.
*/
function zip_upload_submit($form, &$form_state) {
// save the zip file location and current user to the database for the next step
global $user;
db_query("INSERT INTO {zip_upload} (uid, hash_directory) VALUES (%d, '%s')", $user->uid, $form_state['values']['hash_directory']);
return drupal_goto('zip_upload/my');
}
?>Once the files are sorted onto the server and stored in a location which is difficult for the user to access, we then want the user to decide on a categorization for each file and give each file a quick description. Since I've stored the names of the temporary directories in my database at the previous step on a per user basis, this allows me to keep each set of uploaded files separate, and allows users to come back to the final form whenever they want. I decided on a gigantic table for my form with the vocabularies as select elements, the description for each file as a textfield, and then a checkbox next to each file name in each table so that the user could choose which files to either save permanently or delete.
<?php
/**
* Categorize recent resources form
*/
function zip_upload_recent() {
$form = array();
$vocabularies = _zip_upload_get_vocabularies();
// get the current user
global $user;
// look up any folders the user has created because of a zip upload
$result = db_query("SELECT hash_directory FROM {zip_upload} WHERE uid = %d", $user->uid);
$form['files'] = array(
'#type' => 'markup',
'#title' => t('Uploaded files'),
'#description' => t('Categorize each file, then click on Save'),
'#tree' => TRUE,
);
// at this stage files with disallowed extensions should be removed already.
while ($row = db_fetch_array($result)) {
$dirpath = file_directory_path() .'/'. variable_get('zip_upload_import_path', 'zip_upload') .'/'. $row['hash_directory'];
if (file_check_directory($dirpath)) {
$directory = $dirpath;
};
$files = file_scan_directory($directory, '.*');
// add an index to each textfield
$index = 0;
foreach ($files as $filename => $file) {
$form['files'][$index]['checked'] = array(
'#type' => 'checkbox',
'#default_value' => 0,
'#attributes' => array('class' => 'checked'),
);
$form['files'][$index]['filename'] = array(
'#type' => 'value',
'#value' => $filename,
);
// add a description
$form['files'][$index]['description'] = array(
'#type' => 'textfield',
'#size' => 20,
'#maxlength' => 255,
'#value' => '',
'#attributes' => array('class' => 'textfield-' . $index),
);
// now we include the taxonomy terms, bulk of this copied from taxonomy.module
foreach ($vocabularies as $vid => $name) {
// add a taxonomy select
$form['files'][$index]['taxonomy'][$vid] = taxonomy_form($vid, 0, ' ');
// add a class to each taxonomy select to make it easier to grab with jQuery
$form['files'][$index]['taxonomy'][$vid]['#attributes'] = array('class' => 'taxonomy-' . $vid);
// remove the title from each field
unset($form['files'][$index]['taxonomy'][$vid]['#title']);
}
$index++;
}
// remove this directory to help keep the folder clean.
if ($index === 0) {
if (file_check_directory($directory)) {
//_zip_upload_rmdir_recurse($directory);
rmdir($directory);
}
db_query("DELETE FROM {zip_upload} WHERE hash_directory = '%s'", $row['hash_directory']);
}
}
$form['number_of_files'] = array(
'#type' => 'value',
'#value' => $index,
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Save'),
);
$form['delete'] = array(
'#type' => 'submit',
'#value' => t('Delete'),
);
return $form;
}
?>Theming this form was relatively straight forward, I just converted by fields into individual rows and sent off most of the form to be themed as a table. At this stage I also added a CSS file and a JS file for later customization which I suspected was going to be necessary.
<?php
/**
* Theme the upload categorization form
*/
function theme_zip_upload_recent($form) {
$path = drupal_get_path('module', 'zip_upload');
drupal_add_js($path . '/js/zip_upload.js');
drupal_add_css($path . '/css/zip_upload.css');
$vocabularies = _zip_upload_get_vocabularies();
// build the header of the table
$header = array(
t('Select'),
t('File name')
);
foreach ($vocabularies as $vid => $vocabulary) {
$header[] = check_plain($vocabulary);
}
// unlikely we'll have more than 1000 vocubularies created on the same site
$header[] = t('Description');
$rows = array();
for ($index = 0; $index < $form['number_of_files']['#value']; $index++) {
$row = array(
'checked' => drupal_render($form['files'][$index]['checked']),
);
$name = array_pop(explode("/", $form['files'][$index]['filename']['#value']));
// already filtered
$row['filename'] = check_plain($name);
foreach ($vocabularies as $vid => $vocabulary) {
$row[$vid] = drupal_render($form['files'][$index]['taxonomy'][$vid]);
}
$row['description'] = drupal_render($form['files'][$index]['description']);
$rows[] = $row;
}
if (empty($rows)) {
return t('You do not have any files left to perform operations on.');
}
$output .= theme('table', $header, $rows, array('class' => 'zip-upload-table'));
$output .= drupal_render($form);
return $output;
}
?>The next step was to build the submit function for the form. A validation function was unnecessary in this case since I had already validated the file extensions, and my site fixes the input filter for the description at the default filter. Also, the user doesn't actually have access to the file locations, so I wasn't worried about the files being switched somehow at this stage. My submit function basically splits into two cases, one where the user wants to save the file permanently, and one where the user wants to delete the file. In both cases, the final step is to check if the file is in an now empty sub-directory and delete that directory. One issue I am still having is creating a function to check if the entire temporary directory is empty, and removing it if it is.
I decided to use node_submit and node_save here to save the node information rather than drupal_execute because I have found the first two functions easier to work with, and they serve my purpose well. Finally, I was unsure if the validation on the file field would work properly given that the uploaded file being attached to the node wasn't in the official temporary directory. In any case all of the right hooks seem to fire using this code, and my users get points (using the User Points module) as expected.
<?php
/**
* Submit function
*/
function zip_upload_recent_submit($form, &$form_state) {
// aliased for readability
$form_values <