CWIS Developer Documentation
FieldEditingUI.php
Go to the documentation of this file.
1 <?PHP
2 #
3 # FILE: FieldEditingUI.php
4 #
5 # Part of the Collection Workflow Integration System (CWIS)
6 # Copyright 2014 Edward Almasy and Internet Scout Research Group
7 # http://scout.wisc.edu/cwis/
8 #
9 
11 {
12  # constants to define the operators supported by the editing UI
13  const OP_NOP = 0;
14  const OP_SET = 1;
15  const OP_CLEAR = 2;
16  const OP_CLEARALL = 3;
17  const OP_APPEND = 4;
18  const OP_PREPEND = 5;
19  const OP_REPLACE = 6;
20  const OP_FIND_REPLACE = 7;
21 
29  public function __construct(
30  $EditFormName,
32  {
33  $this->EditFormName = $EditFormName;
34  $this->Schema = new MetadataSchema($SchemaId);
35 
36  $this->Fields = array();
37 
38  $this->AllowedFieldTypes =
50  }
51 
60  public function AddField(
61  $FieldNameOrId,
62  $CurrentValue = NULL,
63  $CurrentOperator = NULL,
64  $AllowRemoval = FALSE)
65  {
66  # if a field name was passed in, convert it to a field id
67  if (!is_numeric($FieldNameOrId))
68  {
69  $FieldNameOrId = $this->Schema->GetFieldByName($FieldNameOrId)->Id();
70  }
71 
72  $this->Fields[]= array(
73  "Type" => "Regular",
74  "FieldId" => $FieldNameOrId,
75  "CurrentValue" => $CurrentValue,
76  "CurrentOperator" => $CurrentOperator,
77  "AllowRemoval" => $AllowRemoval );
78  }
79 
94  public function AddSelectableField(
95  $FieldTypesOrIds = NULL,
96  $CurrentFieldId = NULL,
97  $CurrentValue = NULL,
98  $CurrentOperator = NULL,
99  $AllowRemoval = TRUE)
100  {
101  $Options = $this->TypesOrIdsToFieldList($FieldTypesOrIds);
102 
103  if (count($Options)>0)
104  {
105  $this->Fields[]= array(
106  "Type" => "Selectable",
107  "SelectOptions" => $Options,
108  "CurrentValue" => $CurrentValue,
109  "CurrentOperator" => $CurrentOperator,
110  "AllowRemoval" => $AllowRemoval );
111  }
112  }
113 
123  public function AddFieldButton($Label = "Add field", $FieldTypesOrIds = NULL)
124  {
125  $Options = $this->TypesOrIdsToFieldList($FieldTypesOrIds);
126 
127  if (count($Options)>0)
128  {
129  $this->Fields[]= array(
130  "Type" => "AddButton",
131  "SelectOptions" => $Options,
132  "Label" => $Label,
133  "CurrentOperator" => NULL,
134  "CurrentValue" => NULL,
135  "AllowRemoval" => TRUE );
136  }
137  }
138 
147  public function DisplayAsTable($TableId = NULL, $TableStyle = NULL)
148  {
149  print('<table id="'.defaulthtmlentities($TableId).'" '
150  .'class="'.defaulthtmlentities($TableStyle).'">');
151  $this->DisplayAsRows();
152  print('</table>');
153  }
154 
159  public function DisplayAsRows()
160  {
161  # make sure the necessary javascript is required
162  $GLOBALS["AF"]->REquireUIFile("jquery-ui.js");
163  $GLOBALS["AF"]->RequireUIFile("CW-QuickSearch.js");
164  $GLOBALS["AF"]->RequireUIFile("FieldEditingUI.js");
165 
166  # get a list of the fields examined in this chunk of UI, to
167  # use when constructing the value selector
168  $FieldsExamined = array();
169  foreach ($this->Fields as $FieldRow)
170  {
171  if ($FieldRow["Type"] == "Regular")
172  {
173  $FieldsExamined[]= $FieldRow["FieldId"];
174  }
175  else
176  {
177  $FieldsExamined = array_merge(
178  $FieldsExamined,
179  $FieldRow["SelectOptions"]);
180  }
181  }
182  $FieldsExamined = array_unique($FieldsExamined);
183 
184  # iterate over each field adding edit rows for all of them
185 
186  print('<tr class="cw-feui-empty '.$this->EditFormName.'" '.
187  'data-formname="'.$this->EditFormName.'"><td colspan="4">'.
188  '<i>No fields selected for editing</i></td></tr>');
189 
190  # note that all of the fields we create for these rows will be named
191  # $this->EditFormName.'[]' , combining them all into an array of results per
192  # http://php.net/manual/en/faq.html.php#faq.html.arrays
193  foreach ($this->Fields as $FieldRow)
194  {
195  $CurOp = $FieldRow["CurrentOperator"];
196  $CurVal = $FieldRow["CurrentValue"];
197  $AllowRemoval = $FieldRow["AllowRemoval"];
198 
199  print('<tr class="field_row '.$this->EditFormName
200  .($FieldRow["Type"]=="AddButton" ? ' template_row':'').'">');
201 
202  print("<td>");
203  if ($FieldRow["AllowRemoval"])
204  {
205  print("<span class=\"cw-button cw-button-elegant cw-button-constrained "
206  ."cw-feui-delete\">X</span>");
207  }
208  print("</td>");
209 
210  if ($FieldRow["Type"] == "Regular")
211  {
212  $Field = new MetadataField( $FieldRow["FieldId"] );
213 
214  # for fields that cannot be selected, we already know
215  # the type and can print field-specific elements
216  # (operators, etc) rather than relying on js to clean
217  # them up for us.
218  $TypeName = defaulthtmlentities(
219  str_replace(' ', '', strtolower($Field->TypeAsName())));
220  if ($Field->Type() == MetadataSchema::MDFTYPE_OPTION &&
221  $Field->AllowMultiple() )
222  {
223  $TypeName = "mult".$TypeName;
224  }
225 
226  # encode the field for this row in a form value
227  print('<td>'.$Field->Name()
228  .'<input type="hidden"'
229  .' name="'.$this->EditFormName.'[]"'
230  .' class="field-subject field-static field-type-'.$TypeName.'"'
231  .' value="S_'.$Field->Id().'"></td>'."\n");
232 
233  print('<td><select name="'.$this->EditFormName.'[]">'."\n");
234  if (!$AllowRemoval)
235  {
236  $this->PrintOp(self::OP_NOP, $CurOp);
237  }
238  # determine operators, make a select widget
239  switch ($Field->Type())
240  {
244  $this->PrintOp(self::OP_APPEND, $CurOp);
245  $this->PrintOp(self::OP_PREPEND, $CurOp);
246  $this->PrintOp(self::OP_REPLACE, $CurOp);
247  $this->PrintOp(self::OP_FIND_REPLACE, $CurOp);
248  break;
249 
251  $this->PrintOp(self::OP_SET, $CurOp);
252  break;
253 
257  $this->PrintOp(self::OP_SET, $CurOp);
258  if ($Field->Optional())
259  {
260  $this->PrintOp(self::OP_CLEARALL, $CurOp);
261  }
262  break;
263 
268  $this->PrintOp(self::OP_SET, $CurOp);
269  $this->PrintOp(self::OP_CLEAR, $CurOp);
270 
271  if ($Field->Optional() &&
272  ($Field->Type() != MetadataSchema::MDFTYPE_OPTION ||
273  $Field->AllowMultiple() ))
274  {
275  $this->PrintOp(self::OP_CLEARALL, $CurOp);
276  }
277  break;
278 
279  default:
280  throw new Exception("Unsupported field type");
281  }
282  print('</select></td>');
283 
284  print('<td>');
285  switch ($Field->Type())
286  {
293  if ($CurOp == self::OP_FIND_REPLACE)
294  {
295  print('<input type="text" '
296  .'name="'.$this->EditFormName.'[]" '
297  .'value="'.defaulthtmlentities($CurVal[0]).'">');
298  print('<input type="text" class="field-value-repl" '
299  .'name="'.$this->EditFormName.'[]" '
300  .'value="'.defaulthtmlentities($CurVal[1]).'">');
301  }
302  else
303  {
304  print('<input type="text" '
305  .'name="'.$this->EditFormName.'[]" '
306  .'value="'.defaulthtmlentities($CurVal).'">');
307  }
308  break;
309 
313  print('<div class="field-value-qs cw-quicksearch '
314  .'cw-quicksearch-fieldid-'.$Field->Id().'" '
315  .'data-fieldid="'.$Field->Id().'">'
316  .'<input class="cw-quicksearch-display '
317  .'cw-resourceeditor-metadatafield F_'
318  .$this->EditFormName.'"'
319  .'value="'.defaulthtmlentities($CurVal).'" />'
320  .'<input name="'.$this->EditFormName.'[]"'
321  .'class="cw-quicksearch-value" type="hidden"'
322  .'value="'.defaulthtmlentitites($CurVal).'" />'
323  .'<div style="display: none;"'
324  .'class="cw-quicksearch-menu">'
325  .'<div class="cw-quicksearch-message ui-front"></div>'
326  .'</div></div>');
327  break;
328 
331  print('<select name="'.$this->EditFormName.'[]">');
332  foreach ($Field->GetPossibleValues() as $Id => $Val)
333  {
334  print('<option value="'.$Id.'" '
335  .'class="field-id-'.$Field->Id().'"'
336  .( ($CurVal == $Id) ? ' selected' : '')
337  .'>'.defaulthtmlentities($Val)
338  .'</option>'."\n");
339  }
340  print('</select>'."\n");
341  break;
342  }
343  print ('</td>');
344  }
345  else
346  {
347  # for selectable fields, we need to generate all the
348  # html elements that we might need and then depend on
349  # javascript to display only those that are relevant
350 
351  # each field will have five elements
352 
353  # 1. a field selector
354  print('<td><select name="'.$this->EditFormName.'[]" '
355  .'class="field-subject">');
356  foreach ($FieldRow["SelectOptions"] as $FieldId)
357  {
358  $Field = new MetadataField($FieldId);
359  $TypeName = defaulthtmlentities(
360  str_replace(' ', '', strtolower($Field->TypeAsName())));
361 
362  if ($Field->Type() == MetadataSchema::MDFTYPE_OPTION &&
363  $Field->AllowMultiple() )
364  {
365  $TypeName = "mult".$TypeName;
366  }
367 
368  if (!$Field->Optional())
369  {
370  $TypeName .= " required";
371  }
372 
373  print('<option class="field-type-'.$TypeName.'" '
374  .'data-maxnumsearchresults="'.$Field->NumAjaxResults().'" '
375  .'value="'.$Field->Id().'">'
376  .defaulthtmlentities($Field->Name()).'</option>');
377  }
378  print('</select></td>');
379 
380  # 2. an operator selector
381  $TextTypes = array("url","text","paragraph");
382 
383  print('<td><select name="'.$this->EditFormName.'[]" '
384  .'class="field-operator">');
385 
386  # for fields that cannot be removed, allow a 'do nothing' option
387  if (!$AllowRemoval)
388  {
389  $this->PrintOp(self::OP_NOP, $CurOp,
390  array("flag", "tree", "option",
391  "multoption", "controlledname",
392  "reference", "timstamp", "date",
393  "number") );
394  }
395 
396  # display all avaialble operators, annotated such that
397  # js can switch between them
398  $this->PrintOp(self::OP_SET, $CurOp,
399  array("flag","tree", "option", "multoption",
400  "controlledname",
401  "reference",
402  "timestamp", "date", "number"));
403  $this->PrintOp(self::OP_CLEAR, $CurOp,
404  array("tree", "option",
405  "multoption", "controlledname",
406  "reference"));
407  $this->PrintOp(self::OP_CLEARALL, $CurOp,
408  array("tree","controlledname","multoption",
409  "timestamp","date","number"));
410 
411  $this->PrintOp(self::OP_APPEND, $CurOp, $TextTypes );
412  $this->PrintOp(self::OP_PREPEND, $CurOp, $TextTypes );
413  $this->PrintOp(self::OP_REPLACE, $CurOp, $TextTypes );
414  $this->PrintOp(self::OP_FIND_REPLACE, $CurOp, $TextTypes );
415  print('</select></td><td>');
416 
417  # 3. a value selector (for option and flag values)
418  print('<select name="'.$this->EditFormName.'[]" '
419  .'class="field-value-select">');
420  foreach ($FieldsExamined as $FieldId)
421  {
422  $Field = new MetadataField($FieldId);
423  if ($Field->Type() == MetadataSchema::MDFTYPE_FLAG ||
424  $Field->Type() == MetadataSchema::MDFTYPE_OPTION)
425  {
426  foreach ($Field->GetPossibleValues() as $Id => $Val)
427  {
428  print('<option value="'.$Id.'" '
429  .'class="field-id-'.$Field->Id().'">'
430  .defaulthtmlentities($Val)
431  .'</option>'."\n");
432  }
433  }
434  }
435  print('</select>');
436 
437  # 4. two text entries (free-form text and possible replacement)
438  print('<input type="text" class="field-value-edit" '
439  .'name="'.$this->EditFormName.'[]" '
440  .'value="'.defaulthtmlentities($CurVal).'">'
441  .'<input type="text" class="field-value-repl" '
442  .'name="'.$this->EditFormName.'[]" '
443  .'value="'.defaulthtmlentities($CurVal).'">');
444 
445  # 5. an ajax search box
446  # CW-QuickSearch.js wants a textarea
447  print('<div class="field-value-qs cw-quicksearch '
448  . 'cw-quicksearch-template">'
449  .'<input class="cw-quicksearch-display '
450  .'cw-resourceeditor-metadatafield F_'.$this->EditFormName.'" '
451  .'value="'.defaulthtmlentities($CurVal).'" />'
452  .'<input name="'.$this->EditFormName.'[]" '
453  .'class="cw-quicksearch-value" type="hidden" '
454  .'value="'.defaulthtmlentities($CurVal).'" />'
455  .'<div style="display: none;" '
456  .'class="cw-quicksearch-menu">'
457  .'<div class="cw-quicksearch-message ui-front"></div>'
458  .'</div></div>');
459 
460  if ($FieldRow["Type"] == "AddButton")
461  {
462  print('</tr><tr class="button_row"><td colspan="4">'
463  .'<span class="cw-button cw-button-elegant cw-feui-add">'
464  .defaulthtmlentities($FieldRow["Label"]).'</span></td></tr>');
465  }
466  }
467  print('</tr>');
468  }
469  }
470 
477  public function GetValuesFromFormData()
478  {
479  $Results = array();
480 
481  if (!isset($_POST[$this->EditFormName]))
482  {
483  return $Results;
484  }
485 
486  # extract the array of data associated with our EditFormName
487  $FormData = $_POST[$this->EditFormName];
488 
489  while (count($FormData))
490  {
491  # first element of each row is a field id
492  $FieldId = array_shift($FormData);
493  $Op = array_shift($FormData);
494 
495  # when the row was static, it'll have a 'S_' prefix
496  # to make it non-numeric
497  if (!is_numeric($FieldId))
498  {
499  # remove the S_ prefix to get the real field id
500  $FieldId = substr($FieldId, 2);
501 
502  # grab the value(s) for this field
503  $Val = array_shift($FormData);
504  if ($Op == self::OP_FIND_REPLACE)
505  {
506  $Val2 = array_shift($FormData);
507  }
508  }
509  else
510  {
511  # for selectable fields, we'll have all possible
512  # elements and will need to grab the correct ones for
513  # the currently selected field
514  $SelectVal = array_shift($FormData);
515  $TextVal = array_shift($FormData);
516  $TextVal2 = array_shift($FormData);
517  $SearchVal = array_shift($FormData);
518 
519  $Field = new MetadataField($FieldId);
520 
521  switch ($Field->Type())
522  {
529  $Val = $TextVal;
530  break;
531 
535  $Val = $SearchVal;
536  break;
537 
540  $Val = $SelectVal;
541  break;
542 
543  default:
544  throw new Exception("Unsupported field type");
545  }
546  }
547 
548  $ResRow = array(
549  "FieldId" => $FieldId, "Op" => $Op, "Val" => $Val );
550 
551  if ($Op == self::OP_FIND_REPLACE)
552  {
553  $ResRow["Val2"] = $TextVal2;
554  }
555 
556  $Results[]= $ResRow;
557  }
558 
559  return $Results;
560  }
561 
567  public function LoadConfiguration($Data)
568  {
569  foreach ($Data as $Row)
570  {
571  $this->AddField(
572  $Row["FieldId"],
573  ($Row["Op"] == self::OP_FIND_REPLACE) ?
574  array( $Row["Val"], $Row["Val2"] ) :
575  $Row["Val"],
576  $Row["Op"],
577  TRUE);
578  }
579  }
580 
590  public static function ApplyChangesToResource($Resource, $User, $ChangesToApply)
591  {
592  $Changed = FALSE;
593 
594  foreach ($ChangesToApply as $Change)
595  {
596  $Field = new MetadataField($Change["FieldId"]);
597 
598  if ( ($User === NULL ||
599  $Resource->UserCanEditField($User, $Field) ) &&
600  $Field->Editable() )
601  {
602  $OldVal = $Resource->Get( $Field );
603 
604  switch ($Change["Op"])
605  {
606  case self::OP_NOP:
607  break;
608 
609  case self::OP_SET:
610  self::ModifyFieldValue($User, $Resource, $Field, $Change["Val"]);
611  break;
612 
613  case self::OP_REPLACE:
614  if ( strlen($Change["Val"]) > 0 ||
615  $Field->Optional() )
616  {
617  self::ModifyFieldValue($User, $Resource,
618  $Field, $Change["Val"]);
619  }
620  break;
621 
622  case self::OP_FIND_REPLACE:
623  self::ModifyFieldValue(
624  $User,
625  $Resource,
626  $Field,
627  str_replace($Change["Val"], $Change["Val2"], $OldVal));
628  break;
629 
630  case self::OP_CLEAR:
631  if (isset($OldVal[$Change["Val"]]) &&
632  ($Field->Optional() || count($OldVal)>1) )
633  {
634  $Resource->Clear($Field, $Change["Val"]);
635  }
636  break;
637 
638  case self::OP_CLEARALL:
639  if ($Field->Optional())
640  {
641  $Resource->Clear($Field);
642  }
643  break;
644 
645  case self::OP_APPEND:
646  $Sep = $Field->Type() == MetadataSchema::MDFTYPE_PARAGRAPH ?
647  "\n" : " " ;
648  self::ModifyFieldValue($User, $Resource, $Field,
649  $OldVal.$Sep.$Change["Val"]);
650  break;
651 
652  case self::OP_PREPEND:
653  $Sep = $Field->Type() == MetadataSchema::MDFTYPE_PARAGRAPH ?
654  "\n" : " " ;
655  self::ModifyFieldValue($User, $Resource, $Field,
656  $Change["Val"].$Sep.$OldVal);
657  break;
658  }
659 
660  $NewVal = $Resource->Get( $Field );
661  if ($NewVal != $OldVal)
662  {
663  $Changed = TRUE;
664  }
665  }
666  }
667 
668  # if there were any changes
669  if ($Changed)
670  {
671  $Resource->UpdateAutoupdateFields(
673  $GLOBALS["G_User"]);
674 
675  # update search and recommender DBs if configured to do so
676  $Resource->QueueSearchAndRecommenderUpdate();
677 
678  # signal the modified event if the resource isn't a temp one
679  if (!$Resource->IsTempResource())
680  {
681  $GLOBALS["AF"]->SignalEvent(
682  "EVENT_RESOURCE_MODIFY", array("Resource" => $Resource));
683  }
684  }
685 
686  return $Changed;
687  }
688 
689  # ---- PRIVATE INTERFACE -------------------------------------------------
690 
691  private $EditFormName;
692  private $SchemaId;
693  private $Fields;
694 
695  # mapping of operator constants to their friendly names
696  private $OpNames = array(
697  self::OP_NOP => "Do nothing",
698  self::OP_SET => "Set",
699  self::OP_CLEAR => "Clear One Value",
700  self::OP_CLEARALL => "Clear All Values",
701  self::OP_APPEND => "Append",
702  self::OP_PREPEND => "Prepend",
703  self::OP_REPLACE => "Replace With",
704  self::OP_FIND_REPLACE => "Find/Replace" );
705 
706  # to store a bitmask
707  private $AllowedFieldTypes;
708 
717  private function PrintOp($Value, $Selected=NULL, $TypeNames=array())
718  {
719  $Classes = array();
720  foreach ($TypeNames as $Name)
721  {
722  $Classes[]= "field-type-".$Name;
723  }
724 
725  print('<option value="'.$Value.'" '
726  .($Selected==$Value ? 'selected' : '')
727  .'class="'.implode(' ', $Classes).'"'
728  .'>'.$this->OpNames[$Value]
729  .'</option>'."\n");
730  }
731 
738  private function TypesOrIdsToFieldList($FieldTypesOrIds)
739  {
740  if ($FieldTypesOrIds === NULL)
741  {
742  $FieldTypesOrIds = $this->AllowedFieldTypes;
743  }
744 
745  $FieldList = array();
746 
747  if (is_array($FieldTypesOrIds))
748  {
749  # if we were given a list of fields, add all the editable ones
750  foreach ($FieldTypesOrIds as $FieldId)
751  {
752  if ($this->Schema->FieldExists($FieldId))
753  {
754  $Field = $this->Schema->GetField($FieldId);
755  if ($Field->Editable())
756  {
757  $FieldList[]= $FieldId;
758  }
759  }
760  }
761  }
762  else
763  {
764  # otherwise, iterate over all supported fields and add the editable ones
765  foreach ($this->Schema->GetFields($FieldTypesOrIds) as $Field)
766  {
767  if ($Field->Editable())
768  {
769  $FieldList[]= $Field->Id();
770  }
771  }
772  }
773 
774  return $FieldList;
775  }
776 
785  private static function ModifyFieldValue($User, $Resource, $Field, $NewValue)
786  {
787  $ShouldDoDateSubs = array(
792  );
793 
794  $ShouldDoUserSubs = array(
797 
798  );
799 
800  # process substitutions for fields where they apply
801  if (in_array($Field->Type(), $ShouldDoDateSubs))
802  {
803  $Substitutions = array(
804  "X-DATE-X" => date("M j Y"),
805  "X-TIME-X" => date("g:ia T") );
806 
807  $NewValue = str_replace(
808  array_keys($Substitutions),
809  array_values($Substitutions),
810  $NewValue);
811  }
812 
813  if (in_array($Field->Type(), $ShouldDoUserSubs))
814  {
815  $Substitutions = array(
816  "X-USERNAME-X" => ($User!==NULL) ? $User->Get("UserName") : "",
817  "X-USEREMAIL-X" => ($User!==NULL) ? $User->Get("EMail") : "",
818  );
819 
820  $NewValue = str_replace(
821  array_keys($Substitutions),
822  array_values($Substitutions),
823  $NewValue);
824  }
825 
826  # process edit hooks for fields where they apply
827  $ShouldCallHooks = array(
835 
836  if (in_array($Field->Type(), $ShouldCallHooks))
837  {
838  $SignalResult = $GLOBALS["AF"]->SignalEvent(
839  "EVENT_POST_FIELD_EDIT_FILTER", array(
840  "Field" => $Field,
841  "Resource" => $Resource,
842  "Value" => $NewValue));
843  $NewValue = $SignalResult["Value"];
844  }
845 
846  # before updating field value, verify that the provided item is real
847  $Factory = $Field->GetFactory();
848  if (is_null($Factory) ||
849  $Factory->ItemExists($NewValue) !== FALSE)
850  {
851  $Resource->Set($Field, $NewValue);
852  }
853  }
854 }
LoadConfiguration($Data)
Load a configured set of fields.
DisplayAsRows()
Display the table rows for the editing form, without the surrounding.
Metadata schema (in effect a Factory class for MetadataField).
const UPDATEMETHOD_ONRECORDCHANGE
AddField($FieldNameOrId, $CurrentValue=NULL, $CurrentOperator=NULL, $AllowRemoval=FALSE)
Add a field to the list of editable fields.
AddFieldButton($Label="Add field", $FieldTypesOrIds=NULL)
Add a button to create more fields above the button.
DisplayAsTable($TableId=NULL, $TableStyle=NULL)
Display editing form elements enclosed in a.
const MDFTYPE_CONTROLLEDNAME
static ApplyChangesToResource($Resource, $User, $ChangesToApply)
Apply the changes extracted from an editing form to a specified resource.
__construct($EditFormName, $SchemaId=MetadataSchema::SCHEMAID_DEFAULT)
Create a UI for specifing edits to metadata fields.
Object representing a locally-defined type of metadata field.
GetValuesFromFormData()
Extract values from a dynamics field edit/modification form.
AddSelectableField($FieldTypesOrIds=NULL, $CurrentFieldId=NULL, $CurrentValue=NULL, $CurrentOperator=NULL, $AllowRemoval=TRUE)
Add a selectable field to the list of editable fields.