redmine_rate 0.2.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/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
|