Finally, a Real Pastebin Plugin for Redmine

It was rather puzzling to me why Redmine doesn't come with a Pastebin module and why there's no plugin for that.

One of the proposed "solutions" is to start a new wiki page, and put some <code> markup there. While this seems to work, it has some limitations a real pastebin component wouldn't suffer from. First thing that comes to mind is that you need a unique name to start a new wiki page. Second, the wiki code markup is currently (as of redmine-1.1.0) done using spans, which makes it cumbersome to select and copy the code from the page: the line numbers will mess up your pasted code.

Now, for public projects you can always use the pastebin (or any of the similar sites,) but that's not an option for private projects. So I've decided to spend a few hours and create a full-blown pastebin for redmine: https://github.com/commandprompt/redmine_pastebin

Use The Force, Luke

So, I've opened up the official Plugin Tutorial and started to work from there. The tutorial itself is great and I had a working prototype in a really short time, however there are some interesting details which it doesn't cover.

Routes

While following the tutorial, you may notice that URIs for the resources we're adding ("pastes" in our case) are different from what you see throughout standard Redmine modules. Your URIs will look like e.g. /pastes/?project_id=1, while in Redmine they look like /projects/myproject/pastes. How do we make our URIs look like the standard ones?

Well, turns out it's enough to open up the config/routes.rb of your plugin and add the following line:

ActionController::Routing::Routes.draw do |map|
  map.resources :pastes
  map.resources :pastes, :path_prefix => '/projects/:project_id' # <= this one
end
And that's it! If you followed the rails way, all your new_paste_url and friends will now return the nicely-looking URLs (as long as you pass the project_id param.)

Monkey-Patching the Referenced Models

OK, now since pastes (much like the issues) are per-project entities, we've specified the project_id foreign key when we've created the pastes table in our migration. So any given paste belongs to some project. How do we specify that in our plugin?

Easily :) Just open app/models/pastes.rb and add belongs_to :project at the class level. But that's only half of the problem. The project, on the other hand, has many pastes. How do we add that?

The language-supported solution in Ruby to add functionality to existing classes is called 'monkey-patching.' To employ this technique for our needs, we'll create a new file in our plugin's directory called lib/redmine_pastebin/project_pastes_patch.rb and put the below code there:

module RedminePastebin
  module ProjectPastesPatch
    def self.included(base)
      base.class_eval do
        has_many :pastes
      end
    end
  end
end
To actually 'apply the patch' we should open our init.rb file and throw in some code like this:
require 'dispatcher'

Dispatcher.to_prepare :redmine_model_dependencies do
  require_dependency 'project'

  unless Project.included_modules.include? RedminePastebin::ProjectPastesPatch
    Project.send(:include, RedminePastebin::ProjectPastesPatch)
  end
end
And we're all set. There's no need to 'require' the file containing the patch module, since if you've followed the convention, the Rails' automatic dependency loader will try to load lib/redmine_pastebin/projects_pastes_patch.rb first time it hits RedminePastebin::ProjectPastesPatch in the code.

Since every paste belongs to an author (a user,) exactly the same technique was used to monkey-patch the user model.

Making Forms Look Natural

One thing you'll notice when creating your plugin for Redmine, is that your input forms are looking different from those used throughout Redmine itself. This is because in Redmine they're using a custom FormBuilder. You can also use it easily, here's for example, what 'New Paste' form looks like:

<% labelled_tabular_form_for :paste, @paste, :url => { :action => "create",
     :project_id => @paste.project.id } do |f| %>
<div class="box">
  <p><%= f.text_field :title %></p>
  <p><%= f.select :lang, pastebin_language_choices %></p>
  <p><%= f.text_area :text, :rows => 25, :cols => 80 %></p>
</div>
<%= f.submit "Paste!" %>
<% end %>

Showing Pastes in Activity Report

A nice feature of Redmine is Activity timeline report, that can show you which changes were made on the project during specified period of time. This includes creating and updating issues, committing to code repository, editing wiki pages, etc. Let's add pastes to the mix.

Open up your init.rb and put a code block like this at the bottom:

Redmine::Activity.map do |activity|
  activity.register :pastes
end
Now, open app/models/paste.rb and throw in some code like this:
  acts_as_event :title => Proc.new{ |o| o.title },
    :url => Proc.new{ |o| { :controller => 'pastes', :action => 'show',
      :id => o.id } }

  acts_as_activity_provider :find_options => {:include => [:project, :author]},
    :author_key => :author_id
And that should be it. Now newly created pastes will be showing in the project activity report along any other changes.

More Fun with Menus

Initially I was going to add only a single project-level menu item and call it 'Pastebin,' where users could view the existing pastes and provide a link called 'New Paste' at the top of the list. However, this didn't play well with some roles permissions I don't quite recall now (it has something to do with some role being able to add new pastes, but not list the existing ones.)

So I decided to replicate the issues approach, where 'Issues' and 'New Issue' are separate project-level menu items. Naturally I've added another menu :project_menu, ... stanza to my init.rb, and it seemed to work. Except for one minor thing: then you visit 'New Paste' link, the project menu highlights 'Pastes' instead.

After debugging that for quite some time, I've found out that it is required to add some code like this to the pastes_controller.rb to make it work:

  menu_item :new_paste, :only => [:new, :create]
This way the menu item is highlighted as it should be (note the :create action, since on save errors create renders new.) This is not nearly obvious, so I hope this might save someone's time someday. :)

The Ugly Guts

So far so good, we've been able to get what we need using pretty much approved techniques. Time for some dirty hacks. ;)

One thing I've mentioned in the beginning which makes wiki-based "solution" of the pastebin problem unusable is that it doesn't produce such html markup from which you could copy/paste to your code easily. If you look at how syntax highlighting is handled in Redmine you'll notice the nice module called Redmine::SyntaxHighlighting. It is supposed to work as a facade to different pluggable syntax highlighting engines, and CodeRay is the default one.

Now CodeRay itself supports the layout we need for pastebin, but you can't just get to it since the Redmine's module gets in your way. It's a good place to improve Redmine itself, but for now I've just went the easy route and started using CodeRay directly, since it's bundled with Redmine anyway. This ugly hack lives in the pastes_helper.rb:

  def highlighted_content_for_paste(paste)
    content_tag :div, :class => "syntaxhl box" do
      ::CodeRay.scan(paste.text, paste.lang).html(:line_numbers => :table)
    end
  end

At this point everything seemed to be working, until someone tried the 'diff' highlighting which appeared rather messy. Adding some simple CSS code to the show action template solved the problem:

div.paste .syntaxhl div {
  display: block;
}

table.CodeRay td.line_numbers {
  text-align: right;
}
The second CSS rule makes line numbers align nicely by the lowest-order digit. This is a hack again as we need to hard-code 'CodeRay' in the selector...

Conclusion

That's it for this time. As one can see, making a new Redmine plugin is fairly easy and you can create a working prototype from scratch in no time. When it comes to more obscure aspects of integration, the invaluable source of learning is looking at what others have made already and, ultimately reading Redmine's source code and/or debugging it to "Make It Darn Work!" :)