redmine_rate 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYRIGHT.txt +18 -0
- data/CREDITS.txt +5 -0
- data/GPL.txt +339 -0
- data/README.rdoc +93 -0
- data/Rakefile +37 -0
- data/VERSION +1 -0
- data/app/controllers/rate_caches_controller.rb +35 -0
- data/app/controllers/rates_controller.rb +154 -0
- data/app/models/rate.rb +161 -0
- data/app/views/rate_caches/index.html.erb +21 -0
- data/app/views/rates/_form.html.erb +36 -0
- data/app/views/rates/_list.html.erb +42 -0
- data/app/views/rates/create.js.rjs +2 -0
- data/app/views/rates/create_error.js.rjs +5 -0
- data/app/views/rates/edit.html.erb +3 -0
- data/app/views/rates/index.html.erb +5 -0
- data/app/views/rates/new.html.erb +3 -0
- data/app/views/rates/show.html.erb +23 -0
- data/app/views/users/_membership_rate.html.erb +23 -0
- data/app/views/users/_rates.html.erb +17 -0
- data/assets/images/database_refresh.png +0 -0
- data/config/locales/de.yml +18 -0
- data/config/locales/en.yml +18 -0
- data/config/locales/fr.yml +20 -0
- data/config/locales/ru.yml +9 -0
- data/config/routes.rb +4 -0
- data/init.rb +49 -0
- data/lang/de.yml +9 -0
- data/lang/en.yml +9 -0
- data/lang/fr.yml +8 -0
- data/lib/rate_conversion.rb +16 -0
- data/lib/rate_memberships_hook.rb +15 -0
- data/lib/rate_project_hook.rb +106 -0
- data/lib/rate_sort_helper_patch.rb +102 -0
- data/lib/rate_time_entry_patch.rb +66 -0
- data/lib/rate_users_helper_patch.rb +37 -0
- data/lib/redmine_rate/hooks/plugin_timesheet_view_timesheets_report_header_tags_hook.rb +11 -0
- data/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_group_header_hook.rb +9 -0
- data/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_hook.rb +18 -0
- data/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_sum_hook.rb +17 -0
- data/lib/redmine_rate/hooks/plugin_timesheet_views_timesheets_time_entry_row_class_hook.rb +21 -0
- data/lib/redmine_rate/hooks/timesheet_hook_helper.rb +14 -0
- data/lib/redmine_rate/hooks/view_layouts_base_html_head_hook.rb +9 -0
- data/lib/tasks/cache.rake +13 -0
- data/lib/tasks/data.rake +163 -0
- data/rails/init.rb +1 -0
- data/test/functional/rates_controller_test.rb +401 -0
- data/test/integration/admin_panel_test.rb +81 -0
- data/test/integration/routing_test.rb +16 -0
- data/test/test_helper.rb +43 -0
- data/test/unit/lib/rate_time_entry_patch_test.rb +77 -0
- data/test/unit/lib/rate_users_helper_patch_test.rb +37 -0
- data/test/unit/lib/redmine_rate/hooks/plugin_timesheet_view_timesheets_report_header_tags_hook_test.rb +26 -0
- data/test/unit/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_group_header_hook_test.rb +26 -0
- data/test/unit/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_hook_test.rb +47 -0
- data/test/unit/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_sum_hook_test.rb +48 -0
- data/test/unit/lib/redmine_rate/hooks/plugin_timesheet_views_timesheets_time_entry_row_class_hook_test.rb +60 -0
- data/test/unit/rate_for_test.rb +74 -0
- data/test/unit/rate_test.rb +333 -0
- metadata +137 -0
@@ -0,0 +1,154 @@
|
|
1
|
+
class RatesController < ApplicationController
|
2
|
+
unloadable
|
3
|
+
helper :users
|
4
|
+
helper :sort
|
5
|
+
include SortHelper
|
6
|
+
|
7
|
+
before_filter :require_admin
|
8
|
+
before_filter :require_user_id, :only => [:index, :new]
|
9
|
+
before_filter :set_back_url
|
10
|
+
|
11
|
+
ValidSortOptions = {'date_in_effect' => "#{Rate.table_name}.date_in_effect", 'project_id' => "#{Project.table_name}.name"}
|
12
|
+
|
13
|
+
# GET /rates?user_id=1
|
14
|
+
# GET /rates.xml?user_id=1
|
15
|
+
def index
|
16
|
+
sort_init "#{Rate.table_name}.date_in_effect", "desc"
|
17
|
+
sort_update ValidSortOptions
|
18
|
+
|
19
|
+
@rates = Rate.history_for_user(@user, sort_clause)
|
20
|
+
|
21
|
+
respond_to do |format|
|
22
|
+
format.html { render :action => 'index', :layout => !request.xhr?}
|
23
|
+
format.xml { render :xml => @rates }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# GET /rates/1
|
28
|
+
# GET /rates/1.xml
|
29
|
+
def show
|
30
|
+
@rate = Rate.find(params[:id])
|
31
|
+
|
32
|
+
respond_to do |format|
|
33
|
+
format.html # show.html.erb
|
34
|
+
format.xml { render :xml => @rate }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# GET /rates/new?user_id=1
|
39
|
+
# GET /rates/new.xml?user_id=1
|
40
|
+
def new
|
41
|
+
@rate = Rate.new(:user_id => @user.id)
|
42
|
+
|
43
|
+
respond_to do |format|
|
44
|
+
format.html # new.html.erb
|
45
|
+
format.xml { render :xml => @rate }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# GET /rates/1/edit
|
50
|
+
def edit
|
51
|
+
@rate = Rate.find(params[:id])
|
52
|
+
end
|
53
|
+
|
54
|
+
# POST /rates
|
55
|
+
# POST /rates.xml
|
56
|
+
def create
|
57
|
+
@rate = Rate.new(params[:rate])
|
58
|
+
|
59
|
+
respond_to do |format|
|
60
|
+
if @rate.save
|
61
|
+
format.html {
|
62
|
+
flash[:notice] = 'Rate was successfully created.'
|
63
|
+
redirect_back_or_default(rates_url(:user_id => @rate.user_id))
|
64
|
+
}
|
65
|
+
format.xml { render :xml => @rate, :status => :created, :location => @rate }
|
66
|
+
format.js { render :action => 'create.js.rjs'}
|
67
|
+
else
|
68
|
+
format.html { render :action => "new" }
|
69
|
+
format.xml { render :xml => @rate.errors, :status => :unprocessable_entity }
|
70
|
+
format.js {
|
71
|
+
flash.now[:error] = 'Error creating a new Rate.'
|
72
|
+
render :action => 'create_error.js.rjs'
|
73
|
+
}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# PUT /rates/1
|
79
|
+
# PUT /rates/1.xml
|
80
|
+
def update
|
81
|
+
@rate = Rate.find(params[:id])
|
82
|
+
|
83
|
+
respond_to do |format|
|
84
|
+
# Locked rates will fail saving here.
|
85
|
+
if @rate.update_attributes(params[:rate])
|
86
|
+
flash[:notice] = 'Rate was successfully updated.'
|
87
|
+
format.html { redirect_back_or_default(rates_url(:user_id => @rate.user_id)) }
|
88
|
+
format.xml { head :ok }
|
89
|
+
else
|
90
|
+
if @rate.locked?
|
91
|
+
flash[:error] = "Rate is locked and cannot be edited"
|
92
|
+
@rate.reload # Removes attribute changes
|
93
|
+
end
|
94
|
+
format.html { render :action => "edit" }
|
95
|
+
format.xml { render :xml => @rate.errors, :status => :unprocessable_entity }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# DELETE /rates/1
|
101
|
+
# DELETE /rates/1.xml
|
102
|
+
def destroy
|
103
|
+
@rate = Rate.find(params[:id])
|
104
|
+
@rate.destroy
|
105
|
+
|
106
|
+
respond_to do |format|
|
107
|
+
format.html {
|
108
|
+
flash[:error] = "Rate is locked and cannot be deleted" if @rate.locked?
|
109
|
+
redirect_back_or_default(rates_url(:user_id => @rate.user_id))
|
110
|
+
}
|
111
|
+
format.xml { head :ok }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def require_user_id
|
118
|
+
begin
|
119
|
+
@user = User.find(params[:user_id])
|
120
|
+
rescue ActiveRecord::RecordNotFound
|
121
|
+
respond_to do |format|
|
122
|
+
flash[:error] = l(:rate_error_user_not_found)
|
123
|
+
format.html { redirect_to(home_url) }
|
124
|
+
format.xml { render :xml => "User not found", :status => :not_found }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def set_back_url
|
130
|
+
@back_url = params[:back_url]
|
131
|
+
@back_url
|
132
|
+
end
|
133
|
+
|
134
|
+
# Override defination from ApplicationController to make sure it follows a
|
135
|
+
# whitelist
|
136
|
+
def redirect_back_or_default(default)
|
137
|
+
whitelist = %r{(rates|/users/edit)}
|
138
|
+
|
139
|
+
back_url = CGI.unescape(params[:back_url].to_s)
|
140
|
+
if !back_url.blank?
|
141
|
+
begin
|
142
|
+
uri = URI.parse(back_url)
|
143
|
+
if uri.path && uri.path.match(whitelist)
|
144
|
+
super
|
145
|
+
return
|
146
|
+
end
|
147
|
+
rescue URI::InvalidURIError
|
148
|
+
# redirect to default
|
149
|
+
logger.debug("Invalid URI sent to redirect_back_or_default: " + params[:back_url].inspect)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
redirect_to default
|
153
|
+
end
|
154
|
+
end
|
data/app/models/rate.rb
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'lockfile'
|
2
|
+
|
3
|
+
class Rate < ActiveRecord::Base
|
4
|
+
unloadable
|
5
|
+
class InvalidParameterException < Exception; end
|
6
|
+
CACHING_LOCK_FILE_NAME = 'rate_cache'
|
7
|
+
|
8
|
+
belongs_to :project
|
9
|
+
belongs_to :user
|
10
|
+
has_many :time_entries
|
11
|
+
|
12
|
+
validates_presence_of :user_id
|
13
|
+
validates_presence_of :date_in_effect
|
14
|
+
validates_numericality_of :amount
|
15
|
+
|
16
|
+
before_save :unlocked?
|
17
|
+
after_save :update_time_entry_cost_cache
|
18
|
+
before_destroy :unlocked?
|
19
|
+
after_destroy :update_time_entry_cost_cache
|
20
|
+
|
21
|
+
named_scope :history_for_user, lambda { |user, order|
|
22
|
+
{
|
23
|
+
:conditions => { :user_id => user.id },
|
24
|
+
:order => order,
|
25
|
+
:include => :project
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
def locked?
|
30
|
+
return self.time_entries.length > 0
|
31
|
+
end
|
32
|
+
|
33
|
+
def unlocked?
|
34
|
+
return !self.locked?
|
35
|
+
end
|
36
|
+
|
37
|
+
def default?
|
38
|
+
return self.project.nil?
|
39
|
+
end
|
40
|
+
|
41
|
+
def specific?
|
42
|
+
return !self.default?
|
43
|
+
end
|
44
|
+
|
45
|
+
def update_time_entry_cost_cache
|
46
|
+
TimeEntry.update_cost_cache(user, project)
|
47
|
+
end
|
48
|
+
|
49
|
+
# API to find the Rate for a +user+ on a +project+ at a +date+
|
50
|
+
def self.for(user, project = nil, date = Date.today.to_s)
|
51
|
+
# Check input since it's a "public" API
|
52
|
+
if Object.const_defined? 'Group' # 0.8.x compatibility
|
53
|
+
raise Rate::InvalidParameterException.new("user must be a Principal instance") unless user.is_a?(Principal)
|
54
|
+
else
|
55
|
+
raise Rate::InvalidParameterException.new("user must be a User instance") unless user.is_a?(User)
|
56
|
+
end
|
57
|
+
raise Rate::InvalidParameterException.new("project must be a Project instance") unless project.nil? || project.is_a?(Project)
|
58
|
+
Rate.check_date_string(date)
|
59
|
+
|
60
|
+
rate = self.for_user_project_and_date(user, project, date)
|
61
|
+
# Check for a default (non-project) rate
|
62
|
+
rate = self.default_for_user_and_date(user, date) if rate.nil? && project
|
63
|
+
rate
|
64
|
+
end
|
65
|
+
|
66
|
+
# API to find the amount for a +user+ on a +project+ at a +date+
|
67
|
+
def self.amount_for(user, project = nil, date = Date.today.to_s)
|
68
|
+
rate = self.for(user, project, date)
|
69
|
+
|
70
|
+
return nil if rate.nil?
|
71
|
+
return rate.amount
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.update_all_time_entries_with_missing_cost(options={})
|
75
|
+
with_common_lockfile(options[:force]) do
|
76
|
+
TimeEntry.all(:conditions => {:cost => nil}).each do |time_entry|
|
77
|
+
begin
|
78
|
+
time_entry.save_cached_cost
|
79
|
+
rescue Rate::InvalidParameterException => ex
|
80
|
+
puts "Error saving #{time_entry.id}: #{ex.message}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
store_cache_timestamp('last_caching_run', Time.now.utc.to_s)
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.update_all_time_entries_to_refresh_cache(options={})
|
88
|
+
with_common_lockfile(options[:force]) do
|
89
|
+
TimeEntry.find_each do |time_entry| # batch find
|
90
|
+
begin
|
91
|
+
time_entry.save_cached_cost
|
92
|
+
rescue Rate::InvalidParameterException => ex
|
93
|
+
puts "Error saving #{time_entry.id}: #{ex.message}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
store_cache_timestamp('last_cache_clearing_run', Time.now.utc.to_s)
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
def self.for_user_project_and_date(user, project, date)
|
102
|
+
if project.nil?
|
103
|
+
return Rate.find(:first,
|
104
|
+
:order => 'date_in_effect DESC',
|
105
|
+
:conditions => [
|
106
|
+
"user_id IN (?) AND date_in_effect <= ? AND project_id IS NULL",
|
107
|
+
user.id,
|
108
|
+
date
|
109
|
+
])
|
110
|
+
|
111
|
+
else
|
112
|
+
return Rate.find(:first,
|
113
|
+
:order => 'date_in_effect DESC',
|
114
|
+
:conditions => [
|
115
|
+
"user_id IN (?) AND project_id IN (?) AND date_in_effect <= ?",
|
116
|
+
user.id,
|
117
|
+
project.id,
|
118
|
+
date
|
119
|
+
])
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.default_for_user_and_date(user, date)
|
124
|
+
self.for_user_project_and_date(user, nil, date)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Checks a date string to make sure it is in format of +YYYY-MM-DD+, throwing
|
128
|
+
# a Rate::InvalidParameterException otherwise
|
129
|
+
def self.check_date_string(date)
|
130
|
+
raise Rate::InvalidParameterException.new("date must be a valid Date string (e.g. YYYY-MM-DD)") unless date.is_a?(String)
|
131
|
+
|
132
|
+
begin
|
133
|
+
Date.parse(date)
|
134
|
+
rescue ArgumentError
|
135
|
+
raise Rate::InvalidParameterException.new("date must be a valid Date string (e.g. YYYY-MM-DD)")
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.store_cache_timestamp(cache_name, timestamp)
|
140
|
+
Setting.plugin_redmine_rate = Setting.plugin_redmine_rate.merge({cache_name => timestamp})
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.with_common_lockfile(force = false, &block)
|
144
|
+
# Wait 1 second after stealing a forced lock
|
145
|
+
options = {:retries => 0, :suspend => 1}
|
146
|
+
options[:max_age] = 1 if force
|
147
|
+
|
148
|
+
Lockfile(lock_file, options) do
|
149
|
+
block.call
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
if Rails.env.test?
|
154
|
+
public
|
155
|
+
generator_for :date_in_effect => Date.today
|
156
|
+
end
|
157
|
+
|
158
|
+
def self.lock_file
|
159
|
+
Rails.root + 'tmp' + Rate::CACHING_LOCK_FILE_NAME
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<h2><%= l(:text_rate_caches_panel) %></h2>
|
2
|
+
|
3
|
+
<div id="caching-run" class="splitcontentleft">
|
4
|
+
<p>
|
5
|
+
<%= l(:text_last_caching_run) %><%= h(@last_caching_run) %>
|
6
|
+
</p>
|
7
|
+
|
8
|
+
<p>
|
9
|
+
<%= button_to(l(:text_load_missing_caches), {:controller => 'rate_caches', :action => 'update', :cache => 'missing'}, :method => :put) %>
|
10
|
+
</p>
|
11
|
+
</div>
|
12
|
+
|
13
|
+
<div id="cache-clearing-run" class="splitcontentright">
|
14
|
+
<p>
|
15
|
+
<%= l(:text_last_cache_clearing_run) %><%= h(@last_cache_clearing_run) %>
|
16
|
+
</p>
|
17
|
+
|
18
|
+
<p>
|
19
|
+
<%= button_to(l(:text_clear_and_load_all_caches), {:controller => 'rate_caches', :action => 'update', :cache => 'reload'}, :method => :put) %>
|
20
|
+
</p>
|
21
|
+
</div>
|
@@ -0,0 +1,36 @@
|
|
1
|
+
<% form_for(@rate) do |f| %>
|
2
|
+
<table class="list">
|
3
|
+
<thead>
|
4
|
+
<th style="width:15%"><%= l(:label_date) %></th>
|
5
|
+
<th><%= l(:label_project) %></th>
|
6
|
+
<th style="width:15%"><%= l(:rate_label_rate) %></th>
|
7
|
+
<th style="width:5%"></th>
|
8
|
+
</thead>
|
9
|
+
<tbody>
|
10
|
+
<tr class="odd">
|
11
|
+
<td>
|
12
|
+
<%= f.text_field "date_in_effect", :size => 10 %><%= calendar_for('rate_date_in_effect') %>
|
13
|
+
</td>
|
14
|
+
<td>
|
15
|
+
<%= # TODO: move to controller once a hook is in place for the Admin panel
|
16
|
+
projects = Project.find(:all, :conditions => { :status => Project::STATUS_ACTIVE})
|
17
|
+
|
18
|
+
select_tag("rate[project_id]", project_options_for_select_with_selected(projects, @rate.project))
|
19
|
+
%>
|
20
|
+
</td>
|
21
|
+
<td align="right">
|
22
|
+
<%= l(:rate_label_currency) %> <%= f.text_field "amount", :size => 10 %>
|
23
|
+
</td>
|
24
|
+
<td align="center">
|
25
|
+
<%= f.hidden_field "user_id" %>
|
26
|
+
<%= hidden_field_tag "back_url", @back_url %>
|
27
|
+
<% if @rate.unlocked? %>
|
28
|
+
<%= submit_tag((@rate.new_record? ? l(:button_add) : l(:button_update)), :class => 'button-small')-%>
|
29
|
+
<% else %>
|
30
|
+
<%= image_tag('locked.png') %>
|
31
|
+
<% end %>
|
32
|
+
</td>
|
33
|
+
</tr>
|
34
|
+
</tbody>
|
35
|
+
</table>
|
36
|
+
<% end %>
|
@@ -0,0 +1,42 @@
|
|
1
|
+
<table class="list">
|
2
|
+
<thead>
|
3
|
+
<%= rate_sort_header_tag("date_in_effect",
|
4
|
+
:caption => l(:label_date),
|
5
|
+
:default_order => 'desc',
|
6
|
+
:style => "width: 15%",
|
7
|
+
:method => :get,
|
8
|
+
:update => "rate_history",
|
9
|
+
:user_id => @user.id) %>
|
10
|
+
<%= rate_sort_header_tag("project_id",
|
11
|
+
:caption => l(:label_project),
|
12
|
+
:default_order => 'asc',
|
13
|
+
:method => :get,
|
14
|
+
:update => "rate_history",
|
15
|
+
:user_id => @user.id) %>
|
16
|
+
<th style="width:15%"><%= l(:rate_label_rate) %></th>
|
17
|
+
<th style="width:5%"></th>
|
18
|
+
</thead>
|
19
|
+
<tbody>
|
20
|
+
<% @rates.each do |rate| %>
|
21
|
+
<tr class="<%= cycle 'odd', 'even' %>">
|
22
|
+
<td><%= h format_date(rate.date_in_effect) %></td>
|
23
|
+
<td>
|
24
|
+
<% if rate.project %>
|
25
|
+
<%= link_to(h(rate.project), :controller => 'projects', :action => 'show', :id => rate.project) %>
|
26
|
+
<% else %>
|
27
|
+
<em><%= l(:rate_label_default) %></em>
|
28
|
+
<% end %>
|
29
|
+
</td>
|
30
|
+
<td align="right"><%= h rate.amount %></td>
|
31
|
+
<td align="center">
|
32
|
+
<% if rate.unlocked? %>
|
33
|
+
<%= link_to image_tag('edit.png'), edit_rate_path(rate, :back_url => @back_url) %>
|
34
|
+
<%= link_to image_tag('delete.png'), rate_path(rate, :back_url => @back_url), :method => :delete, :confirm => l(:text_are_you_sure) %>
|
35
|
+
<% else %>
|
36
|
+
<%= image_tag('locked.png') %>
|
37
|
+
<% end %>
|
38
|
+
</td>
|
39
|
+
</tr>
|
40
|
+
</tbody>
|
41
|
+
<% end; reset_cycle %>
|
42
|
+
</table>
|
@@ -0,0 +1,23 @@
|
|
1
|
+
<p>
|
2
|
+
<b>Amount:</b>
|
3
|
+
<%=h @rate.amount %>
|
4
|
+
</p>
|
5
|
+
|
6
|
+
<p>
|
7
|
+
<b>User:</b>
|
8
|
+
<%=h @rate.user_id %>
|
9
|
+
</p>
|
10
|
+
|
11
|
+
<p>
|
12
|
+
<b>Project:</b>
|
13
|
+
<%=h @rate.project_id %>
|
14
|
+
</p>
|
15
|
+
|
16
|
+
<p>
|
17
|
+
<b>Date in effect:</b>
|
18
|
+
<%=h @rate.date_in_effect %>
|
19
|
+
</p>
|
20
|
+
|
21
|
+
|
22
|
+
<%= link_to 'Edit', edit_rate_path(@rate) %> |
|
23
|
+
<%= link_to 'Back', rates_path %>
|
@@ -0,0 +1,23 @@
|
|
1
|
+
<td id="rate_<%= membership.project.id %>_<%= membership.user.id %>">
|
2
|
+
<% rate = Rate.for(user, membership.project) %>
|
3
|
+
|
4
|
+
<% if rate.nil? || rate.default? %>
|
5
|
+
<% if rate && rate.default? %>
|
6
|
+
<em><%= number_to_currency(rate.amount) %></em>
|
7
|
+
<% end %>
|
8
|
+
|
9
|
+
<% remote_form_for(:rate, :url => rates_path(:format => 'js')) do |f| %>
|
10
|
+
|
11
|
+
<%= f.text_field :amount %>
|
12
|
+
<%= f.hidden_field :date_in_effect, :value => Date.today.to_s, :id => "" %>
|
13
|
+
<%= f.hidden_field :project_id, :value => membership.project.id %>
|
14
|
+
<%= f.hidden_field :user_id, :value => user.id %>
|
15
|
+
<%= hidden_field_tag "back_url", url_for(:controller => 'users', :action => 'edit', :id => user, :tab => 'memberships') %>
|
16
|
+
|
17
|
+
<%= submit_tag(l(:rate_label_set_rate), :class => "small") %>
|
18
|
+
<% end %>
|
19
|
+
<% else %>
|
20
|
+
<strong><%= link_to number_to_currency(rate.amount), { :action => 'edit', :id => user, :tab => 'rates'} %></strong>
|
21
|
+
<% end %>
|
22
|
+
|
23
|
+
</td>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<h1><%= l(:rate_label_new_rate) %></h1>
|
2
|
+
|
3
|
+
<% @rate = Rate.new(:user => @user ) %>
|
4
|
+
<% @back_url = url_for(:controller => 'users', :action => 'edit', :id => @user, :tab => 'rates') %>
|
5
|
+
<%= render :partial => 'rates/form' %>
|
6
|
+
|
7
|
+
<div id="rate_history">
|
8
|
+
<h1><%= l(:rate_label_rate_history) %></h1>
|
9
|
+
<%# TODO: Refactor out of the view once there is a hook in the controller (Post 0.8.0). %>
|
10
|
+
<%# Can't expect everyone to upgrade at the moment %>
|
11
|
+
<% sort_init "#{Rate.table_name}.date_in_effect", "desc" %>
|
12
|
+
<% sort_update RatesController::ValidSortOptions %>
|
13
|
+
|
14
|
+
<% @rates = Rate.history_for_user(@user, "#{Rate.table_name}.date_in_effect desc") %>
|
15
|
+
|
16
|
+
<%= render :partial => 'rates/list' %>
|
17
|
+
</div>
|
Binary file
|
@@ -0,0 +1,18 @@
|
|
1
|
+
de:
|
2
|
+
rate_label_rates: Betraege
|
3
|
+
rate_label_rate: Betrag
|
4
|
+
rate_label_rate_history: Betragsverlauf
|
5
|
+
rate_label_new_rate: Neuer Betrag
|
6
|
+
rate_label_currency: EUR
|
7
|
+
rate_error_user_not_found: Benutzer nicht gefunden
|
8
|
+
rate_label_set_rate: Betrag setzen
|
9
|
+
rate_label_default: Standard Betrag
|
10
|
+
rate_cost: Kosten
|
11
|
+
text_rate_caches_panel: "Betrags Cache"
|
12
|
+
text_no_cache_run: "kein Cache gefunden"
|
13
|
+
text_last_caching_run: "Zuletzt Cache erstellt: "
|
14
|
+
text_last_cache_clearing_run: "Last cache clearing run at: "
|
15
|
+
text_load_missing_caches: "Load Missing Caches"
|
16
|
+
text_clear_and_load_all_caches: "Clear and Load All Caches"
|
17
|
+
text_caches_loaded_successfully: "Caches loaded successfully"
|
18
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
en:
|
2
|
+
rate_label_rates: Rates
|
3
|
+
rate_label_rate: Rate
|
4
|
+
rate_label_rate_history: Rate History
|
5
|
+
rate_label_new_rate: New Rate
|
6
|
+
rate_label_currency: $
|
7
|
+
rate_error_user_not_found: User not found
|
8
|
+
rate_label_set_rate: Set Rate
|
9
|
+
rate_label_default: Default Rate
|
10
|
+
rate_cost: Cost
|
11
|
+
text_rate_caches_panel: "Rate Caches"
|
12
|
+
text_no_cache_run: "no cache run found"
|
13
|
+
text_last_caching_run: "Last caching run at: "
|
14
|
+
text_last_cache_clearing_run: "Last cache clearing run at: "
|
15
|
+
text_load_missing_caches: "Load Missing Caches"
|
16
|
+
text_clear_and_load_all_caches: "Clear and Load All Caches"
|
17
|
+
text_caches_loaded_successfully: "Caches loaded successfully"
|
18
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
fr:
|
2
|
+
rate_label_rates: Tarifs
|
3
|
+
rate_label_rate: Tarif
|
4
|
+
rate_label_rate_history: Historique du tarif
|
5
|
+
rate_label_new_rate: Nouveau tarif
|
6
|
+
rate_label_currency: €
|
7
|
+
rate_error_user_not_found: Utilisateur introuvable
|
8
|
+
rate_label_set_rate: Parametrage du tarif
|
9
|
+
rate_label_default: Tarif par défaut
|
10
|
+
rate_error_user_not_found: Utilisateur non trouvé
|
11
|
+
rate_label_set_rate: Définir le tarif
|
12
|
+
rate_cost: Coût
|
13
|
+
text_rate_caches_panel: "Caches des tarifs"
|
14
|
+
text_no_cache_run: "pas de cache actif trouvé"
|
15
|
+
text_last_caching_run: "Dernier cache actif à : "
|
16
|
+
text_last_cache_clearing_run: "Dernier cache purgé à : "
|
17
|
+
text_load_missing_caches: "Charger les caches manquants"
|
18
|
+
text_clear_and_load_all_caches: "Recharger tous les caches"
|
19
|
+
text_caches_loaded_successfully: "Caches chargés avec succés"
|
20
|
+
|
@@ -0,0 +1,9 @@
|
|
1
|
+
ru:
|
2
|
+
rate_label_rates: Платежи
|
3
|
+
rate_label_rate: Платеж
|
4
|
+
rate_label_rate_history: История платежей
|
5
|
+
rate_label_new_rate: Новый платежe
|
6
|
+
rate_label_currency: $
|
7
|
+
rate_error_user_not_found: Пользователь не найден
|
8
|
+
rate_label_set_rate: Установить платеж
|
9
|
+
rate_label_default: Платеж по умолчанию
|
data/config/routes.rb
ADDED
data/init.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'redmine'
|
2
|
+
|
3
|
+
# Patches to the Redmine core
|
4
|
+
require 'dispatcher'
|
5
|
+
|
6
|
+
Dispatcher.to_prepare :redmine_rate do
|
7
|
+
gem 'lockfile'
|
8
|
+
|
9
|
+
require_dependency 'sort_helper'
|
10
|
+
SortHelper.send(:include, RateSortHelperPatch)
|
11
|
+
|
12
|
+
require_dependency 'time_entry'
|
13
|
+
TimeEntry.send(:include, RateTimeEntryPatch)
|
14
|
+
|
15
|
+
require_dependency 'users_helper'
|
16
|
+
UsersHelper.send(:include, RateUsersHelperPatch) unless UsersHelper.included_modules.include?(RateUsersHelperPatch)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Hooks
|
20
|
+
require 'rate_project_hook'
|
21
|
+
require 'rate_memberships_hook'
|
22
|
+
|
23
|
+
Redmine::Plugin.register :redmine_rate do
|
24
|
+
name 'Rate'
|
25
|
+
author 'Eric Davis'
|
26
|
+
url 'https://projects.littlestreamsoftware.com/projects/redmine-rate'
|
27
|
+
author_url 'http://www.littlestreamsoftware.com'
|
28
|
+
description "The Rate plugin provides an API that can be used to find the rate for a Member of a Project at a specific date. It also stores historical rate data so calculations will remain correct in the future."
|
29
|
+
version '0.2.0'
|
30
|
+
|
31
|
+
requires_redmine :version_or_higher => '1.0.0'
|
32
|
+
|
33
|
+
# These settings are set automatically when caching
|
34
|
+
settings(:default => {
|
35
|
+
'last_caching_run' => nil
|
36
|
+
})
|
37
|
+
|
38
|
+
permission :view_rate, { }
|
39
|
+
|
40
|
+
menu :admin_menu, :rate_caches, { :controller => 'rate_caches', :action => 'index'}, :caption => :text_rate_caches_panel
|
41
|
+
end
|
42
|
+
|
43
|
+
require 'redmine_rate/hooks/timesheet_hook_helper'
|
44
|
+
require 'redmine_rate/hooks/plugin_timesheet_views_timesheets_time_entry_row_class_hook'
|
45
|
+
require 'redmine_rate/hooks/plugin_timesheet_views_timesheet_group_header_hook'
|
46
|
+
require 'redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_hook'
|
47
|
+
require 'redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_sum_hook'
|
48
|
+
require 'redmine_rate/hooks/plugin_timesheet_view_timesheets_report_header_tags_hook'
|
49
|
+
require 'redmine_rate/hooks/view_layouts_base_html_head_hook'
|
data/lang/de.yml
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# English strings go here
|
2
|
+
rate_label_rates: Betraege
|
3
|
+
rate_label_rate: Betrag
|
4
|
+
rate_label_rate_history: Betragsverlauf
|
5
|
+
rate_label_new_rate: Neuer Betrag
|
6
|
+
rate_label_currency: EUR
|
7
|
+
rate_error_user_not_found: Benutzer nicht gefunden
|
8
|
+
rate_label_set_rate: Betrag setzen
|
9
|
+
rate_label_default: Standard Betrag
|
data/lang/en.yml
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# English strings go here
|
2
|
+
rate_label_rates: Rates
|
3
|
+
rate_label_rate: Rate
|
4
|
+
rate_label_rate_history: Rate History
|
5
|
+
rate_label_new_rate: New Rate
|
6
|
+
rate_label_currency: $
|
7
|
+
rate_error_user_not_found: User not found
|
8
|
+
rate_label_set_rate: Set Rate
|
9
|
+
rate_label_default: Default Rate
|
data/lang/fr.yml
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
rate_label_rates: Tarifs
|
2
|
+
rate_label_rate: Tarif
|
3
|
+
rate_label_rate_history: Historique du tarif
|
4
|
+
rate_label_new_rate: Nouveau tarif
|
5
|
+
rate_label_currency: €
|
6
|
+
rate_error_user_not_found: Utilisateur introuvable
|
7
|
+
rate_label_set_rate: Parametrage du tarif
|
8
|
+
rate_label_default: Tarif par défaut
|