diff --git a/app/helpers/contracts_helper.rb b/app/helpers/contracts_helper.rb index b50249e..b511a0a 100644 --- a/app/helpers/contracts_helper.rb +++ b/app/helpers/contracts_helper.rb @@ -1,12 +1,52 @@ module ContractsHelper def setup_nested_deliverable_records(deliverable) - returning(deliverable) do |d| - d.labor_budgets.build if d.labor_budgets.empty? - d.overhead_budgets.build if d.overhead_budgets.empty? - d.fixed_budgets.build if d.fixed_budgets.empty? + deliverable.labor_budgets.build if deliverable.labor_budgets.empty? + deliverable.overhead_budgets.build if deliverable.overhead_budgets.empty? + deliverable.fixed_budgets.build if deliverable.fixed_budgets.empty? + deliverable + end + + def group_contracts_by_status(contracts) + grouped_contracts = contracts.inject({}) do |grouped, contract| + grouped[contract.status] ||= [] + grouped[contract.status] << contract + grouped + end + grouped_contracts["open"] ||= [] + grouped_contracts["locked"] ||= [] + grouped_contracts["closed"] ||= [] + grouped_contracts + end + + def grouped_deliverable_options_for_select(project, selected_key=nil) + project.contracts.all(:include => :deliverables).inject("") do |html, contract| + if contract.closed? && !contract.includes_deliverable_id?(selected_key) + html + else + html << content_tag(:optgroup, + deliverable_options_for_contract(contract, selected_key).join("\n"), + :label => h(contract.name)) + end end end + def deliverable_options_for_contract(contract, selected_key) + contract.deliverables.collect do |deliverable| + deliverable_option(deliverable, selected_key) + end + end + + def deliverable_option(deliverable, selected_key) + option_attributes = {} + option_attributes[:value] = h(deliverable.id) + option_attributes[:selected] = "selected" if selected_key.to_i == deliverable.id + option_attributes[:disabled] = "disabled" if (deliverable.locked? || deliverable.contract_locked?) && selected_key.to_i != deliverable.id + + return "" if deliverable.closed? && option_attributes[:selected].blank? # Skip unselected, closed + + content_tag(:option, h(deliverable.title), option_attributes) + end + # Simple helper to show the values of a field on an object in a standard format # #

diff --git a/app/models/contract.rb b/app/models/contract.rb index 80d8d61..aa52d8a 100644 --- a/app/models/contract.rb +++ b/app/models/contract.rb @@ -17,7 +17,9 @@ class Contract < ActiveRecord::Base validates_presence_of :start_date validates_presence_of :end_date validates_inclusion_of :discount_type, :in => %w($ %), :allow_blank => true, :allow_nil => true + validates_inclusion_of :status, :in => ["open","locked","closed"], :allow_blank => true, :allow_nil => true validate :start_and_end_date_are_valid + validate_on_update :validate_status_changes # Accessors attr_accessible :name @@ -33,15 +35,49 @@ class Contract < ActiveRecord::Base attr_accessible :po_number attr_accessible :client_point_of_contact attr_accessible :details + attr_accessible :status named_scope :by_name, {:order => "#{Contract.table_name}.name ASC"} - - [:status, :contract_type, + named_scope :with_status, lambda {|statuses| + { + :conditions => ["#{Contract.table_name}.status IN (?)", statuses] + } + } + + [:contract_type, :discount_spent, :discount_budget ].each do |mthd| define_method(mthd) { "TODO in later release" } end + def status + read_attribute(:status) || "open" + end + + def lock! + update_attribute(:status, "locked") + end + + def close! + update_attribute(:status, "closed") + end + + def open? + self.status == "open" + end + + def locked? + self.status == "locked" + end + + def closed? + self.status == "closed" + end + + def includes_deliverable_id?(deliverable_id) + deliverable_ids.include?(deliverable_id.to_i) + end + # ------------------------------------------------------------ # Labor Methods # ------------------------------------------------------------ @@ -229,6 +265,7 @@ class Contract < ActiveRecord::Base def after_initialize self.executed = false unless self.executed.present? + self.status = "open" unless self.status.present? end # Are the start_date and end_date valid? @@ -238,6 +275,30 @@ class Contract < ActiveRecord::Base end end + def valid_status_change? + change_to_status_only? || changing_to_the_open_status? || changing_from_the_open_status? + end + + def change_to_status_only? + ["status"] == changes.keys + end + + def changing_to_the_open_status? + changes["status"].present? && "open" == changes["status"].second + end + + def changing_from_the_open_status? + changes["status"].present? && "open" == changes["status"].first + end + + # TODO: duplicated on Deliverable, refactor after one more duplication + def validate_status_changes + return if valid_status_change? + + errors.add_to_base(:cant_update_locked_contract) if locked? + errors.add_to_base(:cant_update_closed_contract) if closed? + end + # Currency amount of time that is logged to the project or to issues # that are not assigned to a Deliverable def orphaned_time @@ -252,15 +313,17 @@ class Contract < ActiveRecord::Base end if Rails.env.test? - generator_for :name, :method => :next_name + generator_for :name, :start => "Contract 0000" generator_for :executed => true generator_for(:start_date) { Date.yesterday } generator_for(:end_date) { Date.tomorrow } - - def self.next_name - @last_name ||= 'Contract 0000' - @last_name.succ! - end + generator_for :discount, '' + generator_for :details, '' + generator_for :discount_note, '' + generator_for :client_point_of_contact, '' + generator_for :client_ap_contact_information, '' + generator_for :po_number, '' + generator_for :status, 'open' end @@ -270,5 +333,5 @@ class Contract < ActiveRecord::Base def summarize_associated_values(records, value_method) records.inject(0) {|total, record| total += record.send(value_method)} end - + end diff --git a/app/models/deliverable.rb b/app/models/deliverable.rb index 003805b..78a395d 100644 --- a/app/models/deliverable.rb +++ b/app/models/deliverable.rb @@ -19,15 +19,27 @@ class Deliverable < ActiveRecord::Base validates_presence_of :title validates_presence_of :type validates_presence_of :manager + validates_inclusion_of :status, :in => ["open","locked","closed"], :allow_blank => true, :allow_nil => true + validate_on_update :validate_status_changes + validate :validate_contract_status # Accessors include DollarizedAttribute dollarized_attribute :total delegate :name, :to => :contract, :prefix => true, :allow_nil => true + delegate "open?", :to => :contract, :prefix => true, :allow_nil => true + delegate "closed?", :to => :contract, :prefix => true, :allow_nil => true + delegate "locked?", :to => :contract, :prefix => true, :allow_nil => true # Callbacks - + before_destroy :block_on_locked_contracts + before_destroy :block_on_closed_contracts + + def after_initialize + self.status = "open" unless self.status.present? + end + # Register callbacks here, on new records the class isn't set so class-specific # callbacks don't fire. def after_save @@ -37,6 +49,11 @@ class Deliverable < ActiveRecord::Base end named_scope :by_title, {:order => "#{Deliverable.table_name}.title ASC"} + named_scope :with_status, lambda {|statuses| + { + :conditions => ["#{Deliverable.table_name}.status IN (?)", statuses] + } + } def short_type '' @@ -51,6 +68,88 @@ class Deliverable < ActiveRecord::Base nil end + def lock! + update_attribute(:status, "locked") + end + + def close! + update_attribute(:status, "closed") + end + + def open? + self.status == "open" + end + + def locked? + self.status == "locked" + end + + def closed? + self.status == "closed" + end + + def editable? + (new_record? || open?) + end + + def valid_status_change? + change_to_status_only? || changing_to_the_open_status? || changing_from_the_open_status? + end + + def change_to_status_only? + ["status"] == changes.keys + end + + def changing_to_the_open_status? + changes["status"].present? && "open" == changes["status"].second + end + + def changing_from_the_open_status? + changes["status"].present? && "open" == changes["status"].first + end + + # TODO: duplicated on Contract, refactor after one more duplication + def validate_status_changes + return if valid_status_change? + + errors.add_to_base(:cant_update_locked_deliverable) if locked? + errors.add_to_base(:cant_update_closed_deliverable) if closed? + end + + def validate_contract_status + return if contract_open? + return if change_to_status_only? + + if contract_locked? + if new_record? + errors.add_to_base(:cant_create_deliverable_on_locked_contract) + else + errors.add_to_base(:cant_update_locked_contract) + end + end + + if contract_closed? + if new_record? + errors.add_to_base(:cant_create_deliverable_on_closed_contract) + else + errors.add_to_base(:cant_update_closed_contract) + end + end + end + + # No operation method, useful to clean up logic with an optional message + # for documentation + def noop(message="") + end + + def block_on_locked_contracts + !contract_locked? + end + + def block_on_closed_contracts + !contract_closed? + end + def to_s title end @@ -214,7 +313,8 @@ class Deliverable < ActiveRecord::Base if Rails.env.test? generator_for :title, :method => :next_title - + generator_for :status, 'open' + def self.next_title @last_title ||= 'Deliverable 0000' @last_title.succ! diff --git a/app/views/contracts/_form.html.erb b/app/views/contracts/_form.html.erb index 0a8c204..103874a 100644 --- a/app/views/contracts/_form.html.erb +++ b/app/views/contracts/_form.html.erb @@ -1,6 +1,13 @@ +<% if resource.locked? || resource.closed? %> +

+

<%= resource.locked? ? l(:text_contract_locked_warning) : l(:text_contract_closed_warning) %>

+
+<% end %> +
<% form.inputs :name => l(:text_general_legend) do %> <%= form.input :name, :required => true %> + <%= form.input :status, :required => true, :collection => [["Open","open"],["Locked","locked"],["Closed","closed"]] %> <%= form.input :account_executive, :required => true, :collection => @project.users.sort %>
  • <%= label('contract', 'executed') %> diff --git a/app/views/contracts/index.html.erb b/app/views/contracts/index.html.erb index 42d2734..b4534e8 100644 --- a/app/views/contracts/index.html.erb +++ b/app/views/contracts/index.html.erb @@ -12,10 +12,10 @@
  • - <% if collection.empty? %> + <% if group_contracts_by_status(collection)["open"].empty? %>

    <%= l(:label_no_data) %>

    <% else %> - +
    @@ -26,11 +26,11 @@ - <% collection.each do |contract| %> + <% group_contracts_by_status(collection)["open"].each do |contract| %> <% content_tag_for(:tr, contract, :class => cycle('','odd')) do %> - + @@ -42,11 +42,71 @@ <% end %>
    -

    Inactive Contracts

    +

    Locked Contracts

    -

    <%= l(:label_no_data) %>

    -

    <%= release(5, "Contract Status. Split contracts by active and inactive") %>

    + <% if group_contracts_by_status(collection)["locked"].empty? %> +

    <%= l(:label_no_data) %>

    + <% else %> +
    <%= l(:field_id) %> <%= l(:field_name) %><%= l(:field_end_date) %>
    <%= link_to(h(contract.id), contract_path(@project, contract)) %> <%= link_to(h(contract.name), contract_path(@project, contract)) %><%= release(5, "Contract Status") %><%= h(contract.status) %> <%= release(5, "Contract Type") %> <%= h(format_value_field_for_contracts(contract.total_budget)) %>
    + + + + + + + + + + + <% group_contracts_by_status(collection)["locked"].each do |contract| %> + <% content_tag_for(:tr, contract, :class => cycle('','odd')) do %> + + + + + + + + <% end %> + <% end %> + +
    <%= l(:field_id) %><%= l(:field_name) %><%= l(:field_status) %><%= l(:field_type) %><%= l(:field_account_executive_short) %><%= l(:field_total_budget) %><%= l(:field_end_date) %>
    <%= link_to(h(contract.id), contract_path(@project, contract)) %><%= link_to(h(contract.name), contract_path(@project, contract)) %><%= h(contract.status) %><%= release(5, "Contract Type") %><%= h(format_value_field_for_contracts(contract.total_budget)) %><%= h format_date(contract.end_date) %>
    + <% end %> + +
    +

    Closed Contracts

    +
    + + <% if group_contracts_by_status(collection)["closed"].empty? %> +

    <%= l(:label_no_data) %>

    + <% else %> + + + + + + + + + + + + <% group_contracts_by_status(collection)["closed"].each do |contract| %> + <% content_tag_for(:tr, contract, :class => cycle('','odd')) do %> + + + + + + + + <% end %> + <% end %> + +
    <%= l(:field_id) %><%= l(:field_name) %><%= l(:field_status) %><%= l(:field_type) %><%= l(:field_account_executive_short) %><%= l(:field_total_budget) %><%= l(:field_end_date) %>
    <%= link_to(h(contract.id), contract_path(@project, contract)) %><%= link_to(h(contract.name), contract_path(@project, contract)) %><%= h(contract.status) %><%= release(5, "Contract Type") %><%= h(format_value_field_for_contracts(contract.total_budget)) %><%= h format_date(contract.end_date) %>
    + <% end %> + diff --git a/app/views/contracts/show.html.erb b/app/views/contracts/show.html.erb index ea9830a..79e0e81 100644 --- a/app/views/contracts/show.html.erb +++ b/app/views/contracts/show.html.erb @@ -15,10 +15,7 @@
    - - <%# show_field(resource, :status, :html_options => {:class => 'contract-status'}) %> - - + <%= show_field(resource, :status, :html_options => {:class => 'contract-status'}) %> <%= show_field(resource, :account_executive, :html_options => {:class => 'contract-account-manager'}) %> <%# show_field(resource, :contract_type, :html_options => {:class => 'contract-type'}) %> @@ -105,7 +102,7 @@
    <%= release(2, "CSV") %> <%= release(5, "View All/Pagination") %> - <%= link_to(l(:button_add_new), new_contract_deliverable_path(@project, resource), :id => 'new-deliverable') %> + <%= link_to(l(:button_add_new), new_contract_deliverable_path(@project, resource), :id => 'new-deliverable') if resource.open? %>
    @@ -133,7 +130,7 @@ - + <%= format_budget_for_deliverable(deliverable, deliverable.labor_budget_spent, deliverable.labor_budget_total, :class => 'labor') %> <%= format_budget_for_deliverable(deliverable, deliverable.overhead_spent, deliverable.overhead_budget_total, :class => 'overhead') %> diff --git a/app/views/deliverables/_details_row.html.erb b/app/views/deliverables/_details_row.html.erb index 10bcc4c..c301f60 100644 --- a/app/views/deliverables/_details_row.html.erb +++ b/app/views/deliverables/_details_row.html.erb @@ -7,7 +7,7 @@
    <%= link_to(l(:button_edit), edit_contract_deliverable_path(@project, contract, deliverable), :class => 'icon icon-edit') %> - <%= link_to(l(:button_delete), contract_deliverable_path(@project, contract, deliverable), :method => :delete, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') %> + <%= link_to(l(:button_delete), contract_deliverable_path(@project, contract, deliverable), :method => :delete, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') if contract.open? %>
    <%= textilizable(deliverable, :notes) %> diff --git a/app/views/deliverables/_form.html.erb b/app/views/deliverables/_form.html.erb index 1da5da6..fdd4357 100644 --- a/app/views/deliverables/_form.html.erb +++ b/app/views/deliverables/_form.html.erb @@ -2,6 +2,17 @@ <%= javascript_tag("var i18nEndDateEmpty = '#{l(:text_end_date_empty)}'") %> <%= javascript_tag("var i18nChangedPeriodMessage = '#{l(:text_changed_period_message)}'") %> +<% if resource.locked? || resource.closed? || resource.contract_locked? || resource.contract_closed? %> +
    + <% if resource.contract_locked? || resource.contract_closed? %> +

    <%= resource.contract_locked? ? l(:text_contract_locked_warning) : l(:text_contract_closed_warning) %>

    + <% end %> + <% if resource.locked? || resource.closed? %> +

    <%= resource.locked? ? l(:text_deliverable_locked_warning) : l(:text_deliverable_closed_warning) %>

    + <% end %> +
    +<% end %> +
    <% form.inputs :name => l(:text_deliverable_details_legend), :id => 'deliverable-details' do %> <%# Used by jquery to check if this is a new or existing record %> @@ -19,6 +30,7 @@ <%= form.input :type, :as => :hidden, :class => 'type' %> <% end %> + <%= form.input :status, :required => true, :collection => [["Open","open"],["Locked","locked"],["Closed","closed"]] %> <%= form.input :manager, :required => true, :collection => @project.users.sort %> <%= form.input :start_date, :as => :string, :input_html => {:size => 10, :class => 'start-date', :id => 'deliverable_start_date'}, :hint => calendar_for('deliverable_start_date') %> diff --git a/app/views/issues/_bulk_edit_deliverable.html.erb b/app/views/issues/_bulk_edit_deliverable.html.erb index f4a0e97..26ed093 100644 --- a/app/views/issues/_bulk_edit_deliverable.html.erb +++ b/app/views/issues/_bulk_edit_deliverable.html.erb @@ -1,14 +1,10 @@ <% if project.module_enabled?(:contracts) && User.current.allowed_to?(:assign_deliverable_to_issue, project) %>

    <%= label_tag(:deliverable_id, l(:field_deliverable)) %> - <% options = project.contracts.inject([]) {|data, contract| - data << [contract.name, contract.deliverables.collect {|d| [d.title, d.id]} ] - } %> - <%= select_tag('deliverable_id', content_tag('option', l(:label_no_change_option), :value => '') + content_tag('option', l(:label_none), :value => 'none') + - grouped_options_for_select(options)) %> + grouped_deliverable_options_for_select(project)) %>

    <% end %> diff --git a/app/views/issues/_edit_deliverable.html.erb b/app/views/issues/_edit_deliverable.html.erb index 47fd42b..c07a1a7 100644 --- a/app/views/issues/_edit_deliverable.html.erb +++ b/app/views/issues/_edit_deliverable.html.erb @@ -1,9 +1,6 @@ <% if project.module_enabled?(:contracts) && User.current.allowed_to?(:assign_deliverable_to_issue, project) %>

    - <% options = project.contracts.inject([]) {|data, contract| - data << [contract.name, contract.deliverables.collect {|d| [d.title, d.id]} ] - } %> - <%= form.select(:deliverable_id, grouped_options_for_select(options, issue.deliverable_id), {:include_blank => true}) %> + <%= form.select(:deliverable_id, grouped_deliverable_options_for_select(project, issue.deliverable_id), {:include_blank => true}) %>

    <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 661837d..acb3852 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,4 +1,19 @@ en: + activerecord: + errors: + messages: + cant_create_time_on_object: "Can't create a time entry on a %{reason} %{thing}" + cant_assign_to_closed_deliverable: "Can't assign issue to a closed deliverable" + cant_assign_to_locked_deliverable: "Can't assign issue to a locked deliverable" + cant_assign_to_closed_contract: "Can't assign issue to a closed contract" + cant_assign_to_locked_contract: "Can't assign issue to a locked contract" + cant_update_locked_deliverable: "Can't update a locked deliverable" + cant_update_closed_deliverable: "Can't update a closed deliverable" + cant_update_locked_contract: "Can't update a locked contract" + cant_update_closed_contract: "Can't update a closed contract" + cant_create_deliverable_on_locked_contract: "Can't create a deliverable on a locked contract" + cant_create_deliverable_on_closed_contract: "Can't create a deliverable on a closed contract" + field_end_date: End Date field_executed: Executed text_contracts: Contracts @@ -81,4 +96,7 @@ en: text_error_message_orphaned_time: "There is {{amount}} worth of time clocked to issues that are not assigned to any deliverables." text_error_message_update_orphaned_time: "Please update the orphaned issues." field_estimated: Estimated - + text_deliverable_locked_warning: "This deliverable is locked and cannot be saved without changing it's status to Open." + text_deliverable_closed_warning: "This deliverable is closed and cannot be saved without changing it's status to Open." + text_contract_locked_warning: "This contract is locked and cannot be saved without changing it's status to Open." + text_contract_closed_warning: "This contract is closed and cannot be saved without changing it's status to Open." diff --git a/db/migrate/018_add_status_to_contracts.rb b/db/migrate/018_add_status_to_contracts.rb new file mode 100644 index 0000000..58d1823 --- /dev/null +++ b/db/migrate/018_add_status_to_contracts.rb @@ -0,0 +1,10 @@ +class AddStatusToContracts < ActiveRecord::Migration + def self.up + add_column :contracts, :status, :string + add_index :contracts, :status + end + + def self.down + remove_column :contracts, :status + end +end diff --git a/db/migrate/019_add_status_to_deliverables.rb b/db/migrate/019_add_status_to_deliverables.rb new file mode 100644 index 0000000..b4dbb7d --- /dev/null +++ b/db/migrate/019_add_status_to_deliverables.rb @@ -0,0 +1,10 @@ +class AddStatusToDeliverables < ActiveRecord::Migration + def self.up + add_column :deliverables, :status, :string + add_index :deliverables, :status + end + + def self.down + remove_column :deliverables, :status + end +end diff --git a/init.rb b/init.rb index 19e673c..ea166d9 100644 --- a/init.rb +++ b/init.rb @@ -62,6 +62,9 @@ end require 'dispatcher' Dispatcher.to_prepare :redmine_contracts do + + require_dependency 'time_entry' + TimeEntry.send(:include, RedmineContracts::Patches::TimeEntryPatch) gem 'inherited_resources', :version => '1.0.6' require_dependency 'inherited_resources' require_dependency 'inherited_resources/base' @@ -99,6 +102,9 @@ Dispatcher.to_prepare :redmine_contracts do unless Query.available_columns.collect(&:name).include?(:contract_name) Query.add_available_column(QueryColumn.new(:contract_name, :sortable => "#{Contract.table_name}.name", :groupable => 'contracts.name')) end + + require_dependency 'application_controller' + ApplicationController.send(:helper, :contracts) end require 'redmine_contracts/hooks/view_layouts_base_html_head_hook' diff --git a/lib/redmine_contracts/patches/issue_patch.rb b/lib/redmine_contracts/patches/issue_patch.rb index 4df6003..f1157ad 100644 --- a/lib/redmine_contracts/patches/issue_patch.rb +++ b/lib/redmine_contracts/patches/issue_patch.rb @@ -15,6 +15,24 @@ module RedmineContracts def contract_name contract.try(:name) end + + validate :validate_deliverable_status + validate :validate_contract_status + + def validate_deliverable_status + if deliverable.present? && changes["deliverable_id"].present? + errors.add_to_base(:cant_assign_to_closed_deliverable) if deliverable.closed? + errors.add_to_base(:cant_assign_to_locked_deliverable) if deliverable.locked? + end + end + + def validate_contract_status + if deliverable.present? && changes["deliverable_id"].present? && contract.present? + errors.add_to_base(:cant_assign_to_closed_contract) if contract.closed? + errors.add_to_base(:cant_assign_to_locked_contract) if contract.locked? + end + end + end end diff --git a/lib/redmine_contracts/patches/time_entry_patch.rb b/lib/redmine_contracts/patches/time_entry_patch.rb new file mode 100644 index 0000000..cca2541 --- /dev/null +++ b/lib/redmine_contracts/patches/time_entry_patch.rb @@ -0,0 +1,38 @@ +module RedmineContracts + module Patches + module TimeEntryPatch + def self.included(base) + base.extend(ClassMethods) + + base.send(:include, InstanceMethods) + base.class_eval do + unloadable + + validate :validate_deliverable_status + validate :validate_contract_status + + def validate_deliverable_status + if issue.present? && issue.deliverable.present? + errors.add_to_base("#{l(:"activerecord.errors.messages.cant_create_time_on_object", :reason => 'locked', :thing => 'deliverable')}") if issue.deliverable.locked? + errors.add_to_base("#{l(:"activerecord.errors.messages.cant_create_time_on_object", :reason => 'closed', :thing => 'deliverable')}") if issue.deliverable.closed? + end + end + + def validate_contract_status + if issue.present? && issue.deliverable.present? && issue.deliverable.contract.present? + errors.add_to_base("#{l(:"activerecord.errors.messages.cant_create_time_on_object", :reason => 'locked', :thing => 'contract')}") if issue.deliverable.contract.locked? + errors.add_to_base("#{l(:"activerecord.errors.messages.cant_create_time_on_object", :reason => 'closed', :thing => 'contract')}") if issue.deliverable.contract.closed? + end + end + + end + end + + module ClassMethods + end + + module InstanceMethods + end + end + end +end diff --git a/test/integration/contracts_edit_test.rb b/test/integration/contracts_edit_test.rb index 0e10ae7..d302cf4 100644 --- a/test/integration/contracts_edit_test.rb +++ b/test/integration/contracts_edit_test.rb @@ -54,6 +54,133 @@ class ContractsEditTest < ActionController::IntegrationTest assert_template 'contracts/show' assert_equal "An updated name", @contract.reload.name + end + context "locked contract" do + setup do + assert @contract.lock! + end + + should "block edits" do + visit_contract_page(@contract) + click_link 'Update' + assert_response :success + + fill_in "Name", :with => 'An updated name' + click_button 'Save Contract' + + assert_response :success + assert_template 'contracts/edit' + + assert_not_equal "An updated name", @contract.reload.name + end + + should "block edits even when the status is changed to closed" do + visit_contract_page(@contract) + click_link 'Update' + assert_response :success + + fill_in "Name", :with => 'An updated name' + select "Closed", :from => "Status" + click_button 'Save Contract' + + assert_response :success + assert_template 'contracts/edit' + + assert_not_equal "An updated name", @contract.reload.name + assert @contract.reload.locked? + end + + should "be allowed to change the status from locked to open" do + visit_contract_page(@contract) + click_link 'Update' + assert_response :success + + select "Open", :from => "Status" + click_button 'Save Contract' + + assert_response :success + assert_template 'contracts/show' + + assert @contract.reload.open? + end + + should "be allowed to change the status from locked to closed" do + visit_contract_page(@contract) + click_link 'Update' + assert_response :success + + select "Closed", :from => "Status" + click_button 'Save Contract' + + assert_response :success + assert_template 'contracts/show' + + assert @contract.reload.closed? + end + end + + context "closed contract" do + setup do + assert @contract.close! + end + + should "block edits" do + visit_contract_page(@contract) + click_link 'Update' + assert_response :success + + fill_in "Name", :with => 'An updated name' + click_button 'Save Contract' + + assert_response :success + assert_template 'contracts/edit' + + assert_not_equal "An updated name", @contract.reload.name + end + + should "block edits weven when the status is changed to locked" do + visit_contract_page(@contract) + click_link 'Update' + assert_response :success + + fill_in "Name", :with => 'An updated name' + select "Locked", :from => "Status" + click_button 'Save Contract' + + assert_response :success + assert_template 'contracts/edit' + + assert_not_equal "An updated name", @contract.reload.name + assert @contract.reload.closed? + end + + should "be allowed to change the status from closed to open" do + visit_contract_page(@contract) + click_link 'Update' + assert_response :success + + select "Open", :from => "Status" + click_button 'Save Contract' + + assert_response :success + assert_template 'contracts/show' + + assert @contract.reload.open? + end + + should "be allowed to change the status from closed to locked" do + visit_contract_page(@contract) + click_link 'Update' + assert_response :success + + select "Locked", :from => "Status" + click_button 'Save Contract' + + assert_response :success + assert_template 'contracts/show' + + assert @contract.reload.locked? + end end end diff --git a/test/integration/contracts_list_test.rb b/test/integration/contracts_list_test.rb index a80d290..4888b91 100644 --- a/test/integration/contracts_list_test.rb +++ b/test/integration/contracts_list_test.rb @@ -5,8 +5,10 @@ class ContractsListTest < ActionController::IntegrationTest def setup @project = Project.generate!(:identifier => 'main') - @contract = Contract.generate!(:project => @project) - @contract2 = Contract.generate!(:project => @project) + @contract = Contract.generate!(:project => @project, :name => 'Contract1').reload + @contract2 = Contract.generate!(:project => @project, :name => 'Contract2').reload + @contract_locked = Contract.generate!(:project => @project, :status => 'locked', :name => 'LockedContract').reload + @contract_closed = Contract.generate!(:project => @project, :status => 'closed', :name => 'ClosedContract').reload @other_project = Project.generate!(:identifier => 'other') @other_contract = Contract.generate!(:project => @other_project) @@ -44,10 +46,10 @@ class ContractsListTest < ActionController::IntegrationTest visit_contracts_for_project(@project) end - should "list all contracts for the project" do + should "list all contracts for the project grouped by status" do visit_contracts_for_project(@project) - assert_select "table#contracts" do + assert_select "table#contracts.open" do [@contract, @contract2].each do |contract| assert_select "td.id", :text => /#{contract.id}/ assert_select "td.name", :text => /#{contract.name}/ @@ -56,7 +58,23 @@ class ContractsListTest < ActionController::IntegrationTest assert_select "td.total-budget" end end - + + assert_select "table#contracts.locked" do + assert_select "td.id", :text => /#{@contract_locked.id}/ + assert_select "td.name", :text => /#{@contract_locked.name}/ + assert_select "td.account-executive", :text => /#{@contract_locked.account_executive.name}/ + assert_select "td.end-date", :text => /#{format_date(@contract_locked.end_date)}/ + assert_select "td.total-budget" + end + + assert_select "table#contracts.closed" do + assert_select "td.id", :text => /#{@contract_closed.id}/ + assert_select "td.name", :text => /#{@contract_closed.name}/ + assert_select "td.account-executive", :text => /#{@contract_closed.account_executive.name}/ + assert_select "td.end-date", :text => /#{format_date(@contract_closed.end_date)}/ + assert_select "td.total-budget" + end + end should "not list contracts from other projects" do diff --git a/test/integration/contracts_new_test.rb b/test/integration/contracts_new_test.rb index 399d970..6bf55c8 100644 --- a/test/integration/contracts_new_test.rb +++ b/test/integration/contracts_new_test.rb @@ -52,6 +52,7 @@ class ContractsNewTest < ActionController::IntegrationTest fill_in "Start", :with => '2010-01-01' fill_in "End Date", :with => '2010-12-31' select "Net 30", :from => "Payment Terms" + select "Locked", :from => "Status" click_button "Save Contract" @@ -64,6 +65,7 @@ class ContractsNewTest < ActionController::IntegrationTest assert_equal '2010-01-01', @contract.start_date.to_s assert_equal '2010-12-31', @contract.end_date.to_s assert_equal 'Net 30', @contract.payment_term.name + assert_equal "locked", @contract.status end end diff --git a/test/integration/deliverables_edit_test.rb b/test/integration/deliverables_edit_test.rb index 4082190..8d1b569 100644 --- a/test/integration/deliverables_edit_test.rb +++ b/test/integration/deliverables_edit_test.rb @@ -9,7 +9,7 @@ class DeliverablesEditTest < ActionController::IntegrationTest @manager = User.generate! @role = Role.generate! User.add_to_project(@manager, @project, @role) - @fixed_deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'The Title') + @fixed_deliverable = FixedDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'The Title', :notes => "", :feature_sign_off => false, :warranty_sign_off => false) @hourly_deliverable = HourlyDeliverable.generate!(:contract => @contract, :manager => @manager, :title => 'An Hourly') @user = User.generate_user_with_permission_to_manage_budget(:project => @project) @@ -49,6 +49,7 @@ class DeliverablesEditTest < ActionController::IntegrationTest within("#deliverable-details") do fill_in "Title", :with => 'An updated title' + select "Locked", :from => "Status" check "Feature Sign Off" check "Warranty Sign Off" end @@ -61,6 +62,7 @@ class DeliverablesEditTest < ActionController::IntegrationTest assert_equal "FixedDeliverable", @fixed_deliverable.reload.type assert @fixed_deliverable.reload.warranty_sign_off? assert @fixed_deliverable.reload.feature_sign_off? + assert_equal "locked", @fixed_deliverable.reload.status end @@ -78,6 +80,7 @@ class DeliverablesEditTest < ActionController::IntegrationTest within("#deliverable-details") do fill_in "Title", :with => 'An updated title' + select "Locked", :from => "Status" check "Feature Sign Off" check "Warranty Sign Off" end @@ -101,6 +104,7 @@ class DeliverablesEditTest < ActionController::IntegrationTest assert_equal "HourlyDeliverable", @hourly_deliverable.reload.type assert @hourly_deliverable.reload.warranty_sign_off? assert @hourly_deliverable.reload.feature_sign_off? + assert_equal "locked", @hourly_deliverable.reload.status assert_equal 1, @hourly_deliverable.labor_budgets.count @labor_budget = @hourly_deliverable.labor_budgets.first @@ -471,4 +475,240 @@ class DeliverablesEditTest < ActionController::IntegrationTest assert_equal 3, @retainer_deliverable.fixed_budgets.count assert_equal [600, nil, nil], @retainer_deliverable.fixed_budgets.collect(&:budget) end + + context "locked deliverable" do + setup do + assert @fixed_deliverable.lock! + end + + should "block edits to locked deliverables" do + visit_contract_page(@contract) + click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit' + assert_response :success + + within("#deliverable-details") do + fill_in "Title", :with => 'An updated title' + end + + click_button "Save" + + assert_response :success + assert_template 'deliverables/edit' + + assert_not_equal "An updated title", @fixed_deliverable.reload.title + end + + should "block edits to locked deliverables even when status changes to closed" do + visit_contract_page(@contract) + click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit' + assert_response :success + + within("#deliverable-details") do + fill_in "Title", :with => 'An updated title' + select "Closed", :from => "Status" + end + + click_button "Save" + + assert_response :success + assert_template 'deliverables/edit' + + assert_not_equal "An updated title", @fixed_deliverable.reload.title + assert @fixed_deliverable.reload.locked? + end + + should "be allowed to change the status on a locked deliverables to open" do + visit_contract_page(@contract) + click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit' + assert_response :success + + within("#deliverable-details") do + select "Open", :from => "Status" + end + + click_button "Save" + + assert_response :success + assert_template 'contracts/show' + + assert @fixed_deliverable.reload.open? + end + + should "be allowed to change the status on a locked deliverables to closed" do + visit_contract_page(@contract) + click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit' + assert_response :success + + within("#deliverable-details") do + select "Closed", :from => "Status" + end + + click_button "Save" + + assert_response :success + assert_template 'contracts/show' + + assert @fixed_deliverable.reload.closed? + end + + end + + context "closed deliverable" do + setup do + assert @fixed_deliverable.close! + end + + should "block edits to closed deliverables" do + visit_contract_page(@contract) + click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit' + assert_response :success + + within("#deliverable-details") do + fill_in "Title", :with => 'An updated title' + end + + click_button "Save" + + assert_response :success + assert_template 'deliverables/edit' + + assert_not_equal "An updated title", @fixed_deliverable.reload.title + end + + should "block edits to closed deliverables even when the status is changed to locked" do + visit_contract_page(@contract) + click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit' + assert_response :success + + within("#deliverable-details") do + fill_in "Title", :with => 'An updated title' + select "Locked", :from => "Status" + end + + click_button "Save" + + assert_response :success + assert_template 'deliverables/edit' + + assert_not_equal "An updated title", @fixed_deliverable.reload.title + assert @fixed_deliverable.reload.closed? + end + + should "be allowed to change the status on a closed deliverables to open" do + visit_contract_page(@contract) + click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit' + assert_response :success + + within("#deliverable-details") do + select "Open", :from => "Status" + end + + click_button "Save" + + assert_response :success + assert_template 'contracts/show' + + assert @fixed_deliverable.reload.open? + end + + should "be allowed to change the status on a closed deliverables to Locked" do + visit_contract_page(@contract) + click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit' + assert_response :success + + within("#deliverable-details") do + select "Locked", :from => "Status" + end + + click_button "Save" + + assert_response :success + assert_template 'contracts/show' + + assert @fixed_deliverable.reload.locked? + end + end + + context "a Deliverable on a locked Contract" do + setup do + assert @contract.lock! + end + + should "be blocked from editing" do + visit_contract_page(@contract) + click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit' + assert_response :success + + within("#deliverable-details") do + fill_in "Title", :with => 'An updated title' + end + + click_button "Save" + + assert_response :success + assert_template 'deliverables/edit' + + assert_not_equal "An updated title", @fixed_deliverable.reload.title + end + + should "allow status only changes" do + visit_contract_page(@contract) + click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit' + assert_response :success + + within("#deliverable-details") do + select "Locked", :from => "Status" + end + + click_button "Save" + + assert_response :success + assert_template 'contracts/show' + + assert @fixed_deliverable.reload.locked? + end + + end + + context "a Deliverable on a closed Contract" do + setup do + assert @contract.close! + end + + should "be blocked from editing" do + visit_contract_page(@contract) + click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit' + assert_response :success + + within("#deliverable-details") do + fill_in "Title", :with => 'An updated title' + end + + click_button "Save" + + assert_response :success + assert_template 'deliverables/edit' + + assert_not_equal "An updated title", @fixed_deliverable.reload.title + end + + should "allow status only changes" do + visit_contract_page(@contract) + click_link_within "#deliverable_details_#{@fixed_deliverable.id}", 'Edit' + assert_response :success + + within("#deliverable-details") do + select "Locked", :from => "Status" + end + + click_button "Save" + + assert_response :success + assert_template 'contracts/show' + + assert @fixed_deliverable.reload.locked? + end + + end + end diff --git a/test/integration/deliverables_new_test.rb b/test/integration/deliverables_new_test.rb index a55d5f8..97edac4 100644 --- a/test/integration/deliverables_new_test.rb +++ b/test/integration/deliverables_new_test.rb @@ -74,6 +74,7 @@ class DeliverablesNewTest < ActionController::IntegrationTest within("#deliverable-details") do fill_in "Title", :with => 'A New Deliverable' select "Fixed", :from => "Type" + select "Locked", :from => "Status" select @manager.name, :from => "Manager" fill_in "Start", :with => '2010-01-01' fill_in "End Date", :with => '2010-12-31' @@ -95,6 +96,7 @@ class DeliverablesNewTest < ActionController::IntegrationTest assert_equal '2010-12-31', @deliverable.end_date.to_s assert_equal @manager, @deliverable.manager assert_equal 1000.0, @deliverable.total.to_f + assert_equal "locked", @deliverable.status end should "create a new Hourly deliverable" do @@ -109,6 +111,7 @@ class DeliverablesNewTest < ActionController::IntegrationTest within("#deliverable-details") do fill_in "Title", :with => 'A New Deliverable' select "Hourly", :from => "Type" + select "Locked", :from => "Status" select @manager.name, :from => "Manager" fill_in "Start", :with => '2010-01-01' fill_in "End Date", :with => '2010-12-31' @@ -128,7 +131,8 @@ class DeliverablesNewTest < ActionController::IntegrationTest assert_equal '2010-01-01', @deliverable.start_date.to_s assert_equal '2010-12-31', @deliverable.end_date.to_s assert_equal @manager, @deliverable.manager - + assert_equal "locked", @deliverable.status + end should "create a new Retainer deliverable" do @@ -143,6 +147,7 @@ class DeliverablesNewTest < ActionController::IntegrationTest within("#deliverable-details") do fill_in "Title", :with => 'A New Deliverable' select "Retainer", :from => "Type" + select "Locked", :from => "Status" select @manager.name, :from => "Manager" fill_in "Start", :with => '2010-01-01' fill_in "End Date", :with => '2010-12-31' @@ -171,7 +176,8 @@ class DeliverablesNewTest < ActionController::IntegrationTest assert_equal '2010-01-01', @deliverable.start_date.to_s assert_equal '2010-12-31', @deliverable.end_date.to_s assert_equal @manager, @deliverable.manager - + assert_equal "locked", @deliverable.status + # Budget items, one per month labor_budgets = @deliverable.labor_budgets assert_equal 12, labor_budgets.length diff --git a/test/integration/redmine_contracts/hooks/controller_issues_edit_before_save.rb b/test/integration/redmine_contracts/hooks/controller_issues_edit_before_save.rb index 4b475b2..e865245 100644 --- a/test/integration/redmine_contracts/hooks/controller_issues_edit_before_save.rb +++ b/test/integration/redmine_contracts/hooks/controller_issues_edit_before_save.rb @@ -24,10 +24,10 @@ class RedmineContracts::Hooks::ControllerIssuesEditBeforeSaveTest < ActionContro context "for a new issue" do setup do visit_project(@project) - click_link "New issue" end should "set the issue's deliverable" do + click_link "New issue" fill_in "Subject", :with => 'Hook test' select @deliverable2.title, :from => "Deliverable" click_button "Create" @@ -38,6 +38,70 @@ class RedmineContracts::Hooks::ControllerIssuesEditBeforeSaveTest < ActionContro end + should "not allow setting a locked Deliverable" do + assert @deliverable2.lock! + click_link "New issue" + + fill_in "Subject", :with => 'Hook test' + select @deliverable2.title, :from => "Deliverable" + assert_no_difference("Issue.count") do + click_button "Create" + + assert_response :success + end + + assert_equal nil, Issue.last.deliverable + + end + + should "not allow setting a closed Deliverable" do + assert @deliverable2.close! + click_link "New issue" + + fill_in "Subject", :with => 'Hook test' + select @deliverable2.title, :from => "Deliverable" + assert_no_difference("Issue.count") do + click_button "Create" + + assert_response :success + end + + assert_equal nil, Issue.last.deliverable + + end + + should "not allow setting a Deliverable on a locked Contract" do + assert @contract2.lock! + click_link "New issue" + + fill_in "Subject", :with => 'Hook test' + select @deliverable2.title, :from => "Deliverable" + assert_no_difference("Issue.count") do + click_button "Create" + + assert_response :success + end + + assert_equal nil, Issue.last.deliverable + + end + + should "not allow setting a Deliverable on a closed Contract" do + assert @contract2.close! + click_link "New issue" + + fill_in "Subject", :with => 'Hook test' + select @deliverable2.title, :from => "Deliverable" + assert_no_difference("Issue.count") do + click_button "Create" + + assert_response :success + end + + assert_equal nil, Issue.last.deliverable + + end + context "with no permission to Assign Deliverable" do should "not allow setting the Deliverable (force HTTP request)" do @role.permissions.delete(:assign_deliverable_to_issue) @@ -69,6 +133,75 @@ class RedmineContracts::Hooks::ControllerIssuesEditBeforeSaveTest < ActionContro end + should "not allow updating to a locked deliverable" do + assert @deliverable2.lock! + select @deliverable2.title, :from => "Deliverable" + click_button "Submit" + + assert_response :success + + @issue.reload + assert_equal nil, @issue.deliverable + + end + + should "not allow updating to a closed deliverable" do + assert @deliverable2.close! + select @deliverable2.title, :from => "Deliverable" + click_button "Submit" + + assert_response :success + + @issue.reload + assert_equal nil, @issue.deliverable + + end + + should "not allow updating to a deliverable on a locked contract" do + assert @contract2.lock! + select @deliverable2.title, :from => "Deliverable" + click_button "Submit" + + assert_response :success + + @issue.reload + assert_equal nil, @issue.deliverable + + end + + should "not allow updating to a deliverable on a closed contract" do + assert @contract2.close! + select @deliverable2.title, :from => "Deliverable" + click_button "Submit" + + assert_response :success + + @issue.reload + assert_equal nil, @issue.deliverable + + end + + should "allow updating an issue, even if the deliverable is locked as long as the deliverable isn't changed" do + select @deliverable2.title, :from => "Deliverable" + click_button "Submit" + + assert_response :success + + @issue.reload + assert_equal @deliverable2, @issue.deliverable + + # Now normal update after locking + assert @deliverable2.lock! + fill_in "Subject", :with => 'Change subject' + click_button "Submit" + + assert_response :success + @issue.reload + assert_equal "Change subject", @issue.subject + assert_equal @deliverable2, @issue.deliverable + + end + context "with no permission to Assign Deliverable" do should "not allow setting the Deliverable (force HTTP request)" do @role.permissions.delete(:assign_deliverable_to_issue) diff --git a/test/integration/redmine_contracts/hooks/helper_issues_show_detail_after_setting_hook_test.rb b/test/integration/redmine_contracts/hooks/helper_issues_show_detail_after_setting_hook_test.rb index 9debdf2..c96dad8 100644 --- a/test/integration/redmine_contracts/hooks/helper_issues_show_detail_after_setting_hook_test.rb +++ b/test/integration/redmine_contracts/hooks/helper_issues_show_detail_after_setting_hook_test.rb @@ -19,15 +19,15 @@ class RedmineContracts::Hooks::HelperIssuesShowDetailAfterSettingHookTest < Acti # Set first @issue.init_journal(@manager) @issue.deliverable = @deliverable1 - @issue.save! + @issue.save! && @issue.reload # Change @issue.init_journal(@manager) @issue.deliverable = @deliverable2 - @issue.save! + @issue.save! && @issue.reload # Unset @issue.init_journal(@manager) @issue.deliverable = nil - @issue.save! + @issue.save! && @issue.reload login_as('manager', 'existing') diff --git a/test/integration/redmine_contracts/hooks/view_issues_bulk_edit_details_bottom_hook_test.rb b/test/integration/redmine_contracts/hooks/view_issues_bulk_edit_details_bottom_hook_test.rb index fd28f57..f33b8df 100644 --- a/test/integration/redmine_contracts/hooks/view_issues_bulk_edit_details_bottom_hook_test.rb +++ b/test/integration/redmine_contracts/hooks/view_issues_bulk_edit_details_bottom_hook_test.rb @@ -11,14 +11,25 @@ class RedmineContracts::Hooks::ViewIssuesBulkEditDetailsBottomHookTest < ActionC @issue3 = Issue.generate_for_project!(@project) @contract1 = Contract.generate!(:project => @project) @contract2 = Contract.generate!(:project => @project) + @locked_contract = Contract.generate!(:project => @project) + @closed_contract = Contract.generate!(:project => @project) @manager = User.generate!(:login => 'manager', :password => 'existing', :password_confirmation => 'existing') @role = Role.generate!(:permissions => [:view_issues, :edit_issues]) User.add_to_project(@manager, @project, @role) @deliverable1 = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'The Title') @deliverable2 = FixedDeliverable.generate!(:contract => @contract2, :manager => @manager, :title => 'The Title') + @locked_deliverable = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'Locked Deliverable', :status => 'locked') + @closed_deliverable = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'Closed Deliverable', :status => 'closed') + @deliverable1_on_locked_contract = FixedDeliverable.generate!(:contract => @locked_contract, :manager => @manager, :title => 'Deliverable 1 on locked contract') + @deliverable2_on_locked_contract = FixedDeliverable.generate!(:contract => @locked_contract, :manager => @manager, :title => 'Deliverable 2 on locked contract') + @deliverable_on_closed_contract = FixedDeliverable.generate!(:contract => @closed_contract, :manager => @manager, :title => 'Deliverable on closed contract') @issue.deliverable = @deliverable1 + # Set contract statuses now that all deliverables are created + assert @locked_contract.lock! + assert @closed_contract.close! + login_as('manager', 'existing') end @@ -42,6 +53,35 @@ class RedmineContracts::Hooks::ViewIssuesBulkEditDetailsBottomHookTest < ActionC end end end + + should "disable all locked deliverables" do + assert_select "select#deliverable_id" do + assert_select "option[disabled=disabled]", :text => /#{@locked_deliverable.title}/ + end + end + + should "disable all deliverables on locked contracts" do + assert_select "select#deliverable_id" do + assert_select "optgroup[label=?]", @locked_contract.name do + assert_select "option[disabled=disabled]", :text => /#{@deliverable1_on_locked_contract.title}/ + assert_select "option[disabled=disabled]", :text => /#{@deliverable2_on_locked_contract.title}/ + end + end + end + + should "not show closed deliverables" do + assert_select "select#deliverable_id" do + assert_select "option", :text => /#{@closed_deliverable.title}/, :count => 0 + end + end + + should "not show deliverables on closed contracts" do + assert_select "select#deliverable_id" do + assert_select "optgroup[label=?]", @closed_contract.name, :count => 0 + assert_select "option", :text => /#{@deliverable_on_closed_contract.title}/, :count => 0 + end + end + end context "with no permission to Assign Deliverable" do diff --git a/test/integration/redmine_contracts/hooks/view_issues_form_details_bottom_hook_test.rb b/test/integration/redmine_contracts/hooks/view_issues_form_details_bottom_hook_test.rb index e91b926..b2995e8 100644 --- a/test/integration/redmine_contracts/hooks/view_issues_form_details_bottom_hook_test.rb +++ b/test/integration/redmine_contracts/hooks/view_issues_form_details_bottom_hook_test.rb @@ -9,13 +9,25 @@ class RedmineContracts::Hooks::ViewIssuesFormDetailsBottomTest < ActionControlle @issue = Issue.generate_for_project!(@project) @contract1 = Contract.generate!(:project => @project) @contract2 = Contract.generate!(:project => @project) + @locked_contract = Contract.generate!(:project => @project) + @closed_contract = Contract.generate!(:project => @project) @manager = User.generate!(:login => 'manager', :password => 'existing', :password_confirmation => 'existing') @role = Role.generate!(:permissions => [:view_issues, :edit_issues]) User.add_to_project(@manager, @project, @role) - @deliverable1 = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'The Title') - @deliverable2 = FixedDeliverable.generate!(:contract => @contract2, :manager => @manager, :title => 'The Title') + @deliverable1 = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'Deliverable1') + @deliverable2 = FixedDeliverable.generate!(:contract => @contract2, :manager => @manager, :title => 'Deliverable2') + @locked_deliverable = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'Locked Deliverable', :status => 'locked') + @closed_deliverable = FixedDeliverable.generate!(:contract => @contract1, :manager => @manager, :title => 'Closed Deliverable', :status => 'closed') + @deliverable1_on_locked_contract = FixedDeliverable.generate!(:contract => @locked_contract, :manager => @manager, :title => 'Deliverable 1 on locked contract') + @deliverable2_on_locked_contract = FixedDeliverable.generate!(:contract => @locked_contract, :manager => @manager, :title => 'Deliverable 2 on locked contract') + @deliverable_on_closed_contract = FixedDeliverable.generate!(:contract => @closed_contract, :manager => @manager, :title => 'Deliverable on closed contract') @issue.deliverable = @deliverable1 + assert @issue.save + + # Set contract statuses now that all deliverables are created + assert @locked_contract.lock! + assert @closed_contract.close! login_as('manager', 'existing') end @@ -39,6 +51,78 @@ class RedmineContracts::Hooks::ViewIssuesFormDetailsBottomTest < ActionControlle end end end + + should "disable all locked deliverables" do + assert_select "select#issue_deliverable_id" do + assert_select "option[disabled=disabled]", :text => /#{@locked_deliverable.title}/ + end + end + + should "disable all deliverables on locked contracts" do + assert_select "select#issue_deliverable_id" do + assert_select "optgroup[label=?]", @locked_contract.name do + assert_select "option[disabled=disabled]", :text => /#{@deliverable1_on_locked_contract.title}/ + assert_select "option[disabled=disabled]", :text => /#{@deliverable2_on_locked_contract.title}/ + end + end + end + + should "not show closed deliverables" do + assert_select "select#issue_deliverable_id" do + assert_select "option", :text => /#{@closed_deliverable.title}/, :count => 0 + end + end + + should "not show deliverables on closed contracts" do + assert_select "select#issue_deliverable_id" do + assert_select "optgroup[label=?]", @closed_contract.name, :count => 0 + assert_select "option", :text => /#{@deliverable_on_closed_contract.title}/, :count => 0 + end + end + + should "show the assigned deliverable as an option, even if it's locked" do + @deliverable1.lock! + visit_issue_page(@issue) + + assert_select "select#issue_deliverable_id" do + assert_select "option[disabled=disabled]", :text => /#{@deliverable1.title}/, :count => 0 # Not disabled + assert_select "option", :text => /#{@deliverable1.title}/, :count => 1 # Present + end + + end + + should "show the assigned deliverable as an option, even if it's closed" do + @deliverable1.close! + visit_issue_page(@issue) + + assert_select "select#issue_deliverable_id" do + assert_select "option[disabled=disabled]", :text => /#{@deliverable1.title}/, :count => 0 # Not disabled + assert_select "option", :text => /#{@deliverable1.title}/, :count => 1 # Present + end + + end + + should "show the assigned deliverable as an option, even if it's contract is locked" do + @contract1.lock! + visit_issue_page(@issue) + + assert_select "select#issue_deliverable_id" do + assert_select "option[disabled=disabled]", :text => /#{@deliverable1.title}/, :count => 0 # Not disabled + assert_select "option", :text => /#{@deliverable1.title}/, :count => 1 # Present + end + + end + + should "show the assigned deliverable as an option, even if it's contract is closed" do + @contract1.close! + visit_issue_page(@issue) + + assert_select "select#issue_deliverable_id" do + assert_select "option[disabled=disabled]", :text => /#{@deliverable1.title}/, :count => 0 # Not disabled + assert_select "option", :text => /#{@deliverable1.title}/, :count => 1 # Present + end + + end end context "with no permission to Assign Deliverable" do diff --git a/test/unit/contract_test.rb b/test/unit/contract_test.rb index 1eae9a4..5e0242b 100644 --- a/test/unit/contract_test.rb +++ b/test/unit/contract_test.rb @@ -17,6 +17,9 @@ class ContractTest < ActiveSupport::TestCase should_allow_values_for :discount_type, "$", "%", nil, '' should_not_allow_values_for :discount_type, ["amount", "percent", "bar"] + should_allow_values_for :status, "", nil, 'open', 'locked', 'closed' + should_not_allow_values_for :status, "other", "things", "1" + context "end_date" do should "be after start_date" do @contract = Contract.new(:start_date => Date.today, :end_date => Date.yesterday) @@ -32,6 +35,12 @@ class ContractTest < ActiveSupport::TestCase assert_equal false, @contract.executed end + should "default status to open" do + @contract = Contract.new + + assert_equal "open", @contract.status + end + context "#labor_budget" do should "sum all of the labor budgets of the Deliverables" do contract = Contract.generate! diff --git a/test/unit/deliverable_test.rb b/test/unit/deliverable_test.rb index 694adc0..fa32573 100644 --- a/test/unit/deliverable_test.rb +++ b/test/unit/deliverable_test.rb @@ -12,6 +12,13 @@ class DeliverableTest < ActiveSupport::TestCase should_validate_presence_of :type should_validate_presence_of :manager + should_allow_values_for :status, "", nil, 'open', 'locked', 'closed' + should_not_allow_values_for :status, "other", "things", "1" + + should "default status to open" do + assert_equal "open", Deliverable.new.status + end + context "#total=" do should "strip dollar signs when writing" do d = Deliverable.new @@ -35,4 +42,45 @@ class DeliverableTest < ActiveSupport::TestCase end end + context "with a locked contract" do + should "block creating a new deliverable" do + contract = Contract.generate!(:status => "locked") + deliverable = FixedDeliverable.spawn(:contract => contract) + + assert !deliverable.valid? + assert deliverable.errors.on_base.include?("Can't create a deliverable on a locked contract") + end + + should "block deleting a deliverable" do + contract = Contract.generate! + deliverable = FixedDeliverable.generate!(:contract => contract).reload + assert contract.lock! + + assert_no_difference("Deliverable.count") do + deliverable.destroy + end + + end + end + + context "with a closed contract" do + should "block creating a new deliverable" do + contract = Contract.generate!(:status => "closed") + deliverable = FixedDeliverable.spawn(:contract => contract) + + assert !deliverable.valid? + assert deliverable.errors.on_base.include?("Can't create a deliverable on a closed contract") + end + + should "block deleting a deliverable" do + contract = Contract.generate! + deliverable = FixedDeliverable.generate!(:contract => contract).reload + assert contract.close! + + assert_no_difference("Deliverable.count") do + deliverable.destroy + end + + end + end end diff --git a/test/unit/lib/redmine_contracts/hooks/view_issues_show_details_bottom_hook_test.rb b/test/unit/lib/redmine_contracts/hooks/view_issues_show_details_bottom_hook_test.rb index 0e42dfb..845cccb 100644 --- a/test/unit/lib/redmine_contracts/hooks/view_issues_show_details_bottom_hook_test.rb +++ b/test/unit/lib/redmine_contracts/hooks/view_issues_show_details_bottom_hook_test.rb @@ -19,6 +19,9 @@ class RedmineContracts::Hooks::ViewIssuesShowDetailsBottomTest < ActionControlle @controller ||= ApplicationController.new @controller.class.send(:include, ::Redmine::I18n) @controller.response ||= ActionController::TestResponse.new + def @controller.api_request? + false + end # Hack to support render_on @controller.instance_variable_set('@template', template) @controller.response = response diff --git a/test/unit/lib/redmine_contracts/patches/time_entry_patch_test.rb b/test/unit/lib/redmine_contracts/patches/time_entry_patch_test.rb new file mode 100644 index 0000000..52c3d57 --- /dev/null +++ b/test/unit/lib/redmine_contracts/patches/time_entry_patch_test.rb @@ -0,0 +1,123 @@ +require File.dirname(__FILE__) + '/../../../../test_helper' + +class RedmineContracts::Patches::TimeEntryTest < ActionController::TestCase + + def setup + @project = Project.generate! + @contract = Contract.generate!(:project => @project, :status => 'open') + @deliverable = FixedDeliverable.generate!(:contract => @contract, :status => 'open').reload + @issue = Issue.generate_for_project!(@project, :deliverable => @deliverable).reload + assert_equal @deliverable, @issue.deliverable + @user = User.generate! + @role = Role.generate! + User.add_to_project(@user, @project, @role) + @activity = TimeEntryActivity.generate! + end + + def create_time_entry + @issue.reload + @time_entry = TimeEntry.create(:issue => @issue, + :project => @project, + :spent_on => Date.today, + :activity => @activity, + :hours => 10, + :user => @user) + end + + def assert_error_about_locked_deliverable(time_entry) + assert_equal "Can't create a time entry on a locked deliverable", time_entry.errors.on_base + end + + def assert_error_about_locked_contract(time_entry) + assert_equal "Can't create a time entry on a locked contract", time_entry.errors.on_base + end + + def assert_error_about_closed_deliverable(time_entry) + assert_equal "Can't create a time entry on a closed deliverable", time_entry.errors.on_base + end + + def assert_error_about_closed_contract(time_entry) + assert_equal "Can't create a time entry on a closed contract", time_entry.errors.on_base + end + + should "allow logging time to an issue on an open deliverable, open contract" do + assert_difference("TimeEntry.count") { create_time_entry } + end + + should "block logging time to an issue on a locked deliverable, open contract" do + assert @deliverable.lock! + assert @deliverable.locked? + + assert_no_difference("TimeEntry.count") { create_time_entry } + assert_error_about_locked_deliverable(@time_entry) + end + + should "block logging time to an issue on an open deliverable, locked contract" do + assert @contract.lock! + assert @contract.locked? + + assert_no_difference("TimeEntry.count") { create_time_entry } + assert_error_about_locked_contract(@time_entry) + end + + should "block logging time to an issue on a locked deliverable, locked contract" do + assert @deliverable.lock! + assert @deliverable.locked? + assert @contract.lock! + assert @contract.locked? + + assert_no_difference("TimeEntry.count") { create_time_entry } + assert @time_entry.errors.on_base.include?("Can't create a time entry on a locked deliverable") + assert @time_entry.errors.on_base.include?("Can't create a time entry on a locked contract") + end + + should "block logging time to an issue on a closed deliverable, open contract" do + assert @deliverable.close! + assert @deliverable.closed? + + assert_no_difference("TimeEntry.count") { create_time_entry } + assert_error_about_closed_deliverable(@time_entry) + end + + should "block logging time to an issue on a closed deliverable, locked contract" do + assert @deliverable.close! + assert @deliverable.closed? + assert @contract.lock! + assert @contract.locked? + + assert_no_difference("TimeEntry.count") { create_time_entry } + assert @time_entry.errors.on_base.include?("Can't create a time entry on a closed deliverable") + assert @time_entry.errors.on_base.include?("Can't create a time entry on a locked contract") + end + + should "block logging time to an issue on an open deliverable, closed contract" do + assert @contract.close! + assert @contract.closed? + + assert_no_difference("TimeEntry.count") { create_time_entry } + assert_error_about_closed_contract(@time_entry) + end + + should "block logging time to an issue on a locked deliverable, closed contract" do + assert @deliverable.lock! + assert @deliverable.locked? + assert @contract.close! + assert @contract.closed? + + assert_no_difference("TimeEntry.count") { create_time_entry } + assert @time_entry.errors.on_base.include?("Can't create a time entry on a locked deliverable") + assert @time_entry.errors.on_base.include?("Can't create a time entry on a closed contract") + end + + should "block logging time to an issue on a closed deliverable, closed contract" do + assert @deliverable.close! + assert @deliverable.closed? + assert @contract.close! + assert @contract.closed? + + assert_no_difference("TimeEntry.count") { create_time_entry } + assert @time_entry.errors.on_base.include?("Can't create a time entry on a closed deliverable") + assert @time_entry.errors.on_base.include?("Can't create a time entry on a closed contract") + end + +end
    <%= release(5, "Contract status") %>
    <%= h format_date(deliverable.end_date) %> <%= h deliverable.short_type %> <%= h deliverable.title %><%= release(5, "Deliverable status") %><%= h deliverable.status %> <%= h deliverable.manager.try(:name) %>