Copy a page(ContentType)

Topics: Customizing Orchard
Nov 28, 2011 at 11:28 AM
Edited Nov 28, 2011 at 11:31 AM

To fasten up webdevelopment when creating pages, I'm wondering how can I copy a page and all the data etc?
What I'm trying todo is to add a Copy link "View | Publish | Copy" under Content Items, and paste it as a new page with all contentitems attached as new items.
I have a few ideas, but I dont know the best way to it.

Anyone?

Nov 28, 2011 at 7:22 PM

It's something I'd like to do as well. I had some ideas for fairly extreme ways to make this happen, but I just had a little dig around and there's a Copy method on IRepository<T>. So we could use perhaps this to copy data from all the parts on one record to another. I'm not sure though how to request an IRepository<T> without already having the T as a generic type parameter.

Coordinator
Nov 28, 2011 at 7:58 PM

Yes, you hit the problem. We would all love to have that, but it's impossible. Well, who am I kidding, nothing is, but due to the very open architecture we have, it's one of those things that are more difficult than they seem they should be. Here's an idea: hijack the import/export on the drivers to do it. Sure, parts that don't implement the hooks will fall through but properly implemented ones will work just fine.

Nov 28, 2011 at 9:08 PM

Hi fellows. Huhhh.. I thought this could be an already answered question!!! 

But can't we just ask the database for a given id and cast that as a ContentType when returned, give it a new id and insert it as a new item?
This must be resolved to be competitive. It take too much time to set up one page. I'll rather copy and edit existing contentitems.
There must be some realtion between a Page and the contentitems added to it?

"Still a newbe, but I'll dont give up until it's done"

Nov 28, 2011 at 9:14 PM

(Ab)using import/export was something else I thought of. What'd be nice would be a "Clone" hook alongside Import/Export in drivers - it'd be trivial to implement for most ContentParts, just a matter of copying all the fields from one instance to another. Maybe I should add that to 1.5 wishes?

Coordinator
Nov 28, 2011 at 9:18 PM

I'd worry about API inflation, especially as this seems to be adding no really new feature. A helper outside of drivers but using the already existing drivers would have my preference. There is already so much that part developers have to write...

@rickard: you don't know that a content part is stored as a record in the database. There is no obligation to do so, and many parts don't.

Nov 28, 2011 at 9:24 PM
Edited Nov 28, 2011 at 9:26 PM

@rickard - that could work for some cases - the problem is you never really know exactly what different parts are doing or how they model their data. At the very minimum you need to be able to hook into custom cloning code for any type of ContentPart (you could have a default handler that at minimum copied the ContentPartRecord, but even then things might get very complicated, you won't be aware of things like foreign keys that might exist).

There is another system that could be abused: the content editor. Since you want this for the purpose of immediately editing the clone, we can create a new controller (based on the Orchard.Core.Contents.Controllers.AdminController). Its actions need to do the following:

1) In the Clone (GET) action, you build an editor for the existing content item

2) In the Clone (POST) action, create a new item of that content type, and use ContentManager.UpdateEditor to populate it from the form, followed by Save or Publish

I think that could work...

Coordinator
Nov 28, 2011 at 9:28 PM

Not to mention that anytime you do cloning, you have to decide deep vs. shallow copy, reference vs clone, and there isn't always a good way to decide that. Using the editor? Ew. And not all information is exposed in the editor. How do you know you didn't miss an essential part? The import/export seems a lot more promising, as that is pretty much what it was made for: create clones of items, on another system. Scratch the "another system" part and you're in business.

Nov 28, 2011 at 11:15 PM

Uhh.. You know this better than me, But I'm a stubbered guy :-) I only see solutions, not problems ;.) For me Orchard is DNA, and I'm just a bug right now!!!
There must be a way to know what part is added to a webpage... Currently they displays on a webpage. Atleast some of the parts should be knowned!! I'm I wrong?
I realise the problem and I just have that in mind, but since the relationship to ContentItems seams unknown, how does it show on pages?

Nov 28, 2011 at 11:26 PM

When a ContentItem is displayed or edited, all the parts are gathered up in code. They're provided by IContentHandler hooks, which could be welding on any part from anything. Some of those parts may come from a ContentPart<TRecord> (in which case there's a special handler that automatically welds on all content part that have drivers) - but parts can and do come from other sources, it could just be a model you created in memory with no storage, it could have a completely different storage model to the normal NHibernate records. The point is, you only know by the end of the display process which parts are there at that time, not how they got there or even whether there's anything else that could turn up there in a different context. If you don't know how they're stored, there's no automated way to tell how to clone them.

However, I think my suggestion based around BuildEditor / UpdateEditor should work in pretty much all situations, since it accurately reproduces the process of creating an item, just with pre-populated data; it leverages the existing channels so existing drivers should just work. It's probably actually how Versioning works.

Coordinator
Nov 28, 2011 at 11:30 PM

Still really dislike the editor idea. But the versioning one may be a good lead. You should look into it.

Nov 28, 2011 at 11:46 PM
bertrandleroy wrote:

Still really dislike the editor idea. But the versioning one may be a good lead. You should look into it.

I've started by create a new command "Copy" just like Publish or Remove. I also tested to create a version of the Page as a new item, but I can't create a new ContentItem due to protection level, but where here to extend what where doing, right?... I was thinking of asking for the id of a ContentType and then look for referenced items. I've done this before, even though I have to go thrue each assembly to get values to insert.
Is this impossible?

Coordinator
Nov 29, 2011 at 12:06 AM

It's possible but it won't do what you think it will. At least not in all situations.

Nov 29, 2011 at 2:09 AM

You can ask the ContentManager for items of any specific content types (they're only reference by name) but I don't see how that helps. You can also ask the ContentManager for a new item of a specific type, ready to fill all the parts with data if only you know how to do that.

The two bits of code which do know how to do that are Import, and Update. So the only way to achieve this fairly automatically without writing loads of new code must be to use one of those two.

Still there are things that will get tricky; for instance with Containers, do you have to clone all the Contained items as well? Otherwise what'll probably happen with either import or editor methods is that the contained items will get overwritten with a new parent, so the original will always lose its list.

I started looking at Versioning. It does use an IRepository to clone each record, but it only operates on ContentItemVersionRecords. Maybe there's a way we could cheat by firing the Versioning event and doing something similar for non-ContentItemVersionRecords, but it's quite a tricky system to follow and I'm still not sure if it'd work.

Nov 29, 2011 at 6:57 PM

This seams to be a bit tricky to achive! I think Orchard must have some base functionality in core. I'm not worried about writing a lot of new code, if I know it will work in the end. I also started with a look at IRepository, and I think this is where it should be done. Another way I thought about was to add a new table to db that keeps track of all ContentItems when their insterted into db. It could contain id of original Page, ContentType, PageID and zonesid's. This will affect widgets as well. But I dont know if I get all that information from a ContentItem. What do you think?

Coordinator
Nov 29, 2011 at 7:21 PM

No, there isn't anything in core. As we said before, IRepository will work for parts that do have a repository, and that may be enough for your usage, but there is no obligation for a part to have one. So no, this is not where it should be done, unless you are ok with the strong limitations.

I don't understand what you're suggesting with the new tracking table.

Nov 29, 2011 at 7:22 PM

That table already exists; it still doesn't solve the problem that the content item could have additional part storage in a completely different database for all you know. What is the distinction between "id of original Page" and "PageID" - as far as I can see they are the same thing? And zones don't have ids  ...

Mar 10, 2012 at 4:49 PM

I understand the discussion above and figured out a super easy way to implement a copy function. What I wanted to do is to have a copy link next to my edit and delete commands on the List View.

When the user clicks the copy link it is routed to my controller where the following code gets executed:

        //
        // GET: /EventsAdmin/Copy/5
        public ActionResult Copy(int id) {
            var eventPart = _eventService.Get(id);
            if (eventPart == null)
                return HttpNotFound();
 
            dynamic model = _orchardServices.ContentManager.BuildEditor(eventPart);
            return View((object)model);
        }

So, I just build a regular editor, just as if it was an Edit command. Next when the user saves the form post is routed to the controller again to the following code:

        //
        // POST: /EventsAdmin/Copy
        [HttpPostActionName("Copy")]
        private ActionResult CopyEvent(FormCollection collection) {
            var eventPart = _eventService.New();
 
            _eventService.Create(eventPart);
            dynamic model = _orchardServices.ContentManager.UpdateEditor(eventPart, this);
 
            if (!ModelState.IsValid)
                return View(model);
  
            _orchardServices.Notifier.Information(string.IsNullOrWhiteSpace(eventPart.TypeDefinition.DisplayName)
                ? T("That content has been created.")
                : T("That {0} has been created.", eventPart.TypeDefinition.DisplayName));
 
            return RedirectToAction("Index");
        }

So, my copy is basically an Edit (for the Http Get) and Create (for the HttpPost).

I know it's not really a copy from the API perspective but tbh it works and it is simple :)

Developer
Mar 10, 2012 at 5:19 PM

@eriklenaerts Good enough for me, nice!