Improved Newsletter Creation

The site has a custom post type called ‘newsletter’. This is for the weekly newsletters which will be uploaded to the site. The format for this is very specific. The title will be the title of the newsletter, the content will contain a download instruction, followed by the link to the actual PDF file, and two custom fields will hold information about the memory verse of the week.

By using a custom meta box for the memory verse, the only item left that might lead to inconsistent output would be to allow the user to key in whatever they want for the download instruction. Also, the default media upload would require the user to key in the newsletter title again for the attachment to be appropriately titled. All this leads to lots of room for error.

My solution is to disable the default editor and replace it with a meta box which only allows file upload.

Making these changes required quite a few edits to the functions.php file.

Disable Editor

To disable the editor, add the key-value pair 'supports' => array('title') to the arguments array in the register_post_type() function.

Define the Meta Box

This is based on previous posts about creating meta boxes. The original code was taken from deluxeblogtips.com
The above link actually has a download link to a version of the code where file and image upload has been implemented. Although it’s not described in the blog post, it has actually been done.

$meta_boxes[] = array(
	'id' => 'newsletter-upload',
	'title' => 'Newsletter Upload',
	'pages' => array('newsletter'),
	'context' => 'normal',
	'priority' => 'high',
	'fields' => array(
		array(
			'name' => 'Newsletter Upload',
			'desc' => 'Select the MM to upload',
			'id' => $prefix . 'newsletter_file',
			'type' => 'file',
			'std' => ''
		)
	)
);

This declares a meta box with just a file upload field.

Change Form Type

To support file uploads, the form encoding must be declared as ‘multipart/form-data’. To do this without hacking core, jQuery is used to add the attribute and the function is hooked to ‘admin_head’.


add_action('admin_head', array(&$this, 'add_post_enctype'));

	function add_post_enctype() {
		echo '
			
			jQuery(document).ready(function(){
					jQuery("#post").attr("enctype", "multipart/form-data");
					jQuery("#post").attr("encoding", "multipart/form-data");
					});
		';
	}

This code is actually placed inside the My_meta_box class, as explained in the tutorial. The action add is done in the constructor.

Show the Field

The show() function must be updated to include the input ‘file’ type. The code here has been customized so it shows the link to the PDF file which is currently attached to the newsletter.

case 'file':
			//find any attachments assigned to the post
			$children = get_children(array('post_parent' => $post->ID, 'post_type' => 'attachment'));
			if($children) {
				echo 'Currently attached: 
'; foreach ($children as $child) { //there should only be 1 child anyway echo 'guid . '\'>' . $child->guid . '
'; } echo '
'; } echo $meta ? "$meta
" : '', '', '
', $field['desc']; break;

Save the Data

This code is run when the ‘Publish’ button is clicked. The original code had a strange foreach loop in it. I’m not sure what it does, but taking it out allowed my code to work when there’s only 1 file input box.

The code here as some specialized functions. Given a filename in the format prefix-date.pdf, it will rename it to my desired prefix and change the date format to yyyy-mm-dd. This supposes that the original date format is either yyyy-mm-dd or yyyy-MMM-dd. Either spaces or dashes can be used as separators in the filename. Thus the code here will accept the naming convention currently used by the church, and change it to the one used on the site.

I also want the uploads to go into the folder depending on when the file is dated and not when it is uploaded. Thus the year and month information is also used to tell WordPress where to upload the file to. To do this, a parameter $time in the format yyyy-mm must be passed to the wp_handle_upload() function.

Next, if a new attachment is added when is already attached to the post, I want the old one to be unattached. Thus each newsletter post can only have one attached file. Unattaching the old ones allows them to show up as unattached in the media library. Attachments should also take on the title of the parent post. When unattached, their title reverts to one based on their actual filename.

Finally, the content of the newsletter post must be in a specific format. This is dynamically generated with the path to the attached file and the title of the post. The post is then updated with wp_update_post()

if ($field['type'] == 'file' || $field['type'] == 'image') {
			if (!empty($_FILES[$name])) {
				$this->fix_file_array($_FILES[$name]);
				//get the name of the uploaded file, without the extension
				$uploaded_file = $_FILES[$name]['name'];
				$uploaded_file = basename($uploaded_file);
				//replace spaces with '-' to facilitate year/mth extraction
				$uploaded_file = str_replace(' ', '-', $uploaded_file);		

				$name_parts = explode('-', $uploaded_file);
				if( !is_numeric( $name_parts[2] ) ) {	//assume 3 letter month, replace with 2 digit number
					$month_names = array("JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP",
											"OCT", "NOV", "DEC");
					$name_parts[2] = strtoupper($name_parts[2]);
					$name_parts[2] = array_search($name_parts[2], $month_names) + 1;
					$name_parts[2] = sprintf('%02u', $name_parts[2]);
				}
				$time = $name_parts[1] . '-' . $name_parts[2];		//time is in yyyy-mm format

				if(strtoupper($name_parts[0]) == 'WEEKLY') {		//rename weekly to mm
					$name_parts[0] = 'mm';
				}

				if(preg_match('/[0-9]{4}-[0-1][0-9]/', $time) == 0) {		//time does not match format
					$time = null;
				}
				$_FILES[$name]['name'] = implode('-', $name_parts);			//rename file

				$file = wp_handle_upload($_FILES[$name], array('test_form' => false), $time );
				$filename = $file['url'];
				if (!empty($filename)) {
					$currPost = get_post($post_id);

					//unattach current attachment, if any
					$children = get_children(array('post_parent' => $post_id, 'post_type' => 'attachment'));
					if($children) {
						foreach($children as $child) {
							$path_parts = pathinfo($child->guid);
							wp_update_post(array('ID' => $child->ID, 'post_parent' => 0, 
									'post_name' => $path_parts['filename'], 
									'post_title' => $path_parts['filename']));
						}
					}

					//attach attachment post to the main parent post
					$wp_filetype = wp_check_filetype(basename($filename), null);
					$attachment = array(
							'post_mime_type' => $wp_filetype['type'],
							//	'post_title' => preg_replace('/\.[^.]+$/', '', basename($filename)),
							'post_status' => 'inherit',
							'guid' => $filename,
							'post_title' => $currPost -> post_title
						);
					$attach_id = wp_insert_attachment($attachment, $filename, $post_id);
					// you must first include the image.php file
					// for the function wp_generate_attachment_metadata() to work
					require_once(ABSPATH . 'wp-admin/includes/image.php');
					$attach_data = wp_generate_attachment_metadata($attach_id, $filename);
					wp_update_attachment_metadata($attach_id, $attach_data);

					//update post content with the download text and link to the file
					$post_content = 'Download MM: ' . $currPost -> post_title . '';
					wp_update_post(array('ID' => $post_id, 'post_content' => $post_content));
				}
			}
		}

Doing this was a lot of work and took up the whole of Saturday just to get right, but having this functionality in place finally makes it possible to pass on some of the updating work to other people.

File Permissions

I seldom go to the web host’s control panel as most tasks can be accomplished through SSH and its file transfer tool. While checking through to make sure that my automated post insertion program was running as expected, I noticed that media files uploaded through the WordPress admin interface were owned by the web server! This is bad because I would no longer be able to manage or delete the files. Although it’s unlikely that the files will be deleted, I should be able to do so if required.

Since there was nothing I can do about the file ownership, I raised a support request with pair.com. They were very efficient and replied in just a few hours. The rogue file were restored to my username and I can now delete and modify them. They also provided a solution to solve the problem of WordPress uploads coming under the ‘nobody’ user. That involves using a cgi wrapper to run PHP under my own username. They helpfully provided a link to the instructions.

Once I had that setup, I tried uploading a file and found that it is indeed under my own username. Another problem solved.

Migration Gotcha

I’m currently migrating from test to live site, and vice versa, by doing database find and replace for the URL. Some things occasionally slip past.

After uploading the database to the live site, everything seemed to work fine. However, I noticed today that the link from the site title had an extra subfolder attached to it. So instead of showing http://domain.com, I was getting http://domain.com/folder. The link still worked, but it seemed to be redirecting to the wrong place. Instead of domain.com, I was getting http://www.domain.com.

I suspected that this was because I did use a subfolder for my test site and probably forgot to change a database value. Poking around in header.php revealed the answer. The home_url() function gets the option value home from the wp_options table in the database. Sure enough, this value had only been partially updated during my search and replace operations. Fixing this fixed the extra www in the domain name and got the link to point to the right place.

Hierarchical Menu

Problems

After using the menu editor in the admin interface to modify the menu structure, an ugly problem showed up. I had created a subpage, but the drop down menu wasn’t showing. Instead, a scroll bar appeared in the header when I moved my mouse over the menu item, and unless I put focus on the header first so I could scroll down with the arrow keys later, I couldn’t see the menu item at all. Also, the colour was all wrong. The sub-item showed up with a dark gray background colour and black text, not exactly the most readable combination.

Fix the Scrollbars

Scrollbars appear when the overflow property in CSS is set to auto. I found the offending line in #header and removed it. However, the header’s colour no longer filled the header section. It didn’t have enough height. The wrapper <div> container was not expanding to include the heights of the elements.

To solve this, I simply added height: 144px to #branding. The value of 144px came from adding the height values of the contained <div> blocks. The header colour now expands to fill the header section, just like it had before.

Fix the Colours

Examining the elements in Google Chrome revealed that the colours for nested menus are controlled by a few CSS selectors. Note that these are for the twenty ten theme, different themes might use different CSS selectors.

For the current item the mouse is over, the selector is #menu-main .menu-item a:hover

For the unselected parent, the selector is #access li:hover > a, #access ul ul :hover > a

For the child item, the selector is #access ul ul a

Modify the background-color property in these selectors and that’s it!

Favicon

I took the logo and resized the longest dimension down to 16 pixels in GIMP, then created a new 16 by 16 pixel image and pasted it in.

It was necessary to delete the background and extend the layer size to the image size to get the desired effect of a transparent background. After that, just save the image as an .ico file and follow the instructions at the WordPress Codex.

It was necessary to include the line the codex page said to add into header.php.

Remove Posts Menu from Admin

While the content manager has the edit_posts capability so he can edit media items, this also exposes the posts menu. I don’t want the content manager to handle normal posts, so I decided to hide the menu item from the admin interface. It will still be possible to go to the URL manually and edit posts, but removing the item from the menu reduces the chance of user error.

The point is to reduce the number of options in the menu so the user isn’t intimidated. Really preventing the user from editing posts isn’t such an important issue for my use.

The article at sixrevisions was used to figure out what code to include. I determine which role the user holds by checking his capabilities. In my setup, only the content manager and admin has edit_sermons, but only the admin has update_core. Thus being having edit_sermons but not update_core makes the user a content manager.

function mbpc_remove_menu_items() {
	if ( current_user_can( 'edit_sermons' ) && !current_user_can( 'update_core') ) {

		global $menu;
		$restricted = array(__('Posts'));
		end ($menu);
		while (prev($menu)){
			$value = explode(' ',$menu[key($menu)][0]);
			if(in_array($value[0] != NULL?$value[0]:"" , $restricted)) {
				unset($menu[key($menu)]);
			}
		}
	}
}

add_action('admin_menu', 'mbpc_remove_menu_items');

Now when logged in as a content manager, the Posts menu can no longer be seen. Less clutter, less confusion.

Roles and Capabilities for Custom Post Types

WordPress access rights are controlled by Roles and Capabilities. To perform an action, your user role must have that capability. For example, to edit posts, your role must have edit_posts assigned to it.

Custom Taxonomies

Strangely, while managing terms for custom taxonomies requires manage_categories, this does not give the user the right to assign terms to the post. To allow that, add

'capabilities' => array('assign_terms' => 'edit_sermons')

to the arguments array of register_taxonomy(). Replace the word “sermons” with whatever you want to name this capability. Naming it edit_sermons means that the user who can edit sermon custom post types will also be able to assign terms belonging to this taxonomy.

Custom Post Types

Posts and pages have built in capabilities for editing, adding, publishing etc etc. To restrict custom post types, it’s necessary to assign them a new bunch of capabilities. Do this by adding the following code to the array of arguments.

'capability_type' => 'sermon',
'map_meta_cap' => true

The map_meta_cap key is necessary so that edit_sermons, delete_sermons and add_sermons will work as expected.

This is explained in Justin Tadlock’s article, but he had to do it manually then.

Assign Capabilities

Use the Members plugin to create roles and assign capabilities. When assigning capabilities, give edit_{capability_type}, publish_{capability_type}, but add an ‘s’ after that (plural form).