Couchrest Couchdb thumbnail generation with Mini_Magick and Merb (or Rails)
Thumbnails – or avatars – are a standard part of many web apps. Paperclip has made things super easy for active record and datamapper. Here is how I accomplished similar – but basic – thumbnail generation for couchdb. The end solution is RESTful, good for my needs, and hopefully good for your needs too.
You should know that couchdb supports inline attachments and I opted for that route rather than store the images in a file folder like paperclip does. I felt this was more inline with the couchdb way of doing things.
Dependencies
Begin by installing mini_magick
1 sudo gem install mini_magickOr add mini_magick to dependencies.rb
1 dependency "mini_magick", "1.2.5"
I’m using Merb. The setup for rails would be to use config.gem mini_magick I believe.
Setup the view
Place the avatar uploading mechanism in it’s own form. Use the /users/update_avatar POST action to process the thumbnails.
Setup the users.rb update_avatar POST action and route
The update_avatar action will process the ‘avatar’ image file using the custom ‘update_avatar_images’ method. If you are on rails, you will have to pass params[:avatar] here.
Additionally, the avatar route/action is defined in users.rb also. This will allow us to display the thumbnails restfully via /user/scottmotte/avatar & /user/scottmotte/avatar?style=mini
Furthermore, you will see that my validation is not in the model as it ideally should be. I haven’t figured out how to tackle this yet based on how the couchdb attachments work inline as an array.
Setup the user.rb model to process the thumbnails
Rather than pick out the little pieces of the user.rb model that are related to the update_avatar action in users.rb I’m just posting the entire model.
The key method to examine is the update_avatar_images and it’s cousin resize_and_crop – which resizes the uploaded avatar to a square.
The strange AppConfig array I am using there is from my config/app_config/settings.yml file from merbjedi’s merb_app_config. It looks like this the following but you could just as easily define your own array or list each upload separately.
The put_attachment method is from couchrest and puts the images to the couchdb user document.
Now when a user uploads an avatar image, it creates an inline original.png, thumb.png, and mini.png. And the are accessible via /users/scottmotte/avatar?style=mini
That’s how I did things. Let me know if you have a better way. A gem would be ideal. Maybe I’ll morph this into a gem if I find myself repeating it enough.
Add custom methods to Spreedly gem
I am using the spreedly gem again rather than my own custom Subscriber.rb model. I figured out how I could tack on extra methods to the Spreedly::Subscriber class. You just create a model/class called Spreedly::Subscriber. This is probably rudimentary knowledge for many, but for me it wasn’t. Sometimes it sucks to have a business background. =)
I needed to add the activate_free_trial method for the mocking portion of the spreedly gem. I did it by creating a model named spreedly_subscriber.rb and added the following code.
Using Spreedly with Merb via HTTParty
This is almost completely stolen from the spreedly gem, but I added some custom methods and didn’t want to be a slave to the gem. It allows you to do things like Subscriber.create!(id, email=nil, screen_name=nil) & Subscriber.activate_free_trial. I was using active resource with merb previously but Nathaniel’s spreedly gem convinced me to use HTTParty instead.
To use, create a file in app/models/subscriber.rb and paste the following in. I am using AppConfig for the settings. (sudo gem install merb_app_config).
Here’s some example usage.
Unobtrusive Ajax with Merb, Datamapper, and jQuery
This article describes how to add ajax to your merb & datamapper application using jquery. It uses an example from my own project investapp to create a ‘more’ link similar to twitter’s.
First, install the agnostic branch of will_paginate. This isn’t available as a public gem yet, but auxesis maintains a working fork of will_paginate agnostic. Add the following to dependencies.rb
Use will_paginate just like you do in rails applications. Here I’m paginating the messages. I’m also providing .js to the @display messages. Rails does this with the respond_to block.
Add the will_paginate helper links to your view.
Now for the jQuery. First and foremost make sure you are including jquery in your view.
Setup the ajax using jquery. There is a decent amount going on here.
Firstly, create a getWithAjax jquery method. Thanks goes to Ryan Bates jQuery episode for this. This basically tells merb to use the index.js.erb file instead of the index.html.erb file.
Secondly, inside the document ready is code that takes the default will_paginate helper html code and hides all the Prev, 1,2,3,4, Next links, replaces, them with the text ‘More’ like on twitter, and makes them ajaxifiable via a class=‘more-entries’ attribute.
Thirdly, the $(“div.pagination a[rel=‘next’]”).not(‘.next_page’).show(); makes sure only the next page link is viewable.
Finally, we just have to tell merb what to do once the link is clicked and goes to index.js.erb.
Our messages/index action just got hit and returned us @messages. The first //append the partials section appends the next batch of 15 messages below the previous 15 messages – just how twitter does.
The next line shows the next ‘next_page’ link so we will be able to click next again.
And the final line, hides the link we just clicked.
The end.
Merb and Datamapper Single Table Inheritance
To begin add the Discriminator property to your model. I will be using User as my main model, and there will be an administrator and member model to inherit from it.
# app/models/user.rb class User include DataMapper::Resource .. property :type, Discriminator end
rake db:automigrate
Next, add your additional models based that will use the single table inheritance. You could create a separate models/model_name.rb for these, but I just lump them in with models/user.rb
# app/models/user.rb class User include DataMapper::Resource .. property :type, Discriminator .. end class Editor < User; end class Administrator < Editor; end
Now you can run commands like User.all, Administrator. all, and Editor.all. And to create a user with an Administrator role do something like the following.
User.create(:login => 'scottmotte', :password => 'password', :password_confirmation => 'password', :email => 'scott@scottmotte.com', :type => 'Administrator')
Finally, you need to add routes for the Editor and Administrator otherwise resource(user) will give you serious generation errors.
# router.rb resources :users resources :administrators, :controller => :users
How to remove sources from your gem list
1 gem sources -r http://example.com
Create a merb slice with jeweler
A slice is just a gem so we can create and manage our slice by starting with jeweler.
Jeweler
gem sources -a http://gems.github.com sudo gem install technicalpickles-jeweler git config --global user.email johndoe@example.com git config --global user.name 'John Doe' git config --global github.user johndoe git config --global github.token 55555555555555 jeweler --create-repo --summary "Sandy Koufax Slice" sandy_koufax_slice
Slice
Because jeweler is a bit opinionated and creates a structure for us, create your slice in a tmp directory and then copy the app, config, spec, etc files over to our sand_koufax_slice jeweler structure. Yep, a bit sloppy, and there is a more elegant way, but for now copy and paste worked well for me.
Be careful with the Rakefile. It should look something like this:
Once you’ve got standard files copied over let’s start creating the slice. To do so, we first need to set the slice up to work in development mode (this bit of hackery will eventually go away in Merb 1.2 with embeddable apps. I’m excited for that.).
1. Add the following to config/init.rb
require 'config/dependencies.rb' use_orm :datamapper use_test :rspec use_template_engine :erb
2. Create config/dependencies.rb
# dependencies are generated using a strict version, don't forget to edit the dependency versions when upgrading. merb_gems_version = "1.0.10" dm_gems_version = "0.9.10" do_gems_version = "0.9.11" # For more information about each component, please read http://wiki.merbivore.com/faqs/merb_components dependency "merb-core", merb_gems_version dependency "merb-action-args", merb_gems_version dependency "merb-assets", merb_gems_version dependency("merb-cache", merb_gems_version) do Merb::Cache.setup do register(Merb::Cache::FileStore) unless Merb.cache end end dependency "merb-helpers", merb_gems_version dependency "merb-mailer", merb_gems_version dependency "merb-slices", merb_gems_version dependency "merb-auth-core", merb_gems_version dependency "merb-auth-more", merb_gems_version dependency "merb-auth-slice-password", merb_gems_version dependency "merb-param-protection", merb_gems_version dependency "merb-exceptions", merb_gems_version dependency "data_objects", do_gems_version dependency "do_mysql", do_gems_version # If using another database, replace this dependency "dm-core", dm_gems_version dependency "dm-aggregates", dm_gems_version dependency "dm-migrations", dm_gems_version dependency "dm-timestamps", dm_gems_version dependency "dm-types", dm_gems_version dependency "dm-validations", dm_gems_version dependency "dm-serializer", dm_gems_version dependency "merb_datamapper", merb_gems_version
3. Bundle the dependencies
This turned out to be a big headache when it came to installing the gem. Just use the gems on your machine and install with sudo gem install merb as necessary.
Bundling our dependencies makes development easier – especially if we want someone else to work on our slice. But the only way to bundle is to get thor in our slice, and it is not there by default like in a merb app. So let’s add it.
To get the thor tasks go into a tmp directory and generate a fresh merb application, then just use copy and paste to move /tasks folder and its contents into your slice. It should look something like this:
- tasks/
doc.thor
merb.thor/
app_script.rb
common.rb
gem_ext.rb
main.thor
ops.rb
utils.rb
thor merb:gem:install
Now you should be able to run bin/slice and bin/rake to run and test your slice.
4. Development and Test Database
Create config/database.yml and put in the following content
development: adapter: sqlite3 database: sample_development.db test: adapter: sqlite3 database: sample_test.db production: adapter: sqlite3 database: production.db
Create a model and then rake the database
slice -i DataMapper.auto_migrate!
In spec_helper.rb add the before(:all) auto_migrate line for datamapper
Spec::Runner.configure do |config| config.include(Merb::Test::ViewHelper) config.include(Merb::Test::RouteHelper) config.include(Merb::Test::ControllerHelper) config.include(Merb::Test::SliceHelper) config.before(:all) do DataMapper.auto_migrate! if Merb.orm == :datamapper end end
Then you just have to start building your slice – which is tricky. That’s all I’ve got for now.
How to test for subdomains in Merb
I’ve done a couple subdomain applications now in merb. One of the things that continually bothered me was that I was unable to figure out how to test for subdomains. Thanks to Shalon Wood in the Merb google group, I now am able to do so. I don’t think Rails can even do this.
requests/payment_spec.rb
require File.join(File.dirname(__FILE__), '..', 'spec_helper.rb') describe "Authenticated user logged in", :given => "authenticated user" do describe "/payment", :given => 'current_site' do before(:each) do @response = request("http://#{valid_site_attributes[:subdomain]}.example.org/payment") end it "should respond successfully" do @response.should be_successful end end end
spec_helper.rb
def valid_site_attributes(options = {}) { :domain => 'http://www.example.org', :folder => 'exampleorg', :subdomain => 'example', :active => true, :id => 1 }.merge(options) end given "current_site" do Site.all.destroy! @current_site = Site.create(valid_site_attributes) end
app/controllers/application.rb
def get_site # uses @current_site to create pages under appropriate site like @current_site.pages.new @current_site = Site.first(:subdomain => request.first_subdomain) raise NotFound unless @current_site end
PDF Generation in Merb using HTMLDOC
A good part of this is thanks to this tutorial.
Installation
# Download HTMLDoc from <a href="http://www.htmldoc.org/software.php?VERSION=1.9.x-r1586&FILE=htmldoc/snapshots/htmldoc-1.9.x-r1586.tar.gz">here</a> tar zxvf htmldoc-1.9.x-r1586.tar.gz cd htmldoc-1.9.x-r1586 ./configure --prefix=/usr/local make sudo make install
Install gem
sudo gem install htmldoc
Configure merb app
# dependencies.rb dependency "htmldoc", "0.2.1"
# init.rb Merb::BootLoader.after_app_loads do # This will get executed after your app's classes have been loaded. Merb.add_mime_type(:pdf, :to_pdf, %w[application/pdf], "Content-Encoding" => "gzip") end
Create an action for the pdf generation
(there must be a way to just put this in the show action instead of a separate action but i had trouble)
# router.rb resources :proposals, :collection => { :generate => :get }
# proposals.rb controller def generate(id) only_provides :pdf pdf = PDF::HTMLDoc.new pdf.set_option :bodycolor, :white pdf.set_option :toc, false pdf.set_option :portrait, true pdf.set_option :links, true pdf.set_option :webpage, true pdf.set_option :left, '2cm' pdf.set_option :right, '2cm' pdf.set_option :header, "Header here!" pdf << "<h1>Title</h1> <p>This is some <strong>bold</strong> text.</p>" pdf.footer ".t." send_data pdf.generate end
Then just put a link_to in your view
<%= link_to "Generate PDF", "/proposals/generate/#{@proposal.id}.pdf" %>Warning: I was never able to get background images to work. I don’t know if this was a limitation with the htmldoc ruby gem, whether it was something with my htmldoc install, or whether it was a stupid coding mistake on my end.
How to install RMagick on Leopard from source
This is the best tutorial I’ve found for installing rmagick on leopard from source. Installing RMagick on Leopard (without MacPorts or Fink)
1 #!/bin/sh 2 wget http://download.savannah.gnu.org/releases/freetype/freetype-2.3.5.tar.gz 3 tar xzvf freetype-2.3.5.tar.gz 4 cd freetype-2.3.5 5 ./configure --prefix=/usr/local 6 make 7 sudo make install 8 cd .. 9 10 wget http://superb-west.dl.sourceforge.net/sourceforge/libpng/libpng-1.2.22.tar.bz2 11 tar jxvf libpng-1.2.22.tar.bz2 12 cd libpng-1.2.22 13 ./configure --prefix=/usr/local 14 make 15 sudo make install 16 cd .. 17 18 wget ftp://ftp.uu.net/graphics/jpeg/jpegsrc.v6b.tar.gz 19 tar xzvf jpegsrc.v6b.tar.gz 20 cd jpeg-6b 21 ln -s `which glibtool` ./libtool 22 export MACOSX_DEPLOYMENT_TARGET=10.5 23 ./configure --enable-shared --prefix=/usr/local 24 make 25 sudo make install 26 cd .. 27 28 wget ftp://ftp.remotesensing.org/libtiff/tiff-3.8.2.tar.gz 29 tar xzvf tiff-3.8.2.tar.gz 30 cd tiff-3.8.2 31 ./configure --prefix=/usr/local 32 make 33 sudo make install 34 cd .. 35 36 wget http://jaist.dl.sourceforge.net/sourceforge/wvware/libwmf-0.2.8.4.tar.gz 37 tar xzvf libwmf-0.2.8.4.tar.gz 38 cd libwmf-0.2.8.4 39 make clean 40 ./configure 41 make 42 sudo make install 43 cd .. 44 45 wget http://www.littlecms.com/lcms-1.17.tar.gz 46 tar xzvf lcms-1.17.tar.gz 47 cd lcms-1.17 48 make clean 49 ./configure 50 make 51 sudo make install 52 cd .. 53 54 wget http://ufpr.dl.sourceforge.net/sourceforge/ghostscript/ghostscript-8.60.tar.gz 55 tar zxvf ghostscript-8.60.tar.gz 56 cd ghostscript-8.60/ 57 ./configure --prefix=/usr/local 58 make 59 sudo make install 60 cd .. 61 62 wget http://ufpr.dl.sourceforge.net/sourceforge/ghostscript/ghostscript-fonts-std-8.11.tar.gz 63 tar zxvf ghostscript-fonts-std-8.11.tar.gz 64 sudo mv fonts /usr/local/share/ghostscript 65 66 wget http://imagemagick.site2nd.org/imagemagick/ImageMagick-6.3.5-9.tar.gz 67 tar xzvf ImageMagick-6.3.5-9.tar.gz 68 cd ImageMagick-6.3.5 69 export CPPFLAGS=-I/usr/local/include 70 export LDFLAGS=-L/usr/local/lib 71 ./configure --prefix=/usr/local --disable-static --with-modules --without-perl --without-magick-plus-plus --with-quantum-depth=8 --with-gs-font-dir=/usr/local/share/ghostscript/fonts 72 make 73 sudo make install 74 cd .. 75 76 sudo gem install RMagick