3 * Class for editing a record using a templated form.
6 * @subpackage classEditor
7 * @author Andrew McMillan <andrew@mcmillan.net.nz>
8 * @copyright Catalyst IT Ltd, Morphoss Ltd <http://www.morphoss.com/>
9 * @license http://gnu.org/copyleft/gpl.html GNU GPL v2
12 require_once("DataUpdate.php");
13 require_once("DataEntry.php");
16 * A class for the fields in the editor
29 * Creates an EditorField for use in the Editor, possibly initialising the SQL for calculating it's
30 * value, and lookup_sql for use in drop-down lists.
32 * @param unknown $field
34 * @param string $lookup_sql
36 function __construct( $field, $sql="", $lookup_sql="" ) {
38 $this->Field
= $field;
40 $this->LookupSql
= $lookup_sql;
41 $this->Attributes
= array();
44 function Set($value) {
45 $this->Value
= $value;
49 * Set the SQL used for this field, if it is more than just a field name.
52 function SetSql( $sql ) {
57 * Set the lookup SQL to use to populate a SELECT for this field.
58 * @param string $lookup_sql
60 function SetLookup( $lookup_sql ) {
61 $this->LookupSql
= $lookup_sql;
65 * Set the SELECT values explicitly, if they are not available in SQL.
69 * SetOptionList(array('M' => 'Male', 'F' => 'Female', 'O' => 'Other'), 'F', array('maxwidth' => 6, 'translate' => true));
71 * This would present Male/Female/Other drop-down, when in another language the values
72 * would be translated (if available), e.g. in German as Männlich/Weiblich/Andere, except
73 * that in this case Männli/Weibli/Andere, since the values would be truncated to maxwidth.
75 * @param array $options An array of key => value pairs
76 * @param string $current The currently selected key
77 * @param string $parameters An array of parameters (maxwidth & translate are the only valid parameters)
79 function SetOptionList( $options, $current = null, $parameters = null) {
80 if ( gettype($options) == 'array' ) {
81 $this->OptionList
= '';
83 if ( is_array($parameters) ) {
84 if ( isset($parameters['maxwidth']) ) $maxwidth = max(4,intval($parameters['maxwidth']));
85 if ( isset($parameters['translate']) ) $translate = true;
88 foreach( $options AS $k => $v ) {
89 if (is_array($current)) {
90 $selected = ( ( in_array($k,$current,true) ||
in_array($v,$current,true)) ?
' selected="selected"' : '' );
93 $selected = ( ( "$k" == "$current" ||
"$v" == "$current" ) ?
' selected="selected"' : '' );
95 if ( isset($translate) ) $v = translate( $v );
96 if ( isset($maxwidth) ) $v = substr( $v, 0, $maxwidth);
97 $this->OptionList
.= "<option value=\"".htmlspecialchars($k)."\"$selected>".htmlspecialchars($v)."</option>";
101 $this->OptionList
= $options;
105 function GetTarget() {
106 if ( $this->Sql
== "" ) return $this->Field
;
107 return "$this->Sql AS $this->Field";
111 * Add some kind of attribute to this field, such as a 'class' => 'fancyinputthingy'
113 * @param string $k The attribute name
114 * @param string $v The attribute value
116 function AddAttribute( $k, $v ) {
117 $this->Attributes
[$k] = $v;
121 * Render a LABEL around something. In particular it is useful to render a label around checkbox fields to include their labels and make them clickable.
123 * The label value itself must be in the '_label' attribute, and the field must also have an 'id' attribute.
125 * @param string $wrapme The rendered field to be wrapped
128 function RenderLabel( $wrapme ) {
129 if ( !isset($this->Attributes
['_label']) ||
!isset($this->Attributes
['id'])) return $wrapme;
130 $class = (isset($this->Attributes
['class']) ?
$this->Attributes
['class'] : 'entry');
131 $title = (isset($this->Attributes
['title']) ?
' title="'.str_replace('"', ''', $this->Attributes
['title']) . '"' : '');
132 return( sprintf( '<label for="%s" class="%s"%s>%s %s</label>',
133 $this->Attributes
['id'], $class, $title, $wrapme, $this->Attributes
['_label']) );
137 * Render the array of attributes for inclusion in the input tag.
140 function RenderAttributes() {
142 if ( count($this->Attributes
) == 0 ) return $attributes;
143 foreach( $this->Attributes
AS $k => $v ) {
144 if ( $k == '_label' ) continue;
145 $attributes .= " $k=\"" . str_replace('"', ''', $v) . '"';
157 * The class for the Editor form in full
174 var $RecordAvailable;
180 * Constructs an editor widget, with a title and fields.
182 * The second parameter maybe passed as a string, to be interpreted as the name of a table, from
183 * which all fields will be included, or as an array of specific fields, in which case you should
184 * make sure to call SetBaseTable('tablename') so the editor knows where to find those fields!
186 * @param string $title
187 * @param array or string $fields See above
189 function __construct( $title = "", $fields = null ) {
190 global $c, $session, $form_id_increment;
191 $this->Title
= $title;
194 $this->Template
= "";
195 $this->RecordAvailable
= false;
196 $this->SubmitName
= 'submit';
197 $form_id_increment = (isset($form_id_increment)? ++
$form_id_increment : 1);
198 $this->Id
= 'editor_'.$form_id_increment;
200 if ( isset($fields) ) {
201 if ( is_array($fields) ) {
202 foreach( $fields AS $k => $v ) {
206 else if ( is_string($fields) ) {
207 // We've been given a table name, so get all fields for it.
208 $this->BaseTable
= $fields;
209 $field_list = get_fields($fields);
210 foreach( $field_list AS $k => $v ) {
215 @dbg_error_log
( 'editor', 'DBG: New editor called %s', $title);
219 * Creates a new field in the Editor, possibly initialising the SQL for calculating it's
220 * value, and lookup_sql for use in drop-down lists.
222 * @param string $field The name for the field.
223 * @param string $sql The SQL for the target list. Think: "$sql AS $field"
224 * @param string $lookup_sql The SQL for looking up a list of possible stored values and displayed values.
226 function &AddField( $field, $sql="", $lookup_sql="" ) {
227 $this->Fields
[$field] = new EditorField( $field, $sql, $lookup_sql );
228 $this->OrderedFields
[] = $field;
229 return $this->Fields
[$field];
233 * Set the SQL for this field for the target list. Think: "$sql AS $field"
234 * @param string $field
237 function SetSql( $field, $sql ) {
238 $this->Fields
[$field]->SetSql( $sql );
242 * Set the SQL for looking up a list of possible stored values and displayed values.
243 * @param string $field
244 * @param string $lookup_sql
246 function SetLookup( $field, $lookup_sql ) {
247 if (is_object($this->Fields
[$field])) {
248 $this->Fields
[$field]->SetLookup( $lookup_sql );
253 * Gets the value of a field in the record currently assigned to this editor.
254 * @param string $value_field_name
256 function Value( $value_field_name ) {
257 if ( !isset($this->Record
->{$value_field_name}) ) return null;
258 return $this->Record
->{$value_field_name};
262 * Assigns the value of a field in the record currently associated with this editor.
263 * @param string $value_field_name
264 * @param string $new_value
266 function Assign( $value_field_name, $new_value ) {
267 if ( !isset($this->Record
) ) $this->Record
= (object) array();
268 $this->Record
->{$value_field_name} = $new_value;
272 * Sets or returns the form ID used for differentiating this form from others in the page.
275 function Id( $id = null ) {
276 if ( isset($id) ) $this->Id
= preg_replace( '#[^a-z0-9_+-]#', '', $id);
281 * Set the explicit options & parameters for a list of stored/displayed values. See the
282 * description under EditorField::SetOptionList() for full details.
284 * @param string $field
285 * @param array $options A key => value array of valid store => display values.
286 * @param string $current The key of the current row
287 * @param string $parameters Set maxwidth & whether displayed values are translated.
289 function SetOptionList( $field, $options, $current = null, $parameters = null) {
290 $this->Fields
[$field]->SetOptionList( $options, $current, $parameters );
294 * Add an attribute to this field.
295 * @param unknown $field
299 function AddAttribute( $field, $k, $v ) {
300 $this->Fields
[$field]->AddAttribute($k,$v);
305 * Set the base table for the row query.
306 * @param unknown $base_table
308 function SetBaseTable( $base_table ) {
309 $this->BaseTable
= $base_table;
314 * @param unknown $join_list
316 function SetJoins( $join_list ) {
317 $this->Joins
= $join_list;
322 * Accessor for the Title for the editor, which could set the title also.
324 * @param string $new_title The new title for the browser
325 * @return string The current title for the browser
327 function Title( $new_title = null ) {
328 if ( isset($new_title) ) $this->Title
= $new_title;
334 * Set the name of the SUBMIT button
335 * @param unknown $new_submit
337 function SetSubmitName( $new_submit ) {
338 $this->SubmitName
= $new_submit;
341 function IsSubmit() {
342 return isset($_POST[$this->SubmitName
]);
346 * Magically knows whether you are in the processing the result of an update or a create.
349 function IsUpdate() {
350 $is_update = $this->Available();
351 if ( isset( $_POST['_editor_action']) && isset( $_POST['_editor_action'][$this->Id
]) ) {
352 $is_update = ( $_POST['_editor_action'][$this->Id
] == 'update' );
353 @dbg_error_log
( 'editor', 'Checking update: %s => %d', $_POST['_editor_action'][$this->Id
], $is_update );
359 * The opposite of IsUpdate. Really.
362 function IsCreate() {
363 return ! $this->IsUpdate();
367 * Set the row selection criteria
368 * @param unknown $where_clause
370 function SetWhere( $where_clause ) {
371 $this->Where
= $where_clause;
375 * Set the criteria used to find the new row after it got created.
376 * @param unknown $where_clause
378 function WhereNewRecord( $where_clause ) {
379 $this->NewWhere
= $where_clause;
383 * Append more stuff to the WHERE clause
384 * @param unknown $operator
385 * @param unknown $more_where
387 function MoreWhere( $operator, $more_where ) {
388 if ( $this->Where
== "" ) {
389 $this->Where
= $more_where;
392 $this->Where
= "$this->Where $operator $more_where";
395 function AndWhere( $more_where ) {
396 $this->MoreWhere("AND",$more_where);
399 function OrWhere( $more_where ) {
400 $this->MoreWhere("OR",$more_where);
404 * Set this to be the form display template. It's better to use Layout($template) in general.
407 * @param string $template
409 function SetTemplate( $template ) {
410 deprecated('Editor::SetTemplate');
411 $this->Template
= $template;
415 * Like SetTemplate($template) except it surrounds the template with a ##form## ... </form> if
416 * there is not a form already in the template.
418 * @param string $template
420 function Layout( $template ) {
421 if ( strstr( $template, '##form##' ) === false && stristr( $template, '<form' ) === false )
422 $template = '##form##' . $template;
423 if ( stristr( $template, '</form' ) === false ) $template .= '</form>';
424 $this->Template
= $template;
428 * Returns 'true' if we have read a row from the database (or set one through SetRecord()), 'false' otherwise.
432 function Available( ) {
433 return $this->RecordAvailable
;
437 * Set a database row to load the field values from.
440 * @return object The row that was passed in.
442 function SetRecord( $row ) {
443 $this->Record
= $row;
444 $this->RecordAvailable
= is_object($this->Record
);
445 return $this->Record
;
449 * Set some particular values to the ones from the array.
451 * @param array $values An array of fieldname / value pairs
453 function Initialise( $values ) {
454 $this->RecordAvailable
= false;
455 if ( !isset($this->Record
) ) $this->Record
= (object) array();
456 foreach( $values AS $fname => $value ) {
457 $this->Record
->{$fname} = $value;
463 * This will assign $_POST values to the internal Values object for each
464 * field that exists in the Fields array.
466 function PostToValues( $prefix = '' ) {
467 foreach ( $this->Fields
AS $fname => $fld ) {
468 @dbg_error_log
( 'editor', ":PostToValues: %s => %s", $fname, $_POST["$prefix$fname"] );
469 if ( isset($_POST[$prefix.$fname]) ) {
470 $this->Record
->{$fname} = $_POST[$prefix.$fname];
471 @dbg_error_log
( 'editor', ":PostToValues: %s => %s", $fname, $_POST["$prefix$fname"] );
477 * Read the record from the database, optionally overriding the WHERE clause.
479 * @param string $where (optional) An SQL WHERE clause to override any previous SetWhere call.
480 * @return object The row that was read from the database.
482 function GetRecord( $where = "" ) {
485 foreach( $this->Fields
AS $k => $column ) {
486 if ( $target_fields != "" ) $target_fields .= ", ";
487 $target_fields .= $column->GetTarget();
489 if ( $where == "" ) $where = $this->Where
;
490 $sql = sprintf( "SELECT %s FROM %s %s WHERE %s %s %s",
491 $target_fields, $this->BaseTable
, $this->Joins
, $where, $this->Order
, $this->Limit
);
492 $this->Query
= new AwlQuery( $sql );
493 @dbg_error_log
( 'editor', "DBG: EditorGetQry: %s", $sql );
494 if ( $this->Query
->Exec("Browse:$this->Title:DoQuery") ) {
495 $this->Record
= $this->Query
->Fetch();
496 $this->RecordAvailable
= is_object($this->Record
);
498 if ( !$this->RecordAvailable
) {
499 $this->Record
= (object) array();
501 return $this->Record
;
506 * Replace parts into the form template. Parts that are replaceable are listed below:
507 * ##form## A <form ...> tag. You should close this with </form> or use Layout($template) which will take care of it for you.
508 * ##submit## A <input type="submit" ...> tag for the form.
509 * ##f.options## A list of options explicitly specified
510 * ##f.select## A select list from the lookup SQL specified
511 * ##f.checkbox## A checkbox, perhaps with a "_label" attribute
512 * ##f.input## A normal input field.
513 * ##f.file## A file upload field.
514 * ##f.money## A money input field.
515 * ##f.date## A date input field.
516 * ##f.textarea## A textarea
517 * ##f.hidden## A hidden input field
518 * ##f.password## An input field for entering passwords without them being echoed to the screen
519 * ##f.enc## Just print the value with special chars escaped for use in URLs.
520 * ##f.submit## An <input type="submit" where you specify the field name.
522 * Most of these begin with "f", which should be replaced by the name of the field. Many also take an option
523 * after the name as well, so (for example) you can force the current value in ##options## or ##select## by
524 * setting ##field.select.current##. The input, file, money & date all accept the third parameter as a size
525 * value, so ##fieldname.date.14## would be a 14-character-wide date field. Similarly a textarea allows for
526 * a COLSxROWS value, so ##myfield.textarea.80x5## would be an 80-column textarea, five rows high.
528 * For ##fieldname.password.fakevalue## you can set the 'fake' value used to populate the password field so
529 * that you can check for this on submit to be able to tell whether the password field has been edited.
531 * Other attributes are added to the <input ...> tag based on any SetAttributes() that may have been applied.
533 * @param array $matches The matches found which preg_replace_callback is calling us for.
534 * @return string What we want to replace this match with.
536 function ReplaceEditorPart($matches)
540 // $matches[0] is the complete match
541 switch( $matches[0] ) {
542 case "##form##": /** @todo It might be nice to construct a form ID */
543 return sprintf('<form method="POST" enctype="multipart/form-data" class="editor" id="%s">', $this->Id
);
545 $action = ( $this->RecordAvailable ?
'update' : 'insert' );
546 $submittype = ($this->RecordAvailable ?
translate('Apply Changes') : translate('Create'));
547 return sprintf('<input type="hidden" name="_editor_action[%s]" value="%s"><input type="submit" class="submit" name="%s" value="%s">',
548 $this->Id
, $action, $this->SubmitName
, $submittype );
551 // $matches[1] the match for the first subpattern
552 // enclosed in '(...)' and so on
553 $field_name = $matches[1];
554 $what_part = (isset($matches[3]) ?
$matches[3] : null);
555 $part3 = (isset($matches[5]) ?
$matches[5] : null);
557 $value_field_name = $field_name;
558 if ( substr($field_name,0,4) == 'xxxx' ) {
559 // Sometimes we will prepend 'xxxx' to the field name so that the field
560 // name differs from the column name in the database. We also remove it
561 // when it's submitted.
562 $value_field_name = substr($field_name,4);
566 if ( isset($this->Fields
[$field_name]) && is_object($this->Fields
[$field_name]) ) {
567 $field = $this->Fields
[$field_name];
568 $attributes = $field->RenderAttributes();
570 $field_value = (isset($this->Record
->{$value_field_name}) ?
$this->Record
->{$value_field_name} : null);
572 switch( $what_part ) {
575 if ( ! isset($currval) && isset($field_value) )
576 $currval = $field_value;
577 if ( isset($field->OptionList
) && $field->OptionList
!= "" ) {
578 $option_list = $field->OptionList
;
581 @dbg_error_log
( 'editor', "DBG: Current=%s, OptionQuery: %s", $currval, $field->LookupSql
);
582 $opt_qry = new AwlQuery( $field->LookupSql
);
583 $option_list = EntryField
::BuildOptionList($opt_qry, $currval, "FieldOptions: $field_name" );
584 $field->OptionList
= $option_list;
589 if ( ! isset($currval) && isset($field_value) )
590 $currval = $field_value;
591 if ( isset($field->OptionList
) && $field->OptionList
!= "" ) {
592 $option_list = $field->OptionList
;
595 @dbg_error_log
( 'editor', 'DBG: Current=%s, OptionQuery: %s', $currval, $field->LookupSql
);
596 $opt_qry = new AwlQuery( $field->LookupSql
);
597 $option_list = EntryField
::BuildOptionList($opt_qry, $currval, 'FieldOptions: '.$field_name );
598 $field->OptionList
= $option_list;
600 return '<select class="entry" name="'.$field_name.'"'.$attributes.'>'.$option_list.'</select>';
602 if ( !isset($field) ) {
603 @dbg_error_log
("ERROR","Field '$field_name' is not defined.");
604 return "<p>Error: '$field_name' is not defined.</p>";
606 if ( $field_value === true ) {
607 $checked = ' CHECKED';
610 switch ( $field_value ) {
620 $checked = ' CHECKED';
623 return $field->RenderLabel('<input type="hidden" value="off" name="'.$field_name.'"><input class="entry" type="checkbox" value="on" name="'.$field_name.'"'.$checked.$attributes.'>' );
625 $size = (isset($part3) ?
$part3 : 6);
626 return "<input class=\"entry\" value=\"".htmlspecialchars($field_value)."\" name=\"$field_name\" size=\"$size\"$attributes>";
628 $size = (isset($part3) ?
$part3 : 30);
629 return "<input type=\"file\" class=\"entry\" value=\"".htmlspecialchars($field_value)."\" name=\"$field_name\" size=\"$size\"$attributes>";
631 $size = (isset($part3) ?
$part3 : 8);
632 return "<input class=\"money\" value=\"".htmlspecialchars(sprintf("%0.2lf",$field_value))."\" name=\"$field_name\" size=\"$size\"$attributes>";
634 $size = (isset($part3) ?
$part3 : 10);
635 return "<input class=\"date\" value=\"".htmlspecialchars($field_value)."\" name=\"$field_name\" size=\"$size\"$attributes>";
637 list( $cols, $rows ) = explode( 'x', $part3);
638 return "<textarea class=\"entry\" name=\"$field_name\" rows=\"$rows\" cols=\"$cols\"$attributes>".htmlspecialchars($field_value)."</textarea>";
640 return sprintf( "<input type=\"hidden\" value=\"%s\" name=\"$field_name\">", htmlspecialchars($field_value) );
642 return sprintf( "<input type=\"password\" value=\"%s\" name=\"$field_name\" size=\"10\">", htmlspecialchars($part3) );
645 return htmlspecialchars($field_value);
647 $action = ( $this->RecordAvailable ?
'update' : 'insert' );
648 return sprintf('<input type="hidden" name="_editor_action[%s]" value="%s"><input type="submit" class="submit" name="%s" value="%s">',
649 $this->Id
, $action, $this->SubmitName
, $value_field_name );
651 return str_replace( "\n", "<br />", $field_value );
656 * Render the templated component. The heavy lifting is done by the callback...
658 function Render( $title_tag = null ) {
659 @dbg_error_log
( 'editor', "classEditor", "Rendering editor $this->Title" );
660 if ( $this->Template
== "" ) $this->DefaultTemplate();
662 $html = sprintf('<div class="editor" id="%s">', $this->Id
);
663 if ( isset($this->Title
) && $this->Title
!= "" ) {
664 if ( !isset($title_tag) ) $title_tag = 'h1';
665 $html = "<$title_tag>$this->Title</$title_tag>\n";
668 // Stuff like "##fieldname.part## gets converted to the appropriate value
669 $replaced = preg_replace_callback("/##([^#.]+)(\.([^#.]+))?(\.([^#.]+))?##/", array(&$this, "ReplaceEditorPart"), $this->Template
);
677 * Write the record. You might want to consider calling Editor::WhereNewRecord() before this if it might be creating a new record.
678 * @param boolean $is_update Explicitly tell the write whether it's an update or insert. Generally it should be able to figure it out though.
680 function Write( $is_update = null ) {
681 global $c, $component;
683 @dbg_error_log
( 'editor', 'DBG: Writing editor %s', $this->Title
);
685 if ( !isset($is_update) ) {
686 if ( isset( $_POST['_editor_action']) && isset( $_POST['_editor_action'][$this->Id
]) ) {
687 $is_update = ( $_POST['_editor_action'][$this->Id
] == 'update' );
690 /** @todo Our old approach will not work for translation. We need to have a hidden field
691 * containing the submittype. Probably we should add placeholders like ##form##, ##script## etc.
692 * which the editor can use for internal purposes.
694 // Then we dvine the action by looking at the submit button value...
695 $is_update = preg_match( '/(save|update|apply)/i', $_POST[$this->SubmitName
] );
696 dbg_error_log('WARN', $_SERVER['REQUEST_URI']. " is using a deprecated method for controlling insert/update" );
699 $this->Action
= ( $is_update ?
"update" : "create" );
700 $qry = new AwlQuery( sql_from_post( $this->Action
, $this->BaseTable
, "WHERE ".$this->Where
) );
701 if ( !$qry->Exec("Editor::Write") ) {
702 $c->messages
[] = "ERROR: $qry->errorstring";
705 if ( $this->Action
== "create" && isset($this->NewWhere
) ) {
706 $this->GetRecord($this->NewWhere
);
709 $this->GetRecord($this->Where
);
711 return $this->Record
;