diff --git a/assets/default/css/theme.css b/assets/default/css/theme.css index 26680176598c1b8916aaccacb041713d34074a16..ba4fc8e02c8e1db4b67936aaa174a6522b6ba3ca 100644 --- a/assets/default/css/theme.css +++ b/assets/default/css/theme.css @@ -999,7 +999,7 @@ img.sign-in-image { } .span8 { display: inline-block; - width: 66.5%; + width: 66.0%; margin: 0 1%; vertical-align: top; } diff --git a/css/thread.css b/css/thread.css index 5d8ecb7a2014832c05decdcaf34321399cf4f68e..4e810094a70447cb92dd33671fe85bb4c58704eb 100644 --- a/css/thread.css +++ b/css/thread.css @@ -420,12 +420,16 @@ margin: 0; margin-bottom: 10px; border: none; + background: none; + box-shadow: none !important; + text-indent: 0 !important; +} + +.thread-body pre { background: #f5f5f5; background-color: rgba(0,0,0,0.05); border-radius: 5px; padding: 0.5em; - box-shadow: none !important; - text-indent: 0 !important; } .thread-body iframe, diff --git a/include/ajax.thread.php b/include/ajax.thread.php index 4a8b72016d69ce2ff4565d5026134b380f6e0649..470f9276e24a6d3c4c15a7d054422799db70c9b1 100644 --- a/include/ajax.thread.php +++ b/include/ajax.thread.php @@ -114,19 +114,6 @@ class ThreadAjaxAPI extends AjaxController { // FIXME: Refuse to add ticket owner?? if (($c=$thread->addCollaborator($user, array('isactive'=>1), $errors))) { - $note = Format::htmlchars(sprintf(__('%s <%s> added as a collaborator'), - Format::htmlchars($c->getName()), $c->getEmail())); - - $thread->getObject()->postThreadEntry('N', - array( - 'title' => __('New Collaborator Added'), - 'note' => $note - ), - array( - 'poster' => $thisstaff, - 'alert' => false - ) - ); $info = array('msg' => sprintf(__('%s added as a collaborator'), Format::htmlchars($c->getName()))); return self::_collaborators($thread, $info); diff --git a/include/class.faq.php b/include/class.faq.php index f3d9d5bb76cfde2e0d454387b153d12475ade707..9f0b83ccd5161f08700fbfb03cfec1fea7a47d1a 100644 --- a/include/class.faq.php +++ b/include/class.faq.php @@ -367,10 +367,10 @@ class FAQ extends VerySimpleModel { } static function allPublic() { - return static::objects()->exclude(array( + return static::objects()->exclude(Q::any(array( 'ispublished'=>self::VISIBILITY_PRIVATE, 'category__ispublic'=>Category::VISIBILITY_PRIVATE, - )); + ))); } static function countPublishedFAQs() { diff --git a/include/class.file.php b/include/class.file.php index 5f4013ff762f85b780c61bc746d84190deba714b..e0953876d7571c4e6f322d78b4dd7a48a59ea85a 100644 --- a/include/class.file.php +++ b/include/class.file.php @@ -25,6 +25,12 @@ class AttachmentFile extends VerySimpleModel { ), ), ); + static $keyCache = array(); + + function __onload() { + // Cache for lookup in the ::lookupByHash method below + static::$keyCache[$this->key] = $this; + } function getHashtable() { return $this->ht; @@ -532,13 +538,11 @@ class AttachmentFile extends VerySimpleModel { } static function lookupByHash($hash) { - static $keyCache = array(); - - if (isset($keyCache[$hash])) - return $keyCache[$hash]; + if (isset(static::$keyCache[$hash])) + return static::$keyCache[$hash]; // Cache a negative lookup if no such file exists - return $keyCache[$hash] = parent::lookup(array('key' => $hash)); + return parent::lookup(array('key' => $hash)); } static function lookup($id) { diff --git a/include/class.format.php b/include/class.format.php index d062b69fdc5011712aa71863ae3c86c2dd13b65a..d13c2dd8b39a292698b73beda6272052d127eee6 100644 --- a/include/class.format.php +++ b/include/class.format.php @@ -317,7 +317,7 @@ class Format { global $ost; // Find all text between tags - $text = preg_replace_callback(':^[^<]+|>[^<]+:', + return preg_replace_callback(':^[^<]+|>[^<]+:', function($match) { // Scan for things that look like URLs return preg_replace_callback( @@ -344,32 +344,6 @@ class Format { $match[0]); }, $text); - - // Now change @href and @src attributes to come back through our - // system as well - $config = array( - 'hook_tag' => function($e, $a=0) use ($target) { - static $eE = array('area'=>1, 'br'=>1, 'col'=>1, 'embed'=>1, - 'hr'=>1, 'img'=>1, 'input'=>1, 'isindex'=>1, 'param'=>1); - if ($e == 'a' && $a) { - $a['target'] = $target; - $a['class'] = 'no-pjax'; - } - - $at = ''; - if (is_array($a)) { - foreach ($a as $k=>$v) - $at .= " $k=\"$v\""; - return "<{$e}{$at}".(isset($eE[$e])?" /":"").">"; - } else { - return "</{$e}>"; - } - }, - 'schemes' => 'href: aim, feed, file, ftp, gopher, http, https, irc, mailto, news, nntp, sftp, ssh, telnet; *:file, http, https; src: cid, http, https, data', - 'elements' => '*+iframe', - 'spec' => 'span=data-src,width,height;img=data-cid', - ); - return Format::html($text, $config); } function stripEmptyLines($string) { @@ -379,18 +353,9 @@ class Format { function viewableImages($html, $script=false) { $cids = $images = array(); - // Try and get information for all the files in one query - if (preg_match_all('/"cid:([\w._-]{32})"/', $html, $cids)) { - foreach (AttachmentFile::objects() - ->filter(array('key__in' => $cids[1])) - as $file - ) { - $images[strtolower($file->getKey())] = $file; - } - } return preg_replace_callback('/"cid:([\w._-]{32})"/', function($match) use ($script, $images) { - if (!($file = $images[strtolower($match[1])])) + if (!($file = AttachmentFile::lookup($match[1]))) return $match[0]; return sprintf('"%s" data-cid="%s"', $file->getDownloadUrl(false, 'inline', $script), $match[1]); @@ -441,6 +406,7 @@ class Format { function __formatDate($timestamp, $format, $fromDb, $dayType, $timeType, $strftimeFallback, $timezone, $user=false) { global $cfg; + static $cache; if (!$timestamp) return ''; @@ -449,18 +415,28 @@ class Format { $timestamp = Misc::db2gmtime($timestamp); if (class_exists('IntlDateFormatter')) { - $formatter = new IntlDateFormatter( - Internationalization::getCurrentLocale($user), - $dayType, - $timeType, - $timezone, - IntlDateFormatter::GREGORIAN, - $format ?: null - ); - if ($cfg->isForce24HourTime()) { - $format = str_replace(array('a', 'h'), array('', 'H'), - $formatter->getPattern()); - $formatter->setPattern($format); + $locale = Internationalization::getCurrentLocale($user); + $key = "{$locale}:{$dayType}:{$timeType}:{$timezone}:{$format}"; + if (!isset($cache[$key])) { + // Setting up the IntlDateFormatter is pretty expensive, so + // cache it since there aren't many variations of the + // arguments passed to the constructor + $cache[$key] = $formatter = new IntlDateFormatter( + $locale, + $dayType, + $timeType, + $timezone, + IntlDateFormatter::GREGORIAN, + $format ?: null + ); + if ($cfg->isForce24HourTime()) { + $format = str_replace(array('a', 'h'), array('', 'H'), + $formatter->getPattern()); + $formatter->setPattern($format); + } + } + else { + $formatter = $cache[$key]; } return $formatter->format($timestamp); } diff --git a/include/class.forms.php b/include/class.forms.php index a3dc7579634996b2da6d1837bdd4e31b3faf42f4..f4f5491226e4e63f36b338bb3b18a49c1174ff48 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -2124,8 +2124,16 @@ class FileUploadField extends FormField { static function getFileTypes() { static $filetypes; - if (!isset($filetypes)) - $filetypes = YamlDataParser::load(INCLUDE_DIR . '/config/filetype.yaml'); + if (!isset($filetypes)) { + if (function_exists('apc_fetch')) { + $key = md5(SECRET_SALT . GIT_VERSION . 'filetypes'); + $filetypes = apc_fetch($key); + } + if (!$filetypes) + $filetypes = YamlDataParser::load(INCLUDE_DIR . '/config/filetype.yaml'); + if ($key) + apc_store($key, $filetypes, 7200); + } return $filetypes; } diff --git a/include/class.i18n.php b/include/class.i18n.php index f68be50e6d57c00b6088ed2505989e4c21247689..ed3df93bfcdbbb44fcfc9b4a1f7d0e02dcff2ece 100644 --- a/include/class.i18n.php +++ b/include/class.i18n.php @@ -280,6 +280,10 @@ class Internationalization { // Algorithm borrowed from Drupal 7 (locale.inc) static function getDefaultLanguage() { global $cfg; + static $lang; + + if (isset($lang)) + return $lang; if (empty($_SERVER["HTTP_ACCEPT_LANGUAGE"])) return $cfg ? $cfg->getPrimaryLanguage() : 'en_US'; @@ -358,10 +362,9 @@ class Internationalization { } } - if (self::isLanguageInstalled($best_match_langcode)) - return $best_match_langcode; - else - return $cfg->getPrimaryLanguage(); + return $lang = self::isLanguageInstalled($best_match_langcode) + ? $best_match_langcode + : $cfg->getPrimaryLanguage(); } static function getCurrentLanguage($user=false) { diff --git a/include/class.orm.php b/include/class.orm.php index 676390409859f92f996ab167e3987bdf8a304a66..eab05d3a074fa805a428a5c1ab93ad50efd07a19 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -51,23 +51,24 @@ class ModelMeta implements ArrayAccess { function __construct($model) { $this->model = $model; - $parent = get_parent_class($model); // Merge ModelMeta from parent model (if inherited) + $parent = get_parent_class($this->model); if (is_subclass_of($parent, 'VerySimpleModel')) { - $parent::_inspect(); - $meta = $parent::$meta->extend($model::$meta); + $meta = $parent::getMeta()->extend($model::$meta); } else { $meta = $model::$meta + self::$base; } - if (!$meta['table']) - throw new OrmConfigurationException( - sprintf(__('%s: Model does not define meta.table'), $model)); - elseif (!$meta['pk']) - throw new OrmConfigurationException( - sprintf(__('%s: Model does not define meta.pk'), $model)); + if (!$meta['view']) { + if (!$meta['table']) + throw new OrmConfigurationException( + sprintf(__('%s: Model does not define meta.table'), $this->model)); + elseif (!$meta['pk']) + throw new OrmConfigurationException( + sprintf(__('%s: Model does not define meta.pk'), $this->model)); + } // Ensure other supported fields are set and are arrays foreach (array('pk', 'ordering', 'defer', 'select_related') as $f) { @@ -93,10 +94,32 @@ class ModelMeta implements ArrayAccess { return $meta + $this->base + self::$base; } + /** + * Adds some more information to a declared relationship. If the + * relationship is a reverse relation, then the information from the + * reverse relation is loaded into the local definition + * + * Compiled-Join-Structure: + * 'constraint' => array(local => array(foreign_field, foreign_class)), + * Constraint used to construct a JOIN in an SQL query + * 'list' => boolean + * TRUE if an InstrumentedList should be employed to fetch a list + * of related items + * 'broker' => Handler for the 'list' property. Usually a subclass of + * 'InstrumentedList' + * 'null' => boolean + * TRUE if relation is nullable + * 'fkey' => array(class, pk) + * Classname and field of the first item in the constraint that + * points to a PK field of a foreign model + * 'local' => string + * The local field corresponding to the 'fkey' property + */ function processJoin(&$j) { $constraint = array(); if (isset($j['reverse'])) { list($fmodel, $key) = explode('.', $j['reverse']); + // NOTE: It's ok if the forein meta data is not yet inspected. $info = $fmodel::$meta['joins'][$key]; if (!is_array($info['constraint'])) throw new OrmConfigurationException(sprintf(__( @@ -211,11 +234,8 @@ class VerySimpleModel { function get($field, $default=false) { if (array_key_exists($field, $this->ht)) return $this->ht[$field]; - elseif (isset(static::$meta['joins'][$field])) { - // Make sure joins were inspected - if (!static::$meta instanceof ModelMeta) - static::_inspect(); - $j = static::$meta['joins'][$field]; + elseif (($joins = static::getMeta('joins')) && isset($joins[$field])) { + $j = $joins[$field]; // Support instrumented lists and such if (isset($j['list']) && $j['list']) { $class = $j['fkey'][0]; @@ -296,11 +316,9 @@ class VerySimpleModel { function set($field, $value) { // Update of foreign-key by assignment to model instance $related = false; - if (isset(static::$meta['joins'][$field])) { - // XXX: This is likely not necessary - if (!isset(static::$meta['joins'][$field]['fkey'])) - static::_inspect(); - $j = static::$meta['joins'][$field]; + $joins = static::getMeta('joins'); + if (isset($joins[$field])) { + $j = $joins[$field]; if ($j['list'] && ($value instanceof InstrumentedList)) { // Magic list property $this->ht[$field] = $value; @@ -365,12 +383,17 @@ class VerySimpleModel { static function __oninspect() {} static function _inspect() { - if (!static::$meta instanceof ModelMeta) { - static::$meta = new ModelMeta(get_called_class()); + static::$meta = new ModelMeta(get_called_class()); - // Let the model participate - static::__oninspect(); - } + // Let the model participate + static::__oninspect(); + } + + static function getMeta($key=false) { + if (!static::$meta instanceof ModelMeta) + static::_inspect(); + $M = static::$meta; + return ($key) ? $M->offsetGet($key) : $M; } /** @@ -412,14 +435,12 @@ class VerySimpleModel { * no such instance exists. */ static function lookup($criteria) { - // Autoinsepct model - static::_inspect(); - // Model::lookup(1), where >1< is the pk value if (!is_array($criteria)) { $criteria = array(); + $pk = static::getMeta('pk'); foreach (func_get_args() as $i=>$f) - $criteria[static::$meta['pk'][$i]] = $f; + $criteria[$pk[$i]] = $f; // Only consult cache for PK lookup, which is assumed if the // values are passed as args rather than an array @@ -455,14 +476,13 @@ class VerySimpleModel { if ($this->__deleted__) throw new OrmException('Trying to update a deleted object'); - $pk = static::$meta['pk']; + $pk = static::getMeta('pk'); $wasnew = $this->__new__; // First, if any foreign properties of this object are connected to // another *new* object, then save those objects first and set the // local foreign key field values - static::_inspect(); - foreach (static::$meta['joins'] as $prop => $j) { + foreach (static::getMeta('joins') as $prop => $j) { if (isset($this->ht[$prop]) && ($foreign = $this->ht[$prop]) && $foreign instanceof VerySimpleModel @@ -520,7 +540,7 @@ class VerySimpleModel { if ($wasnew) { // Attempt to update foreign, unsaved objects with the PK of // this newly created object - foreach (static::$meta['joins'] as $prop => $j) { + foreach (static::getMeta('joins') as $prop => $j) { if (isset($this->ht[$prop]) && ($foreign = $this->ht[$prop]) && in_array($j['local'], $pk) @@ -557,7 +577,7 @@ class VerySimpleModel { private function getPk() { $pk = array(); - foreach ($this::$meta['pk'] as $f) + foreach ($this::getMeta('pk') as $f) $pk[$f] = $this->ht[$f]; return $pk; } @@ -658,6 +678,8 @@ class SqlCase extends SqlFunction { } function when($expr, $result) { + if (is_array($expr)) + $expr = new Q($expr); $this->cases[] = array($expr, $result); return $this; } @@ -816,14 +838,15 @@ class SqlAggregate extends SqlFunction { list($field, $rmodel) = $compiler->getField($E, $model, $options); if ($this->distinct) { $pk = false; - foreach ($rmodel::$meta['pk'] as $f) { + $fpk = $rmodel::getMeta('pk'); + foreach ($fpk as $f) { $pk |= false !== strpos($field, $f); } if (!$pk) { // Try and use the foriegn primary key - if (count($rmodel::$meta['pk']) == 1) { + if (count($fpk) == 1) { list($field) = $compiler->getField( - $this->expr . '__' . $rmodel::$meta['pk'][0], + $this->expr . '__' . $fpk[0], $model, $options); } else { @@ -856,6 +879,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl var $related = array(); var $values = array(); var $defer = array(); + var $aggregated = false; var $annotations = array(); var $extra = array(); var $distinct = array(); @@ -1017,6 +1041,14 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl return $this->_count = $compiler->compileCount($this); } + function toSql($compiler, $model, $alias) { + // FIXME: Force root model of the compiler to $model + $exec = $this->getQuery(array('compiler' => get_class($compiler))); + foreach ($exec->params as $P) + $compiler->params[] = $P; + return "({$exec})".($alias ? " AS {$alias}" : ''); + } + /** * exists * @@ -1055,6 +1087,17 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl return $this; } + function aggregate($annotations) { + // Aggregate works like annotate, except that it sets up values + // fetching which will disable model creation + $this->annotate($annotations); + $this->values_flat(); + // Disable other fields from being fetched + $this->aggregated = true; + $this->related = false; + return $this; + } + function delete() { $class = $this->compiler; $compiler = new $class(); @@ -1110,14 +1153,14 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl // Load defaults from model $model = $this->model; $query = clone $this; - if (!$options['nosort'] && !$query->ordering && isset($model::$meta['ordering'])) - $query->ordering = $model::$meta['ordering']; - if (false !== $query->related && !$query->values && $model::$meta['select_related']) - $query->related = $model::$meta['select_related']; - if (!$query->defer && $model::$meta['defer']) - $query->defer = $model::$meta['defer']; - - $class = $this->compiler; + if (!$options['nosort'] && !$query->ordering && $model::getMeta('ordering')) + $query->ordering = $model::getMeta('ordering'); + if (false !== $query->related && !$query->values && $model::getMeta('select_related')) + $query->related = $model::getMeta('select_related'); + if (!$query->defer && $model::getMeta('defer')) + $query->defer = $model::getMeta('defer'); + + $class = $options['compiler'] ?: $this->compiler; $compiler = new $class($options); $this->query = $compiler->compileSelect($query); @@ -1178,10 +1221,14 @@ abstract class ResultSet implements Iterator, ArrayAccess, Countable { $this->queryset = $queryset; if ($queryset) { $this->model = $queryset->model; - $this->resource = $queryset->getQuery(); } } + function prime() { + if (!isset($this->resource) && $this->queryset) + $this->resource = $this->queryset->getQuery(); + } + abstract function fillTo($index); function asArray() { @@ -1236,17 +1283,9 @@ class ModelInstanceManager extends ResultSet { static $objectCache = array(); - function __construct($queryset=false) { - parent::__construct($queryset); - if ($queryset) { - $this->map = $this->resource->getMap(); - } - } - function cache($model) { - $model::_inspect(); $key = sprintf('%s.%s', - $model::$meta->model, implode('.', $model->pk)); + $model::$meta->model, implode('.', $model->get('pk'))); self::$objectCache[$key] = $model; } @@ -1264,8 +1303,8 @@ class ModelInstanceManager extends ResultSet { } static function checkCache($modelClass, $fields) { - $key = $modelClass; - foreach ($modelClass::$meta['pk'] as $f) + $key = $modelClass::$meta->model; + foreach ($modelClass::getMeta('pk') as $f) $key .= '.'.$fields[$f]; return @self::$objectCache[$key]; } @@ -1287,7 +1326,7 @@ class ModelInstanceManager extends ResultSet { function getOrBuild($modelClass, $fields, $cache=true) { // Check for NULL primary key, used with related model fetching. If // the PK is NULL, then consider the object to also be NULL - foreach ($modelClass::$meta['pk'] as $pkf) { + foreach ($modelClass::getMeta('pk') as $pkf) { if (!isset($fields[$pkf])) { return null; } @@ -1382,30 +1421,36 @@ class ModelInstanceManager extends ResultSet { } function fillTo($index) { + $this->prime(); $func = ($this->map) ? 'getRow' : 'getArray'; while ($this->resource && $index >= count($this->cache)) { if ($row = $this->resource->{$func}()) { $this->cache[] = $this->buildModel($row); } else { $this->resource->close(); - $this->resource = null; + $this->resource = false; break; } } } + + function prime() { + parent::prime(); + if ($this->resource) { + $this->map = $this->resource->getMap(); + } + } } class FlatArrayIterator extends ResultSet { - function __construct($queryset) { - $this->resource = $queryset->getQuery(); - } function fillTo($index) { + $this->prime(); while ($this->resource && $index >= count($this->cache)) { if ($row = $this->resource->getRow()) { $this->cache[] = $row; } else { $this->resource->close(); - $this->resource = null; + $this->resource = false; break; } } @@ -1413,16 +1458,14 @@ class FlatArrayIterator extends ResultSet { } class HashArrayIterator extends ResultSet { - function __construct($queryset) { - $this->resource = $queryset->getQuery(); - } function fillTo($index) { + $this->prime(); while ($this->resource && $index >= count($this->cache)) { if ($row = $this->resource->getArray()) { $this->cache[] = $row; } else { $this->resource->close(); - $this->resource = null; + $this->resource = false; break; } } @@ -1431,12 +1474,14 @@ class HashArrayIterator extends ResultSet { class InstrumentedList extends ModelInstanceManager { var $key; - var $model; function __construct($fkey, $queryset=false) { list($model, $this->key) = $fkey; - if (!$queryset) + if (!$queryset) { $queryset = $model::objects()->filter($this->key); + if ($related = $model::getMeta('select_related')) + $queryset->select_related($related); + } parent::__construct($queryset); $this->model = $model; } @@ -1466,7 +1511,8 @@ class InstrumentedList extends ModelInstanceManager { if ($delete) $object->delete(); else - $object->set($this->key, null); + foreach ($this->key as $field=>$value) + $object->set($field, null); } function reset() { @@ -1480,10 +1526,10 @@ class InstrumentedList extends ModelInstanceManager { */ function window($constraint) { $model = $this->model; - $meta = $model::$meta; + $fields = $model::getMeta('fields'); $key = $this->key; foreach ($constraint as $field=>$value) { - if (!is_string($field) || false === in_array($field, $meta['fields'])) + if (!is_string($field) || false === in_array($field, $fields)) throw new OrmException('InstrumentedList windowing must be performed on local fields only'); $key[$field] = $value; } @@ -1618,52 +1664,52 @@ class SqlCompiler { } } - $path = array(); + $path = ''; $rootModel = $model; // Call pushJoin for each segment in the join path. A new JOIN // fragment will need to be emitted and/or cached $joins = array(); - $push = function($p, $path, $model) use (&$joins) { - $model::_inspect(); - if (!($info = $model::$meta['joins'][$p])) { + $push = function($p, $model) use (&$joins, &$path) { + $J = $model::getMeta('joins'); + if (!($info = $J[$p])) { throw new OrmException(sprintf( 'Model `%s` does not have a relation called `%s`', $model, $p)); } - $crumb = implode('__', $path); - $tip = ($crumb) ? "{$crumb}__{$p}" : $p; - $joins[] = array($crumb, $tip, $model, $info); + $crumb = $path; + $path = ($path) ? "{$path}__{$p}" : $p; + $joins[] = array($crumb, $path, $model, $info); // Roll to foreign model return $info['fkey']; }; foreach ($parts as $p) { - list($model) = $push($p, $path, $model); - $path[] = $p; + list($model) = $push($p, $model); } // If comparing a relationship, join the foreign table // This is a comparison with a relationship — use the foreign key - if (isset($model::$meta['joins'][$field])) { - list($model, $field) = $push($field, $path, $model); - } - - // Add the conststraint as the last arg to the last join - if (isset($options['constraint'])) { - $joins[count($joins)-1][] = $options['constraint']; + $J = $model::getMeta('joins'); + if (isset($J[$field])) { + list($model, $field) = $push($field, $model); } // Apply the joins list to $this->pushJoin - foreach ($joins as $A) { - $alias = call_user_func_array(array($this, 'pushJoin'), $A); + $last = count($joins) - 1; + $constraint = false; + foreach ($joins as $i=>$A) { + // Add the conststraint as the last arg to the last join + if ($i == $last) + $constraint = $options['constraint']; + $alias = $this->pushJoin($A[0], $A[1], $A[2], $A[3], $contraint); } if (!isset($alias)) { // Determine the alias for the root model table $alias = (isset($this->joins[''])) ? $this->joins['']['alias'] - : $this->quote($rootModel::$meta['table']); + : $this->quote($rootModel::getMeta('table')); } if (isset($options['table']) && $options['table']) @@ -1771,6 +1817,8 @@ class SqlCompiler { } if ($value === null) $filter[] = sprintf('%s IS NULL', $field); + elseif ($value instanceof SqlField) + $filter[] = sprintf($op, $field, $value->toSql($this, $model)); // Allow operators to be callable rather than sprintf // strings elseif (is_callable($op)) @@ -1959,7 +2007,7 @@ class MySqlCompiler extends SqlCompiler { if (isset($this->joins[$tip])) $table = $this->joins[$tip]['alias']; else - $table = $this->quote($model::$meta['table']); + $table = $this->quote($model::getMeta('table')); foreach ($info['constraint'] as $local => $foreign) { list($rmodel, $right) = $foreign; // Support a constant constraint with @@ -1992,10 +2040,10 @@ class MySqlCompiler extends SqlCompiler { if (!isset($rmodel)) $rmodel = $model; // Support inline views - $table = ($rmodel::$meta['view']) + $table = ($rmodel::getMeta('view')) // XXX: Support parameters from the nested query ? $rmodel::getQuery($this) - : $this->quote($rmodel::$meta['table']); + : $this->quote($rmodel::getMeta('table')); $base = "{$join}{$table} {$alias}"; if ($constraints) $base .= ' ON ('.implode(' AND ', $constraints).')'; @@ -2069,8 +2117,7 @@ class MySqlCompiler extends SqlCompiler { function compileCount($queryset) { $model = $queryset->model; - $model::_inspect(); - $table = $model::$meta['table']; + $table = $model::getMeta('table'); list($where, $having) = $this->getWhereHavingClause($queryset); $joins = $this->getJoins($queryset); $sql = 'SELECT COUNT(*) AS count FROM '.$this->quote($table).$joins.$where; @@ -2102,30 +2149,34 @@ class MySqlCompiler extends SqlCompiler { $dir = 'DESC'; $sort = substr($sort, 1); } - list($field) = $this->getField($sort, $model); + // If the field is already an annotation, then don't + // compile the annotation again below. It's included in + // the select clause, which is sufficient + if (isset($this->annotations[$sort])) + $field = $this->quote($sort); + else + list($field) = $this->getField($sort, $model); } - // TODO: Throw exception if $field can be indentified as - // invalid if ($field instanceof SqlFunction) $field = $field->toSql($this, $model); + // TODO: Throw exception if $field can be indentified as + // invalid - $orders[] = $field.' '.$dir; + $orders[] = "{$field} {$dir}"; } $sort = ' ORDER BY '.implode(', ', $orders); } // Compile the field listing - $fields = array(); - $group_by = array(); - $model::_inspect(); - $table = $this->quote($model::$meta['table']).' '.$rootAlias; + $fields = $group_by = array(); + $table = $this->quote($model::getMeta('table')).' '.$rootAlias; // Handle related tables if ($queryset->related) { $count = 0; $fieldMap = $theseFields = array(); $defer = $queryset->defer ?: array(); // Add local fields first - foreach ($model::$meta['fields'] as $f) { + foreach ($model::getMeta('fields') as $f) { // Handle deferreds if (isset($defer[$f])) continue; @@ -2147,8 +2198,7 @@ class MySqlCompiler extends SqlCompiler { $theseFields = array(); list($alias, $fmodel) = $this->getField($full_path, $model, array('table'=>true, 'model'=>true)); - $fmodel::_inspect(); - foreach ($fmodel::$meta['fields'] as $f) { + foreach ($fmodel::getMeta('fields') as $f) { // Handle deferreds if (isset($defer[$sr . '__' . $f])) continue; @@ -2183,10 +2233,9 @@ class MySqlCompiler extends SqlCompiler { } } // Simple selection from one table - else { + elseif (!$queryset->aggregated) { if ($queryset->defer) { - $model::_inspect(); - foreach ($model::$meta['fields'] as $f) { + foreach ($model::getMeta('fields') as $f) { if (isset($queryset->defer[$f])) continue; $fields[$rootAlias .'.'. $this->quote($f)] = true; @@ -2214,7 +2263,7 @@ class MySqlCompiler extends SqlCompiler { } // If no group by has been set yet, use the root model pk if (!$group_by) { - foreach ($model::$meta['pk'] as $pk) + foreach ($model::getMeta('pk') as $pk) $group_by[] = $rootAlias .'.'. $pk; } } @@ -2264,8 +2313,8 @@ class MySqlCompiler extends SqlCompiler { } function compileUpdate(VerySimpleModel $model) { - $pk = $model::$meta['pk']; - $sql = 'UPDATE '.$this->quote($model::$meta['table']); + $pk = $model::getMeta('pk'); + $sql = 'UPDATE '.$this->quote($model::getMeta('table')); $sql .= $this->__compileUpdateSet($model, $pk); // Support PK updates $criteria = array(); @@ -2279,15 +2328,15 @@ class MySqlCompiler extends SqlCompiler { } function compileInsert(VerySimpleModel $model) { - $pk = $model::$meta['pk']; - $sql = 'INSERT INTO '.$this->quote($model::$meta['table']); + $pk = $model::getMeta('pk'); + $sql = 'INSERT INTO '.$this->quote($model::getMeta('table')); $sql .= $this->__compileUpdateSet($model, $pk); return new MySqlExecutor($sql, $this->params); } function compileDelete($model) { - $table = $model::$meta['table']; + $table = $model::getMeta('table'); $where = ' WHERE '.implode(' AND ', $this->compileConstraints(array(new Q($model->pk)), $model)); @@ -2297,7 +2346,7 @@ class MySqlCompiler extends SqlCompiler { function compileBulkDelete($queryset) { $model = $queryset->model; - $table = $model::$meta['table']; + $table = $model::getMeta('table'); list($where, $having) = $this->getWhereHavingClause($queryset); $joins = $this->getJoins($queryset); $sql = 'DELETE '.$this->quote($table).'.* FROM ' @@ -2307,7 +2356,7 @@ class MySqlCompiler extends SqlCompiler { function compileBulkUpdate($queryset, array $what) { $model = $queryset->model; - $table = $model::$meta['table']; + $table = $model::getMeta('table'); $set = array(); foreach ($what as $field=>$value) $set[] = sprintf('%s = %s', $this->quote($field), $this->input($value)); @@ -2394,7 +2443,7 @@ class MySqlExecutor { $types = ''; $ps = array(); - foreach ($params as &$p) { + foreach ($params as $i=>&$p) { if (is_int($p) || is_bool($p)) $types .= 'i'; elseif (is_float($p)) diff --git a/include/class.thread.php b/include/class.thread.php index 00623c85eb1020c12bc70e7500a8bed0ea191c20..8777211cd56b641453027d5e83b6cc591637e279 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -86,15 +86,20 @@ class Thread extends VerySimpleModel { return $this->entries->count(); } + var $_entries; function getEntries($criteria=false) { - $base = $this->entries->annotate(array( - 'has_attachments' => SqlAggregate::COUNT('attachments', false, - new Q(array('attachments__inline'=>0))) - )); - $base->exclude(array('flags__hasbit'=>ThreadEntry::FLAG_HIDDEN)); - if ($criteria) - $base->filter($criteria); - return $base; + if (!isset($this->_entries)) { + $this->_entries = $this->entries->annotate(array( + 'has_attachments' => SqlAggregate::COUNT(SqlCase::N() + ->when(array('attachments__inline'=>0), 1) + ->otherwise(null) + ), + )); + $this->_entries->exclude(array('flags__hasbit'=>ThreadEntry::FLAG_HIDDEN)); + if ($criteria) + $this->_entries->filter($criteria); + } + return $this->_entries; } // Collaborators @@ -134,7 +139,7 @@ class Thread extends VerySimpleModel { return $collaborators; } - function addCollaborator($user, $vars, &$errors) { + function addCollaborator($user, $vars, &$errors, $event=true) { if (!$user) return null; @@ -147,6 +152,16 @@ class Thread extends VerySimpleModel { $this->_collaborators = null; + if ($event) + $this->getEvents()->log($this->getObject(), + 'collab', + array('add' => array($user->getId() => array( + 'name' => $user->getName()->getOriginal(), + 'src' => @$vars['source'], + )) + ) + ); + return $c; } @@ -164,11 +179,9 @@ class Thread extends VerySimpleModel { && $c->delete()) $collabs[] = $c; } - - $this->getObject()->postThreadEntry('N', - array( - 'title' => _S('Collaborators Removed'), - 'note' => implode("<br>", $collabs))); + $this->getEvents()->log($this->getObject(), 'collab', array( + 'del' => array($c->user_id => array('name' => $c->getName()->getOriginal())) + )); } //statuses @@ -205,6 +218,11 @@ class Thread extends VerySimpleModel { if ($type && is_array($type)) $entries->filter(array('type__in' => $type)); + // Precache all the attachments on this thread + AttachmentFile::objects()->filter(array( + 'attachments__thread_entry__thread__id' => $this->id + ))->all(); + $events = $this->getEvents(); $inc = ($mode == self::MODE_STAFF) ? STAFFINC_DIR : CLIENTINC_DIR; include $inc . 'templates/thread-entries.tmpl.php'; @@ -1545,83 +1563,15 @@ class ThreadEvent extends VerySimpleModel { } function getDescription($mode=self::MODE_STAFF) { - static $descs; - if (!isset($descs)) - $descs = array( - 'assigned' => __('Assignee changed by <b>{username}</b> to <strong>{assignees}</strong> {timestamp}'), - 'assigned:staff' => __('<b>{username}</b> assigned this to <strong>{<Staff>data.staff}</strong> {timestamp}'), - 'assigned:team' => __('<b>{username}</b> assigned this to <strong>{<Team>data.team}</strong> {timestamp}'), - 'assigned:claim' => __('<b>{username}</b> claimed this {timestamp}'), - 'collab:org' => __('Collaborators for {<Organization>data.org} organization added'), - 'collab:del' => function($evt) { - $data = $evt->getData(); - $base = __('<b>{username}</b> removed %s from the collaborators.'); - return $data['del'] - ? Format::htmlchars(sprintf($base, implode(', ', $data['del']))) - : 'somebody'; - }, - 'collab:add' => function($evt) { - $data = $evt->getData(); - $base = __('<b>{username}</b> added <strong>%s</strong> as collaborators {timestamp}'); - $collabs = array(); - if ($data['add']) { - foreach ($data['add'] as $c) { - if (is_array($c)) - $c = sprintf(__("%s via %a" - /* e.g. "Me <me@company.me> via Email (to)" */), - $c[0], $c[1]); - $collabs[] = Format::htmlchars($c); - } - } - return $collabs - ? sprintf($base, implode(', ', $collabs)) - : 'somebody'; - }, - 'created' => __('Created by <b>{username}</b> {timestamp}'), - 'closed' => __('Closed by <b>{username}</b> {timestamp}'), - 'reopened' => __('Reopened by <b>{username}</b> {timestamp}'), - 'edited:owner' => __('<b>{username}</b> changed ownership to {<User>data.owner} {timestamp}'), - 'edited:status' => __('<b>{username}</b> changed the status to <strong>{<TicketStatus>data.status}</strong> {timestamp}'), - 'overdue' => __('Flagged as overdue by the system {timestamp}'), - 'transferred' => __('<b>{username}</b> transferred this to <strong>{dept}</strong> {timestamp}'), - 'edited:fields' => function($evt) use ($mode) { - $base = __('Updated by <b>{username}</b> {timestamp} — %s'); - $data = $evt->getData(); - $fields = $changes = array(); - foreach (DynamicFormField::objects()->filter(array( - 'id__in' => array_keys($data['fields']) - )) as $F) { - $fields[$F->id] = $F; - } - foreach ($data['fields'] as $id=>$f) { - $field = $fields[$id]; - if ($mode == self::MODE_CLIENT && !$field->isVisibleToUsers()) - continue; - list($old, $new) = $f; - $impl = $field->getImpl($field); - $before = $impl->to_php($old); - $after = $impl->to_php($new); - $changes[] = sprintf('<strong>%s</strong> %s', - $field->getLocal('label'), $impl->whatChanged($before, $after)); - } - if (!$changes) - return ''; - return sprintf($base, implode(', ', $changes)); - }, - 'resent' => __('<b>{username}</b> resent <strong><a href="#thread-entry-{data.entry}">a previous response</a></strong> {timestamp}'), - ); - $self = $this; - $data = $this->getData(); - $state = $this->state; - if (is_array($data)) { - foreach (array_keys($data) as $k) - if (isset($descs[$state . ':' . $k])) - $state .= ':' . $k; - } - $description = $descs[$state]; - if (is_callable($description)) - $description = $description($this); + // Abstract description + return $this->template(sprintf( + __('%s by {somebody} {timestamp}'), + $this->state + )); + } + function template($description) { + $self = $this; return preg_replace_callback('/\{(<(?P<type>([^>]+))>)?(?P<key>[^}.]+)(\.(?P<data>[^}]+))?\}/', function ($m) use ($self) { switch ($m['key']) { @@ -1636,7 +1586,7 @@ class ThreadEvent extends VerySimpleModel { $assignees[] = $T->getLocalName(); } return implode('/', $assignees); - case 'username': + case 'somebody': $name = $self->getUserName(); if ($url = $self->getAvatar()) $name = "<img class=\"avatar\" src=\"{$url}\"> ".$name; @@ -1680,7 +1630,7 @@ class ThreadEvent extends VerySimpleModel { function render($mode) { $inc = ($mode == self::MODE_STAFF) ? STAFFINC_DIR : CLIENTINC_DIR; - $event = $this; + $event = $this->getTypedEvent(); include $inc . 'templates/thread-event.tmpl.php'; } @@ -1711,6 +1661,22 @@ class ThreadEvent extends VerySimpleModel { ), $user); return $inst; } + + function getTypedEvent() { + static $subclasses; + + if (!isset($subclasses)) { + $parent = get_class($this); + $subclasses = array(); + foreach (get_declared_classes() as $class) { + if (is_subclass_of($class, $parent)) + $subclasses[$class::$state] = $class; + } + } + if (!($class = $subclasses[$this->state])) + return $this; + return new $class($this->ht); + } } class ThreadEvents extends InstrumentedList { @@ -1785,6 +1751,186 @@ class ThreadEvents extends InstrumentedList { } } +class AssignmentEvent extends ThreadEvent { + static $icon = 'hand-right'; + static $state = 'assigned'; + + function getDescription($mode=self::MODE_STAFF) { + $data = $this->getData(); + switch (true) { + case !is_array($data): + default: + $desc = __('Assignee changed by <b>{somebody}</b> to <strong>{assignees}</strong> {timestamp}'); + break; + case isset($data['staff']): + $desc = __('<b>{somebody}</b> assigned this to <strong>{<Staff>data.staff}</strong> {timestamp}'); + break; + case isset($data['team']): + $desc = __('<b>{somebody}</b> assigned this to <strong>{<Team>data.team}</strong> {timestamp}'); + break; + case isset($data['claim']): + $desc = __('<b>{somebody}</b> claimed this {timestamp}'); + break; + } + return $this->template($desc); + } +} + +class CloseEvent extends ThreadEvent { + static $icon = 'thumbs-up-alt'; + static $state = 'closed'; + + function getDescription($mode=self::MODE_STAFF) { + return $this->template(__('Closed by <b>{somebody}</b> {timestamp}')); + } +} + +class CollaboratorEvent extends ThreadEvent { + static $icon = 'group'; + static $state = 'collab'; + + function getDescription($mode=self::MODE_STAFF) { + $data = $this->getData(); + switch (true) { + case isset($data['org']): + $desc = __('Collaborators for {<Organization>data.org} organization added'); + break; + case isset($data['del']): + $base = __('<b>{somebody}</b> removed <strong>%s</strong> from the collaborators {timestamp}'); + $collabs = array(); + $users = User::objects()->filter(array('id__in' => array_keys($data['del']))); + foreach ($data['del'] as $id=>$c) { + $U = false; + foreach ($users as $user) { + if ($user->id == $id) { + $U = $user; + break; + } + } + $collabs[] = Format::htmlchars($U ? $U->getName() : @$c['name'] ?: $c); + } + $desc = sprintf($base, implode(', ', $collabs)); + break; + case isset($data['add']): + $base = __('<b>{somebody}</b> added <strong>%s</strong> as collaborators {timestamp}'); + $collabs = array(); + if ($data['add']) { + $users = User::objects()->filter(array('id__in' => array_keys($data['add']))); + foreach ($data['add'] as $id=>$c) { + $U = false; + foreach ($users as $user) { + if ($user->id == $id) { + $U = $user; + break; + } + } + $c = sprintf(__("%s via %s" + /* e.g. "Me <me@company.me> via Email (to)" */), + Format::htmlchars($U ? $U->getName() : @$c['name'] ?: $c), + $c['src'] ?: '?' + ); + $collabs[] = $c; + } + } + $desc = $collabs + ? sprintf($base, implode(', ', $collabs)) + : 'somebody'; + break; + } + return $this->template($desc); + } +} + +class CreationEvent extends ThreadEvent { + static $icon = 'magic'; + static $state = 'created'; + + function getDescription($mode=self::MODE_STAFF) { + return $this->template(__('Created by <b>{somebody}</b> {timestamp}')); + } +} + +class EditEvent extends ThreadEvent { + static $icon = 'pencil'; + static $state = 'edited'; + + function getDescription($mode=self::MODE_STAFF) { + $data = $this->getData(); + switch (true) { + case isset($data['owner']): + $desc = __('<b>{somebody}</b> changed ownership to {<User>data.owner} {timestamp}'); + break; + case isset($data['status']): + $desc = __('<b>{somebody}</b> changed the status to <strong>{<TicketStatus>data.status}</strong> {timestamp}'); + break; + case isset($data['fields']): + $base = __('Updated by <b>{somebody}</b> {timestamp} — %s'); + $fields = $changes = array(); + foreach (DynamicFormField::objects()->filter(array( + 'id__in' => array_keys($data['fields']) + )) as $F) { + $fields[$F->id] = $F; + } + foreach ($data['fields'] as $id=>$f) { + $field = $fields[$id]; + if ($mode == self::MODE_CLIENT && !$field->isVisibleToUsers()) + continue; + list($old, $new) = $f; + $impl = $field->getImpl($field); + $before = $impl->to_php($old); + $after = $impl->to_php($new); + $changes[] = sprintf('<strong>%s</strong> %s', + $field->getLocal('label'), $impl->whatChanged($before, $after)); + } + $desc = $changes + ? sprintf($base, implode(', ', $changes)) : ''; + break; + } + + return $this->template($desc); + } +} + +class OverdueEvent extends ThreadEvent { + static $icon = 'time'; + static $state = 'overdue'; + + function getDescription($mode=self::MODE_STAFF) { + return $this->template(__('Flagged as overdue by the system {timestamp}')); + } +} + +class ReopenEvent extends ThreadEvent { + static $icon = 'rotate-right'; + static $state = 'reopened'; + + function getDescription($mode=self::MODE_STAFF) { + return $this->template(__('Reopened by <b>{somebody}</b> {timestamp}')); + } +} + +class ResendEvent extends ThreadEvent { + static $icon = 'reply-all icon-flip-horizontal'; + static $state = 'resent'; + + function getDescription($mode=self::MODE_STAFF) { + return $this->template(__('<b>{somebody}</b> resent <strong><a href="#thread-entry-{data.entry}">a previous response</a></strong> {timestamp}')); + } +} + +class TransferEvent extends ThreadEvent { + static $icon = 'share-alt'; + static $state = 'transferred'; + + function getDescription($mode=self::MODE_STAFF) { + return $this->template(__('<b>{somebody}</b> transferred this to <strong>{dept}</strong> {timestamp}')); + } +} + +class ViewEvent extends ThreadEvent { + static $state = 'viewed'; +} + class ThreadEntryBody /* extends SplString */ { static $types = array('text', 'html'); @@ -2125,8 +2271,6 @@ class NoteThreadEntry extends ThreadEntry { // Object specific thread utils. class ObjectThread extends Thread implements TemplateVariable { - private $_entries = array(); - static $types = array( ObjectModel::OBJECT_TYPE_TASK => 'TaskThread', ObjectModel::OBJECT_TYPE_TICKET => 'TicketThread', diff --git a/include/class.ticket.php b/include/class.ticket.php index 8a771c3237eb81f0333b7af69855f358432bd54e..35e5f1e794f01d83b20163f449cd083c1ff535ed 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -208,7 +208,8 @@ class Ticket extends TicketModel implements RestrictedAccess, Threadable { static $meta = array( - 'select_related' => array('topic', 'staff', 'user', 'team', 'dept', 'sla', 'thread'), + 'select_related' => array('topic', 'staff', 'user', 'team', 'dept', 'sla', 'thread', + 'user__default_email'), ); var $lastMsgId; @@ -229,12 +230,15 @@ implements RestrictedAccess, Threadable { function loadDynamicData() { if (!isset($this->_answers)) { $this->_answers = array(); - foreach (DynamicFormEntry::forTicket($this->getId(), true) as $form) { - foreach ($form->getAnswers() as $answer) { - $tag = mb_strtolower($answer->field->name) - ?: 'field.' . $answer->field->id; - $this->_answers[$tag] = $answer; - } + foreach (DynamicFormEntryAnswer::objects() + ->filter(array( + 'entry__object_id' => $this->getId(), + 'entry__object_type' => 'T' + )) as $answer + ) { + $tag = mb_strtolower($answer->field->name) + ?: 'field.' . $answer->field->id; + $this->_answers[$tag] = $answer; } } return $this->_answers; @@ -804,17 +808,10 @@ implements RestrictedAccess, Threadable { if (!$user || $user->getId() == $this->getOwnerId()) return null; - $vars = array_merge(array( - 'threadId' => $this->getThreadId(), - 'userId' => $user->getId()), $vars); - if (!($c=Collaborator::add($vars, $errors))) - return null; - - $this->collaborators = null; - $this->recipients = null; - - if ($event) - $this->logEvent('collab', array('add' => array($c->toString()))); + if ($c = $this->getThread()->addCollaborator($user, $vars, $errors, $event)) { + $this->collaborators = null; + $this->recipients = null; + } return $c; } @@ -2070,7 +2067,10 @@ implements RestrictedAccess, Threadable { if ($c=$this->addCollaborator($user, $info, $errors, false)) // FIXME: This feels very unwise — should be a // string indexed array for future - $collabs[] = array((string)$c, $recipient['source']); + $collabs[$c->user_id] = array( + 'name' => $c->getName()->getOriginal(), + 'src' => $recipient['source'], + ); } // TODO: Can collaborators add others? if ($collabs) { diff --git a/include/client/faq-category.inc.php b/include/client/faq-category.inc.php index eb6606af8b261f9daf9d3e8fed1d17c345423a90..3ce0b7230dcdff9e3ded235c902b8db55b2f9315 100644 --- a/include/client/faq-category.inc.php +++ b/include/client/faq-category.inc.php @@ -13,9 +13,11 @@ if(!defined('OSTCLIENTINC') || !$category || !$category->isPublic()) die('Access <?php $faqs = FAQ::objects() ->filter(array('category'=>$category)) - ->exclude(array('ispublished'=>false)) - ->annotate(array('has_attachments'=>SqlAggregate::COUNT('attachments', false, - array('attachments__inline'=>0)))) + ->exclude(array('ispublished'=>FAQ::VISIBILITY_PRIVATE)) + ->annotate(array('has_attachments' => SqlAggregate::COUNT(SqlCase::N() + ->when(array('attachments__inline'=>0), 1) + ->otherwise(null) + ))) ->order_by('-ispublished', 'question'); if ($faqs->exists(true)) { diff --git a/include/client/kb-categories.inc.php b/include/client/kb-categories.inc.php index c6cc4e930aad3076b4237db87f8d932d6aac1df6..6129180fa8b9a01cf8a66c0e124bdc1c514d447b 100644 --- a/include/client/kb-categories.inc.php +++ b/include/client/kb-categories.inc.php @@ -2,10 +2,13 @@ <div class="span8"> <?php $categories = Category::objects() - ->exclude(Q::any(array('ispublic'=>false, 'faqs__ispublished'=>false))) + ->exclude(Q::any(array( + 'ispublic'=>Category::VISIBILITY_PRIVATE, + 'faqs__ispublished'=>FAQ::VISIBILITY_PRIVATE, + ))) ->annotate(array('faq_count'=>SqlAggregate::COUNT('faqs'))) ->filter(array('faq_count__gt'=>0)); - if ($categories->all()) { ?> + if ($categories->exists(true)) { ?> <div><?php echo __('Click on the category to browse FAQs.'); ?></div> <ul id="kb"> <?php @@ -18,7 +21,7 @@ <?php echo Format::safe_html($C->getLocalDescriptionWithImages()); ?> </div> <?php foreach ($C->faqs - ->exclude(array('ispublished'=>false)) + ->exclude(array('ispublished'=>FAQ::VISIBILITY_PRIVATE)) ->order_by('-views')->limit(5) as $F) { ?> <div class="popular-faq"><i class="icon-file-alt"></i> <a href="faq.php?id=<?php echo $F->getId(); ?>"> diff --git a/include/client/kb-search.inc.php b/include/client/kb-search.inc.php index 5166a6616fbd5ee2ff677124e46aeaaae15b0a2c..a81f5e4fdea5f7064c1f5dacbeb88fab99fb296c 100644 --- a/include/client/kb-search.inc.php +++ b/include/client/kb-search.inc.php @@ -5,7 +5,7 @@ <?php if ($faqs->exists(true)) { echo '<div id="faq">'.sprintf(__('%d FAQs matched your search criteria.'), - count($faqs->all())) + $faqs->count()) .'<ol>'; foreach ($faqs as $F) { echo sprintf( diff --git a/include/staff/templates/thread-entry.tmpl.php b/include/staff/templates/thread-entry.tmpl.php index e339e04555ebcbddc5d823f3edd6b7472956f080..62cd3e3362509d15f13faee81508f9b51060059d 100644 --- a/include/staff/templates/thread-entry.tmpl.php +++ b/include/staff/templates/thread-entry.tmpl.php @@ -60,7 +60,7 @@ if ($user && ($url = $user->get_gravatar(48))) echo $entry->title; ?></span> </span> </div> - <div class="thread-body"> + <div class="thread-body no-pjax"> <div><?php echo $entry->getBody()->toHtml(); ?></div> <div class="clear"></div> <?php diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php index da1d522122e01fd806a756355ade4b2a43fa0ccf..9fe9fe5bcc6af1a06f884cd754260d26541d4208 100644 --- a/include/staff/tickets.inc.php +++ b/include/staff/tickets.inc.php @@ -274,23 +274,21 @@ TicketForm::ensureDynamicDataView(); // Select pertinent columns // ------------------------------------------------------------ -$tickets->values('lock__staff_id', 'staff_id', 'isoverdue', 'team_id', 'ticket_id', 'number', 'cdata__subject', 'user__default_email__address', 'source', 'cdata__:priority__priority_color', 'cdata__:priority__priority_desc', 'status_id', 'status__name', 'status__state', 'dept_id', 'dept__name', 'user__name', 'lastupdate'); +$tickets->values('lock__staff_id', 'staff_id', 'isoverdue', 'team_id', 'ticket_id', 'number', 'cdata__subject', 'user__default_email__address', 'source', 'cdata__:priority__priority_color', 'cdata__:priority__priority_desc', 'status_id', 'status__name', 'status__state', 'dept_id', 'dept__name', 'user__name', 'lastupdate', 'isanswered'); // Add in annotations $tickets->annotate(array( - 'collab_count' => SqlAggregate::COUNT('thread__collaborators', true), - 'attachment_count' => SqlAggregate::COUNT(SqlCase::N() - ->when(new SqlField('thread__entries__attachments__inline'), null) - ->otherwise(new SqlField('thread__entries__attachments')), - true - ), - 'thread_count' => SqlAggregate::COUNT(SqlCase::N() - ->when( - new Q(array('thread__entries__flags__hasbit'=>ThreadEntry::FLAG_HIDDEN)), - null) - ->otherwise(new SqlField('thread__entries__id')), - true - ), + 'collab_count' => TicketThread::objects() + ->filter(array('ticket__ticket_id' => new SqlField('ticket_id'))) + ->aggregate(array('count' => SqlAggregate::COUNT('collaborators__id'))), + 'attachment_count' => TicketThread::objects() + ->filter(array('ticket__ticket_id' => new SqlField('ticket_id'))) + ->filter(array('entries__attachments__inline' => 0)) + ->aggregate(array('count' => SqlAggregate::COUNT('entries__attachments__id'))), + 'thread_count' => TicketThread::objects() + ->filter(array('ticket__ticket_id' => new SqlField('ticket_id'))) + ->filter(Q::not(array('entries__flags__hasbit' => ThreadEntry::FLAG_HIDDEN))) + ->aggregate(array('count' => SqlAggregate::COUNT('entries__id'))), )); // Save the query to the session for exporting diff --git a/js/osticket.js b/js/osticket.js index 28fd56fc8fd5df9153eac06242e8116b8cdf5dc0..0da43e7d94a9836687caf0d45dcdcbe27fa26df8 100644 --- a/js/osticket.js +++ b/js/osticket.js @@ -133,6 +133,10 @@ $(document).ready(function(){ // TODO: Add a hover-button to show just one image }); }); + + $('div.thread-body a').each(function() { + $(this).attr('target', '_blank'); + }); }); showImagesInline = function(urls, thread_id) { diff --git a/scp/js/ticket.js b/scp/js/ticket.js index 3be939612745b3aec9ae319af1903d885cbd918f..e7fd1e3693102def1502d18169471383cee5da08 100644 --- a/scp/js/ticket.js +++ b/scp/js/ticket.js @@ -448,6 +448,10 @@ var ticket_onload = function($) { fx.end = last_entry.offset().top - 50; } }); + + $('div.thread-body a').each(function() { + $(this).attr('target', '_blank'); + }); }; $(ticket_onload); $(document).on('pjax:success', function() { ticket_onload(jQuery); });