golden hour
/home/doctorbruno/public_html/taeionline.com/support/include
⬆️ Go Up
Upload
File/Folder
Size
Actions
.MANIFEST
194.97 KB
Del
OK
.htaccess
14 B
Del
OK
JSON.php
33.13 KB
Del
OK
PasswordHash.php
6.92 KB
Del
OK
Spyc.php
31.74 KB
Del
OK
UniversalClassLoader.php
8.61 KB
Del
OK
ajax.admin.php
7.29 KB
Del
OK
ajax.config.php
4.61 KB
Del
OK
ajax.content.php
9.76 KB
Del
OK
ajax.draft.php
13.03 KB
Del
OK
ajax.email.php
2.33 KB
Del
OK
ajax.export.php
1006 B
Del
OK
ajax.filter.php
874 B
Del
OK
ajax.forms.php
13.84 KB
Del
OK
ajax.i18n.php
5.1 KB
Del
OK
ajax.kbase.php
2.93 KB
Del
OK
ajax.note.php
2.05 KB
Del
OK
ajax.orgs.php
11.73 KB
Del
OK
ajax.plugins.php
2.15 KB
Del
OK
ajax.schedule.php
4.29 KB
Del
OK
ajax.search.php
12.72 KB
Del
OK
ajax.sequence.php
3.2 KB
Del
OK
ajax.staff.php
11.62 KB
Del
OK
ajax.tasks.php
33.87 KB
Del
OK
ajax.thread.php
8.81 KB
Del
OK
ajax.tickets.php
79.7 KB
Del
OK
ajax.tips.php
1.66 KB
Del
OK
ajax.upgrader.php
2.24 KB
Del
OK
ajax.users.php
18.33 KB
Del
OK
api.cron.php
1.06 KB
Del
OK
api.tickets.php
10.36 KB
Del
OK
class.2fa.php
7.76 KB
Del
OK
class.ajax.php
1.45 KB
Del
OK
class.api.php
15.31 KB
Del
OK
class.app.php
1.49 KB
Del
OK
class.attachment.php
6.95 KB
Del
OK
class.auth.php
50.73 KB
Del
OK
class.avatar.php
6.44 KB
Del
OK
class.banlist.php
2.58 KB
Del
OK
class.base32.php
4.07 KB
Del
OK
class.businesshours.php
7.08 KB
Del
OK
class.canned.php
9.05 KB
Del
OK
class.captcha.php
1.73 KB
Del
OK
class.category.php
11.14 KB
Del
OK
class.charset.php
3.41 KB
Del
OK
class.cli.php
9.48 KB
Del
OK
class.client.php
15.39 KB
Del
OK
class.collaborator.php
5.65 KB
Del
OK
class.company.php
2.65 KB
Del
OK
class.config.php
60.36 KB
Del
OK
class.controller.php
1.15 KB
Del
OK
class.cron.php
3.6 KB
Del
OK
class.crypto.php
18.92 KB
Del
OK
class.csrf.php
2.38 KB
Del
OK
class.dept.php
33.21 KB
Del
OK
class.dispatcher.php
7.12 KB
Del
OK
class.draft.php
6.23 KB
Del
OK
class.dynamic_forms.php
64.98 KB
Del
OK
class.email.php
49 KB
Del
OK
class.error.php
1.69 KB
Del
OK
class.export.php
32.92 KB
Del
OK
class.faq.php
15.05 KB
Del
OK
class.file.php
34.59 KB
Del
OK
class.filter.php
31.9 KB
Del
OK
class.filter_action.php
23.67 KB
Del
OK
class.format.php
44.3 KB
Del
OK
class.forms.php
195.58 KB
Del
OK
class.http.php
7.16 KB
Del
OK
class.i18n.php
24.28 KB
Del
OK
class.import.php
6.9 KB
Del
OK
class.json.php
2.71 KB
Del
OK
class.knowledgebase.php
5.8 KB
Del
OK
class.list.php
42 KB
Del
OK
class.lock.php
4.05 KB
Del
OK
class.log.php
1.55 KB
Del
OK
class.mail.php
35.76 KB
Del
OK
class.mailer.php
25.75 KB
Del
OK
class.mailfetch.php
9.72 KB
Del
OK
class.mailparse.php
30.99 KB
Del
OK
class.message.php
6.42 KB
Del
OK
class.migrater.php
5.2 KB
Del
OK
class.misc.php
7.5 KB
Del
OK
class.model.php
2.3 KB
Del
OK
class.nav.php
14.15 KB
Del
OK
class.note.php
2.39 KB
Del
OK
class.oauth2.php
4.04 KB
Del
OK
class.organization.php
22.45 KB
Del
OK
class.orm.php
120.9 KB
Del
OK
class.osticket.php
20.64 KB
Del
OK
class.ostsession.php
21.86 KB
Del
OK
class.page.php
10.59 KB
Del
OK
class.pagenate.php
5.53 KB
Del
OK
class.passwd.php
1.21 KB
Del
OK
class.pdf.php
3.83 KB
Del
OK
class.plugin.php
36.13 KB
Del
OK
class.priority.php
1.81 KB
Del
OK
class.queue.php
103.51 KB
Del
OK
class.report.php
11.76 KB
Del
OK
class.role.php
11.27 KB
Del
OK
class.schedule.php
46.28 KB
Del
OK
class.search.php
61.66 KB
Del
OK
class.sequence.php
7.27 KB
Del
OK
class.session.php
19.25 KB
Del
OK
class.setup.php
3.55 KB
Del
OK
class.signal.php
4.16 KB
Del
OK
class.sla.php
9.11 KB
Del
OK
class.staff.php
60.21 KB
Del
OK
class.task.php
56.21 KB
Del
OK
class.team.php
12.31 KB
Del
OK
class.template.php
23.45 KB
Del
OK
class.thread.php
108.96 KB
Del
OK
class.thread_actions.php
17.08 KB
Del
OK
class.ticket.php
167.39 KB
Del
OK
class.timezone.php
21.94 KB
Del
OK
class.topic.php
20.06 KB
Del
OK
class.translation.php
34.79 KB
Del
OK
class.upgrader.php
13.76 KB
Del
OK
class.user.php
43.4 KB
Del
OK
class.usersession.php
7.55 KB
Del
OK
class.util.php
10.07 KB
Del
OK
class.validator.php
12.94 KB
Del
OK
class.variable.php
11.93 KB
Del
OK
class.xml.php
3.23 KB
Del
OK
class.yaml.php
1.15 KB
Del
OK
cli
-
Del
OK
client
-
Del
OK
config
-
Del
OK
fpdf
-
Del
OK
htmLawed.php
53.53 KB
Del
OK
html2text.php
33.71 KB
Del
OK
i18n
-
Del
OK
index.php
37 B
Del
OK
laminas-mail
-
Del
OK
mpdf
-
Del
OK
mysqli.php
9.55 KB
Del
OK
ost-config.php
5.63 KB
Del
OK
ost-sampleconfig.php
6.24 KB
Del
OK
pear
-
Del
OK
plugins
-
Del
OK
staff
-
Del
OK
tnef_decoder.php
19.82 KB
Del
OK
upgrader
-
Del
OK
Edit: class.queue.php
<?php /********************************************************************* class.queue.php Custom (ticket) queues for osTicket Jared Hancock <jared@osticket.com> Peter Rotich <peter@osticket.com> Copyright (c) 2006-2015 osTicket http://www.osticket.com Released under the GNU General Public License WITHOUT ANY WARRANTY. See LICENSE.TXT for details. vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ class CustomQueue extends VerySimpleModel { static $meta = array( 'table' => QUEUE_TABLE, 'pk' => array('id'), 'ordering' => array('sort'), 'select_related' => array('parent', 'default_sort'), 'joins' => array( 'children' => array( 'reverse' => 'CustomQueue.parent', 'constrain' => ['children__id__gt' => 0], ), 'columns' => array( 'reverse' => 'QueueColumnGlue.queue', 'constrain' => array('staff_id' =>'QueueColumnGlue.staff_id'), 'broker' => 'QueueColumnListBroker', ), 'sorts' => array( 'reverse' => 'QueueSortGlue.queue', 'broker' => 'QueueSortListBroker', ), 'default_sort' => array( 'constraint' => array('sort_id' => 'QueueSort.id'), 'null' => true, ), 'exports' => array( 'reverse' => 'QueueExport.queue', ), 'parent' => array( 'constraint' => array( 'parent_id' => 'CustomQueue.id', ), 'null' => true, ), 'staff' => array( 'constraint' => array( 'staff_id' => 'Staff.staff_id', ) ), ) ); const FLAG_PUBLIC = 0x0001; // Shows up in e'eryone's saved searches const FLAG_QUEUE = 0x0002; // Shows up in queue navigation const FLAG_DISABLED = 0x0004; // NOT enabled const FLAG_INHERIT_CRITERIA = 0x0008; // Include criteria from parent const FLAG_INHERIT_COLUMNS = 0x0010; // Inherit column layout from parent const FLAG_INHERIT_SORTING = 0x0020; // Inherit advanced sorting from parent const FLAG_INHERIT_DEF_SORT = 0x0040; // Inherit default selected sort const FLAG_INHERIT_EXPORT = 0x0080; // Inherit export fields from parent const FLAG_INHERIT_EVERYTHING = 0x158; // Maskf or all INHERIT flags var $criteria; var $_conditions; static function queues() { return parent::objects()->filter(array( 'flags__hasbit' => static::FLAG_QUEUE )); } function __onload() { // Ensure valid state if ($this->hasFlag(self::FLAG_INHERIT_COLUMNS) && !$this->parent_id) $this->clearFlag(self::FLAG_INHERIT_COLUMNS); if ($this->hasFlag(self::FLAG_INHERIT_EXPORT) && !$this->parent_id) $this->clearFlag(self::FLAG_INHERIT_EXPORT); } function getId() { return $this->id; } function getName() { return $this->title; } function getHref() { // TODO: Get base page from getRoot(); $root = $this->getRoot(); return 'tickets.php?queue='.$this->getId(); } function getRoot() { switch ($this->root) { case 'T': default: return 'Ticket'; } } function getPath() { return $this->path ?: $this->buildPath(); } function criteriaRequired() { return true; } function getCriteria($include_parent=false) { if (!isset($this->criteria)) { $this->criteria = is_string($this->config) ? JsonDataParser::decode($this->config) : $this->config; // XXX: Drop this block in v1.12 // Auto-upgrade v1.10 saved-search criteria to new format // But support new style with `conditions` support $old = @$this->config[0] === '{'; if ($old && is_array($this->criteria) && !isset($this->criteria['conditions']) ) { // TODO: Upgrade old ORM path names // Parse criteria out of JSON if any. $this->criteria = self::isolateCriteria($this->criteria, $this->getRoot()); } } $criteria = $this->criteria ?: array(); // Support new style with `conditions` support if (isset($criteria['criteria'])) $criteria = $criteria['criteria']; if ($include_parent && $this->parent_id && $this->parent) { $criteria = array_merge($this->parent->getCriteria(true), $criteria); } return $criteria; } function describeCriteria($criteria=false){ global $account; if (!($all = $this->getSupportedMatches($this->getRoot()))) return ''; $items = array(); $criteria = $criteria ?: $this->getCriteria(true); foreach ($criteria ?: array() as $C) { list($path, $method, $value) = $C; if ($path === ':keywords') { $items[] = Format::htmlchars("\"{$value}\""); continue; } if (!isset($all[$path])) continue; list($label, $field) = $all[$path]; $items[] = $field->describeSearch($method, $value, $label); } return implode("\nAND ", $items); } /** * Fetch an AdvancedSearchForm instance for use in displaying or * configuring this search in the user interface. * * Parameters: * $search - <array> Request parameters ($_POST) used to update the * search beyond the current configuration of the search criteria * $searchables - search fields - default to current if not provided */ function getForm($source=null, $searchable=null) { $fields = array(); if (!isset($searchable)) { $fields = array( ':keywords' => new TextboxField(array( 'id' => 3001, 'configuration' => array( 'size' => 40, 'length' => 400, 'autofocus' => true, 'classes' => 'full-width headline', 'placeholder' => __('Keywords — Optional'), ), 'validators' => function($self, $v) { if (mb_str_wc($v) > 3) $self->addError(__('Search term cannot have more than 3 keywords')); }, )), ); $searchable = $this->getCurrentSearchFields($source); } foreach ($searchable ?: array() as $path => $field) $fields = array_merge($fields, static::getSearchField($field, $path)); $form = new AdvancedSearchForm($fields, $source); // Field selection validator if ($this->criteriaRequired()) { $form->addValidator(function($form) { if (!$form->getNumFieldsSelected()) $form->addError(__('No fields selected for searching')); }); } // Load state from current configuraiton if (!$source) { foreach ($this->getCriteria() as $I) { list($path, $method, $value) = $I; if ($path == ':keywords' && $method === null) { if ($F = $form->getField($path)) $F->value = $value; continue; } if (!($F = $form->getField("{$path}+search"))) continue; $F->value = true; if (!($F = $form->getField("{$path}+method"))) continue; $F->value = $method; if ($value && ($F = $form->getField("{$path}+{$method}"))) $F->value = $value; } } return $form; } /** * Fetch a bucket of fields for a custom search. The fields should be * added to a form before display. One searchable field may encompass 10 * or more actual fields because fields are expanded to support multiple * search methods along with the fields for each search method. This * method returns all the FormField instances for all the searchable * model fields currently in use. * * Parameters: * $source - <array> data from a request. $source['fields'] is expected * to contain a list extra fields by ORM path, of newly added * fields not yet saved in this object's getCriteria(). */ function getCurrentSearchFields($source=array(), $criteria=array()) { static $basic = array( 'Ticket' => array( 'status__id', 'status__state', 'dept_id', 'assignee', 'topic_id', 'created', 'est_duedate', 'duedate', ) ); $all = $this->getSupportedMatches(); $core = array(); // Include basic fields for new searches if (!isset($this->id)) foreach ($basic[$this->getRoot()] as $path) if (isset($all[$path])) $core[$path] = $all[$path]; // Add others from current configuration foreach ($criteria ?: $this->getCriteria() as $C) { list($path) = $C; if (isset($all[$path])) $core[$path] = $all[$path]; } if (isset($source['fields'])) foreach ($source['fields'] as $path) if (isset($all[$path])) $core[$path] = $all[$path]; return $core; } /** * Fetch all supported ORM fields filterable by this search object. */ function getSupportedFilters() { return static::getFilterableFields($this->getRoot()); } /** * Get get supplemental matches for public queues. * */ function getSupplementalMatches() { return array(); } function getSupplementalCriteria() { return array(); } /** * Fetch all supported ORM fields searchable by this search object. The * returned list represents searchable fields, keyed by the ORM path. * Use ::getCurrentSearchFields() or ::getSearchField() to retrieve for * use in the user interface. */ function getSupportedMatches() { return static::getSearchableFields($this->getRoot()); } /** * Trace ORM fields from a base object and retrieve a complete list of * fields which can be used in an ORM query based on the base object. * The base object must implement Searchable interface and extend from * VerySimpleModel. Then all joins from the object are also inspected, * and any which implement the Searchable interface are traversed and * automatically added to the list. The resulting list is cached based * on the $base class, so multiple calls for the same $base return * quickly. * * Parameters: * $base - Class, name of a class implementing Searchable * $recurse - int, number of levels to recurse, default is 2 * $cache - bool, cache results for future class for the same base * $customData - bool, include all custom data fields for all general * forms */ static function getSearchableFields($base, $recurse=2, $customData=true, $exclude=array() ) { static $cache = array(), $otherFields; // Early exit if already cached $fields = &$cache[$base]; if ($fields) return $fields; if (!in_array('Searchable', class_implements($base))) return array(); $fields = $fields ?: array(); foreach ($base::getSearchableFields() as $path=>$F) { if (is_array($F)) { list($label, $field) = $F; } else { $label = $F->getLocal('label'); $field = $F; } $fields[$path] = array($label, $field); } if ($customData && $base::supportsCustomData()) { if (!isset($otherFields)) { $otherFields = array(); $dfs = DynamicFormField::objects() ->filter(array('form__type' => 'G')) ->select_related('form'); foreach ($dfs as $field) { $otherFields[$field->getId()] = array($field->form, $field->getImpl()); } } foreach ($otherFields as $id=>$F) { list($form, $field) = $F; $label = sprintf("%s / %s", $form->getTitle(), $field->getLocal('label')); $fields["entries__answers!{$id}__value"] = array( $label, $field); } } if ($recurse) { $exclude[$base] = 1; foreach ($base::getMeta('joins') as $path=>$j) { $fc = $j['fkey'][0]; if (isset($exclude[$fc]) || (isset($j['list']) && $j['list'] === true) || (isset($j['searchable']) && $j['searchable'] === false)) continue; foreach (static::getSearchableFields($fc, $recurse-1, true, $exclude) as $path2=>$F) { list($label, $field) = $F; $fields["{$path}__{$path2}"] = array( sprintf("%s / %s", $fc, $label), $field); } } } // Sort the field listing by the (localized) label name if (function_exists('collator_create')) { $coll = Collator::create(Internationalization::getCurrentLanguage()); $keys = array_map(function($a) use ($coll) { return $coll->getSortKey($a[0]); #nolint }, $fields); } else { // Fall back to 8-bit string sorting $keys = array_map(function($a) { return $a[0]; }, $fields); } array_multisort($keys, $fields); return $fields; } /** * Fetch all searchable fileds, for the base object which support quick filters. */ function getFilterableFields($object) { $filters = array(); foreach (static::getSearchableFields($object) as $p => $f) { list($label, $field) = $f; if ($field && $field->supportsQuickFilter()) $filters[$p] = $f; } return $filters; } /** * Fetch the FormField instances used when for configuring a searchable * field in the user interface. This is the glue between a field * representing a searchable model field and the configuration of that * search in the user interface. * * Parameters: * $F - <array<string, FormField>> the label and the FormField instance * representing the configurable search * $name - <string> ORM path for the search */ static function getSearchField($F, $name) { list($label, $field) = $F; $pieces = array(); $pieces["{$name}+search"] = new BooleanField(array( 'id' => sprintf('%u', crc32($name)) >> 1, 'configuration' => array( 'desc' => $label ?: $field->getLocal('label'), 'classes' => 'inline', ), )); $methods = $field->getSearchMethods(); //remove future options for datetime fields that can't be in the future if (in_array($field->getLabel(), DateTimeField::getPastPresentLabels())) unset($methods['ndays'], $methods['future'], $methods['distfut']); $pieces["{$name}+method"] = new ChoiceField(array( 'choices' => $methods, 'default' => key($methods), 'visibility' => new VisibilityConstraint(new Q(array( "{$name}+search__eq" => true, )), VisibilityConstraint::HIDDEN), )); $offs = 0; foreach ($field->getSearchMethodWidgets() as $m=>$w) { if (!$w) continue; list($class, $args) = $w; $args['required'] = true; $args['__searchval__'] = true; $args['visibility'] = new VisibilityConstraint(new Q(array( "{$name}+method__eq" => $m, )), VisibilityConstraint::HIDDEN); $pieces["{$name}+{$m}"] = new $class($args); } return $pieces; } function getField($path) { $searchable = $this->getSupportedMatches(); return $searchable[$path]; } // Remove this and adjust advanced-search-criteria template to use the // getCriteria() list and getField() function getSearchFields($form=false) { $form = $form ?: $this->getForm(); $searchable = $this->getCurrentSearchFields(); $info = array(); foreach ($form->getFields() as $f) { if (substr($f->get('name'), -7) == '+search') { $name = substr($f->get('name'), 0, -7); $value = null; // Determine the search method and fetch the original field if (($M = $form->getField("{$name}+method")) && ($method = $M->getClean()) && (list(,$field) = $searchable[$name]) ) { // Request the field to generate a search Q for the // search method and given value if ($value = $form->getField("{$name}+{$method}")) $value = $value->getClean(); } $info[$name] = array( 'field' => $field, 'method' => $method, 'value' => $value, 'active' => $f->getClean(), ); } } return $info; } /** * Take the criteria from the SavedSearch fields setup and isolate the * field name being search, the method used for searhing, and the method- * specific data entered in the UI. */ static function isolateCriteria($criteria, $base='Ticket') { if (!is_array($criteria)) return null; $items = array(); $searchable = static::getSearchableFields($base); foreach ($criteria as $k=>$v) { if (substr($k, -7) === '+method') { list($name,) = explode('+', $k, 2); if (!isset($searchable[$name])) continue; // Require checkbox to be checked too if (!$criteria["{$name}+search"]) continue; // Lookup the field to search this condition list($label, $field) = $searchable[$name]; // Get the search method $method = is_array($v) ? key($v) : $v; // Not all search methods require a value $value = $criteria["{$name}+{$method}"]; $items[] = array($name, $method, $value); } } if (isset($criteria[':keywords']) && ($kw = $criteria[':keywords']) ) { $items[] = array(':keywords', null, $kw); } return $items; } function getConditions() { if (!isset($this->_conditions)) { $this->getCriteria(); $conds = array(); if (is_array($this->criteria) && isset($this->criteria['conditions']) ) { $conds = $this->criteria['conditions']; } foreach ($conds as $C) if ($T = QueueColumnCondition::fromJson($C)) $this->_conditions[] = $T; } return $this->_conditions; } static function getExportableFields() { $cdata = $fields = array(); foreach (TicketForm::getInstance()->getFields() as $f) { // Ignore core fields if (in_array($f->get('name'), array('priority'))) continue; // Ignore non-data fields elseif (!$f->hasData() || $f->isPresentationOnly()) continue; // Ignore disabled fields elseif (!$f->hasFlag(DynamicFormField::FLAG_ENABLED)) continue; $name = $f->get('name') ?: 'field_'.$f->get('id'); $key = 'cdata__'.$name; $cdata[$key] = $f->getLocal('label'); } // Standard export fields if none is provided. $fields = array( 'number' => __('Ticket Number'), 'created' => __('Date Created'), 'cdata__subject' => __('Subject'), 'user__name' => __('From'), 'user__emails__address' => __('From Email'), 'cdata__priority' => __('Priority'), 'dept_id' => __('Department'), 'topic_id' => __('Help Topic'), 'source' => __('Source'), 'status__id' =>__('Current Status'), 'lastupdate' => __('Last Updated'), 'est_duedate' => __('SLA Due Date'), 'sla_id' => __('SLA Plan'), 'duedate' => __('Due Date'), 'closed' => __('Closed Date'), 'isoverdue' => __('Overdue'), 'merged' => __('Merged'), 'linked' => __('Linked'), 'isanswered' => __('Answered'), 'staff_id' => __('Agent Assigned'), 'team_id' => __('Team Assigned'), 'thread_count' => __('Thread Count'), 'reopen_count' => __('Reopen Count'), 'attachment_count' => __('Attachment Count'), 'task_count' => __('Task Count'), ) + $cdata; return $fields; } function getExportFields($inherit=true) { $fields = array(); if ($inherit && $this->parent_id && $this->hasFlag(self::FLAG_INHERIT_EXPORT) && $this->parent ) { $fields = $this->parent->getExportFields(); } elseif (count($this->exports)) { foreach ($this->exports as $f) $fields[$f->path] = $f->getHeading(); } elseif ($this->isAQueue()) $fields = $this->getExportableFields(); if (!count($fields)) $fields = $this->getExportableFields(); 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(); } function getColumns($use_template=false) { if ($this->columns_id && ($q = CustomQueue::lookup($this->columns_id)) ) { // Use columns from cited queue return $q->getColumns(); } elseif ($this->parent_id && $this->hasFlag(self::FLAG_INHERIT_COLUMNS) && $this->parent ) { $columns = $this->parent->getColumns(); foreach ($columns as $c) $c->setQueue($this); return $columns; } elseif (count($this->columns)) { return $this->columns; } // Use the columns of the "Open" queue as a default template if ($use_template && ($template = CustomQueue::lookup(1))) return $template->getColumns(); // Last resort — use standard columns foreach (array( QueueColumn::placeholder(array( "id" => 1, "heading" => __("Number"), "primary" => 'number', "width" => 85, "bits" => QueueColumn::FLAG_SORTABLE, "filter" => "link:ticketP", "annotations" => '[{"c":"TicketSourceDecoration","p":"b"}, {"c":"MergedFlagDecoration","p":">"}]', "conditions" => '[{"crit":["isanswered","nset",null],"prop":{"font-weight":"bold"}}]', )), QueueColumn::placeholder(array( "id" => 2, "heading" => __("Created"), "primary" => 'created', "filter" => 'date:full', "truncate" =>'wrap', "width" => 120, "bits" => QueueColumn::FLAG_SORTABLE, )), QueueColumn::placeholder(array( "id" => 3, "heading" => __("Subject"), "primary" => 'cdata__subject', "width" => 250, "bits" => QueueColumn::FLAG_SORTABLE, "filter" => "link:ticket", "annotations" => '[{"c":"TicketThreadCount","p":">"},{"c":"ThreadAttachmentCount","p":"a"},{"c":"OverdueFlagDecoration","p":"<"}]', "conditions" => '[{"crit":["isanswered","nset",null],"prop":{"font-weight":"bold"}}]', "truncate" => 'ellipsis', )), QueueColumn::placeholder(array( "id" => 4, "heading" => __("From"), "primary" => 'user__name', "width" => 150, "bits" => QueueColumn::FLAG_SORTABLE, )), QueueColumn::placeholder(array( "id" => 5, "heading" => __("Priority"), "primary" => 'cdata__priority', "width" => 120, "bits" => QueueColumn::FLAG_SORTABLE, )), QueueColumn::placeholder(array( "id" => 8, "heading" => __("Assignee"), "primary" => 'assignee', "width" => 100, "bits" => QueueColumn::FLAG_SORTABLE, )), ) as $col) $this->addColumn($col); return $this->getColumns(); } function addColumn(QueueColumn $col) { $this->columns->add($col); $col->queue = $this; } function getColumn($id) { // TODO: Got to be easier way to search instrumented list. foreach ($this->getColumns() as $C) if ($C->getId() == $id) return $C; } function getSortOptions() { if ($this->inheritSorting() && $this->parent) { return $this->parent->getSortOptions(); } return $this->sorts; } function getDefaultSortId() { if ($this->isDefaultSortInherited() && $this->parent && ($sort_id = $this->parent->getDefaultSortId()) ) { return $sort_id; } return $this->sort_id; } function getDefaultSort() { if ($this->isDefaultSortInherited() && $this->parent && ($sort = $this->parent->getDefaultSort()) ) { return $sort; } return $this->default_sort; } function getStatus() { return $this->hasFlag(self::FLAG_DISABLED) ? __('Disabled') : __('Active'); } function getChildren() { return $this->children; } function getPublicChildren() { return $this->children->findAll(array( 'flags__hasbit' => self::FLAG_QUEUE )); } function getMyChildren() { global $thisstaff; if (!$thisstaff instanceof Staff) return array(); return $this->children->findAll(array( 'staff_id' => $thisstaff->getId(), Q::not(array( 'flags__hasbit' => self::FLAG_PUBLIC )) )); } function export(CsvExporter $exporter, $options=array()) { global $thisstaff; if (!$thisstaff || !($query=$this->getQuery()) || !($fields=$this->getExportFields())) return false; // Do not store results in memory $query->setOption(QuerySet::OPT_NOCACHE, true); // See if we have cached export preference if (isset($_SESSION['Export:Q'.$this->getId()])) { $opts = $_SESSION['Export:Q'.$this->getId()]; if (isset($opts['fields'])) { $fields = array_intersect_key($fields, array_flip($opts['fields'])); $exportableFields = CustomQueue::getExportableFields(); foreach ($opts['fields'] as $key => $name) { if (is_null($fields[$name]) && isset($exportableFields)) { $fields[$name] = $exportableFields[$name]; } } } } // 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()); // Get stashed sort or else get the default if (!($sort = $_SESSION['sort'][$this->getId()])) $sort = $this->getDefaultSort(); // Apply sort if ($sort instanceof QueueSort) $sort->applySort($query); elseif ($sort && isset($sort['queuesort'])) $sort['queuesort']->applySort($query, $sort['dir']); elseif ($sort && $sort['col'] && ($C=$this->getColumn($sort['col']))) $query = $C->applySort($query, $sort['dir']); else $query->order_by('-created'); // Distinct ticket_id to avoid duplicate results $query->distinct('ticket_id'); // 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; }; $exporter->write($headers); foreach ($query as $row) $exporter->write($render($row)); } /** * Add critiera to a query based on the constraints configured for this * queue. The criteria of the parent queue is also automatically added * if the queue is configured to inherit the criteria. */ function getBasicQuery() { if ($this->parent && $this->inheritCriteria()) { $query = $this->parent->getBasicQuery(); } else { $root = $this->getRoot(); $query = $root::objects(); } return $this->mangleQuerySet($query); } /** * Retrieve a QuerySet instance based on the type of object (root) of * this Q, which is automatically configured with the data and criteria * of the queue and its columns. * * Returns: * <QuerySet> instance */ function getQuery($form=false, $quick_filter=null) { // Start with basic criteria $query = $this->getBasicQuery($form); // Apply quick filter if (isset($quick_filter) && ($qf = $this->getQuickFilterField($quick_filter)) ) { $filter = @self::getOrmPath($this->getQuickFilter(), $query); $query = $qf->applyQuickFilter($query, $quick_filter, $filter); } // Apply column, annotations and conditions additions foreach ($this->getColumns() as $C) { $C->setQueue($this); $query = $C->mangleQuery($query, $this->getRoot()); } return $query; } function getQuickFilter() { if ($this->filter == '::' && $this->parent) { return $this->parent->getQuickFilter(); } return $this->filter; } function getQuickFilterField($value=null) { if ($this->filter == '::') { if ($this->parent) { return $this->parent->getQuickFilterField($value); } } elseif ($this->filter && ($fields = self::getSearchableFields($this->getRoot())) && (list(,$f) = @$fields[$this->filter]) && $f->supportsQuickFilter() ) { $f->value = $value; return $f; } } /** * Get a description of a field in a search. Expects an entry from the * array retrieved in ::getSearchFields() */ function describeField($info, $name=false) { $name = $name ?: $info['field']->get('label'); return $info['field']->describeSearch($info['method'], $info['value'], $name); } function mangleQuerySet(QuerySet $qs, $form=false) { $qs = clone $qs; $searchable = $this->getSupportedMatches(); // Figure out fields to search on foreach ($this->getCriteria() as $I) { list($name, $method, $value) = $I; // Consider keyword searching if ($name === ':keywords') { global $ost; $qs = $ost->searcher->find($value, $qs, false); } else { $nullable = ($method === 'nset') ? false : null; // XXX: Move getOrmPath to be more of a utility // Ensure the special join is created to support custom data joins $name = @static::getOrmPath($name, $qs, $nullable); if (preg_match('/__answers!\d+__/', $name)) { $qs->annotate(array($name => SqlAggregate::MAX($name))); } // Fetch a criteria Q for the query if (list(,$field) = $searchable[$name]) { // Add annotation if the field supports it. if (is_subclass_of($field, 'AnnotatedField')) $qs = $field->annotate($qs, $name); if ($q = $field->getSearchQ($method, $value, $name)) $qs = $qs->filter($q); } } } return $qs; } function applyDefaultSort($qs) { // Apply default sort if ($sorter = $this->getDefaultSort()) { $qs = $sorter->applySort($qs, false, $this->getRoot()); } return $qs; } function checkAccess(Staff $agent) { return $this->isPublic() || $this->checkOwnership($agent); } function checkOwnership(Staff $agent) { return ($agent->getId() == $this->staff_id && !$this->isAQueue()); } function isOwner(Staff $agent) { return $agent && $this->isPrivate() && $this->checkOwnership($agent); } function isSaved() { return true; } function ignoreVisibilityConstraints(Staff $agent) { // For searches (not queues), some staff can have a permission to // see all records return ($this->isASearch() && $this->isOwner($agent) && $agent->canSearchEverything()); } function inheritCriteria() { return $this->flags & self::FLAG_INHERIT_CRITERIA && $this->parent_id; } function inheritColumns() { return $this->hasFlag(self::FLAG_INHERIT_COLUMNS); } function useStandardColumns() { return ($this->hasFlag(self::FLAG_INHERIT_COLUMNS) || !count($this->columns)); } function inheritExport() { return ($this->hasFlag(self::FLAG_INHERIT_EXPORT) || !count($this->exports)); } function inheritSorting() { return $this->hasFlag(self::FLAG_INHERIT_SORTING); } function isDefaultSortInherited() { return $this->hasFlag(self::FLAG_INHERIT_DEF_SORT); } function buildPath() { if (!$this->id) return; $path = $this->parent ? $this->parent->buildPath() : ''; return rtrim($path, "/") . "/{$this->id}/"; } function getFullName() { $base = $this->getName(); if ($this->parent) $base = sprintf("%s / %s", $this->parent->getFullName(), $base); return $base; } function isASubQueue() { return $this->parent ? $this->parent->isASubQueue() : $this->isAQueue(); } function isAQueue() { return $this->hasFlag(self::FLAG_QUEUE); } function isASearch() { return !$this->isAQueue() || !$this->isSaved(); } function isPrivate() { return !$this->isAQueue() && $this->staff_id; } function isPublic() { return $this->hasFlag(self::FLAG_PUBLIC); } protected function hasFlag($flag) { return ($this->flags & $flag) !== 0; } protected function clearFlag($flag) { return $this->flags &= ~$flag; } protected function setFlag($flag, $value=true) { return $value ? $this->flags |= $flag : $this->clearFlag($flag); } function disable() { $this->setFlag(self::FLAG_DISABLED); } function enable() { $this->clearFlag(self::FLAG_DISABLED); } function getRoughCount() { if (($count = $this->getRoughCountAPC()) !== false) return $count; $query = Ticket::objects(); $Q = $this->getBasicQuery(); $expr = SqlCase::N()->when(new SqlExpr(new Q($Q->constraints)), new SqlField('ticket_id')); $query = $query->aggregate(array( "ticket_count" => SqlAggregate::COUNT($expr) )); $row = $query->values()->one(); return $row['ticket_count']; } function getRoughCountAPC() { if (!function_exists('apcu_store')) return false; $key = "rough.counts.".SECRET_SALT; $cached = false; $counts = apcu_fetch($key, $cached); if ($cached === true && isset($counts["q{$this->id}"])) return $counts["q{$this->id}"]; // Fetch rough counts of all queues. That is, fetch a total of the // counts based on the queue criteria alone. Do no consider agent // access. This should be fast and "rought" $queues = static::objects() ->filter(['flags__hasbit' => CustomQueue::FLAG_PUBLIC]) ->exclude(['flags__hasbit' => CustomQueue::FLAG_DISABLED]); $query = Ticket::objects(); $prefix = ""; foreach ($queues as $queue) { $Q = $queue->getBasicQuery(); $expr = SqlCase::N()->when(new SqlExpr(new Q($Q->constraints)), new SqlField('ticket_id')); $query = $query->aggregate(array( "q{$queue->id}" => SqlAggregate::COUNT($expr) )); } $counts = $query->values()->one(); apcu_store($key, $counts, 900); return @$counts["q{$this->id}"]; } function updateExports($fields, $save=true) { if (!$fields) return false; $order = array_keys($fields); $new = $fields; foreach ($this->exports as $f) { $heading = $f->getHeading(); $key = $f->getPath(); if (!isset($fields[$key])) { $this->exports->remove($f); continue; } $f->set('heading', isset($fields[$key]['heading']) ? $fields[$key]['heading'] : $heading); $f->set('sort', array_search($key, $order)+1); unset($new[$key]); } $exportableFields = CustomQueue::getExportableFields(); foreach ($new as $k => $field) { if (isset($exportableFields[$k])) $heading = $exportableFields[$k]; elseif (is_array($field)) $heading = $field['heading']; else $heading = $field; $f = QueueExport::create(array( 'path' => $k, 'heading' => $heading, 'sort' => array_search($k, $order)+1)); $this->exports->add($f); } $this->exports->sort(function($f) { return $f->sort; }); if (!count($this->exports) && $this->parent) $this->hasFlag(self::FLAG_INHERIT_EXPORT); if ($save) $this->exports->saveAll(); return true; } function update($vars, &$errors=array()) { // Set basic search information if (!$vars['queue-name']) $errors['queue-name'] = __('A title is required'); elseif (($q=CustomQueue::lookup(array( 'title' => Format::htmlchars($vars['queue-name']), 'parent_id' => $vars['parent_id'] ?: 0, 'staff_id' => $this->staff_id))) && $q->getId() != $this->id ) $errors['queue-name'] = __('Saved queue with same name exists'); $this->title = Format::htmlchars($vars['queue-name']); $this->parent_id = @$vars['parent_id'] ?: 0; if ($this->parent_id && !$this->parent) $errors['parent_id'] = __('Select a valid queue'); // Try to avoid infinite recursion determining ancestry if ($this->parent_id && isset($this->id)) { $P = $this; while ($P = $P->parent) if ($P->parent_id == $this->id) $errors['parent_id'] = __('Cannot be a descendent of itself'); } // Configure quick filter options $this->filter = $vars['filter']; if ($vars['sort_id']) { if ($vars['filter'] === '::') { if (!$this->parent) $errors['filter'] = __('No parent selected'); } elseif ($vars['filter'] && !array_key_exists($vars['filter'], static::getSearchableFields($this->getRoot())) ) { $errors['filter'] = __('Select an item from the list'); } } // Set basic queue information $this->path = $this->buildPath(); $this->setFlag(self::FLAG_INHERIT_CRITERIA, $this->parent_id); $this->setFlag(self::FLAG_INHERIT_COLUMNS, $this->parent_id > 0 && isset($vars['inherit-columns'])); $this->setFlag(self::FLAG_INHERIT_EXPORT, $this->parent_id > 0 && isset($vars['inherit-exports'])); $this->setFlag(self::FLAG_INHERIT_SORTING, $this->parent_id > 0 && isset($vars['inherit-sorting'])); // Saved Search - Use standard columns if ($this instanceof SavedSearch && isset($vars['inherit-columns'])) $this->setFlag(self::FLAG_INHERIT_COLUMNS); // Update queue columns (but without save) if (!isset($vars['columns']) && $this->parent) { // No columns -- imply column inheritance $this->setFlag(self::FLAG_INHERIT_COLUMNS); } if ($this->getId() && isset($vars['columns']) && !$this->hasFlag(self::FLAG_INHERIT_COLUMNS)) { if ($this->columns->updateColumns($vars['columns'], $errors, array( 'queue_id' => $this->getId(), 'staff_id' => $this->staff_id))) $this->columns->reset(); } // Update export fields for the queue if (isset($vars['exports']) && !$this->hasFlag(self::FLAG_INHERIT_EXPORT)) { $this->updateExports($vars['exports'], false); } if (!count($this->exports) && $this->parent) $this->hasFlag(self::FLAG_INHERIT_EXPORT); // Update advanced sorting options for the queue if (isset($vars['sorts']) && !$this->hasFlag(self::FLAG_INHERIT_SORTING)) { $new = $order = $vars['sorts']; foreach ($this->sorts as $sort) { $key = $sort->sort_id; $idx = array_search($key, $vars['sorts']); if (false === $idx) { $this->sorts->remove($sort); } else { $sort->set('sort', $idx); unset($new[$idx]); } } // Add new columns foreach ($new as $id) { if (!$sort = QueueSort::lookup($id)) continue; $glue = new QueueSortGlue(array( 'sort_id' => $id, 'queue' => $this, 'sort' => array_search($id, $order), )); $this->sorts->add($sort, $glue); } // Re-sort the in-memory columns array $this->sorts->sort(function($c) { return $c->sort; }); } if (!count($this->sorts) && $this->parent) { // No sorting -- imply sorting inheritance $this->setFlag(self::FLAG_INHERIT_SORTING); } // Configure default sorting $this->setFlag(self::FLAG_INHERIT_DEF_SORT, $this->parent && $vars['sort_id'] === '::'); if ($vars['sort_id']) { if ($vars['sort_id'] === '::') { if (!$this->parent) $errors['sort_id'] = __('No parent selected'); else $this->sort_id = 0; } elseif ($qs = QueueSort::lookup($vars['sort_id'])) { $this->sort_id = $vars['sort_id']; } else { $errors['sort_id'] = __('Select an item from the list'); } } else $this->sort_id = 0; list($this->_conditions, $conditions) = QueueColumn::getConditionsFromPost($vars, $this->id, $this->getRoot()); // TODO: Move this to SavedSearch::update() and adjust // AjaxSearch::_saveSearch() $form = $form ?: $this->getForm($vars); if (!$vars) { $errors['criteria'] = __('No criteria specified'); } elseif (!$form->isValid()) { $errors['criteria'] = __('Validation errors exist on criteria'); } else { $this->criteria = static::isolateCriteria($form->getClean(), $this->getRoot()); $this->config = JsonDataEncoder::encode([ 'criteria' => $this->criteria, 'conditions' => $conditions, ]); // Clear currently set criteria.and conditions. $this->criteria = $this->_conditions = null; } return 0 === count($errors); } function psave() { return parent::save(); } function save($refetch=false) { $nopath = !isset($this->path); $path_changed = isset($this->dirty['parent_id']); if ($this->dirty) $this->updated = SqlFunction::NOW(); $clearCounts = ($this->dirty || $this->__new__); if (!($rv = parent::save($refetch || $this->dirty))) return $rv; if ($nopath) { $this->path = $this->buildPath(); $this->save(); } if ($path_changed) { $this->children->reset(); $move_children = function($q) use (&$move_children) { foreach ($q->children as $qq) { $qq->path = $qq->buildPath(); $qq->save(); $move_children($qq); } }; $move_children($this); } // Refetch the queue counts if ($clearCounts) SavedQueue::clearCounts(); return $this->columns->saveAll() && $this->exports->saveAll() && $this->sorts->saveAll(); } /** * Fetch a tree-organized listing of the queues. Each queue is listed in * the tree exactly once, and every visible queue is represented. The * returned structure is an array where the items are two-item arrays * where the first item is a CustomQueue object an the second is a list * of the children using the same pattern (two-item arrays of a CustomQueue * and its children). Visually: * * [ [ $queue, [ [ $child, [] ], [ $child, [] ] ], [ $queue, ... ] ] * * Parameters: * $staff - <Staff> staff object which should be used to determine * visible queues. * $pid - <int> parent_id of root queue. Default is zero (top-level) */ static function getHierarchicalQueues(Staff $staff, $pid=0, $primary=true) { $query = static::objects() ->annotate(array('_sort' => SqlCase::N() ->when(array('sort' => 0), 999) ->otherwise(new SqlField('sort')))) ->filter(Q::any(array( 'flags__hasbit' => self::FLAG_PUBLIC, 'flags__hasbit' => static::FLAG_QUEUE, 'staff_id' => $staff->getId(), ))) ->exclude(['flags__hasbit' => self::FLAG_DISABLED]) ->order_by('parent_id', '_sort', 'title'); $all = $query->asArray(); // Find all the queues with a given parent $for_parent = function($pid) use ($primary, $all, &$for_parent) { $results = []; foreach (new \ArrayIterator($all) as $q) { if ($q->parent_id != $pid) continue; if ($pid == 0 && ( ($primary && !$q->isAQueue()) || (!$primary && $q->isAQueue()))) continue; $results[] = [ $q, $for_parent($q->getId()) ]; } return $results; }; return $for_parent($pid); } static function getOrmPath($name, $query=null, $nullable=null) { // Special case for custom data `__answers!id__value`. Only add the // join and constraint on the query the first pass, when the query // being mangled is received. $path = array(); if ($query && preg_match('/^(.+?)__(answers!(\d+))/', $name, $path)) { // Add a join to the model of the queryset where the custom data // is forked from — duplicate the 'answers' join and add the // constraint to the query based on the field_id // $path[1] - part before the answers (user__org__entries) // $path[2] - answers!xx join part // $path[3] - the `xx` part of the answers!xx join component $root = $query->model; $meta = $root::getMeta()->getByPath($path[1]); $joins = $meta['joins']; // If the method is 'nset' (ie. IS NULL) we want to force normal // JOIN instead of LEFT JOIN to get proper results if (isset($nullable)) $joins['answers']['null'] = $nullable; if (!isset($joins[$path[2]])) { $meta->addJoin($path[2], $joins['answers']); } // Ensure that the query join through answers!xx is only for the // records which match field_id=xx $query->constrain(array("{$path[1]}__{$path[2]}" => array("{$path[1]}__{$path[2]}__field_id" => (int) $path[3]) )); // Leave $name unchanged } return $name; } static function create($vars=false) { $queue = new static($vars); $queue->created = SqlFunction::NOW(); if (!isset($vars['flags'])) { $queue->setFlag(self::FLAG_PUBLIC); $queue->setFlag(self::FLAG_QUEUE); } return $queue; } static function __create($vars) { $q = static::create($vars); $q->psave(); foreach ($vars['columns'] ?: array() as $info) { $glue = new QueueColumnGlue($info); $glue->queue_id = $q->getId(); $glue->save(); } if (isset($vars['sorts'])) { foreach ($vars['sorts'] as $info) { $glue = new QueueSortGlue($info); $glue->queue_id = $q->getId(); $glue->save(); } } return $q; } } abstract class QueueColumnAnnotation { static $icon = false; static $desc = ''; var $config; function __construct($config) { $this->config = $config; } static function fromJson($config) { $class = $config['c']; if (class_exists($class)) return new $class($config); } static function getDescription() { return __(static::$desc); } static function getIcon() { return static::$icon; } static function getPositions() { return array( "<" => __('Start'), "b" => __('Before'), "a" => __('After'), ">" => __('End'), ); } function decorate($text, $dec) { static $positions = array( '<' => '<span class="pull-left">%2$s</span>%1$s', '>' => '<span class="pull-right">%2$s</span>%1$s', 'a' => '%1$s%2$s', 'b' => '%2$s%1$s', ); $pos = $this->getPosition(); if (!isset($positions[$pos])) return $text; return sprintf($positions[$pos], $text, $dec); } // Render the annotation with the database record $row. $text is the // text of the cell before annotations were applied. function render($row, $cell) { if ($decoration = $this->getDecoration($row, $cell)) return $this->decorate($cell, $decoration); return $cell; } // Add the annotation to a QuerySet abstract static 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 abstract function getDecoration($row, $text); function getPosition() { return strtolower($this->config['p']) ?: 'a'; } function getClassName() { return @$this->config['c'] ?: get_class($this); } static function getAnnotations($root) { // Ticket annotations static $annotations; if (!isset($annotations[$root])) { foreach (get_declared_classes() as $class) if (is_subclass_of($class, get_called_class())) $annotations[$root][] = $class; } return $annotations[$root]; } /** * Estimate the width of the rendered annotation in pixels */ function getWidth($row) { return $this->isVisible($row) ? 25 : 0; } 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 extends QueueColumnAnnotation { static $icon = 'comments-alt'; static $qname = '_thread_count'; static $desc = /* @trans */ 'Thread Count'; static function annotate($query, $name=false) { $name = $name ?: static::$qname; return $query->annotate(array( $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'))) )); } function getDecoration($row, $text) { $threadcount = $row[static::$qname]; if ($threadcount > 1) { return sprintf( '<small class="faded-more"><i class="icon-comments-alt"></i> %s</small>', $threadcount ); } } function isVisible($row) { return $row[static::$qname] > 1; } } class TicketReopenCount extends QueueColumnAnnotation { static $icon = 'folder-open-alt'; static $qname = '_reopen_count'; static $desc = /* @trans */ 'Reopen Count'; static function annotate($query, $name=false) { $name = $name ?: static::$qname; return $query->annotate(array( $name => TicketThread::objects() ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1))) ->filter(array('events__annulled' => 0, 'events__event_id' => Event::getIdByName('reopened'))) ->aggregate(array('count' => SqlAggregate::COUNT('events__id'))) )); } function getDecoration($row, $text) { $reopencount = $row[static::$qname]; if ($reopencount) { return sprintf( ' <small class="faded-more"><i class="icon-%s"></i> %s</small>', static::$icon, $reopencount > 1 ? $reopencount : '' ); } } function isVisible($row) { return $row[static::$qname]; } } class ThreadAttachmentCount extends QueueColumnAnnotation { static $icon = 'paperclip'; static $qname = '_att_count'; static $desc = /* @trans */ 'Attachment Count'; static function annotate($query, $name=false) { // TODO: Convert to Thread attachments $name = $name ?: static::$qname; return $query->annotate(array( $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'))) )); } function getDecoration($row, $text) { $count = $row[static::$qname]; if ($count) { return sprintf( '<i class="small icon-paperclip icon-flip-horizontal" data-toggle="tooltip" title="%s"></i>', $count); } } function isVisible($row) { return $row[static::$qname] > 0; } } class TicketTasksCount extends QueueColumnAnnotation { static $icon = 'list-ol'; static $qname = '_task_count'; static $desc = /* @trans */ 'Tasks Count'; static function annotate($query, $name=false) { $name = $name ?: static::$qname; return $query->annotate(array( $name => Task::objects() ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1))) ->aggregate(array('count' => SqlAggregate::COUNT('id'))) )); } function getDecoration($row, $text) { $count = $row[static::$qname]; if ($count) { return sprintf( '<small class="faded-more"><i class="icon-%s"></i> %s</small>', static::$icon, $count); } } function isVisible($row) { return $row[static::$qname]; } } class ThreadCollaboratorCount extends QueueColumnAnnotation { static $icon = 'group'; static $qname = '_collabs'; static $desc = /* @trans */ 'Collaborator Count'; static function annotate($query, $name=false) { $name = $name ?: static::$qname; return $query->annotate(array( $name => TicketThread::objects() ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1))) ->aggregate(array('count' => SqlAggregate::COUNT('collaborators__id'))) )); } function getDecoration($row, $text) { $count = $row[static::$qname]; if ($count) { return sprintf( '<span class="pull-right faded-more" data-toggle="tooltip" title="%d"><i class="icon-group"></i></span>', $count); } } function isVisible($row) { return $row[static::$qname] > 0; } } class OverdueFlagDecoration extends QueueColumnAnnotation { static $icon = 'exclamation'; static $desc = /* @trans */ 'Overdue Icon'; static function annotate($query, $name=false) { return $query->values('isoverdue'); } function getDecoration($row, $text) { if ($row['isoverdue']) return '<span class="Icon overdueTicket"></span>'; } function isVisible($row) { return $row['isoverdue']; } } class MergedFlagDecoration extends QueueColumnAnnotation { static $icon = 'code-fork'; static $desc = /* @trans */ 'Merged Icon'; static function annotate($query, $name=false) { return $query->values('ticket_pid', 'flags'); } function getDecoration($row, $text) { $flags = $row['flags']; $combine = ($flags & Ticket::FLAG_COMBINE_THREADS) != 0; $separate = ($flags & Ticket::FLAG_SEPARATE_THREADS) != 0; $linked = ($flags & Ticket::FLAG_LINKED) != 0; if ($combine || $separate) { return sprintf('<a data-placement="bottom" data-toggle="tooltip" title="%s" <i class="icon-code-fork"></i></a>', $combine ? __('Combine') : __('Separate')); } elseif ($linked) return '<i class="icon-link"></i>'; } function isVisible($row) { return $row['ticket_pid']; } } class LinkedFlagDecoration extends QueueColumnAnnotation { static $icon = 'link'; static $desc = /* @trans */ 'Linked Icon'; static function annotate($query, $name=false) { return $query->values('ticket_pid', 'flags'); } function getDecoration($row, $text) { $flags = $row['flags']; $linked = ($flags & Ticket::FLAG_LINKED) != 0; if ($linked) return '<i class="icon-link"></i>'; } function isVisible($row) { return $row['ticket_pid']; } } class TicketSourceDecoration extends QueueColumnAnnotation { static $icon = 'phone'; static $desc = /* @trans */ 'Ticket Source'; static function annotate($query, $name=false) { return $query->values('source'); } function getDecoration($row, $text) { return sprintf('<span class="Icon %sTicket"></span>', strtolower($row['source'])); } } class LockDecoration extends QueueColumnAnnotation { static $icon = "lock"; static $desc = /* @trans */ 'Locked'; static function annotate($query, $name=false) { global $thisstaff; return $query ->annotate(array( '_locked' => new SqlExpr(new Q(array( 'lock__expire__gt' => SqlFunction::NOW(), Q::not(array('lock__staff_id' => $thisstaff->getId())), ))) )); } function getDecoration($row, $text) { if ($row['_locked']) return sprintf('<span class="Icon lockedTicket"></span>'); } function isVisible($row) { return $row['_locked']; } } class AssigneeAvatarDecoration extends QueueColumnAnnotation { static $icon = "user"; static $desc = /* @trans */ 'Assignee Avatar'; static function annotate($query, $name=false) { return $query->values('staff_id', 'team_id'); } function getDecoration($row, $text) { if ($row['staff_id'] && ($staff = Staff::lookup($row['staff_id']))) return sprintf('<span class="avatar">%s</span>', $staff->getAvatar(16)); elseif ($row['team_id'] && ($team = Team::lookup($row['team_id']))) { $avatars = []; foreach ($team->getMembers() as $T) $avatars[] = $T->getAvatar(16); return sprintf('<span class="avatar group %s">%s</span>', count($avatars), implode('', $avatars)); } } function isVisible($row) { return $row['staff_id'] + $row['team_id'] > 0; } function getWidth($row) { if (!$this->isVisible($row)) return 0; // If assigned to a team with no members, return 0 width $width = 10; if ($row['team_id'] && ($team = Team::lookup($row['team_id']))) $width += (count($team->getMembers()) - 1) * 10; return $width ? $width + 10 : $width; } } class UserAvatarDecoration extends QueueColumnAnnotation { static $icon = "user"; static $desc = /* @trans */ 'User Avatar'; static function annotate($query, $name=false) { return $query->values('user_id'); } function getDecoration($row, $text) { if ($row['user_id'] && ($user = User::lookup($row['user_id']))) return sprintf('<span class="avatar">%s</span>', $user->getAvatar(16)); } function isVisible($row) { return $row['user_id'] > 0; } } class DataSourceField extends ChoiceField { function getChoices($verbose=false, $options=array()) { $config = $this->getConfiguration(); $root = $config['root']; $fields = array(); foreach (CustomQueue::getSearchableFields($root) as $path=>$f) { list($label,) = $f; $fields[$path] = $label; } return $fields; } } class QueueColumnCondition { var $config; var $queue; var $properties = array(); static $uid = 1; function __construct($config, $queue=null) { $this->config = $config; $this->queue = $queue; if (is_array($config['prop'])) $this->properties = $config['prop']; } function getProperties() { return $this->properties; } // Add the annotation to a QuerySet function annotate($query) { if (!($Q = $this->getSearchQ($query))) return $query; // Add an annotation to the query return $query->annotate(array( $this->getAnnotationName() => new SqlExpr(array($Q)) )); } function getField($name=null) { // FIXME #$root = $this->getColumn()->getRoot(); $root = 'Ticket'; $searchable = CustomQueue::getSearchableFields($root); if (!isset($name)) list($name) = $this->config['crit']; // Lookup the field to search this condition if (isset($searchable[$name])) { return $searchable[$name]; } } function getFieldName() { list($name) = $this->config['crit']; return $name; } function getCriteria() { return $this->config['crit']; } function getSearchQ($query) { list($name, $method, $value) = $this->config['crit']; // XXX: Move getOrmPath to be more of a utility // Ensure the special join is created to support custom data joins $name = @CustomQueue::getOrmPath($name, $query); $name2 = null; if (preg_match('/__answers!\d+__/', $name)) { // Ensure that only one record is returned from the join through // the entry and answers joins $name2 = $this->getAnnotationName().'2'; $query->annotate(array($name2 => SqlAggregate::MAX($name))); } // Fetch a criteria Q for the query if (list(,$field) = $this->getField($name)) return $field->getSearchQ($method, $value, $name2 ?: $name); } /** * Take the criteria from the SavedSearch fields setup and isolate the * field name being search, the method used for searhing, and the method- * specific data entered in the UI. */ static function isolateCriteria($criteria, $base='Ticket') { $searchable = CustomQueue::getSearchableFields($base); foreach ($criteria as $k=>$v) { if (substr($k, -7) === '+method') { list($name,) = explode('+', $k, 2); if (!isset($searchable[$name])) continue; // Lookup the field to search this condition list($label, $field) = $searchable[$name]; // Get the search method and value $method = $v; // Not all search methods require a value $value = $criteria["{$name}+{$method}"]; return array($name, $method, $value); } } } function render($row, $text, &$styles=array()) { if ($V = $row[$this->getAnnotationName()]) { foreach ($this->getProperties() as $css=>$value) { $field = QueueColumnConditionProperty::getField($css); $field->value = $value; $V = $field->getClean(); if (is_array($V)) $V = current($V); $styles[$css] = $V; } } return $text; } function getAnnotationName() { // This should be predictable based on the criteria so that the // query can deduplicate the same annotations used in different // conditions if (!isset($this->annotation_name)) { $this->annotation_name = $this->getShortHash(); } return $this->annotation_name; } function __toString() { list($name, $method, $value) = $this->config['crit']; if (is_array($value)) $value = implode('+', $value); return "{$name} {$method} {$value}"; } function getHash($binary=false) { return sha1($this->__toString(), $binary); } function getShortHash() { return substr(base64_encode($this->getHash(true)), 0, 7); } static function getUid() { return static::$uid++; } static function fromJson($config, $queue=null) { if (is_string($config)) $config = JsonDataParser::decode($config); if (!is_array($config)) throw new BadMethodCallException('$config must be string or array'); return new static($config, $queue); } } class QueueColumnConditionProperty extends ChoiceField { static $properties = array( 'background-color' => 'ColorChoiceField', 'color' => 'ColorChoiceField', 'font-family' => array( 'monospace', 'serif', 'sans-serif', 'cursive', 'fantasy', ), 'font-size' => array( 'small', 'medium', 'large', 'smaller', 'larger', ), 'font-style' => array( 'normal', 'italic', 'oblique', ), 'font-weight' => array( 'lighter', 'normal', 'bold', 'bolder', ), 'text-decoration' => array( 'none', 'underline', ), 'text-transform' => array( 'uppercase', 'lowercase', 'captalize', ), ); function __construct($property) { $this->property = $property; } static function getProperties() { return array_keys(static::$properties); } static function getField($prop) { $choices = static::$properties[$prop]; if (!isset($choices)) return null; if (is_array($choices)) return new ChoiceField(array( 'name' => $prop, 'choices' => array_combine($choices, $choices), )); elseif (class_exists($choices)) return new $choices(array('name' => $prop)); } function getChoices($verbose=false, $options=array()) { if (isset($this->property)) return static::$properties[$this->property]; $keys = array_keys(static::$properties); return array_combine($keys, $keys); } } class LazyDisplayWrapper { function __construct($field, $value) { $this->field = $field; $this->value = $value; $this->safe = false; } /** * Allow a filter to change the value of this to a "safe" value which * will not be automatically encoded with htmlchars() */ function changeTo($what, $safe=false) { $this->field = null; $this->value = $what; $this->safe = $safe; } function __toString() { return $this->display(); } function display(&$styles=array()) { if (isset($this->field)) return $this->field->display( $this->field->to_php($this->value), $styles); if ($this->safe) return $this->value; return Format::htmlchars($this->value); } } /** * A column of a custom queue. Columns have many customizable features * including: * * * Data Source (primary and secondary) * * Heading * * Link (to an object like the ticket) * * Size and truncate settings * * annotations (like counts and flags) * * Conditions (which change the formatting like bold text) * * Columns are stored in a separate table from the queue itself, but other * breakout items for the annotations and conditions, for instance, are stored * as JSON text in the QueueColumn model. */ class QueueColumn extends VerySimpleModel { static $meta = array( 'table' => COLUMN_TABLE, 'pk' => array('id'), 'ordering' => array('name'), ); const FLAG_SORTABLE = 0x0001; var $_annotations; var $_conditions; var $_queue; // Apparent queue if being inherited var $_fields; function getId() { return $this->id; } function getFilter() { if ($this->filter && ($F = QueueColumnFilter::getInstance($this->filter))) return $F; } function getName() { return $this->name; } // These getters fetch data from the annotated overlay from the // queue_column table function getQueue() { if (!isset($this->_queue)) { $queue = $this->queue; if (!$queue && ($queue_id = $this->queue_id) && is_numeric($queue_id)) $queue = CustomQueue::lookup($queue_id); $this->_queue = $queue; } return $this->_queue; } /** * If a column is inherited into a child queue and there are conditions * added to that queue, then the column will need to be linked at * run-time to the child queue rather than the parent. */ function setQueue(CustomQueue $queue) { $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 ((isset($fields[$secondary]) && ($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; } function getHeading() { return $this->heading; } function getTranslateTag($subtag) { return _H(sprintf('column.%s.%s.%s', $subtag, $this->queue_id, $this->id)); } function getLocal($subtag) { $tag = $this->getTranslateTag($subtag); $T = CustomDataTranslation::translate($tag); return $T != $tag ? $T : $this->get($subtag); } function getLocalHeading() { return $this->getLocal('heading'); } protected function setFlag($flag, $value=true, $field='flags') { return $value ? $this->{$field} |= $flag : $this->clearFlag($flag, $field); } protected function clearFlag($flag, $field='flags') { return $this->{$field} &= ~$flag; } function isSortable() { return $this->bits & self::FLAG_SORTABLE; } function setSortable($sortable) { $this->setFlag(self::FLAG_SORTABLE, $sortable, 'bits'); } function render($row) { // Basic data $text = $this->renderBasicValue($row); // Filter if ($text && ($filter = $this->getFilter())) { $text = $filter->filter($text, $row) ?: $text; } $styles = array(); if ($text instanceof LazyDisplayWrapper) { $text = $text->display($styles); } // Truncate $text = $this->applyTruncate($text, $row); // annotations and conditions foreach ($this->getAnnotations() as $D) { $text = $D->render($row, $text); } foreach ($this->getConditions() as $C) { $text = $C->render($row, $text, $styles); } $style = Format::array_implode(':', ';', $styles); return array($text, $style); } function renderBasicValue($row) { $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]) && ($T = $F->from_query($row, $primary)) ) { return new LazyDisplayWrapper($F, $T); } if (isset($fields[$secondary]) && ($F = $fields[$secondary]) && ($T = $F->from_query($row, $secondary)) ) { return new LazyDisplayWrapper($F, $T); } 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) || is_numeric($val)) $val = $f->display($val); return $val; } function applyTruncate($text, $row) { $offset = 0; foreach ($this->getAnnotations() as $a) $offset += $a->getWidth($row); $width = $this->width - $offset; $class = array(); switch ($this->truncate) { case 'lclip': $linfo = Internationalization::getCurrentLanguageInfo(); // Use `rtl` class to cut the beginning of LTR text. But, wrap // the text with an appropriate direction so the ending // punctuation is not rearranged. $dir = $linfo['direction'] ?: 'ltr'; $text = sprintf('<span class="%s">%s</span>', $dir, $text); $class[] = $dir == 'rtl' ? 'ltr' : 'rtl'; case 'clip': $class[] = 'bleed'; case 'ellipsis': $class[] = 'truncate'; return sprintf('<span class="%s" style="max-width:%dpx">%s</span>', implode(' ', $class), $width, $text); default: case 'wrap': return $text; } } function addToQuery($query, $field, $path) { if (preg_match('/__answers!\d+__/', $path)) { // Ensure that only one record is returned from the join through // the entry and answers joins return $query->annotate(array( $path => SqlAggregate::MAX($path) )); } return $field->addToQuery($query, $path); } function mangleQuery($query, $root=null) { // Basic data $fields = $this->getFields(); if ($field = $fields[$this->primary]) { $query = $this->addToQuery($query, $field, CustomQueue::getOrmPath($this->primary, $query)); } if (isset($fields[$this->secondary]) && ($field = $fields[$this->secondary])) { $query = $this->addToQuery($query, $field, CustomQueue::getOrmPath($this->secondary, $query)); } if ($filter = $this->getFilter()) $query = $filter->mangleQuery($query, $this); // annotations foreach ($this->getAnnotations() as $D) { $query = $D->annotate($query); } // Conditions foreach ($this->getConditions() as $C) { $query = $C->annotate($query); } return $query; } function applySort($query, $reverse=false) { $root = ($q = $this->getQueue()) ? $q->getRoot() : 'Ticket'; $fields = CustomQueue::getSearchableFields($root); $keys = array(); if ($primary = $fields[$this->primary]) { list(,$field) = $primary; $keys[] = array(CustomQueue::getOrmPath($this->primary, $query), $field); } if ($secondary = $fields[$this->secondary]) { list(,$field) = $secondary; $keys[] = array(CustomQueue::getOrmPath($this->secondary, $query), $field); } if (count($keys) > 1) { $fields = array(); foreach ($keys as $key) { list($path, $field) = $key; foreach ($field->getSortKeys($path) as $field) $fields[] = new SqlField($field); } // Force nulls to the buttom. $fields[] = 'zzz'; $alias = sprintf('C%d', $this->getId()); $expr = call_user_func_array(array('SqlFunction', 'COALESCE'), $fields); $query->annotate(array($alias => $expr)); $reverse = $reverse ? '-' : ''; $query = $query->order_by("{$reverse}{$alias}"); } elseif($keys[0]) { list($path, $field) = $keys[0]; $query = $field->applyOrderBy($query, $reverse, $path); } return $query; } function getDataConfigForm($source=false) { return new QueueColDataConfigForm($source ?: $this->getDbFields(), array('id' => $this->id)); } function getAnnotations() { if (!isset($this->_annotations)) { $this->_annotations = array(); if ($this->annotations && ($anns = JsonDataParser::decode($this->annotations)) ) { foreach ($anns as $D) if ($T = QueueColumnAnnotation::fromJson($D)) $this->_annotations[] = $T; } } return $this->_annotations; } function getConditions($include_queue=true) { if (!isset($this->_conditions)) { $this->_conditions = array(); if ($this->conditions && ($conds = JsonDataParser::decode($this->conditions)) ) { foreach ($conds as $C) if ($T = QueueColumnCondition::fromJson($C)) $this->_conditions[] = $T; } // Support row-spanning conditions if ($include_queue && ($q = $this->getQueue()) && ($q_conds = $q->getConditions()) ) { $this->_conditions = array_merge($q_conds, $this->_conditions); } } return $this->_conditions; } static function __create($vars) { $c = new static($vars); $c->save(); return $c; } static function placeholder($vars) { return static::__hydrate($vars); } function update($vars, $root='Ticket') { $form = $this->getDataConfigForm($vars); foreach ($form->getClean() as $k=>$v) $this->set($k, $v); // Do the annotations $this->_annotations = $annotations = array(); if (isset($vars['annotations'])) { foreach (@$vars['annotations'] as $i=>$class) { if ($vars['deco_column'][$i] != $this->id) continue; if (!class_exists($class) || !is_subclass_of($class, 'QueueColumnAnnotation')) continue; $json = array('c' => $class, 'p' => $vars['deco_pos'][$i]); $annotations[] = $json; $this->_annotations[] = QueueColumnAnnotation::fromJson($json); } } // Do the conditions $this->_conditions = $conditions = array(); if (isset($vars['conditions'])) { list($this->_conditions, $conditions) = self::getConditionsFromPost($vars, $this->id, $root); } // Store as JSON array $this->annotations = JsonDataEncoder::encode($annotations); $this->conditions = JsonDataEncoder::encode($conditions); } static function getConditionsFromPost(array $vars, $myid, $root='Ticket') { $condition_objects = $conditions = array(); if (!isset($vars['conditions'])) return array($condition_objects, $conditions); foreach (@$vars['conditions'] as $i=>$id) { if ($vars['condition_column'][$i] != $myid) // Not a condition for this column continue; // Determine the criteria $name = $vars['condition_field'][$i]; $fields = CustomQueue::getSearchableFields($root); if (!isset($fields[$name])) // No such field exists for this queue root type continue; $parts = CustomQueue::getSearchField($fields[$name], $name); $search_form = new SimpleForm($parts, $vars, array('id' => $id)); $search_form->getField("{$name}+search")->value = true; $crit = $search_form->getClean(); // Check the box to enable searching on the field $crit["{$name}+search"] = true; // Isolate only the critical parts of the criteria $crit = QueueColumnCondition::isolateCriteria($crit); // Determine the properties $props = array(); foreach ($vars['properties'] as $i=>$cid) { if ($cid != $id) // Not a property for this condition continue; // Determine the property configuration $prop = $vars['property_name'][$i]; if (!($F = QueueColumnConditionProperty::getField($prop))) { // Not a valid property continue; } $prop_form = new SimpleForm(array($F), $vars, array('id' => $cid)); $props[$prop] = $prop_form->getField($prop)->getClean(); } $json = array('crit' => $crit, 'prop' => $props); $condition_objects[] = QueueColumnCondition::fromJson($json); $conditions[] = $json; } return array($condition_objects, $conditions); } } class QueueConfig extends VerySimpleModel { static $meta = array( 'table' => QUEUE_CONFIG_TABLE, 'pk' => array('queue_id', 'staff_id'), 'joins' => array( 'queue' => array( 'constraint' => array( 'queue_id' => 'CustomQueue.id'), ), 'staff' => array( 'constraint' => array( 'staff_id' => 'Staff.staff_id', ) ), 'columns' => array( 'reverse' => 'QueueColumnGlue.config', 'constrain' => array('staff_id' =>'QueueColumnGlue.staff_id'), 'broker' => 'QueueColumnListBroker', ), ), ); function getSettings() { return JsonDataParser::decode($this->setting); } function update($vars, &$errors) { // settings of interest $setting = array( 'sort_id' => (int) $vars['sort_id'], 'filter' => $vars['filter'], 'inherit-sort' => ($vars['sort_id'] == '::'), 'inherit-columns' => isset($vars['inherit-columns']), 'criteria' => $vars['criteria'] ?: array(), ); if (!$setting['inherit-columns'] && $vars['columns']) { if (!$this->columns->updateColumns($vars['columns'], $errors, array( 'queue_id' => $this->queue_id, 'staff_id' => $this->staff_id))) $setting['inherit-columns'] = true; $this->columns->reset(); } $this->setting = JsonDataEncoder::encode($setting); return $this->save(true); } function save($refetch=false) { if ($this->dirty) $this->updated = SqlFunction::NOW(); return parent::save($refetch || $this->dirty); } static function create($vars=false) { $inst = new static($vars); return $inst; } } class QueueExport extends VerySimpleModel { static $meta = array( 'table' => QUEUE_EXPORT_TABLE, 'pk' => array('id'), 'joins' => array( 'queue' => array( 'constraint' => array('queue_id' => 'CustomQueue.id'), ), ), 'select_related' => array('queue'), 'ordering' => array('sort'), ); function getPath() { return $this->path; } function getField() { return $this->getPath(); } function getHeading() { return $this->heading; } static function create($vars=false) { $inst = new static($vars); return $inst; } } class QueueColumnGlue extends VerySimpleModel { static $meta = array( 'table' => QUEUE_COLUMN_TABLE, 'pk' => array('queue_id', 'staff_id', 'column_id'), 'joins' => array( 'column' => array( 'constraint' => array('column_id' => 'QueueColumn.id'), ), 'queue' => array( 'constraint' => array( 'queue_id' => 'CustomQueue.id', 'staff_id' => 'CustomQueue.staff_id'), ), 'config' => array( 'constraint' => array( 'queue_id' => 'QueueConfig.queue_id', 'staff_id' => 'QueueConfig.staff_id'), ), ), 'select_related' => array('column'), 'ordering' => array('sort'), ); } class QueueColumnGlueMIM extends ModelInstanceManager { function getOrBuild($modelClass, $fields, $cache=true) { $m = parent::getOrBuild($modelClass, $fields, $cache); if ($m && $modelClass === 'QueueColumnGlue') { // Instead, yield the QueueColumn instance with the local fields // in the association table as annotations $m = AnnotatedModel::wrap($m->column, $m, 'QueueColumn'); } return $m; } } class QueueColumnListBroker extends InstrumentedList { function __construct($fkey, $queryset=false) { parent::__construct($fkey, $queryset, 'QueueColumnGlueMIM'); $this->queryset->select_related('column'); } function add($column, $glue=null, $php7_is_annoying=true) { $glue = $glue ?: new QueueColumnGlue(); $glue->column = $column; $anno = AnnotatedModel::wrap($column, $glue); parent::add($anno, false); return $anno; } function updateColumns($columns, &$errors, $options=array()) { $new = $columns; $order = array_keys($new); foreach ($this as $col) { $key = $col->column_id; if (!isset($columns[$key])) { $this->remove($col); continue; } $info = $columns[$key]; $col->set('sort', array_search($key, $order)); $col->set('heading', $info['heading']); $col->set('width', $info['width']); $col->setSortable($info['sortable']); unset($new[$key]); } // Add new columns foreach ($new as $info) { $glue = new QueueColumnGlue(array( 'staff_id' => $options['staff_id'] ?: 0 , 'queue_id' => $options['queue_id'] ?: 0, 'column_id' => $info['column_id'], 'sort' => array_search($info['column_id'], $order), 'heading' => $info['heading'], 'width' => $info['width'] ?: 100, 'bits' => $info['sortable'] ? QueueColumn::FLAG_SORTABLE : 0, )); $this->add(QueueColumn::lookup($info['column_id']), $glue); } // Re-sort the in-memory columns array $this->sort(function($c) { return $c->sort; }); return $this->saveAll(); } } class QueueSort extends VerySimpleModel { static $meta = array( 'table' => QUEUE_SORT_TABLE, 'pk' => array('id'), 'ordering' => array('name'), 'joins' => array( 'queue' => array( 'constraint' => array('queue_id' => 'CustomQueue.id'), ), ), ); var $_columns; var $_extra; function getRoot($hint=false) { switch ($hint ?: $this->root) { case 'T': default: return 'Ticket'; } } function getName() { return $this->name; } function getId() { return $this->id; } function getExtra() { if (isset($this->extra) && !isset($this->_extra)) $this->_extra = JsonDataParser::decode($this->extra); return $this->_extra; } function applySort(QuerySet $query, $reverse=false, $root=false) { $fields = CustomQueue::getSearchableFields($this->getRoot($root)); foreach ($this->getColumnPaths() as $path=>$descending) { $descending = $reverse ? !$descending : $descending; if (isset($fields[$path])) { list(,$field) = $fields[$path]; $query = $field->applyOrderBy($query, $descending, CustomQueue::getOrmPath($path, $query)); } } // Add index hint if defined if (($extra = $this->getExtra()) && isset($extra['index'])) { $query->setOption(QuerySet::OPT_INDEX_HINT, $extra['index']); } return $query; } function getColumnPaths() { if (!isset($this->_columns)) { $columns = array(); foreach (JsonDataParser::decode($this->columns) as $path) { if ($descending = $path[0] == '-') $path = substr($path, 1); $columns[$path] = $descending; } $this->_columns = $columns; } return $this->_columns; } function getColumns() { $columns = array(); $paths = $this->getColumnPaths(); $everything = CustomQueue::getSearchableFields($this->getRoot()); foreach ($paths as $p=>$descending) { if (isset($everything[$p])) { $columns[$p] = array($everything[$p], $descending); } } return $columns; } function getDataConfigForm($source=false) { return new QueueSortDataConfigForm($source ?: $this->getDbFields(), array('id' => $this->id)); } function getAdvancedConfigForm($source=false) { return new QueueSortAdvancedConfigForm($source ?: $this->getExtra(), array('id' => $this->id)); } static function forQueue(CustomQueue $queue) { return static::objects()->filter([ 'root' => $queue->root ?: 'T', ]); } function save($refetch=false) { if ($this->dirty) $this->updated = SqlFunction::NOW(); return parent::save($refetch || $this->dirty); } function update($vars, &$errors=array()) { if (!isset($vars['name'])) $errors['name'] = __('A title is required'); $this->name = $vars['name']; if (isset($vars['root'])) $this->root = $vars['root']; elseif (!isset($this->root)) $this->root = 'T'; $fields = CustomQueue::getSearchableFields($this->getRoot($vars['root'])); $columns = array(); if (@is_array($vars['columns'])) { foreach ($vars['columns']as $path=>$info) { $descending = (int) @$info['descending']; // TODO: Check if column is valid, stash in $columns if (!isset($fields[$path])) continue; $columns[] = ($descending ? '-' : '') . $path; } $this->columns = JsonDataEncoder::encode($columns); } if ($this->getExtra() !== null) { $extra = $this->getAdvancedConfigForm($vars)->getClean(); $this->extra = JsonDataEncoder::encode($extra); } if (count($errors)) return false; return $this->save(); } static function __create($vars) { $c = new static($vars); $c->save(); return $c; } } class QueueSortGlue extends VerySimpleModel { static $meta = array( 'table' => QUEUE_SORTING_TABLE, 'pk' => array('sort_id', 'queue_id'), 'joins' => array( 'ordering' => array( 'constraint' => array('sort_id' => 'QueueSort.id'), ), 'queue' => array( 'constraint' => array('queue_id' => 'CustomQueue.id'), ), ), 'select_related' => array('ordering', 'queue'), 'ordering' => array('sort'), ); } class QueueSortGlueMIM extends ModelInstanceManager { function getOrBuild($modelClass, $fields, $cache=true) { $m = parent::getOrBuild($modelClass, $fields, $cache); if ($m && $modelClass === 'QueueSortGlue') { // Instead, yield the QueueColumn instance with the local fields // in the association table as annotations $m = AnnotatedModel::wrap($m->ordering, $m, 'QueueSort'); } return $m; } } class QueueSortListBroker extends InstrumentedList { function __construct($fkey, $queryset=false) { parent::__construct($fkey, $queryset, 'QueueSortGlueMIM'); $this->queryset->select_related('ordering'); } function add($ordering, $glue=null, $php7_is_annoying=true) { $glue = $glue ?: new QueueSortGlue(); $glue->ordering = $ordering; $anno = AnnotatedModel::wrap($ordering, $glue); parent::add($anno, false); return $anno; } } abstract class QueueColumnFilter { static $registry; static $id = null; static $desc = null; static function register($filter, $group) { if (!isset($filter::$id)) throw new Exception('QueueColumnFilter must define $id'); if (isset(static::$registry[$filter::$id])) throw new Exception($filter::$id . ': QueueColumnFilter already registered under that id'); if (!is_subclass_of($filter, get_called_class())) throw new Exception('Filter must extend QueueColumnFilter'); static::$registry[$filter::$id] = array($group, $filter); } static function getFilters() { $list = static::$registry; $base = array(); foreach ($list as $id=>$stuff) { list($group, $class) = $stuff; $base[$group][$id] = __($class::$desc); } return $base; } static function getInstance($id) { if (isset(static::$registry[$id])) { list(, $class) = @static::$registry[$id]; if ($class && class_exists($class)) return new $class(); } } function mangleQuery($query, $column) { return $query; } abstract function filter($value, $row); } class TicketLinkFilter extends QueueColumnFilter { static $id = 'link:ticket'; static $desc = /* @trans */ "Ticket Link"; function filter($text, $row) { if ($link = $this->getLink($row)) return sprintf('<a style="display:inline" href="%s">%s</a>', $link, $text); } function mangleQuery($query, $column) { static $fields = array( 'link:ticket' => 'ticket_id', 'link:ticketP' => 'ticket_id', 'link:user' => 'user_id', 'link:org' => 'user__org_id', ); if (isset($fields[static::$id])) { $query = $query->values($fields[static::$id]); } return $query; } function getLink($row) { return Ticket::getLink($row['ticket_id']); } } class UserLinkFilter extends TicketLinkFilter { static $id = 'link:user'; static $desc = /* @trans */ "User Link"; function getLink($row) { return User::getLink($row['user_id']); } } class OrgLinkFilter extends TicketLinkFilter { static $id = 'link:org'; static $desc = /* @trans */ "Organization Link"; function getLink($row) { return Organization::getLink($row['user__org_id']); } } QueueColumnFilter::register('TicketLinkFilter', __('Link')); QueueColumnFilter::register('UserLinkFilter', __('Link')); QueueColumnFilter::register('OrgLinkFilter', __('Link')); class TicketLinkWithPreviewFilter extends TicketLinkFilter { static $id = 'link:ticketP'; static $desc = /* @trans */ "Ticket Link with Preview"; function filter($text, $row) { $link = $this->getLink($row); return sprintf('<a style="display: inline" class="preview" data-preview="#tickets/%d/preview" href="%s">%s</a>', $row['ticket_id'], $link, $text); } } QueueColumnFilter::register('TicketLinkWithPreviewFilter', __('Link')); class DateTimeFilter extends QueueColumnFilter { static $id = 'date:full'; static $desc = /* @trans */ "Date and Time"; function filter($text, $row) { return $text ? $text->changeTo(Format::datetime($text->value)) : ''; } } class HumanizedDateFilter extends QueueColumnFilter { static $id = 'date:human'; static $desc = /* @trans */ "Relative Date and Time"; function filter($text, $row) { return sprintf( '<time class="relative" datetime="%s" title="%s">%s</time>', date(DateTime::W3C, Misc::db2gmtime($text->value) ?: 0), Format::daydatetime($text->value), Format::relativeTime(Misc::db2gmtime($text->value)) ); } } QueueColumnFilter::register('DateTimeFilter', __('Date Format')); QueueColumnFilter::register('HumanizedDateFilter', __('Date Format')); class QueueColDataConfigForm extends AbstractForm { function buildFields() { return array( 'primary' => new DataSourceField(array( 'label' => __('Primary Data Source'), 'required' => true, 'configuration' => array( 'root' => 'Ticket', ), 'layout' => new GridFluidCell(6), )), 'secondary' => new DataSourceField(array( 'label' => __('Secondary Data Source'), 'configuration' => array( 'root' => 'Ticket', ), 'layout' => new GridFluidCell(6), )), 'name' => new TextboxField(array( 'label' => __('Name'), 'required' => true, 'layout' => new GridFluidCell(4), )), 'filter' => new ChoiceField(array( 'label' => __('Filter'), 'required' => false, 'choices' => QueueColumnFilter::getFilters(), 'layout' => new GridFluidCell(4), )), 'truncate' => new ChoiceField(array( 'label' => __('Text Overflow'), 'choices' => array( 'wrap' => __("Wrap Lines"), 'ellipsis' => __("Add Ellipsis"), 'clip' => __("Clip Text"), 'lclip' => __("Clip Beginning Text"), ), 'default' => 'wrap', 'layout' => new GridFluidCell(4), )), ); } } class QueueSortDataConfigForm extends AbstractForm { function getInstructions() { return __('Add, and remove the fields in this list using the options below. Sorting can be performed on any field, whether displayed in the queue or not.'); } function buildFields() { return array( 'name' => new TextboxField(array( 'required' => true, 'layout' => new GridFluidCell(12), 'translatable' => isset($this->options['id']) ? _H('queuesort.name.'.$this->options['id']) : false, 'configuration' => array( 'placeholder' => __('Sort Criteria Title'), ), )), ); } } class QueueSortAdvancedConfigForm extends AbstractForm { function getInstructions() { return __('If unsure, leave these options blank and unset'); } function buildFields() { return array( 'index' => new TextboxField(array( 'label' => __('Database Index'), 'hint' => __('Use this index when sorting on this column'), 'required' => false, 'layout' => new GridFluidCell(12), 'configuration' => array( 'placeholder' => __('Automatic'), ), )), ); } }
Save