From 43b74f4a240101ba50a31bb582d34823810f51a9 Mon Sep 17 00:00:00 2001 From: Jared Hancock <jared@osticket.com> Date: Thu, 26 Sep 2013 22:52:33 +0000 Subject: [PATCH] Completion of dynamic forms concept Moved to an initial form which specifies the ticket's priority and issue and changed the rendering to render things properly. Now the user can decide where priority shows on the client side, and the priority privacy setting is placed in the dynamic form wizard. The standard form is added to every ticket without option. Extra forms can be defined and associated with help topics which can additionally be added to tickets upon creation. This allows for standardization of the dynamic data location for searches and filtering. Implemented advanced search for dynamic data. Along with reinstating the basic ticket search on keywords Implemented ticket filtering on dynamic data for both keyword searches as well as searches for special fields (drop-down lists, etc.) Phone number for users is now completely optional --- css/osticket.css | 7 + include/ajax.forms.php | 15 +- include/ajax.tickets.php | 101 ++++-- include/api.tickets.php | 28 +- include/class.dynamic_forms.php | 219 +++++++++---- include/class.filter.php | 62 ++-- include/class.forms.php | 307 ++++++++++++++---- include/class.orm.php | 6 +- include/class.ticket.php | 79 +++-- include/class.topic.php | 9 - include/client/open.inc.php | 30 +- .../client/templates/dynamic-form.tmpl.php | 20 +- include/client/tickets.inc.php | 4 +- include/i18n/en_US/form.yaml | 91 ++++-- include/staff/dynamic-form.inc.php | 78 ++++- include/staff/dynamic-forms.inc.php | 42 ++- include/staff/filter.inc.php | 13 +- include/staff/helptopic.inc.php | 6 +- include/staff/templates/dynamic-form.tmpl.php | 15 +- include/staff/ticket-edit.inc.php | 21 +- include/staff/ticket-open.inc.php | 42 +-- include/staff/ticket-view.inc.php | 15 +- include/staff/tickets.inc.php | 113 ++----- .../streams/core/d51f303a-DYNAMICF.patch.sql | 210 ++++++------ .../streams/core/d51f303a-DYNAMICF.task.php | 21 ++ open.php | 31 +- scp/css/scp.css | 16 +- scp/forms.php | 24 +- scp/tickets.php | 35 +- setup/inc/streams/core/install-mysql.sql | 10 +- 30 files changed, 1018 insertions(+), 652 deletions(-) create mode 100644 include/upgrader/streams/core/d51f303a-DYNAMICF.task.php diff --git a/css/osticket.css b/css/osticket.css index 87e6c6aed..8b986fef4 100644 --- a/css/osticket.css +++ b/css/osticket.css @@ -43,3 +43,10 @@ display: block; opacity: 0.3; } + +div.section-break { + margin-top: 1em; + margin-bottom: 0.5em; + padding-top: 0.8em !important; + border-top: 1px solid #ccc; +} diff --git a/include/ajax.forms.php b/include/ajax.forms.php index ac49c8cdb..17cae9189 100644 --- a/include/ajax.forms.php +++ b/include/ajax.forms.php @@ -15,12 +15,8 @@ class DynamicFormsAjaxAPI extends AjaxController { function getFormsForHelpTopic($topic_id, $client=false) { $topic = Topic::lookup($topic_id); - $form =DynamicForm::lookup($topic->ht['form_id']); - $set=$form; - if ($client) - include(CLIENTINC_DIR . 'templates/dynamic-form.tmpl.php'); - else - include(STAFFINC_DIR . 'templates/dynamic-form.tmpl.php'); + if ($form =DynamicForm::lookup($topic->ht['form_id'])) + $form->render(!$client); } function getClientFormsForHelpTopic($topic_id) { @@ -66,8 +62,10 @@ class DynamicFormsAjaxAPI extends AjaxController { $static->data($data); $custom = array(); - foreach ($user->getDynamicData() as $cd) + foreach ($user->getDynamicData() as $cd) { + $cd->addMissingFields(); $custom[] = $cd->getForm(); + } include(STAFFINC_DIR . 'templates/user-info.tmpl.php'); } @@ -80,8 +78,9 @@ class DynamicFormsAjaxAPI extends AjaxController { $custom_data = $user->getDynamicData(); $custom = array(); foreach ($custom_data as $cd) { + $cd->addMissingFields(); $cf = $custom[] = $cd->getForm(); - $valid &= $cf->isValid(); + $valid &= $cd->isValid(); } if (!$valid) { diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index 771dcf7f6..c664466fb 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -17,6 +17,7 @@ if(!defined('INCLUDE_DIR')) die('403'); include_once(INCLUDE_DIR.'class.ticket.php'); +require_once(INCLUDE_DIR.'class.ajax.php'); class TicketsAjaxAPI extends AjaxController { @@ -77,7 +78,7 @@ class TicketsAjaxAPI extends AjaxController { $sql.=' OR dept_id IN ('.implode(',', db_input($depts)).')'; $sql.=' ) ' - .' GROUP BY email.value ' + .' GROUP BY email.address ' .' ORDER BY ticket.created LIMIT '.$limit; if(($res=db_query($sql)) && db_num_rows($res)) { @@ -88,11 +89,11 @@ class TicketsAjaxAPI extends AjaxController { return $this->json_encode($tickets); } - function search() { + function _search($req) { global $thisstaff, $cfg; $result=array(); - $select = 'SELECT count( DISTINCT ticket.ticket_id) as tickets '; + $select = 'SELECT DISTINCT ticket.ticket_id'; $from = ' FROM '.TICKET_TABLE.' ticket '; $where = ' WHERE 1 '; @@ -108,15 +109,15 @@ class TicketsAjaxAPI extends AjaxController { $where.=' ) '; //Department - if($_REQUEST['deptId']) - $where.=' AND ticket.dept_id='.db_input($_REQUEST['deptId']); + if($req['deptId']) + $where.=' AND ticket.dept_id='.db_input($req['deptId']); //Help topic - if($_REQUEST['topicId']) - $where.=' AND ticket.topic_id='.db_input($_REQUEST['topicId']); + if($req['topicId']) + $where.=' AND ticket.topic_id='.db_input($req['topicId']); //Status - switch(strtolower($_REQUEST['status'])) { + switch(strtolower($req['status'])) { case 'open': $where.=' AND ticket.status="open" '; break; @@ -132,9 +133,9 @@ class TicketsAjaxAPI extends AjaxController { } //Assignee - if(isset($_REQUEST['assignee']) && strcasecmp($_REQUEST['status'], 'closed')) { - $id=preg_replace("/[^0-9]/", "", $_REQUEST['assignee']); - $assignee = $_REQUEST['assignee']; + if(isset($req['assignee']) && strcasecmp($req['status'], 'closed')) { + $id=preg_replace("/[^0-9]/", "", $req['assignee']); + $assignee = $req['assignee']; $where.= ' AND ( ( ticket.status="open" '; if($assignee[0]=='t') $where.=' AND ticket.team_id='.db_input($id); @@ -145,19 +146,19 @@ class TicketsAjaxAPI extends AjaxController { $where.=')'; - if($_REQUEST['staffId'] && !$_REQUEST['status']) //Assigned TO + Closed By - $where.= ' OR (ticket.staff_id='.db_input($_REQUEST['staffId']). ' AND ticket.status="closed") '; - elseif(isset($_REQUEST['staffId'])) // closed by any + if($req['staffId'] && !$req['status']) //Assigned TO + Closed By + $where.= ' OR (ticket.staff_id='.db_input($req['staffId']). ' AND ticket.status="closed") '; + elseif(isset($req['staffId'])) // closed by any $where.= ' OR ticket.status="closed" '; $where.= ' ) '; - } elseif($_REQUEST['staffId']) { - $where.=' AND (ticket.staff_id='.db_input($_REQUEST['staffId']).' AND ticket.status="closed") '; + } elseif($req['staffId']) { + $where.=' AND (ticket.staff_id='.db_input($req['staffId']).' AND ticket.status="closed") '; } //dates - $startTime =($_REQUEST['startDate'] && (strlen($_REQUEST['startDate'])>=8))?strtotime($_REQUEST['startDate']):0; - $endTime =($_REQUEST['endDate'] && (strlen($_REQUEST['endDate'])>=8))?strtotime($_REQUEST['endDate']):0; + $startTime =($req['startDate'] && (strlen($req['startDate'])>=8))?strtotime($req['startDate']):0; + $endTime =($req['endDate'] && (strlen($req['endDate'])>=8))?strtotime($req['endDate']):0; if( ($startTime && $startTime>time()) or ($startTime>$endTime && $endTime>0)) $startTime=$endTime=0; @@ -168,23 +169,65 @@ class TicketsAjaxAPI extends AjaxController { $where.=' AND ticket.created<=FROM_UNIXTIME('.$endTime.')'; //Query - if($_REQUEST['query']) { - $queryterm=db_real_escape($_REQUEST['query'], false); - - $from.=' LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON (ticket.ticket_id=thread.ticket_id )'; - $where.=" AND ( ticket.email LIKE '%$queryterm%'" - ." OR ticket.name LIKE '%$queryterm%'" - ." OR ticket.subject LIKE '%$queryterm%'" + if($req['query']) { + $queryterm=db_real_escape($req['query'], false); + + $from.=' LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON (ticket.ticket_id=thread.ticket_id )' + .' LEFT JOIN '.FORM_ENTRY_TABLE.' tentry ON (tentry.object_id = ticket.ticket_id + AND tentry.object_type="T") + LEFT JOIN '.FORM_ANSWER_TABLE.' tans ON (tans.entry_id = tentry.id + AND tans.value_id IS NULL) + LEFT JOIN '.FORM_ENTRY_TABLE.' uentry ON (uentry.object_id = ticket.user_id + AND uentry.object_type="U") + LEFT JOIN '.FORM_ANSWER_TABLE.' uans ON (uans.entry_id = uentry.id + AND uans.value_id IS NULL) + LEFT JOIN '.USER_TABLE.' user ON (ticket.user_id = user.id) + LEFT JOIN '.USER_EMAIL_TABLE.' uemail ON (user.id = uemail.user_id)'; + + $where.=" AND ( uemail.address LIKE '%$queryterm%'" + ." OR user.name LIKE '%$queryterm%'" + ." OR tans.value LIKE '%$queryterm%'" + ." OR uans.value LIKE '%$queryterm%'" ." OR thread.title LIKE '%$queryterm%'" ." OR thread.body LIKE '%$queryterm%'" .' )'; } + // Dynamic fields + $dynfields='(SELECT entry.object_id, value, value_id FROM '.FORM_ANSWER_TABLE.' ans '. + 'LEFT JOIN '.FORM_ENTRY_TABLE.' entry ON entry.id=ans.entry_id '. + 'LEFT JOIN '.FORM_FIELD_TABLE.' field ON field.id=ans.field_id '. + 'WHERE field.name = %1$s AND entry.object_type="T")'; + foreach (TicketForm::getInstance()->getFields() as $f) { + if ($f->get('name') && isset($req[$f->getFormName()]) + && ($val = $req[$f->getFormName()])) { + $name = 'dyn_'.$f->get('id'); + $from .= ' LEFT JOIN '.sprintf($dynfields, db_input($f->get('name'))) + ." $name ON ($name.object_id = ticket.ticket_id)"; + $where .= " AND ($name.value_id = ".db_input($val) + . " OR $name.value LIKE '%".db_real_escape($val)."%')"; + } + } + $sql="$select $from $where"; - if(($tickets=db_result(db_query($sql)))) { - $result['success'] =sprintf("Search criteria matched %s - <a href='tickets.php?%s'>view</a>", - ($tickets>1?"$tickets tickets":"$tickets ticket"), - str_replace(array('&', '&'), array('&', '&'), $_SERVER['QUERY_STRING'])); + $res = db_query($sql); + while (list($tickets[]) = db_fetch_row($res)); + $tickets = array_filter($tickets); + + return $tickets; + } + + function search() { + $tickets = self::_search($_REQUEST); + + if (count($tickets)) { + $uid = md5($_SERVER['QUERY_STRING']); + $_SESSION["adv_$uid"] = $tickets; + $result['success'] =sprintf( + "Search criteria matched %d %s - <a href='tickets.php?%s'>view</a>", + count($tickets), (count($tickets)>1?"tickets":"ticket"), + 'advsid='.$uid + ); } else { $result['fail']='No tickets found matching your search criteria.'; } diff --git a/include/api.tickets.php b/include/api.tickets.php index b57f65125..81dac7a92 100644 --- a/include/api.tickets.php +++ b/include/api.tickets.php @@ -21,9 +21,12 @@ class TicketApiController extends ApiController { if (isset($data['topicId'])) { $topic=Topic::lookup($data['topicId']); $form=DynamicForm::lookup($topic->ht['form_id']); - foreach ($form->getFields() as $field) + foreach ($form->getDynamicFields() as $field) $supported[] = $field->get('name'); } + $form = TicketForm::lookup()->instanciate(); + foreach ($form->getDynamicFields() as $field) + $supported[] = $field->get('name'); if(!strcasecmp($format, 'email')) { $supported = array_merge($supported, array('header', 'mid', @@ -98,16 +101,13 @@ class TicketApiController extends ApiController { # Create the ticket with the data (attempt to anyway) $errors = array(); - $topic=Topic::lookup($data['topicId']); - $form=DynamicForm::lookup($topic->ht['form_id'])->instanciate(); - # Collect name, email address, and subject for banning and such - foreach ($form->getFields() as $field) { - $fname = $field->get('name'); - if ($fname && isset($data[$fname])) - $field->value = $data[$fname]; + if ($topic=Topic::lookup($data['topicId'])) { + if ($form=DynamicForm::lookup($topic->ht['form_id'])) { + $form = $form->instanciate(); + if (!$form->isValid()) + $errors += $form->errors(); + } } - if (!$form->isValid()) - $errors = array_merge($errors, $form->errors()); $ticket = Ticket::create($data, $errors, $data['source'], $autorespond, $alert); # Return errors (?) @@ -124,10 +124,10 @@ class TicketApiController extends ApiController { return $this->exerr(500, "Unable to create new ticket: unknown error"); } - # Save dynamic forms - foreach ($forms as $f) { - $f->setTicketId($ticket->getId()); - $f->save(); + # Save dynamic form + if (isset($form)) { + $form->setTicketId($ticket->getId()); + $form->save(); } return $ticket; diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php index 0418bab1e..f38f09bf6 100644 --- a/include/class.dynamic_forms.php +++ b/include/class.dynamic_forms.php @@ -45,7 +45,8 @@ class DynamicForm extends VerySimpleModel { if (!isset($this->_fields)) { $this->_fields = array(); foreach ($this->getDynamicFields() as $f) - $this->_fields[] = $f->getImpl(); + // TODO: Index by field name or id + $this->_fields[$f->get('id')] = $f->getImpl($f); } return $this->_fields; } @@ -111,22 +112,24 @@ class DynamicForm extends VerySimpleModel { } return $inst; } - - static function addFormTypes($types) { - static::$types += $types; - } - - static function allTypes() { - return static::$types; - } } class UserForm extends DynamicForm { + static $instance; + static function objects() { $os = parent::objects(); return $os->filter(array('type'=>'U')); } + static function getInstance() { + if (!isset(static::$instance)) { + $o = static::objects(); + static::$instance = $o[0]->instanciate(); + } + return static::$instance; + } + function getStaticForm() { static $form = null; if (!$form) @@ -134,21 +137,45 @@ class UserForm extends DynamicForm { 'email' => new TextboxField(array( 'id'=>'email', 'label'=>'Email Address', 'required'=>true, 'validator' => 'email', 'configuration'=>array( - 'autocomplete'=>false, 'classes'=>array('typeahead'), + 'autocomplete'=>false, 'classes'=>'typeahead', 'size'=>40) )), 'name' => new TextboxField(array( 'id'=>'name', 'label'=>'Full Name', 'required'=>true, 'configuration' => array('size'=>40), )), - 'phone' => new PhoneField(array( - 'id'=>'c', 'label'=>'Phone Number', 'required'=>false, 'default'=>'', - )), ), 'User Information'); return $form; } } +class TicketForm extends DynamicForm { + static $instance; + + static function objects() { + $os = parent::objects(); + return $os->filter(array('type'=>'T')); + } + + static function getInstance() { + if (!isset(static::$instance)) { + $o = static::objects(); + static::$instance = $o[0]->instanciate(); + } + return static::$instance; + } +} +// Add fields from the standard ticket form to the ticket filterable fields +Filter::addSupportedMatches('Dynamic Fields', function() { + $matches = array(); + foreach (TicketForm::getInstance()->getFields() as $f) { + if (!$f->hasData()) + continue; + $matches['field.'.$f->get('id')] = $f->getLabel(); + } + return $matches; +}); + require_once(INCLUDE_DIR . "class.json.php"); class DynamicFormField extends VerySimpleModel { @@ -200,8 +227,8 @@ class DynamicFormField extends VerySimpleModel { * errors. If false, the errors were written into the received errors * array. */ - function setConfiguration($errors) { - $errors = $config = array(); + function setConfiguration(&$errors=array()) { + $config = array(); foreach ($this->getConfigurationForm() as $name=>$field) { $config[$name] = $field->getClean(); $errors = array_merge($errors, $field->errors()); @@ -212,6 +239,19 @@ class DynamicFormField extends VerySimpleModel { return count($errors) === 0; } + function isDeletable() { + return $this->get('edit_mask') & 1; + } + function isNameEditable() { + return $this->get('edit_mask') & 2; + } + function isPrivacyForced() { + return $this->get('edit_mask') & 4; + } + function isRequirementForced() { + return $this->get('edit_mask') & 8; + } + function delete() { // Don't really delete form fields as that will screw up the data // model. Instead, just drop the association with the form which @@ -265,6 +305,8 @@ class DynamicFormEntry extends VerySimpleModel { var $_values; var $_fields; var $_form; + var $_errors = false; + var $_clean = false; function getAnswers() { if (!isset($this->_values)) { @@ -283,6 +325,16 @@ class DynamicFormEntry extends VerySimpleModel { return $ans->getValue(); return null; } + function setAnswer($name, $value, $id=false) { + foreach ($this->getAnswers() as $ans) { + if ($ans->getField()->get('name') == $name) { + $ans->set('value', $value); + if ($id !== false) + $ans->set('value_id', $id); + break; + } + } + } function errors() { return $this->_errors; @@ -292,7 +344,7 @@ class DynamicFormEntry extends VerySimpleModel { function getInstructions() { return $this->getForm()->getInstructions(); } function getForm() { - if (!$this->_form) { + if (!isset($this->_form)) { $this->_form = DynamicForm::lookup($this->get('form_id')); if ($this->id) $this->_form->data($this); @@ -301,7 +353,7 @@ class DynamicFormEntry extends VerySimpleModel { } function getFields() { - if (!$this->_fields) { + if (!isset($this->_fields)) { $this->_fields = array(); foreach ($this->getAnswers() as $a) $this->_fields[] = $a->getField(); @@ -350,6 +402,10 @@ class DynamicFormEntry extends VerySimpleModel { $this->object_id = $user_id; } + function render($staff=true) { + return $this->getForm()->render($staff); + } + /** * addMissingFields * @@ -359,20 +415,26 @@ class DynamicFormEntry extends VerySimpleModel { * entry. */ function addMissingFields() { - foreach ($this->getForm()->getFields() as $field) { + foreach ($this->getForm()->getDynamicFields() as $field) { $found = false; foreach ($this->getAnswers() as $answer) { if ($answer->get('field_id') == $field->get('id')) { $found = true; break; } } - if (!$found) { + if (!$found && ($field = $field->getImpl($field)) + && !$field->isPresentationOnly()) { $a = DynamicFormEntryAnswer::create( array('field_id'=>$field->get('id'), 'entry_id'=>$this->id)); $a->field = $field; + $a->entry = $this; // Add to list of answers $this->_values[] = $a; - $a->save(); + $this->_fields[] = $field; + // Omit fields without data + if ($field->hasData()) + $a->save(); + unset($this->_form); } } } @@ -382,9 +444,18 @@ class DynamicFormEntry extends VerySimpleModel { $this->set('updated', new SqlFunction('NOW')); parent::save(); foreach ($this->getAnswers() as $a) { - $a->set('value', $a->getField()->to_database($a->getField()->getClean())); + $field = $a->getField(); + $val = $field->to_database($field->getClean()); + if (is_array($val)) { + $a->set('value', $val[0]); + $a->set('value_id', $val[1]); + } + else + $a->set('value', $val); $a->set('entry_id', $this->get('id')); - $a->save(); + // Don't save answers for presentation-only fields + if ($field->hasData() && !$field->isPresentationOnly()) + $a->save(); } $this->_values = array(); } @@ -393,6 +464,7 @@ class DynamicFormEntry extends VerySimpleModel { $inst = parent::create($ht); $inst->set('created', new SqlFunction('NOW')); foreach ($inst->getForm()->getFields() as $f) { + if (!$f->hasData()) continue; $a = DynamicFormEntryAnswer::create( array('field_id'=>$f->get('id'))); $a->field = $f; @@ -440,7 +512,8 @@ class DynamicFormEntryAnswer extends VerySimpleModel { function getField() { if (!isset($this->field)) { - $this->field = DynamicFormField::lookup($this->get('field_id'))->getImpl(); + $f = DynamicFormField::lookup($this->get('field_id')); + $this->field = $f->getImpl($f); $this->field->answer = $this; } return $this->field; @@ -448,10 +521,15 @@ class DynamicFormEntryAnswer extends VerySimpleModel { function getValue() { if (!$this->_value) - $this->_value = $this->getField()->to_php($this->get('value')); + $this->_value = $this->getField()->to_php( + $this->get('value'), $this->get('value_id')); return $this->_value; } + function getIdValue() { + return $this->get('value_id'); + } + function toString() { return $this->getField()->toString($this->getValue()); } @@ -490,7 +568,7 @@ class DynamicList extends VerySimpleModel { } function getPluralName() { - if ($name = $this->get('plural_name')) + if ($name = $this->get('name_plural')) return $name; else return $this->get('name') . 's'; @@ -530,13 +608,13 @@ class DynamicList extends VerySimpleModel { $selections = array(); foreach (DynamicList::objects() as $list) { $selections['list-'.$list->id] = - array('Selection: ' . $list->getPluralName(), + array($list->getPluralName(), SelectionField, $list->get('id')); } return $selections; } } -FormField::addFieldTypes(array(DynamicList, 'getSelections')); +FormField::addFieldTypes('Custom Lists', array(DynamicList, 'getSelections')); /** * Represents a single item in a dynamic list @@ -576,12 +654,14 @@ class DynamicListItem extends VerySimpleModel { } class SelectionField extends FormField { + function getListId() { + list(,$list_id) = explode('-', $this->get('type')); + return $list_id; + } + function getList() { - if (!$this->_list) { - $list_id = explode('-', $this->get('type')); - $list_id = $list_id[1]; - $this->_list = DynamicList::lookup($list_id); - } + if (!$this->_list) + $this->_list = DynamicList::lookup($this->getListId()); return $this->_list; } @@ -589,33 +669,35 @@ class SelectionField extends FormField { return new SelectionWidget($this); } - function parse($id) { - return $this->to_php($id); + function parse($value) { + return $this->to_php($value); } - function to_php($id) { - if (!$id) - return null; - list($id, $value) = explode(':', $id); - $item = DynamicListItem::lookup($id); + function to_php($value, $id=false) { + $item = DynamicListItem::lookup($id ? $id : $value); # Attempt item lookup by name too if (!$item) { - $item = DynamicListItem::objects()->filter(array( - 'value'=>$id, - 'list_id'=>$this->getList()->get('id'))); - $item = (count($item)) ? $item[0] : null; + $item = DynamicListItem::lookup(array( + 'value'=>$value, + 'list_id'=>$this->getListId())); } - return $item; + return ($item) ? $item : $id; } function to_database($item) { - if ($item && $item->get('id')) - return $item->id . ':' . $item->value; + if ($item instanceof DynamicListItem) + return array($item->value, $item->id); return null; } function toString($item) { - return ($item) ? $item->toString() : ''; + return ($item instanceof DynamicListItem) ? $item->toString() : $item; + } + + function validateEntry($item) { + parent::validateEntry($item); + if ($item && !$item instanceof DynamicListItem) + $this->_errors[] = 'Select a value from the list'; } function getConfigurationOptions() { @@ -627,21 +709,30 @@ class SelectionField extends FormField { 'hint'=>'Typeahead will work better for large lists')), ); } + + function getChoices() { + if (!$this->_choices) { + $this->_choices = array(); + foreach ($this->getList()->getItems() as $i) + $this->_choices[$i->get('id')] = $i->get('value'); + } + return $this->_choices; + } } class SelectionWidget extends ChoicesWidget { function render() { $config = $this->field->getConfiguration(); $value = false; - if (is_object($this->value) && get_class($this->value) == 'DynamicListItem') { + if ($this->value instanceof DynamicListItem) { // Loaded from database $value = $this->value->get('id'); $name = $this->value->get('value'); } elseif ($this->value) { // Loaded from POST $value = $this->value; - $name = DynamicListItem::lookup($this->value); - $name = ($name) ? $name->get('value') : null; + $name = DynamicListItem::lookup($value); + $name = ($name) ? $name->get('value') : $value; } if (!$config['typeahead']) { @@ -652,22 +743,20 @@ class SelectionWidget extends ChoicesWidget { $source = array(); foreach ($this->field->getList()->getItems() as $i) $source[] = array( - 'info' => $i->get('value'), - 'value' => strtolower($i->get('value').' '.$i->get('extra')), - 'id' => $i->get('id')); + 'value' => $i->get('value'), + 'info' => $i->get('value')." -- ".$i->get('extra'), + ); ?> <span style="display:inline-block"> - <input type="hidden" name="<?php echo $this->name; ?>" - value="<?php echo $value; ?>" /> - <input type="text" size="30" id="<?php echo $this->name; ?>" - value="<?php echo $name; ?>" /> + <input type="text" size="30" name="<?php echo $this->name; ?>" + value="<?php echo $name; ?>" autocomplete="off" /> <script type="text/javascript"> $(function() { - $('#<?php echo $this->name; ?>').typeahead({ + $('input[name=<?php echo $this->name; ?>]').typeahead({ source: <?php echo JsonDataEncoder::encode($source); ?>, + property: 'info', onselect: function(item) { - $('#<?php echo $this->name; ?>').val(item['info']) - $('input[name="<?php echo $this->name; ?>"]').val(item['id']) + $('input[name="<?php echo $this->name; ?>"]').val(item['value']) } }); }); @@ -675,15 +764,5 @@ class SelectionWidget extends ChoicesWidget { </span> <?php } - - function getChoices() { - if (!$this->_choices) { - $this->_choices = array(); - foreach ($this->field->getList()->getItems() as $i) - $this->_choices[$i->get('id')] = $i->get('value'); - } - return $this->_choices; - } } - ?> diff --git a/include/class.filter.php b/include/class.filter.php index 1f9412079..2ee4fbbf2 100644 --- a/include/class.filter.php +++ b/include/class.filter.php @@ -18,6 +18,17 @@ class Filter { var $id; var $ht; + static $match_types = array( + 'Basic Fields' => array( + 'name' => 'Name', + 'email' => 'Email', + 'subject' => 'Subject', + 'body' => 'Body/Text', + 'reply-to' => 'Reply-To Email', + 'reply-to-name' => 'Reply-To Name', + ), + ); + function Filter($id) { $this->id=0; $this->load($id); @@ -299,13 +310,28 @@ class Filter { $ticket['cannedResponseId'] = $this->getCannedResponse(); } /* static */ function getSupportedMatches() { - return array( - 'name'=> 'Name', - 'email'=> 'Email', - 'subject'=> 'Subject', - 'body'=> 'Body/Text' - ); + foreach (static::$match_types as $k=>&$v) { + if (is_callable($v)) + $v = $v(); + } + unset($v); + return static::$match_types; + } + + static function addSupportedMatches($group, $callable) { + static::$match_types[$group] = $callable; } + + static function getSupportedMatchFields() { + $keys = array(); + foreach (static::getSupportedMatches() as $group=>$matches) { + foreach ($matches as $key=>$label) { + $keys[] = $key; + } + } + return $keys; + } + /* static */ function getSupportedMatchTypes() { return array( 'equal'=> 'Equal', @@ -370,7 +396,7 @@ class Filter { function save_rules($id,$vars,&$errors) { - $matches = array_keys(self::getSupportedMatches()); + $matches = array_keys(self::getSupportedMatchFields()); $types = array_keys(self::getSupportedMatchTypes()); $rules=array(); @@ -382,7 +408,9 @@ class Filter { $errors["rule_$i"]='Invalid match type selection'; elseif(!$vars["rule_v$i"]) $errors["rule_$i"]='Value required'; - elseif($vars["rule_w$i"]=='email' && $vars["rule_h$i"]=='equal' && !Validator::is_email($vars["rule_v$i"])) + elseif($vars["rule_w$i"]=='email' + && $vars["rule_h$i"]=='equal' + && !Validator::is_email($vars["rule_v$i"])) $errors["rule_$i"]='Valid email required for the match type'; else //for everything-else...we assume it's valid. $rules[]=array('what'=>$vars["rule_w$i"], @@ -640,22 +668,18 @@ class TicketFilter { * deal with the data in the incoming ticket (based on $vars) will be considered. * @see ::quickList() for more information. */ - function TicketFilter($origin, $vars=null) { + function TicketFilter($origin, $vars=array()) { //Normalize the target based on ticket's origin. $this->target = self::origin2target($origin); //Extract the vars we care about (fields we filter by!). - $this->vars = array_filter(array_map('trim', - array( - 'email' => $vars['email'], - 'subject' => $vars['subject'], - 'name' => $vars['name'], - 'body' => $vars['message'], - 'emailId' => $vars['emailId'], - 'reply-to' => @$vars['reply-to'], - 'reply-to-name' => @$vars['reply-to-name'], - ))); + $this->vars = array('body'=>$vars['message']); + $interest = Filter::getSupportedMatchFields(); + foreach ($vars as $k=>$v) { + if (in_array($k, $interest)) + $this->vars[$k] = trim($v); + } //Init filters. $this->build(); diff --git a/include/class.forms.php b/include/class.forms.php index cc5d07f07..b9a6a2dbe 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -20,7 +20,7 @@ */ class Form { var $fields = array(); - var $title = 'Unnamed Form'; + var $title = ''; var $instructions = ''; var $_errors; @@ -93,14 +93,21 @@ class FormField { ); var $_cform; + var $_clean; + var $_errors = array(); + var $parent; static $types = array( - 'text' => array('Short Answer', TextboxField), - 'memo' => array('Long Answer', TextareaField), - 'datetime' => array('Date and Time', DatetimeField), - 'phone' => array('Phone Number', PhoneField), - 'bool' => array('Checkbox', BooleanField), - 'choices' => array('Choices', ChoiceField), + 'Basic Fields' => array( + 'text' => array('Short Answer', TextboxField), + 'memo' => array('Long Answer', TextareaField), + 'thread' => array('Thread Entry', ThreadEntryField, false), + 'datetime' => array('Date and Time', DatetimeField), + 'phone' => array('Phone Number', PhoneField), + 'bool' => array('Checkbox', BooleanField), + 'choices' => array('Choices', ChoiceField), + 'break' => array('Section Break', SectionBreakField), + ), ); static $more_types = array(); @@ -114,20 +121,25 @@ class FormField { $this->ht['id'] = $uid++; } - static function addFieldTypes($callable) { - static::$more_types[] = $callable; + static function addFieldTypes($group, $callable) { + static::$more_types[$group] = $callable; } static function allTypes() { if (static::$more_types) { - foreach (static::$more_types as $c) - static::$types = array_merge(static::$types, - call_user_func($c)); + foreach (static::$more_types as $group=>$c) + static::$types[$group] = call_user_func($c); static::$more_types = array(); } return static::$types; } + static function getFieldType($type) { + foreach (static::allTypes() as $group=>$types) + if (isset($types[$type])) + return $types[$type]; + } + function get($what) { return $this->ht[$what]; } @@ -141,15 +153,16 @@ class FormField { * user-entered data. */ function getClean() { - $value = $this->getWidget()->value; - $value = $this->parse($value); - $this->validateEntry($value); - return $value; + if (!isset($this->_clean)) { + $value = $this->getWidget()->value; + $this->_clean = $this->parse($value); + $this->validateEntry($this->_clean); + } + return $this->_clean; } function errors() { - if (!$this->_errors) return array(); - else return $this->_errors; + return $this->_errors; } function isValidEntry() { @@ -168,14 +181,13 @@ class FormField { * $value - (string) input from the user */ function validateEntry($value) { + if (!$value && count($this->_errors)) + return; + # Validates a user-input into an instance of this field on a dynamic # form - if (!is_array($this->_errors)) { - $this->_errors = array(); - - if ($this->get('required') && !$value) - $this->_errors[] = $this->getLabel() . ' is a required field'; - } + if ($this->get('required') && !$value && $this->hasData()) + $this->_errors[] = sprintf('%s is a required field', $this->getLabel()); } /** @@ -253,13 +265,20 @@ class FormField { * For instance, if the value of this field is 'text', a TextField * instance will be returned. */ - function getImpl() { + function getImpl($parent=null) { // Allow registration with ::addFieldTypes and delayed calling - $types = static::allTypes(); - $clazz = $types[$this->get('type')][1]; + $this->parent = $parent; + $type = static::getFieldType($this->get('type')); + $clazz = $type[1]; return new $clazz($this->ht); } + function __call($what, $args) { + // XXX: Throw exception if $this->parent is not set + return call_user_func_array( + array($this->parent, $what), $args); + } + function getAnswer() { return $this->answer; } function getFormName() { @@ -269,8 +288,12 @@ class FormField { return $this->get('id'); } - function render() { - $this->getWidget()->render(); + function render($mode=null) { + $this->getWidget()->render($mode); + } + + function renderExtras($mode=null) { + return; } function getConfigurationOptions() { @@ -300,14 +323,49 @@ class FormField { return $this->_config; } + /** + * If the [Config] button should be shown to allow for the configuration + * of this field + */ function isConfigurable() { return true; } + /** + * Field type is changeable in the admin interface + */ + function isChangeable() { + return true; + } + + /** + * Field does not contain data that should be saved to the database. Ie. + * non data fields like section headers + */ + function hasData() { + return true; + } + + /** + * Returns true if the field/widget should be rendered as an entire + * block in the target form. + */ + function isBlockLevel() { + return false; + } + + /** + * Fields should not be saved with the dynamic data. It is assumed that + * some static processing will store the data elsewhere. + */ + function isPresentationOnly() { + return false; + } + function getConfigurationForm() { if (!$this->_cform) { - $types = static::allTypes(); - $clazz = $types[$this->get('type')][1]; + $type = static::getFieldType($this->get('type')); + $clazz = $type[1]; $T = new $clazz(); $this->_cform = $T->getConfigurationOptions(); } @@ -333,6 +391,10 @@ class TextboxField extends FormField { 'id'=>3, 'label'=>'Validator', 'required'=>false, 'default'=>'', 'choices' => array('phone'=>'Phone Number','email'=>'Email Address', 'ip'=>'IP Address', 'number'=>'Number', ''=>'None'))), + 'validator-error' => new TextboxField(array( + 'id'=>4, 'label'=>'Validation Error', 'default'=>'', + 'configuration'=>array('size'=>40), + 'hint'=>'Message shown to user if the input does not match the validator')), ); } @@ -354,6 +416,8 @@ class TextboxField extends FormField { $config = $this->getConfiguration(); $valid = $config['validator']; } + if (!$value || !isset($validators[$valid])) + return; $func = $validators[$valid]; if (is_array($func) && is_callable($func[0])) if (!call_user_func($func[0], $value)) @@ -442,6 +506,34 @@ class ChoiceField extends FormField { 'id'=>1, 'label'=>'Choices', 'required'=>false, 'default'=>'')), ); } + + function toString($value) { + $choices = $this->getChoices(); + if (isset($choices[$value])) + return $choices[$value]; + else + return $choices[$this->get('default')]; + } + + function getChoices() { + if ($this->_choices === null) { + // Allow choices to be set in this->ht (for configurationOptions) + $this->_choices = $this->get('choices'); + if (!$this->_choices) { + $this->_choices = array(); + $config = $this->getConfiguration(); + $choices = explode("\n", $config['choices']); + foreach ($choices as $choice) { + // Allow choices to be key: value + list($key, $val) = explode(':', $choice); + if ($val == null) + $val = $key; + $this->_choices[trim($key)] = trim($val); + } + } + } + return $this->_choices; + } } class DatetimeField extends FormField { @@ -516,6 +608,93 @@ class DatetimeField extends FormField { } } +/** + * This is kind-of a special field that doesn't have any data. It's used as + * a field to provide a horizontal section break in the display of a form + */ +class SectionBreakField extends FormField { + function getWidget() { + return new SectionBreakWidget($this); + } + + function hasData() { + return false; + } + + function isBlockLevel() { + return true; + } +} + +class ThreadEntryField extends FormField { + function getWidget() { + return new ThreadEntryWidget($this); + } + function isChangeable() { + return false; + } + function isBlockLevel() { + return true; + } + function isPresentationOnly() { + return true; + } + function renderExtras($mode=null) { + if ($mode == 'client') + $this->getWidget()->showAttachments(); + } +} + +class PriorityField extends ChoiceField { + function getWidget() { + $widget = parent::getWidget(); + if ($widget->value instanceof Priority) + $widget->value = $widget->value->getId(); + return $widget; + } + + function getChoices() { + $this->ht['default'] = 0; + + $sql = 'SELECT priority_id, priority_desc FROM '.PRIORITY_TABLE + .' ORDER BY priority_urgency DESC'; + $choices = array(0 => '— Default —'); + if (!($res = db_query($sql))) + return $choices; + + while ($row = db_fetch_row($res)) + $choices[$row[0]] = $row[1]; + return $choices; + } + + function parse($id) { + return $this->to_php(null, $id); + } + + function to_php($value, $id) { + return Priority::lookup($id); + } + + function to_database($prio) { + return ($prio instanceof Priority) + ? array($prio->getDesc(), $prio->getId()) + : $prio; + } + + function toString($value) { + return ($value instanceof Priority) ? $value->getDesc() : $value; + } + + function getConfigurationOptions() { + return array(); + } +} +FormField::addFieldTypes('Built-in Lists', function() { + return array( + 'priority' => array('Priority Level', PriorityField), + ); +}); + class Widget { function Widget() { # Not called in PHP5 @@ -546,7 +725,7 @@ class TextboxWidget extends Widget { if (isset($config['length'])) $maxlength = "maxlength=\"{$config['length']}\""; if (isset($config['classes'])) - $classes = 'class="'.implode(' ', $config['classes']).'"'; + $classes = 'class="'.$config['classes'].'"'; if (isset($config['autocomplete'])) $autocomplete = 'autocomplete="'.($config['autocomplete']?'on':'off').'"'; ?> @@ -604,7 +783,7 @@ class ChoicesWidget extends Widget { // Determine the value for the default (the one listed if nothing is // selected) $def_key = $this->field->get('default'); - $choices = $this->getChoices(); + $choices = $this->field->getChoices(); $have_def = isset($choices[$def_key]); if (!$have_def) $def_val = 'Select '.$this->field->get('label'); @@ -619,34 +798,14 @@ class ChoicesWidget extends Widget { foreach ($choices as $key=>$name) { if (!$have_def && $key == $def_key) continue; ?> - <option value="<?php echo $key; ?>" - <?php if ($this->value == $key) echo 'selected="selected"'; + <option value="<?php echo $key; ?>" <?php + if ($this->value == $key) echo 'selected="selected"'; ?>><?php echo $name; ?></option> <?php } ?> </select> </span> <?php } - - function getChoices() { - if ($this->_choices === null) { - // Allow choices to be set in this->ht (for configurationOptions) - $this->_choices = $this->field->get('choices'); - if (!$this->_choices) { - $this->_choices = array(); - $config = $this->field->getConfiguration(); - $choices = explode("\n", $config['choices']); - foreach ($choices as $choice) { - // Allow choices to be key: value - list($key, $val) = explode(':', $choice); - if ($val == null) - $val = $key; - $this->_choices[trim($key)] = trim($val); - } - } - } - return $this->_choices; - } } class CheckboxWidget extends Widget { @@ -730,4 +889,42 @@ class DatetimePickerWidget extends Widget { } } +class SectionBreakWidget extends Widget { + function render() { + ?><div class="form-header section-break"><h3><?php + echo Format::htmlchars($this->field->get('label')); + ?></h3><em><?php echo Format::htmlchars($this->field->get('hint')); + ?></em></div> + <?php + } +} + +class ThreadEntryWidget extends Widget { + function render($mode=null) { + echo '<strong>'.Format::htmlchars($this->field->get('label')); + ?></strong>: + <br/> + <textarea name="<?php echo $this->field->get('name'); ?>" + cols="21" rows="8" style="width:80%;"><?php echo + $this->value; ?></textarea> + <?php + } + + function showAttachments() { + global $cfg, $thisclient; + + if(($cfg->allowOnlineAttachments() + && !$cfg->allowAttachmentsOnlogin()) + || ($cfg->allowAttachmentsOnlogin() + && ($thisclient && $thisclient->isValid()))) { ?> + <hr/> + <div><strong>Attachments:</strong></div> + <div class="uploads"></div><br> + <input type="file" class="multifile" name="attachments[]" id="attachments" size="30" value="" /> + <font class="error"> <?php echo $errors['attachments']; ?></font> + <?php + } + } +} + ?> diff --git a/include/class.orm.php b/include/class.orm.php index 86d3015f1..50aae3b23 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -151,8 +151,6 @@ class VerySimpleModel { function save($refetch=false) { $pk = static::$meta['pk']; - if (!$this->isValid()) - return false; if (!is_array($pk)) $pk=array($pk); if ($this->__new__) $sql = 'INSERT INTO '.static::$meta['table']; @@ -632,7 +630,9 @@ class SqlCompiler { list($field, $op) = $this->getField($field, $model); // Allow operators to be callable rather than sprintf // strings - if (is_callable($op)) + if ($value === null) + $filter[] = sprintf('%s IS NULL', $field); + elseif (is_callable($op)) $filter[] = call_user_func($op, $field, $value); else $filter[] = sprintf($op, $field, $this->input($value)); diff --git a/include/class.ticket.php b/include/class.ticket.php index 12c30c02a..ef82b7104 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -111,7 +111,7 @@ class Ticket { foreach (DynamicFormEntry::forTicket($this->getId()) as $form) foreach ($form->getAnswers() as $answer) $this->_answers[$answer->getField()->get('name')] = - $answer->toString(); + $answer->getValue(); } function reload() { @@ -216,7 +216,7 @@ class Ticket { function getName(){ if ($o = $this->getOwner()) - return $o->getFullName(); + return $o->getName(); return null; } @@ -288,11 +288,17 @@ class Ticket { } function getPriorityId() { - return $this->ht['priority_id']; + global $cfg; + + if ($a = $this->_answers['priority']) + return $a->getId(); + return $cfg->getDefaultPriorityId(); } - function getPriority() { //TODO: Make it an obj. - return $this->ht['priority_desc']; + function getPriority() { + if ($a = $this->_answers['priority']) + return $a->getDesc(); + return '...ummm...'; } function getPhone() { @@ -565,20 +571,6 @@ class Ticket { return $this->lastMsgId=$msgid; } - function setPriority($priorityId) { - - //XXX: what happens to SLA priority??? - - if(!$priorityId || $priorityId==$this->getPriorityId()) - return ($priorityId); - - $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW() ' - .', priority_id='.db_input($priorityId) - .' WHERE ticket_id='.db_input($this->getId()); - - return (($res=db_query($sql)) && db_affected_rows($res)); - } - //DeptId can NOT be 0. No orphans please! function setDeptId($deptId) { @@ -1664,7 +1656,6 @@ class Ticket { $fields=array(); $fields['topicId'] = array('type'=>'int', 'required'=>1, 'error'=>'Help topic required'); - $fields['priorityId'] = array('type'=>'int', 'required'=>1, 'error'=>'Priority required'); $fields['slaId'] = array('type'=>'int', 'required'=>0, 'error'=>'Select SLA'); $fields['duedate'] = array('type'=>'date', 'required'=>0, 'error'=>'Invalid date - must be MM/DD/YY'); @@ -1687,7 +1678,6 @@ class Ticket { if($errors) return false; $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW() ' - .' ,priority_id='.db_input($vars['priorityId']) .' ,topic_id='.db_input($vars['topicId']) .' ,sla_id='.db_input($vars['slaId']) .' ,duedate='.($vars['duedate']?db_input(date('Y-m-d G:i',Misc::dbtime($vars['duedate'].' '.$vars['time']))):'NULL'); @@ -1895,6 +1885,23 @@ class Ticket { } } + // Create and verify the dynamic form entry for the new ticket + $form = TicketForm::getInstance(); + // If submitting via email, ensure we have a subject and such + foreach ($form->getFields() as $field) { + $fname = $field->get('name'); + if ($fname && isset($vars[$fname]) && !$field->value) + $field->value = $vars[$fname]; + } + + // Don't enforce form validation for email + if (!$form->isValid() && strtolower($origin) != 'email') + $errors += $form->errors(); + + // Unpack dynamic variables into $vars for filter application + foreach ($form->getFields() as $f) + $vars['field.'.$f->get('id')] = $f->toString($f->getClean()); + //Init ticket filters... $ticket_filter = new TicketFilter($origin, $vars); // Make sure email contents should not be rejected @@ -1930,7 +1937,6 @@ class Ticket { # TODO: Return error message $errors['err']=$errors['origin'] = 'Invalid origin given'; } - $fields['priorityId'] = array('type'=>'int', 'required'=>0, 'error'=>'Invalid Priority'); if(!Validator::process($fields, $vars, $errors) && !$errors['err']) $errors['err'] ='Missing or invalid data - check the errors and try again'; @@ -1945,16 +1951,19 @@ class Ticket { $errors['duedate']='Due date must be in the future'; } - //Any error above is fatal. - if($errors) return 0; - # Identify the user creating the ticket $user_info = UserForm::getStaticForm()->getClean(); + // Data is slightly different between HTTP posts and emails if (isset($vars['emailId']) || !isset($user_info['email'])) $user_info = $vars; + elseif (!UserForm::getStaticForm()->isValid()) + $errors['user'] = 'Incomplete client information'; $user = User::fromForm($user_info); $user_email = UserEmail::ensure($user_info['email']); + //Any error above is fatal. + if($errors) return 0; + # Perform ticket filter actions on the new ticket arguments if ($ticket_filter) $ticket_filter->apply($vars); @@ -1964,13 +1973,13 @@ class Ticket { // OK...just do it. $deptId=$vars['deptId']; //pre-selected Dept if any. - $priorityId=$vars['priorityId']; $source=ucfirst($vars['source']); $topic=NULL; // Intenal mapping magic...see if we need to override anything if(isset($vars['topicId']) && ($topic=Topic::lookup($vars['topicId']))) { //Ticket created via web by user/or staff $deptId=$deptId?$deptId:$topic->getDeptId(); - $priorityId=$priorityId?$priorityId:$topic->getPriorityId(); + if (!$form->getAnswer('priority')) + $form->setAnswer('priority', null, $topic->getPriorityId()); if($autorespond) $autorespond=$topic->autoRespond(); $source=$vars['source']?$vars['source']:'Web'; @@ -1988,13 +1997,15 @@ class Ticket { }elseif($vars['emailId'] && !$vars['deptId'] && ($email=Email::lookup($vars['emailId']))) { //Emailed Tickets $deptId=$email->getDeptId(); - $priorityId=$priorityId?$priorityId:$email->getPriorityId(); + if (!$form->getAnswer('priority')) + $form->setAnswer('priority', null, $email->getPriorityId()); if($autorespond) $autorespond=$email->autoRespond(); $email=null; $source='Email'; } //Last minute checks - $priorityId=$priorityId?$priorityId:$cfg->getDefaultPriorityId(); + if (!$form->getAnswer('priority')) + $form->setAnswer('priority', null, $cfg->getDefaultPriorityId()); $deptId=$deptId?$deptId:$cfg->getDefaultDeptId(); $topicId=$vars['topicId']?$vars['topicId']:0; $ipaddress=$vars['ip']?$vars['ip']:$_SERVER['REMOTE_ADDR']; @@ -2008,7 +2019,6 @@ class Ticket { .' ,ticketID='.db_input($extId) .' ,dept_id='.db_input($deptId) .' ,topic_id='.db_input($topicId) - .' ,priority_id='.db_input($priorityId) .' ,ip_address='.db_input($ipaddress) .' ,source='.db_input($source); @@ -2029,6 +2039,10 @@ class Ticket { //TODO: RETHING what happens if this fails?? [At the moment on failure random ID is used...making stuff usable] } + // Save the (common) dynamic form + $form->setTicketId($id); + $form->save(); + $dept = $ticket->getDept(); //post the message. @@ -2100,11 +2114,6 @@ class Ticket { if($vars['source'] && !in_array(strtolower($vars['source']),array('email','phone','other'))) $errors['source']='Invalid source - '.Format::htmlchars($vars['source']); - if(!$vars['issue']) - $errors['issue']='Summary of the issue required'; - else - $vars['message']=$vars['issue']; - if(!($ticket=Ticket::create($vars, $errors, 'staff', false, (!$vars['assignId'])))) return false; diff --git a/include/class.topic.php b/include/class.topic.php index d924f2ab7..8f1eb7365 100644 --- a/include/class.topic.php +++ b/include/class.topic.php @@ -209,15 +209,6 @@ class Topic { elseif(($tid=self::getIdByName($vars['topic'], $vars['pid'])) && $tid!=$id) $errors['topic']='Topic already exists'; - if (!$vars['form_id']) - $errors['form_id'] = 'You must select a form'; - else { - $form=DynamicForm::lookup($vars['form_id']); - foreach (array('subject') as $f) - if (!$form->hasField($f)) - $errors['form_id']="Form must define the '$f' field"; - } - if(!$vars['dept_id']) $errors['dept_id']='You must select a department'; diff --git a/include/client/open.inc.php b/include/client/open.inc.php index ac8a9353d..7bb1d9599 100644 --- a/include/client/open.inc.php +++ b/include/client/open.inc.php @@ -39,7 +39,9 @@ $info=($_POST && $errors)?Format::htmlchars($_POST):$info; <font class="error">* <?php echo $errors['topicId']; ?></font> </td> </tr> - <?php UserForm::getStaticForm()->render(false, 'Your Information'); ?> +<?php + UserForm::getStaticForm()->render(false, 'Your Information'); + TicketForm::getInstance()->render(false); ?> </tbody> <tbody id="dynamic-form"> <?php if ($forms) { @@ -49,32 +51,6 @@ $info=($_POST && $errors)?Format::htmlchars($_POST):$info; } ?> </tbody> <tbody> - <tr><td colspan="2"><hr /></td></tr> - <tr> - <td class="required">Message:</td> - <td> - <div style="margin-bottom:0.5em;"><em>Please provide as much detail as possible so we can best assist you.</em> <font class="error">* <?php echo $errors['message']; ?></font> - </div> - <textarea id="message" cols="60" rows="8" name="message" - class="richtext ifhtml draft" - data-draft-namespace="ticket.client" - data-draft-object-id="<?php echo substr(session_id(), -12); ?>" - ><?php echo $info['message']; ?></textarea> - </td> - </tr> - - <?php if(($cfg->allowOnlineAttachments() && !$cfg->allowAttachmentsOnlogin()) - || ($cfg->allowAttachmentsOnlogin() && ($thisclient && $thisclient->isValid()))) { ?> - <tr> - <td>Attachments:</td> - <td> - <div class="uploads"></div><br> - <input type="file" class="multifile" name="attachments[]" id="attachments" size="30" value="" /> - <font class="error"> <?php echo $errors['attachments']; ?></font> - </td> - </tr> - <tr><td colspan=2> </td></tr> - <?php } ?> <?php if($cfg->allowPriorityChange() && ($priorities=Priority::getPriorities())) { ?> <tr> diff --git a/include/client/templates/dynamic-form.tmpl.php b/include/client/templates/dynamic-form.tmpl.php index 49ff01697..1e4e79e8c 100644 --- a/include/client/templates/dynamic-form.tmpl.php +++ b/include/client/templates/dynamic-form.tmpl.php @@ -30,14 +30,22 @@ if ($field->get('private')) continue; ?> - <tr><td class="<?php if ($field->get('required')) echo 'required'; ?>"> - <?php echo Format::htmlchars($field->get('label')); ?>:</td> - <td><?php $field->render(); ?> + <tr> + <?php if ($field->isBlockLevel()) { ?> + <td colspan="2"> + <?php + } + else { ?> + <td class="<?php if ($field->get('required')) echo 'required'; ?>"> + <?php echo Format::htmlchars($field->get('label')); ?>:</td><td> + <?php + } + $field->render('client'); ?> <?php if ($field->get('required')) { ?> <font class="error">*</font> <?php } - if ($field->get('hint')) { ?> + if ($field->get('hint') && !$field->isBlockLevel()) { ?> <br /><em style="color:gray;display:inline-block"><?php echo Format::htmlchars($field->get('hint')); ?></em> <?php @@ -45,7 +53,9 @@ foreach ($field->errors() as $e) { ?> <br /> <font class="error"><?php echo $e; ?></font> - <?php } ?> + <?php } + $field->renderExtras('client'); + ?> </td> </tr> <?php diff --git a/include/client/tickets.inc.php b/include/client/tickets.inc.php index 04898d5e4..d38d113ce 100644 --- a/include/client/tickets.inc.php +++ b/include/client/tickets.inc.php @@ -46,14 +46,14 @@ $qselect='SELECT ticket.ticket_id,ticket.ticketID,ticket.dept_id,isanswered, ' $dynfields='(SELECT entry.object_id, value FROM '.FORM_ANSWER_TABLE.' ans '. 'LEFT JOIN '.FORM_ENTRY_TABLE.' entry ON entry.id=ans.entry_id '. 'LEFT JOIN '.FORM_FIELD_TABLE.' field ON field.id=ans.field_id '. - 'WHERE field.name = "%1$s" entry.object_type="T") %1$s ON ticket.ticket_id = %1$s.ticket_id '; + 'WHERE field.name = "%1$s" AND entry.object_type="T")'; $subject_sql = sprintf($dynfields, 'subject'); $qfrom='FROM '.TICKET_TABLE.' ticket ' .' LEFT JOIN '.DEPT_TABLE.' dept ON (ticket.dept_id=dept.dept_id) ' .' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id' .' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id' - .' LEFT JOIN '.$subject_sql; + .' LEFT JOIN '.$subject_sql.' subject ON ticket.ticket_id = subject.object_id '; $qwhere =' WHERE email.address='.db_input($thisclient->getEmail()); diff --git a/include/i18n/en_US/form.yaml b/include/i18n/en_US/form.yaml index 868b76422..1de219b60 100644 --- a/include/i18n/en_US/form.yaml +++ b/include/i18n/en_US/form.yaml @@ -1,44 +1,83 @@ # -# Default (dynamic) form configuration +# Default (dynamic) form configuration. This data is used as the initial, +# minimal data for dynamic forms that ships with the system. # # Fields: +# id: Used only if associated with a help topic +# title: Bold section title of the form +# instructions: Title deck, detailed instructions on entering form data +# notes: Notes for the form, shown under the fields +# deletable: True if the form can be removed from the system +# fields: List of fields for the form +# type: Field type (short name) (eg. 'text', 'memo', 'phone', ...) +# label: Field label shown to the user +# name: Name used with the data internally. This is especially +# useful for page and email templates, where %{ ticket.<name> } +# will be used to retrieve the data from the field. +# hint: Help text shown with the field +# edit_mask: Mask out edits to the field (1=>delete, 2=>change name, +# 4=>privacy setting, 8=>requirement setting) +# private: True if the field should be hidden from the client +# required: True if entry for the field is required +# configuration: Field-specific configuration +# size: (text) width of the field +# length: (text) maximum size of the data in the field +# cols: (memo) width of the textarea +# rows: (memo) height of the textarea +# --- - id: 1 - title: User Information + type: U # notrans + title: Client Details + deletable: false fields: - - type: text # notrans - name: email # notrans - label: Email Address - required: true - sort: 10 - configuration: - size: 40 - length: 40 - validator: email # notrans - - - type: text # notrans - name: name # notrans - label: Full Name - required: true - sort: 20 - configuration: - size: 40 - length: 40 - - type: phone # notrans name: phone # notrans label: Phone Number required: false - sort: 30 + sort: 1 + + - type: memo # notrans + name: notes + label: Internal Notes + required: false + private: true + sort: 2 + configuration: + rows: 4 + cols: 40 - id: 2 - title: Ticket Details + type: T # notrans + title: Standard Ticket Form + notes: | + This form will be attached to every ticket, regardless of its source. + You can add any fields to this form and they will be available to all + tickets, and will be searchable with advanced search and filterable. + deletable: false fields: - type: text # notrans name: subject # notrans label: Subject hint: Issue Summary - sort: 10 + required: true + edit_mask: 15 + sort: 1 configuration: - size: 40 - length: 50 + size: 40 + length: 50 + + - type: thread # notrans + name: message # notrans + label: Issue + hint: Details on the reason(s) for opening the ticket. + required: true + edit_mask: 15 + sort: 2 + + - type: priority # notrans + name: priority # notrans + label: Priority Level + required: false + edit_mask: 3 + sort: 3 diff --git a/include/staff/dynamic-form.inc.php b/include/staff/dynamic-form.inc.php index 4af7cde38..34ba48ffd 100644 --- a/include/staff/dynamic-form.inc.php +++ b/include/staff/dynamic-form.inc.php @@ -16,7 +16,7 @@ if($form && $_REQUEST['a']!='add') { $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); ?> -<form action="?" method="post" id="save"> +<form action="?id=<?php echo urlencode($_REQUEST['id']); ?>" method="post" id="save"> <?php csrf_token(); ?> <input type="hidden" name="do" value="<?php echo $action; ?>"> <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> @@ -43,12 +43,6 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); echo $info['instructions']; ?></textarea> </td> </tr> - <tr> - <td width="180">Internal Notes:</td> - <td><textarea name="notes" rows="4" cols="80"><?php - echo $info['notes']; ?></textarea> - </td> - </tr> </tbody> </table> <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> @@ -78,11 +72,22 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <td>Email Address</td><td>Short Answer</td><td>email</td> <td><input type="checkbox" disabled="disabled"/></td> <td><input type="checkbox" disabled="disabled" checked="checked"/></td></tr> + <?php + $uform = UserForm::objects()->all(); + $ftypes = FormField::allTypes(); + foreach ($uform[0]->getFields() as $f) { + if ($f->get('private')) continue; + ?> <tr> <td><input type="checkbox" disabled="disabled"/></td> - <td>Phone Number</td><td>Phone Number</td><td>phone</td> + <td><?php echo $f->get('label'); ?></td> + <td><?php $t=FormField::getFieldType($f->get('type')); echo $t[0]; ?></td> + <td><?php echo $f->get('name'); ?></td> <td><input type="checkbox" disabled="disabled"/></td> - <td><input type="checkbox" disabled="disabled"/></td></tr> + <td><input type="checkbox" disabled="disabled" + <?php echo $f->get('required') ? 'checked="checked"' : ''; ?>/></td></tr> + + <?php } ?> </tbody> <thead> <tr> @@ -102,9 +107,14 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <tbody class="sortable-rows" data-sort="sort-"> <?php if ($form) foreach ($form->getFields() as $f) { $id = $f->get('id'); + $deletable = ($f->get('editable') & 1) ? 'disabled="disabled"' : ''; + $force_name = ($f->get('editable') & 2) ? 'disabled="disabled"' : ''; + $force_privacy = ($f->get('editable') & 4) ? 'disabled="disabled"' : ''; + $force_required = ($f->get('editable') & 8) ? 'disabled="disabled"' : ''; $errors = $f->errors(); ?> <tr> - <td><input type="checkbox" name="delete-<?php echo $id; ?>"/> + <td><input type="checkbox" name="delete-<?php echo $id; ?>" + <?php echo $deletable; ?>/> <input type="hidden" name="sort-<?php echo $id; ?>" value="<?php echo $f->get('sort'); ?>"/> <font class="error"><?php @@ -113,11 +123,18 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </td> <td><input type="text" size="32" name="label-<?php echo $id; ?>" value="<?php echo $f->get('label'); ?>"/></td> - <td><select name="type-<?php echo $id; ?>"> - <?php foreach (FormField::allTypes() as $type=>$nfo) { ?> + <td><select name="type-<?php echo $id; ?>" <?php + if (!$f->isChangeable()) echo 'disabled="disabled"'; ?>> + <?php foreach (FormField::allTypes() as $group=>$types) { + ?><optgroup label="<?php echo Format::htmlchars($group); ?>"><?php + foreach ($types as $type=>$nfo) { + if ($f->get('type') != $type + && isset($nfo[2]) && !$nfo[2]) continue; ?> <option value="<?php echo $type; ?>" <?php if ($f->get('type') == $type) echo 'selected="selected"'; ?>> <?php echo $nfo[0]; ?></option> + <?php } ?> + </optgroup> <?php } ?> </select> <?php if ($f->isConfigurable()) { ?> @@ -133,15 +150,24 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <?php } ?></td> <td> <input type="text" size="20" name="name-<?php echo $id; ?>" - value="<?php echo $f->get('name'); ?>"/> + value="<?php echo $f->get('name'); ?>" <?php echo $force_name ?>/> <font class="error"><?php if ($errors['name']) echo '<br/>'; echo $errors['name']; ?></font> </td> <td><input type="checkbox" name="private-<?php echo $id; ?>" - <?php if ($f->get('private')) echo 'checked="checked"'; ?>/></td> + <?php if ($f->get('private')) echo 'checked="checked"'; ?> + <?php echo $force_privacy ?>/></td> <td><input type="checkbox" name="required-<?php echo $id; ?>" - <?php if ($f->get('required')) echo 'checked="checked"'; ?>/></td> + <?php if ($f->get('required')) echo 'checked="checked"'; ?> + <?php echo $force_required ?>/> + <?php if (!$f->get('editable')) { ?> + <input type="hidden" name="private-<?php echo $id; ?>" value="<?php + echo ($f->get('private')) ? 'on' : ''; ?>" /> + <input type="hidden" name="required-<?php echo $id; ?>" value="<?php + echo ($f->get('required')) ? 'on' : ''; ?>" /> + <?php + } ?></td> </tr> <?php } @@ -150,17 +176,35 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <input type="hidden" name="sort-new-<?php echo $i; ?>"/></td> <td><input type="text" size="32" name="label-new-<?php echo $i; ?>"/></td> <td><select name="type-new-<?php echo $i; ?>"> - <?php foreach (FormField::allTypes() as $type=>$nfo) { ?> + <?php foreach (FormField::allTypes() as $group=>$types) { + ?><optgroup label="<?php echo Format::htmlchars($group); ?>"><?php + foreach ($types as $type=>$nfo) { ?> <option value="<?php echo $type; ?>"> <?php echo $nfo[0]; ?></option> + <?php } ?> + </optgroup> <?php } ?> </select></td> <td><input type="text" size="20" name="name-new-<?php echo $i; ?>"/></td> - <td><input type="checkbox" name="private-new-<?php echo $i; ?>"/></td> + <td><input type="checkbox" name="private-new-<?php echo $i; ?>" + <?php if ($form && $form->get('type') == 'U') + echo 'checked="checked"'; ?>/></td> <td><input type="checkbox" name="required-new-<?php echo $i; ?>"/></td> </tr> <?php } ?> </tbody> + <tbody> + <tr> + <th colspan="7"> + <em><strong>Internal Notes:</strong> be liberal, they're internal</em> + </th> + </tr> + <tr> + <td colspan="7"><textarea name="notes" rows="6" cols="80" style="width:95%"><?php + echo $info['notes']; ?></textarea> + </td> + </tr> + </tbody> </table> <p style="padding-left:225px;"> <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> diff --git a/include/staff/dynamic-forms.inc.php b/include/staff/dynamic-forms.inc.php index e1c590831..7c590d435 100644 --- a/include/staff/dynamic-forms.inc.php +++ b/include/staff/dynamic-forms.inc.php @@ -7,13 +7,50 @@ <?php $page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1; -$count = DynamicForm::objects()->count(); +$count = DynamicForm::objects()->filter(array('type'=>'G'))->count(); $pageNav = new Pagenate($count, $page, PAGE_LIMIT); $pageNav->setURL('forms.php'); $showing=$pageNav->showing().' forms'; ?> <table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <thead> + <tr> + <th width="7"> </th> + <th>Client Information Form <em>Added to all clients</em></th> + <th>Last Updated</th> + </tr> + </thead> + <tbody> + <?php foreach (UserForm::objects()->order_by('title') + ->limit($pageNav->getLimit()) + ->offset($pageNav->getStart()) as $form) { ?> + <tr> + <td/> + <td><a href="?id=<?php echo $form->get('id'); ?>"><?php echo $form->get('title'); ?></a></td> + <td><?php echo $form->get('updated'); ?></td> + </tr> + <?php } ?> + </tbody> + <thead> + <tr> + <th width="7"> </th> + <th>Common Ticket Form <em>Added to all tickets</em></th> + <th>Last Updated</th> + </tr> + </thead> + <tbody> + <?php foreach (TicketForm::objects()->order_by('title') + ->limit($pageNav->getLimit()) + ->offset($pageNav->getStart()) as $form) { ?> + <tr> + <td/> + <td><a href="?id=<?php echo $form->get('id'); ?>"><?php echo $form->get('title'); ?></a></td> + <td><?php echo $form->get('updated'); ?></td> + </tr> + <?php } ?> + </tbody> + <tbody> <caption><?php echo $showing; ?></caption> <thead> <tr> @@ -23,7 +60,8 @@ $showing=$pageNav->showing().' forms'; </tr> </thead> <tbody> - <?php foreach (DynamicForm::objects()->order_by('title') + <?php foreach (DynamicForm::objects()->filter(array('type'=>'G')) + ->order_by('title') ->limit($pageNav->getLimit()) ->offset($pageNav->getStart()) as $form) { ?> <tr> diff --git a/include/staff/filter.inc.php b/include/staff/filter.inc.php index b029fbb68..37ac6c699 100644 --- a/include/staff/filter.inc.php +++ b/include/staff/filter.inc.php @@ -125,11 +125,14 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <select name="rule_w<?php echo $i; ?>"> <option value="">— Select One ‐</option> <?php - foreach($matches as $k=>$v){ - $sel=($info["rule_w$i"]==$k)?'selected="selected"':''; - echo sprintf('<option value="%s" %s>%s</option>',$k,$sel,$v); - } - ?> + foreach ($matches as $group=>$ms) { ?> + <optgroup label="<?php echo $group; ?>"><?php + foreach ($ms as $k=>$v) { + $sel=($info["rule_w$i"]==$k)?'selected="selected"':''; + echo sprintf('<option value="%s" %s>%s</option>',$k,$sel,$v); + } ?> + </optgroup> + <?php } ?> </select> <select name="rule_h<?php echo $i; ?>"> <option value="0">— Select One ‐</option> diff --git a/include/staff/helptopic.inc.php b/include/staff/helptopic.inc.php index 8aa9be725..319ce2850 100644 --- a/include/staff/helptopic.inc.php +++ b/include/staff/helptopic.inc.php @@ -92,8 +92,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <tr> <td><strong>Dynamic Form</strong>:</td> <td><select name="form_id"> - <option value="0">— Select a Form —</option> - <?php foreach (DynamicForm::objects() as $group) { ?> + <option value="0">— No Extra Fields —</option> + <?php foreach (DynamicForm::objects()->filter(array('type'=>'G')) as $group) { ?> <option value="<?php echo $group->get('id'); ?>" <?php if ($group->get('id') == $info['form_id']) echo 'selected="selected"'; ?>> @@ -101,7 +101,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </option> <?php } ?> </select> - <em>Information for tickets associated with this help topic</em> + <em>Extra information for tickets associated with this help topic</em> <span class="error"> <?php echo $errors['form_id']; ?></span> </td> </tr> diff --git a/include/staff/templates/dynamic-form.tmpl.php b/include/staff/templates/dynamic-form.tmpl.php index e54d5bfc1..4a67b38e2 100644 --- a/include/staff/templates/dynamic-form.tmpl.php +++ b/include/staff/templates/dynamic-form.tmpl.php @@ -7,14 +7,21 @@ } foreach ($form->getFields() as $field) { ?> - <tr><td class="multi-line <?php if ($field->get('required')) echo 'required'; ?>"> - <?php echo Format::htmlchars($field->get('label')); ?>:</td> - <td><?php $field->render(); ?> + <tr><?php if ($field->isBlockLevel()) { ?> + <td colspan="2" class="<?php if ($field->get('required')) echo 'required'; ?>"> + <?php + } + else { ?> + <td class="multi-line <?php if ($field->get('required')) echo 'required'; ?>"> + <?php echo Format::htmlchars($field->get('label')); ?>:</td> + <td><?php + } + $field->render(); ?> <?php if ($field->get('required')) { ?> <font class="error">*</font> <?php } - if ($field->get('hint')) { ?> + if ($field->get('hint') && !$field->isBlockLevel()) { ?> <br /><em style="color:gray;display:inline-block"><?php echo Format::htmlchars($field->get('hint')); ?></em> <?php diff --git a/include/staff/ticket-edit.inc.php b/include/staff/ticket-edit.inc.php index 643e147c0..e2d7864a4 100644 --- a/include/staff/ticket-edit.inc.php +++ b/include/staff/ticket-edit.inc.php @@ -51,25 +51,6 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$ticket->getUpdateInfo()); <font class="error"><b>*</b> <?php echo $errors['topicId']; ?></font> </td> </tr> - <tr> - <td width="160" class="required"> - Priority Level: - </td> - <td> - <select name="priorityId"> - <option value="" selected >— Select Priority —</option> - <?php - if($priorities=Priority::getPriorities()) { - foreach($priorities as $id =>$name) { - echo sprintf('<option value="%d" %s>%s</option>', - $id, ($info['priorityId']==$id)?'selected="selected"':'',$name); - } - } - ?> - </select> - <font class="error">* <?php echo $errors['priorityId']; ?></font> - </td> - </tr> <tr> <td width="160"> SLA Plan: @@ -111,7 +92,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$ticket->getUpdateInfo()); <tbody id="dynamic-form"> <?php if ($forms) foreach ($forms as $form) { - include(STAFFINC_DIR . 'templates/dynamic-form.tmpl.php'); + $form->render(true); } ?> </tbody> <tbody> diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php index e72514b93..46356bbba 100644 --- a/include/staff/ticket-open.inc.php +++ b/include/staff/ticket-open.inc.php @@ -88,25 +88,6 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <font class="error"><b>*</b> <?php echo $errors['topicId']; ?></font> </td> </tr> - <tr> - <td width="160"> - Priority: - </td> - <td> - <select name="priorityId"> - <option value="0" selected >— System Default —</option> - <?php - if($priorities=Priority::getPriorities()) { - foreach($priorities as $id =>$name) { - echo sprintf('<option value="%d" %s>%s</option>', - $id, ($info['priorityId']==$id)?'selected="selected"':'',$name); - } - } - ?> - </select> - <font class="error"> <?php echo $errors['priorityId']; ?></font> - </td> - </tr> <tr> <td width="160"> SLA Plan: @@ -178,29 +159,16 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </td> </tr> <?php - } ?> + } + TicketForm::getInstance()->render(true); + ?> </tbody> <tbody id="dynamic-form"> - <?php if ($form) - include(STAFFINC_DIR . 'templates/dynamic-form.tmpl.php'); + <?php + if ($form) $form->render(true); ?> </tbody> <tbody> - <tr> - <th colspan="2"> - <em><strong>Issue</strong>: The user will be able to see the issue summary below and any associated responses.</em> - </th> - </tr> - <tr> - <td colspan=2> - <div><em><strong>Issue</strong>: Details on the reason(s) for opening the ticket.</em> <font class="error">* <?php echo $errors['issue']; ?></font></div> - <textarea class="richtext ifhtml draft draft-delete" - placeholder="Details on the reason(s) for opening the ticket." - data-draft-namespace="ticket.staff" name="issue" - cols="21" rows="8" style="width:80%;" - ><?php echo $info['issue']; ?></textarea> - </td> - </tr> <?php //is the user allowed to post replies?? if($thisstaff->canPostReply()) { diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index e99560eb6..a3ef79d5d 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -140,7 +140,7 @@ if($ticket->isOverdue()) </tr> </table> </td> - <td width="50%"> + <td width="50%" style="vertical-align:top"> <table border="0" cellspacing="" cellpadding="4" width="100%"> <tr> <th width="100">Client:</th> @@ -177,15 +177,11 @@ if($ticket->isOverdue()) </td> </tr> <tr> - <th>Default Email:</th> + <th>Email:</th> <td> <?php echo $ticket->getEmail(); ?> </td> </tr> - <tr> - <th>Phone:</th> - <td><?php echo $ticket->getPhoneNumber(); ?></td> - </tr> <tr> <th>Source:</th> <td><?php @@ -193,8 +189,6 @@ if($ticket->isOverdue()) if($ticket->getIP()) echo ' <span class="faded">('.$ticket->getIP().')</span>'; - - ?> </td> </tr> @@ -285,7 +279,7 @@ foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) { // array('email', ...)))); $answers = array_filter($form->getAnswers(), function ($a) { return !in_array($a->getField()->get('name'), - array('email','subject','name','phone')); + array('email','subject','name','priority')); }); if (count($answers) == 0) continue; @@ -293,7 +287,8 @@ foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) { </tr><tr> <td colspan="2"> <table cellspacing="0" cellpadding="4" width="100%" border="0"> - <?php foreach($answers as $a) { ?> + <?php foreach($answers as $a) { + if (!$a->toString()) continue; ?> <tr> <th width="100"><?php echo $a->getField()->get('label'); diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php index 5d1ea6238..43e68f525 100644 --- a/include/staff/tickets.inc.php +++ b/include/staff/tickets.inc.php @@ -49,7 +49,7 @@ switch(strtolower($_REQUEST['status'])){ //Status is overloaded $results_type='Answered Tickets'; break; default: - if(!$search) + if(!$search && !isset($_REQUEST['advsid'])) $_REQUEST['status']=$status='open'; } @@ -109,90 +109,36 @@ if($search): if($searchTerm){ $qstr.='&query='.urlencode($searchTerm); $queryterm=db_real_escape($searchTerm,false); //escape the term ONLY...no quotes. - if(is_numeric($searchTerm)){ + if (is_numeric($searchTerm)) { $qwhere.=" AND ticket.ticketID LIKE '$queryterm%'"; - }elseif(strpos($searchTerm,'@') && Validator::is_email($searchTerm)){ //pulling all tricks! + } elseif (strpos($searchTerm,'@') && Validator::is_email($searchTerm)) { + //pulling all tricks! # XXX: What about searching for email addresses in the body of # the thread message $qwhere.=" AND email.address='$queryterm'"; - }else{//Deep search! + } else {//Deep search! //This sucks..mass scan! search anything that moves! + require_once(INCLUDE_DIR.'ajax.tickets.php'); - $deep_search=true; - } - } - // OwnerId - if ($_REQUEST['ownerId']) { - $qwhere .= ' AND ticket.user_id='.db_input($_REQUEST['ownerId']); - } - //department - if($_REQUEST['deptId'] && in_array($_REQUEST['deptId'],$thisstaff->getDepts())) { - //This is dept based search..perm taken care above..put the sucker in. - $qwhere.=' AND ticket.dept_id='.db_input($_REQUEST['deptId']); - $qstr.='&deptId='.urlencode($_REQUEST['deptId']); - } - - //Help topic - if($_REQUEST['topicId']) { - $qwhere.=' AND ticket.topic_id='.db_input($_REQUEST['topicId']); - $qstr.='&topicId='.urlencode($_REQUEST['topicId']); - } - - //Assignee - if(isset($_REQUEST['assignee']) && strcasecmp($_REQUEST['status'], 'closed')) { - $id=preg_replace("/[^0-9]/", "", $_REQUEST['assignee']); - $assignee = $_REQUEST['assignee']; - $qstr.='&assignee='.urlencode($_REQUEST['assignee']); - $qwhere.= ' AND ( - ( ticket.status="open" '; - - if($assignee[0]=='t') - $qwhere.=' AND ticket.team_id='.db_input($id); - elseif($assignee[0]=='s') - $qwhere.=' AND ticket.staff_id='.db_input($id); - elseif(is_numeric($id)) - $qwhere.=' AND ticket.staff_id='.db_input($id); - - $qwhere.=' ) '; - - if($_REQUEST['staffId'] && !$_REQUEST['status']) { //Assigned TO + Closed By - $qwhere.= ' OR (ticket.staff_id='.db_input($_REQUEST['staffId']). ' AND ticket.status="closed") '; - $qstr.='&staffId='.urlencode($_REQUEST['staffId']); - }elseif(isset($_REQUEST['staffId'])) { - $qwhere.= ' OR ticket.status="closed" '; - $qstr.='&staffId='.urlencode($_REQUEST['staffId']); - } - - $qwhere.= ' ) '; - } elseif($_REQUEST['staffId']) { - $qwhere.=' AND (ticket.staff_id='.db_input($_REQUEST['staffId']).' AND ticket.status="closed") '; - $qstr.='&staffId='.urlencode($_REQUEST['staffId']); - } - - //dates - $startTime =($_REQUEST['startDate'] && (strlen($_REQUEST['startDate'])>=8))?strtotime($_REQUEST['startDate']):0; - $endTime =($_REQUEST['endDate'] && (strlen($_REQUEST['endDate'])>=8))?strtotime($_REQUEST['endDate']):0; - if( ($startTime && $startTime>time()) or ($startTime>$endTime && $endTime>0)){ - $errors['err']='Entered date span is invalid. Selection ignored.'; - $startTime=$endTime=0; - }else{ - //Have fun with dates. - if($startTime){ - $qwhere.=' AND ticket.created>=FROM_UNIXTIME('.$startTime.')'; - $qstr.='&startDate='.urlencode($_REQUEST['startDate']); - - } - if($endTime){ - $qwhere.=' AND ticket.created<=FROM_UNIXTIME('.$endTime.')'; - $qstr.='&endDate='.urlencode($_REQUEST['endDate']); + $tickets = TicketsAjaxApi::_search(array('query'=>$queryterm)); + if (count($tickets)) + $qwhere .= ' AND ticket.ticket_id IN ('. + implode(',',db_input($tickets)).')'; } } endif; -$sortOptions=array('date'=>'ticket.created','ID'=>'ticketID','pri'=>'priority_urgency','name'=>'name.value', - 'subj'=>'subject.value','status'=>'ticket.status','assignee'=>'assigned','staff'=>'staff', - 'dept'=>'dept_name'); +if ($_REQUEST['advsid'] && isset($_SESSION['adv_'.$_REQUEST['advsid']])) { + $qstr.='advsid='.$_REQUEST['advsid']; + $qwhere .= ' AND ticket.ticket_id IN ('. implode(',', + db_input($_SESSION['adv_'.$_REQUEST['advsid']])).')'; +} + +$sortOptions=array('date'=>'ticket.created','ID'=>'ticketID', + 'pri'=>'priority_urgency','name'=>'user.name','subj'=>'subject.value', + 'status'=>'ticket.status','assignee'=>'assigned','staff'=>'staff', + 'dept'=>'dept_name'); $orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); @@ -241,11 +187,12 @@ $$x=' class="'.strtolower($order).'" '; if($_GET['limit']) $qstr.='&limit='.urlencode($_GET['limit']); -$dynfields='(SELECT entry.object_id, value FROM '.FORM_ANSWER_TABLE.' ans '. +$dynfields='(SELECT entry.object_id, value, value_id FROM '.FORM_ANSWER_TABLE.' ans '. 'LEFT JOIN '.FORM_ENTRY_TABLE.' entry ON entry.id=ans.entry_id '. 'LEFT JOIN '.FORM_FIELD_TABLE.' field ON field.id=ans.field_id '. 'WHERE field.name = "%1$s" AND entry.object_type="T")'; $subject_sql=sprintf($dynfields, 'subject'); +$prio_sql=sprintf($dynfields, 'priority'); $qselect ='SELECT DISTINCT ticket.ticket_id,lock_id,ticketID,ticket.dept_id,ticket.staff_id,ticket.team_id ' .' ,subject.value as subject' @@ -257,7 +204,8 @@ $qfrom=' FROM '.TICKET_TABLE.' ticket '. ' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id'. ' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id'. ' LEFT JOIN '.DEPT_TABLE.' dept ON ticket.dept_id=dept.dept_id '. - ' LEFT JOIN '.$subject_sql.' subject ON subject.object_id = ticket.ticket_id'; + ' LEFT JOIN '.$subject_sql.' subject ON subject.object_id = ticket.ticket_id'. + ' LEFT JOIN '.$prio_sql.' tprio ON tprio.object_id = ticket.ticket_id'; $sjoin=''; if($search && $deep_search) { @@ -282,7 +230,7 @@ $qselect.=' ,count(attach.attach_id) as attachments ' .' ,IF(staff.staff_id IS NULL,team.name,CONCAT_WS(" ", staff.lastname, staff.firstname)) as assigned ' .' ,IF(ptopic.topic_pid IS NULL, topic.topic, CONCAT_WS(" / ", ptopic.topic, topic.topic)) as helptopic '; -$qfrom.=' LEFT JOIN '.TICKET_PRIORITY_TABLE.' pri ON (ticket.priority_id=pri.priority_id) ' +$qfrom.=' LEFT JOIN '.TICKET_PRIORITY_TABLE.' pri ON (tprio.value_id=pri.priority_id) ' .' LEFT JOIN '.TICKET_LOCK_TABLE.' tlock ON (ticket.ticket_id=tlock.ticket_id AND tlock.expire>NOW() AND tlock.staff_id!='.db_input($thisstaff->getId()).') ' .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON (ticket.ticket_id=attach.ticket_id) ' @@ -644,6 +592,17 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. <span>TO</span> <input class="dp" type="input" size="20" name="endDate"> </fieldset> + <fieldset> + <?php + foreach (TicketForm::getInstance()->getFields() as $f) { + if (in_array($f->get('type'), array('text', 'memo', 'phone', 'thread'))) + continue; + elseif (!$f->hasData()) + continue; + ?><label><?php echo $f->getLabel(); ?>:</label> + <div style="display:inline-block;width: 12.5em;"><?php $f->render(); ?></div> + <?php } ?> + </fieldset> <p> <span class="buttons"> <input type="submit" value="Search"> diff --git a/include/upgrader/streams/core/d51f303a-DYNAMICF.patch.sql b/include/upgrader/streams/core/d51f303a-DYNAMICF.patch.sql index 940b9a12b..5fa040726 100644 --- a/include/upgrader/streams/core/d51f303a-DYNAMICF.patch.sql +++ b/include/upgrader/streams/core/d51f303a-DYNAMICF.patch.sql @@ -10,48 +10,28 @@ * fields are dropped from the ticket table. */ -DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_formset`; -CREATE TABLE `%TABLE_PREFIX%dynamic_formset` ( - `id` int(11) unsigned auto_increment, - `title` varchar(255) NOT NULL, - `instructions` varchar(512), - `notes` text, - `created` datetime NOT NULL, - `updated` datetime NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_formset_sections`; -CREATE TABLE `%TABLE_PREFIX%dynamic_formset_sections` ( - `id` int(11) unsigned NOT NULL auto_increment, - `formset_id` int(11) NOT NULL, - `section_id` int(11) NOT NULL, - `title` varchar(255), - `instructions` text, - -- Allow more than one form, sorted in this order - `sort` int(11) NOT NULL DEFAULT 1, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_form_section`; -CREATE TABLE `%TABLE_PREFIX%dynamic_form` ( +DROP TABLE IF EXISTS `%TABLE_PREFIX%form`; +CREATE TABLE `%TABLE_PREFIX%form` ( `id` int(11) unsigned NOT NULL auto_increment, + `type` char(1) NOT NULL DEFAULT 'G', + `deletable` tinyint(1) NOT NULL DEFAULT 1, `title` varchar(255) NOT NULL, `instructions` varchar(512), `notes` text, `created` datetime NOT NULL, `updated` datetime NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_form_field`; -CREATE TABLE `%TABLE_PREFIX%dynamic_form_field` ( +DROP TABLE IF EXISTS `%TABLE_PREFIX%form_field`; +CREATE TABLE `%TABLE_PREFIX%form_field` ( `id` int(11) unsigned NOT NULL auto_increment, - `section_id` int(11) unsigned NOT NULL, + `form_id` int(11) unsigned NOT NULL, `type` varchar(255) NOT NULL DEFAULT 'text', `label` varchar(255) NOT NULL, `required` tinyint(1) NOT NULL DEFAULT 0, `private` tinyint(1) NOT NULL DEFAULT 0, + `edit_mask` tinyint(1) NOT NULL DEFAULT 1, `name` varchar(64) NOT NULL, `configuration` text, `sort` int(11) unsigned NOT NULL, @@ -59,65 +39,33 @@ CREATE TABLE `%TABLE_PREFIX%dynamic_form_field` ( `created` datetime NOT NULL, `updated` datetime NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; - --- Create a default form to mimic the previous default form of osTicket < 1.7.1 -INSERT INTO `%TABLE_PREFIX%dynamic_form_section` SET - `id` = 1, `title` = 'User Information', `created` = NOW(), - `updated` = NOW(); -INSERT INTO `%TABLE_PREFIX%dynamic_form_section` SET - `id` = 2, `title` = 'Ticket Details', `created` = NOW(), - `updated` = NOW(); - -INSERT INTO `%TABLE_PREFIX%dynamic_formset` SET - `id` = 1, `title` = 'Default', `created` = NOW(), `updated` = NOW(); - -INSERT INTO `%TABLE_PREFIX%dynamic_formset_sections` SET - `formset_id` = 1, `section_id` = 1, `sort` = 10; -INSERT INTO `%TABLE_PREFIX%dynamic_formset_sections` SET - `formset_id` = 1, `section_id` = 2, `sort` = 20; - -INSERT INTO `%TABLE_PREFIX%dynamic_form_field` SET - `section_id` = 1, `type` = 'text', `label` = 'Email Address', - `required` = 1, `configuration` = '{"size":40,"length":120,"validator":"email"}', - `name` = 'email', `sort` = 10, `created` = NOW(), `updated` = NOW(); -INSERT INTO `%TABLE_PREFIX%dynamic_form_field` SET - `section_id` = 1, `type` = 'text', `label` = 'Full Name', - `required` = 1, `configuration` = '{"size":40,"length":32}', - `name` = 'name', `sort` = 20, `created` = NOW(), `updated` = NOW(); -INSERT INTO `%TABLE_PREFIX%dynamic_form_field` SET - `section_id` = 1, `type` = 'phone', `label` = 'Phone Number', - `name` = 'phone', `sort` = 30, `created` = NOW(), `updated` = NOW(); - -INSERT INTO `%TABLE_PREFIX%dynamic_form_field` SET - `section_id` = 2, `type` = 'text', `label` = 'Subject', - `hint` = 'Issue summary', `required` = 1, - `configuration` = '{"size":40,"length":64}', - `name` = 'subject', `sort` = 10, `created` = NOW(), `updated` = NOW(); - -DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_form_entry`; -CREATE TABLE `%TABLE_PREFIX%dynamic_form_entry` ( +) DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%form_entry`; +CREATE TABLE `%TABLE_PREFIX%form_entry` ( `id` int(11) unsigned NOT NULL auto_increment, - `section_id` int(11) unsigned NOT NULL, - `ticket_id` int(11) unsigned, + `form_id` int(11) unsigned NOT NULL, + `object_id` int(11) unsigned, + `object_type` char(1) NOT NULL DEFAULT 'T', `sort` int(11) unsigned NOT NULL DEFAULT 1, `created` datetime NOT NULL, `updated` datetime NOT NULL, PRIMARY KEY (`id`), KEY `ticket_dyn_form_lookup` (`ticket_id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_form_entry_values`; -CREATE TABLE `%TABLE_PREFIX%dynamic_form_entry_values` ( - -- references dynamic_form_entry.id +DROP TABLE IF EXISTS `%TABLE_PREFIX%form_entry_values`; +CREATE TABLE `%TABLE_PREFIX%form_entry_values` ( + -- references form_entry.id `entry_id` int(11) unsigned NOT NULL, `field_id` int(11) unsigned NOT NULL, `value` text, + `value_id` int(11), PRIMARY KEY (`entry_id`, `field_id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_list`; -CREATE TABLE `%TABLE_PREFIX%dynamic_list` ( +DROP TABLE IF EXISTS `%TABLE_PREFIX%list`; +CREATE TABLE `%TABLE_PREFIX%list` ( `id` int(11) unsigned NOT NULL auto_increment, `name` varchar(255) NOT NULL, `name_plural` varchar(255), @@ -126,10 +74,10 @@ CREATE TABLE `%TABLE_PREFIX%dynamic_list` ( `created` datetime NOT NULL, `updated` datetime NOT NULL, PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; +) DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_list_items`; -CREATE TABLE `%TABLE_PREFIX%dynamic_list_items` ( +DROP TABLE IF EXISTS `%TABLE_PREFIX%list_items`; +CREATE TABLE `%TABLE_PREFIX%list_items` ( `id` int(11) unsigned NOT NULL auto_increment, `list_id` int(11), `value` varchar(255) NOT NULL, @@ -137,68 +85,94 @@ CREATE TABLE `%TABLE_PREFIX%dynamic_list_items` ( `extra` varchar(255), `sort` int(11) NOT NULL DEFAULT 1, PRIMARY KEY (`id`), - KEY `dynamic_list_item_lookup` (`list_id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; + KEY `list_item_lookup` (`list_id`) +) DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%user`; +CREATE TABLE `%TABLE_PREFIX%user` ( + `id` int(10) unsigned NOT NULL auto_increment, + `default_email_id` int(10) NOT NULL, + `name` varchar(128) NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`) +); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%user_email`; +CREATE TABLE `%TABLE_PREFIX%user_email` ( + `id` int(10) unsigned NOT NULL auto_increment, + `user_id` int(10) unsigned NOT NULL, + `address` varchar(128) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `address` (`address`) +); + +ALTER TABLE `%TABLE_PREFIX%filter_rule` + CHANGE `what` `what` varchar(32) NOT NULL; ALTER TABLE `%TABLE_PREFIX%help_topic` ADD `formset_id` int(11) unsigned NOT NULL default '0' AFTER `sla_id`; --- All help topics will link to the default formset -UPDATE `%TABLE_PREFIX%help_topic` SET `formset_id` = 1; -- Port data from the ticket table -- 1. Create form entries for each ticket -INSERT INTO `%TABLE_PREFIX%dynamic_form_entry` ( +INSERT INTO `%TABLE_PREFIX%form_entry` ( `section_id`, `ticket_id`, `sort`, `created`, `updated`) SELECT 1, `ticket_id`, 10, `created`, `updated` FROM `%TABLE_PREFIX%ticket`; -INSERT INTO `%TABLE_PREFIX%dynamic_form_entry` ( - `section_id`, `ticket_id`, `sort`, `created`, `updated`) - SELECT 2, `ticket_id`, 20, `created`, `updated` - FROM `%TABLE_PREFIX%ticket`; - --- 2. Copy Name, Email, and Phone from the ticket table to section #1 -INSERT INTO `%TABLE_PREFIX%dynamic_form_entry_values` ( - `field_id`, `entry_id`, `value`) - SELECT A3.`field_id`, A2.`id`, A1.`name` - FROM `%TABLE_PREFIX%ticket` A1 - INNER JOIN `%TABLE_PREFIX%dynamic_form_entry` A2 ON (A1.`ticket_id` - = A2.`ticket_id` AND A2.`section_id` = 1), - INNER JOIN `%TABLE_PREFIX%dynamic_form_field` A3 ON (A2.`section_id` - = A3.`section_id`) - WHERE A3.`name` = 'name' AND LENGTH(A1.`name`); - -INSERT INTO `%TABLE_PREFIX%dynamic_form_entry_values` ( +-- 2. Copy subject lines from the ticket table into section #2 +INSERT INTO `%TABLE_PREFIX%form_entry_values` ( `field_id`, `entry_id`, `value`) - SELECT A3.`field_id`, A2.`id`, A1.`email` + SELECT A3.`field_id`, A2.`id`, A1.`subject` FROM `%TABLE_PREFIX%ticket` A1 - INNER JOIN `%TABLE_PREFIX%dynamic_form_entry` A2 ON (A1.`ticket_id` - = A2.`ticket_id` AND A2.`section_id` = 1), - INNER JOIN `%TABLE_PREFIX%dynamic_form_field` A3 ON (A2.`section_id` + INNER JOIN `%TABLE_PREFIX%form_entry` A2 ON (A1.`ticket_id` + = A2.`ticket_id` AND A2.`section_id` = 2), + INNER JOIN `%TABLE_PREFIX%form_field` A3 ON (A2.`section_id` = A3.`section_id`) - WHERE A3.`name` = 'email' AND LENGTH(A1.`email`); + WHERE A3.`name` = 'subject'; -INSERT INTO `%TABLE_PREFIX%dynamic_form_entry_values` ( +-- TODO: Move this to a client info dynamic entry +-- 3. Copy Phone from the ticket table to section #1 +INSERT INTO `%TABLE_PREFIX%form_entry_values` ( `field_id`, `entry_id`, `value`) SELECT A3.`field_id`, A2.`id`, CONCAT(A1.`phone`, 'X', A1.`phone_ext`) FROM `%TABLE_PREFIX%ticket` A1 - INNER JOIN `%TABLE_PREFIX%dynamic_form_entry` A2 ON (A1.`ticket_id` + INNER JOIN `%TABLE_PREFIX%form_entry` A2 ON (A1.`ticket_id` = A2.`ticket_id` AND A2.`section_id` = 1), - INNER JOIN `%TABLE_PREFIX%dynamic_form_field` A3 ON (A2.`section_id` + INNER JOIN `%TABLE_PREFIX%form_field` A3 ON (A2.`section_id` = A3.`section_id`) WHERE A3.`name` = 'phone' AND LENGTH(A1.`phone`); --- 3. Copy subject lines from the ticket table into section #2 -INSERT INTO `%TABLE_PREFIX%dynamic_form_entry_values` ( - `field_id`, `entry_id`, `value`) - SELECT A3.`field_id`, A2.`id`, A1.`subject` +-- 4. Create <user> accounts for everybody +-- - Start with creating email addresses for the accounts +INSERT INTO `%TABLE_PREFIX%user_email` (`address`) + SELECT DISTINCT `email` FROM `%TABLE_PREFIX%ticket`; + +-- - Then create the accounts and link the `default_email`s +INSERT INTO `%TABLE_PREFIX%user` (`first`, `default_email_id`) + SELECT MAX(`name`), A2.`id` FROM `%TABLE_PREFIX%ticket` A1 - INNER JOIN `%TABLE_PREFIX%dynamic_form_entry` A2 ON (A1.`ticket_id` - = A2.`ticket_id` AND A2.`section_id` = 2), - INNER JOIN `%TABLE_PREFIX%dynamic_form_field` A3 ON (A2.`section_id` - = A3.`section_id`) - WHERE A3.`name` = 'subject'; + INNER JOIN `%TABLE_PREFIX%user_email` A2 ON (A1.`email` = A2.`address`); + GROUP BY A2.`id` + +-- - Now link the user and user_email tables +ALTER TABLE `%TABLE_PREFIX%user` ADD KEY `def_eml_id` (`default_email_id`, `id`); +UPDATE `%TABLE_PREFIX%user_email` A1 + SET user_id = ( + SELECT A2.`id` FROM `%TABLE_PREFIX%user` A2 + WHERE `default_email_id` = A1.`id`); +ALTER TABLE `%TABLE_PREFIX%user` DROP INDEX `def_eml_id`; + +-- - Update the ticket table +ALTER TABLE `%TABLE_PREFIX%ticket` + ADD `user_id` int(11) UNSIGNED NOT NULL DEFAULT 0 AFTER `ticket_id`, + ADD `user_email_id` int(11) UNSIGNED NOT NULL DEFAULT 0 AFTER `user_id`; + +UPDATE `%TABLE_PREFIX%ticket` A1 + JOIN `%TABLE_PREFIX%user_email` A2 ON A2.`address` = A1.`email` + SET `user_id` = A2.`user_id`, + `user_email_id` = A2.`id`; -- 4. Remove columns from ticket table ALTER TABLE `%TABLE_PREFIX%ticket` diff --git a/include/upgrader/streams/core/d51f303a-DYNAMICF.task.php b/include/upgrader/streams/core/d51f303a-DYNAMICF.task.php new file mode 100644 index 000000000..98d128fbd --- /dev/null +++ b/include/upgrader/streams/core/d51f303a-DYNAMICF.task.php @@ -0,0 +1,21 @@ +<?php + +/* + * Loads the initial data for dynamic forms into the system. This is + * preferred over providing the data inside the SQL scripts + */ + +class DynamicFormLoader extends MigrationTask { + var $description = "Loading initial data for dynamic forms"; + + function run($max_time) { + $i18n = new Internationalization('en_US'); + $forms = $i18n->getTemplate('form.yaml')->getData(); + foreach ($forms as $f) + DynamicForm::create($f); + } +} + +return 'DynamicFormLoader'; + +?> diff --git a/open.php b/open.php index 1d1511c3a..1370e4c9c 100644 --- a/open.php +++ b/open.php @@ -30,24 +30,15 @@ if($_POST): $errors['captcha']='Invalid - try again!'; } - $interest=array('name','email','subject'); - if ($topic=Topic::lookup($vars['topicId'])) { - $form=DynamicForm::lookup($topic->ht['form_id'])->instanciate(); - # Collect name, email, and subject address for banning and such - foreach ($form->getAnswers() as $answer) { - $fname = $answer->getField()->get('name'); - if (in_array($fname, $interest)) - # XXX: Assigning to _POST not considered great PHP - # coding style - $vars[$fname] = $answer->getField()->getClean(); + $interest = array('subject'); + if ($topic = Topic::lookup($vars['topicId'])) { + if ($form = DynamicForm::lookup($topic->ht['form_id'])) { + $form = $form->instanciate(); + if (!$form->isValid()) + $errors += $form->errors(); } - if (!$form->isValid()) - $errors = array_merge($errors, $form->errors()); } - $form2 = UserForm::getStaticForm(); - if (!$form2->isValid()) - $errors += $form2->errors(); - if(!$errors && $cfg->allowOnlineAttachments() && $_FILES['attachments']) + if (!$errors && $cfg->allowOnlineAttachments() && $_FILES['attachments']) $vars['files'] = AttachmentFile::format($_FILES['attachments'], true); //Ticket::create...checks for errors.. @@ -55,9 +46,11 @@ if($_POST): $msg='Support ticket request created'; Draft::deleteForNamespace('ticket.client.'.substr(session_id(), -12)); # TODO: Save dynamic form(s) - $form->setTicketId($ticket->getId()); - $form->save(); - $ticket->loadDynamicData(); + if (isset($form)) { + $form->setTicketId($ticket->getId()); + $form->save(); + $ticket->loadDynamicData(); + } //Logged in...simply view the newly created ticket. if($thisclient && $thisclient->isValid()) { if(!$cfg->showRelatedTickets()) diff --git a/scp/css/scp.css b/scp/css/scp.css index 8f58ac452..070aafeeb 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -567,23 +567,27 @@ a.print { border:1px solid #f00; } -.form_table th { +.form_table th, div.section-break { text-align:left; border:1px solid #ccc; background:#eee; padding:0; + padding:5px; +} + +div.section-break h3 { + margin: 0; + padding: 0; } .form_table th h4 { margin:0; - padding:5px; color:#fff; background:#929292; } .form_table th em { display:block; - padding:5px; color:#000; } @@ -1197,13 +1201,13 @@ time { background:#f8f8f8; border:2px solid #2a67ac; display:none; - z-index:1200; + z-index:5; box-shadow: 0 5px 60px #001; border-radius: 5px; } .redactor_air { - z-index: 1201 !important; + z-index: 6 !important; } .dialog#advanced-search { @@ -1405,7 +1409,7 @@ ul.progress li.no small {color:red;} width: 100%; height: 100%; background: #000; - z-index: 1000; + z-index: 1; -webkit-transform: translate3d(0,0,0); } diff --git a/scp/forms.php b/scp/forms.php index 399d8b5d0..a26aa7dfc 100644 --- a/scp/forms.php +++ b/scp/forms.php @@ -18,20 +18,24 @@ if($_POST) { $form->save(); foreach ($form->getDynamicFields() as $field) { $id = $field->get('id'); - if ($_POST["delete-$id"] == 'on') { + if ($_POST["delete-$id"] == 'on' && $field->isDeletable()) { $field->delete(); + // Don't bother updating the field continue; } - foreach (array('sort','label','type','name') as $f) - if (isset($_POST["$f-$id"])) - $field->set($f, $_POST["$f-$id"]); + if (isset($_POST["type-$id"]) && $field->isChangeable()) + $field->set('type', $_POST["type-$id"]); + if (isset($_POST["name-$id"]) && $field->isNameEditable()) + $field->set('name', $_POST["name-$id"]); # TODO: make sure all help topics still have all required fields - $field->set('required', $_POST["required-$id"] == 'on' ? 1 : 0); - $field->set('private', $_POST["private-$id"] == 'on' ? 1 : 0); - # Core fields are forced required and public - if (in_array($field->get('name'), $required)) { - $field->set('required', 1); - $field->set('private', 0); + if (!$field->isRequirementForced()) + $field->set('required', $_POST["required-$id"] == 'on' ? 1 : 0); + if (!$field->isPrivacyForced()) + $field->set('private', $_POST["private-$id"] == 'on' ? 1 : 0); + foreach (array('sort','label') as $f) { + if (isset($_POST["$f-$id"])) { + $field->set($f, $_POST["$f-$id"]); + } } if ($field->isValid()) $field->save(); diff --git a/scp/tickets.php b/scp/tickets.php index 296bbc7b2..c5ade077c 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -205,7 +205,6 @@ if($_POST && !$errors): $_REQUEST['a'] = null; //Clear edit action - going back to view. //Check to make sure the staff STILL has access post-update (e.g dept change). foreach ($forms as $f) $f->save(); - $ticket->setOwner($form2->getClean()); if(!$ticket->checkStaffAccess($thisstaff)) $ticket=null; } elseif(!$errors['err']) { @@ -460,22 +459,20 @@ if($_POST && !$errors): $ticket=null; $interest=array('name','email','subject'); if ($topic=Topic::lookup($_POST['topicId'])) { - $form = DynamicForm::lookup($topic->ht['form_id']); - $form = $form->instanciate(); - # Collect name, email, and subject address for banning and such - foreach ($form->getAnswers() as $answer) { - $fname = $answer->getField()->get('name'); - if (in_array($fname, $interest)) - # XXX: Assigning to _POST not considered great PHP - # coding style - $_POST[$fname] = $answer->getField()->getClean(); + if ($form = DynamicForm::lookup($topic->ht['form_id'])) { + $form = $form->instanciate(); + # Collect name, email, and subject address for banning and such + foreach ($form->getAnswers() as $answer) { + $fname = $answer->getField()->get('name'); + if (in_array($fname, $interest)) + # XXX: Assigning to _POST not considered great PHP + # coding style + $_POST[$fname] = $answer->getField()->getClean(); + } + if (!$form->isValid()) + $errors = array_merge($errors, $form->errors()); } - if (!$form->isValid()) - $errors = array_merge($errors, $form->errors()); } - $form2 = UserForm::getStaticForm(); - if (!$form2->isValid()) - $errors = array_merge($errors, $form2->errors()); if(!$thisstaff || !$thisstaff->canCreateTickets()) { $errors['err']='You do not have permission to create tickets. Contact admin for such access'; } else { @@ -487,9 +484,11 @@ if($_POST && !$errors): $msg='Ticket created successfully'; $_REQUEST['a']=null; # TODO: Save dynamic form(s) - $form->setTicketId($ticket->getId()); - $form->save(); - $ticket->loadDynamicData(); + if (isset($form)) { + $form->setTicketId($ticket->getId()); + $form->save(); + $ticket->loadDynamicData(); + } if(!$ticket->checkStaffAccess($thisstaff) || $ticket->isClosed()) $ticket=null; Draft::deleteForNamespace('ticket.staff%', $thisstaff->getId()); diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index 8faf7ac5c..78d3a68c5 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -173,7 +173,8 @@ INSERT INTO `%TABLE_PREFIX%config` (`namespace`, `key`, `value`) VALUES DROP TABLE IF EXISTS `%TABLE_PREFIX%form`; CREATE TABLE `%TABLE_PREFIX%form` ( `id` int(11) unsigned NOT NULL auto_increment, - `type` char(1) NOT NULL DEFAULT 'T', + `type` char(1) NOT NULL DEFAULT 'G', + `deletable` tinyint(1) NOT NULL DEFAULT 1, `title` varchar(255) NOT NULL, `instructions` varchar(512), `notes` text, @@ -190,6 +191,7 @@ CREATE TABLE `%TABLE_PREFIX%form_field` ( `label` varchar(255) NOT NULL, `required` tinyint(1) NOT NULL DEFAULT 0, `private` tinyint(1) NOT NULL DEFAULT 0, + `edit_mask` tinyint(1) NOT NULL DEFAULT 1, `name` varchar(64) NOT NULL, `configuration` text, `sort` int(11) unsigned NOT NULL, @@ -218,6 +220,7 @@ CREATE TABLE `%TABLE_PREFIX%form_entry_values` ( `entry_id` int(11) unsigned NOT NULL, `field_id` int(11) unsigned NOT NULL, `value` text, + `value_id` int(11), PRIMARY KEY (`entry_id`, `field_id`) ) DEFAULT CHARSET=utf8; @@ -349,7 +352,7 @@ DROP TABLE IF EXISTS `%TABLE_PREFIX%filter_rule`; CREATE TABLE `%TABLE_PREFIX%filter_rule` ( `id` int(11) unsigned NOT NULL auto_increment, `filter_id` int(10) unsigned NOT NULL default '0', - `what` enum('name','email','subject','body','header') NOT NULL, + `what` varchar(32) NOT NULL, `how` enum('equal','not_equal','contains','dn_contain','starts','ends') NOT NULL, `val` varchar(255) NOT NULL, `isactive` tinyint(1) unsigned NOT NULL DEFAULT '1', @@ -756,8 +759,7 @@ DROP TABLE IF EXISTS `%TABLE_PREFIX%user`; CREATE TABLE `%TABLE_PREFIX%user` ( `id` int(10) unsigned NOT NULL auto_increment, `default_email_id` int(10) NOT NULL, - `first` varchar(64) NOT NULL, - `last` varchar(64), + `name` varchar(128) NOT NULL, `created` datetime NOT NULL, `updated` datetime NOT NULL, PRIMARY KEY (`id`) -- GitLab