Initial import of tasks application
This initial import is a heavily modified version of the code I had here, as Arse was modified for other purposes in the meantime and the application no longer worked with it. In addition: * I did not import the user management part yet, * task dependencies are supported in-base, but there is no interface for that yet.
This commit is contained in:
commit
9677ad4dd3
36 changed files with 3919 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
includes/config.inc.php
|
||||
database/config.sql
|
||||
site/.htaccess
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "arse"]
|
||||
path = arse
|
||||
url = git@github.com:tseeker/arse.git
|
7
README
Normal file
7
README
Normal file
|
@ -0,0 +1,7 @@
|
|||
Requires PostgreSQL 9.0+
|
||||
|
||||
1) Copy/rename database/config-sample.sql to database/config.sql,
|
||||
2) Set the web app's user name and the database's name in database/config.sql,
|
||||
3) Run the database.sql script from psql,
|
||||
4) Copy/rename includes/config-sample.inc.php to includes/config.inc.php,
|
||||
5) Set user name / password in includes/config.inc.php
|
1
arse
Submodule
1
arse
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit b80ac7ee91df3c6aa935e38515907c6f3a0ab63c
|
17
database.sql
Normal file
17
database.sql
Normal file
|
@ -0,0 +1,17 @@
|
|||
\i database/config.sql
|
||||
\c :db_name
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Tables from the main database structure
|
||||
\i database/create-tables.sql
|
||||
|
||||
-- Items tree management and associated functions
|
||||
\i database/items-tree-triggers.sql
|
||||
\i database/items-functions.sql
|
||||
|
||||
-- Task management and task dependencies
|
||||
\i database/tasks-functions.sql
|
||||
\i database/task-dependencies.sql
|
||||
|
||||
COMMIT;
|
2
database/config-sample.sql
Normal file
2
database/config-sample.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
\set webapp_user test
|
||||
\set db_name test
|
133
database/create-tables.sql
Normal file
133
database/create-tables.sql
Normal file
|
@ -0,0 +1,133 @@
|
|||
-- Sequences
|
||||
CREATE SEQUENCE items_item_id_seq INCREMENT 1
|
||||
MINVALUE 1 MAXVALUE 9223372036854775807
|
||||
START 1 CACHE 1;
|
||||
GRANT SELECT,UPDATE ON items_item_id_seq TO :webapp_user;
|
||||
|
||||
CREATE SEQUENCE users_user_id_seq INCREMENT 1
|
||||
MINVALUE 1 MAXVALUE 9223372036854775807
|
||||
START 1 CACHE 1;
|
||||
GRANT SELECT,UPDATE ON users_user_id_seq TO :webapp_user;
|
||||
|
||||
CREATE SEQUENCE tasks_task_id_seq INCREMENT 1
|
||||
MINVALUE 1 MAXVALUE 9223372036854775807
|
||||
START 1 CACHE 1;
|
||||
GRANT SELECT,UPDATE ON tasks_task_id_seq TO :webapp_user;
|
||||
|
||||
CREATE SEQUENCE notes_note_id_seq INCREMENT 1
|
||||
MINVALUE 1 MAXVALUE 9223372036854775807
|
||||
START 1 CACHE 1;
|
||||
GRANT SELECT,UPDATE ON notes_note_id_seq TO :webapp_user;
|
||||
|
||||
CREATE SEQUENCE task_dependencies_taskdep_id_seq INCREMENT 1
|
||||
MINVALUE 1 MAXVALUE 9223372036854775807
|
||||
START 1 CACHE 1;
|
||||
|
||||
-- Tables
|
||||
|
||||
|
||||
-- Table items
|
||||
CREATE TABLE items (
|
||||
item_id INT NOT NULL DEFAULT NEXTVAL('items_item_id_seq'::TEXT),
|
||||
item_name VARCHAR(128) NOT NULL,
|
||||
item_id_parent INT,
|
||||
item_ordering INT NOT NULL,
|
||||
PRIMARY KEY(item_id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX i_items_unicity ON items (item_name,item_id_parent);
|
||||
CREATE UNIQUE INDEX i_items_ordering ON items (item_ordering);
|
||||
|
||||
-- Make sure top-level items are unique
|
||||
CREATE UNIQUE INDEX i_items_top_unicity
|
||||
ON items ( item_name )
|
||||
WHERE item_id_parent IS NULL;
|
||||
|
||||
ALTER TABLE items ADD FOREIGN KEY (item_id_parent)
|
||||
REFERENCES items(item_id) ON UPDATE NO ACTION ON DELETE CASCADE;
|
||||
|
||||
GRANT SELECT,INSERT,UPDATE,DELETE ON items TO :webapp_user;
|
||||
|
||||
|
||||
|
||||
-- Table users
|
||||
CREATE TABLE users (
|
||||
user_id INT NOT NULL DEFAULT NEXTVAL('users_user_id_seq'::TEXT),
|
||||
user_email VARCHAR(256) NOT NULL,
|
||||
user_salt CHAR(8) NOT NULL,
|
||||
user_iterations INT NOT NULL,
|
||||
user_hash CHAR(40) NOT NULL,
|
||||
PRIMARY KEY(user_id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX i_users_user_email ON users (LOWER(user_email));
|
||||
GRANT SELECT,INSERT,UPDATE ON users TO :webapp_user;
|
||||
|
||||
|
||||
|
||||
-- Table tasks
|
||||
CREATE TABLE tasks (
|
||||
task_id INT NOT NULL DEFAULT NEXTVAL('tasks_task_id_seq'::TEXT),
|
||||
item_id INT NOT NULL REFERENCES items(item_id) ON UPDATE NO ACTION ON DELETE CASCADE,
|
||||
task_title VARCHAR(256) NOT NULL,
|
||||
task_priority INT NOT NULL,
|
||||
task_description TEXT NOT NULL,
|
||||
task_added TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
|
||||
user_id INT NOT NULL REFERENCES users(user_id) ON UPDATE NO ACTION ON DELETE CASCADE,
|
||||
PRIMARY KEY(task_id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX i_tasks_item_id_task_title ON tasks (item_id,task_title);
|
||||
GRANT SELECT,INSERT,UPDATE,DELETE ON tasks TO :webapp_user;
|
||||
|
||||
|
||||
|
||||
-- Table items_tree
|
||||
CREATE TABLE items_tree (
|
||||
item_id_parent INT NOT NULL REFERENCES items(item_id) ON UPDATE NO ACTION ON DELETE CASCADE,
|
||||
item_id_child INT NOT NULL REFERENCES items(item_id) ON UPDATE NO ACTION ON DELETE CASCADE,
|
||||
pt_depth INT NOT NULL,
|
||||
PRIMARY KEY(item_id_parent,item_id_child)
|
||||
);
|
||||
GRANT SELECT ON items_tree TO :webapp_user;
|
||||
|
||||
|
||||
|
||||
-- Table completed_tasks
|
||||
CREATE TABLE completed_tasks (
|
||||
task_id INT NOT NULL REFERENCES tasks(task_id) ON UPDATE NO ACTION ON DELETE CASCADE,
|
||||
completed_task_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
|
||||
user_id INT NOT NULL REFERENCES users(user_id) ON UPDATE NO ACTION ON DELETE CASCADE,
|
||||
PRIMARY KEY(task_id)
|
||||
);
|
||||
|
||||
GRANT SELECT,INSERT,UPDATE,DELETE ON completed_tasks TO :webapp_user;
|
||||
|
||||
|
||||
|
||||
-- Table task_dependencies
|
||||
CREATE TABLE task_dependencies (
|
||||
taskdep_id INT NOT NULL DEFAULT NEXTVAL('task_dependencies_taskdep_id_seq'::TEXT),
|
||||
task_id INT NOT NULL REFERENCES tasks(task_id) ON UPDATE NO ACTION ON DELETE CASCADE,
|
||||
task_id_depends INT NOT NULL REFERENCES tasks(task_id) ON UPDATE NO ACTION ON DELETE CASCADE,
|
||||
PRIMARY KEY(taskdep_id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX i_taskdep_unicity ON task_dependencies (task_id,task_id_depends);
|
||||
CREATE INDEX i_taskdep_bydependency ON task_dependencies (task_id_depends);
|
||||
GRANT SELECT,INSERT,DELETE ON task_dependencies TO :webapp_user;
|
||||
|
||||
|
||||
|
||||
-- Table notes
|
||||
CREATE TABLE notes (
|
||||
note_id INT NOT NULL DEFAULT NEXTVAL('notes_note_id_seq'::TEXT),
|
||||
task_id INT NOT NULL REFERENCES tasks(task_id) ON UPDATE NO ACTION ON DELETE CASCADE,
|
||||
user_id INT NOT NULL REFERENCES users(user_id) ON UPDATE NO ACTION ON DELETE CASCADE,
|
||||
note_added TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
|
||||
note_text TEXT NOT NULL,
|
||||
PRIMARY KEY(note_id)
|
||||
);
|
||||
|
||||
GRANT SELECT,INSERT,UPDATE,DELETE ON notes TO :webapp_user;
|
||||
|
276
database/items-functions.sql
Normal file
276
database/items-functions.sql
Normal file
|
@ -0,0 +1,276 @@
|
|||
--
|
||||
-- Re-order items
|
||||
-- * first create a temporary table containing text paths
|
||||
-- * use that temporary table to re-order the main table
|
||||
--
|
||||
CREATE OR REPLACE FUNCTION reorder_items( )
|
||||
RETURNS VOID
|
||||
STRICT VOLATILE
|
||||
SECURITY INVOKER
|
||||
AS $$
|
||||
DECLARE
|
||||
i_id INT;
|
||||
i_parent INT;
|
||||
i_ordering INT;
|
||||
i_path TEXT;
|
||||
BEGIN
|
||||
-- Create and fill temporary table
|
||||
CREATE TEMPORARY TABLE items_ordering (
|
||||
item_id INT NOT NULL PRIMARY KEY ,
|
||||
item_ordering_path TEXT NOT NULL
|
||||
) ON COMMIT DROP;
|
||||
|
||||
FOR i_id , i_parent , i_ordering IN
|
||||
SELECT p.item_id , p.item_id_parent , p.item_ordering
|
||||
FROM items p
|
||||
INNER JOIN items_tree pt
|
||||
ON pt.item_id_child = p.item_id
|
||||
GROUP BY p.item_id , p.item_id_parent , p.item_ordering
|
||||
ORDER BY MAX( pt.pt_depth )
|
||||
LOOP
|
||||
IF i_parent IS NULL THEN
|
||||
i_path := '';
|
||||
ELSE
|
||||
SELECT INTO i_path item_ordering_path || '/' FROM items_ordering WHERE item_id = i_parent;
|
||||
END IF;
|
||||
i_path := i_path || to_char( i_ordering , '000000000000' );
|
||||
INSERT INTO items_ordering VALUES ( i_id , i_path );
|
||||
END LOOP;
|
||||
|
||||
-- Move all rows out of the way
|
||||
UPDATE items SET item_ordering = item_ordering + (
|
||||
SELECT 1 + 2 * max( item_ordering ) FROM items );
|
||||
|
||||
-- Re-order items
|
||||
UPDATE items p1 SET item_ordering = 2 * p2.rn
|
||||
FROM ( SELECT item_id , row_number() OVER( ORDER BY item_ordering_path ) AS rn FROM items_ordering ) p2
|
||||
WHERE p1.item_id = p2.item_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
-- Insert a item before another
|
||||
CREATE OR REPLACE FUNCTION insert_item_before( i_name TEXT , i_before INT )
|
||||
RETURNS INT
|
||||
STRICT VOLATILE
|
||||
SECURITY DEFINER
|
||||
AS $insert_item_before$
|
||||
DECLARE
|
||||
i_ordering INT;
|
||||
i_parent INT;
|
||||
BEGIN
|
||||
PERFORM 1 FROM items FOR UPDATE;
|
||||
|
||||
SELECT INTO i_ordering , i_parent item_ordering - 1 , item_id_parent
|
||||
FROM items
|
||||
WHERE item_id = i_before;
|
||||
IF NOT FOUND THEN
|
||||
RETURN 2;
|
||||
END IF;
|
||||
|
||||
BEGIN
|
||||
INSERT INTO items ( item_name , item_id_parent , item_ordering )
|
||||
VALUES ( i_name , i_parent , i_ordering );
|
||||
EXCEPTION
|
||||
WHEN unique_violation THEN
|
||||
RETURN 1;
|
||||
END;
|
||||
PERFORM reorder_items( );
|
||||
RETURN 0;
|
||||
END;
|
||||
$insert_item_before$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
-- Insert item as the last child of another
|
||||
CREATE OR REPLACE FUNCTION insert_item_under( i_name TEXT , i_parent INT )
|
||||
RETURNS INT
|
||||
STRICT VOLATILE
|
||||
SECURITY DEFINER
|
||||
AS $insert_item_under$
|
||||
DECLARE
|
||||
i_ordering INT;
|
||||
BEGIN
|
||||
PERFORM 1 FROM items FOR UPDATE;
|
||||
|
||||
SELECT INTO i_ordering max( item_ordering ) + 1 FROM items;
|
||||
BEGIN
|
||||
INSERT INTO items ( item_name , item_id_parent , item_ordering )
|
||||
VALUES ( i_name , i_parent , i_ordering );
|
||||
EXCEPTION
|
||||
WHEN unique_violation THEN
|
||||
RETURN 1;
|
||||
WHEN foreign_key_violation THEN
|
||||
RETURN 2;
|
||||
END;
|
||||
PERFORM reorder_items( );
|
||||
RETURN 0;
|
||||
END;
|
||||
$insert_item_under$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
-- Add a item as the last root element
|
||||
CREATE OR REPLACE FUNCTION insert_item_last( i_name TEXT )
|
||||
RETURNS INT
|
||||
STRICT VOLATILE
|
||||
SECURITY DEFINER
|
||||
AS $insert_item_last$
|
||||
DECLARE
|
||||
i_ordering INT;
|
||||
BEGIN
|
||||
PERFORM 1 FROM items FOR UPDATE;
|
||||
|
||||
SELECT INTO i_ordering max( item_ordering ) + 1 FROM items;
|
||||
IF i_ordering IS NULL THEN
|
||||
i_ordering := 0;
|
||||
END IF;
|
||||
BEGIN
|
||||
INSERT INTO items ( item_name , item_ordering )
|
||||
VALUES ( i_name , i_ordering );
|
||||
EXCEPTION
|
||||
WHEN unique_violation THEN
|
||||
RETURN 1;
|
||||
END;
|
||||
PERFORM reorder_items( );
|
||||
RETURN 0;
|
||||
END;
|
||||
$insert_item_last$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
-- Rename a item
|
||||
CREATE OR REPLACE FUNCTION rename_item( i_id INT , i_name TEXT )
|
||||
RETURNS INT
|
||||
STRICT VOLATILE
|
||||
SECURITY DEFINER
|
||||
AS $rename_item$
|
||||
BEGIN
|
||||
UPDATE items SET item_name = $2 WHERE item_id = $1;
|
||||
RETURN 0;
|
||||
EXCEPTION
|
||||
WHEN unique_violation THEN
|
||||
RETURN 1;
|
||||
END
|
||||
$rename_item$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
|
||||
-- Move a item before another
|
||||
CREATE OR REPLACE FUNCTION move_item_before( i_id INT , i_before INT )
|
||||
RETURNS INT
|
||||
STRICT VOLATILE
|
||||
SECURITY DEFINER
|
||||
AS $move_item_before$
|
||||
DECLARE
|
||||
i_ordering INT;
|
||||
i_parent INT;
|
||||
BEGIN
|
||||
PERFORM 1 FROM items FOR UPDATE;
|
||||
|
||||
IF i_before = i_id THEN
|
||||
RETURN 1;
|
||||
ELSE
|
||||
SELECT INTO i_ordering , i_parent item_ordering - 1 , item_id_parent
|
||||
FROM items
|
||||
WHERE item_id = i_before;
|
||||
IF NOT FOUND THEN
|
||||
RETURN 2;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
BEGIN
|
||||
UPDATE items SET item_ordering = i_ordering , item_id_parent = i_parent
|
||||
WHERE item_id = i_id;
|
||||
EXCEPTION
|
||||
WHEN unique_violation THEN
|
||||
RETURN 1;
|
||||
END;
|
||||
|
||||
PERFORM reorder_items( );
|
||||
RETURN 0;
|
||||
END;
|
||||
$move_item_before$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
-- Move a item at the end of another's children
|
||||
CREATE OR REPLACE FUNCTION move_item_under( i_id INT , i_parent INT )
|
||||
RETURNS INT
|
||||
STRICT VOLATILE
|
||||
SECURITY DEFINER
|
||||
AS $move_item_under$
|
||||
DECLARE
|
||||
i_ordering INT;
|
||||
BEGIN
|
||||
PERFORM 1 FROM items FOR UPDATE;
|
||||
|
||||
IF i_parent = i_id THEN
|
||||
RETURN 1;
|
||||
ELSE
|
||||
SELECT INTO i_ordering MAX( item_ordering ) + 1
|
||||
FROM items
|
||||
WHERE item_id_parent = i_parent;
|
||||
IF i_ordering IS NULL THEN
|
||||
i_ordering := 1;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
BEGIN
|
||||
UPDATE items SET item_ordering = i_ordering , item_id_parent = i_parent
|
||||
WHERE item_id = i_id;
|
||||
EXCEPTION
|
||||
WHEN unique_violation THEN
|
||||
RETURN 1;
|
||||
WHEN foreign_key_violation THEN
|
||||
RETURN 2;
|
||||
END;
|
||||
|
||||
PERFORM reorder_items( );
|
||||
RETURN 0;
|
||||
END;
|
||||
$move_item_under$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
-- Move a item to the end of the tree
|
||||
CREATE OR REPLACE FUNCTION move_item_last( i_id INT )
|
||||
RETURNS INT
|
||||
STRICT VOLATILE
|
||||
SECURITY DEFINER
|
||||
AS $move_item_last$
|
||||
DECLARE
|
||||
i_ordering INT;
|
||||
BEGIN
|
||||
PERFORM 1 FROM items FOR UPDATE;
|
||||
|
||||
SELECT INTO i_ordering MAX( item_ordering ) + 1
|
||||
FROM items;
|
||||
IF i_ordering IS NULL THEN
|
||||
i_ordering := 0;
|
||||
END IF;
|
||||
|
||||
BEGIN
|
||||
UPDATE items SET item_ordering = i_ordering , item_id_parent = NULL
|
||||
WHERE item_id = i_id;
|
||||
EXCEPTION
|
||||
WHEN unique_violation THEN
|
||||
RETURN 1;
|
||||
END;
|
||||
|
||||
PERFORM reorder_items( );
|
||||
RETURN 0;
|
||||
END;
|
||||
$move_item_last$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
|
||||
-- Delete a item, moving all children to the item's parent
|
||||
CREATE OR REPLACE FUNCTION delete_item( i_id INT )
|
||||
RETURNS VOID
|
||||
STRICT VOLATILE
|
||||
SECURITY DEFINER
|
||||
AS $delete_item$
|
||||
DECLARE
|
||||
i_parent INT;
|
||||
BEGIN
|
||||
PERFORM 1 FROM items FOR UPDATE;
|
||||
DELETE FROM items WHERE item_id = i_id;
|
||||
PERFORM reorder_items( );
|
||||
END;
|
||||
$delete_item$ LANGUAGE plpgsql;
|
93
database/items-tree-triggers.sql
Normal file
93
database/items-tree-triggers.sql
Normal file
|
@ -0,0 +1,93 @@
|
|||
--
|
||||
-- Insert tree data for new rows
|
||||
--
|
||||
|
||||
CREATE OR REPLACE FUNCTION items_tree_ai( )
|
||||
RETURNS TRIGGER
|
||||
SECURITY DEFINER
|
||||
AS $items_tree_ai$
|
||||
BEGIN
|
||||
INSERT INTO items_tree( item_id_parent , item_id_child , pt_depth )
|
||||
VALUES ( NEW.item_id , NEW.item_id , 0 );
|
||||
INSERT INTO items_tree( item_id_parent , item_id_child , pt_depth )
|
||||
SELECT x.item_id_parent, NEW.item_id, x.pt_depth + 1
|
||||
FROM items_tree x WHERE x.item_id_child = NEW.item_id_parent;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$items_tree_ai$ LANGUAGE 'plpgsql';
|
||||
|
||||
CREATE TRIGGER items_tree_ai
|
||||
AFTER INSERT ON items FOR EACH ROW
|
||||
EXECUTE PROCEDURE items_tree_ai( );
|
||||
|
||||
|
||||
--
|
||||
-- Make sure the changes are OK before updating
|
||||
--
|
||||
|
||||
CREATE OR REPLACE FUNCTION items_tree_bu( )
|
||||
RETURNS TRIGGER
|
||||
SECURITY DEFINER
|
||||
AS $items_tree_bu$
|
||||
BEGIN
|
||||
IF NEW.item_id <> OLD.item_id THEN
|
||||
RAISE EXCEPTION 'Changes to identifiers are forbidden.';
|
||||
END IF;
|
||||
|
||||
IF NOT OLD.item_id_parent IS DISTINCT FROM NEW.item_id_parent THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
PERFORM 1 FROM items_tree
|
||||
WHERE ( item_id_parent , item_id_child ) = ( NEW.item_id , NEW.item_id_parent );
|
||||
IF FOUND THEN
|
||||
RAISE EXCEPTION 'Update blocked, it would create a loop.';
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$items_tree_bu$ LANGUAGE 'plpgsql';
|
||||
|
||||
CREATE TRIGGER items_tree_bu
|
||||
BEFORE UPDATE ON items FOR EACH ROW
|
||||
EXECUTE PROCEDURE items_tree_bu( );
|
||||
|
||||
|
||||
--
|
||||
-- Update tree data when a row's parent is changed
|
||||
--
|
||||
|
||||
CREATE OR REPLACE FUNCTION items_tree_au( )
|
||||
RETURNS TRIGGER
|
||||
SECURITY DEFINER
|
||||
AS $items_tree_au$
|
||||
BEGIN
|
||||
IF NOT OLD.item_id_parent IS DISTINCT FROM NEW.item_id_parent THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
-- Remove existing lineage for the updated object and its children
|
||||
IF OLD.item_id_parent IS NOT NULL THEN
|
||||
DELETE FROM items_tree AS te2
|
||||
USING items_tree te1
|
||||
WHERE te2.item_id_child = te1.item_id_child
|
||||
AND te1.item_id_parent = NEW.item_id
|
||||
AND te2.pt_depth > te1.pt_depth;
|
||||
END IF;
|
||||
|
||||
-- Create new lineage
|
||||
IF NEW.item_id_parent IS NOT NULL THEN
|
||||
INSERT INTO items_tree ( item_id_parent , item_id_child , pt_depth )
|
||||
SELECT te1.item_id_parent , te2.item_id_child , te1.pt_depth + te2.pt_depth + 1
|
||||
FROM items_tree te1 , items_tree te2
|
||||
WHERE te1.item_id_child = NEW.item_id_parent
|
||||
AND te2.item_id_parent = NEW.item_id;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$items_tree_au$ LANGUAGE 'plpgsql';
|
||||
|
||||
CREATE TRIGGER items_tree_au
|
||||
AFTER UPDATE ON items FOR EACH ROW
|
||||
EXECUTE PROCEDURE items_tree_au( );
|
302
database/task-dependencies.sql
Normal file
302
database/task-dependencies.sql
Normal file
|
@ -0,0 +1,302 @@
|
|||
--
|
||||
-- Table, indexes and foreign keys
|
||||
--
|
||||
|
||||
CREATE TABLE taskdep_nodes(
|
||||
task_id INT NOT NULL
|
||||
REFERENCES tasks( task_id )
|
||||
ON DELETE CASCADE ,
|
||||
tnode_reverse BOOLEAN NOT NULL ,
|
||||
tnode_id SERIAL NOT NULL ,
|
||||
|
||||
tnode_id_parent INT ,
|
||||
tnode_depth INT NOT NULL ,
|
||||
|
||||
task_id_copyof INT NOT NULL ,
|
||||
tnode_id_copyof INT ,
|
||||
|
||||
taskdep_id INT
|
||||
REFERENCES task_dependencies( taskdep_id )
|
||||
ON DELETE CASCADE ,
|
||||
|
||||
PRIMARY KEY( task_id , tnode_reverse , tnode_id )
|
||||
);
|
||||
|
||||
CREATE INDEX i_tnode_reversetasks ON taskdep_nodes ( tnode_reverse , tnode_id_parent );
|
||||
CREATE INDEX i_tnode_copyof ON taskdep_nodes ( task_id_copyof );
|
||||
CREATE INDEX i_tnode_objdep ON taskdep_nodes ( taskdep_id );
|
||||
|
||||
ALTER TABLE taskdep_nodes
|
||||
ADD CONSTRAINT fk_tnode_copyof
|
||||
FOREIGN KEY( task_id_copyof , tnode_reverse , tnode_id_copyof )
|
||||
REFERENCES taskdep_nodes( task_id , tnode_reverse , tnode_id )
|
||||
ON DELETE CASCADE ,
|
||||
ADD CONSTRAINT fk_tnode_parent
|
||||
FOREIGN KEY( task_id , tnode_reverse , tnode_id_parent )
|
||||
REFERENCES taskdep_nodes( task_id , tnode_reverse , tnode_id )
|
||||
ON DELETE CASCADE;
|
||||
|
||||
GRANT SELECT ON taskdep_nodes TO :webapp_user;
|
||||
|
||||
|
||||
--
|
||||
-- When a task is added, the corresponding dependency tree and
|
||||
-- reverse dependency tree must be created
|
||||
--
|
||||
CREATE OR REPLACE FUNCTION tgf_task_ai( )
|
||||
RETURNS TRIGGER
|
||||
STRICT VOLATILE
|
||||
SECURITY DEFINER
|
||||
AS $tgf_task_ai$
|
||||
BEGIN
|
||||
INSERT INTO taskdep_nodes ( task_id , tnode_reverse , tnode_depth , task_id_copyof )
|
||||
VALUES ( NEW.task_id , FALSE , 0 , NEW.task_id );
|
||||
INSERT INTO taskdep_nodes ( task_id , tnode_reverse , tnode_depth , task_id_copyof )
|
||||
VALUES ( NEW.task_id , TRUE , 0 , NEW.task_id );
|
||||
RETURN NEW;
|
||||
END;
|
||||
$tgf_task_ai$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER tg_task_ai
|
||||
AFTER INSERT ON tasks
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE tgf_task_ai( );
|
||||
|
||||
REVOKE EXECUTE ON FUNCTION tgf_task_ai() FROM PUBLIC;
|
||||
|
||||
|
||||
--
|
||||
-- Copy the contents of a tree <src> as a child of node <node> on tree <dest>.
|
||||
--
|
||||
CREATE OR REPLACE FUNCTION tdtree_copy_tree(
|
||||
is_reverse BOOLEAN , src_id INT , dest_id INT ,
|
||||
node_id INT , depth INT , dep_id INT
|
||||
)
|
||||
RETURNS VOID
|
||||
STRICT VOLATILE
|
||||
AS $tdtree_copy_tree$
|
||||
DECLARE
|
||||
node RECORD;
|
||||
objid INT;
|
||||
BEGIN
|
||||
CREATE TEMPORARY TABLE tdtree_copy_ids(
|
||||
old_id INT ,
|
||||
new_id INT
|
||||
);
|
||||
|
||||
FOR node IN
|
||||
SELECT * FROM taskdep_nodes nodes
|
||||
WHERE task_id = src_id
|
||||
AND tnode_reverse = is_reverse
|
||||
ORDER BY tnode_depth ASC
|
||||
LOOP
|
||||
IF node.tnode_id_copyof IS NULL THEN
|
||||
node.task_id_copyof := src_id;
|
||||
node.tnode_id_copyof := node.tnode_id;
|
||||
END IF;
|
||||
IF node.tnode_id_parent IS NULL THEN
|
||||
node.tnode_id_parent := node_id;
|
||||
node.taskdep_id := dep_id;
|
||||
ELSE
|
||||
SELECT INTO node.tnode_id_parent new_id
|
||||
FROM tdtree_copy_ids
|
||||
WHERE old_id = node.tnode_id_parent;
|
||||
END IF;
|
||||
node.tnode_depth := node.tnode_depth + depth;
|
||||
|
||||
INSERT INTO taskdep_nodes ( task_id , tnode_reverse , tnode_id_parent ,
|
||||
tnode_depth , task_id_copyof , tnode_id_copyof ,
|
||||
taskdep_id )
|
||||
VALUES ( dest_id , is_reverse , node.tnode_id_parent , node.tnode_depth ,
|
||||
node.task_id_copyof , node.tnode_id_copyof ,
|
||||
node.taskdep_id )
|
||||
RETURNING tnode_id INTO objid;
|
||||
INSERT INTO tdtree_copy_ids VALUES ( node.tnode_id , objid );
|
||||
END LOOP;
|
||||
|
||||
DROP TABLE tdtree_copy_ids;
|
||||
END;
|
||||
$tdtree_copy_tree$ LANGUAGE plpgsql;
|
||||
|
||||
REVOKE EXECUTE ON FUNCTION tdtree_copy_tree( BOOLEAN , INT , INT , INT , INT , INT ) FROM PUBLIC;
|
||||
|
||||
|
||||
--
|
||||
-- Add the contents of tree <src> as a child of the root of tree <dest>.
|
||||
-- Also copy <src> to copies of <dest>.
|
||||
--
|
||||
CREATE OR REPLACE FUNCTION tdtree_set_child( is_reverse BOOLEAN , src_id INT , dest_id INT , dep_id INT )
|
||||
RETURNS VOID
|
||||
STRICT VOLATILE
|
||||
AS $tdtree_set_child$
|
||||
DECLARE
|
||||
tree_id INT;
|
||||
node_id INT;
|
||||
depth INT;
|
||||
BEGIN
|
||||
FOR tree_id , node_id , depth IN
|
||||
SELECT task_id , tnode_id , tnode_depth + 1
|
||||
FROM taskdep_nodes
|
||||
WHERE tnode_reverse = is_reverse
|
||||
AND task_id_copyof = dest_id
|
||||
LOOP
|
||||
PERFORM tdtree_copy_tree( is_reverse , src_id , tree_id , node_id , depth , dep_id );
|
||||
END LOOP;
|
||||
END;
|
||||
$tdtree_set_child$ LANGUAGE plpgsql;
|
||||
|
||||
REVOKE EXECUTE ON FUNCTION tdtree_set_child( BOOLEAN , INT , INT , INT ) FROM PUBLIC;
|
||||
|
||||
|
||||
--
|
||||
-- When a dependency between tasks is added, the corresponding trees must
|
||||
-- be updated.
|
||||
--
|
||||
CREATE OR REPLACE FUNCTION tgf_taskdep_ai( )
|
||||
RETURNS TRIGGER
|
||||
STRICT VOLATILE
|
||||
SECURITY DEFINER
|
||||
AS $tgf_taskdep_ai$
|
||||
BEGIN
|
||||
PERFORM tdtree_set_child( FALSE , NEW.task_id_depends , NEW.task_id , NEW.taskdep_id );
|
||||
PERFORM tdtree_set_child( TRUE , NEW.task_id , NEW.task_id_depends , NEW.taskdep_id );
|
||||
RETURN NEW;
|
||||
END;
|
||||
$tgf_taskdep_ai$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER tg_taskdep_ai
|
||||
AFTER INSERT ON task_dependencies
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE tgf_taskdep_ai( );
|
||||
|
||||
REVOKE EXECUTE ON FUNCTION tgf_taskdep_ai() FROM PUBLIC;
|
||||
|
||||
|
||||
--
|
||||
-- Before inserting a dependency, we need to lock all trees that have something
|
||||
-- to do with either nodes. Then we need to make sure there are no cycles and
|
||||
-- that the new dependency is not redundant.
|
||||
--
|
||||
CREATE OR REPLACE FUNCTION tgf_taskdep_bi( )
|
||||
RETURNS TRIGGER
|
||||
STRICT VOLATILE
|
||||
SECURITY DEFINER
|
||||
AS $tgf_taskdep_bi$
|
||||
BEGIN
|
||||
-- Lock all trees
|
||||
PERFORM 1
|
||||
FROM taskdep_nodes n1
|
||||
INNER JOIN taskdep_nodes n2
|
||||
USING ( task_id )
|
||||
WHERE n1.task_id_copyof IN ( NEW.task_id , NEW.task_id_depends )
|
||||
FOR UPDATE OF n2;
|
||||
|
||||
-- Check for cycles
|
||||
PERFORM 1 FROM taskdep_nodes
|
||||
WHERE task_id = NEW.task_id
|
||||
AND task_id_copyof = NEW.task_id_depends
|
||||
AND tnode_reverse;
|
||||
IF FOUND THEN
|
||||
RAISE EXCEPTION 'Cycle detected'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
-- Check for redundant dependencies
|
||||
PERFORM 1
|
||||
FROM taskdep_nodes n1
|
||||
INNER JOIN task_dependencies d
|
||||
ON d.task_id = n1.task_id_copyof
|
||||
WHERE n1.task_id = NEW.task_id
|
||||
AND n1.tnode_reverse
|
||||
AND d.task_id_depends = NEW.task_id_depends;
|
||||
IF FOUND THEN
|
||||
RAISE EXCEPTION '% is the parent of some child of %' , NEW.task_id_depends , NEW.task_id
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
PERFORM 1
|
||||
FROM task_dependencies d1
|
||||
INNER JOIN taskdep_nodes n
|
||||
ON n.task_id = d1.task_id_depends
|
||||
WHERE d1.task_id = NEW.task_id
|
||||
AND n.tnode_reverse
|
||||
AND n.task_id_copyof = NEW.task_id_depends;
|
||||
IF FOUND THEN
|
||||
RAISE EXCEPTION '% is the child of some ancestor of %' , NEW.task_id , NEW.task_id_depends
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
PERFORM 1 FROM taskdep_nodes
|
||||
WHERE task_id = NEW.task_id
|
||||
AND task_id_copyof = NEW.task_id_depends
|
||||
AND NOT tnode_reverse;
|
||||
IF FOUND THEN
|
||||
RAISE EXCEPTION '% is already an ancestor of %' , NEW.task_id_depends , NEW.task_id
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$tgf_taskdep_bi$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
CREATE TRIGGER tg_taskdep_bi
|
||||
BEFORE INSERT ON task_dependencies
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE tgf_taskdep_bi( );
|
||||
|
||||
REVOKE EXECUTE ON FUNCTION tgf_taskdep_bi() FROM PUBLIC;
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- List all dependencies that can be added to a task.
|
||||
--
|
||||
CREATE OR REPLACE FUNCTION tasks_possible_dependencies( o_id INT )
|
||||
RETURNS SETOF tasks
|
||||
STRICT STABLE
|
||||
AS $tasks_possible_dependencies$
|
||||
SELECT * FROM tasks
|
||||
WHERE task_id NOT IN (
|
||||
SELECT d.task_id_depends AS id
|
||||
FROM taskdep_nodes n1
|
||||
INNER JOIN task_dependencies d
|
||||
ON d.task_id = n1.task_id_copyof
|
||||
WHERE n1.task_id = $1 AND n1.tnode_reverse
|
||||
UNION ALL SELECT n.task_id_copyof AS id
|
||||
FROM task_dependencies d1
|
||||
INNER JOIN taskdep_nodes n
|
||||
ON n.task_id = d1.task_id_depends
|
||||
WHERE d1.task_id = $1 AND n.tnode_reverse
|
||||
UNION ALL SELECT task_id_copyof AS id
|
||||
FROM taskdep_nodes
|
||||
WHERE task_id = $1
|
||||
);
|
||||
$tasks_possible_dependencies$ LANGUAGE sql;
|
||||
|
||||
REVOKE EXECUTE ON FUNCTION tasks_possible_dependencies( INT ) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION tasks_possible_dependencies( INT ) TO :webapp_user;
|
||||
|
||||
|
||||
--
|
||||
-- Add a dependency
|
||||
--
|
||||
CREATE OR REPLACE FUNCTION tasks_add_dependency( t_id INT , t_dependency INT )
|
||||
RETURNS INT
|
||||
STRICT VOLATILE
|
||||
SECURITY INVOKER
|
||||
AS $tasks_add_dependency$
|
||||
BEGIN
|
||||
INSERT INTO task_dependencies( task_id , task_id_depends_on )
|
||||
VALUES ( t_id , t_dependency );
|
||||
RETURN 0;
|
||||
EXCEPTION
|
||||
WHEN foreign_key_violation THEN
|
||||
RETURN 1;
|
||||
WHEN check_violation THEN
|
||||
RETURN 2;
|
||||
END;
|
||||
$tasks_add_dependency$ LANGUAGE plpgsql;
|
||||
|
||||
REVOKE EXECUTE ON FUNCTION tasks_add_dependency( INT , INT ) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION tasks_add_dependency( INT , INT ) TO :webapp_user;
|
78
database/tasks-functions.sql
Normal file
78
database/tasks-functions.sql
Normal file
|
@ -0,0 +1,78 @@
|
|||
-- Create a new task
|
||||
CREATE OR REPLACE FUNCTION add_task( t_item INT , t_title TEXT , t_description TEXT , t_priority INT , t_user INT )
|
||||
RETURNS INT
|
||||
STRICT VOLATILE
|
||||
SECURITY INVOKER
|
||||
AS $add_task$
|
||||
BEGIN
|
||||
INSERT INTO tasks ( item_id , task_title , task_description , task_priority , user_id )
|
||||
VALUES ( t_item , t_title , t_description , t_priority , t_user );
|
||||
RETURN 0;
|
||||
EXCEPTION
|
||||
WHEN unique_violation THEN
|
||||
RETURN 1;
|
||||
WHEN foreign_key_violation THEN
|
||||
RETURN 2;
|
||||
END;
|
||||
$add_task$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
-- Mark a task as finished
|
||||
CREATE OR REPLACE FUNCTION finish_task( t_id INT , u_id INT , n_text TEXT )
|
||||
RETURNS INT
|
||||
STRICT VOLATILE
|
||||
SECURITY INVOKER
|
||||
AS $finish_task$
|
||||
BEGIN
|
||||
BEGIN
|
||||
INSERT INTO completed_tasks ( task_id , user_id )
|
||||
VALUES ( t_id , u_id );
|
||||
EXCEPTION
|
||||
WHEN unique_violation THEN
|
||||
RETURN 1;
|
||||
END;
|
||||
|
||||
INSERT INTO notes ( task_id , user_id , note_text )
|
||||
VALUES ( t_id , u_id , n_text );
|
||||
RETURN 0;
|
||||
END;
|
||||
$finish_task$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
-- Restart a task
|
||||
CREATE OR REPLACE FUNCTION restart_task( t_id INT , u_id INT , n_text TEXT )
|
||||
RETURNS INT
|
||||
STRICT VOLATILE
|
||||
SECURITY INVOKER
|
||||
AS $restart_task$
|
||||
BEGIN
|
||||
DELETE FROM completed_tasks WHERE task_id = t_id;
|
||||
IF NOT FOUND THEN
|
||||
RETURN 1;
|
||||
END IF;
|
||||
INSERT INTO notes ( task_id , user_id , note_text )
|
||||
VALUES ( t_id , u_id , n_text );
|
||||
RETURN 0;
|
||||
END;
|
||||
$restart_task$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
-- Update a task
|
||||
CREATE OR REPLACE FUNCTION update_task( t_id INT , p_id INT , t_title TEXT , t_description TEXT , t_priority INT )
|
||||
RETURNS INT
|
||||
STRICT VOLATILE
|
||||
SECURITY INVOKER
|
||||
AS $update_task$
|
||||
BEGIN
|
||||
UPDATE tasks SET item_id = p_id , task_title = t_title ,
|
||||
task_description = t_description ,
|
||||
task_priority = t_priority
|
||||
WHERE task_id = t_id;
|
||||
RETURN 0;
|
||||
EXCEPTION
|
||||
WHEN unique_violation THEN
|
||||
RETURN 1;
|
||||
WHEN foreign_key_violation THEN
|
||||
RETURN 2;
|
||||
END;
|
||||
$update_task$ LANGUAGE plpgsql;
|
7
includes/config-sample.inc.php
Normal file
7
includes/config-sample.inc.php
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
$config[ 'core' ][ 'db' ][ 'name' ] = '...';
|
||||
$config[ 'core' ][ 'db' ][ 'user' ] = '...';
|
||||
$config[ 'core' ][ 'db' ][ 'password' ] = '...';
|
||||
|
||||
$config[ 'core' ][ 'pages' ][ 'baseTitle' ] = 'Tasks';
|
121
includes/t-basics/controllers.inc.php
Normal file
121
includes/t-basics/controllers.inc.php
Normal file
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
|
||||
class Ctrl_HomePage
|
||||
extends Controller
|
||||
{
|
||||
|
||||
public final function handle( Page $page )
|
||||
{
|
||||
session_start( );
|
||||
if ( array_key_exists( 'uid' , $_SESSION ) ) {
|
||||
return 'items';
|
||||
} else {
|
||||
return 'login';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class Ctrl_Logout
|
||||
extends Controller
|
||||
{
|
||||
|
||||
public function handle( Page $page )
|
||||
{
|
||||
session_start( );
|
||||
session_destroy( );
|
||||
return 'home';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
class Ctrl_CheckSession
|
||||
extends Controller
|
||||
{
|
||||
private $loginURL;
|
||||
private $sessionKey;
|
||||
|
||||
public function __construct( $url = 'login' , $key = 'uid' )
|
||||
{
|
||||
$this->loginURL = $url;
|
||||
$this->sessionKey = $key;
|
||||
}
|
||||
|
||||
|
||||
public function handle( Page $page )
|
||||
{
|
||||
session_start( );
|
||||
if ( array_key_exists( $this->sessionKey , $_SESSION ) ) {
|
||||
return null;
|
||||
}
|
||||
return $this->loginURL;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Ctrl_LogInForm
|
||||
extends Controller
|
||||
{
|
||||
|
||||
public function handle( Page $page )
|
||||
{
|
||||
return Loader::Create( 'Form' , 'Log in' , 'login' )
|
||||
->addField( Loader::Create( 'Field' , 'email' , 'text' )
|
||||
->setDescription( 'E-mail address:' ) )
|
||||
->addField( Loader::Create( 'Field' , 'pass' , 'password' )
|
||||
->setDescription( 'Password:' ) )
|
||||
->setSuccessURL( 'home' )
|
||||
->addController( Loader::Ctrl( 'log_in' ) )
|
||||
->controller( );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class Ctrl_LogIn
|
||||
extends Controller
|
||||
implements FormAware
|
||||
{
|
||||
|
||||
private $form;
|
||||
|
||||
public function setForm( Form $form )
|
||||
{
|
||||
$this->form = $form;
|
||||
}
|
||||
|
||||
public function handle( Page $page )
|
||||
{
|
||||
$email = $this->form->field( 'email' );
|
||||
$pass = $this->form->field( 'pass' );
|
||||
|
||||
$user = Loader::DAO( 'users' )->checkLogin( $email->value( ) , $pass->value( ) );
|
||||
if ( $user == null ) {
|
||||
$email->putError( 'Invalid credentials.' );
|
||||
return null;
|
||||
}
|
||||
|
||||
$_SESSION[ 'uid' ] = $user->user_id;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Ctrl_LoggedOut
|
||||
extends Controller
|
||||
{
|
||||
|
||||
public function handle( Page $page )
|
||||
{
|
||||
session_start( );
|
||||
if ( array_key_exists( 'uid' , $_SESSION ) ) {
|
||||
return 'home';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
22
includes/t-basics/package.inc.php
Normal file
22
includes/t-basics/package.inc.php
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?
|
||||
|
||||
$package[ 'requires' ][] = 'form';
|
||||
$package[ 'requires' ][] = 'hub-page';
|
||||
$package[ 'requires' ][] = 't-users';
|
||||
|
||||
$package[ 'files' ][] = 'controllers';
|
||||
$package[ 'files' ][] = 'pages';
|
||||
|
||||
$package[ 'extras' ][] = 'AuthenticatedPage';
|
||||
|
||||
$package[ 'ctrls' ][] = 'check_session';
|
||||
$package[ 'ctrls' ][] = 'home_page';
|
||||
$package[ 'ctrls' ][] = 'log_in';
|
||||
$package[ 'ctrls' ][] = 'log_in_form';
|
||||
$package[ 'ctrls' ][] = 'logged_out';
|
||||
$package[ 'ctrls' ][] = 'logout';
|
||||
|
||||
$package[ 'pages' ][] = 'tasks_home';
|
||||
$package[ 'pages' ][] = 'tasks_login';
|
||||
$package[ 'pages' ][] = 'tasks_logout';
|
||||
|
67
includes/t-basics/pages.inc.php
Normal file
67
includes/t-basics/pages.inc.php
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
abstract class AuthenticatedPage
|
||||
extends HubPage
|
||||
{
|
||||
public function __construct( $pages )
|
||||
{
|
||||
parent::__construct( $pages );
|
||||
$this->addController( Loader::Ctrl( 'check_session' ) );
|
||||
}
|
||||
|
||||
protected function getMenu( )
|
||||
{
|
||||
return array(
|
||||
'items' => 'Items' ,
|
||||
'tasks' => 'Tasks' ,
|
||||
'logout' => 'Log out'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Page_TasksHome
|
||||
extends HTMLPage
|
||||
{
|
||||
|
||||
public function __construct( )
|
||||
{
|
||||
parent::__construct( );
|
||||
$this->addController( Loader::Ctrl( 'home_page' ) );
|
||||
}
|
||||
|
||||
protected function getMenu( )
|
||||
{
|
||||
return array();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Page_TasksLogin
|
||||
extends HTMLPage
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct( );
|
||||
$this->addController( Loader::Ctrl( 'logged_out' ) );
|
||||
$this->addController( Loader::Ctrl( 'log_in_form' ) );
|
||||
}
|
||||
|
||||
protected function getMenu( )
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class Page_TasksLogout
|
||||
extends Page_Basic
|
||||
{
|
||||
public function __construct( )
|
||||
{
|
||||
parent::__construct( );
|
||||
$this->addController( Loader::Ctrl( 'logout' ) );
|
||||
}
|
||||
}
|
333
includes/t-data/dao_items.inc.php
Normal file
333
includes/t-data/dao_items.inc.php
Normal file
|
@ -0,0 +1,333 @@
|
|||
<?php
|
||||
|
||||
class DAO_Items
|
||||
extends DAO
|
||||
{
|
||||
private $loaded = array( );
|
||||
private $tree = null;
|
||||
private $treeList = null;
|
||||
|
||||
private $activeTasksCounted = false;
|
||||
|
||||
|
||||
public function createBefore( $name , $before )
|
||||
{
|
||||
$query = $this->query( 'SELECT insert_item_before( $1 , $2 ) AS error' );
|
||||
$result = $query->execute( $name , $before );
|
||||
return $result[ 0 ]->error;
|
||||
}
|
||||
|
||||
|
||||
public function createUnder( $name , $under )
|
||||
{
|
||||
$query = $this->query( 'SELECT insert_item_under( $1 , $2 ) AS error' );
|
||||
$result = $query->execute( $name , $under );
|
||||
return $result[ 0 ]->error;
|
||||
}
|
||||
|
||||
|
||||
public function createLast( $name )
|
||||
{
|
||||
$query = $this->query( 'SELECT insert_item_last( $1 ) AS error' );
|
||||
$result = $query->execute( $name );
|
||||
return $result[ 0 ]->error;
|
||||
}
|
||||
|
||||
|
||||
public function get( $identifier )
|
||||
{
|
||||
$identifier = (int)$identifier;
|
||||
if ( array_key_exists( $identifier , $this->loaded ) ) {
|
||||
return $this->loaded[ $identifier ];
|
||||
}
|
||||
|
||||
$getNameQuery = $this->query( 'SELECT item_name FROM items WHERE item_id = $1' , true );
|
||||
$result = $getNameQuery->execute( $identifier );
|
||||
if ( empty( $result ) ) {
|
||||
$rObj = null;
|
||||
} else {
|
||||
$rObj = new Data_Item( $identifier , $result[ 0 ]->item_name );
|
||||
}
|
||||
$this->loaded[ $identifier ] = $rObj;
|
||||
|
||||
return $rObj;
|
||||
}
|
||||
|
||||
|
||||
public function getLineage( Data_Item $item )
|
||||
{
|
||||
if ( is_array( $item->lineage ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$query = $this->query(
|
||||
'SELECT p.item_id , p.item_name FROM items_tree pt '
|
||||
. 'INNER JOIN items p '
|
||||
. 'ON p.item_id = pt.item_id_parent '
|
||||
. 'WHERE pt.item_id_child = $1 AND pt.pt_depth > 0 '
|
||||
. 'ORDER BY pt.pt_depth DESC' );
|
||||
$result = $query->execute( $item->id );
|
||||
|
||||
$stack = array( );
|
||||
foreach ( $result as $entry ) {
|
||||
if ( array_key_exists( $entry->item_id , $this->loaded ) ) {
|
||||
$object = $this->loaded[ $entry->item_id ];
|
||||
} else {
|
||||
$object = new Data_Item( $entry->item_id , $entry->item_name );
|
||||
$this->loaded[ $entry->item_id ] = $object;
|
||||
}
|
||||
$object->lineage = $stack;
|
||||
array_push( $stack , $entry->item_id );
|
||||
}
|
||||
$item->lineage = $stack;
|
||||
}
|
||||
|
||||
|
||||
private function loadTree( )
|
||||
{
|
||||
$query = $this->query(
|
||||
'SELECT p.item_id , p.item_name , MAX( t.pt_depth ) as depth '
|
||||
. 'FROM items p '
|
||||
. 'INNER JOIN items_tree t ON t.item_id_child = p.item_id '
|
||||
. 'GROUP BY p.item_id, p.item_name , p.item_ordering '
|
||||
. 'ORDER BY p.item_ordering' );
|
||||
$result = $query->execute( );
|
||||
|
||||
$prevEntry = null;
|
||||
$stack = array( );
|
||||
$stackSize = 0;
|
||||
$this->tree = array( );
|
||||
$this->treeList = array( );
|
||||
foreach ( $result as $entry ) {
|
||||
if ( $entry->depth > $stackSize ) {
|
||||
array_push( $stack , $prevEntry );
|
||||
$stackSize ++;
|
||||
} elseif ( $entry->depth < $stackSize ) {
|
||||
while ( $stackSize > $entry->depth ) {
|
||||
array_pop( $stack );
|
||||
$stackSize --;
|
||||
}
|
||||
}
|
||||
|
||||
if ( array_key_exists( $entry->item_id , $this->loaded ) ) {
|
||||
$object = $this->loaded[ $entry->item_id ];
|
||||
} else {
|
||||
$object = new Data_Item( $entry->item_id , $entry->item_name );
|
||||
$this->loaded[ $entry->item_id ] = $object;
|
||||
}
|
||||
$object->children = array( );
|
||||
$object->lineage = $stack;
|
||||
if ( $object->depth = $entry->depth ) {
|
||||
$object->hasParent = true;
|
||||
$object->parent = $stack[ $stackSize - 1 ];
|
||||
array_push( $this->loaded[ $object->parent ]->children , $object->id );
|
||||
} else {
|
||||
$object->hasParent = false;
|
||||
}
|
||||
|
||||
$this->loaded[ $object->id ] = $object;
|
||||
if ( $object->depth == 0 ) {
|
||||
array_push( $this->tree , $object );
|
||||
}
|
||||
array_push( $this->treeList , $object );
|
||||
$prevEntry = $object->id;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getTree( )
|
||||
{
|
||||
if ( $this->tree !== null ) {
|
||||
return $this->tree;
|
||||
}
|
||||
$this->loadTree( );
|
||||
return $this->tree;
|
||||
}
|
||||
|
||||
|
||||
public function getTreeList( )
|
||||
{
|
||||
if ( $this->tree !== null ) {
|
||||
return $this->tree;
|
||||
}
|
||||
$this->loadTree( );
|
||||
return $this->treeList;
|
||||
}
|
||||
|
||||
|
||||
public function getAll( $input )
|
||||
{
|
||||
$output = array( );
|
||||
foreach ( $input as $id ) {
|
||||
array_push( $output , $this->get( $id ) );
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function countActiveTasks( )
|
||||
{
|
||||
if ( $this->activeTasksCounted ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$query = $this->query(
|
||||
'SELECT p.item_id , p.item_name , COUNT(*) AS t_count '
|
||||
. 'FROM items p '
|
||||
. 'INNER JOIN tasks t USING( item_id ) '
|
||||
. 'LEFT OUTER JOIN completed_tasks c USING( task_id ) '
|
||||
. 'WHERE c.task_id IS NULL '
|
||||
. 'GROUP BY item_id, p.item_name' );
|
||||
$results = $query->execute( );
|
||||
|
||||
foreach ( $results as $entry ) {
|
||||
if ( array_key_exists( $entry->item_id , $this->loaded ) ) {
|
||||
$object = $this->loaded[ $entry->item_id ];
|
||||
} else {
|
||||
$object = new Data_Item( $entry->item_id , $entry->item_name );
|
||||
$this->loaded[ $entry->item_id ] = $object;
|
||||
}
|
||||
$object->activeTasks = $entry->t_count;
|
||||
}
|
||||
|
||||
$this->activeTasksCounted = true;
|
||||
}
|
||||
|
||||
|
||||
private function checkActiveTasksIn( Data_Item $item )
|
||||
{
|
||||
if ( (int) $item->activeTasks > 0 ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ( $this->getAll( $item->children ) as $child ) {
|
||||
if ( $this->checkActiveTasksIn( $child ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public function canDelete( Data_Item $item )
|
||||
{
|
||||
if ( $this->tree === null ) {
|
||||
$this->loadTree( );
|
||||
}
|
||||
$this->countActiveTasks( );
|
||||
return ! $this->checkActiveTasksIn( $item );
|
||||
}
|
||||
|
||||
|
||||
public function getMoveTargetsIn( $tree , $parent , $item )
|
||||
{
|
||||
$positions = array( );
|
||||
$count = count( $tree );
|
||||
$nameProblem = false;
|
||||
for ( $i = 0 ; $i <= $count ; $i ++ ) {
|
||||
// Completely skip the selected item and its children
|
||||
if ( $i != $count && $tree[ $i ]->id == $item->id ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for invalid positions (i.e. before/after selected item)
|
||||
$invalidPos = ( $i > 0 && $tree[ $i - 1 ]->id == $item->id );
|
||||
|
||||
// Check for duplicate name
|
||||
$nameProblem = $nameProblem || ( $i != $count && $tree[ $i ]->name == $item->name );
|
||||
|
||||
// Get children positions
|
||||
if ( $i < $count ) {
|
||||
$sub = $this->getMoveTargetsIn( $this->getAll( $tree[ $i ]->children ) , $tree[ $i ] , $item );
|
||||
} else {
|
||||
$sub = array( );
|
||||
}
|
||||
|
||||
array_push( $positions , array(
|
||||
'item' => ( $i < $count ) ? $tree[ $i ]->id : ( is_null( $parent ) ? null : $parent->id ) ,
|
||||
'end' => ( $i == $count ) ? 1 : 0 ,
|
||||
'valid' => ! $invalidPos ,
|
||||
'sub' => $sub
|
||||
) );
|
||||
}
|
||||
|
||||
// Add all data to output array
|
||||
$realPos = array( );
|
||||
foreach ( $positions as $pos ) {
|
||||
if ( $pos['valid'] && ! $nameProblem ) {
|
||||
array_push( $realPos , $pos['end'] . ':' . $pos[ 'item' ] );
|
||||
}
|
||||
$realPos = array_merge( $realPos , $pos[ 'sub' ] );
|
||||
}
|
||||
|
||||
return $realPos;
|
||||
}
|
||||
|
||||
|
||||
public function getMoveTargets( Data_Item $item )
|
||||
{
|
||||
//
|
||||
// A destination is a (parent,position) couple, where the
|
||||
// position corresponds to the item before which the selected
|
||||
// item is to be moved.
|
||||
//
|
||||
// A destination is valid if:
|
||||
// - there is no parent or the parent does not have the
|
||||
// selected item in its lineage;
|
||||
// - there is no item in the parent (or at the root if
|
||||
// there is no parent) that uses the same name as the selected
|
||||
// item, unless that item *is* the selected item;
|
||||
// - the item at the specified position is not the selected
|
||||
// item, or there is no item at the specified position;
|
||||
// - the item before the specified position is not the
|
||||
// selected item, or the specified position is 0.
|
||||
//
|
||||
|
||||