16 Commits
Author SHA1 Message Date
Bruno Andrade dd7c6892f3 fixes for rm2.x 2013-08-16 15:32:50 -03:00
Richard Říman 193a41aa46 code cleanup 2012-12-07 02:25:49 +01:00
Richard Říman 9799244a8f some progress in porting for rm2.x 2012-12-07 02:21:19 +01:00
Richard Říman 2a324d6dca Merge https://github.com/daviscabral/redmine_rate into rm2.x 2012-11-23 15:24:05 +01:00
Davis Zanetti Cabral bc56e88e35 Redmine 2.x support - Initial work 2012-10-23 02:26:09 -02:00
Richard Říman bbe55e098a Merge branch 'master' of github.com:railsformers/redmine_rate 2012-09-12 16:45:37 +02:00
Richard Říman dd3bcd9dce added missing czech translations 2012-09-12 16:44:57 +02:00
Richard Říman a9dcecc838 Merge https://github.com/edavis10/redmine_rate 2012-06-12 12:37:11 +02:00
Eric Davis f42b419bf3 [#6898] Replace SortHelper patch with a plain helper 2012-05-15 09:09:43 -07:00
Richard Říman 8288455356 add original github source location for automation plugin updates 2012-03-02 15:57:03 +01:00
Richard dc8c5eefd2 translates 2011-11-30 08:29:02 +01:00
Eric Davis 00453672c6 Add Gemfile for bundler 2011-07-30 19:29:08 -07:00
Eric Davis 268b1b7107 Remove Rubyforge 2011-04-28 11:18:47 -07:00
Eric Davis 25c3bf5898 Release v0.2.1 2011-04-28 11:17:06 -07:00
Eric Davis 4c3e6b6b6d [#5734] Fix the cost caching case where Time Entry attributes changed
A cost value wasn't getting recaclulated by TimeEntry#cost when the
attributes were changing. It only was recalculated when .cost changed.
2011-04-06 09:27:20 -07:00
Eric Davis b20ebe587b Tweak ObjectDaddy so it's loaded into Rate when testing 2011-04-06 09:18:54 -07:00
18 changed files with 160 additions and 150 deletions
+1
View File
@@ -0,0 +1 @@
https://github.com/edavis10/redmine_rate.git
+1
View File
@@ -0,0 +1 @@
gem 'lockfile'
-4
View File
@@ -19,7 +19,6 @@ begin
s.homepage = "https://projects.littlestreamsoftware.com/projects/redmine-rate" s.homepage = "https://projects.littlestreamsoftware.com/projects/redmine-rate"
s.description = "The Rate plugin stores billing rates for Users. It also provides an API that can be used to find the rate for a Member of a Project at a specific date." s.description = "The Rate plugin stores billing rates for Users. It also provides an API that can be used to find the rate for a Member of a Project at a specific date."
s.authors = ["Eric Davis"] s.authors = ["Eric Davis"]
s.rubyforge_project = "redmine_rate" # TODO
s.files = FileList[ s.files = FileList[
"[A-Z]*", "[A-Z]*",
"init.rb", "init.rb",
@@ -29,9 +28,6 @@ begin
] ]
end end
Jeweler::GemcutterTasks.new Jeweler::GemcutterTasks.new
Jeweler::RubyforgeTasks.new do |rubyforge|
rubyforge.doc_task = "rdoc"
end
rescue LoadError rescue LoadError
puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com" puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
end end
+1 -1
View File
@@ -1 +1 @@
0.2.0 0.2.1
@@ -1,12 +1,7 @@
module RateSortHelperPatch module RateHelper
def self.included(base) # :nodoc:
base.send(:include, InstanceMethods)
end
module InstanceMethods
# Allows more parameters than the standard sort_header_tag # Allows more parameters than the standard sort_header_tag
def rate_sort_header_tag(column, options = {}) def rate_sort_header_tag(column, options = {})
caption = options.delete(:caption) || titleize(Inflector::humanize(column)) caption = options.delete(:caption) || titleize(ActiveSupport::Inflector::humanize(column))
default_order = options.delete(:default_order) || 'asc' default_order = options.delete(:default_order) || 'asc'
options[:title]= l(:label_sort_by, "\"#{caption}\"") unless options[:title] options[:title]= l(:label_sort_by, "\"#{caption}\"") unless options[:title]
content_tag('th', content_tag('th',
@@ -32,7 +27,7 @@ module RateSortHelperPatch
# Trunk version of sort_link. Was modified in r2571 of Redmine # Trunk version of sort_link. Was modified in r2571 of Redmine
def rate_sort_link_trunk_version(column, caption, default_order, options = { }) def rate_sort_link_trunk_version(column, caption, default_order, options = { })
css, order = nil, default_order css, order = nil, default_order
if column.to_s == @sort_criteria.first_key if column.to_s == @sort_criteria.first_key
if @sort_criteria.first_asc? if @sort_criteria.first_asc?
css = 'sort asc' css = 'sort asc'
@@ -47,7 +42,7 @@ module RateSortHelperPatch
sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param } sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param }
# don't reuse params if filters are present # don't reuse params if filters are present
url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options) url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options)
# Add project_id to url_options # Add project_id to url_options
url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id) url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id)
@@ -57,11 +52,11 @@ module RateSortHelperPatch
url_options[:user_id] ||= options[:user_id] url_options[:user_id] ||= options[:user_id]
##### #####
link_to(caption,
link_to_remote(caption, {:remote => true, :update => options[:update] || "content", :url => url_options, :method => options[:method] || :post},
{:update => options[:update] || "content", :url => url_options, :method => options[:method] || :post},
{:href => url_for(url_options), {:href => url_for(url_options),
:class => css}) :class => css})
# link_to caption, :remote => true
end end
private private
@@ -80,12 +75,12 @@ module RateSortHelperPatch
icon = nil icon = nil
order = default_order order = default_order
end end
caption = titleize(Inflector::humanize(column)) unless caption caption = titleize(ActiveSupport::Inflector::humanize(column)) unless caption
sort_options = { :sort_key => column, :sort_order => order } sort_options = { :sort_key => column, :sort_order => order }
# don't reuse params if filters are present # don't reuse params if filters are present
url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options) url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options)
##### Hard code url to the Rates index ##### Hard code url to the Rates index
url_options[:controller] = 'rates' url_options[:controller] = 'rates'
url_options[:action] = 'index' url_options[:action] = 'index'
@@ -93,10 +88,9 @@ module RateSortHelperPatch
##### #####
link_to_remote(caption, link_to_remote(caption,
{:update => options[:update] || "content", :url => url_options, :method => options[:method] || :post}, {:remote => true, :update => options[:update] || "content", :url => url_options, :method => options[:method] || :post},
{:href => url_for(url_options)}) + {:href => url_for(url_options)}) +
(icon ? nbsp(2) + image_tag(icon) : '') (icon ? nbsp(2) + image_tag(icon) : '')
end end
end
end end
+20 -17
View File
@@ -8,36 +8,36 @@ class Rate < ActiveRecord::Base
belongs_to :project belongs_to :project
belongs_to :user belongs_to :user
has_many :time_entries has_many :time_entries
validates_presence_of :user_id validates_presence_of :user_id
validates_presence_of :date_in_effect validates_presence_of :date_in_effect
validates_numericality_of :amount validates_numericality_of :amount
before_save :unlocked? before_save :unlocked?
after_save :update_time_entry_cost_cache after_save :update_time_entry_cost_cache
before_destroy :unlocked? before_destroy :unlocked?
after_destroy :update_time_entry_cost_cache after_destroy :update_time_entry_cost_cache
named_scope :history_for_user, lambda { |user, order| scope :history_for_user, lambda { |user, order|
{ {
:conditions => { :user_id => user.id }, :conditions => { :user_id => user.id },
:order => order, :order => order,
:include => :project :include => :project
} }
} }
def locked? def locked?
return self.time_entries.length > 0 return self.time_entries.length > 0
end end
def unlocked? def unlocked?
return !self.locked? return !self.locked?
end end
def default? def default?
return self.project.nil? return self.project.nil?
end end
def specific? def specific?
return !self.default? return !self.default?
end end
@@ -45,7 +45,7 @@ class Rate < ActiveRecord::Base
def update_time_entry_cost_cache def update_time_entry_cost_cache
TimeEntry.update_cost_cache(user, project) TimeEntry.update_cost_cache(user, project)
end end
# API to find the Rate for a +user+ on a +project+ at a +date+ # API to find the Rate for a +user+ on a +project+ at a +date+
def self.for(user, project = nil, date = Date.today.to_s) def self.for(user, project = nil, date = Date.today.to_s)
# Check input since it's a "public" API # Check input since it's a "public" API
@@ -56,13 +56,13 @@ class Rate < ActiveRecord::Base
end end
raise Rate::InvalidParameterException.new("project must be a Project instance") unless project.nil? || project.is_a?(Project) raise Rate::InvalidParameterException.new("project must be a Project instance") unless project.nil? || project.is_a?(Project)
Rate.check_date_string(date) Rate.check_date_string(date)
rate = self.for_user_project_and_date(user, project, date) rate = self.for_user_project_and_date(user, project, date)
# Check for a default (non-project) rate # Check for a default (non-project) rate
rate = self.default_for_user_and_date(user, date) if rate.nil? && project rate = self.default_for_user_and_date(user, date) if rate.nil? && project
rate rate
end end
# API to find the amount for a +user+ on a +project+ at a +date+ # API to find the amount for a +user+ on a +project+ at a +date+
def self.amount_for(user, project = nil, date = Date.today.to_s) def self.amount_for(user, project = nil, date = Date.today.to_s)
rate = self.for(user, project, date) rate = self.for(user, project, date)
@@ -96,7 +96,7 @@ class Rate < ActiveRecord::Base
end end
store_cache_timestamp('last_cache_clearing_run', Time.now.utc.to_s) store_cache_timestamp('last_cache_clearing_run', Time.now.utc.to_s)
end end
private private
def self.for_user_project_and_date(user, project, date) def self.for_user_project_and_date(user, project, date)
if project.nil? if project.nil?
@@ -107,7 +107,7 @@ class Rate < ActiveRecord::Base
user.id, user.id,
date date
]) ])
else else
return Rate.find(:first, return Rate.find(:first,
:order => 'date_in_effect DESC', :order => 'date_in_effect DESC',
@@ -117,9 +117,9 @@ class Rate < ActiveRecord::Base
project.id, project.id,
date date
]) ])
end end
end end
def self.default_for_user_and_date(user, date) def self.default_for_user_and_date(user, date)
self.for_user_project_and_date(user, nil, date) self.for_user_project_and_date(user, nil, date)
end end
@@ -128,7 +128,7 @@ class Rate < ActiveRecord::Base
# a Rate::InvalidParameterException otherwise # a Rate::InvalidParameterException otherwise
def self.check_date_string(date) def self.check_date_string(date)
raise Rate::InvalidParameterException.new("date must be a valid Date string (e.g. YYYY-MM-DD)") unless date.is_a?(String) raise Rate::InvalidParameterException.new("date must be a valid Date string (e.g. YYYY-MM-DD)") unless date.is_a?(String)
begin begin
Date.parse(date) Date.parse(date)
rescue ArgumentError rescue ArgumentError
@@ -144,13 +144,16 @@ class Rate < ActiveRecord::Base
# Wait 1 second after stealing a forced lock # Wait 1 second after stealing a forced lock
options = {:retries => 0, :suspend => 1} options = {:retries => 0, :suspend => 1}
options[:max_age] = 1 if force options[:max_age] = 1 if force
Lockfile(lock_file, options) do Lockfile(lock_file, options) do
block.call block.call
end end
end end
if Rails.env.test? if Rails.env.test?
require 'object_daddy'
include ObjectDaddy
public public
generator_for :date_in_effect => Date.today generator_for :date_in_effect => Date.today
end end
@@ -0,0 +1,18 @@
%td{:align => "left", :id => "rate_#{project.id}_#{member.user.id}"}
- if rate.nil? || rate.default?
- if rate && rate.default?
%em #{number_to_currency(rate.amount)}
- if User.current.admin?
= form_tag form_url, :method => :post, :remote => true do
= text_field :rate, :amount, :size => 8
= hidden_field(:rate,:date_in_effect, :value => Date.today.to_s)
= hidden_field(:rate, :project_id, :value => project.id)
= hidden_field(:rate, :user_id, :value => member.user.id)
= hidden_field_tag("back_url", url_for(:controller => 'projects', :action => 'settings', :id => project, :tab => 'members'))
= submit_tag(l(:rate_label_set_rate), :class => "small")
- else
%strong
- if User.current.admin?
= link_to number_to_currency(rate.amount), :controller => 'users', :action => 'edit', :id => member.user, :tab => 'rates'
- else
= number_to_currency(rate.amount)
+2 -2
View File
@@ -1,4 +1,4 @@
<% form_for(@rate) do |f| %> <%= form_for(@rate) do |f| %>
<table class="list"> <table class="list">
<thead> <thead>
<th style="width:15%"><%= l(:label_date) %></th> <th style="width:15%"><%= l(:label_date) %></th>
@@ -14,7 +14,7 @@
<td> <td>
<%= # TODO: move to controller once a hook is in place for the Admin panel <%= # TODO: move to controller once a hook is in place for the Admin panel
projects = Project.find(:all, :conditions => { :status => Project::STATUS_ACTIVE}) projects = Project.find(:all, :conditions => { :status => Project::STATUS_ACTIVE})
select_tag("rate[project_id]", project_options_for_select_with_selected(projects, @rate.project)) select_tag("rate[project_id]", project_options_for_select_with_selected(projects, @rate.project))
%> %>
</td> </td>
+9 -13
View File
@@ -3,21 +3,17 @@
<% if rate.nil? || rate.default? %> <% if rate.nil? || rate.default? %>
<% if rate && rate.default? %> <% if rate && rate.default? %>
<em><%= number_to_currency(rate.amount) %></em> <em><%= number_to_currency(rate.amount) %></em>
<% end %> <% end %>
<%= form_for(:rate, :url => rates_path(:format => 'js'), :remote => true) do |f| %>
<% remote_form_for(:rate, :url => rates_path(:format => 'js')) do |f| %> <%= f.text_field :amount %>
<%= f.hidden_field :date_in_effect, :value => Date.today.to_s, :id => "" %>
<%= f.text_field :amount %> <%= f.hidden_field :project_id, :value => membership.project.id %>
<%= f.hidden_field :date_in_effect, :value => Date.today.to_s, :id => "" %> <%= f.hidden_field :user_id, :value => user.id %>
<%= f.hidden_field :project_id, :value => membership.project.id %> <%= hidden_field_tag "back_url", url_for(:controller => 'users', :action => 'edit', :id => user, :tab => 'memberships') %>
<%= f.hidden_field :user_id, :value => user.id %> <%= submit_tag(l(:rate_label_set_rate), :class => "small") %>
<%= hidden_field_tag "back_url", url_for(:controller => 'users', :action => 'edit', :id => user, :tab => 'memberships') %>
<%= submit_tag(l(:rate_label_set_rate), :class => "small") %>
<% end %> <% end %>
<% else %> <% else %>
<strong><%= link_to number_to_currency(rate.amount), { :action => 'edit', :id => user, :tab => 'rates'} %></strong> <strong><%= link_to number_to_currency(rate.amount), { :action => 'edit', :id => user, :tab => 'rates'} %></strong>
<% end %> <% end %>
</td> </td>
+18
View File
@@ -0,0 +1,18 @@
cs:
rate_label_rates: Míra
rate_label_rate: Míra
rate_label_rate_history: Historie míry
rate_label_new_rate: Nová míra
rate_label_currency: Kč
rate_error_user_not_found: Uživatel nebyl nalezen
rate_label_set_rate: Nastavit míru
rate_label_default: Výchozí míra
rate_cost: Cena
text_rate_caches_panel: "Míra mezipaměti"
text_no_cache_run: "nebyla nalezena žádná běžící mezipaměť"
text_last_caching_run: "Poslední spuštěná mezipaměť: "
text_last_cache_clearing_run: "Poslední čištění mezipaměti: "
text_load_missing_caches: "Nahrát chybějící mezipaměť"
text_clear_and_load_all_caches: "Vyčistit a nahrát všechny mezipaměti"
text_caches_loaded_successfully: "Mezipaměti byly nahrány"
permission_view_rate: "Zobrazit míry"
+4 -4
View File
@@ -1,4 +1,4 @@
ActionController::Routing::Routes.draw do |map| resources :rates
map.resources :rates
map.connect 'rate_caches', :conditions => {:method => :put}, :controller => 'rate_caches', :action => 'update' match 'rate_caches', :to => 'rate_caches#index', :via => "get"
end match 'rate_caches', :to => 'rate_caches#update', :via => "put"
+8 -9
View File
@@ -1,13 +1,12 @@
require 'redmine' require 'redmine'
# Patches to the Redmine core # Patches to the Redmine core
require 'dispatcher' ActionDispatch::Callbacks.to_prepare do
Dispatcher.to_prepare :redmine_rate do
gem 'lockfile' gem 'lockfile'
require_dependency 'sort_helper' require_dependency 'application_controller'
SortHelper.send(:include, RateSortHelperPatch) ApplicationController.send(:include, RateHelper)
ApplicationController.send(:helper, :rate)
require_dependency 'time_entry' require_dependency 'time_entry'
TimeEntry.send(:include, RateTimeEntryPatch) TimeEntry.send(:include, RateTimeEntryPatch)
@@ -17,8 +16,8 @@ Dispatcher.to_prepare :redmine_rate do
end end
# Hooks # Hooks
require 'rate_project_hook' require_dependency 'rate_project_hook'
require 'rate_memberships_hook' require_dependency 'rate_memberships_hook'
Redmine::Plugin.register :redmine_rate do Redmine::Plugin.register :redmine_rate do
name 'Rate' name 'Rate'
@@ -26,9 +25,9 @@ Redmine::Plugin.register :redmine_rate do
url 'https://projects.littlestreamsoftware.com/projects/redmine-rate' url 'https://projects.littlestreamsoftware.com/projects/redmine-rate'
author_url 'http://www.littlestreamsoftware.com' author_url 'http://www.littlestreamsoftware.com'
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." 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."
version '0.2.0' version '0.2.1'
requires_redmine :version_or_higher => '1.0.0' requires_redmine :version_or_higher => '2.0.0'
# These settings are set automatically when caching # These settings are set automatically when caching
settings(:default => { settings(:default => {
+6 -6
View File
@@ -5,11 +5,11 @@ class RateMembershipsHook < Redmine::Hook::ViewListener
def view_users_memberships_table_row(context={}) def view_users_memberships_table_row(context={})
return context[:controller].send(:render_to_string, { return context[:controller].send(:render_to_string, {
:partial => 'users/membership_rate', :partial => 'users/membership_rate',
:locals => { :locals => {
:membership => context[:membership], :membership => context[:membership],
:user => context[:user] :user => context[:user]
}}) }
})
end end
end end
+21 -64
View File
@@ -1,20 +1,20 @@
# Hooks to attach to the Redmine Projects. # Hooks to attach to the Redmine Projects.
class RateProjectHook < Redmine::Hook::ViewListener class RateProjectHook < Redmine::Hook::ViewListener
def protect_against_forgery? def protect_against_forgery?
false false
end end
# Renders an additional table header to the membership setting # Renders an additional table header to the membership setting
# #
# Context: # Context:
# * :project => Current project # * :project => Current project
# #
def view_projects_settings_members_table_header(context ={ }) def view_projects_settings_members_table_header(context={})
return '' unless (User.current.allowed_to?(:view_rate, context[:project]) || User.current.admin?) return '' unless (User.current.allowed_to?(:view_rate, context[:project]) || User.current.admin?)
return "<th>#{l(:rate_label_rate)} #{l(:rate_label_currency)}</td>" return content_tag(:th, "#{l(:rate_label_rate)} #{l(:rate_label_currency)}")
end end
# Renders an AJAX from to update the member's billing rate # Renders an AJAX from to update the member's billing rate
# #
# Context: # Context:
@@ -22,72 +22,29 @@ class RateProjectHook < Redmine::Hook::ViewListener
# * :member => Current Member record # * :member => Current Member record
# #
# TODO: Move to a view # TODO: Move to a view
def view_projects_settings_members_table_row(context = { }) def view_projects_settings_members_table_row(context={})
member = context[:member] return '' unless (User.current.allowed_to?(:view_rate, context[:project]) || User.current.admin?)
project = context[:project]
return '' unless (User.current.allowed_to?(:view_rate, project) || User.current.admin?)
if Object.const_defined? 'Group' # 0.8.x compatibility if Object.const_defined? 'Group' # 0.8.x compatibility
# Groups cannot have a rate # Groups cannot have a rate
return content_tag(:td,'') if member.principal.is_a? Group return content_tag(:td,'') if context[:member].principal.is_a? Group
rate = Rate.for(member.principal, project) rate = Rate.for(context[:member].principal, context[:project])
else else
rate = Rate.for(member.user, project) rate = Rate.for(context[:member].user, context[:project])
end end
content = '' return context[:controller].send(:render_to_string, {
partial: "projects/settings_members",
if rate.nil? || rate.default? locals: {
if rate && rate.default? form_url: {
content << "<em>#{number_to_currency(rate.amount)}</em> " :controller => 'rates',
end :action => 'create'
},
if (User.current.admin?) rate: rate,
member: context[:member],
url = { project: context[:project]
:controller => 'rates',
:action => 'create',
:method => :post,
:protocol => Setting.protocol,
:host => Setting.host_name
} }
# Build a form_remote_tag by hand since this isn't in the scope of a controller })
# and url_rewriter doesn't like that fact.
form = form_tag(url, :onsubmit => remote_function(:url => url,
:host => Setting.host_name,
:protocol => Setting.protocol,
:form => true,
:method => 'post',
:return => 'false' )+ '; return false;')
form << text_field(:rate, :amount)
form << hidden_field(:rate,:date_in_effect, :value => Date.today.to_s)
form << hidden_field(:rate, :project_id, :value => project.id)
form << hidden_field(:rate, :user_id, :value => member.user.id)
form << hidden_field_tag("back_url", url_for(:controller => 'projects', :action => 'settings', :id => project, :tab => 'members', :protocol => Setting.protocol, :host => Setting.host_name))
form << submit_tag(l(:rate_label_set_rate), :class => "small")
form << "</form>"
content << form
end
else
if (User.current.admin?)
content << content_tag(:strong, link_to(number_to_currency(rate.amount), {
:controller => 'users',
:action => 'edit',
:id => member.user,
:tab => 'rates',
:protocol => Setting.protocol,
:host => Setting.host_name
}))
else
content << content_tag(:strong, number_to_currency(rate.amount))
end
end
return content_tag(:td, content, :align => 'left', :id => "rate_#{project.id}_#{member.user.id}" )
end end
def model_project_copy_before_save(context = {}) def model_project_copy_before_save(context = {})
+21 -8
View File
@@ -9,7 +9,7 @@ module RateTimeEntryPatch
unloadable # Send unloadable so it will not be unloaded in development unloadable # Send unloadable so it will not be unloaded in development
belongs_to :rate belongs_to :rate
before_save :cost before_save :recalculate_cost
end end
@@ -18,11 +18,11 @@ module RateTimeEntryPatch
module ClassMethods module ClassMethods
# Updated the cached cost of all TimeEntries for user and project # Updated the cached cost of all TimeEntries for user and project
def update_cost_cache(user, project=nil) def update_cost_cache(user, project=nil)
c = ARCondition.new #c = ARCondition.new
c << ["#{TimeEntry.table_name}.user_id = ?", user] c = "#{TimeEntry.table_name}.user_id = %d" % [user.id]
c << ["#{TimeEntry.table_name}.project_id = ?", project] if project c << " AND #{TimeEntry.table_name}.project_id = %d" % [project.id] if project
TimeEntry.all(:conditions => c.conditions).each do |time_entry| TimeEntry.all(:conditions => c ).each do |time_entry|
time_entry.save_cached_cost time_entry.save_cached_cost
end end
end end
@@ -32,7 +32,9 @@ module RateTimeEntryPatch
# Returns the current cost of the TimeEntry based on it's rate and hours # Returns the current cost of the TimeEntry based on it's rate and hours
# #
# Is a read-through cache method # Is a read-through cache method
def cost def cost(options={})
store_to_db = options[:store] || false
unless read_attribute(:cost) unless read_attribute(:cost)
if self.rate.nil? if self.rate.nil?
amount = Rate.amount_for(self.user, self.project, self.spent_on.to_s) amount = Rate.amount_for(self.user, self.project, self.spent_on.to_s)
@@ -43,8 +45,13 @@ module RateTimeEntryPatch
if amount.nil? if amount.nil?
write_attribute(:cost, 0.0) write_attribute(:cost, 0.0)
else else
# Write the cost to the database for caching if store_to_db
update_attribute(:cost, amount.to_f * hours.to_f) # Write the cost to the database for caching
update_attribute(:cost, amount.to_f * hours.to_f)
else
# Cache to object only
write_attribute(:cost, amount.to_f * hours.to_f)
end
end end
end end
@@ -59,6 +66,12 @@ module RateTimeEntryPatch
clear_cost_cache clear_cost_cache
update_attribute(:cost, cost) update_attribute(:cost, cost)
end end
def recalculate_cost
clear_cost_cache
cost(:store => false)
true # for callback
end
end end
end end
@@ -2,7 +2,7 @@ module RedmineRate
module Hooks module Hooks
class ViewLayoutsBaseHtmlHeadHook < Redmine::Hook::ViewListener class ViewLayoutsBaseHtmlHeadHook < Redmine::Hook::ViewListener
def view_layouts_base_html_head(context={}) def view_layouts_base_html_head(context={})
return content_tag(:style, "#admin-menu a.rate-caches { background-image: url('#{image_path('database_refresh.png', :plugin => 'redmine_rate')}'); }", :type => 'text/css') content_tag(:style, "#admin-menu a.rate-caches { background-image: url('#{image_path('database_refresh.png')}'); }", :type => 'text/css')
end end
end end
end end
+2 -3
View File
@@ -5,11 +5,11 @@
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = %q{redmine_rate} s.name = %q{redmine_rate}
s.version = "0.2.0" s.version = "0.2.1"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Eric Davis"] s.authors = ["Eric Davis"]
s.date = %q{2011-03-03} s.date = %q{2011-04-28}
s.description = %q{The Rate plugin stores billing rates for Users. It also provides an API that can be used to find the rate for a Member of a Project at a specific date.} s.description = %q{The Rate plugin stores billing rates for Users. It also provides an API that can be used to find the rate for a Member of a Project at a specific date.}
s.email = %q{edavis@littlestreamsoftware.com} s.email = %q{edavis@littlestreamsoftware.com}
s.extra_rdoc_files = [ s.extra_rdoc_files = [
@@ -79,7 +79,6 @@ Gem::Specification.new do |s|
s.homepage = %q{https://projects.littlestreamsoftware.com/projects/redmine-rate} s.homepage = %q{https://projects.littlestreamsoftware.com/projects/redmine-rate}
s.rdoc_options = ["--charset=UTF-8"] s.rdoc_options = ["--charset=UTF-8"]
s.require_paths = ["lib"] s.require_paths = ["lib"]
s.rubyforge_project = %q{redmine_rate}
s.rubygems_version = %q{1.3.7} s.rubygems_version = %q{1.3.7}
s.summary = %q{A Rate plugin for Redmine to store billing rate for user.} s.summary = %q{A Rate plugin for Redmine to store billing rate for user.}
s.test_files = [ s.test_files = [
@@ -71,6 +71,21 @@ class RateTimeEntryPatchTest < ActiveSupport::TestCase
assert_equal 2000.0, @time_entry.read_attribute(:cost) assert_equal 2000.0, @time_entry.read_attribute(:cost)
end end
should "clear and recalculate the cache when the attribute is already set but stale" do
# Set the cost
assert @time_entry.save
assert_equal 2000.0, @time_entry.read_attribute(:cost)
@time_entry.reload
@time_entry.hours = 20
assert @time_entry.save
assert_equal 4000.0, @time_entry.read_attribute(:cost)
assert_equal 4000.0, @time_entry.reload.cost
end
end end