app/models/project.rb
#encoding: utf-8
#############################################################################
#
# Estimancy, Open Source project estimation web application
# Copyright (c) 2014 Estimancy (http://www.estimancy.com)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# ======================================================================
#
# ProjEstimate, Open Source project estimation web application
# Copyright (c) 2013 Spirula (http://www.spirula.fr)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
class Project < ActiveRecord::Base
attr_accessible :title, :description, :version, :alias, :state, :estimation_status_id, :status_comment,
:start_date, :is_model, :organization_id, :project_area_id, :project_category_id,
:acquisition_category_id, :platform_category_id, :parent_id
attr_accessor :product_name, :project_organization_statuses, :new_status_comment, :available_inline_columns
include ActionView::Helpers
include ActiveModel::Dirty
#require 'organization.rb'
has_ancestry # For the Ancestry gem
belongs_to :organization
belongs_to :project_area
belongs_to :acquisition_category
belongs_to :platform_category
belongs_to :project_category
belongs_to :creator, :class_name => 'User', :foreign_key => 'creator_id'
belongs_to :original_model, :class_name => 'Project', :foreign_key => 'original_model_id'
belongs_to :estimation_status
has_many :module_projects, :dependent => :destroy
has_many :pemodules, :through => :module_projects
has_many :project_securities, :dependent => :destroy
has_many :project_fields, :dependent => :destroy
has_and_belongs_to_many :groups
has_many :pe_wbs_projects
has_many :pbs_project_elements, :through => :pe_wbs_projects
has_many :wbs_project_elements, :through => :pe_wbs_projects
default_scope order('title ASC, version ASC')
serialize :included_wbs_activities, Array
validates_presence_of :organization_id, :estimation_status_id
validates :title, :presence => true, :uniqueness => { :scope => [:version,:organization_id], case_sensitive: false, :message => I18n.t(:error_validation_project) }
###validates :alias, :presence => true, :uniqueness => { :scope => :organization_id, case_sensitive: false, :message => I18n.t(:error_validation_project) }
validates :version, :presence => true, :length => { :maximum => 64 }, :uniqueness => { :scope => [:title, :organization_id], case_sensitive: false, :message => I18n.t(:error_validation_project) }
#Search fields
scoped_search :on => [:title, :alias, :description, :start_date, :created_at, :updated_at]
scoped_search :in => :organization, :on => :name
scoped_search :in => :pbs_project_elements, :on => :name
scoped_search :in => :wbs_project_elements, :on => [:name, :description]
amoeba do
enable
include_association [:pe_wbs_projects, :module_projects, :project_securities, :project_fields]
customize(lambda { |original_project, new_project|
new_copy_number = original_project.copy_number.to_i+1
new_project.copy_id = original_project.id
new_project.title = "#{original_project.title}(#{new_copy_number})" ###"Copy_#{ original_project.copy_number.to_i+1} of #{original_project.title}"
new_project.alias = "#{original_project.alias}(#{new_copy_number})" ###"Copy_#{ original_project.copy_number.to_i+1} of #{original_project.alias}"
#new_project.version = '1.0'
new_project.description = " #{original_project.description} \n \n This project is a duplication of project \"#{original_project.title} (#{original_project.alias}) - #{original_project.version}\" "
new_project.copy_number = 0
original_project.copy_number = new_copy_number
})
propagate
end
# get the selectable/available inline columns
class_attribute :available_inline_columns
self.available_inline_columns =
[
QueryColumn.new(:product_name, :sortable => "#{Project.table_name}.product_name", :caption => "label_product_name"),
#QueryColumn.new(:title, :sortable => "#{Project.table_name}.title", :caption => I18n.t(:label_project_name)),
QueryColumn.new(:version, :sortable => "#{Project.table_name}.version", :caption => "label_version"),
QueryColumn.new(:status_name, :sortable => "#{EstimationStatus.table_name}.name", :caption => "state"),
QueryColumn.new(:project_area, :sortable => "#{ProjectArea.table_name}.name", :caption => "project_area"),
QueryColumn.new(:project_category, :sortable => "#{ProjectCategory.table_name}.name", :caption => "category"),
QueryColumn.new(:acquisition_category, :sortable => "#{AcquisitionCategory.table_name}.name", :caption => "label_acquisition"),
QueryColumn.new(:platform_category, :sortable => "#{PlatformCategory.table_name}.name", :caption => "label_platform"),
QueryColumn.new(:description, :sortable => "#{Project.table_name}.description", :caption => "description"),
QueryColumn.new(:start_date, :sortable => "#{Project.table_name}.start_date", :caption => "label_date"),
QueryColumn.new(:creator, :sortable => "#{User.table_name}.first_name", :caption => "author"),
QueryColumn.new(:created_at, :sortable => "#{Project.table_name}.created_at", :caption => "created_at"),
QueryColumn.new(:updated_at, :sortable => "#{Project.table_name}.updated_at", :caption => "updated_at")
]
#class_attribute :selected_inline_columns
#self.selected_inline_columns = update_selected_inline_columns(Project)
#self.selected_inline_columns = self.available_inline_columns.select{ |column| column.name.to_s.in?(@current_organization.project_selected_columns)}
class_attribute :default_selected_columns
self.default_selected_columns = ["product_name", "version", "start_date", "status_name", "description"]
def self.selectable_inline_columns
[
[I18n.t(:label_product_name), "product_name"], [I18n.t(:label_project_name), "title"], [I18n.t(:label_version),"version"],
[I18n.t(:state), "estimation_status_id"], [ I18n.t(:project_area), "project_area_id"], [I18n.t(:category), "project_category_id"],
[I18n.t(:label_acquisition), "acquisition_category_id"], [I18n.t(:label_platform), "platform_category_id"], [I18n.t(:description), "description"],
[I18n.t(:start_date), "start_date"], [I18n.t(:author), "creator_id"], [I18n.t(:created_at), "created_at"], [I18n.t(:updated_at), "updated_at"]
]
end
#Get the project's WBS-Activity
def project_wbs_activity
project_wbs_activity = nil
# Project pe_wbs_activity
pe_wbs_activity = self.pe_wbs_projects.activities_wbs.first
# Get the wbs_project_element which contain the wbs_activity_ratio
project_wbs_project_elt_root = pe_wbs_activity.wbs_project_elements.elements_root.first
# If we manage more than one wbs_activity per project, this will be depend on the wbs_project_element ancestry(witch has the wbs_activity_ratio)
wbs_project_elt_with_ratio = project_wbs_project_elt_root.children.where('is_added_wbs_root = ?', true).first
if wbs_project_elt_with_ratio
project_wbs_activity = wbs_project_elt_with_ratio.wbs_activity # Select only Wbs-Activities affected to current project's organization
end
project_wbs_activity
end
# Estimation status name
def status_name
self.estimation_status.nil? ? nil : self.estimation_status.name
end
def author
self.creator_id.nil? ? "" : self.creator
end
# The status background color for estimations list
def status_background_color
self.estimation_status.nil? ? "#999999" : "##{self.estimation_status.status_color}"
end
# Estimation statuses possible transitions according to the project status
def project_estimation_statuses(organization=nil)
if new_record? || self.estimation_status.nil? #|| !self.organization.estimation_statuses.include?(self.estimation_status)
# For new record
if organization.nil?
nil
else
initial_status = organization.estimation_statuses.order(:status_number).first_or_create(organization_id: organization.id, status_number: 0, status_alias: 'preliminary', name: 'Préliminaire', status_color: 'F5FFFD')
[[initial_status.name, initial_status.id]]
end
#nil
else
estimation_statuses = self.estimation_status.to_transition_statuses
estimation_statuses << self.estimation_status
estimation_statuses.uniq.sort{|s1, s2| s1 <=> s2 }
end
end
def get_project_organization_statuses
self.project_organization_statuses = self.organization.estimation_statuses
initial_state = EstimationStatus.order(:status_number).first
# Define existing estimation_status as aasm_state
self.project_organization_statuses.all.each do |status|
#aasm.state status.status_alias.to_sym
aasm do # defaults to aasm_state
state status.status_alias.to_sym
# Workflow definition for the commit event # Redesign the 'commit' event AASM workflow with the estimation_statuses workflow
event :commit do
# generate workflow according to the defining workflow in organizations
StatusTransition.all.each do |status_transition|
to_transition_status = EstimationStatus.find(status_transition.to_transition_status_id)
from_transitions = to_transition_status.from_transition_statuses.map(&:status_alias).map(&:to_sym)
transitions :from => from_transitions, :to => to_transition_status.status_alias.to_sym
end
end
end
end
end
def self.encoding
['Big5', 'CP874', 'CP932', 'CP949', 'gb18030', 'ISO-8859-1', 'ISO-8859-13', 'ISO-8859-15', 'ISO-8859-2', 'ISO-8859-8', 'ISO-8859-9', 'UTF-8', 'Windows-874']
end
#Return the root pbs_project_element of the pe-wbs-project and consequently of the project.
def root_component
self.pe_wbs_projects.products_wbs.first.pbs_project_elements.select { |i| i.is_root = true }.first unless self.pe_wbs_projects.products_wbs.first.nil?
end
def wbs_project_element_root
self.pe_wbs_projects.activities_wbs.first.wbs_project_elements.select { |i| i.is_root = true }.first unless self.pe_wbs_projects.activities_wbs.first.nil?
end
#Override
def to_s
"#{title} - #{version}"
end
# Change project status according to the project's organization estimation statuses
def commit_status
#Get the project's current status
current_status_number = self.estimation_status.status_number
# According to the status transitions map, only possible statuses will consider
possible_statuses = self.project_estimation_statuses.map(&:status_number).sort #self.estimation_status.to_transition_statuses.map(&:status_number).uniq.sort
current_status_index = possible_statuses.index(current_status_number)
# By default the first possible status is candidate
next_status_number = possible_statuses.first
# If the current status is not the last element of the array, the next status is next element after the current status
if current_status_number != possible_statuses.last
next_status_number = possible_statuses[current_status_index+1]
end
begin
# Get the next status
next_status = self.organization.estimation_statuses.find_by_status_number(next_status_number)
self.update_attribute(:estimation_status_id, next_status.id)
rescue
end
end
#Return project value
def project_value(attr)
self.send(attr.project_value.gsub('_id', ''))
end
def self.table_search(search)
if search
where('title LIKE ? or alias LIKE ? or state LIKE ?', "%#{search}%", "%#{search}%", "%#{search}%")
else
scoped
end
end
#Estimation plan o project is locked or not?
def locked?
(self.is_locked.nil? or self.is_locked == true) ? true : false
end
def in_frozen_status?
(self.state.in?(%w(rejected released checkpoint))) ? true : false
end
def self.json_tree(nodes)
nodes.map do |node, sub_nodes|
#{:id => node.id.to_s, :name => node.title, :title => node.title, :version => node.version, :data => {}, :children => json_tree(sub_nodes).compact}
#{id: node.id.to_s, name: node.title, title: node.title, version: node.version, data: {}, children: json_tree(sub_nodes).compact}
{:id => node.id.to_s, :name => node.version, :data => {:title => node.title, :version => node.version, :state => node.status_name.to_s}, :children => json_tree(sub_nodes).compact}
end
end
# Method that execute the duplication core
def self.execute_duplication_SAVE_NOT_WORKING(project_id, parameters, create_from_template = nil)
#Project.transaction do
begin
old_prj = Project.find(project_id)
new_prj = old_prj.amoeba_dup #amoeba gem is configured in Project class model
new_prj.ancestry = nil
new_prj.is_model = false
#if creation from template
if !create_from_template.nil? #!parameters[:create_project_from_template].nil?
new_prj.original_model_id = old_prj.id
#Update some parameters with the form input data
new_prj.title = parameters['project']['title']
new_prj.alias = parameters['project']['alias']
new_prj.version = parameters['project']['version']
new_prj.description = parameters['project']['description']
start_date = (parameters['project']['start_date'].nil? || parameters['project']['start_date'].blank?) ? Time.now.to_date : parameters['project']['start_date']
new_prj.start_date = start_date
#Only the securities for the generated project will be taken in account
new_prj.project_securities = new_prj.project_securities.where(is_model_permission: [false, nil])
end
if new_prj.save
old_prj.save #Original project copy number will be incremented to 1
#Update the project securities for the current user who create the estimation from model
#if parameters[:action_name] == "create_project_from_template"
if !create_from_template.nil? #!parameters[:create_project_from_template].nil?
creator_securities = old_prj.creator.project_securities_for_select(new_prj.id)
unless creator_securities.nil?
creator_securities.update_attribute(:user_id, current_user.id)
end
end
#Managing the component tree : PBS
pe_wbs_product = new_prj.pe_wbs_projects.products_wbs.first
# For PBS
new_prj_components = pe_wbs_product.pbs_project_elements
new_prj_components.each do |new_c|
new_ancestor_ids_list = []
new_c.ancestor_ids.each do |ancestor_id|
ancestor_id = PbsProjectElement.find_by_pe_wbs_project_id_and_copy_id(new_c.pe_wbs_project_id, ancestor_id).id
new_ancestor_ids_list.push(ancestor_id)
end
new_c.ancestry = new_ancestor_ids_list.join('/')
new_c.save
end
# For ModuleProject associations
old_prj.module_projects.group(:id).each do |old_mp|
new_mp = ModuleProject.find_by_project_id_and_copy_id(new_prj.id, old_mp.id)
# ModuleProject Associations for the new project
old_mp.associated_module_projects.each do |associated_mp|
new_associated_mp = ModuleProject.where('project_id = ? AND copy_id = ?', new_prj.id, associated_mp.id).first
new_mp.associated_module_projects << new_associated_mp
end
#Copy the views and widgets for the new project
new_view = View.create(organization_id: new_prj.organization_id, name: "#{new_prj.to_s} : view for #{new_mp.to_s}", description: "Please rename the view's name and description if needed.")
#We have to copy all the selected view's widgets in a new view for the current module_project
if old_mp.view
old_mp_view_widgets = old_mp.view.views_widgets.all
old_mp_view_widgets.each do |view_widget|
new_view_widget_mp = ModuleProject.find_by_project_id_and_copy_id(new_prj.id, view_widget.module_project_id)
new_view_widget_mp_id = new_view_widget_mp.nil? ? nil : new_view_widget_mp.id
widget_est_val = view_widget.estimation_value
unless widget_est_val.nil?
in_out = widget_est_val.in_out
widget_pe_attribute_id = widget_est_val.pe_attribute_id
unless new_view_widget_mp.nil?
new_estimation_value = new_view_widget_mp.estimation_values.where('pe_attribute_id = ? AND in_out=?', widget_pe_attribute_id, in_out).last
estimation_value_id = new_estimation_value.nil? ? nil : new_estimation_value.id
widget_copy = ViewsWidget.create(view_id: new_view.id, module_project_id: new_view_widget_mp_id, estimation_value_id: estimation_value_id, name: view_widget.name, show_name: view_widget.show_name,
icon_class: view_widget.icon_class, color: view_widget.color, show_min_max: view_widget.show_min_max, widget_type: view_widget.widget_type,
width: view_widget.width, height: view_widget.height, position: view_widget.position, position_x: view_widget.position_x, position_y: view_widget.position_y)
pf = ProjectField.where(project_id: new_prj.id, views_widget_id: view_widget.id).first
unless pf.nil?
pf.views_widget_id = widget_copy.id
pf.save
end
end
end
end
end
#update the new module_project view
new_mp.update_attribute(:view_id, new_view.id)
#Update the Unit of works's groups
new_mp.guw_unit_of_work_groups.each do |guw_group|
new_pbs_project_element = new_prj_components.find_by_copy_id(guw_group.pbs_project_element_id)
new_pbs_project_element_id = new_pbs_project_element.nil? ? nil : new_pbs_project_element.id
guw_group.update_attribute(:pbs_project_element_id, new_pbs_project_element_id)
# Update the group unit of works and attributes
guw_group.guw_unit_of_works.each do |guw_uow|
new_uow_mp = ModuleProject.find_by_project_id_and_copy_id(new_prj.id, guw_uow.module_project_id)
new_uow_mp_id = new_uow_mp.nil? ? nil : new_uow_mp.id
new_pbs = new_prj_components.find_by_copy_id(guw_uow.pbs_project_element_id)
new_pbs_id = new_pbs.nil? ? nil : new_pbs.id
guw_uow.update_attributes(module_project_id: new_uow_mp_id, pbs_project_element_id: new_pbs_id)
end
end
new_mp.uow_inputs.each do |uo|
new_pbs_project_element = new_prj_components.find_by_copy_id(uo.pbs_project_element_id)
new_pbs_project_element_id = new_pbs_project_element.nil? ? nil : new_pbs_project_element.id
uo.update_attribute(:pbs_project_element_id, new_pbs_project_element_id)
end
["input", "output"].each do |io|
new_mp.pemodule.pe_attributes.each do |attr|
old_prj.pbs_project_elements.each do |old_component|
new_prj_components.each do |new_component|
ev = new_mp.estimation_values.where(pe_attribute_id: attr.id, in_out: io).first
unless ev.nil?
ev.string_data_low[new_component.id.to_i] = ev.string_data_low.delete old_component.id
ev.string_data_most_likely[new_component.id.to_i] = ev.string_data_most_likely.delete old_component.id
ev.string_data_high[new_component.id.to_i] = ev.string_data_high.delete old_component.id
ev.string_data_probable[new_component.id.to_i] = ev.string_data_probable.delete old_component.id
ev.save
end
end
end
end
end
end
else
new_prj = nil
end
rescue
#raise ActiveRecord::Rollback
new_prj = nil
end
new_prj
#end
end
end