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> <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> <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