# SPDX-License-Identifier: Apache-2.0 # Copyright (C) 2021-2022 OKTET Labs Ltd. All rights reserved. # # Class Project for Diary Management Application. # PROJECT_TEMPLATE = AmritaTemplate.new("project.html") require 'dbi' require_relative 'diary_datamapper' require_relative 'diary_policy' class Project STATUS = ["NEW", "ACTIVE", "TERMINATED"] STATUS_DEFAULT = "ACTIVE" @@mapper = nil def initialize(id = nil, values = nil) @id = id @attributes = values ? values : Hash.new @cc = nil @approvals = nil end def self.get(id) mapper.find(id) end def self.where(clause, *args) mapper.where(clause, *args) end def save_approvals self.class.mapper.save_approvals(@id, @approvals) end def approvals return @approvals unless @approvals.nil? @approvals = self.class.mapper.find_approvals(@id) end def id @id end def [](key) @attributes[key] end def <=>(rhs) to_s <=> rhs.to_s end def to_s @attributes["customer"] + "/" + @attributes["tag"] end def self.mapper return @@mapper unless @@mapper.nil? @@mapper = ProjectMapper.new end def customer Organization.find_or_create(@attributes["customer"]) end # we should not hardcode things def transform_links(text) return text end def self.all(args) policy = DiaryEnv.instance.policy res = mapper.all(args) res = res & policy.project_list if policy.restriction.include?(:project) return res end end class ProjectMapper < DataMapper def initialize super("project") end def instantiate(id, values) Project.new(id, values) end def insert(prj) super(prj) if prj.cc and prj.cc.length > 0 @@database.do("insert into cc_list (prj_id, person) values " + (["(#{prj.id}, ?)"] * prj.cc.length).join(", "), *prj.cc); end save_approvals(prj.id, prj.approvals) end def find_approvals(id) app = Hash.new @@database.query("select * from approval where prj_id = '%s'" % id).collect { |row| app[row["who"]] = row["approver"] } app end def save_approvals(id, app) cmd = "delete from approval where prj_id = '%s'" % id.to_s @@database.query(cmd) app.each do |who, approver| cmd = "insert into approval (prj_id, who, approver) values ('%s', '%s', '%s')" % [@@database.escape(id.to_s), @@database.escape(who.to_s), @@database.escape(approver.to_s)] @@database.query(cmd) end end end class ProjectUI < Hash PERSONS = %w(manager leader cust_man cust_tech) MANDATORY = %w(tag name prj_status manager customer cust_man prj_desc hide_hrs) OPTIONAL = %w(status_desc issues leader cust_tech maillists svndir storagedir bugzilla_prod extra_rights) def status_list(selected) MyCGI::list_options( Project::STATUS.collect {|status| [status.to_s, status.to_s]}, selected) end def update(id = @id) @id = id self.clear if @id DataMapper.database.query("select * from project where id = #{@id}") do |q| ### For some reason MySQL driver always returns 0 in q.rows # if q.rows != 1 # raise "Project #{@id} has #{q.rows} instances" # end row = q.fetch_hash raise "Project #{@id} has no instances" if not row self.replace(row) self.delete("id") end self["cc"] = Array.new DataMapper.database.query("select person from cc_list where " + "prj_id = #{@id}").collect do |row| self["cc"].push(row["person"]) end end true end def initialize(cgi) @id = nil @cgi = cgi @user = DiaryEnv.instance.user @policy = DiaryEnv.instance.policy end def create(data) return unless @policy.can_edit_project? #raise "Create project #{data["name"]}" inst = data.keys inst -= ["cc"] cmd = ("insert into project (" + inst.join(", ") + ") values (" + (inst.collect do "'%s'" end).join(", ") + ")") % (inst.collect do |field| DataMapper.database.escape(data[field]) end) DataMapper.database.query(cmd) @id = DataMapper.database.query("select last_insert_id() as id").first["id"] if data["cc"].length > 0 DataMapper.database.do("insert into cc_list (prj_id, person) values " + (["(#{@id}, ?)"] * data["cc"].length).join(", "), *data["cc"]); end # Send notifications XXX end def modify(current, data) return if not @policy.can_edit_project?(@id) changes = Array.new data.each_key do |field| if data[field] != current[field] changes.push(field) end end if (changes - ["cc"]).length > 0 cmd = ("update project set " + (changes.collect do |field| field + " = %s" if field != "cc" end).join(", ") + " where id = #{@id}") % (changes.collect do |field| if data[field].class == DateTime t = "STR_TO_DATE('%s'" % data[field] + ", '%Y-%m-%dT%H:%i:%s+00:00')" else t = "'%s'" % DataMapper.database.escape(data[field].to_s) end t end) DataMapper.database.query(cmd); end if (current["cc"] - data["cc"]).length > 0 DataMapper.database.do("delete from cc_list where " + "prj_id = #{@id} and (" + (["person = ?"] * (current["cc"] - data["cc"]).length ).join(" or ") + ")", *(current["cc"] - data["cc"])) end if (data["cc"] - current["cc"]).length > 0 DataMapper.database.do("insert into cc_list (prj_id, person) " + "values " + (["(#{@id}, ?)"] * (data["cc"] - current["cc"]).length ).join(", "), *(data["cc"] - current["cc"])) end # Send notifications XXX #@people.notify("mis", "Project '#{data["name"]}' is changed") end # Set project data to SQL tables. def set(data) MANDATORY.each do |field| if not data[field] or data[field].length == 0 raise "Mandatory field #{field} is not set" end end data.each_key do |field| if not MANDATORY.include?(field) and not OPTIONAL.include?(field) and field != "cc" raise "Field #{field} is unknown" end end data["svndir"].sub!(/^\/*/, '') data["storagedir"].sub!(/^\/*/, '') data = data.delete_if do |field, value| value.length == 0 end data.each do |field, value| data[field] = value end PERSONS.each do |pers| if data[pers] and not Person.find(data[pers]) raise "No such person #{pers}: #{data[pers]} " end end if not data["cc"] data["cc"] = Array.new end if @id modify(self, data) else create(data) end end attr_reader :id def list return [] if @cgi.tree_params["prj"]["list"]["prj_status"].empty? args = Hash.new customer = @cgi.tree_params["prj"]["list"]["customer"] args[:customer] = customer if customer.is_a?(Array) and not customer.include?("*") args[:prj_status] = @cgi.tree_params["prj"]["list"]["prj_status"] || [Project::STATUS_DEFAULT] Project.all(args).collect do |prj| a(:__id__ => MyCGI::element_id(prj.id)){ list_item(prj, @cgi.tree_params["prj"]["list"]["edit"]. include?(prj.id.to_s))} end end def list_item(values = nil, edit = false) if (values == nil) edit = add = true values = Hash.new else add = false end if not @policy.can_edit_project?(@id) edit = add = false end if edit edit_data = { :hiddens => @cgi.hiddens(@cgi.tree_flatten(["prj", "list"]). delete_if do |x| x == ["prj:list:edit", values["id"].to_s] end), :name => a(:value => values["name"]), :tag => a(:value => values["tag"], :size => Person::MAX_LENGTH), :customer => MyCGI::list_options(Organization.all.collect { |org| [org.uid, org.o] }, values["customer"]), :cust_man => a(:value => values["cust_man"], :size => Person::MAX_LENGTH), :cust_tech => a(:value => values["cust_tech"], :size => Person::MAX_LENGTH), :manager => a(:value => values["manager"] || DiaryEnv.director[0].uid, :size => Person::MAX_LENGTH), :leader => a(:value => values["leader"], :size => Person::MAX_LENGTH), :prj_desc => values["prj_desc"] || "", :prj_status => status_list(@cgi.tree_params["prj"]["list"]["prj_status"]), :status_desc => values["status_desc"] || "", :issues => values["issues"] || "", :maillists => a(:value => values["maillists"]), :storagedir => a(:value => values["storagedir"]), :svndir => a(:value => values["svndir"]), :extra_rights => a(:value => values["extra_rights"]), :bugzilla_prod => a(:value => values["bugzilla_prod"]), # :cclist => MyCGI::list_options(values["cc"] ? values["cc"] : []) :hide_hrs => a(:checked => values["hide_hrs"] ? "checked" : nil) } edit_data[:id] = a(:value => values["id"]) if values["id"] return { :edit => edit_data } else view = { :id => a(:name => values["id"]), :edit_id => a(:value => values["id"]), :hiddens => @cgi.hiddens(@cgi.tree_flatten(["prj","list"])), :name => values["name"], :tag => values["tag"], :customer => Organization.find_or_create(values["customer"]).to_html, :cust_man => Person.find_or_create(values["cust_man"]).to_html, :manager => Person.find_or_create(values["manager"]).to_html, :prj_desc => values["prj_desc"], :prj_status => values["prj_status"] } if @policy.can_edit_project?(@id) approval_items = MyCGI::list_options( values.approvals.collect do |who, approver| [ who, "#{Person.find_or_create(who).to_s} => " + "#{Person.find_or_create(approver).to_s}"] end) view[:approval] = { :person => MyCGI::list_options( Person.find_by_org(DiaryEnv::HOME_ORGANIZATION).sort.collect { |x| [x.uid, x.to_s] }) } view[:approval][:approval_list] = { :approval_item => approval_items } if not approval_items.empty? end view[:edit] = {} if @policy.can_edit_project?(@id) view[:cust_tech] = MyCGI::optional( Person.find_or_create(values["cust_tech"]).to_html) if values["cust_tech"] view[:leader] = MyCGI::optional(Person.find_or_create(values["leader"]).to_html) if values["leader"] view[:status_desc] = MyCGI::optional(values["status_desc"]) if values["status_desc"] view[:issues] = MyCGI::optional(values["issues"]) if values["issues"] view[:maillists] = MyCGI::optional(values["maillists"]) if values["maillists"] view[:storagedir] = MyCGI::optional(a(:href => "/storage/" + values["storagedir"]){values["storagedir"]}) if values["storagedir"] view[:svndir] = MyCGI::optional(a(:href => "/svnroot/" + values["svndir"]){ values["svndir"] }) if values["svndir"] view[:extra_rights] = MyCGI::optional(values["extra_rights"]) if values["extra_rights"] return { :view => a(:action => "#" + MyCGI::element_id(values["id"])){ view }} end end def menu return { :status => status_list( @cgi.tree_params["prj"]["list"]["prj_status"].empty? ? Project::STATUS_DEFAULT : @cgi.tree_params["prj"]["list"]["prj_status"]), :customer => MyCGI::list_options([["*", "- All -"]] + @policy.customer_list.collect { |org| [org.uid, org.to_s] }, @cgi.tree_params["prj"]["list"]["customer"]) } end def add_approval(id, who, approver) # TODO: user rights prj = Project.get(id) raise "No project #{id}" unless prj raise "You have no rights to edit project" unless @policy.can_edit_approval?(prj) prj.approvals[who] = approver prj.save_approvals end def delete_approval(id, who) who = [who] unless who.is_a? Array # TODO: user rights prj = Project.get(id) raise "No project #{id}" unless prj raise "You have no rights to edit project" unless @policy.can_edit_approval?(prj) raise NeedConfirm.new("cancel_approval"), "Cancelling the approval mode will approve " + "ALL diary entries of " + "#{who.collect{ |x| Person.find(x).to_s }.join(", ")} " + "in project \"#{prj["name"]} (#{prj.to_s})\"!" if not DiaryEnv.confirmed?("cancel_approval") who.each do |x| prj.approvals.delete(x) Diary.approve_all(prj, x) end prj.save_approvals end def action if @cgi.tree_params["prj"].include?("approve") id = @cgi.tree_params["prj"]["action"]["id"] case @cgi.tree_params["prj"]["action"]["verb"] when "Add" add_approval(id, @cgi.tree_params["prj"]["approve"]["who"], @cgi.tree_params["prj"]["approve"]["approver"]) @cgi.tree_params["prj"]["list"]["edit"].delete(id.to_s) return when "Delete" delete_approval(id, @cgi.tree_params["prj"]["approve"]["list"]) @cgi.tree_params["prj"]["list"]["edit"].delete(id.to_s) return end end if @cgi.tree_params["prj"].include?("edit") attrs = @cgi.tree_params["prj"]["edit"] attrs["hide_hrs"] = attrs.include?("hide_hrs") ? "1" : "0" if attrs.include?("id") update(attrs["id"]) attrs.delete("id") set(attrs) else attrs["prj_status"] = attrs.include?("make_active") ? "ACTIVE" : "NEW" attrs.delete("make_active") set(attrs) end end end def show project_data = Hash.new MyCGI::add_commons(project_data) project_data[:menu] = menu project_data[:new] = list_item() if @policy.can_edit_project? prj_list = list if prj_list.length == 0 project_data[:nodata] = {} else project_data[:list] = list end s = String.new PROJECT_TEMPLATE.expand(s, project_data) s end end