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.
- data/README.rdoc +1 -0
- data/Rakefile +2 -2
- data/VERSION +1 -1
- data/app/controllers/timesheet_controller.rb +16 -3
- data/app/helpers/timesheet_helper.rb +13 -8
- data/app/models/timesheet.rb +120 -12
- data/app/views/timesheet/_form.rhtml +5 -31
- data/app/views/timesheet/_issue_time_entries.rhtml +9 -2
- data/app/views/timesheet/index.rhtml +2 -5
- data/app/views/timesheet/report.rhtml +2 -5
- data/assets/images/csv.png +0 -0
- data/assets/javascripts/timesheet.js +17 -0
- data/assets/stylesheets/timesheet.css +6 -0
- data/config/locales/hu.yml +1 -1
- data/config/locales/hy.yml +9 -0
- data/config/locales/ja.yml +10 -0
- data/config/locales/pt-br.yml +10 -0
- data/config/locales/ru.yml +1 -0
- data/config/locales/sr.yml +10 -0
- data/config/locales/sv.yml +10 -0
- data/config/locales/uk.yml +10 -0
- data/config/routes.rb +4 -0
- data/init.rb +47 -1
- data/lang/hu.yml +1 -1
- data/lang/hy.yml +8 -0
- data/lang/ja.yml +9 -0
- data/lang/pt-br.yml +9 -0
- data/lang/ru.yml +1 -0
- data/lang/sr.yml +9 -0
- data/lang/sv.yml +9 -0
- data/lang/uk.yml +9 -0
- data/lib/timesheet_compatibility.rb +12 -1
- data/rails/init.rb +1 -29
- data/test/functional/timesheet_controller_test.rb +256 -0
- data/test/integration/timesheet_menu_test.rb +53 -0
- data/test/test_helper.rb +24 -0
- data/test/unit/sanity_test.rb +20 -0
- data/test/unit/timesheet_test.rb +653 -0
- metadata +28 -7
- data/lib/tasks/plugin_stat.rake +0 -38
- data/spec/controllers/timesheet_controller_spec.rb +0 -263
- data/spec/models/timesheet_spec.rb +0 -537
- data/spec/sanity_spec.rb +0 -7
- data/spec/spec_helper.rb +0 -40
data/README.rdoc
CHANGED
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 = [:
|
9
|
-
plugin.tasks = [:doc, :release, :clean, :
|
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.
|
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
|
-
|
85
|
-
|
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
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
:
|
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)
|
data/app/models/timesheet.rb
CHANGED
@@ -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 =
|
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
|
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
|
-
|
116
|
-
|
117
|
-
|
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 (
|
120
|
-
|
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 (
|
124
|
-
|
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="
|
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="
|
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="
|
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(
|
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
|
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
|
-
|
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
|
-
|
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; }
|
data/config/locales/hu.yml
CHANGED
@@ -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: グループ化
|