diff --git a/bootstrap.php b/bootstrap.php index 03007ba9b7f9f9e6db4d97afecc6a13560188833..41cd7cf4f5d700772919a697182d4eb50f882f1f 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -179,6 +179,7 @@ class Bootstrap { function loadCode() { #include required files + require_once INCLUDE_DIR.'class.util.php'; require(INCLUDE_DIR.'class.signal.php'); require(INCLUDE_DIR.'class.user.php'); require(INCLUDE_DIR.'class.auth.php'); diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index 23c978a93e608833839f6a80fd0176a1d83124b8..708bb6dbdc88b3f6adadba7205cc60efb5da8b4c 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -176,15 +176,15 @@ class TicketsAjaxAPI extends AjaxController { if($req['staffId'] && !$req['status']) //Assigned TO + Closed By $where.= ' OR (ticket.staff_id='.db_input($req['staffId']). - ' AND status.state IN("resolved", "closed")) '; + ' AND status.state IN("closed")) '; elseif($req['staffId']) // closed by any - $where.= ' OR status.state IN("resolved", "closed") '; + $where.= ' OR status.state IN("closed") '; $where.= ' ) '; } elseif($req['staffId']) { # closed-by $where.=' AND (ticket.staff_id='.db_input($req['staffId']).' AND - status.state IN("resolved", "closed")) '; - $criteria['state__in'] = array('resolved','closed'); + status.state IN("closed")) '; + $criteria['state__in'] = array('closed'); $criteria['staff_id'] = $req['staffId']; } @@ -721,9 +721,6 @@ class TicketsAjaxAPI extends AjaxController { case 'reopen': $state = 'open'; break; - case 'resolve': - $state = 'resolved'; - break; case 'close': if (!$thisstaff->canCloseTickets()) Http::response(403, 'Access denied'); @@ -772,7 +769,6 @@ class TicketsAjaxAPI extends AjaxController { $errors['err'] = sprintf(__('You do not have permission %s.'), __('to reopen tickets')); break; - case 'resolved': case 'closed': if (!$thisstaff->canCloseTickets()) $errors['err'] = sprintf(__('You do not have permission %s.'), @@ -836,9 +832,6 @@ class TicketsAjaxAPI extends AjaxController { case 'reopen': $state = 'open'; break; - case 'resolve': - $state = 'resolved'; - break; case 'close': if (!$thisstaff->canCloseTickets()) Http::response(403, 'Access denied'); @@ -884,7 +877,6 @@ class TicketsAjaxAPI extends AjaxController { $errors['err'] = sprintf(__('You do not have permission %s.'), __('to reopen tickets')); break; - case 'resolved': case 'closed': if (!$thisstaff->canCloseTickets()) $errors['err'] = sprintf(__('You do not have permission %s.'), diff --git a/include/api.tickets.php b/include/api.tickets.php index 47d978a498aac405381d4f5659f2b8072d092da6..6371daa632ff5240b0bee90cb3bed69c0ac56384 100644 --- a/include/api.tickets.php +++ b/include/api.tickets.php @@ -149,6 +149,10 @@ class TicketApiController extends ApiController { $data = $this->getEmailRequest(); if (($thread = ThreadEntry::lookupByEmailHeaders($data)) + && ($t=$thread->getTicket()) + && ($data['staffId'] + || !$t->isClosed() + || $t->isReopenable()) && $thread->postEmail($data)) { return $thread->getTicket(); } diff --git a/include/class.client.php b/include/class.client.php index 7dc764d1c8cfe4b59e083abf270089d26ac781e4..bfcffeaca09f2a1251a0d3ae7fd8125cd5d08e1b 100644 --- a/include/class.client.php +++ b/include/class.client.php @@ -250,17 +250,13 @@ class EndUser extends AuthenticatedUser { if (!($stats=$this->getTicketStats())) return 0; - return $stats['open']+$stats['resolved']+ $stats['closed']; + return $stats['open']+$stats['closed']; } function getNumOpenTickets() { return ($stats=$this->getTicketStats())?$stats['open']:0; } - function getNumResolvedTickets() { - return ($stats=$this->getTicketStats())?$stats['resolved']:0; - } - function getNumClosedTickets() { return ($stats=$this->getTicketStats())?$stats['closed']:0; } @@ -303,14 +299,6 @@ class EndUser extends AuthenticatedUser { . $join . $where - .'UNION SELECT \'resolved\', count( ticket.ticket_id ) AS tickets ' - .'FROM ' . TICKET_TABLE . ' ticket ' - .'INNER JOIN '.TICKET_STATUS_TABLE. ' status - ON (ticket.status_id=status.id - AND status.state=\'resolved\') ' - . $join - . $where - .'UNION SELECT \'closed\', count( ticket.ticket_id ) AS tickets ' .'FROM ' . TICKET_TABLE . ' ticket ' .'INNER JOIN '.TICKET_STATUS_TABLE. ' status diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php index eaa20067974cf9afbce123e9296356b100cc2e42..8637598c23f3b05d3be1450a337a036ed9b1a772 100644 --- a/include/class.dynamic_forms.php +++ b/include/class.dynamic_forms.php @@ -53,11 +53,11 @@ class DynamicForm extends VerySimpleModel { $fields = &$this->_fields; if (!$fields) { - $fields = new ArrayObject(); + $fields = new ListObject(); foreach ($this->getDynamicFields() as $f) - // TODO: Index by field name or id - $fields[$f->get('id')] = $f->getImpl($f); + $fields->append($f->getImpl($f)); } + return $fields; } diff --git a/include/class.forms.php b/include/class.forms.php index 585772fdf9a914b937bcbc0501e582b58abf65ea..0ebf717d835c6f211a230fcc4530e2be5aeac6ba 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -236,7 +236,21 @@ class FormField { if (!isset($this->_clean)) { $this->_clean = (isset($this->value)) ? $this->value : $this->parse($this->getWidget()->value); - $this->validateEntry($this->_clean); + + if ($vs = $this->get('cleaners')) { + if (is_array($vs)) { + foreach ($vs as $cleaner) + if (is_callable($cleaner)) + $this->_clean = call_user_func_array( + $cleaner, array($this, $this->_clean)); + } + elseif (is_callable($vs)) + $this->_clean = call_user_func_array( + $vs, array($this, $this->_clean)); + } + + if ($this->isVisible()) + $this->validateEntry($this->_clean); } return $this->_clean; } @@ -291,6 +305,31 @@ class FormField { } } + /** + * isVisible + * + * If this field has visibility configuration, then it will parse the + * constraints with the visibility configuration to determine if the + * field is visible and should be considered for validation + */ + function isVisible() { + $config = $this->getConfiguration(); + if ($this->get('visibility') instanceof VisibilityConstraint) { + return $this->get('visibility')->isVisible($this); + } + return true; + } + + /** + * FIXME: Temp + * + */ + + function isEditable() { + return (($this->get('edit_mask') & 32) == 0); + } + + /** * parse * @@ -452,7 +491,11 @@ class FormField { } function render($mode=null) { - return $this->getWidget()->render($mode); + $rv = $this->getWidget()->render($mode); + if ($v = $this->get('visibility')) { + $v->emitJavascript($this); + } + return $rv; } function renderExtras($mode=null) { @@ -619,7 +662,27 @@ class TextboxField extends FormField { 'validator' => new ChoiceField(array( 'id'=>3, 'label'=>__('Validator'), 'required'=>false, 'default'=>'', 'choices' => array('phone'=>__('Phone Number'),'email'=>__('Email Address'), - 'ip'=>__('IP Address'), 'number'=>__('Number'), ''=>__('None')))), + 'ip'=>__('IP Address'), 'number'=>__('Number'), + 'regex'=>__('Custom (Regular Expression)'), ''=>__('None')))), + 'regex' => new TextboxField(array( + 'id'=>6, 'label'=>__('Regular Expression'), 'required'=>true, + 'configuration'=>array('size'=>40, 'length'=>100), + 'visibility' => new VisibilityConstraint( + new Q(array('validator__eq'=>'regex')), + VisibilityConstraint::HIDDEN + ), + 'cleaners' => function ($self, $value) { + $wrapped = "/".$value."/iu"; + if (false === @preg_match($value, ' ') + && false !== @preg_match($wrapped, ' ')) { + return $wrapped; + } + return $value; + }, + 'validators' => function($self, $v) { + if (false === @preg_match($v, ' ')) + $self->addError(__('Cannot compile this regular expression')); + })), 'validator-error' => new TextboxField(array( 'id'=>4, 'label'=>__('Validation Error'), 'default'=>'', 'configuration'=>array('size'=>40, 'length'=>60), @@ -647,7 +710,13 @@ class TextboxField extends FormField { __('Enter a valid phone number')), 'ip' => array(array('Validator', 'is_ip'), __('Enter a valid IP address')), - 'number' => array('is_numeric', __('Enter a number')) + 'number' => array('is_numeric', __('Enter a number')), + 'regex' => array( + function($v) use ($config) { + $regex = $config['regex']; + return @preg_match($regex, $v); + }, __('Value does not match required pattern') + ), ); // Support configuration forms, as well as GUI-based form fields $valid = $this->get('validator'); @@ -995,11 +1064,12 @@ class DatetimeField extends FormField { 'hint'=>__('Earliest date selectable'))), 'max' => new DatetimeField(array( 'id'=>4, 'label'=>__('Latest'), 'required'=>false, - 'default'=>null)), + 'default'=>null, 'hint'=>__('Latest date selectable'))), 'future' => new BooleanField(array( 'id'=>5, 'label'=>__('Allow Future Dates'), 'required'=>false, 'default'=>true, 'configuration'=>array( - 'desc'=>__('Allow entries into the future' /* Used in the date field */)))), + 'desc'=>__('Allow entries into the future' /* Used in the date field */)), + )), ); } @@ -1159,10 +1229,6 @@ class TicketStateField extends ChoiceField { 'name' => /* @trans, @context "ticket state name" */ 'Open', 'verb' => /* @trans, @context "ticket state action" */ 'Open' ), - 'resolved' => array( - 'name' => /* @trans, @context "ticket state name" */ 'Resolved', - 'verb' => /* @trans, @context "ticket state action" */ 'Resolve' - ), 'closed' => array( 'name' => /* @trans, @context "ticket state name" */ 'Closed', 'verb' => /* @trans, @context "ticket state action" */ 'Close' @@ -1582,6 +1648,7 @@ class Widget { function __construct($field) { $this->field = $field; $this->name = $field->getFormName(); + $this->id = '_' . $this->name; } function parseValue() { @@ -1601,6 +1668,18 @@ class Widget { return $data[$this->field->get('name')]; return null; } + + /** + * getJsValueGetter + * + * Used with the dependent fields feature, this function should return a + * single javascript expression which can be used in a larger expression + * (<> == true, where <> is the result of this function). The %s token + * will be replaced with a jQuery variable representing this widget. + */ + function getJsValueGetter() { + return '%s.val()'; + } } class TextboxWidget extends Widget { @@ -1621,7 +1700,7 @@ class TextboxWidget extends Widget { ?> <span style="display:inline-block"> <input type="<?php echo static::$input_type; ?>" - id="<?php echo $this->name; ?>" + id="<?php echo $this->id; ?>" <?php echo implode(' ', array_filter(array( $size, $maxlength, $classes, $autocomplete, $disabled))) .' placeholder="'.$config['placeholder'].'"'; ?> @@ -1663,6 +1742,7 @@ class TextareaWidget extends Widget { <span style="display:inline-block;width:100%"> <textarea <?php echo $rows." ".$cols." ".$maxlength." ".$class .' placeholder="'.$config['placeholder'].'"'; ?> + id="<?php echo $this->id; ?>" name="<?php echo $this->name; ?>"><?php echo Format::htmlchars($this->value); ?></textarea> @@ -1676,7 +1756,7 @@ class PhoneNumberWidget extends Widget { $config = $this->field->getConfiguration(); list($phone, $ext) = explode("X", $this->value); ?> - <input type="text" name="<?php echo $this->name; ?>" value="<?php + <input id="<?php echo $this->id; ?>" type="text" name="<?php echo $this->name; ?>" value="<?php echo Format::htmlchars($phone); ?>"/><?php // Allow display of extension field even if disabled if the phone // number being edited has an extension @@ -1748,7 +1828,7 @@ class ChoicesWidget extends Widget { ?> <select name="<?php echo $this->name; ?>[]" - id="<?php echo $this->name; ?>" + id="<?php echo $this->id; ?>" data-prompt="<?php echo $prompt; ?>" <?php if ($config['multiselect']) echo ' multiple="multiple" class="multiselect"'; ?>> @@ -1793,6 +1873,10 @@ class ChoicesWidget extends Widget { } return $values; } + + function getJsValueGetter() { + return '%s.find(":selected").val()'; + } } class CheckboxWidget extends Widget { @@ -1806,7 +1890,8 @@ class CheckboxWidget extends Widget { if (!isset($this->value)) $this->value = $this->field->get('default'); ?> - <input style="vertical-align:top;" type="checkbox" name="<?php echo $this->name; ?>[]" <?php + <input id="<?php echo $this->id; ?>" style="vertical-align:top;" + type="checkbox" name="<?php echo $this->name; ?>[]" <?php if ($this->value) echo 'checked="checked"'; ?> value="<?php echo $this->field->get('id'); ?>"/> <?php @@ -1822,6 +1907,10 @@ class CheckboxWidget extends Widget { return @in_array($this->field->get('id'), $data[$this->name]); return parent::getValue(); } + + function getJsValueGetter() { + return '%s.is(":checked")'; + } } class DatetimePickerWidget extends Widget { @@ -1841,6 +1930,7 @@ class DatetimePickerWidget extends Widget { } ?> <input type="text" name="<?php echo $this->name; ?>" + id="<?php echo $this->id; ?>" value="<?php echo Format::htmlchars($this->value); ?>" size="12" autocomplete="off" class="dp" /> <script type="text/javascript"> @@ -2035,4 +2125,172 @@ class FileUploadWidget extends Widget { class FileUploadError extends Exception {} +class VisibilityConstraint { + + const HIDDEN = 0x0001; + const VISIBLE = 0x0002; + + var $initial; + var $constraint; + + function __construct($constraint, $initial=self::VISIBLE) { + $this->constraint = $constraint; + $this->initial = $initial; + } + + function emitJavascript($field) { + $func = 'recheck'; + $form = $field->getForm(); +?> + <script type="text/javascript"> + !(function() { + var <?php echo $func; ?> = function() { + var target = $('#field<?php echo $field->getWidget()->id; ?>'); + +<?php $fields = $this->getAllFields($this->constraint); + foreach ($fields as $f) { + $field = $form->getField($f); + echo sprintf('var %1$s = $("#%1$s");', + $field->getWidget()->id); + } + $expression = $this->compileQ($this->constraint, $form); +?> + if (<?php echo $expression; ?>) + target.slideDown('fast', function (){ + $(this).trigger('show'); + }); + else + target.slideUp('fast', function (){ + $(this).trigger('hide'); + }); + }; + +<?php foreach ($fields as $f) { + $w = $form->getField($f)->getWidget(); +?> + $('#<?php echo $w->id; ?>').on('change', <?php echo $func; ?>); + $('#field<?php echo $w->id; ?>').on('show hide', <?php + echo $func; ?>); +<?php } ?> + })(); + </script><?php + } + + /** + * Determines if the field was visible when the form was submitted + */ + function isVisible($field) { + return $this->compileQPhp($this->constraint, $field); + } + + function compileQPhp(Q $Q, $field) { + $form = $field->getForm(); + $expr = array(); + foreach ($Q->constraints as $c=>$value) { + if ($value instanceof Q) { + $expr[] = $this->compileQPhp($value, $field); + } + else { + @list($f, $op) = explode('__', $c, 2); + $field = $form->getField($f); + $wval = $field->getClean(); + switch ($op) { + case 'eq': + case null: + $expr[] = ($wval == $value && $field->isVisible()); + } + } + } + $glue = $Q->isOred() + ? function($a, $b) { return $a || $b; } + : function($a, $b) { return $a && $b; }; + $initial = !$Q->isOred(); + $expression = array_reduce($expr, $glue, $initial); + if ($Q->isNegated) + $expression = !$expression; + return $expression; + } + + function getAllFields(Q $Q, &$fields=array()) { + foreach ($Q->constraints as $c=>$value) { + if ($c instanceof Q) { + $this->getAllFields($c, $fields); + } + else { + list($f, $op) = explode('__', $c, 2); + $fields[$f] = true; + } + } + return array_keys($fields); + } + + function compileQ($Q, $form) { + $expr = array(); + foreach ($Q->constraints as $c=>$value) { + if ($value instanceof Q) { + $expr[] = $this->compileQ($value, $form); + } + else { + list($f, $op) = explode('__', $c, 2); + $widget = $form->getField($f)->getWidget(); + $id = $widget->id; + switch ($op) { + case 'eq': + case null: + $expr[] = sprintf('(%s.is(":visible") && %s)', + $id, + sprintf('%s == %s', + sprintf($widget->getJsValueGetter(), $id), + JsonDataEncoder::encode($value)) + ); + } + } + } + $glue = $Q->isOred() ? ' || ' : ' && '; + $expression = implode($glue, $expr); + if (count($expr) > 1) + $expression = '('.$expression.')'; + if ($Q->isNegated) + $expression = '!'.$expression; + return $expression; + } +} + +class Q { + const NEGATED = 0x0001; + const ANY = 0x0002; + + var $constraints; + var $flags; + var $negated = false; + var $ored = false; + + function __construct($filter, $flags=0) { + $this->constraints = $filter; + $this->negated = $flags & self::NEGATED; + $this->ored = $flags & self::ANY; + } + + function isNegated() { + return $this->negated; + } + + function isOred() { + return $this->ored; + } + + function negate() { + $this->negated = !$this->negated; + return $this; + } + + static function not(array $constraints) { + return new static($constraints, self::NEGATED); + } + + static function any(array $constraints) { + return new static($constraints, self::ORED); + } +} + ?> diff --git a/include/class.list.php b/include/class.list.php index 274009de9e1a2f82ae458143f0b405aa78363aa4..dfee67d558d3ef2a56ac81f16e14fa8a090c82a7 100644 --- a/include/class.list.php +++ b/include/class.list.php @@ -554,7 +554,8 @@ class DynamicListItem extends VerySimpleModel implements CustomListItem { $this->_form = DynamicForm::lookup( array('type'=>'L'.$this->get('list_id')))->getForm($source); if (!$source && $config) { - foreach ($this->_form->getFields() as $f) { + $fields = $this->_form->getFields(); + foreach ($fields as $f) { $name = $f->get('id'); if (isset($config[$name])) $f->value = $f->to_php($config[$name]); @@ -563,6 +564,7 @@ class DynamicListItem extends VerySimpleModel implements CustomListItem { } } } + return $this->_form; } @@ -766,7 +768,6 @@ class TicketStatus extends VerySimpleModel implements CustomListItem { var $_list; var $_form; - var $_config; var $_settings; var $_properties; @@ -777,7 +778,6 @@ class TicketStatus extends VerySimpleModel implements CustomListItem { function __construct() { call_user_func_array(array('parent', '__construct'), func_get_args()); - $this->_config = new Config('TS.'.$this->getId()); } protected function hasFlag($field, $flag) { @@ -793,7 +793,7 @@ class TicketStatus extends VerySimpleModel implements CustomListItem { } protected function hasProperties() { - return ($this->_config->get('properties')); + return ($this->get('properties')); } function getForm() { @@ -804,21 +804,81 @@ class TicketStatus extends VerySimpleModel implements CustomListItem { return $this->_form; } + function getExtraConfigOptions($source=null) { + + + $status_choices = array( 0 => __('System Default')); + if (($statuses=TicketStatusList::getStatuses( + array( 'enabled' => true, 'states' => + array('open'))))) + foreach ($statuses as $s) + $status_choices[$s->getId()] = $s->getName(); + + + return array( + 'allowreopen' => new BooleanField(array( + 'label' =>__('Allow Reopen'), + 'default' => isset($source['allowreopen']) + ? $source['allowreopen']: true, + 'id' => 'allowreopen', + 'name' => 'allowreopen', + 'configuration' => array( + 'desc'=>__('Allow tickets on this status to be reopened by end users'), + ), + 'visibility' => new VisibilityConstraint( + new Q(array('state__eq'=>'closed')), + VisibilityConstraint::HIDDEN + ), + )), + 'reopenstatus' => new ChoiceField(array( + 'label' => __('Reopen Status'), + 'required' => false, + 'default' => isset($source['reopenstatus']) + ? $source['reopenstatus'] : 0, + 'id' => 'reopenstatus', + 'name' => 'reopenstatus', + 'choices' => $status_choices, + 'configuration' => array( + 'widget' => 'dropdown', + 'multiselect' =>false + ), + 'visibility' => new VisibilityConstraint( + new Q(array('allowreopen__eq'=> true)), + VisibilityConstraint::HIDDEN + ), + )) + ); + } + function getConfigurationForm($source=null) { - if ($form = $this->getForm()) { - $config = $this->getConfiguration(); - $form = $form->getForm($source); - if (!$source && $config) { - foreach ($form->getFields() as $f) { - $name = $f->get('id'); - if (isset($config[$name])) - $f->value = $f->to_php($config[$name]); - else if ($f->get('default')) - $f->value = $f->get('default'); + if (!($form = $this->getForm())) + return null; + + $config = $this->getConfiguration(); + $form = $form->getForm($source); + $fields = $form->getFields(); + foreach ($fields as $k => $f) { + if ($f->get('name') == 'state' //TODO: check if editable. + && ($extras=$this->getExtraConfigOptions($source))) { + foreach ($extras as $extra) { + $extra->setForm($form); + $fields->insert(++$k, $extra); } + break; } } + + if (!$source && $config) { + foreach ($fields as $f) { + $name = $f->get('id'); + if (isset($config[$name])) + $f->value = $f->to_php($config[$name]); + else if ($f->get('default')) + $f->value = $f->get('default'); + } + } + return $form; } @@ -826,6 +886,34 @@ class TicketStatus extends VerySimpleModel implements CustomListItem { return $this->hasFlag('mode', self::ENABLED); } + function isReopenable() { + + if (strcasecmp($this->get('state'), 'closed')) + return true; + + if (($c=$this->getConfiguration()) + && $c['allowreopen'] + && isset($c['reopenstatus'])) + return true; + + return false; + } + + function getReopenStatus() { + global $cfg; + + $status = null; + if ($this->isReopenable() + && ($c = $this->getConfiguration()) + && isset($c['reopenstatus'])) + $status = TicketStatus::lookup( + $c['reopenstatus'] ?: $cfg->getDefaultTicketStatusId()); + + return ($status + && !strcasecmp($status->getState(), 'open')) + ? $status : null; + } + function enable() { // Ticket status without properties cannot be enabled! @@ -900,7 +988,7 @@ class TicketStatus extends VerySimpleModel implements CustomListItem { private function getProperties() { if (!isset($this->_properties)) { - $this->_properties = $this->_config->get('properties'); + $this->_properties = $this->get('properties'); if (is_string($this->_properties)) $this->_properties = JsonDataParser::parse($this->_properties); elseif (!$this->_properties) @@ -980,20 +1068,16 @@ class TicketStatus extends VerySimpleModel implements CustomListItem { } if (count($errors) === 0) { + if ($properties && is_array($properties)) + $properties = JsonDataEncoder::encode($properties); + + $this->set('properties', $properties); $this->save(true); - $this->setProperties($properties); } return count($errors) === 0; } - function setProperties($properties) { - if ($properties && is_array($properties)) - $properties = JsonDataEncoder::encode($properties); - - $this->_config->set('properties', $properties); - } - function update($vars, &$errors) { $fields = array('value' => 'name', 'sort' => 'sort'); @@ -1042,13 +1126,9 @@ class TicketStatus extends VerySimpleModel implements CustomListItem { static function __create($ht, &$error=false) { global $ost; - $properties = JsonDataEncoder::encode($ht['properties']); - unset($ht['properties']); - if ($status = TicketStatus::create($ht)) { + $ht['properties'] = JsonDataEncoder::encode($ht['properties']); + if (($status = TicketStatus::create($ht))) $status->save(true); - $status->_config = new Config('TS.'.$status->getId()); - $status->_config->set('properties', $properties); - } return $status; } diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php index 45758c75c052057061c354e77e2c5faef639e4ff..4e6546549f44d7ad520670bb01af8e946534aaea 100644 --- a/include/class.mailfetch.php +++ b/include/class.mailfetch.php @@ -721,6 +721,10 @@ class MailFetcher { $seen = false; if (($thread = ThreadEntry::lookupByEmailHeaders($vars, $seen)) + && ($t=$thread->getTicket()) + && ($vars['staffId'] + || !$t->isClosed() + || $t->isReopenable()) && ($message = $thread->postEmail($vars))) { if (!$message instanceof ThreadEntry) // Email has been processed previously diff --git a/include/class.ticket.php b/include/class.ticket.php index e4ccfd0769ece4d7c5d84b6d9705a9f25b9e1f6a..0dd0759524db699f4645db80604be81dfb0c4195 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -142,8 +142,8 @@ class Ticket { return ($this->getReopenDate()); } - function isResolved() { - return $this->hasState('resolved'); + function isReopenable() { + return $this->getStatus()->isReopenable(); } function isClosed() { @@ -849,7 +849,6 @@ class Ticket { $ecb = null; switch($status->getState()) { - case 'resolved': case 'closed': $sql.=', closed=NOW(), duedate=NULL '; if ($thisstaff) @@ -944,11 +943,19 @@ class Ticket { return (db_query($sql) && db_affected_rows()); } - //set status to open on a closed ticket. - function reopen($isanswered=0) { + function reopen() { global $cfg; - return $this->setStatus($cfg->getDefaultTicketStatusId()); + if (!$this->isClosed()) + return false; + + // Set status to open based on current closed status settings + // If the closed status doesn't have configured "reopen" status then use the + // the default ticket status. + if (!($status=$this->getStatus()->getReopenStatus())) + $status = $cfg->getDefaultTicketStatusId(); + + return $status ? $this->setStatus($status, 'Reopened') : false; } function onNewTicket($message, $autorespond=true, $alertstaff=true) { @@ -1154,8 +1161,9 @@ class Ticket { } } - // Reopen if NOT open - if(!$this->isOpen()) $this->reopen(); + // Reopen if closed AND reopenable + if ($this->isClosed() && $this->isReopenable()) + $this->reopen(); /********** double check auto-response ************/ if (!($user = $message->getUser())) @@ -2281,14 +2289,6 @@ class Ticket { .'WHERE ticket.staff_id = ' . db_input($staff->getId()) . ' ' . $where - .'UNION SELECT \'resolved\', count( ticket.ticket_id ) AS tickets ' - .'FROM ' . TICKET_TABLE . ' ticket ' - .'INNER JOIN '.TICKET_STATUS_TABLE. ' status - ON (ticket.status_id=status.id - AND status.state=\'resolved\') ' - .'WHERE 1 ' - . $where - .'UNION SELECT \'closed\', count( ticket.ticket_id ) AS tickets ' .'FROM ' . TICKET_TABLE . ' ticket ' .'INNER JOIN '.TICKET_STATUS_TABLE. ' status diff --git a/include/class.user.php b/include/class.user.php index c7c668caf98f5c4b1533b8bdcdc4293d91535d9d..19f83efafb2f825109053e971ba0ea998eb22f19 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -14,7 +14,8 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ -require_once(INCLUDE_DIR . 'class.orm.php'); +require_once INCLUDE_DIR . 'class.orm.php'; +require_once INCLUDE_DIR . 'class.util.php'; class UserEmailModel extends VerySimpleModel { static $meta = array( @@ -1089,45 +1090,12 @@ class UserAccountStatus { /* * Generic user list. */ -class UserList implements IteratorAggregate, ArrayAccess { - private $users; - - function __construct($list = array()) { - $this->users = $list; - } - - function add($user) { - $this->offsetSet(null, $user); - } - - function offsetSet($offset, $value) { - - if (is_null($offset)) - $this->users[] = $value; - else - $this->users[$offset] = $value; - } - - function offsetExists($offset) { - return isset($this->users[$offset]); - } - - function offsetUnset($offset) { - unset($this->users[$offset]); - } - - function offsetGet($offset) { - return isset($this->users[$offset]) ? $this->users[$offset] : null; - } - - function getIterator() { - return new ArrayIterator($this->users); - } +class UserList extends ListObject { function __toString() { $list = array(); - foreach($this->users as $user) { + foreach($this->storage as $user) { if (is_object($user)) $list [] = $user->getName(); } @@ -1135,6 +1103,7 @@ class UserList implements IteratorAggregate, ArrayAccess { return $list ? implode(', ', $list) : ''; } } + require_once(INCLUDE_DIR . 'class.organization.php'); User::_inspect(); UserAccount::_inspect(); diff --git a/include/class.util.php b/include/class.util.php new file mode 100644 index 0000000000000000000000000000000000000000..8fb9b3967a4ec5cb4e6d2117c58ece03baa1b0fd --- /dev/null +++ b/include/class.util.php @@ -0,0 +1,169 @@ +<?php +/** + * Jared Hancock <jared@osticket.com> + * Copyright (c) 2014 + * + * Lightweight implementation of the Python list in PHP. This allows for + * treating an array like a simple list of items. The numeric indexes are + * automatically updated so that the indeces of the list will alway be from + * zero and increasing positively. + * + * Negative indexes are supported which reference from the end of the list. + * Therefore $queue[-1] will refer to the last item in the list. + */ +class ListObject implements IteratorAggregate, ArrayAccess, Serializable, Countable { + + protected $storage = array(); + + function __construct(array $array=array()) { + foreach ($array as $v) + $this->storage[] = $v; + } + + function append($what) { + if (is_array($what)) + return $this->extend($what); + + $this->storage[] = $what; + } + + function add($what) { + $this->append($what); + } + + function extend($iterable) { + foreach ($iterable as $v) + $this->storage[] = $v; + } + + function insert($i, $value) { + array_splice($this->storage, $i, 0, array($value)); + } + + function remove($value) { + if (!($k = $this->index($value))) + throw new OutOfRangeException('No such item in the list'); + unset($this->storage[$k]); + } + + function pop($at=false) { + if ($at === false) + return array_pop($this->storage); + elseif (!isset($this->storage[$at])) + throw new OutOfRangeException('Index out of range'); + else { + $rv = array_splice($this->storage, $at, 1); + return $rv[0]; + } + } + + function slice($offset, $length=null) { + return array_slice($this->storage, $offset, $length); + } + + function splice($offset, $length=0, $replacement=null) { + return array_splice($this->storage, $offset, $length, $replacement); + } + + function index($value) { + return array_search($this->storage, $value); + } + + /** + * Sort the list in place. + * + * Parameters: + * $key - (callable|int) A callable function to produce the sort keys + * or one of the SORT_ constants used by the array_multisort + * function + * $reverse - (bool) true if the list should be sorted descending + */ + function sort($key=false, $reverse=false) { + if (is_callable($key)) { + $keys = array_map($key, $this->storage); + array_multisort($keys, $this->storage, + $reverse ? SORT_DESC : SORT_ASC); + } + elseif ($key) { + array_multisort($this->storage, + $reverse ? SORT_DESC : SORT_ASC, $key); + } + elseif ($reverse) { + rsort($this->storage); + } + else + sort($this->storage); + } + + function reverse() { + return array_reverse($this->storage); + } + + function filter($callable) { + $new = new static(); + foreach ($this->storage as $i=>$v) + if ($callable($v, $i)) + $new[] = $v; + return $new; + } + + // IteratorAggregate + function getIterator() { + return new ArrayIterator($this->storage); + } + + // Countable + function count($mode=COUNT_NORMAL) { + return count($this->storage, $mode); + } + + // ArrayAccess + function offsetGet($offset) { + if (!is_int($offset)) + throw new InvalidArgumentException('List indices should be integers'); + elseif ($offset < 0) + $offset += count($this->storage); + if (!isset($this->storage[$offset])) + throw new OutOfBoundsException('List index out of range'); + return $this->storage[$offset]; + } + function offsetSet($offset, $value) { + if ($offset === null) + return $this->storage[] = $value; + elseif (!is_int($offset)) + throw new InvalidArgumentException('List indices should be integers'); + elseif ($offset < 0) + $offset += count($this->storage); + + if (!isset($this->storage[$offset])) + throw new OutOfBoundsException('List assignment out of range'); + + $this->storage[$offset] = $value; + } + function offsetExists($offset) { + if (!is_int($offset)) + throw new InvalidArgumentException('List indices should be integers'); + elseif ($offset < 0) + $offset += count($this->storage); + return isset($this->storage[$offset]); + } + function offsetUnset($offset) { + if (!is_int($offset)) + throw new InvalidArgumentException('List indices should be integers'); + elseif ($offset < 0) + $offset += count($this->storage); + unset($this->storage[$offset]); + } + + // Serializable + function serialize() { + return serialize($this->storage); + } + function unserialize($what) { + $this->storage = unserialize($what); + } + + function __toString() { + return '['.implode(', ', $this->storage).']'; + } +} diff --git a/include/client/tickets.inc.php b/include/client/tickets.inc.php index 5b13aa8234249fb665267a982d7e97aed0c6533b..c2150b6515931efabfb1410e060b1f480af9da88 100644 --- a/include/client/tickets.inc.php +++ b/include/client/tickets.inc.php @@ -62,7 +62,6 @@ $qwhere = sprintf(' WHERE ( ticket.user_id=%d OR collab.user_id=%d )', $states = array( 'open' => 'open', - 'resolved' => 'resolved', 'closed' => 'closed'); if($status && isset($states[$status])){ $qwhere.=' AND status.state='.db_input($states[$status]); @@ -121,21 +120,14 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting <select name="status"> <option value="">— <?php echo __('Any Status');?> —</option> <option value="open" - <?php echo ($status=='open')?'selected="selected"':'';?>><?php echo _P('ticket-status', 'Open');?> (<?php echo $thisclient->getNumOpenTickets(); ?>)</option> - <?php - if($thisclient->getNumResolvedTickets()) { - ?> - <option value="resolved" - <?php echo ($status=='resolved')?'selected="selected"':'';?>><?php - echo __('Resolved'); ?> (<?php echo $thisclient->getNumResolvedTickets(); ?>)</option> - <?php - } ?> - + <?php echo ($status=='open') ? 'selected="selected"' : '';?>> + <?php echo _P('ticket-status', 'Open');?> (<?php echo $thisclient->getNumOpenTickets(); ?>)</option> <?php if($thisclient->getNumClosedTickets()) { ?> <option value="closed" - <?php echo ($status=='closed')?'selected="selected"':'';?>><?php echo __('Closed');?> (<?php echo $thisclient->getNumClosedTickets(); ?>)</option> + <?php echo ($status=='closed') ? 'selected="selected"' : '';?>> + <?php echo __('Closed');?> (<?php echo $thisclient->getNumClosedTickets(); ?>)</option> <?php } ?> </select> diff --git a/include/client/view.inc.php b/include/client/view.inc.php index 454cc9e7d80e8977d32803417dc927d8167ef0e7..1867ac467ab9b3ac50496c6f00c63c0abad3d150 100644 --- a/include/client/view.inc.php +++ b/include/client/view.inc.php @@ -4,6 +4,10 @@ if(!defined('OSTCLIENTINC') || !$thisclient || !$ticket || !$ticket->checkUserAc $info=($_POST && $errors)?Format::htmlchars($_POST):array(); $dept = $ticket->getDept(); + +if ($ticket->isClosed() && !$ticket->isReopenable()) + $warn = __('This ticket is marked as closed and cannot be reopened.'); + //Making sure we don't leak out internal dept names if(!$dept || !$dept->isPublic()) $dept = $cfg->getDefaultDept(); @@ -146,6 +150,10 @@ if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) { <?php }elseif($warn) { ?> <div id="msg_warning"><?php echo $warn; ?></div> <?php } ?> + +<?php + +if (!$ticket->isClosed() || $ticket->isReopenable()) { ?> <form id="reply" action="tickets.php?id=<?php echo $ticket->getId(); ?>#reply" name="reply" method="post" enctype="multipart/form-data"> <?php csrf_token(); ?> <h2><?php echo __('Post a Reply');?></h2> @@ -183,3 +191,5 @@ if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) { <input type="button" value="<?php echo __('Cancel');?>" onClick="history.go(-1)"> </p> </form> +<?php +} ?> diff --git a/include/i18n/en_US/ticket_status.yaml b/include/i18n/en_US/ticket_status.yaml index 836deb2a5a83c7911c389927e6df5ecb72df9546..1a5d268a405e86f8ba16f8107c9d3861febaddb3 100644 --- a/include/i18n/en_US/ticket_status.yaml +++ b/include/i18n/en_US/ticket_status.yaml @@ -5,7 +5,7 @@ # id - (int:optional) id number in the database # name - (string) descriptive name of the status # state - (string) Main status of a ticket -# (open, resolved, closed, archived, deleted) +# (open, closed, archived, deleted) # mode - (bit) access mask (1 - enabled, 2 - internal) # flags - (bit) flags that can be set on a ticket # properties: @@ -24,14 +24,15 @@ - id: 2 name: Resolved - state: resolved - mode: 3 + state: closed + mode: 1 sort: 2 flags: 0 properties: + allowreopen: true + reopenstatus: 0 description: > - Resolved tickets are closed tickets that can be reopened by the end user. This might be useful - when a trigger is used to close resolved tickets with notice sent to end user. + Resolved tickets - id: 3 name: Closed @@ -40,8 +41,10 @@ sort: 3 flags: 0 properties: + allowreopen: true + reopenstatus: 0 description: > - Tickets marked as closed cannot be reopened by the end user. Tickets will still be accessible on client and staff panels. + Closed tickets. Tickets will still be accessible on client and staff panels. - id: 4 name: Archived @@ -52,7 +55,7 @@ properties: description: > Tickets only adminstratively available but no longer accessible on - ticket queues. + ticket queues and client panel. - id: 5 name: Deleted diff --git a/include/staff/templates/dynamic-field-config.tmpl.php b/include/staff/templates/dynamic-field-config.tmpl.php index af257465289439f8748ffc948911be5fc62e2f1c..5b2716e4e1e53d09034af80cdcfd7663b2668e21 100644 --- a/include/staff/templates/dynamic-field-config.tmpl.php +++ b/include/staff/templates/dynamic-field-config.tmpl.php @@ -8,10 +8,15 @@ $form = $field->getConfigurationForm(); echo $form->getMedia(); foreach ($form->getFields() as $name=>$f) { ?> - <div class="flush-left custom-field"> - <div class="field-label"> + <div class="flush-left custom-field" id="field<?php echo $f->getWidget()->id; + ?>" <?php if (!$f->isVisible()) echo 'style="display:none;"'; ?>> + <div class="field-label <?php if ($f->get('required')) echo 'required'; ?>"> <label for="<?php echo $f->getWidget()->name; ?>"> - <?php echo Format::htmlchars($f->get('label')); ?>:</label> + <?php echo Format::htmlchars($f->get('label')); ?>: + <?php if ($f->get('required')) { ?> + <span class="error">*</span> + <?php } ?> + </label> <?php if ($f->get('hint')) { ?> <br/><em style="color:gray;display:inline-block"><?php @@ -21,10 +26,6 @@ </div><div> <?php $f->render(); - if ($f->get('required')) { ?> - <font class="error">*</font> - <?php - } ?> </div> <?php diff --git a/include/staff/templates/list-item-properties.tmpl.php b/include/staff/templates/list-item-properties.tmpl.php index 51e26b65a55a3a205011cef58951d11280875619..a8d158f98e5641ade0376c8090fec148fe8ff62f 100644 --- a/include/staff/templates/list-item-properties.tmpl.php +++ b/include/staff/templates/list-item-properties.tmpl.php @@ -10,8 +10,12 @@ $internal = $item->isInternal(); $form = $item->getConfigurationForm(); echo $form->getMedia(); - foreach ($form->getFields() as $f) { ?> - <div class="custom-field"> + foreach ($form->getFields() as $f) { + ?> + <div class="custom-field" id="field<?php + echo $f->getWidget()->id; ?>" + <?php + if (!$f->isVisible()) echo 'style="display:none;"'; ?>> <div class="field-label"> <label for="<?php echo $f->getWidget()->name; ?>" style="vertical-align:top;padding-top:0.2em"> diff --git a/include/staff/templates/status-options.tmpl.php b/include/staff/templates/status-options.tmpl.php index a0047553c67e01a6c283317b0e030e41f2ffef68..78372ed09195a50376f55cd34696db6c43af91a6 100644 --- a/include/staff/templates/status-options.tmpl.php +++ b/include/staff/templates/status-options.tmpl.php @@ -3,13 +3,8 @@ global $thisstaff, $ticket; // Map states to actions $actions= array( 'closed' => array( - 'icon' => 'icon-repeat', - 'action' => 'close', - 'href' => 'tickets.php' - ), - 'resolved' => array( 'icon' => 'icon-ok-circle', - 'action' => 'resolve', + 'action' => 'close', 'href' => 'tickets.php' ), 'open' => array( @@ -34,8 +29,7 @@ $actions= array( <?php $states = array('open'); if ($thisstaff->canCloseTickets()) - $states = array_merge($states, - array('resolved', 'closed')); + $states = array_merge($states, array('closed')); $statusId = $ticket ? $ticket->getStatusId() : 0; foreach (TicketStatusList::getStatuses( diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php index d677070ed7588a759d75e0fb248ea1412f5e4a10..5ca5cb5436b480e319d5c2b93a522ad036735249 100644 --- a/include/staff/ticket-open.inc.php +++ b/include/staff/ticket-open.inc.php @@ -317,7 +317,7 @@ print $response_form->getField('attachments')->render(); $statusId = $info['statusId'] ?: $cfg->getDefaultTicketStatusId(); $states = array('open'); if ($thisstaff->canCloseTickets()) - $states = array_merge($states, array('resolved', 'closed')); + $states = array_merge($states, array('closed')); foreach (TicketStatusList::getStatuses( array('states' => $states)) as $s) { if (!$s->isEnabled()) continue; diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index 1b137738fec15f6baab1e27682c3c6ca49517301..b0dc9ee4aa4ee6cc4f91271c3be2a46adbc5c2d4 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -22,15 +22,27 @@ $lock = $ticket->getLock(); //Ticket lock obj $id = $ticket->getId(); //Ticket ID. //Useful warnings and errors the user might want to know! -if($ticket->isAssigned() && ( - ($staff && $staff->getId()!=$thisstaff->getId()) - || ($team && !$team->hasMember($thisstaff)) +if ($ticket->isClosed() && !$ticket->isReopenable()) + $warn = sprintf( + __('Current ticket status (%s) does not allow the end user to reply.'), + $ticket->getStatus()); +elseif ($ticket->isAssigned() + && (($staff && $staff->getId()!=$thisstaff->getId()) + || ($team && !$team->hasMember($thisstaff)) )) - $warn.=' <span class="Icon assignedTicket">'.sprintf(__('Ticket is assigned to %s'),implode('/', $ticket->getAssignees())).'</span>'; -if(!$errors['err'] && ($lock && $lock->getStaffId()!=$thisstaff->getId())) - $errors['err']=sprintf(__('This ticket is currently locked by %s'),$lock->getStaffName()); -if(!$errors['err'] && ($emailBanned=TicketFilter::isBanned($ticket->getEmail()))) - $errors['err']=__('Email is in banlist! Must be removed before any reply/response'); + $warn.= sprintf(' <span class="Icon assignedTicket">%</span>', + sprintf(__('Ticket is assigned to %s'), + implode('/', $ticket->getAssignees()) + )); + +if (!$errors['err']) { + + if ($lock && $lock->getStaffId()!=$thisstaff->getId()) + $errors['err'] = sprintf(__('This ticket is currently locked by %s'), + $lock->getStaffName()); + elseif (($emailBanned=TicketFilter::isBanned($ticket->getEmail()))) + $errors['err'] = __('Email is in banlist! Must be removed before any reply/response'); +} $unbannable=($emailBanned) ? BanList::includes($ticket->getEmail()) : false; @@ -201,13 +213,6 @@ if($ticket->isOverdue()) echo sprintf('<li><a href="tickets.php?a=search&status=open&uid=%s"><i class="icon-folder-open-alt icon-fixed-width"></i> %s</a></li>', $user->getId(), sprintf(_N('%d Open Ticket', '%d Open Tickets', $open), $open)); - if(($resolved=$user->getNumResolvedTickets())) - echo sprintf('<li><a href="tickets.php?a=search&status=resolved&uid=%d"><i - class="icon-folder-close-alt icon-fixed-width"></i> %s</a></li>', - $user->getId(), sprintf(_N('%d Resolved Ticket', '%d Resolved Tickets', $resolved), $resolved)); - - - if(($closed=$user->getNumClosedTickets())) echo sprintf('<li><a href="tickets.php?a=search&status=closed&uid=%d"><i class="icon-folder-close-alt icon-fixed-width"></i> %s</a></li>', @@ -606,7 +611,7 @@ print $response_form->getField('attachments')->render(); $statusId = $info['reply_status_id'] ?: $ticket->getStatusId(); $states = array('open'); if ($thisstaff->canCloseTickets()) - $states = array_merge($states, array('resolved', 'closed')); + $states = array_merge($states, array('closed')); foreach (TicketStatusList::getStatuses( array('states' => $states)) as $s) { @@ -687,7 +692,7 @@ print $note_form->getField('attachments')->render(); $statusId = $info['note_status_id'] ?: $ticket->getStatusId(); $states = array('open'); if ($thisstaff->canCloseTickets()) - $states = array_merge($states, array('resolved', 'closed')); + $states = array_merge($states, array('closed')); foreach (TicketStatusList::getStatuses( array('states' => $states)) as $s) { if (!$s->isEnabled()) continue; diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php index a5ad1dcdda5758228392cd5c45c2652406f48bf8..3b716905e3a12150365c39da98d19458daa2e23c 100644 --- a/include/staff/tickets.inc.php +++ b/include/staff/tickets.inc.php @@ -35,10 +35,6 @@ switch(strtolower($_REQUEST['status'])){ //Status is overloaded $results_type=__('Closed Tickets'); $showassigned=true; //closed by. break; - case 'resolved': - $status='resolved'; - $showassigned=true; - break; case 'overdue': $status='open'; $showoverdue=true; @@ -84,7 +80,6 @@ $qwhere .= ' )'; //STATUS to states $states = array( 'open' => array('open'), - 'resolved' => array('resolved'), 'closed' => array('closed')); if($status && isset($states[$status])) { @@ -552,8 +547,7 @@ if ($results) { <option value="">— <?php echo __('Any Status');?> —</option> <?php foreach (TicketStatusList::getStatuses( - array('states' => - array('open', 'resolved', 'closed'))) as $s) { + array('states' => array('open', 'closed'))) as $s) { echo sprintf('<option data-state="%s" value="%d">%s</option>', $s->getState(), $s->getId(), __($s->getName())); } diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig index 30a5aca33dc5932c1e09ede64d418368d54367cb..3c66643467933d668c14629d21d672cb2db1eaf3 100644 --- a/include/upgrader/streams/core.sig +++ b/include/upgrader/streams/core.sig @@ -1 +1 @@ -03ff59bf35a58a102e9b32ad33c2839f +b26f29a6bb5dbb3510b057632182d138 diff --git a/include/upgrader/streams/core/03ff59bf-b26f29a6.cleanup.sql b/include/upgrader/streams/core/03ff59bf-b26f29a6.cleanup.sql new file mode 100644 index 0000000000000000000000000000000000000000..66bc02c564fc09a580cddd5a7f93dacacbf529ec --- /dev/null +++ b/include/upgrader/streams/core/03ff59bf-b26f29a6.cleanup.sql @@ -0,0 +1,7 @@ +DELETE FROM `%TABLE_PREFIX%config` + WHERE `key` = 'properties' AND `namespace` LIKE 'TS.%'; + +DELETE FROM `%TABLE_PREFIX%ticket_status` + WHERE `state` = 'resolved'; + +OPTIMIZE TABLE `%TABLE_PREFIX%ticket_status`; diff --git a/include/upgrader/streams/core/03ff59bf-b26f29a6.patch.sql b/include/upgrader/streams/core/03ff59bf-b26f29a6.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..162c635587773da7f8c9fa5f0f5612741461f228 --- /dev/null +++ b/include/upgrader/streams/core/03ff59bf-b26f29a6.patch.sql @@ -0,0 +1,46 @@ +/** + * @version v1.9.4 + * @signature 519d98cd885f060e220da7b30a6f78ae + * @title Add properties filed and drop 'resolved' state + * + * This patch drops resolved state and any associated statuses + * + */ + +-- Move tickets in resolved state to the default closed status +SET @statusId = ( + SELECT id FROM `%TABLE_PREFIX%ticket_status` + WHERE `state` = 'closed' ORDER BY id ASC LIMIT 1); + +UPDATE `%TABLE_PREFIX%ticket` t1 + LEFT JOIN `%TABLE_PREFIX%ticket_status` t2 + ON ( t2.id = t1.status_id AND t2.state="resolved") + SET t1.status_id = @statusId; + +-- add properties field +ALTER TABLE `%TABLE_PREFIX%ticket_status` + ADD `properties` TEXT NOT NULL AFTER `sort`, + DROP `notes`; + +UPDATE `%TABLE_PREFIX%ticket_status` s + INNER JOIN `ost_config` c + ON(c.namespace = CONCAT('TS.', s.id) AND c.key='properties') + SET s.properties = c.value; + +-- add default reopen settings to existing closed state statuses +UPDATE `%TABLE_PREFIX%ticket_status` + SET `properties`= INSERT(`properties`, 2, 0, '"allowreopen":true,"reopenstatus":0,') + WHERE `state` = 'closed'; + +-- change thread body text to 16Mb. +ALTER TABLE `%TABLE_PREFIX%ticket_thread` + CHANGE `body` `body` mediumtext NOT NULL; + +-- index ext id +ALTER TABLE `%TABLE_PREFIX%note` + ADD INDEX (`ext_id`); + +-- Set new schema signature +UPDATE `%TABLE_PREFIX%config` + SET `value` = '519d98cd885f060e220da7b30a6f78ae' + WHERE `key` = 'schema_signature' AND `namespace` = 'core'; diff --git a/scp/css/scp.css b/scp/css/scp.css index 9389be034598f57f880b0124b14b8b79c31924be..5e3579d5ee8d48e692522b76ed039b0d09a132ae 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -1948,3 +1948,6 @@ table.custom-info td { direction: ltr; unicode-bidi: embed; } +.required { + font-weight: bold; +} diff --git a/scp/js/scp.js b/scp/js/scp.js index aa446f00ac243208b7510f3f0d2d3ea6cbc39a41..d1493f59036b5e762cdf3ccfefa02e96c35e573b 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -372,7 +372,6 @@ var scp_prep = function() { $('select#staffId').removeAttr('disabled'); break; case 'open': - case 'resolved': $('select#staffId') .attr('disabled','disabled') .find('option:first') diff --git a/scp/tickets.php b/scp/tickets.php index 4d6e5c1a5fc7b9ebda27c6d12e94c5f3143832d3..9ff8bb455d847841b947eee3b515765b82f42e4b 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -426,13 +426,6 @@ if($thisstaff->showAssignedOnly() && $stats['closed']) { ($_REQUEST['status']=='closed')); } else { - if ($stats['resolved']) - $nav->addSubMenu(array('desc' => __('Resolved').' ('.number_format($stats['resolved']).')', - 'title'=>__('Resolved Tickets'), - 'href'=>'tickets.php?status=resolved', - 'iconclass'=>'closedTickets'), - ($_REQUEST['status']=='resolved')); - $nav->addSubMenu(array('desc' => __('Closed').' ('.number_format($stats['closed']).')', 'title'=>__('Closed Tickets'), 'href'=>'tickets.php?status=closed', diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index 4879e53ce514f2aa1f4ba813b1fd967b3ae3f2cb..430a0b1ea44841ca299526a20fab8578bad4c8e0 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -489,7 +489,8 @@ CREATE TABLE `%TABLE_PREFIX%note` ( `sort` int(11) unsigned NOT NULL DEFAULT 0, `created` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', `updated` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `ext_id` (`ext_id`) ) DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `%TABLE_PREFIX%session`; @@ -681,7 +682,7 @@ CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%ticket_status` ( `mode` int(11) unsigned NOT NULL DEFAULT '0', `flags` int(11) unsigned NOT NULL DEFAULT '0', `sort` int(11) unsigned NOT NULL DEFAULT '0', - `notes` text NOT NULL, + `properties` text NOT NULL, `created` datetime NOT NULL, `updated` datetime NOT NULL, PRIMARY KEY (`id`), @@ -715,7 +716,7 @@ CREATE TABLE `%TABLE_PREFIX%ticket_thread` ( `poster` varchar(128) NOT NULL default '', `source` varchar(32) NOT NULL default '', `title` varchar(255), - `body` text NOT NULL, + `body` mediumtext NOT NULL, `format` varchar(16) NOT NULL default 'html', `ip_address` varchar(64) NOT NULL default '', `created` datetime NOT NULL,