Browse Source

Issue/Project creation and display

Nathaniel van Diepen 8 years ago
parent
commit
96fabdf6e3

+ 1 - 0
config.php

@@ -7,4 +7,5 @@
 	define('URL_BASE','/bugs/');
 	define('URL_HOST','localhost');
 	define('ADMIN_EMAIL','bugs@localhost');
+	define('DEFAULT_PRIORITY',4);
 ?>

+ 1 - 1
install/db_install/00_tables/issues.sql

@@ -9,7 +9,7 @@
 DROP TABLE IF EXISTS `issues`;
 CREATE TABLE IF NOT EXISTS `issues` (
   `id` int(10) NOT NULL,
-  `p_id` int(10) NOT NULL,
+  `p_id` int(10) NULL,
   `u_id` int(10) DEFAULT NULL,
   `pr_id` int(10) NOT NULL,
   `s_id` int(10) NOT NULL,

+ 1 - 1
install/db_install/00_tables/projects.sql

@@ -9,7 +9,7 @@
 DROP TABLE IF EXISTS `projects`;
 CREATE TABLE IF NOT EXISTS `projects` (
   `id` int(10) NOT NULL,
-  `p_id` int(10) NOT NULL,
+  `p_id` int(10) NULL,
   `s_id` int(10) NOT NULL,
   `u_id` int(10) NOT NULL,
   `name` varchar(50) COLLATE utf8_bin NOT NULL,

+ 10 - 0
install/db_install/01_triggers/project_insert.sql

@@ -0,0 +1,10 @@
+DROP TRIGGER IF EXISTS `project_insert`;
+CREATE TRIGGER `project_insert`
+BEFORE INSERT ON `projects`
+	FOR EACH ROW
+		IF new.name REGEXP '^[[:alpha:]]' THEN
+			SET new.date_modified = NOW();
+		ELSE
+			SIGNAL SQLSTATE '45000'
+			SET MESSAGE_TEXT = 'Project names must start with a letter';
+		END IF;

+ 0 - 5
install/db_install/01_triggers/project_insert_date_modified.sql

@@ -1,5 +0,0 @@
-DROP TRIGGER IF EXISTS `project_insert_date_modified`;
-CREATE TRIGGER `project_insert_date_modified`
-BEFORE INSERT ON `projects`
-	FOR EACH ROW
-		SET new.date_modified = NOW();

+ 10 - 0
install/db_install/01_triggers/project_update.sql

@@ -0,0 +1,10 @@
+DROP TRIGGER IF EXISTS `project_update`;
+CREATE TRIGGER `project_update`
+BEFORE UPDATE ON `projects`
+	FOR EACH ROW
+		IF new.name REGEXP '^[[:alpha:]]' THEN
+			SET new.date_modified = NOW();
+		ELSE
+			SIGNAL SQLSTATE '45000'
+			SET MESSAGE_TEXT = 'Project names must start with a letter';
+		END IF;

+ 0 - 5
install/db_install/01_triggers/project_update_date_modified.sql

@@ -1,5 +0,0 @@
-DROP TRIGGER IF EXISTS `project_update_date_modified`;
-CREATE TRIGGER `project_update_date_modified`
-BEFORE UPDATE ON `projects`
-	FOR EACH ROW
-		SET new.date_modified = NOW();

+ 1 - 1
install/db_install/02_constraints/projects.sql

@@ -3,5 +3,5 @@
 --
 ALTER TABLE `projects`
   ADD CONSTRAINT `projects_ibfk_1` FOREIGN KEY (`u_id`) REFERENCES `users` (`id`),
-  ADD CONSTRAINT `projects_ibfk_2` FOREIGN KEY (`p_id`) REFERENCES `projects` (`id`),
+  ADD CONSTRAINT `projects_ibfk_2` FOREIGN KEY (`p_id`) REFERENCES `projects` (`id`) ON UPDATE CASCADE,
   ADD CONSTRAINT `projects_ibfk_3` FOREIGN KEY (`s_id`) REFERENCES `statuses` (`id`);

+ 4 - 0
install/index.php

@@ -47,6 +47,10 @@
 						$info = '';
 						if($user){
 							$user->active = true;
+							Bugs::$sql->query("
+								INSERT INTO r_permission_user (per_id,u_id)
+								VALUES (1,?)
+							",'i',$user->id)->execute();
 							if(!Bugs::login($user,$_POST['password'])){
 								$pass = false;
 								$info = 'Failed to automatically log in.';

+ 34 - 0
js/issue.js

@@ -0,0 +1,34 @@
+ready(function(){
+	dom.get('#form-issue')
+		.on('submit',function(e){
+			var form = this,
+				id = dom.get(form).get('[name=id]').value;
+			global.settings.fetch.native = true;
+			fetch(id===null?'./issue/complete':BASE_URL+'/issue/'+id+'/update',{
+				method: 'post',
+				body: new FormData(form),
+				mode: 'cors',
+				credentials: 'include'
+			})
+			.then(function(res){
+				return res.json();
+			})
+			.then(function(data){
+				console.log(data);
+				if(data.error){
+					alert(data.error);
+				}else{
+					form.reset();
+					location.assign(BASE_URL+'/!'+data.id);
+				}
+			})
+			.catch(function(e){
+				alert(e);
+			});
+			e.stopPropagation();
+			if(e.cancelable){
+				e.preventDefault();
+			}
+			return false;
+		});
+});

+ 1 - 1
js/juju

@@ -1 +1 @@
-Subproject commit 3682577cff727b3e98ef655d611b1206dc64febd
+Subproject commit b38fdca0819e943dda9957d1cf794ff44d710a4b

+ 34 - 0
js/project.js

@@ -0,0 +1,34 @@
+ready(function(){
+	dom.get('#form-project')
+		.on('submit',function(e){
+			var form = this,
+				id = dom.get(form).get('[name=id]').value;
+			global.settings.fetch.native = true;
+			fetch(id===null?BASE_URL+'/create/project/complete':BASE_URL+'/project/'+id+'/update',{
+				method: 'post',
+				body: new FormData(form),
+				mode: 'cors',
+				credentials: 'include'
+			})
+			.then(function(res){
+				return res.json();
+			})
+			.then(function(data){
+				console.log(data);
+				if(data.error){
+					alert(data.error);
+				}else{
+					form.reset();
+					location.assign(BASE_URL+'/project/'+data.name);
+				}
+			})
+			.catch(function(e){
+				alert(e);
+			});
+			e.stopPropagation();
+			if(e.cancelable){
+				e.preventDefault();
+			}
+			return false;
+		});
+});

+ 57 - 7
lib/bugs.class.php

@@ -11,7 +11,10 @@
 		public static $sql;
 		public static $cache = array(
 			'users'=>array(),
-			'issue'=>array()
+			'issue'=>array(),
+			'projects'=>array(),
+			'statuses'=>array(),
+			'priorities'=>array()
 		);
 		public static $user = false;
 		public function __construct(){
@@ -151,19 +154,46 @@
 				return $user['id'];
 			}
 		}
+		static function project_id($name){
+			$project = static::$sql->query("
+				SELECT id
+				FROM projects
+				WHERE name = ?;
+			",'s',$name)->assoc_result;
+			if(is_null($project)){
+				return false;
+			}else{
+				return $project['id'];
+			}
+		}
 		static function issue($id){
+			if(func_num_args()>1){
+				$id = new Issue(
+					func_get_arg(0),
+					func_get_arg(1),
+					func_num_args()>=3?func_get_arg(2):null,
+					func_num_args()>=4?func_get_arg(3):null,
+					func_num_args()==5?func_get_arg(4):null
+				);
+				$id = $id->id;
+			}
 			if(!isset(static::$cache['issues'][$id])){
 				static::$cache['issues'][$id] = new Issue($id);
 			}
 			return static::$cache['issues'][$id];
 		}
 		static function project($id){
-			if(is_string($id)){
-				$id = static::$sql->query("
-					SELECT id
-					FROM projects
-					WHERE name = ?;
-				",'s',$id)->assoc_result['id'];
+			if(func_num_args()==1){
+				if(is_string($id)){
+					$id = static::$sql->query("
+						SELECT id
+						FROM projects
+						WHERE name = ?;
+					",'s',$id)->assoc_result['id'];
+				}
+			}else{
+				$id = new Project(func_get_arg(0),func_get_arg(1),func_num_args()==3?func_get_arg(2):null);
+				$id = $id->id;
 			}
 			if(!isset(static::$cache['projects'][$id])){
 				static::$cache['projects'][$id] = new Project($id);
@@ -179,6 +209,26 @@
 				static::action($action);
 			}
 		}
+		static function status($id){
+			if(empty(static::$cache['statuses'][$id])){
+				static::$cache['statuses'][$id] = static::$sql->query("
+					SELECT max(name) name
+					FROM statuses
+					WHERE id = ?
+				",'i',intval($id))->assoc_result['name'];
+			}
+			return static::$cache['statuses'][$id];
+		}
+		static function priority($id){
+			if(empty(static::$cache['priorities'][$id])){
+				static::$cache['priorities'][$id] = static::$sql->query("
+					SELECT max(name) name
+					FROM priorities
+					WHERE id = ?
+				",'i',intval($id))->assoc_result['name'];
+			}
+			return static::$cache['priorities'][$id];
+		}
 		static function action($action){
 			$id = static::$sql->query("
 				SELECT id

+ 129 - 19
lib/issue.class.php

@@ -1,4 +1,7 @@
 <?php
+	if(!defined('DEFAULT_PRIORITY')){
+		define('DEFAULT_PRIORITY',4);
+	}
 	class Issue implements JsonSerializable{
 		public $id;
 		public $cache = array(
@@ -12,21 +15,49 @@
 			'date_modified'=>null
 		);
 		public function __construct($id){
-			$this->id = intval($id);
-			$cache = Bugs::$sql->query("
-				SELECT	p_id,
-						u_id,
-						pr_id,
-						s_id,
-						name,
-						description,
-						date_created,
-						date_modified
-				FROM issues
-				WHERE id = ?;
-			",'i',$this->id)->assoc_result;
-			foreach($cache as $key => $value){
-				$this->cache[$key] = $value;
+			//name,description[,priority[,user[,project]]]
+			switch(func_num_args()){
+				case 5:case 4:case 3:case 2:
+					$name = func_get_arg(0);
+					if(
+						Bugs::$sql->query("
+							INSERT INTO issues (name,description,pr_id,u_id,p_id,s_id)
+							VALUES (?,?,?,?,?,1);
+						",'ssiii',
+							$name,
+							func_get_arg(1),
+							func_num_args()>=3&&!empty(func_get_arg(2))?func_get_arg(2):DEFAULT_PRIORITY,
+							func_num_args()>=4&&!empty(func_get_arg(3))?func_get_arg(3)->id:Bugs::$user->id,
+							func_num_args()==5&&!empty(func_get_arg(4))?func_get_arg(4)->id:null
+						)->execute()
+					){
+						$id = Bugs::$sql->insert_id;
+						if($id === 0){
+							trigger_error("Failed to create issue with name {$name}.");
+						}
+					}else{
+						trigger_error(Bugs::$sql->error);
+					}
+				case 1:
+					$this->id = intval($id);
+					$cache = Bugs::$sql->query("
+						SELECT	p_id,
+								u_id,
+								pr_id,
+								s_id,
+								name,
+								description,
+								date_created,
+								date_modified
+						FROM issues
+						WHERE id = ?;
+					",'i',$this->id)->assoc_result;
+					foreach($cache as $key => $value){
+						$this->cache[$key] = $value;
+					}
+				break;
+				default:
+					trigger_error('Invalid Arguments');
 			}
 		}
 		public function jsonSerialize(){
@@ -45,14 +76,15 @@
 			switch($name){
 				case 'name':case 'description':
 					Bugs::$sql->query("
-						UPDATE users
+						UPDATE issues
 						SET {$name} = ?
 						WHERE id = ?
 					",'si',$value,$this->id)->execute();
+					$this->cache[$name] = $value;
 				break;
 				case 'p_id':case 's_id':case 'u_id':case 'pr_id':
 					Bugs::$sql->query("
-						UPDATE users
+						UPDATE issues
 						SET {$name} = ?
 						WHERE id = ?
 					",'ii',$value,$this->id)->execute();
@@ -64,7 +96,7 @@
 				break;
 				case 'user':
 					if($value instanceof User){
-						$this->p_id = $value->id;
+						$this->u_id = $value->id;
 					}
 				break;
 				default:
@@ -75,14 +107,92 @@
 		}
 		public function __get($name){
 			switch($name){
-				case 'date_registered':case 'date_modified':
+				case 'date_created':case 'date_modified':
 					return strtotime($this->cache[$name]);
 				break;
+				case 'user_ids':
+					return array_column(
+						Bugs::$sql->query("
+							SELECT	distinct r.u_id
+							FROM r_issue_user r
+							RIGHT JOIN issue_roles ir
+								ON ir.id = r.r_id
+							WHERE r.i_id = ?
+						",'i',$this->id)->assoc_results,
+						'u_id'
+					);
+				break;
+				case 'users':
+					$users = Bugs::$sql->query("
+						SELECT	r.u_id,
+								pr.name
+						FROM r_issue_user r
+						RIGHT JOIN issue_roles ir
+							ON ir.id = r.r_id
+						WHERE r.i_id = ?
+					",'i',$this->id)->assoc_results;
+					$ret = array();
+					foreach($users as $user){
+						if(!isset($ret[$user['name']])){
+							$ret[$user['name']] = array();
+						}
+						if(!is_null($user['u_id'])){
+							array_push($ret[$user['name']],Bugs::user($user['u_id']));
+						}
+					}
+					return $ret;
+				break;
+				case 'roles':
+					return $this->roles(Bugs::$user);
+				break;
+				case 'user':
+					return Bugs::user($this->u_id);
+				break;
+				case 'project':
+					return $this->p_id?Bugs::project($this->p_id):false;
+				break;
+				case 'status':
+					return Bugs::status($this->s_id);
+				break;
+				case 'priority':
+					return Bugs::priority($this->pr_id);
+				break;
 				default:
 					if(isset($this->cache)){
 						return $this->cache[$name];
 					}
 			}
 		}
+		public function permission($permission,$user=null){
+			$user = is_null($user)?Bugs::$user:$user;
+			return $user->admin || (
+				$user->permission('issue_'.$permission) &&
+				in_array($user->id, $this->user_ids)
+			);
+		}
+		public function roles($user){
+			return array_column(
+				Bugs::$sql->query("
+					SELECT	distinct pr.name
+					FROM r_issue_user r
+					RIGHT JOIN issue_roles ir
+						ON ir.id = r.r_id
+					WHERE r.i_id = ?
+					AND r.u_id = ?
+				",'ii',$this->id,$user->id)->assoc_results,
+				'name'
+			);
+		}
+		public function role($role,$user=null){
+			return Bugs::$sql->query("
+				SELECT	count(1) count
+				FROM r_issue_user r
+				RIGHT JOIN issue_roles ir
+					ON ir.id = r.r_id
+					AND ir.name = ?
+				WHERE r.i_id = ?
+				AND r.u_id = ?
+			",'sii',$role,$this->id,$user?$user->id:Bugs::$user->id)->assoc_result['count']!==0;
+		}
 	}
 ?>

+ 138 - 17
lib/project.class.php

@@ -12,20 +12,47 @@
 			'date_modified'=>null
 		);
 		public function __construct($id){
-			$this->id = intval($id);
-			$cache = Bugs::$sql->query("
-				SELECT	p_id,
-						s_id,
-						u_id,
-						name,
-						description,
-						date_created,
-						date_modified
-				FROM issues
-				WHERE id = ?;
-			",'i',$this->id)->assoc_result;
-			foreach($cache as $key => $value){
-				$this->cache[$key] = $value;
+			switch(func_num_args()){
+				// name, description, user
+				case 3:
+					$user = func_get_arg(2)?func_get_arg(2):Bugs::$user;
+					$name= func_get_arg(0);
+					Bugs::$sql->query("
+						INSERT INTO projects (name,description,u_id,s_id)
+						VALUES (?,?,?,1)
+					",'ssi',
+						func_get_arg(0),
+						func_get_arg(1),
+						$user->id
+					)->execute();
+					$id = Bugs::$sql->insert_id;
+					if($id === 0){
+						trigger_error("Failed to create project with name {$name}.");
+					}
+				// id
+				case 1:
+					$this->id = intval($id);
+					$cache = Bugs::$sql->query("
+						SELECT	p_id,
+								s_id,
+								u_id,
+								name,
+								description,
+								date_created,
+								date_modified
+						FROM projects
+						WHERE id = ?;
+					",'i',$this->id)->assoc_result;
+					if($cache){
+						foreach($cache as $key => $value){
+							$this->cache[$key] = $value;
+						}
+					}else{
+						trigger_error("Project with id {$id} does not exist");
+					}
+				break;
+				default:
+					trigger_error("Invalid Arguments");
 			}
 		}
 		public function jsonSerialize(){
@@ -44,17 +71,19 @@
 			switch($name){
 				case 'name':case 'description':
 					Bugs::$sql->query("
-						UPDATE users
+						UPDATE projects
 						SET {$name} = ?
 						WHERE id = ?
 					",'si',$value,$this->id)->execute();
+					$this->cache[$name] = $value;
 				break;
 				case 'p_id':case 's_id':case 'u_id':
 					Bugs::$sql->query("
-						UPDATE users
+						UPDATE projects
 						SET {$name} = ?
 						WHERE id = ?
 					",'ii',$value,$this->id)->execute();
+					$this->cache[$name] = $value;
 				break;
 				case 'parent':
 					if($value instanceof Project){
@@ -79,14 +108,106 @@
 						return Bugs::Project($this->p_id);
 					}
 				break;
-				case 'date_registered':case 'date_modified':
+				case 'date_created':case 'date_modified':
 					return strtotime($this->cache[$name]);
 				break;
+				case 'user':
+					return Bugs::user($this->u_id);
+				break;
+				case 'users':
+					$users = Bugs::$sql->query("
+						SELECT	r.u_id,
+								pr.name
+						FROM r_project_user r
+						RIGHT JOIN project_roles pr
+							ON pr.id = r.r_id
+						WHERE r.p_id = ?
+					",'i',$this->id)->assoc_results;
+					$ret = array();
+					foreach($users as $user){
+						if(!isset($ret[$user['name']])){
+							$ret[$user['name']] = array();
+						}
+						if(!is_null($user['u_id'])){
+							array_push($ret[$user['name']],Bugs::user($user['u_id']));
+						}
+					}
+					return $ret;
+				break;
+				case 'user_ids':
+					return array_column(
+						Bugs::$sql->query("
+							SELECT	distinct r.u_id
+							FROM r_project_user r
+							RIGHT JOIN project_roles pr
+								ON pr.id = r.r_id
+							WHERE r.p_id = ?
+						",'i',$this->id)->assoc_results,
+						'u_id'
+					);
+				break;
+				case 'roles':
+					return $this->roles(Bugs::$user);
+				break;
+				case 'issue_ids':
+					return array_column(
+						Bugs::$sql->query("
+							SELECT id
+							FROM issues
+							WHERE p_id = ?
+						",'i',$this->id)->assoc_results,
+						'id'
+					);
+				break;
+				case 'issues':
+					$issues = array();
+					foreach($this->issue_ids as $id){
+						array_push($issues,Bugs::issue($id));
+					}
+					return $issues;
+				break;
+				case 'status':
+					return Bugs::status($this->s_id);
+				break;
 				default:
 					if(isset($this->cache)){
 						return $this->cache[$name];
 					}
 			}
 		}
+		public function permission($permission,$user=null){
+			$user = is_null($user)?Bugs::$user:$user;
+			return $user->admin || (
+				$user->permission('project_'.$permission) &&
+				in_array($user->id, $this->user_ids)
+			);
+		}
+		public function roles($user){
+			return array_column(
+				Bugs::$sql->query("
+					SELECT	distinct pr.name
+					FROM r_project_user r
+					RIGHT JOIN project_roles pr
+						ON pr.id = r.r_id
+					WHERE r.p_id = ?
+					AND r.u_id = ?
+				",'ii',$this->id,$user->id)->assoc_results,
+				'name'
+			);
+		}
+		public function role($role,$user=null){
+			return Bugs::$sql->query("
+				SELECT	count(1) count
+				FROM r_project_user r
+				RIGHT JOIN project_roles pr
+					ON pr.id = r.r_id
+					AND pr.name = ?
+				WHERE r.p_id = ?
+				AND r.u_id = ?
+			",'sii',$role,$this->id,$user?$user->id:Bugs::$user->id)->assoc_result['count']!==0;
+		}
+		public function issue($name,$description,$priority=null,$user=null){
+			return Bugs::issue($name,$description,$priority,$user,$this);
+		}
 	}
 ?>

+ 19 - 3
lib/sql.class.php

@@ -17,6 +17,7 @@
 		*/
 		private $sql;
 		public $insert_id;
+		private $emsg;
 		public function __construct($server,$user,$pass,$db){
 			$this->sql = new mysqli($server,$user,$pass,$db) or die('Unable to connect to mysql');
 		}
@@ -26,7 +27,14 @@
 		public function __get($name){
 			switch($name){
 				case 'error':
-					return $this->sql->error;
+					return empty($this->sql->error)?$this->emsg:$this->sql->error;
+				break;
+			}
+		}
+		public function __set($name,$value){
+			switch($name){
+				case 'error':
+					$this->emsg = $value;
 				break;
 			}
 		}
@@ -67,8 +75,12 @@
 			$this->parent = $sql;
 			$this->sql = $sql();
 			$this->query = $sql()->prepare($source);
-			if(!is_null($types)){
-				call_user_func_array(array($this->query, 'bind_param'),make_referenced($args)) or trigger_error($sql()->error);
+			if($this->query){
+				if(!is_null($types)){
+					call_user_func_array(array($this->query, 'bind_param'),make_referenced($args)) or trigger_error($sql()->error);
+				}
+			}else{
+				trigger_error($sql->error);
 			}
 		}
 		public function __invoke(){
@@ -78,6 +90,7 @@
 			if($this->query){
 				$r = $this->query->execute();
 				$this->parent->insert_id = $this->sql->insert_id;
+				$this->parent->error = $this->error;
 				$this->sql->commit();
 				return $r;
 			}else{
@@ -175,6 +188,9 @@
 				case 'insert_id':
 					return $this->parent->insert_id;
 				break;
+				case 'error':
+					return $this->query->error;
+				break;
 			}
 		}
 	}

+ 37 - 0
lib/user.class.php

@@ -126,6 +126,43 @@
 					}
 					return $perms;
 				break;
+				case 'admin':
+					return $this->permission('*');
+				break;
+				case 'project_ids':
+					return array_column(
+						Bugs::$sql->query("
+							SELECT id
+							FROM projects
+							where u_id = ?
+						",'i',$this->id)->assoc_results,
+						'id'
+					);
+				break;
+				case 'projects':
+					$projects = array();
+					foreach($this->project_ids as $id){
+						array_push($projects,Bugs::project($id));
+					}
+					return $projects;
+				break;
+				case 'issue_ids':
+					return array_column(
+						Bugs::$sql->query("
+							SELECT id
+							FROM issues
+							where u_id = ?
+						",'i',$this->id)->assoc_results,
+						'id'
+					);
+				break;
+				case 'issues':
+					$issues = array();
+					foreach($this->issue_ids as $id){
+						array_push($issues,Bugs::issue($id));
+					}
+					return $issues;
+				break;
 				default:
 					if(isset($this->cache)){
 						return $this->cache[$name];

+ 33 - 4
paths/issue.php

@@ -1,13 +1,42 @@
 <?php
 	Router::paths(array(
 		'/!{issue}'=>function($res,$args){
-			$res->header('Content-Type','application/json');
-			$res->json(array(
-				'issue'=> Bugs::issue($args->issue)
-			));
+			$res->write(
+				Bugs::template('issue')
+					->run(Bugs::issue($args->issue))
+			);
 		},
 		'/issue/{issue}'=>function($res,$args){
 			Router::redirect(Router::url(Router::$base.'/!'.$args->issue));
+		},
+		'/issue/{issue}/update'=>function($res,$args){
+			if(!empty($_POST['name'])&&!empty($_POST['description'])){
+				$issue = Bugs::issue($args->issue);
+				$issue->name = $_POST['name'];
+				$issue->description = $_POST['description'];
+				$res->json(array(
+					'id'=>$issue->id
+				));
+			}else{
+				$res->json(array(
+					'error'=>'You must specify a name and description.'
+				));
+			}
+		},
+		'/create/issue'=>function($res,$args){
+			$res->write(Bugs::template('issue'));
+		},
+		'/create/issue/complete'=>function($res,$args){
+			if(!empty($_POST['name'])&&!empty($_POST['description'])){
+				$issue = Bugs::issue($_POST['name'],$_POST['description']);
+				$res->json(array(
+					'id'=>$issue->id
+				));
+			}else{
+				$res->json(array(
+					'error'=>'You must specify a name and description.'
+				));
+			}
 		}
 	));
 ?>

+ 61 - 4
paths/project.php

@@ -1,16 +1,73 @@
 <?php
+	Bugs::actions(
+		'project_create',
+		'project_update',
+		'project_delete'
+	);
 	Router::paths(array(
 		'/project/{project}'=>function($res,$args){
-			$res->header('Content-Type','application/json');
-			$res->json(array(
-				'project'=> Bugs::project($args->project)
-			));
+			$res->write(
+				Bugs::template('project')
+					->run(Bugs::project(is_numeric($args->project)?intval($args->project):$args->project)
+				)
+			);
 		},
 		'/project/{project}/issue/{issue}'=>function($res,$args){
 			Router::redirect(Router::url(Router::$base."/!{$args->issue}"));
 		},
 		'/project/{project}/!{issue}'=>function($res,$args){
 			Router::redirect(Router::url(Router::$base."!{$args->issue}"));
+		},
+		'/project/{project}/update'=>function($res,$args){
+			if(!empty($_POST['name'])&&!empty($_POST['description'])){
+				$project = Bugs::project(is_numeric($args->project)?intval($args->project):$args->project);
+				$project->name = $_POST['name'];
+				$project->description = $_POST['description'];
+				$res->json(array(
+					'name'=>$project->name
+				));
+			}else{
+				$res->json(array(
+					'error'=>'You must specify a name and description.'
+				));
+			}
+		},
+		'/project/{project}/create/issue'=>function($res,$args){
+			$res->write(Bugs::template('issue'));
+		},
+		'/project/{project}/create/issue/complete'=>function($res,$args){
+			if(!empty($_POST['name'])&&!empty($_POST['description'])){
+				$issue = Bugs::project(is_numeric($args->project)?intval($args->project):$args->project)
+					->issue($_POST['name'],$_POST['description']);
+				$res->json(array(
+					'id'=>$issue->id
+				));
+			}else{
+				$res->json(array(
+					'error'=>'You must specify a name and description.'
+				));
+			}
+		},
+		'/create/project'=>function($res,$args){
+			$res->write(Bugs::template('project'));
+		},
+		'/create/project/complete'=>function($res,$args){
+			if(!empty($_POST['name'])){
+				if(!Bugs::project_id($_POST['name'])){
+					$project = Bugs::project($_POST['name'],$_POST['description']);
+					$res->json(array(
+						'name'=>$project->name
+					));
+				}else{
+					$res->json(array(
+						'error'=>"A project with the name {$_POST['name']} already exists."
+					));
+				}
+			}else{
+				$res->json(array(
+					'error'=>'No name specified'
+				));
+			}
 		}
 	));
 ?>

+ 4 - 2
paths/register.php

@@ -1,6 +1,7 @@
 <?php
 	Bugs::actions(
-		'register'
+		'user_register',
+		'user_activate'
 	);
 	Router::paths(array(
 		'/register'=>function($res,$args){
@@ -39,7 +40,7 @@
 					$user->email('Registered',"
 						<a href=\"http://".URL_HOST.URL_BASE."/register/activate/{$user->name}/{$user->activation_code}\">Activate Account</a>
 					");
-					Bugs::activity('register',$_POST['name'].' has registered.');
+					Bugs::activity('user_register',$_POST['name'].' has registered.');
 				}else{
 					Router::redirect(
 						Router::url(Router::$base."/register/error/User {$_POST['name']} already exists.")
@@ -56,6 +57,7 @@
 						Bugs::template('activated')
 							->run($user)
 					);
+					Bugs::activity('user_activate',$_POST['name'].' has activated their account.');
 				}else{
 					trigger_error("User is already active");
 				}

+ 2 - 1
paths/user.php

@@ -1,6 +1,6 @@
 <?php
 	Bugs::actions(
-		'view_profile'
+		'user_update'
 	);
 	Router::paths(array(
 		'/~{user}'=>function($res,$args){
@@ -30,6 +30,7 @@
 						if($user->id == Bugs::$user->id){
 							Bugs::login($user,$_POST['password']);
 						}
+						Bugs::activity('user_update',$_POST['name'].' has updated their profile.');
 					}else{
 						$res->json(array(
 							'error'=>'Invalid password.'

+ 86 - 0
templates/issue.php

@@ -0,0 +1,86 @@
+<?php
+	// Expecting the context to be a issue or nothing at all
+	global $context;
+	($context?$context->permission('read'):Bugs::$user->permission('issue_read')) or trigger_error('You are not allowed to view this issue');
+	$update = $context?$context->permission('update'):Bugs::$user->permission('issue_create');
+	$delete = $context?$context->permission('delete'):Bugs::$user->permission('issue_delete');
+	function getval($name){
+		global $context;
+		return $context?$context->{$name}:null;
+	}
+?>
+<!doctype html>
+	<head>
+		<meta charset="utf8"/>
+		<title>Issue <?=getval('name');?></title>
+		<script src="<?=Router::url(Router::$base)?>/js/juju/core.js"></script>
+		<script src="<?=Router::url(Router::$base)?>/js/juju/page.js"></script>
+		<script src="<?=Router::url(Router::$base)?>/js/juju/dom.js"></script>
+		<script src="<?=Router::url(Router::$base)?>/js/juju/keyboard.js"></script>
+		<script src="<?=Router::url(Router::$base)?>/js/juju/mouse.js"></script>
+		<script src="<?=Router::url(Router::$base)?>/js/issue.js"></script>
+		<script>
+			BASE_URL = '<?=Router::url(Router::$base)?>';
+		</script>
+		<link rel="stylesheet" href="<?=Router::url(Router::$base)?>/css/main.css"></link>
+	</head>
+	<body>
+		<a href="<?=Router::url(Router::$base)?>">Home</a>
+		<form id="form-issue" method="post">
+			<div>
+				<label for="name">Name:</label>
+				<input value="<?=getval('name');?>" <?=$update?'name="name"':'disabled="disabled"';?>/>
+			</div>
+			<div>
+				<label for="description">Description:</label>
+				<input type="description" value="<?=getval('description');?>" <?=$update?'name="description"':'disabled="disabled"';?>/>
+			</div>
+			<?php
+				if($context){
+			?>
+				<div>
+					<label>Status:</label>
+					<?=getval('status')?>
+				</div>
+				<div>
+					<label>Priority:</label>
+					<?=getval('priority')?>
+				</div>
+				<div>
+					<label>User:</label>
+					<a href="<?=Router::url(Router::$base.'/~'.getval('user')->name)?>">
+						<?=getval('user')->name;?>
+					</a>
+				</div>
+				<?php
+					if($context->project){
+				?>
+					<div>
+						<label>Project:</label>
+						<a href="<?=Router::url(Router::$base.'/project/'.getval('project')->name)?>">
+							<?=getval('project')->name;?>
+						</a>
+					</div>
+				<?php
+					}
+				?>
+				<div>
+					<label>Date Registered:</label>
+					<time datetime="<?=date('c',getval('date_created'));?>"><?=date('Y-m-d',getval('date_created'));?></time>
+				</div>
+				<div>
+					<label>Date Modified:</label>
+					<time datetime="<?=date('c',getval('date_modified'));?>"><?=date('Y-m-d',getval('date_modified'));?></time>
+				</div>
+				<input type="hidden" name="id" value="<?=getval('id')?>"/>
+			<?php
+				}
+				if($update){
+			?>
+				<input type="submit" value="<?=$context?'Update':'Create'?>"/>
+			<?php
+				}
+			?>
+		</form>
+	</body>
+</html>

+ 22 - 0
templates/main.php

@@ -20,6 +20,28 @@
 			<a href="<?=Router::url(Router::$base.'/sessions')?>">Sessions</a>
 			<a href="<?=Router::url(Router::$base.'/~'.Bugs::$user->name)?>">Profile</a>
 			<a href="<?=Router::url(Router::$base.'/logout')?>">Logout</a>
+			<div>
+				<h3>Projects</h3>
+				<a href="<?=Router::url(Router::$base.'/create/project')?>">New</a>
+				<ul>
+					<?php
+						foreach(Bugs::$user->projects as $project){
+							echo "<li>({$project->status}) <a href=\"".Router::url(Router::$base."/project/{$project->name}")."\">{$project->name}</a></li>";
+						}
+					?>
+				</ul>
+			</div>
+			<div>
+				<h3>Issues</h3>
+				<a href="<?=Router::url(Router::$base.'/create/issue')?>">New</a>
+				<ul>
+					<?php
+						foreach(Bugs::$user->issues as $issue){
+							echo "<li>({$issue->status} - {$issue->priority}) <a href=\"".Router::url(Router::$base."/!{$issue->id}")."\">{$issue->name}</a></li>";
+						}
+					?>
+				</ul>
+			</div>
 		<?php
 			}else{
 		?>

+ 87 - 0
templates/project.php

@@ -0,0 +1,87 @@
+<?php
+	// Expecting the context to be a project or nothing at all
+	global $context;
+	($context?$context->permission('read'):Bugs::$user->permission('project_read')) or trigger_error('You are not allowed to view this project');
+	$update = $context?$context->permission('update'):Bugs::$user->permission('project_create');
+	$delete = $context?$context->permission('delete'):Bugs::$user->permission('project_delete');
+	function getval($name){
+		global $context;
+		return $context?$context->{$name}:null;
+	}
+?>
+<!doctype html>
+	<head>
+		<meta charset="utf8"/>
+		<title>Project <?=getval('name');?></title>
+		<script src="<?=Router::url(Router::$base)?>/js/juju/core.js"></script>
+		<script src="<?=Router::url(Router::$base)?>/js/juju/page.js"></script>
+		<script src="<?=Router::url(Router::$base)?>/js/juju/dom.js"></script>
+		<script src="<?=Router::url(Router::$base)?>/js/juju/keyboard.js"></script>
+		<script src="<?=Router::url(Router::$base)?>/js/juju/mouse.js"></script>
+		<script src="<?=Router::url(Router::$base)?>/js/project.js"></script>
+		<script>
+			BASE_URL = '<?=Router::url(Router::$base)?>';
+		</script>
+		<link rel="stylesheet" href="<?=Router::url(Router::$base)?>/css/main.css"></link>
+	</head>
+	<body>
+		<a href="<?=Router::url(Router::$base)?>">Home</a>
+		<form id="form-project" method="post">
+			<div>
+				<label for="name">Name:</label>
+				<input value="<?=getval('name');?>" <?=$update?'name="name"':'disabled="disabled"';?>/>
+			</div>
+			<div>
+				<label for="description">Description:</label>
+				<input type="description" value="<?=getval('description');?>" <?=$update?'name="description"':'disabled="disabled"';?>/>
+			</div>
+			<?php
+				if($context){
+			?>
+				<div>
+					<label>Status:</label>
+					<?=getval('status')?>
+				</div>
+				<div>
+					<label>User:</label>
+					<a href="<?=Router::url(Router::$base.'/~'.getval('user')->name)?>">
+						<?=getval('user')->name;?>
+					</a>
+				</div>
+				<div>
+					<label>Date Registered:</label>
+					<time datetime="<?=date('c',getval('date_created'));?>"><?=date('Y-m-d',getval('date_created'));?></time>
+				</div>
+				<div>
+					<label>Date Modified:</label>
+					<time datetime="<?=date('c',getval('date_modified'));?>"><?=date('Y-m-d',getval('date_modified'));?></time>
+				</div>
+				<input type="hidden" name="id" value="<?=getval('id')?>"/>
+			<?php
+				}
+				if($update){
+			?>
+				<input type="submit" value="<?=$context?'Update':'Create'?>"/>
+			<?php
+				}
+			?>
+		</form>
+		<?php
+			if($context){
+		?>
+			<div>
+				<h3>Issues</h3>
+				<a href="<?=Router::url(Router::$base."/project/{$context->name}/create/issue")?>">New</a>
+				<ul>
+					<?php
+						foreach($context->issues as $issue){
+							echo "<li>({$issue->status} - {$issue->priority}) <a href=\"".Router::url(Router::$base."/!{$issue->id}")."\">{$issue->name}</a></li>";
+						}
+					?>
+				</ul>
+			</div>
+		<?php
+			}
+		?>
+	</body>
+</html>

+ 20 - 0
templates/user.php

@@ -44,5 +44,25 @@
 				}
 			?>
 		</form>
+		<div>
+			<h3>Projects</h3>
+			<ul>
+				<?php
+					foreach($context->projects as $project){
+						echo "<li>({$project->status}) <a href=\"".Router::url(Router::$base."/project/{$project->name}")."\">{$project->name}</a></li>";
+					}
+				?>
+			</ul>
+		</div>
+		<div>
+			<h3>Issues</h3>
+			<ul>
+				<?php
+					foreach($context->issues as $issue){
+						echo "<li>({$issue->status} - {$issue->priority}) <a href=\"".Router::url(Router::$base."/!{$issue->id}")."\">{$issue->name}</a></li>";
+					}
+				?>
+			</ul>
+		</div>
 	</body>
 </html>