Posts tagged onlyconnect
Talking to WordPress with ActiveRecord
Jul 24th
As mentioned in yesterday’s announcement I’m pulling some content across from this blog (running on wordpress) into the new Ket Lai site (a merb app). I’ve found myself doing similar things a few times lately, such as on Only Connect (on which more, soon) and so have built out a selection of ActiveRecord models to help me talk to a wordpress database from a ruby app.
At Matt‘s urging (he’s been using them to move data from a legacy site), I’ve finally put those models up on github. Being a single file they arguably should have been a gist, but I’m reserving the right to reorganise them in future.
They’re far from complete in that there are lots of validations I could have added in, named scopes that would probably be handy, and loads of convenience methods that some might like, but hopefully they’ll be of use to someone and evolve over time.
(on the subject of wordpress, I’m glad to see the addition of changelogs to the plugin directory – being asked to upgrade plugins without any idea what’s changed has long been a bugbear of mine, so hopefully this will resolve some of that)
Hacking wordpress to support per-post banner images
Feb 23rd
I seem to be spending a lot of time with wordpress at the moment. It’s become so ubiquitous that it often makes far more sense to set it up and integrate with an existing app than to set up some other blogging system and re-train users. As a result I’ve been writing a few wordpress plugins. Most of them are too specialised to be worth sharing, but one seemed worth opening up…
Implementing a (not quite public yet) design recently I had need of a way to specify a banner image for each post. While wp has pretty good support for adding various media into the body of posts, this needed to sit outside the post body.
I whipped together a quick plugin to handle uploading a banner and storing its details in the metadata for the post. It was a simple process, nicely self-contained, except that the post edit form doesn’t have the appropriate enctype=”multipart/form-data”. I looked around for any hooks that would allow me to cleanly add attributes to the form tag, but in the end resorted to editing wp-admin/edit-form-advanced.php to add it.
I’d hoped that there’d be time to find a cleaner way to do all this before telling people about it—perhaps some javascript that hooks into the existing media selector but allows it to populate a custom data field?—but it hasn’t, so I’m throwing it out there to the wider world as-is. The code is at github. Feel free to take it and use it as-is, to fork it and update it to be a better wordpress citizen, to email me patches to apply to my copy, or even to employ me to spend more time cleaning it up! Either way, it deserves to be out in the open and hopefully it’ll be of us to somebody besides me.
Handling custom file attachments in WordPress plugins
Dec 31st
I’ve been working on a wordpress plugin to make it easy to set a banner image for any blog entry outside the post content. The current approach is flawed as it requires hacking at the form tag for the post edit page to allow file uploads, but while I look around for a better way to do that without sacrificing the simplicity of the UI, I wanted to share some digging I did to figure out how to handle file uploads with wordpress. All this data is available in the documentation, but it took quite a bit of looking to find it and there aren’t enough examples kicking around.
So without further ado…
When wordpress receives a file upload it hands it to the function wp_handle_upload. This takes two arguments:
/*
* @param array $file Reference to a single element of $_FILES. Call the function once for each uploaded file.
* @param array $overrides Optional. An associative array of names=>values to override default variables with extract( $overrides, EXTR_OVERWRITE ).
*/and returns:
/*
* @return array On success, returns an associative array of file attributes. On failure, returns $overrides['upload_error_handler'](&$file, $message ) or array( 'error'=>$message ).
*/In practice that means a successful run will give you something like:
array ( 'file' => '/path/to/webroot/wp-content/uploads/myfile.png', 'url' => 'http://my.host/wp-content/uploads/myfile.png', 'type' => 'image/png', )
It can be found in wp-admin/includes/file.php.
A simplified example of how I’m using it in my wordpress-post-banners plugin follows:
/* Define the callback that will be run when a post is saved */ add_action('save_post', 'post_banners_process_saved_post'); /** * Process any new file that has been uploaded for this post * * If a new file has been uploaded, we hand it to wordpress to process * and then store the details in a custom field. * * @param integer $post_id may be the actual ID, may be a revision ID * @return null */ function post_banners_process_saved_post($post_id) { /* If the file we want has been uploaded */ if (isset($_FILES['post_banner_image'])) { $file = wp_handle_upload($_FILES['post_banner_image']); /* Check if it was an error */ if (isset($file['error'])) { // handle error } else { // store the image details in a custom field global $wpdb; $wpdb->query($wpdb->prepare('REPLACE INTO ' . $wpdb->postmeta . ' (`post_id`, `meta_key`, `meta_value` ) VALUES (%s, %s, %s)', $post_ID, 'post_banner_image', $file['url'])); } } }
I’ll post again once the plugin settles down, but in the meantime hopefully an extra example of how to use wp_handle_upload will help someone.
Using amazon and amazon-ecs to identify genres
Dec 22nd
I’m currently building a site where users will enter details of books and music they’re listening to and we want to provide lists of that on their profile and also find ways of matching users based on those choices. We’re looking at a number of ways of doing that, including matching based on genre, and in order to achieve that I needed a way to identify the genres for their listed books and music.
I knew that amazon provided that data through their Associate Web Services (formerly E-Commerce services) so I fired up the trusty amazon-ecs gem and pulled down the data for a couple of books and CDs. eg.
res = Amazon::Ecs.item_search(asin, :search_index => 'Music', :country => 'uk', :response_group => 'Large')
Looking at the results I see that amazon assign a varying number of ‘browsenodes’ to each item. browsenodes are a flexible system that allows amazon to associate arbitrary hierarchical data to items in their catalogue. They describe them in the documentation as:
Browse nodes are categories into which items for sale are organized. A single node might have many items associated with it. In the above example, the child node, “Boxed Sets,” might have the items “Abott and Costell Collection,” and “Laurel and Hardy Collection” associated with it.
The number of items associated with a browse node can change radically over time as items are added for sale, or as items go out of stock and are no longer sold. For example, for the browse node, TopSellers, items are attached and unattached according to their sales.
Even browse nodes themselves are created and deleted as items demand. When, for example, a new toy starts selling briskly, there may not be a node that appropriately categorizes the toy. In that case, a node would be created and the toy would be associated with the node. Then, if the sale of the toy died out, the node might be deleted. Other nodes are much longer lived. Top level nodes, for example, “Books” and “Apparel,” have remained unchanged for years.
That means that an album may have browsenodes giving us values of “Pop, Jazz, and Hard Bop”, but the same item may also have browsenodes with names like “Bestsellers”, “40% off sale”, and all sorts of other useless data. Thankfully it’s fairly easy to identify that genre data all sits under “Styles” (for music) and “Subjects” (for books) which have IDs 520920 and 1025612 respectively in the UK.
Once I had those parents identified it was easy enough to extend the Amazon::Element class to add a method to read out the appropriate browsenodes and return them:
class Amazon::Element def extract_genre_children_of(browse_node_id) browse_nodes = elem.search('browsenodes>browsenode') genres = browse_nodes.inject([]) do |collection, bnode| if bnode.search("//ancestors//browsenode/browsenodeid[text()='#{browse_node_id}']").length > 0 collection += bnode.search('name').collect(&:inner_text) end collection end genres.uniq end end
That code’s far from perfect – it returns a few higher level items I subsequently filter out – but it’s doing the job for me.
Of course, the resulting data is variable in quality. I certainly wouldn’t call Battles’ Mirrored “Adult Contemporary”, but it’s a start towards finding some genres automatically and hopefully as we build up a large enough store of data it will begin to become more useful. In the meantime, we’re also experimenting with musicbrainz to pull in some extra data from their tags.