Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbe55e098a | ||
|
|
dd3bcd9dce | ||
|
|
a9dcecc838 | ||
|
|
f42b419bf3 | ||
|
|
8288455356 | ||
|
|
dc8c5eefd2 | ||
|
|
00453672c6 | ||
|
|
268b1b7107 | ||
|
|
25c3bf5898 | ||
|
|
4c3e6b6b6d | ||
|
|
b20ebe587b | ||
|
|
e971b13ee9 | ||
|
|
994c7b2306 | ||
|
|
eaccdb34e4 | ||
|
|
6f65166afb | ||
|
|
4933496709 | ||
|
|
ab175831f2 | ||
|
|
80b05852e7 | ||
|
|
211e0b6bdf | ||
|
|
920c8cdda5 | ||
|
|
5dc6feeb81 | ||
|
|
65919d382b | ||
|
|
3fc0eb5a35 | ||
|
|
4e705fd1eb | ||
|
|
5200eacd36 | ||
|
|
52622a678e | ||
|
|
f9bd3c0e53 | ||
|
|
e4f1a623f1 | ||
|
|
7824eadc94 | ||
|
|
073dfe1177 | ||
|
|
fb3b0a41ae | ||
|
|
16a7a3146f | ||
|
|
33cf4d5c5c | ||
|
|
41b1a3cd54 | ||
|
|
43a8fe27dc | ||
|
|
c97979bd06 | ||
|
|
f7b03b825f | ||
|
|
d656121b20 | ||
|
|
dbfc6c5335 | ||
|
|
cec35ad3af | ||
|
|
f60a5154dd | ||
|
|
593352e317 | ||
|
|
3ccb88e31a | ||
|
|
e541270461 | ||
|
|
7192adb231 | ||
|
|
a341be0344 | ||
|
|
81158be9b9 | ||
|
|
44963ac96b | ||
|
|
5b372b2bef | ||
|
|
0c3d83fe62 | ||
|
|
22cbcdf847 | ||
|
|
3403e74941 | ||
|
|
b3b4b92450 | ||
|
|
8672619f15 | ||
|
|
585f70c0b2 | ||
|
|
d99d0add59 | ||
|
|
119a4c1557 | ||
|
|
8058e938ae | ||
|
|
8a70287e99 | ||
|
|
7f7b400f15 | ||
|
|
40a7f74aeb | ||
|
|
23820188b4 | ||
|
|
31e3f2be9d | ||
|
|
2341b99e23 | ||
|
|
ef394da702 | ||
|
|
5f277b9656 | ||
|
|
724dd470ba | ||
|
|
43c20ae820 | ||
|
|
c0006e1040 | ||
|
|
391f136710 | ||
|
|
0117e454a3 |
1
.github_origin
Normal file
1
.github_origin
Normal file
@@ -0,0 +1 @@
|
||||
https://github.com/edavis10/redmine_rate.git
|
||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
*.tar.gz
|
||||
*.zip
|
||||
.DS_Store
|
||||
coverage
|
||||
doc
|
||||
pkg
|
||||
rdoc
|
||||
41
README.rdoc
41
README.rdoc
@@ -1,6 +1,6 @@
|
||||
= Redmine Rate Plugin
|
||||
|
||||
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.
|
||||
The Rate plugin stores billable 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.
|
||||
|
||||
== Features
|
||||
|
||||
@@ -12,6 +12,7 @@ The Rate plugin provides an API that can be used to find the rate for a Member o
|
||||
* Rate.for API for other plugins
|
||||
* Integration with the Billing plugin
|
||||
* Integration with the Budget plugin
|
||||
* Integration with the Contracts plugin
|
||||
|
||||
== Getting the plugin
|
||||
|
||||
@@ -22,28 +23,30 @@ A copy of the plugin can be downloaded from {Little Stream Software}[https://pro
|
||||
|
||||
There are two sets of steps to install this plugin. The first one should be done if you have used version 0.1.0 of the Budget Plugin or 0.2.0 of the Billing Plugin. This is because the rate data needs to be migrated out of the Budget plugin and into this plugin.
|
||||
|
||||
=== If you have data from a previous version of Budget or Billing
|
||||
=== Option #1: If you have data from a previous version of Budget or Billing
|
||||
|
||||
These installation instructions are very specific because the Rate plugin adjusts data inside the Budget plugin so several data integrity checks are needed.
|
||||
|
||||
0. Backup up your data! Backup your data!
|
||||
1. Follow the Redmine plugin installation steps a http://www.redmine.org/wiki/redmine/Plugins Make sure the plugin is installed to +vendor/plugins/redmine_rate+
|
||||
2. Make sure you are running the 0.1.0 version of the Budget plugin and 0.0.1 version of the Billing plugin
|
||||
3. Run the pre_install_export to export your current budget and billing data to file +rake rate_plugin:pre_install_export+
|
||||
4. Run the plugin migrations +rake db:migrate_plugins+ in order to get the new tables for Rates
|
||||
5. Upgrade the budget plugin to 0.2.0 and the billing plugin to 0.3.0
|
||||
6. Rerun the plugin migrations +rake db:migrate_plugins+ in order to update to Budget's 0.2.0 schema
|
||||
7. Run the post_install_check to check your exported data (from #3 above) against the new Rate data. +rake rate_plugin:post_install_check+
|
||||
8. If the script reports no errors, proceed. If errors are found, please file a bug report and revert to your backups
|
||||
9. Restart your Redmine web servers (e.g. mongrel, thin, mod_rails)
|
||||
10. Setup the "View Rate" permission for any Role that should be allowed to see the user rates in a Project
|
||||
1. Install the Lockfile gem
|
||||
2. Follow the Redmine plugin installation steps a http://www.redmine.org/wiki/redmine/Plugins Make sure the plugin is installed to +vendor/plugins/redmine_rate+
|
||||
3. Make sure you are running the 0.1.0 version of the Budget plugin and 0.0.1 version of the Billing plugin
|
||||
4. Run the pre_install_export to export your current budget and billing data to file +rake rate_plugin:pre_install_export+
|
||||
5. Run the plugin migrations +rake db:migrate_plugins+ in order to get the new tables for Rates
|
||||
6. Upgrade the budget plugin to 0.2.0 and the billing plugin to 0.3.0
|
||||
7. Rerun the plugin migrations +rake db:migrate_plugins+ in order to update to Budget's 0.2.0 schema
|
||||
8. Run the post_install_check to check your exported data (from #3 above) against the new Rate data. +rake rate_plugin:post_install_check+
|
||||
9. If the script reports no errors, proceed. If errors are found, please file a bug report and revert to your backups
|
||||
10. Restart your Redmine web servers (e.g. mongrel, thin, mod_rails)
|
||||
11. Setup the "View Rate" permission for any Role that should be allowed to see the user rates in a Project
|
||||
|
||||
=== If you do not have any data from Budget or Billing
|
||||
=== Option #2: If you do not have any data from Budget or Billing
|
||||
|
||||
1. Follow the Redmine plugin installation steps a http://www.redmine.org/wiki/redmine/Plugins Make sure the plugin is installed to +vendor/plugins/redmine_rate+
|
||||
2. Run the plugin migrations +rake db:migrate_plugins+ in order to get the new tables for Rates
|
||||
3. Restart your Redmine web servers (e.g. mongrel, thin, mod_rails)
|
||||
4. Setup the "View Rate" permission for any Role that should be allowed to see the user rates in a Project
|
||||
1. Install the Lockfile gem
|
||||
2. Follow the Redmine plugin installation steps a http://www.redmine.org/wiki/redmine/Plugins Make sure the plugin is installed to +vendor/plugins/redmine_rate+
|
||||
3. Run the plugin migrations +rake db:migrate_plugins+ in order to get the new tables for Rates
|
||||
4. Restart your Redmine web servers (e.g. mongrel, thin, mod_rails)
|
||||
5. Setup the "View Rate" permission for any Role that should be allowed to see the user rates in a Project
|
||||
|
||||
== Usage
|
||||
|
||||
@@ -76,6 +79,10 @@ A default rate is a user's Rate that doesn't correspond to a specific project.
|
||||
|
||||
Currently this feature is only available through the Rate API. A Rate will become locked once a valid TimeEntry is assigned to the Rate.
|
||||
|
||||
=== Caching
|
||||
|
||||
The plugin includes some simple caching for time entries cost. Instead of doing a lookup for each time entry, the rate plugin will cache the total cost for each time entry to the database. The caching is done transparently but you can run and purge the caches from the Administration Panel or using the provided rate tasks (rake rate_plugin:update_cost_cache, rake rate_plugin:refresh_cost_cache).
|
||||
|
||||
== License
|
||||
|
||||
This plugin is licensed under the GNU GPL v2. See COPYRIGHT.txt and GPL.txt for details.
|
||||
|
||||
26
Rakefile
26
Rakefile
@@ -5,5 +5,29 @@ Dir[File.expand_path(File.dirname(__FILE__)) + "/lib/tasks/**/*.rake"].sort.each
|
||||
|
||||
RedminePluginSupport::Base.setup do |plugin|
|
||||
plugin.project_name = 'redmine_rate'
|
||||
plugin.default_task = [:spec, :features]
|
||||
plugin.default_task = [:test]
|
||||
plugin.tasks = [:db, :doc, :release, :clean, :test, :stats, :metrics]
|
||||
plugin.redmine_root = File.expand_path(File.dirname(__FILE__) + '/../../../')
|
||||
end
|
||||
|
||||
begin
|
||||
require 'jeweler'
|
||||
Jeweler::Tasks.new do |s|
|
||||
s.name = "redmine_rate"
|
||||
s.summary = "A Rate plugin for Redmine to store billing rate for user."
|
||||
s.email = "edavis@littlestreamsoftware.com"
|
||||
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.authors = ["Eric Davis"]
|
||||
s.files = FileList[
|
||||
"[A-Z]*",
|
||||
"init.rb",
|
||||
"rails/init.rb",
|
||||
"{bin,generators,lib,test,app,assets,config,lang}/**/*",
|
||||
'lib/jeweler/templates/.gitignore'
|
||||
]
|
||||
end
|
||||
Jeweler::GemcutterTasks.new
|
||||
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"
|
||||
end
|
||||
|
||||
35
app/controllers/rate_caches_controller.rb
Normal file
35
app/controllers/rate_caches_controller.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
class RateCachesController < ApplicationController
|
||||
unloadable
|
||||
|
||||
layout 'admin'
|
||||
|
||||
before_filter :require_admin
|
||||
|
||||
def index
|
||||
@last_caching_run = if Setting.plugin_redmine_rate['last_caching_run'].present? && Setting.plugin_redmine_rate['last_caching_run'].to_date
|
||||
format_time(Setting.plugin_redmine_rate['last_caching_run'])
|
||||
else
|
||||
l(:text_no_cache_run)
|
||||
end
|
||||
|
||||
@last_cache_clearing_run = if Setting.plugin_redmine_rate['last_cache_clearing_run'].present? && Setting.plugin_redmine_rate['last_cache_clearing_run'].to_date
|
||||
format_time(Setting.plugin_redmine_rate['last_cache_clearing_run'])
|
||||
else
|
||||
l(:text_no_cache_run)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def update
|
||||
if params[:cache].present?
|
||||
if params[:cache].match(/missing/)
|
||||
Rate.update_all_time_entries_with_missing_cost(:force => true)
|
||||
flash[:notice] = l(:text_caches_loaded_successfully)
|
||||
elsif params[:cache].match(/reload/)
|
||||
Rate.update_all_time_entries_to_refresh_cache(:force => true)
|
||||
flash[:notice] = l(:text_caches_loaded_successfully)
|
||||
end
|
||||
end
|
||||
redirect_to :action => 'index'
|
||||
end
|
||||
end
|
||||
@@ -1,14 +1,7 @@
|
||||
require_dependency 'sort_helper'
|
||||
|
||||
module RateSortHelperPatch
|
||||
def self.included(base) # :nodoc:
|
||||
base.send(:include, InstanceMethods)
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
module RateHelper
|
||||
# Allows more parameters than the standard sort_header_tag
|
||||
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'
|
||||
options[:title]= l(:label_sort_by, "\"#{caption}\"") unless options[:title]
|
||||
content_tag('th',
|
||||
@@ -82,7 +75,7 @@ module RateSortHelperPatch
|
||||
icon = nil
|
||||
order = default_order
|
||||
end
|
||||
caption = titleize(Inflector::humanize(column)) unless caption
|
||||
caption = titleize(ActiveSupport::Inflector::humanize(column)) unless caption
|
||||
|
||||
sort_options = { :sort_key => column, :sort_order => order }
|
||||
# don't reuse params if filters are present
|
||||
@@ -100,8 +93,4 @@ module RateSortHelperPatch
|
||||
(icon ? nbsp(2) + image_tag(icon) : '')
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
require 'lockfile'
|
||||
|
||||
class Rate < ActiveRecord::Base
|
||||
unloadable
|
||||
class InvalidParameterException < Exception; end
|
||||
CACHING_LOCK_FILE_NAME = 'rate_cache'
|
||||
|
||||
belongs_to :project
|
||||
belongs_to :user
|
||||
@@ -11,7 +14,9 @@ class Rate < ActiveRecord::Base
|
||||
validates_numericality_of :amount
|
||||
|
||||
before_save :unlocked?
|
||||
after_save :update_time_entry_cost_cache
|
||||
before_destroy :unlocked?
|
||||
after_destroy :update_time_entry_cost_cache
|
||||
|
||||
named_scope :history_for_user, lambda { |user, order|
|
||||
{
|
||||
@@ -37,10 +42,18 @@ class Rate < ActiveRecord::Base
|
||||
return !self.default?
|
||||
end
|
||||
|
||||
def update_time_entry_cost_cache
|
||||
TimeEntry.update_cost_cache(user, project)
|
||||
end
|
||||
|
||||
# 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)
|
||||
# Check input since it's a "public" API
|
||||
if Object.const_defined? 'Group' # 0.8.x compatibility
|
||||
raise Rate::InvalidParameterException.new("user must be a Principal instance") unless user.is_a?(Principal)
|
||||
else
|
||||
raise Rate::InvalidParameterException.new("user must be a User instance") unless user.is_a?(User)
|
||||
end
|
||||
raise Rate::InvalidParameterException.new("project must be a Project instance") unless project.nil? || project.is_a?(Project)
|
||||
Rate.check_date_string(date)
|
||||
|
||||
@@ -58,6 +71,32 @@ class Rate < ActiveRecord::Base
|
||||
return rate.amount
|
||||
end
|
||||
|
||||
def self.update_all_time_entries_with_missing_cost(options={})
|
||||
with_common_lockfile(options[:force]) do
|
||||
TimeEntry.all(:conditions => {:cost => nil}).each do |time_entry|
|
||||
begin
|
||||
time_entry.save_cached_cost
|
||||
rescue Rate::InvalidParameterException => ex
|
||||
puts "Error saving #{time_entry.id}: #{ex.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
store_cache_timestamp('last_caching_run', Time.now.utc.to_s)
|
||||
end
|
||||
|
||||
def self.update_all_time_entries_to_refresh_cache(options={})
|
||||
with_common_lockfile(options[:force]) do
|
||||
TimeEntry.find_each do |time_entry| # batch find
|
||||
begin
|
||||
time_entry.save_cached_cost
|
||||
rescue Rate::InvalidParameterException => ex
|
||||
puts "Error saving #{time_entry.id}: #{ex.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
store_cache_timestamp('last_cache_clearing_run', Time.now.utc.to_s)
|
||||
end
|
||||
|
||||
private
|
||||
def self.for_user_project_and_date(user, project, date)
|
||||
if project.nil?
|
||||
@@ -96,4 +135,30 @@ class Rate < ActiveRecord::Base
|
||||
raise Rate::InvalidParameterException.new("date must be a valid Date string (e.g. YYYY-MM-DD)")
|
||||
end
|
||||
end
|
||||
|
||||
def self.store_cache_timestamp(cache_name, timestamp)
|
||||
Setting.plugin_redmine_rate = Setting.plugin_redmine_rate.merge({cache_name => timestamp})
|
||||
end
|
||||
|
||||
def self.with_common_lockfile(force = false, &block)
|
||||
# Wait 1 second after stealing a forced lock
|
||||
options = {:retries => 0, :suspend => 1}
|
||||
options[:max_age] = 1 if force
|
||||
|
||||
Lockfile(lock_file, options) do
|
||||
block.call
|
||||
end
|
||||
end
|
||||
|
||||
if Rails.env.test?
|
||||
require 'object_daddy'
|
||||
include ObjectDaddy
|
||||
|
||||
public
|
||||
generator_for :date_in_effect => Date.today
|
||||
end
|
||||
|
||||
def self.lock_file
|
||||
Rails.root + 'tmp' + Rate::CACHING_LOCK_FILE_NAME
|
||||
end
|
||||
end
|
||||
|
||||
21
app/views/rate_caches/index.html.erb
Normal file
21
app/views/rate_caches/index.html.erb
Normal file
@@ -0,0 +1,21 @@
|
||||
<h2><%= l(:text_rate_caches_panel) %></h2>
|
||||
|
||||
<div id="caching-run" class="splitcontentleft">
|
||||
<p>
|
||||
<%= l(:text_last_caching_run) %><%= h(@last_caching_run) %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= button_to(l(:text_load_missing_caches), {:controller => 'rate_caches', :action => 'update', :cache => 'missing'}, :method => :put) %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="cache-clearing-run" class="splitcontentright">
|
||||
<p>
|
||||
<%= l(:text_last_cache_clearing_run) %><%= h(@last_cache_clearing_run) %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= button_to(l(:text_clear_and_load_all_caches), {:controller => 'rate_caches', :action => 'update', :cache => 'reload'}, :method => :put) %>
|
||||
</p>
|
||||
</div>
|
||||
23
app/views/users/_membership_rate.html.erb
Normal file
23
app/views/users/_membership_rate.html.erb
Normal file
@@ -0,0 +1,23 @@
|
||||
<td id="rate_<%= membership.project.id %>_<%= membership.user.id %>">
|
||||
<% rate = Rate.for(user, membership.project) %>
|
||||
|
||||
<% if rate.nil? || rate.default? %>
|
||||
<% if rate && rate.default? %>
|
||||
<em><%= number_to_currency(rate.amount) %></em>
|
||||
<% end %>
|
||||
|
||||
<% 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.hidden_field :project_id, :value => membership.project.id %>
|
||||
<%= f.hidden_field :user_id, :value => user.id %>
|
||||
<%= 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 %>
|
||||
<% else %>
|
||||
<strong><%= link_to number_to_currency(rate.amount), { :action => 'edit', :id => user, :tab => 'rates'} %></strong>
|
||||
<% end %>
|
||||
|
||||
</td>
|
||||
@@ -1,72 +0,0 @@
|
||||
<%# TODO: Override the default view until post 0.8.x which will have the hooks %>
|
||||
<% if @memberships.any? %>
|
||||
<table class="list memberships">
|
||||
<thead>
|
||||
<th><%= l(:label_project) %></th>
|
||||
<th><%= l(:label_role) %></th>
|
||||
<th style="width:15%"></th>
|
||||
<%# TODO: Hook %>
|
||||
<th><%= l(:rate_label_rate) %> <%= l(:rate_label_currency) %></td>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @memberships.each do |membership| %>
|
||||
<% next if membership.new_record? %>
|
||||
<tr class="<%= cycle 'odd', 'even' %>">
|
||||
<td><%=h membership.project %></td>
|
||||
<td align="center">
|
||||
<% form_tag({ :action => 'edit_membership', :id => @user, :membership_id => membership }) do %>
|
||||
<%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name", membership.role_id) %>
|
||||
<%= submit_tag l(:button_change), :class => "small" %>
|
||||
<% end %>
|
||||
</td>
|
||||
<td align="center">
|
||||
<%= link_to l(:button_delete), {:action => 'destroy_membership', :id => @user, :membership_id => membership }, :method => :post, :class => 'icon icon-del' %>
|
||||
</td>
|
||||
<%# TODO: Hook %>
|
||||
<td id="rate_<%= membership.project.id %>_<%= membership.user.id %>">
|
||||
<% rate = Rate.for(@user, membership.project) %>
|
||||
|
||||
<% if rate.nil? || rate.default? %>
|
||||
<% if rate && rate.default? %>
|
||||
<em><%= number_to_currency(rate.amount) %></em>
|
||||
<% end %>
|
||||
|
||||
<% remote_form_for(:rate, :url => formatted_rates_path('js')) do |f| %>
|
||||
|
||||
<%= f.text_field :amount %>
|
||||
<%= f.hidden_field :date_in_effect, :value => Date.today.to_s, :id => "" %>
|
||||
<%= f.hidden_field :project_id, :value => membership.project.id %>
|
||||
<%= f.hidden_field :user_id, :value => @user.id %>
|
||||
<%= 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 %>
|
||||
<% else %>
|
||||
<strong><%= link_to number_to_currency(rate.amount), { :action => 'edit', :id => @user, :tab => 'rates'} %></strong>
|
||||
<% end %>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<% end; reset_cycle %>
|
||||
</table>
|
||||
<% else %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% end %>
|
||||
|
||||
<% if @projects.any? %>
|
||||
<p>
|
||||
<label><%=l(:label_project_new)%></label><br/>
|
||||
<% form_tag({ :action => 'edit_membership', :id => @user }) do %>
|
||||
<%# 0.8.x compatibility %>
|
||||
<% if respond_to?(:options_for_membership_project_select) %>
|
||||
<%= select_tag 'membership[project_id]', options_for_membership_project_select(@user, @projects) %>
|
||||
<% else %>
|
||||
<%= select_tag 'membership[project_id]', projects_options_for_select(@projects) %>
|
||||
<% end %>
|
||||
<%= l(:label_role) %>:
|
||||
<%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name") %>
|
||||
<%= submit_tag l(:button_add) %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
BIN
assets/images/database_refresh.png
Normal file
BIN
assets/images/database_refresh.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 770 B |
3
autotest/discover.rb
Normal file
3
autotest/discover.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
Autotest.add_discovery do
|
||||
"rails"
|
||||
end
|
||||
18
config/locales/cs.yml
Normal file
18
config/locales/cs.yml
Normal 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"
|
||||
18
config/locales/de.yml
Normal file
18
config/locales/de.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
de:
|
||||
rate_label_rates: Betraege
|
||||
rate_label_rate: Betrag
|
||||
rate_label_rate_history: Betragsverlauf
|
||||
rate_label_new_rate: Neuer Betrag
|
||||
rate_label_currency: EUR
|
||||
rate_error_user_not_found: Benutzer nicht gefunden
|
||||
rate_label_set_rate: Betrag setzen
|
||||
rate_label_default: Standard Betrag
|
||||
rate_cost: Kosten
|
||||
text_rate_caches_panel: "Betrags Cache"
|
||||
text_no_cache_run: "kein Cache gefunden"
|
||||
text_last_caching_run: "Zuletzt Cache erstellt: "
|
||||
text_last_cache_clearing_run: "Last cache clearing run at: "
|
||||
text_load_missing_caches: "Load Missing Caches"
|
||||
text_clear_and_load_all_caches: "Clear and Load All Caches"
|
||||
text_caches_loaded_successfully: "Caches loaded successfully"
|
||||
|
||||
@@ -7,3 +7,12 @@ en:
|
||||
rate_error_user_not_found: User not found
|
||||
rate_label_set_rate: Set Rate
|
||||
rate_label_default: Default Rate
|
||||
rate_cost: Cost
|
||||
text_rate_caches_panel: "Rate Caches"
|
||||
text_no_cache_run: "no cache run found"
|
||||
text_last_caching_run: "Last caching run at: "
|
||||
text_last_cache_clearing_run: "Last cache clearing run at: "
|
||||
text_load_missing_caches: "Load Missing Caches"
|
||||
text_clear_and_load_all_caches: "Clear and Load All Caches"
|
||||
text_caches_loaded_successfully: "Caches loaded successfully"
|
||||
|
||||
|
||||
20
config/locales/fr.yml
Normal file
20
config/locales/fr.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
fr:
|
||||
rate_label_rates: Tarifs
|
||||
rate_label_rate: Tarif
|
||||
rate_label_rate_history: Historique du tarif
|
||||
rate_label_new_rate: Nouveau tarif
|
||||
rate_label_currency: €
|
||||
rate_error_user_not_found: Utilisateur introuvable
|
||||
rate_label_set_rate: Parametrage du tarif
|
||||
rate_label_default: Tarif par défaut
|
||||
rate_error_user_not_found: Utilisateur non trouvé
|
||||
rate_label_set_rate: Définir le tarif
|
||||
rate_cost: Coût
|
||||
text_rate_caches_panel: "Caches des tarifs"
|
||||
text_no_cache_run: "pas de cache actif trouvé"
|
||||
text_last_caching_run: "Dernier cache actif à : "
|
||||
text_last_cache_clearing_run: "Dernier cache purgé à : "
|
||||
text_load_missing_caches: "Charger les caches manquants"
|
||||
text_clear_and_load_all_caches: "Recharger tous les caches"
|
||||
text_caches_loaded_successfully: "Caches chargés avec succés"
|
||||
|
||||
9
config/locales/ru.yml
Normal file
9
config/locales/ru.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
ru:
|
||||
rate_label_rates: Платежи
|
||||
rate_label_rate: Платеж
|
||||
rate_label_rate_history: История платежей
|
||||
rate_label_new_rate: Новый платежe
|
||||
rate_label_currency: $
|
||||
rate_error_user_not_found: Пользователь не найден
|
||||
rate_label_set_rate: Установить платеж
|
||||
rate_label_default: Платеж по умолчанию
|
||||
4
config/routes.rb
Normal file
4
config/routes.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
ActionController::Routing::Routes.draw do |map|
|
||||
map.resources :rates
|
||||
map.connect 'rate_caches', :conditions => {:method => :put}, :controller => 'rate_caches', :action => 'update'
|
||||
end
|
||||
9
db/migrate/004_add_cost_to_time_entries.rb
Normal file
9
db/migrate/004_add_cost_to_time_entries.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
class AddCostToTimeEntries < ActiveRecord::Migration
|
||||
def self.up
|
||||
add_column :time_entries, :cost, :decimal, :precision => 15, :scale => 2
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :time_entries, :cost
|
||||
end
|
||||
end
|
||||
@@ -1,93 +0,0 @@
|
||||
# Commonly used webrat steps
|
||||
# http://github.com/brynary/webrat
|
||||
|
||||
When /^I press "(.*)"$/ do |button|
|
||||
click_button(button)
|
||||
end
|
||||
|
||||
When /^I follow "(.*)"$/ do |link|
|
||||
click_link(link)
|
||||
end
|
||||
|
||||
When /^I fill in "(.*)" with "(.*)"$/ do |field, value|
|
||||
fill_in(field, :with => value)
|
||||
end
|
||||
|
||||
When /^I select "(.*)" from "(.*)"$/ do |value, field|
|
||||
select(value, :from => field)
|
||||
end
|
||||
|
||||
# Use this step in conjunction with Rail's datetime_select helper. For example:
|
||||
# When I select "December 25, 2008 10:00" as the date and time
|
||||
When /^I select "(.*)" as the date and time$/ do |time|
|
||||
select_datetime(time)
|
||||
end
|
||||
|
||||
# Use this step when using multiple datetime_select helpers on a page or
|
||||
# you want to specify which datetime to select. Given the following view:
|
||||
# <%= f.label :preferred %><br />
|
||||
# <%= f.datetime_select :preferred %>
|
||||
# <%= f.label :alternative %><br />
|
||||
# <%= f.datetime_select :alternative %>
|
||||
# The following steps would fill out the form:
|
||||
# When I select "November 23, 2004 11:20" as the "Preferred" data and time
|
||||
# And I select "November 25, 2004 10:30" as the "Alternative" data and time
|
||||
When /^I select "(.*)" as the "(.*)" date and time$/ do |datetime, datetime_label|
|
||||
select_datetime(datetime, :from => datetime_label)
|
||||
end
|
||||
|
||||
# Use this step in conjuction with Rail's time_select helper. For example:
|
||||
# When I select "2:20PM" as the time
|
||||
# Note: Rail's default time helper provides 24-hour time-- not 12 hour time. Webrat
|
||||
# will convert the 2:20PM to 14:20 and then select it.
|
||||
When /^I select "(.*)" as the time$/ do |time|
|
||||
select_time(time)
|
||||
end
|
||||
|
||||
# Use this step when using multiple time_select helpers on a page or you want to
|
||||
# specify the name of the time on the form. For example:
|
||||
# When I select "7:30AM" as the "Gym" time
|
||||
When /^I select "(.*)" as the "(.*)" time$/ do |time, time_label|
|
||||
select_time(time, :from => time_label)
|
||||
end
|
||||
|
||||
# Use this step in conjuction with Rail's date_select helper. For example:
|
||||
# When I select "February 20, 1981" as the date
|
||||
When /^I select "(.*)" as the date$/ do |date|
|
||||
select_date(date)
|
||||
end
|
||||
|
||||
# Use this step when using multiple date_select helpers on one page or
|
||||
# you want to specify the name of the date on the form. For example:
|
||||
# When I select "April 26, 1982" as the "Date of Birth" date
|
||||
When /^I select "(.*)" as the "(.*)" date$/ do |date, date_label|
|
||||
select_date(date, :from => date_label)
|
||||
end
|
||||
|
||||
When /^I check "(.*)"$/ do |field|
|
||||
check(field)
|
||||
end
|
||||
|
||||
When /^I uncheck "(.*)"$/ do |field|
|
||||
uncheck(field)
|
||||
end
|
||||
|
||||
When /^I choose "(.*)"$/ do |field|
|
||||
choose(field)
|
||||
end
|
||||
|
||||
When /^I attach the file at "(.*)" to "(.*)" $/ do |path, field|
|
||||
attach_file(field, path)
|
||||
end
|
||||
|
||||
Then /^I should see "(.*)"$/ do |text|
|
||||
response.body.should =~ /#{text}/m
|
||||
end
|
||||
|
||||
Then /^I should not see "(.*)"$/ do |text|
|
||||
response.body.should_not =~ /#{text}/m
|
||||
end
|
||||
|
||||
Then /^the "(.*)" checkbox should be checked$/ do |label|
|
||||
field_labeled(label).should be_checked
|
||||
end
|
||||
@@ -1,22 +0,0 @@
|
||||
# Sets up the Rails environment for Cucumber
|
||||
ENV["RAILS_ENV"] = "test"
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../../../../../config/environment')
|
||||
require 'cucumber/rails/world'
|
||||
Cucumber::Rails.use_transactional_fixtures
|
||||
|
||||
require 'webrat/rails'
|
||||
|
||||
# Comment out the next two lines if you're not using RSpec's matchers (should / should_not) in your steps.
|
||||
require 'cucumber/rails/rspec'
|
||||
require 'webrat/rspec-rails'
|
||||
|
||||
require 'ruby-debug'
|
||||
|
||||
# require the entire app if we're running under coverage testing,
|
||||
# so we measure 0% covered files in the report
|
||||
#
|
||||
# http://www.pervasivecode.com/blog/2008/05/16/making-rcov-measure-your-whole-rails-app-even-if-tests-miss-entire-source-files/
|
||||
if defined?(Rcov)
|
||||
all_app_files = Dir.glob('{app,lib}/**/*.rb')
|
||||
all_app_files.each{|rb| require rb}
|
||||
end
|
||||
41
init.rb
41
init.rb
@@ -2,28 +2,49 @@ require 'redmine'
|
||||
|
||||
# Patches to the Redmine core
|
||||
require 'dispatcher'
|
||||
require 'rate_sort_helper_patch'
|
||||
require 'rate_time_entry_patch'
|
||||
require 'rate_users_helper_patch'
|
||||
|
||||
Dispatcher.to_prepare do
|
||||
SortHelper.send(:include, RateSortHelperPatch)
|
||||
Dispatcher.to_prepare :redmine_rate do
|
||||
gem 'lockfile'
|
||||
|
||||
require_dependency 'application_controller'
|
||||
ApplicationController.send(:include, RateHelper)
|
||||
ApplicationController.send(:helper, :rate)
|
||||
|
||||
require_dependency 'time_entry'
|
||||
TimeEntry.send(:include, RateTimeEntryPatch)
|
||||
UsersHelper.send(:include, RateUsersHelperPatch)
|
||||
|
||||
require_dependency 'users_helper'
|
||||
UsersHelper.send(:include, RateUsersHelperPatch) unless UsersHelper.included_modules.include?(RateUsersHelperPatch)
|
||||
end
|
||||
|
||||
# Hooks
|
||||
require 'rate_project_hook'
|
||||
require 'rate_memberships_hook'
|
||||
|
||||
Redmine::Plugin.register :redmine_rate do
|
||||
name 'Rate Plugin'
|
||||
name 'Rate'
|
||||
author 'Eric Davis'
|
||||
url 'https://projects.littlestreamsoftware.com/projects/show/redmine-rate'
|
||||
url 'https://projects.littlestreamsoftware.com/projects/redmine-rate'
|
||||
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."
|
||||
version '0.1.0'
|
||||
version '0.2.1'
|
||||
|
||||
requires_redmine :version_or_higher => '0.8.0'
|
||||
requires_redmine :version_or_higher => '1.0.0'
|
||||
|
||||
# These settings are set automatically when caching
|
||||
settings(:default => {
|
||||
'last_caching_run' => nil
|
||||
})
|
||||
|
||||
permission :view_rate, { }
|
||||
|
||||
menu :admin_menu, :rate_caches, { :controller => 'rate_caches', :action => 'index'}, :caption => :text_rate_caches_panel
|
||||
end
|
||||
|
||||
require 'redmine_rate/hooks/timesheet_hook_helper'
|
||||
require 'redmine_rate/hooks/plugin_timesheet_views_timesheets_time_entry_row_class_hook'
|
||||
require 'redmine_rate/hooks/plugin_timesheet_views_timesheet_group_header_hook'
|
||||
require 'redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_hook'
|
||||
require 'redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_sum_hook'
|
||||
require 'redmine_rate/hooks/plugin_timesheet_view_timesheets_report_header_tags_hook'
|
||||
require 'redmine_rate/hooks/view_layouts_base_html_head_hook'
|
||||
|
||||
9
lang/de.yml
Normal file
9
lang/de.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
# English strings go here
|
||||
rate_label_rates: Betraege
|
||||
rate_label_rate: Betrag
|
||||
rate_label_rate_history: Betragsverlauf
|
||||
rate_label_new_rate: Neuer Betrag
|
||||
rate_label_currency: EUR
|
||||
rate_error_user_not_found: Benutzer nicht gefunden
|
||||
rate_label_set_rate: Betrag setzen
|
||||
rate_label_default: Standard Betrag
|
||||
8
lang/fr.yml
Normal file
8
lang/fr.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
rate_label_rates: Tarifs
|
||||
rate_label_rate: Tarif
|
||||
rate_label_rate_history: Historique du tarif
|
||||
rate_label_new_rate: Nouveau tarif
|
||||
rate_label_currency: €
|
||||
rate_error_user_not_found: Utilisateur introuvable
|
||||
rate_label_set_rate: Parametrage du tarif
|
||||
rate_label_default: Tarif par défaut
|
||||
15
lib/rate_memberships_hook.rb
Normal file
15
lib/rate_memberships_hook.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
class RateMembershipsHook < Redmine::Hook::ViewListener
|
||||
def view_users_memberships_table_header(context={})
|
||||
return content_tag(:th, l(:rate_label_rate) + ' ' + l(:rate_label_currency))
|
||||
end
|
||||
|
||||
def view_users_memberships_table_row(context={})
|
||||
return context[:controller].send(:render_to_string, {
|
||||
:partial => 'users/membership_rate',
|
||||
:locals => {
|
||||
:membership => context[:membership],
|
||||
:user => context[:user]
|
||||
}})
|
||||
|
||||
end
|
||||
end
|
||||
@@ -28,7 +28,13 @@ class RateProjectHook < Redmine::Hook::ViewListener
|
||||
|
||||
return '' unless (User.current.allowed_to?(:view_rate, project) || User.current.admin?)
|
||||
|
||||
if Object.const_defined? 'Group' # 0.8.x compatibility
|
||||
# Groups cannot have a rate
|
||||
return content_tag(:td,'') if member.principal.is_a? Group
|
||||
rate = Rate.for(member.principal, project)
|
||||
else
|
||||
rate = Rate.for(member.user, project)
|
||||
end
|
||||
|
||||
content = ''
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
require_dependency 'time_entry'
|
||||
|
||||
module RateTimeEntryPatch
|
||||
def self.included(base) # :nodoc:
|
||||
base.extend(ClassMethods)
|
||||
@@ -11,28 +9,71 @@ module RateTimeEntryPatch
|
||||
unloadable # Send unloadable so it will not be unloaded in development
|
||||
belongs_to :rate
|
||||
|
||||
before_save :recalculate_cost
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Updated the cached cost of all TimeEntries for user and project
|
||||
def update_cost_cache(user, project=nil)
|
||||
c = ARCondition.new
|
||||
c << ["#{TimeEntry.table_name}.user_id = ?", user]
|
||||
c << ["#{TimeEntry.table_name}.project_id = ?", project] if project
|
||||
|
||||
TimeEntry.all(:conditions => c.conditions).each do |time_entry|
|
||||
time_entry.save_cached_cost
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
# Returns the current cost of the TimeEntry based on it's rate and hours
|
||||
def cost
|
||||
#
|
||||
# Is a read-through cache method
|
||||
def cost(options={})
|
||||
store_to_db = options[:store] || false
|
||||
|
||||
unless read_attribute(:cost)
|
||||
if self.rate.nil?
|
||||
amount = Rate.amount_for(self.user, self.project, self.spent_on.to_s)
|
||||
else
|
||||
amount = rate.amount
|
||||
end
|
||||
|
||||
return 0.0 if amount.nil?
|
||||
|
||||
return amount.to_f * hours.to_f
|
||||
if amount.nil?
|
||||
write_attribute(:cost, 0.0)
|
||||
else
|
||||
if store_to_db
|
||||
# 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
|
||||
|
||||
read_attribute(:cost)
|
||||
end
|
||||
|
||||
def clear_cost_cache
|
||||
write_attribute(:cost, nil)
|
||||
end
|
||||
|
||||
def save_cached_cost
|
||||
clear_cost_cache
|
||||
update_attribute(:cost, cost)
|
||||
end
|
||||
|
||||
def recalculate_cost
|
||||
clear_cost_cache
|
||||
cost(:store => false)
|
||||
true # for callback
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
require_dependency 'users_helper'
|
||||
|
||||
module RateUsersHelperPatch
|
||||
def self.included(base) # :nodoc:
|
||||
base.send(:include, InstanceMethods)
|
||||
|
||||
base.class_eval do
|
||||
alias_method_chain :user_settings_tabs, :rate_tab
|
||||
end
|
||||
@@ -22,10 +19,14 @@ module RateUsersHelperPatch
|
||||
options = content_tag('option', "--- #{l(:rate_label_default)} ---", :value => '')
|
||||
projects_by_root = projects.group_by(&:root)
|
||||
projects_by_root.keys.sort.each do |root|
|
||||
options << content_tag('option', h(root.name), :value => root.id, :disabled => (!projects.include?(root)), :selected => root == selected)
|
||||
root_selected = (root == selected) ? 'selected' : nil
|
||||
|
||||
options << content_tag('option', h(root.name), :value => root.id, :disabled => (!projects.include?(root)), :selected => root_selected)
|
||||
projects_by_root[root].sort.each do |project|
|
||||
next if project == root
|
||||
options << content_tag('option', '» ' + h(project.name), :value => project.id, :selected => project == selected)
|
||||
child_selected = (project == selected) ? 'selected' : nil
|
||||
|
||||
options << content_tag('option', '» ' + h(project.name), :value => project.id, :selected => child_selected)
|
||||
end
|
||||
end
|
||||
options
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
module RedmineRate
|
||||
module Hooks
|
||||
class PluginTimesheetViewTimesheetsReportHeaderTagsHook < Redmine::Hook::ViewListener
|
||||
def plugin_timesheet_view_timesheets_report_header_tags(context={})
|
||||
return content_tag(:style,
|
||||
'tr.missing-rate td.cost { color: red; }',
|
||||
:type => 'text/css')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
module RedmineRate
|
||||
module Hooks
|
||||
class PluginTimesheetViewsTimesheetGroupHeaderHook < Redmine::Hook::ViewListener
|
||||
def plugin_timesheet_views_timesheet_group_header(context={})
|
||||
return content_tag(:th, l(:rate_cost), :width => '8%')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
module RedmineRate
|
||||
module Hooks
|
||||
class PluginTimesheetViewsTimesheetTimeEntryHook < Redmine::Hook::ViewListener
|
||||
include TimesheetHookHelper
|
||||
|
||||
def plugin_timesheet_views_timesheet_time_entry(context={})
|
||||
cost = cost_item(context[:time_entry])
|
||||
if cost
|
||||
td_cell(number_to_currency(cost))
|
||||
else
|
||||
td_cell(' ')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
module RedmineRate
|
||||
module Hooks
|
||||
class PluginTimesheetViewsTimesheetTimeEntrySumHook < Redmine::Hook::ViewListener
|
||||
include TimesheetHookHelper
|
||||
|
||||
def plugin_timesheet_views_timesheet_time_entry_sum(context={})
|
||||
time_entries = context[:time_entries]
|
||||
costs = time_entries.collect {|time_entry| cost_item(time_entry)}.compact.sum
|
||||
if costs >= 0
|
||||
return td_cell(number_to_currency(costs))
|
||||
else
|
||||
return td_cell(' ')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
module RedmineRate
|
||||
module Hooks
|
||||
class PluginTimesheetViewsTimesheetsTimeEntryRowClassHook < Redmine::Hook::ViewListener
|
||||
include TimesheetHookHelper
|
||||
|
||||
def plugin_timesheet_views_timesheets_time_entry_row_class(context={})
|
||||
time_entry = context[:time_entry]
|
||||
return "" unless time_entry
|
||||
|
||||
cost = cost_item(time_entry)
|
||||
return "" unless cost # Permissions
|
||||
|
||||
if cost && cost <= 0
|
||||
return "missing-rate"
|
||||
else
|
||||
return ""
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
14
lib/redmine_rate/hooks/timesheet_hook_helper.rb
Normal file
14
lib/redmine_rate/hooks/timesheet_hook_helper.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
module TimesheetHookHelper
|
||||
# Returns the cost of a time entry, checking user permissions
|
||||
def cost_item(time_entry)
|
||||
if User.current.logged? && (User.current.allowed_to?(:view_rate, time_entry.project) || User.current.admin?)
|
||||
return time_entry.cost
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
def td_cell(html)
|
||||
return content_tag(:td, html, :align => 'right', :class => 'cost')
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
module RedmineRate
|
||||
module Hooks
|
||||
class ViewLayoutsBaseHtmlHeadHook < Redmine::Hook::ViewListener
|
||||
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')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
13
lib/tasks/cache.rake
Normal file
13
lib/tasks/cache.rake
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace :rate_plugin do
|
||||
namespace :cache do
|
||||
desc "Update Time Entry cost caches for Time Entries without a cost"
|
||||
task :update_cost_cache => :environment do
|
||||
Rate.update_all_time_entries_with_missing_cost
|
||||
end
|
||||
|
||||
desc "Clear and update all Time Entry cost caches"
|
||||
task :refresh_cost_cache => :environment do
|
||||
Rate.update_all_time_entries_to_refresh_cache
|
||||
end
|
||||
end
|
||||
end
|
||||
1
rails/init.rb
Normal file
1
rails/init.rb
Normal file
@@ -0,0 +1 @@
|
||||
require File.dirname(__FILE__) + "/../init"
|
||||
110
redmine_rate.gemspec
Normal file
110
redmine_rate.gemspec
Normal file
@@ -0,0 +1,110 @@
|
||||
# Generated by jeweler
|
||||
# DO NOT EDIT THIS FILE DIRECTLY
|
||||
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = %q{redmine_rate}
|
||||
s.version = "0.2.1"
|
||||
|
||||
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
||||
s.authors = ["Eric Davis"]
|
||||
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.email = %q{edavis@littlestreamsoftware.com}
|
||||
s.extra_rdoc_files = [
|
||||
"README.rdoc"
|
||||
]
|
||||
s.files = [
|
||||
"COPYRIGHT.txt",
|
||||
"CREDITS.txt",
|
||||
"GPL.txt",
|
||||
"README.rdoc",
|
||||
"Rakefile",
|
||||
"VERSION",
|
||||
"app/controllers/rate_caches_controller.rb",
|
||||
"app/controllers/rates_controller.rb",
|
||||
"app/models/rate.rb",
|
||||
"app/views/rate_caches/index.html.erb",
|
||||
"app/views/rates/_form.html.erb",
|
||||
"app/views/rates/_list.html.erb",
|
||||
"app/views/rates/create.js.rjs",
|
||||
"app/views/rates/create_error.js.rjs",
|
||||
"app/views/rates/edit.html.erb",
|
||||
"app/views/rates/index.html.erb",
|
||||
"app/views/rates/new.html.erb",
|
||||
"app/views/rates/show.html.erb",
|
||||
"app/views/users/_membership_rate.html.erb",
|
||||
"app/views/users/_rates.html.erb",
|
||||
"assets/images/database_refresh.png",
|
||||
"config/locales/de.yml",
|
||||
"config/locales/en.yml",
|
||||
"config/locales/fr.yml",
|
||||
"config/locales/ru.yml",
|
||||
"config/routes.rb",
|
||||
"init.rb",
|
||||
"lang/de.yml",
|
||||
"lang/en.yml",
|
||||
"lang/fr.yml",
|
||||
"lib/rate_conversion.rb",
|
||||
"lib/rate_memberships_hook.rb",
|
||||
"lib/rate_project_hook.rb",
|
||||
"lib/rate_sort_helper_patch.rb",
|
||||
"lib/rate_time_entry_patch.rb",
|
||||
"lib/rate_users_helper_patch.rb",
|
||||
"lib/redmine_rate/hooks/plugin_timesheet_view_timesheets_report_header_tags_hook.rb",
|
||||
"lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_group_header_hook.rb",
|
||||
"lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_hook.rb",
|
||||
"lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_sum_hook.rb",
|
||||
"lib/redmine_rate/hooks/plugin_timesheet_views_timesheets_time_entry_row_class_hook.rb",
|
||||
"lib/redmine_rate/hooks/timesheet_hook_helper.rb",
|
||||
"lib/redmine_rate/hooks/view_layouts_base_html_head_hook.rb",
|
||||
"lib/tasks/cache.rake",
|
||||
"lib/tasks/data.rake",
|
||||
"rails/init.rb",
|
||||
"test/functional/rates_controller_test.rb",
|
||||
"test/integration/admin_panel_test.rb",
|
||||
"test/integration/routing_test.rb",
|
||||
"test/test_helper.rb",
|
||||
"test/unit/lib/rate_time_entry_patch_test.rb",
|
||||
"test/unit/lib/rate_users_helper_patch_test.rb",
|
||||
"test/unit/lib/redmine_rate/hooks/plugin_timesheet_view_timesheets_report_header_tags_hook_test.rb",
|
||||
"test/unit/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_group_header_hook_test.rb",
|
||||
"test/unit/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_hook_test.rb",
|
||||
"test/unit/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_sum_hook_test.rb",
|
||||
"test/unit/lib/redmine_rate/hooks/plugin_timesheet_views_timesheets_time_entry_row_class_hook_test.rb",
|
||||
"test/unit/rate_for_test.rb",
|
||||
"test/unit/rate_test.rb"
|
||||
]
|
||||
s.homepage = %q{https://projects.littlestreamsoftware.com/projects/redmine-rate}
|
||||
s.rdoc_options = ["--charset=UTF-8"]
|
||||
s.require_paths = ["lib"]
|
||||
s.rubygems_version = %q{1.3.7}
|
||||
s.summary = %q{A Rate plugin for Redmine to store billing rate for user.}
|
||||
s.test_files = [
|
||||
"test/test_helper.rb",
|
||||
"test/integration/routing_test.rb",
|
||||
"test/integration/admin_panel_test.rb",
|
||||
"test/unit/rate_for_test.rb",
|
||||
"test/unit/lib/rate_time_entry_patch_test.rb",
|
||||
"test/unit/lib/rate_users_helper_patch_test.rb",
|
||||
"test/unit/lib/redmine_rate/hooks/plugin_timesheet_views_timesheets_time_entry_row_class_hook_test.rb",
|
||||
"test/unit/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_hook_test.rb",
|
||||
"test/unit/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_group_header_hook_test.rb",
|
||||
"test/unit/lib/redmine_rate/hooks/plugin_timesheet_views_timesheet_time_entry_sum_hook_test.rb",
|
||||
"test/unit/lib/redmine_rate/hooks/plugin_timesheet_view_timesheets_report_header_tags_hook_test.rb",
|
||||
"test/unit/rate_test.rb",
|
||||
"test/functional/rates_controller_test.rb"
|
||||
]
|
||||
|
||||
if s.respond_to? :specification_version then
|
||||
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
||||
s.specification_version = 3
|
||||
|
||||
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
||||
else
|
||||
end
|
||||
else
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,486 +0,0 @@
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
||||
|
||||
describe "unauthorized", :shared => true do
|
||||
it 'should not be successful' do
|
||||
do_action
|
||||
response.should_not be_success
|
||||
end
|
||||
|
||||
it 'should return a 403 status code' do
|
||||
do_action
|
||||
response.code.should eql("403")
|
||||
end
|
||||
|
||||
it 'should display the standard unauthorized page' do
|
||||
do_action
|
||||
response.should render_template('common/403')
|
||||
end
|
||||
|
||||
describe "with mime type of xml" do
|
||||
|
||||
it "should return a 403 error" do
|
||||
request.env["HTTP_ACCEPT"] = "application/xml"
|
||||
do_action
|
||||
response.response_code.should eql(403)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe RatesController, "as regular user" do
|
||||
integrate_views
|
||||
|
||||
def mock_rate(stubs={})
|
||||
@mock_rate ||= mock_model(Rate, stubs)
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
@user = mock_model(User, :logged? => true, :admin? => false, :anonymous? => false, :name => "Normal User", :memberships => [], :allowed_to? => true, :language => :en, :projects => Project)
|
||||
User.stub!(:current).and_return(@user)
|
||||
end
|
||||
|
||||
describe "responding to GET index" do
|
||||
|
||||
def do_action
|
||||
get :index
|
||||
end
|
||||
|
||||
it_should_behave_like "unauthorized"
|
||||
|
||||
end
|
||||
|
||||
describe "responding to GET show" do
|
||||
|
||||
def do_action
|
||||
get :show, :id => "37"
|
||||
end
|
||||
|
||||
it_should_behave_like "unauthorized"
|
||||
|
||||
end
|
||||
|
||||
describe "responding to GET new" do
|
||||
|
||||
def do_action
|
||||
get :new
|
||||
end
|
||||
|
||||
it_should_behave_like "unauthorized"
|
||||
|
||||
end
|
||||
|
||||
describe "responding to GET edit" do
|
||||
|
||||
def do_action
|
||||
get :edit, :id => "37"
|
||||
end
|
||||
|
||||
it_should_behave_like "unauthorized"
|
||||
|
||||
end
|
||||
|
||||
describe "responding to POST create" do
|
||||
|
||||
def do_action
|
||||
post :create, :rate => {:these => 'params'}
|
||||
end
|
||||
|
||||
it_should_behave_like "unauthorized"
|
||||
|
||||
end
|
||||
|
||||
describe "responding to PUT udpate" do
|
||||
|
||||
def do_action
|
||||
put :update, :id => "37", :rate => {:these => 'params'}
|
||||
end
|
||||
|
||||
it_should_behave_like "unauthorized"
|
||||
|
||||
end
|
||||
|
||||
describe "responding to DELETE destroy" do
|
||||
|
||||
def do_action
|
||||
delete :destroy, :id => "37"
|
||||
end
|
||||
|
||||
it_should_behave_like "unauthorized"
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe RatesController, "as an administrator" do
|
||||
integrate_views
|
||||
|
||||
def mock_rate(stubs={})
|
||||
project = mock_model(Project)
|
||||
stubs = {
|
||||
:date_in_effect => Date.today,
|
||||
:project => project,
|
||||
:project_id => project.id,
|
||||
:amount => 100.0,
|
||||
:user => @user,
|
||||
:user_id => @user.id,
|
||||
:unlocked? => true,
|
||||
:locked? => false
|
||||
}.merge(stubs)
|
||||
@mock_rate ||= mock_model(Rate, stubs)
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
@user = mock_model(User, :logged? => true, :admin? => true, :anonymous? => false, :name => "Admin User", :memberships => [], :allowed_to? => true, :language => :en, :projects => Project)
|
||||
User.stub!(:current).and_return(@user)
|
||||
controller.stub!(:find_current_user).and_return(@user)
|
||||
end
|
||||
|
||||
describe "responding to GET index" do
|
||||
|
||||
it "should redirect to the homepage" do
|
||||
get :index
|
||||
response.should redirect_to(home_url)
|
||||
end
|
||||
|
||||
it "should display an error flash message" do
|
||||
get :index
|
||||
flash[:error].should_not be_nil
|
||||
end
|
||||
|
||||
describe "with mime type of xml" do
|
||||
|
||||
it "should return a 404 error" do
|
||||
request.env["HTTP_ACCEPT"] = "application/xml"
|
||||
get :index
|
||||
response.response_code.should eql(404)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "responding to GET index with user" do
|
||||
before(:each) do
|
||||
User.stub!(:find).with(@user.id.to_s).and_return(@user)
|
||||
@default_sort = "#{Rate.table_name}.date_in_effect desc"
|
||||
controller.stub!(:sort_clause).and_return(@default_sort)
|
||||
end
|
||||
|
||||
it "should expose all historic rates for the user as @rates" do
|
||||
Rate.should_receive(:history_for_user).with(@user, @default_sort).and_return([mock_rate])
|
||||
get :index, :user_id => @user.id
|
||||
assigns[:rates].should == [mock_rate]
|
||||
end
|
||||
|
||||
describe "with mime type of xml" do
|
||||
|
||||
it "should render all rates as xml" do
|
||||
request.env["HTTP_ACCEPT"] = "application/xml"
|
||||
Rate.should_receive(:history_for_user).with(@user, @default_sort).and_return(rates = mock("Array of Rates"))
|
||||
rates.should_receive(:to_xml).and_return("generated XML")
|
||||
get :index, :user_id => @user.id
|
||||
response.body.should == "generated XML"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "responding to GET show" do
|
||||
|
||||
it "should expose the requested rate as @rate" do
|
||||
Rate.should_receive(:find).with("37").and_return(mock_rate)
|
||||
get :show, :id => "37"
|
||||
assigns[:rate].should equal(mock_rate)
|
||||
end
|
||||
|
||||
describe "with mime type of xml" do
|
||||
|
||||
it "should render the requested rate as xml" do
|
||||
request.env["HTTP_ACCEPT"] = "application/xml"
|
||||
Rate.should_receive(:find).with("37").and_return(mock_rate)
|
||||
mock_rate.should_receive(:to_xml).and_return("generated XML")
|
||||
get :show, :id => "37"
|
||||
response.body.should == "generated XML"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "responding to GET new" do
|
||||
|
||||
it "should redirect to the homepage" do
|
||||
get :new
|
||||
response.should redirect_to(home_url)
|
||||
end
|
||||
|
||||
it "should display an error flash message" do
|
||||
get :new
|
||||
flash[:error].should_not be_nil
|
||||
end
|
||||
|
||||
describe "with mime type of xml" do
|
||||
|
||||
it "should return a 404 error" do
|
||||
request.env["HTTP_ACCEPT"] = "application/xml"
|
||||
get :new
|
||||
response.response_code.should eql(404)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
describe "responding to GET new with user" do
|
||||
before(:each) do
|
||||
@rate = mock_rate(:user_id => @user.id)
|
||||
User.stub!(:find).with(@user.id.to_s).and_return(@user)
|
||||
Rate.stub!(:new).and_return(@rate)
|
||||
end
|
||||
|
||||
it 'should be successful' do
|
||||
get :new, :user_id => @user.id
|
||||
response.should be_success
|
||||
end
|
||||
|
||||
it "should expose a new rate as @rate" do
|
||||
get :new, :user_id => @user.id
|
||||
assigns[:rate].should equal(@rate)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "responding to GET edit" do
|
||||
|
||||
it "should expose the requested rate as @rate" do
|
||||
Rate.should_receive(:find).with("37").and_return(mock_rate)
|
||||
get :edit, :id => "37"
|
||||
assigns[:rate].should equal(mock_rate)
|
||||
end
|
||||
|
||||
describe "on a locked rate" do
|
||||
it 'should not have a Update button' do
|
||||
Rate.should_receive(:find).with("37").and_return(mock_rate(:unlocked? => false))
|
||||
get :edit, :id => "37"
|
||||
response.should_not have_tag("input[type=submit]")
|
||||
|
||||
end
|
||||
|
||||
it 'should show the locked icon' do
|
||||
Rate.should_receive(:find).with("37").and_return(mock_rate(:unlocked? => false))
|
||||
get :edit, :id => "37"
|
||||
response.should have_tag("img[src*=locked.png]")
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "responding to POST create" do
|
||||
|
||||
describe "with valid params" do
|
||||
|
||||
it "should expose a newly created rate as @rate" do
|
||||
Rate.should_receive(:new).with({'these' => 'params'}).and_return(mock_rate(:save => true))
|
||||
post :create, :rate => {:these => 'params'}
|
||||
assigns(:rate).should equal(mock_rate)
|
||||
end
|
||||
|
||||
it "should redirect to the rate list" do
|
||||
Rate.stub!(:new).and_return(mock_rate(:save => true))
|
||||
post :create, :rate => {}
|
||||
response.should redirect_to(rates_url(:user_id => @user.id))
|
||||
end
|
||||
|
||||
it 'should redirect to the back_url if set' do
|
||||
back_url = '/rates'
|
||||
Rate.stub!(:new).and_return(mock_rate(:save => true))
|
||||
post :create, :rate => {}, :back_url => back_url
|
||||
response.should redirect_to(back_url)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "with invalid params" do
|
||||
|
||||
it "should expose a newly created but unsaved rate as @rate" do
|
||||
Rate.stub!(:new).with({'these' => 'params'}).and_return(mock_rate(:save => false))
|
||||
post :create, :rate => {:these => 'params'}
|
||||
assigns(:rate).should equal(mock_rate)
|
||||
end
|
||||
|
||||
it "should re-render the 'new' template" do
|
||||
Rate.stub!(:new).and_return(mock_rate(:save => false))
|
||||
post :create, :rate => {}
|
||||
response.should render_template('new')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "responding to PUT udpate" do
|
||||
|
||||
describe "with valid params" do
|
||||
|
||||
it "should update the requested rate" do
|
||||
Rate.should_receive(:find).with("37").and_return(mock_rate)
|
||||
mock_rate.should_receive(:update_attributes).with({'these' => 'params'})
|
||||
put :update, :id => "37", :rate => {:these => 'params'}
|
||||
end
|
||||
|
||||
it "should expose the requested rate as @rate" do
|
||||
Rate.stub!(:find).and_return(mock_rate(:update_attributes => true))
|
||||
put :update, :id => "1"
|
||||
assigns(:rate).should equal(mock_rate)
|
||||
end
|
||||
|
||||
it "should redirect to the rate list" do
|
||||
Rate.stub!(:find).and_return(mock_rate(:update_attributes => true))
|
||||
put :update, :id => "1"
|
||||
response.should redirect_to(rates_url(:user_id => @user.id))
|
||||
end
|
||||
|
||||
it 'should redirect to the back_url if set' do
|
||||
back_url = '/rates'
|
||||
Rate.stub!(:find).and_return(mock_rate(:update_attributes => true))
|
||||
put :update, :id => "1", :back_url => back_url
|
||||
response.should redirect_to(back_url)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "with invalid params" do
|
||||
|
||||
it "should update the requested rate" do
|
||||
Rate.should_receive(:find).with("37").and_return(mock_rate)
|
||||
mock_rate.should_receive(:update_attributes).with({'these' => 'params'})
|
||||
put :update, :id => "37", :rate => {:these => 'params'}
|
||||
end
|
||||
|
||||
it "should expose the rate as @rate" do
|
||||
Rate.stub!(:find).and_return(mock_rate(:update_attributes => false))
|
||||
put :update, :id => "1"
|
||||
assigns(:rate).should equal(mock_rate)
|
||||
end
|
||||
|
||||
it "should re-render the 'edit' template" do
|
||||
Rate.stub!(:find).and_return(mock_rate(:update_attributes => false))
|
||||
put :update, :id => "1"
|
||||
response.should render_template('edit')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "on a locked rate" do
|
||||
def mock_locked_rate(stubs = { })
|
||||
mock_rate(stubs.merge(:locked? => true,
|
||||
:unlocked? => false,
|
||||
:update_attributes => false,
|
||||
:reload => nil
|
||||
))
|
||||
end
|
||||
|
||||
it "should try to update the requested rate" do
|
||||
Rate.should_receive(:find).with("37").and_return(mock_locked_rate)
|
||||
mock_locked_rate.should_receive(:update_attributes).with({'these' => 'params'})
|
||||
put :update, :id => "37", :rate => {:these => 'params'}
|
||||
end
|
||||
|
||||
it "should not save the rate" do
|
||||
Rate.should_receive(:find).with("37").and_return(mock_locked_rate)
|
||||
mock_locked_rate.should_receive(:update_attributes).and_return(false)
|
||||
put :update, :id => "37", :rate => {:these => 'params'}
|
||||
end
|
||||
|
||||
it "should reload the locked rate as @rate" do
|
||||
Rate.stub!(:find).and_return(mock_locked_rate(:id => 37))
|
||||
mock_locked_rate.should_receive(:reload).and_return(mock_locked_rate(:id => 37))
|
||||
put :update, :id => "37", :rate => { :amount => 200.0 }
|
||||
assigns(:rate).should equal(mock_locked_rate)
|
||||
end
|
||||
|
||||
it "should re-render the 'edit' template" do
|
||||
Rate.stub!(:find).and_return(mock_locked_rate)
|
||||
put :update, :id => "1"
|
||||
response.should render_template('edit')
|
||||
end
|
||||
|
||||
it "should render an error message" do
|
||||
Rate.stub!(:find).and_return(mock_locked_rate)
|
||||
put :update, :id => "1"
|
||||
flash[:error].should match(/locked/)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "responding to DELETE destroy" do
|
||||
|
||||
it "should destroy the requested rate" do
|
||||
Rate.should_receive(:find).with("37").and_return(mock_rate)
|
||||
mock_rate.should_receive(:destroy)
|
||||
delete :destroy, :id => "37"
|
||||
end
|
||||
|
||||
it "should redirect to the user's rates list" do
|
||||
Rate.stub!(:find).and_return(mock_rate(:destroy => true))
|
||||
delete :destroy, :id => "1"
|
||||
response.should redirect_to(rates_url(:user_id => @user.id))
|
||||
end
|
||||
|
||||
it 'should redirect to the back_url if set' do
|
||||
back_url = '/rates'
|
||||
Rate.stub!(:find).and_return(mock_rate(:destroy => true))
|
||||
delete :destroy, :id => "1", :back_url => back_url
|
||||
response.should redirect_to(back_url)
|
||||
end
|
||||
|
||||
describe "on a locked rate" do
|
||||
it "should display an error message" do
|
||||
Rate.stub!(:find).and_return(mock_rate(:destroy => false, :locked? => true))
|
||||
delete :destroy, :id => "1"
|
||||
flash[:error].should match(/locked/)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "set_back_url (private)" do
|
||||
it "should set the back_url based on the params" do
|
||||
controller.params = { :back_url => '/back' }
|
||||
controller.send(:set_back_url).should eql('/back')
|
||||
end
|
||||
end
|
||||
|
||||
describe "redirect_back_or_default (private)" do
|
||||
before(:each) do
|
||||
@default_url = "/default_response"
|
||||
end
|
||||
|
||||
it "should allow redirecting back to the user's edit panel" do
|
||||
allowed = "/users/edit"
|
||||
controller.should_receive(:redirect_to).with(allowed).and_return(true)
|
||||
controller.params = { :back_url => allowed }
|
||||
controller.send(:redirect_back_or_default, @default_url)
|
||||
end
|
||||
|
||||
it "should allow redirecting back to /rates" do
|
||||
controller.should_receive(:redirect_to).with("/rates").and_return(true)
|
||||
controller.params = { :back_url => '/rates' }
|
||||
controller.send(:redirect_back_or_default, @default_url)
|
||||
end
|
||||
|
||||
it "should not allow redirecting elsewhere" do
|
||||
controller.should_receive(:redirect_to).with(@default_url).and_return(true)
|
||||
controller.params = { :back_url => '/back' }
|
||||
controller.send(:redirect_back_or_default, @default_url)
|
||||
end
|
||||
|
||||
it "should not allow redirecting to an invalid uri" do
|
||||
controller.should_receive(:redirect_to).with(@default_url).and_return(true)
|
||||
controller.params = { :back_url => 'http://' }
|
||||
controller.send(:redirect_back_or_default, @default_url)
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,59 +0,0 @@
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
||||
|
||||
describe RatesController do
|
||||
describe "route generation" do
|
||||
it "should map #index" do
|
||||
route_for(:controller => "rates", :action => "index").should == "/rates"
|
||||
end
|
||||
|
||||
it "should map #new" do
|
||||
route_for(:controller => "rates", :action => "new").should == "/rates/new"
|
||||
end
|
||||
|
||||
it "should map #show" do
|
||||
route_for(:controller => "rates", :action => "show", :id => 1).should == "/rates/1"
|
||||
end
|
||||
|
||||
it "should map #edit" do
|
||||
route_for(:controller => "rates", :action => "edit", :id => 1).should == "/rates/1/edit"
|
||||
end
|
||||
|
||||
it "should map #update" do
|
||||
route_for(:controller => "rates", :action => "update", :id => 1).should == "/rates/1"
|
||||
end
|
||||
|
||||
it "should map #destroy" do
|
||||
route_for(:controller => "rates", :action => "destroy", :id => 1).should == "/rates/1"
|
||||
end
|
||||
end
|
||||
|
||||
describe "route recognition" do
|
||||
it "should generate params for #index" do
|
||||
params_from(:get, "/rates").should == {:controller => "rates", :action => "index"}
|
||||
end
|
||||
|
||||
it "should generate params for #new" do
|
||||
params_from(:get, "/rates/new").should == {:controller => "rates", :action => "new"}
|
||||
end
|
||||
|
||||
it "should generate params for #create" do
|
||||
params_from(:post, "/rates").should == {:controller => "rates", :action => "create"}
|
||||
end
|
||||
|
||||
it "should generate params for #show" do
|
||||
params_from(:get, "/rates/1").should == {:controller => "rates", :action => "show", :id => "1"}
|
||||
end
|
||||
|
||||
it "should generate params for #edit" do
|
||||
params_from(:get, "/rates/1/edit").should == {:controller => "rates", :action => "edit", :id => "1"}
|
||||
end
|
||||
|
||||
it "should generate params for #update" do
|
||||
params_from(:put, "/rates/1").should == {:controller => "rates", :action => "update", :id => "1"}
|
||||
end
|
||||
|
||||
it "should generate params for #destroy" do
|
||||
params_from(:delete, "/rates/1").should == {:controller => "rates", :action => "destroy", :id => "1"}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,29 +0,0 @@
|
||||
require File.dirname(__FILE__) + '/../spec_helper'
|
||||
|
||||
describe TimeEntry, 'cost' do
|
||||
before(:each) do
|
||||
@user = mock_model(User)
|
||||
@project = mock_model(Project)
|
||||
@date = Date.today.to_s
|
||||
@time_entry = TimeEntry.new({:user => @user, :project => @project, :spent_on => @date, :hours => 10.0})
|
||||
end
|
||||
|
||||
it 'should return 0.0 if there are no rates for the user' do
|
||||
Rate.should_receive(:amount_for).with(@user, @project, @date).and_return(nil)
|
||||
@time_entry.cost.should eql(0.0)
|
||||
end
|
||||
|
||||
describe 'should return the product of hours by' do
|
||||
it 'the results of Rate.amount_for' do
|
||||
Rate.should_receive(:amount_for).with(@user, @project, @date).and_return(200.0)
|
||||
@time_entry.cost.should eql(200.0 * @time_entry.hours)
|
||||
end
|
||||
|
||||
it 'the assigned rate' do
|
||||
rate = mock_model(Rate, :amount => 100.0)
|
||||
@time_entry.should_receive(:rate).at_least(:twice).and_return(rate)
|
||||
@time_entry.cost.should eql(rate.amount * @time_entry.hours)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,38 +0,0 @@
|
||||
require File.dirname(__FILE__) + '/../spec_helper'
|
||||
|
||||
class UsersHelperWrapper
|
||||
include UsersHelper
|
||||
end
|
||||
|
||||
describe UsersHelper, 'user_settings' do
|
||||
it 'should return 3 tabs' do
|
||||
helper = UsersHelperWrapper.new
|
||||
helper.user_settings_tabs.should have(3).things
|
||||
|
||||
end
|
||||
|
||||
it 'should include a rate tab at the end' do
|
||||
helper = UsersHelperWrapper.new
|
||||
rate_tab = helper.user_settings_tabs[-1]
|
||||
rate_tab.should_not be_nil
|
||||
end
|
||||
|
||||
describe 'rate tab' do
|
||||
before(:each) do
|
||||
helper = UsersHelperWrapper.new
|
||||
@rate_tab = helper.user_settings_tabs[-1]
|
||||
end
|
||||
|
||||
it 'should have the name of "rates"' do
|
||||
@rate_tab[:name].should eql('rates')
|
||||
end
|
||||
|
||||
it 'should use the rates partial' do
|
||||
@rate_tab[:partial].should eql('users/rates')
|
||||
end
|
||||
|
||||
it 'should use the i18n rates label' do
|
||||
@rate_tab[:label].should eql(:rate_label_rate_history)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,76 +0,0 @@
|
||||
require File.dirname(__FILE__) + '/../spec_helper'
|
||||
|
||||
describe Rate, 'calculated for' do
|
||||
before(:each) do
|
||||
@user = User.new(:mail => 'metest@example.com', :lastname => 'Test', :firstname => 'Mr')
|
||||
@user.login = 'mr-test'
|
||||
@user.save!
|
||||
end
|
||||
|
||||
after(:each) do
|
||||
User.destroy_all
|
||||
end
|
||||
|
||||
describe 'a user with no Rates' do
|
||||
it 'should return nil' do
|
||||
Rate.for(@user).should be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'a user with one default Rate' do
|
||||
it 'should return the Rate if the Rate is effective today' do
|
||||
rate = Rate.create!({ :user_id => @user.id, :amount => 100.0, :date_in_effect => Date.today})
|
||||
Rate.for(@user).should eql(rate)
|
||||
end
|
||||
|
||||
it 'should return nil if the Rate is not effective yet' do
|
||||
Rate.for(@user).should be_nil
|
||||
end
|
||||
|
||||
it 'should return the same default Rate on all projects' do
|
||||
project = mock_model(Project)
|
||||
rate = Rate.create!({ :user_id => @user.id, :amount => 100.0, :date_in_effect => Date.today})
|
||||
Rate.for(@user, project).should eql(rate)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'a user with two default Rates' do
|
||||
it 'should return the newest Rate before the todays date' do
|
||||
rate = Rate.create!({ :user_id => @user.id, :amount => 100.0, :date_in_effect => Date.yesterday})
|
||||
rate2 = Rate.create!({ :user_id => @user.id, :amount => 300.0, :date_in_effect => Date.today})
|
||||
Rate.for(@user).should eql(rate2)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'a user with a default Rate and Rate on a project' do
|
||||
it 'should return the project Rate if its effective today' do
|
||||
project = mock_model(Project)
|
||||
rate = Rate.create!({ :user_id => @user.id, :project => project, :amount => 100.0, :date_in_effect => Date.yesterday})
|
||||
rate2 = Rate.create!({ :user_id => @user.id, :amount => 300.0, :date_in_effect => Date.today})
|
||||
Rate.for(@user, project).should eql(rate)
|
||||
end
|
||||
|
||||
it 'should return the default Rate if the project Rate isnt effective yet but the default Rate is' do
|
||||
project = mock_model(Project)
|
||||
rate = Rate.create!({ :user_id => @user.id, :project => project, :amount => 100.0, :date_in_effect => Date.tomorrow})
|
||||
rate2 = Rate.create!({ :user_id => @user.id, :amount => 300.0, :date_in_effect => Date.today})
|
||||
Rate.for(@user, project).should eql(rate2)
|
||||
end
|
||||
|
||||
it 'should return nil if neither Rate is effective yet' do
|
||||
project = mock_model(Project)
|
||||
rate = Rate.create!({ :user_id => @user.id, :project => project, :amount => 100.0, :date_in_effect => Date.tomorrow})
|
||||
rate2 = Rate.create!({ :user_id => @user.id, :amount => 300.0, :date_in_effect => Date.tomorrow})
|
||||
Rate.for(@user, project).should be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'a user with two Rates on a project' do
|
||||
it 'should return the newest Rate before the todays date' do
|
||||
project = mock_model(Project)
|
||||
rate = Rate.create!({ :user_id => @user.id, :project => project, :amount => 100.0, :date_in_effect => Date.yesterday})
|
||||
rate2 = Rate.create!({ :user_id => @user.id, :project => project, :amount => 300.0, :date_in_effect => Date.today})
|
||||
Rate.for(@user, project).should eql(rate2)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,298 +0,0 @@
|
||||
require File.dirname(__FILE__) + '/../spec_helper'
|
||||
|
||||
module RateSpecHelper
|
||||
def rate_valid_attributes
|
||||
{
|
||||
:user => mock_model(User),
|
||||
:project => mock_model(Project),
|
||||
:date_in_effect => Date.new(Date.today.year, 1, 1),
|
||||
:amount => 100.50
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe Rate do
|
||||
include RateSpecHelper
|
||||
|
||||
it 'should be valid with a user' do
|
||||
rate = Rate.new(rate_valid_attributes)
|
||||
rate.should be_valid
|
||||
end
|
||||
|
||||
it 'should be valid with a project' do
|
||||
rate = Rate.new(rate_valid_attributes)
|
||||
rate.should be_valid
|
||||
end
|
||||
|
||||
it 'should be valid without a project' do
|
||||
rate = Rate.new(rate_valid_attributes.except(:project))
|
||||
rate.should be_valid
|
||||
end
|
||||
|
||||
it 'should not be valid without a user' do
|
||||
rate = Rate.new(rate_valid_attributes.except(:user))
|
||||
rate.should_not be_valid
|
||||
rate.should have(1).error_on(:user_id)
|
||||
end
|
||||
|
||||
it 'should not be valid without a date_in_effect' do
|
||||
rate = Rate.new(rate_valid_attributes.except(:date_in_effect))
|
||||
rate.should_not be_valid
|
||||
rate.should have(1).error_on(:date_in_effect)
|
||||
end
|
||||
end
|
||||
|
||||
describe Rate, 'associations' do
|
||||
it 'should have many time entries' do
|
||||
Rate.should have_association(:time_entries, :has_many)
|
||||
end
|
||||
|
||||
it 'should belong to a single user' do
|
||||
Rate.should have_association(:user, :belongs_to)
|
||||
end
|
||||
|
||||
it 'should belong to a single project' do
|
||||
Rate.should have_association(:project, :belongs_to)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe Rate, 'locked?' do
|
||||
it 'should be true if a Time Entry is associated' do
|
||||
rate = Rate.new
|
||||
rate.time_entries << mock_model(TimeEntry)
|
||||
rate.locked?.should be_true
|
||||
end
|
||||
|
||||
it 'should be false if no Time Entries are associated' do
|
||||
rate = Rate.new
|
||||
rate.locked?.should be_false
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe Rate, 'locked?' do
|
||||
it 'should be false if a Time Entry is associated' do
|
||||
rate = Rate.new
|
||||
rate.time_entries << mock_model(TimeEntry)
|
||||
rate.unlocked?.should be_false
|
||||
end
|
||||
|
||||
it 'should be true if no Time Entries are associated' do
|
||||
rate = Rate.new
|
||||
rate.unlocked?.should be_true
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe Rate, 'save' do
|
||||
include RateSpecHelper
|
||||
|
||||
it 'should save if a Rate is unlocked' do
|
||||
rate = Rate.new(rate_valid_attributes)
|
||||
rate.stub!(:locked?).and_return(false)
|
||||
rate.save.should eql(true)
|
||||
end
|
||||
|
||||
it 'should not save if a Rate is locked' do
|
||||
rate = Rate.new(rate_valid_attributes)
|
||||
rate.stub!(:locked?).and_return(true)
|
||||
rate.save.should eql(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe Rate, 'destroy' do
|
||||
include RateSpecHelper
|
||||
|
||||
it 'should destroy the Rate if it is unlocked' do
|
||||
rate = Rate.create(rate_valid_attributes)
|
||||
rate.stub!(:locked?).and_return(false)
|
||||
proc {
|
||||
rate.destroy
|
||||
}.should change(Rate, :count).by(-1)
|
||||
|
||||
end
|
||||
|
||||
it 'should not destroy the Rate if it is locked' do
|
||||
rate = Rate.create(rate_valid_attributes)
|
||||
rate.stub!(:locked?).and_return(true)
|
||||
proc {
|
||||
rate.destroy
|
||||
}.should_not change(Rate, :count)
|
||||
end
|
||||
end
|
||||
|
||||
describe Rate, 'for' do
|
||||
before(:each) do
|
||||
@user = mock_model(User)
|
||||
@project = mock_model(Project)
|
||||
@date = '2009-01-01'
|
||||
@rate = mock_model(Rate, :amount => 50.50)
|
||||
end
|
||||
|
||||
describe 'parameters' do
|
||||
it 'should be passed user' do
|
||||
lambda {Rate.for}.should raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it 'can be passed an optional project' do
|
||||
lambda {Rate.for(@user)}.should_not raise_error(ArgumentError)
|
||||
lambda {Rate.for(@user, @project)}.should_not raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it 'can be passed an optional date string' do
|
||||
lambda {Rate.for(@user)}.should_not raise_error(ArgumentError)
|
||||
lambda {Rate.for(@user, nil, @date)}.should_not raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe 'returns' do
|
||||
it 'a Rate object when there is a rate' do
|
||||
Rate.stub!(:for_user_project_and_date).with(@user, @project, @date).and_return(@rate)
|
||||
Rate.for(@user, @project, @date).should eql(@rate)
|
||||
end
|
||||
|
||||
it 'a nil when there is no rate' do
|
||||
Rate.for(@user, @project, @date).should be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with a user, project, and date' do
|
||||
it 'should find the rate for a user on the project before the date' do
|
||||
Rate.should_receive(:for_user_project_and_date).with(@user, @project, @date).and_return(@rate)
|
||||
Rate.for(@user, @project, @date)
|
||||
end
|
||||
|
||||
it 'should return the most recent rate found' do
|
||||
Rate.should_receive(:for_user_project_and_date).with(@user, @project, @date).and_return(@rate)
|
||||
Rate.for(@user, @project, @date).should eql(@rate)
|
||||
end
|
||||
|
||||
it 'should check for a default rate if no rate is found' do
|
||||
Rate.should_receive(:for_user_project_and_date).with(@user, @project, @date).and_return(nil)
|
||||
Rate.should_receive(:default_for_user_and_date).with(@user, @date).and_return(@rate)
|
||||
Rate.for(@user, @project, @date).should eql(@rate)
|
||||
end
|
||||
|
||||
it 'should return nil if no set or default rate is found' do
|
||||
Rate.should_receive(:for_user_project_and_date).with(@user, @project, @date).and_return(nil)
|
||||
Rate.should_receive(:default_for_user_and_date).with(@user, @date).and_return(nil)
|
||||
Rate.for(@user, @project, @date).should be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with a user and project' do
|
||||
it 'should find the rate for a user on the project before today' do
|
||||
Rate.should_receive(:for_user_project_and_date).with(@user, @project, Date.today.to_s).and_return(@rate)
|
||||
Rate.for(@user, @project)
|
||||
end
|
||||
|
||||
it 'should return the most recent rate found' do
|
||||
Rate.should_receive(:for_user_project_and_date).with(@user, @project, Date.today.to_s).and_return(@rate)
|
||||
Rate.for(@user, @project).should eql(@rate)
|
||||
end
|
||||
|
||||
it 'should return nil if no set or default rate is found' do
|
||||
Rate.should_receive(:for_user_project_and_date).with(@user, @project, Date.today.to_s).and_return(nil)
|
||||
Rate.should_receive(:default_for_user_and_date).with(@user, Date.today.to_s).and_return(nil)
|
||||
Rate.for(@user, @project).should be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with a user' do
|
||||
it 'should find the rate without a project for a user on the project before today' do
|
||||
Rate.should_receive(:for_user_project_and_date).with(@user, nil, Date.today.to_s).and_return(@rate)
|
||||
Rate.for(@user)
|
||||
end
|
||||
|
||||
it 'should return the most recent rate found' do
|
||||
Rate.should_receive(:for_user_project_and_date).with(@user, nil, Date.today.to_s).and_return(@rate)
|
||||
Rate.for(@user).should eql(@rate)
|
||||
end
|
||||
|
||||
it 'should return nil if no set or default rate is found' do
|
||||
Rate.should_receive(:for_user_project_and_date).with(@user, nil, Date.today.to_s).and_return(nil)
|
||||
Rate.for(@user).should be_nil
|
||||
end
|
||||
end
|
||||
|
||||
it 'with an invalid user should raise an InvalidParameterException' do
|
||||
object = mock('random_object_with_id_attribute')
|
||||
Rate.should_not_receive(:for_user_project_and_date)
|
||||
lambda {
|
||||
Rate.for(object)
|
||||
}.should raise_error(Rate::InvalidParameterException, "user must be a User instance")
|
||||
end
|
||||
|
||||
it 'with an invalid project should raise an InvalidParameterException' do
|
||||
object = mock('random_object_with_id_attribute')
|
||||
Rate.should_not_receive(:for_user_project_and_date)
|
||||
lambda {
|
||||
Rate.for(@user, object)
|
||||
}.should raise_error(Rate::InvalidParameterException, "project must be a Project instance")
|
||||
end
|
||||
|
||||
it 'with an invalid object for date should raise an InvalidParameterException' do
|
||||
object = mock('random_object_thats_not_a_string')
|
||||
Rate.should_not_receive(:for_user_project_and_date)
|
||||
lambda {
|
||||
Rate.for(@user, @project, object)
|
||||
}.should raise_error(Rate::InvalidParameterException, "date must be a valid Date string (e.g. YYYY-MM-DD)")
|
||||
end
|
||||
|
||||
it 'with an invalid date string should raise an InvalidParameterException' do
|
||||
Rate.should_not_receive(:for_user_project_and_date)
|
||||
lambda {
|
||||
Rate.for(@user, @project, '2000-13-40')
|
||||
}.should raise_error(Rate::InvalidParameterException, "date must be a valid Date string (e.g. YYYY-MM-DD)")
|
||||
end
|
||||
|
||||
|
||||
|
||||
end
|
||||
|
||||
describe Rate, 'for_user_project_and_date (private)' do
|
||||
before(:each) do
|
||||
@user = mock_model(User)
|
||||
@project = mock_model(Project)
|
||||
@date = '2009-01-01'
|
||||
@rate = mock_model(Rate, :amount => 50.50)
|
||||
end
|
||||
|
||||
it 'should find the rate for a user on the project before the date' do
|
||||
Rate.should_receive(:find).with(:first, {
|
||||
:conditions => ["user_id IN (?) AND project_id IN (?) AND date_in_effect <= ?",
|
||||
@user.id,
|
||||
@project.id,
|
||||
@date
|
||||
],
|
||||
:order => 'date_in_effect DESC'
|
||||
}).and_return(@rate)
|
||||
|
||||
Rate.send(:for_user_project_and_date, @user, @project, @date)
|
||||
end
|
||||
|
||||
it 'should return the value of the most recent rate found' do
|
||||
Rate.should_receive(:find).with(:first, {
|
||||
:conditions => ["user_id IN (?) AND project_id IN (?) AND date_in_effect <= ?",
|
||||
@user.id,
|
||||
@project.id,
|
||||
@date
|
||||
],
|
||||
:order => 'date_in_effect DESC'
|
||||
}).and_return(@rate1)
|
||||
Rate.send(:for_user_project_and_date, @user, @project, @date).should eql(@rate1)
|
||||
end
|
||||
|
||||
it 'should search rates without a project when +project+ is nil' do
|
||||
Rate.should_receive(:find).with(:first, {
|
||||
:conditions => ["user_id IN (?) AND date_in_effect <= ? AND project_id IS NULL",
|
||||
@user.id,
|
||||
@date
|
||||
],
|
||||
:order => 'date_in_effect DESC'
|
||||
}).and_return(@rate1)
|
||||
Rate.send(:for_user_project_and_date, @user, nil, @date).should eql(@rate1)
|
||||
end
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
require File.dirname(__FILE__) + '/spec_helper'
|
||||
|
||||
describe Class do
|
||||
it "should be a class of Class" do
|
||||
Class.class.should eql(Class)
|
||||
end
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
--colour
|
||||
--format
|
||||
progress
|
||||
--loadby
|
||||
mtime
|
||||
--reverse
|
||||
--backtrace
|
||||
@@ -1,82 +0,0 @@
|
||||
# This file is copied to ~/spec when you run 'ruby script/generate rspec'
|
||||
# from the project root directory.
|
||||
ENV["RAILS_ENV"] = "test"
|
||||
redmine_root = ENV["REDMINE_ROOT"] || File.dirname(__FILE__) + "/../../../.."
|
||||
require File.expand_path(redmine_root + "/config/environment")
|
||||
require 'spec'
|
||||
require 'spec/rails'
|
||||
require 'ruby-debug'
|
||||
|
||||
Spec::Runner.configure do |config|
|
||||
# If you're not using ActiveRecord you should remove these
|
||||
# lines, delete config/database.yml and disable :active_record
|
||||
# in your config/boot.rb
|
||||
config.use_transactional_fixtures = true
|
||||
config.use_instantiated_fixtures = false
|
||||
config.fixture_path = RAILS_ROOT + '/spec/fixtures/'
|
||||
|
||||
# == Fixtures
|
||||
#
|
||||
# You can declare fixtures for each example_group like this:
|
||||
# describe "...." do
|
||||
# fixtures :table_a, :table_b
|
||||
#
|
||||
# Alternatively, if you prefer to declare them only once, you can
|
||||
# do so right here. Just uncomment the next line and replace the fixture
|
||||
# names with your fixtures.
|
||||
#
|
||||
# config.global_fixtures = :table_a, :table_b
|
||||
#
|
||||
# If you declare global fixtures, be aware that they will be declared
|
||||
# for all of your examples, even those that don't use them.
|
||||
#
|
||||
# == Mock Framework
|
||||
#
|
||||
# RSpec uses it's own mocking framework by default. If you prefer to
|
||||
# use mocha, flexmock or RR, uncomment the appropriate line:
|
||||
#
|
||||
# config.mock_with :mocha
|
||||
# config.mock_with :flexmock
|
||||
# config.mock_with :rr
|
||||
end
|
||||
|
||||
# require the entire app if we're running under coverage testing,
|
||||
# so we measure 0% covered files in the report
|
||||
#
|
||||
# http://www.pervasivecode.com/blog/2008/05/16/making-rcov-measure-your-whole-rails-app-even-if-tests-miss-entire-source-files/
|
||||
if defined?(Rcov)
|
||||
all_app_files = Dir.glob('{app,lib}/**/*.rb')
|
||||
all_app_files.each{|rb| require rb}
|
||||
end
|
||||
|
||||
|
||||
module AssociationMatcher
|
||||
class Association
|
||||
def initialize(field, association_type)
|
||||
@field = field
|
||||
@association_type = association_type
|
||||
end
|
||||
|
||||
def matches?(model)
|
||||
@model=model
|
||||
association = @model.reflect_on_association(@field)
|
||||
return false if association.nil?
|
||||
|
||||
return association.name == @field && association.macro == @association_type
|
||||
end
|
||||
|
||||
def failure_message
|
||||
"expected <#{@model.name}> to have a #{@association_type} association for #{@field}"
|
||||
end
|
||||
|
||||
def negative_failure_message
|
||||
"expected <#{@model.name}> to not have a #{@association_type} association for #{@field} but one was found"
|
||||
end
|
||||
end
|
||||
|
||||
def have_association(field, association_type)
|
||||
Association.new(field, association_type)
|
||||
end
|
||||
end
|
||||
|
||||
include AssociationMatcher
|
||||
401
test/functional/rates_controller_test.rb
Normal file
401
test/functional/rates_controller_test.rb
Normal file
@@ -0,0 +1,401 @@
|
||||
require File.dirname(__FILE__) + '/../test_helper'
|
||||
|
||||
class RatesControllerTest < ActionController::TestCase
|
||||
|
||||
def self.should_be_unauthorized(&block)
|
||||
should 'should return a forbidden status code' do
|
||||
instance_eval(&block)
|
||||
assert_response :forbidden
|
||||
end
|
||||
|
||||
should 'should display the standard unauthorized page' do
|
||||
instance_eval(&block)
|
||||
assert_template 'common/error'
|
||||
end
|
||||
|
||||
context "with mime type of xml" do
|
||||
|
||||
should "should return an forbidden error" do
|
||||
@request.env["HTTP_ACCEPT"] = "application/xml"
|
||||
instance_eval(&block)
|
||||
assert_response :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def mock_rate(stubs={})
|
||||
@project = Project.generate!
|
||||
stubs = {
|
||||
:date_in_effect => Date.today,
|
||||
:project => @project,
|
||||
:amount => 100.0,
|
||||
:user => @user
|
||||
}.merge(stubs)
|
||||
@mock_rate = Rate.generate(stubs)
|
||||
end
|
||||
|
||||
def mock_locked_rate(stubs={})
|
||||
@mock_rate = mock_rate
|
||||
@mock_rate.time_entries << TimeEntry.generate!
|
||||
@mock_rate
|
||||
end
|
||||
|
||||
context "as regular user" do
|
||||
setup do
|
||||
@user = User.generate!
|
||||
@request.session[:user_id] = @user.id
|
||||
end
|
||||
|
||||
context "responding to GET index" do
|
||||
should_be_unauthorized { get :index }
|
||||
end
|
||||
|
||||
context "responding to GET show" do
|
||||
should_be_unauthorized { get :show, :id => "37" }
|
||||
end
|
||||
|
||||
context "responding to GET new" do
|
||||
should_be_unauthorized { get :new }
|
||||
end
|
||||
|
||||
context "responding to GET edit" do
|
||||
should_be_unauthorized { get :edit, :id => "37" }
|
||||
end
|
||||
|
||||
context "responding to POST create" do
|
||||
should_be_unauthorized { post :create, :rate => {:these => 'params'} }
|
||||
end
|
||||
|
||||
context "responding to PUT update" do
|
||||
should_be_unauthorized { put :update, :id => "37", :rate => {:these => 'params'} }
|
||||
end
|
||||
|
||||
context "responding to DELETE destroy" do
|
||||
should_be_unauthorized { delete :destroy, :id => "37" }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
context "as an administrator" do
|
||||
|
||||
setup do
|
||||
@user = User.generate!(:admin => true)
|
||||
@request.session[:user_id] = @user.id
|
||||
end
|
||||
|
||||
context "responding to GET index" do
|
||||
|
||||
should "should redirect to the homepage" do
|
||||
get :index
|
||||
assert_redirected_to home_url
|
||||
end
|
||||
|
||||
should "should display an error flash message" do
|
||||
get :index
|
||||
assert_match /not found/, flash[:error]
|
||||
end
|
||||
|
||||
context "with mime type of xml" do
|
||||
|
||||
should "should return a 404 error" do
|
||||
@request.env["HTTP_ACCEPT"] = "application/xml"
|
||||
get :index
|
||||
assert_response :not_found
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "responding to GET index with user" do
|
||||
setup do
|
||||
mock_rate
|
||||
end
|
||||
|
||||
should "should expose all historic rates for the user as @rates" do
|
||||
get :index, :user_id => @user.id
|
||||
assert_equal assigns(:rates), [@mock_rate]
|
||||
end
|
||||
|
||||
context "with mime type of xml" do
|
||||
|
||||
should "should render all rates as xml" do
|
||||
@request.env["HTTP_ACCEPT"] = "application/xml"
|
||||
get :index, :user_id => @user.id
|
||||
|
||||
assert_select 'rates' do
|
||||
assert_select 'rate' do
|
||||
assert_select 'id', :text => @mock_rate.id
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "responding to GET show" do
|
||||
setup do
|
||||
mock_rate
|
||||
end
|
||||
|
||||
should "should expose the @requested rate as @rate" do
|
||||
get :show, :id => @mock_rate.id
|
||||
assert_equal assigns(:rate), @mock_rate
|
||||
end
|
||||
|
||||
context "with mime type of xml" do
|
||||
|
||||
should "should render the requested rate as xml" do
|
||||
@request.env["HTTP_ACCEPT"] = "application/xml"
|
||||
get :show, :id => @mock_rate.id
|
||||
|
||||
assert_select 'rate' do
|
||||
assert_select 'id', :text => @mock_rate.id
|
||||
assert_select 'amount', :text => /100/
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "responding to GET new" do
|
||||
|
||||
should "should redirect to the homepage" do
|
||||
get :new
|
||||
assert_redirected_to home_url
|
||||
end
|
||||
|
||||
should "should display an error flash message" do
|
||||
get :new
|
||||
assert_match /not found/, flash[:error]
|
||||
end
|
||||
|
||||
context "with mime type of xml" do
|
||||
|
||||
should "should return a 404 error" do
|
||||
@request.env["HTTP_ACCEPT"] = "application/xml"
|
||||
get :new
|
||||
assert_response :not_found
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
context "responding to GET new with user" do
|
||||
should 'should be successful' do
|
||||
get :new, :user_id => @user.id
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
should "should expose a new rate as @rate" do
|
||||
get :new, :user_id => @user.id
|
||||
assert assigns(:rate)
|
||||
assert assigns(:rate).new_record?
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "responding to GET edit" do
|
||||
setup do
|
||||
mock_rate
|
||||
end
|
||||
|
||||
should "should expose the requested rate as @rate" do
|
||||
get :edit, :id => @mock_rate.id
|
||||
assert_equal assigns(:rate), @mock_rate
|
||||
end
|
||||
|
||||
context "on a locked rate" do
|
||||
setup do
|
||||
mock_locked_rate
|
||||
end
|
||||
|
||||
should 'should not have a Update button' do
|
||||
get :edit, :id => @mock_rate.id
|
||||
assert_select "input[type=submit]", :count => 0
|
||||
end
|
||||
|
||||
should 'should show the locked icon' do
|
||||
get :edit, :id => @mock_rate.id
|
||||
assert_select "img[src*=locked.png]"
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "responding to POST create" do
|
||||
|
||||
context "with valid params" do
|
||||
setup do
|
||||
@project = Project.generate!
|
||||
end
|
||||
|
||||
should "should expose a newly created rate as @rate" do
|
||||
post :create, :rate => {:project_id => @project.id, :amount => '50', :date_in_effect => Date.today.to_s, :user_id => @user.id}
|
||||
assert assigns(:rate)
|
||||
end
|
||||
|
||||
should "should redirect to the rate list" do
|
||||
post :create, :rate => {:project_id => @project.id, :amount => '50', :date_in_effect => Date.today.to_s, :user_id => @user.id}
|
||||
|
||||
assert_redirected_to rates_url(:user_id => @user.id)
|
||||
end
|
||||
|
||||
should 'should redirect to the back_url if set' do
|
||||
back_url = '/rates'
|
||||
post :create, :rate => {:project_id => @project.id, :amount => '50', :date_in_effect => Date.today.to_s, :user_id => @user.id}, :back_url => back_url
|
||||
|
||||
assert_redirected_to back_url
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "with invalid params" do
|
||||
should "should expose a newly created but unsaved rate as @rate" do
|
||||
post :create, :rate => {}
|
||||
assert assigns(:rate).new_record?
|
||||
end
|
||||
|
||||
should "should re-render the 'new' template" do
|
||||
post :create, :rate => {}
|
||||
assert_template 'new'
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "responding to PUT udpate" do
|
||||
|
||||
context "with valid params" do
|
||||
setup do
|
||||
mock_rate
|
||||
end
|
||||
|
||||
should "should update the requested rate" do
|
||||
put :update, :id => @mock_rate.id, :rate => {:amount => '150'}
|
||||
|
||||
assert_equal 150.0, @mock_rate.reload.amount
|
||||
end
|
||||
|
||||
should "should expose the requested rate as @rate" do
|
||||
put :update, :id => @mock_rate.id
|
||||
|
||||
assert_equal assigns(:rate), @mock_rate
|
||||
end
|
||||
|
||||
should "should redirect to the rate list" do
|
||||
put :update, :id => "1"
|
||||
|
||||
assert_redirected_to rates_url(:user_id => @user.id)
|
||||
end
|
||||
|
||||
should 'should redirect to the back_url if set' do
|
||||
back_url = '/rates'
|
||||
put :update, :id => "1", :back_url => back_url
|
||||
|
||||
assert_redirected_to back_url
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "with invalid params" do
|
||||
setup do
|
||||
mock_rate
|
||||
end
|
||||
|
||||
should "should not update the requested rate" do
|
||||
put :update, :id => @mock_rate.id, :rate => {:amount => 'asdf'}
|
||||
|
||||
assert_equal 100.0, @mock_rate.reload.amount
|
||||
end
|
||||
|
||||
should "should expose the rate as @rate" do
|
||||
put :update, :id => @mock_rate.id, :rate => {:amount => 'asdf'}
|
||||
|
||||
assert_equal assigns(:rate), @mock_rate
|
||||
end
|
||||
|
||||
should "should re-render the 'edit' template" do
|
||||
put :update, :id => @mock_rate.id, :rate => {:amount => 'asdf'}
|
||||
|
||||
assert_template 'edit'
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "on a locked rate" do
|
||||
setup do
|
||||
mock_locked_rate
|
||||
end
|
||||
|
||||
should "should not save the rate" do
|
||||
put :update, :id => @mock_rate.id, :rate => {:amount => '150'}
|
||||
|
||||
assert_equal 100, @mock_rate.reload.amount
|
||||
end
|
||||
|
||||
should "should set the locked rate as @rate" do
|
||||
put :update, :id => @mock_rate.id, :rate => { :amount => 200.0 }
|
||||
|
||||
assert_equal assigns(:rate), @mock_rate
|
||||
end
|
||||
|
||||
should "should re-render the 'edit' template" do
|
||||
put :update, :id => @mock_rate.id
|
||||
|
||||
assert_template 'edit'
|
||||
end
|
||||
|
||||
should "should render an error message" do
|
||||
put :update, :id => @mock_rate.id
|
||||
|
||||
assert_match /locked/, flash[:error]
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "responding to DELETE destroy" do
|
||||
setup do
|
||||
mock_rate
|
||||
end
|
||||
|
||||
should "should destroy the requested rate" do
|
||||
assert_difference('Rate.count', -1) do
|
||||
delete :destroy, :id => @mock_rate.id
|
||||
end
|
||||
end
|
||||
|
||||
should "should redirect to the user's rates list" do
|
||||
delete :destroy, :id => @mock_rate.id
|
||||
assert_redirected_to rates_url(:user_id => @user.id)
|
||||
end
|
||||
|
||||
should 'should redirect to the back_url if set' do
|
||||
back_url = '/rates'
|
||||
delete :destroy, :id => "1", :back_url => back_url
|
||||
|
||||
assert_redirected_to back_url
|
||||
end
|
||||
|
||||
context "on a locked rate" do
|
||||
setup do
|
||||
mock_locked_rate
|
||||
end
|
||||
|
||||
should "should display an error message" do
|
||||
delete :destroy, :id => @mock_rate.id
|
||||
assert_match /locked/, flash[:error]
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
81
test/integration/admin_panel_test.rb
Normal file
81
test/integration/admin_panel_test.rb
Normal file
@@ -0,0 +1,81 @@
|
||||
require 'test_helper'
|
||||
|
||||
class AdminPanelTest < ActionController::IntegrationTest
|
||||
include Redmine::I18n
|
||||
|
||||
def setup
|
||||
@last_caching_run = 4.days.ago.to_s
|
||||
@last_cache_clearing_run = 7.days.ago.to_s
|
||||
|
||||
Setting.plugin_redmine_rate = {
|
||||
'last_caching_run' => @last_caching_run,
|
||||
'last_cache_clearing_run' => @last_cache_clearing_run
|
||||
}
|
||||
|
||||
@user = User.generate!(:admin => true, :password => 'rates', :password_confirmation => 'rates')
|
||||
|
||||
login_as(@user.login, 'rates')
|
||||
end
|
||||
|
||||
context "Rate Caches admin panel" do
|
||||
should "be listed in the main Admin section" do
|
||||
click_link "Administration"
|
||||
assert_response :success
|
||||
|
||||
assert_select "#admin-menu" do
|
||||
assert_select "a.rate-caches"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
should "show the last run timestamp for the last caching run" do
|
||||
click_link "Administration"
|
||||
click_link "Rate Caches"
|
||||
|
||||
assert_select '#caching-run' do
|
||||
assert_select 'p', :text => /#{format_time(@last_caching_run)}/
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
should "show the last run timestamp for the last cache clearing run" do
|
||||
click_link "Administration"
|
||||
click_link "Rate Caches"
|
||||
|
||||
assert_select '#cache-clearing-run' do
|
||||
assert_select 'p', :text => /#{format_time(@last_cache_clearing_run)}/
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
should "have a button to force a caching run" do
|
||||
click_link "Administration"
|
||||
click_link "Rate Caches"
|
||||
click_button "Load Missing Caches"
|
||||
|
||||
assert_response :success
|
||||
|
||||
appx_clear_time = Date.today.strftime("%m/%d/%Y")
|
||||
|
||||
assert_select '#caching-run' do
|
||||
assert_select 'p', :text => /#{appx_clear_time}/
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
should "have a button to force a cache clearing run" do
|
||||
click_link "Administration"
|
||||
click_link "Rate Caches"
|
||||
click_button "Clear and Load All Caches"
|
||||
|
||||
assert_response :success
|
||||
|
||||
appx_clear_time = Date.today.strftime("%m/%d/%Y")
|
||||
|
||||
assert_select '#cache-clearing-run' do
|
||||
assert_select 'p', :text => /#{appx_clear_time}/
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
16
test/integration/routing_test.rb
Normal file
16
test/integration/routing_test.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
require "#{File.dirname(__FILE__)}/../test_helper"
|
||||
|
||||
class RoutingTest < ActionController::IntegrationTest
|
||||
context "routing rates" do
|
||||
should_route :get, "/rates", :controller => "rates", :action => "index"
|
||||
should_route :get, "/rates/new", :controller => "rates", :action => "new"
|
||||
should_route :get, "/rates/1", :controller => "rates", :action => "show", :id => "1"
|
||||
should_route :get, "/rates/1/edit", :controller => "rates", :action => "edit", :id => "1"
|
||||
|
||||
should_route :post, "/rates", :controller => "rates", :action => "create"
|
||||
|
||||
should_route :put, "/rates/1", :controller => "rates", :action => "update", :id => "1"
|
||||
|
||||
should_route :delete, "/rates/1", :controller => "rates", :action => "destroy", :id => "1"
|
||||
end
|
||||
end
|
||||
43
test/test_helper.rb
Normal file
43
test/test_helper.rb
Normal file
@@ -0,0 +1,43 @@
|
||||
# Load the normal Rails helper
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../../../../test/test_helper')
|
||||
|
||||
# Ensure that we are using the temporary fixture path
|
||||
Engines::Testing.set_fixture_path
|
||||
|
||||
require "webrat"
|
||||
|
||||
Webrat.configure do |config|
|
||||
config.mode = :rails
|
||||
end
|
||||
|
||||
module IntegrationTestHelper
|
||||
def login_as(user="existing", password="existing")
|
||||
visit "/login"
|
||||
fill_in 'Login', :with => user
|
||||
fill_in 'Password', :with => password
|
||||
click_button 'login'
|
||||
assert_response :success
|
||||
assert User.current.logged?
|
||||
end
|
||||
|
||||
def logout
|
||||
visit '/logout'
|
||||
assert_response :success
|
||||
assert !User.current.logged?
|
||||
end
|
||||
|
||||
def assert_forbidden
|
||||
assert_response :forbidden
|
||||
assert_template 'common/error'
|
||||
end
|
||||
|
||||
def assert_requires_login
|
||||
assert_response :success
|
||||
assert_template 'account/login'
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
class ActionController::IntegrationTest
|
||||
include IntegrationTestHelper
|
||||
end
|
||||
92
test/unit/lib/rate_time_entry_patch_test.rb
Normal file
92
test/unit/lib/rate_time_entry_patch_test.rb
Normal file
@@ -0,0 +1,92 @@
|
||||
require File.dirname(__FILE__) + '/../../test_helper'
|
||||
|
||||
class RateTimeEntryPatchTest < ActiveSupport::TestCase
|
||||
def setup
|
||||
@user = User.generate!
|
||||
@project = Project.generate!
|
||||
@date = Date.today.to_s
|
||||
@time_entry = TimeEntry.new({:user => @user, :project => @project, :spent_on => @date, :hours => 10.0, :activity => TimeEntryActivity.generate!})
|
||||
@rate = Rate.generate!(:user => @user, :project => @project, :date_in_effect => @date, :amount => 200.0)
|
||||
end
|
||||
|
||||
should 'should return 0.0 if there are no rates for the user' do
|
||||
@rate.destroy
|
||||
assert_equal 0.0, @time_entry.cost
|
||||
end
|
||||
|
||||
context 'should return the product of hours by' do
|
||||
should 'the results of Rate.amount_for' do
|
||||
assert_equal((200.0 * @time_entry.hours), @time_entry.cost)
|
||||
end
|
||||
|
||||
should 'the assigned rate' do
|
||||
rate = Rate.generate!(:user => @user, :project => @project, :date_in_effect => @date, :amount => 100.0)
|
||||
@time_entry.rate = rate
|
||||
assert_equal rate.amount * @time_entry.hours, @time_entry.cost
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#cost" do
|
||||
setup do
|
||||
@time_entry.save!
|
||||
end
|
||||
|
||||
context "without a cache" do
|
||||
should "return the calculated cost" do
|
||||
@time_entry.update_attribute(:cost, nil)
|
||||
assert_equal 2000.0, @time_entry.cost
|
||||
end
|
||||
|
||||
should "cache the cost to the field" do
|
||||
@time_entry.update_attribute(:cost, nil)
|
||||
@time_entry.cost
|
||||
|
||||
assert_equal 2000.0, @time_entry.read_attribute(:cost)
|
||||
assert_equal 2000.0, @time_entry.reload.read_attribute(:cost)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "with a cache" do
|
||||
setup do
|
||||
@time_entry.update_attribute(:cost, 2000.0)
|
||||
@time_entry.reload
|
||||
end
|
||||
|
||||
should "return the cached cost" do
|
||||
assert_equal 2000.0, @time_entry.read_attribute(:cost)
|
||||
assert_equal 2000.0, @time_entry.cost
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "before save" do
|
||||
should "clear and recalculate the cache" do
|
||||
assert_equal nil, @time_entry.read_attribute(:cost)
|
||||
|
||||
assert @time_entry.save
|
||||
|
||||
assert_equal 2000.0, @time_entry.read_attribute(:cost)
|
||||
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
|
||||
37
test/unit/lib/rate_users_helper_patch_test.rb
Normal file
37
test/unit/lib/rate_users_helper_patch_test.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
require File.dirname(__FILE__) + '/../../test_helper'
|
||||
|
||||
class UsersHelperWrapper
|
||||
include UsersHelper
|
||||
end
|
||||
|
||||
class RateUsersHelperPatchTest < ActiveSupport::TestCase
|
||||
should 'should return 3 tabs' do
|
||||
helper = UsersHelperWrapper.new
|
||||
assert_equal 3, helper.user_settings_tabs.length
|
||||
end
|
||||
|
||||
should 'should include a rate tab at the end' do
|
||||
helper = UsersHelperWrapper.new
|
||||
rate_tab = helper.user_settings_tabs[-1]
|
||||
assert_not_nil rate_tab
|
||||
end
|
||||
|
||||
context 'rate tab' do
|
||||
setup do
|
||||
helper = UsersHelperWrapper.new
|
||||
@rate_tab = helper.user_settings_tabs[-1]
|
||||
end
|
||||
|
||||
should 'should have the name of "rates"' do
|
||||
assert_equal 'rates', @rate_tab[:name]
|
||||
end
|
||||
|
||||
should 'should use the rates partial' do
|
||||
assert_equal 'users/rates', @rate_tab[:partial]
|
||||
end
|
||||
|
||||
should 'should use the i18n rates label' do
|
||||
assert_equal :rate_label_rate_history, @rate_tab[:label]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
require File.dirname(__FILE__) + '/../../../../test_helper'
|
||||
|
||||
class RedmineRate::Hooks::PluginTimesheetViewTimesheetsReportHeaderTagsTest < ActionController::TestCase
|
||||
include Redmine::Hook::Helper
|
||||
|
||||
def controller
|
||||
@controller ||= ApplicationController.new
|
||||
@controller.response ||= ActionController::TestResponse.new
|
||||
@controller
|
||||
end
|
||||
|
||||
def request
|
||||
@request ||= ActionController::TestRequest.new
|
||||
end
|
||||
|
||||
def hook(args={})
|
||||
call_hook :plugin_timesheet_view_timesheets_report_header_tags, args
|
||||
end
|
||||
|
||||
context "#plugin_timesheet_view_timesheets_report_header_tags" do
|
||||
should "return a css string" do
|
||||
@response.body = hook
|
||||
assert_select "style", :text => /missing-rate/
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
require File.dirname(__FILE__) + '/../../../../test_helper'
|
||||
|
||||
class RedmineRate::Hooks::PluginTimesheetViewsTimesheetGroupHeaderTest < ActionController::TestCase
|
||||
include Redmine::Hook::Helper
|
||||
|
||||
def controller
|
||||
@controller ||= ApplicationController.new
|
||||
@controller.response ||= ActionController::TestResponse.new
|
||||
@controller
|
||||
end
|
||||
|
||||
def request
|
||||
@request ||= ActionController::TestRequest.new
|
||||
end
|
||||
|
||||
def hook(args={})
|
||||
call_hook :plugin_timesheet_views_timesheet_group_header, args
|
||||
end
|
||||
|
||||
context "#plugin_timesheet_views_timesheet_group_header" do
|
||||
should "render the cost table header" do
|
||||
@response.body = hook
|
||||
assert_select "th", :text => "Cost"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
require File.dirname(__FILE__) + '/../../../../test_helper'
|
||||
|
||||
class RedmineRate::Hooks::PluginTimesheetViewsTimesheetTimeEntryTest < ActionController::TestCase
|
||||
include Redmine::Hook::Helper
|
||||
|
||||
def controller
|
||||
@controller ||= ApplicationController.new
|
||||
@controller.response ||= ActionController::TestResponse.new
|
||||
@controller
|
||||
end
|
||||
|
||||
def request
|
||||
@request ||= ActionController::TestRequest.new
|
||||
end
|
||||
|
||||
def hook(args={})
|
||||
call_hook :plugin_timesheet_views_timesheet_time_entry, args
|
||||
end
|
||||
|
||||
context "#plugin_timesheet_views_timesheet_time_entry" do
|
||||
context "for users with view rate permission" do
|
||||
should "render a cost cell showing the cost for the time entry" do
|
||||
User.current = User.generate!(:admin => true)
|
||||
rate = Rate.generate!(:amount => 100)
|
||||
time_entry = TimeEntry.generate!(:hours => 2, :rate => rate)
|
||||
|
||||
@response.body = hook(:time_entry => time_entry)
|
||||
|
||||
assert_select 'td', :text => "$200.00"
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
context "for users without view rate permission" do
|
||||
should "render an empty cost cell" do
|
||||
User.current = nil
|
||||
rate = Rate.generate!(:amount => 100)
|
||||
time_entry = TimeEntry.generate!(:hours => 2, :rate => rate)
|
||||
|
||||
@response.body = hook(:time_entry => time_entry)
|
||||
|
||||
assert_select 'td', :text => ' '
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,48 @@
|
||||
require File.dirname(__FILE__) + '/../../../../test_helper'
|
||||
|
||||
class RedmineRate::Hooks::PluginTimesheetViewsTimesheetTimeEntrySumTest < ActionController::TestCase
|
||||
include Redmine::Hook::Helper
|
||||
|
||||
def controller
|
||||
@controller ||= ApplicationController.new
|
||||
@controller.response ||= ActionController::TestResponse.new
|
||||
@controller
|
||||
end
|
||||
|
||||
def request
|
||||
@request ||= ActionController::TestRequest.new
|
||||
end
|
||||
|
||||
def hook(args={})
|
||||
call_hook :plugin_timesheet_views_timesheet_time_entry_sum, args
|
||||
end
|
||||
|
||||
context "#plugin_timesheet_views_timesheet_time_entry_sum" do
|
||||
context "for users with view rate permission" do
|
||||
should "render a cost cell showing the total cost for the time entries" do
|
||||
User.current = User.generate!(:admin => true)
|
||||
rate = Rate.generate!(:amount => 100)
|
||||
time_entry1 = TimeEntry.generate!(:hours => 2, :rate => rate)
|
||||
time_entry2 = TimeEntry.generate!(:hours => 10, :rate => rate)
|
||||
|
||||
@response.body = hook(:time_entries => [time_entry1, time_entry2])
|
||||
|
||||
assert_select 'td', :text => "$1,200.00"
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
context "for users without view rate permission" do
|
||||
should "render an empty cost cell" do
|
||||
User.current = nil
|
||||
rate = Rate.generate!(:amount => 100)
|
||||
time_entry = TimeEntry.generate!(:hours => 2, :rate => rate)
|
||||
|
||||
@response.body = hook(:time_entries => [time_entry])
|
||||
|
||||
assert_select 'td', :text => '$0.00'
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,60 @@
|
||||
require File.dirname(__FILE__) + '/../../../../test_helper'
|
||||
|
||||
class RedmineRate::Hooks::PluginTimesheetViewsTimesheetsTimeEntryRowClassTest < ActionController::TestCase
|
||||
include Redmine::Hook::Helper
|
||||
|
||||
def controller
|
||||
@controller ||= ApplicationController.new
|
||||
@controller.response ||= ActionController::TestResponse.new
|
||||
@controller
|
||||
end
|
||||
|
||||
def request
|
||||
@request ||= ActionController::TestRequest.new
|
||||
end
|
||||
|
||||
def hook(args={})
|
||||
call_hook :plugin_timesheet_views_timesheets_time_entry_row_class, args
|
||||
end
|
||||
|
||||
context "#plugin_timesheet_views_timesheets_time_entry_row_class" do
|
||||
context "for users with view rate permission" do
|
||||
setup do
|
||||
User.current = User.generate!(:admin => true)
|
||||
end
|
||||
|
||||
should "render a missing rate css class if the time entry has no cost" do
|
||||
time_entry = TimeEntry.generate!(:hours => 2, :rate => nil)
|
||||
|
||||
assert_equal "missing-rate", hook(:time_entry => time_entry)
|
||||
end
|
||||
|
||||
should "render nothing if the time entry has a cost" do
|
||||
rate = Rate.generate!(:amount => 100)
|
||||
time_entry = TimeEntry.generate!(:hours => 2, :rate => rate)
|
||||
|
||||
assert_equal "", hook(:time_entry => time_entry)
|
||||
end
|
||||
end
|
||||
|
||||
context "for users without view rate permission" do
|
||||
setup do
|
||||
User.current = nil
|
||||
end
|
||||
|
||||
should "render nothing if the time entry has no cost" do
|
||||
time_entry = TimeEntry.generate!(:hours => 2, :rate => nil)
|
||||
|
||||
assert_equal "", hook(:time_entry => time_entry)
|
||||
end
|
||||
|
||||
should "render nothing if the time entry has a cost" do
|
||||
rate = Rate.generate!(:amount => 100)
|
||||
time_entry = TimeEntry.generate!(:hours => 2, :rate => rate)
|
||||
|
||||
assert_equal "", hook(:time_entry => time_entry)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
74
test/unit/rate_for_test.rb
Normal file
74
test/unit/rate_for_test.rb
Normal file
@@ -0,0 +1,74 @@
|
||||
require File.dirname(__FILE__) + '/../test_helper'
|
||||
|
||||
# Test cases for the main Rate#for API
|
||||
class RateForTest < ActiveSupport::TestCase
|
||||
context 'calculated for' do
|
||||
setup do
|
||||
@user = User.generate!
|
||||
end
|
||||
|
||||
context 'a user with no Rates' do
|
||||
should 'should return nil' do
|
||||
assert_nil Rate.for(@user)
|
||||
end
|
||||
end
|
||||
|
||||
context 'a user with one default Rate' do
|
||||
should 'should return the Rate if the Rate is effective today' do
|
||||
rate = Rate.create!({ :user_id => @user.id, :amount => 100.0, :date_in_effect => Date.today})
|
||||
assert_equal rate, Rate.for(@user)
|
||||
end
|
||||
|
||||
should 'should return nil if the Rate is not effective yet' do
|
||||
assert_nil Rate.for(@user)
|
||||
end
|
||||
|
||||
should 'should return the same default Rate on all projects' do
|
||||
project = Project.generate!
|
||||
rate = Rate.create!({ :user_id => @user.id, :amount => 100.0, :date_in_effect => Date.today})
|
||||
assert_equal rate, Rate.for(@user, project)
|
||||
end
|
||||
end
|
||||
|
||||
context 'a user with two default Rates' do
|
||||
should 'should return the newest Rate before the todays date' do
|
||||
rate = Rate.create!({ :user_id => @user.id, :amount => 100.0, :date_in_effect => Date.yesterday})
|
||||
rate2 = Rate.create!({ :user_id => @user.id, :amount => 300.0, :date_in_effect => Date.today})
|
||||
assert_equal rate2, Rate.for(@user)
|
||||
end
|
||||
end
|
||||
|
||||
context 'a user with a default Rate and Rate on a project' do
|
||||
should 'should return the project Rate if its effective today' do
|
||||
project = Project.generate!
|
||||
rate = Rate.create!({ :user_id => @user.id, :project => project, :amount => 100.0, :date_in_effect => Date.yesterday})
|
||||
rate2 = Rate.create!({ :user_id => @user.id, :amount => 300.0, :date_in_effect => Date.today})
|
||||
assert_equal rate, Rate.for(@user, project)
|
||||
end
|
||||
|
||||
should 'should return the default Rate if the project Rate isnt effective yet but the default Rate is' do
|
||||
project = Project.generate!
|
||||
rate = Rate.create!({ :user_id => @user.id, :project => project, :amount => 100.0, :date_in_effect => Date.tomorrow})
|
||||
rate2 = Rate.create!({ :user_id => @user.id, :amount => 300.0, :date_in_effect => Date.today})
|
||||
assert_equal rate2, Rate.for(@user, project)
|
||||
end
|
||||
|
||||
should 'should return nil if neither Rate is effective yet' do
|
||||
project = Project.generate!
|
||||
rate = Rate.create!({ :user_id => @user.id, :project => project, :amount => 100.0, :date_in_effect => Date.tomorrow})
|
||||
rate2 = Rate.create!({ :user_id => @user.id, :amount => 300.0, :date_in_effect => Date.tomorrow})
|
||||
assert_nil Rate.for(@user, project)
|
||||
end
|
||||
end
|
||||
|
||||
context 'a user with two Rates on a project' do
|
||||
should 'should return the newest Rate before the todays date' do
|
||||
project = Project.generate!
|
||||
rate = Rate.create!({ :user_id => @user.id, :project => project, :amount => 100.0, :date_in_effect => Date.yesterday})
|
||||
rate2 = Rate.create!({ :user_id => @user.id, :project => project, :amount => 300.0, :date_in_effect => Date.today})
|
||||
assert_equal rate2, Rate.for(@user, project)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
333
test/unit/rate_test.rb
Normal file
333
test/unit/rate_test.rb
Normal file
@@ -0,0 +1,333 @@
|
||||
require File.dirname(__FILE__) + '/../test_helper'
|
||||
|
||||
class RateTest < ActiveSupport::TestCase
|
||||
def rate_valid_attributes
|
||||
{
|
||||
:user => User.generate!,
|
||||
:project => Project.generate!,
|
||||
:date_in_effect => Date.new(Date.today.year, 1, 1),
|
||||
:amount => 100.50
|
||||
}
|
||||
end
|
||||
|
||||
should_belong_to :project
|
||||
should_belong_to :user
|
||||
should_have_many :time_entries
|
||||
|
||||
should_validate_presence_of :user_id
|
||||
should_validate_presence_of :date_in_effect
|
||||
should_validate_numericality_of :amount
|
||||
|
||||
|
||||
context '#locked?' do
|
||||
should 'should be true if a Time Entry is associated' do
|
||||
rate = Rate.new
|
||||
rate.time_entries << TimeEntry.generate!
|
||||
assert rate.locked?
|
||||
end
|
||||
|
||||
should 'should be false if no Time Entries are associated' do
|
||||
rate = Rate.new
|
||||
assert ! rate.locked?
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
context '#unlocked?' do
|
||||
should 'should be false if a Time Entry is associated' do
|
||||
rate = Rate.new
|
||||
rate.time_entries << TimeEntry.generate!
|
||||
assert ! rate.unlocked?
|
||||
end
|
||||
|
||||
should 'should be true if no Time Entries are associated' do
|
||||
rate = Rate.new
|
||||
assert rate.unlocked?
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context '#save' do
|
||||
|
||||
should 'should save if a Rate is unlocked' do
|
||||
rate = Rate.new(rate_valid_attributes)
|
||||
assert rate.save
|
||||
end
|
||||
|
||||
should 'should not save if a Rate is locked' do
|
||||
rate = Rate.new(rate_valid_attributes)
|
||||
rate.time_entries << TimeEntry.generate!
|
||||
assert !rate.save
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
context '#destroy' do
|
||||
|
||||
should 'should destroy the Rate if should is unlocked' do
|
||||
rate = Rate.create(rate_valid_attributes)
|
||||
assert_difference('Rate.count', -1) do
|
||||
rate.destroy
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
should 'should not destroy the Rate if should is locked' do
|
||||
rate = Rate.create(rate_valid_attributes)
|
||||
rate.time_entries << TimeEntry.generate!
|
||||
|
||||
assert_difference('Rate.count', 0) do
|
||||
rate.destroy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "after save" do
|
||||
should "recalculate all of the cached cost of all Time Entries for the user" do
|
||||
@user = User.generate!
|
||||
@project = Project.generate!
|
||||
@date = Date.today.to_s
|
||||
@past_date = 1.month.ago.strftime('%Y-%m-%d')
|
||||
@rate = Rate.generate!(:user => @user, :project => @project, :date_in_effect => @date, :amount => 200.0)
|
||||
@time_entry1 = TimeEntry.generate!({:user => @user, :project => @project, :spent_on => @date, :hours => 10.0, :activity => TimeEntryActivity.generate!})
|
||||
@time_entry2 = TimeEntry.generate!({:user => @user, :project => @project, :spent_on => @past_date, :hours => 20.0, :activity => TimeEntryActivity.generate!})
|
||||
|
||||
|
||||
assert_equal 2000.00, @time_entry1.cost
|
||||
assert_equal 0, @time_entry2.cost
|
||||
|
||||
@old_rate = Rate.generate!(:user => @user, :project => @project, :date_in_effect => 2.months.ago.strftime('%Y-%m-%d'), :amount => 10.0)
|
||||
|
||||
assert_equal 2000.00, TimeEntry.find(@time_entry1.id).cost
|
||||
assert_equal 200.00, TimeEntry.find(@time_entry2.id).cost
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "after destroy" do
|
||||
should "recalculate all of the cached cost of all Time Entries for the user" do
|
||||
@user = User.generate!
|
||||
@project = Project.generate!
|
||||
@date = Date.today.to_s
|
||||
@past_date = 1.month.ago.strftime('%Y-%m-%d')
|
||||
@rate = Rate.generate!(:user => @user, :project => @project, :date_in_effect => @date, :amount => 200.0)
|
||||
@old_rate = Rate.generate!(:user => @user, :project => @project, :date_in_effect => 2.months.ago.strftime('%Y-%m-%d'), :amount => 10.0)
|
||||
|
||||
@time_entry1 = TimeEntry.generate!({:user => @user, :project => @project, :spent_on => @date, :hours => 10.0, :activity => TimeEntryActivity.generate!})
|
||||
@time_entry2 = TimeEntry.generate!({:user => @user, :project => @project, :spent_on => @past_date, :hours => 20.0, :activity => TimeEntryActivity.generate!})
|
||||
|
||||
|
||||
assert_equal 2000.00, @time_entry1.cost
|
||||
assert_equal 2000.00, @time_entry1.read_attribute(:cost)
|
||||
assert_equal 200.0, @time_entry2.cost
|
||||
assert_equal 200.0, @time_entry2.read_attribute(:cost)
|
||||
|
||||
@old_rate.destroy
|
||||
|
||||
assert_equal 2000.0, TimeEntry.find(@time_entry1.id).cost
|
||||
assert_equal 0, TimeEntry.find(@time_entry2.id).cost
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context '#for' do
|
||||
setup do
|
||||
@user = User.generate!
|
||||
@project = Project.generate!
|
||||
@date = '2009-01-01'
|
||||
@date = Date.new(Date.today.year, 1, 1).to_s
|
||||
@default_rate = Rate.generate!(:amount => 100.10, :date_in_effect => @date, :project => nil, :user => @user)
|
||||
@rate = Rate.generate!(:amount => 50.50, :date_in_effect => @date, :project => @project, :user => @user)
|
||||
end
|
||||
|
||||
context 'parameters' do
|
||||
should 'should be passed user' do
|
||||
assert_raises ArgumentError do
|
||||
Rate.for
|
||||
end
|
||||
end
|
||||
|
||||
should 'can be passed an optional project' do
|
||||
assert_nothing_raised do
|
||||
Rate.for(@user)
|
||||
end
|
||||
|
||||
assert_nothing_raised do
|
||||
Rate.for(@user, @project)
|
||||
end
|
||||
end
|
||||
|
||||
should 'can be passed an optional date string' do
|
||||
assert_nothing_raised do
|
||||
Rate.for(@user)
|
||||
end
|
||||
|
||||
assert_nothing_raised do
|
||||
Rate.for(@user, nil, @date)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context 'returns' do
|
||||
should 'a Rate object when there is a rate' do
|
||||
assert_equal @rate, Rate.for(@user, @project, @date)
|
||||
end
|
||||
|
||||
should 'a nil when there is no rate' do
|
||||
assert @rate.destroy
|
||||
assert @default_rate.destroy
|
||||
|
||||
assert_equal nil, Rate.for(@user, @project, @date)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a user, project, and date' do
|
||||
should 'should find the rate for a user on the project before the date' do
|
||||
assert_equal @rate, Rate.for(@user, @project, @date)
|
||||
end
|
||||
|
||||
should 'should return the most recent rate found' do
|
||||
assert_equal @rate, Rate.for(@user, @project, @date)
|
||||
end
|
||||
|
||||
should 'should check for a default rate if no rate is found' do
|
||||
assert @rate.destroy
|
||||
|
||||
assert_equal @default_rate, Rate.for(@user, @project, @date)
|
||||
end
|
||||
|
||||
should 'should return nil if no set or default rate is found' do
|
||||
assert @rate.destroy
|
||||
assert @default_rate.destroy
|
||||
|
||||
assert_equal nil, Rate.for(@user, @project, @date)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a user and project' do
|
||||
should 'should find the rate for a user on the project before today' do
|
||||
assert_equal @rate, Rate.for(@user, @project)
|
||||
end
|
||||
|
||||
should 'should return the most recent rate found' do
|
||||
assert_equal @rate, Rate.for(@user, @project)
|
||||
end
|
||||
|
||||
should 'should return nil if no set or default rate is found' do
|
||||
assert @rate.destroy
|
||||
assert @default_rate.destroy
|
||||
|
||||
assert_equal nil, Rate.for(@user, @project)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a user' do
|
||||
should 'should find the rate without a project for a user on the project before today' do
|
||||
assert_equal @default_rate, Rate.for(@user)
|
||||
end
|
||||
|
||||
should 'should return the most recent rate found' do
|
||||
assert_equal @default_rate, Rate.for(@user)
|
||||
end
|
||||
|
||||
should 'should return nil if no set or default rate is found' do
|
||||
assert @rate.destroy
|
||||
assert @default_rate.destroy
|
||||
|
||||
assert_equal nil, Rate.for(@user)
|
||||
end
|
||||
end
|
||||
|
||||
should 'with an invalid user should raise an InvalidParameterException' do
|
||||
object = Object.new
|
||||
assert_raises Rate::InvalidParameterException do
|
||||
Rate.for(object)
|
||||
end
|
||||
end
|
||||
|
||||
should 'with an invalid project should raise an InvalidParameterException' do
|
||||
object = Object.new
|
||||
assert_raises Rate::InvalidParameterException do
|
||||
Rate.for(@user, object)
|
||||
end
|
||||
end
|
||||
|
||||
should 'with an invalid object for date should raise an InvalidParameterException' do
|
||||
object = Object.new
|
||||
assert_raises Rate::InvalidParameterException do
|
||||
Rate.for(@user, @project, object)
|
||||
end
|
||||
end
|
||||
|
||||
should 'with an invalid date string should raise an InvalidParameterException' do
|
||||
assert_raises Rate::InvalidParameterException do
|
||||
Rate.for(@user, @project, '2000-13-40')
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#update_all_time_entries_with_missing_cost" do
|
||||
setup do
|
||||
@user = User.generate!
|
||||
@project = Project.generate!
|
||||
@date = Date.today.to_s
|
||||
@rate = Rate.generate!(:user => @user, :project => @project, :date_in_effect => @date, :amount => 200.0)
|
||||
@time_entry1 = TimeEntry.generate!({:user => @user, :project => @project, :spent_on => @date, :hours => 10.0, :activity => TimeEntryActivity.generate!})
|
||||
@time_entry2 = TimeEntry.generate!({:user => @user, :project => @project, :spent_on => @date, :hours => 20.0, :activity => TimeEntryActivity.generate!})
|
||||
end
|
||||
|
||||
should "update the caches of all Time Entries" do
|
||||
TimeEntry.update_all('cost = null')
|
||||
|
||||
# Check that cost is NULL in the database, which skips the caching
|
||||
assert_equal 2, ActiveRecord::Base.connection.select_all('select count(*) as count from time_entries where cost IS NULL').first["count"].to_i
|
||||
|
||||
Rate.update_all_time_entries_with_missing_cost
|
||||
|
||||
assert_equal 0, ActiveRecord::Base.connection.select_all('select count(*) as count from time_entries where cost IS NULL').first["count"].to_i
|
||||
|
||||
end
|
||||
|
||||
should "timestamp a successful run" do
|
||||
assert_equal nil, Setting.plugin_redmine_rate['last_caching_run']
|
||||
|
||||
Rate.update_all_time_entries_with_missing_cost
|
||||
|
||||
assert Setting.plugin_redmine_rate['last_caching_run'], "Last run not timestamped"
|
||||
assert Time.parse(Setting.plugin_redmine_rate['last_caching_run']), "Last run timestamp not parseable"
|
||||
end
|
||||
end
|
||||
|
||||
context "#update_all_time_entries_to_refresh_cache" do
|
||||
setup do
|
||||
@user = User.generate!
|
||||
@project = Project.generate!
|
||||
@date = Date.today.to_s
|
||||
@rate = Rate.generate!(:user => @user, :project => @project, :date_in_effect => @date, :amount => 200.0)
|
||||
@time_entry1 = TimeEntry.generate!({:user => @user, :project => @project, :spent_on => @date, :hours => 10.0, :activity => TimeEntryActivity.generate!})
|
||||
@time_entry2 = TimeEntry.generate!({:user => @user, :project => @project, :spent_on => @date, :hours => 20.0, :activity => TimeEntryActivity.generate!})
|
||||
end
|
||||
|
||||
should "update the caches of all Time Entries" do
|
||||
assert_equal 0, ActiveRecord::Base.connection.select_all('select count(*) as count from time_entries where cost IS NULL').first["count"].to_i
|
||||
|
||||
Rate.update_all_time_entries_to_refresh_cache
|
||||
|
||||
assert_equal 0, ActiveRecord::Base.connection.select_all('select count(*) as count from time_entries where cost IS NULL').first["count"].to_i
|
||||
|
||||
end
|
||||
|
||||
should "timestamp a successful run" do
|
||||
assert_equal nil, Setting.plugin_redmine_rate['last_cache_clearing_run']
|
||||
|
||||
Rate.update_all_time_entries_to_refresh_cache
|
||||
|
||||
assert Setting.plugin_redmine_rate['last_cache_clearing_run'], "Last run not timestamped"
|
||||
assert Time.parse(Setting.plugin_redmine_rate['last_cache_clearing_run']), "Last run timestamp not parseable"
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user