Browse Source

Adding hashtag suppport #64

Alan Hardman 5 years ago
parent
commit
7048f62874

+ 49 - 0
app/controller/tag.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace Controller;
+
+class Tag extends \Controller {
+
+	protected $_userId;
+
+	public function __construct() {
+		$this->_userId = $this->_requireLogin();
+	}
+
+	/**
+	 * Tag index route (/tag/)
+	 * @param \Base $f3
+	 */
+	public function index($f3) {
+		$tag = new \Model\Issue\Tag;
+		$cloud = $tag->cloud();
+		shuffle($cloud);
+		$f3->set("title", "Issue Tags");
+		$f3->set("cloud", $cloud);
+		$this->_render("tag/index.html");
+	}
+
+	/**
+	 * Single tag route (/tag/@tag)
+	 * @param \Base $f3
+	 * @param array $params
+	 */
+	public function single($f3, $params) {
+		$tag = new \Model\Issue\Tag;
+		$tag->load(array("tag = ?", $params["tag"]));
+
+		if(!$tag->id) {
+			$f3->error(404);
+			return;
+		}
+
+		$issue = new \Model\Issue\Detail;
+		$issue_ids = implode(',', $tag->issues());
+
+		$f3->set("title", "#" . $params["tag"]);
+		$f3->set("tag", $tag);
+		$f3->set("issues.subset", $issue->find("id IN ($issue_ids)"));
+		$this->_render("tag/single.html");
+	}
+
+}

+ 1 - 1
app/controller/user.php

@@ -64,7 +64,7 @@ class User extends \Controller {
 		));
 
 		$watchlist = new \Model\Issue\Watcher();
-		$f3->set("watchlist", $watchlist->findby_watcher($f3, $this->_userId, $order));
+		$f3->set("watchlist", $watchlist->findby_watcher($this->_userId, $order));
 
 
 		$tasks = new \Model\Issue\Detail();

+ 6 - 0
app/dict/en.ini

@@ -182,6 +182,12 @@ dict.project_tree=Project Tree
 dict.n_complete={0} complete
 dict.n_child_issues={0} child issues
 
+; Tags
+dict.issue_tags=Issue Tags
+dict.no_tags_created=No issue tags have been created yet.
+dict.tag_help_1=Tag an issue by adding a #hashtag to its description.
+dict.tag_help_2=Hashtags can contain letters, numbers, and hyphens, and must start with a letter.
+
 ; Taskboard
 dict.hours_remaining=Hours Remaining
 dict.ideal_hours_remaining=Ideal Hours Remaining

+ 2 - 1
app/helper/view.php

@@ -29,9 +29,10 @@ class View extends \Template {
 			->setDimensionlessImages(true);
 		$val = $tex->parse($str);
 
-		// Find issue IDs and convert to links
+		// Find issue IDs and tags, and convert them to links
 		$siteUrl = $f3->get("site.url");
 		$val = preg_replace("/(?<=[\s,\(^])#([0-9]+)(?=[\s,\)\.,$])/", "<a href=\"{$siteUrl}issues/$1\">#$1</a>", $val);
+		$val = preg_replace("/(?<=\W|^)#([a-z][a-z0-9_-]*[a-z0-9]+)(?=\W|$)/i", "<a href=\"{$siteUrl}tag/$1\">#$1</a>", $val);
 
 		// Convert URLs to links
 		$val = $this->make_clickable($val);

+ 24 - 2
app/model/issue.php

@@ -229,10 +229,33 @@ class Issue extends \Model {
 			return $issue;
 		}
 
+		$this->saveTags();
+
 		return empty($issue) ? parent::save() : $issue;
 	}
 
 	/**
+	 * Finds and saves the current issue's tags
+	 * @return Issue
+	 */
+	function saveTags() {
+		$tag = new \Model\Issue\Tag;
+		$issue_id = $this->get("id");
+		$str = $this->get("description");
+		$count = preg_match_all("/(?<=\W#|^#)[a-z][a-z0-9_-]*[a-z0-9]+(?=\W|$)/i", $str, $matches);
+		$tag->deleteByIssueId($issue_id);
+		if($count) {
+			foreach($matches[0] as $match) {
+				$tag->reset();
+				$tag->tag = str_replace("_", "-", $match);
+				$tag->issue_id = $issue_id;
+				$tag->save();
+			}
+		}
+		return $this;
+	}
+
+	/**
 	 * Duplicate issue and all sub-issues
 	 * @return Issue
 	 */
@@ -305,8 +328,7 @@ class Issue extends \Model {
 			if($replace_existing) {
 				$query .= " AND sprint_id IS NULL";
 			}
-			$db = $f3->get("db.instance");
-			$db->exec(
+			$this->db->exec(
 				$query,
 				array(
 					":sprint" => $this->get("sprint_id"),

+ 45 - 0
app/model/issue/tag.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace Model\Issue;
+
+class Tag extends \Model {
+
+	protected $_table_name = "issue_tag";
+
+	/**
+	 * Delete all stored tags for an issue
+	 * @param  int $issue_id
+	 * @return Tag
+	 */
+	public function deleteByIssueId($issue_id) {
+		$this->db->exec("DELETE FROM {$this->_table_name} WHERE issue_id = ?", $issue_id);
+		return $this;
+	}
+
+	/**
+	 * Get a multidimensional array representing a tag cloud
+	 * @return array
+	 */
+	public function cloud() {
+		return $this->db->exec("SELECT tag, COUNT(*) AS freq FROM {$this->_table_name} GROUP BY tag");
+	}
+
+	/**
+	 * Find issues with the given/current tag
+	 * @param  string $tag
+	 * @return array Issue IDs
+	 */
+	public function issues($tag = '') {
+		if(!$tag) {
+			$tag = $this->get("tag");
+		}
+		$result = $this->db->exec("SELECT DISTINCT issue_id FROM {$this->_table_name} WHERE tag = ?", $tag);
+		$return = array();
+		foreach($result as $r) {
+			$return[] = $r["issue_id"];
+		}
+		return $return;
+	}
+
+}
+

+ 8 - 3
app/model/issue/watcher.php

@@ -6,9 +6,14 @@ class Watcher extends \Model {
 
 	protected $_table_name = "issue_watcher";
 
-	public function findby_watcher ($f3, $user_id, $orderby = 'id') {
-		$db = $f3->get("db.instance");
-		return $db->exec(
+	/**
+	 * Find watched issues by user ID
+	 * @param  int    $user_id
+	 * @param  string $orderby
+	 * @return array
+	 */
+	public function findby_watcher ($user_id, $orderby = 'id') {
+		return $this->db->exec(
 			'SELECT i.* FROM issue_detail i JOIN issue_watcher w on i.id = w.issue_id  '.
 			'WHERE w.user_id = :user_id AND  i.deleted_date IS NULL AND i.closed_date IS NULL AND i.status_closed = 0 AND i.owner_id != :user_id2 '.
 			'ORDER BY :orderby ',

+ 3 - 5
app/model/user.php

@@ -111,8 +111,6 @@ class User extends \Model {
 	 * @return array
 	 */
 	public function stats($time = 0) {
-		$db = \Base::instance()->get("db.instance");
-
 		\Helper\View::instance()->utc2local();
 		$offset = \Base::instance()->get("site.timeoffset");
 
@@ -121,7 +119,7 @@ class User extends \Model {
 		}
 
 		$result = array();
-		$result["spent"] = $db->exec(
+		$result["spent"] = $this->db->exec(
 			"SELECT DATE(DATE_ADD(u.created_date, INTERVAL :offset SECOND)) AS `date`, SUM(f.new_value - f.old_value) AS `val`
 			FROM issue_update u
 			JOIN issue_update_field f ON u.id = f.issue_update_id AND f.field = 'hours_spent'
@@ -129,14 +127,14 @@ class User extends \Model {
 			GROUP BY DATE(DATE_ADD(u.created_date, INTERVAL :offset2 SECOND))",
 			array(":user" => $this->id, ":offset" => $offset, ":offset2" => $offset, ":date" => date("Y-m-d H:i:s", $time))
 		);
-		$result["closed"] = $db->exec(
+		$result["closed"] = $this->db->exec(
 			"SELECT DATE(DATE_ADD(i.closed_date, INTERVAL :offset SECOND)) AS `date`, COUNT(*) AS `val`
 			FROM issue i
 			WHERE i.owner_id = :user AND i.closed_date > :date
 			GROUP BY DATE(DATE_ADD(i.closed_date, INTERVAL :offset2 SECOND))",
 			array(":user" => $this->id, ":offset" => $offset, ":offset2" => $offset, ":date" => date("Y-m-d H:i:s", $time))
 		);
-		$result["created"] = $db->exec(
+		$result["created"] = $this->db->exec(
 			"SELECT DATE(DATE_ADD(i.created_date, INTERVAL :offset SECOND)) AS `date`, COUNT(*) AS `val`
 			FROM issue i
 			WHERE i.author_id = :user AND i.created_date > :date

+ 4 - 0
app/routes.ini

@@ -37,6 +37,10 @@ GET /issues/close/@id = Controller\Issues->close
 GET /issues/reopen/@id = Controller\Issues->reopen
 GET /issues/copy/@id = Controller\Issues->copy
 
+; Tags
+GET /tag = Controller\Tag->index
+GET /tag/@tag = Controller\Tag->single
+
 ; User pages
 GET /user = Controller\User->account
 POST /user = Controller\User->save

+ 24 - 0
app/view/tag/index.html

@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+	<include href="blocks/head.html" />
+</head>
+<body>
+	<include href="blocks/navbar.html" />
+	<div class="container">
+		<h1>{{ @dict.issue_tags }}</h1>
+		<check if="{{ !@cloud }}">
+			<p>{{ @dict.no_tags_created }}</p>
+		</check>
+		<p>{{ @dict.tag_help_1 }}<br>
+			<small>{{ @dict.tag_help_2 }}</small>
+		</p>
+		<div class="tag-cloud">
+			<repeat group="{{ @cloud }}" value="{{ @item }}">
+				<a href="{{ @site.url }}tag/{{ @item.tag }}" style="font-size: {{ 14 + (@item.freq * 2) }}px;">{{ @item.tag }}</a>&ensp;
+			</repeat>
+		</div>
+		<include href="blocks/footer.html" />
+	</div>
+</body>
+</html>

+ 17 - 0
app/view/tag/single.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+	<include href="blocks/head.html" />
+</head>
+<body>
+	<include href="blocks/navbar.html" />
+	<div class="container">
+		<h1>#{{ @tag.tag }}</h1>
+		<p>{{ @dict.tag_help_1 }}<br>
+			<small>{{ @dict.tag_help_2 }}</small>
+		</p>
+		<include href="blocks/issue-list.html" />
+		<include href="blocks/footer.html" />
+	</div>
+</body>
+</html>

+ 11 - 0
db/15.01.31.sql

@@ -0,0 +1,11 @@
+DROP TABLE IF EXISTS `issue_tag`;
+CREATE TABLE `issue_tag`(
+	`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
+	`tag` VARCHAR(60) NOT NULL,
+	`issue_id` INT UNSIGNED NOT NULL,
+	PRIMARY KEY (`id`),
+	INDEX `issue_tag_tag` (`tag`, `issue_id`),
+	CONSTRAINT `issue_tag_issue` FOREIGN KEY (`issue_id`) REFERENCES `issue`(`id`) ON UPDATE CASCADE ON DELETE CASCADE
+) ENGINE=INNODB CHARSET=utf8;
+
+UPDATE `config` SET `value` = '15.01.31' WHERE `attribute` = 'version';

+ 11 - 2
db/database.sql

@@ -177,6 +177,16 @@ CREATE TABLE `issue_watcher` (
 	UNIQUE KEY `unique_watch` (`issue_id`,`user_id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
+DROP TABLE IF EXISTS `issue_tag`;
+CREATE TABLE `issue_tag`(
+	`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
+	`tag` VARCHAR(60) NOT NULL,
+	`issue_id` INT UNSIGNED NOT NULL,
+	PRIMARY KEY (`id`),
+	INDEX `issue_tag_tag` (`tag`, `issue_id`),
+	CONSTRAINT `issue_tag_issue` FOREIGN KEY (`issue_id`) REFERENCES `issue`(`id`) ON UPDATE CASCADE ON DELETE CASCADE
+) ENGINE=INNODB CHARSET=utf8;
+
 DROP TABLE IF EXISTS `sprint`;
 CREATE TABLE `sprint` (
 	`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
@@ -256,7 +266,6 @@ CREATE TABLE `session` (
 	PRIMARY KEY(`session_id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
-
 DROP TABLE IF EXISTS `config`;
 CREATE TABLE `config` (
 	`id` int(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
@@ -265,4 +274,4 @@ CREATE TABLE `config` (
 	UNIQUE KEY `attribute` (`attribute`)
 ) ;
 
-INSERT INTO `config` (`attribute`, `value`) VALUES ('version', '14.12.30');
+INSERT INTO `config` (`attribute`, `value`) VALUES ('version', '15.01.31');