diff --git a/file.php b/file.php index 4a2ce7962788c2ce0fe4f5f9b8c3af411d6ea1cd..00193f7a0b4b810964d9388a2a9a893f00406907 100644 --- a/file.php +++ b/file.php @@ -64,7 +64,8 @@ if ($file->verifySignature($_GET['signature'], $_GET['expires'])) { // Download the file.. $filename = $attachment ? $attachment->name : $file->getName(); - $file->download(@$_GET['disposition'] ?: false, $_GET['expires'], $filename); + $disposition = @$_GET['disposition'] ?: false; + $file->download($filename, $disposition, @$_GET['expires']); } catch (Exception $ex) { Http::response(500, 'Unable to find that file: '.$ex->getMessage()); diff --git a/include/ajax.search.php b/include/ajax.search.php index 93da251e7d164e9548550dce3b35cf16051bbcf3..dbc0a291b86d3c7bebb6f1c67333735b23739b9e 100644 --- a/include/ajax.search.php +++ b/include/ajax.search.php @@ -135,7 +135,7 @@ class SearchAjaxAPI extends AjaxController { $_SESSION[$key] = $keep; } } - + function _hashCriteria($criteria, $size=10) { $parts = array(); foreach ($criteria as $C) { diff --git a/include/ajax.tasks.php b/include/ajax.tasks.php index 4cc99a0d614f569e06ff1f43c6ae84303f3abe40..cd85cf613cbf696280ae79182946d6c9e6b46fea 100644 --- a/include/ajax.tasks.php +++ b/include/ajax.tasks.php @@ -113,7 +113,7 @@ class TasksAjaxAPI extends AjaxController { if ($desc && $desc->isAttachmentsEnabled() && ($attachments=$desc->getWidget()->getAttachments())) - $vars['cannedattachments'] = $attachments->getClean(); + $vars['files'] = $attachments->getFiles(); $vars['staffId'] = $thisstaff->getId(); $vars['poster'] = $thisstaff; $vars['ip_address'] = $_SERVER['REMOTE_ADDR']; @@ -811,9 +811,9 @@ class TasksAjaxAPI extends AjaxController { switch ($_POST['a']) { case 'postnote': $vars = $_POST; - $attachments = $note_form->getField('attachments')->getClean(); - $vars['cannedattachments'] = array_merge( - $vars['cannedattachments'] ?: array(), $attachments); + $attachments = $note_form->getField('attachments')->getFiles(); + $vars['files'] = array_merge( + $vars['files'] ?: array(), $attachments); if(($note=$task->postNote($vars, $errors, $thisstaff))) { $msg=__('Note posted successfully'); // Clear attachment list diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index a529c193862912a97be84195e512f1bbaa320710..2864efda74a09e4c5ceed50e699f15edbff6f5ef 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -1408,7 +1408,7 @@ function refer($tid, $target=null) { if ($desc && $desc->isAttachmentsEnabled() && ($attachments=$desc->getWidget()->getAttachments())) - $vars['cannedattachments'] = $attachments->getClean(); + $vars['files'] = $attachments->getFiles(); $vars['staffId'] = $thisstaff->getId(); $vars['poster'] = $thisstaff; $vars['ip_address'] = $_SERVER['REMOTE_ADDR']; @@ -1490,9 +1490,9 @@ function refer($tid, $target=null) { $vars = $_POST; switch ($_POST['a']) { case 'postnote': - $attachments = $note_attachments_form->getField('attachments')->getClean(); - $vars['cannedattachments'] = array_merge( - $vars['cannedattachments'] ?: array(), $attachments); + $attachments = $note_attachments_form->getField('attachments')->getFiles(); + $vars['files'] = array_merge( + $vars['files'] ?: array(), $attachments); if (($note=$task->postNote($vars, $errors, $thisstaff))) { $msg=__('Note posted successfully'); // Clear attachment list @@ -1506,9 +1506,9 @@ function refer($tid, $target=null) { } break; case 'postreply': - $attachments = $reply_attachments_form->getField('attachments')->getClean(); - $vars['cannedattachments'] = array_merge( - $vars['cannedattachments'] ?: array(), $attachments); + $attachments = $reply_attachments_form->getField('attachments')->getFiles(); + $vars['files'] = array_merge( + $vars['files'] ?: array(), $attachments); if (($response=$task->postReply($vars, $errors))) { $msg=__('Update posted successfully'); // Clear attachment list diff --git a/include/class.attachment.php b/include/class.attachment.php index 9c6cd7c40b6e980b1e387e341b135ca9840f0082..bb5e8e292f11aa0342f82299f7d2b5c1bec04dd8 100644 --- a/include/class.attachment.php +++ b/include/class.attachment.php @@ -60,6 +60,10 @@ class Attachment extends VerySimpleModel { return $this->name ?: $this->file->name; } + function getName() { + return $this->getFilename(); + } + function getHashtable() { return $this->ht; } @@ -133,7 +137,7 @@ extends InstrumentedList { foreach ($files as $file) { if (is_numeric($file)) $fileId = $file; - elseif (is_array($file) && isset($file['id']) && $file['id'] != 0) + elseif (is_array($file) && isset($file['id']) && $file['id']) $fileId = $file['id']; elseif (isset($file['tmp_name']) && ($F = AttachmentFile::upload($file))) $fileId = $F->getId(); diff --git a/include/class.canned.php b/include/class.canned.php index 21cd9aa6367f2b4ec81959d1790177fbb751ad65..388420c5f541a7667f7d3c33a020df5000a68b36 100644 --- a/include/class.canned.php +++ b/include/class.canned.php @@ -138,7 +138,7 @@ extends VerySimpleModel { $resp['files'] = array(); foreach ($this->getAttachedFiles(!$html) as $file) { - $_SESSION[':cannedFiles'][$file->id] = 1; + $_SESSION[':cannedFiles'][$file->id] = $file->name; $resp['files'][] = array( 'id' => $file->id, 'name' => $file->name, diff --git a/include/class.file.php b/include/class.file.php index 4d67b93ae5b1f8645fd9328415d0272da0cd2a64..5bbd3d8859b1303648bc6464a9075040b8a1e3c9 100644 --- a/include/class.file.php +++ b/include/class.file.php @@ -248,7 +248,7 @@ class AttachmentFile extends VerySimpleModel { return hash_hmac('sha1', implode("\n", $pieces), SECRET_SALT); } - function download($disposition=false, $expires=false, $name=false) { + function download($name=false, $disposition=false, $expires=false) { $disposition = $disposition ?: 'inline'; $bk = $this->open(); if ($bk->sendRedirectUrl($disposition)) @@ -258,7 +258,9 @@ class AttachmentFile extends VerySimpleModel { $type = $this->getType() ?: 'application/octet-stream'; if (isset($_REQUEST['overridetype'])) $type = $_REQUEST['overridetype']; - Http::download($name ?: $this->getName(), $type, null, 'inline'); + elseif (!strcasecmp($disposition, 'attachment')) + $type = 'application/octet-stream'; + Http::download($name ?: $this->getName(), $type, null, $disposition); header('Content-Length: '.$this->getSize()); $this->sendData(false); exit(); diff --git a/include/class.format.php b/include/class.format.php index a2d72340dcf02e3cba45990e4e455ab8146604e3..d3d9d7ae9ae68e7994e302fc70be0d079f6cae35 100644 --- a/include/class.format.php +++ b/include/class.format.php @@ -667,20 +667,20 @@ class Format { '%x', $timezone ?: $cfg->getTimezone(), $user); } - function datetime($timestamp, $fromDb=true, $timezone=false, $user=false) { + function datetime($timestamp, $fromDb=true, $format=false, $timezone=false, $user=false) { global $cfg; return self::__formatDate($timestamp, - $cfg->getDateTimeFormat(), $fromDb, + $format ?: $cfg->getDateTimeFormat(), $fromDb, IDF_SHORT, IDF_SHORT, '%x %X', $timezone ?: $cfg->getTimezone(), $user); } - function daydatetime($timestamp, $fromDb=true, $timezone=false, $user=false) { + function daydatetime($timestamp, $fromDb=true, $format=false, $timezone=false, $user=false) { global $cfg; return self::__formatDate($timestamp, - $cfg->getDayDateTimeFormat(), $fromDb, + $format ?: $cfg->getDayDateTimeFormat(), $fromDb, IDF_FULL, IDF_SHORT, '%x %X', $timezone ?: $cfg->getTimezone(), $user); } diff --git a/include/class.forms.php b/include/class.forms.php index 32a0cd9e23813cd009ce343852f06b8cb87b8f5d..ea29885cbac6003708e8a68ab755651a04d6810c 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -1960,6 +1960,40 @@ class ChoiceField extends FormField { } } +class NumericField extends FormField { + + function getSearchMethods() { + return array( + 'equal' => __('Equal'), + 'greater' => __('Greater Than'), + 'less' => __('Less Than'), + ); + } + + function getSearchMethodWidgets() { + return array( + 'equal' => array('TextboxField', array( + 'configuration' => array( + 'validator' => 'number', + 'size' => 6 + ), + )), + 'greater' => array('TextboxField', array( + 'configuration' => array( + 'validator' => 'number', + 'size' => 6 + ), + )), + 'less' => array('TextboxField', array( + 'configuration' => array( + 'validator' => 'number', + 'size' => 6 + ), + )), + ); + } +} + class DatetimeField extends FormField { static $widget = 'DatetimePickerWidget'; @@ -2043,7 +2077,7 @@ class DatetimeField extends FormField { function to_php($value) { - if (strtotime($value) <= 0) + if (!is_numeric($value) && strtotime($value) <= 0) return 0; return $value; @@ -2056,8 +2090,9 @@ class DatetimeField extends FormField { return ''; $config = $this->getConfiguration(); + $format = $config['format'] ?: false; if ($config['gmt']) - return $this->format((int) $datetime->format('U')); + return $this->format((int) $datetime->format('U'), $format); // Force timezone if field has one. if ($config['timezone']) { @@ -2066,10 +2101,10 @@ class DatetimeField extends FormField { } $value = $this->format($datetime->format('U'), - $datetime->getTimezone()->getName()); - + $datetime->getTimezone()->getName(), + $format); // No need to show timezone - if (!$config['time']) + if (!$config['time'] || $format) return $value; // Display is NOT timezone aware show entry's timezone. @@ -2083,16 +2118,16 @@ class DatetimeField extends FormField { return ($timestamp > 0) ? $timestamp : ''; } - function format($timestamp, $timezone=false) { + function format($timestamp, $timezone=false, $format=false) { if (!$timestamp || $timestamp <= 0) return ''; $config = $this->getConfiguration(); if ($config['time']) - $formatted = Format::datetime($timestamp, false, $timezone); + $formatted = Format::datetime($timestamp, false, $format, $timezone); else - $formatted = Format::date($timestamp, false, false, $timezone); + $formatted = Format::date($timestamp, false, $format, $timezone); return $formatted; } @@ -3402,6 +3437,10 @@ class FileUploadField extends FormField { if ($a && ($f=$a->getFile())) $files[] = $f; } + + foreach (@$this->getClean() as $key => $value) + $files[] = array('id' => $key, 'name' => $value); + $this->files = $files; } return $this->files; @@ -4462,6 +4501,7 @@ class FileUploadWidget extends Widget { $F = AttachmentFile::objects() ->filter(array('id__in' => array_keys($new))) ->all(); + foreach ($F as $f) { $f->tmp_name = $new[$f->getId()]; $files[] = array( @@ -4510,7 +4550,7 @@ class FileUploadWidget extends Widget { foreach (AttachmentFile::format($_FILES[$this->name]) as $file) { try { $F = $this->field->uploadFile($file); - $ids[] = $F->getId(); + $ids[$F->getId()] = $F->getName(); } catch (FileUploadError $ex) {} } @@ -4521,17 +4561,17 @@ class FileUploadWidget extends Widget { // identified in the session // // If no value was sent, assume an empty list - if (!($_files = parent::getValue())) + if (!($files = parent::getValue())) return array(); if ($_SERVER['REQUEST_METHOD'] == 'POST') { - foreach ($_files as $info) { - if (@list($id,$name) = explode(',', $info, 2)) - $files[$id] = $name; + $_files = array(); + foreach ($files as $info) { + if (@list($id, $name) = explode(',', $info, 2)) + $_files[$id] = $name; } + $files = $_files; } - else - $files = $_files; $allowed = array(); // Files already attached to the field are allowed @@ -4554,7 +4594,7 @@ class FileUploadWidget extends Widget { continue; // Keep the values as the IDs - $ids[$id] = $name ?: $allowed[$id] ?: $id; + $ids[$id] = $name; } return $ids; diff --git a/include/class.list.php b/include/class.list.php index e5c84fbd91b58cc2f8dc742bbec745bba1151882..f8055805c2e692eec4fb446db79c8585241260f8 100644 --- a/include/class.list.php +++ b/include/class.list.php @@ -1421,6 +1421,9 @@ implements CustomListItem, TemplateVariable, Searchable { } function display() { + + return $this->getLocalName(); + return sprintf('<a class="preview" href="#" data-preview="#list/%d/items/%d/preview">%s</a>', $this->getListId(), diff --git a/include/class.queue.php b/include/class.queue.php index 8526de7005006f2310744bff29abf57ae133d4d8..48f82c81f44e47f7ee8dac4a8bac2eb75c84679b 100644 --- a/include/class.queue.php +++ b/include/class.queue.php @@ -574,7 +574,7 @@ class CustomQueue extends VerySimpleModel { continue; $name = $f->get('name') ?: 'field_'.$f->get('id'); - $key = 'cdata.'.$name; + $key = 'cdata__'.$name; $cdata[$key] = $f->getLocal('label'); } @@ -582,22 +582,22 @@ class CustomQueue extends VerySimpleModel { $fields = array( 'number' => __('Ticket Number'), 'created' => __('Date Created'), - 'cdata.subject' => __('Subject'), - 'user.name' => __('From'), - 'user.default_email.address' => __('From Email'), - 'cdata.:priority.priority_desc' => __('Priority'), - 'dept::getLocalName' => __('Department'), - 'topic::getName' => __('Help Topic'), + 'cdata__subject' => __('Subject'), + 'user__name' => __('From'), + 'user__emails__address' => __('From Email'), + 'cdata__priority' => __('Priority'), + 'dept_id' => __('Department'), + 'topic_id' => __('Help Topic'), 'source' => __('Source'), - 'status::getName' =>__('Current Status'), + 'status__id' =>__('Current Status'), 'lastupdate' => __('Last Updated'), 'est_duedate' => __('SLA Due Date'), 'duedate' => __('Due Date'), 'closed' => __('Closed Date'), 'isoverdue' => __('Overdue'), 'isanswered' => __('Answered'), - 'staff::getName' => __('Agent Assigned'), - 'team::getName' => __('Team Assigned'), + 'staff_id' => __('Agent Assigned'), + 'team_id' => __('Team Assigned'), 'thread_count' => __('Thread Count'), 'reopen_count' => __('Reopen Count'), 'attachment_count' => __('Attachment Count'), @@ -629,6 +629,22 @@ class CustomQueue extends VerySimpleModel { return $fields; } + function getExportColumns($fields=array()) { + $columns = array(); + $fields = $fields ?: $this->getExportFields(); + $i = 0; + foreach ($fields as $path => $label) { + $c = QueueColumn::placeholder(array( + 'id' => $i++, + 'heading' => $label, + 'primary' => $path, + )); + $c->setQueue($this); + $columns[$path] = $c; + } + return $columns; + } + function getStandardColumns() { return $this->getColumns(); } @@ -775,14 +791,13 @@ class CustomQueue extends VerySimpleModel { } function export($options=array()) { + global $thisstaff; - if (!($query=$this->getBasicQuery())) - return false; - - if (!($fields=$this->getExportFields())) + if (!$thisstaff + || !($query=$this->getBasicQuery()) + || !($fields=$this->getExportFields())) return false; - $filename = sprintf('%s Tickets-%s.csv', $this->getName(), strftime('%Y%m%d')); @@ -799,14 +814,45 @@ class CustomQueue extends VerySimpleModel { $filename ="$filename.csv"; } - if (isset($opts['delimiter'])) + if (isset($opts['delimiter']) && !$options['delimiter']) $options['delimiter'] = $opts['delimiter']; } + // Apply columns + $columns = $this->getExportColumns($fields); + $headers = array(); // Reset fields based on validity of columns + foreach ($columns as $column) { + $query = $column->mangleQuery($query, $this->getRoot()); + $headers[] = $column->getHeading(); + } + + // Apply visibility + if (!$this->ignoreVisibilityConstraints($thisstaff)) + $query->filter($thisstaff->getTicketsVisibility()); - return Export::saveTickets($query, $fields, $filename, 'csv', - $options); + // Render Util + $render = function ($row) use($columns) { + if (!$row) return false; + + $record = array(); + foreach ($columns as $path => $column) { + $record[] = (string) $column->from_query($row) ?: + $row[$path] ?: ''; + } + return $record; + }; + + $delimiter = $options['delimiter'] ?: + Internationalization::getCSVDelimiter(); + $output = fopen('php://output', 'w'); + Http::download($filename, "text/csv"); + fputs($output, chr(0xEF) . chr(0xBB) . chr(0xBF)); + fputcsv($output, $headers, $delimiter); + foreach ($query as $row) + fputcsv($output, $render($row), $delimiter); + fclose($output); + exit(); } /** @@ -1427,7 +1473,7 @@ abstract class QueueColumnAnnotation { } // Add the annotation to a QuerySet - abstract function annotate($query); + abstract function annotate($query, $name); // Fetch some HTML to render the decoration on the page. This function // can return boolean FALSE to indicate no decoration should be applied @@ -1462,6 +1508,17 @@ abstract class QueueColumnAnnotation { function isVisible($row) { return true; } + + static function addToQuery($query, $name=false) { + $name = $name ?: static::$qname; + $annotation = new Static(array()); + return $annotation->annotate($query, $name); + } + + static function from_query($row, $name=false) { + $name = $name ?: static::$qname; + return $row[$name]; + } } class TicketThreadCount @@ -1470,9 +1527,10 @@ extends QueueColumnAnnotation { static $qname = '_thread_count'; static $desc = /* @trans */ 'Thread Count'; - function annotate($query) { + function annotate($query, $name=false) { + $name = $name ?: static::$qname; return $query->annotate(array( - static::$qname => TicketThread::objects() + $name => TicketThread::objects() ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1))) ->exclude(array('entries__flags__hasbit' => ThreadEntry::FLAG_HIDDEN)) ->aggregate(array('count' => SqlAggregate::COUNT('entries__id'))) @@ -1500,9 +1558,10 @@ extends QueueColumnAnnotation { static $qname = '_reopen_count'; static $desc = /* @trans */ 'Reopen Count'; - function annotate($query) { + function annotate($query, $name=false) { + $name = $name ?: static::$qname; return $query->annotate(array( - static::$qname => TicketThread::objects() + $name => TicketThread::objects() ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1))) ->filter(array('events__annulled' => 0, 'events__state' => 'reopened')) ->aggregate(array('count' => SqlAggregate::COUNT('events__id'))) @@ -1531,10 +1590,11 @@ extends QueueColumnAnnotation { static $qname = '_att_count'; static $desc = /* @trans */ 'Attachment Count'; - function annotate($query) { + function annotate($query, $name=false) { // TODO: Convert to Thread attachments + $name = $name ?: static::$qname; return $query->annotate(array( - static::$qname => TicketThread::objects() + $name => TicketThread::objects() ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1))) ->filter(array('entries__attachments__inline' => 0)) ->aggregate(array('count' => SqlAggregate::COUNT('entries__attachments__id'))) @@ -1561,9 +1621,10 @@ extends QueueColumnAnnotation { static $qname = '_collabs'; static $desc = /* @trans */ 'Collaborator Count'; - function annotate($query) { + function annotate($query, $name=false) { + $name = $name ?: static::$qname; return $query->annotate(array( - static::$qname => TicketThread::objects() + $name => TicketThread::objects() ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1))) ->aggregate(array('count' => SqlAggregate::COUNT('collaborators__id'))) )); @@ -1588,7 +1649,7 @@ extends QueueColumnAnnotation { static $icon = 'exclamation'; static $desc = /* @trans */ 'Overdue Icon'; - function annotate($query) { + function annotate($query, $name=false) { return $query->values('isoverdue'); } @@ -1607,7 +1668,7 @@ extends QueueColumnAnnotation { static $icon = 'phone'; static $desc = /* @trans */ 'Ticket Source'; - function annotate($query) { + function annotate($query, $name=false) { return $query->values('source'); } @@ -1622,7 +1683,7 @@ extends QueueColumnAnnotation { static $icon = "lock"; static $desc = /* @trans */ 'Locked'; - function annotate($query) { + function annotate($query, $name=false) { global $thisstaff; return $query @@ -1649,7 +1710,7 @@ extends QueueColumnAnnotation { static $icon = "user"; static $desc = /* @trans */ 'Assignee Avatar'; - function annotate($query) { + function annotate($query, $name=false) { return $query->values('staff_id', 'team_id'); } @@ -1688,7 +1749,7 @@ extends QueueColumnAnnotation { static $icon = "user"; static $desc = /* @trans */ 'User Avatar'; - function annotate($query) { + function annotate($query, $name=false) { return $query->values('user_id'); } @@ -1984,6 +2045,7 @@ extends VerySimpleModel { var $_annotations; var $_conditions; var $_queue; // Apparent queue if being inherited + var $_fields; function getId() { return $this->id; @@ -2022,6 +2084,25 @@ extends VerySimpleModel { $this->_queue = $queue; } + function getFields() { + if (!isset($this->_fields)) { + $root = ($q = $this->getQueue()) ? $q->getRoot() : 'Ticket'; + $fields = CustomQueue::getSearchableFields($root); + $primary = CustomQueue::getOrmPath($this->primary); + $secondary = CustomQueue::getOrmPath($this->secondary); + if (($F = $fields[$primary]) && (list(,$field) = $F)) + $this->_fields[$primary] = $field; + if (($F = $fields[$secondary]) && (list(,$field) = $F)) + $this->_fields[$secondary] = $field; + } + return $this->_fields; + } + + function getField($path=null) { + $fields = $this->getFields(); + return @$fields[$path ?: $this->primary]; + } + function getWidth() { return $this->width ?: 100; } @@ -2089,29 +2170,36 @@ extends VerySimpleModel { } function renderBasicValue($row) { - $root = ($q = $this->getQueue()) ? $q->getRoot() : 'Ticket'; - $fields = CustomQueue::getSearchableFields($root); + $fields = $this->getFields(); $primary = CustomQueue::getOrmPath($this->primary); $secondary = CustomQueue::getOrmPath($this->secondary); // Return a lazily ::display()ed value so that the value to be // rendered by the field could be changed or display()ed when // converted to a string. - if (($F = $fields[$primary]) - && (list(,$field) = $F) - && ($T = $field->from_query($row, $primary)) + && ($T = $F->from_query($row, $primary)) ) { - return new LazyDisplayWrapper($field, $T); + return new LazyDisplayWrapper($F, $T); } if (($F = $fields[$secondary]) - && (list(,$field) = $F) - && ($T = $field->from_query($row, $secondary)) + && ($T = $F->from_query($row, $secondary)) ) { - return new LazyDisplayWrapper($field, $T); + return new LazyDisplayWrapper($F, $T); } - return new LazyDisplayWrapper($field, ''); + return new LazyDisplayWrapper($F, ''); + } + + function from_query($row) { + if (!($f = $this->getField($this->primary))) + return ''; + + $val = $f->to_php($f->from_query($row, $this->primary)); + if (!is_string($val)) + $val = $f->display($val); + + return $val; } function applyTruncate($text, $row) { @@ -2155,14 +2243,12 @@ extends VerySimpleModel { function mangleQuery($query, $root=null) { // Basic data - $fields = CustomQueue::getSearchableFields($root ?: $this->getQueue()->getRoot()); - if ($primary = $fields[$this->primary]) { - list(,$field) = $primary; + $fields = $this->getFields(); + if ($field = $fields[$this->primary]) { $query = $this->addToQuery($query, $field, CustomQueue::getOrmPath($this->primary, $query)); } - if ($secondary = $fields[$this->secondary]) { - list(,$field) = $secondary; + if ($field = $fields[$this->secondary]) { $query = $this->addToQuery($query, $field, CustomQueue::getOrmPath($this->secondary, $query)); } diff --git a/include/class.search.php b/include/class.search.php index e4c6111b5fe313438577394dcbae5cebf271ec3a..c32916504b3d4200ccdde403e00ebe4656c4a9cb 100644 --- a/include/class.search.php +++ b/include/class.search.php @@ -1004,21 +1004,30 @@ class AdvancedSearchSelectionField extends ChoiceField { } class HelpTopicChoiceField extends AdvancedSearchSelectionField { + static $_topics; + function hasIdValue() { return true; } function getChoices($verbose=false) { - return Topic::getHelpTopics(false, Topic::DISPLAY_DISABLED); + if (!isset($this->_topics)) + $this->_topics = Topic::getHelpTopics(false, Topic::DISPLAY_DISABLED); + + return $this->_topics; } } require_once INCLUDE_DIR . 'class.dept.php'; class DepartmentChoiceField extends AdvancedSearchSelectionField { - var $_choices = null; + static $_depts; + var $_choices; function getChoices($verbose=false) { - return Dept::getDepartments(); + if (!isset($this->_depts)) + $this->_depts = Dept::getDepartments(); + + return $this->_depts; } function getQuickFilterChoices() { @@ -1241,13 +1250,23 @@ trait ZeroMeansUnset { class AgentSelectionField extends AdvancedSearchSelectionField { use ZeroMeansUnset; + static $_agents; + function getChoices($verbose=false) { - return array('M' => __('Me')) + Staff::getStaffMembers(); + if (!isset($this->_agents)) { + $this->_agents = array('M' => __('Me')) + + Staff::getStaffMembers(); + } + return $this->_agents; } function toString($value) { + $choices = $this->getChoices(); $selection = array(); + if (!is_array($value)) + $value = array($value => $value); + foreach ($value as $k => $v) if (isset($choices[$k])) $selection[] = $choices[$k]; @@ -1278,9 +1297,13 @@ class AgentSelectionField extends AdvancedSearchSelectionField { } class DepartmentManagerSelectionField extends AgentSelectionField { + static $_members; function getChoices($verbose=false) { - return Staff::getStaffMembers(); + if (isset($this->_members)) + $this->_members = Staff::getStaffMembers(); + + return $this->_members; } function getSearchQ($method, $value, $name=false) { @@ -1289,9 +1312,14 @@ class DepartmentManagerSelectionField extends AgentSelectionField { } class TeamSelectionField extends AdvancedSearchSelectionField { + static $_teams; function getChoices($verbose=false) { - return array('T' => __('One of my teams')) + Team::getTeams(); + if (!isset($this->_teams)) + $this->_teams = array('T' => __('One of my teams')) + + Team::getTeams(); + + return $this->_teams; } function getSearchQ($method, $value, $name=false) { @@ -1315,6 +1343,19 @@ class TeamSelectionField extends AdvancedSearchSelectionField { $reverse = $reverse ? '-' : ''; return $query->order_by("{$reverse}team__name"); } + + function toString($value) { + $choices = $this->getChoices(); + $selection = array(); + if (!is_array($value)) + $value = array($value => $value); + foreach ($value as $k => $v) + if (isset($choices[$k])) + $selection[] = $choices[$k]; + return $selection ? implode(',', $selection) : + parent::toString($value); + } + } class TicketStateChoiceField extends AdvancedSearchSelectionField { @@ -1395,6 +1436,7 @@ class OpenClosedTicketStatusList extends TicketStatusList { return $rv; } } + class TicketStatusChoiceField extends SelectionField { static $widget = 'ChoicesWidget'; @@ -1427,6 +1469,50 @@ class TicketStatusChoiceField extends SelectionField { } } +class TicketThreadCountField extends NumericField { + + function addToQuery($query, $name=false) { + return TicketThreadCount::addToQuery($query, $name); + } + + function from_query($row, $name=false) { + return TicketThreadCount::from_query($row, $name); + } +} + +class TicketReopenCountField extends NumericField { + + function addToQuery($query, $name=false) { + return TicketReopenCount::addToQuery($query, $name); + } + + function from_query($row, $name=false) { + return TicketReopenCount::from_query($row, $name); + } +} + +class ThreadAttachmentCountField extends NumericField { + + function addToQuery($query, $name=false) { + return ThreadAttachmentCount::addToQuery($query, $name); + } + + function from_query($row, $name=false) { + return ThreadAttachmentCount::from_query($row, $name); + } +} + +class ThreadCollaboratorCountField extends NumericField { + + function addToQuery($query, $name=false) { + return ThreadCollaboratorCount::addToQuery($query, $name); + } + + function from_query($row, $name=false) { + return ThreadCollaboratorCount::from_query($row, $name); + } +} + interface Searchable { // Fetch an array of [ orm__path => Field() ] pairs. The field label is // used when this list is rendered in a dropdown, and the field search diff --git a/include/class.thread.php b/include/class.thread.php index bb65c26f61e4e1048bb72ad14759cca063611a54..3d367a902f496b69e54d4bc5fc3fd710a4ef3e0a 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -1084,19 +1084,16 @@ implements TemplateVariable { $ids = array(); foreach ($files as $id => $info) { $F = array('inline' => is_array($info) && @$info['inline']); + $AF = null; - if (is_array($info) && isset($info['id'])) - $fileId = $info['id']; - elseif (is_numeric($id) && $id != 0) - $fileId = $id; - elseif ($info instanceof AttachmentFile) + if ($info instanceof AttachmentFile) $fileId = $info->getId(); elseif ($AF = AttachmentFile::create($info)) $fileId = $AF->getId(); elseif ($add_error) { $error = $info['error'] - ?: sprintf(_S('Unable to import attachment - %s'), - $id ?: $info['name']); + ?: sprintf(_S('Unable to save attachment - %s'), + $info['name'] ?: $info['id']); if (is_numeric($error) && isset($error_descriptions[$error])) { $error = sprintf(_S('Error #%1$d: %2$s'), $error, _S($error_descriptions[$error])); @@ -1157,11 +1154,13 @@ implements TemplateVariable { elseif (is_string($name)) { $filename = $name; } + if ($filename) { // This should be a noop since the ORM caches on PK $F = @$file['file'] ?: AttachmentFile::lookup($file['id']); // XXX: This is not Unicode safe - if ($F && 0 !== strcasecmp($F->name, $filename)) + // TODO: fix name lookup + if ($F && strcasecmp($F->name, $filename) !== 0) $att->name = $filename; } @@ -1578,7 +1577,7 @@ implements TemplateVariable { $attached_files = array(); foreach (array( // Web uploads and canned attachments - $vars['files'], $vars['cannedattachments'], + $vars['files'], // Emailed or API attachments $vars['attachments'], // Inline images (attached to the draft) diff --git a/include/class.ticket.php b/include/class.ticket.php index 550785e5c3ecc13fabf8f5799c19e678f1798678..0347004deffe2efb3a92ff50ea94d8434cf1a5d6 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -2114,27 +2114,39 @@ implements RestrictedAccess, Threadable, Searchable { )), 'created' => new DatetimeField(array( 'label' => __('Create Date'), - 'configuration' => array('fromdb' => true), + 'configuration' => array( + 'fromdb' => true, 'time' => true, + 'format' => 'y-MM-dd HH:mm:ss'), )), 'duedate' => new DatetimeField(array( 'label' => __('Due Date'), - 'configuration' => array('fromdb' => true), + 'configuration' => array( + 'fromdb' => true, 'time' => true, + 'format' => 'y-MM-dd HH:mm:ss'), )), 'est_duedate' => new DatetimeField(array( 'label' => __('SLA Due Date'), - 'configuration' => array('fromdb' => true), + 'configuration' => array( + 'fromdb' => true, 'time' => true, + 'format' => 'y-MM-dd HH:mm:ss'), )), 'reopened' => new DatetimeField(array( 'label' => __('Reopen Date'), - 'configuration' => array('fromdb' => true), + 'configuration' => array( + 'fromdb' => true, 'time' => true, + 'format' => 'y-MM-dd HH:mm:ss'), )), 'closed' => new DatetimeField(array( 'label' => __('Close Date'), - 'configuration' => array('fromdb' => true), + 'configuration' => array( + 'fromdb' => true, 'time' => true, + 'format' => 'y-MM-dd HH:mm:ss'), )), 'lastupdate' => new DatetimeField(array( 'label' => __('Last Update'), - 'configuration' => array('fromdb' => true), + 'configuration' => array( + 'fromdb' => true, 'time' => true, + 'format' => 'y-MM-dd HH:mm:ss'), )), 'assignee' => new AssigneeChoiceField(array( 'label' => __('Assignee'), @@ -2171,6 +2183,18 @@ implements RestrictedAccess, Threadable, Searchable { 'isassigned' => new AssignedField(array( 'label' => __('Assigned'), )), + 'thread_count' => new TicketThreadCountField(array( + 'label' => __('Thread Count'), + )), + 'attachment_count' => new ThreadAttachmentCountField(array( + 'label' => __('Attachment Count'), + )), + 'collaborator_count' => new ThreadCollaboratorCountField(array( + 'label' => __('Collaborator Count'), + )), + 'reopen_count' => new TicketReopenCountField(array( + 'label' => __('Reopen Count'), + )), 'ip_address' => new TextboxField(array( 'label' => __('IP Address'), 'configuration' => array('validator' => 'ip'), @@ -2837,9 +2861,9 @@ implements RestrictedAccess, Threadable, Searchable { return false; } $files = array(); - foreach ($canned->attachments->getAll() as $file) { - $files[] = $file->file_id; - $_SESSION[':cannedFiles'][$file->file_id] = 1; + foreach ($canned->attachments->getAll() as $att) { + $files[] = array('id' => $att->file_id, 'name' => $att->getName()); + $_SESSION[':cannedFiles'][$att->file_id] = $att->getName(); } if ($cfg->isRichTextEnabled()) @@ -2852,7 +2876,7 @@ implements RestrictedAccess, Threadable, Searchable { $info = array('msgId' => $message instanceof ThreadEntry ? $message->getId() : 0, 'poster' => __('SYSTEM (Canned Reply)'), 'response' => $response, - 'cannedattachments' => $files + 'files' => $files ); $errors = array(); if (!($response=$this->postReply($info, $errors, false, false))) @@ -4088,8 +4112,8 @@ implements RestrictedAccess, Threadable, Searchable { $vars['note'] = ThreadEntryBody::clean($vars['note']); $create_vars = $vars; $tform = TicketForm::objects()->one()->getForm($create_vars); - $create_vars['cannedattachments'] - = $tform->getField('message')->getWidget()->getAttachments()->getClean(); + $create_vars['files'] + = $tform->getField('message')->getWidget()->getAttachments()->getFiles(); if (!($ticket=self::create($create_vars, $errors, 'staff', false))) return false; diff --git a/open.php b/open.php index a507bc979515181f5d46eb035a8ccaf9ade92e59..ef1dde53285c8d406c3ba5406fcb3f52df151854 100644 --- a/open.php +++ b/open.php @@ -33,7 +33,7 @@ if ($_POST) { $messageField = $tform->getField('message'); $attachments = $messageField->getWidget()->getAttachments(); if (!$errors && $messageField->isAttachmentsEnabled()) - $vars['cannedattachments'] = $attachments->getClean(); + $vars['files'] = $attachments->getFiles(); // Drop the draft.. If there are validation errors, the content // submitted will be displayed back to the user diff --git a/scp/tasks.php b/scp/tasks.php index aa8fc88793d858957ede515d756eac951e094c36..5375261863e375f72e088b9958b80967299383b3 100644 --- a/scp/tasks.php +++ b/scp/tasks.php @@ -48,7 +48,7 @@ if($_POST && !$errors): switch(strtolower($_POST['a'])): case 'postnote': /* Post Internal Note */ $vars = $_POST; - $vars['cannedattachments'] = $note_attachments_form->getField('attachments')->getClean(); + $vars['files'] = $note_attachments_form->getField('attachments')->getFiles(); $wasOpen = ($task->isOpen()); if(($note=$task->postNote($vars, $errors, $thisstaff))) { @@ -76,7 +76,7 @@ if($_POST && !$errors): break; case 'postreply': /* Post an update */ $vars = $_POST; - $vars['cannedattachments'] = $reply_attachments_form->getField('attachments')->getClean(); + $vars['files'] = $reply_attachments_form->getField('attachments')->getFiles(); $wasOpen = ($task->isOpen()); if (($response=$task->postReply($vars, $errors))) { diff --git a/scp/tickets.php b/scp/tickets.php index e9c310f4fdd9e5cc2c136024a94e83ca1962a1ab..d100f38d7b1af8e1505301922754e5925bc264dc 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -149,7 +149,7 @@ if($_POST && !$errors): $errors['err'] = __('Action denied. Contact admin for access'); } else { $vars = $_POST; - $vars['cannedattachments'] = $response_form->getField('attachments')->getClean(); + $vars['files'] = $response_form->getField('attachments')->getFiles(); $vars['response'] = ThreadEntryBody::clean($vars['response']); if(!$vars['response']) $errors['response']=__('Response required'); @@ -211,7 +211,7 @@ if($_POST && !$errors): break; case 'postnote': /* Post Internal Note */ $vars = $_POST; - $vars['cannedattachments'] = $note_form->getField('attachments')->getClean(); + $vars['files'] = $note_form->getField('attachments')->getFiles(); $vars['note'] = ThreadEntryBody::clean($vars['note']); if ($cfg->isTicketLockEnabled()) { @@ -403,7 +403,7 @@ if($_POST && !$errors): if ($vars['uid'] && !($user=User::lookup($vars['uid']))) $vars['uid'] = 0; - $vars['cannedattachments'] = $response_form->getField('attachments')->getClean(); + $vars['files'] = $response_form->getField('attachments')->getFiles(); if(($ticket=Ticket::open($vars, $errors))) { $msg=__('Ticket created successfully'); diff --git a/tickets.php b/tickets.php index fa88e5e55775b4aeff32dda04841b35eed6fc178..560b16f3dc963008e05aa8c610607b82992fee95 100644 --- a/tickets.php +++ b/tickets.php @@ -84,7 +84,7 @@ if ($_POST && is_object($ticket) && $ticket->getId()) { 'poster' => (string) $thisclient->getName(), 'message' => $_POST['message'] ); - $vars['cannedattachments'] = $attachments->getClean(); + $vars['files'] = $attachments->getFiles(); if (isset($_POST['draft_id'])) $vars['draft_id'] = $_POST['draft_id'];