Commit 3be00e3b authored by Enrique García Cota's avatar Enrique García Cota

modulized redmine_project_filtering. Updated featured_projects module

parent 7bc1a6fc
......@@ -16,3 +16,6 @@
[submodule "vendor/plugins/featured_projects"]
path = vendor/plugins/featured_projects
url = git://github.com/splendeo/featured_projects.git
[submodule "vendor/plugins/redmine_project_filtering"]
path = vendor/plugins/redmine_project_filtering
url = git://github.com/splendeo/redmine_project_filtering.git
Subproject commit 1320e4ff634147ae46bc966a600a59a7c50731f1
Copyright (C) 2011 by Splendeo Innovación
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
== Redmine/Chiliproject Project Filtering Plugin
This plugin adds a "filtering text box" on the projects/index screen.
In addition to text-based search, it's possible to add additional filters (see below).
This plugin can be used in conjunction with the Featured Projects plugin ( https://github.com/splendeo/featured_projects )
If this plugin is detected, projects marked as "featured" will appear first, on a box, followed by the rest of the projects.
== Adding filters
Filters can be added via Custom Fields.
* Log-in as the main administrator
* Go to the custom fields view. You can do so via the Admin/Custom Fields menu, or just typing /custom_fields/ on the url bar.
* Select the "Projects" tab
* Add a new custom field.
* Choose the "list" type, and fill in the name and the list of values.
* Mark the custom field as Searchable
* Save the new field
That's it - a new filter should appear on the projects/index screen
== Removing filters from the view
By default, any fields created as above will be used as filters. If you wish to hide any searchable custom field of type "list" you can deactivate it on the plugin settings screen (Admin -> Plugins -> Configure)
== Installation
1. Copy the plugin directory into the vendor/plugins directory
2. Start Redmine
(This plugin does not require migrations)
Installed plugins are listed and can be configured from 'Admin -> Plugins' screen.
== Credits
Development of this plugin was financed by the Open Hardware Repository - www.ohwr.org
<p class='custom_field custom_field_<%= custom_field.id %>'>
<%= label_tag 'custom_field.name', l(custom_field.name, :default => custom_field.name) %>
<%= select_tag("custom_fields[#{custom_field.id}]",
options_from_collection_for_select(
[nil] + custom_field.possible_values,
'to_s',
'to_s',
@custom_fields[custom_field.id.to_s]
)
)
%>
</p>
<% if @featured_projects && @featured_projects.any? %>
<div class="box featured_projects">
<h3 class="icon icon-featured"><%=l(:project_filtering_featured_projects_label) %></h3>
<%= render_project_hierarchy_with_filtering(@featured_projects, @custom_fields, @question) %>
</div>
<% end %>
<%= render_project_hierarchy_with_filtering(@projects, @custom_fields, @question) %>
<% content_for :header_tags do %>
<%= auto_discovery_link_tag(:atom, {:action => 'index', :format => 'atom', :key => User.current.rss_key}) %>
<%= stylesheet_link_tag "redmine_project_filtering.css", :plugin => "redmine_project_filtering" %>
<% end %>
<div class="contextual">
<%= link_to(l(:label_project_new), {:controller => 'projects', :action => 'new'}, :class => 'icon icon-add') + ' |' if User.current.allowed_to?(:add_project, nil, :global => true) %>
<%= link_to(l(:label_issue_view_all), { :controller => 'issues' }) + ' |' if User.current.allowed_to?(:view_issues, nil, :global => true) %>
<%= link_to(l(:label_overall_spent_time), { :controller => 'time_entries' }) + ' |' if User.current.allowed_to?(:view_time_entries, nil, :global => true) %>
<%= link_to l(:label_overall_activity), { :controller => 'activities', :action => 'index' }%>
</div>
<h2><%=l(:label_project_plural)%></h2>
<% form_tag('/projects', :method => :get, :id => :project_filtering) do %>
<fieldset id="filters" class="collapsible">
<legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
<div>
<p class='q'>
<%= label_tag 'q', l('project_filtering_q_label') %>
<%= text_field_tag 'q', @question, :size => 30, :id => 'search-input' %>
</p>
<%= render :partial => 'custom_field', :collection => @custom_fields_used_for_project_filtering %>
<p class='buttons'><%= submit_tag( l(:button_send), :id => 'filter_button') -%></p>
</div>
</fieldset>
<% end %>
<%= javascript_tag "Field.focus('search-input');" %>
<%= javascript_tag "$('filter_button').hide();" %>
<%= observe_form( :project_filtering,
:frequency => 0.5,
:url => { :controller => :projects, :action => :index, :format => :js },
:method => :get
)
%>
<div id="projects">
<%= render :partial => 'filtered_projects' %>
</div>
<% if User.current.logged? %>
<p style="text-align:right;">
<span class="my-project"><%= l(:label_my_projects) %></span>
</p>
<% end %>
<% other_formats_links do |f| %>
<%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
<% end %>
<% html_title(l(:label_project_plural)) -%>
xml.instruct!
xml.projects :type => 'array' do
@projects.each do |project|
xml.project do
xml.id project.id
xml.name project.name
xml.identifier project.identifier
xml.description project.description
xml.parent(:id => project.parent_id, :name => project.parent.name) unless project.parent.nil?
xml.custom_fields do
project.custom_field_values.each do |custom_value|
xml.custom_field custom_value.value, :id => custom_value.custom_field_id, :name => custom_value.custom_field.name
end
end unless project.custom_field_values.empty?
xml.created_on project.created_on
xml.updated_on project.updated_on
end
end
end
<p><%= l(:project_filtering_info) %></p>
<fieldset>
<%= content_tag(:legend, l(:project_filtering_text_filters)) %>
<% @project_custom_fields.each do |field| %>
<p>
<% plugin_settings = @settings['used_fields'] %>
<% dom_id = "settings[used_fields][#{field.id}]" %>
<% checked = plugin_settings.present? && plugin_settings[field.id.to_s].present? %>
<%= check_box_tag dom_id, 1, checked %>
<%= label_tag dom_id, field.name %>
</p>
<% end %>
</fieldset>
#project_filtering label { display: block; }
#project_filtering p { float: left; }
#project_filtering p.q { width: 30em; }
#project_filtering p.custom_field { width: 10em; }
#project_filtering p.buttons { clear: both; with: 100%; float: none; }
ul.filter_fields { padding: 0; }
ul.filter_fields li {
list-style-type: none;
display: inline;
margin: 0 10px 0 0;
}
.icon-featured { background-image: url(../images/ribbon-16x16.png); }
# English strings go here
en:
project_filtering_text_filters: "Active filters"
project_filtering_info: Use this window to deactivate any custom fields that you don't want to use for filtering.
project_filtering_q_label: "Textual search"
project_filtering_featured_projects_label: "Featured Projects"
button_send: "Send"
require 'redmine'
Dispatcher.to_prepare :redmine_project_filtering do
require_dependency 'redmine_project_filtering'
require_dependency 'redmine_project_filtering/with_custom_values'
require_dependency 'projects_helper'
ProjectsHelper.send(:include, RedmineProjectFiltering::Patches::ProjectsHelperPatch)
require_dependency 'custom_field'
CustomField.send(:include, RedmineProjectFiltering::Patches::CustomFieldPatch)
require_dependency 'project'
Project.send(:include, RedmineProjectFiltering::Patches::ProjectPatch)
require_dependency 'projects_controller'
ProjectsController.send(:include, RedmineProjectFiltering::Patches::ProjectsControllerPatch)
require_dependency 'settings_controller'
SettingsController.send(:include, RedmineProjectFiltering::Patches::SettingsControllerPatch)
end
# will not work on development mode
Redmine::Plugin.register :redmine_project_filtering do
name 'Redmine Project filtering plugin'
author 'Enrique García Cota'
url 'http://development.splendeo.es/projects/redm-project-filter'
author_url 'http://www.splendeo.es'
description 'Adds filtering capabilities to the the project/index page'
version '0.9.5'
settings :default => {'used_fields' => {}}, :partial => 'settings/redmine_project_filtering'
end
module RedmineProjectFiltering
# transforms a question and a list of custom fields into something that Project.search can process
def self.calculate_tokens(question, custom_fields=nil)
list = []
list << custom_fields.values if custom_fields.present?
list << question if question.present?
tokens = list.join(' ').scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)})
tokens = tokens.collect{ |m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '') }
# tokens must be at least 2 characters long
tokens.select {|w| w.length > 1 }
end
end
module RedmineProjectFiltering
module Patches
module CustomFieldPatch
def self.included(base) # :nodoc:
base.send(:include, InstanceMethods)
base.class_eval do
unloadable
named_scope( :used_for_project_filtering, lambda do |*args|
used_field_setting = Setting['plugin_redmine_project_filtering']['used_fields'] || {}
used_fields = used_field_setting.keys.collect(&:to_i)
{ :conditions => [ "custom_fields.type = 'ProjectCustomField'
AND custom_fields.field_format = 'list'
AND custom_fields.id IN (?)", used_fields ] }
end)
named_scope( :usable_for_project_filtering, {
:conditions => {
:type => 'ProjectCustomField',
:field_format => 'list'
},
:order => 'custom_fields.position ASC'
})
after_create :configure_use_in_project_filtering
end
end
module InstanceMethods
def configure_use_in_project_filtering
if(self.type == 'ProjectCustomField' and field_format=='list')
plugin_settings = Setting[:plugin_redmine_project_filtering]
plugin_settings[:used_fields] ||= {}
plugin_settings[:used_fields][self.id.to_s] = "1"
Setting[:plugin_redmine_project_filtering] = plugin_settings
end
end
end
end
end
end
module RedmineProjectFiltering
module Patches
module ProjectPatch
def self.included(base)
base.send(:include, RedmineProjectFiltering::WithCustomValues)
base.extend ClassMethods
base.class_eval do
unloadable
end
end
module ClassMethods
def search_by_question(question)
if question.length > 1
search(RedmineProjectFiltering.calculate_tokens(question), nil, :all_words => true).first.sort_by(&:lft)
else
all(:order => 'lft')
end
end
end
end
end
end
module RedmineProjectFiltering
module Patches
module ProjectsControllerPatch
def self.included(base) # :nodoc:
base.send(:include, InstanceMethods)
base.class_eval do
unloadable
before_filter :calculate_custom_fields, :only => :index
before_filter :calculate_project_filtering_settings, :only => :index
alias_method_chain :index, :project_filtering
end
end
module InstanceMethods
def calculate_custom_fields
@custom_fields_used_for_project_filtering = CustomField.used_for_project_filtering
end
def calculate_project_filtering_settings
@project_filtering_settings = Setting[:plugin_redmine_project_filtering]
end
def index_with_project_filtering
respond_to do |format|
format.any(:html, :xml) {
calculate_filtered_projects
}
format.js {
calculate_filtered_projects
render :update do |page|
page.replace_html 'projects', :partial => 'filtered_projects'
end
}
format.atom {
projects = Project.visible.find(:all, :order => 'created_on DESC',
:limit => Setting.feeds_limit.to_i)
render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
}
end
end
private
def calculate_filtered_projects
@question = (params[:q] || "").strip
@custom_fields = params[:custom_fields] || {}
@projects = Project.visible
unless @custom_fields.empty?
@projects = @projects.with_custom_values(params[:custom_fields])
end
@featured_projects = @projects.featured if Project.respond_to? :featured
@projects = @projects.search_by_question(@question)
@featured_projects = @featured_projects.search_by_question(@question) if @featured_projects
end
end
end
end
end
module RedmineProjectFiltering
module Patches
module ProjectsHelperPatch
def self.included(base) # :nodoc:
base.send(:include, InstanceMethods)
base.class_eval do
unloadable
end
end
module InstanceMethods
# Renders a tree of projects as a nested set of unordered lists
# The given collection may be a subset of the whole project tree
# (eg. some intermediate nodes are private and can not be seen)
def render_project_hierarchy_with_filtering(projects,custom_fields,question)
s = []
if projects.any?
tokens = RedmineProjectFiltering.calculate_tokens(question, custom_fields)
ancestors = []
original_project = @project
projects.each do |project|
# set the project environment to please macros.
@project = project
if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>"
else
ancestors.pop
s << "</li>"
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
ancestors.pop
s << "</ul></li>"
end
end
classes = (ancestors.empty? ? 'root' : 'child')
s << "<li class='#{classes}'><div class='#{classes}'>" +
link_to( highlight_tokens(project.name, tokens),
{:controller => 'projects', :action => 'show', :id => project},
:class => "project #{User.current.member_of?(project) ? 'my-project' : nil}"
)
s << "<ul class='filter_fields'>"
CustomField.usable_for_project_filtering.each do |field|
value_model = project.custom_value_for(field.id)
value = value_model.present? ? value_model.value : nil
s << "<li><b>#{field.name.humanize}:</b> #{highlight_tokens(value, tokens)}</li>" if value.present?
end
s << "</ul>"
s << "<div class='clear'></div>"
unless project.description.blank?
s << "<div class='wiki description'>"
s << "<b>#{ t(:field_description) }:</b>"
s << highlight_tokens(textilizable(project.short_description, :project => project), tokens)
s << "\n</div>"
end
s << "</div>"
ancestors << project
end
ancestors.size.times{ s << "</li></ul>" }
@project = original_project
end
s.join "\n"
end
private
# copied from search_helper. This one doesn't escape html or limit the text length
def highlight_tokens(text, tokens)
return text unless text && tokens && !tokens.empty?
re_tokens = tokens.collect {|t| Regexp.escape(t)}
regexp = Regexp.new "(#{re_tokens.join('|')})", Regexp::IGNORECASE
result = ''
text.split(regexp).each_with_index do |words, i|
words = words.mb_chars
if i.even?
result << words
else
t = (tokens.index(words.downcase) || 0) % 4
result << content_tag('span', words, :class => "highlight token-#{t}")
end
end
result
end
end
end
end
end
module RedmineProjectFiltering
module Patches
module SettingsControllerPatch
def self.included(base) # :nodoc:
base.send(:include, InstanceMethods)
base.class_eval do
unloadable
before_filter :calculate_custom_fields_usable_for_project_filtering, :only => :plugin
end
end
module InstanceMethods
def calculate_custom_fields_usable_for_project_filtering
if params[:id] == 'redmine_project_filtering'
@project_custom_fields = CustomField.usable_for_project_filtering
@settings = @settings.blank? ? {'used_fields' => []} : @settings
end
end
end
end
end
end
# adds a with_custom_values named scope to the model it is included in
module RedmineProjectFiltering
module WithCustomValues
def self.included(base) # :nodoc:
base.class_eval do
named_scope( :with_custom_values,
lambda do |*args|
fields = args.first
strings = []
values = []
joins = []
fields.each do|key, value|
if(value.present?)
table_name = "custom_values_filtering_on_#{key}"
strings << "(#{table_name}.custom_field_id = ? AND #{table_name}.value = ?)"
values << key.to_i
values << value
joins << "left join custom_values #{table_name} on #{table_name}.customized_id = #{base.table_name}.id"
end
end
if strings.length == 0
{ :conditions => true }
else
{ :joins => joins.join(' '),
:conditions => [strings.join(' AND '), *values]
}
end
end
)
end
end
end
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment