Making Models Searchable - Redmine Plugin Extension and Development (2014)

Redmine Plugin Extension and Development (2014)

Chapter 5. Making Models Searchable

Our knowledgebase plugin is meant to facilitate the creation and management of large quantities of categorized information. Once this system grows to a certain size, it will no longer be feasible to simply navigate directly to content when a user is trying to find something generic.

Redmine provides an extremely versatile search system for all of its own internal models, which can easily be extended to plugin models through the application of a couple of built-in plugins.

This chapter will introduce the Redmine search subsystem and how our models can quickly hook into it.

We will cover the following topics in this chapter:

· Initializing our plugin to be included in Redmine searches

· Setting up the required result formatting through acts_as_event

· Getting our model ready to actually be searched using acts_as_searchable

· How Redmine permissions limit the availability of search functionality

· How custom permissions can be used to override search results

Registering our plugin

The first step to setting up our plugin to be incorporated into Redmine's internal search is to register our model.

This is done in our plugin's init.rb file anywhere outside the Redmine::Plugin.register block, using the following code:

Redmine::Search.available_search_types << 'kb_articles'

Redmine is now aware of our plugin's model and will be looking for it whenever a project or global search is requested.

Preparing our models to be searched

The acts_as_event plugin is used internally by Redmine in order to maintain consistency between various models that need to be grouped together.

In our case, the models that are being searched need to have acts_as_event implemented in order to determine what constitutes a title, how the title will be formatted, what the description field is, and so on.

Note that acts_as_event is a dependency of acts_as_searchable; therefore, if it isn't included in our model, Redmine will crash when a search is attempted.

The function prototype for acts_as_event is a standard class method that accepts an options hash:

def acts_as_event(options = {})

As we'll be marking our knowledgebase articles as searchable, we will begin by adding acts_as_event to our article model:

class KbArticle < ActiveRecord::Base

# ...

acts_as_event :datetime => :updated_at,

:description => :summary,

:title => Proc.new { |o| "#{l(:label_title_articles)} ##{o.id} - #{o.title}" },

:url => Proc.new { |o| { :controller => 'articles',

:action => 'show',

:id => o.id,

:project_id => o.project }

}

# ...

end

The acts_as_event options hash can be initialized with the following keys:

· :datetime: The field within our model that will be used as a timestamp. The default is :created_on. Its value can be either Proc or Symbol.

· :title: The model field that will be used when rendering an event title. The default value is :title. Its value can be either Proc or Symbol.

· :description: The model fields that will be used when displaying additional information about an entry. The default value is :description. Its value can be either Proc or Symbol.

· :author: The model field that will be used when author information about an entry is displayed. The default value is :author. Its value can be either Proc or Symbol.

· :url: The URL to the record an event refers to. The default value is { :controller => 'welcome' }. This value can be Proc, a Symbol, Hash, or String.

· :type: Used by other plugins to filter certain types of events (for example, see the acts_as_activity_provider class method to find events in the Redmine source). The default value is the result of self.name.underscore.dasherize (for example, kb-article).

· :group: Used by the ActivitiesHelper when sorting activity events. The default group value is self (the model acts_as_event is implemented), but can be overridden by providing a value to this option. See /path/to/redmine/app/helpers/activities_helper.rb for more information.

In order to implement acts_as_searchable, the bare minimum options required in order to ensure that searching will function are :datetime, :description, :title, and :url.

Configuring search options

Once acts_as_event has been implemented, we can finish preparing our model by implementing acts_as_searchable.

As with all previous internal plugins, the class extension needs to be included in the model we will be making searchable.

class KbArticle < ActiveRecord::Base

validates :title, :presence => true

validates :category_id, :presence => true

# ..

acts_as_searchable :columns => [ "#{table_name}.title", "#{table_name}.content"],

:include => [ :project ],

:order_column => "#{table_name}.id",

:date_column => "#{table_name}.created_at"

# ..

end

This example allows our article model to be incorporated into Redmine's default search infrastructure, with article titles and the article contents being used as searchable targets.

The acts_as_searchable plugin takes a number of optional values in the form of a hash. The most common values are as follows:

· :columns: This specifies the column or array of columns to search.

· :project_key: This is the project foreign key. The default value assumes that the model we're attaching acts_as_attachable to has a project_id field defined in the schema.

· :date_column: This is name of the datetime column. The default value is created_on.

· :sort_order: This is used to sort a column using it's column name. The default value is either the previously defined :date_column or implied created_on column.

· :permission: This is the permission required to search the model. This optional property is used to define a custom permission value that a user must have in order to be able to search our model. The default value is in the format:view_"model", so in our case, it would be :view_kb_articles. If a permission is defined and the current user doesn't have that permission applied explicitly, the model we're making searchable won't show up as a filter option in the Redmine search.

Configuring search options

If the user didn't have the appropriate permissions, there would not be an option for Knowledgebase articles.

Filtering search results using custom permissions

In Chapter 3, Permissions and Security, we introduced a custom permission model that allowed us to whitelist users against certain knowledgebase categories.

As this is functionality that we added to the system, Redmine doesn't understand how this content needs to be filtered.

To illustrate the process, first we'll ensure that one of our categories has an explicit whitelist defined.

Filtering search results using custom permissions

This category currently contains a number of articles that contain references to viruses, which is what we'll be using as a search term.

If we were to execute this search as the current user, the results would contain all articles that contain a reference to the word "virus" either in the title, or in the content of the article. This is shown in the following screenshot:

Filtering search results using custom permissions

If the user wasn't on the whitelist, trying to select the article would result in an error, which is the desired behavior in this situation. This situation violates our security policy though, as it exposes some information about the article even without allowing the user to access it. A much better solution would be for the results of the search to not even include content we've explicitly revoked access to.

Since the acts_as_searchable extension adds a search method, we'll override this method in order to ensure our custom permissions are applied to the results.

# override the acts_as_searchable search method in order to filter the results

# by category permissions

def self.search(tokens, projects=nil, options={})

# the results are presented as an array with two entries:

# [0] => an array of the models returned in the search result

# [1] => the count of the results

result = super(tokens, projects, options)

# first we want to check if any of the results shouldn't be

# visible to the current user

result[0].delete_if { |article| article.category.blacklisted?(User.current) }

# update the total count just in case the results were further filtered

result[1] = result[0].length

result

end

Although this accomplishes the desired result of reducing the result set to only content our users should see, it does so after the search has already been executed.

Including article content in the search

In the search we previously executed, there were two issue items returned as well as one knowledgebase article. When the search results were rendered, the issue items contained description text with highlighted matches, but our article didn't.

When we first set up acts_as_event, the :description field was mapped to the article's summary field. As this field is optional in our plugin and may not always be populated, we want to change this to something that will be present in all articles we'll be searching.

In order to do this, the implementation of acts_as_event needs to be updated with a new description mapping of :description to :content.

class KbArticle < ActiveRecord::Base

# ...

acts_as_event

:datetime => :updated_at,

:description => :content,

:title => Proc.new { |o| "#{l(:label_title_articles)} ##{o.id}- #{o.title}" },

:url => Proc.new { |o| { :controller => 'articles',

:action => 'show',

:id => o.id,

:project_id => o.project } }

# ...

end

Once this has been done, if we run the search for "virus" again, the results will contain the article contents as well, as shown in the following screenshot:

Including article content in the search

Summary

No matter how big or how small our knowledgebase is, having the ability to search for content makes the system much more useful to our end users.

In this chapter, we learned how the Redmine search system can be extended to include our custom models in search results as well as how to format the results to be consistent with other Redmine searchable items.

In the next chapter, we'll be adding our articles and categories to Redmine's activity stream.