From aba37fceb3c993b273d2ba8c4e240003da23a55f Mon Sep 17 00:00:00 2001
From: Peter Rotich <peter@osticket.com>
Date: Fri, 31 Jul 2015 18:08:08 +0000
Subject: [PATCH] Add restrictions to ticket/task closure

- Tickets cannot be closed when an open ticket exists
- Tickets/Tasks cannot be closed when required for close fields are missing
data
- Add isCloseable routine to ticket/task classes
---
 include/ajax.tickets.php                      |  5 ++
 include/class.task.php                        | 46 +++++++++++++++++++
 include/class.ticket.php                      | 37 +++++++++++++--
 .../templates/dynamic-field-config.tmpl.php   |  8 ++--
 include/staff/templates/task-view.tmpl.php    | 13 +++++-
 include/staff/ticket-view.inc.php             | 17 +++----
 6 files changed, 108 insertions(+), 18 deletions(-)

diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php
index c458db823..6cdf27629 100644
--- a/include/ajax.tickets.php
+++ b/include/ajax.tickets.php
@@ -394,6 +394,11 @@ class TicketsAjaxAPI extends AjaxController {
                 if (!$role->hasPerm(TicketModel::PERM_CLOSE))
                     Http::response(403, 'Access denied');
                 $state = 'closed';
+
+                // Check if ticket is closeable
+                if (is_string($closeable=$ticket->isCloseable()))
+                    $info['warn'] =  $closeable;
+
                 break;
             case 'delete':
                 if (!$role->hasPerm(TicketModel::PERM_DELETE))
diff --git a/include/class.task.php b/include/class.task.php
index 348e69393..00c52c402 100644
--- a/include/class.task.php
+++ b/include/class.task.php
@@ -49,6 +49,14 @@ class TaskModel extends VerySimpleModel {
                 'constraint' => array('id' => 'TaskCData.task_id'),
                 'list' => false,
             ),
+            'entries' => array(
+                'constraint' => array(
+                    "'A'" => 'DynamicFormEntry.object_type',
+                    'id' => 'DynamicFormEntry.object_id',
+                ),
+                'list' => true,
+            ),
+
             'ticket' => array(
                 'constraint' => array(
                     'object_type' => "'T'",
@@ -174,6 +182,22 @@ class TaskModel extends VerySimpleModel {
         return !$this->isOpen();
     }
 
+    function isCloseable() {
+
+        if ($this->isClosed())
+            return true;
+
+        $warning = null;
+        if ($this->getMissingRequiredFields()) {
+            $warning = sprintf(
+                    __( '%1$s is missing data on %2$s one or more required fields %3$s and cannot be closed'),
+                    __('This task'),
+                    '', '');
+        }
+
+        return $warning ?: true;
+    }
+
     protected function close() {
         return $this->clearFlag(self::ISOPEN);
     }
@@ -332,6 +356,20 @@ class Task extends TaskModel implements RestrictedAccess, Threadable {
         return $this->lastrespondent;
     }
 
+    function getMissingRequiredFields() {
+        $fields = DynamicFormField::objects()->filter(array(
+                'id__in' => $this->entries
+                    ->filter(array(
+                        'answers__field__flags__hasbit' => DynamicFormField::FLAG_CLOSE_REQUIRED,
+                        'answers__value__isnull' => true,
+                    ))
+                    ->values_flat('answers__field_id')
+                ));
+
+        return ($fields && count($fields)) ? $fields : array();
+
+    }
+
     function getParticipants() {
         $participants = array();
         foreach ($this->getThread()->collaborators as $c)
@@ -454,6 +492,14 @@ class Task extends TaskModel implements RestrictedAccess, Threadable {
             if ($this->isClosed())
                 return false;
 
+            // Check if task is closeable
+            $closeable = $this->isCloseable();
+            if ($closeable !== true)
+                $errors['err'] = $closeable ?: sprintf(__('%s cannot be closed'), __('This task'));
+
+            if ($errors)
+                return false;
+
             $this->close();
             $this->closed = SqlFunction::NOW();
             $ecb = function($t) {
diff --git a/include/class.ticket.php b/include/class.ticket.php
index 6a6af2ce1..e38786a2d 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -277,6 +277,25 @@ implements RestrictedAccess, Threadable {
          return $this->hasState('closed');
     }
 
+    function isCloseable() {
+
+        if ($this->isClosed())
+            return true;
+
+        $warning = null;
+        if ($this->getMissingRequiredFields()) {
+            $warning = sprintf(
+                    __( '%1$s is missing data on %2$s one or more required fields %3$s and cannot be closed'),
+                    __('This ticket'),
+                    '', '');
+        } elseif (($num=$this->getNumOpenTasks())) {
+            $warning = sprintf(__('%1$s has %2$d open tasks and cannot be closed'),
+                    __('This ticket'), $num);
+        }
+
+        return $warning ?: true;
+    }
+
     function isArchived() {
          return $this->hasState('archived');
     }
@@ -725,6 +744,12 @@ implements RestrictedAccess, Threadable {
         return count($this->tasks);
     }
 
+    function getNumOpenTasks() {
+        return count($this->tasks->filter(array(
+                        'flags__hasbit' => TaskModel::ISOPEN)));
+    }
+
+
     function getThreadId() {
         if ($this->thread)
             return $this->thread->id;
@@ -1041,12 +1066,14 @@ implements RestrictedAccess, Threadable {
         $ecb = null;
         switch ($status->getState()) {
             case 'closed':
-                if ($this->getMissingRequiredFields()) {
-                    $errors['err'] = sprintf(__(
-                        'This ticket is missing data on %s one or more required fields %s and cannot be closed'),
-                    '', '');
+                // Check if ticket is closeable
+                $closeable = $this->isCloseable();
+                if ($closeable !== true)
+                    $errors['err'] = $closeable ?: sprintf(__('%s cannot be closed'), __('This ticket'));
+
+                if ($errors)
                     return false;
-                }
+
                 $this->closed = $this->lastupdate = SqlFunction::NOW();
                 $this->duedate = null;
                 if ($thisstaff && $set_closing_agent)
diff --git a/include/staff/templates/dynamic-field-config.tmpl.php b/include/staff/templates/dynamic-field-config.tmpl.php
index 732ec709b..daf8cd0f3 100644
--- a/include/staff/templates/dynamic-field-config.tmpl.php
+++ b/include/staff/templates/dynamic-field-config.tmpl.php
@@ -95,21 +95,21 @@
         ?>> <?php echo __('For Agents'); ?><br/>
     </div>
 
-<?php if (in_array($field->get('form')->get('type'), array('G', 'T'))) { ?>
+<?php if (in_array($field->get('form')->get('type'), array('G', 'T', 'A'))) { ?>
     <hr class="faded"/>
 
     <div class="span4">
         <div style="margin-bottom:5px"><strong>Data Integrity</strong>
         <i class="help-tip icon-question-sign"
-            data-title="<?php echo __('Required to close a ticket'); ?>"
-            data-content="<?php echo __('Optionally, this field can prevent closing a ticket until it has valid data.'); ?>"></i>
+            data-title="<?php echo __('Required to close a case'); ?>"
+            data-content="<?php echo __('Optionally, this field can prevent closing a case until it has valid data.'); ?>"></i>
         </div>
     </div>
     <div class="span6">
         <input type="checkbox" name="flags[]" value="<?php
             echo DynamicFormField::FLAG_CLOSE_REQUIRED; ?>" <?php
             if ($field->hasFlag(DynamicFormField::FLAG_CLOSE_REQUIRED)) echo 'checked="checked"';
-        ?>> <?php echo __('Required to close a ticket'); ?><br/>
+        ?>> <?php echo __('Required data to close'); ?><br/>
     </div>
 <?php } ?>
 <?php } ?>
diff --git a/include/staff/templates/task-view.tmpl.php b/include/staff/templates/task-view.tmpl.php
index 69eb3095c..34c41599f 100644
--- a/include/staff/templates/task-view.tmpl.php
+++ b/include/staff/templates/task-view.tmpl.php
@@ -6,8 +6,9 @@ if (!defined('OSTSCPINC')
 
 global $cfg;
 
+$iscloseable = $task->isCloseable();
+$canClose = ($role->hasPerm(TaskModel::PERM_CLOSE) && $iscloseable === true);
 $actions = array();
-
 $actions += array(
         'print' => array(
             'href' => sprintf('tasks.php?id=%d&a=print', $task->getId()),
@@ -398,10 +399,15 @@ else
                                 echo $task->isOpen() ?
                                 'selected="selected"': ''; ?>> <?php
                                 echo _('Open'); ?></option>
+                            <?php
+                            if ($task->isClosed() || $canClose) {
+                                ?>
                             <option value="closed" <?php
                                 echo $task->isClosed() ?
                                 'selected="selected"': ''; ?>> <?php
                                 echo _('Closed'); ?></option>
+                            <?php
+                            } ?>
                         </select>
                         &nbsp;<span class='error'><?php echo
                         $errors['task_status']; ?></span>
@@ -453,10 +459,15 @@ else
                                 echo $task->isOpen() ?
                                 'selected="selected"': ''; ?>> <?php
                                 echo _('Open'); ?></option>
+                            <?php
+                            if ($task->isClosed() || $canClose) {
+                                ?>
                             <option value="closed" <?php
                                 echo $task->isClosed() ?
                                 'selected="selected"': ''; ?>> <?php
                                 echo _('Closed'); ?></option>
+                            <?php
+                            } ?>
                         </select>
                         &nbsp;<span class='error'><?php echo
                         $errors['task_status']; ?></span>
diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php
index f1a4560b7..7b7cee624 100644
--- a/include/staff/ticket-view.inc.php
+++ b/include/staff/ticket-view.inc.php
@@ -620,13 +620,13 @@ $tcount = $ticket->getThreadEntries($types)->count();
                 </td>
                 <td>
                     <?php
-                    if ($outstanding = $ticket->getMissingRequiredFields()) { ?>
-                    <div class="warning-banner"><?php echo sprintf(__(
-                        'This ticket is missing data on %s one or more required fields %s and cannot be closed'),
-                        "<a href=\"tickets.php?id={$ticket->getId()}&a=edit\">",
-                        '</a>'
-                    ); ?></div>
-<?php               } ?>
+                    $outstanding = false;
+                    if ($role->hasPerm(TicketModel::PERM_CLOSE)
+                            && ($warning=$ticket->isCloseable())
+                            && $warning !==true) {
+                        $outstanding =  true;
+                        echo sprintf('<div class="warning-banner">%s</div>', $warning);
+                    } ?>
                     <select name="reply_status_id">
                     <?php
                     $statusId = $info['reply_status_id'] ?: $ticket->getStatusId();
@@ -716,7 +716,8 @@ $tcount = $ticket->getThreadEntries($types)->count();
                         <?php
                         $statusId = $info['note_status_id'] ?: $ticket->getStatusId();
                         $states = array('open');
-                        if ($role->hasPerm(TicketModel::PERM_CLOSE))
+                        if ($ticket->isCloseable() === true
+                                && $role->hasPerm(TicketModel::PERM_CLOSE))
                             $states = array_merge($states, array('closed'));
                         foreach (TicketStatusList::getStatuses(
                                     array('states' => $states)) as $s) {
-- 
GitLab