Attaching Files to Models - Redmine Plugin Extension and Development (2014)

Redmine Plugin Extension and Development (2014)

Chapter 4. Attaching Files to Models

One very common extension we end up having to implement in our models, be it in Redmine or in another project, is the ability to attach files. As we're working on a knowledgebase plugin, our articles could be made more informative by allowing external files to be attached as reference items.

If we were writing our own Ruby on Rails application, the process of adding file attachment capabilities is generally delegated to an external library or gem such as paperclip (https://github.com/thoughtbot/paperclip) or carrierwave (https://github.com/carrierwaveuploader/carrierwave).

Redmine has conveniently abstracted this all away for us, which makes adding file upload and attachment features almost no work at all.

We will cover the following topics in this chapter:

· How our models are updated with the internal acts_as_attachable plugin

· How to implement the existing file attachment view partial and what options it takes

· Using the link_to_attachments view helper

· How to manage attachment permissions and how to further restrict the deletion of attachments

Model preparation

Redmine has implemented a number of internal plugins, which are located under /path/to/redmine/lib/plugins.

These plugins follow the traditional Rails naming idiom of acts_as_* (for more information on this topic, visit http://guides.rubyonrails.org/plugins.html#add-an-acts-as-method-to-active-record), which implies that we'll be including a class level method, which is named the same as the plugin.

The class we'll be extending is the model that our knowledgebase plugin uses to manage articles.

class KbArticle < ActiveRecord::Base

validates :title, :presence => true

validates :category_id, :presence => true

belongs_to :project

belongs_to :category, :class_name => "KbCategory"

belongs_to :author, :class_name => 'User',:foreign_key => 'author_id'

# class method from Redmine::Acts::Attachable::ClassMethods

acts_as_attachable

# class definition continues ...

End

By including acts_as_attachable in our class, a has-many association is established and a number of instance methods are automatically injected into the class. These methods are used by the view helpers in order to validate and save attachments.

A couple of handy helper functions provided by acts_as_attachable are:

· attachments_visible?(user=User.current)

· attachments_deletable?(user=User.current)

Both methods check whether a Redmine user has the permission to perform a certain action on an attachment that has been added to an associated model.

These permissions are covered in detail in the Managing attachment permissions section later in this chapter.

The default permissions are formed by joining a prefix with a pluralized and underscored representation of the model's name that acts_as_attachable is being added to. For example, our article model would have :view_kb_article and :edit_kb_article by default.

Note that our model must respond to self.project either by having a project association (belongs_to) or by means of an instance method. This is due to the attachments_visible? and attachments_deleteable? methods using the User#allowed_to? method to validate access and interaction with a model's attachments.

Enabling attachments in our views

Adding multiple file attachment capabilities to our article creation and update form is also simple and straightforward. To make our implementation even easier, Redmine already has a styled and structured sample available in the issue management code that we can reuse.

The following code snippet needs to be copied and pasted into the form (or form partial) that we're using to create and edit articles. Assuming we're using the standard Rails method of building forms using FormBuilder (visithttp://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html), the following code would need to be inserted within a view's form_for block:

<div class="box">

<p>

<label><%=l(:label_attachment_plural)%></label>

<%= render :partial => 'attachments/form' %>

</p>

</div>

This incorporates the stock Redmine view partial that allows files to be uploaded asynchronously and attached to our model.

The preceding partial adds an additional form field array called attachments, which takes attachments via file_field_tag. Multiple files can be attached at once as the partial we're using dynamically adds each file to the form (see/path/to/redmine/app/views/attachments/_form.html.erb for the full implementation).

Enabling attachments in our views

The attached files can also be deleted and have optional descriptions provided.

Note

Note that the Maximum size value is based on the value provided at Administration | Settings under the General tab in the Maximum attachment size field.

Controller modifications to accommodate attachments

Since we're using the standard Redmine view partials to manage our attachments, when a new article is created or updated, attachments are submitted to our controller in an attachments collection, which is available from params[:attachments]:

"attachments"=>

{"1"=>

{"filename"=>"8748OS_04_01.png",

"description"=>"",

"token"=>"4.f5bd5eabd62c9ec71b427d8195f18285"},

"2"=>

{"filename"=>"8748OS_04_02.png",

"description"=>"",

"token"=>"5.f8bd02cfcb83aba081bf69ac06fdb085"},

"3"=>

{"filename"=>"8748OS_04_03.png",

"description"=>"",

"token"=>"6.80b7dd688925171341b4004fc9ddcf69"}}

The actual file uploads are handled asynchronously by Redmine before we even submit the form, but we still have to associate these files with our model.

One of the instance methods that acts_as_attachable provides our models with is save_attachments. This can be used in our controller to complete the upload process by associating the uploaded files with our model.

The following example is a modified update method for our ArticlesController that incorporates this new functionality:

def update

@article = KbArticle.find(params[:id])

# ...

if @article.update_attributes(params[:article])

@article.save_attachments(params[:attachments])

render_attachment_warning_if_needed(@article)

# ...

redirect_to { :action => 'show', :id => @article.id,:project_id => @project }

end

end

We have included a call to render_attachment_warning_if_needed(obj) in the previous example as a convenience. This method is not part of acts_as_attachable but can be added to any plugin's controller as it is a method of Redmine's ApplicationController. It adds a warning to the Rails Flash (visit http://api.rubyonrails.org/classes/ActionDispatch/Flash.html) if any attachments remain unsaved.

Listing and managing attachments

Redmine provides a view partial for listing the existing attachments on a model as well. In order to quickly implement this functionality, the /path/to/redmine/app/views/attachments/_links.html.erb partial can be plugged into any view and passed a collection of attachments as follows:

<%= render :partial => 'attachments/links',

:locals => { :attachments => @article.attachments,

:options => { :deletable => User.current.logged? }

} %>

This will change the view as shown in the following screenshot:

Listing and managing attachments

In addition to the attachments collection that is required, the links partial also accepts an options hash. This hash only accepts the following keys:

· :author: If the value evaluates to true, the attachment author is listed along with the timestamp of when the attachment was created

· :deletable: If the value evaluates to true, a delete link will be rendered, which allows the current user the ability to permanently remove the attachment

· :thumbnails: When this option is provided and evaluates to true, attachment thumbnails will be displayed if enabled by the system administrator (Administration | Settings | Display | Display attachment thumbnails)

The :deleteable option is an all-or-nothing option for the entire attachment collection that is passed to the partial view. As such, if we're looking to implement a more granular security setup, we could render the attachments/links partial more than once with filtered collections.

<% @article.attachments.group_by { |f| File.extname(f.filename).include?("exe") }.each do |group| %>

<%= render :partial => 'attachments/links',

:locals => { :attachments => group[1],

:options => { :deletable => User.current.logged? && group[0] }

} %>

<% end %>

This example will group the attachments collection based on whether or not the filename includes the .exe extension, and if it does, will allow deletion.

Listing and managing attachments

As the grouping approach leaves a visible separation between groups, another option would be to clone the partial provided in the Redmine source and extend it according to our specific criteria.

For a quicker implementation of the attachments/links partial, Redmine provides a method in AttachmentsHelper:

link_to_attachments(container, options = {})

This view helper method checks the container (model instance) for any attachments, and renders the attachments/links partial if any exist.

The helper method here takes a container that has attachments associated with it. In this case, we would pass our @article instance, not the attachments collections directly.

The implementation we just described using the attachment/links partial directly could now be shortened to something along the lines of this:

<%= link_to_attachments @article,

:thumbnails => true,

:author => true %>

This example omits the :deletable option but provides a couple of additional options.

When :thumbnails is provided, if an attachment is an image (see the image? method in /path/to/redmine/app/models/attachment.rb), a thumbnail representation of the attachment will be included below the standard attachment links.

The :author option is used to toggle whether the name of the user who originally uploaded the attachment should be listed along with the attachment.

Listing and managing attachments

Managing attachment permissions

Adding attachment functionality to our models through acts_ast_attachable comes with two preconfigured management permissions: a view permission and a delete permission.

In order to properly implement these permissions, they would have to be declared along with our plugin's other named permissions in our init.rb file. You can refer to Chapter 1, Introduction to Redmine Plugins, for a quick refresher on declaring custom permissions.

Both of these permissions are dynamically generated based on the class name of the model we've added attachments to.

The format of both the view and delete permissions by default are:

"view_#{self.name.pluralize.underscore}".to_sym

"delete_#{self.name.pluralize.underscore}".to_sym

As our knowledgebase articles are declared in a KbArticle class, the resulting generated permissions would be :view_kb_articles and :delete_kb_articles.

If we have attachments in an article and try to delete them without properly declaring and assigning these permissions, Redmine's authorization system will reject the request and display the following output:

Started DELETE "/attachments/10" for 127.0.0.1 at 2013-12-17 21:50:12 -0500

Processing by AttachmentsController#destroy as HTML

...

Filter chain halted as :delete_authorize rendered or redirected

Completed 403 Forbidden in 32ms (Views: 19.6ms | ActiveRecord: 2.2ms)

If we prefer to supply our own permissions to acts_as_attachable, this is done in our model by providing our own permission symbols to either :delete_permission or :view_permission.

class KbArticle < ActiveRecord::Base

# ...

acts_as_attachable :delete_permission => :manage_articles

# ...

end

In this example, instead of declaring a :delete_kb_articles permission in our init.rb file, we would instead declare a :manage_articles permission. This permission would subsequently be used by attachments_deleteable?(user) when checking to see whether the current user is allowed to delete an attachment.

Summary

In this chapter, we covered the basics of quickly adding the ability to attach files to our models and how our views and partials could be extended with Redmine's existing file attachment partials.

We also learned about the various options that are available to acts_as_attachable as well as how attachment permissions are managed.

In the next chapter, we'll be making our models searchable.