timesheet_plugin 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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: グループ化