timesheet_plugin 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. data/README.rdoc +1 -0
  2. data/Rakefile +2 -2
  3. data/VERSION +1 -1
  4. data/app/controllers/timesheet_controller.rb +16 -3
  5. data/app/helpers/timesheet_helper.rb +13 -8
  6. data/app/models/timesheet.rb +120 -12
  7. data/app/views/timesheet/_form.rhtml +5 -31
  8. data/app/views/timesheet/_issue_time_entries.rhtml +9 -2
  9. data/app/views/timesheet/index.rhtml +2 -5
  10. data/app/views/timesheet/report.rhtml +2 -5
  11. data/assets/images/csv.png +0 -0
  12. data/assets/javascripts/timesheet.js +17 -0
  13. data/assets/stylesheets/timesheet.css +6 -0
  14. data/config/locales/hu.yml +1 -1
  15. data/config/locales/hy.yml +9 -0
  16. data/config/locales/ja.yml +10 -0
  17. data/config/locales/pt-br.yml +10 -0
  18. data/config/locales/ru.yml +1 -0
  19. data/config/locales/sr.yml +10 -0
  20. data/config/locales/sv.yml +10 -0
  21. data/config/locales/uk.yml +10 -0
  22. data/config/routes.rb +4 -0
  23. data/init.rb +47 -1
  24. data/lang/hu.yml +1 -1
  25. data/lang/hy.yml +8 -0
  26. data/lang/ja.yml +9 -0
  27. data/lang/pt-br.yml +9 -0
  28. data/lang/ru.yml +1 -0
  29. data/lang/sr.yml +9 -0
  30. data/lang/sv.yml +9 -0
  31. data/lang/uk.yml +9 -0
  32. data/lib/timesheet_compatibility.rb +12 -1
  33. data/rails/init.rb +1 -29
  34. data/test/functional/timesheet_controller_test.rb +256 -0
  35. data/test/integration/timesheet_menu_test.rb +53 -0
  36. data/test/test_helper.rb +24 -0
  37. data/test/unit/sanity_test.rb +20 -0
  38. data/test/unit/timesheet_test.rb +653 -0
  39. metadata +28 -7
  40. data/lib/tasks/plugin_stat.rake +0 -38
  41. data/spec/controllers/timesheet_controller_spec.rb +0 -263
  42. data/spec/models/timesheet_spec.rb +0 -537
  43. data/spec/sanity_spec.rb +0 -7
  44. data/spec/spec_helper.rb +0 -40
data/README.rdoc CHANGED
@@ -18,6 +18,7 @@ A plugin to show and filter timelogs across all projects in Redmine.
18
18
  * Permalinks to reports
19
19
  * Plugin hook support for changing the behavior of the plugin
20
20
  * User configurable precision for hours
21
+ * CSV exports
21
22
 
22
23
  == Getting the plugin
23
24
 
data/Rakefile CHANGED
@@ -5,8 +5,8 @@ Dir[File.expand_path(File.dirname(__FILE__)) + "/lib/tasks/**/*.rake"].sort.each
5
5
 
6
6
  RedminePluginSupport::Base.setup do |plugin|
7
7
  plugin.project_name = 'timesheet_plugin'
8
- plugin.default_task = [:spec]
9
- plugin.tasks = [:doc, :release, :clean, :spec]
8
+ plugin.default_task = [:test]
9
+ plugin.tasks = [:doc, :release, :clean, :stats, :test, :db]
10
10
  # TODO: gem not getting this automaticly
11
11
  plugin.redmine_root = File.expand_path(File.dirname(__FILE__) + '/../../../')
12
12
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.0
1
+ 0.6.0
@@ -14,6 +14,8 @@ class TimesheetController < ApplicationController
14
14
 
15
15
  SessionKey = 'timesheet_filter'
16
16
 
17
+ verify :method => :delete, :only => :reset, :render => {:nothing => true, :status => :method_not_allowed }
18
+
17
19
  def index
18
20
  load_filters_from_session
19
21
  unless @timesheet
@@ -81,8 +83,10 @@ class TimesheetController < ApplicationController
81
83
 
82
84
  @grand_total = @total.collect{|k,v| v}.inject{|sum,n| sum + n}
83
85
 
84
- send_csv and return if 'csv' == params[:export]
85
- render :action => 'details', :layout => false if request.xhr?
86
+ respond_to do |format|
87
+ format.html { render :action => 'details', :layout => false if request.xhr? }
88
+ format.csv { send_data @timesheet.to_csv, :filename => 'timesheet.csv', :type => "text/csv" }
89
+ end
86
90
  end
87
91
 
88
92
  def context_menu
@@ -90,6 +94,11 @@ class TimesheetController < ApplicationController
90
94
  render :layout => false
91
95
  end
92
96
 
97
+ def reset
98
+ clear_filters_from_session
99
+ redirect_to :action => 'index'
100
+ end
101
+
93
102
  private
94
103
  def get_list_size
95
104
  @list_size = Setting.plugin_timesheet_plugin['list_size'].to_i
@@ -114,10 +123,14 @@ class TimesheetController < ApplicationController
114
123
  if User.current.admin?
115
124
  return Project.find(:all, :order => 'name ASC')
116
125
  else
117
- return User.current.projects.find(:all, :order => 'name ASC')
126
+ return Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name ASC')
118
127
  end
119
128
  end
120
129
 
130
+ def clear_filters_from_session
131
+ session[SessionKey] = nil
132
+ end
133
+
121
134
  def load_filters_from_session
122
135
  if session[SessionKey]
123
136
  @timesheet = Timesheet.new(session[SessionKey])
@@ -7,14 +7,19 @@ module TimesheetHelper
7
7
  link_to(l(:timesheet_permalink),
8
8
  :controller => 'timesheet',
9
9
  :action => 'report',
10
- :timesheet => {
11
- :projects => timesheet.projects.collect(&:id),
12
- :date_from => timesheet.date_from,
13
- :date_to => timesheet.date_to,
14
- :activities => timesheet.activities,
15
- :users => timesheet.users,
16
- :sort => timesheet.sort
17
- })
10
+ :timesheet => timesheet.to_param)
11
+ end
12
+
13
+ def link_to_csv_export(timesheet)
14
+ link_to('CSV',
15
+ {
16
+ :controller => 'timesheet',
17
+ :action => 'report',
18
+ :format => 'csv',
19
+ :timesheet => timesheet.to_param
20
+ },
21
+ :method => 'post',
22
+ :class => 'icon icon-timesheet')
18
23
  end
19
24
 
20
25
  def toggle_issue_arrows(issue_id)
@@ -38,7 +38,7 @@ class Timesheet
38
38
  unless options[:users].nil?
39
39
  self.users = options[:users].collect { |u| u.to_i }
40
40
  else
41
- self.users = User.find(:all).collect(&:id)
41
+ self.users = Timesheet.viewable_users.collect {|user| user.id.to_i }
42
42
  end
43
43
 
44
44
  if !options[:sort].nil? && options[:sort].respond_to?(:to_sym) && ValidSortOptions.keys.include?(options[:sort].to_sym)
@@ -107,21 +107,114 @@ class Timesheet
107
107
  end
108
108
  self
109
109
  end
110
+
111
+ def to_param
112
+ {
113
+ :projects => projects.collect(&:id),
114
+ :date_from => date_from,
115
+ :date_to => date_to,
116
+ :activities => activities,
117
+ :users => users,
118
+ :sort => sort
119
+ }
120
+ end
121
+
122
+ def to_csv
123
+ returning '' do |out|
124
+ FCSV.generate out do |csv|
125
+ csv << csv_header
126
+
127
+ # Write the CSV based on the group/sort
128
+ case sort
129
+ when :user, :project
130
+ time_entries.sort.each do |entryname, entry|
131
+ entry[:logs].each do |e|
132
+ csv << time_entry_to_csv(e)
133
+ end
134
+ end
135
+ when :issue
136
+ time_entries.sort.each do |project, entries|
137
+ entries[:issues].sort {|a,b| a[0].id <=> b[0].id}.each do |issue, time_entries|
138
+ time_entries.each do |e|
139
+ csv << time_entry_to_csv(e)
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ def self.viewable_users
149
+ User.active.select {|user|
150
+ user.allowed_to?(:log_time, nil, :global => true)
151
+ }
152
+ end
110
153
 
111
154
  protected
112
155
 
113
- def conditions(users)
156
+ def csv_header
157
+ csv_data = [
158
+ '#',
159
+ l(:label_date),
160
+ l(:label_member),
161
+ l(:label_activity),
162
+ l(:label_project),
163
+ l(:label_issue),
164
+ l(:field_comments),
165
+ l(:field_hours)
166
+ ]
167
+ Redmine::Hook.call_hook(:plugin_timesheet_model_timesheet_csv_header, { :timesheet => self, :csv_data => csv_data})
168
+ return csv_data
169
+ end
170
+
171
+ def time_entry_to_csv(time_entry)
172
+ csv_data = [
173
+ time_entry.id,
174
+ time_entry.spent_on,
175
+ time_entry.user.name,
176
+ time_entry.activity.name,
177
+ time_entry.project.name,
178
+ ("#{time_entry.issue.tracker.name} ##{time_entry.issue.id}" if time_entry.issue),
179
+ time_entry.comments,
180
+ time_entry.hours
181
+ ]
182
+ Redmine::Hook.call_hook(:plugin_timesheet_model_timesheet_time_entry_to_csv, { :timesheet => self, :time_entry => time_entry, :csv_data => csv_data})
183
+ return csv_data
184
+ end
185
+
186
+ # Array of users to find
187
+ # String of extra conditions to add onto the query (AND)
188
+ def conditions(users, extra_conditions=nil)
114
189
  if self.potential_time_entry_ids.empty?
115
- if self.date_from && self.date_to
116
- conditions = ["spent_on >= (?) AND spent_on <= (?) AND #{TimeEntry.table_name}.project_id IN (?) AND activity_id IN (?) AND user_id IN (?)",
117
- self.date_from, self.date_to, self.projects, self.activities, users ]
190
+ # TODO: Rails 2.1.2 doesn't define #present?
191
+ if !self.date_from.blank? && !self.date_to.blank?
192
+ 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) #{TimesheetCompatibility::Enumeration.project_specific_sql})",
193
+ {
194
+ :from => self.date_from,
195
+ :to => self.date_to,
196
+ :projects => self.projects,
197
+ :activities => self.activities,
198
+ :users => users
199
+ }]
118
200
  else # All time
119
- conditions = ["#{TimeEntry.table_name}.project_id IN (?) AND activity_id IN (?) AND user_id IN (?)",
120
- self.projects, self.activities, users ]
201
+ conditions = ["#{TimeEntry.table_name}.project_id IN (:projects) AND user_id IN (:users) AND (activity_id IN (:activities) #{TimesheetCompatibility::Enumeration.project_specific_sql})",
202
+ {
203
+ :projects => self.projects,
204
+ :activities => self.activities,
205
+ :users => users
206
+ }]
121
207
  end
122
208
  else
123
- conditions = ["user_id IN (?) AND #{TimeEntry.table_name}.id IN (?)",
124
- users, self.potential_time_entry_ids ]
209
+ conditions = ["user_id IN (:users) AND #{TimeEntry.table_name}.id IN (:potential_time_entries)",
210
+ {
211
+ :users => users,
212
+ :potential_time_entries => self.potential_time_entry_ids
213
+ }]
214
+ end
215
+
216
+ if extra_conditions
217
+ conditions[0] = conditions.first + ' AND ' + extra_conditions
125
218
  end
126
219
 
127
220
  Redmine::Hook.call_hook(:plugin_timesheet_model_timesheet_conditions, { :timesheet => self, :conditions => conditions})
@@ -168,9 +261,11 @@ class Timesheet
168
261
  :order => "spent_on ASC")
169
262
  end
170
263
 
171
- def time_entries_for_user(user)
264
+ def time_entries_for_user(user, options={})
265
+ extra_conditions = options.delete(:conditions)
266
+
172
267
  return TimeEntry.find(:all,
173
- :conditions => self.conditions([user]),
268
+ :conditions => self.conditions([user], extra_conditions),
174
269
  :include => self.includes,
175
270
  :order => "spent_on ASC"
176
271
  )
@@ -218,6 +313,10 @@ class Timesheet
218
313
  elsif User.current.id == user_id
219
314
  # Users can see their own their time entries
220
315
  logs = time_entries_for_user(user_id)
316
+ elsif User.current.allowed_to?(:see_project_timesheets, nil, :global => true)
317
+ # User can see project timesheets in at least once place, so
318
+ # fetch the user timelogs for those projects
319
+ logs = time_entries_for_user(user_id, :conditions => Project.allowed_to_condition(User.current, :see_project_timesheets))
221
320
  else
222
321
  # Rest can see nothing
223
322
  end
@@ -273,5 +372,14 @@ class Timesheet
273
372
  end
274
373
  end
275
374
  end
276
-
375
+
376
+
377
+ # TODO: Redmine 0.8 compatibility hack
378
+ def l(*args)
379
+ if defined?(GLoc)
380
+ GLoc.l(*args)
381
+ else
382
+ I18n.t(*args)
383
+ end
384
+ end
277
385
  end
@@ -26,7 +26,7 @@
26
26
  </p>
27
27
 
28
28
  <p>
29
- <label for="timesheet[projects][]" class="select_all"><%= l(:timesheet_project_label)%>:</label><br />
29
+ <label for="timesheet_projects_" class="select_all"><%= l(:timesheet_project_label)%>:</label><br />
30
30
  <%= select_tag 'timesheet[projects][]',
31
31
  options_from_collection_for_select(@timesheet.allowed_projects, :id, :name, @timesheet.projects.collect(&:id)),
32
32
  { :multiple => true, :size => @list_size}
@@ -35,7 +35,7 @@
35
35
 
36
36
 
37
37
  <p>
38
- <label for="timesheet[activities][]" class="select_all"><%= l(:timesheet_activities_label)%>:</label><br />
38
+ <label for="timesheet_activities_" class="select_all"><%= l(:timesheet_activities_label)%>:</label><br />
39
39
  <%= select_tag 'timesheet[activities][]',
40
40
  options_from_collection_for_select(@activities, :id, :name, @timesheet.activities),
41
41
  { :multiple => true, :size => @list_size}
@@ -43,9 +43,9 @@
43
43
  </p>
44
44
 
45
45
  <p>
46
- <label for="timesheet[users][]" class="select_all"><%= l(:timesheet_users_label)%>:</label><br />
46
+ <label for="timesheet_users_" class="select_all"><%= l(:timesheet_users_label)%>:</label><br />
47
47
  <%= select_tag 'timesheet[users][]',
48
- options_from_collection_for_select(User.find(:all, :conditions => ['status = ?', User::STATUS_ACTIVE]).sort { |a,b| a.to_s.downcase <=> b.to_s.downcase }, :id, :name, @timesheet.users),
48
+ options_from_collection_for_select(Timesheet.viewable_users.sort { |a,b| a.to_s.downcase <=> b.to_s.downcase }, :id, :name, @timesheet.users),
49
49
  { :multiple => true, :size => @list_size}
50
50
 
51
51
  %>
@@ -59,32 +59,6 @@
59
59
  <%= submit_tag l(:button_apply),:class => 'button-small' -%>
60
60
 
61
61
  <% end %>
62
+ <%= button_to(l(:button_reset), {:controller => 'timesheet', :action => 'reset'}, :method => 'delete') %>
62
63
  </fieldset>
63
64
  </div>
64
-
65
- <% content_for(:header_tags) do %>
66
- <style type="text/css">
67
- #date-options { margin-left: 10px; }
68
- #date-options input[type='radio'] { margin-left: -20px; }
69
- </style>
70
- <script type="text/javascript">
71
-
72
- function targetField(label_element) {
73
- return $(label_element.attributes.for.value);
74
- }
75
-
76
- function selectAllOptions(element) {
77
- for (var i = 0; i < element.options.length; i++) {
78
- element.options[i].selected = true;
79
- }
80
- }
81
-
82
- Event.observe(window, 'load',
83
- function() {
84
- $$('label.select_all').each(function(element) {
85
- Event.observe(element, 'click', function (e) { selectAllOptions(targetField(this)); });
86
- });
87
- }
88
- );
89
- </script>
90
- <% end %>
@@ -10,8 +10,15 @@
10
10
  <%= toggle_issue_arrows(issue.id) %>
11
11
  </td>
12
12
  <td align="center"></td>
13
- <td align="center"><%= issue.assigned_to.to_s %></td>
14
- <td><%= link_to("##{issue.id}", :controller => 'issues', :action => 'show', :id => issue.id) %>: <%= h issue.subject %></td>
13
+ <td align="center"><%= l(:field_assigned_to) %>:<br /><%= issue.assigned_to.to_s %></td>
14
+ <td>
15
+ <div class="tooltip">
16
+ <%= link_to_issue issue %>
17
+ <span class="tip">
18
+ <%= render_issue_tooltip issue %>
19
+ </span>
20
+ </div>
21
+ </td>
15
22
  <td align="center"><strong><%= number_with_precision(displayed_time_entries_for_issue(time_entries), @precision) %></strong></td>
16
23
  <%= Redmine::Hook.call_hook(:plugin_timesheet_views_timesheet_time_entry_sum, {:time_entries => time_entries, :precision => @precision }) %>
17
24
  <td align="center"></td>
@@ -3,9 +3,6 @@
3
3
  <%= render :partial => 'form' %>
4
4
 
5
5
  <% content_for(:header_tags) do %>
6
- <style type="text/css">
7
-
8
- div#timesheet-form p { padding:0px 10px; float:left; }
9
-
10
- </style>
6
+ <%= stylesheet_link_tag "timesheet.css", :plugin => "timesheet_plugin", :media => 'all' %>
7
+ <%= javascript_include_tag 'timesheet.js', :plugin => 'timesheet_plugin' %>
11
8
  <% end %>
@@ -1,4 +1,5 @@
1
1
  <div class="contextual">
2
+ <%= link_to_csv_export(@timesheet) %>
2
3
  <%= permalink_to_timesheet(@timesheet) %>
3
4
  </div>
4
5
 
@@ -34,11 +35,7 @@ end # form_tag
34
35
  <% content_for(:header_tags) do %>
35
36
  <%= javascript_include_tag 'context_menu' %>
36
37
  <%= stylesheet_link_tag 'context_menu' %>
37
- <style type="text/css">
38
-
39
- div#timesheet-form p { padding:0px 10px; float:left; }
40
-
41
- </style>
38
+ <%= stylesheet_link_tag "timesheet.css", :plugin => "timesheet_plugin", :media => 'all' %>
42
39
  <%# TODO: Typo on hook %>
43
40
  <%= call_hook(:plugin_timesheet_view_timesheets_report_header_tags, { :timesheet => @timesheet }) %>
44
41
  <%= call_hook(:plugin_timesheet_views_timesheets_report_header_tags, { :timesheet => @timesheet }) %>
Binary file
@@ -0,0 +1,17 @@
1
+ function targetField(label_element) {
2
+ return $(label_element.attributes.for.value);
3
+ }
4
+
5
+ function selectAllOptions(element) {
6
+ for (var i = 0; i < element.options.length; i++) {
7
+ element.options[i].selected = true;
8
+ }
9
+ }
10
+
11
+ Event.observe(window, 'load',
12
+ function() {
13
+ $$('label.select_all').each(function(element) {
14
+ Event.observe(element, 'click', function (e) { selectAllOptions(targetField(this)); });
15
+ });
16
+ }
17
+ );
@@ -0,0 +1,6 @@
1
+ div#timesheet-form p { padding:0px 10px; float:left; }
2
+ .icon-timesheet { background-image: url(../images/csv.png); }
3
+
4
+ #date-options { margin-left: 10px; }
5
+ #date-options input[type='radio'] { margin-left: -20px; }
6
+ #timesheet-form .button-to div {display:inline; }
@@ -1,4 +1,4 @@
1
- hu:
1
+ hu:
2
2
  timesheet_title: Időlap
3
3
  timesheet_date_from_label: Kezdő dátum
4
4
  timesheet_date_to_label: Vég dátum
@@ -0,0 +1,9 @@
1
+ "hy":
2
+ timesheet_title: Ծախսված ժամանակի աղյուսակ
3
+ timesheet_date_from_label: Սկսած
4
+ timesheet_date_to_label: մինչև
5
+ timesheet_project_label: Նախագիծ
6
+ timesheet_activities_label: Ակտիվությունը
7
+ timesheet_users_label: Օգտագործողներ
8
+ timesheet_permalink: '(Մշտական հղում դեպի այս աղյուսակը)'
9
+ timesheet_group_by: Դասավորել ըստ
@@ -0,0 +1,10 @@
1
+ ja:
2
+ timesheet_title: 記録時間表
3
+ timesheet_date_from_label: 期間
4
+ timesheet_date_to_label: から
5
+ timesheet_project_label: プロジェクト
6
+ timesheet_activities_label: 活動
7
+ timesheet_users_label: ユーザ
8
+ timesheet_showing_users: 'ユーザ: '
9
+ timesheet_permalink: '(この記録時間表へ直接リンク)'
10
+ timesheet_group_by: グループ化