Duplicate linked DataObjects

Duplicate linked DataObjects from a ComplexTableField

 

When building a multilingual site, somehow you always get to that point where you wish you could duplicate a set of linked 'has_many' DataObjects along with a translated page. As it stands, you just can't. Would be nice though to have a built-in button, say in the ComplexTableField, that will do this...

This is an article about creating just such a button - and about finding out how to add buttons to the CTF that refresh the table on completion of whatever they're doing, using jQuery. The functionality is still rough, but it's not over yet...

A module

This could be a module called 'translatablectf' with the following file-setup:

  • translatablectf/_config.php
  • translatablectf/code/TranslatableComplexTableField.php
  • translatablectf/templates/TranslatableComplexTableField.ss
  • translatablectf/javascript/TranslatableComplexTableField.js

/translatablectf/_config.php

<?php
/* 
 * TranslatableComplexTableField
 */

Step 1: add a button to the template

Since hacking core templates is not an option, copy the sapphire/templates/ComplexTableField.ss into our TransLatableComplexTableField.ss. To the end of the template we'll add the new button as a link - CTFCanDuplicate checkswhether the button should be displayed. So the bottom part of the template will look like this:

/translatablectf/templates/TranslatableComplexTableField.ss

	...
	<div class="utility">
		<% if Can(export) %>
			<a href="$ExportLink" target="_blank">
				<% _t('CSVEXPORT', 'Export to CSV' ) %>
			</a>
		<% end_if %>
		<% if CTFCanDuplicate %>
			<a name="$Name" id="duplicate" href="$Link(duplicate)">
				<% _t('DUPLICATEFROMMASTER', 'Duplicate from master') %>
			</a>
		<% end_if %>
	</div>
	</div>
</div>

Note: href="$Link(duplicate)" will add an action called 'duplicate' to the end of the URL, that will tell the TranslatableComplexTableFiel to execut its duplicate() method. So we need to create that.

Step 2: extend the ComplexTableField class

We'll extend the ComplexTableField for three reasons:

  1. because we want it to use the new template,
  2. because it needs the 'duplicate' method
  3. because it needs to load some extra javascript, defining how to reload the CTF after duplicating the records

/translatablectf/code/TranslatableComplexTableField.php

class TranslatableComplexTableField extends ComplexTableField {

	// use a new template to allow for a new 'duplicate' button
	protected $template = "TranslatableComplexTableField";


	// load the javascipt for the new duplicate button
	function FieldHolder() {
		Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js');
		Requirements::javascript(SAPPHIRE_DIR . '/javascript/jquery_improvements.js');
		Requirements::javascript(
			"translatablectf/javascript/TranslatableComplexTableField.js"
		);
		return parent::FieldHolder();
	}

	// Make sure the SiteTree is translatable, and we're not on the 
	// masterpage, befor showing the button...
	function CTFCanDuplicate() {
		if (Object::has_extension('SiteTree', 'Translatable')) {
			$ID = $this->controller->ID;
			$groupID = $this->controller->getTranslationGroup();
			if ($ID != $groupID) return true;
		}
		return false;
	}

	// the duplicate action that will duplicate master objects
	function duplicate() {
		return $this->FieldHolder();
	}
}

After a /dev/build/flush=1 it should now be possible to use the TransLatableComplexTableField just like the basic ComplexTableField.

Step 3 -basic jQuery

The only thing this very basic jQuery script does is load the url from the duplicate button's href. This will invoke the duplicate() method, that (in this example) simply returns the FieldHolder() and reloads the field. Nothing else.

/translatablectf/javascript/TranslatableComplexTableField.js

(function($){
	$('#duplicate').live('click', function() {

		var fieldname = $(this).attr('name')
		var container = $('#Form_EditForm_' + fieldname);
		var wrapper  = container.wrap('<div id="tempContainer" />');

		wrapper.load(
			$(this).attr('href'), 
			function() {
				newcontainer = wrapper.find('#Form_EditForm_' + fieldname).unwrap(); 
				Behaviour.apply(newcontainer.get(0));
			}
		);
		return false;
	});
})(jQuery);

Btw: how does this work?
When the button is clicked, we locate the container it belongs to and wrap it in a new div called tempContainer. The updated field returned by the duplicate() method, is now loaded in the tempContainer. On completion, the newly loaded field is unwrapped again and the default ComplexTableField behaviour is (re)attached to it, to makel links and buttons work again.

Note: I don't really speak Prototype and I would never call myself a jQuery expert, so there might be a better way to do this - but for now I'm happy :-)

Step 4: do the duplication...

So now the duplicate() method will retreive the DataObjects from the master page and duplicate them for the translated page:

	// the duplicate action that will duplicate master objects
	function duplicate() {

		$ID = $this->controller->ID;
		$groupID = $this->controller->getTranslationGroup();
		$sourceClass = $this->sourceClass;
		$parentClass = $this->getParentClass();  // can I use $this->controller->ClassName?
		$duplicates = '';

		$relationField = $this->getParentIdName($parentClass, $sourceClass);

		if ($ID != $groupID) {

			$masterPage = DataObject::get_by_id('Page', $groupID);
			if (!empty($masterPage)) {

				$fieldName = $this->Name();
				$duplicates = $masterPage->$fieldName();

				if (!empty($duplicates)) {
					foreach($duplicates as $duplicate) {
						$duplicate->ID = null;
						$duplicate->$relationField = $ID;
						$duplicate->write();
					}
				}
			}
		}
		return $this->FieldHolder();
	}

This will do a straight copy of all DataObjects from the Masterpage - but only if the current page is not the Masterpage, which is good. It doesn't care if the objects are translatable, no translations are made, just copies. It will keep existing records, and still duplicate everything whenever that button is pushed.

Translatable objects?

It isn't very hard to have the duplicate function make a distinction between objects that have the Translatable extension, and objects that haven't, and based on that you can either make a hardcopy or a new translation. But I try to limit the use of Translatable objects as much as possible and there are some issues with the ComplexTablefield ...

It's not very hard to add the proper Locale to the new duplicate, if you can just use the current page's Locale. just change the following lines in the duplicate() function:

...
if (!empty($duplicates)) {

	// check if Translatable is enabled for this object
	$isTranslatable = (Object::has_extension($this->sourceClass, 'Translatable'));
						
	foreach($duplicates as $duplicate) {

		// store the ID of the master object, then set the ID to null 
		// to force an insert
		$originalID = $duplicate->ID;
		$duplicate->ID = null;

		$duplicate->$relationField = $ID;

		if ($isTranslatable) {

			// copy the Locale from the current Page
			$duplicate->Locale = $this->controller->Locale;

			// this adds the record to the proper TranslationGroup, 
			// though I don't really know why...
			$duplicate->_TranslationGroupID = $originalID;
		}
		$duplicate->write();
	}
}
...

This way of adding the new translation to the TranslationGroup feels very 'hacky'. I feel that I should get addTranslationGroup() to work - but I just don't get it... These things are handled in the onBeforeWrite() and onAfterWrite() handles in the Translatable class. Anyone have a clue - let me know...

Orderable!

Unfortunately the CTF doesn't support reordering objects, but fortunately ajshort did a great job developing the Orderable module (find it here). And it's easy to extend the OrderableComplexTableField instead of the regular ComplexTableField. To make this work, copy the Orderable template in stead of the ComplexTableField.ss, and add the button in just the same way.

Don't forget to install the Orderable module - and make the DataObject you're using orderable:

Object::add_extension('MyDataObject', 'Orderable');

Note:

This is for use with the ComplexTableField only. I'm not going any further with this, since in version 3 the CTF functionality will be handed over to the DataGrid anyway..

Ah... Should I change this to a CompleTableField decorator? Is such a thing possible/doable/interesting?

Comments

  • Thanks Juan, I've updated the article - hope I got them all.

    Verstuurd door Martine, 06/11/2011 5:46pm (2 jaar geleden)

  • Nice work!

    I’d like to point you some trivial errors:
    – There is always a capital L in TransLatable filenames.
    – The javascript path is not correct (translatabletestfiles instead of translatablectf).

    Thanks to your code and a temporary hack I’ve been able to duplicate Albums from UncleCheese’s image_gallery module and simplified the process of associating the same Photos to the new translated Albums.

    Thanks again!
    Juan

    Verstuurd door Juanitou, 05/11/2011 12:21am (2 jaar geleden)

Het versturen van reacties is uitgeschakeld.

RSS feed voor reacties op deze pagina