From 9df5344050ec0a2b8bec03c7a89fff9d7d41ce2f Mon Sep 17 00:00:00 2001
From: Malf Furious <m@lfurio.us>
Date: Sat, 20 Oct 2018 21:24:01 -0400
Subject: issue:  Add author and authored fields

---
 app/class/issue.class.php | 29 +++++++++++++++++++++++++++++
 srvs/mysql.sql            |  2 ++
 2 files changed, 31 insertions(+)

diff --git a/app/class/issue.class.php b/app/class/issue.class.php
index 1c77894..651096e 100644
--- a/app/class/issue.class.php
+++ b/app/class/issue.class.php
@@ -32,8 +32,10 @@ class issue extends obj
             "guid",
             "numb",
             "assignee",
+            "author",
             "seen",
             "description",
+            "authored",
             "due",
             "tags",
         );
@@ -58,6 +60,9 @@ class issue extends obj
         $issue->name = $name;
         $issue->objtype = "issue";
         $issue->numb = $numb;
+        $issue->setAuthor($owner);
+        $issue->saveObj(); // get timestamp
+        $issue->authored = $issue->created;
         $issue->saveObj();
         return $issue;
     }
@@ -83,6 +88,30 @@ class issue extends obj
         $this->saveObj();
     }
 
+    /*
+     * Get the author of this issue.  This is usually the user
+     * that opened the issue, but may differ if this issue was
+     * elevated from a previous discussion thread.
+     */
+    public function getAuthor() : user
+    {
+        if (!isset($this->author) || $this->author == "")
+            return NULL;
+
+        return new user($this->author);
+    }
+
+    /*
+     * Set the author of this issue.  This should usually only
+     * be done while constructing a new message or to clear out
+     * references to a user that got removed.
+     */
+    public function setAuthor(user $author) : void
+    {
+        $this->author = $author->guid;
+        $this->saveObj();
+    }
+
     /*
      * Get the pad this issue exists under
      */
diff --git a/srvs/mysql.sql b/srvs/mysql.sql
index 13db8c7..178fb57 100644
--- a/srvs/mysql.sql
+++ b/srvs/mysql.sql
@@ -180,8 +180,10 @@ CREATE TABLE issues (
     guid        varchar(8)          NOT NULL,
     numb        int(32)             NOT NULL    DEFAULT 0,
     assignee    varchar(8)          NOT NULL    DEFAULT '',
+    author      varchar(8)          NOT NULL    DEFAULT '',
     seen        int(1)              NOT NULL    DEFAULT 0,      /* has the assignee seen this yet? */
     description text                NOT NULL,
+    authored    varchar(64)         NOT NULL    DEFAULT '',
     due         varchar(64)         NOT NULL    DEFAULT '',
     tags        text                NOT NULL,
 
-- 
cgit v1.2.3


From b093b3affe3ac6878e2242bff310dc466687a825 Mon Sep 17 00:00:00 2001
From: Malf Furious <m@lfurio.us>
Date: Sat, 20 Oct 2018 21:43:46 -0400
Subject: issue:  Add open/close data

---
 app/class/issue.class.php | 31 ++++++++++++++++++++++++++++++-
 srvs/mysql.sql            |  3 +++
 2 files changed, 33 insertions(+), 1 deletion(-)

diff --git a/app/class/issue.class.php b/app/class/issue.class.php
index 651096e..5d1daec 100644
--- a/app/class/issue.class.php
+++ b/app/class/issue.class.php
@@ -33,9 +33,12 @@ class issue extends obj
             "numb",
             "assignee",
             "author",
+            "closer",
             "seen",
             "description",
+            "opened",
             "authored",
+            "closed",
             "due",
             "tags",
         );
@@ -62,6 +65,7 @@ class issue extends obj
         $issue->numb = $numb;
         $issue->setAuthor($owner);
         $issue->saveObj(); // get timestamp
+        $issue->opened = $issue->created;
         $issue->authored = $issue->created;
         $issue->saveObj();
         return $issue;
@@ -112,6 +116,27 @@ class issue extends obj
         $this->saveObj();
     }
 
+    /*
+     * Get the user that closed this issue.  If the issue is still
+     * open, NULL is returned.
+     */
+    public function getCloser() : ?user
+    {
+        if (!isset($this->closer) || $this->closer == "")
+            return NULL;
+
+        return new user($this->closer);
+    }
+
+    /*
+     * Mark the user that closed this issue.
+     */
+    public function setCloser(user $closer) : void
+    {
+        $this->closer = $closer->guid;
+        $this->saveObj();
+    }
+
     /*
      * Get the pad this issue exists under
      */
@@ -145,12 +170,16 @@ class issue extends obj
     /*
      * Mark this issue as completed and closed.
      */
-    public function close() : void
+    public function close(user $closer) : void
     {
         $pad = $this->getParent()->getParent();
 
         if ($pad)
+        {
+            $this->closed = self::getCurrentTimestamp();
+            $this->setCloser($closer);
             $this->setParent($pad);
+        }
     }
 }
 
diff --git a/srvs/mysql.sql b/srvs/mysql.sql
index 178fb57..9f3193b 100644
--- a/srvs/mysql.sql
+++ b/srvs/mysql.sql
@@ -181,9 +181,12 @@ CREATE TABLE issues (
     numb        int(32)             NOT NULL    DEFAULT 0,
     assignee    varchar(8)          NOT NULL    DEFAULT '',
     author      varchar(8)          NOT NULL    DEFAULT '',
+    closer      varchar(8)          NOT NULL    DEFAULT '',
     seen        int(1)              NOT NULL    DEFAULT 0,      /* has the assignee seen this yet? */
     description text                NOT NULL,
+    opened      varchar(64)         NOT NULL    DEFAULT '',
     authored    varchar(64)         NOT NULL    DEFAULT '',
+    closed      varchar(64)         NOT NULL    DEFAULT '',
     due         varchar(64)         NOT NULL    DEFAULT '',
     tags        text                NOT NULL,
 
-- 
cgit v1.2.3


From b690505b0e1e255e5081adcf49c724186bb831c2 Mon Sep 17 00:00:00 2001
From: Malf Furious <m@lfurio.us>
Date: Sat, 20 Oct 2018 21:49:32 -0400
Subject: issue:  Add assigned timestamp

---
 app/class/issue.class.php | 2 ++
 srvs/mysql.sql            | 1 +
 2 files changed, 3 insertions(+)

diff --git a/app/class/issue.class.php b/app/class/issue.class.php
index 5d1daec..57fc588 100644
--- a/app/class/issue.class.php
+++ b/app/class/issue.class.php
@@ -37,6 +37,7 @@ class issue extends obj
             "seen",
             "description",
             "opened",
+            "assigned",
             "authored",
             "closed",
             "due",
@@ -89,6 +90,7 @@ class issue extends obj
     {
         $this->seen = 0;
         $this->assignee = $assignee->guid;
+        $this->assigned = self::getCurrentTimestamp();
         $this->saveObj();
     }
 
diff --git a/srvs/mysql.sql b/srvs/mysql.sql
index 9f3193b..9bad437 100644
--- a/srvs/mysql.sql
+++ b/srvs/mysql.sql
@@ -185,6 +185,7 @@ CREATE TABLE issues (
     seen        int(1)              NOT NULL    DEFAULT 0,      /* has the assignee seen this yet? */
     description text                NOT NULL,
     opened      varchar(64)         NOT NULL    DEFAULT '',
+    assigned    varchar(64)         NOT NULL    DEFAULT '',
     authored    varchar(64)         NOT NULL    DEFAULT '',
     closed      varchar(64)         NOT NULL    DEFAULT '',
     due         varchar(64)         NOT NULL    DEFAULT '',
-- 
cgit v1.2.3


From 792e741899cc895651e7b12a38b688c1da6406db Mon Sep 17 00:00:00 2001
From: Malf Furious <m@lfurio.us>
Date: Sat, 20 Oct 2018 21:55:53 -0400
Subject: issue:  Add function isOpen()

---
 app/class/issue.class.php | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/app/class/issue.class.php b/app/class/issue.class.php
index 57fc588..0fc12e4 100644
--- a/app/class/issue.class.php
+++ b/app/class/issue.class.php
@@ -183,6 +183,14 @@ class issue extends obj
             $this->setParent($pad);
         }
     }
+
+    /*
+     * Check whether issue is currently open or closed.
+     */
+    public function isOpen() : bool
+    {
+        return self::typeOf($this->parent) != "pad";
+    }
 }
 
 ?>
-- 
cgit v1.2.3


From 62872702dc413b7abab94d8a5a7bd21770b5d241 Mon Sep 17 00:00:00 2001
From: Malf Furious <m@lfurio.us>
Date: Sat, 20 Oct 2018 22:14:44 -0400
Subject: mesg:  Update function makeIssue()

This function is patched to co-operate with structural changes to the
issue class.
---
 app/class/mesg.class.php | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/app/class/mesg.class.php b/app/class/mesg.class.php
index 2e5c9d6..d40028c 100644
--- a/app/class/mesg.class.php
+++ b/app/class/mesg.class.php
@@ -266,13 +266,14 @@ class mesg extends obj
      * object is created and this message object will be destroyed.  If
      * this is not an eligible message for promotion, NULL is returned.
      */
-    public function makeIssue(stage $parent) : ?issue
+    public function makeIssue(user $owner, stage $parent) : ?issue
     {
         if ($this->getParent()->objtype != "pad")
             return NULL;
 
-        $issue = issue::initNew($this->name, $this->getOwner(), $parent);
-        $issue->created = $this->created;
+        $issue = issue::initNew($this->name, $owner, $parent);
+        $issue->author = $this->author;
+        $issue->authored = $this->created;
         $issue->description = $this->mesg;
         $issue->saveObj();
 
-- 
cgit v1.2.3


From 479fa31398d18f105616de83b5b5108278b75c59 Mon Sep 17 00:00:00 2001
From: Malf Furious <m@lfurio.us>
Date: Sun, 21 Oct 2018 21:18:21 -0400
Subject: issue:  Redesign schema

I found myself complicating the data model of this class of objects and
wanted to take a clean approach to its design.  The key differences are
as follows:

        * We now reference a message object for the issue's OP, as
        opposed to directly containing the message data

        This affords the OP _all_ of the standard features of a Scrott
        message, including separately tracked authorship data, file
        attachments.

        * Multiple assignees is implemented in the design

        Finally.

        * Seen flag is removed

        This can be implicitly tracked via all sub-object messages and
        the views meta-table.
---
 srvs/mysql.sql | 31 +++++++++++++++++++++----------
 1 file changed, 21 insertions(+), 10 deletions(-)

diff --git a/srvs/mysql.sql b/srvs/mysql.sql
index 9bad437..72167d4 100644
--- a/srvs/mysql.sql
+++ b/srvs/mysql.sql
@@ -69,6 +69,23 @@ CREATE TABLE views (
     PRIMARY KEY (guid, viewer)
 );
 
+/*
+ * Scrott issues may have multiple assignees.  This table is used to
+ * co-relate assignees and issues along with additional meta-data.
+ */
+DROP TABLE IF EXISTS assignees;
+CREATE TABLE assignees (
+    guid        varchar(8)          NOT NULL,                   /* guid of issue */
+    assignee    varchar(8)          NOT NULL,                   /* user */
+    assigner    varchar(8)          NOT NULL,                   /* user */
+    assigned    datetime            NOT NULL,                   /* timestamp */
+    dismisser   varchar(8)          NOT NULL    DEFAULT '',     /* user */
+    dismissed   varchar(64)         NOT NULL    DEFAULT '',     /* timestamp */
+    signedoff   varchar(64)         NOT NULL    DEFAULT '',     /* timestamp */
+
+    PRIMARY KEY (guid, assignee)
+);
+
 /*
  * Base table for Scrott objects
  *
@@ -179,16 +196,10 @@ DROP TABLE IF EXISTS issues;
 CREATE TABLE issues (
     guid        varchar(8)          NOT NULL,
     numb        int(32)             NOT NULL    DEFAULT 0,
-    assignee    varchar(8)          NOT NULL    DEFAULT '',
-    author      varchar(8)          NOT NULL    DEFAULT '',
-    closer      varchar(8)          NOT NULL    DEFAULT '',
-    seen        int(1)              NOT NULL    DEFAULT 0,      /* has the assignee seen this yet? */
-    description text                NOT NULL,
-    opened      varchar(64)         NOT NULL    DEFAULT '',
-    assigned    varchar(64)         NOT NULL    DEFAULT '',
-    authored    varchar(64)         NOT NULL    DEFAULT '',
-    closed      varchar(64)         NOT NULL    DEFAULT '',
-    due         varchar(64)         NOT NULL    DEFAULT '',
+    mesg        varchar(8)          NOT NULL    DEFAULT '',
+    closer      varchar(8)          NOT NULL    DEFAULT '',     /* user */
+    closed      varchar(64)         NOT NULL    DEFAULT '',     /* timestamp */
+    due         varchar(64)         NOT NULL    DEFAULT '',     /* timestamp */
     tags        text                NOT NULL,
 
     PRIMARY KEY (guid)
-- 
cgit v1.2.3


From a18205b9991f9e5d6ac0a86aa37f049bf53980f6 Mon Sep 17 00:00:00 2001
From: Malf Furious <m@lfurio.us>
Date: Sun, 21 Oct 2018 21:59:55 -0400
Subject: issue:  Rewrite issue class

Revised implementation of redesigned data model.
---
 app/class/issue.class.php | 179 ++++++++++++++++++++++++++++++++--------------
 1 file changed, 125 insertions(+), 54 deletions(-)

diff --git a/app/class/issue.class.php b/app/class/issue.class.php
index 0fc12e4..47adfa1 100644
--- a/app/class/issue.class.php
+++ b/app/class/issue.class.php
@@ -18,8 +18,8 @@ require_once "class/user.class.php";
 require_once "class/mesg.class.php";
 
 /*
- * This class models Scrott issues.  Issues represent units of work, can
- * be assigned to users, and advance through a pipeline.
+ * This class models Scrott issues.  Issues represent units of work, track
+ * messages, can be assigned to users, and advance through a pipeline.
  */
 class issue extends obj
 {
@@ -31,14 +31,8 @@ class issue extends obj
         $this->fields['issues'] = array(
             "guid",
             "numb",
-            "assignee",
-            "author",
+            "mesg",
             "closer",
-            "seen",
-            "description",
-            "opened",
-            "assigned",
-            "authored",
             "closed",
             "due",
             "tags",
@@ -49,10 +43,11 @@ class issue extends obj
     }
 
     /*
-     * Initialize a new issue object with the given name, parent, and
-     * owner.
+     * Initialize a new issue object with the given message, parent,
+     * and owner.  The issue's name is taken from the message's name.
+     * The given message object is updated to parent the new issue.
      */
-    public static function initNew(string $name, user $owner, stage $parent) : issue
+    public static function initNew(mesg $mesg, user $owner, stage $parent) : issue
     {
         $pad = $parent->getParent();
         $numb = $pad->issueNumb++;
@@ -61,61 +56,146 @@ class issue extends obj
         $issue = new issue();
         $issue->setOwner($owner);
         $issue->setParent($parent);
-        $issue->name = $name;
+        $issue->name = $mesg->name;
         $issue->objtype = "issue";
         $issue->numb = $numb;
-        $issue->setAuthor($owner);
-        $issue->saveObj(); // get timestamp
-        $issue->opened = $issue->created;
-        $issue->authored = $issue->created;
+        $issue->mesg = $mesg->guid;
         $issue->saveObj();
+
+        $mesg->setParent($issue);
+
         return $issue;
     }
 
     /*
-     * Get the assignee for this issue
+     * Get all assignees.  Assignees are returned as an array of
+     * stdClass instances.  Objects contain data from the 'assignees'
+     * database table with no defined operations.  Pointers to users
+     * will be followed and an instanciated user object will be
+     * in their place.  Limit of zero returns all assignees.
      */
-    public function getAssignee() : ?user
+    public function getAssignees(int $limit = 0) : array
     {
-        if (!isset($this->assignee) || $this->assignee == "")
-            return NULL;
+        $assign = array();
+        $query = "SELECT * FROM assignees WHERE guid = '" .
+            database::esc($this->guid) . "'";
+
+        if ($limit != 0)
+            $query .= " LIMIT " . database::esc($limit);
+
+        $res = database::query($query);
+
+        foreach ($res as $a)
+        {
+            $obj = new stdClass();
+
+            foreach ($a as $k => $v)
+                $obj->{$k} = $v;
+
+            $obj->assignee = new user($obj->assignee);
+            $obj->assigner = new user($obj->assigner);
+
+            if (isset($obj->dismisser) && $obj->dismisser != "")
+                $obj->dismisser = new user($obj->dismisser);
 
-        return new user($this->assignee);
+            $assign[] = $obj;
+        }
+
+        return $assign;
     }
 
     /*
-     * Reset the seen flag and reassign this issue.
+     * Add an assignee to this issue.  Returns false if user is
+     * already assigned, or if another error occurs; true
+     * otherwise.
      */
-    public function assignTo(user $assignee) : void
+    public function addAssignee(user $assignee, user $assigner) : bool
     {
-        $this->seen = 0;
-        $this->assignee = $assignee->guid;
-        $this->assigned = self::getCurrentTimestamp();
-        $this->saveObj();
+        if ($assignee->isAssignedTo($this) || !isset($assignee->guid)
+            || !isset($assigner->guid))
+            return false;
+
+        $query = "INSERT INTO assignees (guid, assignee, assigner, assigned)" .
+            " VALUES ('" . database::esc($this->guid) . "', '" .
+            database::esc($assignee->guid) . "', '" . database::esc($assigner->guid) .
+            "', '" . database::esc(self::getCurrentTimestamp()) . "')";
+
+        database::query($query);
+        return true;
     }
 
     /*
-     * Get the author of this issue.  This is usually the user
-     * that opened the issue, but may differ if this issue was
-     * elevated from a previous discussion thread.
+     * Dismiss an assignee, effectively unassigning them.  Returns
+     * false if user is not already an active assignee, or if another
+     * error occurs; true otherwise.
      */
-    public function getAuthor() : user
+    public function dismissAssignee(user $assignee, user $dismisser) : bool
     {
-        if (!isset($this->author) || $this->author == "")
-            return NULL;
+        if (!$assignee->isAssignedTo($this) || !isset($assignee->guid)
+            || !isset($dismisser->guid))
+            return false;
+
+        $query = "UPDATE assignees SET dismisser = '" . database::esc($dismisser->guid) .
+            "', dismissed = '" . database::esc(self::getCurrentTimestamp()) .
+            "' WHERE guid = '" . database::esc($this->guid) . "' AND assignee = '" .
+            database::esc($assignee->guid) . "'";
+
+        database::query($query);
+        return true;
+    }
+
+    /*
+     * Signoff an assignee's portion of this issue.  Returns false
+     * if user is not already an active assignee, or if another
+     * error occurs; true otherwise.
+     */
+    public function signoffAssignee(user $assignee) : bool
+    {
+        if (!$assignee->isAssignedTo($this) || !isset($assignee->guid))
+            return false;
+
+        $query = "UPDATE assignees SET signedoff = '" .
+            database::esc(self::getCurrentTimestamp()) . "' WHERE guid = '" .
+            database::esc($this->guid) . "' AND assignee = '" .
+            database::esc($assignee->guid) . "'";
+
+        database::query($query);
+        return true;
+    }
 
-        return new user($this->author);
+    /*
+     * Get the OP message for this issue.
+     */
+    public function getOPMesg() : mesg
+    {
+        return new mesg($this->mesg);
     }
 
     /*
-     * Set the author of this issue.  This should usually only
-     * be done while constructing a new message or to clear out
-     * references to a user that got removed.
+     * Get all messages on this issue.  The OP message is filtered
+     * from results.  Messages are sorted by date created.
      */
-    public function setAuthor(user $author) : void
+    public function getMesgs_ordByDatetime() : array
     {
-        $this->author = $author->guid;
-        $this->saveObj();
+        $mesgs = parent::getMesgs_ordByDatetime();
+        $i = -1;
+
+        foreach ($mesgs as $k => $m)
+        {
+            if ($m->guid == $this->mesg)
+            {
+                $i = $k;
+                break;
+            }
+        }
+
+        if ($i != -1)
+        {
+            unset($mesgs[$i]);
+            $mesgs = array_values($mesgs);
+        }
+
+        return $mesgs;
     }
 
     /*
@@ -130,15 +210,6 @@ class issue extends obj
         return new user($this->closer);
     }
 
-    /*
-     * Mark the user that closed this issue.
-     */
-    public function setCloser(user $closer) : void
-    {
-        $this->closer = $closer->guid;
-        $this->saveObj();
-    }
-
     /*
      * Get the pad this issue exists under
      */
@@ -154,9 +225,9 @@ class issue extends obj
 
     /*
      * Advance this issue in the pipeline, closing it if already in the
-     * last stage.
+     * last stage.  A closer is needed incase a close takes place.
      */
-    public function advance() : void
+    public function advance(user $closer) : void
     {
         $stage = $this->getParent();
 
@@ -164,7 +235,7 @@ class issue extends obj
             return;
 
         if (!($next = $stage->getNext()))
-            $this->close();
+            $this->close($closer);
         else
             $this->setParent($next);
     }
@@ -178,8 +249,8 @@ class issue extends obj
 
         if ($pad)
         {
+            $this->closer = $closer->guid;
             $this->closed = self::getCurrentTimestamp();
-            $this->setCloser($closer);
             $this->setParent($pad);
         }
     }
-- 
cgit v1.2.3


From c2d42ce0239c8da0cb9acea922f6dea183196225 Mon Sep 17 00:00:00 2001
From: Malf Furious <m@lfurio.us>
Date: Sun, 21 Oct 2018 23:07:10 -0400
Subject: agent:  Add function isAssignedTo()

---
 app/class/agent.class.php | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/app/class/agent.class.php b/app/class/agent.class.php
index c8e6436..4c75f0b 100644
--- a/app/class/agent.class.php
+++ b/app/class/agent.class.php
@@ -74,6 +74,21 @@ abstract class agent extends obj
         return false;
     }
 
+    /*
+     * Check whether this agent is assigned to the given issue
+     */
+    public function isAssignedTo(issue $issue) : bool
+    {
+        foreach ($issue->getAssignees() as $assign)
+        {
+            if ($assign->assignee->guid == $this->guid
+                && $assign->dismissed == "")
+                return true;
+        }
+
+        return false;
+    }
+
     /*
      * Send an email message to this agent using stored configuration
      * parameters.  If config is not established, delivery is not
-- 
cgit v1.2.3


From 384f2649b714d310b385a59cc34d11fff1d85ef2 Mon Sep 17 00:00:00 2001
From: Malf Furious <m@lfurio.us>
Date: Sun, 21 Oct 2018 23:28:08 -0400
Subject: Revert "mesg:  Update function makeIssue()"

This reverts commit 62872702dc413b7abab94d8a5a7bd21770b5d241.
---
 app/class/mesg.class.php | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/app/class/mesg.class.php b/app/class/mesg.class.php
index d40028c..2e5c9d6 100644
--- a/app/class/mesg.class.php
+++ b/app/class/mesg.class.php
@@ -266,14 +266,13 @@ class mesg extends obj
      * object is created and this message object will be destroyed.  If
      * this is not an eligible message for promotion, NULL is returned.
      */
-    public function makeIssue(user $owner, stage $parent) : ?issue
+    public function makeIssue(stage $parent) : ?issue
     {
         if ($this->getParent()->objtype != "pad")
             return NULL;
 
-        $issue = issue::initNew($this->name, $owner, $parent);
-        $issue->author = $this->author;
-        $issue->authored = $this->created;
+        $issue = issue::initNew($this->name, $this->getOwner(), $parent);
+        $issue->created = $this->created;
         $issue->description = $this->mesg;
         $issue->saveObj();
 
-- 
cgit v1.2.3