-- In the true Arsdigita spirit,AAAAAAA -- the ticket tracker contains parts -- of code by Eve, Jin, Ben, and Tracy! -- but this latest release is maintained by Henry Minsky (hqm@arsdigita.com) -- (from ACS 2.0, July 1999) -- massively changed and ported to PGSQL by Ben Adida (ben@openforce.net) -- Create an admin group for BITS select administration_group_add ('BITS Administration', 'bits', 'bits', '', 'f', '/team'); -- create a group type for Teams insert into user_group_types (group_type, pretty_name, pretty_plural, approval_policy, group_module_administration) values ('team', 'Team', 'Teams', 'closed', 'full'); -- first thing is first(we need to create ticket_issues before we reference it) -js create sequence ticket_project_id_sequence; create table ticket_projects ( project_id integer not null primary key, parent_project_id integer references ticket_projects, title varchar(100), -- person who request the project and will be the owner customer_id integer not null references users, start_date datetime, end_date datetime, -- person who gets defaultly assigned to new tickets in the project default_assignee integer references users ); create index ticket_proj_parent_indx on ticket_projects(parent_project_id); create view open_ticket_projects as select * from ticket_projects where end_date is null; create table ticket_priorities ( priority integer not null primary key, name varchar(20) ); insert into ticket_priorities values (3, 'low'); insert into ticket_priorities values (2, 'medium'); insert into ticket_priorities values (1, 'high'); create sequence ticket_issue_id_sequence; create table ticket_issues ( msg_id integer not null primary key, project_id integer not null references ticket_projects, user_id integer references users, group_id integer references user_groups, root_project_id integer references ticket_projects, release_id integer, fixed_release_id integer, posting_time datetime not null, modification_time datetime, ticket_type varchar(100), one_line varchar(700), message varchar(6000), indexed_stuff varchar(6000), close_date datetime, closed_by integer references users, fix_date datetime, fixed_by integer references users, -- When it is important for it to be finished. deadline datetime, -- Status: open, development, fixed waiting approval, closed status varchar(100), priority integer references ticket_priorities, severity varchar(100), -- A NEW FIELD for LEVEL of PRIVACY of issues privacy integer, -- who was responsible for creating this message source varchar(100), -- user name who last modified last_modified_by integer references users, -- When was the last "nag" notification sent last_notification datetime, -- Ticket author's contact info contact_name varchar(200), contact_email varchar(200), contact_info1 varchar(700), contact_info2 varchar(700), -- product-specific fields -- data1 varchar(700), -- data2 varchar(700), -- data3 varchar(700), -- data4 varchar(700), -- data5 varchar(700), -- is this ticket visible to customers public_p char(1) default 't' check(public_p in ('t','f')), -- if notify_p is 't', member of that project will receive notification email notify_p char(1) default 't' check(notify_p in ('t','f')) ); create view ticket_defects as select * from ticket_issues where ticket_type='Defect'; create view ticket_enhancements as select * from ticket_issues where ticket_type='Enhancement Request'; create view ticket_issues_issues as select * from ticket_issues where ticket_type='Issue'; create sequence ticket_field_id_sequence start 1; create table ticket_projects_fields ( project_id integer not null, field_id integer not null, primary key(project_id, field_id), field_name varchar(200) not null, field_pretty_name varchar(200) not null, view_in_list char(1) check (view_in_list in ('t','f')), -- potential vals, separated by vertical bars field_vals varchar(4000), field_type varchar(100) not null ); create unique index ticket_project_field_name_indx on ticket_projects_fields(project_id, field_name); -- We do things this way because creating a new table -- for every project seems pretty bad, and not that useful create table ticket_projects_field_vals ( project_id integer not null, field_id integer not null, foreign key (project_id, field_id) references ticket_projects_fields, issue_id integer not null references ticket_issues, primary key (project_id, field_id, issue_id), field_val varchar(200) ); create index ticket_proj_fval_issue_id on ticket_projects_field_vals(issue_id); create table ticket_project_teams ( project_id integer not null primary key references ticket_projects, team_id integer not null references user_groups ); -- -- releases create table ticket_project_releases ( project_id integer not null, release_id integer not null, release varchar(100) not null, creation_date datetime, release_date datetime, build_file varchar(200), primary key (project_id, release_id) ); create sequence release_id_sequence; -- a trigger to make sure that we always have releases at the top level create function trig_ticket_release_edit() returns opaque as ' DECLARE BEGIN NEW.project_id:= project_root_project_id(NEW.project_id); return NEW; END; ' language 'plpgsql'; create trigger ticket_release_edit before insert or update on ticket_project_releases for each row execute procedure trig_ticket_release_edit(); -- A table to assign people to projects create sequence ticket_assignment_id_sequence; create table ticket_assignments ( project_id integer references ticket_projects, user_id integer references users, rate integer, -- e.g. 125 purpose varchar(4000), -- e.g. "HTML, Java, etc..." -- we add this active flag in case someone gets taken off the -- project. active_p char(1) check (active_p in ('t','f')), admin_p char(1) default 'f' check (admin_p in ('t','f')), primary key (project_id, user_id) ); create table ticket_issue_user_interest_map ( msg_id integer not null references ticket_issues, user_id integer not null references users, primary key (msg_id, user_id), start_date datetime ); -- quick postgres hack create function parent_project_p(integer,integer) returns char as ' DECLARE BEGIN if $1 = $2 THEN return ''t''; else return ''f''; END IF; END; ' language 'plpgsql'; -- pl/sql for parent projects -- create function parent_project_p(integer, integer) -- returns char as ' -- DECLARE -- v_parent_project_id alias for $1; -- v_project_id alias for $2; -- count_check integer; -- BEGIN -- select count(*) into count_check from ticket_projects where project_id=v_project_id connect by parent_project_id= prior project_id start with project_id=v_parent_project_id; -- if (count_check > 0) -- then return ''t''; -- else return ''f''; -- end if; -- END; -- ' language 'plpgsql'; -- pl/sql hack create function project_root_project_id(integer) returns integer as ' DECLARE p_id alias for $1; BEGIN return p_id; END; ' language 'plpgsql'; -- pl/sql to get root project_id -- create function project_root_project_id(integer) -- returns integer as ' -- DECLARE -- v_project_id alias for $1; -- return_project_id integer; -- BEGIN -- select project_id into return_project_id from ticket_projects where parent_project_id is NULL connect by project_id=prior parent_project_id start with project_id=v_project_id; -- return return_project_id; -- END; -- ' language 'plpgsql'; -- pl/sql to get the full name -- quick postgres hack create function project_full_name(integer) returns varchar as ' DECLARE v_project_id alias for $1; v_name varchar(200); BEGIN select title into v_name from ticket_projects where project_id= v_project_id; return(v_name); END; ' language 'plpgsql'; -- create function project_full_name(integer) -- returns char as ' -- DECLARE -- v_project_id alias for $1; -- full_name varchar(4000); -- CURSOR select_parents is -- select title from ticket_projects -- connect by project_id= prior parent_project_id -- start with project_id= v_project_id; -- BEGIN -- full_name:= NULL; -- FOR one_record IN select_parents LOOP -- IF full_name IS NOT NULL -- THEN full_name:= '' // '' || full_name; -- ELSE full_name:= ''''; -- END IF; -- full_name:= one_record.title || full_name; -- END LOOP; -- return(full_name); -- END; -- ' language 'plpgsql'; -- pl/sql for permissions create function ticket_user_can_see_project_p(integer, integer) returns char as ' DECLARE v_user_id alias for $1; v_project_id alias for $2; count_check integer; BEGIN if ad_admin_group_member_p(''bits'','''', v_user_id) = ''t'' then return ''t''; end if; select count(*) into count_check from user_group_map where user_id= v_user_id and group_id in (select team_id from ticket_project_teams where parent_project_p(project_id,v_project_id)=''t''); IF count_check > 0 THEN return(''t''); ELSE return(''f''); END IF; END; ' language 'plpgsql'; create function ticket_user_can_edit_project_p(integer, integer) returns char as ' DECLARE v_user_id alias for $1; v_project_id alias for $2; count_check integer; BEGIN if ad_admin_group_member_p(''bits'','''', v_user_id) = ''t'' then return ''t''; end if; select count(*) into count_check from user_group_map where user_id= v_user_id and (role=''internal'' or role=''administrator'') and group_id in (select team_id from ticket_project_teams where parent_project_p(project_id,v_project_id)=''t''); IF count_check > 0 THEN return(''t''); ELSE return(''f''); END IF; END; ' language 'plpgsql'; -- drop function ticket_user_can_close_issue_p(integer, integer); create function ticket_user_can_close_issue_p(integer, integer) returns char as ' DECLARE v_user_id alias for $1; v_msg_id alias for $2; msg_closeable record; BEGIN if ad_admin_group_member_p(''bits'','''', v_user_id) = ''t'' then return ''t''; end if; select into msg_closeable count(ti.msg_id) as chk, max(ti.project_id) as pid from ticket_issues ti where ti.user_id = v_user_id and ti.msg_id = v_msg_id and 0 < (select count(*) from user_group_map where user_id= v_user_id and (role=''internal'' or role=''administrator'') and group_id = (select tm.team_id from ticket_project_teams tm where parent_project_p(tm.project_id,ti.project_id)=''t'')); IF msg_closeable.chk > 0 THEN return(''t''); ELSE return(''f''); END IF; END; ' language 'plpgsql'; -------------- From The Community System --------------- -- table state, country_codes, users -------------------------------------------------------- -- to make sure we keep the root_project_id synchronized create function trig_ticket_issue_modify() returns opaque AS ' DECLARE BEGIN NEW.root_project_id:= project_root_project_id(NEW.project_id); IF NEW.status != ''fixed waiting approval'' AND NEW.status != ''closed'' THEN NEW.fix_date:= NULL; NEW.fixed_by:= NULL; END IF; IF TG_OP = ''INSERT'' THEN RETURN NEW; END IF; IF NEW.status = ''fixed waiting approval'' AND OLD.status != NEW.status THEN NEW.fix_date:= sysdate(); NEW.fixed_by:= NEW.last_modified_by; END IF; RETURN NEW; END; ' language 'plpgsql'; create trigger ticket_issue_modify before insert or update on ticket_issues for each row execute procedure trig_ticket_issue_modify(); -- Reference the release table -- alter table ticket_issues add -- (constraint R_issue_release foreign key (root_project_id, release_id) -- references ticket_project_releases); -- alter table ticket_issues add -- (constraint R_issue_fixed_release foreign key (root_project_id, fixed_release_id) -- references ticket_project_releases); create function trig_ticket_modification_time() returns opaque as ' DECLARE BEGIN NEW.modification_time:= sysdate(); return NEW; END; ' language 'plpgsql'; create trigger ticket_modification_time before insert or update on ticket_issues for each row execute procedure trig_ticket_modification_time(); -- the ticket_changes table can reference ticket_issues -- but only in Oracle 8.1.5 or newer; Oracle 8.0.5 gets -- bent out of shape with a mutating trigger from -- ticket_activity_logger --- keep track of changes to a ticket create table ticket_changes ( msg_id integer not null, -- references ticket_issues who varchar(256), what varchar(256), old_value varchar(256), new_value varchar(256), modification_date datetime ); create index ticket_changes_by_msg_id on ticket_changes(msg_id); -- track changes to tickets create function trig_ticket_activity_logger() returns opaque as ' DECLARE BEGIN if (OLD.project_id != NEW.project_id) then insert into ticket_changes (msg_id, who, what, old_value, new_value, modification_date) values (NEW.msg_id, NEW.last_modified_by, ''Project ID'', OLD.project_id, NEW.project_id, sysdate()); end if; if (OLD.ticket_type != NEW.ticket_type) then insert into ticket_changes (msg_id, who, what, old_value, new_value, modification_date) values (NEW.msg_id, NEW.last_modified_by, ''Ticket Type'', OLD.ticket_type, NEW.ticket_type, sysdate()); end if; if (OLD.one_line != NEW.one_line) then insert into ticket_changes (msg_id, who,what, old_value, new_value, modification_date) values (NEW.msg_id, NEW.last_modified_by, ''Synopsis'', OLD.one_line, NEW.one_line, sysdate()); end if; if (OLD.deadline != NEW.deadline) then insert into ticket_changes (msg_id, who,what, old_value, new_value, modification_date) values (NEW.msg_id, NEW.last_modified_by, ''Deadline'', OLD.deadline, NEW.deadline, sysdate()); end if; if (OLD.status != NEW.status) then insert into ticket_changes (msg_id, who,what, old_value, new_value, modification_date) values (NEW.msg_id, NEW.last_modified_by, ''Status'', OLD.status, NEW.status, sysdate()); end if; if (OLD.priority != NEW.priority) then insert into ticket_changes (msg_id, who,what, old_value, new_value, modification_date) values (NEW.msg_id, NEW.last_modified_by, ''Priority'', OLD.priority, NEW.priority, sysdate()); end if; if (OLD.severity != NEW.severity) then insert into ticket_changes (msg_id, who,what, old_value, new_value, modification_date) values (NEW.msg_id, NEW.last_modified_by, ''Severity'', OLD.severity, NEW.severity, sysdate()); end if; RETURN NEW; end; ' language 'plpgsql'; create trigger ticket_activity_logger before update on ticket_issues for each row execute procedure trig_ticket_activity_logger(); -- attachments create table ticket_issues_attachments ( msg_id integer not null references ticket_issues, attachment_name varchar(200), filename varchar(200) ); create index ticket_attachments_by_msg_id on ticket_issues_attachments(msg_id); -- For notification purposes insert into user_notification_classes (notification_class_id, notification_class, item_url, class_url, item_pretty_name, item_varname) values (nextval('notification_class_id_sequence'), 'Ticket Tracker', '/new-ticket/issue-view.tcl', '/new-ticket', 'Issue ID', 'msg_id'); create function ticket_notification_class_id() returns integer as ' DECLARE BEGIN return notification_class_id from user_notification_classes where notification_class=''Ticket Tracker''; END; ' language 'plpgsql'; create function trig_ticket_change_insert() returns opaque as ' DECLARE v_check integer; BEGIN v_check:= user_add_notification(ticket_notification_class_id(), NEW.msg_id, NEW.who || '' changed '' || NEW.what || '' from '' || NEW.old_value || '' to '' || NEW.new_value); RETURN NEW; END; ' language 'plpgsql'; create trigger ticket_change_insert before insert on ticket_changes for each row execute procedure trig_ticket_change_insert(); --- a table to assign users to issues --- the selection list for this will be the --- ticket_assignments table constratained by the appropriate project create table ticket_issue_assignments ( msg_id integer not NULL references ticket_issues, user_id integer not null references users, purpose varchar(4000), -- e.g. "HTML, Java, etc..." -- we add this active flag in case someone gets taken off the -- issue. active_p char(1) check (active_p in ('t','f')), unique (msg_id, user_id) ); create function ticket_n_assigned(integer) returns integer as ' DECLARE v_msg_id alias for $1; v_count integer; BEGIN select count(*) into v_count from ticket_issue_assignments where msg_id= v_msg_id; return v_count; END; ' language 'plpgsql'; -- cross reference table mapping issues to other issues create table ticket_xrefs ( from_ticket integer not null references ticket_issues(msg_id), to_ticket integer not null references ticket_issues(msg_id), primary key (from_ticket,to_ticket) ); create sequence ticket_response_id_sequence; create table ticket_issue_responses ( response_id integer not null primary key, response_to integer not null references ticket_issues, user_id integer references users, posting_time datetime not null, public_p char(1) default('t') check(public_p in ('t','f')), message varchar(6000) ); -- update the ticket's modification timestamp create function trig_ticket_response_mod_time() returns opaque as ' DECLARE BEGIN update ticket_issues set modification_time=sysdate() where msg_id= NEW.response_to; return NEW; END; ' language 'plpgsql'; create trigger ticket_response_mod_time before insert or update on ticket_issue_responses for each row execute procedure trig_ticket_response_mod_time(); create table ticket_issue_notifications ( msg_id integer not null references ticket_issues, user_id integer not null references users, primary key (msg_id, user_id) ); -- called by /tcl/email-queue.tcl -- and /ticket/issue-response-2.tcl create function ticket_update_for_response(integer) returns integer AS ' DECLARE v_response_id alias for $1; v_response_row ticket_issue_responses%ROWTYPE; BEGIN select ticket_issue_responses.* into v_response_row from ticket_issue_responses where response_id = v_response_id; if v_response_row.message is not null then update ticket_issues set indexed_stuff= indexed_stuff || v_response_row.message where msg_id= v_response_row.response_to; return user_add_notification(ticket_notification_class_id(), v_response_row.response_to, ''New Response to issue:\n'' || v_response_row.message); end if; return 0; END; ' language 'plpgsql'; create function ticket_one_if_high_priority(integer, varchar) returns integer as ' DECLARE priority alias for $1; status alias for $2; BEGIN IF ((priority = 1) AND (status <> ''closed'') AND (status <> ''deferred'')) THEN return 1; ELSE return 0; END IF; END; ' language 'plpgsql'; create function ticket_one_if_blocker(varchar,varchar) returns integer as ' DECLARE severity alias for $1; status alias for $2; BEGIN IF ((severity = ''blocker'') AND (status <> ''closed'') AND (status <> ''deferred'')) THEN return 1; ELSE return 0; END IF; END; ' language 'plpgsql'; create function ticket_assignees (integer) returns varchar as ' DECLARE v_msg_id alias for $1; v_one_user users%ROWTYPE; v_assignees varchar(1000); BEGIN v_assignees:= ''''; FOR v_one_user IN select * from users where user_id in (select user_id from ticket_issue_assignments where msg_id= v_msg_id) LOOP IF v_assignees != '''' THEN v_assignees:= v_assignees || '', ''; END IF; v_assignees := v_assignees || v_one_user.first_names || '' '' || substr(v_one_user.last_name, 1,1); END LOOP; return v_assignees; END; ' language 'plpgsql'; create function ticket_release_name(integer) returns varchar as ' DECLARE v_release_id alias for $1; v_release varchar(100); BEGIN select release into v_release from ticket_project_releases where release_id= v_release_id; return v_release; END; ' language 'plpgsql'; -- for custom field fetching create function ticket_fetch_custom_field (integer, integer, varchar) returns char as ' DECLARE v_msg_id alias for $1; v_project_id alias for $2; v_field alias for $3; v_field_id integer; BEGIN select field_id into v_field_id from ticket_projects_fields where project_id= v_project_id and field_name= v_field; if v_field_id is null then return null; end if; return field_val from ticket_projects_field_vals where field_id = v_field_id and project_id=v_project_id and issue_id= v_msg_id; END; ' language 'plpgsql';