PageRenderTime 68ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 1ms

/library/Adapto/Relation/OneToMany.php

http://github.com/egeniq/adapto
PHP | 1489 lines | 872 code | 147 blank | 470 comment | 114 complexity | 057120989577caf1c0ab8e6cb72ed818 MD5 | raw file

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

  1. <?php
  2. /**
  3. * This file is part of the Adapto Toolkit.
  4. * Detailed copyright and licensing information can be found
  5. * in the doc/COPYRIGHT and doc/LICENSE files which should be
  6. * included in the distribution.
  7. *
  8. * @package adapto
  9. * @subpackage relations
  10. *
  11. * @copyright (c)2000-2004 Ivo Jansch
  12. * @license http://www.achievo.org/atk/licensing ATK Open Source License
  13. *
  14. */
  15. /**
  16. * @internal Include base class.
  17. */
  18. userelation("atkrelation");
  19. /**
  20. * Only allow deletion of master item when there are no child records
  21. */
  22. define("AF_RESTRICTED_DELETE", AF_SPECIFIC_1);
  23. /**
  24. * Use pop-up dialogs for adding records
  25. */
  26. define("AF_ONETOMANY_ADD_DIALOG", AF_SPECIFIC_2);
  27. /**
  28. * Use pop-up dialog for whatever a new record must be copied or must be added.
  29. */
  30. define("AF_ONETOMANY_ADDORCOPY_DIALOG", AF_SPECIFIC_3);
  31. /**
  32. * Show the OTM in add mode.
  33. * Warning! Not on by default because this only works in simple cases.
  34. *
  35. * What ATK does is, when you are in OTM add mode, it stores everything you add
  36. * in the session, then when you're actually saving, it persists everything to
  37. * the database.
  38. *
  39. * However, as you may guess, not having an id will lead to strange results for:
  40. * - Entitys that use the foreign key in their descriptor
  41. * - Entitys with unique records (AF_UNIQUE always just checks the database)
  42. * - Combined primary keys
  43. */
  44. define("AF_ONETOMANY_SHOW_ADD", AF_SPECIFIC_4);
  45. /**
  46. * Implementation of one-to-many relationships.
  47. *
  48. * Can be used to create one to many relations ('1 library has N books').
  49. * A common term for this type of relation is a master-detail relationship.
  50. * The detailrecords can be edited inline.
  51. *
  52. * @author ijansch
  53. * @package adapto
  54. * @subpackage relations
  55. *
  56. */
  57. class Adapto_Relation_OneToMany extends Adapto_Relation
  58. {
  59. public $m_recordlist; // defaulted to public
  60. /**
  61. * Instance of atk.recordlist.atkrecordlistcache
  62. * @access private
  63. * @var Object
  64. */
  65. public $m_recordlistcache; // defaulted to public
  66. /**
  67. * List of keys from the destination entity that refer to the master record.
  68. * @access private
  69. * @var array
  70. */
  71. public $m_refKey = array(); // defaulted to public
  72. /**
  73. * The maximum number of detail records. If the number of detail records
  74. * exceeds this maximum, the link for adding new details disappears.
  75. * @access private
  76. * @var int
  77. */
  78. public $m_maxRecords = 0; // defaulted to public
  79. /**
  80. * The load method might build a custom filter. When it does, we might want
  81. * to use it again in other methods.
  82. * @access private
  83. * @var string
  84. */
  85. public $m_loadFilter = ""; // defaulted to public
  86. /**
  87. * The field that the foreign key in the destination points to.
  88. * Is set to the primary key if no value is provided.
  89. * @access private
  90. * @var array;
  91. */
  92. public $m_ownerFields = array(); // defaulted to public
  93. /**
  94. * Use destination filter for autolink add link?
  95. *
  96. * @access private
  97. * @var boolean
  98. */
  99. public $m_useFilterForAddLink = true; // defaulted to public
  100. /**
  101. * Use destination filter for edit link (edit button)?
  102. *
  103. * @access private
  104. * @var boolean
  105. */
  106. public $m_useFilterForEditLink = true; // defaulted to public
  107. /**
  108. * Use referential key for load filter?
  109. *
  110. * @var boolean
  111. */
  112. protected $m_useRefKeyForFilter = true;
  113. /**
  114. * Function names for recordlist header/footer generation
  115. *
  116. * @access private
  117. * @var string
  118. */
  119. public $m_headerName = ""; // defaulted to public
  120. public $m_footerName = ""; // defaulted to public
  121. /**
  122. * Fields to exclude in the grid
  123. *
  124. * @access private
  125. * @var array
  126. */
  127. public $m_excludes = array(); // defaulted to public
  128. /**
  129. * Default constructor.
  130. *
  131. * <b>Example: </b> Suppose a department has many employees. To edit the
  132. * list of employees in a department, this relationship can be built like
  133. * this, in the department entity:
  134. * <code>
  135. * $this->add(new Adapto_Relation_OneToMany("employees", "mymod.employee", "department_id"));
  136. * </code>
  137. *
  138. * @param String $name The unique name of this relation within an entity.
  139. * In contrast with most other attributes, the name
  140. * does not correspond to a database field. (Because
  141. * in one2many relations, the databasefield that
  142. * stores the link, is in the destination entity and not
  143. * in the owner entity).
  144. * @param String $destination The entity to which the relationship is made
  145. * (in module.entityname notation).
  146. * @param mixed $refKey For regular oneToMany relationships, $refKey is
  147. * name of the referential key in the destination
  148. * entity. In the case of multi-foreign key
  149. * relationships, $refKey can be an array of fields.
  150. * @param int $flags Attribute flags that influence this attributes'
  151. * behavior.
  152. */
  153. public function __construct($name, $destination, $refKey = "", $flags = 0)
  154. {
  155. parent::__construct($name, $destination, $flags | AF_NO_SORT | AF_HIDE_ADD);
  156. if (is_array($refKey)) {
  157. $this->m_refKey = $refKey;
  158. } else if (empty($refKey)) {
  159. $this->m_refKey = array();
  160. } else {
  161. $this->m_refKey[] = $refKey;
  162. }
  163. $this->setGridExcludes($this->m_refKey);
  164. }
  165. public function addFlag($flag)
  166. {
  167. $ret = parent::addFlag($flag);
  168. if (hasFlag($this->m_flags, AF_ONETOMANY_SHOW_ADD)) {
  169. $this->removeFlag(AF_HIDE_ADD);
  170. }
  171. return $ret;
  172. }
  173. /**
  174. * Set the ownerfields
  175. *
  176. * @param array $ownerfields
  177. */
  178. function setOwnerFields($ownerfields)
  179. {
  180. $this->m_ownerFields = $ownerfields;
  181. }
  182. /**
  183. * Get the owner fields
  184. *
  185. * @return mixed Array or String with ownerfield(s)
  186. */
  187. function getOwnerFields()
  188. {
  189. if (is_array($this->m_ownerFields) && count($this->m_ownerFields) > 0) {
  190. return $this->m_ownerFields;
  191. }
  192. return $this->m_ownerInstance->m_primaryKey;
  193. }
  194. /**
  195. * Use destination filter for auto add link?
  196. *
  197. * @param boolean $useFilter use destination filter for add link?
  198. */
  199. function setUseFilterForAddLink($useFilter)
  200. {
  201. $this->m_useFilterForAddLink = $useFilter;
  202. }
  203. /**
  204. * Use destination filter for edit link (edit button)?
  205. *
  206. * @param boolean $useFilter use destnation filter for edit link (edit button)?
  207. */
  208. function setUseFilterForEditLink($useFilter)
  209. {
  210. $this->m_useFilterForEditLink = $useFilter;
  211. }
  212. /**
  213. * Use referential key for filtering the records. If you disable this only the
  214. * explicitly set destination filter will be used.
  215. *
  216. * @param bool $useRefKey
  217. */
  218. public function setUseRefKeyForFilter($useRefKey)
  219. {
  220. $this->m_useRefKeyForFilter = $useRefKey;
  221. }
  222. /**
  223. * Create the datagrid for the edit and display actions. The datagrid is
  224. * configured with the correct entity filter, excludes etc.
  225. *
  226. * The datagrid uses for both the edit and display actions the partial_grid
  227. * method to update it's view.
  228. *
  229. * @param array $record the record
  230. * @param string $mode the mode
  231. * @param string $action the action
  232. * @param boolean $useSession use session?
  233. *
  234. * @return atkDataGrid grid
  235. */
  236. protected function createGrid($record, $mode, $action, $useSession = true)
  237. {
  238. $this->createDestination();
  239. $grid = atkDataGrid::create($this->m_destInstance, str_replace('.', '_', $this->getOwnerInstance()->atkEntityType()) . '_' . $this->fieldName() . '_grid',
  240. null, true, $useSession);
  241. $grid->setMode($mode);
  242. $grid->setMasterEntity($this->getOwnerInstance());
  243. $grid->setMasterRecord($record);
  244. $grid->removeFlag(atkDataGrid::EXTENDED_SEARCH);
  245. if ($action == 'view') {
  246. $grid->removeFlag(atkDataGrid::MULTI_RECORD_ACTIONS);
  247. $grid->removeFlag(atkDataGrid::MULTI_RECORD_PRIORITY_ACTIONS);
  248. $grid->removeFlag(atkDataGrid::LOCKING);
  249. }
  250. $grid->setBaseUrl(partial_url($this->getOwnerInstance()->atkEntityType(), $action, 'attribute.' . $this->fieldName() . '.grid'));
  251. $grid->setExcludes($this->getGridExcludes());
  252. $grid->addFilter($this->_getLoadWhereClause($record));
  253. if ($this->m_destinationFilter != '') {
  254. $grid->addFilter($this->parseFilter($this->m_destinationFilter, $record));
  255. }
  256. $this->modifyDataGrid($grid, atkDataGrid::CREATE);
  257. return $grid;
  258. }
  259. /**
  260. * Updates the datagrid for the edit and display actions.
  261. *
  262. * @return string grid html
  263. */
  264. public function partial_grid()
  265. {
  266. $this->createDestination();
  267. $entity = $this->getDestination();
  268. try {
  269. $grid = atkDataGrid::resume($entity);
  270. $this->modifyDataGrid($grid, atkDataGrid::RESUME);
  271. } catch (Exception $e) {
  272. $grid = atkDataGrid::create($entity);
  273. $this->modifyDataGrid($grid, atkDataGrid::CREATE);
  274. }
  275. return $grid->render();
  276. }
  277. /**
  278. * Modify grid.
  279. *
  280. * @param atkDataGrid $grid grid
  281. * @param int $mode CREATE or RESUME
  282. */
  283. protected function modifyDataGrid(atkDataGrid $grid, $mode)
  284. {
  285. $method = 'modifyDataGrid';
  286. if (method_exists($this->getDestination(), $method)) {
  287. $this->getDestination()->$method($grid, $mode);
  288. }
  289. $method = $this->fieldName() . '_modifyDataGrid';
  290. if (method_exists($this->getOwnerInstance(), $method)) {
  291. $this->getOwnerInstance()->$method($grid, $mode);
  292. }
  293. }
  294. /**
  295. * Returns a displayable string for this value, to be used in HTML pages.
  296. *
  297. * The Adapto_Relation_OneToMany displays a list of detail records in "view"
  298. * mode, in the form of a read-only data grid. In "list" mode, a plain
  299. * list of detail record descriptors is displayed.
  300. *
  301. * @param array $record The record that holds the value for this attribute
  302. * @param String $mode The display mode ("view" for viewpages, or "list"
  303. * for displaying in recordlists)
  304. * @return String HTML String
  305. */
  306. public function display($record, $mode = "list")
  307. {
  308. // for the view mode we use the datagrid and load the records ourselves
  309. if ($mode == 'view' || ($mode == 'edit' && $this->hasFlag(AF_READONLY_EDIT))) {
  310. $grid = $this->createGrid($record, 'admin', 'view');
  311. $grid->loadRecords(); // load records early
  312. $grid->setEmbedded(false);
  313. // no records
  314. if ($grid->getCount() == 0) {
  315. if (!in_array($mode, array("csv", "plain"))) {
  316. return $this->text("none");
  317. } else {
  318. return '';
  319. }
  320. }
  321. $actions = array();
  322. if (!$this->m_destInstance->hasFlag(EF_NO_VIEW)) {
  323. $actions['view'] = dispatch_url($this->m_destination, "view", array("atkselector" => "[pk]", "atkfilter" => $this->m_destinationFilter));
  324. }
  325. $grid->setDefaultActions($actions);
  326. return $grid->render();
  327. }
  328. // records should be loaded inside the load method
  329. $records = $record[$this->fieldName()];
  330. // no records
  331. if (count($records) == 0) {
  332. return $this->text('none');
  333. }
  334. if ($mode == "list") // list mode
  335. {
  336. $result = "<ul>";
  337. foreach ($records as $current) {
  338. $result .= sprintf("<li>%s</li>", $this->m_destInstance->descriptor($current));
  339. }
  340. $result .= "</ul>";
  341. return $result;
  342. } else // cvs / plain mode
  343. {
  344. $result = "";
  345. foreach ($records as $i => $current) {
  346. $result .= ($i > 0 ? ', ' : '') . $this->m_destInstance->descriptor($current);
  347. }
  348. return $result;
  349. }
  350. }
  351. /**
  352. * Returns a piece of html code that can be used in a form to edit this
  353. * attribute's value.
  354. *
  355. * The Adapto_Relation_OneToMany's edit method returns a recordlist in which
  356. * detail records can be removed, added and edited.
  357. *
  358. * @param array $record The record that holds the value for this attribute.
  359. * @param String $fieldprefix The fieldprefix to put in front of the name
  360. * of any html form element for this attribute.
  361. * @param String $mode The mode we're in ('add' or 'edit')
  362. *
  363. * @return String A piece of htmlcode for editing this attribute
  364. */
  365. public function edit($record = "", $fieldprefix = "", $mode = '')
  366. {
  367. $page = Adapto_ClassLoader::getInstance('atk.ui.atkpage');
  368. $page->register_script(Adapto_Config::getGlobal("atkroot") . "atk/javascript/tools.js");
  369. $page->register_script(Adapto_Config::getGlobal("atkroot") . "atk/javascript/class.atkonetomanyrelation.js");
  370. $grid = $this->createGrid($record, 'admin', $mode);
  371. $params = array();
  372. if ($this->m_useFilterForEditLink && $this->m_destinationFilter != "") {
  373. $params["atkfilter"] = $this->m_destinationFilter;
  374. }
  375. if ($mode === 'add') {
  376. //All actions in the grid should be done in session store mode
  377. $params['atkstore'] = 'session';
  378. $params['atkstore_key'] = $this->getSessionStoreKey();
  379. // Make the grid use the OTM Session Grid Handler
  380. // which makes the grid get it's records from the session.
  381. $handler = new Adapto_OneToManyRelationSessionGridHandler($this->getSessionStoreKey());
  382. $grid->setCountHandler(array($handler, 'countHandlerForAdd'));
  383. $grid->setSelectHandler(array($handler, 'selectHandlerForAdd'));
  384. // No searching and sorting on session data... for now...
  385. $grid->removeFlag(atkDataGrid::SEARCH);
  386. $grid->removeFlag(atkDataGrid::SORT);
  387. $grid->removeFlag(atkDataGrid::EXTENDED_SORT);
  388. }
  389. $actions = $this->m_destInstance->defaultActions("relation", $params);
  390. $grid->setDefaultActions($actions);
  391. $grid->loadRecords(); // force early load of records
  392. $output = $this->editHeader($record, $grid->getRecords()) . $grid->render() . $this->editFooter($record, $grid->getRecords());
  393. if ($this->m_destInstance->allowed("add")) {
  394. $this->_addAddToEditOutput($output, $grid->getRecords(), $record, $mode, $fieldprefix);
  395. }
  396. return $output;
  397. }
  398. /**
  399. * Adds the 'add' option to the onetomany, either integrated or as a link
  400. *
  401. * @param String $output The HTML output of the edit function
  402. * @param Array $myrecords The records that are loaded into the recordlist
  403. * @param array $record The master record that is being edited.
  404. */
  405. function _addAddToEditOutput(&$output, $myrecords, $record, $mode = "", $fieldprefix = "")
  406. {
  407. $add_link = '';
  408. if (!$this->getDestination()->hasFlag(EF_NO_ADD)) {
  409. $add_link = $this->_getAddLink($myrecords, $record, true, $mode, $fieldprefix);
  410. }
  411. $add_link .= '<br />';
  412. if (Adapto_Config::getGlobal("onetomany_addlink_position", "bottom") == "top") {
  413. $output = $add_link . $output;
  414. } else if (Adapto_Config::getGlobal("onetomany_addlink_position", "bottom") == "bottom") {
  415. $output .= $add_link;
  416. }
  417. }
  418. /**
  419. * Get the buttons for the embedded mode of the onetomany relation.
  420. * @todo Move this to a template
  421. * @return String The HTML buttons
  422. */
  423. function _getEmbeddedButtons()
  424. {
  425. $fname = $this->fieldName();
  426. $output .= '<input type="submit" class="otm_add" name="' . $fname . '_save" value="' . atktext("add") . '">';
  427. return $output . '<input type="button" onClick="toggleAddForm(\'' . $fname
  428. . "_integrated',
  429. '" . $fname
  430. . "_integrated_link');\"
  431. class=\"otm_add\" name=\"" . $fname . "_cancel\" value=\"" . atktext("cancel") . '">';
  432. }
  433. /**
  434. * Internal function to get the add link for a Adapto_Relation_OneToMany
  435. * @param Array $myrecords The load of all attributes (see comment in edit() code)
  436. * @param Array $record The record that holds the value for this attribute.
  437. * @param bool $saveform Save the form values?
  438. * @return String The link to add records to the onetomany
  439. */
  440. function _getAddLink($myrecords, $record, $saveform = true, $mode = "", $fieldprefix = "")
  441. {
  442. $params = array();
  443. if ($mode === 'add') {
  444. $ownerfields = $this->getOwnerFields();
  445. foreach ($ownerfields as $ownerfield) {
  446. $record[$ownerfield] = $this->getSessionAddFakeId();
  447. }
  448. $params['atkstore'] = 'session';
  449. $params['atkstore_key'] = $this->getSessionStoreKey();
  450. }
  451. $is_addorcopy_mode = $this->hasFlag(AF_ONETOMANY_ADDORCOPY_DIALOG) || $this->m_destInstance->hasFlag(EF_ADDORCOPY_DIALOG);
  452. if ($is_addorcopy_mode) {
  453. $filter = $this->getAddFilterString($record);
  454. $showDialog = atkAddOrCopyHandler::hasCopyableRecords($this->m_destInstance, $filter);
  455. if ($showDialog) {
  456. return $this->_getDialogAddLink($record, 'addorcopy', $params);
  457. }
  458. }
  459. $is_dialog_mode = $this->hasFlag(AF_ONETOMANY_ADD_DIALOG) || $this->m_destInstance->hasFlag(EF_ADD_DIALOG);
  460. if ($is_dialog_mode)
  461. return $this->_getDialogAddLink($record, 'add', $params);
  462. else
  463. return $this->_getNestedAddLink($myrecords, $record, $saveform, $fieldprefix, $params);
  464. }
  465. /**
  466. * Return a fake ID for adding to the session.
  467. *
  468. * We use a high negative number because we have to sneak this in
  469. * as if it's a REAL id for the owner, tricking MTOs in the destination
  470. * that point back to us into thinking they already have an id.
  471. * But we also have to make sure it's recognizable, so when we
  472. * persist the records from the session to the database, then
  473. * we can set the proper id.
  474. *
  475. * @return string
  476. */
  477. public function getSessionAddFakeId()
  478. {
  479. return "-999999";
  480. }
  481. /**
  482. * Return the key to use when storing records for the OTM destination
  483. * in the session if the OTM is used in add mode.
  484. *
  485. * @return string
  486. */
  487. public function getSessionStoreKey()
  488. {
  489. return $this->getOwnerInstance()->atkEntityType() . ':' . $this->fieldName();
  490. }
  491. /**
  492. * Uses the given record to create an add filter string.
  493. *
  494. * @param array $record
  495. * @return string filter string
  496. */
  497. function getAddFilterString($record)
  498. {
  499. $filterelems = $this->_getFilterElements($record);
  500. $strfilter = implode(" AND ", $filterelems);
  501. if ($this->m_useFilterForAddLink && $this->m_destinationFilter != "") {
  502. $strfilter .= ' AND ' . $this->parseFilter($this->m_destinationFilter, $record);
  503. }
  504. return $strfilter;
  505. }
  506. /**
  507. * Get the add link when using a dialog
  508. *
  509. * @param array $record
  510. * @param string $action
  511. * @return string The dialog add link html-code
  512. */
  513. function _getDialogAddLink($record, $action, $params = array())
  514. {
  515. $ui = &$this->m_ownerInstance->getUi();
  516. $filter = $this->getAddFilterString($record);
  517. if (!empty($filter)) {
  518. $params['atkfilter'] = $filter;
  519. }
  520. $dialog = new Adapto_Dialog($this->m_ownerInstance->atkEntityType(), 'edit', 'attribute.' . $this->fieldName() . '.' . $action . '_dialog', $params);
  521. $title = $ui->title($this->m_destInstance->m_module, $this->m_destInstance->m_type, $action);
  522. $dialog->setTitle($title);
  523. $dialog->setModifierObject($this->m_destInstance);
  524. $dialog->setSessionStatus(SESSION_PARTIAL);
  525. $onClick = $dialog->getCall();
  526. return '<a href="javascript:void(0)" onclick="' . $onClick . '" class="valignMiddle">' . $this->getAddLabel() . '</a>';
  527. }
  528. /**
  529. * Internal function to get the add link for a Adapto_Relation_OneToMany.
  530. *
  531. * @param Array $myrecords The load of all attributes (see comment in edit() code)
  532. * @param Array $record The record that holds the value for this attribute.
  533. * @param bool $saveform Save the values of the form?
  534. * @return String The link to add records to the onetomany
  535. */
  536. function _getNestedAddLink($myrecords, $record, $saveform = true, $fieldprefix = '', $params = array())
  537. {
  538. $url = "";
  539. if ((int) $this->m_maxRecords !== 0 && $this->m_maxRecords <= count($myrecords))
  540. return $url;
  541. if (!$this->createDestination())
  542. return $url;
  543. if ($this->m_destInstance->hasFlag(EF_NO_ADD))
  544. return $url;
  545. $filter = $this->getAddFilterString($record);
  546. if (!empty($filter))
  547. $params['atkfilter'] = $filter;
  548. $onchange = '';
  549. if (count($this->m_onchangecode)) {
  550. $onchange = 'onChange="' . $this->fieldName() . '_onChange(this);"';
  551. $this->_renderChangeHandler($fieldprefix);
  552. }
  553. $add_url = $this->getAddURL($params);
  554. $label = $this->getAddLabel();
  555. return href($add_url, $label, SESSION_NESTED, $saveform, $onchange . ' class="atkonetomanyrelation"');
  556. }
  557. /**
  558. * Get filter elements
  559. *
  560. * @param array $record
  561. * @return array Array with filter elements
  562. */
  563. function _getFilterElements($record)
  564. {
  565. $filterelems = array();
  566. $ownerfields = $this->getOwnerFields();
  567. if ($this->destinationHasRelation()) {
  568. // we need to set the filter of the record we are going to add.
  569. // The referential key must be set to the value of the current
  570. // primary key.
  571. $this->createDestination();
  572. for ($i = 0, $_i = count($this->m_refKey); $i < $_i; $i++) {
  573. $primkeyattr = &$this->m_ownerInstance->m_attribList[$ownerfields[$i]];
  574. $value = $primkeyattr->value2db($record);
  575. if (!$value)
  576. continue;
  577. $filterelems[] = $this->m_refKey[0] . "." . $ownerfields[$i] . "='" . $this->escapeSQL($value) . "'";
  578. }
  579. } else {
  580. for ($i = 0, $_i = count($this->m_refKey); $i < $_i; $i++) {
  581. $value = $record[$ownerfields[$i]];
  582. if (!$value)
  583. continue;
  584. $filterelems[] = $this->_addTablePrefix($this->m_refKey[$i]) . "='" . $this->escapeSQL($value) . "'";
  585. }
  586. }
  587. return $filterelems;
  588. }
  589. /**
  590. * Prefix the passed column name with the table name if there is no prefix in the column name yet.
  591. *
  592. * @param string $columnName
  593. * @param string $destAlias
  594. * @return string
  595. */
  596. function _addTablePrefix($columnName, $destAlias = '')
  597. {
  598. $prefix = '';
  599. if (strpos($columnName, '.') === false) {
  600. $prefix = $destAlias ? $destAlias : ($this->m_destInstance->getTable());
  601. $prefix .= '.';
  602. }
  603. return $prefix . $columnName;
  604. }
  605. protected function getAddURL($params = array())
  606. {
  607. return dispatch_url($this->m_destination, "add", $params);
  608. }
  609. /**
  610. * Attempts to get a translated label which can be used when composing an "add" link
  611. *
  612. * @return String Localised "add" label
  613. */
  614. function getAddLabel()
  615. {
  616. $key = "link_" . $this->fieldName() . "_add";
  617. $label = atktext($key, $this->m_ownerInstance->m_module, $this->m_ownerInstance->m_type, "", "", true);
  618. if ($label == "") {
  619. $key = "link_" . $this->fieldName() . "_add";
  620. $label = atktext($key, $this->m_destInstance->m_module, "", "", "", true);
  621. if ($label == "") {
  622. $key = "link_" . getEntityType($this->m_destination) . "_add";
  623. $label = atktext($key, $this->m_destInstance->m_module, "", "", "", true);
  624. if ($label == "") {
  625. $label = atktext(getEntityType($this->m_destination), $this->m_destInstance->m_module) . " " . strtolower(atktext("add", "atk"));
  626. }
  627. }
  628. }
  629. return $label;
  630. }
  631. /**
  632. * Retrieve header for the recordlist.
  633. *
  634. * The regular Adapto_Relation_OneToMany has no implementation for this method,
  635. * but it may be overridden in derived classes to add extra information
  636. * (text, links, whatever) to the top of the attribute, right before the
  637. * recordlist. This is similar to the adminHeader() method in atkEntity.
  638. *
  639. * @param array $record The master record that is being edited.
  640. * @param array $childrecords The childrecords in this master/detail
  641. * relationship.
  642. * @return String a String to be added to the header of the recordlist.
  643. */
  644. function editHeader($record = NULL, $childrecords = NULL)
  645. {
  646. if (!empty($this->m_headerName)) {
  647. $methodname = $this->m_headerName;
  648. return $this->m_ownerInstance->$methodname($record, $childrecords, $this);
  649. } else
  650. return "";
  651. }
  652. /**
  653. * Retrieve footer for the recordlist.
  654. *
  655. * The regular Adapto_Relation_OneToMany has no implementation for this method,
  656. * but it may be overridden in derived classes to add extra information
  657. * (text, links, whatever) to the bottom of the attribute, just after the
  658. * recordlist. This is similar to the adminFooter() method in atkEntity.
  659. *
  660. * @param array $record The master record that is being edited.
  661. * @param array $childrecords The childrecords in this master/detail
  662. * relationship.
  663. * @return String a String to be added at the bottom of the recordlist.
  664. */
  665. function editFooter($record = NULL, $childrecords = NULL)
  666. {
  667. if (!empty($this->m_footerName)) {
  668. $methodname = $this->m_footerName;
  669. return $this->m_ownerInstance->$methodname($record, $childrecords, $this);
  670. } else
  671. return "";
  672. }
  673. /**
  674. * Create the where clause for the referential key that is used to
  675. * retrieve the destination records.
  676. * @access private
  677. *
  678. * @param array $record The master record
  679. * @return String SQL where clause
  680. */
  681. function _getLoadWhereClause($record)
  682. {
  683. if (!$this->m_useRefKeyForFilter)
  684. return '';
  685. $whereelems = array();
  686. if (count($this->m_refKey) == 0 || $this->m_refKey[0] == "")
  687. $this->m_refKey[0] = $this->m_owner;
  688. $ownerfields = $this->getOwnerFields();
  689. for ($i = 0, $_i = count($this->m_refKey); $i < $_i; $i++) {
  690. $primkeyattr = &$this->m_ownerInstance->m_attribList[$ownerfields[$i]];
  691. if (!$primkeyattr->isEmpty($record)) {
  692. $whereelems[] = $this->_addTablePrefix($this->m_refKey[$i]) . "='" . $primkeyattr->value2db($record) . "'";
  693. }
  694. }
  695. $result = implode(" AND ", $whereelems);
  696. return $result == '' ? '1=0' : $result;
  697. }
  698. /**
  699. * Define a dummy function to use as a dummy handler function in load() below
  700. */
  701. public function ___dummyCount()
  702. {
  703. }
  704. /**
  705. * Retrieve detail records from the database.
  706. *
  707. * Called by the framework to load the detail records.
  708. *
  709. * @param atkDb $db The database used by the entity.
  710. * @param array $record The master record
  711. * @param String $mode The mode for loading (admin, select, copy, etc)
  712. * @param bool $paging divide the result records on multiple pages ($config_recordsperpage)
  713. *
  714. * @return array Recordset containing detailrecords, or NULL if no detail
  715. * records are present. Note: when $mode is edit, this
  716. * method will always return NULL. This is a framework
  717. * optimization because in edit pages, the records are
  718. * loaded on the fly.
  719. */
  720. function load(&$db, $record, $mode = "", $paging = false)
  721. {
  722. $result = null;
  723. // for edit and view mode we don't load any records unless a display override exists
  724. // we use the grid to load records because it makes things easier
  725. if (($mode != 'add' && $mode != 'edit' && $mode != 'view')
  726. || ($mode == 'view' && method_exists($this->getOwnerInstance(), $this->fieldName() . "_display"))
  727. || ($mode == 'edit' && $this->hasFlag(AF_READONLY_EDIT) && method_exists($this->getOwnerInstance(), $this->fieldName() . "_display"))) {
  728. $grid = $this->createGrid($record, $mode == 'copy' ? 'copy' : 'admin', $mode, false);
  729. $grid->setPostvar('atklimit', -1); // all records
  730. $grid->setCountHandler(array($this, '___dummyCount')); // don't count
  731. $grid->loadRecords();
  732. $result = $grid->getRecords();
  733. $grid->destroy(); // clean-up
  734. }
  735. return $result;
  736. }
  737. /**
  738. * Override isEmpty function - in a oneToMany relation we should check if the
  739. * relation contains any records. When there aren't any, the relation is empty,
  740. * otherwise it isn't
  741. *
  742. * @param array &$record The record to check
  743. * @return bool true if a destination record is present. False if not.
  744. */
  745. function isEmpty($record)
  746. {
  747. if (!isset($record[$this->fieldName()]) || (is_array($record[$this->fieldName()]) && count($record[$this->fieldName()]) == 0)) {
  748. // empty. It might be that the record has not yet been fetched. In this case, we do
  749. // a forced load to see if it's really empty.
  750. $recs = $this->load($this->m_ownerInstance->getDb(), $record);
  751. return (count($recs) == 0);
  752. }
  753. return false;
  754. }
  755. /**
  756. * The delete method is called by the framework to inform the attribute
  757. * that the master record is deleted.
  758. *
  759. * Note that the framework only calls the method when the
  760. * AF_CASCADE_DELETE flag is set. When calling this method, all detail
  761. * records belonging to the master record are deleted.
  762. *
  763. * @param array $record The record that is deleted.
  764. * @return boolean true if cleanup was successful, false otherwise.
  765. */
  766. function delete($record)
  767. {
  768. $classname = $this->m_destination;
  769. $cache_id = $this->m_owner . "." . $this->m_name;
  770. $rel = &getEntity($classname, $cache_id);
  771. $ownerfields = $this->getOwnerFields();
  772. for ($i = 0, $_i = count($this->m_refKey); $i < $_i; $i++) {
  773. $primkeyattr = &$this->m_ownerInstance->m_attribList[$ownerfields[$i]];
  774. $whereelems[] = $this->_addTablePrefix($this->m_refKey[$i]) . "='" . $primkeyattr->value2db($record) . "'";
  775. }
  776. $where = implode(" AND ", $whereelems);
  777. if ($where != "") // double check, so we never by accident delete the entire db
  778. {
  779. return $rel->deleteDb($where);
  780. }
  781. return true;
  782. }
  783. /**
  784. * Store detail records in the database.
  785. *
  786. * For onetomanyrelation, this function does not have much use, since it
  787. * stores records using its 'add link'.
  788. * There are however two modes that use this:
  789. * - 'copy' mode
  790. * The copyDb function, to clone detail records.
  791. * - 'add' mode
  792. * When the OTM was used in add mode, we have to transfer
  793. * the records stored in the session to the database.
  794. *
  795. * other than those this method does not do anything.
  796. *
  797. * @param atkDb $db The database used by the entity.
  798. * @param array $record The master record which has the detail records
  799. * embedded.
  800. * @param string $mode The mode we're in ("add", "edit", "copy")
  801. * @return boolean true if store was successful, false otherwise.
  802. */
  803. function store(atkDb $db, $record, $mode)
  804. {
  805. switch ($mode) {
  806. case 'add':
  807. return $this->storeAdd($db, $record, $mode);
  808. case 'copy':
  809. return $this->storeCopy($db, $record, $mode);
  810. default:
  811. return true;
  812. }
  813. }
  814. /**
  815. * Persist records from the session (in add mode) to the database.
  816. *
  817. * @param atkDb $db
  818. * @param array $record
  819. * @param string $mode
  820. * @return bool
  821. */
  822. private function storeAdd(atkDb $db, $record, $mode)
  823. {
  824. if (!$this->createDestination())
  825. return false;
  826. $rows = atkSessionStore::getInstance($this->getSessionStoreKey())->getData();
  827. foreach ($rows as $row) {
  828. $this->updateSessionAddFakeId($row, $this->getSessionAddFakeId(), $record);
  829. $this->m_destInstance->addDb($row);
  830. }
  831. // after saving the rows, we can clear the sessionstore
  832. atkSessionStore::getInstance($this->getSessionStoreKey())->setData(null);
  833. return true;
  834. }
  835. /**
  836. * Recursive method to look for the fake id in the record and replace it
  837. * with the proper id.
  838. *
  839. * @param array $row Destination record
  840. * @param mixed $id Fake id to look for
  841. * @param array $record Owner record
  842. */
  843. private function updateSessionAddFakeId(&$row, $id, $record)
  844. {
  845. $row_keys = array_keys($row);
  846. foreach ($row_keys as $key) {
  847. if (is_array($row[$key])) {
  848. $this->updateSessionAddFakeId($row[$key], $id, $record);
  849. } else {
  850. if ($row[$key] === $id)
  851. $row[$key] = $record[$key];
  852. }
  853. }
  854. }
  855. /**
  856. * Copy detail records.
  857. *
  858. * @param atkDb $db Datbase connection to use
  859. * @param array $record Owner record
  860. * @param string $mode Mode ('copy')
  861. * @return bool
  862. */
  863. private function storeCopy(atkDb $db, $record, $mode)
  864. {
  865. $onetomanyrecs = $record[$this->fieldName()];
  866. if (!is_array($onetomanyrecs) || count($onetomanyrecs) <= 0)
  867. return true;
  868. if (!$this->createDestination())
  869. return true;
  870. $ownerfields = $this->getOwnerFields();
  871. for ($i = 0; $i < count($onetomanyrecs); $i++) {
  872. // original record
  873. $original = $onetomanyrecs[$i];
  874. $onetomanyrecs[$i]['atkorgrec'] = $original;
  875. // the referential key of the onetomanyrecs could be wrong, if we
  876. // are called for example from a copy function. So just in case,
  877. // we reset the correct key.
  878. if (!$this->destinationHasRelation()) {
  879. for ($j = 0, $_j = count($this->m_refKey); $j < $_j; $j++) {
  880. $onetomanyrecs[$i][$this->m_refKey[$j]] = $record[$ownerfields[$j]];
  881. }
  882. } else {
  883. for ($j = 0, $_j = count($this->m_refKey); $j < $_j; $j++) {
  884. $onetomanyrecs[$i][$this->m_refKey[0]][$ownerfields[$j]] = $record[$ownerfields[$j]];
  885. }
  886. }
  887. if (!$this->m_destInstance->addDb($onetomanyrecs[$i], true, $mode)) {
  888. // error
  889. return false;
  890. }
  891. }
  892. return true;
  893. }
  894. /**
  895. * Returns a piece of html code for hiding this attribute in an HTML form.
  896. *
  897. * Because the oneToMany has nothing to hide, we override the default
  898. * hide() implementation with a dummy method.
  899. *
  900. * @return String An empty string.
  901. */
  902. function hide($record = '', $fieldprefix = '')
  903. {
  904. //Nothing to hide..
  905. return "";
  906. }
  907. /**
  908. * Retrieve the list of searchmodes supported by the attribute.
  909. *
  910. * Note that not all modes may be supported by the database driver.
  911. * Compare this list to the one returned by the databasedriver, to
  912. * determine which searchmodes may be used.
  913. *
  914. * @return array List of supported searchmodes
  915. */
  916. function getSearchModes()
  917. {
  918. return array('substring');
  919. }
  920. /**
  921. * Returns the condition (SQL) that should be used when we want to join an owner
  922. * entity with the destination entity of the Adapto_Relation_OneToMany.
  923. *
  924. * @param atkQuery $query The query object.
  925. * @param String $ownerAlias The owner table alias.
  926. * @param String $destAlias The destination table alias.
  927. *
  928. * @return String SQL string for joining the owner with the destination.
  929. */
  930. function getJoinCondition(&$query, $ownerAlias = "", $destAlias = "")
  931. {
  932. if (!$this->createDestination())
  933. return false;
  934. if ($ownerAlias == "")
  935. $ownerAlias = $this->m_ownerInstance->m_table;
  936. $conditions = array();
  937. $ownerfields = $this->getOwnerFields();
  938. for ($i = 0, $_i = count($this->m_refKey); $i < $_i; $i++) {
  939. $conditions[] = $this->_addTablePrefix($this->m_refKey[$i], $destAlias) . "=" . $ownerAlias . "." . $ownerfields[$i];
  940. }
  941. return implode(" AND ", $conditions);
  942. }
  943. /**
  944. * Creates a smart search condition for a given search value, and adds it
  945. * to the query that will be used for performing the actual search.
  946. *
  947. * @param Integer $id The unique smart search criterium identifier.
  948. * @param Integer $nr The element number in the path.
  949. * @param Array $path The remaining attribute path.
  950. * @param atkQuery $query The query to which the condition will be added.
  951. * @param String $ownerAlias The owner table alias to use.
  952. * @param Mixed $value The value the user has entered in the searchbox.
  953. * @param String $mode The searchmode to use.
  954. */
  955. function smartSearchCondition($id, $nr, $path, &$query, $ownerAlias, $value, $mode)
  956. {
  957. // one-to-many join means we need to perform a distinct select
  958. $query->setDistinct(true);
  959. if (count($path) > 0) {
  960. $this->createDestination();
  961. $destAlias = "ss_{$id}_{$nr}_" . $this->fieldName();
  962. $query
  963. ->addJoin($this->m_destInstance->m_table, $destAlias, $this->getJoinCondition($query, $ownerAlias, $destAlias), false);
  964. $attrName = array_shift($path);
  965. $attr = &$this->m_destInstance->getAttribute($attrName);
  966. if (is_object($attr)) {
  967. $attr->smartSearchCondition($id, $nr + 1, $path, $query, $destAlias, $value, $mode);
  968. }
  969. } else {
  970. $this->searchCondition($query, $ownerAlias, $value, $mode);
  971. }
  972. }
  973. /**
  974. * Adds a search condition for a given search value
  975. *
  976. * @param atkQuery $query The query to which the condition will be added.
  977. * @param String $table The name of the table in which this attribute
  978. * is stored
  979. * @param mixed $value The value the user has entered in the searchbox
  980. * @param String $searchmode The searchmode to use. This can be any one
  981. * of the supported modes, as returned by this
  982. * attribute's getSearchModes() method.
  983. * @param string $fieldaliasprefix optional prefix for the fieldalias in the table
  984. */
  985. function searchCondition(&$query, $table, $value, $searchmode, $fieldaliasprefix = '')
  986. {
  987. if ($this->createDestination()) {
  988. $searchcondition = $this->getSearchCondition($query, $table, $value, $searchmode);
  989. if (!empty($searchcondition)) {
  990. $query->addSearchCondition($searchcondition);
  991. $query->setDistinct(true);
  992. // @todo: is this still needed?
  993. if ($this->m_ownerInstance->m_postvars["atkselector"]) {
  994. $query->addTable($this->m_destInstance->m_table);
  995. $query->addCondition($this->translateSelector($this->m_ownerInstance->m_postvars['atkselector']));
  996. }
  997. }
  998. }
  999. }
  1000. /**
  1001. * Creates a searchcondition for the field,
  1002. * was once part of searchCondition, however,
  1003. * searchcondition() also immediately adds the search condition.
  1004. *
  1005. * @param atkQuery $query The query object where the search condition should be placed on
  1006. * @param String $table The name of the table in which this attribute
  1007. * is stored
  1008. * @param mixed $value The value the user has entered in the searchbox
  1009. * @param String $searchmode The searchmode to use. This can be any one
  1010. * of the supported modes, as returned by this
  1011. * attribute's getSearchModes() method.
  1012. * @return String The searchcondition to use.
  1013. */
  1014. function getSearchCondition(&$query, $table, $value, $searchmode)
  1015. {
  1016. $usedfields = array();
  1017. $searchconditions = array();
  1018. if (!is_array($value)) {
  1019. foreach ($this->m_destInstance->descriptorFields() as $field) {
  1020. if (!in_array($field, $usedfields)) {
  1021. $sc = $this->_callSearchConditionOnDestField($query, $this->m_destInstance->m_table, $value, $searchmode, $field, $table);
  1022. if (!empty($sc))
  1023. $searchconditions[] = $sc;
  1024. $usedfields[] = $field;
  1025. }
  1026. }
  1027. } else {
  1028. foreach ($value as $key => $val) {
  1029. if ($val) {
  1030. $sc = $this->_callSearchConditionOnDestField($query, $this->m_destInstance->m_table, $val, $searchmode, $key, $table);
  1031. if (!empty($sc))
  1032. $searchconditions[] = $sc;
  1033. }
  1034. }
  1035. }
  1036. if (count($searchconditions) > 0)
  1037. return "(" . implode(" OR ", $searchconditions) . ")";
  1038. else
  1039. return false;
  1040. }
  1041. /**
  1042. * Calls searchCondition on an attribute in the destination
  1043. * To hook the destination attribute on the query
  1044. * @param atkQuery &$query The query object
  1045. * @param String $table The table to search on
  1046. * @param mixed $value The value to search
  1047. * @param mixed $searchmode The mode used when searching
  1048. * @param String $field The name of the attribute
  1049. * @param String $reftable
  1050. */
  1051. function _callSearchConditionOnDestField(&$query, $table, $value, $searchmode, $field, $reftable)
  1052. {
  1053. if ($this->createDestination()) {
  1054. $alias = $this->fieldName() . "_AE_" . $this->m_destInstance->m_table;
  1055. $attr = &$this->m_destInstance->getAttribute($field);
  1056. $query->addJoin($table, $alias, $this->getJoinCondition($query, $reftable, $alias), false);
  1057. return $attr->getSearchCondition($query, $alias, $value, $searchmode);
  1058. }
  1059. }
  1060. /**
  1061. * Determine the type of the foreign key on the other side.
  1062. *
  1063. * On the other side of a oneToManyRelation (in the destination entity),
  1064. * there may be a regular atkAttribute for the referential key, or an
  1065. * atkManyToOneRelation pointing back at the source. This method discovers
  1066. * which of the 2 cases we are dealing with.
  1067. * @return boolean True if the foreign key on the other side is a
  1068. * relation, false if not.
  1069. */
  1070. function destinationHasRelation()
  1071. {
  1072. if ($this->createDestination()) {
  1073. // If there's a relation back, it's in the destination entity under the name of the first refkey element.
  1074. $attrib = $this->m_destInstance->m_attribList[$this->m_refKey[0]];
  1075. if (is_object($attrib) && strpos(get_class($attrib), "elation") !== false)
  1076. return true;
  1077. }
  1078. return false;
  1079. }
  1080. /**
  1081. * Are we allowed to delete a record?
  1082. *
  1083. * @return mixed bool if allowed or string with not allowed message
  1084. */
  1085. function deleteAllowed()
  1086. {
  1087. if ($this->hasFlag(AF_RESTRICTED_DELETE)) {
  1088. // Get the destination entity
  1089. $classname = $this->m_destination;
  1090. $cache_id = $this->m_owner . "." . $this->m_name;
  1091. $rel = &getEntity($classname, $cache_id);
  1092. // Get the current atkselector
  1093. $where = $this->translateSelector($this->m_ownerInstance->m_postvars['atkselector']);
  1094. if ($where) {
  1095. $childrecords = $rel->selectDb($where);
  1096. if (!empty($childrecords))
  1097. return atktext("restricted_delete_error");
  1098. } else
  1099. return;
  1100. }
  1101. return true;
  1102. }
  1103. /**
  1104. * Here we check if the selector is on the owner or on the destination
  1105. * if it's on the destination, we leave it alone.
  1106. * Otherwise we translate it back to the destination.
  1107. *
  1108. * @todo when we translate the selector, we get the last used refKey
  1109. * but how do we know what is the right one?
  1110. * @param string $selector the selector we have to translate
  1111. * @return string the new selector
  1112. */
  1113. function translateSelector($selector)
  1114. {
  1115. // All standard SQL operators
  1116. $sqloperators = array('=', '<>', '>', '<', '>=', '<=', 'BETWEEN', 'LIKE', 'IN');
  1117. $this->createDestination();
  1118. // Check the filter for every SQL operators
  1119. for ($counter = 0; $counter < count($sqloperators); $counter++) {
  1120. if ($sqloperators[$counter]) {
  1121. list($key, $value) = explode($sqloperators[$counter], $selector);
  1122. // if the operator is in the filter
  1123. if ($value) {
  1124. // check if it's on the destination
  1125. for ($refkeycount = 0; $refkeycount < count($this->m_refKey); $refkeycount++) {
  1126. $destinationkey = $this->m_destInstance->m_table . "." . $this->m_refKey[$refkeycount];
  1127. // if the selector is on the destination, we pass it back
  1128. if ($key == $destinationkey || $key == $this->m_refKey[$refkeycount]) {
  1129. return $selector;
  1130. }
  1131. }
  1132. // otherwise we set it on the destination
  1133. return $destinationkey . $sqloperators[$counter] . $value;
  1134. }
  1135. }
  1136. }
  1137. // We never found a value, something is wrong with the filter
  1138. return "";
  1139. }
  1140. /**
  1141. * Add dialog.
  1142. */
  1143. function partial_add_dialog()
  1144. {
  1145. $this->createDestination();
  1146. $this->m_destInstance->m_partial = 'dialog';
  1147. $handler = &$this->m_destInstance->getHandler('add');
  1148. $handler->m_postvars = $this->m_ownerInstance->m_postvars;
  1149. // Reset postvars of ownerinstance because it might interfere with relations
  1150. // which point back to this ownerinstance and it doesn't need them anymore anyway.
  1151. $this->m_ownerInstance->m_postvars = array();
  1152. $handler->setDialogSaveUrl(partial_url($this->m_ownerInstance->atkEntityType(), 'edit', 'attribute.' . $this->fieldName() . '.add_process'));
  1153. $result = $handler->renderAddDialog();
  1154. $page = &$this->m_ownerInstance->getPage();
  1155. $page->addContent($result);
  1156. }
  1157. /**
  1158. * Process add dialog save action.
  1159. */
  1160. function partial_add_process()
  1161. {
  1162. $this->createDestination();
  1163. $handler = &$this->m_destInstance->getHandler('save');
  1164. $handler->m_postvars = $this->m_ownerInstance->m_postvars;
  1165. // Reset postvars of ownerinstance because it might interfere with relations
  1166. // which point back to this ownerinstance and it doesn't need them anymore anyway.
  1167. $this->m_ownerInstance->m_postvars = array();
  1168. $handler->setDialogSaveUrl(partial_url($this->m_ownerInstance->atkEntityType(), 'edit', 'attribute.' . $this->fieldName() . '.add_process'));
  1169. $handler->handleSave($this->getPartialSaveUrl());
  1170. }
  1171. /**
  1172. * assamble the partial save handler url
  1173. * this allows dynamically updating the attribute
  1174. */
  1175. public function getPartialSaveUrl()
  1176. {
  1177. return partial_url($this->m_ownerInstance->atkEntityType(), 'edit', 'attribute.' . $this->fieldName() . '.refresh');
  1178. }
  1179. /**
  1180. * Add or copy dialog.
  1181. */
  1182. function partial_addorcopy_dialog()
  1183. {
  1184. $this->createDestination();
  1185. $this->m_destInstance->addFilter($this->m_ownerInstance->m_postvars['atkfilter']);
  1186. $handler = &$this->m_destInstance->getHandler('addorcopy');
  1187. $handler
  1188. ->setProcessUrl(
  1189. partial_url($this->m_ownerInstance->atkEntityType(), 'edit', 'attribute.' . $this->fieldName() . '.addorcopy_process',
  1190. array('atkfilter' => $this->m_ownerInstance->m_postvars['atkfilter'])));
  1191. $handler->handleDialog();
  1192. }
  1193. /**
  1194. * Process add or copy action.
  1195. */
  1196. function partial_addorcopy_process()
  1197. {
  1198. $this->createDestination();
  1199. $addOrCopy = $this->m_ownerInstance->m_postvars['addorcopy'];
  1200. // user has choosen to copy an existing record, le…

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