PageRenderTime 69ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/library/Adapto/Relation/ManyToOne.php

http://github.com/egeniq/adapto
PHP | 1683 lines | 901 code | 160 blank | 622 comment | 195 complexity | 34915e09a3c8ef37093933492be42597 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * This file is part of the Adapto Toolkit.
  4. * Detailed copyright and licensing information can be found
  5. * in the doc/COPYRIGHT and doc/LICENSE files which should be
  6. * included in the distribution.
  7. *
  8. * @package adapto
  9. * @subpackage relations
  10. *
  11. * @copyright (c)2000-2004 Ibuildings.nl BV
  12. * @copyright (c)2000-2004 Ivo Jansch
  13. * @license http://www.achievo.org/atk/licensing ATK Open Source License
  14. *
  15. */
  16. /**
  17. * Create edit/view links for the items in a manytoonerelation dropdown.
  18. */
  19. define("AF_RELATION_AUTOLINK", AF_SPECIFIC_1);
  20. /**
  21. * Create edit/view links for the items in a manytoonerelation dropdown.
  22. */
  23. define("AF_MANYTOONE_AUTOLINK", AF_RELATION_AUTOLINK);
  24. /**
  25. * Do not add null option under any circumstance
  26. */
  27. define("AF_RELATION_NO_NULL_ITEM", AF_SPECIFIC_2);
  28. /**
  29. * Do not add null option ever
  30. */
  31. define("AF_MANYTOONE_NO_NULL_ITEM", AF_RELATION_NO_NULL_ITEM);
  32. /**
  33. * Use auto-completition instead of drop-down / selection page
  34. */
  35. define("AF_RELATION_AUTOCOMPLETE", AF_SPECIFIC_3);
  36. /**
  37. * Use auto-completition instead of drop-down / selection page
  38. */
  39. define("AF_MANYTOONE_AUTOCOMPLETE", AF_RELATION_AUTOCOMPLETE);
  40. /**
  41. * Lazy load
  42. */
  43. define("AF_MANYTOONE_LAZY", AF_SPECIFIC_4);
  44. /**
  45. * Add a default null option to obligatory relations
  46. */
  47. define("AF_MANYTOONE_OBLIGATORY_NULL_ITEM", AF_SPECIFIC_5);
  48. /**
  49. * @internal include base class
  50. */
  51. userelation("atkrelation");
  52. /**
  53. * A N:1 relation between two classes.
  54. *
  55. * For example, projects all have one coordinator, but one
  56. * coordinator can have multiple projects. So in the project
  57. * class, there's a ManyToOneRelation to a coordinator.
  58. *
  59. * This relation essentially creates a dropdown box, from which
  60. * you can select from a set of records.
  61. *
  62. * @author ijansch
  63. * @package adapto
  64. * @subpackage relations
  65. *
  66. */
  67. class Adapto_Relation_ManyToOne extends Adapto_Relation
  68. {
  69. const SEARCH_MODE_EXACT = "exact";
  70. const SEARCH_MODE_STARTSWITH = "startswith";
  71. const SEARCH_MODE_CONTAINS = "contains";
  72. /**
  73. * By default, we do a left join. this means that records that don't have
  74. * a record in this relation, will be displayed anyway. NOTE: set this to
  75. * false only if you know what you're doing. When in doubt, 'true' is
  76. * usually the best option.
  77. * @var boolean
  78. */
  79. public $m_leftjoin = true; // defaulted to public
  80. /**
  81. * The array of referential key fields.
  82. * @access private
  83. * @var array
  84. */
  85. public $m_refKey = array(); // defaulted to public
  86. /**
  87. * SQL statement with extra filter for the join that retrieves the
  88. * selected record.
  89. * @var String
  90. */
  91. public $m_joinFilter = ""; // defaulted to public
  92. /**
  93. * Hide the relation when there are no records to select.
  94. * @access private
  95. * @var boolean
  96. */
  97. public $m_hidewhenempty = false; // defaulted to public
  98. /**
  99. * List columns.
  100. * @access private
  101. * @var Array
  102. */
  103. public $m_listColumns = array(); // defaulted to public
  104. /**
  105. * Always show list columns?
  106. * @access private
  107. * @var boolean
  108. */
  109. public $m_alwaysShowListColumns = false; // defaulted to public
  110. /**
  111. * Label to use for the 'none' option.
  112. *
  113. * @access private
  114. * @var String
  115. */
  116. public $m_noneLabel = NULL; // defaulted to public
  117. /**
  118. * Minimum number of character a user needs to enter before auto-completion kicks in.
  119. *
  120. * @access private
  121. * @var int
  122. */
  123. public $m_autocomplete_minchars = 2; // defaulted to public
  124. /**
  125. * An array with the fieldnames of the destination entity in which the autocompletion must search
  126. * for results.
  127. *
  128. * @access private
  129. * @var array
  130. */
  131. public $m_autocomplete_searchfields = ""; // defaulted to public
  132. /**
  133. * The search mode of the autocomplete fields. Can be 'startswith', 'exact' or 'contains'.
  134. *
  135. * @access private
  136. * @var String
  137. */
  138. public $m_autocomplete_searchmode = "contains"; // defaulted to public
  139. /**
  140. * Value determines wether the search of the autocompletion is case-sensitive.
  141. *
  142. * @var boolean
  143. */
  144. public $m_autocomplete_search_case_sensitive = false; // defaulted to public
  145. /**
  146. * Value determines if select link for autocomplete should use atkSubmit or not (for use in admin screen for example)
  147. *
  148. * @var boolean
  149. */
  150. public $m_autocomplete_saveform = true; // defaulted to public
  151. /**
  152. * Set the minimal number of records for showing the automcomplete. If there are less records
  153. * the normal dropdown is shown
  154. *
  155. * @access private
  156. * @var integer
  157. */
  158. public $m_autocomplete_minrecords = -1; // defaulted to public
  159. /**
  160. * Set the size attribute of the autocompletion input element
  161. *
  162. * @access protected
  163. * @var integer
  164. */
  165. protected $m_autocomplete_size = 30;
  166. /**
  167. * Destination entity for auto links (edit, new)
  168. *
  169. * @var string
  170. */
  171. protected $m_autolink_destination = "";
  172. // override onchangehandler init
  173. public $m_onchangehandler_init = "newvalue = el.options[el.selectedIndex].value;\n"; // defaulted to public
  174. /**
  175. * Use destination filter for autolink add link?
  176. *
  177. * @access private
  178. * @var boolean
  179. */
  180. public $m_useFilterForAddLink = true; // defaulted to public
  181. /**
  182. * Set a function to use for determining the descriptor in the getConcatFilter function
  183. *
  184. * @access private
  185. * @var string
  186. */
  187. public $m_concatDescriptorFunction = ''; // defaulted to public
  188. /**
  189. * When autosearch is set to true, this attribute will automatically submit
  190. * the search form onchange. This will only happen in the admin action.
  191. *
  192. * @var boolean
  193. */
  194. protected $m_autoSearch = false;
  195. /**
  196. * Selectable records for edit mode.
  197. *
  198. * @see Adapto_Relation_ManyToOne::preAddToEditArray
  199. *
  200. * @var array
  201. */
  202. protected $m_selectableRecords = null;
  203. /**
  204. * Constructor.
  205. * @param String $name The name of the attribute. This is the name of the
  206. * field that is the referential key to the
  207. * destination.
  208. * For relations with more than one field in the
  209. * foreign key, you should pass an array of
  210. * referential key fields. The order of the fields
  211. * must match the order of the primary key attributes
  212. * in the destination entity.
  213. * @param String $destination The entity we have a relationship with.
  214. * @param int $flags Flags for the relation
  215. */
  216. public function __construct($name, $destination, $flags = 0)
  217. {
  218. if (Adapto_Config::getGlobal("manytoone_autocomplete_default", false))
  219. $flags |= AF_RELATION_AUTOCOMPLETE;
  220. if (Adapto_Config::getGlobal("manytoone_autocomplete_large", true) && hasFlag($flags, AF_LARGE))
  221. $flags |= AF_RELATION_AUTOCOMPLETE;
  222. $this->m_autocomplete_minchars = Adapto_Config::getGlobal("manytoone_autocomplete_minchars", 2);
  223. $this->m_autocomplete_searchmode = Adapto_Config::getGlobal("manytoone_autocomplete_searchmode", "contains");
  224. $this->m_autocomplete_search_case_sensitive = Adapto_Config::getGlobal("manytoone_autocomplete_search_case_sensitive", false);
  225. if (is_array($name)) {
  226. $this->m_refKey = $name;
  227. // ATK can't handle an array as name, so we initialize the
  228. // underlying attribute with the first name of the referential
  229. // keys.
  230. // Languagefiles, overrides, etc should use this first name to
  231. // override the relation.
  232. parent::__construct($name[0], $destination, $flags);
  233. } else {
  234. $this->m_refKey[] = $name;
  235. parent::__construct($name, $destination, $flags);
  236. }
  237. if ($this->hasFlag(AF_MANYTOONE_LAZY) && (count($this->m_refKey) > 1 || $this->m_refKey[0] != $this->fieldName())) {
  238. throw new Adapto_Exception("AF_MANYTOONE_LAZY flag is not supported for multi-column reference key or a reference key that uses another column.");
  239. }
  240. }
  241. /**
  242. * Adds a flag to the manyToOne relation
  243. * Note that adding flags at any time after the constructor might not
  244. * always work. There are flags that are processed only at
  245. * constructor time.
  246. *
  247. * @param int $flag The flag to add to the attribute
  248. * @return Adapto_Relation_ManyToOne The instance of this Adapto_Relation_ManyToOne
  249. */
  250. function addFlag($flag)
  251. {
  252. parent::addFlag($flag);
  253. if (Adapto_Config::getGlobal("manytoone_autocomplete_large", true) && hasFlag($flag, AF_LARGE))
  254. $this->m_flags |= AF_RELATION_AUTOCOMPLETE;
  255. return $this;
  256. }
  257. /**
  258. * When autosearch is set to true, this attribute will automatically submit
  259. * the search form onchange. This will only happen in the admin action.
  260. * @param bool $auto
  261. * @return void
  262. */
  263. public function setAutoSearch($auto = false)
  264. {
  265. $this->m_autoSearch = $auto;
  266. }
  267. /**
  268. * Set join filter.
  269. *
  270. * @param string $filter join filter
  271. */
  272. function setJoinFilter($filter)
  273. {
  274. $this->m_joinFilter = $filter;
  275. }
  276. /**
  277. * Set the searchfields for the autocompletion.
  278. *
  279. * @param array $searchfields
  280. */
  281. function setAutoCompleteSearchFields($searchfields)
  282. {
  283. $this->m_autocomplete_searchfields = $searchfields;
  284. }
  285. /**
  286. * Set the searchmode for the autocompletion:
  287. * exact, startswith(default) or contains.
  288. *
  289. * @param array $mode
  290. */
  291. function setAutoCompleteSearchMode($mode)
  292. {
  293. $this->m_autocomplete_searchmode = $mode;
  294. }
  295. /**
  296. * Set the case-sensitivity for the autocompletion search (true or false).
  297. *
  298. * @param array $case_sensitive
  299. */
  300. function setAutoCompleteCaseSensitive($case_sensitive)
  301. {
  302. $this->m_autocomplete_search_case_sensitive = $case_sensitive;
  303. }
  304. /**
  305. * Sets the minimum number of characters before auto-completion kicks in.
  306. *
  307. * @param int $chars
  308. */
  309. function setAutoCompleteMinChars($chars)
  310. {
  311. $this->m_autocomplete_minchars = $chars;
  312. }
  313. /**
  314. * Set if the select link should save form (atkSubmit) or not (for use in admin screen for example)
  315. *
  316. * @param boolean $saveform
  317. */
  318. function setAutoCompleteSaveForm($saveform = true)
  319. {
  320. $this->m_autocomplete_saveform = $saveform;
  321. }
  322. /**
  323. * Set the minimal number of records for the autocomplete to show
  324. * If there are less records the normal dropdown is shown
  325. *
  326. * @param integer $minrecords
  327. */
  328. function setAutoCompleteMinRecords($minrecords)
  329. {
  330. $this->m_autocomplete_minrecords = $minrecords;
  331. }
  332. /**
  333. * Set the size of the rendered autocompletion input element
  334. *
  335. * @param integer $size
  336. */
  337. function setAutoCompleteSize($size)
  338. {
  339. $this->m_autocomplete_size = $size;
  340. }
  341. /**
  342. * Use destination filter for auto add link?
  343. *
  344. * @param boolean $useFilter use destnation filter for add link?
  345. */
  346. function setUseFilterForAddLink($useFilter)
  347. {
  348. $this->m_useFilterForAddLink = $useFilter;
  349. }
  350. /**
  351. * Set the function for determining the descriptor in the getConcatFilter function
  352. * This function should be implemented in the destination entity
  353. *
  354. * @param string $function
  355. */
  356. function setConcatDescriptorFunction($function)
  357. {
  358. $this->m_concatDescriptorFunction = $function;
  359. }
  360. /**
  361. * Return the function for determining the descriptor in the getConcatFilter function
  362. *
  363. * @return string
  364. */
  365. function getConcatDescriptorFunction()
  366. {
  367. return $this->m_concatDescriptorFunction;
  368. }
  369. /**
  370. * Add list column. An attribute of the destination entity
  371. * that (only) will be displayed in the recordlist.
  372. *
  373. * @param string $attr The attribute to add to the listcolumn
  374. * @return Adapto_Relation_ManyToOne The instance of this Adapto_Relation_ManyToOne
  375. */
  376. function addListColumn($attr)
  377. {
  378. $this->m_listColumns[] = $attr;
  379. return $this;
  380. }
  381. /**
  382. * Add multiple list columns. Attributes of the destination entity
  383. * that (only) will be displayed in the recordlist.
  384. * @return Adapto_Relation_ManyToOne The instance of this Adapto_Relation_ManyToOne
  385. */
  386. function addListColumns()
  387. {
  388. $attrs = func_get_args();
  389. foreach ($attrs as $attr)
  390. $this->m_listColumns[] = $attr;
  391. return $this;
  392. }
  393. public function getListColumns()
  394. {
  395. return $this->m_listColumns;
  396. }
  397. /**
  398. * Reset the list columns and add multiple list columns. Attributes of the
  399. * destination entity that (only) will be displayed in the recordlist.
  400. * @return Adapto_Relation_ManyToOne The instance of this Adapto_Relation_ManyToOne
  401. */
  402. public function setListColumns()
  403. {
  404. $this->m_listColumns = array();
  405. $attrs = func_get_args();
  406. if (count($attrs) === 1 && is_array($attrs[0])) {
  407. $columns = $attrs[0];
  408. } else {
  409. $columns = $attrs;
  410. }
  411. foreach ($columns as $column) {
  412. $this->m_listColumns[] = $column;
  413. }
  414. return $this;
  415. }
  416. /**
  417. * Always show list columns in list view,
  418. * even if the attribute itself is hidden?
  419. *
  420. * @param bool $value always show list columns?
  421. * @return Adapto_Relation_ManyToOne The instance of this Adapto_Relation_ManyToOne
  422. */
  423. function setAlwaysShowListColumns($value)
  424. {
  425. $this->m_alwaysShowListColumns = $value;
  426. if ($this->m_alwaysShowListColumns)
  427. $this->addFlag(AF_FORCE_LOAD);
  428. return $this;
  429. }
  430. /**
  431. * Convert value to DataBase value
  432. * @param array $rec Record to convert
  433. * @return int Database safe value
  434. */
  435. function value2db($rec)
  436. {
  437. if ($this->isEmpty($rec)) {
  438. Adapto_Util_Debugger::debug($this->fieldName() . " IS EMPTY!");
  439. return NULL;
  440. } else {
  441. if ($this->createDestination()) {
  442. if (is_array($rec[$this->fieldName()])) {
  443. $pkfield = $this->m_destInstance->m_primaryKey[0];
  444. $pkattr = &$this->m_destInstance->getAttribute($pkfield);
  445. return $pkattr->value2db($rec[$this->fieldName()]);
  446. } else {
  447. return $rec[$this->fieldName()];
  448. }
  449. }
  450. }
  451. // This never happens, does it?
  452. return "";
  453. }
  454. /**
  455. * Fetch value out of record
  456. * @param array $postvars Postvars
  457. * @return decoded value
  458. */
  459. function fetchValue($postvars)
  460. {
  461. if ($this->isPosted($postvars)) {
  462. $result = array();
  463. // support specifying the value as a single number if the
  464. // destination's primary key consists of a single field
  465. if (is_numeric($postvars[$this->fieldName()])) {
  466. $result[$this->getDestination()->primaryKeyField()] = $postvars[$this->fieldName()];
  467. } else {
  468. // Split the primary key of the selected record into its
  469. // referential key elements.
  470. $keyelements = decodeKeyValueSet($postvars[$this->fieldName()]);
  471. foreach ($keyelements as $key => $value) {
  472. // Tablename must be stripped out because it is in the way..
  473. if (strpos($key, '.') > 0) {
  474. $field = substr($key, strrpos($key, '.') + 1);
  475. } else {
  476. $field = $key;
  477. }
  478. $result[$field] = $value;
  479. }
  480. }
  481. if (count($result) == 0) {
  482. return null;
  483. }
  484. // add descriptor fields, this means they can be shown in the title
  485. // bar etc. when updating failed for example
  486. $record = array($this->fieldName() => $result);
  487. $this->populate($record);
  488. $result = $record[$this->fieldName()];
  489. return $result;
  490. }
  491. return NULL;
  492. }
  493. /**
  494. * Converts DataBase value to normal value
  495. * @param array $rec Record
  496. * @return decoded value
  497. */
  498. function db2value($rec)
  499. {
  500. $this->createDestination();
  501. if (isset($rec[$this->fieldName()]) && is_array($rec[$this->fieldName()])
  502. && (!isset($rec[$this->fieldName()][$this->m_destInstance->primaryKeyField()])
  503. || empty($rec[$this->fieldName()][$this->m_destInstance->primaryKeyField()]))) {
  504. return NULL;
  505. }
  506. if (isset($rec[$this->fieldName()])) {
  507. $myrec = $rec[$this->fieldName()];
  508. if (is_array($myrec)) {
  509. $result = array();
  510. if ($this->createDestination()) {
  511. foreach (array_keys($this->m_destInstance->m_attribList) as $attrName) {
  512. $attr = &$this->m_destInstance->m_attribList[$attrName];
  513. $result[$attrName] = $attr->db2value($myrec);
  514. }
  515. }
  516. return $result;
  517. } else {
  518. // if the record is not an array, probably only the value of the primary key was loaded.
  519. // This workaround only works for single-field primary keys.
  520. if ($this->createDestination())
  521. return array($this->m_destInstance->primaryKeyField() => $myrec);
  522. }
  523. }
  524. }
  525. /**
  526. * Set none label.
  527. *
  528. * @param string $label The label to use for the "none" option
  529. */
  530. function setNoneLabel($label)
  531. {
  532. $this->m_noneLabel = $label;
  533. }
  534. /**
  535. * Get none label.
  536. *
  537. * @return String The label for the "none" option
  538. */
  539. function getNoneLabel()
  540. {
  541. if ($this->m_noneLabel !== NULL)
  542. return $this->m_noneLabel;
  543. $entityname = $this->m_destInstance->m_type;
  544. $modulename = $this->m_destInstance->m_module;
  545. $ownermodulename = $this->m_ownerInstance->m_module;
  546. $label = atktext($this->fieldName() . '_select_none', $ownermodulename, $this->m_owner, "", "", true);
  547. if ($label == "")
  548. $label = atktext('select_none', $modulename, $entityname);
  549. return $label;
  550. }
  551. /**
  552. * Returns a displayable string for this value.
  553. *
  554. * @param array $record The record that holds the value for this attribute
  555. * @param String $mode The display mode ("view" for viewpages, or "list"
  556. * for displaying in recordlists, "edit" for
  557. * displaying in editscreens, "add" for displaying in
  558. * add screens. "csv" for csv files. Applications can
  559. * use additional modes.
  560. * @return a displayable string
  561. */
  562. function display($record, $mode = 'list')
  563. {
  564. if ($this->createDestination()) {
  565. if (count($record[$this->fieldName()]) == count($this->m_refKey))
  566. $this->populate($record);
  567. if (!$this->isEmpty($record)) {
  568. $result = $this->m_destInstance->descriptor($record[$this->fieldName()]);
  569. if ($this->hasFlag(AF_RELATION_AUTOLINK) && (!in_array($mode, array("csv", "plain", "list")))) // create link to edit/view screen
  570. {
  571. if (($this->m_destInstance->allowed("view")) && !$this->m_destInstance->hasFlag(EF_NO_VIEW) && $result != "") {
  572. $saveForm = $mode == 'add' || $mode == 'edit';
  573. $result = href(
  574. dispatch_url($this->m_destination, "view",
  575. array(
  576. "atkselector" => $this->m_destInstance->primaryKey($record[$this->fieldName()]))), $result, SESSION_NESTED,
  577. $saveForm);
  578. }
  579. }
  580. } else {
  581. $result = (!in_array($mode, array("csv", "plain")) ? $this->getNoneLabel() : ''); // no record
  582. }
  583. return $result;
  584. } else {
  585. Adapto_Util_Debugger::debug("Can't create destination! ($this->m_destination");
  586. }
  587. return "";
  588. }
  589. /**
  590. * Populate the record with the destination record data.
  591. *
  592. * @param array $record record
  593. * @param mixed $fullOrFields load all data, only the given fields or only the descriptor fields?
  594. */
  595. public function populate(&$record, $fullOrFields = false)
  596. {
  597. if (!is_array($record) || $record[$this->fieldName()] == "")
  598. return;
  599. Adapto_Util_Debugger::debug("Delayed loading of " . ($fullOrFields || is_array($fullOrFields) ? "" : "descriptor ") . "fields for " . $this->m_name);
  600. $this->createDestination();
  601. $includes = "";
  602. if (is_array($fullOrFields)) {
  603. $includes = array_merge($this->m_destInstance->m_primaryKey, $fullOrFields);
  604. } else if (!$fullOrFields) {
  605. $includes = $this->m_destInstance->descriptorFields();
  606. }
  607. $result = $this->m_destInstance->select($this->m_destInstance->primaryKey($record[$this->fieldName()]))
  608. ->orderBy($this->m_destInstance->getColumnConfig()->getOrderByStatement())->includes($includes)->firstRow();
  609. if ($result != null) {
  610. $record[$this->fieldName()] = $result;
  611. }
  612. }
  613. /**
  614. * Creates HTML for the selection and auto links.
  615. *
  616. * @param string $id attribute id
  617. * @param array $record record
  618. * @return string
  619. */
  620. function createSelectAndAutoLinks($id, $record)
  621. {
  622. $links = array();
  623. $newsel = $id;
  624. $filter = $this->parseFilter($this->m_destinationFilter, $record);
  625. $links[] = $this->_getSelectLink($newsel, $filter);
  626. if ($this->hasFlag(AF_RELATION_AUTOLINK)) // auto edit/view link
  627. {
  628. if ($this->m_destInstance->allowed("add")) {
  629. $links[] = href(dispatch_url($this->getAutoLinkDestination(), "add", array("atkpkret" => $id, "atkfilter" => ($filter != "" ? $filter : ""))),
  630. atktext("new"), SESSION_NESTED, true);
  631. }
  632. if ($this->m_destInstance->allowed("edit") && $record[$this->fieldName()] != NULL) {
  633. //we laten nu altijd de edit link zien, maar eigenlijk mag dat niet, want
  634. //de app crasht als er geen waarde is ingevuld.
  635. $editUrl = session_url(dispatch_url($this->getAutoLinkDestination(), "edit", array("atkselector" => "REPLACEME")), SESSION_NESTED);
  636. $links[] = "<span id=\"" . $id . "_edit\" style=\"\"><a href='javascript:atkSubmit(mto_parse(\"" . atkurlencode($editUrl)
  637. . "\", document.entryform." . $id . ".value))'>" . atktext('edit') . "</a></span>";
  638. }
  639. }
  640. return implode("&nbsp;", $links);
  641. }
  642. /**
  643. * Set destination entity for the Autolink links (new/edit)
  644. *
  645. * @param string $entity
  646. */
  647. function setAutoLinkDestination($entity)
  648. {
  649. $this->m_autolink_destination = $entity;
  650. }
  651. /**
  652. * Get destination entity for the Autolink links (new/edit)
  653. *
  654. * @return string
  655. */
  656. function getAutoLinkDestination()
  657. {
  658. if (!empty($this->m_autolink_destination)) {
  659. return $this->m_autolink_destination;
  660. }
  661. return $this->m_destination;
  662. }
  663. /**
  664. * Prepare for editing, make sure we already have the selectable records
  665. * loaded and update the record with the possible selection of the first
  666. * record.
  667. *
  668. * @param array $record reference to the record
  669. * @param string $fieldPrefix field prefix
  670. * @param string $mode edit mode
  671. */
  672. public function preAddToEditArray(&$record, $fieldPrefix, $mode)
  673. {
  674. if ((!$this->hasFlag(AF_RELATION_AUTOCOMPLETE) && !$this->hasFlag(AF_LARGE)) || $this->m_autocomplete_minrecords > -1) {
  675. $this->m_selectableRecords = $this->_getSelectableRecords($record, 'select');
  676. if (count($this->m_selectableRecords) > 0 && !$this->getConfigOptionObligatoryNullOption()
  677. && (($this->hasFlag(AF_OBLIGATORY) && !$this->hasFlag(AF_MANYTOONE_OBLIGATORY_NULL_ITEM))
  678. || (!$this->hasFlag(AF_OBLIGATORY) && $this->hasFlag(AF_RELATION_NO_NULL_ITEM)))) {
  679. if (!isset($record[$this->fieldName()]) || !is_array($record[$this->fieldName()])) {
  680. $record[$this->fieldName()] = $this->m_selectableRecords[0];
  681. } else if (!$this->_isSelectableRecord($record, 'select')) {
  682. $record[$this->fieldName()] = $this->m_selectableRecords[0];
  683. } else {
  684. $current = $this->getDestination()->primaryKey($record[$this->fieldName()]);
  685. $record[$this->fieldName()] = null;
  686. foreach ($this->m_selectableRecords as $selectable) {
  687. if ($this->getDestination()->primaryKey($selectable) == $current) {
  688. $record[$this->fieldName()] = $selectable;
  689. break;
  690. }
  691. }
  692. }
  693. }
  694. } else if (is_array($record[$this->fieldName()]) && !$this->_isSelectableRecord($record, 'select')) {
  695. $record[$this->fieldName()] = null;
  696. } else if (is_array($record[$this->fieldName()])) {
  697. $this->populate($record);
  698. }
  699. }
  700. /**
  701. * Returns the configuration option called: list_obligatory_null_item, which is a
  702. * boolean.
  703. * @return boolean
  704. */
  705. public function getConfigOptionObligatoryNullOption()
  706. {
  707. return Adapto_Config::getGlobal("list_obligatory_null_item");
  708. }
  709. /**
  710. * Returns a piece of html code that can be used in a form to edit this
  711. * attribute's value.
  712. * @param array $record The record that holds the value for this attribute.
  713. * @param String $fieldprefix The fieldprefix to put in front of the name
  714. * of any html form element for this attribute.
  715. * @param String $mode The mode we're in ('add' or 'edit')
  716. * @return Piece of html code that can be used in a form to edit this
  717. */
  718. function edit($record, $fieldprefix = "", $mode = "edit")
  719. {
  720. if (!$this->createDestination()) {
  721. return throw new Adapto_Exception("Could not create destination for destination: $this->m_destination!");
  722. }
  723. $recordset = $this->m_selectableRecords;
  724. // load records for bwc
  725. if ($recordset === null && $this->hasFlag(AF_RELATION_AUTOCOMPLETE) && $this->m_autocomplete_minrecords > -1) {
  726. $recordset = $this->_getSelectableRecords($record, 'select');
  727. }
  728. if ($this->hasFlag(AF_RELATION_AUTOCOMPLETE) && (is_object($this->m_ownerInstance))
  729. && ((is_array($recordset) && count($recordset) > $this->m_autocomplete_minrecords) || $this->m_autocomplete_minrecords == -1)) {
  730. return $this->drawAutoCompleteBox($record, $fieldprefix, $mode);
  731. }
  732. $id = $fieldprefix . $this->fieldName();
  733. $filter = $this->parseFilter($this->m_destinationFilter, $record);
  734. $autolink = $this->getRelationAutolink($id, $filter);
  735. $editflag = true;
  736. $value = isset($record[$this->fieldName()]) ? $record[$this->fieldName()] : null;
  737. $currentPk = $value != null ? $this->getDestination()->primaryKey($value) : null;
  738. if (!$this->hasFlag(AF_LARGE)) // normal dropdown..
  739. {
  740. // load records for bwc
  741. if ($recordset == null) {
  742. $recordset = $this->_getSelectableRecords($record, 'select');
  743. }
  744. if (count($recordset) == 0) {
  745. $editflag = false;
  746. }
  747. $onchange = '';
  748. if (count($this->m_onchangecode)) {
  749. $onchange = 'onChange="' . $id . '_onChange(this);"';
  750. $this->_renderChangeHandler($fieldprefix);
  751. }
  752. // autoselect if there is only one record (if obligatory is not set,
  753. // we don't autoselect, since user may wist to select 'none' instead
  754. // of the 1 record.
  755. $result = '';
  756. if (count($recordset) == 0) {
  757. $result = $this->getNoneLabel();
  758. } else {
  759. $this->registerKeyListener($id, KB_CTRLCURSOR | KB_LEFTRIGHT);
  760. $this->registerJavaScriptObservers($id);
  761. $result = '<select id="' . $id . '" name="' . $id . '" class="atkmanytoonerelation" ' . $onchange . '>';
  762. // relation may be empty, so we must provide an empty selectable..
  763. if ($this->hasFlag(AF_MANYTOONE_OBLIGATORY_NULL_ITEM) || (!$this->hasFlag(AF_OBLIGATORY) && !$this->hasFlag(AF_RELATION_NO_NULL_ITEM))
  764. || ($this->getConfigOptionObligatoryNullOption() && !is_array($value))) {
  765. $result .= '<option value="">' . $this->getNoneLabel() . '</option>';
  766. }
  767. foreach ($recordset as $selectable) {
  768. $pk = $this->getDestination()->primaryKey($selectable);
  769. $sel = $pk == $currentPk ? 'selected="selected"' : '';
  770. $result .= '<option value="' . $pk . '" ' . $sel . '>'
  771. . str_replace(' ', '&nbsp;', Adapto_htmlentities(strip_tags($this->m_destInstance->descriptor($selectable)))) . '</option>';
  772. }
  773. $result .= '</select>';
  774. }
  775. } else {
  776. $destrecord = $record[$this->fieldName()];
  777. if (is_array($destrecord)) {
  778. $result .= '<span id="' . $id . '_current" >' . $this->m_destInstance->descriptor($destrecord) . "&nbsp;&nbsp;";
  779. if (!$this->hasFlag(AF_OBLIGATORY)) {
  780. $result .= '<a href="#" onClick="document.getElementById(\'' . $id . '\').value=\'\'; document.getElementById(\'' . $id
  781. . '_current\').style.display=\'none\'">' . atktext("unselect") . '</a>&nbsp;&nbsp;';
  782. }
  783. $result .= '</span>';
  784. }
  785. $result .= $this->hide($record, $fieldprefix);
  786. $result .= $this->_getSelectLink($id, $filter);
  787. }
  788. $result .= $editflag && isset($autolink['edit']) ? $autolink['edit'] : "";
  789. $result .= isset($autolink['add']) ? $autolink['add'] : "";
  790. return $result;
  791. }
  792. /**
  793. * Get the select link to select the value using a select action on the destination entity
  794. *
  795. * @param string $selname
  796. * @param string $filter
  797. * @return String HTML-code with the select link
  798. */
  799. function _getSelectLink($selname, $filter)
  800. {
  801. $result = "";
  802. // we use the current level to automatically return to this page
  803. // when we come from the select..
  804. $atktarget = atkurlencode(getDispatchFile() . "?atklevel=" . atkLevel() . "&" . $selname . "=[atkprimkey]");
  805. $linkname = atktext("link_select_" . getEntityType($this->m_destination), $this->getOwnerInstance()->getModule(), $this->getOwnerInstance()->getType(),
  806. '', '', true);
  807. if (!$linkname)
  808. $linkname = atktext("link_select_" . getEntityType($this->m_destination), getEntityModule($this->m_destination), getEntityType($this->m_destination), '',
  809. '', true);
  810. if (!$linkname)
  811. $linkname = atktext("select_a") . ' '
  812. . strtolower(atktext(getEntityType($this->m_destination), getEntityModule($this->m_destination), getEntityType($this->m_destination)));
  813. if ($this->m_destinationFilter != "") {
  814. $result .= href(dispatch_url($this->m_destination, "select", array("atkfilter" => $filter, "atktarget" => $atktarget)), $linkname, SESSION_NESTED,
  815. $this->m_autocomplete_saveform, 'class="atkmanytoonerelation"');
  816. } else {
  817. $result .= href(dispatch_url($this->m_destination, "select", array("atktarget" => $atktarget)), $linkname, SESSION_NESTED,
  818. $this->m_autocomplete_saveform, 'class="atkmanytoonerelation"');
  819. }
  820. return $result;
  821. }
  822. /**
  823. * Creates and returns the auto edit/view links
  824. * @param String $id The field id
  825. * @param String $filter Filter that we want to apply on the destination entity
  826. * @return array The HTML code for the autolink links
  827. */
  828. function getRelationAutolink($id, $filter)
  829. {
  830. $autolink = array();
  831. if ($this->hasFlag(AF_RELATION_AUTOLINK)) // auto edit/view link
  832. {
  833. $page = &atkPage::getInstance();
  834. $page->register_script(Adapto_Config::getGlobal("atkroot") . "atk/javascript/class.atkmanytoonerelation.js");
  835. if ($this->m_destInstance->allowed("edit")) {
  836. $editlink = session_url(dispatch_url($this->getAutoLinkDestination(), "edit", array("atkselector" => "REPLACEME")), SESSION_NESTED);
  837. $autolink['edit'] = "&nbsp;<a href='javascript:atkSubmit(mto_parse(\"" . atkurlencode($editlink) . "\", document.entryform." . $id
  838. . ".value))'>" . atktext('edit') . "</a>";
  839. }
  840. if ($this->m_destInstance->allowed("add")) {
  841. $autolink['add'] = "&nbsp;"
  842. . href(
  843. dispatch_url($this->getAutoLinkDestination(), "add",
  844. array("atkpkret" => $id, "atkfilter" => ($this->m_useFilterForAddLink && $filter != "" ? $filter : ""))),
  845. atktext("new"), SESSION_NESTED, true);
  846. }
  847. }
  848. return $autolink;
  849. }
  850. /**
  851. * Returns a piece of html code for hiding this attribute in an HTML form,
  852. * while still posting its value. (<input type="hidden">)
  853. *
  854. * @param array $record The record that holds the value for this attribute
  855. * @param String $fieldprefix The fieldprefix to put in front of the name
  856. * of any html form element for this attribute.
  857. * @return String A piece of htmlcode with hidden form elements that post
  858. * this attribute's value without showing it.
  859. */
  860. function hide($record = "", $fieldprefix = "")
  861. {
  862. if (!$this->createDestination())
  863. return '';
  864. $currentPk = "";
  865. if (isset($record[$this->fieldName()]) && $record[$this->fieldName()] != null) {
  866. $this->fixDestinationRecord($record);
  867. $currentPk = $this->m_destInstance->primaryKey($record[$this->fieldName()]);
  868. }
  869. $result = '<input type="hidden" id="' . $fieldprefix . $this->formName() . '"
  870. name="' . $fieldprefix . $this->formName() . '"
  871. value="' . $currentPk . '">';
  872. return $result;
  873. }
  874. /**
  875. * Support for destination "records" where only the id is set and the
  876. * record itself isn't converted to a real record (array) yet
  877. *
  878. * @param array $record The record to fix
  879. */
  880. function fixDestinationRecord(&$record)
  881. {
  882. if ($this->createDestination() && isset($record[$this->fieldName()]) && $record[$this->fieldName()] != null && !is_array($record[$this->fieldName()])) {
  883. $record[$this->fieldName()] = array($this->m_destInstance->primaryKeyField() => $record[$this->fieldName()]);
  884. }
  885. }
  886. /**
  887. * Retrieve the html code for placing this attribute in an edit page.
  888. *
  889. * The difference with the edit() method is that the edit() method just
  890. * generates the HTML code for editing the attribute, while the getEdit()
  891. * method is 'smart', and implements a hide/readonly policy based on
  892. * flags and/or custom override methodes in the entity.
  893. * (<attributename>_edit() and <attributename>_display() methods)
  894. *
  895. * Framework method, it should not be necessary to call this method
  896. * directly.
  897. *
  898. * @param String $mode The edit mode ("add" or "edit")
  899. * @param array $record The record holding the values for this attribute
  900. * @param String $fieldprefix The fieldprefix to put in front of the name
  901. * of any html form element for this attribute.
  902. * @return String the HTML code for this attribute that can be used in an
  903. * editpage.
  904. */
  905. function getEdit($mode, &$record, $fieldprefix)
  906. {
  907. $this->fixDestinationRecord($record);
  908. return parent::getEdit($mode, $record, $fieldprefix);
  909. }
  910. /**
  911. * Converts a record filter to a record array.
  912. *
  913. * @param string $filter filter string
  914. * @return array record
  915. */
  916. protected function filterToArray($filter)
  917. {
  918. $result = array();
  919. $values = decodeKeyValueSet($filter);
  920. foreach ($values as $field => $value) {
  921. $parts = explode('.', $field);
  922. $ref = &$result;
  923. foreach ($parts as $part) {
  924. $ref = &$ref[$part];
  925. }
  926. $ref = $value;
  927. }
  928. return $result;
  929. }
  930. /**
  931. * Returns a piece of html code that can be used to get search terms input
  932. * from the user.
  933. *
  934. * @param array $record Array with values
  935. * @param boolean $extended if set to false, a simple search input is
  936. * returned for use in the searchbar of the
  937. * recordlist. If set to true, a more extended
  938. * search may be returned for the 'extended'
  939. * search page. The atkAttribute does not
  940. * make a difference for $extended is true, but
  941. * derived attributes may reimplement this.
  942. * @param string $fieldprefix The fieldprefix of this attribute's HTML element.
  943. * @param atkDataGrid $grid The datagrid
  944. *
  945. * @return String A piece of html-code
  946. */
  947. function search($record = array(), $extended = false, $fieldprefix = "", atkDataGrid $grid = null)
  948. {
  949. $useautocompletion = Adapto_Config::getGlobal("manytoone_search_autocomplete", true) && $this->hasFlag(AF_RELATION_AUTOCOMPLETE);
  950. if (!$this->hasFlag(AF_LARGE) && !$useautocompletion) {
  951. if ($this->createDestination()) {
  952. if ($this->m_destinationFilter != "") {
  953. $filterRecord = array();
  954. if ($grid != null) {
  955. foreach ($grid->getFilters() as $filter) {
  956. $filter = $filter['filter'];
  957. $arr = $this->filterToArray($filter);
  958. $arr = (isset($arr[$this->getOwnerInstance()->m_table]) && is_array($arr[$this->getOwnerInstance()->m_table])) ? $arr[$this
  959. ->getOwnerInstance()->m_table] : array();
  960. foreach ($arr as $attrName => $value) {
  961. $attr = $this->getOwnerInstance()->getAttribute($attrName);
  962. if (!is_array($value) && is_a($attr, 'Adapto_Relation_ManyToOne') && count($attr->m_refKey) == 1) {
  963. $attr->createDestination();
  964. $arr[$attrName] = array($attr->getDestination()->primaryKeyField() => $value);
  965. }
  966. }
  967. $filterRecord = array_merge($filterRecord, $arr);
  968. }
  969. }
  970. $record = array_merge($filterRecord, is_array($record) ? $record : array());
  971. }
  972. $recordset = $this->_getSelectableRecords($record, 'search');
  973. $result = '<select class="' . get_class($this) . '" ';
  974. if ($extended) {
  975. $result .= 'multiple size="' . min(5, count($recordset) + 1) . '"';
  976. if (isset($record[$this->fieldName()][$this->fieldName()]))
  977. $record[$this->fieldName()] = $record[$this->fieldName()][$this->fieldName()];
  978. }
  979. // if we use autosearch, register an onchange event that submits the grid
  980. if (!is_null($grid) && !$extended && $this->m_autoSearch) {
  981. $id = $this->getSearchFieldName($fieldprefix);
  982. $result .= ' id="' . $id . '" ';
  983. $code = '$(\'' . $id . '\').observe(\'change\', function(event) { '
  984. . $grid->getUpdateCall(array('atkstartat' => 0), array(), 'ATK.DataGrid.extractSearchOverrides') . ' return false; });';
  985. $this->getOwnerInstance()->getPage()->register_loadscript($code);
  986. }
  987. $result .= 'name="' . $this->getSearchFieldName($fieldprefix) . '[]">';
  988. $pkfield = $this->m_destInstance->primaryKeyField();
  989. $result .= '<option value="">' . atktext('search_all') . '</option>';
  990. if (!$this->hasFlag(AF_OBLIGATORY))
  991. $result .= '<option value="__NONE__"'
  992. . (isset($record[$this->fieldName()]) && Adapto_in_array('__NONE__', $record[$this->fieldName()]) ? ' selected="selected"' : '')
  993. . '>' . atktext('search_none') . '</option>';
  994. for ($i = 0; $i < count($recordset); $i++) {
  995. $pk = $recordset[$i][$pkfield];
  996. if (is_array($record) && isset($record[$this->fieldName()]) && Adapto_in_array($pk, $record[$this->fieldName()]))
  997. $sel = "selected";
  998. else
  999. $sel = "";
  1000. $result .= '<option value="' . $pk . '" ' . $sel . '>'
  1001. . str_replace(' ', '&nbsp;', Adapto_htmlentities(strip_tags($this->m_destInstance->descriptor($recordset[$i])))) . '</option>';
  1002. }
  1003. $result .= '</select>';
  1004. return $result;
  1005. }
  1006. return "";
  1007. } else {
  1008. $id = $this->getSearchFieldName($fieldprefix);
  1009. if (is_array($record[$this->fieldName()]) && isset($record[$this->fieldName()][$this->fieldName()]))
  1010. $record[$this->fieldName()] = $record[$this->fieldName()][$this->fieldName()];
  1011. $this->registerKeyListener($id, KB_CTRLCURSOR | KB_UPDOWN);
  1012. $result = '<input type="text" id="' . $id . '" class="' . get_class($this) . '" name="' . $id . '" value="' . $record[$this->fieldName()] . '"'
  1013. . ($useautocompletion ? ' onchange=""' : '') . ($this->m_searchsize > 0 ? ' size="' . $this->m_searchsize . '"' : '')
  1014. . ($this->m_maxsize > 0 ? ' maxlength="' . $this->m_maxsize . '"' : '') . '>';
  1015. if ($useautocompletion) {
  1016. $page = &$this->m_ownerInstance->getPage();
  1017. $url = partial_url($this->m_ownerInstance->atkEntityType(), $this->m_ownerInstance->m_action,
  1018. 'attribute.' . $this->fieldName() . '.autocomplete_search');
  1019. $code = "Adapto.ManyToOneRelation.completeSearch('{$id}', '{$id}_result', '{$url}', {$this->m_autocomplete_minchars});";
  1020. $this->m_ownerInstance->addStyle("atkmanytoonerelation.css");
  1021. $page->register_script(Adapto_Config::getGlobal('atkroot') . 'atk/javascript/class.atkmanytoonerelation.js');
  1022. $page->register_loadscript($code);
  1023. $result .= '<div id="' . $id . '_result" style="display: none" class="atkmanytoonerelation_result"></div>';
  1024. }
  1025. return $result;
  1026. }
  1027. }
  1028. /**
  1029. * Retrieve the list of searchmodes supported by the attribute.
  1030. *
  1031. * Note that not all modes may be supported by the database driver.
  1032. * Compare this list to the one returned by the databasedriver, to
  1033. * determine which searchmodes may be used.
  1034. *
  1035. * @return array List of supported searchmodes
  1036. */
  1037. function getSearchModes()
  1038. {
  1039. if ($this->hasFlag(AF_LARGE) || $this->hasFlag(AF_MANYTOONE_AUTOCOMPLETE)) {
  1040. return array("substring", "exact", "wildcard", "regex");
  1041. }
  1042. return array("exact"); // only support exact search when searching with dropdowns
  1043. }
  1044. /**
  1045. * Creates a smart search condition for a given search value, and adds it
  1046. * to the query that will be used for performing the actual search.
  1047. *
  1048. * @param Integer $id The unique smart search criterium identifier.
  1049. * @param Integer $nr The element number in the path.
  1050. * @param Array $path The remaining attribute path.
  1051. * @param atkQuery $query The query to which the condition will be added.
  1052. * @param String $ownerAlias The owner table alias to use.
  1053. * @param Mixed $value The value the user has entered in the searchbox.
  1054. * @param String $mode The searchmode to use.
  1055. */
  1056. function smartSearchCondition($id, $nr, $path, &$query, $ownerAlias, $value, $mode)
  1057. {
  1058. if (count($path) > 0) {
  1059. $this->createDestination();
  1060. $destAlias = "ss_{$id}_{$nr}_" . $this->fieldName();
  1061. $query->addJoin($this->m_destInstance->m_table, $destAlias, $this->getJoinCondition($query, $ownerAlias, $destAlias), false);
  1062. $attrName = array_shift($path);
  1063. $attr = &$this->m_destInstance->getAttribute($attrName);
  1064. if (is_object($attr)) {
  1065. $attr->smartSearchCondition($id, $nr + 1, $path, $query, $destAlias, $value, $mode);
  1066. }
  1067. } else {
  1068. $this->searchCondition($query, $ownerAlias, $value, $mode);
  1069. }
  1070. }
  1071. /**
  1072. * Creates a searchcondition for the field,
  1073. * was once part of searchCondition, however,
  1074. * searchcondition() also immediately adds the search condition.
  1075. *
  1076. * @param atkQuery $query The query object where the search condition should be placed on
  1077. * @param String $table The name of the table in which this attribute
  1078. * is stored
  1079. * @param mixed $value The value the user has entered in the searchbox
  1080. * @param String $searchmode The searchmode to use. This can be any one
  1081. * of the supported modes, as returned by this
  1082. * attribute's getSearchModes() method.
  1083. * @param string $fieldaliasprefix The prefix for the field
  1084. * @return String The searchcondition to use.
  1085. */
  1086. function getSearchCondition(&$query, $table, $value, $searchmode, $fieldaliasprefix = '')
  1087. {
  1088. if (!$this->createDestination())
  1089. return;
  1090. if (is_array($value)) {
  1091. foreach ($this->m_listColumns as $attr) {
  1092. $attrValue = $value[$attr];
  1093. if (!empty($attrValue)) {
  1094. $p_attrib = &$this->m_destInstance->m_attribList[$attr];
  1095. if (!$p_attrib == NULL) {
  1096. $p_attrib->searchCondition($query, $this->fieldName(), $attrValue, $this->getChildSearchMode($searchmode, $p_attrib->formName()));
  1097. }
  1098. }
  1099. }
  1100. if (isset($value[$this->fieldName()])) {
  1101. $value = $value[$this->fieldName()];
  1102. }
  1103. }
  1104. if (empty($value)) {
  1105. return '';
  1106. } else if (!$this->hasFlag(AF_LARGE) && !$this->hasFlag(AF_RELATION_AUTOCOMPLETE)) {
  1107. // We only support 'exact' matches.
  1108. // But you can select more than one value, which we search using the IN() statement,
  1109. // which should work in any ansi compatible database.
  1110. if (!is_array($value)) // This last condition is for when the user selected the 'search all' option, in which case, we don't add conditions at all.
  1111. {
  1112. $value = array($value);
  1113. }
  1114. if (count($value) == 1) // exactly one value
  1115. {
  1116. if ($value[0] == "__NONE__") {
  1117. return $query->nullCondition($table . "." . $this->fieldName(), true);
  1118. } elseif ($value[0] != "") {
  1119. return $query->exactCondition($table . "." . $this->fieldName(), $this->escapeSQL($value[0]));
  1120. }
  1121. } else // search for more values using IN()
  1122. {
  1123. return $table . "." . $this->fieldName() . " IN ('" . implode("','", $value) . "')";
  1124. }
  1125. } else // AF_LARGE || AF_RELATION_AUTOCOMPLETE
  1126. {
  1127. // If we have a descriptor with multiple fields, use CONCAT
  1128. $attribs = $this->m_destInstance->descriptorFields();
  1129. $alias = $fieldaliasprefix . $this->fieldName();
  1130. if (count($attribs) > 1) {
  1131. $searchcondition = $this->getConcatFilter($value, $alias);
  1132. } else {
  1133. // ask the destination entity for it's search condition
  1134. $searchcondition = $this->m_destInstance
  1135. ->getSearchCondition($query, $alias, $fieldaliasprefix, $value, $this->getChildSearchMode($searchmode, $this->formName()));
  1136. }
  1137. return $searchcondition;
  1138. }
  1139. }
  1140. /**
  1141. * Adds this attribute to database quer…

Large files files are truncated, but you can click here to view the full file