PageRenderTime 63ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/relations/class.atkmanytoonerelation.inc

https://github.com/ibuildingsnl/ATK
PHP | 2483 lines | 1458 code | 242 blank | 783 comment | 257 complexity | c0bf5b202591ec9c534a0eba89cd1723 MD5 | raw file
Possible License(s): LGPL-2.0, LGPL-2.1, MPL-2.0-no-copyleft-exception, LGPL-3.0
  1. <?php
  2. /**
  3. * This file is part of the Achievo ATK distribution.
  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 atk
  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. * @version $Revision: 6309 $
  16. * $Id$
  17. */
  18. /**
  19. * Create edit/view links for the items in a manytoonerelation dropdown.
  20. */
  21. define("AF_RELATION_AUTOLINK", AF_SPECIFIC_1);
  22. /**
  23. * Create edit/view links for the items in a manytoonerelation dropdown.
  24. */
  25. define("AF_MANYTOONE_AUTOLINK", AF_RELATION_AUTOLINK);
  26. /**
  27. * Do not add null option under any circumstance
  28. */
  29. define("AF_RELATION_NO_NULL_ITEM", AF_SPECIFIC_2);
  30. /**
  31. * Do not add null option ever
  32. */
  33. define("AF_MANYTOONE_NO_NULL_ITEM", AF_RELATION_NO_NULL_ITEM);
  34. /**
  35. * Use auto-completition instead of drop-down / selection page
  36. */
  37. define("AF_RELATION_AUTOCOMPLETE", AF_SPECIFIC_3);
  38. /**
  39. * Use auto-completition instead of drop-down / selection page
  40. */
  41. define("AF_MANYTOONE_AUTOCOMPLETE", AF_RELATION_AUTOCOMPLETE);
  42. /**
  43. * Lazy load
  44. */
  45. define("AF_MANYTOONE_LAZY", AF_SPECIFIC_4);
  46. /**
  47. * Add a default null option to obligatory relations
  48. */
  49. define("AF_MANYTOONE_OBLIGATORY_NULL_ITEM", AF_SPECIFIC_5);
  50. /**
  51. * @internal include base class
  52. */
  53. userelation("atkrelation");
  54. /**
  55. * A N:1 relation between two classes.
  56. *
  57. * For example, projects all have one coordinator, but one
  58. * coordinator can have multiple projects. So in the project
  59. * class, there's a ManyToOneRelation to a coordinator.
  60. *
  61. * This relation essentially creates a dropdown box, from which
  62. * you can select from a set of records.
  63. *
  64. * @author Ivo Jansch <ivo@achievo.org>
  65. * @package atk
  66. * @subpackage relations
  67. *
  68. */
  69. class atkManyToOneRelation extends atkRelation
  70. {
  71. const SEARCH_MODE_EXACT = "exact";
  72. const SEARCH_MODE_STARTSWITH = "startswith";
  73. const SEARCH_MODE_CONTAINS = "contains";
  74. /**
  75. * By default, we do a left join. this means that records that don't have
  76. * a record in this relation, will be displayed anyway. NOTE: set this to
  77. * false only if you know what you're doing. When in doubt, 'true' is
  78. * usually the best option.
  79. * @var boolean
  80. */
  81. var $m_leftjoin = true;
  82. /**
  83. * The array of referential key fields.
  84. * @access private
  85. * @var array
  86. */
  87. var $m_refKey = array();
  88. /**
  89. * SQL statement with extra filter for the join that retrieves the
  90. * selected record.
  91. * @var String
  92. */
  93. var $m_joinFilter = "";
  94. /**
  95. * Hide the relation when there are no records to select.
  96. * @access private
  97. * @var boolean
  98. */
  99. var $m_hidewhenempty = false;
  100. /**
  101. * List columns.
  102. * @access private
  103. * @var Array
  104. */
  105. var $m_listColumns = array();
  106. /**
  107. * Always show list columns?
  108. * @access private
  109. * @var boolean
  110. */
  111. var $m_alwaysShowListColumns = false;
  112. /**
  113. * Label to use for the 'none' option.
  114. *
  115. * @access private
  116. * @var String
  117. */
  118. var $m_noneLabel = NULL;
  119. /**
  120. * Minimum number of character a user needs to enter before auto-completion kicks in.
  121. *
  122. * @access private
  123. * @var int
  124. */
  125. var $m_autocomplete_minchars = 2;
  126. /**
  127. * An array with the fieldnames of the destination node in which the autocompletion must search
  128. * for results.
  129. *
  130. * @access private
  131. * @var array
  132. */
  133. var $m_autocomplete_searchfields = "";
  134. /**
  135. * The search mode of the autocomplete fields. Can be 'startswith', 'exact' or 'contains'.
  136. *
  137. * @access private
  138. * @var String
  139. */
  140. var $m_autocomplete_searchmode = "contains";
  141. /**
  142. * Value determines wether the search of the autocompletion is case-sensitive.
  143. *
  144. * @var boolean
  145. */
  146. var $m_autocomplete_search_case_sensitive = false;
  147. /**
  148. * Value determines if select link for autocomplete should use atkSubmit or not (for use in admin screen for example)
  149. *
  150. * @var boolean
  151. */
  152. var $m_autocomplete_saveform = true;
  153. /**
  154. * Set the minimal number of records for showing the automcomplete. If there are less records
  155. * the normal dropdown is shown
  156. *
  157. * @access private
  158. * @var integer
  159. */
  160. var $m_autocomplete_minrecords = -1;
  161. /**
  162. * Destination node for auto links (edit, new)
  163. *
  164. * @var string
  165. */
  166. protected $m_autolink_destination = "";
  167. // override onchangehandler init
  168. var $m_onchangehandler_init = "newvalue = el.options[el.selectedIndex].value;\n";
  169. /**
  170. * Use destination filter for autolink add link?
  171. *
  172. * @access private
  173. * @var boolean
  174. */
  175. var $m_useFilterForAddLink = true;
  176. /**
  177. * Set a function to use for determining the descriptor in the getConcatFilter function
  178. *
  179. * @access private
  180. * @var string
  181. */
  182. var $m_concatDescriptorFunction = '';
  183. /**
  184. * When autosearch is set to true, this attribute will automatically submit
  185. * the search form onchange. This will only happen in the admin action.
  186. *
  187. * @var boolean
  188. */
  189. protected $m_autoSearch = false;
  190. /**
  191. * Selectable records for edit mode.
  192. *
  193. * @see atkManyToOneRelation::preAddToEditArray
  194. *
  195. * @var array
  196. */
  197. protected $m_selectableRecords = null;
  198. /**
  199. * Constructor.
  200. * @param String $name The name of the attribute. This is the name of the
  201. * field that is the referential key to the
  202. * destination.
  203. * For relations with more than one field in the
  204. * foreign key, you should pass an array of
  205. * referential key fields. The order of the fields
  206. * must match the order of the primary key attributes
  207. * in the destination node.
  208. * @param String $destination The node we have a relationship with.
  209. * @param int $flags Flags for the relation
  210. */
  211. function atkManyToOneRelation($name, $destination, $flags=0)
  212. {
  213. if (atkconfig("manytoone_autocomplete_default", false))
  214. $flags |= AF_RELATION_AUTOCOMPLETE;
  215. if (atkconfig("manytoone_autocomplete_large", true) && hasFlag($flags, AF_LARGE))
  216. $flags |= AF_RELATION_AUTOCOMPLETE;
  217. $this->m_autocomplete_minchars = atkconfig("manytoone_autocomplete_minchars", 2);
  218. $this->m_autocomplete_searchmode = atkconfig("manytoone_autocomplete_searchmode", "contains");
  219. $this->m_autocomplete_search_case_sensitive = atkconfig("manytoone_autocomplete_search_case_sensitive", false);
  220. if (is_array($name))
  221. {
  222. $this->m_refKey = $name;
  223. // ATK can't handle an array as name, so we initialize the
  224. // underlying attribute with the first name of the referential
  225. // keys.
  226. // Languagefiles, overrides, etc should use this first name to
  227. // override the relation.
  228. $this->atkRelation($name[0], $destination, $flags);
  229. }
  230. else
  231. {
  232. $this->m_refKey[] = $name;
  233. $this->atkRelation($name, $destination, $flags);
  234. }
  235. if ($this->hasFlag(AF_MANYTOONE_LAZY) && (count($this->m_refKey) > 1 || $this->m_refKey[0] != $this->fieldName()))
  236. {
  237. atkerror("AF_MANYTOONE_LAZY flag is not supported for multi-column reference key or a reference key that uses another column.");
  238. }
  239. }
  240. /**
  241. * Adds a flag to the manyToOne relation
  242. * Note that adding flags at any time after the constructor might not
  243. * always work. There are flags that are processed only at
  244. * constructor time.
  245. *
  246. * @param int $flag The flag to add to the attribute
  247. * @return atkManyToOneRelation The instance of this atkManyToOneRelation
  248. */
  249. function addFlag($flag)
  250. {
  251. parent::addFlag($flag);
  252. if (atkconfig("manytoone_autocomplete_large", true) && hasFlag($flag, AF_LARGE))
  253. $this->m_flags |= AF_RELATION_AUTOCOMPLETE;
  254. return $this;
  255. }
  256. /**
  257. * When autosearch is set to true, this attribute will automatically submit
  258. * the search form onchange. This will only happen in the admin action.
  259. * @param bool $auto
  260. * @return void
  261. */
  262. public function setAutoSearch($auto = false)
  263. {
  264. $this->m_autoSearch = $auto;
  265. }
  266. /**
  267. * Set join filter.
  268. *
  269. * @param string $filter join filter
  270. */
  271. function setJoinFilter($filter)
  272. {
  273. $this->m_joinFilter = $filter;
  274. }
  275. /**
  276. * Set the searchfields for the autocompletion.
  277. *
  278. * @param array $searchfields
  279. */
  280. function setAutoCompleteSearchFields($searchfields)
  281. {
  282. $this->m_autocomplete_searchfields = $searchfields;
  283. }
  284. /**
  285. * Set the searchmode for the autocompletion:
  286. * exact, startswith(default) or contains.
  287. *
  288. * @param array $mode
  289. */
  290. function setAutoCompleteSearchMode($mode)
  291. {
  292. $this->m_autocomplete_searchmode = $mode;
  293. }
  294. /**
  295. * Set the case-sensitivity for the autocompletion search (true or false).
  296. *
  297. * @param array $case_sensitive
  298. */
  299. function setAutoCompleteCaseSensitive($case_sensitive)
  300. {
  301. $this->m_autocomplete_search_case_sensitive = $case_sensitive;
  302. }
  303. /**
  304. * Sets the minimum number of characters before auto-completion kicks in.
  305. *
  306. * @param int $chars
  307. */
  308. function setAutoCompleteMinChars($chars)
  309. {
  310. $this->m_autocomplete_minchars = $chars;
  311. }
  312. /**
  313. * Set if the select link should save form (atkSubmit) or not (for use in admin screen for example)
  314. *
  315. * @param boolean $saveform
  316. */
  317. function setAutoCompleteSaveForm($saveform=true)
  318. {
  319. $this->m_autocomplete_saveform = $saveform;
  320. }
  321. /**
  322. * Set the minimal number of records for the autocomplete to show
  323. * If there are less records the normal dropdown is shown
  324. *
  325. * @param integer $minrecords
  326. */
  327. function setAutoCompleteMinRecords($minrecords)
  328. {
  329. $this->m_autocomplete_minrecords = $minrecords;
  330. }
  331. /**
  332. * Use destination filter for auto add link?
  333. *
  334. * @param boolean $useFilter use destnation filter for add link?
  335. */
  336. function setUseFilterForAddLink($useFilter)
  337. {
  338. $this->m_useFilterForAddLink = $useFilter;
  339. }
  340. /**
  341. * Set the function for determining the descriptor in the getConcatFilter function
  342. * This function should be implemented in the destination node
  343. *
  344. * @param string $function
  345. */
  346. function setConcatDescriptorFunction($function)
  347. {
  348. $this->m_concatDescriptorFunction = $function;
  349. }
  350. /**
  351. * Return the function for determining the descriptor in the getConcatFilter function
  352. *
  353. * @return string
  354. */
  355. function getConcatDescriptorFunction()
  356. {
  357. return $this->m_concatDescriptorFunction;
  358. }
  359. /**
  360. * Add list column. An attribute of the destination node
  361. * that (only) will be displayed in the recordlist.
  362. *
  363. * @param string $attr The attribute to add to the listcolumn
  364. * @return atkManyToOneRelation The instance of this atkManyToOneRelation
  365. */
  366. function addListColumn($attr)
  367. {
  368. $this->m_listColumns[] = $attr;
  369. return $this;
  370. }
  371. /**
  372. * Add multiple list columns. Attributes of the destination node
  373. * that (only) will be displayed in the recordlist.
  374. * @return atkManyToOneRelation The instance of this atkManyToOneRelation
  375. */
  376. function addListColumns()
  377. {
  378. $attrs = func_get_args();
  379. foreach ($attrs as $attr)
  380. $this->m_listColumns[] = $attr;
  381. return $this;
  382. }
  383. public function getListColumns()
  384. {
  385. return $this->m_listColumns;
  386. }
  387. /**
  388. * Reset the list columns and add multiple list columns. Attributes of the
  389. * destination node that (only) will be displayed in the recordlist.
  390. * @return atkManyToOneRelation The instance of this atkManyToOneRelation
  391. */
  392. public function setListColumns()
  393. {
  394. $this->m_listColumns = array();
  395. $attrs = func_get_args();
  396. if (count($attrs)===1 && is_array($attrs[0]))
  397. {
  398. $columns = $attrs[0];
  399. }
  400. else
  401. {
  402. $columns = $attrs;
  403. }
  404. foreach ($columns as $column)
  405. {
  406. $this->m_listColumns[] = $column;
  407. }
  408. return $this;
  409. }
  410. /**
  411. * Always show list columns in list view,
  412. * even if the attribute itself is hidden?
  413. *
  414. * @param bool $value always show list columns?
  415. * @return atkManyToOneRelation The instance of this atkManyToOneRelation
  416. */
  417. function setAlwaysShowListColumns($value)
  418. {
  419. $this->m_alwaysShowListColumns = $value;
  420. if($this->m_alwaysShowListColumns)
  421. $this->addFlag(AF_FORCE_LOAD);
  422. return $this;
  423. }
  424. /**
  425. * Convert value to DataBase value
  426. * @param array $rec Record to convert
  427. * @return int Database safe value
  428. */
  429. function value2db($rec)
  430. {
  431. if ($this->isEmpty($rec))
  432. {
  433. atkdebug($this->fieldName()." IS EMPTY!");
  434. return NULL;
  435. }
  436. else
  437. {
  438. if ($this->createDestination())
  439. {
  440. if (is_array($rec[$this->fieldName()]))
  441. {
  442. $pkfield = $this->m_destInstance->m_primaryKey[0];
  443. $pkattr = &$this->m_destInstance->getAttribute($pkfield);
  444. return $pkattr->value2db($rec[$this->fieldName()]);
  445. }
  446. else
  447. {
  448. return $rec[$this->fieldName()];
  449. }
  450. }
  451. }
  452. // This never happens, does it?
  453. return "";
  454. }
  455. /**
  456. * Fetch value out of record
  457. * @param array $postvars Postvars
  458. * @return decoded value
  459. */
  460. function fetchValue($postvars)
  461. {
  462. if ($this->isPosted($postvars))
  463. {
  464. $result = array();
  465. // support specifying the value as a single number if the
  466. // destination's primary key consists of a single field
  467. if (is_numeric($postvars[$this->fieldName()]))
  468. {
  469. $result[$this->getDestination()->primaryKeyField()] = $postvars[$this->fieldName()];
  470. }
  471. else
  472. {
  473. // Split the primary key of the selected record into its
  474. // referential key elements.
  475. $keyelements = decodeKeyValueSet($postvars[$this->fieldName()]);
  476. foreach ($keyelements as $key=>$value)
  477. {
  478. // Tablename must be stripped out because it is in the way..
  479. if (strpos($key,'.')>0)
  480. {
  481. $field = substr($key,strrpos($key,'.')+1);
  482. }
  483. else
  484. {
  485. $field = $key;
  486. }
  487. $result[$field] = $value;
  488. }
  489. }
  490. if (count($result) == 0)
  491. {
  492. return null;
  493. }
  494. // add descriptor fields, this means they can be shown in the title
  495. // bar etc. when updating failed for example
  496. $record = array($this->fieldName() => $result);
  497. $this->populate($record);
  498. $result = $record[$this->fieldName()];
  499. return $result;
  500. }
  501. return NULL;
  502. }
  503. /**
  504. * Converts DataBase value to normal value
  505. * @param array $rec Record
  506. * @return decoded value
  507. */
  508. function db2value($rec)
  509. {
  510. $this->createDestination();
  511. if (isset($rec[$this->fieldName()]) &&
  512. is_array($rec[$this->fieldName()]) &&
  513. (!isset($rec[$this->fieldName()][$this->m_destInstance->primaryKeyField()]) ||
  514. empty($rec[$this->fieldName()][$this->m_destInstance->primaryKeyField()])))
  515. {
  516. return NULL;
  517. }
  518. if (isset($rec[$this->fieldName()]))
  519. {
  520. $myrec = $rec[$this->fieldName()];
  521. if (is_array($myrec))
  522. {
  523. $result = array();
  524. if ($this->createDestination())
  525. {
  526. foreach (array_keys($this->m_destInstance->m_attribList) as $attrName)
  527. {
  528. $attr = &$this->m_destInstance->m_attribList[$attrName];
  529. $result[$attrName] = $attr->db2value($myrec);
  530. }
  531. }
  532. return $result;
  533. }
  534. else
  535. {
  536. // if the record is not an array, probably only the value of the primary key was loaded.
  537. // This workaround only works for single-field primary keys.
  538. if ($this->createDestination())
  539. return array($this->m_destInstance->primaryKeyField() => $myrec);
  540. }
  541. }
  542. }
  543. /**
  544. * Set none label.
  545. *
  546. * @param string $label The label to use for the "none" option
  547. */
  548. function setNoneLabel($label)
  549. {
  550. $this->m_noneLabel = $label;
  551. }
  552. /**
  553. * Get none label.
  554. *
  555. * @return String The label for the "none" option
  556. */
  557. function getNoneLabel()
  558. {
  559. if ($this->m_noneLabel !== NULL)
  560. return $this->m_noneLabel;
  561. $nodename = $this->m_destInstance->m_type;
  562. $modulename = $this->m_destInstance->m_module;
  563. $ownermodulename = $this->m_ownerInstance->m_module;
  564. $label = atktext($this->fieldName() . '_select_none', $ownermodulename, $this->m_owner, "", "", true);
  565. if ($label == "")
  566. $label = atktext('select_none', $modulename, $nodename);
  567. return $label;
  568. }
  569. /**
  570. * Returns a displayable string for this value.
  571. *
  572. * @param array $record The record that holds the value for this attribute
  573. * @param String $mode The display mode ("view" for viewpages, or "list"
  574. * for displaying in recordlists, "edit" for
  575. * displaying in editscreens, "add" for displaying in
  576. * add screens. "csv" for csv files. Applications can
  577. * use additional modes.
  578. * @return a displayable string
  579. */
  580. function display($record, $mode='list')
  581. {
  582. if ($this->createDestination())
  583. {
  584. if (count($record[$this->fieldName()])==count($this->m_refKey))
  585. $this->populate($record);
  586. if(!$this->isEmpty($record))
  587. {
  588. $result = $this->m_destInstance->descriptor($record[$this->fieldName()]);
  589. if ($this->hasFlag(AF_RELATION_AUTOLINK) && (!in_array($mode, array("csv", "plain", "list")))) // create link to edit/view screen
  590. {
  591. if (($this->m_destInstance->allowed("view"))&&!$this->m_destInstance->hasFlag(NF_NO_VIEW)&&$result!="")
  592. {
  593. $saveForm = $mode == 'add' || $mode == 'edit';
  594. $result = href(dispatch_url($this->m_destination,"view",array("atkselector"=>$this->m_destInstance->primaryKey($record[$this->fieldName()]))), $result, SESSION_NESTED, $saveForm);
  595. }
  596. }
  597. }
  598. else
  599. {
  600. $result = (!in_array($mode, array("csv", "plain"))?$this->getNoneLabel():''); // no record
  601. }
  602. return $result;
  603. }
  604. else
  605. {
  606. atkdebug("Can't create destination! ($this->m_destination");
  607. }
  608. return "";
  609. }
  610. /**
  611. * Populate the record with the destination record data.
  612. *
  613. * @param array $record record
  614. * @param mixed $fullOrFields load all data, only the given fields or only the descriptor fields?
  615. */
  616. public function populate(&$record, $fullOrFields=false)
  617. {
  618. if (!is_array($record)||$record[$this->fieldName()] == "") return;
  619. atkdebug("Delayed loading of ".($fullOrFields || is_array($fullOrFields) ? "" : "descriptor ")."fields for ".$this->m_name);
  620. $this->createDestination();
  621. $includes = "";
  622. if (is_array($fullOrFields))
  623. {
  624. $includes = array_merge($this->m_destInstance->m_primaryKey, $fullOrFields);
  625. }
  626. else if (!$fullOrFields)
  627. {
  628. $includes = $this->m_destInstance->descriptorFields();
  629. }
  630. $result =
  631. $this->m_destInstance
  632. ->select($this->m_destInstance->primaryKey($record[$this->fieldName()]))
  633. ->orderBy($this->m_destInstance->getColumnConfig()->getOrderByStatement())
  634. ->includes($includes)
  635. ->firstRow();
  636. if ($result != null)
  637. {
  638. $record[$this->fieldName()] = $result;
  639. }
  640. }
  641. /**
  642. * Creates HTML for the selection and auto links.
  643. *
  644. * @param string $id attribute id
  645. * @param array $record record
  646. * @return string
  647. */
  648. function createSelectAndAutoLinks($id, $record)
  649. {
  650. $links = array();
  651. $newsel = $id;
  652. $filter = $this->parseFilter($this->m_destinationFilter, $record);
  653. $links[] = $this->_getSelectLink($newsel, $filter);
  654. if ($this->hasFlag(AF_RELATION_AUTOLINK)) // auto edit/view link
  655. {
  656. if ($this->m_destInstance->allowed("add"))
  657. {
  658. $links[] = href(dispatch_url($this->getAutoLinkDestination(),"add",array("atkpkret"=>$id,"atkfilter"=>($filter!=""?$filter:""))),
  659. atktext("new"),SESSION_NESTED,true);
  660. }
  661. if ($this->m_destInstance->allowed("edit") && $record[$this->fieldName()] != NULL)
  662. {
  663. //we laten nu altijd de edit link zien, maar eigenlijk mag dat niet, want
  664. //de app crasht als er geen waarde is ingevuld.
  665. $editUrl = session_url(dispatch_url($this->getAutoLinkDestination(),"edit",array("atkselector"=>"REPLACEME")),SESSION_NESTED);
  666. $links[] = "<span id=\"".$id."_edit\" style=\"\"><a href='javascript:atkSubmit(mto_parse(\"".atkurlencode($editUrl)."\", document.entryform.".$id.".value))'>".atktext('edit')."</a></span>";
  667. }
  668. }
  669. return implode("&nbsp;", $links);
  670. }
  671. /**
  672. * Set destination node for the Autolink links (new/edit)
  673. *
  674. * @param string $node
  675. */
  676. function setAutoLinkDestination($node)
  677. {
  678. $this->m_autolink_destination = $node;
  679. }
  680. /**
  681. * Get destination node for the Autolink links (new/edit)
  682. *
  683. * @return string
  684. */
  685. function getAutoLinkDestination()
  686. {
  687. if(!empty($this->m_autolink_destination))
  688. {
  689. return $this->m_autolink_destination;
  690. }
  691. return $this->m_destination;
  692. }
  693. /**
  694. * Prepare for editing, make sure we already have the selectable records
  695. * loaded and update the record with the possible selection of the first
  696. * record.
  697. *
  698. * @param array $record reference to the record
  699. * @param string $fieldPrefix field prefix
  700. * @param string $mode edit mode
  701. */
  702. public function preAddToEditArray(&$record, $fieldPrefix, $mode)
  703. {
  704. if ((!$this->hasFlag(AF_RELATION_AUTOCOMPLETE) && !$this->hasFlag(AF_LARGE)) ||
  705. $this->m_autocomplete_minrecords > -1)
  706. {
  707. $this->m_selectableRecords = $this->_getSelectableRecords($record, 'select');
  708. if (count($this->m_selectableRecords) > 0 &&
  709. !atkconfig("list_obligatory_null_item") &&
  710. (($this->hasFlag(AF_OBLIGATORY) && !$this->hasFlag(AF_MANYTOONE_OBLIGATORY_NULL_ITEM)) ||
  711. (!$this->hasFlag(AF_OBLIGATORY) && $this->hasFlag(AF_RELATION_NO_NULL_ITEM))))
  712. {
  713. if (!isset($record[$this->fieldName()]) || !is_array($record[$this->fieldName()]))
  714. {
  715. $record[$this->fieldName()] = $this->m_selectableRecords[0];
  716. }
  717. else if (!$this->_isSelectableRecord($record, 'select'))
  718. {
  719. $record[$this->fieldName()] = $this->m_selectableRecords[0];
  720. }
  721. else
  722. {
  723. $current = $this->getDestination()->primaryKey($record[$this->fieldName()]);
  724. $record[$this->fieldName()] = null;
  725. foreach ($this->m_selectableRecords as $selectable)
  726. {
  727. if ($this->getDestination()->primaryKey($selectable) == $current)
  728. {
  729. $record[$this->fieldName()] = $selectable;
  730. break;
  731. }
  732. }
  733. }
  734. }
  735. }
  736. else if (is_array($record[$this->fieldName()]) && !$this->_isSelectableRecord($record, 'select'))
  737. {
  738. $record[$this->fieldName()] = null;
  739. }
  740. else if (is_array($record[$this->fieldName()]))
  741. {
  742. $this->populate($record);
  743. }
  744. }
  745. /**
  746. * Returns a piece of html code that can be used in a form to edit this
  747. * attribute's value.
  748. * @param array $record The record that holds the value for this attribute.
  749. * @param String $fieldprefix The fieldprefix to put in front of the name
  750. * of any html form element for this attribute.
  751. * @param String $mode The mode we're in ('add' or 'edit')
  752. * @return Piece of html code that can be used in a form to edit this
  753. */
  754. function edit($record, $fieldprefix="", $mode="edit")
  755. {
  756. if (!$this->createDestination())
  757. {
  758. return atkerror("Could not create destination for destination: $this->m_destination!");
  759. }
  760. $recordset = $this->m_selectableRecords;
  761. // load records for bwc
  762. if ($recordset === null && $this->hasFlag(AF_RELATION_AUTOCOMPLETE) && $this->m_autocomplete_minrecords > -1)
  763. {
  764. $recordset = $this->_getSelectableRecords($record, 'select');
  765. }
  766. if ($this->hasFlag(AF_RELATION_AUTOCOMPLETE) && (is_object($this->m_ownerInstance)) && ((is_array($recordset) && count($recordset) > $this->m_autocomplete_minrecords) || $this->m_autocomplete_minrecords == -1))
  767. {
  768. return $this->drawAutoCompleteBox($record, $fieldprefix, $mode);
  769. }
  770. $id = $fieldprefix.$this->fieldName();
  771. $filter = $this->parseFilter($this->m_destinationFilter, $record);
  772. $autolink = $this->getRelationAutolink($id, $filter);
  773. $editflag = true;
  774. $value = isset($record[$this->fieldName()]) ? $record[$this->fieldName()] : null;
  775. $currentPk = $value != null ? $this->getDestination()->primaryKey($value) : null;
  776. if (!$this->hasFlag(AF_LARGE)) // normal dropdown..
  777. {
  778. // load records for bwc
  779. if ($recordset == null)
  780. {
  781. $recordset = $this->_getSelectableRecords($record, 'select');
  782. }
  783. if (count($recordset) == 0)
  784. {
  785. $editflag = false;
  786. }
  787. $onchange='';
  788. if (count($this->m_onchangecode))
  789. {
  790. $onchange = 'onChange="'.$id.'_onChange(this);"';
  791. $this->_renderChangeHandler($fieldprefix);
  792. }
  793. // autoselect if there is only one record (if obligatory is not set,
  794. // we don't autoselect, since user may wist to select 'none' instead
  795. // of the 1 record.
  796. $result = '';
  797. if (count($recordset) == 0)
  798. {
  799. $result = $this->getNoneLabel();
  800. }
  801. else
  802. {
  803. $this->registerKeyListener($id, KB_CTRLCURSOR|KB_LEFTRIGHT);
  804. $this->registerJavaScriptObservers($id);
  805. $result = '<select id="'.$id.'" name="'.$id.'" class="atkmanytoonerelation" '.$onchange.'>';
  806. // relation may be empty, so we must provide an empty selectable..
  807. if ($this->hasFlag(AF_MANYTOONE_OBLIGATORY_NULL_ITEM) ||
  808. (!$this->hasFlag(AF_OBLIGATORY) && !$this->hasFlag(AF_RELATION_NO_NULL_ITEM)) ||
  809. (atkconfig("list_obligatory_null_item") && !is_array($value)))
  810. {
  811. $result.= '<option value="">'.$this->getNoneLabel();
  812. }
  813. foreach ($recordset as $selectable)
  814. {
  815. $pk = $this->getDestination()->primaryKey($selectable);
  816. $sel = $pk == $currentPk ? 'selected="selected"' : '';
  817. $result .= '<option value="'.$pk.'" '.$sel.'>'.str_replace(' ', '&nbsp;', atk_htmlentities($this->m_destInstance->descriptor($selectable)));
  818. }
  819. $result .= '</select>';
  820. }
  821. }
  822. else
  823. {
  824. $destrecord = $record[$this->fieldName()];
  825. if (is_array($destrecord))
  826. {
  827. $result.= '<span id="'.$id.'_current" >'.$this->m_destInstance->descriptor($destrecord)."&nbsp;&nbsp;";
  828. if (!$this->hasFlag(AF_OBLIGATORY))
  829. {
  830. $result.= '<a href="#" onClick="document.getElementById(\''.
  831. $id.'\').value=\'\'; document.getElementById(\''.$id.'_current\').style.display=\'none\'">'.atktext("unselect").'</a>&nbsp;&nbsp;';
  832. }
  833. $result.= '</span>';
  834. }
  835. $result .= $this->hide($record, $fieldprefix);
  836. $result .= $this->_getSelectLink($id, $filter);
  837. }
  838. $result .= $editflag && isset($autolink['edit']) ? $autolink['edit'] : "";
  839. $result .= isset($autolink['add']) ? $autolink['add'] : "";
  840. return $result;
  841. }
  842. /**
  843. * Get the select link to select the value using a select action on the destination node
  844. *
  845. * @param string $selname
  846. * @param string $filter
  847. * @return String HTML-code with the select link
  848. */
  849. function _getSelectLink($selname, $filter)
  850. {
  851. $result = "";
  852. // we use the current level to automatically return to this page
  853. // when we come from the select..
  854. $atktarget = atkurlencode(getDispatchFile()."?atklevel=".atkLevel()."&".$selname."=[atkprimkey]");
  855. $linkname = atktext("link_select_".getNodeType($this->m_destination), $this->getOwnerInstance()->getModule(), $this->getOwnerInstance()->getType(),'','',true);
  856. if (!$linkname) $linkname = atktext("link_select_".getNodeType($this->m_destination), getNodeModule($this->m_destination),getNodeType($this->m_destination),'','',true);
  857. if (!$linkname) $linkname = atktext("select_a").' '.strtolower(atktext(getNodeType($this->m_destination), getNodeModule($this->m_destination),getNodeType($this->m_destination)));
  858. if ($this->m_destinationFilter!="")
  859. {
  860. $result.= href(dispatch_url($this->m_destination,"select",array("atkfilter"=>$filter,"atktarget"=>$atktarget)),
  861. $linkname,
  862. SESSION_NESTED,
  863. $this->m_autocomplete_saveform,'class="atkmanytoonerelation"');
  864. }
  865. else
  866. {
  867. $result.= href(dispatch_url($this->m_destination,"select",array("atktarget"=>$atktarget)),
  868. $linkname,
  869. SESSION_NESTED,
  870. $this->m_autocomplete_saveform,'class="atkmanytoonerelation"');
  871. }
  872. return $result;
  873. }
  874. /**
  875. * Creates and returns the auto edit/view links
  876. * @param String $id The field id
  877. * @param String $filter Filter that we want to apply on the destination node
  878. * @return array The HTML code for the autolink links
  879. */
  880. function getRelationAutolink($id, $filter)
  881. {
  882. $autolink = array();
  883. if ($this->hasFlag(AF_RELATION_AUTOLINK)) // auto edit/view link
  884. {
  885. $page = &atkPage::getInstance();
  886. $page->register_script(atkconfig("atkroot")."atk/javascript/class.atkmanytoonerelation.js");
  887. if ($this->m_destInstance->allowed("edit"))
  888. {
  889. $editlink = session_url(dispatch_url($this->getAutoLinkDestination(),"edit",array("atkselector"=>"REPLACEME")),SESSION_NESTED);
  890. $autolink['edit'] = "&nbsp;<a href='javascript:atkSubmit(mto_parse(\"".atkurlencode($editlink)."\", document.entryform.".$id.".value))'>".atktext('edit')."</a>";
  891. }
  892. if ($this->m_destInstance->allowed("add"))
  893. {
  894. $autolink['add'] = "&nbsp;".href(dispatch_url($this->getAutoLinkDestination(),"add",array("atkpkret"=>$id,"atkfilter"=>($this->m_useFilterForAddLink && $filter != "" ? $filter : ""))),
  895. atktext("new"),SESSION_NESTED,true);
  896. }
  897. }
  898. return $autolink;
  899. }
  900. /**
  901. * Returns a piece of html code for hiding this attribute in an HTML form,
  902. * while still posting its value. (<input type="hidden">)
  903. *
  904. * @param array $record The record that holds the value for this attribute
  905. * @param String $fieldprefix The fieldprefix to put in front of the name
  906. * of any html form element for this attribute.
  907. * @return String A piece of htmlcode with hidden form elements that post
  908. * this attribute's value without showing it.
  909. */
  910. function hide($record="", $fieldprefix="")
  911. {
  912. if (!$this->createDestination()) return '';
  913. $currentPk = "";
  914. if (isset($record[$this->fieldName()]) && $record[$this->fieldName()] != null)
  915. {
  916. $this->fixDestinationRecord($record);
  917. $currentPk = $this->m_destInstance->primaryKey($record[$this->fieldName()]);
  918. }
  919. $result =
  920. '<input type="hidden" id="'.$fieldprefix.$this->formName().'"
  921. name="'.$fieldprefix.$this->formName().'"
  922. value="'.$currentPk.'">';
  923. return $result;
  924. }
  925. /**
  926. * Support for destination "records" where only the id is set and the
  927. * record itself isn't converted to a real record (array) yet
  928. *
  929. * @param array $record The record to fix
  930. */
  931. function fixDestinationRecord(&$record)
  932. {
  933. if ($this->createDestination() &&
  934. isset($record[$this->fieldName()]) &&
  935. $record[$this->fieldName()] != null &&
  936. !is_array($record[$this->fieldName()]))
  937. {
  938. $record[$this->fieldName()] = array($this->m_destInstance->primaryKeyField() => $record[$this->fieldName()]);
  939. }
  940. }
  941. /**
  942. * Retrieve the html code for placing this attribute in an edit page.
  943. *
  944. * The difference with the edit() method is that the edit() method just
  945. * generates the HTML code for editing the attribute, while the getEdit()
  946. * method is 'smart', and implements a hide/readonly policy based on
  947. * flags and/or custom override methodes in the node.
  948. * (<attributename>_edit() and <attributename>_display() methods)
  949. *
  950. * Framework method, it should not be necessary to call this method
  951. * directly.
  952. *
  953. * @param String $mode The edit mode ("add" or "edit")
  954. * @param array $record The record holding the values for this attribute
  955. * @param String $fieldprefix The fieldprefix to put in front of the name
  956. * of any html form element for this attribute.
  957. * @return String the HTML code for this attribute that can be used in an
  958. * editpage.
  959. */
  960. function getEdit($mode, &$record, $fieldprefix)
  961. {
  962. $this->fixDestinationRecord($record);
  963. return parent::getEdit($mode, $record, $fieldprefix);
  964. }
  965. /**
  966. * Converts a record filter to a record array.
  967. *
  968. * @param string $filter filter string
  969. * @return array record
  970. */
  971. protected function filterToArray($filter)
  972. {
  973. $result = array();
  974. $values = decodeKeyValueSet($filter);
  975. foreach ($values as $field => $value)
  976. {
  977. $parts = explode('.', $field);
  978. $ref = &$result;
  979. foreach ($parts as $part)
  980. {
  981. $ref = &$ref[$part];
  982. }
  983. $ref = $value;
  984. }
  985. return $result;
  986. }
  987. /**
  988. * Returns a piece of html code that can be used to get search terms input
  989. * from the user.
  990. *
  991. * @param array $record Array with values
  992. * @param boolean $extended if set to false, a simple search input is
  993. * returned for use in the searchbar of the
  994. * recordlist. If set to true, a more extended
  995. * search may be returned for the 'extended'
  996. * search page. The atkAttribute does not
  997. * make a difference for $extended is true, but
  998. * derived attributes may reimplement this.
  999. * @param string $fieldprefix The fieldprefix of this attribute's HTML element.
  1000. * @param atkDataGrid $grid The datagrid
  1001. *
  1002. * @return String A piece of html-code
  1003. */
  1004. function search($record=array(), $extended=false, $fieldprefix="", atkDataGrid $grid=null)
  1005. {
  1006. $useautocompletion = atkConfig("manytoone_search_autocomplete", true) && $this->hasFlag(AF_RELATION_AUTOCOMPLETE);
  1007. if (!$this->hasFlag(AF_LARGE) && !$useautocompletion)
  1008. {
  1009. if ($this->createDestination())
  1010. {
  1011. if ($this->m_destinationFilter!="")
  1012. {
  1013. $filterRecord = array();
  1014. if ($grid != null)
  1015. {
  1016. foreach ($grid->getFilters() as $filter)
  1017. {
  1018. $filter = $filter['filter'];
  1019. $arr = $this->filterToArray($filter);
  1020. $arr = (isset($arr[$this->getOwnerInstance()->m_table]) && is_array($arr[$this->getOwnerInstance()->m_table])) ? $arr[$this->getOwnerInstance()->m_table] : array();
  1021. foreach ($arr as $attrName => $value)
  1022. {
  1023. $attr = $this->getOwnerInstance()->getAttribute($attrName);
  1024. if (!is_array($value) && is_a($attr, 'atkManyToOneRelation') && count($attr->m_refKey) == 1)
  1025. {
  1026. $attr->createDestination();
  1027. $arr[$attrName] = array($attr->getDestination()->primaryKeyField() => $value);
  1028. }
  1029. }
  1030. $filterRecord = array_merge($filterRecord, $arr);
  1031. }
  1032. }
  1033. $record = array_merge($filterRecord, is_array($record) ? $record : array());
  1034. }
  1035. $recordset = $this->_getSelectableRecords($record, 'search');
  1036. $result = '<select class="'.get_class($this).'" ';
  1037. if ($extended)
  1038. {
  1039. $result.='multiple size="'.min(5,count($recordset)+1).'"';
  1040. if(isset($record[$this->fieldName()][$this->fieldName()]))
  1041. $record[$this->fieldName()] = $record[$this->fieldName()][$this->fieldName()];
  1042. }
  1043. // if we use autosearch, register an onchange event that submits the grid
  1044. if (!is_null($grid) && !$extended && $this->m_autoSearch) {
  1045. $id = $this->getSearchFieldName($fieldprefix);
  1046. $result .= ' id="'.$id.'" ';
  1047. $code = '$(\''.$id.'\').observe(\'change\', function(event) { ' .
  1048. $grid->getUpdateCall(array('atkstartat' => 0), array(), 'ATK.DataGrid.extractSearchOverrides') .
  1049. ' return false; });'
  1050. ;
  1051. $this->getOwnerInstance()->getPage()->register_loadscript($code);
  1052. }
  1053. $result.='name="'.$this->getSearchFieldName($fieldprefix).'[]">';
  1054. $pkfield = $this->m_destInstance->primaryKeyField();
  1055. $result.= '<option value="">'.atktext('search_all').'</option>';
  1056. if (!$this->hasFlag(AF_OBLIGATORY))
  1057. $result.= '<option value="__NONE__"'.(isset($record[$this->fieldName()]) && atk_in_array('__NONE__', $record[$this->fieldName()]) ? ' selected="selected"' : '').'>'.atktext('search_none').'</option>';
  1058. for ($i=0;$i<count($recordset);$i++)
  1059. {
  1060. $pk = $recordset[$i][$pkfield];
  1061. if (is_array($record)&&isset($record[$this->fieldName()])&&
  1062. atk_in_array($pk, $record[$this->fieldName()])) $sel = "selected"; else $sel = "";
  1063. $result.= '<option value="'.$pk.'" '.$sel.'>'.str_replace(' ', '&nbsp;', atk_htmlentities($this->m_destInstance->descriptor($recordset[$i]))).'</option>';
  1064. }
  1065. $result.='</select>';
  1066. return $result;
  1067. }
  1068. return "";
  1069. }
  1070. else
  1071. {
  1072. $id = $this->getSearchFieldName($fieldprefix);
  1073. if(is_array($record[$this->fieldName()]) && isset($record[$this->fieldName()][$this->fieldName()]))
  1074. $record[$this->fieldName()] = $record[$this->fieldName()][$this->fieldName()];
  1075. $this->registerKeyListener($id, KB_CTRLCURSOR|KB_UPDOWN);
  1076. $result = '<input type="text" id="'.$id.'" class="'.get_class($this).'" name="'.$id.'" value="'.$record[$this->fieldName()].'"'.
  1077. ($useautocompletion ? ' onchange=""' : '').
  1078. ($this->m_searchsize > 0 ? ' size="'.$this->m_searchsize.'"' : '').
  1079. ($this->m_maxsize > 0 ? ' maxlength="'.$this->m_maxsize.'"' : '').'>';
  1080. if ($useautocompletion)
  1081. {
  1082. $page = &$this->m_ownerInstance->getPage();
  1083. $url = partial_url($this->m_ownerInstance->atkNodeType(), $this->m_ownerInstance->m_action, 'attribute.'.$this->fieldName().'.autocomplete_search');
  1084. $code = "ATK.ManyToOneRelation.completeSearch('{$id}', '{$id}_result', '{$url}', {$this->m_autocomplete_minchars});";
  1085. $this->m_ownerInstance->addStyle("atkmanytoonerelation.css");
  1086. $page->register_script(atkconfig('atkroot').'atk/javascript/class.atkmanytoonerelation.js');
  1087. $page->register_loadscript($code);
  1088. $result .= '<div id="'.$id.'_result" style="display: none" class="atkmanytoonerelation_result"></div>';
  1089. }
  1090. return $result;
  1091. }
  1092. }
  1093. /**
  1094. * Retrieve the list of searchmodes supported by the attribute.
  1095. *
  1096. * Note that not all modes may be supported by the database driver.
  1097. * Compare this list to the one returned by the databasedriver, to
  1098. * determine which searchmodes may be used.
  1099. *
  1100. * @return array List of supported searchmodes
  1101. */
  1102. function getSearchModes()
  1103. {
  1104. if ($this->hasFlag(AF_LARGE) || $this->hasFlag(AF_MANYTOONE_AUTOCOMPLETE))
  1105. {
  1106. return array("substring","exact","wildcard","regex");
  1107. }
  1108. return array("exact"); // only support exact search when searching with dropdowns
  1109. }
  1110. /**
  1111. * Creates a smart search condition for a given search value, and adds it
  1112. * to the query that will be used for performing the actual search.
  1113. *
  1114. * @param Integer $id The unique smart search criterium identifier.
  1115. * @param Integer $nr The element number in the path.
  1116. * @param Array $path The remaining attribute path.
  1117. * @param atkQuery $query The query to which the condition will be added.
  1118. * @param String $ownerAlias The owner table alias to use.
  1119. * @param Mixed $value The value the user has entered in the searchbox.
  1120. * @param String $mode The searchmode to use.
  1121. */
  1122. function smartSearchCondition($id, $nr, $path, &$query, $ownerAlias, $value, $mode)
  1123. {
  1124. if (count($path) > 0)
  1125. {
  1126. $this->createDestination();
  1127. $destAlias = "ss_{$id}_{$nr}_".$this->fieldName();
  1128. $query->addJoin(
  1129. $this->m_destInstance->m_table, $destAlias,
  1130. $this->getJoinCondition($query, $ownerAlias, $destAlias),
  1131. false
  1132. );
  1133. $attrName = array_shift($path);
  1134. $attr = &$this->m_destInstance->getAttribute($attrName);
  1135. if (is_object($attr))
  1136. {
  1137. $attr->smartSearchCondition($id, $nr + 1, $path, $query, $destAlias, $value, $mode);
  1138. }
  1139. }
  1140. else
  1141. {
  1142. $this->searchCondition($query, $ownerAlias, $value, $mode);
  1143. }
  1144. }
  1145. /**
  1146. * Creates a searchcondition for the field,
  1147. * was once part of searchCondition, however,
  1148. * searchcondition() also immediately adds the search condition.
  1149. *
  1150. * @param atkQuery $query The query object where the search condition should be placed on
  1151. * @param String $table The name of the table in which this attribute
  1152. * is stored
  1153. * @param mixed $value The value the user has entered in the searchbox
  1154. * @param String $searchmode The searchmode to use. This can be any one
  1155. * of the supported modes, as returned by this
  1156. * attribute's getSearchModes() method.
  1157. * @param string $fieldaliasprefix The prefix for the field
  1158. * @return String The searchcondition to use.
  1159. */
  1160. function getSearchCondition(&$query, $table, $value, $searchmode, $fieldaliasprefix='')
  1161. {
  1162. if (!$this->createDestination()) return;
  1163. if (is_array($value))
  1164. {
  1165. foreach ($this->m_listColumns as $attr)
  1166. {
  1167. $attrValue = $value[$attr];
  1168. if (!empty($attrValue))
  1169. {
  1170. $p_attrib = &$this->m_destInstance->m_attribList[$attr];
  1171. if (!$p_attrib == NULL)
  1172. {
  1173. $p_attrib->searchCondition($query, $this->fieldName(), $attrValue, $this->getChildSearchMode($searchmode, $p_attrib->formName()));
  1174. }
  1175. }
  1176. }
  1177. if (isset($value[$this->fieldName()]))
  1178. {
  1179. $value = $value[$this->fieldName()];
  1180. }
  1181. }
  1182. if (empty($value))
  1183. {
  1184. return '';
  1185. }
  1186. else if (!$this->hasFlag(AF_LARGE) && !$this->hasFlag(AF_RELATION_AUTOCOMPLETE))
  1187. {
  1188. // We only support 'exact' matches.
  1189. // But you can select more than one value, which we search using the IN() statement,
  1190. // which should work in any ansi compatible database.
  1191. 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.
  1192. {
  1193. $value = array($value);
  1194. }
  1195. if (count($value)==1) // exactly one value
  1196. {
  1197. if ($value[0] == "__NONE__")
  1198. {
  1199. return $query->nullCondition($table.".".$this->fieldName(), true);
  1200. }
  1201. elseif ($value[0] != "")
  1202. {
  1203. return $query->exactCondition($table.".".$this->fieldName(),$this->escapeSQL($value[0]));
  1204. }
  1205. }
  1206. else // search for more values using IN()
  1207. {
  1208. return $table.".".$this->fieldName()." IN ('".implode("','",$value)."')";
  1209. }
  1210. }
  1211. else // AF_LARGE || AF_RELATION_AUTOCOMPLETE
  1212. {
  1213. // If we have a descriptor with multiple fields, use CONCAT
  1214. $attribs = $this->m_destInstance->descriptorFields();
  1215. $alias = $fieldaliasprefix . $this->fieldName();
  1216. if(count($attribs)>1)
  1217. {
  1218. $searchcondition = $this->getConcatFilter($value,$alias);
  1219. }
  1220. else
  1221. {
  1222. // ask the destination node for it's search condition
  1223. $searchcondition = $this->m_destInstance->getSearchCondition($query, $alias, $fieldaliasprefix, $value, $this->getChildSearchMode($searchmode, $this->formName()));
  1224. }
  1225. return $searchcondition;
  1226. }
  1227. }
  1228. /**
  1229. * Adds this attribute to database queries.
  1230. *
  1231. * Database queries (select, insert and update) are passed to this method
  1232. * so the attribute can 'hook' itself into the query.
  1233. *
  1234. * @param atkQuery $query The SQL query object
  1235. * @param String $tablename The name of the table of this attribute
  1236. * @param String $fieldaliasprefix Prefix to use in front of the alias
  1237. * in the query.
  1238. * @param Array $rec The record that contains the value of this attribute.
  1239. * @param int $level Recursion level if relations point to eachother, an
  1240. * endless loop could occur if they keep loading
  1241. * eachothers data. The $level is used to detect this
  1242. * loop. If overriden in a derived class, any subcall to
  1243. * an addToQuery method should pass the $level+1.
  1244. * @param String $mode Indicates what kind of query is being processing:
  1245. * This can be any action performed on a node (edit,
  1246. * add, etc) Mind you that "add" and "update" are the
  1247. * actions that store something in the database,
  1248. * whereas the rest are probably select queries.
  1249. */
  1250. function addToQuery(&$query, $tablename="", $fieldaliasprefix="", $rec="", $level=0, $mode="")
  1251. {
  1252. if ($this->hasFlag(AF_MANYTOONE_LAZY))
  1253. {
  1254. parent::addToQuery($query, $tablename, $fieldaliasprefix, $rec, $level, $mode);
  1255. return;
  1256. }
  1257. if ($this->createDestination())
  1258. {
  1259. if ($mode != "update" && $mode != "add")
  1260. {
  1261. $alias = $fieldaliasprefix . $this->fieldName();
  1262. $query->addJoin($this->m_destInstance->m_table,
  1263. $alias,
  1264. $this->getJoinCondition($query, $tablename, $alias),
  1265. $this->m_leftjoin);
  1266. $this->m_destInstance->addToQuery($query, $alias, $level+1, false, $mode, $this->m_listColumns);
  1267. }
  1268. else
  1269. {
  1270. for ($i=0, $_i=count($this->m_refKey); $i<$_i; $i++)
  1271. {
  1272. if ($rec[$this->fieldName()]===NULL)
  1273. {
  1274. $query->addField($this->m_refKey[$i],"NULL","","",false);
  1275. }
  1276. else
  1277. {
  1278. $value = $rec[$this->fieldName()];
  1279. if (is_array($value))
  1280. {
  1281. $fk = &$this->m_destInstance->getAttribute($this->m_destInstance->m_primaryKey[$i]);
  1282. $value = $fk->value2db($value);
  1283. }
  1284. $query->addField($this->m_refKey[$i],$value,"","",!$this->hasFlag(AF_NO_QUOTES));
  1285. }
  1286. }
  1287. }
  1288. }
  1289. }
  1290. /**
  1291. * Retrieve detail records from the database.
  1292. *
  1293. * Called by the framework to load the detail records.
  1294. *
  1295. * @param atkDb $db The database used by the node.
  1296. * @param array $record The master record
  1297. * @param String $mode The mode for loading (admin, select, copy, etc)
  1298. *
  1299. * @return array Recordset containing detailrecords, or NULL if no detail
  1300. * records are present. Note: when $mode is edit, this
  1301. * method will always return NULL. This is a framework
  1302. * optimization because in edit pages, the records are
  1303. * loaded on the fly.
  1304. */
  1305. function load(&$db, $record, $mode)
  1306. {
  1307. return $this->_getSelectedRecord($record, $mode);
  1308. }
  1309. /**
  1310. * Determine the load type of this attribute.
  1311. *
  1312. * With this method, the attribute tells the framework whether it wants
  1313. * to be loaded in the main query (addToQuery) or whether the attribute
  1314. * has its own load() implementation.
  1315. * For the atkOneToOneRelation, this depends on the presence of the
  1316. * AF_ONETOONE_LAZY flag.
  1317. *
  1318. * Framework method. It should not be necesary to call this method
  1319. * directly.
  1320. *
  1321. * @param String $mode The type of load (view,admin,edit etc)
  1322. *
  1323. * @return int Bitmask containing information about load requirements.
  1324. * POSTLOAD|ADDTOQUERY when AF_ONETOONE_LAZY is set.
  1325. * ADDTOQUERY when AF_ONETOONE_LAZY is not set.
  1326. */
  1327. function loadType($mode)
  1328. {
  1329. if (isset($this->m_loadType[$mode]) && $this->m_loadType[$mode] !== null)
  1330. return $this->m_loadType[$mode];
  1331. else if (isset($this->m_loadType[null]) && $this->m_loadType[null] !== null)
  1332. return $this->m_loadType[null];
  1333. // Default backwardscompatible behaviour:
  1334. else if ($this->hasFlag(AF_MANYTOONE_LAZY))
  1335. return POSTLOAD|ADDTOQUERY;
  1336. else
  1337. return ADDTOQUERY;
  1338. }
  1339. /**
  1340. * Validate if the record we are referring to really exists.
  1341. *
  1342. * @param array $record
  1343. * @param string $mode
  1344. */
  1345. function validate(&$record, $mode)
  1346. {
  1347. $sessionmanager = atkGetSessionManager();
  1348. if ($sessionmanager) $storetype = $sessionmanager->stackVar('atkstore');
  1349. if ($storetype!=='session' && !$this->_isSelectableRecord($record))
  1350. {
  1351. triggerError($record, $this->fieldName(), 'error_integrity_violation');
  1352. }
  1353. }
  1354. /**
  1355. * Check if two records have the same value for this attribute
  1356. *
  1357. * @param array $recA Record A
  1358. * @param array $recB Record B
  1359. * @return boolean to indicate if the records are equal
  1360. */
  1361. function equal($recA, $recB)
  1362. {
  1363. if ($this->createDestination())
  1364. {
  1365. return (($recA[$this->fieldName()][$this->m_destInstance->primaryKeyField()]
  1366. ==
  1367. $recB[$this->fieldName()][$this->m_destInstance->primaryKeyField()])
  1368. ||
  1369. ($this->isEmpty($recA)&&$this->isEmpty($recB)));
  1370. // we must also check empty values, because empty values need not necessarily
  1371. // be equal (can be "", NULL or 0.
  1372. }
  1373. return false;
  1374. }
  1375. /**
  1376. * Return the database field type of the attribute.
  1377. *
  1378. * Note that the type returned is a 'generic' type. Each database
  1379. * vendor might have his own types, therefor, the type should be
  1380. * converted to a database specific type using $db->fieldType().
  1381. *
  1382. * If the type was read from the table metadata, that value will
  1383. * be used. Else, the attribute will analyze its flags to guess
  1384. * what type it should be. If AF_AUTO_INCREMENT is set, the field
  1385. * is probaly "number". If not, it's probably "string".
  1386. *
  1387. * @return String The 'generic' type of the database field for this
  1388. * attribute.
  1389. */
  1390. function dbFieldType()
  1391. {
  1392. // The type of field that we need to store the foreign key, is equal to
  1393. // the type of field of the primary key of the node we have a
  1394. // relationship with.
  1395. if ($this->createDestination())
  1396. {
  1397. if(count($this->m_refKey)>1)
  1398. {
  1399. $keys = array();
  1400. for($i=0, $_i=count($this->m_refKey); $i<$_i; $i++)
  1401. {
  1402. $keys [] = $this->m_destInstance->m_attribList[$this->m_destInstance->m_primaryKey[$i]]->dbFieldType();
  1403. }
  1404. return $keys;
  1405. }
  1406. else
  1407. return $this->m_destInstance->m_attribList[$this->m_destInstance->primaryKeyField()]->dbFieldType();
  1408. }
  1409. return "";
  1410. }
  1411. /**
  1412. * Return the size of the field in the database.
  1413. *
  1414. * If 0 is returned, the size is unknown. In this case, the
  1415. * return value should not be used to create table columns.
  1416. *
  1417. * Ofcourse, the size does not make sense for every field type.
  1418. * So only interpret the result if a size has meaning for
  1419. * the field type of this attribute. (For example, if the
  1420. * database field is of type 'date', the size has no meaning)
  1421. *
  1422. * @return int The database field size
  1423. */
  1424. function dbFieldSize()
  1425. {
  1426. // The size of the field we need to store the foreign key, is equal to
  1427. // the size of the field of the primary key of the node we have a
  1428. // relationship with.
  1429. if ($this->createDestination())
  1430. {
  1431. if(count($this->m_refKey)>1)
  1432. {
  1433. $keys = array();
  1434. for($i=0, $_i=count($this->m_refKey); $i<$_i; $i++)
  1435. {
  1436. $keys [] = $this->m_destInstance->m_attribList[$this->m_destInstance->m_primaryKey[$i]]->dbFieldSize();
  1437. }
  1438. return $keys;
  1439. }
  1440. else
  1441. return $this->m_destInstance->m_attribList[$this->m_destInstance->primaryKeyField()]->dbFieldSize();
  1442. }
  1443. return 0;
  1444. }
  1445. /**
  1446. * Returns the selected record for this many-to-one relation. Uses
  1447. * the owner instance $this->fieldName()."_selected" method if it exists.
  1448. *
  1449. * @param array $record The record
  1450. * @param string $mode The mode we're in
  1451. * @return Array with the selected record
  1452. */
  1453. function _getSelectedRecord($record=array(), $mode="select")
  1454. {
  1455. $method = $this->fieldName()."_selected";
  1456. if (method_exists($this->m_ownerInstance, $method))
  1457. return $this->m_ownerInstance->$method($record, $mode);
  1458. else return $this->getSelectedRecord($record, $mode);
  1459. }
  1460. /**
  1461. * Returns the currently selected record.
  1462. *
  1463. * @param array $record The record
  1464. * @param string $mode The mode we're in
  1465. * @return Array with the selected record
  1466. */
  1467. function getSelectedRecord($record=array(), $mode="select")
  1468. {
  1469. $this->createDestination();
  1470. $condition = $this->m_destInstance->m_table.'.'.$this->m_destInstance->primaryKeyField().
  1471. "='".$record[$this->fieldName()][$this->m_destInstance->primaryKeyField()]."'";
  1472. $filter = $this->createFilter($record);
  1473. if (!empty($filter))
  1474. {
  1475. $condition = $condition.' AND '.$filter;
  1476. }
  1477. $record = $this->m_destInstance->select($condition)->mode($mode)->firstRow();
  1478. return $record;
  1479. }
  1480. /**
  1481. * Returns the selectable records for this many-to-one relation. Uses
  1482. * the owner instance $this->fieldName()."_selection" method if it exists.
  1483. *
  1484. * @param array $record The record
  1485. * @param string $mode The mode we're in
  1486. * @return Array with the selectable records
  1487. */
  1488. function _getSelectableRecords($record=array(), $mode="select")
  1489. {
  1490. $method = $this->fieldName()."_selection";
  1491. if (method_exists($this->m_ownerInstance, $method))
  1492. return $this->m_ownerInstance->$method($record, $mode);
  1493. else return $this->getSelectableRecords($record, $mode);
  1494. }
  1495. /**
  1496. * Is selectable record? Uses the owner instance $this->fieldName()."_selectable"
  1497. * method if it exists.
  1498. *
  1499. * @param array $record The record
  1500. * @param string $mode The mode we're in
  1501. * @return Boolean to indicate if the record is selectable
  1502. */
  1503. function _isSelectableRecord($record=array(), $mode="select")
  1504. {
  1505. $method = $this->fieldName()."_selectable";
  1506. if (method_exists($this->m_ownerInstance, $method))
  1507. return $this->m_ownerInstance->$method($record, $mode);
  1508. else return $this->isSelectableRecord($record, $mode);
  1509. }
  1510. /**
  1511. * Create the destination filter for the given record.
  1512. *
  1513. * @param array $record
  1514. * @return string filter
  1515. */
  1516. function createFilter($record)
  1517. {
  1518. if ($this->m_destinationFilter != "")
  1519. {
  1520. atkimport("atk.utils.atkstringparser");
  1521. $parser = new atkStringParser($this->m_destinationFilter);
  1522. return $parser->parse($record);
  1523. }
  1524. else
  1525. {
  1526. return "";
  1527. }
  1528. }
  1529. /**
  1530. * Is selectable record?
  1531. *
  1532. * Use this one from your selectable override when needed.
  1533. *
  1534. * @param array $record The record
  1535. * @param string $mode The mode we're in
  1536. * @return Boolean to indicate if the record is selectable
  1537. */
  1538. function isSelectableRecord($record=array(), $mode="select")
  1539. {
  1540. if ($record[$this->fieldName()] == NULL) return false;
  1541. $this->createDestination();
  1542. // if the value is set directly in the record field we first
  1543. // need to convert the value to an array
  1544. if (!is_array($record[$this->fieldName()]))
  1545. {
  1546. $record[$this->fieldName()] = array(
  1547. $this->m_destInstance->primaryKeyField() => $record[$this->fieldName()]
  1548. );
  1549. }
  1550. $selectedKey = $this->m_destInstance->primaryKey($record[$this->fieldName()]);
  1551. if ($selectedKey == NULL) return false;
  1552. // If custom selection method exists we use this one, although this is
  1553. // way more inefficient, so if you create a selection override you should
  1554. // also think about creating a selectable override!
  1555. $method = $this->fieldName()."_selection";
  1556. if (method_exists($this->m_ownerInstance, $method))
  1557. {
  1558. $rows = $this->m_ownerInstance->$method($record, $mode);
  1559. foreach ($rows as $row)
  1560. {
  1561. $key = $this->m_destInstance->primaryKey($row);
  1562. if ($key == $selectedKey) return true;
  1563. }
  1564. return false;
  1565. }
  1566. // No selection override exists, simply add the record key to the selector.
  1567. $filter = $this->createFilter($record);
  1568. $selector = "($selectedKey)".($filter != NULL ? " AND ($filter)" : "");
  1569. return $this->m_destInstance->select($selector)->getRowCount() > 0;
  1570. }
  1571. /**
  1572. * Returns the selectable records.
  1573. *
  1574. * Use this one from your selection override when needed.
  1575. *
  1576. * @param array $record The record
  1577. * @param string $mode The mode we're in
  1578. * @return Array with the selectable records
  1579. */
  1580. function getSelectableRecords($record=array(), $mode="select")
  1581. {
  1582. $this->createDestination();
  1583. $selector = $this->createFilter($record);
  1584. $result =
  1585. $this->m_destInstance
  1586. ->select($selector)
  1587. ->orderBy($this->getDestination()->getOrder())
  1588. ->includes(atk_array_merge($this->m_destInstance->descriptorFields(),$this->m_destInstance->m_primaryKey))
  1589. ->mode($mode)
  1590. ->allRows();
  1591. return $result;
  1592. }
  1593. /**
  1594. * Returns the condition (SQL) that should be used when we want to join a relation's
  1595. * owner node with the parent node.
  1596. *
  1597. * @param atkQuery $query The query object
  1598. * @param String $tablename The tablename on which to join
  1599. * @param String $fieldalias The fieldalias
  1600. * @return String SQL string for joining the owner with the destination.
  1601. * Returns false when impossible (f.e. attrib is not a relation).
  1602. */
  1603. function getJoinCondition(&$query, $tablename="",$fieldalias="")
  1604. {
  1605. if (!$this->createDestination()) return false;
  1606. if ($tablename!="") $realtablename=$tablename;
  1607. else $realtablename = $this->m_ownerInstance->m_table;
  1608. $joinconditions = array();
  1609. for ($i=0, $_i=count($this->m_refKey); $i<$_i; $i++)
  1610. {
  1611. $joinconditions[] = $realtablename.".".$this->m_refKey[$i].
  1612. "=".
  1613. $fieldalias.".".$this->m_destInstance->m_primaryKey[$i];
  1614. }
  1615. if ($this->m_joinFilter!="")
  1616. {
  1617. atkimport('atk.utils.atkstringparser');
  1618. $parser = new atkStringParser($this->m_joinFilter);
  1619. $filter = $parser->parse(array('table' => $realtablename, 'owner' => $realtablename, 'destination' => $fieldalias));
  1620. $joinconditions[] = $filter;
  1621. }
  1622. return implode(" AND ",$joinconditions);
  1623. }
  1624. /**
  1625. * Make this relation hide itself from the form when there are no items to select
  1626. *
  1627. * @param boolean $hidewhenempty true - hide when empty, false - always show
  1628. */
  1629. function setHideWhenEmpty($hidewhenempty)
  1630. {
  1631. $this->m_hidewhenempty = $hidewhenempty;
  1632. }
  1633. /**
  1634. * Adds the attribute's edit / hide HTML code to the edit array.
  1635. *
  1636. * This method is called by the node if it wants the data needed to create
  1637. * an edit form.
  1638. *
  1639. * This is a framework method, it should never be called directly.
  1640. *
  1641. * @param String $mode the edit mode ("add" or "edit")
  1642. * @param array $arr pointer to the edit array
  1643. * @param array $defaults pointer to the default values array
  1644. * @param array $error pointer to the error array
  1645. * @param String $fieldprefix the fieldprefix
  1646. */
  1647. function addToEditArray($mode, &$arr, &$defaults, &$error, $fieldprefix)
  1648. {
  1649. if ($this->createDestination())
  1650. {
  1651. // check if destination table is empty
  1652. // only check if hidewhenempty is set to true
  1653. if ($this->m_hidewhenempty)
  1654. {
  1655. $recs = $this->_getSelectableRecords($defaults, 'select');
  1656. if (count($recs)==0) return $this->hide($defaults, $fieldprefix);
  1657. }
  1658. }
  1659. return parent::addToEditArray($mode, $arr, $defaults, $error, $fieldprefix);
  1660. }
  1661. /**
  1662. * Retrieves the ORDER BY statement for the relation.
  1663. *
  1664. * @param Array $extra A list of attribute names to add to the order by
  1665. * statement
  1666. * @param String $table The table name (if not given uses the owner node's table name)
  1667. * @param String $direction Sorting direction (ASC or DESC)
  1668. * @return String The ORDER BY statement for this attribute
  1669. */
  1670. function getOrderByStatement($extra='', $table='', $direction='ASC')
  1671. {
  1672. if (!$this->createDestination())
  1673. return parent::getOrderByStatement();
  1674. if (!empty($table))
  1675. {
  1676. $table = $table.'_AE_'.$this->fieldName();
  1677. }
  1678. else
  1679. {
  1680. $table = $this->fieldName();
  1681. }
  1682. if (!empty($extra) && in_array($extra, $this->m_listColumns))
  1683. {
  1684. return $this->getDestination()->getAttribute($extra)->getOrderByStatement('', $table, $direction);
  1685. }
  1686. $order = $this->m_destInstance->getOrder();
  1687. if (!empty($order))
  1688. {
  1689. $newParts = array();
  1690. $parts = explode(',', $order);
  1691. foreach ($parts as $part)
  1692. {
  1693. $split = preg_split('/\s+/', trim($part));
  1694. $field = isset($split[0]) ? $split[0] : null;
  1695. $fieldDirection = empty($split[1]) ? 'ASC' : strtoupper($split[1]);
  1696. // if our default direction is DESC (the opposite of the default ASC)
  1697. // we always have to switch the given direction to be the opposite, e.g.
  1698. // DESC => ASC and ASC => DESC, this way we respect the default ordering
  1699. // in the destination node even if the default is descending
  1700. if ($fieldDirection == 'DESC')
  1701. {
  1702. $fieldDirection = $direction == 'DESC' ? 'ASC' : 'DESC';
  1703. }
  1704. else
  1705. {
  1706. $fieldDirection = $direction;
  1707. }
  1708. if (strpos($field, '.') !== false)
  1709. {
  1710. list(,$field) = explode('.', $field);
  1711. }
  1712. $newPart = $this->getDestination()->getAttribute($field)->getOrderByStatement('', $table, $fieldDirection);
  1713. // realias if destination order contains the wrong tablename.
  1714. if (strpos($newPart, $this->m_destInstance->m_table.'.') !== false)
  1715. {
  1716. $newPart= str_replace($this->m_destInstance->m_table.'.', $table.'.', $newPart);
  1717. }
  1718. $newParts[] = $newPart;
  1719. }
  1720. return implode(', ', $newParts);
  1721. }
  1722. else
  1723. {
  1724. $fields = $this->m_destInstance->descriptorFields();
  1725. if (count($fields) == 0)
  1726. $fields = array($this->m_destInstance->primaryKeyField());
  1727. $order = "";
  1728. foreach ($fields as $field)
  1729. $order .= (empty($order) ? '' : ', ').$table.".".$field;
  1730. return $order;
  1731. }
  1732. }
  1733. /**
  1734. * Adds the attribute / field to the list header. This includes the column name and search field.
  1735. *
  1736. * Framework method. It should not be necessary to call this method directly.
  1737. *
  1738. * @param String $action the action that is being performed on the node
  1739. * @param array $arr reference to the the recordlist array
  1740. * @param String $fieldprefix the fieldprefix
  1741. * @param int $flags the recordlist flags
  1742. * @param array $atksearch the current ATK search list (if not empty)
  1743. * @param atkColumnConfig $columnConfig Column configuration object
  1744. * @param atkDataGrid $grid The atkDataGrid this attribute lives on.
  1745. * @param string $column child column (null for this attribute, * for this attribute and all childs)
  1746. */
  1747. public function addToListArrayHeader($action, &$arr, $fieldprefix, $flags, $atksearch, $atkorderby, atkDataGrid $grid=null, $column='*')
  1748. {
  1749. if ($column == null || $column == '*')
  1750. {
  1751. $prefix = $fieldprefix.$this->fieldName()."_AE_";
  1752. parent::addToListArrayHeader($action, $arr, $prefix, $flags, $atksearch[$this->fieldName()], $atkorderby, $grid, null);
  1753. }
  1754. if ($column == '*')
  1755. {
  1756. // only add extra columns when needed
  1757. if ($this->hasFlag(AF_HIDE_LIST) && !$this->m_alwaysShowListColumns) return;
  1758. if (!$this->createDestination() || count($this->m_listColumns) == 0) return;
  1759. foreach ($this->m_listColumns as $column)
  1760. {
  1761. $this->_addColumnToListArrayHeader($column, $action, $arr, $fieldprefix, $flags, $atksearch, $atkorderby, $grid);
  1762. }
  1763. }
  1764. else if ($column != null)
  1765. {
  1766. $this->_addColumnToListArrayHeader($column, $action, $arr, $fieldprefix, $flags, $atksearch, $atkorderby, $grid);
  1767. }
  1768. }
  1769. /**
  1770. * Adds the child attribute / field to the list row.
  1771. *
  1772. * Framework method. It should not be necessary to call this method directly.
  1773. *
  1774. * @param string $column child column (null for this attribute, * for this attribute and all childs)
  1775. * @param String $action the action that is being performed on the node
  1776. * @param array $arr reference to the the recordlist array
  1777. * @param String $fieldprefix the fieldprefix
  1778. * @param int $flags the recordlist flags
  1779. * @param array $atksearch the current ATK search list (if not empty)
  1780. * @param string $atkorderby order by
  1781. * @param atkDataGrid $grid The atkDataGrid this attribute lives on.
  1782. */
  1783. protected function _addColumnToListArrayHeader($column, $action, &$arr, $fieldprefix, $flags, $atksearch, $atkorderby, atkDataGrid $grid=null)
  1784. {
  1785. $prefix = $fieldprefix.$this->fieldName()."_AE_";
  1786. $p_attrib = $this->m_destInstance->getAttribute($column);
  1787. if ($p_attrib == null)
  1788. {
  1789. throw new Exception("Invalid list column {$column} for atkManyToOneRelation ".$this->getOwnerInstance()->atkNodeType().'::'.$this->fieldName());
  1790. }
  1791. $p_attrib->m_flags |= AF_HIDE_LIST;
  1792. $p_attrib->m_flags ^= AF_HIDE_LIST;
  1793. $p_attrib->addToListArrayHeader($action, $arr, $prefix, $flags, $atksearch[$this->fieldName()], $atkorderby, $grid, null);
  1794. // fix order by clause
  1795. $needle = $prefix.$column;
  1796. foreach (array_keys($arr['heading']) as $key)
  1797. {
  1798. if (strpos($key, $needle) !== 0) continue;
  1799. $order = $arr['heading'][$key]['order'];
  1800. if (empty($order)) continue;
  1801. $order = $this->fieldName().'.'.$order;
  1802. if (is_object($atkorderby) &&
  1803. isset($atkorderby->m_colcfg[$this->fieldName()])
  1804. && isset($atkorderby->m_colcfg[$this->fieldName()]['extra'])
  1805. && $atkorderby->m_colcfg[$this->fieldName()]['extra'] == $column)
  1806. {
  1807. $direction = $atkorderby->getDirection($this->fieldName());
  1808. if ($direction=="asc") $order.=" desc";
  1809. }
  1810. $arr['heading'][$key]['order'] = $order;
  1811. }
  1812. }
  1813. /**
  1814. * Adds the attribute / field to the list row. And if the row is totalisable also to the total.
  1815. *
  1816. * Framework method. It should not be necessary to call this method directly.
  1817. *
  1818. * @param String $action the action that is being performed on the node
  1819. * @param array $arr reference to the the recordlist array
  1820. * @param int $nr the current row number
  1821. * @param String $fieldprefix the fieldprefix
  1822. * @param int $flags the recordlist flags
  1823. * @param boolean $edit editing?
  1824. * @param atkDataGrid $grid data grid
  1825. * @param string $column child column (null for this attribute, * for this attribute and all childs)
  1826. */
  1827. public function addToListArrayRow($action, &$arr, $nr, $fieldprefix, $flags, $edit=false, atkDataGrid $grid=null, $column='*')
  1828. {
  1829. if ($column == null || $column == '*')
  1830. {
  1831. $prefix = $fieldprefix.$this->fieldName()."_AE_";
  1832. parent::addToListArrayRow($action, $arr, $nr, $prefix, $flags, $edit, $grid, null);
  1833. }
  1834. if ($column == '*')
  1835. {
  1836. // only add extra columns when needed
  1837. if ($this->hasFlag(AF_HIDE_LIST) && !$this->m_alwaysShowListColumns) return;
  1838. if (!$this->createDestination() || count($this->m_listColumns) == 0) return;
  1839. foreach ($this->m_listColumns as $column)
  1840. {
  1841. $this->_addColumnToListArrayRow($column, $action, $arr, $nr, $fieldprefix, $flags, $edit, $grid);
  1842. }
  1843. }
  1844. else if ($column != null)
  1845. {
  1846. $this->_addColumnToListArrayRow($column, $action, $arr, $nr, $fieldprefix, $flags, $edit, $grid);
  1847. }
  1848. }
  1849. /**
  1850. * Adds the child attribute / field to the list row.
  1851. *
  1852. * @param string $column child attribute name
  1853. * @param String $action the action that is being performed on the node
  1854. * @param array $arr reference to the the recordlist array
  1855. * @param int $nr the current row number
  1856. * @param String $fieldprefix the fieldprefix
  1857. * @param int $flags the recordlist flags
  1858. * @param boolean $edit editing?
  1859. * @param atkDataGrid $grid data grid
  1860. */
  1861. protected function _addColumnToListArrayRow($column, $action, &$arr, $nr, $fieldprefix, $flags, $edit=false, atkDataGrid $grid=null)
  1862. {
  1863. $prefix = $fieldprefix.$this->fieldName()."_AE_";
  1864. // small trick, the destination record is in a subarray. The destination
  1865. // addToListArrayRow will not expect this though, so we have to modify the
  1866. // record a bit before passing it to the detail columns.
  1867. $backup = $arr["rows"][$nr]["record"];
  1868. $arr["rows"][$nr]["record"] = $arr["rows"][$nr]["record"][$this->fieldName()];
  1869. $p_attrib = $this->m_destInstance->getAttribute($column);
  1870. if ($p_attrib == null)
  1871. {
  1872. throw new Exception("Invalid list column {$column} for atkManyToOneRelation ".$this->getOwnerInstance()->atkNodeType().'::'.$this->fieldName());
  1873. }
  1874. $p_attrib->m_flags |= AF_HIDE_LIST;
  1875. $p_attrib->m_flags ^= AF_HIDE_LIST;
  1876. $p_attrib->addToListArrayRow($action, $arr, $nr, $prefix, $flags, $edit, $grid, null);
  1877. $arr["rows"][$nr]["record"] = $backup;
  1878. }
  1879. /**
  1880. * Adds the needed searchbox(es) for this attribute to the fields array. This
  1881. * method should only be called by the atkSearchHandler.
  1882. * Overridden method; in the integrated version, we should let the destination
  1883. * attributes hook themselves into the fieldlist instead of hooking the relation
  1884. * in it.
  1885. *
  1886. * @param array $fields The array containing fields to use in the
  1887. * extended search
  1888. * @param atkNode $node The node where the field is in
  1889. * @param array $record A record containing default values to put
  1890. * into the search fields.
  1891. * @param array $fieldprefix search / mode field prefix
  1892. */
  1893. function addToSearchformFields(&$fields, &$node, &$record, $fieldprefix = "")
  1894. {
  1895. $prefix = $fieldprefix.$this->fieldName()."_AE_";
  1896. parent::addToSearchformFields($fields, $node, $record, $prefix);
  1897. // only add extra columns when needed
  1898. if ($this->hasFlag(AF_HIDE_LIST) && !$this->m_alwaysShowListColumns) return;
  1899. if (!$this->createDestination() || count($this->m_listColumns) == 0) return;
  1900. foreach ($this->m_listColumns as $attribname)
  1901. {
  1902. $p_attrib = &$this->m_destInstance->m_attribList[$attribname];
  1903. $p_attrib->m_flags |= AF_HIDE_LIST;
  1904. $p_attrib->m_flags ^= AF_HIDE_LIST;
  1905. if (!$p_attrib->hasFlag(AF_HIDE_SEARCH))
  1906. {
  1907. $p_attrib->addToSearchformFields($fields,$node,$record[$this->fieldName()], $prefix);
  1908. }
  1909. }
  1910. }
  1911. /**
  1912. * Retrieve the sortorder for the listheader based on the
  1913. * atkColumnConfig
  1914. *
  1915. * @param atkColumnConfig $columnConfig The config that contains options for
  1916. * extended sorting and grouping to a
  1917. * recordlist.
  1918. * @return String Returns sort order ASC or DESC
  1919. */
  1920. function listHeaderSortOrder(&$columnConfig)
  1921. {
  1922. $order = $this->fieldName();
  1923. // only add desc if not one of the listColumns is used for the sorting
  1924. if (isset($columnConfig->m_colcfg[$order]) && empty($columnConfig->m_colcfg[$order]['extra']))
  1925. {
  1926. $direction = $columnConfig->getDirection($order);
  1927. if ($direction=="asc") $order.=" desc";
  1928. }
  1929. return $order;
  1930. }
  1931. /**
  1932. * Creates and registers the on change handler caller function.
  1933. * This method will be used to message listeners for a change
  1934. * event as soon as a new value is selected.
  1935. *
  1936. * @param string $fieldId
  1937. * @param string $fieldPrefix
  1938. * @param string $none
  1939. * @return String function name
  1940. */
  1941. function createOnChangeCaller($fieldId, $fieldPrefix, $none='null')
  1942. {
  1943. $function = $none;
  1944. if (count($this->m_onchangecode) > 0)
  1945. {
  1946. $function = "{$fieldId}_callChangeHandler";
  1947. $js = "
  1948. function {$function}() {
  1949. {$fieldId}_onChange(\$('{$fieldId}'));
  1950. }
  1951. ";
  1952. $this->m_onchangehandler_init = "newvalue = el.value;\n";
  1953. $page = &$this->m_ownerInstance->getPage();
  1954. $page->register_scriptcode($js);
  1955. $this->_renderChangeHandler($fieldPrefix);
  1956. }
  1957. return $function;
  1958. }
  1959. /**
  1960. * Draw the auto-complete box.
  1961. *
  1962. * @param array $record The record
  1963. * @param string $fieldPrefix The fieldprefix
  1964. * @param string $mode The mode we're in
  1965. */
  1966. function drawAutoCompleteBox($record, $fieldPrefix, $mode)
  1967. {
  1968. $this->createDestination();
  1969. // register base JavaScript code and stylesheet
  1970. $page = &$this->m_ownerInstance->getPage();
  1971. $page->register_script(atkconfig('atkroot').'atk/javascript/class.atkmanytoonerelation.js');
  1972. $this->m_ownerInstance->addStyle("atkmanytoonerelation.css");
  1973. $id = $this->getHtmlId($fieldPrefix);
  1974. // validate is this is a selectable record and if so
  1975. // retrieve the display label and hidden value
  1976. if ($this->_isSelectableRecord($record, 'select'))
  1977. {
  1978. $current = $record[$this->fieldName()];
  1979. $label = $this->m_destInstance->descriptor($record[$this->fieldName()]);
  1980. $value = $this->m_destInstance->primaryKey($record[$this->fieldName()]);
  1981. }
  1982. else
  1983. {
  1984. $current = NULL;
  1985. $label = '';
  1986. $value = '';
  1987. }
  1988. // create the widget
  1989. $links = $this->createSelectAndAutoLinks($id, $record);
  1990. $result =
  1991. '<input type="hidden" id="'.$id.'" name="'.$id.'" value="'.$value.'" />
  1992. <input type="text" id="'.$id.'_search" value="'.atk_htmlentities($label).'" class="atkmanytoonerelation_search" size="30" onfocus="this.select()" />
  1993. <img id="'.$id.'_spinner" src="atk/images/spinner.gif" style="vertical-align: middle; display: none"> '.$links.'
  1994. <div id="'.$id.'_result" style="display: none" class="atkmanytoonerelation_result"></div>';
  1995. // register JavaScript code that attaches the auto-complete behaviour to the search box
  1996. $url = partial_url($this->m_ownerInstance->atkNodeType(), $mode, 'attribute.'.$this->fieldName().'.autocomplete');
  1997. $function = $this->createOnChangeCaller($id, $fieldPrefix);
  1998. $code = "ATK.ManyToOneRelation.completeEdit('{$id}_search', '{$id}_result', '$id', '{$id}_spinner', '$url', $function, 1);";
  1999. $page->register_loadscript($code);
  2000. return $result;
  2001. }
  2002. /**
  2003. * Auto-complete partial.
  2004. *
  2005. * @param string $mode add/edit mode?
  2006. */
  2007. function partial_autocomplete($mode)
  2008. {
  2009. $searchvalue = $this->m_ownerInstance->m_postvars['value'];
  2010. if (atk_strlen($searchvalue) < $this->m_autocomplete_minchars)
  2011. {
  2012. return '<ul><li class="minimum_chars">'.sprintf($this->text('autocomplete_minimum_chars'), $this->m_autocomplete_minchars).'</li></ul>';
  2013. }
  2014. $this->createDestination();
  2015. $fieldprefix = (isset($this->m_ownerInstance->m_postvars['atkfieldprefix'])?$this->m_ownerInstance->m_postvars['atkfieldprefix']:"");
  2016. $searchvalue = $this->escapeSQL($searchvalue);
  2017. $record = $this->m_ownerInstance->updateRecord();
  2018. $filter = $this->createSearchFilter($searchvalue);
  2019. $this->addDestinationFilter($filter);
  2020. $records = $this->_getSelectableRecords($record, 'select');
  2021. if (count($records) == 0)
  2022. {
  2023. if(in_array($this->m_autocomplete_searchmode,array("exact","startswith","contains")))
  2024. $str = $this->text('autocomplete_no_results_'.$this->m_autocomplete_searchmode);
  2025. else
  2026. $str = $this->text('autocomplete_no_results');
  2027. return '<ul><li class="no_results">'.$str.'</li></ul>';
  2028. }
  2029. $result = '';
  2030. foreach ($records as $rec)
  2031. {
  2032. $option = atk_htmlentities($this->m_destInstance->descriptor($rec));
  2033. $value = $this->m_destInstance->primaryKey($rec);
  2034. $highlightedOption = $this->highlight_search_result_match($searchvalue, $option);
  2035. $result .= '
  2036. <li title="'.$option.'">
  2037. '.$highlightedOption.'
  2038. <span class="selection" style="display: none">'.$option.'</span>
  2039. <span class="value" style="display: none">'.$value.'</span>
  2040. </li>';
  2041. }
  2042. return "<ul>$result</ul>";
  2043. }
  2044. function highlight_search_result_match($search_value, $result)
  2045. {
  2046. $escaped_searchvalue = str_replace('/', '\/', preg_quote($search_value));
  2047. return preg_replace('/('.$escaped_searchvalue.')/i', '<span class="atkmanytoone_highlite">\\1</span>', $result);
  2048. }
  2049. /**
  2050. * Auto-complete search partial.
  2051. *
  2052. * @return HTML code with autocomplete result
  2053. */
  2054. function partial_autocomplete_search()
  2055. {
  2056. $this->createDestination();
  2057. $searchvalue = $this->m_ownerInstance->m_postvars['value'];
  2058. $searchvalue = $this->escapeSQL($searchvalue);
  2059. $filter = $this->createSearchFilter($searchvalue);
  2060. $this->addDestinationFilter($filter);
  2061. $record = array();
  2062. $records = $this->_getSelectableRecords($record, 'search');
  2063. $result = '';
  2064. foreach ($records as $rec)
  2065. {
  2066. $option = $this->m_destInstance->descriptor($rec);
  2067. $value = $this->m_destInstance->primaryKey($rec);
  2068. $result .= '
  2069. <li title="'.atk_htmlentities($option).'">'.atk_htmlentities($option).'</li>';
  2070. }
  2071. return "<ul>$result</ul>";
  2072. }
  2073. /**
  2074. * Creates a search filter with the given search value on the given
  2075. * descriptor fields
  2076. *
  2077. * @param String $searchvalue A searchstring
  2078. * @return String a search string (WHERE clause)
  2079. */
  2080. function createSearchFilter($searchvalue)
  2081. {
  2082. if($this->m_autocomplete_searchfields=="")
  2083. $searchfields = $this->m_destInstance->descriptorFields();
  2084. else
  2085. $searchfields = $this->m_autocomplete_searchfields;
  2086. $parts = preg_split('/\s+/', $searchvalue);
  2087. $mainFilter = array();
  2088. foreach ($parts as $part)
  2089. {
  2090. $filter = array();
  2091. foreach($searchfields as $attribname)
  2092. {
  2093. if (strstr($attribname, '.')) $table = '';
  2094. else $table = $this->m_destInstance->m_table.".";
  2095. if(!$this->m_autocomplete_search_case_sensitive)
  2096. $tmp = "LOWER(".$table.$attribname.")";
  2097. else
  2098. $tmp = $table.$attribname;
  2099. switch($this->m_autocomplete_searchmode)
  2100. {
  2101. case self::SEARCH_MODE_EXACT:
  2102. if(!$this->m_autocomplete_search_case_sensitive)
  2103. $tmp.= " = LOWER('{$part}')";
  2104. else
  2105. $tmp.= " = '{$part}'";
  2106. break;
  2107. case self::SEARCH_MODE_STARTSWITH:
  2108. if(!$this->m_autocomplete_search_case_sensitive)
  2109. $tmp.= " LIKE LOWER('{$part}%')";
  2110. else
  2111. $tmp.= " LIKE '{$part}%'";
  2112. break;
  2113. case self::SEARCH_MODE_CONTAINS:
  2114. if(!$this->m_autocomplete_search_case_sensitive)
  2115. $tmp.= " LIKE LOWER('%{$part}%')";
  2116. else
  2117. $tmp.= " LIKE '%{$part}%'";
  2118. break;
  2119. default:
  2120. $tmp.= " = LOWER('{$part}')";
  2121. }
  2122. $filter[] = $tmp;
  2123. }
  2124. if (count($filter) > 0)
  2125. $mainFilter[] = "(".implode(") OR (", $filter).")";
  2126. }
  2127. if (count($mainFilter) > 0)
  2128. $searchFilter = "(".implode(") AND (", $mainFilter).")";
  2129. else $searchFilter = "";
  2130. // When no searchfields are specified and we use the CONTAINS mode
  2131. // add a concat filter
  2132. if($this->m_autocomplete_searchmode == self::SEARCH_MODE_CONTAINS && $this->m_autocomplete_searchfields=="")
  2133. {
  2134. $filter = $this->getConcatFilter($searchvalue);
  2135. if($filter)
  2136. {
  2137. if($searchFilter!='') $searchFilter.= " OR ";
  2138. $searchFilter.= $filter;
  2139. }
  2140. }
  2141. return $searchFilter;
  2142. }
  2143. /**
  2144. * Get Concat filter
  2145. *
  2146. * @param string $searchValue Search value
  2147. * @param string $fieldaliasprefix Field alias prefix
  2148. * @return string|boolean
  2149. */
  2150. function getConcatFilter($searchValue,$fieldaliasprefix="")
  2151. {
  2152. // If we have a descriptor with multiple fields, use CONCAT
  2153. $attribs = $this->m_destInstance->descriptorFields();
  2154. if(count($attribs)>1)
  2155. {
  2156. $fields = array();
  2157. foreach($attribs as $attribname)
  2158. {
  2159. $post = '';
  2160. if (strstr($attribname, '.'))
  2161. {
  2162. if ($fieldaliasprefix != '') $table = $fieldaliasprefix.'_AE_';
  2163. else $table = '';
  2164. $post = substr($attribname,strpos($attribname,'.'));
  2165. $attribname = substr($attribname,0,strpos($attribname,'.'));
  2166. }
  2167. elseif($fieldaliasprefix!='') $table = $fieldaliasprefix.".";
  2168. else $table = $this->m_destInstance->m_table.".";
  2169. $p_attrib = $this->m_destInstance->m_attribList[$attribname];
  2170. $fields[$p_attrib->fieldName()] = $table.$p_attrib->fieldName().$post;
  2171. }
  2172. $value = $this->escapeSQL(trim($searchValue));
  2173. $value = str_replace(" " , " ", $value);
  2174. if(!$value)
  2175. {
  2176. return false;
  2177. }
  2178. else
  2179. {
  2180. $function = $this->getConcatDescriptorFunction();
  2181. if ($function != '' && method_exists($this->m_destInstance, $function))
  2182. {
  2183. $descriptordef = $this->m_destInstance->$function();
  2184. }
  2185. elseif ($this->m_destInstance->m_descTemplate != NULL)
  2186. {
  2187. $descriptordef = $this->m_destInstance->m_descTemplate;
  2188. }
  2189. elseif(method_exists($this->m_destInstance,"descriptor_def"))
  2190. {
  2191. $descriptordef = $this->m_destInstance->descriptor_def();
  2192. }
  2193. else
  2194. {
  2195. $descriptordef = $this->m_destInstance->descriptor();
  2196. }
  2197. atkimport("atk.utils.atkstringparser");
  2198. $parser = new atkStringParser($descriptordef);
  2199. $concatFields = $parser->getAllParsedFieldsAsArray($fields, true);
  2200. $concatTags = $concatFields['tags'];
  2201. $concatSeparators = $concatFields['separators'];
  2202. // to search independent of characters between tags, like spaces and comma's,
  2203. // we remove all these separators so we can search for just the concatenated tags in concat_ws [Jeroen]
  2204. foreach ($concatSeparators as $separator)
  2205. {
  2206. $value = str_replace($separator, "", $value);
  2207. }
  2208. $db = $this->getDb();
  2209. $searchcondition = "UPPER(".$db->func_concat_ws($concatTags, "", true).") LIKE UPPER('%".$value."%')";
  2210. }
  2211. return $searchcondition;
  2212. }
  2213. return false;
  2214. }
  2215. }
  2216. ?>