From 149391d75c7f0405d132f5fade2f44a84a6a1e9d Mon Sep 17 00:00:00 2001 From: AdamTomecek Date: Sat, 14 Apr 2012 19:02:59 +0200 Subject: [PATCH] Grouping by project, sorting, XLSX export --- app/controllers/timesheet_controller.rb | 40 ++++++ app/helpers/timesheet_helper.rb | 36 +++++ app/models/timesheet.rb | 138 +++++++++++++++++-- app/views/settings/_timesheet_settings.rhtml | 5 +- app/views/timesheet/_time_entry.rhtml | 6 +- app/views/timesheet/_timesheet_group.rhtml | 12 +- app/views/timesheet/report.rhtml | 1 + assets/stylesheets/timesheet.css | 5 + init.rb | 17 ++- 9 files changed, 241 insertions(+), 19 deletions(-) diff --git a/app/controllers/timesheet_controller.rb b/app/controllers/timesheet_controller.rb index 0e94679..c2bcf43 100644 --- a/app/controllers/timesheet_controller.rb +++ b/app/controllers/timesheet_controller.rb @@ -13,10 +13,22 @@ class TimesheetController < ApplicationController helper :timelog SessionKey = 'timesheet_filter' + SessionKey2 = 'timesheet_session' verify :method => :delete, :only => :reset, :render => {:nothing => true, :status => :method_not_allowed } + # save filter values into session because of sorting + def save_session(values) + session[SessionKey2] = values + end + + def load_session + values = session[SessionKey2] + return values + end + def index + session[SessionKey2] = nil load_filters_from_session unless @timesheet @timesheet ||= Timesheet.new @@ -30,6 +42,33 @@ def index end def report + @p = params + if params[:sort].nil? + save_session(params[:timesheet]) + params[:sort] = "date" + params[:type] = "asc" + else + sort = params[:sort] + type = params[:type] + params[:timesheet] = load_session + + # new ordering + case sort + when "date" + params[:timesheet][:order] = "spent_on #{type}" + when "member" + params[:timesheet][:order] = "users.firstname #{type}" + when "activity" + params[:timesheet][:order] = "enumerations.name #{type}" + when "hours" + params[:timesheet][:order] = "hours #{type}" + else + params[:timesheet][:order] = "spent_on ASC" + end + end + + @ts = params[:timesheet] + if params && params[:timesheet] @timesheet = Timesheet.new( params[:timesheet] ) else @@ -84,6 +123,7 @@ def report respond_to do |format| format.html { render :action => 'details', :layout => false if request.xhr? } + format.xlsx { send_file @timesheet.to_xlsx } format.csv { send_data @timesheet.to_csv, :filename => 'timesheet.csv', :type => "text/csv" } end end diff --git a/app/helpers/timesheet_helper.rb b/app/helpers/timesheet_helper.rb index e108092..24f67e4 100644 --- a/app/helpers/timesheet_helper.rb +++ b/app/helpers/timesheet_helper.rb @@ -3,6 +3,26 @@ def showing_users(users) l(:timesheet_showing_users) + users.collect(&:name).join(', ') end + # generate link with parameters to order by specific field + def link_to_sort_by(label, field) + unless params[:sort].nil? + type = params[:type] == "desc" ? "asc" : "desc" if params[:sort] == field + else + type = "asc" + end + + css = "" + # css class for arrow must be opossite to new sort type + css = type == "desc" ? "asc" : "desc" if field == params[:sort] + + return link_to label, + {:controller => 'timesheet', + :action => 'report', + :sort => field, + :type => type}, + :class => css + end + def permalink_to_timesheet(timesheet) link_to(l(:timesheet_permalink), :controller => 'timesheet', @@ -21,6 +41,18 @@ def link_to_csv_export(timesheet) :method => 'post', :class => 'icon icon-timesheet') end + + def link_to_xlsx_export(timesheet) + link_to('XLSX', + { + :controller => 'timesheet', + :action => 'report', + :format => 'xlsx', + :timesheet => timesheet.to_param + }, + :method => 'post', + :class => 'icon, icon-timesheet') + end def toggle_issue_arrows(issue_id) js = "toggleTimeEntries('#{issue_id}'); return false;" @@ -70,4 +102,8 @@ def user_options(timesheet) selected_users) end + + # def number_with_custom_delimiter(number) + # number.to_s.gsub('.', Setting.plugin_timesheet_plugin['custom_delimiter']) + # end end diff --git a/app/models/timesheet.rb b/app/models/timesheet.rb index 505cbc6..dc177da 100644 --- a/app/models/timesheet.rb +++ b/app/models/timesheet.rb @@ -1,5 +1,5 @@ -class Timesheet - attr_accessor :date_from, :date_to, :projects, :activities, :users, :allowed_projects, :period, :period_type + class Timesheet + attr_accessor :date_from, :date_to, :projects, :activities, :users, :allowed_projects, :period, :period_type, :version, :order # Time entries on the Timesheet in the form of: # project.name => {:logs => [time entries], :users => [users shown in logs] } @@ -15,7 +15,8 @@ class Timesheet ValidSortOptions = { :project => 'Project', :user => 'User', - :issue => 'Issue' + :issue => 'Issue', + :version => 'Version' } ValidPeriodType = { @@ -63,6 +64,7 @@ def initialize(options = { }) self.period_type = ValidPeriodType[:free_period] end self.period = options[:period] || nil + self.order = options[:order] || nil end # Gets all the time_entries for all the projects @@ -75,6 +77,8 @@ def fetch_time_entries fetch_time_entries_by_user when :issue fetch_time_entries_by_issue + when :version + fetch_time_entries_by_version else fetch_time_entries_by_project end @@ -133,7 +137,7 @@ def to_csv # Write the CSV based on the group/sort case sort - when :user, :project + when :user, :project, :version time_entries.sort.each do |entryname, entry| entry[:logs].each do |e| csv << time_entry_to_csv(e) @@ -152,6 +156,28 @@ def to_csv end end + def to_xlsx + path ="#{RAILS_ROOT}/files/timesheet.xlsx" + FileUtils.rm(path) if FileTest.exists?(path) + + case sort + when :user, :project, :version + SimpleXlsx::Serializer.new(path) do |doc| + doc.add_sheet("Sesit") do |sheet| + sheet.add_row csv_header + time_entries.sort.each do |entryname, entry| + entry[:logs].each do |e| + sheet.add_row time_entry_to_csv(e) + end + end + end + end + end + + return path + + end + def self.viewable_users if Setting['plugin_timesheet_plugin'].present? && Setting['plugin_timesheet_plugin']['user_status'] == 'all' user_scope = User.all @@ -248,7 +274,7 @@ 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") + :order => self.order) end def time_entries_for_current_user(project) @@ -256,7 +282,36 @@ def time_entries_for_current_user(project) :conditions => self.conditions(User.current.id), :include => self.includes, :include => [:activity, :user, {:issue => [:tracker, :assigned_to, :priority]}], - :order => "spent_on ASC") + :order => self.order) + end + + def empty_version_time_entries_for_all_users(project) + return project.time_entries.find(:all, + :conditions => self.conditions(self.users, "version_id is NULL"), + :include => self.includes, + :order => self.order) + end + + def empty_version_time_entries_for_current_user(project) + return project.time_entries.find(:all, + :conditions => self.conditions(User.current.id, "version_id is NULL"), + :include => self.includes, + :order => self.order) + end + + def version_time_entries_for_all_users(version) + return version.time_entries.find(:all, + :conditions => self.conditions(self.users), + :include => self.includes, + :order => self.order) + end + + def version_time_entries_for_current_user(version) + return version.time_entries.find(:all, + :conditions => self.conditions(User.current.id), + :include => self.includes, + :include => [:activity, :user, {:issue => [:tracker, :assigned_to, :priority]}], + :order => self.order) end def issue_time_entries_for_all_users(issue) @@ -264,7 +319,7 @@ def issue_time_entries_for_all_users(issue) :conditions => self.conditions(self.users), :include => self.includes, :include => [:activity, :user], - :order => "spent_on ASC") + :order => self.order) end def issue_time_entries_for_current_user(issue) @@ -272,7 +327,7 @@ def issue_time_entries_for_current_user(issue) :conditions => self.conditions(User.current.id), :include => self.includes, :include => [:activity, :user], - :order => "spent_on ASC") + :order => self.order) end def time_entries_for_user(user, options={}) @@ -281,9 +336,70 @@ def time_entries_for_user(user, options={}) return TimeEntry.find(:all, :conditions => self.conditions([user], extra_conditions), :include => self.includes, - :order => "spent_on ASC" + :order => self.order ) end + + def fetch_time_entries_by_version + self.projects.each do |project| + project.versions.each do |version| + unless version.nil? + logs = [] + users = [] + + if User.current.admin? + # Administrators can see all time entries + logs = version_time_entries_for_all_users(version) + users = logs.collect(&:user).uniq.sort + elsif User.current.allowed_to_on_single_potentially_archived_project?(:see_project_timesheets, project) + # Users with the Role and correct permission can see all time entries + logs = time_entries_for_all_users(project) + users = logs.collect(&:user).uniq.sort + elsif User.current.allowed_to_on_single_potentially_archived_project?(:view_time_entries, project) + # Users with permission to see their time entries + logs = version_time_entries_for_current_user(version) + users = logs.collect(&:user).uniq.sort + else + # Rest can see nothing + end + + # Append project and version name + unless logs.empty? + self.time_entries[project.name + ' / v: ' + version.name] = { :logs => logs, :users => users } + end + end + end + + logs = [] + users = [] + if User.current.admin? + # Administrators can see all time entries + logs = empty_version_time_entries_for_all_users(project) + users = logs.collect(&:user).uniq.sort + elsif User.current.allowed_to_on_single_potentially_archived_project?(:see_project_timesheets, project) + # Users with the Role and correct permission can see all time entries + logs = time_entries_for_all_users(project) + users = logs.collect(&:user).uniq.sort + elsif User.current.allowed_to_on_single_potentially_archived_project?(:view_time_entries, project) + # Users with permission to see their time entries + logs = empty_version_time_entries_for_current_user(project) + users = logs.collect(&:user).uniq.sort + 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 } + end + else + unless logs.empty? + self.time_entries[project.parent.name + ' / ' + project.name] = { :logs => logs, :users => users } + end + end + end + end def fetch_time_entries_by_project self.projects.each do |project| @@ -391,4 +507,8 @@ def fetch_time_entries_by_issue def l(*args) I18n.t(*args) end + + # def number_with_custom_delimiter(number) + # number.to_s.gsub('.', Setting.plugin_timesheet_plugin['custom_delimiter']) + # end end diff --git a/app/views/settings/_timesheet_settings.rhtml b/app/views/settings/_timesheet_settings.rhtml index 21c86ce..73f0afa 100644 --- a/app/views/settings/_timesheet_settings.rhtml +++ b/app/views/settings/_timesheet_settings.rhtml @@ -2,6 +2,8 @@

<%= text_field_tag 'settings[precision]', @settings['precision'] %>

+ +

<%= select_tag('settings[project_status]', @@ -16,4 +18,5 @@ options_for_select({ l(:text_active_users) => 'active', l(:text_all_users) => 'all'}, @settings['user_status'])) %> -

+

+ diff --git a/app/views/timesheet/_time_entry.rhtml b/app/views/timesheet/_time_entry.rhtml index abebd40..4ecb69b 100644 --- a/app/views/timesheet/_time_entry.rhtml +++ b/app/views/timesheet/_time_entry.rhtml @@ -3,8 +3,10 @@ <%= check_box_tag 'ids[]', time_entry.id, false, { :class => 'checkbox' } %> <%= format_date(time_entry.spent_on) %> <%= time_entry.user.name %> + <% time_entry.user.groups.each do |group| %><%= group.name %> <% end %> <%= time_entry.activity.name %> - <%= time_entry.project.name %> + <%= link_to_project time_entry.project %> + <%= time_entry.version %> <% if time_entry.issue %>
@@ -16,7 +18,7 @@ <% end %> <%=h time_entry.comments %> - <%= number_with_precision(time_entry.hours, @precision) %> + <%= number_with_precision(time_entry.hours, :precision => @precision) %> <%= Redmine::Hook.call_hook(:plugin_timesheet_views_timesheet_time_entry, {:time_entry => time_entry, :precision => @precision }) %> <% if time_entry.editable_by?(User.current) -%> diff --git a/app/views/timesheet/_timesheet_group.rhtml b/app/views/timesheet/_timesheet_group.rhtml index 626e269..6d8e115 100644 --- a/app/views/timesheet/_timesheet_group.rhtml +++ b/app/views/timesheet/_timesheet_group.rhtml @@ -4,13 +4,15 @@ <%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "table")); return false;', :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}", :class => 'toggle-all' %> - <%= l(:label_date) %> - <%= l(:label_member) %> - <%= l(:label_activity) %> - <%= l(:label_project) %> + <%= link_to_sort_by l(:label_date), "date" %> + <%= link_to_sort_by l(:label_member), "member" %> + <%= l(:label_group) %> + <%= link_to_sort_by l(:label_activity), "activity" %> + <%= l(:label_project) %> + <%= l(:label_version) %> <%= l(:label_issue) %> <%= l(:field_comments) %> - <%= l(:field_hours) %> + <%= link_to_sort_by l(:field_hours), "hours" %> <%= Redmine::Hook.call_hook(:plugin_timesheet_views_timesheet_group_header, { }) %> diff --git a/app/views/timesheet/report.rhtml b/app/views/timesheet/report.rhtml index 8979145..ce52ca7 100644 --- a/app/views/timesheet/report.rhtml +++ b/app/views/timesheet/report.rhtml @@ -1,4 +1,5 @@
+ <%= link_to_xlsx_export(@timesheet) %> <%= link_to_csv_export(@timesheet) %> <%= permalink_to_timesheet(@timesheet) %>
diff --git a/assets/stylesheets/timesheet.css b/assets/stylesheets/timesheet.css index af57205..3a42345 100644 --- a/assets/stylesheets/timesheet.css +++ b/assets/stylesheets/timesheet.css @@ -4,3 +4,8 @@ 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; } + +table.list th a { text-decoration: underline; } +table.list th a.asc:after { content: url('../images/sort_up.gif');} +table.list th a.desc:after { content: url('../images/sort_down.gif');} +table.list th a:hover { text-decoration: none; } \ No newline at end of file diff --git a/init.rb b/init.rb index 5a8235f..4baed0b 100644 --- a/init.rb +++ b/init.rb @@ -8,15 +8,27 @@ FCSV = CSV end +require 'simple_xlsx' require 'dispatcher' -Dispatcher.to_prepare :timesheet_plugin do +require 'timesheet_plugin/hooks/timesheet_hooks' +require_dependency 'timesheet_plugin/hooks/timesheet_hooks' + +require 'timesheet_plugin/patches/time_entry_patch' +require 'timesheet_plugin/patches/version_patch' +Dispatcher.to_prepare :timesheet_plugin do require_dependency 'principal' require_dependency 'user' User.send(:include, TimesheetPlugin::Patches::UserPatch) require_dependency 'project' Project.send(:include, TimesheetPlugin::Patches::ProjectPatch) + + require_dependency 'time_entry' + TimeEntry.send(:include, TimesheetPlugin::Patches::TimeEntryPatch) + + require_dependency 'version' + Version.send(:include, TimesheetPlugin::Patches::VersionPatch) # Needed for the compatibility check begin require_dependency 'time_entry_activity' @@ -41,7 +53,8 @@ 'list_size' => '5', 'precision' => '2', 'project_status' => 'active', - 'user_status' => 'active' + 'user_status' => 'active', + 'custom_delimiter' => '.' }, :partial => 'settings/timesheet_settings') permission :see_project_timesheets, { }, :require => :member