Duplicate an Item

Hello

I am working with Omeka and I have installed the newest version.

A lot of items of my collection are very similar and it could be very helpful and easy for me to duplicate an item and correct it afterwards.

But I am not able to see any option in Omeka for duplicate items which are very similar. Is there a way or not? Maybe with a plugin? A solution would save me a lot of time and a lot of enregy.

Thank you.

Toni

Thereā€™s not a way in core, but a plugin could probably do it. It would just read all the data for an item, arrange it into the array structure that insert_item() uses.

Thank you.
But I am afraid, I donā€™t have the time and the skills to create a plugin. Is this an easy task?
I can not estimate the work and the level of knowledge to create such a plugin.

I think itā€™d be fairly mechanical and easy. Getting an array of all the element texts is straightforward with all_element_texts(). Not 100% sure, but that might even give data in the same format that insert_item() wants. If not, the data massaging should be easy.

Then youā€™d probably just use a hook to add a button to the item edit page that would fire off that process.

Another option might be to use the CSV import plugin. Then you could create a spreadsheet in which you can quickly duplicate the desired metadata in multiple rows, and change the parts that are different, and then import into Omeka. Take a look at the documentation for that plugin and see if you think it might help.

Iā€™ve been thinking of a different approach: what if the duplicate page is, in fact, an edit page, but the ā€œsaveā€ button creates a new item?

Say, for instance, you want to duplicate Item #22. You press the ā€œduplicate itemā€ button, and a page like the edit one opens up, filling all fields with the data of record #22; by pushing the ā€œadd itemā€ button one should just submit it as a new record, instead of editing the old one.
Does it make sense? I think this would save a lot of coding, I suppose we just need to be sure that the submit button does what he has to :slightly_smiling_face:

My first question is about the mechanism for filling in all the field data. Iā€™m not really sure how that would happen. It might be more straightforward to use the afterSaveItem hook to create the new duplicate item, then redirect to the new itemā€™s edit page. That basically would reverse the order you describe, creating the duplicate first, then editing, but might be easier in the long run.

The other question, which might or might not apply to your site, is how to handle plugin data associated with an item. For example, if the Geolocation module has added map data for an item, then youā€™d also need for the plugin to know to grab the map data and duplicate it for the new item. It would depend on what plugins are in play in your site.

As for your first question, I am not sure I understand the problem. The edit page is already filling in all fields with the data taken from a record, so what I am suggesting is that we just save it as a new record instead of updating the old one.

I would not create a duplicate first to later edit it, since user may decide not to save it after all (and, that being the case, we would have to delete the record just createdā€¦ extra work).

Your second question makes a good point: I donā€™t think it would be that easy to handle plugin data associated with the duplicate item. Unless we fired the pluginsā€™ "data-creating functions"when saving the new record. But I think itā€™s the same problem with CSV import, isnā€™t it? Besides, at least the file, if not also tags and some fields (title, subject, date), would need to be changed anyway. So, I would be happy enough if there was a chance to duplicate the main data of the item, that would help a lot in terms of time spent to introduce the data.

So, this all said, is there a way to load an edit page and change the behaviour of the save button (turning into an ā€œadd itemā€ instead of an ā€œedit itemā€)?

I think Iā€™m not quite following the sequence of steps you have in mind. I had thought you were talking about taking the data from one edit page, and filling it in another, different edit page. Instead, if Iā€™m closer to on track, you are talking about either a new button on the edit page that would save the data to a new item, rather than the original item?

If thatā€™s more accurate to what you are thinking, youā€™d need to create your own controller and action that basically copies what the Item/add action does. Iā€™m not entirely sure that we have a good way to add your button as an alternate to the submit button for the form, but it might be there somewhere. It would have to be different from the save button, since youā€™d need the data to not go through the normal save process.

Exactly my thought. Although I must admit I was hoping for it to be a simpler process, while your comments make me think itā€™s not going to be that simpleā€¦ At the same time, I donā€™t see how we could use your alternative suggestion, if we didnā€™t want to save those data before as a new record.

I think Iā€™ll need some help, with this.

This is what Iā€™ve done so far:

  • tried creating a plugin, to introduce all the needed changes on the fly; Iā€™ve managed to change the Items/Browse page, injecting some javascript (via hookAdminHead) to add an additional link in the position I wanted it (see below picture for an example)
    it
  1. problem here: how to inject the js code only in the target page (and not in all of them, where it would be useless and possibly affecting performances)?
  2. another (possible) problem: the code (see an example below) that does the injection might be changed into something better/better performing, I supposeā€¦ any idea?
 document.addEventListener('DOMContentLoaded', function() {
 	var items = document.getElementsByClassName('action-links group');
 	var regex = />([^<][a-zA-Z]*)</gi;
 	for (i=0; i < items.length; i++) {
 		var listItems = items[i].children;
 		for (ii=0; ii < listItems.length; ii++) {
 			var newLI = listItems[ii].innerHTML;
 			if (newLI.indexOf('items/edit') > 0) {
 				newLI = newLI.replace('items/edit', 'items/duplicate');
 				newLI = newLI.replace(regex, '>" . __(ITEM_DUPLICATOR_DUPLICATE) . "<');
 				var entry = document.createElement('li');
 				entry.innerHTML = newLI;
 				items[i].appendChild(entry);
 				break;
 			}
 		}
 	}
 }, false);
  • put aside the plugin for the moment, Iā€™ve decided to directly intervene on the core pages (browse, show and dashboard), to first focus on the actual duplication. I then copied the Items/Edit page, renamed the copy duplicate.php and hereā€™s the ā€¦
  1. biggest problem: I cannot access the page! Whenever I click on the Duplicate link, that should take me to /admin/items/duplicate/#, I instead receive a ā€œpage not foundā€ warning; I suppose somewhere the duplicate.php page is not recognized as one of the accessible ones, but I have no idea where and how to fix it, so Iā€™m basically stuck.

Any suggestion or hint? Or anybody else who would like to work with me on this idea?

Reporting some progress, here: Iā€™ve managed to solve (1) (thanks to @medhievalā€™s help), and (3) (needed to add proper actions for the new page), while I still have to work on (2).

Now, the Duplicate functionality is working, with a few things to fix (the Duplicate page is still showing the files attached to the original Item, while it should not as when saved the Duplicated Item do not contain those files; and I need to find a way to compulsory change at least one fields (the Title, probably) since the duplicated Item cannot be exactly equal to the original one.

The point is that, to reach this result, Iā€™ve had to edit several core files, so I will need to find a solution that goes through a plugin instead. So far, the files edited are:

application/libraries/Omeka/Controller/AbstractActionController.php
application/controllers/ItemsController.php
admin/themes/default/items/tag-form.php
admin/themes/default/items/browse.php
admin/themes/default/items/show.php
admin/themes/default/index.php

and the added one is, of course,
admin/themes/default/items/duplicate.php

Again, if anybody wants to start working on this with me, theyā€™re more than welcome.

Since Iā€™ve already looked into this a bit, Iā€™d be willing to work with you to try to get this into a proper plugin structure and out of hacking on core. If you share the specific changes youā€™ve made to these files (here or via DM/github/etc) I can look into getting them or similar functionality in a plugin.

Thatā€™s very good news, @medhieval. Thanks a lot for your help.

Itā€™s easier for me to put the specific changes here, Iā€™d say. So, here we goā€¦

admin/themes/default/index.php

<?php echo link_to_item(__('Edit'), array(), 'edit')?>

becomes

<?php echo link_to_item(__('Edit'), array(), 'edit') . "  " . link_to_item(__('Duplicate'), array(), 'duplicate'); ?>

I guess here we can use the trick with the injection of code.

admin/themes/default/items/show.php
added (again, we could use injection)

<?php echo link_to_item(__('Duplicate'), array('class'=>'big green button'), 'duplicate'); ?>

just after

<?php echo link_to_item(__('Edit'), array('class'=>'big green button'), 'edit'); ?>

admin/themes/default/items/browse.php
added (again, we could use injection)

<li><?php echo link_to_item(__('Duplicate'), array(), 'duplicate'); ?></li>

just after

<li><?php echo link_to_item(__('Edit'), array(), 'edit'); ?></li>

admin/themes/default/items/tag-form.php
added (necessary; otherwise, loaded existing tags will not be saved for the new item)

$request = Zend_Controller_Front::getInstance()->getRequest();
$controller = $request->getControllerName();
$action = $request->getActionName();
if ($controller == ā€˜itemsā€™ && $action == ā€˜duplicateā€™) {
$tags_text = implode(option(ā€˜tag_delimiterā€™), $tags);
} else {
$tags_text = ā€˜ā€™;
}

just after

$tags = $item->getTags();

and modified

<input type=ā€œhiddenā€ name=ā€œtags-to-addā€ id=ā€œtags-to-addā€ value="ā€™ />

into

<input type=ā€œhiddenā€ name=ā€œtags-to-addā€ id=ā€œtags-to-addā€ value="<?php echo $tags_text?>ā€™ />

(watch out for last single quotes, they actually are double quotes but could not visualize the code here if not using a single quote)

As for application/controllers/ItemsController.php, Iā€™ve added a duplicateAction exactly identical to the editAction, just after it; and a getDuplicateSuccessMessage function just after the getEditSuccessMessage, as follows:

protected function _getDuplicateSuccessMessage($item)
{
$itemTitle = $this->_getElementMetadata($item, ā€˜Dublin Coreā€™, ā€˜Titleā€™);
if ($itemTitle != ā€˜ā€™) {
return __(ā€˜The item ā€œ%sā€ was successfully duplicated!ā€™, $itemTitle);
} else {
return __(ā€˜The item #%s was successfully duplicated!ā€™, strval($item->id));
}
}

As for application/libraries/Omeka/Controller/AbstractActionController.php, Iā€™ve added (always just after the correspondent edit part,

/**
 * Similar to 'edit' action, except this saves record as new.
 *
 * Every request to this action must pass a record ID in the 'id' parameter.
 *
 * @uses Omeka_Controller_Action_Helper_Db::getDefaultModelName()
 * @uses Omeka_Controller_Action_Helper_Db::findById()
 * @uses self::_getDuplicateSuccessMessage()
 * @uses self::_redirectAfterDuplicate()
 */
public function duplicateAction()
{
    $class = $this->_helper->db->getDefaultModelName();
    $varName = $this->view->singularize($class);

    $record = $this->_helper->db->findById();

    if ($this->_autoCsrfProtection) {
        $csrf = new Omeka_Form_SessionCsrf;
        $this->view->csrf = $csrf;
    }
  
    if ($this->getRequest()->isPost()) {
  	$record = new $class();
        if ($this->_autoCsrfProtection && !$csrf->isValid($_POST)) {
            $this->_helper->_flashMessenger(__('There was an error on the form. Please try again.'), 'error');
            $this->view->$varName = $record;
            return;
        }
        $record->setPostData($_POST);
        if ($record->save(false)) {
            $successMessage = $this->_getDuplicateSuccessMessage($record);
            if ($successMessage != '') {
                $this->_helper->flashMessenger($successMessage, 'success');
            }
            $this->_redirectAfterDuplicate($record);
        } else {
            $this->_helper->flashMessenger($record->getErrors());
        }
    }

    $this->view->$varName = $record;
}	

and

/**
 * Redirect to another page after a record is successfully duplicated.
 *
 * The default is to redirect to this record's show page.
 *
 * @param Omeka_Record_AbstractRecord $record
 */
protected function _redirectAfterDuplicate($record)
{
    $this->_helper->redirector('show', null, null, array('id' => $record->id));
}

As for this ones, I was wondering whether injection or rerouting should make the trick.

Finally, Iā€™ve created ex-novo a admin/themes/default/items/duplicate.php that is a copy of admin/themes/default/items/edit.php with a couple of changes:

$itemTitle = __(ā€˜Duplicate Item #%sā€™, metadata(ā€˜itemā€™, ā€˜idā€™)) . $itemTitle;

and

<input type=ā€œsubmitā€ name=ā€œsubmitā€ class=ā€œsubmit big green buttonā€ id=ā€œadd_itemā€ value="<?php echo __('Duplicate Item'); ?>ā€™ />

Beware: last one is a double quote, not a single one.

The germ of the plugin in on Github, now: https://github.com/DBinaghi/plugin-ItemDuplicator.

Iā€™ve managed to include changes to some of the pages in the plugin, so no need for hard-code them.
Still, some need to be implemented:

  • application/libraries/Omeka/Controller/AbstractActionController.php
  • application/Controllers/ItemsController.php
  • admin/themes/default/items/duplicate.php
  • admin/themes/default/items/tag-form.php

Besides, I would ideally want to empty some of the fields (like Title, for instance), when duplicating, as the new Item should have some differences from the original one.

Anyone interested in working at this plugin with me is, as usual, more than welcome.

1 Like

Hello,
thank you for all this work. I would very much like to implement this plugin on our omeka instance: it would save us a lot of time on data entry.
Unfortunately after installing it, when I click on the [duplicate] button a 404 error is displayed. Could you help us?
Thank you in advance!
Here is the information about our instance:
Version of omeka 2.6.1
Default theme 2.5
php : 7.0.33
my sql : 5.7.26
public website (in progress) www.pierre-henry.org

Translated with www.DeepL.com/Translator

Hello, @cyrilledel.
Error 404 normally means that the server could not find what was requested, aka some page. I wonder whether you have created the admin/themes/default/items/duplicate.php page as explained above, that might be the origin of the issue: as said, the plugin is not complete, some parts have to be done manually.
Let me know if this helps.

Reporting some progress here: Iā€™ve managed to reduce the amount of files to be hard coded to 1 (application/Controllers/ItemsController.php), by copying the themes default files (or adding the new ones) to views/admin/items folder of the plugin (see https://github.com/DBinaghi/plugin-ItemDuplicator).

What I still need to do:

  1. extend the ItemsController in the plugin with the new functions added, instead of editing it in the core;
  2. find a way (it it exists) to reference to files in admin/themes/default/items folder from the plugin, instead of having to copy there the ones that are unchanged too (the only 2 files changed are tag-form and files-form, while the only added one is duplicate; itā€™d be great if there was no need to include in the plugin also form-tabs, form and item-type-form, and just reference to them in their original position).

As usual, any help/assistance/suggestion would be really appreciated.