diff --git a/include/class.dept.php b/include/class.dept.php index e1ee486a3e1c095589e5c40043d0dd2ce5541feb..8abf4919468b4c9a9df8ec3f3dc84e37187f9c6c 100644 --- a/include/class.dept.php +++ b/include/class.dept.php @@ -20,6 +20,10 @@ class Dept extends VerySimpleModel { 'table' => DEPT_TABLE, 'pk' => array('id'), 'joins' => array( + 'parent' => array( + 'constraint' => array('pid' => 'Dept.id'), + 'null' => true, + ), 'email' => array( 'constraint' => array('email_id' => 'EmailModel.email_id'), 'null' => true, @@ -90,6 +94,10 @@ class Dept extends VerySimpleModel { return _H(sprintf('dept.%s.%s', $subtag, $this->getId())); } + function getFullName() { + return self::getNameById($this->getId()); + } + function getEmailId() { return $this->email_id; } @@ -343,6 +351,34 @@ class Dept extends VerySimpleModel { return $this->getName(); } + function getParent() { + return static::lookup($this->ht['pid']); + } + + /** + * getFullPath + * + * Utility function to retrieve a '/' separated list of department IDs + * in the ancestry of this department. This is used to populate the + * `path` field in the database and is used for access control rather + * than the ID field since nesting of departments is necessary and + * department access can be cascaded. + * + * Returns: + * Slash-separated string of ID ancestry of this department. The string + * always starts and ends with a slash, and will always contain the ID + * of this department last. + */ + function getFullPath() { + $path = ''; + if ($p = $this->getParent()) + $path .= $p->getFullPath(); + else + $path .= '/'; + $path .= $this->getId() . '/'; + return $path; + } + /*----Static functions-------*/ static function getIdByName($name) { $row = static::objects() @@ -354,11 +390,8 @@ class Dept extends VerySimpleModel { } function getNameById($id) { - - if($id && ($dept=static::lookup($id))) - $name= $dept->getName(); - - return $name; + $names = static::getDepartments(); + return $names[$id]; } function getDefaultDeptName() { @@ -371,10 +404,11 @@ class Dept extends VerySimpleModel { : null; } - static function getDepartments( $criteria=null) { + static function getDepartments( $criteria=null, $localize=true) { static $depts = null; if (!isset($depts) || $criteria) { + // XXX: This will upset the static $depts array $depts = array(); $query = self::objects(); if (isset($criteria['publiconly'])) @@ -394,17 +428,39 @@ class Dept extends VerySimpleModel { } $query->order_by('name') - ->values_flat('id', 'name'); + ->values('id', 'pid', 'name', 'parent'); - $names = array(); foreach ($query as $row) - $names[$row[0]] = $row[1]; + $depts[$row['id']] = $row; + + $localize_this = function($id, $default) use ($localize) { + if (!$localize) + return $default; + + $tag = _H("dept.name.{$id}"); + $T = CustomDataTranslation::translate($tag); + return $T != $tag ? $T : $default; + }; - // Fetch local names - foreach (CustomDataTranslation::getDepartmentNames(array_keys($names)) as $id=>$name) { - // Translate the department - $names[$id] = $name; + // Resolve parent names + $names = array(); + foreach ($depts as $id=>$info) { + $name = $info['name']; + $loop = array($id=>true); + $parent = false; + while ($info['pid'] && ($info = $depts[$info['pid']])) { + $name = sprintf('%s / %s', $info['name'], $name); + if (isset($loop[$info['pid']])) + break; + $loop[$info['pid']] = true; + $parent = $info; + } + // Fetch local names + $names[$id] = $localize_this($id, $name); } + asort($names); + + // TODO: Use locale-aware sorting mechanism if ($criteria) return $names; @@ -460,9 +516,13 @@ class Dept extends VerySimpleModel { if (!$vars['ispublic'] && $cfg && ($vars['id']==$cfg->getDefaultDeptId())) $errors['ispublic']=__('System default department cannot be private'); + if ($vars['pid'] && !($p = static::lookup($vars['pid']))) + $errors['pid'] = __('Department selection is required'); + if ($errors) return false; + $this->pid = $vars['pid']; $this->updated = SqlFunction::NOW(); $this->ispublic = isset($vars['ispublic'])?$vars['ispublic']:0; $this->email_id = isset($vars['email_id'])?$vars['email_id']:0; diff --git a/include/class.orm.php b/include/class.orm.php index 37178b276c6513cedd70458ca340f7020f19adc1..6ed7d52f164bd5f2687207ab78a1bb425759a3db 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -1243,17 +1243,8 @@ class InstrumentedList extends ModelInstanceManager { } // QuerySet overriedes - function filter() { - return call_user_func_array(array($this->objects(), 'filter'), func_get_args()); - } - function exclude() { - return call_user_func_array(array($this->objects(), 'exclude'), func_get_args()); - } - function order_by() { - return call_user_func_array(array($this->objects(), 'order_by'), func_get_args()); - } - function limit($how) { - return $this->objects()->limit($how); + function __call($what, $how) { + return call_user_func_array(array($this->objects(), $what), $how); } } diff --git a/include/class.staff.php b/include/class.staff.php index 048db0984691bb4837a9bbf9f3480757a1cb91f1..9a8ac128b05ca7f6842c0ea361c2bb3f071608c8 100644 --- a/include/class.staff.php +++ b/include/class.staff.php @@ -208,28 +208,50 @@ implements AuthenticatedUser { } function getDepartments() { + // TODO: Cache this in the agent's session as it is unlikely to + // change while logged in if (!isset($this->departments)) { // Departments the staff is "allowed" to access... // based on the group they belong to + user's primary dept + user's managed depts. - $dept_ids = array(); - $depts = Dept::objects() - ->filter(Q::any(array( - 'id' => $this->dept_id, - 'groups__group_id' => $this->group_id, - 'manager_id' => $this->getId(), - ))) + $sql='SELECT DISTINCT d.id FROM '.STAFF_TABLE.' s ' + .' LEFT JOIN '.GROUP_DEPT_TABLE.' g ON (s.group_id=g.group_id) ' + .' INNER JOIN '.DEPT_TABLE.' d ON (LOCATE(CONCAT("/", s.dept_id, "/"), d.path) OR d.manager_id=s.staff_id OR LOCATE(CONCAT("/", g.dept_id, "/"), d.path)) ' + .' WHERE s.staff_id='.db_input($this->getId()); + $depts = array(); + if (($res=db_query($sql)) && db_num_rows($res)) { + while(list($id)=db_fetch_row($res)) + $depts[] = $id; + } + + /* ORM method — about 2.0ms slower + $q = Q::any(array( + 'path__contains' => '/'.$this->dept_id.'/', + 'manager_id' => $this->getId(), + )); + // Add in group access + foreach ($this->group->depts->values_flat('dept_id') as $row) { + // Skip primary dept + if ($row[0] == $this->dept_id) + continue; + $q->add(new Q(array('path__contains'=>'/'.$row[0].'/'))); + } + + $dept_ids = Dept::objects() + ->filter($q) + ->distinct('id') ->values_flat('id'); - foreach ($depts as $row) - $dept_ids[] = $row[0]; + foreach ($dept_ids as $row) + $depts[] = $row[0]; + */ - if (!$dept_ids) { //Neptune help us! (fallback) - $dept_ids = array_merge($this->getGroup()->getDepartments(), array($this->getDeptId())); + if (!$depts) { //Neptune help us! (fallback) + $depts = array_merge($this->getGroup()->getDepartments(), array($this->getDeptId())); } - $this->departments = array_filter(array_unique($dept_ids)); + $this->departments = $depts; } return $this->departments; diff --git a/include/class.ticket.php b/include/class.ticket.php index eb46e86c4278659806e2f54349063885b08c7964..44d3d7a7dac4a5722073fe1ab521bb42333e6d70 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -487,7 +487,7 @@ class Ticket { function getDeptName() { if(!$this->ht['dept_name'] && ($dept = $this->getDept())) - $this->ht['dept_name'] = $dept->getName(); + $this->ht['dept_name'] = $dept->getFullName(); return $this->ht['dept_name']; } diff --git a/include/class.translation.php b/include/class.translation.php index 141e13051c98dd709bce0a4bcc705a1aaba7751e..1c09dbb6aa29a31b63df9ed210a9dd582aa1534e 100644 --- a/include/class.translation.php +++ b/include/class.translation.php @@ -1032,29 +1032,6 @@ class CustomDataTranslation extends VerySimpleModel { return static::objects()->filter($criteria)->all(); } - - static function getDepartmentNames($ids) { - global $cfg; - - $tags = array(); - $names = array(); - foreach ($ids as $i) - $tags[_H('dept.name.'.$i)] = $i; - - if (($lang = Internationalization::getCurrentLanguage()) - && $lang != $cfg->getPrimaryLanguage() - ) { - foreach (CustomDataTranslation::objects()->filter(array( - 'object_hash__in'=>array_keys($tags), - 'lang'=>$lang - )) as $translation - ) { - $names[$tags[$translation->object_hash]] = $translation->text; - } - } - return $names; - } - } class CustomTextDomain { diff --git a/include/staff/department.inc.php b/include/staff/department.inc.php index 6828b06925d9bdee216ed73825707b5e8eafeae7..9b6d9d4636ef0740e6f13d5464402d6152b06fae 100644 --- a/include/staff/department.inc.php +++ b/include/staff/department.inc.php @@ -51,6 +51,23 @@ $info = Format::htmlchars(($errors && $_POST) ? $_POST : $info); </tr> </thead> <tbody> + <tr> + <td width="180"> + <?php echo __('Parent');?>: + </td> + <td> + <select name="pid"> + <option value="">— <?php echo __('Top-Level Deptartment'); ?> —</option> +<?php foreach (Dept::getDepartments() as $id=>$name) { + if ($info['id'] && $id == $info['id']) + continue; ?> + <option value="<?php echo $id; ?>" <?php + if ($info['pid'] == $id) echo 'selected="selected"'; + ?>><?php echo $name; ?></option> +<?php } ?> + </select> + </td> + </tr> <tr> <td width="180" class="required"> <?php echo __('Name');?>: diff --git a/include/staff/departments.inc.php b/include/staff/departments.inc.php index 20018b8dabde7baa4df3475a0951cd8681dca5f6..4b58fea69028f9636061a7cde8a041442cd24f35 100644 --- a/include/staff/departments.inc.php +++ b/include/staff/departments.inc.php @@ -100,7 +100,7 @@ $qstr.='&order='.($order=='DESC'?'ASC':'DESC'); <?php echo $default? 'disabled="disabled"' : ''; ?> > </td> <td><a href="departments.php?id=<?php echo $id; ?>"><?php - echo $dept->getName(); ?></a> <?php echo $default; ?></td> + echo Dept::getNameById($id); ?></a> <?php echo $default; ?></td> <td><?php echo $dept->isPublic() ? __('Public') :'<b>'.__('Private').'</b>'; ?></td> <td> <b> diff --git a/include/staff/helptopic.inc.php b/include/staff/helptopic.inc.php index 21dd951f3fa1d6f1ab48343775ab53f79b1c688c..ee8616919d13d61a0b3f38d6db876f93011115f0 100644 --- a/include/staff/helptopic.inc.php +++ b/include/staff/helptopic.inc.php @@ -118,12 +118,9 @@ if ($info['form_id'] == Topic::FORM_USE_PARENT) echo 'selected="selected"'; <select name="dept_id"> <option value="0">— <?php echo __('System Default'); ?> —</option> <?php - if (($depts=Dept::getDepartments())) { - foreach ($depts as $id => $name) { - $selected=($info['dept_id'] && $id==$info['dept_id'])?'selected="selected"':''; - echo sprintf('<option value="%d" %s>%s</option>', - $id, $selected, $name); - } + foreach (Dept::getDepartments() as $id=>$name) { + $selected=($info['dept_id'] && $id==$info['dept_id'])?'selected="selected"':''; + echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name); } ?> </select> diff --git a/include/staff/templates/sequence-manage.tmpl.php b/include/staff/templates/sequence-manage.tmpl.php index 6827cd6267d5d5f0c8cbae54a0af44cf162c91be..10ac7391ba63dba74a9f07ccb21fbe15e2dfba58 100644 --- a/include/staff/templates/sequence-manage.tmpl.php +++ b/include/staff/templates/sequence-manage.tmpl.php @@ -20,7 +20,7 @@ foreach ($sequences as $e) { <i class="icon-sort-by-order"></i> <div style="display:inline-block" class="name"> <?php echo $e->getName(); ?> </div> <div class="manage-buttons pull-right"> - <span class="faded">next</span> + <span class="faded"><?php echo __('next'); ?></span> <span class="current"><?php echo $e->current(); ?></span> </div> <div class="button-group"> diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig index 081d688556ac2572177f83cc5301da6512f45267..2888135a10a7347cf981c5bc3bd82404849bde5d 100644 --- a/include/upgrader/streams/core.sig +++ b/include/upgrader/streams/core.sig @@ -1 +1 @@ -e675900fdac39ec981a8d65fc82907b7 +e7a338f95ed622678123386a8a59dfc0 diff --git a/include/upgrader/streams/core/36f6b328-e675900f.cleanup.sql b/include/upgrader/streams/core/36f6b328-e7a338f9.cleanup.sql similarity index 100% rename from include/upgrader/streams/core/36f6b328-e675900f.cleanup.sql rename to include/upgrader/streams/core/36f6b328-e7a338f9.cleanup.sql diff --git a/include/upgrader/streams/core/36f6b328-e675900f.patch.sql b/include/upgrader/streams/core/36f6b328-e7a338f9.patch.sql similarity index 92% rename from include/upgrader/streams/core/36f6b328-e675900f.patch.sql rename to include/upgrader/streams/core/36f6b328-e7a338f9.patch.sql index 651b4cd407e8bc7941e6e00be2567bb769befcd7..0150b4fd1b4ad4e7a7106e56c32f0140406e6ca4 100644 --- a/include/upgrader/streams/core/36f6b328-e675900f.patch.sql +++ b/include/upgrader/streams/core/36f6b328-e7a338f9.patch.sql @@ -1,6 +1,6 @@ /** * @version v1.9.6 - * @signature e675900fdac39ec981a8d65fc82907b7 + * @signature e7a338f95ed622678123386a8a59dfc0 * @title Add support for ticket tasks * * This patch adds ability to thread anything and introduces tasks @@ -110,7 +110,14 @@ UPDATE `%TABLE_PREFIX%config` SET `key` = 'ticket_sequence_id' WHERE `key` = 'sequence_id' AND `namespace` = 'core'; +ALTER TABLE `%TABLE_PREFIX%department` + ADD `pid` int(11) unsigned default NULL AFTER `id`, + ADD `path` varchar(128) NOT NULL default '/' AFTER `message_auto_response`; + +UPDATE `%TABLE_PREFIX%department` + SET `path` = CONCAT('/', id, '/'); + -- Set new schema signature UPDATE `%TABLE_PREFIX%config` - SET `value` = 'e675900fdac39ec981a8d65fc82907b7' + SET `value` = 'e7a338f95ed622678123386a8a59dfc0' WHERE `key` = 'schema_signature' AND `namespace` = 'core'; diff --git a/include/upgrader/streams/core/36f6b328-e675900f.task.php b/include/upgrader/streams/core/36f6b328-e7a338f9.task.php similarity index 100% rename from include/upgrader/streams/core/36f6b328-e675900f.task.php rename to include/upgrader/streams/core/36f6b328-e7a338f9.task.php diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index d17ef3cdb1c998a20946d10787a955dcce2fe37b..9cc9a40035c59a836bddd2d926bf67023a1ef544 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -200,6 +200,7 @@ CREATE TABLE `%TABLE_PREFIX%list_items` ( DROP TABLE IF EXISTS `%TABLE_PREFIX%department`; CREATE TABLE `%TABLE_PREFIX%department` ( `id` int(11) unsigned NOT NULL auto_increment, + `pid` int(11) unsigned default NULL, `tpl_id` int(10) unsigned NOT NULL default '0', `sla_id` int(10) unsigned NOT NULL default '0', `email_id` int(10) unsigned NOT NULL default '0', @@ -211,6 +212,7 @@ CREATE TABLE `%TABLE_PREFIX%department` ( `group_membership` tinyint(1) NOT NULL default '0', `ticket_auto_response` tinyint(1) NOT NULL default '1', `message_auto_response` tinyint(1) NOT NULL default '0', + `path` varchar(128) NOT NULL default '/', `updated` datetime NOT NULL, `created` datetime NOT NULL, PRIMARY KEY (`id`),