end
@versions = @project.versions.sort
end
+
+ def save_activities
+ if request.post? && params[:enumerations]
+ params[:enumerations].each do |id, activity|
+ @project.update_or_build_time_entry_activity(id, activity)
+ end
+ @project.save
+ end
+
+ redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
+ end
+
+ def reset_activities
+ @project.time_entry_activities.each do |time_entry_activity|
+ time_entry_activity.destroy
+ end
+ redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
+ end
def list_files
sort_init 'filename', 'asc'
{:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
{:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
{:name => 'repository', :action => :manage_repository, :partial => 'projects/settings/repository', :label => :label_repository},
- {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural}
+ {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural},
+ {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities}
]
tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
end
before_destroy :check_integrity
validates_presence_of :name
- validates_uniqueness_of :name, :scope => [:type]
+ validates_uniqueness_of :name, :scope => [:type, :project_id]
validates_length_of :name, :maximum => 30
# Backwards compatiblity named_scopes.
end
# End backwards compatiblity named_scopes
- named_scope :all, :order => 'position'
+ named_scope :all, :order => 'position', :conditions => { :project_id => nil }
named_scope :active, lambda {
{
def self.get_subclasses
@@subclasses[Enumeration]
end
+
+ # Does the +new+ Hash override the previous Enumeration?
+ def self.overridding_change?(new, previous)
+ if (same_active_state?(new['active'], previous.active)) && same_custom_values?(new,previous)
+ return false
+ else
+ return true
+ end
+ end
+
+ # Does the +new+ Hash have the same custom values as the previous Enumeration?
+ def self.same_custom_values?(new, previous)
+ previous.custom_field_values.each do |custom_value|
+ if custom_value.value != new["custom_field_values"][custom_value.custom_field_id.to_s]
+ return false
+ end
+ end
+
+ return true
+ end
+
+ # Are the new and previous fields equal?
+ def self.same_active_state?(new, previous)
+ new = (new == "1" ? true : false)
+ return new == previous
+ end
private
def check_integrity
STATUS_ACTIVE = 1
STATUS_ARCHIVED = 9
- has_many :time_entry_activities, :conditions => {:active => true } # Specific overidden Activities
+ # Specific overidden Activities
+ has_many :time_entry_activities do
+ def active
+ find(:all, :conditions => {:active => true})
+ end
+ end
has_many :members, :include => :user, :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
has_many :member_principals, :class_name => 'Member',
:include => :principal,
statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
end
- # Returns all the Systemwide and project specific activities
- def activities
- overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
+ # Returns the Systemwide and project specific activities
+ def activities(include_inactive=false)
+ if include_inactive
+ return all_activities
+ else
+ return active_activities
+ end
+ end
- if overridden_activity_ids.empty?
- return TimeEntryActivity.active
+ # Will build a new Project specific Activity or update an existing one
+ def update_or_build_time_entry_activity(id, activity_hash)
+ if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
+ self.build_time_entry_activity_if_needed(activity_hash)
else
- return system_activities_and_project_overrides
+ activity = project.time_entry_activities.find_by_id(id.to_i)
+ activity.update_attributes(activity_hash) if activity
+ end
+ end
+
+ # Builds new activity
+ def build_time_entry_activity_if_needed(activity)
+ # Only new override activities are built
+ if activity['parent_id']
+
+ parent_activity = TimeEntryActivity.find(activity['parent_id'])
+ activity['name'] = parent_activity.name
+ activity['position'] = parent_activity.position
+
+ if Enumeration.overridding_change?(activity, parent_activity)
+ self.time_entry_activities.build(activity)
+ end
end
end
@actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
end
- # Returns the systemwide activities merged with the project specific overrides
- def system_activities_and_project_overrides
- return TimeEntryActivity.active.
- find(:all,
- :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
- self.time_entry_activities
+ # Returns all the active Systemwide and project specific activities
+ def active_activities
+ overridden_activity_ids = self.time_entry_activities.active.collect(&:parent_id)
+
+ if overridden_activity_ids.empty?
+ return TimeEntryActivity.active
+ else
+ return system_activities_and_project_overrides
+ end
+ end
+
+ # Returns all the Systemwide and project specific activities
+ # (inactive and active)
+ def all_activities
+ overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
+
+ if overridden_activity_ids.empty?
+ return TimeEntryActivity.all
+ else
+ return system_activities_and_project_overrides(true)
+ end
+ end
+
+ # Returns the systemwide active activities merged with the project specific overrides
+ def system_activities_and_project_overrides(include_inactive=false)
+ if include_inactive
+ return TimeEntryActivity.all.
+ find(:all,
+ :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
+ self.time_entry_activities
+ else
+ return TimeEntryActivity.active.
+ find(:all,
+ :conditions => ["id NOT IN (?)", self.time_entry_activities.active.collect(&:parent_id)]) +
+ self.time_entry_activities.active
+ end
end
end
--- /dev/null
+<% form_tag({:controller => 'projects', :action => 'save_activities', :id => @project}, :class => "tabular") do %>
+
+<table class="list">
+ <tr>
+ <th><%= l(:field_name) %></th>
+ <th><%= l(:enumeration_system_activity) %></th>
+ <% TimeEntryActivity.new.available_custom_fields.each do |value| %>
+ <th><%= h value.name %></th>
+ <% end %>
+ <th style="width:15%;"><%= l(:field_active) %></th>
+ </tr>
+
+ <% @project.activities(true).each do |enumeration| %>
+ <% fields_for "enumerations[#{enumeration.id}]", enumeration do |ff| %>
+ <tr class="<%= cycle('odd', 'even') %>">
+ <td>
+ <%= ff.hidden_field :parent_id, :value => enumeration.id unless enumeration.project %>
+ <%= h(enumeration) %>
+ </td>
+ <td align="center" style="width:15%;"><%= image_tag('true.png') unless enumeration.project %></td>
+ <% enumeration.custom_field_values.each do |value| %>
+ <td align="center">
+ <%= custom_field_tag "enumerations[#{enumeration.id}]", value %>
+ </td>
+ <% end %>
+ <td align="center" style="width:15%;">
+ <%= ff.check_box :active %>
+ </td>
+ </tr>
+ <% end %>
+ <% end %>
+</table>
+
+<%= submit_tag l(:button_save) %>
+<% end %>
+<%= button_to l(:button_reset), {:controller => 'projects', :action => 'reset_activities', :id => @project}, :method => :delete, :confirm => l(:text_are_you_sure), :style => "position: relative; top: -20px; left: 60px;" %>
+
enumeration_issue_priorities: Issue priorities
enumeration_doc_categories: Document categories
enumeration_activities: Activities (time tracking)
+ enumeration_system_activity: System Activity
project_actions.connect 'projects/:id/files/new', :action => 'add_file'
project_actions.connect 'projects/:id/versions/new', :action => 'add_version'
project_actions.connect 'projects/:id/categories/new', :action => 'add_issue_category'
+ project_actions.connect 'projects/:id/activities/save', :action => 'save_activities'
+ end
+
+ projects.with_options :conditions => {:method => :delete} do |project_actions|
+ project_actions.conditions 'projects/:id/reset_activities', :action => 'reset_activities'
end
end
map.permission :view_time_entries, :timelog => [:details, :report]
map.permission :edit_time_entries, {:timelog => [:edit, :destroy]}, :require => :member
map.permission :edit_own_time_entries, {:timelog => [:edit, :destroy]}, :require => :loggedin
+ map.permission :manage_project_activities, {:projects => [:save_activities, :reset_activities]}, :require => :member
end
map.project_module :news do |map|
customized_id: 10
id: 15
value: true
+custom_values_016:
+ customized_type: Enumeration
+ custom_field_id: 7
+ customized_id: 11
+ id: 16
+ value: true
- :browse_repository
- :manage_repository
- :view_changesets
+ - :manage_project_activities
position: 1
roles_002:
- :view_changesets
position: 5
-
\ No newline at end of file
+
class ProjectsControllerTest < ActionController::TestCase
fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
:trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
- :attachments
+ :attachments, :custom_fields, :custom_values
def setup
@controller = ProjectsController.new
assert_response :success
assert_template 'show'
end
+
+ def test_reset_activities_routing
+ assert_routing({:method => :delete, :path => 'projects/64/reset_activities'},
+ :controller => 'projects', :action => 'reset_activities', :id => '64')
+ end
+
+ def test_reset_activities
+ @request.session[:user_id] = 2 # manager
+ project_activity = TimeEntryActivity.new({
+ :name => 'Project Specific',
+ :parent => TimeEntryActivity.find(:first),
+ :project => Project.find(1),
+ :active => true
+ })
+ assert project_activity.save
+ project_activity_two = TimeEntryActivity.new({
+ :name => 'Project Specific Two',
+ :parent => TimeEntryActivity.find(:last),
+ :project => Project.find(1),
+ :active => true
+ })
+ assert project_activity_two.save
+
+ delete :reset_activities, :id => 1
+ assert_response :redirect
+ assert_redirected_to 'projects/ecookbook/settings/activities'
+
+ assert_nil TimeEntryActivity.find_by_id(project_activity.id)
+ assert_nil TimeEntryActivity.find_by_id(project_activity_two.id)
+ end
+ def test_save_activities_routing
+ assert_routing({:method => :post, :path => 'projects/64/activities/save'},
+ :controller => 'projects', :action => 'save_activities', :id => '64')
+ end
+
+ def test_save_activities_to_override_system_activities
+ @request.session[:user_id] = 2 # manager
+ billable_field = TimeEntryActivityCustomField.find_by_name("Billable")
+
+ post :save_activities, :id => 1, :enumerations => {
+ "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design, De-activate
+ "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"}, # Development, Change custom value
+ "14"=>{"parent_id"=>"14", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"}, # Inactive Activity, Activate with custom value
+ "11"=>{"parent_id"=>"11", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"} # QA, no changes
+ }
+
+ assert_response :redirect
+ assert_redirected_to 'projects/ecookbook/settings/activities'
+
+ # Created project specific activities...
+ project = Project.find('ecookbook')
+
+ # ... Design
+ design = project.time_entry_activities.find_by_name("Design")
+ assert design, "Project activity not found"
+
+ assert_equal 9, design.parent_id # Relate to the system activity
+ assert_not_equal design.parent.id, design.id # Different records
+ assert_equal design.parent.name, design.name # Same name
+ assert !design.active?
+
+ # ... Development
+ development = project.time_entry_activities.find_by_name("Development")
+ assert development, "Project activity not found"
+
+ assert_equal 10, development.parent_id # Relate to the system activity
+ assert_not_equal development.parent.id, development.id # Different records
+ assert_equal development.parent.name, development.name # Same name
+ assert development.active?
+ assert_equal "0", development.custom_value_for(billable_field).value
+
+ # ... Inactive Activity
+ previously_inactive = project.time_entry_activities.find_by_name("Inactive Activity")
+ assert previously_inactive, "Project activity not found"
+
+ assert_equal 14, previously_inactive.parent_id # Relate to the system activity
+ assert_not_equal previously_inactive.parent.id, previously_inactive.id # Different records
+ assert_equal previously_inactive.parent.name, previously_inactive.name # Same name
+ assert previously_inactive.active?
+ assert_equal "1", previously_inactive.custom_value_for(billable_field).value
+
+ # ... QA
+ assert_equal nil, project.time_entry_activities.find_by_name("QA"), "Custom QA activity created when it wasn't modified"
+ end
+
+ def test_save_activities_will_update_project_specific_activities
+ @request.session[:user_id] = 2 # manager
+
+ project_activity = TimeEntryActivity.new({
+ :name => 'Project Specific',
+ :parent => TimeEntryActivity.find(:first),
+ :project => Project.find(1),
+ :active => true
+ })
+ assert project_activity.save
+ project_activity_two = TimeEntryActivity.new({
+ :name => 'Project Specific Two',
+ :parent => TimeEntryActivity.find(:last),
+ :project => Project.find(1),
+ :active => true
+ })
+ assert project_activity_two.save
+
+
+ post :save_activities, :id => 1, :enumerations => {
+ project_activity.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # De-activate
+ project_activity_two.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"} # De-activate
+ }
+
+ assert_response :redirect
+ assert_redirected_to 'projects/ecookbook/settings/activities'
+
+ # Created project specific activities...
+ project = Project.find('ecookbook')
+ assert_equal 2, project.time_entry_activities.count
+
+ activity_one = project.time_entry_activities.find_by_name(project_activity.name)
+ assert activity_one, "Project activity not found"
+ assert_equal project_activity.id, activity_one.id
+ assert !activity_one.active?
+
+ activity_two = project.time_entry_activities.find_by_name(project_activity_two.name)
+ assert activity_two, "Project activity not found"
+ assert_equal project_activity_two.id, activity_two.id
+ assert !activity_two.active?
+ end
+
# A hook that is manually registered later
class ProjectBasedTemplate < Redmine::Hook::ViewListener
def view_layouts_base_html_head(context)
end
def test_activities_should_handle_nils
+ overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
TimeEntryActivity.delete_all
+ # No activities
project = Project.find(1)
assert project.activities.empty?
+
+ # No system, one overridden
+ assert overridden_activity.save!
+ project.reload
+ assert_equal [overridden_activity], project.activities
end
def test_activities_should_override_system_activities_with_project_activities
assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
end
+ def test_activities_should_include_inactive_activities_if_specified
+ project = Project.find(1)
+ overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
+ assert overridden_activity.save!
+
+ assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
+ end
+
context "Project#copy" do
setup do
ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests