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.
|
||||||
|
//
|
||||||
|
|
||||||
|
$result = $this->getMoveTargetsIn( $this->getTree( ) , null , $item );
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canMove( Data_Item $item )
|
||||||
|
{
|
||||||
|
$result = $this->getMoveTargets( $item );
|
||||||
|
return ! empty( $result );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function moveBefore( $item , $before )
|
||||||
|
{
|
||||||
|
$result = $this->query( 'SELECT move_item_before( $1 , $2 ) AS error' )
|
||||||
|
->execute( $item , $before );
|
||||||
|
return $result[ 0 ]->error;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function moveUnder( $item , $under )
|
||||||
|
{
|
||||||
|
$result = $this->query( 'SELECT move_item_under( $1 , $2 ) AS error' )
|
||||||
|
->execute( $item , $under );
|
||||||
|
return $result[ 0 ]->error;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function moveLast( $item )
|
||||||
|
{
|
||||||
|
$result = $this->query( 'SELECT move_item_last( $1 ) AS error' )
|
||||||
|
->execute( $item );
|
||||||
|
return $result[ 0 ]->error;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function destroy( $item )
|
||||||
|
{
|
||||||
|
$this->query( 'SELECT delete_item( $1 )' )->execute( $item );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function rename( $item , $name )
|
||||||
|
{
|
||||||
|
$result = $this->query( 'SELECT rename_item( $1 , $2 ) AS error' )
|
||||||
|
->execute( $item , $name );
|
||||||
|
return $result[ 0 ]->error;
|
||||||
|
}
|
||||||
|
}
|
176
includes/t-data/dao_tasks.inc.php
Normal file
176
includes/t-data/dao_tasks.inc.php
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class DAO_Tasks
|
||||||
|
extends DAO
|
||||||
|
{
|
||||||
|
private static $priorities = array(
|
||||||
|
'1' => 'Lowest' ,
|
||||||
|
'2' => 'Low' ,
|
||||||
|
'3' => 'Normal' ,
|
||||||
|
'4' => 'High' ,
|
||||||
|
'5' => 'Very high' ,
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
public function translatePriority( $value )
|
||||||
|
{
|
||||||
|
return DAO_Tasks::$priorities[ "$value" ];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getAllTasks( )
|
||||||
|
{
|
||||||
|
return $this->query(
|
||||||
|
'SELECT t.task_id AS id, t.item_id AS item, t.task_title AS title, '
|
||||||
|
. 't.task_description AS description, t.task_added AS added_at, '
|
||||||
|
. 'u1.user_email AS added_by, ct.completed_task_time AS completed_at, '
|
||||||
|
. 'u2.user_email AS completed_by , t.task_priority AS priority '
|
||||||
|
. 'FROM tasks t '
|
||||||
|
. 'INNER JOIN users u1 ON u1.user_id = t.user_id '
|
||||||
|
. 'LEFT OUTER JOIN completed_tasks ct ON ct.task_id = t.task_id '
|
||||||
|
. 'LEFT OUTER JOIN users u2 ON u2.user_id = ct.user_id '
|
||||||
|
. 'ORDER BY ( CASE WHEN ct.task_id IS NULL THEN t.task_priority ELSE -1 END ) DESC , '
|
||||||
|
. 't.task_added DESC' )->execute( );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAllActiveTasks( )
|
||||||
|
{
|
||||||
|
return $this->query(
|
||||||
|
'SELECT t.task_id AS id, t.item_id AS item, t.task_title AS title, '
|
||||||
|
. 't.task_description AS description, t.task_added AS added_at, '
|
||||||
|
. 'u1.user_email AS added_by, NULL AS completed_at, NULL AS completed_by , '
|
||||||
|
. 't.task_priority AS priority '
|
||||||
|
. 'FROM tasks t '
|
||||||
|
. 'INNER JOIN users u1 ON u1.user_id = t.user_id '
|
||||||
|
. 'LEFT OUTER JOIN completed_tasks ct ON ct.task_id = t.task_id '
|
||||||
|
. 'WHERE ct.task_id IS NULL '
|
||||||
|
. 'ORDER BY t.task_priority DESC , t.task_added DESC' )->execute( );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getTasksAt( Data_Item $item )
|
||||||
|
{
|
||||||
|
return $this->query(
|
||||||
|
'SELECT t.task_id AS id, t.task_title AS title, '
|
||||||
|
. 't.task_description AS description, t.task_added AS added_at, '
|
||||||
|
. 'u1.user_email AS added_by, ct.completed_task_time AS completed_at, '
|
||||||
|
. 'u2.user_email AS completed_by , t.task_priority AS priority '
|
||||||
|
. 'FROM tasks t '
|
||||||
|
. 'INNER JOIN users u1 ON u1.user_id = t.user_id '
|
||||||
|
. 'LEFT OUTER JOIN completed_tasks ct ON ct.task_id = t.task_id '
|
||||||
|
. 'LEFT OUTER JOIN users u2 ON u2.user_id = ct.user_id '
|
||||||
|
. 'WHERE t.item_id = $1'
|
||||||
|
. 'ORDER BY ( CASE WHEN ct.task_id IS NULL THEN t.task_priority ELSE -1 END ) DESC , '
|
||||||
|
. 't.task_added DESC' )->execute( $item->id );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function addTask( $item , $title , $priority , $description )
|
||||||
|
{
|
||||||
|
$result = $this->query( 'SELECT add_task( $1 , $2 , $3 , $4 , $5 ) AS error' )
|
||||||
|
->execute( $item , $title , $description , $priority , $_SESSION[ 'uid' ] );
|
||||||
|
return $result[0]->error;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function get( $id )
|
||||||
|
{
|
||||||
|
$result = $this->query(
|
||||||
|
'SELECT t.task_id AS id, t.task_title AS title, t.item_id AS item ,'
|
||||||
|
. 't.task_description AS description, t.task_added AS added_at, '
|
||||||
|
. 'u1.user_email AS added_by, ct.completed_task_time AS completed_at, '
|
||||||
|
. 'u2.user_email AS completed_by, t.user_id AS uid , '
|
||||||
|
. 't.task_priority AS priority '
|
||||||
|
. 'FROM tasks t '
|
||||||
|
. 'INNER JOIN users u1 ON u1.user_id = t.user_id '
|
||||||
|
. 'LEFT OUTER JOIN completed_tasks ct ON ct.task_id = t.task_id '
|
||||||
|
. 'LEFT OUTER JOIN users u2 ON u2.user_id = ct.user_id '
|
||||||
|
. 'WHERE t.task_id = $1' )->execute( $id );
|
||||||
|
if ( empty( $result ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$task = $result[ 0 ];
|
||||||
|
$task->notes = $this->query(
|
||||||
|
'SELECT n.note_id AS id , n.user_id AS uid , u.user_email AS author , '
|
||||||
|
. 'n.note_added AS added_at , n.note_text AS "text" '
|
||||||
|
. 'FROM notes n '
|
||||||
|
. 'INNER JOIN users u USING (user_id) '
|
||||||
|
. 'WHERE n.task_id = $1 '
|
||||||
|
. 'ORDER BY n.note_added DESC' )->execute( $id );
|
||||||
|
return $task;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function canDelete( $task )
|
||||||
|
{
|
||||||
|
if ( $task->completed_by !== null ) {
|
||||||
|
$ts = strtotime( $task->completed_at );
|
||||||
|
return ( time() - $ts > 7 * 3600 * 24 );
|
||||||
|
}
|
||||||
|
$ts = strtotime( $task->added_at );
|
||||||
|
return ( time() - $ts < 600 ) && ( $task->uid == $_SESSION[ 'uid' ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function delete( $task )
|
||||||
|
{
|
||||||
|
$this->query( 'DELETE FROM tasks WHERE task_id = $1' )->execute( $task );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function finish( $task , $noteText )
|
||||||
|
{
|
||||||
|
$this->query( 'SELECT finish_task( $1 , $2 , $3 )' )
|
||||||
|
->execute( $task , $_SESSION[ 'uid' ] , $noteText );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function restart( $task , $noteText )
|
||||||
|
{
|
||||||
|
$this->query( 'SELECT restart_task( $1 , $2 , $3 )' )
|
||||||
|
->execute( $task , $_SESSION[ 'uid' ] , $noteText );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateTask( $id , $item , $title , $priority , $description )
|
||||||
|
{
|
||||||
|
$result = $this->query( 'SELECT update_task( $1 , $2 , $3 , $4 , $5 ) AS error' )
|
||||||
|
->execute( $id , $item , $title , $description , $priority );
|
||||||
|
return $result[0]->error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addNote( $task , $note )
|
||||||
|
{
|
||||||
|
$this->query( 'INSERT INTO notes ( task_id , user_id , note_text ) VALUES ( $1 , $2 , $3 )' )
|
||||||
|
->execute( $task , $_SESSION[ 'uid' ] , $note );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNote( $id )
|
||||||
|
{
|
||||||
|
$query = $this->query(
|
||||||
|
'SELECT n.note_id AS id , n.note_text AS text , n.note_added AS added_at , '
|
||||||
|
. 'n.task_id AS task , '
|
||||||
|
. '( n.user_id = $2 AND t.task_id IS NULL ) AS editable '
|
||||||
|
. 'FROM notes n '
|
||||||
|
. 'LEFT OUTER JOIN completed_tasks t USING (task_id) '
|
||||||
|
. 'WHERE n.note_id = $1' );
|
||||||
|
$result = $query->execute( $id , $_SESSION[ 'uid' ] );
|
||||||
|
if ( empty( $result ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$result[ 0 ]->editable = ( $result[ 0 ]->editable == 't' );
|
||||||
|
return $result[ 0 ];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteNote( $id )
|
||||||
|
{
|
||||||
|
$this->query( 'DELETE FROM notes WHERE note_id = $1' )->execute( $id );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateNote( $id , $text )
|
||||||
|
{
|
||||||
|
$this->query( 'UPDATE notes SET note_text = $2 , note_added = now( ) WHERE note_id = $1' )
|
||||||
|
->execute( $id , $text );
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
42
includes/t-data/item.inc.php
Normal file
42
includes/t-data/item.inc.php
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Data_Item
|
||||||
|
{
|
||||||
|
public $id;
|
||||||
|
public $name;
|
||||||
|
public $hasParent;
|
||||||
|
public $parent;
|
||||||
|
public $children;
|
||||||
|
public $depth;
|
||||||
|
public $lineage;
|
||||||
|
|
||||||
|
public $activeTasks;
|
||||||
|
public $inactiveTasks;
|
||||||
|
|
||||||
|
public function __construct( $id , $name )
|
||||||
|
{
|
||||||
|
$this->id = $id;
|
||||||
|
$this->name = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getIdentifier( )
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getName( )
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getDepth( )
|
||||||
|
{
|
||||||
|
if ( $this->depth === null ) {
|
||||||
|
throw new Exception( "Method not implemented" );
|
||||||
|
}
|
||||||
|
return $this->depth;
|
||||||
|
}
|
||||||
|
}
|
10
includes/t-data/package.inc.php
Normal file
10
includes/t-data/package.inc.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$package[ 'requires' ][] = 'core';
|
||||||
|
|
||||||
|
$package[ 'files' ][] = 'item';
|
||||||
|
$package[ 'files' ][] = 'dao_items';
|
||||||
|
$package[ 'files' ][] = 'dao_tasks';
|
||||||
|
|
||||||
|
$package[ 'daos' ][] = 'items';
|
||||||
|
$package[ 'daos' ][] = 'tasks';
|
282
includes/t-items/controllers.inc.php
Normal file
282
includes/t-items/controllers.inc.php
Normal file
|
@ -0,0 +1,282 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_ItemsTree
|
||||||
|
extends Controller
|
||||||
|
implements TitleProvider
|
||||||
|
{
|
||||||
|
private $useParameter;
|
||||||
|
|
||||||
|
public function __construct( $useParameter = false )
|
||||||
|
{
|
||||||
|
$this->useParameter = $useParameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
$items = Loader::DAO( 'items' );
|
||||||
|
$tree = $items->getTree( );
|
||||||
|
$items->countActiveTasks( );
|
||||||
|
|
||||||
|
if ( $this->useParameter ) {
|
||||||
|
$root = (int) $this->getParameter( $this->useParameter , 'GET' );
|
||||||
|
} else {
|
||||||
|
$root = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$buttonURL = 'items/add';
|
||||||
|
if ( $root != null ) {
|
||||||
|
$rootObj = $items->get( $root );
|
||||||
|
$tree = $items->getAll( $rootObj->children );
|
||||||
|
$boxTitle = 'Child items';
|
||||||
|
$buttonURL .= "?from=$root";
|
||||||
|
} else {
|
||||||
|
$boxTitle = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Loader::View( 'box' , $boxTitle , Loader::View( 'items_tree' , $tree ) )
|
||||||
|
->setClass( 'list' )
|
||||||
|
->addButton( BoxButton::create( 'Add item' , $buttonURL )
|
||||||
|
->setClass( 'list-add' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getTitle( )
|
||||||
|
{
|
||||||
|
return 'Items';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_ItemDetails
|
||||||
|
extends Controller
|
||||||
|
{
|
||||||
|
private $item;
|
||||||
|
|
||||||
|
public function __construct( Data_Item $item )
|
||||||
|
{
|
||||||
|
$this->item = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
$items = Loader::DAO( 'items');
|
||||||
|
$items->getTree( );
|
||||||
|
|
||||||
|
$box = Loader::View( 'box' , 'Details' , Loader::View( 'item_details' , $this->item ) )
|
||||||
|
->addButton( BoxButton::create( 'Edit item' , 'items/edit?id=' . $this->item->id )
|
||||||
|
->setClass( 'icon edit' ) );
|
||||||
|
|
||||||
|
if ( $items->canMove( $this->item ) ) {
|
||||||
|
$box->addButton( BoxButton::create( 'Move item' , 'items/move?id=' . $this->item->id )
|
||||||
|
->setClass( 'icon move' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $items->canDelete( $this->item ) ) {
|
||||||
|
$box->addButton( BoxButton::create( 'Delete item' , 'items/delete?id=' . $this->item->id )
|
||||||
|
->setClass( 'icon delete' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $box;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_AddItem
|
||||||
|
extends Controller
|
||||||
|
implements FormAware
|
||||||
|
{
|
||||||
|
private $form;
|
||||||
|
|
||||||
|
public function setForm( Form $form )
|
||||||
|
{
|
||||||
|
$this->form = $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
$name = $this->form->field( 'name' );
|
||||||
|
$before = $this->form->field( 'before' );
|
||||||
|
list( $after , $id ) = explode( ':' , $before->value( ) );
|
||||||
|
|
||||||
|
$items = Loader::DAO( 'items' );
|
||||||
|
if ( $id === '' ) {
|
||||||
|
$error = $items->createLast( $name->value( ) );
|
||||||
|
} elseif ( $after == 1 ) {
|
||||||
|
$error = $items->createUnder( $name->value( ) , $id );
|
||||||
|
} else {
|
||||||
|
$error = $items->createBefore( $name->value( ) , $id );
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ( $error ) {
|
||||||
|
|
||||||
|
case 0:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
$name->putError( 'This name is not unique' );
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
$before->putError( 'The item you selected no longer exists' );
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$name->putError( 'An unknown error occurred (' . $error . ')' );
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_MoveItem
|
||||||
|
extends Controller
|
||||||
|
implements FormAware
|
||||||
|
{
|
||||||
|
private $form;
|
||||||
|
|
||||||
|
|
||||||
|
public function setForm( Form $form )
|
||||||
|
{
|
||||||
|
$this->form = $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
$srcId = (int) $this->form->field( 'id' )->value( );
|
||||||
|
$dest = $this->form->field( 'destination' );
|
||||||
|
list( $after , $id ) = explode( ':' , $dest->value( ) );
|
||||||
|
|
||||||
|
$items = Loader::DAO( 'items' );
|
||||||
|
if ( $id === '' ) {
|
||||||
|
$error = $items->moveLast( $srcId );
|
||||||
|
} elseif ( $after == 1 ) {
|
||||||
|
$error = $items->moveUnder( $srcId , $id );
|
||||||
|
} else {
|
||||||
|
$error = $items->moveBefore( $srcId , $id );
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ( $error ) {
|
||||||
|
|
||||||
|
case 0:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
$dest->putError( 'Invalid destination' );
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
$before->putError( 'The place you selected no longer exists.' );
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$name->putError( 'An unknown error occurred (' . $error . ')' );
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_DeleteItem
|
||||||
|
extends Controller
|
||||||
|
implements FormAware
|
||||||
|
{
|
||||||
|
private $form;
|
||||||
|
|
||||||
|
|
||||||
|
public function setForm( Form $form )
|
||||||
|
{
|
||||||
|
$this->form = $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
$id = (int) $this->form->field( 'id' )->value( );
|
||||||
|
|
||||||
|
$items = Loader::DAO( 'items' );
|
||||||
|
if ( ! $items->canDelete( $items->get( $id ) ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$items->destroy( $id );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_EditItem
|
||||||
|
extends Controller
|
||||||
|
implements FormAware
|
||||||
|
{
|
||||||
|
private $form;
|
||||||
|
|
||||||
|
|
||||||
|
public function setForm( Form $form )
|
||||||
|
{
|
||||||
|
$this->form = $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
$id = (int) $this->form->field( 'id' )->value( );
|
||||||
|
$items = Loader::DAO( 'items' );
|
||||||
|
$item = $items->get( $id );
|
||||||
|
|
||||||
|
$name = $this->form->field( 'name' );
|
||||||
|
if ( $name->value( ) === $item->name ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = $items->rename( $id , $name->value( ) );
|
||||||
|
switch ( $error ) {
|
||||||
|
|
||||||
|
case 0:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
$name->putError( 'Ce nom n\'est pas unique' );
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$name->putError( 'Une erreur inconnue s\'est produite (' . $error . ')' );
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_ItemTasks
|
||||||
|
extends Controller
|
||||||
|
{
|
||||||
|
private $item;
|
||||||
|
|
||||||
|
public function __construct( Data_Item $item )
|
||||||
|
{
|
||||||
|
$this->item = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
$tasks = Loader::DAO( 'tasks' )->getTasksAt( $this->item );
|
||||||
|
|
||||||
|
return Loader::View( 'box' , 'Tasks' , Loader::View( 'tasks' , $tasks ) )
|
||||||
|
->addButton( BoxButton::create( 'Add task' , 'tasks/add?to=' . $this->item->id )
|
||||||
|
->setClass( 'list-add' ) );
|
||||||
|
}
|
||||||
|
}
|
44
includes/t-items/fields.inc.php
Normal file
44
includes/t-items/fields.inc.php
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
class Item_LocationField
|
||||||
|
implements FieldValidator , FieldModifier
|
||||||
|
{
|
||||||
|
|
||||||
|
private $okLocations;
|
||||||
|
|
||||||
|
public function __construct( $okLocations )
|
||||||
|
{
|
||||||
|
$this->okLocations = $okLocations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function replace( $value )
|
||||||
|
{
|
||||||
|
$exploded = explode( ':' , $value );
|
||||||
|
if ( count( $exploded ) != 2 ) {
|
||||||
|
$exploded = array( 0 , $value );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $exploded[ 1 ] == '' ) {
|
||||||
|
$exploded[ 0 ] = 1;
|
||||||
|
} else {
|
||||||
|
$exploded[ 0 ] = ( $exploded[ 0 ] == '0' ) ? 0 : 1;
|
||||||
|
$exploded[ 1 ] = (int) $exploded[ 1 ];
|
||||||
|
}
|
||||||
|
|
||||||
|
return join( ':' , $exploded );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validate( $value )
|
||||||
|
{
|
||||||
|
list( $inside , $before ) = explode( ':' , $value );
|
||||||
|
if ( $before != '' && Loader::DAO( 'items' )->get( $before ) == null ) {
|
||||||
|
return array( 'This item no longer exists.' );
|
||||||
|
}
|
||||||
|
if ( ! ( empty( $this->okLocations ) || in_array( $value , $this->okLocations ) ) ) {
|
||||||
|
return array( 'Invalid destination' );
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
32
includes/t-items/package.inc.php
Normal file
32
includes/t-items/package.inc.php
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$package[ 'requires' ][] = 'form';
|
||||||
|
$package[ 'requires' ][] = 't-basics';
|
||||||
|
$package[ 'requires' ][] = 't-data';
|
||||||
|
|
||||||
|
$package[ 'files' ][] = 'controllers';
|
||||||
|
$package[ 'files' ][] = 'fields';
|
||||||
|
$package[ 'files' ][] = 'page_controllers';
|
||||||
|
$package[ 'files' ][] = 'pages';
|
||||||
|
$package[ 'files' ][] = 'views';
|
||||||
|
|
||||||
|
$package[ 'extras' ][] = 'Item_NameField';
|
||||||
|
$package[ 'extras' ][] = 'Item_LocationField';
|
||||||
|
|
||||||
|
$package[ 'ctrls' ][] = 'add_item';
|
||||||
|
$package[ 'ctrls' ][] = 'add_item_form';
|
||||||
|
$package[ 'ctrls' ][] = 'delete_item';
|
||||||
|
$package[ 'ctrls' ][] = 'delete_item_form';
|
||||||
|
$package[ 'ctrls' ][] = 'edit_item';
|
||||||
|
$package[ 'ctrls' ][] = 'edit_item_form';
|
||||||
|
$package[ 'ctrls' ][] = 'move_item';
|
||||||
|
$package[ 'ctrls' ][] = 'move_item_form';
|
||||||
|
$package[ 'ctrls' ][] = 'item_details';
|
||||||
|
$package[ 'ctrls' ][] = 'item_tasks';
|
||||||
|
$package[ 'ctrls' ][] = 'items_tree';
|
||||||
|
$package[ 'ctrls' ][] = 'view_item';
|
||||||
|
|
||||||
|
$package[ 'views' ][] = 'item_details';
|
||||||
|
$package[ 'views' ][] = 'items_tree';
|
||||||
|
|
||||||
|
$package[ 'pages' ][] = 'tasks_items';
|
278
includes/t-items/page_controllers.inc.php
Normal file
278
includes/t-items/page_controllers.inc.php
Normal file
|
@ -0,0 +1,278 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_ViewItem
|
||||||
|
extends Controller
|
||||||
|
{
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$id = (int) $this->getParameter( 'id' );
|
||||||
|
} catch ( ParameterException $e ) {
|
||||||
|
return $page->getBaseURL() . '/items';
|
||||||
|
}
|
||||||
|
|
||||||
|
$item = Loader::DAO( 'items' )->get( $id );
|
||||||
|
if ( $item === null ) {
|
||||||
|
return $page->getBaseURL() . '/items';
|
||||||
|
}
|
||||||
|
$page->setTitle( $item->name . ' (item)' );
|
||||||
|
|
||||||
|
return array(
|
||||||
|
Loader::Ctrl( 'item_details' , $item ) ,
|
||||||
|
Loader::Ctrl( 'items_tree' , 'id' ) ,
|
||||||
|
Loader::Ctrl( 'item_tasks' , $item )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_AddItemForm
|
||||||
|
extends Controller
|
||||||
|
implements TitleProvider
|
||||||
|
{
|
||||||
|
private $items;
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
$this->items = Loader::DAO( 'items' );
|
||||||
|
|
||||||
|
$locationField = Loader::Create( 'Item_LocationField' , array( ) );
|
||||||
|
|
||||||
|
$form = Loader::Create( 'Form' , 'Add this item' , 'create-item' , 'New item information' )
|
||||||
|
->addField( Loader::Create( 'Field' , 'name' , 'text' )
|
||||||
|
->setDescription( 'Item name:' )
|
||||||
|
->setModifier( Loader::Create( 'Modifier_TrimString' ) )
|
||||||
|
->setValidator( Loader::Create( 'Validator_StringLength' , 'This name' , 2 , 128 ) ) )
|
||||||
|
->addField( $before = Loader::Create( 'Field' , 'before' , 'select' )
|
||||||
|
->setDescription( 'Insert before:' )
|
||||||
|
->setMandatory( false )
|
||||||
|
->setModifier( $locationField )
|
||||||
|
->setValidator( $locationField ) )
|
||||||
|
->addField( Loader::Create( 'Field' , 'from' , 'hidden' ) )
|
||||||
|
->addController( Loader::Ctrl( 'add_item' ) );
|
||||||
|
|
||||||
|
// Add options to the insert location selector
|
||||||
|
$this->addTreeOptions( $before , $this->items->getTree( ) );
|
||||||
|
$before->addOption( '1:' , 'the end of the list' );
|
||||||
|
|
||||||
|
// Try to guess return page and default insert location
|
||||||
|
try {
|
||||||
|
$from = (int) $this->getParameter( 'from' );
|
||||||
|
} catch ( ParameterException $e ) {
|
||||||
|
$from = 0;
|
||||||
|
}
|
||||||
|
if ( $from == 0 ) {
|
||||||
|
$returnURL = 'items';
|
||||||
|
$defBefore = '1:';
|
||||||
|
} else {
|
||||||
|
$returnURL = 'items/view?id=' . $from;
|
||||||
|
$defBefore = '1:' . $from;
|
||||||
|
}
|
||||||
|
$form->setURL( $returnURL );
|
||||||
|
$form->field( 'from' )->setDefaultValue( $from );
|
||||||
|
$before->setDefaultValue( $defBefore );
|
||||||
|
|
||||||
|
return $form->controller( );
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addTreeOptions( $before , $tree )
|
||||||
|
{
|
||||||
|
foreach ( $tree as $item ) {
|
||||||
|
$name = '-' . str_repeat( '--' , $item->getDepth( ) ) . ' ' . $item->getName( );
|
||||||
|
$before->addOption( '0:' . $item->getIdentifier( ) , $name );
|
||||||
|
|
||||||
|
if ( !empty( $item->children ) ) {
|
||||||
|
$this->addTreeOptions( $before , $this->items->getAll( $item->children ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = '-' . str_repeat( '--' , $item->getDepth( ) + 1 ) . ' the end of ' . $item->getName( );
|
||||||
|
$before->addOption( '1:' . $item->getIdentifier( ) , $name );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle( )
|
||||||
|
{
|
||||||
|
return 'Add item';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_MoveItemForm
|
||||||
|
extends Controller
|
||||||
|
{
|
||||||
|
private $items;
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
// Check selected item
|
||||||
|
try {
|
||||||
|
$id = (int) $this->getParameter( 'id' );
|
||||||
|
} catch ( ParameterException $e ) {
|
||||||
|
return 'items';
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->items = Loader::DAO( 'items' );
|
||||||
|
$item = $this->items->get( $id );
|
||||||
|
if ( $item === null ) {
|
||||||
|
return 'items';
|
||||||
|
}
|
||||||
|
|
||||||
|
$destinations = $this->items->getMoveTargets( $item );
|
||||||
|
if ( empty( $destinations ) ) {
|
||||||
|
return 'items/view?id=' . $item->id;
|
||||||
|
}
|
||||||
|
$page->setTitle( $item->name . ' (item)' );
|
||||||
|
|
||||||
|
// Field modifier / validator
|
||||||
|
$locationField = Loader::Create( 'Item_LocationField' , $destinations );
|
||||||
|
|
||||||
|
// Generate form
|
||||||
|
$form = Loader::Create( 'Form' , 'Move item' , 'move-item' )
|
||||||
|
->addField( Loader::Create( 'Field' , 'id' , 'hidden' )->setDefaultValue( $item->id ) )
|
||||||
|
->addField( $dest = Loader::Create( 'Field' , 'destination' , 'select' )
|
||||||
|
->setDescription( 'Move before:' )
|
||||||
|
->setModifier( $locationField )
|
||||||
|
->setValidator( $locationField ) )
|
||||||
|
->setURL( 'items/view?id=' . $item->id )
|
||||||
|
->addController( Loader::Ctrl( 'move_item' ) );
|
||||||
|
$this->addDestinations( $dest , $this->items->getTree( ) , $destinations );
|
||||||
|
if ( in_array( '1:' , $destinations ) ) {
|
||||||
|
$dest->addOption( '1:' , 'the end of the list' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $form->controller( );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function addDestinations( $field , $tree , $destinations )
|
||||||
|
{
|
||||||
|
foreach ( $tree as $item ) {
|
||||||
|
$id = '0:' . $item->id;
|
||||||
|
$disabled = ! in_array( $id , $destinations );
|
||||||
|
if ( $disabled && ! $this->checkChildren( $item , $destinations ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = '-' . str_repeat( '--' , $item->getDepth( ) ) . ' ' . $item->getName( );
|
||||||
|
$field->addOption( $id , $name , $disabled );
|
||||||
|
|
||||||
|
if ( !empty( $item->children ) ) {
|
||||||
|
$this->addDestinations( $field , $this->items->getAll( $item->children ) , $destinations );
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = '1:' . $item->id;
|
||||||
|
if ( ! in_array( $id , $destinations ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$name = '-' . str_repeat( '--' , $item->getDepth( ) + 1 ) . ' the end of ' . $item->getName( );
|
||||||
|
$field->addOption( $id , $name );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function checkChildren( $item , $destinations )
|
||||||
|
{
|
||||||
|
$children = $this->items->getAll( $item->children );
|
||||||
|
foreach ( $children as $child ) {
|
||||||
|
if ( in_array( '0:' . $child->id , $destinations ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( in_array( '1:' . $item->id , $destinations ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $children as $child ) {
|
||||||
|
if ( $this->checkChildren( $child , $destinations ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_DeleteItemForm
|
||||||
|
extends Controller
|
||||||
|
{
|
||||||
|
private $items;
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
// Check selected item
|
||||||
|
try {
|
||||||
|
$id = (int) $this->getParameter( 'id' );
|
||||||
|
} catch ( ParameterException $e ) {
|
||||||
|
return 'items';
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = Loader::DAO( 'items' );
|
||||||
|
$item = $items->get( $id );
|
||||||
|
if ( $item === null ) {
|
||||||
|
return 'items';
|
||||||
|
}
|
||||||
|
if ( ! $items->canDelete( $item ) ) {
|
||||||
|
return 'items/view?id=' . $id;
|
||||||
|
}
|
||||||
|
$page->setTitle( $item->name . ' (item)' );
|
||||||
|
|
||||||
|
// Generate confirmation text
|
||||||
|
$confText = HTML::make( 'div' )
|
||||||
|
->appendElement( HTML::make( 'p' )
|
||||||
|
->appendText( 'You are about to delete this item.' ) )
|
||||||
|
->appendElement( HTML::make( 'p' )
|
||||||
|
->appendText( 'All child items and all tasks the item contains will be deleted permanently.' ) )
|
||||||
|
->appendElement( HTML::make( 'p' )
|
||||||
|
->appendText( 'It is impossible to undo this operation.' ) );
|
||||||
|
|
||||||
|
// Generate form
|
||||||
|
$form = Loader::Create( 'Form' , 'Delete the item' , 'delete-item' , 'Please confirm' )
|
||||||
|
->addField( Loader::Create( 'Field' , 'id' , 'hidden' )
|
||||||
|
->setDefaultValue( $item->id ) )
|
||||||
|
->addField( Loader::Create( 'Field' , 'confirm' , 'html' )->setDefaultValue( $confText ) )
|
||||||
|
->setCancelURL( 'items/view?id=' . $item->id )
|
||||||
|
->setSuccessURL( 'items' ) // XXX: use lineage
|
||||||
|
->addController( Loader::Ctrl( 'delete_item' ) );
|
||||||
|
|
||||||
|
return $form->controller( );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_EditItemForm
|
||||||
|
extends Controller
|
||||||
|
{
|
||||||
|
private $items;
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
// Check selected item
|
||||||
|
try {
|
||||||
|
$id = (int) $this->getParameter( 'id' );
|
||||||
|
} catch ( ParameterException $e ) {
|
||||||
|
return 'items';
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->items = Loader::DAO( 'items' );
|
||||||
|
$item = $this->items->get( $id );
|
||||||
|
if ( $item === null ) {
|
||||||
|
return 'items';
|
||||||
|
}
|
||||||
|
$page->setTitle( $item->name . ' (item)' );
|
||||||
|
|
||||||
|
return Loader::Create( 'Form' , 'Update item' , 'edit-item' )
|
||||||
|
->setURL( 'items/view?id=' . $item->id )
|
||||||
|
->addField( Loader::Create( 'Field' , 'name' , 'text' )
|
||||||
|
->setDescription( 'Name of the item:' )
|
||||||
|
->setModifier( Loader::Create( 'Modifier_TrimString' ) )
|
||||||
|
->setValidator( Loader::Create( 'Validator_StringLength' , 'This name' , 2 , 128 ) )
|
||||||
|
->setDefaultValue( $item->name ) )
|
||||||
|
->addField( Loader::Create( 'Field' , 'id' , 'hidden' )
|
||||||
|
->setDefaultValue( $item->id ) )
|
||||||
|
->addController( Loader::Ctrl( 'edit_item' ) )
|
||||||
|
->controller( );
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
19
includes/t-items/pages.inc.php
Normal file
19
includes/t-items/pages.inc.php
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Page_TasksItems
|
||||||
|
extends AuthenticatedPage
|
||||||
|
{
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct( array(
|
||||||
|
'' => 'items_tree' ,
|
||||||
|
'view' => 'view_item' ,
|
||||||
|
'add' => 'add_item_form' ,
|
||||||
|
'move' => 'move_item_form' ,
|
||||||
|
'edit' => 'edit_item_form' ,
|
||||||
|
'delete' => 'delete_item_form' ,
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
96
includes/t-items/views.inc.php
Normal file
96
includes/t-items/views.inc.php
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class View_ItemsTree
|
||||||
|
extends BaseURLAwareView
|
||||||
|
{
|
||||||
|
private $tree;
|
||||||
|
private $minDepth;
|
||||||
|
|
||||||
|
public function __construct( $tree )
|
||||||
|
{
|
||||||
|
$this->tree = $tree;
|
||||||
|
if ( ! empty( $tree ) ) {
|
||||||
|
$this->minDepth = $tree[ 0 ]->depth;
|
||||||
|
} else {
|
||||||
|
$this->minDepth = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render( )
|
||||||
|
{
|
||||||
|
if ( empty( $this->tree ) ) {
|
||||||
|
return HTML::make( 'div' )
|
||||||
|
->setAttribute( 'class' , 'no-table' )
|
||||||
|
->appendText( 'No items have been defined.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = HTML::make( 'table' )
|
||||||
|
->appendElement( HTML::make( 'tr' )
|
||||||
|
->setAttribute( 'class' , 'header' )
|
||||||
|
->appendElement( HTML::make( 'th' )
|
||||||
|
->appendText( 'Item name' ) )
|
||||||
|
->appendElement( HTML::make( 'th' )
|
||||||
|
->setAttribute( 'class' , 'align-right' )
|
||||||
|
->appendText( 'Tasks' ) ) );
|
||||||
|
foreach ( $this->tree as $item ) {
|
||||||
|
$this->renderItem( $table , $item );
|
||||||
|
}
|
||||||
|
return $table;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderItem( $table , $item )
|
||||||
|
{
|
||||||
|
$children = Loader::DAO( 'items' )->getAll( $item->children );
|
||||||
|
$padding = 5 + ( $item->depth - $this->minDepth ) * 16;
|
||||||
|
$table->appendElement( HTML::make( 'tr' )
|
||||||
|
->appendElement( HTML::make( 'td' )
|
||||||
|
->setAttribute( 'style' , 'padding-left:' . $padding . 'px' )
|
||||||
|
->appendElement( HTML::make( 'a' )
|
||||||
|
->setAttribute( 'href' , $this->base . '/items/view?id=' . $item->id )
|
||||||
|
->appendText( $item->name ) ) )
|
||||||
|
->appendElement( HTML::make( 'td' )
|
||||||
|
->setAttribute( 'class' , 'align-right' )
|
||||||
|
->appendRaw( (int) $item->activeTasks ) ) );
|
||||||
|
|
||||||
|
foreach ( $children as $child ) {
|
||||||
|
$this->renderItem( $table , $child );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class View_ItemDetails
|
||||||
|
extends BaseURLAwareView
|
||||||
|
{
|
||||||
|
private $item;
|
||||||
|
|
||||||
|
public function __construct( Data_Item $item )
|
||||||
|
{
|
||||||
|
$this->item = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render( )
|
||||||
|
{
|
||||||
|
$items = Loader::DAO( 'items' );
|
||||||
|
|
||||||
|
$contents = array( );
|
||||||
|
if ( empty( $this->item->lineage ) ) {
|
||||||
|
array_push( $contents , HTML::make( 'em' ) ->appendText( 'None' ) );
|
||||||
|
} else {
|
||||||
|
foreach ( $items->getAll( $this->item->lineage ) as $ancestor ) {
|
||||||
|
if ( ! empty( $contents ) ) {
|
||||||
|
array_push( $contents , ' » ' );
|
||||||
|
}
|
||||||
|
array_push( $contents , HTML::make( 'a' )
|
||||||
|
->setAttribute( 'href' , $this->base . '/items/view?id=' . $ancestor->id )
|
||||||
|
->appendText( $ancestor->name ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return HTML::make( 'dl' )
|
||||||
|
->appendElement( HTML::make( 'dt' )->appendText( 'Path:' ) )
|
||||||
|
->appendElement( HTML::make( 'dd' )
|
||||||
|
->setAttribute( 'style' , 'font-size: 10pt' )
|
||||||
|
->append( $contents ) );
|
||||||
|
}
|
||||||
|
}
|
309
includes/t-tasks/controllers.inc.php
Normal file
309
includes/t-tasks/controllers.inc.php
Normal file
|
@ -0,0 +1,309 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_AddTask
|
||||||
|
extends Controller
|
||||||
|
implements FormAware
|
||||||
|
{
|
||||||
|
private $form;
|
||||||
|
|
||||||
|
public function setForm( Form $form )
|
||||||
|
{
|
||||||
|
$this->form = $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
$item = $this->form->field( 'item' );
|
||||||
|
$name = $this->form->field( 'title' );
|
||||||
|
$priority = $this->form->field( 'priority' );
|
||||||
|
$description = $this->form->field( 'description' );
|
||||||
|
|
||||||
|
$error = Loader::DAO( 'tasks' )->addTask( (int) $item->value( ) , $name->value( ) ,
|
||||||
|
(int) $priority->value( ) , $description->value( ) );
|
||||||
|
switch ( $error ) {
|
||||||
|
|
||||||
|
case 0:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
$name->putError( 'Duplicate task name for this item.' );
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
$item->putError( 'This item has been deleted' );
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$name->putError( "An unknown error occurred ($error)" );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_TaskDetails
|
||||||
|
extends Controller
|
||||||
|
{
|
||||||
|
private $task;
|
||||||
|
|
||||||
|
public function __construct( $task )
|
||||||
|
{
|
||||||
|
$this->task = $task;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
if ( $this->task->completed_at !== null ) {
|
||||||
|
$bTitle = "Completed task";
|
||||||
|
} else {
|
||||||
|
$bTitle = "Active task";
|
||||||
|
}
|
||||||
|
$items = Loader::DAO( 'items' );
|
||||||
|
$items->getLineage( $this->task->item = $items->get( $this->task->item ) );
|
||||||
|
|
||||||
|
$box = Loader::View( 'box' , $bTitle , Loader::View( 'task_details' , $this->task ) );
|
||||||
|
if ( $this->task->completed_by === null ) {
|
||||||
|
$box->addButton( BoxButton::create( 'Edit task' , 'tasks/edit?id=' . $this->task->id )
|
||||||
|
->setClass( 'icon edit' ) )
|
||||||
|
->addButton( BoxButton::create( 'Mark as completed' ,
|
||||||
|
'tasks/finish?id=' . $this->task->id )
|
||||||
|
->setClass( 'icon stop' ) );
|
||||||
|
} else {
|
||||||
|
$box->addButton( BoxButton::create( 'Re-activate' , 'tasks/restart?id=' . $this->task->id )
|
||||||
|
->setClass( 'icon start' ) );
|
||||||
|
$timestamp = strtotime( $this->task->completed_at );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( Loader::DAO( 'tasks' )->canDelete( $this->task ) ) {
|
||||||
|
$box->addButton( BoxButton::create( 'Delete' , 'tasks/delete?id=' . $this->task->id )
|
||||||
|
->setClass( 'icon delete' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_TaskNotes
|
||||||
|
extends Controller
|
||||||
|
{
|
||||||
|
private $task;
|
||||||
|
|
||||||
|
public function __construct( $task )
|
||||||
|
{
|
||||||
|
$this->task = $task;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
$result = array( );
|
||||||
|
foreach ( $this->task->notes as $note ) {
|
||||||
|
$box = Loader::View( 'box' , null , Loader::View( 'task_note' , $note ) );
|
||||||
|
if ( $this->task->completed_at === null && $note->uid == $_SESSION[ 'uid' ] ) {
|
||||||
|
$box->addButton( BoxButton::create( 'Edit comment' , 'tasks/notes/edit?id=' . $note->id )
|
||||||
|
->setClass( 'icon edit' ) )
|
||||||
|
->addButton( BoxButton::create( 'Delete comment' , 'tasks/notes/delete?id=' . $note->id )
|
||||||
|
->setClass( 'icon delete' ) );
|
||||||
|
}
|
||||||
|
array_push( $result , $box );
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_DeleteTask
|
||||||
|
extends Controller
|
||||||
|
implements FormAware
|
||||||
|
{
|
||||||
|
private $form;
|
||||||
|
|
||||||
|
public function setForm( Form $form )
|
||||||
|
{
|
||||||
|
$this->form = $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
Loader::DAO( 'tasks' )->delete( (int) $this->form->field( 'id' )->value( ) );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_ToggleTask
|
||||||
|
extends Controller
|
||||||
|
{
|
||||||
|
private $restart;
|
||||||
|
|
||||||
|
public function __construct( $restart )
|
||||||
|
{
|
||||||
|
$this->isRestart = $restart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
// Check selected task
|
||||||
|
try {
|
||||||
|
$id = (int) $this->getParameter( 'id' , 'GET' );
|
||||||
|
} catch ( ParameterException $e ) {
|
||||||
|
return 'tasks';
|
||||||
|
}
|
||||||
|
|
||||||
|
$tasks = Loader::DAO( 'tasks' );
|
||||||
|
$task = $tasks->get( $id );
|
||||||
|
if ( $task === null ) {
|
||||||
|
return 'tasks';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $this->isRestart ) {
|
||||||
|
$tasks->restart( $id , '[AUTO] Task re-activated.' );
|
||||||
|
} else {
|
||||||
|
$tasks->finish( $id , '[AUTO] Task completed.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'tasks/view?id=' . $id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_EditTask
|
||||||
|
extends Controller
|
||||||
|
implements FormAware
|
||||||
|
{
|
||||||
|
private $form;
|
||||||
|
|
||||||
|
public function setForm( Form $form )
|
||||||
|
{
|
||||||
|
$this->form = $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
$id = $this->form->field( 'id' );
|
||||||
|
$item = $this->form->field( 'item' );
|
||||||
|
$name = $this->form->field( 'title' );
|
||||||
|
$priority = $this->form->field( 'priority' );
|
||||||
|
$description = $this->form->field( 'description' );
|
||||||
|
|
||||||
|
$error = Loader::DAO( 'tasks' )->updateTask( (int) $id->value( ) ,
|
||||||
|
(int) $item->value( ) , $name->value( ) ,
|
||||||
|
(int) $priority->value( ) , $description->value( ) );
|
||||||
|
|
||||||
|
switch ( $error ) {
|
||||||
|
|
||||||
|
case 0:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
$name->putError( "Une tâche porte déjà ce nom à cet endroit" );
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
$item->putError( "Cet endroit a été supprimé" );
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$name->putError( "Une erreur inconnue s'est produite ($error)" );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_AddTaskNoteForm
|
||||||
|
extends Controller
|
||||||
|
{
|
||||||
|
private $task;
|
||||||
|
|
||||||
|
public function __construct( $task )
|
||||||
|
{
|
||||||
|
$this->task = $task;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
return Loader::Create( 'Form' , 'Add' , 'add-note' , 'Add a comment' )
|
||||||
|
->setSuccessURL( 'tasks/view?id=' . $this->task->id )
|
||||||
|
->setAction( '?id=' . $this->task->id . '#add-note-form' )
|
||||||
|
->addField( Loader::Create( 'Field' , 'text' , 'textarea' )
|
||||||
|
->setDescription( 'Comment:' )
|
||||||
|
->setValidator( Loader::Create( 'Validator_StringLength' , 'This comment' , 5 ) ) )
|
||||||
|
->addController( Loader::Ctrl( 'add_task_note' , $this->task ) )
|
||||||
|
->controller( );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_AddTaskNote
|
||||||
|
extends Controller
|
||||||
|
implements FormAware
|
||||||
|
{
|
||||||
|
private $task;
|
||||||
|
private $form;
|
||||||
|
|
||||||
|
public function __construct( $task )
|
||||||
|
{
|
||||||
|
$this->task = $task;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setForm( Form $form )
|
||||||
|
{
|
||||||
|
$this->form = $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
Loader::DAO( 'tasks' )->addNote( $this->task->id , $this->form->field( 'text' )->value( ) );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_DeleteNote
|
||||||
|
extends Controller
|
||||||
|
implements FormAware
|
||||||
|
{
|
||||||
|
private $form;
|
||||||
|
|
||||||
|
public function setForm( Form $form )
|
||||||
|
{
|
||||||
|
$this->form = $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
Loader::DAO( 'tasks' )->deleteNote( (int) $this->form->field( 'id' )->value( ) );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_EditNote
|
||||||
|
extends Controller
|
||||||
|
implements FormAware
|
||||||
|
{
|
||||||
|
private $form;
|
||||||
|
|
||||||
|
public function setForm( Form $form )
|
||||||
|
{
|
||||||
|
$this->form = $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
$id = (int) $this->form->field( 'id' )->value( );
|
||||||
|
$text = $this->form->field( 'text' )->value( );
|
||||||
|
Loader::DAO( 'tasks' )->updateNote( $id , $text );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
35
includes/t-tasks/package.inc.php
Normal file
35
includes/t-tasks/package.inc.php
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$package[ 'requires' ][] = 'form';
|
||||||
|
$package[ 'requires' ][] = 't-data';
|
||||||
|
$package[ 'requires' ][] = 't-basics';
|
||||||
|
|
||||||
|
$package[ 'files' ][] = 'controllers';
|
||||||
|
$package[ 'files' ][] = 'page_controllers';
|
||||||
|
$package[ 'files' ][] = 'pages';
|
||||||
|
$package[ 'files' ][] = 'views';
|
||||||
|
|
||||||
|
$package[ 'ctrls' ][] = 'all_tasks';
|
||||||
|
$package[ 'ctrls' ][] = 'add_task_form';
|
||||||
|
$package[ 'ctrls' ][] = 'add_task';
|
||||||
|
$package[ 'ctrls' ][] = 'add_task_note_form';
|
||||||
|
$package[ 'ctrls' ][] = 'add_task_note';
|
||||||
|
$package[ 'ctrls' ][] = 'delete_note_form';
|
||||||
|
$package[ 'ctrls' ][] = 'delete_note';
|
||||||
|
$package[ 'ctrls' ][] = 'delete_task_form';
|
||||||
|
$package[ 'ctrls' ][] = 'delete_task';
|
||||||
|
$package[ 'ctrls' ][] = 'edit_note_form';
|
||||||
|
$package[ 'ctrls' ][] = 'edit_note';
|
||||||
|
$package[ 'ctrls' ][] = 'edit_task_form';
|
||||||
|
$package[ 'ctrls' ][] = 'edit_task';
|
||||||
|
$package[ 'ctrls' ][] = 'task_details';
|
||||||
|
$package[ 'ctrls' ][] = 'task_notes';
|
||||||
|
$package[ 'ctrls' ][] = 'toggle_task';
|
||||||
|
$package[ 'ctrls' ][] = 'view_task';
|
||||||
|
|
||||||
|
$package[ 'views' ][] = 'all_tasks';
|
||||||
|
$package[ 'views' ][] = 'tasks';
|
||||||
|
$package[ 'views' ][] = 'task_details';
|
||||||
|
$package[ 'views' ][] = 'task_note';
|
||||||
|
|
||||||
|
$package[ 'pages' ][] = 'tasks_tasks';
|
352
includes/t-tasks/page_controllers.inc.php
Normal file
352
includes/t-tasks/page_controllers.inc.php
Normal file
|
@ -0,0 +1,352 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Ctrl_AllTasks
|
||||||
|
extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$mode = $this->getParameter( 'mode' , 'GET' );
|
||||||
|
} catch ( ParameterException $e ) {
|
||||||
|
$mode = 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $mode == 'active' ) {
|
||||||
|
$tasks = Loader::DAO( 'tasks' )->getAllActiveTasks( );
|
||||||
|
$title = 'Active tasks';
|
||||||
|
$bTitle = 'Display all tasks';
|
||||||
|
$bMode = 'all';
|
||||||
|
} else {
|
||||||
|
$mode = 'all';
|
||||||
|
$tasks = Loader::DAO( 'tasks' )->getAllTasks( );
|
||||||
|
$title = 'All tasks';
|
||||||
|
$bTitle = 'Display active tasks only';
|
||||||
|
$bMode = 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
$tree = Loader::DAO( 'items' )->getTree( );
|
||||||
|
$box = Loader::View( 'box' , $title , Loader::View( 'all_tasks' , $tasks , $mode ) )
|
||||||
|
->addButton( BoxButton::create( $bTitle , 'tasks?mode=' . $bMode )
|
||||||
|
->setClass( 'icon refresh' ) );
|
||||||
|
if ( !empty( $tree ) ) {
|
||||||
|
$box ->addButton( BoxButton::create( 'New task' , 'tasks/add' )
|
||||||
|
->setClass( 'list-add' ) );
|
||||||
|
}
|
||||||
|
return $box;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
abstract class Ctrl_TaskFormBase
|
||||||
|
extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
protected final function createPrioritySelector( )
|
||||||
|
{
|
||||||
|
$select = Loader::Create( 'Field' , 'priority' , 'select' )
|
||||||
|
->setDescription( 'Priority:' )
|
||||||
|
->setValidator( Loader::Create( 'Validator_IntValue' , 'Priorité invalide' )
|
||||||
|
->setMinValue( 1 )->setMaxValue( 5 ) );
|
||||||
|
$tasks = Loader::DAO( 'tasks' );
|
||||||
|
|
||||||
|
for ( $i = 5 ; $i >= 1 ; $i -- ) {
|
||||||
|
$select->addOption( $i , $tasks->translatePriority( $i ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $select;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_AddTaskForm
|
||||||
|
extends Ctrl_TaskFormBase
|
||||||
|
{
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$target = (int) $this->getParameter( 'to' );
|
||||||
|
} catch ( ParameterException $e ) {
|
||||||
|
$target = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = Loader::Create( 'Form' , 'Add this task' , 'create-task' );
|
||||||
|
|
||||||
|
if ( $target === null ) {
|
||||||
|
$returnURL = 'tasks';
|
||||||
|
if ( ! $this->addItemSelector( $form ) ) {
|
||||||
|
return 'items';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$item = Loader::DAO( 'items' )->get( $target );
|
||||||
|
if ( $item === null ) {
|
||||||
|
return 'items';
|
||||||
|
}
|
||||||
|
$returnURL = 'items/view?id=' . $target;
|
||||||
|
|
||||||
|
$form->addField( Loader::Create( 'Field' , 'to' , 'hidden' )
|
||||||
|
->setDefaultValue( $target ) )
|
||||||
|
->addField( Loader::Create( 'Field' , 'item' , 'hidden' )
|
||||||
|
->setDefaultValue( $target ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$page->setTitle( 'New task' );
|
||||||
|
|
||||||
|
return $form->addField( Loader::Create( 'Field' , 'title' , 'text' )
|
||||||
|
->setDescription( 'Task title:' )
|
||||||
|
->setModifier( Loader::Create( 'Modifier_TrimString' ) )
|
||||||
|
->setValidator( Loader::Create( 'Validator_StringLength' , 'This title' , 5 , 256 ) ) )
|
||||||
|
->addField( $this->createPrioritySelector( )
|
||||||
|
->setDefaultValue( 3 ) )
|
||||||
|
->addField( Loader::Create( 'Field' , 'description' , 'textarea' )
|
||||||
|
->setDescription( 'Description:' )
|
||||||
|
->setMandatory( false ) )
|
||||||
|
->setURL( $returnURL )
|
||||||
|
->addController( Loader::Ctrl( 'add_task' ) )
|
||||||
|
->controller( );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function addItemSelector( $form )
|
||||||
|
{
|
||||||
|
$form->addField( $select = Loader::Create( 'Field' , 'item' , 'select' )
|
||||||
|
->setDescription( 'Item:' )
|
||||||
|
->addOption( '' , '(please select an item)' ) );
|
||||||
|
|
||||||
|
$items = Loader::DAO( 'items' )->getTreeList( );
|
||||||
|
if ( empty( $items ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
foreach ( $items as $item ) {
|
||||||
|
$name = '-' . str_repeat( '--' , $item->depth ) . ' ' . $item->name;
|
||||||
|
$select->addOption( $item->id , $name );
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_ViewTask
|
||||||
|
extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$id = (int) $this->getParameter( 'id' , 'GET' );
|
||||||
|
} catch ( ParameterException $e ) {
|
||||||
|
return 'tasks';
|
||||||
|
}
|
||||||
|
|
||||||
|
$task = Loader::DAO( 'tasks' )->get( $id );
|
||||||
|
if ( $task === null ) {
|
||||||
|
return 'tasks';
|
||||||
|
}
|
||||||
|
$page->setTitle( $task->title . ' (task)' );
|
||||||
|
|
||||||
|
$result = array( Loader::Ctrl( 'task_details' , $task ) );
|
||||||
|
|
||||||
|
if ( $task->completed_by === null ) {
|
||||||
|
array_push( $result , Loader::Ctrl( 'add_task_note_form' , $task ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
array_push( $result , Loader::Ctrl( 'task_notes' , $task ) );
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_DeleteTaskForm
|
||||||
|
extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
// Check selected task
|
||||||
|
try {
|
||||||
|
$id = (int) $this->getParameter( 'id' );
|
||||||
|
} catch ( ParameterException $e ) {
|
||||||
|
return 'tasks';
|
||||||
|
}
|
||||||
|
|
||||||
|
$tasks = Loader::DAO( 'tasks' );
|
||||||
|
$task = $tasks->get( $id );
|
||||||
|
if ( $task === null ) {
|
||||||
|
return 'tasks';
|
||||||
|
}
|
||||||
|
if ( ! $tasks->canDelete( $task ) ) {
|
||||||
|
return 'tasks/view?id=' . $id;
|
||||||
|
}
|
||||||
|
$page->setTitle( $task->title . ' (task)' );
|
||||||
|
|
||||||
|
// Generate confirmation text
|
||||||
|
$confText = HTML::make( 'div' )
|
||||||
|
->appendElement( HTML::make( 'p' )
|
||||||
|
->appendText( "You are about to delete this task, and any comment attached to it." ) )
|
||||||
|
->appendElement( HTML::make( 'p' )
|
||||||
|
->appendText( "This operation cannot be undone." ) );
|
||||||
|
|
||||||
|
// Generate form
|
||||||
|
return Loader::Create( 'Form' , 'Delete the task' , 'delete-task' , 'Task deletion' )
|
||||||
|
->addField( Loader::Create( 'Field' , 'id' , 'hidden' )
|
||||||
|
->setDefaultValue( $task->id ) )
|
||||||
|
->addField( Loader::Create( 'Field' , 'confirm' , 'html' )->setDefaultValue( $confText ) )
|
||||||
|
->setCancelURL( 'tasks/view?id=' . $task->id )
|
||||||
|
->setSuccessURL( 'items/view?id=' . $task->item )
|
||||||
|
->addController( Loader::Ctrl( 'delete_task' ) )
|
||||||
|
->controller( );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_EditTaskForm
|
||||||
|
extends Ctrl_TaskFormBase
|
||||||
|
{
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$id = (int) $this->getParameter( 'id' );
|
||||||
|
} catch ( ParameterException $e ) {
|
||||||
|
return 'tasks';
|
||||||
|
}
|
||||||
|
|
||||||
|
$task = Loader::DAO( 'tasks' )->get( $id );
|
||||||
|
if ( $task === null ) {
|
||||||
|
return 'tasks';
|
||||||
|
}
|
||||||
|
$page->setTitle( $task->title . ' (task)' );
|
||||||
|
|
||||||
|
|
||||||
|
return Loader::Create( 'Form' , 'Update task' , 'edit-task' , 'Editing task' )
|
||||||
|
->setURL( 'tasks/view?id=' . $task->id )
|
||||||
|
->addField( Loader::Create( 'Field' , 'id' , 'hidden' )
|
||||||
|
->setDefaultValue( $task->id ) )
|
||||||
|
->addField( $this->createItemSelector( )
|
||||||
|
->setDefaultValue( $task->item ) )
|
||||||
|
->addField( Loader::Create( 'Field' , 'title' , 'text' )
|
||||||
|
->setDescription( 'Title:' )
|
||||||
|
->setModifier( Loader::Create( 'Modifier_TrimString' ) )
|
||||||
|
->setValidator( Loader::Create( 'Validator_StringLength' , 'This title' , 5 , 256 ) )
|
||||||
|
->setDefaultValue( $task->title ) )
|
||||||
|
->addField( $this->createPrioritySelector( )
|
||||||
|
->setDefaultValue( $task->priority ) )
|
||||||
|
->addField( Loader::Create( 'Field' , 'description' , 'textarea' )
|
||||||
|
->setDescription( 'Description:' )
|
||||||
|
->setMandatory( false )
|
||||||
|
->setDefaultValue( $task->description ) )
|
||||||
|
->addController( Loader::Ctrl( 'edit_task' ) )
|
||||||
|
->controller( );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function createItemSelector( )
|
||||||
|
{
|
||||||
|
$select = Loader::Create( 'Field' , 'item' , 'select' )
|
||||||
|
->setDescription( 'On item:' );
|
||||||
|
|
||||||
|
$items = Loader::DAO( 'items' )->getTreeList( );
|
||||||
|
foreach ( $items as $item ) {
|
||||||
|
$name = '-' . str_repeat( '--' , $item->depth ) . ' ' . $item->name;
|
||||||
|
$select->addOption( $item->id , $name );
|
||||||
|
}
|
||||||
|
return $select;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_DeleteNoteForm
|
||||||
|
extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
// Check selected note
|
||||||
|
try {
|
||||||
|
$id = (int) $this->getParameter( 'id' );
|
||||||
|
} catch ( ParameterException $e ) {
|
||||||
|
return 'tasks';
|
||||||
|
}
|
||||||
|
|
||||||
|
$tasks = Loader::DAO( 'tasks' );
|
||||||
|
$note = $tasks->getNote( $id );
|
||||||
|
if ( $note === null ) {
|
||||||
|
return 'tasks';
|
||||||
|
}
|
||||||
|
if ( !$note->editable ) {
|
||||||
|
return 'tasks/view?id=' . $note->task;
|
||||||
|
}
|
||||||
|
$task = $tasks->get( $note->task );
|
||||||
|
$page->setTitle( $task->title . ' (task)' );
|
||||||
|
|
||||||
|
// Generate confirmation text
|
||||||
|
$confText = HTML::make( 'div' )
|
||||||
|
->appendElement( HTML::make( 'p' )
|
||||||
|
->appendText( 'You are about to delete a comment attached to this task.' ) )
|
||||||
|
->appendElement( HTML::make( 'p' )
|
||||||
|
->appendText( 'This operation cannot be undone.' ) );
|
||||||
|
|
||||||
|
// Generate form
|
||||||
|
return Loader::Create( 'Form' , 'Delete this comment' , 'delete-note' , 'Comment deletion' )
|
||||||
|
->addField( Loader::Create( 'Field' , 'id' , 'hidden' )
|
||||||
|
->setDefaultValue( $note->id ) )
|
||||||
|
->addField( Loader::Create( 'Field' , 'confirm' , 'html' )->setDefaultValue( $confText ) )
|
||||||
|
->setURL( 'tasks/view?id=' . $note->task )
|
||||||
|
->addController( Loader::Ctrl( 'delete_note' ) )
|
||||||
|
->controller( );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ctrl_EditNoteForm
|
||||||
|
extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function handle( Page $page )
|
||||||
|
{
|
||||||
|
// Check selected note
|
||||||
|
try {
|
||||||
|
$id = (int) $this->getParameter( 'id' );
|
||||||
|
} catch ( ParameterException $e ) {
|
||||||
|
return 'tasks';
|
||||||
|
}
|
||||||
|
|
||||||
|
$tasks = Loader::DAO( 'tasks' );
|
||||||
|
$note = $tasks->getNote( $id );
|
||||||
|
if ( $note === null ) {
|
||||||
|
return 'tasks';
|
||||||
|
}
|
||||||
|
if ( !$note->editable ) {
|
||||||
|
return 'tasks/view?id=' . $note->task;
|
||||||
|
}
|
||||||
|
$task = $tasks->get( $note->task );
|
||||||
|
$page->setTitle( $task->title . ' (task)' );
|
||||||
|
|
||||||
|
// Generate form
|
||||||
|
return Loader::Create( 'Form' , 'Update comment' , 'edit-note' )
|
||||||
|
->addField( Loader::Create( 'Field' , 'id' , 'hidden' )
|
||||||
|
->setDefaultValue( $note->id ) )
|
||||||
|
->addField( Loader::Create( 'Field' , 'text' , 'textarea' )
|
||||||
|
->setDescription( 'Comment:' )
|
||||||
|
->setValidator( Loader::Create( 'Validator_StringLength' , 'Le texte' , 5 ) )
|
||||||
|
->setDefaultValue( $note->text ) )
|
||||||
|
->setURL( 'tasks/view?id=' . $note->task )
|
||||||
|
->addController( Loader::Ctrl( 'edit_note' ) )
|
||||||
|
->controller( );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
22
includes/t-tasks/pages.inc.php
Normal file
22
includes/t-tasks/pages.inc.php
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Page_TasksTasks
|
||||||
|
extends AuthenticatedPage
|
||||||
|
{
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct( array(
|
||||||
|
'' => 'all_tasks' ,
|
||||||
|
'add' => 'add_task_form' ,
|
||||||
|
'delete' => 'delete_task_form' ,
|
||||||
|
'edit' => 'edit_task_form' ,
|
||||||
|
'finish' => array( 'toggle_task' , false ) ,
|
||||||
|
'restart' => array( 'toggle_task' , true ) ,
|
||||||
|
'view' => 'view_task' ,
|
||||||
|
'notes/edit' => 'edit_note_form' ,
|
||||||
|
'notes/delete' => 'delete_note_form' ,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
301
includes/t-tasks/views.inc.php
Normal file
301
includes/t-tasks/views.inc.php
Normal file
|
@ -0,0 +1,301 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
abstract class View_TasksBase
|
||||||
|
extends BaseURLAwareView
|
||||||
|
{
|
||||||
|
protected $tasks;
|
||||||
|
protected $dao;
|
||||||
|
|
||||||
|
|
||||||
|
protected function __construct( )
|
||||||
|
{
|
||||||
|
$this->dao = Loader::DAO( 'tasks' );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public final function render( )
|
||||||
|
{
|
||||||
|
if ( empty( $this->tasks ) ) {
|
||||||
|
return HTML::make( 'div' )
|
||||||
|
->setAttribute( 'class' , 'no-table' )
|
||||||
|
->appendText( 'No tasks to display.' );
|
||||||
|
}
|
||||||
|
return HTML::make( 'dl' )
|
||||||
|
->append( $this->generateList( ) )
|
||||||
|
->setAttribute( 'class' , 'tasks' );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function generateList( )
|
||||||
|
{
|
||||||
|
$result = array( );
|
||||||
|
$prevPriority = 6;
|
||||||
|
foreach ( $this->tasks as $task ) {
|
||||||
|
$priority = ( $task->completed_by === null ) ? $task->priority : -1;
|
||||||
|
if ( $priority !== $prevPriority ) {
|
||||||
|
if ( $priority == -1 ) {
|
||||||
|
$text = 'Completed tasks';
|
||||||
|
$extraClass = ' completed';
|
||||||
|
} else {
|
||||||
|
$text = $this->dao->translatePriority( $priority ) . ' priority';
|
||||||
|
$extraClass = '';
|
||||||
|
}
|
||||||
|
$prevPriority = $priority;
|
||||||
|
|
||||||
|
array_push( $result , HTML::make( 'dt' )
|
||||||
|
->setAttribute( 'class' , 'sub-title' . $extraClass )
|
||||||
|
->appendText( $text ) );
|
||||||
|
}
|
||||||
|
$result = array_merge( $result , $this->generateItem( $task ) );
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract function generateItem( $task );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class View_AllTasks
|
||||||
|
extends View_TasksBase
|
||||||
|
{
|
||||||
|
|
||||||
|
public function __construct( $tasks )
|
||||||
|
{
|
||||||
|
parent::__construct( );
|
||||||
|
$this->tasks = $tasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function generateItem( $task )
|
||||||
|
{
|
||||||
|
$cell = array( );
|
||||||
|
array_push( $cell , HTML::make( 'dt' )
|
||||||
|
->appendElement( HTML::make( 'a' )
|
||||||
|
->setAttribute( 'href' , $this->base . '/tasks/view?id=' . $task->id )
|
||||||
|
->appendText( $task->title ) ) );
|
||||||
|
|
||||||
|
array_push( $cell , HTML::make( 'dd' )->append( $this->formatPlaceLineage( $task->item ) ) );
|
||||||
|
|
||||||
|
$addedAt = strtotime( $task->added_at );
|
||||||
|
$addedAtDate = date( 'd/m/o' , $addedAt );
|
||||||
|
$addedAtTime = date( 'H:i:s' , $addedAt );
|
||||||
|
array_push( $cell ,
|
||||||
|
HTML::make( 'dd' )->appendText( "Added $addedAtDate at $addedAtTime by {$task->added_by}" ) );
|
||||||
|
|
||||||
|
if ( $task->completed_by !== null ) {
|
||||||
|
$completedAt = strtotime( $task->completed_at );
|
||||||
|
$completedAtDate = date( 'd/m/o' , $completedAt );
|
||||||
|
$completedAtTime = date( 'H:i:s' , $completedAt );
|
||||||
|
array_push( $cell , HTML::make( 'dd' )->appendText(
|
||||||
|
"Completed $completedAtDate at $completedAtTime by {$task->completed_by}" ) );
|
||||||
|
|
||||||
|
foreach ( $cell as $entry ) {
|
||||||
|
$entry->setAttribute( 'class' , 'completed' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatPlaceLineage( $item )
|
||||||
|
{
|
||||||
|
$item = Loader::DAO( 'items' )->get( $item );
|
||||||
|
$lineage = $item->lineage;
|
||||||
|
array_push( $lineage , $item->id );
|
||||||
|
|
||||||
|
$contents = array( );
|
||||||
|
foreach ( Loader::DAO( 'items' )->getAll( $lineage ) as $ancestor ) {
|
||||||
|
if ( ! empty( $contents ) ) {
|
||||||
|
array_push( $contents , ' » ' );
|
||||||
|
}
|
||||||
|
array_push( $contents , HTML::make( 'a' )
|
||||||
|
->setAttribute( 'href' , $this->base . '/items/view?id=' . $ancestor->id )
|
||||||
|
->appendText( $ancestor->name ) );
|
||||||
|
}
|
||||||
|
array_unshift( $contents, 'On ' );
|
||||||
|
|
||||||
|
return $contents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class View_Tasks
|
||||||
|
extends View_TasksBase
|
||||||
|
{
|
||||||
|
public function __construct( $tasks )
|
||||||
|
{
|
||||||
|
parent::__construct( );
|
||||||
|
$this->tasks = $tasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected function generateItem( $task )
|
||||||
|
{
|
||||||
|
$cell = array( );
|
||||||
|
array_push( $cell , HTML::make( 'dt' )
|
||||||
|
->appendElement( HTML::make( 'a' )
|
||||||
|
->setAttribute( 'href' , $this->base . '/tasks/view?id=' . $task->id )
|
||||||
|
->appendText( $task->title ) ) );
|
||||||
|
|
||||||
|
$addedAt = strtotime( $task->added_at );
|
||||||
|
$addedAtDate = date( 'd/m/o' , $addedAt );
|
||||||
|
$addedAtTime = date( 'H:i:s' , $addedAt );
|
||||||
|
array_push( $cell ,
|
||||||
|
HTML::make( 'dd' )->appendText( "Added $addedAtDate at $addedAtTime by {$task->added_by}" ) );
|
||||||
|
|
||||||
|
if ( $task->completed_by !== null ) {
|
||||||
|
$completedAt = strtotime( $task->completed_at );
|
||||||
|
$completedAtDate = date( 'd/m/o' , $completedAt );
|
||||||
|
$completedAtTime = date( 'H:i:s' , $completedAt );
|
||||||
|
array_push( $cell , HTML::make( 'dd' )->appendText(
|
||||||
|
"Completed $completedAtDate at $completedAtTime by {$task->completed_by}" ) );
|
||||||
|
foreach ( $cell as $entry ) {
|
||||||
|
$entry->setAttribute( 'class' , 'completed' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class View_TaskDetails
|
||||||
|
extends BaseURLAwareView
|
||||||
|
{
|
||||||
|
private $task;
|
||||||
|
|
||||||
|
public function __construct( $task )
|
||||||
|
{
|
||||||
|
$this->task = $task;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render( )
|
||||||
|
{
|
||||||
|
$list = HTML::make( 'dl' )
|
||||||
|
->setAttribute( 'class' , 'tasks' )
|
||||||
|
->appendElement( HTML::make( 'dt' )
|
||||||
|
->appendText( 'On item:' ) )
|
||||||
|
->appendElement( HTML::make( 'dd' )
|
||||||
|
->append( $this->formatPlaceLineage( $this->task->item ) ) );
|
||||||
|
|
||||||
|
if ( $this->task->description != '' ) {
|
||||||
|
$list->appendElement( HTML::make( 'dt' )
|
||||||
|
->appendText( 'Description:' ) )
|
||||||
|
->appendElement( HTML::make( 'dd' )
|
||||||
|
->appendRaw( $this->formatDescription( ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$list->appendElement( HTML::make( 'dt' )
|
||||||
|
->appendText( 'Added:' ) )
|
||||||
|
->appendElement( HTML::make( 'dd' )
|
||||||
|
->appendText( $this->formatAction( $this->task->added_at , $this->task->added_by ) ) );
|
||||||
|
|
||||||
|
if ( $this->task->completed_by === null ) {
|
||||||
|
$list->appendElement( HTML::make( 'dt' )
|
||||||
|
->appendText( 'Priority:' ) )
|
||||||
|
->appendElement( HTML::make( 'dd' )
|
||||||
|
->appendText( Loader::DAO( 'tasks' )
|
||||||
|
->translatePriority( $this->task->priority ) ) );
|
||||||
|
} else {
|
||||||
|
$list->appendElement( HTML::make( 'dt' )
|
||||||
|
->appendText( 'Completed:' ) )
|
||||||
|
->appendElement( HTML::make( 'dd' )
|
||||||
|
->appendText( $this->formatAction(
|
||||||
|
$this->task->completed_at , $this->task->completed_by ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatPlaceLineage( $item )
|
||||||
|
{
|
||||||
|
$lineage = $item->lineage;
|
||||||
|
array_push( $lineage , $item->id );
|
||||||
|
|
||||||
|
$contents = array( );
|
||||||
|
foreach ( Loader::DAO( 'items' )->getAll( $lineage ) as $ancestor ) {
|
||||||
|
if ( ! empty( $contents ) ) {
|
||||||
|
array_push( $contents , ' » ' );
|
||||||
|
}
|
||||||
|
array_push( $contents , HTML::make( 'a' )
|
||||||
|
->setAttribute( 'href' , $this->base . '/items/view?id=' . $ancestor->id )
|
||||||
|
->appendText( $ancestor->name ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function formatDescription( )
|
||||||
|
{
|
||||||
|
$description = HTML::from( $this->task->description );
|
||||||
|
return preg_replace( '/\n/s' , '<br/>' , $description );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function formatAction( $timestamp , $user )
|
||||||
|
{
|
||||||
|
$ts = strtotime( $timestamp );
|
||||||
|
$tsDate = date( 'd/m/o' , $ts );
|
||||||
|
$tsTime = date( 'H:i:s' , $ts );
|
||||||
|
return "$tsDate at $tsTime by $user";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class View_TaskNote
|
||||||
|
implements View
|
||||||
|
{
|
||||||
|
private $note;
|
||||||
|
|
||||||
|
public function __construct( $note )
|
||||||
|
{
|
||||||
|
$this->note = $note;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render( )
|
||||||
|
{
|
||||||
|
$text = HTML::make( 'p' )
|
||||||
|
->appendRaw( preg_replace( '/\n/s' , '<br/>' , HTML::from( $this->note->text ) ) );
|
||||||
|
|
||||||
|
$ts = strtotime( $this->note->added_at );
|
||||||
|
$tsDate = date( 'd/m/o' , $ts );
|
||||||
|
$tsTime = date( 'H:i:s' , $ts );
|
||||||
|
$details = HTML::make( 'div')
|
||||||
|
->setAttribute( 'style' , 'font-size: 9pt' )
|
||||||
|
->appendElement( HTML::make( 'em' )
|
||||||
|
->appendText( "Note added $tsDate at $tsTime by {$this->note->author}" ) );
|
||||||
|
|
||||||
|
return array( $text , $details );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class View_TaskDependencies
|
||||||
|
implements View
|
||||||
|
{
|
||||||
|
private $task;
|
||||||
|
private $reverse;
|
||||||
|
|
||||||
|
public function __construct( $task , $reverse )
|
||||||
|
{
|
||||||
|
$this->task = $task;
|
||||||
|
$this->reverse = $reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render( )
|
||||||
|
{
|
||||||
|
$source = $this->reverse ? 'reverseDependencies' : 'dependencies';
|
||||||
|
$list = HTML::make( 'ul' )->setAttribute( 'class' , 'dep-list' );
|
||||||
|
foreach ( $this->task->$source as $dependency ) {
|
||||||
|
$link = HTML::make( 'a' )
|
||||||
|
->setAttribute( 'href' , 'tasks/view?id=' . $dependency->id )
|
||||||
|
->appendText( $dependency->title );
|
||||||
|
if ( ! $this->reverse ) {
|
||||||
|
$link->setAttribute( 'class' , ( $dependency->completed == 't' )
|
||||||
|
? 'satisfied' : 'missing' );
|
||||||
|
}
|
||||||
|
$list->appendElement( HTML::make( 'li' )->appendElement( $link ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
includes/t-users/package.inc.php
Normal file
5
includes/t-users/package.inc.php
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$package[ 'requires' ][] = 'core';
|
||||||
|
$package[ 'files' ][] = 'users';
|
||||||
|
$package[ 'daos' ][] = 'users';
|
43
includes/t-users/users.inc.php
Normal file
43
includes/t-users/users.inc.php
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
class Dao_Users
|
||||||
|
extends DAO
|
||||||
|
{
|
||||||
|
private function hashPassword( $password , $salt , $iterations )
|
||||||
|
{
|
||||||
|
$hash = $password;
|
||||||
|
$salt = trim( $salt );
|
||||||
|
do {
|
||||||
|
$hash = sha1( "$salt$hash$salt" );
|
||||||
|
$iterations --;
|
||||||
|
} while ( $iterations > 0 );
|
||||||
|
return $hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getUser( $email )
|
||||||
|
{
|
||||||
|
$query = $this->query( 'SELECT * FROM users WHERE user_email = LOWER( $1 )' );
|
||||||
|
$results = $query->execute( $email );
|
||||||
|
if ( empty( $results ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return array_shift( $results );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function checkLogin( $email , $password )
|
||||||
|
{
|
||||||
|
$userData = $this->getUser( $email );
|
||||||
|
if ( $userData != null ) {
|
||||||
|
$hashed = $this->hashPassword( $password ,
|
||||||
|
$userData->user_salt ,
|
||||||
|
$userData->user_iterations );
|
||||||
|
if ( $hashed === $userData->user_hash ) {
|
||||||
|
return $userData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
5
site/.htaccess.sample
Normal file
5
site/.htaccess.sample
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
RewriteEngine on
|
||||||
|
#RewriteBase /
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^(.*)$ index.php/$1 [L,QSA]
|
BIN
site/icons.png
Normal file
BIN
site/icons.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
8
site/index.php
Normal file
8
site/index.php
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
include ( '../arse/includes/loader.inc.php' );
|
||||||
|
Loader::AddPath( dirname( __FILE__ ) . '/../includes' );
|
||||||
|
Loader::Create( 'URLMapper' , 'tasks' )->fromPathInfo( );
|
||||||
|
|
||||||
|
|
||||||
|
?>
|
395
site/style.css
Normal file
395
site/style.css
Normal file
|
@ -0,0 +1,395 @@
|
||||||
|
/*
|
||||||
|
* PAGE
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
text-align: center;
|
||||||
|
background-color: #3f3f3f;
|
||||||
|
min-width: 792px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: "DejaVu Serif", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.page-container {
|
||||||
|
margin: 10px auto;
|
||||||
|
width: 752px;
|
||||||
|
min-height: 300px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 20px 20px 32px 20px;
|
||||||
|
text-align: left;
|
||||||
|
box-shadow: 0 0 500px 20px white;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.page-container > h1:first-child {
|
||||||
|
background-color: black;
|
||||||
|
border-color: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
color: white;
|
||||||
|
font-size: 16pt;
|
||||||
|
margin: -20px -20px 0px -20px;
|
||||||
|
padding: 30px 10px 10px 15px;
|
||||||
|
box-shadow: 0px 5px 15px #3f3f3f;
|
||||||
|
text-shadow: 3px 3px 2px #7f7f7f;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.page-container > h1.no-menu:first-child {
|
||||||
|
padding-top: 10px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.page-container > ul.page-menu {
|
||||||
|
margin: -75px -20px 75px -20px;
|
||||||
|
border-color: white;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 20px 20px 0px 0px;
|
||||||
|
border-width: 1px 1px 0px 1px;
|
||||||
|
background-color: #3f3f7f;
|
||||||
|
padding: 5px 15px 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.page-container > ul.page-menu li {
|
||||||
|
display: inline-block;
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.page-container > ul.page-menu a {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 5px;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 10pt;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.page-container > ul.page-menu a:hover {
|
||||||
|
color: yellow;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* CONTENT
|
||||||
|
*/
|
||||||
|
|
||||||
|
dt {
|
||||||
|
font-size: 12pt;
|
||||||
|
color: #3f3f3f;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #3f3f7f;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* BOXES (GENERAL)
|
||||||
|
*/
|
||||||
|
|
||||||
|
div.box {
|
||||||
|
margin: 20px 0 0 60px;
|
||||||
|
width: 600px;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 5px 5px 15px #3f3f7f;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: #3f3f7f;
|
||||||
|
background-color: #afafff;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2.box-title {
|
||||||
|
font-size: 13pt;
|
||||||
|
margin: -15px -15px 0 -15px;
|
||||||
|
padding: 3px 0px 3px 20px;
|
||||||
|
background-color: #3f3f7f;
|
||||||
|
color: white;
|
||||||
|
border-radius: 14px;
|
||||||
|
text-shadow: 2px 2px 1px #7f7f7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.box-buttons {
|
||||||
|
margin: -24px 0px 0px 0px;
|
||||||
|
height: 18px;
|
||||||
|
width: 610px;
|
||||||
|
text-align: right;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.box-buttons a {
|
||||||
|
display: inline-block;
|
||||||
|
background-clip: content-box;
|
||||||
|
background-color: white;
|
||||||
|
background-origin: content-box;
|
||||||
|
margin-left: 4px;
|
||||||
|
color: black;
|
||||||
|
font-size: 10pt;
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.box-buttons:first-child a {
|
||||||
|
border-color: #3f3f7f;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.box-buttons a:hover {
|
||||||
|
background-color: black;
|
||||||
|
color: yellow;
|
||||||
|
box-shadow: 0px 0px 5px 3px yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.box-buttons:first-child a:hover {
|
||||||
|
border-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.box-buttons a span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.box-contents {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.box-contents:first-child {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FORMS
|
||||||
|
*/
|
||||||
|
|
||||||
|
div.form a.form-cancel::after {
|
||||||
|
content: 'X';
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form form , .form dl {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form dt:first-child { margin: 0 }
|
||||||
|
.form dt { margin-top: 10px }
|
||||||
|
|
||||||
|
.form dt.mandatory.field label:after {
|
||||||
|
content: ' (required)';
|
||||||
|
color: #0000ff;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form dd.field { padding-top: 3px }
|
||||||
|
|
||||||
|
.form dt.field + dd.form-error { padding-top: 3px }
|
||||||
|
|
||||||
|
.form dd {
|
||||||
|
margin-right: 40px;
|
||||||
|
margin-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form dd.form-error {
|
||||||
|
font-size: 11pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #7f0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form input , .form select , .form textarea {
|
||||||
|
border-radius: 10px;
|
||||||
|
border-color: #efefff;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
background-color: #cfcfff;
|
||||||
|
width: 100%;
|
||||||
|
padding: 3px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form input:focus , .form select:focus, .form textarea:focus {
|
||||||
|
border-color: #7f7fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form textarea {
|
||||||
|
font-family: "DejaVu Mono", monospace;
|
||||||
|
font-size: 10pt;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form dd.erroneous * {
|
||||||
|
border-color: #ff0000;
|
||||||
|
background-color: #ffcfcf;
|
||||||
|
color: #7f0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form dd.erroneous select > option:disabled {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form dt.submit-button { margin-top: 20px }
|
||||||
|
.form dt.submit-button input {
|
||||||
|
padding: 5px 0;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #3f3f7f;
|
||||||
|
border-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form dd.html-section {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form dd.html-section p {
|
||||||
|
font-size: 11pt;
|
||||||
|
text-align: justify;
|
||||||
|
text-indent: 20px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form dd.html-section p:first-child {
|
||||||
|
padding-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form dd.html-section p:last-child {
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* LISTS
|
||||||
|
*/
|
||||||
|
|
||||||
|
div.list div.box-contents {
|
||||||
|
margin-left: -15px;
|
||||||
|
margin-right: -15px;
|
||||||
|
width: 630px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.list table {
|
||||||
|
width: 630px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.list tr > *:first-child { padding-left: 5px }
|
||||||
|
div.list tr > *:last-child { padding-right: 5px }
|
||||||
|
div.list th , div.list td { text-align: left }
|
||||||
|
div.list tr:nth-child(2) > * { padding-top: 10px }
|
||||||
|
div.list tr:nth-child(odd) { background-color: #cfcfff }
|
||||||
|
div.list tr.header { background-color: #afafff }
|
||||||
|
|
||||||
|
div.list .header th {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0 0 1px 0;
|
||||||
|
border-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.list th.align-center, div.list td.align-center { text-align: center }
|
||||||
|
div.list th.align-right, div.list td.align-right { text-align: right }
|
||||||
|
|
||||||
|
div.list td a {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.box-contents div.no-table {
|
||||||
|
margin: 20px 0 10px 0;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* VARIOUS BOX BUTTONS
|
||||||
|
*/
|
||||||
|
|
||||||
|
div.box-buttons a.icon::after {
|
||||||
|
content: 'x';
|
||||||
|
color: transparent;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.box-buttons a.icon {
|
||||||
|
background-image: url( "icons.png" );
|
||||||
|
}
|
||||||
|
|
||||||
|
div.box-buttons a.icon + a:not(.icon ) {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.box-buttons a.move { background-position: 0px 0px }
|
||||||
|
div.box-buttons a.move:hover { background-position: -18px 0px }
|
||||||
|
|
||||||
|
div.box-buttons a.edit { background-position: 0px -18px }
|
||||||
|
div.box-buttons a.edit:hover { background-position: -18px -18px }
|
||||||
|
|
||||||
|
div.box-buttons a.delete { background-position: 0px -36px }
|
||||||
|
div.box-buttons a.delete:hover { background-position: -18px -36px }
|
||||||
|
|
||||||
|
div.box-buttons a.refresh { background-position: 0px -54px }
|
||||||
|
div.box-buttons a.refresh:hover { background-position: -18px -54px }
|
||||||
|
|
||||||
|
div.box-buttons a.stop { background-position: 0px -72px }
|
||||||
|
div.box-buttons a.stop:hover { background-position: -18px -72px }
|
||||||
|
|
||||||
|
div.box-buttons a.start { background-position: 0px -90px }
|
||||||
|
div.box-buttons a.start:hover { background-position: -18px -90px }
|
||||||
|
|
||||||
|
div.box-buttons a.list-add::after {
|
||||||
|
content: '+';
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TASKS
|
||||||
|
*/
|
||||||
|
|
||||||
|
dl.tasks dt:not(:first-child) {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.tasks dd, dl.tasks dd * {
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.tasks dt.sub-title {
|
||||||
|
margin: 20px -15px 0px -15px;
|
||||||
|
padding: 22px 5px 2px 5px;
|
||||||
|
background-color: #5F5FAF;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 2px 2px 1px #7F7F7F;
|
||||||
|
box-shadow: inset 0px 20px 20px #AFAFFF, inset 0px -5px 5px #AFAFFF;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.tasks .completed, dl.tasks .completed * {
|
||||||
|
color: #3f3f3f
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.tasks dt.sub-title.completed {
|
||||||
|
color: #bfbfbf;
|
||||||
|
text-shadow: 1px 1px 2px #3f3f3f;
|
||||||
|
}
|
Loading…
Reference in a new issue