1<?php
2/*********************************************************************
3    class.dept.php
4
5    Department class
6
7    Peter Rotich <peter@osticket.com>
8    Copyright (c)  2006-2013 osTicket
9    http://www.osticket.com
10
11    Released under the GNU General Public License WITHOUT ANY WARRANTY.
12    See LICENSE.TXT for details.
13
14    vim: expandtab sw=4 ts=4 sts=4:
15**********************************************************************/
16require_once INCLUDE_DIR . 'class.search.php';
17require_once INCLUDE_DIR.'class.role.php';
18
19class Dept extends VerySimpleModel
20implements TemplateVariable, Searchable {
21
22    static $meta = array(
23        'table' => DEPT_TABLE,
24        'pk' => array('id'),
25        'joins' => array(
26            'parent' => array(
27                'constraint' => array('pid' => 'Dept.id'),
28                'null' => true,
29            ),
30            'email' => array(
31                'constraint' => array('email_id' => 'Email.email_id'),
32                'null' => true,
33             ),
34            'sla' => array(
35                'constraint' => array('sla_id' => 'SLA.id'),
36                'null' => true,
37            ),
38            'manager' => array(
39                'null' => true,
40                'constraint' => array('manager_id' => 'Staff.staff_id'),
41            ),
42            'members' => array(
43                'null' => true,
44                'list' => true,
45                'reverse' => 'Staff.dept',
46            ),
47            'extended' => array(
48                'null' => true,
49                'list' => true,
50                'reverse' => 'StaffDeptAccess.dept'
51            ),
52        ),
53    );
54
55    var $_members;
56    var $_primary_members;
57    var $_extended_members;
58
59    var $_groupids;
60    var $config;
61
62    var $schedule;
63
64    var $template;
65    var $autorespEmail;
66
67    const ALERTS_DISABLED = 2;
68    const DISPLAY_DISABLED = 2;
69    const ALERTS_DEPT_AND_EXTENDED = 1;
70    const ALERTS_DEPT_ONLY = 0;
71    const ALERTS_ADMIN_ONLY = 3;
72
73    const FLAG_ASSIGN_MEMBERS_ONLY = 0x0001;
74    const FLAG_DISABLE_AUTO_CLAIM  = 0x0002;
75    const FLAG_ACTIVE = 0x0004;
76    const FLAG_ARCHIVED = 0x0008;
77    const FLAG_ASSIGN_PRIMARY_ONLY = 0x0010;
78    const FLAG_DISABLE_REOPEN_AUTO_ASSIGN = 0x0020;
79
80    const PERM_DEPT = 'visibility.departments';
81
82    static protected $perms = array(
83        self::PERM_DEPT => array(
84            'title' => /* @trans */ 'Department',
85            'desc'  => /* @trans */ 'Ability to see all Departments',
86            'primary' => true,
87        ),
88    );
89
90    function asVar() {
91        return $this->getName();
92    }
93
94    static function getVarScope() {
95        return array(
96            'name' => 'Department name',
97            'manager' => array(
98                'class' => 'Staff', 'desc' => 'Department manager',
99                'exclude' => 'dept',
100            ),
101            'members' => array(
102                'class' => 'UserList', 'desc' => 'Department members',
103            ),
104            'parent' => array(
105                'class' => 'Dept', 'desc' => 'Parent department',
106            ),
107            'sla' => array(
108                'class' => 'SLA', 'desc' => 'Service Level Agreement',
109            ),
110            'signature' => 'Department signature',
111        );
112    }
113
114    function getVar($tag) {
115        switch ($tag) {
116        case 'members':
117            return new UserList($this->getMembers()->all());
118        }
119    }
120
121    static function getSearchableFields() {
122        return array(
123            'name' => new TextboxField(array(
124                'label' => __('Name'),
125            )),
126            'manager' => new DepartmentManagerSelectionField(array(
127                'label' => __('Manager'),
128            )),
129        );
130    }
131
132    static function supportsCustomData() {
133        return false;
134    }
135
136    function getId() {
137        return $this->id;
138    }
139
140    function getName() {
141        return $this->name;
142    }
143
144    function getLocalName($locale=false) {
145        $tag = $this->getTranslateTag();
146        $T = CustomDataTranslation::translate($tag);
147        return $T != $tag ? $T : $this->name;
148    }
149    static function getLocalById($id, $subtag, $default) {
150        $tag = _H(sprintf('dept.%s.%s', $subtag, $id));
151        $T = CustomDataTranslation::translate($tag);
152        return $T != $tag ? $T : $default;
153    }
154    static function getLocalNameById($id, $default) {
155        return static::getLocalById($id, 'name', $default);
156    }
157
158    function getTranslateTag($subtag='name') {
159        return _H(sprintf('dept.%s.%s', $subtag, $this->getId()));
160    }
161
162    function getFullName() {
163        return self::getNameById($this->getId());
164    }
165
166    function getStatus() {
167        if($this->flags & self::FLAG_ACTIVE)
168          return __('Active');
169        elseif($this->flags & self::FLAG_ARCHIVED)
170          return __('Archived');
171        else
172          return __('Disabled');
173    }
174
175    function allowsReopen() {
176      return !($this->flags & self::FLAG_ARCHIVED);
177    }
178
179    function isActive() {
180        return !!($this->flags & self::FLAG_ACTIVE);
181    }
182
183    function getEmailId() {
184        return $this->email_id;
185    }
186
187    /**
188     * getAlertEmail
189     *
190     * Fetches either the department email (for replies) if configured.
191     * Otherwise, the system alert email address is used.
192     */
193    function getAlertEmail() {
194        global $cfg;
195
196        if ($this->email)
197            return $this->email;
198
199        return $cfg ? $cfg->getDefaultEmail() : null;
200    }
201
202    function getEmail() {
203        global $cfg;
204
205        if ($this->email)
206            return $this->email;
207
208        return $cfg? $cfg->getDefaultEmail() : null;
209    }
210
211    function getNumMembers() {
212        return count($this->getMembers());
213    }
214
215    function getMembers() {
216        if (!isset($this->_members)) {
217            $members = Staff::objects()
218                ->distinct('staff_id')
219                ->constrain(array(
220                    // Ensure that joining through dept_access is only relevant
221                    // for this department, so that the `alerts` annotation
222                    // can work properly
223                    'dept_access' => new Q(array('dept_access__dept_id' => $this->getId()))
224                ))
225                ->filter(Q::any(array(
226                    'dept_id' => $this->getId(),
227                    'staff_id' => $this->manager_id,
228                    'dept_access__dept_id' => $this->getId(),
229                )));
230
231            $this->_members = Staff::nsort($members);
232        }
233        return $this->_members;
234    }
235
236    function getAvailableMembers() {
237        $members = clone $this->getMembers();
238        return $members->filter(array(
239            'isactive' => 1,
240            'onvacation' => 0,
241        ));
242    }
243
244    function getPrimaryMembers() {
245        if (!isset($this->_primary_members)) {
246            $members = clone $this->getMembers();
247            $members->filter(array('dept_id' =>$this->getId()));
248            $this->_primary_members = $members->all();
249        }
250
251        return $this->_primary_members;
252    }
253
254    function getExtendedMembers() {
255        if (!isset($this->_exended_members)) {
256            // We need a query set so we can sort the names
257            $members = StaffDeptAccess::objects();
258            $members->filter(array('dept_id' => $this->getId()));
259            $members = Staff::nsort($members, 'staff__');
260            $extended = array();
261            foreach($members as $member) {
262                if (!$member->staff)
263                    continue;
264                // Annoted the staff model with alerts and role
265                $extended[] = AnnotatedModel::wrap($member->staff, array(
266                    'alerts'  => $member->isAlertsEnabled(),
267                    'role_id' => $member->role_id,
268                ));
269            }
270
271            $this->_extended_members = $extended;
272        }
273
274        return $this->_extended_members;
275    }
276
277    // Get eligible members only
278    function getAssignees($criteria=array()) {
279        if (!$this->assignPrimaryOnly() && !$this->assignMembersOnly()) {
280            // this is for if all agents is set - assignment is not restricted
281            // based on department membership.
282            $members =  Staff::objects()->filter(array(
283                'onvacation' => 0,
284                'isactive' => 1,
285            ));
286        } else {
287            //this gets just the members of the dept including extended access
288            $members = clone $this->getAvailableMembers();
289
290            //Restrict to the primary members only
291            if ($this->assignPrimaryOnly())
292                $members->filter(array('dept_id' => $this->getId()));
293        }
294
295        // Restrict agents based on visibility of the assigner
296        if (($staff=$criteria['staff']))
297            $members = $staff->applyDeptVisibility($members);
298
299        // Sort based on set name format
300        return Staff::nsort($members);
301    }
302
303    function getMembersForAlerts() {
304        if ($this->isGroupMembershipEnabled() == self::ALERTS_DISABLED) {
305            // Disabled for this department
306            $rv = array();
307        }
308        else {
309            $rv = clone $this->getAvailableMembers();
310            $rv->filter(Q::any(array(
311                // Ensure "Alerts" is enabled — must be a primary member or
312                // have alerts enabled on your membership and have alerts
313                // configured to extended to extended access members
314                'dept_id' => $this->getId(),
315                // NOTE: Manager is excluded here if not a member
316                Q::all(array(
317                    'dept_access__dept__group_membership' => self::ALERTS_DEPT_AND_EXTENDED,
318                    'dept_access__flags__hasbit' => StaffDeptAccess::FLAG_ALERTS,
319                )),
320            )));
321        }
322        return $rv;
323    }
324
325    function getNumMembersForAlerts() {
326        return count($this->getMembersForAlerts());
327    }
328
329    function getSLAId() {
330        return $this->sla_id;
331    }
332
333    function getSLA() {
334        return $this->sla;
335    }
336
337    function getScheduleId() {
338        return $this->schedule_id;
339    }
340
341    function getSchedule() {
342        if (!isset($this->schedule) && $this->getScheduleId())
343            $this->schedule = BusinessHoursSchedule::lookup(
344                        $this->getScheduleId());
345
346        return $this->schedule;
347    }
348
349    function getTemplateId() {
350         return $this->tpl_id;
351    }
352
353    function getTemplate() {
354        global $cfg;
355
356        if (!$this->template) {
357            if (!($this->template = EmailTemplateGroup::lookup($this->getTemplateId())))
358                $this->template = $cfg->getDefaultTemplate();
359        }
360
361        return $this->template;
362    }
363
364    function getAutoRespEmail() {
365
366        if (!$this->autorespEmail) {
367            if (!$this->autoresp_email_id
368                    || !($this->autorespEmail = Email::lookup($this->autoresp_email_id)))
369                $this->autorespEmail = $this->getEmail();
370        }
371
372        return $this->autorespEmail;
373    }
374
375    function getEmailAddress() {
376        if(($email=$this->getEmail()))
377            return $email->getAddress();
378    }
379
380    function getSignature() {
381        return $this->signature;
382    }
383
384    function canAppendSignature() {
385        return ($this->getSignature() && $this->isPublic());
386    }
387
388    // Check if an agent or team is eligible for assignment
389    function canAssign($assignee) {
390
391
392        if ($assignee instanceof Staff) {
393            // Primary members only
394            if ($this->assignPrimaryOnly() && !$this->isPrimaryMember($assignee))
395                return false;
396
397            // Extended members only
398            if ($this->assignMembersOnly() && !$this->isMember($assignee))
399                return false;
400        } elseif (!$assignee instanceof Team) {
401            // Assignee can only be an Agent or a Team
402            return false;
403        }
404
405        // Make sure agent / team  is availabe for assignment
406        if (!$assignee->isAvailable())
407             return false;
408
409        return true;
410    }
411
412    function getManagerId() {
413        return $this->manager_id;
414    }
415
416    function getManager() {
417        return $this->manager;
418    }
419
420    function isManager(Staff $staff) {
421        $staff = $staff->getId();
422
423        return ($this->getManagerId() && $this->getManagerId()==$staff);
424    }
425
426    function isMember(Staff $staff) {
427        $staff = $staff->getId();
428
429        return $this->getMembers()->findFirst(array(
430            'staff_id' => $staff
431        ));
432    }
433
434    function isPrimaryMember(Staff $staff) {
435        return ($staff->getDeptId() == $this->getId());
436    }
437
438    function isPublic() {
439         return $this->ispublic;
440    }
441
442    function autoRespONNewTicket() {
443        return $this->ticket_auto_response;
444    }
445
446    function autoRespONNewMessage() {
447        return $this->message_auto_response;
448    }
449
450    function noreplyAutoResp() {
451         return $this->noreply_autoresp;
452    }
453
454    function assignMembersOnly() {
455        return $this->flags & self::FLAG_ASSIGN_MEMBERS_ONLY;
456    }
457
458    function assignPrimaryOnly() {
459        return $this->flags & self::FLAG_ASSIGN_PRIMARY_ONLY;
460    }
461
462    function getAssignmentFlag() {
463        if($this->flags & self::FLAG_ASSIGN_MEMBERS_ONLY)
464          return 'members';
465        elseif($this->flags & self::FLAG_ASSIGN_PRIMARY_ONLY)
466          return 'primary';
467        else
468          return 'all';
469    }
470
471    function disableAutoClaim() {
472        return $this->flags & self::FLAG_DISABLE_AUTO_CLAIM;
473    }
474
475    function disableReopenAutoAssign() {
476        return $this->flags & self::FLAG_DISABLE_REOPEN_AUTO_ASSIGN;
477    }
478
479    function isGroupMembershipEnabled() {
480        return $this->group_membership;
481    }
482
483    function getHashtable() {
484        $ht = $this->ht;
485        if (static::$meta['joins'])
486            foreach (static::$meta['joins'] as $k => $v)
487                unset($ht[$k]);
488
489        $ht['disable_auto_claim'] =  $this->disableAutoClaim();
490        $ht['status'] = $this->getStatus();
491        $ht['assignment_flag'] = $this->getAssignmentFlag();
492        $ht['disable_reopen_auto_assign'] =  $this->disableReopenAutoAssign();
493        return $ht;
494    }
495
496    function getInfo() {
497        return $this->getHashtable();
498    }
499
500    function delete() {
501        global $cfg;
502
503        if (!$cfg
504            // Default department cannot be deleted
505            || $this->getId()==$cfg->getDefaultDeptId()
506            // Department  with users cannot be deleted
507            || $this->members->count()
508        ) {
509            return 0;
510        }
511
512        $id = $this->getId();
513        if (parent::delete()) {
514            $type = array('type' => 'deleted');
515            Signal::send('object.deleted', $this, $type);
516
517            // DO SOME HOUSE CLEANING
518            //Move tickets to default Dept. TODO: Move one ticket at a time and send alerts + log notes.
519            Ticket::objects()
520                ->filter(array('dept_id' => $id))
521                ->update(array('dept_id' => $cfg->getDefaultDeptId()));
522
523            // Move tasks
524            Task::objects()
525                ->filter(array('dept_id' => $id))
526                ->update(array('dept_id' => $cfg->getDefaultDeptId()));
527
528            //Move Dept members: This should never happen..since delete should be issued only to empty Depts...but check it anyways
529            Staff::objects()
530                ->filter(array('dept_id' => $id))
531                ->update(array('dept_id' => $cfg->getDefaultDeptId()));
532
533            // Clear any settings using dept to default back to system default
534            Topic::objects()
535                ->filter(array('dept_id' => $id))
536                ->update(array('dept_id' => 0));
537
538            Email::objects()
539                ->filter(array('dept_id' => $id))
540                ->update(array('dept_id' => 0));
541
542            // Delete extended access entries
543            StaffDeptAccess::objects()
544                ->filter(array('dept_id' => $id))
545                ->delete();
546        }
547        return true;
548    }
549
550    function __toString() {
551        return $this->getName();
552    }
553
554    function getParent() {
555        return static::lookup($this->ht['pid']);
556    }
557
558    /**
559     * getFullPath
560     *
561     * Utility function to retrieve a '/' separated list of department IDs
562     * in the ancestry of this department. This is used to populate the
563     * `path` field in the database and is used for access control rather
564     * than the ID field since nesting of departments is necessary and
565     * department access can be cascaded.
566     *
567     * Returns:
568     * Slash-separated string of ID ancestry of this department. The string
569     * always starts and ends with a slash, and will always contain the ID
570     * of this department last.
571     */
572    function getFullPath() {
573        $path = '';
574        if ($p = $this->getParent())
575            $path .= $p->getFullPath();
576        else
577            $path .= '/';
578        $path .= $this->getId() . '/';
579        return $path;
580    }
581
582    /**
583     * setFlag
584     *
585     * Utility method to set/unset flag bits
586     *
587     */
588
589    public function setFlag($flag, $val) {
590
591        if ($val)
592            $this->flags |= $flag;
593        else
594            $this->flags &= ~$flag;
595    }
596
597    function hasFlag($flag) {
598        return ($this->get('flags', 0) & $flag) != 0;
599    }
600
601    function flagChanged($flag, $var) {
602        if (($this->hasFlag($flag) && !$var) ||
603            (!$this->hasFlag($flag) && $var))
604                return true;
605    }
606
607    function export($dept, $criteria=null, $filename='') {
608        include_once(INCLUDE_DIR.'class.error.php');
609        $members = $dept->getMembers();
610
611        //Sort based on name formating
612        $members = Staff::nsort($members);
613        Export::departmentMembers($dept, $members, $filename);
614    }
615
616    /*----Static functions-------*/
617    static function getIdByName($name, $pid=null) {
618        $row = static::objects()
619            ->filter(array(
620                        'name' => $name,
621                        'pid'  => $pid ?: null))
622            ->values_flat('id')
623            ->first();
624
625        return $row ? $row[0] : 0;
626    }
627
628    static function getEmailIdById($id) {
629        $row = static::objects()
630            ->filter(array('id' => $id))
631            ->values_flat('email_id')
632            ->first();
633
634        return $row ? $row[0] : 0;
635    }
636
637    function getNameById($id) {
638        $names = Dept::getDepartments();
639        return $names[$id];
640    }
641
642    function getDefaultDeptName() {
643        global $cfg;
644
645        return ($cfg
646            && ($did = $cfg->getDefaultDeptId())
647            && ($names = self::getDepartments()))
648            ? $names[$did]
649            : null;
650    }
651
652    static function getDepartments($criteria=null, $localize=true, $disabled=true) {
653        $depts = null;
654
655        if (!isset($depts) || $criteria) {
656            // XXX: This will upset the static $depts array
657            $depts = array();
658            $query = self::objects();
659            if (isset($criteria['publiconly']) && $criteria['publiconly'])
660                $query->filter(array(
661                             'ispublic' => ($criteria['publiconly'] ? 1 : 0)));
662
663            if (isset($criteria['activeonly']) && $criteria['activeonly'])
664                $query->filter(array(
665                            'flags__hasbit' => Dept::FLAG_ACTIVE));
666
667            if ($manager=$criteria['manager'])
668                $query->filter(array(
669                            'manager_id' => is_object($manager)?$manager->getId():$manager));
670
671            if (isset($criteria['nonempty'])) {
672                $query->annotate(array(
673                    'user_count' => SqlAggregate::COUNT('members')
674                ))->filter(array(
675                    'user_count__gt' => 0
676                ));
677            }
678
679            $query->order_by('name')
680                 ->values('id', 'pid', 'flags', 'name', 'parent');
681
682            foreach ($query as $row) {
683              $display = ($row['flags'] & self::FLAG_ACTIVE);
684
685              $depts[$row['id']] = array('id' => $row['id'], 'pid'=>$row['pid'], 'name'=>$row['name'],
686                  'parent'=>$row['parent'], 'disabled' => !$display);
687            }
688
689            $localize_this = function($id, $default) use ($localize) {
690                if (!$localize)
691                    return $default;
692
693                $tag = _H("dept.name.{$id}");
694                $T = CustomDataTranslation::translate($tag);
695                return $T != $tag ? $T : $default;
696            };
697
698            // Resolve parent names
699            $names = array();
700            foreach ($depts as $id=>$info) {
701                $name = $info['name'];
702                $loop = array($id=>true);
703                $parent = false;
704                while ($info['pid'] && ($info = $depts[$info['pid']])) {
705                    $name = sprintf('%s / %s', $info['name'], $name);
706                    if (isset($loop[$info['pid']]))
707                        break;
708                    $loop[$info['pid']] = true;
709                    $parent = $info;
710                }
711                // Fetch local names
712                $names[$id] = $localize_this($id, $name);
713            }
714
715            // Apply requested filters
716            $requested_names = array();
717            foreach ($names as $id=>$n) {
718                $info = $depts[$id];
719                if (!$disabled && $info['disabled'])
720                    continue;
721                if ($disabled === self::DISPLAY_DISABLED && $info['disabled'])
722                    $n .= " - ".__("(disabled)");
723
724                $requested_names[$id] = $n;
725            }
726            asort($requested_names);
727
728            // TODO: Use locale-aware sorting mechanism
729            if ($criteria)
730                return $requested_names;
731
732            $depts = $requested_names;
733        }
734
735        return $requested_names;
736    }
737
738    static function getPublicDepartments() {
739        $depts =null;
740
741        if (!$depts)
742            $depts = self::getDepartments(array('publiconly'=>true));
743
744        return $depts;
745    }
746
747    static function getActiveDepartments() {
748        $depts =null;
749
750        if (!$depts)
751            $depts = self::getDepartments(array('activeonly'=>true));
752
753        return $depts;
754    }
755
756    static function create($vars=false, &$errors=array()) {
757        $dept = new static($vars);
758        $dept->created = SqlFunction::NOW();
759        return $dept;
760    }
761
762    static function __create($vars, &$errors) {
763        $dept = self::create($vars);
764        if (!$dept->update($vars, $errors))
765          return false;
766
767       $dept->save();
768
769       return $dept;
770    }
771
772    function save($refetch=false) {
773        if ($this->dirty)
774            $this->updated = SqlFunction::NOW();
775
776        return parent::save($refetch || $this->dirty);
777    }
778
779    function update($vars, &$errors) {
780        global $cfg;
781
782        $id = $this->id;
783        if ($id && $id != $vars['id'])
784            $errors['err']=__('Missing or invalid Dept ID.')
785                .' '.__('Internal error occurred');
786
787        if (!$vars['name']) {
788            $errors['name']=__('Name required');
789        } elseif (($did = static::getIdByName($vars['name'], $vars['pid']))
790                && $did != $id) {
791            $errors['name']=__('Department already exists');
792        }
793
794        if (!$vars['ispublic'] && $cfg && ($vars['id']==$cfg->getDefaultDeptId()))
795            $errors['ispublic']=__('System default department cannot be private');
796
797        if ($vars['pid'] && !($p = static::lookup($vars['pid'])))
798            $errors['pid'] = __('Department selection is required');
799
800        $dept = Dept::lookup($vars['pid']);
801        if ($dept) {
802          if (!$dept->isActive())
803            $errors['dept_id'] = sprintf(__('%s selected must be active'), __('Parent Department'));
804          elseif (strpos($dept->getFullPath(), '/'.$this->getId().'/') !== false)
805            $errors['pid'] = sprintf(__('%s cannot contain the current %s'), __('Parent Department'), __('Department'));
806        }
807
808        if ($vars['sla_id'] && !SLA::lookup($vars['sla_id']))
809            $errors['sla_id'] = __('Invalid SLA');
810
811        if ($vars['manager_id'] && !Staff::lookup($vars['manager_id']))
812            $errors['manager_id'] = __('Unknown Staff');
813
814        if ($vars['email_id'] && !Email::lookup($vars['email_id']))
815            $errors['email_id'] = __('Unknown System Email');
816
817        if ($vars['tpl_id'] && !EmailTemplateGroup::lookup($vars['tpl_id']))
818            $errors['tpl_id'] = __('Unknown Template Set');
819
820        if ($vars['autoresp_email_id'] && !Email::lookup($vars['autoresp_email_id']))
821            $errors['autoresp_email_id'] = __('Unkown System Email');
822
823        // Format access update as [array(dept_id, role_id, alerts?)]
824        $access = array();
825        if (isset($vars['members'])) {
826            foreach (@$vars['members'] as $staff_id) {
827                $access[] = array($staff_id, $vars['member_role'][$staff_id],
828                    @$vars['member_alerts'][$staff_id]);
829            }
830        }
831        $this->updateAccess($access, $errors);
832
833        if ($errors)
834            return false;
835
836        $vars['disable_auto_claim'] = isset($vars['disable_auto_claim']) ? 1 : 0;
837        if ($this->getId()) {
838            //flags
839            $disableAutoClaim = $this->flagChanged(self::FLAG_DISABLE_AUTO_CLAIM, $vars['disable_auto_claim']);
840            $disableAutoAssign = $this->flagChanged(self::FLAG_DISABLE_REOPEN_AUTO_ASSIGN, $vars['disable_reopen_auto_assign']);
841            $ticketAssignment = ($this->getAssignmentFlag() != $vars['assignment_flag']);
842            foreach ($vars as $key => $value) {
843                if ($key == 'status' && $this->getStatus() && strtolower($this->getStatus()) != $value) {
844                    $type = array('type' => 'edited', 'status' => ucfirst($value));
845                    Signal::send('object.edited', $this, $type);
846                } elseif ((isset($this->$key) && ($this->$key != $value) && $key != 'members') ||
847                         ($disableAutoClaim && $key == 'disable_auto_claim') ||
848                          $ticketAssignment && $key == 'assignment_flag' ||
849                          $disableAutoAssign && $key == 'disable_reopen_auto_assign') {
850                    $type = array('type' => 'edited', 'key' => $key);
851                    Signal::send('object.edited', $this, $type);
852                }
853            }
854        }
855        if ($vars['disable_auto_claim'] !== 1)
856            unset($vars['disable_auto_claim']);
857
858        $this->pid = $vars['pid'] ?: null;
859        $this->ispublic = isset($vars['ispublic']) ? (int) $vars['ispublic'] : 0;
860        $this->email_id = isset($vars['email_id']) ? (int) $vars['email_id'] : 0;
861        $this->tpl_id = isset($vars['tpl_id']) ? (int) $vars['tpl_id'] : 0;
862        $this->sla_id = isset($vars['sla_id']) ? (int) $vars['sla_id'] : 0;
863        $this->schedule_id = isset($vars['schedule_id']) ? (int) $vars['schedule_id'] : 0;
864        $this->autoresp_email_id = isset($vars['autoresp_email_id']) ? (int) $vars['autoresp_email_id'] : 0;
865        $this->manager_id = $vars['manager_id'] ?: 0;
866        $this->name = Format::striptags($vars['name']);
867        $this->signature = Format::sanitize($vars['signature']);
868        $this->group_membership = $vars['group_membership'];
869        $this->ticket_auto_response = isset($vars['ticket_auto_response'])?$vars['ticket_auto_response']:1;
870        $this->message_auto_response = isset($vars['message_auto_response'])?$vars['message_auto_response']:1;
871        $this->flags = $vars['flags'] ?: 0;
872
873        $this->setFlag(self::FLAG_ASSIGN_MEMBERS_ONLY, isset($vars['assign_members_only']));
874        $this->setFlag(self::FLAG_DISABLE_AUTO_CLAIM, isset($vars['disable_auto_claim']));
875        $this->setFlag(self::FLAG_DISABLE_REOPEN_AUTO_ASSIGN, isset($vars['disable_reopen_auto_assign']));
876
877        $filter_actions = FilterAction::objects()->filter(array('type' => 'dept', 'configuration' => '{"dept_id":'. $this->getId().'}'));
878        if ($filter_actions && $vars['status'] == 'active')
879          FilterAction::setFilterFlags($filter_actions, 'Filter::FLAG_INACTIVE_DEPT', false);
880        else
881          FilterAction::setFilterFlags($filter_actions, 'Filter::FLAG_INACTIVE_DEPT', true);
882
883        if ($cfg && ($this->getId() == $cfg->getDefaultDeptId()))
884            $vars['status'] = 'active';
885
886        switch ($vars['status']) {
887          case 'active':
888            $this->setFlag(self::FLAG_ACTIVE, true);
889            $this->setFlag(self::FLAG_ARCHIVED, false);
890            break;
891
892          case 'disabled':
893            $this->setFlag(self::FLAG_ACTIVE, false);
894            $this->setFlag(self::FLAG_ARCHIVED, false);
895            break;
896
897          case 'archived':
898            $this->setFlag(self::FLAG_ACTIVE, false);
899            $this->setFlag(self::FLAG_ARCHIVED, true);
900        }
901
902        switch ($vars['assignment_flag']) {
903          case 'all':
904            $this->setFlag(self::FLAG_ASSIGN_MEMBERS_ONLY, false);
905            $this->setFlag(self::FLAG_ASSIGN_PRIMARY_ONLY, false);
906            break;
907          case 'members':
908            $this->setFlag(self::FLAG_ASSIGN_MEMBERS_ONLY, true);
909            $this->setFlag(self::FLAG_ASSIGN_PRIMARY_ONLY, false);
910            break;
911          case 'primary':
912            $this->setFlag(self::FLAG_ASSIGN_MEMBERS_ONLY, false);
913            $this->setFlag(self::FLAG_ASSIGN_PRIMARY_ONLY, true);
914            break;
915        }
916
917        $this->path = $this->getFullPath();
918
919        $wasnew = $this->__new__;
920        if ($this->save() && $this->extended->saveAll()) {
921            if ($wasnew) {
922                // The ID wasn't available until after the commit
923                $this->path = $this->getFullPath();
924                $this->save();
925            }
926            return true;
927        }
928
929        if (isset($this->id))
930            $errors['err']=sprintf(__('Unable to update %s.'), __('this department'))
931               .' '.__('Internal error occurred');
932        else
933            $errors['err']=sprintf(__('Unable to create %s.'), __('this department'))
934               .' '.__('Internal error occurred');
935
936        return false;
937    }
938
939    function updateAccess($access, &$errors) {
940      reset($access);
941      $dropped = array();
942      foreach ($this->extended as $DA)
943          $dropped[$DA->staff_id] = 1;
944      while (list(, list($staff_id, $role_id, $alerts)) = each($access)) {
945          unset($dropped[$staff_id]);
946          if (!$role_id || !Role::lookup($role_id))
947              $errors['members'][$staff_id] = __('Select a valid role');
948          if (!$staff_id || !($staff=Staff::lookup($staff_id)))
949              $errors['members'][$staff_id] = __('No such agent');
950
951          if ($staff->dept_id == $this->id) {
952
953              // If primary member then simply update the role.
954              if (($m = $this->members->findFirst(array(
955                                  'staff_id' => $staff_id))))
956                  $m->role_id = $role_id;
957              continue;
958          }
959
960          $da = $this->extended->findFirst(array('staff_id' => $staff_id));
961          if (!isset($da)) {
962              $da = new StaffDeptAccess(array(
963                  'staff_id' => $staff_id, 'role_id' => $role_id
964              ));
965              $this->extended->add($da);
966              $type = array('type' => 'edited', 'key' => 'Staff Added');
967              Signal::send('object.edited', $this, $type);
968          }
969          else {
970              $da->role_id = $role_id;
971          }
972          $da->setAlerts($alerts);
973
974      }
975
976      if ($errors)
977          return false;
978
979      if ($dropped) {
980          $type = array('type' => 'edited', 'key' => 'Staff Removed');
981          Signal::send('object.edited', $this, $type);
982          $this->extended->saveAll();
983          $this->extended
984              ->filter(array('staff_id__in' => array_keys($dropped)))
985              ->delete();
986          $this->extended->reset();
987      }
988
989      // Save any role change.
990      $this->members->saveAll();
991
992      return true;
993    }
994
995    static function getPermissions() {
996        return self::$perms;
997    }
998}
999RolePermission::register(/* @trans */ 'Miscellaneous', Dept::getPermissions());
1000
1001class DepartmentQuickAddForm
1002extends Form {
1003    function getFields() {
1004        if ($this->fields)
1005            return $this->fields;
1006
1007        return $this->fields = array(
1008            'pid' => new ChoiceField(array(
1009                'label' => '',
1010                'default' => 0,
1011                'choices' =>
1012                    array(0 => '— '.__('Top-Level Department').' —')
1013                    + Dept::getPublicDepartments()
1014            )),
1015            'name' => new TextboxField(array(
1016                'required' => true,
1017                'configuration' => array(
1018                    'placeholder' => __('Name'),
1019                    'classes' => 'span12',
1020                    'autofocus' => true,
1021                    'length' => 128,
1022                ),
1023            )),
1024            'email_id' => new ChoiceField(array(
1025                'label' => __('Email Mailbox'),
1026                'default' => 0,
1027                'choices' =>
1028                    array(0 => '— '.__('System Default').' —')
1029                    + Email::getAddresses(),
1030                'configuration' => array(
1031                    'classes' => 'span12',
1032                ),
1033            )),
1034            'private' => new BooleanField(array(
1035                'configuration' => array(
1036                    'classes' => 'form footer',
1037                    'desc' => __('This department is for internal use'),
1038                ),
1039            )),
1040        );
1041    }
1042
1043    function getClean($validate = true) {
1044        $clean = parent::getClean();
1045
1046        $clean['ispublic'] = !$clean['private'];
1047        unset($clean['private']);
1048
1049        return $clean;
1050    }
1051
1052    function render($staff=true, $title=false, $options=array()) {
1053        return parent::render($staff, $title, $options + array('template' => 'dynamic-form-simple.tmpl.php'));
1054    }
1055}
1056