From e31408bb7c797c1baf3345f0ccc4ef2da0bc8d4d Mon Sep 17 00:00:00 2001
From: Peter Rotich <peter@osticket.com>
Date: Mon, 28 Jul 2014 19:08:33 +0000
Subject: [PATCH] Retire BuiltInList and used a generalized DynamicList with
 CustomListHandler

---
 include/ajax.forms.php                        |  16 +-
 include/class.dynamic_forms.php               |   8 +-
 include/class.list.php                        | 382 +++++++++---------
 include/staff/dynamic-list.inc.php            |  18 +-
 include/staff/dynamic-lists.inc.php           |  36 +-
 .../templates/list-item-properties.tmpl.php   |   5 +-
 .../streams/core/8f99b8bf-cbf8c933.patch.sql  |  29 ++
 scp/lists.php                                 |   7 +-
 8 files changed, 246 insertions(+), 255 deletions(-)
 create mode 100644 include/upgrader/streams/core/8f99b8bf-cbf8c933.patch.sql

diff --git a/include/ajax.forms.php b/include/ajax.forms.php
index bd71f3924..47ace2a54 100644
--- a/include/ajax.forms.php
+++ b/include/ajax.forms.php
@@ -60,12 +60,8 @@ class DynamicFormsAjaxAPI extends AjaxController {
 
     function getListItemProperties($list_id, $item_id) {
 
-        if (is_numeric($list_id))
-            $list = DynamicList::lookup($list_id);
-        else
-            $list = BuiltInCustomList::lookup($list_id);
-
-        if (!($item = $list->getItem( (int) $item_id)))
+        $list = DynamicList::lookup($list_id);
+        if (!$list || !($item = $list->getItem( (int) $item_id)))
             Http::response(404, 'No such list item');
 
         include(STAFFINC_DIR . 'templates/list-item-properties.tmpl.php');
@@ -73,12 +69,8 @@ class DynamicFormsAjaxAPI extends AjaxController {
 
     function saveListItemProperties($list_id, $item_id) {
 
-        if (is_numeric($list_id))
-            $list = DynamicList::lookup($list_id);
-        else
-            $list = BuiltInCustomList::lookup($list_id);
-
-        if (!($item = $list->getItem( (int) $item_id)))
+        $list = DynamicList::lookup($list_id);
+        if (!$list || !($item = $list->getItem( (int) $item_id)))
             Http::response(404, 'No such list item');
 
         if (!$item->setConfiguration())
diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php
index f097a2e4d..a2a3027c5 100644
--- a/include/class.dynamic_forms.php
+++ b/include/class.dynamic_forms.php
@@ -436,7 +436,7 @@ class DynamicFormField extends VerySimpleModel {
     }
 
     function isDeletable() {
-        return ($this->get('edit_mask') & 1) == 0;
+        return (($this->get('edit_mask') & 1) == 0);
     }
     function isNameForced() {
         return $this->get('edit_mask') & 2;
@@ -449,11 +449,11 @@ class DynamicFormField extends VerySimpleModel {
     }
 
     function  isChangeable() {
-        return ($this->get('edit_mask') & 16) == 0;
+        return (($this->get('edit_mask') & 16) == 0);
     }
 
-    function  isConfigurable() {
-        return ($this->get('edit_mask') & 32) == 0;
+    function  isEditable() {
+        return (($this->get('edit_mask') & 32) == 0);
     }
 
     /**
diff --git a/include/class.list.php b/include/class.list.php
index 89564ad62..462c14079 100644
--- a/include/class.list.php
+++ b/include/class.list.php
@@ -46,7 +46,8 @@ interface CustomList {
     function getSortMode();
     function getListOrderBy();
 
-    function isBuiltIn();
+    function allowAdd();
+    function hasAbbrev();
 
     function update($vars, &$errors);
     function delete();
@@ -82,98 +83,41 @@ interface CustomListItem {
 
 
 /*
- * Base class for Built-in Custom Lists
+ * Base class for Custom List handlers
  *
- * Built-in custom lists are lists that can be extended but within the
- * constrains of system defined parameters.
+ * Custom list handler extends custom list and might store data outside the
+ * typical dynamic list store.
  *
  */
 
-abstract class BuiltInCustomList implements CustomList {
-    static $sort_modes = array(
-            'Alpha'     => 'Alphabetical',
-            '-Alpha'    => 'Alphabetical (Reversed)',
-            'SortCol'   => 'Manually Sorted'
-    );
+abstract class CustomListHandler {
 
-    var $config = null;
+    var $_list;
 
-    function __construct() {
-        $this->config = new Config('CL'.$this->getId());
+    function __construct($list) {
+        $this->_list = $list;
     }
 
-    abstract function getId();
-    abstract function getName();
-    abstract function getPluralName();
+    function __call($name, $args) {
 
-    abstract  function getInfo();
+        $rv = null;
+        if ($this->_list && is_callable(array($this->_list, $name)))
+            $rv = $args
+                ? call_user_func_array(array($this->_list, $name), $args)
+                : call_user_func(array($this->_list, $name));
 
-    abstract function getNumItems();
-    abstract function getAllItems();
-    abstract function getItems($criteria);
-
-    abstract function addItem($vars, &$errors);
-
-    abstract function getForm(); // Config form
-    abstract function hasProperties();
-
-    abstract function getListOrderBy();
-
-    abstract function getSortMode();
-
-    function getSortModes() {
-        return static::$sort_modes;
+        return $rv;
     }
 
-    function isBuiltIn() {
-        return true;
-    }
-
-    function set($field, $value) {
-
-        if (!$this->config)
-            return false;
-
-        return $this->config->set($field, $value);
-    }
-
-    abstract function update($vars, &$errors);
-
-    // Built-in list cannot be deleted
-    function delete() {
-        return false;
-    }
-
-    // Built-in list is defined - not created.
-    static function create($vars, &$errors) {
-        return false;
-    }
-
-    static function lookup($id) {
-
-        if (!($list=static::getLists())
-                // Built-in list exists
-                || !isset($list[$id])
-                // Handler exits
-                || !($handler = $list[$id]['handler'])
-                // It's a collable handler
-                || !class_exists($handler))
-           return null;
-
-        return new $handler();
-    }
-
-    static function getLists() {
-
-        $list['status'] = array ( //Ticket statuses
-                'name' => 'Ticket Status',
-                'handler' => 'TicketStatusList',
-                'icon' => 'icon-flag',
-                );
-
-        return $list;
+    function update($vars, &$errors) {
+        return $this->_list->update($vars, $errors);
     }
 
+    abstract function getNumItems();
+    abstract function getAllItems();
+    abstract function getItems($criteria);
+    abstract function getItem($id);
+    abstract function addItem($vars, &$errors);
 }
 
 /**
@@ -197,16 +141,23 @@ class DynamicList extends VerySimpleModel implements CustomList {
     // Required fields
     static $fields = array('name', 'name_plural', 'sort_mode', 'notes');
 
+    // Supported masks
+    const MASK_EDIT     = 0x0001;
+    const MASK_ADD      = 0x0002;
+    const MASK_DELETE   = 0x0004;
+    const MASK_ABBREV   = 0x0008;
 
     var $_items;
     var $_form;
+    var $_config;
 
-    function getId() {
-        return $this->get('id');
+    function __construct() {
+        call_user_func_array(array('parent', '__construct'), func_get_args());
+        $this->_config = new Config('list.'.$this->getId());
     }
 
-    function isBuiltIn() {
-        return false;
+    function getId() {
+        return $this->get('id');
     }
 
     function getInfo() {
@@ -314,6 +265,34 @@ class DynamicList extends VerySimpleModel implements CustomList {
         return $this->_form;
     }
 
+    function isDeleteable() {
+        return !$this->hasMask(static::MASK_DELETE);
+    }
+
+    function isEditable() {
+        return !$this->hasMask(static::MASK_EDIT);
+    }
+
+    function allowAdd() {
+        return !$this->hasMask(static::MASK_ADD);
+    }
+
+    function hasAbbrev() {
+        return !$this->hasMask(static::MASK_ABBREV);
+    }
+
+    protected function hasMask($mask) {
+        return 0 !== ($this->get('masks') & $mask);
+    }
+
+    protected function clearMask($mask) {
+        return $this->set('masks', $this->get('masks') & ~$mask);
+    }
+
+    protected function setFlag($mask) {
+        return $this->set('mask', $this->get('mask') | $mask);
+    }
+
     private function createConfigurationForm() {
 
         $form = DynamicForm::create(array(
@@ -328,8 +307,16 @@ class DynamicList extends VerySimpleModel implements CustomList {
         return $this->getConfigurationForm($autocreate);
     }
 
+    function getConfiguration() {
+        return JsonDataParser::parse($this->_config->get('configuration'));
+    }
+
     function update($vars, &$errors) {
-        $required = array('name');
+
+        $required = array();
+        if ($this->isEditable())
+            $required = array('name');
+
         foreach (static::$fields as $f) {
             if (in_array($f, $required) && !$vars[$f])
                 $errors[$f] = sprintf('%s is required', mb_convert_case($f, MB_CASE_TITLE));
@@ -397,9 +384,45 @@ class DynamicList extends VerySimpleModel implements CustomList {
     static function create($ht=false, &$errors=array()) {
         $inst = parent::create($ht);
         $inst->set('created', new SqlFunction('NOW'));
+
+        if (isset($ht['properties'])) {
+            $inst->save();
+            $ht['properties']['type'] = 'L'.$inst->getId();
+            $form = DynamicForm::create($ht['properties']);
+            $form->save();
+        }
+
+        if (isset($ht['configuration'])) {
+            $inst->save();
+            $c = new Config('list.'.$inst->getId());
+            $c->set('configuration', JsonDataEncoder::encode($ht['configuration']));
+        }
+
+        if (isset($ht['items'])) {
+            $inst->save();
+            foreach ($ht['items'] as $i) {
+                $i['list_id'] = $inst->getId();
+                $item = DynamicListItem::create($i);
+                $item->save();
+            }
+        }
+
         return $inst;
     }
 
+    static function lookup($id) {
+
+        if (!($list = parent::lookup($id)))
+            return null;
+
+        if (($config = $list->getConfiguration())) {
+            if (($lh=$config['handler']) && class_exists($lh))
+                $list = new $lh($list);
+        }
+
+        return $list;
+    }
+
     static function getSelections() {
         $selections = array();
         foreach (DynamicList::objects() as $list) {
@@ -441,7 +464,8 @@ class DynamicListItem extends VerySimpleModel implements CustomListItem {
     var $_config;
     var $_form;
 
-    const ENABLED               = 0x0001;
+    const ENABLED   = 0x0001;
+    const INTERNAL  = 0x0002;
 
     protected function hasStatus($flag) {
         return 0 !== ($this->get('status') & $flag);
@@ -456,7 +480,7 @@ class DynamicListItem extends VerySimpleModel implements CustomListItem {
     }
 
     function isInternal() {
-        return false;
+        return  $this->hasStatus(self::INTERNAL);
     }
 
     function isEnableable() {
@@ -575,22 +599,47 @@ class DynamicListItem extends VerySimpleModel implements CustomListItem {
         $this->set('list_id', null);
         return $this->save();
     }
+
+    static function create($ht=false, &$errors=array()) {
+
+        if (isset($ht['properties']) && is_array($ht['properties']))
+            $ht['properties'] = JsonDataEncoder::encode($ht['properties']);
+
+        $inst = parent::create($ht);
+        $inst->save(true);
+
+        // Auto-config properties if any
+        if ($ht['configuration'] && is_array($ht['configuration'])) {
+            $config = $inst->getConfiguration();
+            if (($form = $inst->getConfigurationForm())) {
+                foreach ($form->getFields() as $f) {
+                    if (!isset($ht['configuration'][$f->get('name')]))
+                        continue;
+
+                    if (is_array($ht['configuration'][$f->get('name')]))
+                        $val = JsonDataEncoder::encode(
+                                $ht['configuration'][$f->get('name')]);
+                    else
+                        $val = $ht['configuration'][$f->get('name')];
+
+                    $config[$f->get('id')] = $val;
+                }
+            }
+
+            $inst->set('properties', JsonDataEncoder::encode($config));
+        }
+
+        return $inst;
+    }
 }
 
 
 /*
  * Ticket status List
  *
- *
  */
 
-class TicketStatusList extends BuiltInCustomList {
-
-    var $ht = array(
-            'id' => 'status',
-            'name' => 'Status',
-            'name_plural' => 'Statuses',
-    );
+class TicketStatusList extends CustomListHandler {
 
     // Fields of interest we need to store
     static $config_fields = array('sort_mode', 'notes');
@@ -598,30 +647,6 @@ class TicketStatusList extends BuiltInCustomList {
     var $_items;
     var $_form;
 
-    function getId() {
-        return $this->ht['id'];
-    }
-
-    function getName() {
-        return $this->ht['name'];
-    }
-
-    function getPluralName() {
-        return $this->ht['name_plural'];
-    }
-
-    function getSortMode() {
-        return $this->ht['sort_mode'];
-    }
-
-    function getNotes() {
-        return $this->ht['notes'];
-    }
-
-    function getInfo() {
-        return $this->config->getInfo() + $this->ht;
-    }
-
     function getNumItems() {
         return TicketStatus::objects()->count();
     }
@@ -650,13 +675,13 @@ class TicketStatusList extends BuiltInCustomList {
         if (!is_int($val))
             $val = array('name' => $val);
 
-         return TicketStatus::lookup($val);
+         return TicketStatus::lookup($val, $this);
     }
 
     function addItem($vars, &$errors) {
 
         $item = TicketStatus::create(array(
-            'flags' => 0, //Disable  until configured.
+            'flags' => 0,
             'sort'  => $vars['sort'],
             'name' => $vars['value'],
         ));
@@ -667,49 +692,6 @@ class TicketStatusList extends BuiltInCustomList {
         return $item;
     }
 
-
-    function hasProperties() {
-        return ($this->getForm());
-    }
-
-    function getListOrderBy() {
-        switch ($this->getSortMode()) {
-            case 'Alpha':
-                return 'name';
-            case '-Alpha':
-                return '-name';
-            case 'SortCol':
-            default:
-                return 'sort';
-        }
-    }
-
-    function update($vars, &$errors) {
-
-        foreach (static::$config_fields as $f) {
-            if (!isset($vars[$f])) continue;
-
-            if (parent::set($f, $vars[$f]))
-                $this->ht[$f] = $vars[$f];
-        }
-
-        return true;
-    }
-
-    function getForm() {
-
-        if (!isset($this->_form)) {
-            $o = DynamicForm::objects()->filter(array('type'=>'S'));
-            if ($o && $o[0])
-                $this->_form =  $o[0];
-            else // Auto-load the data
-                $this->_form = self::__load();
-        }
-
-        return $this->_form;
-    }
-
-
     static function __load() {
         require_once(INCLUDE_DIR.'class.i18n.php');
 
@@ -744,35 +726,13 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
         'pk' => array('id'),
     );
 
-    // Major statuses (states)
-    static $_states = array( 1 => 'open', 'closed', 'archived', 'deleted');
-
-    // Supported flags (TODO: move to configurable custom list)
-    static $_flags = array(
-            'onhold' => array(
-                'flag' => 1,
-                'name' => 'Onhold',
-                'states' => array('open'),
-                ),
-            'overdue' => array(
-                'flag' => 2,
-                'name' => 'Overdue',
-                'states' => array('open'),
-                ),
-            'answered' => array(
-                'flag' => 4,
-                'name' => 'Answered',
-                'states' => array('open'),
-                )
-            );
-
+    var $_list;
     var $_form;
     var $_config;
     var $_settings;
 
-
-    const ENABLED = 0x0001;
-    const INTERNAL = 0x0002; // Forbid deletion or name and status change.
+    const ENABLED   = 0x0001;
+    const INTERNAL  = 0x0002; // Forbid deletion or name and status change.
 
 
 
@@ -793,14 +753,19 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
         return $this->set($field, $this->get($field) | $flag);
     }
 
+    protected function hasProperties() {
+        return ($this->_config->get('properties'));
+    }
+
     function getForm() {
         return $this->getConfigurationForm();
     }
 
     function getConfigurationForm() {
 
-        if (!$this->_form) {
-            $this->_form = DynamicForm::lookup(array('type'=>'S'));
+        if (!$this->_form && $this->_list) {
+            $this->_form = DynamicForm::lookup(
+                    array('type'=>'L'.$this->_list->getId()));
         }
 
         return $this->_form;
@@ -825,7 +790,7 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
     }
 
     function isEnableable() {
-        return ($this->getForm());
+        return $this->hasProperties();
     }
 
     function isDeletable() {
@@ -871,12 +836,12 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
                     $id = $f->get('id');
                     switch($name) {
                         case 'flags':
-                            foreach (static::$_flags as $k => $v)
+                            foreach (TicketFlagField::$_flags as $k => $v)
                                 if ($this->hasFlag('flags', $v['flag']))
-                                    $this->_settings[$id][] = $k;
+                                    $this->_settings[$id][$k] = $v['name'];
                             break;
                         case 'state':
-                            $this->_settings[$id] = $this->get('state');
+                            $this->_settings[$id][$this->get('state')] = $this->get('state');
                             break;
                         default:
                             if (!$this->_settings[$id] && $this->_settings[$name])
@@ -892,15 +857,20 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
     function setConfiguration(&$errors=array()) {
         $properties = array();
         foreach ($this->getConfigurationForm()->getFields() as $f) {
+            if ($this->isInternal() //Item is internal.
+                    && !$f->isEditable())
+                continue;
             $val = $f->getClean();
+            $errors = array_merge($errors, $f->errors());
+            if ($f->errors()) continue;
             $name = mb_strtolower($f->get('name'));
             switch ($name) {
                 case 'flags':
                     if ($val && is_array($val)) {
                         $flags = 0;
-                        foreach ($val as $v) {
-                            if (isset(static::$_flags[$v]))
-                                $flags += static::$_flags[$v]['flag'];
+                        foreach ($val as $k => $v) {
+                            if (isset(TicketFlagField::$_flags[$k]))
+                                $flags += TicketFlagField::$_flags[$k]['flag'];
                             elseif (!$f->errors())
                                 $f->addError('Unknown or invalid flag', $name);
                         }
@@ -910,15 +880,15 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
                     }
                     break;
                 case 'state':
-                    $val = $f->to_database($val);
-                    if ($val && in_array($val, static::$_states))
-                        $this->set('state', $val);
+                    if ($val && is_array($val))
+                        $this->set('state', key($val));
                     else
                         $f->addError('Unknown or invalid state', $name);
                     break;
                 default: //Custom properties the user might add.
                     $properties[$f->get('id')] = $f->to_php($val);
             }
+            // Add field specific validation errors (warnings)
             $errors = array_merge($errors, $f->errors());
         }
 
@@ -965,6 +935,18 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
         return $this->toString();
     }
 
+
+    static function lookup($var, $list= false) {
+
+        if (!($item = parent::lookup($var)))
+            return null;
+
+        $item->_list = $list;
+
+        return $item;
+    }
+
+
     static function __create($ht, &$error=false) {
         global $ost;
 
diff --git a/include/staff/dynamic-list.inc.php b/include/staff/dynamic-list.inc.php
index ef9f9c5a0..f67e5d8ea 100644
--- a/include/staff/dynamic-list.inc.php
+++ b/include/staff/dynamic-list.inc.php
@@ -48,7 +48,7 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
             <td width="180" class="required">Name:</td>
             <td>
                 <?php
-                if ($list && $list->isBuiltIn())
+                if ($list && !$list->isEditable())
                     echo $list->getName();
                 else {
                     echo sprintf('<input size="50" type="text" name="name"
@@ -63,7 +63,7 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
             <td width="180">Plural Name:</td>
             <td>
                 <?php
-                    if ($list && $list->isBuiltIn())
+                    if ($list && !$list->isEditable())
                         echo $list->getPluralName();
                     else
                         echo sprintf('<input size="50" type="text"
@@ -229,7 +229,7 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
             <th></th>
             <th>Value</th>
             <?php
-            if (!$list || !$list->isBuiltIn()) { ?>
+            if (!$list || $list->hasAbbrev()) { ?>
             <th>Abbrev <em style="display:inline">&mdash; Abbreviations and such</em></th>
             <?php
             } ?>
@@ -272,7 +272,7 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
                 ?>
             </td>
             <?php
-            if (!$list->isBuiltIn()) { ?>
+            if ($list->hasAbbrev()) { ?>
             <td><input type="text" size="30" name="abbrev-<?php echo $id; ?>"
                 value="<?php echo $i->getAbbrev(); ?>"/></td>
             <?php
@@ -302,20 +302,24 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
         </tr>
     <?php }
     }
-    for ($i=0; $i<$newcount; $i++) { ?>
+
+    if (!$list || $list->allowAdd()) {
+       for ($i=0; $i<$newcount; $i++) { ?>
         <tr>
             <td><?php echo $icon; ?> <em>+</em>
                 <input type="hidden" name="sort-new-<?php echo $i; ?>"/></td>
             <td><input type="text" size="40" name="value-new-<?php echo $i; ?>"/></td>
             <?php
-            if (!$list || !$list->isBuiltIn()) { ?>
+            if (!$list || $list->hasAbbrev()) { ?>
             <td><input type="text" size="30" name="abbrev-new-<?php echo $i; ?>"/></td>
             <?php
             } ?>
             <td>&nbsp;</td>
             <td>&nbsp;</td>
         </tr>
-    <?php } ?>
+    <?php
+       }
+    }?>
     </tbody>
     </table>
 </div>
diff --git a/include/staff/dynamic-lists.inc.php b/include/staff/dynamic-lists.inc.php
index 9a818ec5d..e6c69cbb2 100644
--- a/include/staff/dynamic-lists.inc.php
+++ b/include/staff/dynamic-lists.inc.php
@@ -12,9 +12,6 @@ $pageNav = new Pagenate($count, $page, PAGE_LIMIT);
 $pageNav->setURL('lists.php');
 $showing=$pageNav->showing().' Dynamic Lists';
 
-// Get built-in list
-$builtInList = BuiltInCustomList::getLists();
-
 ?>
 <form action="lists.php" method="POST" name="lists">
 <?php csrf_token(); ?>
@@ -22,25 +19,6 @@ $builtInList = BuiltInCustomList::getLists();
 <input type="hidden" id="action" name="a" value="" >
 <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
     <caption>Custom Lists</caption>
-    <?php
-    if ($builtInList) { ?>
-    <thead>
-        <tr>
-            <th width="7">&nbsp;</th>
-            <th colspan=3>&nbsp;Built-In Lists</th>
-        </tr>
-    </thead>
-    <tbody>
-    <?php foreach ($builtInList as $id => $list) { ?>
-        <tr>
-            <td><i class="<?php echo $list['icon']; ?>"></i></td>
-            <td colspan=3><a href="?id=<?php echo $id; ?>"><?php echo $list['name']; ?></a></td>
-        </tr>
-    <?php }
-    ?>
-    </tbody>
-    <?php
-    } ?>
     <thead>
         <tr>
             <th width="7">&nbsp;</th>
@@ -50,7 +28,7 @@ $builtInList = BuiltInCustomList::getLists();
         </tr>
     </thead>
     <tbody>
-    <?php foreach (DynamicList::objects()->order_by('name')
+    <?php foreach (DynamicList::objects()->order_by('-type', 'name')
                 ->limit($pageNav->getLimit())
                 ->offset($pageNav->getStart()) as $list) {
             $sel = false;
@@ -58,11 +36,19 @@ $builtInList = BuiltInCustomList::getLists();
                 $sel = true; ?>
         <tr>
             <td>
+                <?php
+                if ($list->isDeleteable()) { ?>
                 <input width="7" type="checkbox" class="ckb" name="ids[]"
                 value="<?php echo $list->getId(); ?>"
-                    <?php echo $sel?'checked="checked"':''; ?>></td>
+                    <?php echo $sel?'checked="checked"':''; ?>>
+                <?php
+                } else {
+                    echo '&nbsp;';
+                }
+                ?>
+            </td>
             <td><a href="?id=<?php echo $list->getId(); ?>"><?php echo
-            $list->getName(); ?></a></td>
+            $list->getPluralName() ?: $list->getName(); ?></a></td>
             <td><?php echo $list->get('created'); ?></td>
             <td><?php echo $list->get('updated'); ?></td>
         </tr>
diff --git a/include/staff/templates/list-item-properties.tmpl.php b/include/staff/templates/list-item-properties.tmpl.php
index 6c6f09656..08b6a3815 100644
--- a/include/staff/templates/list-item-properties.tmpl.php
+++ b/include/staff/templates/list-item-properties.tmpl.php
@@ -19,10 +19,11 @@
         <?php
         echo csrf_token();
         $config = $item->getConfiguration();
+        $internal = $item->isInternal();
         foreach ($item->getConfigurationForm()->getFields() as $f) {
             $name = $f->get('id');
             if (isset($config[$name]))
-                $f->value = $config[$name];
+                $f->value = $f->to_php($config[$name]);
             else if ($f->get('default'))
                 $f->value = $f->get('default');
             ?>
@@ -33,7 +34,7 @@
             </td><td>
             <span style="display:inline-block;width:100%">
             <?php
-                if ($item->isInternal() && $f->isConfigurable())
+                if ($internal && !$f->isEditable())
                     $f->render('view');
                 else {
                     $f->render();
diff --git a/include/upgrader/streams/core/8f99b8bf-cbf8c933.patch.sql b/include/upgrader/streams/core/8f99b8bf-cbf8c933.patch.sql
new file mode 100644
index 000000000..6d1b62c33
--- /dev/null
+++ b/include/upgrader/streams/core/8f99b8bf-cbf8c933.patch.sql
@@ -0,0 +1,29 @@
+/**
+ * @version v1.9.3
+ * @signature cbf8c933d6d2eaaa971042eb2efce247
+ * @title Add custom ticket status support
+ *
+ */
+
+ALTER TABLE  `%TABLE_PREFIX%list`
+    ADD  `masks` INT UNSIGNED NOT NULL DEFAULT  '0' AFTER  `sort_mode`,
+    ADD `type` VARCHAR( 16 ) NULL DEFAULT NULL AFTER `masks`,
+    ADD INDEX ( `type` );
+
+CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%ticket_status` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `name` varchar(60) NOT NULL DEFAULT '',
+  `state` varchar(16) NOT NULL DEFAULT 'open',
+  `mode` int(11) unsigned NOT NULL DEFAULT '0',
+  `flags` int(10) unsigned NOT NULL DEFAULT '0',
+  `sort` int(11) unsigned NOT NULL DEFAULT '0',
+  `notes` text NOT NULL,
+  `created` datetime NOT NULL,
+  `updated` datetime NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`)
+) DEFAULT CHARSET=utf8;
+
+UPDATE `%TABLE_PREFIX%config`
+    SET `value` = 'cbf8c933d6d2eaaa971042eb2efce247'
+    WHERE `key` = 'schema_signature' AND `namespace` = 'core';
diff --git a/scp/lists.php b/scp/lists.php
index 84fe3bdc6..562a1ffd7 100644
--- a/scp/lists.php
+++ b/scp/lists.php
@@ -5,10 +5,7 @@ require_once(INCLUDE_DIR.'class.list.php');
 
 $list=null;
 if ($_REQUEST['id']) {
-    if (is_numeric($_REQUEST['id']))
-        $list = DynamicList::lookup($_REQUEST['id']);
-    else
-        $list = BuiltInCustomList::lookup($_REQUEST['id']);
+    $list = DynamicList::lookup($_REQUEST['id']);
 
     if ($list)
          $form = $list->getForm();
@@ -140,7 +137,7 @@ if($_POST) {
             break;
     }
 
-    if ($list) {
+    if ($list && $list->allowAdd()) {
         for ($i=0; isset($_POST["sort-new-$i"]); $i++) {
             if (!$_POST["value-new-$i"])
                 continue;
-- 
GitLab