From 02b73d5f7e18b6e2fa767740ffda888834ce46b7 Mon Sep 17 00:00:00 2001 From: Ludovic Gasc Date: Fri, 22 Oct 2010 13:22:36 +0200 Subject: [PATCH 1/2] Add custom fields support (filter + display + csv) --- app/controllers/timesheet_controller.rb | 20 ++-- app/models/timesheet.rb | 91 ++++++++++++------- app/views/timesheet/_by_issue.rhtml | 6 +- app/views/timesheet/_form.rhtml | 12 +++ app/views/timesheet/_issue_time_entries.rhtml | 7 ++ app/views/timesheet/_time_entry.rhtml | 3 + app/views/timesheet/_timesheet_group.rhtml | 4 + assets/stylesheets/timesheet.css | 2 + 8 files changed, 100 insertions(+), 45 deletions(-) diff --git a/app/controllers/timesheet_controller.rb b/app/controllers/timesheet_controller.rb index 99d1923..b721c88 100644 --- a/app/controllers/timesheet_controller.rb +++ b/app/controllers/timesheet_controller.rb @@ -11,6 +11,8 @@ class TimesheetController < ApplicationController helper :issues include ApplicationHelper helper :timelog + helper :custom_fields + include CustomFieldsHelper SessionKey = 'timesheet_filter' @@ -36,19 +38,19 @@ def report redirect_to :action => 'index' return end - + @timesheet.allowed_projects = allowed_projects - + if @timesheet.allowed_projects.empty? render :action => 'no_projects' return end if !params[:timesheet][:projects].blank? - @timesheet.projects = @timesheet.allowed_projects.find_all { |project| + @timesheet.projects = @timesheet.allowed_projects.find_all { |project| params[:timesheet][:projects].include?(project.id.to_s) } - else + else @timesheet.projects = @timesheet.allowed_projects end @@ -79,7 +81,7 @@ def report end end end - + @grand_total = @total.collect{|k,v| v}.inject{|sum,n| sum + n} respond_to do |format| @@ -87,7 +89,7 @@ def report format.csv { send_data @timesheet.to_csv, :filename => 'timesheet.csv', :type => "text/csv" } end end - + def context_menu @time_entries = TimeEntry.find(:all, :conditions => ['id IN (?)', params[:ids]]) render :layout => false @@ -105,7 +107,7 @@ def get_list_size def get_precision precision = Setting.plugin_timesheet_plugin['precision'] - + if precision.blank? # Set precision to a high number @precision = 10 @@ -117,7 +119,7 @@ def get_precision def get_activities @activities = TimeEntryActivity.all end - + def allowed_projects if User.current.admin? Project.timesheet_order_by_name @@ -140,7 +142,7 @@ def load_filters_from_session end if session[SessionKey] && session[SessionKey]['projects'] - @timesheet.projects = allowed_projects.find_all { |project| + @timesheet.projects = allowed_projects.find_all { |project| session[SessionKey]['projects'].include?(project.id.to_s) } end diff --git a/app/models/timesheet.rb b/app/models/timesheet.rb index ed3640a..23f069f 100644 --- a/app/models/timesheet.rb +++ b/app/models/timesheet.rb @@ -1,12 +1,12 @@ class Timesheet - attr_accessor :date_from, :date_to, :projects, :activities, :users, :allowed_projects, :period, :period_type + attr_accessor :date_from, :date_to, :projects, :activities, :users, :allowed_projects, :period, :period_type, :custom_field_values # Time entries on the Timesheet in the form of: # project.name => {:logs => [time entries], :users => [users shown in logs] } # project.name => {:logs => [time entries], :users => [users shown in logs] } # project.name could be the parent project name also attr_accessor :time_entries - + # Array of TimeEntry ids to fetch attr_accessor :potential_time_entry_ids @@ -22,19 +22,20 @@ class Timesheet :free_period => 0, :default => 1 } - + def initialize(options = { }) self.projects = [ ] self.time_entries = options[:time_entries] || { } self.potential_time_entry_ids = options[:potential_time_entry_ids] || [ ] self.allowed_projects = options[:allowed_projects] || [ ] + self.custom_field_values = options[:custom_field_values] || [ ] unless options[:activities].nil? self.activities = options[:activities].collect { |a| a.to_i } else self.activities = TimeEntryActivity.all.collect { |a| a.id.to_i } end - + unless options[:users].nil? self.users = options[:users].collect { |u| u.to_i } else @@ -46,7 +47,7 @@ def initialize(options = { }) else self.sort = :project end - + self.date_from = options[:date_from] || Date.today.to_s self.date_to = options[:date_to] || Date.today.to_s @@ -115,6 +116,7 @@ def to_param :date_to => date_to, :activities => activities, :users => users, + :custom_field_values => custom_field_values, :sort => sort } end @@ -150,7 +152,7 @@ def self.viewable_users user.allowed_to?(:log_time, nil, :global => true) } end - + protected def csv_header @@ -162,9 +164,15 @@ def csv_header l(:label_project), l(:label_issue), "#{l(:label_issue)} #{l(:field_subject)}", - l(:field_comments), - l(:field_hours) + l(:field_comments) ] + time_entry_header = TimeEntry.new + time_entry_header.custom_field_values.each do |value| + csv_data << value.custom_field.name + end + + csv_data << l(:field_hours) + Redmine::Hook.call_hook(:plugin_timesheet_model_timesheet_csv_header, { :timesheet => self, :csv_data => csv_data}) return csv_data end @@ -178,9 +186,15 @@ def time_entry_to_csv(time_entry) time_entry.project.name, ("#{time_entry.issue.tracker.name} ##{time_entry.issue.id}" if time_entry.issue), (time_entry.issue.subject if time_entry.issue), - time_entry.comments, - time_entry.hours + time_entry.comments ] + + custom_fields_helper = Object.new.extend(CustomFieldsHelper) + time_entry.custom_field_values.each do |value| + csv_data << custom_fields_helper.show_value(value) + end + + csv_data << time_entry.hours Redmine::Hook.call_hook(:plugin_timesheet_model_timesheet_time_entry_to_csv, { :timesheet => self, :time_entry => time_entry, :csv_data => csv_data}) return csv_data end @@ -189,8 +203,15 @@ def time_entry_to_csv(time_entry) # String of extra conditions to add onto the query (AND) def conditions(users, extra_conditions=nil) if self.potential_time_entry_ids.empty? + + # @todo: Add multiple custom fields selection + custom_field_conditions = "" + self.custom_field_values.each do |value| + custom_field_conditions << " AND #{CustomValue.table_name}.custom_field_id = #{value[0]} AND #{CustomValue.table_name}.value = '#{value[1]}'" unless value[1].empty? + end + if self.date_from.present? && self.date_to.present? - conditions = ["spent_on >= (:from) AND spent_on <= (:to) AND #{TimeEntry.table_name}.project_id IN (:projects) AND user_id IN (:users) AND (activity_id IN (:activities) OR (#{::Enumeration.table_name}.parent_id IN (:activities) AND #{::Enumeration.table_name}.project_id IN (:projects)))", + conditions = ["spent_on >= (:from) AND spent_on <= (:to) AND #{TimeEntry.table_name}.project_id IN (:projects) AND user_id IN (:users) AND (activity_id IN (:activities) OR (#{::Enumeration.table_name}.parent_id IN (:activities) AND #{::Enumeration.table_name}.project_id IN (:projects))) #{custom_field_conditions}", { :from => self.date_from, :to => self.date_to, @@ -199,7 +220,7 @@ def conditions(users, extra_conditions=nil) :users => users }] else # All time - conditions = ["#{TimeEntry.table_name}.project_id IN (:projects) AND user_id IN (:users) AND (activity_id IN (:activities) OR (#{::Enumeration.table_name}.parent_id IN (:activities) AND #{::Enumeration.table_name}.project_id IN (:projects)))", + conditions = ["#{TimeEntry.table_name}.project_id IN (:projects) AND user_id IN (:users) AND (activity_id IN (:activities) OR (#{::Enumeration.table_name}.parent_id IN (:activities) AND #{::Enumeration.table_name}.project_id IN (:projects))) #{custom_field_conditions}", { :projects => self.projects, :activities => self.activities, @@ -217,61 +238,61 @@ def conditions(users, extra_conditions=nil) if extra_conditions conditions[0] = conditions.first + ' AND ' + extra_conditions end - + Redmine::Hook.call_hook(:plugin_timesheet_model_timesheet_conditions, { :timesheet => self, :conditions => conditions}) return conditions end def includes - includes = [:activity, :user, :project, {:issue => [:tracker, :assigned_to, :priority]}] + includes = [:activity, :user, :project, {:issue => [:tracker, :assigned_to, :priority]}, :custom_values] Redmine::Hook.call_hook(:plugin_timesheet_model_timesheet_includes, { :timesheet => self, :includes => includes}) return includes end private - + def time_entries_for_all_users(project) return project.time_entries.find(:all, :conditions => self.conditions(self.users), :include => self.includes, :order => "spent_on ASC") end - + def time_entries_for_current_user(project) return project.time_entries.find(:all, :conditions => self.conditions(User.current.id), :include => self.includes, - :include => [:activity, :user, {:issue => [:tracker, :assigned_to, :priority]}], + :include => [:activity, :user, {:issue => [:tracker, :assigned_to, :priority]}, :custom_values], :order => "spent_on ASC") end - + def issue_time_entries_for_all_users(issue) return issue.time_entries.find(:all, :conditions => self.conditions(self.users), :include => self.includes, - :include => [:activity, :user], + :include => [:activity, :user, :custom_values], :order => "spent_on ASC") end - + def issue_time_entries_for_current_user(issue) return issue.time_entries.find(:all, :conditions => self.conditions(User.current.id), :include => self.includes, - :include => [:activity, :user], + :include => [:activity, :user, :custom_values], :order => "spent_on ASC") end - + def time_entries_for_user(user, options={}) extra_conditions = options.delete(:conditions) - + return TimeEntry.find(:all, :conditions => self.conditions([user], extra_conditions), :include => self.includes, :order => "spent_on ASC" ) end - + def fetch_time_entries_by_project self.projects.each do |project| logs = [] @@ -291,11 +312,11 @@ def fetch_time_entries_by_project else # Rest can see nothing end - + # Append the parent project name if project.parent.nil? unless logs.empty? - self.time_entries[project.name] = { :logs => logs, :users => users } + self.time_entries[project.name] = { :logs => logs, :users => users } end else unless logs.empty? @@ -304,7 +325,7 @@ def fetch_time_entries_by_project end end end - + def fetch_time_entries_by_user self.users.each do |user_id| logs = [] @@ -321,20 +342,20 @@ def fetch_time_entries_by_user else # Rest can see nothing end - + unless logs.empty? user = User.find_by_id(user_id) self.time_entries[user.name] = { :logs => logs } unless user.nil? end end end - + # project => { :users => [users shown in logs], - # :issues => + # :issues => # { issue => {:logs => [time entries], # issue => {:logs => [time entries], # issue => {:logs => [time entries]} - # + # def fetch_time_entries_by_issue self.projects.each do |project| logs = [] @@ -356,19 +377,19 @@ def fetch_time_entries_by_issue logs.flatten! if logs.respond_to?(:flatten!) logs.uniq! if logs.respond_to?(:uniq!) - + unless logs.empty? users << logs.collect(&:user).uniq.sort - + issues = logs.collect(&:issue).uniq issue_logs = { } issues.each do |issue| issue_logs[issue] = logs.find_all {|time_log| time_log.issue == issue } # TimeEntry is for this issue end - + # TODO: TE without an issue - + self.time_entries[project] = { :issues => issue_logs, :users => users} end end diff --git a/app/views/timesheet/_by_issue.rhtml b/app/views/timesheet/_by_issue.rhtml index 97d0610..97f569b 100644 --- a/app/views/timesheet/_by_issue.rhtml +++ b/app/views/timesheet/_by_issue.rhtml @@ -8,9 +8,13 @@ <%= l(:label_date) %> <%= l(:label_member) %> <%= l(:label_issue) %> / <%= l(:field_comments) %> + <% time_entry = TimeEntry.new + time_entry.custom_field_values.each do |value| %> + <%= value.custom_field.name %> + <% end %> <%= l(:field_hours) %> <%= Redmine::Hook.call_hook(:plugin_timesheet_views_timesheet_group_header, { }) %> - + <%= render :partial => "issue_time_entries", :collection => entry[:issues] %> diff --git a/app/views/timesheet/_form.rhtml b/app/views/timesheet/_form.rhtml index 923e458..07c6cd7 100644 --- a/app/views/timesheet/_form.rhtml +++ b/app/views/timesheet/_form.rhtml @@ -41,6 +41,18 @@ <%= select_tag 'timesheet[users][]', user_options(@timesheet), { :multiple => true, :size => @list_size} %>

+ +<% time_entry = TimeEntry.new +time_entry.custom_field_values = @timesheet.custom_field_values unless @timesheet.custom_field_values.empty? +time_entry.custom_field_values.each do |value| + # Remove required attribute for the search + value.custom_field.is_required = false + value.custom_field.default_value = "" + value.value = "" if @timesheet.custom_field_values.empty? + %> +

<%= custom_field_tag_with_label :timesheet, value %>

+<% end %> + <%# TODO: Typo on hook %> <%= call_hook(:plugin_timesheet_view_timesheet_form, { :timesheet => @timesheet, :params => params, :list_size => @list_size }) %> <%= call_hook(:plugin_timesheet_views_timesheet_form, { :timesheet => @timesheet, :params => params, :list_size => @list_size }) %> diff --git a/app/views/timesheet/_issue_time_entries.rhtml b/app/views/timesheet/_issue_time_entries.rhtml index b8a484f..3c8f853 100644 --- a/app/views/timesheet/_issue_time_entries.rhtml +++ b/app/views/timesheet/_issue_time_entries.rhtml @@ -19,6 +19,10 @@ +<% time_entry = TimeEntry.new +time_entry.custom_field_values.each do |value| %> + +<% end %> <%= number_with_precision(displayed_time_entries_for_issue(time_entries), @precision) %> <%= Redmine::Hook.call_hook(:plugin_timesheet_views_timesheet_time_entry_sum, {:time_entries => time_entries, :precision => @precision }) %> @@ -33,6 +37,9 @@ <%= format_date(time_entry.spent_on) %> <%= time_entry.user.name %> <%= h time_entry.comments %> +<% time_entry.custom_field_values.each do |value| %> + <%= show_value value %> +<% end %> <%= number_with_precision(time_entry.hours, @precision) %> <%= Redmine::Hook.call_hook(:plugin_timesheet_views_timesheet_time_entry, {:time_entry => time_entry, :precision => @precision }) %> diff --git a/app/views/timesheet/_time_entry.rhtml b/app/views/timesheet/_time_entry.rhtml index 901c04f..f1df4bc 100644 --- a/app/views/timesheet/_time_entry.rhtml +++ b/app/views/timesheet/_time_entry.rhtml @@ -16,6 +16,9 @@ <% end %> <%=h time_entry.comments %> +<% time_entry.custom_field_values.each do |value| %> + <%= show_value value %> +<% end %> <%= number_with_precision(time_entry.hours, @precision) %> <%= Redmine::Hook.call_hook(:plugin_timesheet_views_timesheet_time_entry, {:time_entry => time_entry, :precision => @precision }) %> diff --git a/app/views/timesheet/_timesheet_group.rhtml b/app/views/timesheet/_timesheet_group.rhtml index 52748f4..c911168 100644 --- a/app/views/timesheet/_timesheet_group.rhtml +++ b/app/views/timesheet/_timesheet_group.rhtml @@ -10,6 +10,10 @@ <%= l(:label_project) %> <%= l(:label_issue) %> <%= l(:field_comments) %> + <% time_entry = TimeEntry.new + time_entry.custom_field_values.each do |value| %> + <%= value.custom_field.name %> + <% end %> <%= l(:field_hours) %> <%= Redmine::Hook.call_hook(:plugin_timesheet_views_timesheet_group_header, { }) %> diff --git a/assets/stylesheets/timesheet.css b/assets/stylesheets/timesheet.css index af57205..5f8958d 100644 --- a/assets/stylesheets/timesheet.css +++ b/assets/stylesheets/timesheet.css @@ -4,3 +4,5 @@ div#timesheet-form p { padding:0px 10px; float:left; } #date-options { margin-left: 10px; } #date-options input[type='radio'] { margin-left: -20px; } #timesheet-form .button-to div {display:inline; } + +#timesheet-form label { margin-right: 5px; } \ No newline at end of file From 396d9e043cbdea7cc1e22b496e7d40cf32044de6 Mon Sep 17 00:00:00 2001 From: Ludovic Gasc Date: Fri, 22 Oct 2010 13:52:53 +0200 Subject: [PATCH 2/2] Apply the option 'projects with the status' also for the admins --- app/controllers/timesheet_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/timesheet_controller.rb b/app/controllers/timesheet_controller.rb index b721c88..e29029f 100644 --- a/app/controllers/timesheet_controller.rb +++ b/app/controllers/timesheet_controller.rb @@ -121,7 +121,7 @@ def get_activities end def allowed_projects - if User.current.admin? + if User.current.admin? && Setting.plugin_timesheet_plugin['project_status'] == 'all' Project.timesheet_order_by_name elsif Setting.plugin_timesheet_plugin['project_status'] == 'all' Project.timesheet_order_by_name.timesheet_with_membership(User.current)