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.
- 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: グループ化
|