PageRenderTime 47ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/library/Adapto/Relation/ManyToManySelect.php

http://github.com/egeniq/adapto
PHP | 623 lines | 265 code | 103 blank | 255 comment | 32 complexity | 9a830c52bd7c2026673aef7521417d13 MD5 | raw 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-2007 Ivo Jansch
  12. * @license http://www.achievo.org/atk/licensing ATK Open Source License
  13. *
  14. */
  15. /** @internal includes and defines **/
  16. userelation("atkmanytomanyrelation");
  17. userelation('atkmanytoonerelation');
  18. define('AF_MANYTOMANYSELECT_DETAILEDIT', AF_SPECIFIC_1);
  19. define('AF_MANYTOMANYSELECT_DETAILADD', AF_SPECIFIC_2);
  20. define('AF_MANYTOMANYSELECT_NO_AUTOCOMPLETE', AF_SPECIFIC_3);
  21. /**
  22. * Many-to-many select relation.
  23. *
  24. * The relation shows allows you to add one record at a time to a many-to-many
  25. * relation using auto-completion or a select page. If a position attribute has
  26. * been set (setPositionAttribute) the order of the items can be changed using
  27. * simple drag & drop.
  28. *
  29. *
  30. * @author petercv
  31. * @package adapto
  32. * @subpackage relations
  33. */
  34. class Adapto_Relation_ManyToManySelect extends Adapto_ManyToManyRelation
  35. {
  36. const SEARCH_MODE_EXACT = atkManyToOneRelation::SEARCH_MODE_EXACT;
  37. const SEARCH_MODE_STARTSWITH = atkManyToOneRelation::SEARCH_MODE_STARTSWITH;
  38. const SEARCH_MODE_CONTAINS = atkManyToOneRelation::SEARCH_MODE_CONTAINS;
  39. /**
  40. * The many-to-one relation.
  41. *
  42. * @var atkManyToOneRelation
  43. */
  44. private $m_manyToOneRelation = null;
  45. /**
  46. * The name of the attribute/column where the position of the item in
  47. * the set should be stored.
  48. *
  49. * @var string
  50. */
  51. private $m_positionAttribute;
  52. /**
  53. * The html to be output next to a positional attribute label
  54. *
  55. * @var string
  56. */
  57. private $m_positionAttributeHtmlModifier;
  58. /**
  59. * Constructs a new many-to-many select relation.
  60. *
  61. * @param string $name The name of the relation
  62. * @param string $link The full name of the entity that is used as
  63. * intermediairy entity. The intermediairy entity is
  64. * assumed to have 2 attributes that are named
  65. * after the entitys at both ends of the relation.
  66. * For example, if entity 'project' has a M2M relation
  67. * with 'activity', then the intermediairy entity
  68. * 'project_activity' is assumed to have an attribute
  69. * named 'project' and one that is named 'activity'.
  70. * You can set your own keys by calling setLocalKey()
  71. * and setRemoteKey()
  72. * @param string $destination The full name of the entity that is the other
  73. * end of the relation.
  74. * @param int $flags Flags for the relation.
  75. */
  76. public function __construct($name, $link, $destination, $flags = 0)
  77. {
  78. parent::__construct($name, $link, $destination, $flags);
  79. $relation = new Adapto_ManyToOneRelation($this->fieldName() . '_m2msr_add', $this->m_destination, AF_MANYTOONE_AUTOCOMPLETE | AF_HIDE);
  80. $relation->setDisabledModes(DISABLED_VIEW | DISABLED_EDIT);
  81. $relation->setLoadType(NOLOAD);
  82. $relation->setStorageType(NOSTORE);
  83. $relation->setNoneLabel($this->text('list_null_value_obligatory'));
  84. $this->m_manyToOneRelation = $relation;
  85. }
  86. /**
  87. * Initialize.
  88. */
  89. public function init()
  90. {
  91. $this->getOwnerInstance()->add($this->getManyToOneRelation());
  92. }
  93. /**
  94. * Return the many-to-one relation we will use for the selection
  95. * of new records etc.
  96. *
  97. * @return atkManyToOneRelation
  98. */
  99. protected function getManyToOneRelation()
  100. {
  101. return $this->m_manyToOneRelation;
  102. }
  103. /**
  104. * Create the instance of the destination and copy the destination to
  105. * the the many to one relation
  106. *
  107. * If succesful, the instance is stored in the m_destInstance member variable.
  108. *
  109. * @return boolean true if succesful, false if something went wrong.
  110. */
  111. public function createDestination()
  112. {
  113. $result = parent::createDestination();
  114. $this->getManyToOneRelation()->m_destInstance = $this->m_destInstance;
  115. return $result;
  116. }
  117. /**
  118. * Order selected records in the same way as the selected keys. We only do
  119. * this if the position attribute has been set.
  120. *
  121. * @param array $selectedRecords selected records
  122. * @param array $selectedKey selected keys
  123. */
  124. private function orderSelectedRecords(&$selectedRecords, $selectedKeys)
  125. {
  126. $orderedRecords = array();
  127. foreach ($selectedKeys as $key) {
  128. foreach ($selectedRecords as $record) {
  129. if ($key == $this->getDestination()->primaryKey($record)) {
  130. $orderedRecords[] = $record;
  131. }
  132. }
  133. }
  134. $selectedRecords = $orderedRecords;
  135. }
  136. /**
  137. * Return a piece of html code to edit the attribute.
  138. *
  139. * @param array $record The record that holds the value for this attribute.
  140. * @param String $fieldprefix The fieldprefix to put in front of the name
  141. * of any html form element for this attribute.
  142. * @param String $mode The mode we're in ('add' or 'edit')
  143. *
  144. * @return string piece of html code
  145. */
  146. public function edit($record, $fieldprefix = "", $mode = "")
  147. {
  148. if ($this->hasFlag(AF_MANYTOMANYSELECT_NO_AUTOCOMPLETE)) {
  149. $this->getManyToOneRelation()->removeFlag(AF_MANYTOONE_AUTOCOMPLETE);
  150. }
  151. $this->createDestination();
  152. $this->createLink();
  153. $this->getOwnerInstance()->getPage()->register_script(Adapto_Config::getGlobal('atkroot') . 'atk/javascript/class.' . strtolower(__CLASS__) . '.js');
  154. $this->getOwnerInstance()->addStyle('atkmanytomanyselectrelation.css');
  155. $id = $this->getHtmlId($fieldprefix);
  156. $selectId = "{$id}_selection";
  157. $selectedKeys = $this->getSelectedKeys($record, $id);
  158. $selectedRecords = array();
  159. if (count($selectedKeys) > 0) {
  160. $selector = '(' . implode(') OR (', $selectedKeys) . ')';
  161. $selectedRecords = $this->getDestination()->select($selector)->includes($this->getDestination()->descriptorFields());
  162. $this->orderSelectedRecords($selectedRecords, $selectedKeys);
  163. }
  164. $result = '<input type="hidden" name="' . $this->getHtmlId($fieldprefix) . '" value="" />' . // Post an empty value if none selected (instead of not posting anything)
  165. '<div class="atkmanytomanyselectrelation">
  166. <ul id="' . $selectId . '" class="atkmanytomanyselectrelation-selection">';
  167. foreach ($selectedRecords as $selectedRecord) {
  168. $result .= $this->renderSelectedRecord($selectedRecord, $fieldprefix);
  169. }
  170. $result .= $this->renderAdditionField($record, $fieldprefix, $mode);
  171. $result .= '
  172. </ul>
  173. </div>';
  174. if (($this->hasFlag(AF_MANYTOMANYSELECT_DETAILADD)) && ($this->m_destInstance->allowed("add")))
  175. $result .= href(dispatch_url($this->m_destination, "add", array('atkfilter' => 'clear', "atkpkret" => $id . "_newsel")), $this->getAddLabel(),
  176. SESSION_NESTED) . "\n";
  177. if ($this->hasPositionAttribute()) {
  178. $this->getOwnerInstance()->getPage()->register_loadscript("ATK.ManyToManySelectRelation.makeItemsSortable('{$selectId}');");
  179. }
  180. return $result;
  181. }
  182. /**
  183. * Return the selected keys for a given record
  184. *
  185. * @param array $record The record that holds the value for this attribute.
  186. * @param String $id is the html id of the relation
  187. * @param int $uniqueFilter is the type of array_unique filter to use on
  188. * the results. Use boolean false to dissable
  189. *
  190. * @return array of selected keys in the order they were submitted
  191. */
  192. public function getSelectedKeys($record, $id, $enforceUnique = true)
  193. {
  194. // Get Existing selected records
  195. $selectedKeys = $this->getSelectedRecords($record);
  196. // Get records added this time
  197. if (isset($record[$this->getManyToOneRelation()->fieldName()]) && is_array($record[$this->getManyToOneRelation()->fieldName()])) {
  198. $selectedKeys[] = $this->getDestination()->primaryKey($record[$this->getManyToOneRelation()->fieldName()]);
  199. }
  200. // Get New Selection records
  201. if (isset($this->getOwnerInstance()->m_postvars[$id . '_newsel'])) {
  202. $selectedKeys[] = $this->getOwnerInstance()->m_postvars[$id . '_newsel'];
  203. }
  204. // Ensure we're only adding an item once
  205. if ($enforceUnique && is_array($selectedKeys) && count($selectedKeys)) {
  206. $selectedKeys = array_unique($selectedKeys);
  207. }
  208. return $selectedKeys;
  209. }
  210. /**
  211. * Load function.
  212. *
  213. * @param atkDb $db database instance.
  214. * @param array $record record
  215. * @param string $mode load mode
  216. */
  217. public function load(atkDb $db, $record, $mode)
  218. {
  219. if (!$this->hasPositionAttribute()) {
  220. return parent::load($db, $record, $mode);
  221. }
  222. $this->createLink();
  223. $where = $this->_getLoadWhereClause($record);
  224. $link = &$this->getLink();
  225. return $link->select()->where($where)->orderBy($link->getTable() . '.' . $this->getPositionAttribute())->allRows();
  226. }
  227. /**
  228. * Perform the create action on a record that is new
  229. *
  230. * @param array $selectedKey the selected keys
  231. * @param array $selectedRecord the selected record
  232. * @param array $ownerRecord the owner record
  233. * @param int $index the index of the item in the set
  234. * @access protected
  235. * @return array the newly created record
  236. */
  237. protected function _createRecord($selectedKey, $selectedRecord, $ownerRecord, $index)
  238. {
  239. $record = parent::_createRecord($selectedKey, $selectedRecord, $ownerRecord, $index);
  240. if ($this->hasPositionAttribute()) {
  241. $record[$this->getPositionAttribute()] = $index + 1;
  242. }
  243. return $record;
  244. }
  245. /**
  246. * Perform the update action on a record that's been changed
  247. *
  248. * @param array $record the record that has been changed
  249. * @param int $index the index of the item in the set
  250. * @access protected
  251. * @return boolean true if the update was performed successfuly, false if there were issues
  252. */
  253. protected function _updateRecord($record, $index)
  254. {
  255. // If the parent class didn't manage to update the record
  256. // then don't attempt to perform this update
  257. if (!parent::_updateRecord($record, $index)) {
  258. return false;
  259. }
  260. if ($this->hasPositionAttribute()) {
  261. $record[$this->getPositionAttribute()] = $index + 1;
  262. if (!$this->getLink()->updateDb($record, true, '', array($this->getPositionAttribute()))) {
  263. return false;
  264. }
  265. }
  266. return true;
  267. }
  268. /**
  269. * Render selected record.
  270. *
  271. * @param array $record selected record
  272. * @param string $fieldprefix field prefix
  273. */
  274. protected function renderSelectedRecord($record, $fieldprefix)
  275. {
  276. $name = $this->getHtmlId($fieldprefix) . '[][' . $this->getRemoteKey() . ']';
  277. $key = $record[$this->getDestination()->primaryKeyField()];
  278. // Get the descriptor and ensure it's presentible
  279. $descriptor = nl2br(Adapto_htmlentities($this->getdestination()->descriptor($record)));
  280. // Build the record
  281. $result = '
  282. <li class="atkmanytomanyselectrelation-selected">
  283. <input type="hidden" name="' . $name . '" value="' . Adapto_htmlentities($key) . '"/>
  284. <span>' . $descriptor . '</span>
  285. ' . $this->renderSelectedRecordActions($record) . '
  286. </li>
  287. ';
  288. return $result;
  289. }
  290. /*
  291. * Renders the action links for a given record
  292. *
  293. * @param array $record is the selected record
  294. * @return string the actions in their html link form
  295. */
  296. protected function renderSelectedRecordActions($record)
  297. {
  298. $actions = array();
  299. if ($this->hasFlag(AF_MANYTOMANYSELECT_DETAILEDIT) && $this->getDestination()->allowed('edit', $record)) {
  300. $actions[] = 'edit';
  301. }
  302. if (!$this->getLink()->hasFlag(EF_NO_DELETE)) {
  303. $actions[] = 'delete';
  304. }
  305. $this->recordActions($record, $actions);
  306. // Call the renderButton action for those actions
  307. $actionLinks = array();
  308. foreach ($actions as $action) {
  309. $actionLink = $this->getActionLink($action, $record);
  310. if ($actionLink != null)
  311. $actionLinks[] = $actionLink;
  312. }
  313. $htmlActionLinks = '';
  314. if (count($actionLink)) {
  315. $htmlActionLinks = '(' . implode(' / ', $actionLinks) . ')';
  316. }
  317. return $htmlActionLinks;
  318. }
  319. /**
  320. * This method returns the HTML for the link of a certain action
  321. *
  322. * @param string $action
  323. * @param array $record
  324. * @return string
  325. */
  326. protected function getActionLink($action, $record)
  327. {
  328. $actionMethod = "get{$action}ActionLink";
  329. if (method_exists($this, $actionMethod)) {
  330. return $this->$actionMethod($record);
  331. } else {
  332. atkwarning('Missing ' . $actionMethod . ' method on manytomanyselectrelation. ');
  333. }
  334. }
  335. /**
  336. * The default edit link
  337. *
  338. * @param array $record
  339. * @return string
  340. */
  341. protected function getEditActionLink($record)
  342. {
  343. return href(dispatch_url($this->getdestination()->atkentitytype(), 'edit', array('atkselector' => $this->getdestination()->primarykey($record))),
  344. $this->text('edit'), SESSION_NESTED, true);
  345. }
  346. /**
  347. * The default delete link
  348. *
  349. * @param array $record
  350. * @return string
  351. */
  352. protected function getDeleteActionLink($record)
  353. {
  354. return '<a href="javascript:void(0)" onclick="ATK.ManyToManySelectRelation.deleteItem(this); return false;">' . $this->text('delete') . '</a>';
  355. }
  356. /**
  357. * Function that is called for each record in a recordlist, to determine
  358. * what actions may be performed on the record.
  359. *
  360. * @param array $record The record for which the actions need to be
  361. * determined.
  362. * @param array &$actions Reference to an array with the already defined
  363. * actions.
  364. */
  365. function recordActions($record, &$actions)
  366. {
  367. }
  368. /**
  369. * Render addition field.
  370. *
  371. * @param string $fieldprefix field prefix
  372. * @param string $mode
  373. */
  374. protected function renderAdditionField($record, $fieldprefix, $mode)
  375. {
  376. if ($this->getLink()->hasFlag(EF_NO_ADD)) {
  377. return '';
  378. }
  379. $url = partial_url($this->getOwnerInstance()->atkEntityType(), $mode, 'attribute.' . $this->fieldName() . '.selectedrecord',
  380. array('fieldprefix' => $fieldprefix));
  381. $relation = $this->getManyToOneRelation();
  382. $hasPositionAttribute = $this->hasPositionAttribute() ? 'true' : 'false';
  383. $relation->addOnChangeHandler("ATK.ManyToManySelectRelation.add(el, '{$url}', {$hasPositionAttribute});");
  384. return '<li class="atkmanytomanyselectrelation-addition">' . $relation->edit($record, $fieldprefix, $mode) . '</li>';
  385. }
  386. /**
  387. * Partial selected record.
  388. */
  389. public function partial_selectedrecord()
  390. {
  391. $this->createDestination();
  392. $this->createLink();
  393. $fieldprefix = $this->getOwnerInstance()->m_postvars['fieldprefix'];
  394. $selector = $this->getOwnerInstance()->m_postvars['selector'];
  395. if (empty($selector))
  396. return '';
  397. $record = $this->getDestination()->select($selector)->includes($this->getDestination()->descriptorFields())->firstRow();
  398. return $this->renderSelectedRecord($record, $fieldprefix);
  399. }
  400. /**
  401. * Set the searchfields for the autocompletion. By default the
  402. * descriptor fields are used.
  403. *
  404. * @param array $searchFields
  405. */
  406. public function setAutoCompleteSearchFields($searchFields)
  407. {
  408. $this->getManyToOneRelation()->setAutoCompleteSearchFields($searchFields);
  409. }
  410. /**
  411. * Set the searchmode for the autocompletion:
  412. * exact, startswith(default) or contains.
  413. *
  414. * @param array $mode
  415. */
  416. public function setAutoCompleteSearchMode($mode)
  417. {
  418. $this->getManyToOneRelation()->setAutoCompleteSearchMode($mode);
  419. }
  420. /**
  421. * Set the case-sensitivity for the autocompletion search (true or false).
  422. *
  423. * @param array $caseSensitive
  424. */
  425. public function setAutoCompleteCaseSensitive($caseSensitive)
  426. {
  427. $this->getManyToOneRelation()->setAutoCaseSensitive($caseSensitive);
  428. }
  429. /**
  430. * Sets the minimum number of characters before auto-completion kicks in.
  431. *
  432. * @param int $chars
  433. */
  434. public function setAutoCompleteMinChars($chars)
  435. {
  436. $this->getManyToOneRelation()->setAutoCompleteMinChars($chars);
  437. }
  438. /**
  439. * Adds a filter value to the destination filter.
  440. *
  441. * @param string $filter
  442. */
  443. public function addDestinationFilter($filter)
  444. {
  445. return $this->getManyToOneRelation()->addDestinationFilter($filter);
  446. }
  447. /**
  448. * Sets the destination filter.
  449. *
  450. * @param string $filter
  451. */
  452. public function setDestinationFilter($filter)
  453. {
  454. return $this->getManyToOneRelation()->setDestinationFilter($filter);
  455. }
  456. /**
  457. * Set the positional attribute/column of the many to many join. It is the column
  458. * in the join table that denotes the position of the item in the set.
  459. *
  460. * @param string $attr the position attribute/column name of the join
  461. * @param string $htmlIdentifier is the html string to add to the end of the label.
  462. * Defaults to an up down image.
  463. * @return void
  464. */
  465. public function setPositionAttribute($attr, $htmlIdentifier = null)
  466. {
  467. $this->m_positionAttribute = $attr;
  468. $this->m_positionAttributeHtmlModifier = $htmlIdentifier;
  469. }
  470. /**
  471. * Get the positional attribute of the many to many join. It is the column
  472. * in the join table that denotes the position of the item in the set.
  473. *
  474. * @access public
  475. * @return string the position column name of the join
  476. */
  477. public function getPositionAttribute()
  478. {
  479. return $this->m_positionAttribute;
  480. }
  481. /**
  482. * Check if positon attribute is set
  483. *
  484. * @return boolean true if the position attribute has been set
  485. */
  486. public function hasPositionAttribute()
  487. {
  488. return $this->getPositionAttribute() != null;
  489. }
  490. /**
  491. * Get the HTML label of the attribute.
  492. *
  493. * The difference with the label() method is that the label method always
  494. * returns the HTML label, while the getLabel() method is 'smart', by
  495. * taking the AF_NOLABEL and AF_BLANKLABEL flags into account.
  496. *
  497. * @param array $record The record holding the value for this attribute.
  498. * @param string $mode The mode ("add", "edit" or "view")
  499. * @return String The HTML compatible label for this attribute, or an
  500. * empty string if the label should be blank, or NULL if no
  501. * label at all should be displayed.
  502. */
  503. function getLabel($record = array(), $mode = '')
  504. {
  505. $additional = '';
  506. if ($this->hasPositionAttribute()) {
  507. if (is_null($this->m_positionAttributeHtmlModifier)) {
  508. $additional = ' <img src="' . Adapto_Config::getGlobal('atkroot')
  509. . 'atk/images/up-down.gif"
  510. width="10" height="10"
  511. alt="Sortable Ordering"
  512. title="Sortable Ordering" />';
  513. } else {
  514. $additional = $this->m_positionAttributeHtmlModifier;
  515. }
  516. }
  517. return parent::getLabel($record, $mode) . $additional;
  518. }
  519. }