Showing posts with label plugins. Show all posts
Showing posts with label plugins. Show all posts

Saturday, 24 December 2011

Autocomplete with acts_as_taggable_on

Basic tagging sorted, I went looking for a way to add auto-complete - cos it's a nice UI improvement that's pretty common on teh intarwebs these days. I did a lot of searching, as most of the solutions seem to require switching to jquery, and for reasons of laziness (and legacy code), I don't wanna do that right now.


Luckily my searching dug up a tutorial by Michael Schuerig called: auto-completion for tag lists that works with acts_as_taggable_on and the auto_complete plugin.


It pretty much covers all the bases for tagging, so I won't repeat it all here. But I think it's awesome, because it will auto-complete on just that part of the field after the last comma - just what you need for a keyword-list.


Do read all the way to the bottom of the page, as his later updates can change the way it functions a bit.


I did make one addition myself. We have one form that lets you upload multiple items at once, with a "add a new form" link. It just repeatedly inserts a partial-template onto the end of the existing list, each time it's clicked... but because it's called *after* the "onLoad" event, the auto-complete wasn't getting added to it.


To work with that, we needed to add the installAutocompletion(); call to our addNewForm function, and also make sure that each "tags_list" field had a unique id.


I also needed to slightly update the controller code so that it could *either* take params[:my_widget][:tag_list] *or* take params[:my_widgets].first[:tag_list], because the multiple-forms-on-the-page form always sends through an array of widgets instead of just the one.


Then I realised that installAutocompletion would just keep adding autocompleters every time it was called... even to fields that already had one. So I updated the code by adding the following around everything after:


/* only add it if it doesn't already exist */
   if ($(completions.id) == null) {
       element.parentNode.insertBefore(completions, element.nextSibling);
       ... everything else after the above line too
   }

After that it was all gravy.

Wednesday, 7 December 2011

Acts-as-taggable-on

Tagging is pretty popular these days, and it was time to add it to our site. Unfortunately, lots of the gems are old, and it's had to know if that means "good and has stuck around" or "buggy, obsolete and no longer supported".

The rubytoolbox page on rails tagging has several gems - most of which are marked as inactive now. The only one that looked like it had any recent activity is: acts-as-taggable-on. It's also the top-most-downloaded, so that looked good.

Best news: it is rails-3 compatible AND supports a recent build-version that is still Rails-2 compatible. As I've mentioned before, my current client is still on Rails-2 - because the upgrade pain is not currently outweighed by the new features.

The only annoyance is that the rdoc only has rails-3 post-install instructions rails generate acts_as_taggable_on:migration. These don't work for rails-2, and if you try just substituting "script/" for "rails ", it'll give you an error saying: Couldn't find 'acts_as_taggable_on:migration' generator

I had to hack about a bit to find the new migration name, but what you need is: script/generate acts_as_taggable_on_migration

After that I used this extremely good tutorial on tagging with acts-as-taggable-on.

I don't like the tag-cloud style of tag-selection, and instead prefer something much more like Stack Overflow. So I created my own tag-list as per the code below.

It lists all current tags for the class (assuming similar code setup to the tutorial above), and filter based on that keyword - incorporating any existing search or pagination conditions you already have. It will highlight the current keyword, and change that link to a "deselect if you click" link.


    # code in index page
    <% @tags.sort_by(&:count).reverse.each do |k| %>
      <% url_opts = {:action => "index", :controller => "posts"}
         link_name = "#{k.name} (#{k.count})"
      %>
      <% if @keyword == k.name %>
        <%= link_to link_name, url_opts.merge(:keyword => nil), :class => "tag current_tag", :title => "Click again to see all" %>
      <% else %>
        <%= link_to link_name,  url_opts.merge(:keyword => k.name), :class => "tag", :title => "Click to filter by #{k.name}" %>
      <% end %>
    <% end %>


   # code in controller
   options = {} # any search/pagination conditions go here
   @tags = Post.tag_counts_on(:keywords)
   klass = Post
   klass = klass.tagged_with(@keyword) if (@keyword = params[:keyword]).present?
   @posts = klass.paginate( options )




  /**** and associated tag-cloud styles ****/
  /* basic tag-box */
  .tag {
    background-color: #eee;
    border: 2px solid #ccc;
    color: orange;
    border-radius: 7px;
    -moz-border-radius: 7px;
    padding: 2px 15px;
    text-decoration: none;
  }
  .current_tag {
    background-color: #ddd;
    color: orange;
    border: 2px solid orange;
    border-radius: 7px;
    -moz-border-radius: 7px;
    font-weight: bold;
  }
  .tag:hover, .current_tag:hover {
    background-color: #bbb;
    color: red;
    border: 2px solid red;
    border-radius: 7px;
    -moz-border-radius: 7px;
  }

Next up is to figure out ye olde ajax auto-suggest when I add them.

Thursday, 30 December 2010

Simple private messaging

Ran across this plugin the other day. It gives you very simple, private messaging for Users on your app.

Needs some fiddling to make the messaging private - otherwise anybody can read anybody else's message... but otherwise, pretty neat. It basically does most of the heavy-lifting for you, and lets you get on with the customisation... which is the point of using other people's plugins.

Thursday, 9 April 2009

You shoulda test your plugin!

So, lets assume you have a plugin "acts_as_teapot" that you want to include in some of your model classes. You've written a number of useful methods for teapot-like models to use, and now want to check that the models that are implementing acts_as_teapot actually are able to make use of the full spectrum of teapotly functionality.

You could write a set of asserts that you can tell your plugin's users to "please include these asserts/tests in your model classes"... but it's not DRY, and the users might miss one, and they'll get out of date real quick... what you want is kinda one big assert they can just put in once and that calls some library on the plugin itself (so it keeps up to date with the latest plugin code).

Luckily, shoulda is here to save the day! You can create a big context full of all the right tests and save it in the plugin file itself. This will be drawn in by the model at the time it's included. Then the user just has to call a single "shoulda" and it's all done for you.

Now, I was against shoulda for a long time - mainly wondering why anybody would use a half-arsed version of rSpec if they didn't actually want rSpec... but for me, plugin-testing is the killer-app that forced me to re-evaluate shoulda, and so far it actually looks ok. :)

So, to the code...

Plugin code

module Acts
  module Teapot
    def describe_me
      "short and stout"
    end
    def tip_me_over
      "pour me out"
    end
  end
end
class Test::Unit::TestCase
  def self.should_act_as_a_teapot
    klass = model_class

    context "A #{klass.name}" do
      setup { @new_klass = klass.new }

      should "respond to teapotly functions" do
        [:tip_me_over, :describe_me].each do |f|
          assert @new_klass.respond_to?(f), "#{klass.name} should respond to the function: #{f}"
        end
      end
      should "be short and stout" do
        assert_equal "short and stout", @new_klass.describe_me, "#{klass.name} is a funny-looking teapot."
      end
      should "pour me out" do
        assert_equal "pour me out", @new_klass.tip_me_over, "#{klass.name} doesn't make very good tea!."
      end
    end
  end
end

and testing the model:

class MyTeapot < ActiveRecord::Base
  acts_as_a_teapot
  #...
end

class MyTeapotTest < ActiveSupport::TestCase
  fixtures :my_teapots

  # plugin contexts
  should_act_as_a_teapot

end

There's a real-world example in the paperclip shoulda test

Thursday, 2 April 2009

ActiveRecord::Validations in ActiveResource

The holy grail for ActiveResource users is for ARes to actually behave like ActiveRecord. In theory, ARes is just like AR, but in practice it's only kinda, sorta like AR... but missing a few bits that really seem to make all the difference.

ARes is still missing fundamental functionality that we have all grown to know and love... It all looks alright on the surface, but you can't help but notice the giant glowing absence the moment you decide to hide your models away in a Web service and then try to use ARes to implement a Railsy front-end.

Needed functionality includes:

  • Associations (ie has_many/belongs_to)
  • the usual suspects of callbacks (eg before_save)
  • safe-making your attributes (eg attr_accessible)
  • Widget.count
  • Actual conditions in finders (eg :conditions => {:name => 'Joe Bloggs'}
  • and the all-important Validations

I need my validations. The funky way rails handles AR errors is one of the things that makes Rails so special. I love to be able to just type validates_presence_of :foo and for everything else to Just Work.

ARes doesn't bother with them at all - and in that case I hardly see why Rails can call it AR-like when these are missing. Oh yes, sure, you can overload the validate method on your model object, but that seems very crude! Like having to hand-write your database connection code for each model. :P

All I can say is why can't I have validates_presence_of independently to the database connection? It's not really necessary - so why are Validations still locked inside the db-wrapper?

In my opinion, ActiveResource needs a lot of upgrading. Unfortunately, that looks like a fair bit of work... and we don't know how long that will take, and whether it will just be superceded once Merb merges with Rails.

Luckily we have an interim solution, in the form of a plugin called HyperactiveResource. It is fairly crude, but it seems to roughly give us an ActiveRecord-like interface that works fairly well.

Before I hit the code, the plugin already came with a lot of the currently-missing functions - rebuilt with ARes-style processing. They also had a rough implementation of Associations (not entirely AR-like, but getting there). I've been working on adding validations and validation-callbacks.

I can't say it works perfectly as I've just started working on it, as of this morning; but I've already got the basic validations working without falling over completely and I'm using the models auto-generated by restful-authentication to test it.

It's a start...

Thursday, 4 September 2008

Snippet: monkey-patching a gem

Need to make a quick monkey patch to a gem you're using? Follow the steps below:

  1. If you don't have a vendor gems directory: svn mkdir vendor/gems
  2. cd vendor/gems
  3. Unpack the gem into your vendor/gems directory with
    unpack gem <the_gem_name>
  4. Checkin the above changes so you have a clean version of the gem to start with
  5. Make your monkey patches and check them in
  6. Make sure vendor/gems is in the loadpath for gems

And you're done

Friday, 25 May 2007

Scope out your models

Just found a nitfy plugin called AutoScope. It lets you define scopes for a model. For example:

class User < ActiveRecord::Base
   auto_scope \
       :fresh => {:find => {:conditions => ['activated_at IS NULL AND destroyed_at IS NULL']},
       :active => {:find => {:conditions => ['activated_at IS NOT NULL AND destroyed_at IS NULL']}},
       :archived => {:find => {:conditions => ['destroyed_at IS NOT NULL']}}
end

These set up some scoped methods

 new_count = User.fresh.count
 old_users = User.archived.find(:all)
 User.fresh.find(:all).each {|u| u.update_attribute(:activated_at, Time.now.to_utc)} 

I've found these great for filtering collection views that group users distinctly, but span across multiple columns.


    def filter_users
      return default_search unless params[:user_scope]
      scope = params[:user_scope].to_sym 
      case scope
      when :fresh, :active, :archived
        # scopes we recognise
        users = User.send(scope).find(:all)
      else
        users = User.find(:all)
      end
      users
    end

Do note, though, to be careful what to name your scopes. I started out by calling my "fresh" scope "new". Unfortunately this overwrote the "new" function on my User model... not a Good Thing.

Tuesday, 3 April 2007

Plugins and engines and gems, oh my!

So I was talking to friend of mine about what I was doing and how a gem was too heavy for what I wanted. He pointed out that I should consider doing it as an engine instead of a plugin.

What's the difference between a plugin and an engine you ask?

I didn't know either... engines seem to be the forgotten children of Rails in all the current plugin-love that's going around (not knocking it - I like plugins too).

Anyway, it turns out that an engine is a plugin - with some differences. An engine requires that you install the "engines" plugin first - you can't just script/install the plugin itself. However, adding that engines plugin allows your own baby to do a hell of a lot more than your standard plugin-ly functions.

An engine is a complete, vertical slice of MVC. An ordinary plugin, by comparison, is generally a small chunk of functionality such as a helper library. Engines can have an /app/ directory structure identical to a full rails site. You can also add routes and migrations (with some user-tweaking required). This allows a fully encapsulated chunk of functionality (such as what I was proposing for my little blog).

Now, mephisto and typo are both gems. This is because they contain a huge amount of functionality (they well deserve their leading status in the Rails-blog field). They contain more functionality than what most people associate with a simple plugin... more than what people would associate with an engine (if people used engines more commonly) - so they are distributed as gems.

So, to sum up:

  • plugins are for small bundles of helpful code
  • engines are for encapsulated slices of vertical functionality and
  • gems are for complex, multi-functional systems.

So how do I make an engine?

The information on how to make an engine is a bit scattered about the web. There doesn't seem to be a whole lot out there in the way of tutorials (at least not compared to plugins).

Some of the info seems to be downright incorrect. The article that looked most promising was: the alterthoughts one, but it seems to be based on an old version of engine-construction that Just Doesn't Work. I couldn't get script/generate engine my_blog to give me anything but "can't find a generator for engine". This article is clearly built on an earlier version of both Rails and engines

The railsdoc on engines is fairly complete (though it's not a step-by-step tutorial). It turns out that you can just start out with a rails app - and turn it into an engine. Two things to remember:

Routes
Copy your engine's routes.rb into the root directory. Then instruct your users to add map.from_plugin :your_plugin into their routes.rb.
Migrations
If you have migrations, make sure they are numbered from 001. Then leave instructions for your user to run script/generate plugin_migration, then rake db:migrate.
Shared plugins
A blog needs to share the authentication plugin. But which authentication mechanism are you using? I could assume you're using the same as me (RESTful auth right?) but that's not likely. The best option is to write a wrapper library that allows people to override the functions with their own authentication system, if they choose.
Other plugins
If your plugin depends on other plugins there may be some duplication. I'm sure there must be a way that minimises double-loading of plugins eg if your plugin uses acts_as_taggable, and so does their site - maybe your install.rb could check for that and not bother installing it - but I think that may be over-optimisation.

I'm sure there must be a way to put all these tasks into "install.rb" - but I haven't had a chance to do that yet.

So, what have you done?

So I wrote a very tiny blog app (currently all is does are posts). It was enough to demonstrate the point for me while I figure out how to do the engine thing. I'm still playing with it in my spare time - but I probably won't get in installed until after the Easter long-weekend. At that point I'll also find somewhere to upload it so people can have a play with it.

Friday, 30 March 2007

A blog to call my own

So, I've been thinking about shifting my blog over to my homepage recently. Why? well blogger's great, but I really need to move out and find a blog of my own. I want a tighter control over the content, I want to integrate it with my website's styling and layout and maintaining it in two places violates the DRY principle...

Ok, so I'm lazy and don't want to have to maintain stylesheets/layouts in two places :P

So anyway, in Rails, the major blog movers-and-shakers atm are Mephisto and Typo. They're both great. They provide heaps of functionality and have a thriving developer community. But they're a little on the top-heavy side. All the bells an whistles, Mephisto is almost a full CMS... you have to pretty much install either one as a new app (or hack it a lot ot fit with your existing one).

This is great if:

  1. you're starting an entirely new website from scratch
  2. you like using a CMS-approach to organise your site for you
  3. you're ok with your blog being a completely independant rails app.

In my case:

  1. I already have a pre-existing website. It might not be much, but I like what I've done and I don't want to have to re-hack it to fit around my blog software. Conversely, I don't want to spend hours hacking my blog software to fit in with my website - and risk destabilising the upgrade path (which kinda defeats the purpose).
  2. I'm a rails freelancer - not a stay-at-home mum. I like to get my hands dirty in the innards of my site. I don't like having the structure of said site dictated to me by some upstart CMS (no matter how shiny). I like to call the shots on my own turf
  3. We're seriously violating the DRY principle here. Why on earth would I want to maintain two separate sets of layout/styling? or two separate authentication systems? Single sign-on is a PITA. Yes, there are solutions, but why invite the problem into your home? and you couldn't integrate the layouts (eg put your latest post on the homepage) without contortions. No RESTful articles_path for you!

What I really want is to be able to do something like
script/plugin install blog
and have it all Just Work(tm). That doesn't seem available anywhere.

So, I've decided to write my own.

It won't be anything flash, just something bog-simple to allow me to post articles, tag them, let people comment and generate an RSS feed. It won't have whizz-bang flickr integration (well, probably not), but it's all that most people seem to need.

I'll post updates here as I progress.

Thursday, 4 January 2007

File uploads

I wasted time today.

I didn't intend to - I was hopng to change an admin CRUD interface over to use AJAX scaffold - which is prettier than what I had before. But when it was all changed over suddenly all my file-uploading stopped working.

I checked to make sure all the file-saving code was still in place, the filenames were correct, everything but it kept simply not finding the StringIO that was clearly there in the form.

It took me way too long to discover that you Can't upload files with AJAX. It seems the JS doesn't accept the multi-part get/post data. :(

So I changed back...

but in the meantime (while I was checking all my code) I discovered the file_column plugin which is a neat little library that does all of your file-uploading for you. It even integrates with RMagick to let you do some speccy resizing etc. So it wasn't *totally* wasted effort... just not what I'd planned spending the morning working on :P