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