diff --git a/umbraco.Test/DocumentTest.cs b/umbraco.Test/DocumentTest.cs index 60ca65ac66..29a6ea7143 100644 --- a/umbraco.Test/DocumentTest.cs +++ b/umbraco.Test/DocumentTest.cs @@ -13,6 +13,7 @@ using umbraco.editorControls.textfield; using umbraco.cms.businesslogic.propertytype; using umbraco.cms.businesslogic.property; using umbraco.cms.businesslogic.language; +using umbraco.BusinessLogic.console; namespace umbraco.Test { @@ -35,6 +36,50 @@ namespace umbraco.Test #region Unit Tests + /// + /// Creates a bunch of nodes in a heirarchy, then deletes the top most node (moves to the recycle bin + /// and completely deletes from system.) This should completely delete all of these nodes from the database. + /// + [TestMethod()] + public void Document_DeleteHeirarchyPermanentlyTest() + { + var docList = new List(); + var total = 20; + var dt = new DocumentType(GetExistingDocTypeId()); + //allow the doc type to be created underneath itself + dt.AllowedChildContentTypeIDs = new int[] { dt.Id }; + dt.Save(); + + //create 20 content nodes underneath each other, this will test deleting with heirarchy as well + var lastParentId = -1; + for (var i = 0; i < total; i++) + { + var newDoc = Document.MakeNew(i.ToString() + Guid.NewGuid().ToString("N"), dt, m_User, lastParentId); + docList.Add(newDoc); + Assert.IsTrue(docList[docList.Count - 1].Id > 0); + lastParentId = newDoc.Id; + } + + //now delete all of them permanently, since they are nested, we only need to delete one + docList.First().delete(true); + + //make sure they are all gone + foreach (var d in docList) + { + Assert.IsFalse(Document.IsNode(d.Id)); + } + + } + + /// + ///A test for PublishWithResult + /// + [TestMethod()] + public void Document_PublishWithResultTest() + { + var val = m_NewRootDoc.PublishWithResult(m_User); + } + /// /// Creates a doc type, assigns a domain to it and removes it /// @@ -83,26 +128,28 @@ namespace umbraco.Test { //System.Diagnostics.Debugger.Break(); Document target = new Document(GetExistingNodeId()); - int parentId = target.Level == 1 ? -1 : target.Parent.Id; + int parentId = target.ParentId; bool RelateToOrignal = false; //get children ids for the current parent - var childrenIds = target.Level == 1 ? Document.GetRootDocuments().ToList().Select(x => x.Id) : target.Parent.Children.ToList().Select(x => x.Id); + var childrenIds = GetChildNodesOfParent(target).Select(x => x.Id); //copy the node target.Copy(parentId, m_User, RelateToOrignal); //test that the child id count + 1 is equal to the total child count - Assert.AreEqual(childrenIds.Count() + 1, target.Level == 1 ? Document.GetRootDocuments().Count() : target.Parent.ChildCount); + Assert.AreEqual(childrenIds.Count() + 1, GetChildNodesOfParent(target).Count(), "Child node counts do not match"); //get the list of new child ids from the parent - var newChildIds = target.Level == 1 ? Document.GetRootDocuments().ToList().Select(x => x.Id) : target.Parent.Children.ToList().Select(x => x.Id); + var newChildIds = GetChildNodesOfParent(target).Select(x => x.Id); //get the children difference which should be the new node var diff = newChildIds.Except(childrenIds); - + Assert.AreEqual(1, diff.Count()); + //get the node that is the difference to compare Document newDoc = new Document(diff.First()); + Assert.AreEqual(parentId, newDoc.ParentId); RecycleAndDelete(newDoc); } @@ -115,15 +162,15 @@ namespace umbraco.Test { //System.Diagnostics.Debugger.Break(); Document target = new Document(GetExistingNodeId()); - int parentId = target.Level == 1 ? -1 : target.Parent.Id; + int parentId = target.ParentId; bool RelateToOrignal = true; - //get children ids - var childrenIds = target.Parent.Children.ToList().Select(x => x.Id); + //get children ids + var childrenIds = GetChildNodesOfParent(target).Select(x => x.Id); target.Copy(parentId, m_User, RelateToOrignal); - Assert.AreEqual(childrenIds.Count() + 1, target.Level == 1 ? Document.GetRootDocuments().Count() : target.Parent.ChildCount); + Assert.AreEqual(childrenIds.Count() + 1, GetChildNodesOfParent(target).Count()); Document parent = new Document(parentId); //get the children difference which should be the new node @@ -355,7 +402,7 @@ namespace umbraco.Test var doc = new Document(GetExistingNodeId()); //create new content based on the existing content in the same heirarchy var dt = new DocumentType(doc.ContentType.Id); - var parentId = doc.Level == 1 ? -1 : doc.Parent.Id; + var parentId = doc.ParentId; var newDoc = Document.MakeNew("NewDoc" + Guid.NewGuid().ToString("N"), dt, m_User, parentId); Assert.IsTrue(newDoc.Id > 0); @@ -379,34 +426,36 @@ namespace umbraco.Test [TestMethod] public void Document_EmptyRecycleBinTest() { - var totalTrashedItems = RecycleBin.Count(RecycleBin.RecycleBinType.Content); - var docList = new List(); var total = 20; - var dt = new DocumentType(GetExistingDocTypeId()); - //create 20 content nodes + var dt = m_ExistingDocType; + //allow the doc type to be created underneath itself + dt.AllowedChildContentTypeIDs = new int[] { dt.Id }; + dt.Save(); + + //create 20 content nodes underneath each other, this will test deleting with heirarchy as well + var lastParentId = -1; for (var i = 0; i < total; i++) { - docList.Add(Document.MakeNew(i.ToString() + Guid.NewGuid().ToString("N"), dt, m_User, -1)); + var newDoc = Document.MakeNew("R-" + i.ToString() + Guid.NewGuid().ToString("N"), dt, m_User, lastParentId); + docList.Add(newDoc); Assert.IsTrue(docList[docList.Count - 1].Id > 0); + Assert.AreEqual(lastParentId, newDoc.ParentId); + lastParentId = newDoc.Id; } - //now delete all of them - foreach (var d in docList) - { - d.delete(); - Assert.IsTrue(d.IsTrashed); - } + //now delete all of them, since they are nested, we only need to delete one + docList.First().delete(); //a callback action for each item removed from the recycle bin var totalDeleted = 0; - var deleteCallback = new Action(x => - { - Assert.AreEqual((total + totalTrashedItems) - (++totalDeleted), x); - }); var bin = new RecycleBin(RecycleBin.RecycleBinType.Content); - bin.CallTheGarbageMan(deleteCallback); + var totalTrashedItems = bin.GetDescendants().Cast().Count(); + bin.CallTheGarbageMan(x => + { + Assert.AreEqual(totalTrashedItems - (++totalDeleted), x); + }); Assert.AreEqual(0, RecycleBin.Count(RecycleBin.RecycleBinType.Content)); } @@ -540,21 +589,7 @@ namespace umbraco.Test // Assert.Inconclusive("A method that does not return a value cannot be verified."); //} - ///// - /////A test for PublishWithResult - ///// - //[TestMethod()] - //public void PublishWithResultTest() - //{ - // Guid id = new Guid(); // TODO: Initialize to an appropriate value - // Document target = new Document(id); // TODO: Initialize to an appropriate value - // User u = null; // TODO: Initialize to an appropriate value - // bool expected = false; // TODO: Initialize to an appropriate value - // bool actual; - // actual = target.PublishWithResult(u); - // Assert.AreEqual(expected, actual); - // Assert.Inconclusive("Verify the correctness of this test method."); - //} + ///// /////A test for PublishWithChildrenWithResult @@ -745,15 +780,20 @@ namespace umbraco.Test } var id = d.Id; - //now recycle it - d.delete(); - Assert.IsTrue(d.IsTrashed); + //check if it is already trashed + var alreadyTrashed = d.IsTrashed; - Document recycled = new Document(id); - //now delete it - recycled.delete(); + if (!alreadyTrashed) + { + //now recycle it + d.delete(); + Assert.IsTrue(d.IsTrashed); + } + + //now permanently delete + d.delete(true); Assert.IsFalse(Document.IsNode(id)); //check with sql that it is gone @@ -814,6 +854,32 @@ namespace umbraco.Test return ids[index]; } + /// + /// A helper method to get the parent node. + /// The reason we need this is because the API currently throws an exception if we access the Parent property + /// of a node and the node is on level 1. This also causes issues if the node is in level 1 in the recycle bin. + /// + /// + /// + private IEnumerable GetChildNodesOfParent(Document d) + { + if (d.ParentId == (int)RecycleBin.RecycleBinType.Content) + { + return new RecycleBin(RecycleBin.RecycleBinType.Content).Children.ToList(); + } + else + { + if (d.Level == 1) + { + return Document.GetRootDocuments(); + } + else + { + return d.Parent.Children; + } + } + } + private DocumentType GetExistingDocType() { DocumentType dct = new DocumentType(GetExistingDocTypeId()); diff --git a/umbraco.Test/DocumentTypeTest.cs b/umbraco.Test/DocumentTypeTest.cs index fe95eddd56..2ee7f4d9a4 100644 --- a/umbraco.Test/DocumentTypeTest.cs +++ b/umbraco.Test/DocumentTypeTest.cs @@ -22,18 +22,80 @@ namespace umbraco.Test [TestClass()] public class DocumentTypeTest { + [TestMethod()] + public void DocumentType_DeleteDocTypeWithContennt() + { + var dt = CreateNewDocType(); + var doc = Document.MakeNew("TEST" + Guid.NewGuid().ToString("N"), dt, m_User, -1); + Assert.IsInstanceOfType(doc, typeof(Document)); + Assert.IsTrue(doc.Id > 0); + + DeleteDocType(dt); + + Assert.IsFalse(Document.IsNode(doc.Id)); + } + + /// + /// This will create 3 document types, and create nodes in the following structure: + /// - root + /// -- node1 (of doc type #1) + /// --- node 2 (of doc type #2) + /// ---- node 3 (of doc type #1) + /// ----- node 4 (of doc type #3) + /// + /// Then we'll delete doc type #1. The result should be that node1 and node3 are completely deleted from the database and node2 and node4 are + /// moved to the recycle bin. + /// + [TestMethod()] + public void DocumentType_DeleteDocTypeWithContentAndChildrenOfDifferentDocTypes() + { + //System.Diagnostics.Debugger.Break(); + + //create the doc types + var dt1 = CreateNewDocType(); + var dt2 = CreateNewDocType(); + var dt3 = CreateNewDocType(); + + //create the heirarchy + dt1.AllowedChildContentTypeIDs = new int[] { dt2.Id, dt3.Id }; + dt1.Save(); + dt2.AllowedChildContentTypeIDs = new int[] { dt1.Id }; + dt2.Save(); + + //create the content tree + var node1 = Document.MakeNew("TEST" + Guid.NewGuid().ToString("N"), dt1, m_User, -1); + var node2 = Document.MakeNew("TEST" + Guid.NewGuid().ToString("N"), dt2, m_User, node1.Id); + var node3 = Document.MakeNew("TEST" + Guid.NewGuid().ToString("N"), dt1, m_User, node2.Id); + var node4 = Document.MakeNew("TEST" + Guid.NewGuid().ToString("N"), dt3, m_User, node3.Id); + + //do the deletion of doc type #1 + DeleteDocType(dt1); + + //do our checks + Assert.IsFalse(Document.IsNode(node1.Id), "node1 is not deleted"); //this was of doc type 1, should be gone + Assert.IsFalse(Document.IsNode(node3.Id), "node3 is not deleted"); //this was of doc type 1, should be gone + + Assert.IsTrue(Document.IsNode(node2.Id), "node2 is deleted"); + Assert.IsTrue(Document.IsNode(node4.Id), "node4 is deleted"); + + node2 = new Document(node2.Id);//need to re-query the node + Assert.IsTrue(node2.IsTrashed, "node2 is not in the trash"); + node4 = new Document(node4.Id); //need to re-query the node + Assert.IsTrue(node4.IsTrashed, "node 4 is not in the trash"); + + //remove the old data + DeleteDocType(dt2); + DeleteDocType(dt3); + + } /// ///A test for creating a new document type /// [TestMethod()] public void DocumentType_MakeNewTest() - { - var dt = DocumentType.MakeNew(m_User, "TEST" + Guid.NewGuid().ToString("N")); - Assert.IsTrue(dt.Id > 0); - Assert.AreEqual(DateTime.Now.Date, dt.CreateDateTime.Date); - - DeleteDocType(dt); + { + Assert.IsInstanceOfType(m_NewDocType, typeof(DocumentType)); } /// @@ -43,18 +105,14 @@ namespace umbraco.Test public void DocumentType_AddPropertiesToTabThenDeleteItTest() { //System.Diagnostics.Debugger.Break(); - - var dt = DocumentType.MakeNew(m_User, "TEST" + Guid.NewGuid().ToString("N")); - Assert.IsTrue(dt.Id > 0); - Assert.AreEqual(DateTime.Now.Date, dt.CreateDateTime.Date); - + //allow itself to be created under itself - dt.AllowedChildContentTypeIDs = new int[] { dt.Id }; + m_NewDocType.AllowedChildContentTypeIDs = new int[] { m_NewDocType.Id }; //create a tab - dt.AddVirtualTab("TEST"); + m_NewDocType.AddVirtualTab("TEST"); //test the tab - var tabs = dt.getVirtualTabs.ToList(); + var tabs = m_NewDocType.getVirtualTabs.ToList(); Assert.AreEqual(1, tabs.Count); //create a property @@ -63,22 +121,20 @@ namespace umbraco.Test foreach (var dataType in allDataTypes) { //add a property type of the first type found in the list - dt.AddPropertyType(dataType, "testProperty" + (++i).ToString(), "Test Property" + i.ToString()); + m_NewDocType.AddPropertyType(dataType, "testProperty" + (++i).ToString(), "Test Property" + i.ToString()); //test the prop - var prop = dt.getPropertyType("testProperty" + i.ToString()); + var prop = m_NewDocType.getPropertyType("testProperty" + i.ToString()); Assert.IsTrue(prop.Id > 0); Assert.AreEqual("Test Property" + i.ToString(), prop.Name); //put the properties to the tab - dt.SetTabOnPropertyType(prop, tabs[0].Id); + m_NewDocType.SetTabOnPropertyType(prop, tabs[0].Id); //re-get the property since data is cached in the object - prop = dt.getPropertyType("testProperty" + i.ToString()); + prop = m_NewDocType.getPropertyType("testProperty" + i.ToString()); Assert.AreEqual(tabs[0].Id, prop.TabId); } //now we need to delete the tab - dt.DeleteVirtualTab(tabs[0].Id); - - dt.delete(); + m_NewDocType.DeleteVirtualTab(tabs[0].Id); } /// @@ -688,6 +744,23 @@ namespace umbraco.Test private User m_User = new User(0); + /// + /// before each test starts, this object is created so it can be used for testing. + /// + private DocumentType m_NewDocType; + + /// + /// Create a brand new document type + /// + /// + private DocumentType CreateNewDocType() + { + var dt = DocumentType.MakeNew(m_User, "TEST" + Guid.NewGuid().ToString("N")); + Assert.IsTrue(dt.Id > 0); + Assert.AreEqual(DateTime.Now.Date, dt.CreateDateTime.Date); + return dt; + } + private void DeleteDocType(DocumentType dt) { var id = dt.Id; @@ -739,18 +812,25 @@ namespace umbraco.Test //{ //} // - //Use TestInitialize to run code before running each test - //[TestInitialize()] - //public void MyTestInitialize() - //{ - //} - // - //Use TestCleanup to run code after each test has run - //[TestCleanup()] - //public void MyTestCleanup() - //{ - //} - // + + /// + /// Create a new document type for use in tests + /// + [TestInitialize()] + public void MyTestInitialize() + { + m_NewDocType = CreateNewDocType(); + } + + /// + /// Remove the created document type + /// + [TestCleanup()] + public void MyTestCleanup() + { + DeleteDocType(m_NewDocType); + } + #endregion } diff --git a/umbraco/cms/businesslogic/CMSNode.cs b/umbraco/cms/businesslogic/CMSNode.cs index be208fd0ad..c135609836 100644 --- a/umbraco/cms/businesslogic/CMSNode.cs +++ b/umbraco/cms/businesslogic/CMSNode.cs @@ -12,6 +12,7 @@ using System.Text.RegularExpressions; using System.ComponentModel; using umbraco.IO; using umbraco.cms.businesslogic.media; +using System.Collections; namespace umbraco.cms.businesslogic { @@ -51,6 +52,7 @@ namespace umbraco.cms.businesslogic #endregion #region Private static + private static readonly string m_DefaultIconCssFile = IOHelper.MapPath(SystemDirectories.Umbraco_client + "/Tree/treeIcons.css"); private static List m_DefaultIconClasses = new List(); private static void initializeIconClasses() @@ -74,6 +76,12 @@ namespace umbraco.cms.businesslogic m_DefaultIconClasses.Add(cssClass); } } + private const string m_SQLSingle = "SELECT id, createDate, trashed, parentId, nodeObjectType, nodeUser, level, path, sortOrder, uniqueID, text FROM umbracoNode WHERE id = @id"; + private const string m_SQLDescendants = @" + SELECT id, createDate, trashed, parentId, nodeObjectType, nodeUser, level, path, sortOrder, uniqueID, text + FROM umbracoNode + WHERE path LIKE '%,{0},%'"; + #endregion #region Public static @@ -408,56 +416,35 @@ order by level,sortOrder"; return base.ToString(); } - /// - /// Moves the CMSNode from the current position in the hierarchy to the target - /// - /// Target CMSNode id - public void Move(int newParentId) + private void Move(CMSNode parent) { - //first we need to establish if the node already exists under the parent node - var isSameParent = (Path.Contains("," + newParentId + ",")); - MoveEventArgs e = new MoveEventArgs(); FireBeforeMove(e); if (!e.Cancel) { - CMSNode n = new CMSNode(newParentId); + //first we need to establish if the node already exists under the parent node + var isSameParent = (Path.Contains("," + parent.Id + ",")); //if it's the same parent, we can save some SQL calls since we know these wont change. //level and path might change even if it's the same parent because the parent could be moving somewhere. if (!isSameParent) { - int maxSortOrder = SqlHelper.ExecuteScalar( - "select coalesce(max(sortOrder),0) from umbracoNode where parentid = @parentId", - SqlHelper.CreateParameter("@parentId", newParentId)); + int maxSortOrder = SqlHelper.ExecuteScalar("select coalesce(max(sortOrder),0) from umbracoNode where parentid = @parentId", + SqlHelper.CreateParameter("@parentId", parent.Id)); - this.Parent = n; + this.Parent = parent; this.sortOrder = maxSortOrder + 1; - } - - this.Level = n.Level + 1; - this.Path = n.Path + "," + this.Id.ToString(); + } + + this.Level = parent.Level + 1; + this.Path = parent.Path + "," + this.Id.ToString(); //this code block should not be here but since the class structure is very poor and doesn't use //overrides (instead using shadows/new) for the Children property, when iterating over the children //and calling Move(), the super classes overridden OnMove or Move methods never get fired, so //we now need to hard code this here :( - //make sure the node type is a document/media, if it is a recycle bin then this will not be equal - if (n.nodeObjectType == Document._objectType) - { - //regenerate the xml for the parent node - var d = new Document(n.Id); - d.XmlGenerate(new XmlDocument()); - } - else if (n.nodeObjectType == Media._objectType) - { - //regenerate the xml for the parent node - var m = new Media(n.Id); - m.XmlGenerate(new XmlDocument()); - } - if (Path.Contains("," + ((int)RecycleBin.RecycleBinType.Content).ToString() + ",") || Path.Contains("," + ((int)RecycleBin.RecycleBinType.Media).ToString() + ",")) { @@ -469,16 +456,40 @@ order by level,sortOrder"; if (IsTrashed) IsTrashed = false; //don't update if it's not necessary } + //make sure the node type is a document/media, if it is a recycle bin then this will not be equal + if (!IsTrashed && parent.nodeObjectType == Document._objectType) + { + //regenerate the xml for the parent node + var d = new Document(parent.Id); + d.XmlGenerate(new XmlDocument()); + } + else if (!IsTrashed && parent.nodeObjectType == Media._objectType) + { + //regenerate the xml for the parent node + var m = new Media(parent.Id); + m.XmlGenerate(new XmlDocument()); + } + var children = this.Children; foreach (CMSNode c in children) { - c.Move(this.Id); + c.Move(this); } FireAfterMove(e); } } + /// + /// Moves the CMSNode from the current position in the hierarchy to the target + /// + /// Target CMSNode id + public void Move(int newParentId) + { + CMSNode parent = new CMSNode(newParentId); + Move(parent); + } + /// /// Deletes this instance. /// @@ -500,7 +511,6 @@ order by level,sortOrder"; } } - /// /// Does the current CMSNode have any child nodes. /// @@ -524,7 +534,31 @@ order by level,sortOrder"; _hasChildrenInitialized = true; _hasChildren = value; } - } + } + + /// + /// Returns all descendant nodes from this node. + /// + /// + /// + /// This doesn't return a strongly typed IEnumerable object so that we can override in in super clases + /// and since this class isn't a generic (thought it should be) this is not strongly typed. + /// + public virtual IEnumerable GetDescendants() + { + var descendants = new List(); + using (IRecordsReader dr = SqlHelper.ExecuteReader(string.Format(m_SQLDescendants, Id))) + { + while (dr.Read()) + { + var node = new CMSNode(dr.GetInt("id"), true); + node.PopulateCMSNodeFromReader(dr); + descendants.Add(node); + } + } + return descendants; + } + #endregion #region Public properties @@ -602,6 +636,13 @@ order by level,sortOrder"; get { return _id; } } + /// + /// Get the parent id of the node + /// + public int ParentId + { + get { return _parentid; } + } /// /// Given the hierarchical tree structure a CMSNode has only one parent but can have many children @@ -690,7 +731,8 @@ order by level,sortOrder"; { System.Collections.ArrayList tmp = new System.Collections.ArrayList(); using (IRecordsReader dr = SqlHelper.ExecuteReader("SELECT id, createDate, trashed, parentId, nodeObjectType, nodeUser, level, path, sortOrder, uniqueID, text FROM umbracoNode WHERE ParentID = @ParentID AND nodeObjectType = @type order by sortOrder", - SqlHelper.CreateParameter("@type", this.nodeObjectType), SqlHelper.CreateParameter("ParentID", this.Id))) + SqlHelper.CreateParameter("@type", this.nodeObjectType), + SqlHelper.CreateParameter("ParentID", this.Id))) { while (dr.Read()) { @@ -816,9 +858,8 @@ order by level,sortOrder"; /// protected virtual void setupNode() { - using (IRecordsReader dr = SqlHelper.ExecuteReader( - "SELECT createDate, trashed, parentId, nodeObjectType, nodeUser, level, path, sortOrder, uniqueID, text FROM umbracoNode WHERE id = " + this.Id - )) + using (IRecordsReader dr = SqlHelper.ExecuteReader(m_SQLSingle, + SqlHelper.CreateParameter("@id",this.Id))) { if (dr.Read()) { diff --git a/umbraco/cms/businesslogic/RecycleBin.cs b/umbraco/cms/businesslogic/RecycleBin.cs index a7aea12c35..e12f296164 100644 --- a/umbraco/cms/businesslogic/RecycleBin.cs +++ b/umbraco/cms/businesslogic/RecycleBin.cs @@ -5,6 +5,7 @@ using System.Linq; using umbraco.DataLayer; using umbraco.cms.businesslogic.web; using umbraco.cms.businesslogic.media; +using System.Threading; namespace umbraco.cms.businesslogic { @@ -134,12 +135,18 @@ namespace umbraco.cms.businesslogic { lock (m_Locker) { + //first, move all nodes underneath the recycle bin directly under the recycle bin node (flatten heirarchy) + //then delete them all. + + SqlHelper.ExecuteNonQuery("UPDATE umbracoNode SET parentID=@parentID, level=1 WHERE path LIKE '%," + ((int)m_BinType).ToString() + ",%'", + SqlHelper.CreateParameter("@parentID", (int)m_BinType)); + foreach (var c in Children.ToList()) { switch (m_BinType) { case RecycleBinType.Content: - new Document(c.Id).delete(); + new Document(c.Id).delete(true); itemDeletedCallback(RecycleBin.Count(m_BinType)); break; case RecycleBinType.Media: diff --git a/umbraco/cms/businesslogic/web/Document.cs b/umbraco/cms/businesslogic/web/Document.cs index 09169bff26..b7147ef96c 100644 --- a/umbraco/cms/businesslogic/web/Document.cs +++ b/umbraco/cms/businesslogic/web/Document.cs @@ -146,41 +146,38 @@ namespace umbraco.cms.businesslogic.web umbracoNode.createDate, umbracoNode.trashed, umbracoNode.parentId, umbracoNode.nodeObjectType, umbracoNode.nodeUser, umbracoNode.level, umbracoNode.path, umbracoNode.sortOrder, umbracoNode.uniqueId, umbracoNode.text from umbracoNode - inner join - cmsContentVersion on cmsContentVersion.contentID = umbracoNode.id - inner join - cmsDocument on cmsDocument.versionId = cmsContentVersion.versionId - inner join - cmsContent on cmsDocument.nodeId = cmsContent.NodeId - inner join - cmsContentType on cmsContentType.nodeId = cmsContent.ContentType - inner join - umbracoNode contentTypeNode on contentTypeNode.id = cmsContentType.nodeId - left join cmsDocumentType on - cmsDocumentType.contentTypeNodeId = cmsContent.contentType and cmsDocumentType.IsDefault = 1 - where - {0} - order by - {1} - "; - private const string m_SQLOptimizedChildren = @" - select count(children.id) as children, umbracoNode.id, umbracoNode.uniqueId, umbracoNode.level, umbracoNode.parentId, cmsDocument.documentUser, - coalesce(cmsDocument.templateId, cmsDocumentType.templateNodeId) as templateId, umbracoNode.path, umbracoNode.sortOrder, coalesce(publishCheck.published,0) as published, umbracoNode.createDate, cmsDocument.text, cmsDocument.updateDate, cmsContentVersion.versionDate, cmsContentType.icon, cmsContentType.alias, cmsContentType.thumbnail, cmsContentType.description, cmsContentType.masterContentType, cmsContentType.nodeId as contentTypeId - from umbracoNode - left join umbracoNode children on children.parentId = umbracoNode.id - inner join cmsContent on cmsContent.nodeId = umbracoNode.id - inner join cmsContentType on cmsContentType.nodeId = cmsContent.contentType - inner join (select contentId, max(versionDate) AS versionDate from cmsContentVersion - inner join umbracoNode on umbracoNode.id = cmsContentVersion.contentId and umbracoNode.parentId = @parentId - group by contentId) AS temp - on temp.contentId = cmsContent.nodeId - inner join cmsContentVersion on cmsContentVersion.contentId = temp.contentId and cmsContentVersion.versionDate = temp.versionDate - inner join cmsDocument on cmsDocument.versionId = cmsContentversion.versionId - left join cmsDocument publishCheck on publishCheck.nodeId = cmsContent.nodeID and publishCheck.published = 1 - left join cmsDocumentType on - cmsDocumentType.contentTypeNodeId = cmsContent.contentType and cmsDocumentType.IsDefault = 1 + inner join cmsContentVersion on cmsContentVersion.contentID = umbracoNode.id + inner join cmsDocument on cmsDocument.versionId = cmsContentVersion.versionId + inner join cmsContent on cmsDocument.nodeId = cmsContent.NodeId + inner join cmsContentType on cmsContentType.nodeId = cmsContent.ContentType + inner join umbracoNode contentTypeNode on contentTypeNode.id = cmsContentType.nodeId + left join cmsDocumentType on cmsDocumentType.contentTypeNodeId = cmsContent.contentType and cmsDocumentType.IsDefault = 1 where {0} - group by umbracoNode.id, umbracoNode.uniqueId, umbracoNode.level, umbracoNode.parentId, cmsDocument.documentUser, cmsDocument.templateId, cmsDocumentType.templateNodeId, umbracoNode.path, umbracoNode.sortOrder, coalesce(publishCheck.published,0), umbracoNode.createDate, cmsDocument.text, cmsDocument.updateDate, cmsContentVersion.versionDate, cmsContentType.icon, cmsContentType.alias, cmsContentType.thumbnail, cmsContentType.description, cmsContentType.masterContentType, cmsContentType.nodeId + order by {1} + "; + private const string m_SQLOptimizedMany = @" + select count(children.id) as children, umbracoNode.id, umbracoNode.uniqueId, umbracoNode.level, umbracoNode.parentId, + cmsDocument.documentUser, coalesce(cmsDocument.templateId, cmsDocumentType.templateNodeId) as templateId, + umbracoNode.path, umbracoNode.sortOrder, coalesce(publishCheck.published,0) as published, umbracoNode.createDate, + cmsDocument.text, cmsDocument.updateDate, cmsContentVersion.versionDate, cmsContentType.icon, cmsContentType.alias, + cmsContentType.thumbnail, cmsContentType.description, cmsContentType.masterContentType, cmsContentType.nodeId as contentTypeId + from umbracoNode + left join umbracoNode children on children.parentId = umbracoNode.id + inner join cmsContent on cmsContent.nodeId = umbracoNode.id + inner join cmsContentType on cmsContentType.nodeId = cmsContent.contentType + inner join cmsContentVersion on cmsContentVersion.contentId = umbracoNode.id + inner join (select contentId, max(versionDate) as versionDate from cmsContentVersion group by contentId) temp + on cmsContentVersion.contentId = temp.contentId and cmsContentVersion.versionDate = temp.versionDate + inner join cmsDocument on cmsDocument.versionId = cmsContentversion.versionId + left join cmsDocument publishCheck on publishCheck.nodeId = cmsContent.nodeID and publishCheck.published = 1 + left join cmsDocumentType on cmsDocumentType.contentTypeNodeId = cmsContent.contentType and cmsDocumentType.IsDefault = 1 + where {0} + group by + umbracoNode.id, umbracoNode.uniqueId, umbracoNode.level, umbracoNode.parentId, cmsDocument.documentUser, + cmsDocument.templateId, cmsDocumentType.templateNodeId, umbracoNode.path, umbracoNode.sortOrder, + coalesce(publishCheck.published,0), umbracoNode.createDate, cmsDocument.text, + cmsContentType.icon, cmsContentType.alias, cmsContentType.thumbnail, cmsContentType.description, + cmsContentType.masterContentType, cmsContentType.nodeId, cmsDocument.updateDate, cmsContentVersion.versionDate order by {1} "; @@ -396,11 +393,13 @@ namespace umbraco.cms.businesslogic.web //PPH make sure that there is only 1 newest node, this is important in regard to schedueled publishing... SqlHelper.ExecuteNonQuery("update cmsDocument set newest = 0 where nodeId = " + Id); - SqlHelper.ExecuteNonQuery("insert into cmsDocument (newest, nodeId, published, documentUser, versionId, Text, TemplateId) values (1," + - Id + ", 0, " + u.Id + ", @versionId, @text," - + _template + ")", - SqlHelper.CreateParameter("@versionId", newVersion), - SqlHelper.CreateParameter("@text", Text)); + SqlHelper.ExecuteNonQuery("insert into cmsDocument (newest, nodeId, published, documentUser, versionId, Text, TemplateId) values (1,@id, 0, @userId, @versionId, @text, @template)", + SqlHelper.CreateParameter("@id", Id), + SqlHelper.CreateParameter("@template", _template > 0 ? (object)_template : (object)DBNull.Value), //pass null in if the template doesn't have a valid id + SqlHelper.CreateParameter("@userId", u.Id), + SqlHelper.CreateParameter("@versionId", newVersion), + SqlHelper.CreateParameter("@text", Text)); + SqlHelper.ExecuteNonQuery("update cmsDocument set published = 0 where nodeId = " + Id); SqlHelper.ExecuteNonQuery("update cmsDocument set published = 1, newest = 0 where versionId = @versionId", SqlHelper.CreateParameter("@versionId", tempVersion)); @@ -648,6 +647,38 @@ namespace umbraco.cms.businesslogic.web _published = InitPublished; } + protected void PopulateDocumentFromReader(IRecordsReader dr) + { + bool _hc = false; + + if (dr.GetInt("children") > 0) + _hc = true; + + int? masterContentType = null; + + if (!dr.IsNull("masterContentType")) + masterContentType = dr.GetInt("masterContentType"); + + SetupDocumentForTree(dr.GetGuid("uniqueId") + , dr.GetShort("level") + , dr.GetInt("parentId") + , dr.GetInt("documentUser") + , (dr.GetInt("published") == 1) + , dr.GetString("path") + , dr.GetString("text") + , dr.GetDateTime("createDate") + , dr.GetDateTime("updateDate") + , dr.GetDateTime("versionDate") + , dr.GetString("icon") + , _hc + , dr.GetString("alias") + , dr.GetString("thumbnail") + , dr.GetString("description") + , masterContentType + , dr.GetInt("contentTypeId") + , dr.GetInt("templateId")); + } + public override string Text { get @@ -836,8 +867,12 @@ namespace umbraco.cms.businesslogic.web // Make the new document Document NewDoc = MakeNew(Text, new DocumentType(ContentType.Id), u, CopyTo); - // update template - NewDoc.Template = Template; + // update template if a template is set + if (this.Template > 0) + NewDoc.Template = Template; + + //update the trashed property as it could be copied inside the recycle bin + NewDoc.IsTrashed = this.IsTrashed; // Copy the properties of the current document var props = getProperties; @@ -955,12 +990,6 @@ namespace umbraco.cms.businesslogic.web { get { - //SD: Removed old, non-optimized method! - //IconI[] tmp = base.Children; - //Document[] retval = new Document[tmp.Length]; - //for (int i = 0; i < tmp.Length; i++) retval[i] = new Document(tmp[i].Id); - //return retval; - //cache the documents children so that this db call doesn't have to occur again if (this._children == null) this._children = Document.GetChildrenForTree(this.Id); @@ -982,32 +1011,40 @@ namespace umbraco.cms.businesslogic.web } /// - /// Deletes the current document (and all children recursive) + /// Puts the current document in the trash /// public override void delete() { - // Check for recyle bin - if (!Path.Contains("," + ((int)RecycleBin.RecycleBinType.Content).ToString() + ",")) + MoveToTrash(); + } + + /// + /// With either move the document to the trash or permanently remove it from the database. + /// + /// flag to set whether or not to completely remove it from the database or just send to trash + public void delete(bool deletePermanently) + { + if (!deletePermanently) { MoveToTrash(); } else { - DeletePermanently(false); + DeletePermanently(); } } /// /// Used internally to permanently delete the data from the database /// - /// - /// if onlyCurrentDocType is set, this means that we shouldn't delete any children that are not - /// the current document type and instead just move them to the recycle bin. This is effective if + /// + /// if onlyThisDocType is set, this means that we shouldn't delete any children that are not + /// the document type specified and instead just move them to the recycle bin. This is effective if /// we're deleting an entire document type but don't want to delete other data that isn't this document type /// but the ndoe exists as a child of the document type that is being deleted. /// /// returns true if deletion isn't cancelled - private bool DeletePermanently(bool onlyCurrentDocType) + private bool DeletePermanently() { DeleteEventArgs e = new DeleteEventArgs(); @@ -1015,20 +1052,9 @@ namespace umbraco.cms.businesslogic.web if (!e.Cancel) { - - var c = Children; - foreach (Document d in c) + foreach (Document d in Children.ToList()) { - if (onlyCurrentDocType && (d.ContentType.Id != this.ContentType.Id)) - { - //if we're only supposed to be deleting an exact document type, then just move the document - //to the trash - d.MoveToTrash(); - } - else - { - d.DeletePermanently(onlyCurrentDocType); - } + d.DeletePermanently(); } umbraco.BusinessLogic.Actions.Action.RunActionHandlers(this, ActionDelete.Instance); @@ -1085,16 +1111,45 @@ namespace umbraco.cms.businesslogic.web /// The type of which documents should be deleted public static void DeleteFromType(DocumentType dt) { - var objs = getContentOfContentType(dt); - foreach (Content c in objs) + //get all document for the document type and order by level (top level first) + var docs = Document.GetDocumentsOfDocumentType(dt.Id) + .OrderByDescending(x => x.Level); + + foreach (Document doc in docs) { - // due to recursive structure document might already been deleted.. - if (IsNode(c.UniqueId)) + //before we delete this document, we need to make sure we don't end up deleting other documents that + //are not of this document type that are children. So we'll move all of it's children to the trash first. + foreach (Document c in doc.GetDescendants()) { - Document d = new Document(c.UniqueId); - d.DeletePermanently(true); + if (c.ContentType.Id != dt.Id) + { + c.MoveToTrash(); + } + } + + doc.DeletePermanently(); + } + } + + /// + /// Returns all decendants of the current document + /// + /// + public override IEnumerable GetDescendants() + { + var tmp = new List(); + using (IRecordsReader dr = SqlHelper.ExecuteReader( + string.Format(m_SQLOptimizedMany, "umbracoNode.path LIKE '%," + this.Id + ",%'", "umbracoNode.level"))) + { + while (dr.Read()) + { + Document d = new Document(dr.GetInt("id"), true); + d.PopulateDocumentFromReader(dr); + tmp.Add(d); } } + + return tmp.ToArray(); } /// @@ -1317,6 +1372,25 @@ namespace umbraco.cms.businesslogic.web return temp; } + public static IEnumerable GetDocumentsOfDocumentType(int docTypeId) + { + var tmp = new List(); + using (IRecordsReader dr = + SqlHelper.ExecuteReader( + string.Format(m_SQLOptimizedMany, "cmsContent.contentType = @contentTypeId", "umbracoNode.sortOrder"), + SqlHelper.CreateParameter("@contentTypeId", docTypeId))) + { + while (dr.Read()) + { + Document d = new Document(dr.GetInt("id"), true); + d.PopulateDocumentFromReader(dr); + tmp.Add(d); + } + } + + return tmp.ToArray(); + } + /// /// Performance tuned method for use in the tree /// @@ -1324,49 +1398,21 @@ namespace umbraco.cms.businesslogic.web /// public static Document[] GetChildrenForTree(int NodeId) { - ArrayList tmp = new ArrayList(); + var tmp = new List(); using (IRecordsReader dr = SqlHelper.ExecuteReader( - string.Format(m_SQLOptimizedChildren, "umbracoNode.parentID = @parentId", "umbracoNode.sortOrder"), + string.Format(m_SQLOptimizedMany, "umbracoNode.parentID = @parentId", "umbracoNode.sortOrder"), SqlHelper.CreateParameter("@parentId", NodeId))) { while (dr.Read()) { Document d = new Document(dr.GetInt("id"), true); - bool _hc = false; - if (dr.GetInt("children") > 0) - _hc = true; - int? masterContentType = null; - if (!dr.IsNull("masterContentType")) - masterContentType = dr.GetInt("masterContentType"); - d.SetupDocumentForTree(dr.GetGuid("uniqueId") - , dr.GetShort("level") - , dr.GetInt("parentId") - , dr.GetInt("documentUser") - , (dr.GetInt("published") == 1) - , dr.GetString("path") - , dr.GetString("text") - , dr.GetDateTime("createDate") - , dr.GetDateTime("updateDate") - , dr.GetDateTime("versionDate") - , dr.GetString("icon") - , _hc - , dr.GetString("alias") - , dr.GetString("thumbnail") - , dr.GetString("description") - , masterContentType - , dr.GetInt("contentTypeId") - , dr.GetInt("templateId")); + d.PopulateDocumentFromReader(dr); tmp.Add(d); } } - Document[] retval = new Document[tmp.Count]; - - for (int i = 0; i < tmp.Count; i++) - retval[i] = (Document)tmp[i]; - - return retval; + return tmp.ToArray(); } private void SetupDocumentForTree(Guid uniqueId, int level, int parentId, int user, bool publish, string path, diff --git a/umbraco/presentation/umbraco/webservices/legacyAjaxCalls.asmx.cs b/umbraco/presentation/umbraco/webservices/legacyAjaxCalls.asmx.cs index 785090f2e4..bce62c9926 100644 --- a/umbraco/presentation/umbraco/webservices/legacyAjaxCalls.asmx.cs +++ b/umbraco/presentation/umbraco/webservices/legacyAjaxCalls.asmx.cs @@ -17,6 +17,8 @@ using System.Diagnostics; using System.Net; using System.Web.UI; using umbraco.IO; +using umbraco.cms.businesslogic.web; +using umbraco.cms.businesslogic.media; namespace umbraco.presentation.webservices @@ -32,7 +34,7 @@ namespace umbraco.presentation.webservices { /// - /// Overloaded method to accept a string value for the node id. Used for tree's such as python + /// method to accept a string value for the node id. Used for tree's such as python /// and xslt since the file names are the node IDs /// /// @@ -52,6 +54,37 @@ namespace umbraco.presentation.webservices else presentation.create.dialogHandler_temp.Delete(nodeType, 0, nodeId); } + + /// + /// Permanently deletes a document/media object. + /// Used to remove an item from the recycle bin. + /// + /// + [WebMethod] + [ScriptMethod] + public void DeleteContentPermanently(string nodeId, string nodeType) + { + Authorize(); + + int intNodeID; + if (int.TryParse(nodeId, out intNodeID)) + { + switch (nodeType) + { + case "media": + new Media(intNodeID).delete(); + break; + case "content": + new Document(intNodeID).delete(true); + break; + } + new Document(intNodeID).delete(true); + } + else + { + throw new ArgumentException("The nodeId argument could not be parsed to an integer"); + } + } [WebMethod] [ScriptMethod] diff --git a/umbraco/presentation/umbraco_client/Application/UmbracoApplicationActions.js b/umbraco/presentation/umbraco_client/Application/UmbracoApplicationActions.js index f7d1ae9044..b8293c2c52 100644 --- a/umbraco/presentation/umbraco_client/Application/UmbracoApplicationActions.js +++ b/umbraco/presentation/umbraco_client/Application/UmbracoApplicationActions.js @@ -317,21 +317,41 @@ Umbraco.Application.Actions = function() { actionDelete: function() { /// + var actionNode = UmbClientMgr.mainTree().getActionNode(); if (UmbClientMgr.mainTree().getActionNode().nodeType == "content" && UmbClientMgr.mainTree().getActionNode().nodeId == '-1') return; this._debug("actionDelete"); + if (confirm(uiKeys['defaultdialogs_confirmdelete'] + ' "' + UmbClientMgr.mainTree().getActionNode().nodeName + '"?\n\n')) { //raise nodeDeleting event jQuery(window.top).trigger("nodeDeleting", []); var _this = this; - umbraco.presentation.webservices.legacyAjaxCalls.Delete(UmbClientMgr.mainTree().getActionNode().nodeId, "", UmbClientMgr.mainTree().getActionNode().nodeType, function() { - _this._debug("actionDelete: Raising event"); - //raise nodeDeleted event - jQuery(window.top).trigger("nodeDeleted", []); - }); + + //check if it's in the recycle bin + if (actionNode.jsNode.closest("li[id='-20']").length == 1 || actionNode.jsNode.closest("li[id='-21']").length == 1) { + umbraco.presentation.webservices.legacyAjaxCalls.DeleteContentPermanently( + UmbClientMgr.mainTree().getActionNode().nodeId, + UmbClientMgr.mainTree().getActionNode().nodeType, + function() { + _this._debug("actionDelete: Raising event"); + //raise nodeDeleted event + jQuery(window.top).trigger("nodeDeleted", []); + }); + } + else { + umbraco.presentation.webservices.legacyAjaxCalls.Delete( + UmbClientMgr.mainTree().getActionNode().nodeId, "", + UmbClientMgr.mainTree().getActionNode().nodeType, + function() { + _this._debug("actionDelete: Raising event"); + //raise nodeDeleted event + jQuery(window.top).trigger("nodeDeleted", []); + }); + } } + }, actionDisable: function() {