From 6b205bd725279b86a0309edd93fe9896106bd434 Mon Sep 17 00:00:00 2001 From: Morten Christensen Date: Fri, 10 Jan 2014 11:45:41 +0100 Subject: [PATCH 01/11] Updates from PR#127 from Lars-Erik. Had to manually copy in the changes because the PR was closed. Adds Import of Dictionary Items to the PackagingService along with tests and a fix for the DictionaryRepository. --- .../Repositories/DictionaryRepository.cs | 2 +- src/Umbraco.Core/Services/PackagingService.cs | 68 ++++++++++ .../Repositories/DictionaryRepositoryTest.cs | 30 ++++- .../Services/Importing/Dictionary-Package.xml | 32 +++++ .../Importing/ImportResources.Designer.cs | 28 +++- .../Services/Importing/ImportResources.resx | 3 + .../Services/Importing/PackageImportTests.cs | 126 +++++++++++++++++- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + .../businesslogic/Packager/Installer.cs | 14 +- 9 files changed, 290 insertions(+), 14 deletions(-) create mode 100644 src/Umbraco.Tests/Services/Importing/Dictionary-Package.xml diff --git a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs index 0b456cda91..eee020eee7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs @@ -180,7 +180,7 @@ namespace Umbraco.Core.Persistence.Repositories } else { - translation.Id = Convert.ToInt32(Database.Insert(dto)); + translation.Id = Convert.ToInt32(Database.Insert(textDto)); translation.Key = entity.Key; } } diff --git a/src/Umbraco.Core/Services/PackagingService.cs b/src/Umbraco.Core/Services/PackagingService.cs index 150bf23da3..1836a52ab6 100644 --- a/src/Umbraco.Core/Services/PackagingService.cs +++ b/src/Umbraco.Core/Services/PackagingService.cs @@ -837,6 +837,74 @@ namespace Umbraco.Core.Services #endregion #region Dictionary Items + public IEnumerable ImportDictionaryItems(XElement dictionaryItemElementList) + { + var languages = _localizationService.GetAllLanguages().ToList(); + return ImportDictionaryItems(dictionaryItemElementList, languages); + } + + private IEnumerable ImportDictionaryItems(XElement dictionaryItemElementList, List languages) + { + var items = new List(); + foreach (var dictionaryItemElement in dictionaryItemElementList.Elements("DictionaryItem")) + items.AddRange(ImportDictionaryItem(dictionaryItemElement, languages)); + return items; + } + + private IEnumerable ImportDictionaryItem(XElement dictionaryItemElement, List languages) + { + var items = new List(); + + IDictionaryItem dictionaryItem; + var key = dictionaryItemElement.Attribute("Key").Value; + if (_localizationService.DictionaryItemExists(key)) + dictionaryItem = GetAndUpdateDictionaryItem(key, dictionaryItemElement, languages); + else + dictionaryItem = CreateNewDictionaryItem(key, dictionaryItemElement, languages); + _localizationService.Save(dictionaryItem); + items.Add(dictionaryItem); + items.AddRange(ImportDictionaryItems(dictionaryItemElement, languages)); + return items; + } + + private IDictionaryItem GetAndUpdateDictionaryItem(string key, XElement dictionaryItemElement, List languages) + { + var dictionaryItem = _localizationService.GetDictionaryItemByKey(key); + var translations = dictionaryItem.Translations.ToList(); + foreach (var valueElement in dictionaryItemElement.Elements("Value").Where(v => DictionaryValueIsNew(translations, v))) + AddDictionaryTranslation(translations, valueElement, languages); + dictionaryItem.Translations = translations; + return dictionaryItem; + } + + private static DictionaryItem CreateNewDictionaryItem(string key, XElement dictionaryItemElement, List languages) + { + var dictionaryItem = new DictionaryItem(key); + var translations = new List(); + + foreach (var valueElement in dictionaryItemElement.Elements("Value")) + AddDictionaryTranslation(translations, valueElement, languages); + + dictionaryItem.Translations = translations; + return dictionaryItem; + } + + private static bool DictionaryValueIsNew(IEnumerable translations, XElement valueElement) + { + return translations.All(t => + String.Compare(t.Language.IsoCode, valueElement.Attribute("LanguageCultureAlias").Value, StringComparison.InvariantCultureIgnoreCase) != 0 + ); + } + + private static void AddDictionaryTranslation(ICollection translations, XElement valueElement, IEnumerable languages) + { + var languageId = valueElement.Attribute("LanguageCultureAlias").Value; + var language = languages.SingleOrDefault(l => l.IsoCode == languageId); + if (language == null) + return; + var translation = new DictionaryTranslation(language, valueElement.Value); + translations.Add(translation); + } #endregion #region Files diff --git a/src/Umbraco.Tests/Persistence/Repositories/DictionaryRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DictionaryRepositoryTest.cs index 554c0f7ec4..e8e25068db 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DictionaryRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DictionaryRepositoryTest.cs @@ -180,7 +180,6 @@ namespace Umbraco.Tests.Persistence.Repositories } } - [NUnit.Framework.Ignore] [Test] public void Can_Perform_Update_On_DictionaryRepository() { @@ -209,6 +208,35 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Perform_Update_WithNewTranslation_On_DictionaryRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(); + var unitOfWork = provider.GetUnitOfWork(); + var languageRepository = new LanguageRepository(unitOfWork); + var repository = new DictionaryRepository(unitOfWork, languageRepository); + + var languageNo = new Language("nb-NO") { CultureName = "nb-NO" }; + ServiceContext.LocalizationService.Save(languageNo); + + // Act + var item = repository.Get(1); + var translations = item.Translations.ToList(); + translations.Add(new DictionaryTranslation(languageNo, "Les mer")); + item.Translations = translations; + + repository.AddOrUpdate(item); + unitOfWork.Commit(); + + var dictionaryItem = repository.Get(1); + + // Assert + Assert.That(dictionaryItem, Is.Not.Null); + Assert.That(dictionaryItem.Translations.Count(), Is.EqualTo(3)); + Assert.That(dictionaryItem.Translations.Single(t => t.Language.IsoCode == "nb-NO").Value, Is.EqualTo("Les mer")); + } + [Test] public void Can_Perform_Delete_On_DictionaryRepository() { diff --git a/src/Umbraco.Tests/Services/Importing/Dictionary-Package.xml b/src/Umbraco.Tests/Services/Importing/Dictionary-Package.xml new file mode 100644 index 0000000000..7c5cf87856 --- /dev/null +++ b/src/Umbraco.Tests/Services/Importing/Dictionary-Package.xml @@ -0,0 +1,32 @@ + + + + + + Dictionary-Package + 1.0 + MIT license + http://not.available + + 3 + 0 + 0 + + + + Test + http://not.available + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Umbraco.Tests/Services/Importing/ImportResources.Designer.cs b/src/Umbraco.Tests/Services/Importing/ImportResources.Designer.cs index 9b0017ff5e..693a6a576b 100644 --- a/src/Umbraco.Tests/Services/Importing/ImportResources.Designer.cs +++ b/src/Umbraco.Tests/Services/Importing/ImportResources.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.18213 +// Runtime Version:4.0.30319.18051 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -88,6 +88,32 @@ namespace Umbraco.Tests.Services.Importing { } } + /// + /// Looks up a localized string similar to <?xml version="1.0" encoding="utf-8" ?> + ///<umbPackage> + /// <files /> + /// <info> + /// <package> + /// <name>Dictionary-Package</name> + /// <version>1.0</version> + /// <license url="http://www.opensource.org/licenses/mit-license.php">MIT license</license> + /// <url>http://not.available</url> + /// <requirements> + /// <major>3</major> + /// <minor>0</minor> + /// <patch>0</patch> + /// </requirements> + /// </package> + /// <author> + /// <name>Test</name> + /// <website>http://not.available</w [rest of string was truncated]";. + /// + internal static string Dictionary_Package { + get { + return ResourceManager.GetString("Dictionary_Package", resourceCulture); + } + } + /// /// Looks up a localized string similar to <?xml version="1.0" encoding="UTF-8" standalone="no"?> ///<umbPackage> diff --git a/src/Umbraco.Tests/Services/Importing/ImportResources.resx b/src/Umbraco.Tests/Services/Importing/ImportResources.resx index 26f4589221..aebb452083 100644 --- a/src/Umbraco.Tests/Services/Importing/ImportResources.resx +++ b/src/Umbraco.Tests/Services/Importing/ImportResources.resx @@ -142,4 +142,7 @@ checkboxlist-content-package.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + dictionary-package.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + \ No newline at end of file diff --git a/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs b/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs index b4301fe835..1e13712458 100644 --- a/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs +++ b/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs @@ -2,8 +2,8 @@ using System.Linq; using System.Xml.Linq; using NUnit.Framework; +using Umbraco.Core.Models; using Umbraco.Core.Models.Rdbms; -using umbraco.editorControls.MultiNodeTreePicker; namespace Umbraco.Tests.Services.Importing { @@ -375,5 +375,129 @@ namespace Umbraco.Tests.Services.Importing Assert.That(allTemplates.Count(), Is.EqualTo(numberOfTemplates)); Assert.That(allTemplates.First(x => x.Alias == "umbHomepage").Content, Contains.Substring("THIS HAS BEEN UPDATED!")); } + + [Test] + public void PackagingService_Can_Import_DictionaryItems() + { + // Arrange + const string expectedEnglishParentValue = "ParentValue"; + const string expectedNorwegianParentValue = "ForelderVerdi"; + const string expectedEnglishChildValue = "ChildValue"; + const string expectedNorwegianChildValue = "BarnVerdi"; + + var newPackageXml = XElement.Parse(ImportResources.Dictionary_Package); + var dictionaryItemsElement = newPackageXml.Elements("DictionaryItems").First(); + + AddLanguages(); + + // Act + ServiceContext.PackagingService.ImportDictionaryItems(dictionaryItemsElement); + + // Assert + AssertDictionaryItem("Parent", expectedEnglishParentValue, "en-GB"); + AssertDictionaryItem("Parent", expectedNorwegianParentValue, "nb-NO"); + AssertDictionaryItem("Child", expectedEnglishChildValue, "en-GB"); + AssertDictionaryItem("Child", expectedNorwegianChildValue, "nb-NO"); + } + + [Test] + public void PackagingService_WhenExistingDictionaryKey_ImportsNewChildren() + { + // Arrange + const string expectedEnglishParentValue = "ExistingParentValue"; + const string expectedNorwegianParentValue = "EksisterendeForelderVerdi"; + const string expectedEnglishChildValue = "ChildValue"; + const string expectedNorwegianChildValue = "BarnVerdi"; + + var newPackageXml = XElement.Parse(ImportResources.Dictionary_Package); + var dictionaryItemsElement = newPackageXml.Elements("DictionaryItems").First(); + + AddLanguages(); + AddExistingEnglishAndNorwegianParentDictionaryItem(expectedEnglishParentValue, expectedNorwegianParentValue); + + // Act + ServiceContext.PackagingService.ImportDictionaryItems(dictionaryItemsElement); + + // Assert + AssertDictionaryItem("Parent", expectedEnglishParentValue, "en-GB"); + AssertDictionaryItem("Parent", expectedNorwegianParentValue, "nb-NO"); + AssertDictionaryItem("Child", expectedEnglishChildValue, "en-GB"); + AssertDictionaryItem("Child", expectedNorwegianChildValue, "nb-NO"); + } + + [Test] + public void PackagingService_WhenExistingDictionaryKey_OnlyAddsNewLanguages() + { + // Arrange + const string expectedEnglishParentValue = "ExistingParentValue"; + const string expectedNorwegianParentValue = "ForelderVerdi"; + const string expectedEnglishChildValue = "ChildValue"; + const string expectedNorwegianChildValue = "BarnVerdi"; + + var newPackageXml = XElement.Parse(ImportResources.Dictionary_Package); + var dictionaryItemsElement = newPackageXml.Elements("DictionaryItems").First(); + + AddLanguages(); + AddExistingEnglishParentDictionaryItem(expectedEnglishParentValue); + + // Act + ServiceContext.PackagingService.ImportDictionaryItems(dictionaryItemsElement); + + // Assert + AssertDictionaryItem("Parent", expectedEnglishParentValue, "en-GB"); + AssertDictionaryItem("Parent", expectedNorwegianParentValue, "nb-NO"); + AssertDictionaryItem("Child", expectedEnglishChildValue, "en-GB"); + AssertDictionaryItem("Child", expectedNorwegianChildValue, "nb-NO"); + } + + private void AddLanguages() + { + var norwegian = new Core.Models.Language("nb-NO"); + var english = new Core.Models.Language("en-GB"); + ServiceContext.LocalizationService.Save(norwegian, 0); + ServiceContext.LocalizationService.Save(english, 0); + } + + private void AssertDictionaryItem(string key, string expectedValue, string cultureCode) + { + Assert.That(ServiceContext.LocalizationService.DictionaryItemExists(key), "DictionaryItem key does not exist"); + var dictionaryItem = ServiceContext.LocalizationService.GetDictionaryItemByKey(key); + var translation = dictionaryItem.Translations.SingleOrDefault(i => i.Language.IsoCode == cultureCode); + Assert.IsNotNull(translation, "Translation to {0} was not added", cultureCode); + var value = translation.Value; + Assert.That(value, Is.EqualTo(expectedValue), "Translation value was not set"); + } + + private void AddExistingEnglishParentDictionaryItem(string expectedEnglishParentValue) + { + var languages = ServiceContext.LocalizationService.GetAllLanguages().ToList(); + var englishLanguage = languages.Single(l => l.IsoCode == "en-GB"); + ServiceContext.LocalizationService.Save( + new DictionaryItem("Parent") + { + Translations = new List + { + new DictionaryTranslation(englishLanguage, expectedEnglishParentValue), + } + } + ); + } + + private void AddExistingEnglishAndNorwegianParentDictionaryItem(string expectedEnglishParentValue, string expectedNorwegianParentValue) + { + var languages = ServiceContext.LocalizationService.GetAllLanguages().ToList(); + var englishLanguage = languages.Single(l => l.IsoCode == "en-GB"); + var norwegianLanguage = languages.Single(l => l.IsoCode == "nb-NO"); + ServiceContext.LocalizationService.Save( + new DictionaryItem("Parent") + { + Translations = new List + { + new DictionaryTranslation(englishLanguage, expectedEnglishParentValue), + new DictionaryTranslation(norwegianLanguage, expectedNorwegianParentValue), + } + } + ); + } } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 7493eac540..db4fb63e84 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -553,6 +553,7 @@ Designer + diff --git a/src/umbraco.cms/businesslogic/Packager/Installer.cs b/src/umbraco.cms/businesslogic/Packager/Installer.cs index dc73fd9f10..31bebe9bb8 100644 --- a/src/umbraco.cms/businesslogic/Packager/Installer.cs +++ b/src/umbraco.cms/businesslogic/Packager/Installer.cs @@ -333,18 +333,12 @@ namespace umbraco.cms.businesslogic.packager #endregion #region Dictionary items - foreach (XmlNode n in _packageConfig.DocumentElement.SelectNodes("./DictionaryItems/DictionaryItem")) + var dictionaryItemsElement = rootElement.Descendants("DictionaryItems").FirstOrDefault(); + if (dictionaryItemsElement != null) { - Dictionary.DictionaryItem newDi = Dictionary.DictionaryItem.Import(n); - - if (newDi != null) - { - insPack.Data.DictionaryItems.Add(newDi.id.ToString()); - //saveNeeded = true; - } + var insertedDictionaryItems = packagingService.ImportDictionaryItems(dictionaryItemsElement); + insPack.Data.DictionaryItems.AddRange(insertedDictionaryItems.Select(d => d.Id.ToString())); } - - //if (saveNeeded) { insPack.Save(); saveNeeded = false; } #endregion #region Macros From 6e344f335e7dcb5229f3e7be1b9e743acfdc7dd2 Mon Sep 17 00:00:00 2001 From: Morten Christensen Date: Fri, 10 Jan 2014 13:08:33 +0100 Subject: [PATCH 02/11] Adding Export of DictionaryItems including test --- src/Umbraco.Core/Services/PackagingService.cs | 36 ++++++++ .../Services/PackagingServiceTests.cs | 87 +++++++++++++------ 2 files changed, 95 insertions(+), 28 deletions(-) diff --git a/src/Umbraco.Core/Services/PackagingService.cs b/src/Umbraco.Core/Services/PackagingService.cs index 1836a52ab6..faf8a967a3 100644 --- a/src/Umbraco.Core/Services/PackagingService.cs +++ b/src/Umbraco.Core/Services/PackagingService.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using System.Threading; +using System.Xml; using System.Xml.Linq; using Umbraco.Core.Configuration; using Umbraco.Core.IO; @@ -837,6 +838,40 @@ namespace Umbraco.Core.Services #endregion #region Dictionary Items + + public XElement Export(IEnumerable dictionaryItem, bool includeChildren = true) + { + var xml = new XElement("DictionaryItems"); + foreach (var item in dictionaryItem) + { + xml.Add(Export(item, includeChildren)); + } + return xml; + } + + private XElement Export(IDictionaryItem dictionaryItem, bool includeChildren) + { + var xml = new XElement("DictionaryItem", new XAttribute("Key", dictionaryItem.ItemKey)); + foreach (var translation in dictionaryItem.Translations) + { + xml.Add(new XElement("Value", + new XAttribute("LanguageId", translation.Language.Id), + new XAttribute("LanguageCultureAlias", translation.Language.IsoCode), + new XCData(translation.Value))); + } + + if (includeChildren) + { + var children = _localizationService.GetDictionaryItemChildren(dictionaryItem.Key); + foreach (var child in children) + { + xml.Add(Export(child, true)); + } + } + + return xml; + } + public IEnumerable ImportDictionaryItems(XElement dictionaryItemElementList) { var languages = _localizationService.GetAllLanguages().ToList(); @@ -905,6 +940,7 @@ namespace Umbraco.Core.Services var translation = new DictionaryTranslation(language, valueElement.Value); translations.Add(translation); } + #endregion #region Files diff --git a/src/Umbraco.Tests/Services/PackagingServiceTests.cs b/src/Umbraco.Tests/Services/PackagingServiceTests.cs index abdfb2f9f3..2abf808d04 100644 --- a/src/Umbraco.Tests/Services/PackagingServiceTests.cs +++ b/src/Umbraco.Tests/Services/PackagingServiceTests.cs @@ -1,41 +1,72 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Tests.Services.Importing; using Umbraco.Tests.TestHelpers.Entities; namespace Umbraco.Tests.Services { - //[TestFixture] - //public class PackagingServiceTests : BaseServiceTest - //{ - // [Test] - // public void Export_Content() - // { - // var yesNo = DataTypesResolver.Current.GetById(new Guid(Constants.PropertyEditors.TrueFalse)); - // var txtField = DataTypesResolver.Current.GetById(new Guid(Constants.PropertyEditors.Textbox)); + [TestFixture] + public class PackagingServiceTests : BaseServiceTest + { + [SetUp] + public override void Initialize() + { + base.Initialize(); + } - // var contentWithDataType = MockedContentTypes.CreateSimpleContentType( - // "test", - // "Test", - // new PropertyTypeCollection( - // new PropertyType[] - // { - // new PropertyType(new DataTypeDefinition(-1, txtField.Id) - // { - // Name = "Testing Textfield", DatabaseType = DataTypeDatabaseType.Ntext - // }), - // new PropertyType(new DataTypeDefinition(-1, yesNo.Id) - // { - // Name = "Testing intfield", DatabaseType = DataTypeDatabaseType.Integer - // }) - // })); + [TearDown] + public override void TearDown() + { + base.TearDown(); + } - // var content = MockedContent.CreateSimpleContent(contentWithDataType); - // content.Name = "Test"; + [Test] + public void PackagingService_Can_Export_DictionaryItems() + { + // Arrange + CreateTestData(); + var dictionaryItem = ServiceContext.LocalizationService.GetDictionaryItemByKey("Parent"); - // var exported = ServiceContext.PackagingService.Export(content); + var newPackageXml = XElement.Parse(ImportResources.Dictionary_Package); + var dictionaryItemsElement = newPackageXml.Elements("DictionaryItems").First(); - // } - //} + // Act + var xml = ServiceContext.PackagingService.Export(new []{dictionaryItem}); + + // Assert + Assert.That(xml.ToString(), Is.EqualTo(dictionaryItemsElement.ToString())); + } + + public void CreateTestData() + { + var languageNbNo = new Language("nb-NO") { CultureName = "nb-NO" }; + ServiceContext.LocalizationService.Save(languageNbNo); + + var languageEnGb = new Language("en-GB") { CultureName = "en-GB" }; + ServiceContext.LocalizationService.Save(languageEnGb); + + var parentItem = new DictionaryItem("Parent"); + var parentTranslations = new List + { + new DictionaryTranslation(languageNbNo, "ForelderVerdi"), + new DictionaryTranslation(languageEnGb, "ParentValue") + }; + parentItem.Translations = parentTranslations; + ServiceContext.LocalizationService.Save(parentItem); + + var childItem = new DictionaryItem(parentItem.Key, "Child"); + var childTranslations = new List + { + new DictionaryTranslation(languageNbNo, "BarnVerdi"), + new DictionaryTranslation(languageEnGb, "ChildValue") + }; + childItem.Translations = childTranslations; + ServiceContext.LocalizationService.Save(childItem); + } + } } \ No newline at end of file From 880636c5a6f1c3bb16e9c8905295eb760dbf2cd2 Mon Sep 17 00:00:00 2001 From: Morten Christensen Date: Fri, 10 Jan 2014 14:47:31 +0100 Subject: [PATCH 03/11] Adding languages xml to existing xml file for testing purposes --- src/Umbraco.Core/Models/DictionaryItem.cs | 1 - src/Umbraco.Core/Models/Language.cs | 1 - src/Umbraco.Tests/Services/Importing/Dictionary-Package.xml | 4 ++++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Models/DictionaryItem.cs b/src/Umbraco.Core/Models/DictionaryItem.cs index db3f47ba13..c670a75108 100644 --- a/src/Umbraco.Core/Models/DictionaryItem.cs +++ b/src/Umbraco.Core/Models/DictionaryItem.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Reflection; using System.Runtime.Serialization; using Umbraco.Core.Models.EntityBase; -using Umbraco.Core.Persistence.Mappers; namespace Umbraco.Core.Models { diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs index 6986fae027..ef8ebbf931 100644 --- a/src/Umbraco.Core/Models/Language.cs +++ b/src/Umbraco.Core/Models/Language.cs @@ -3,7 +3,6 @@ using System.Globalization; using System.Reflection; using System.Runtime.Serialization; using Umbraco.Core.Models.EntityBase; -using Umbraco.Core.Persistence.Mappers; namespace Umbraco.Core.Models { diff --git a/src/Umbraco.Tests/Services/Importing/Dictionary-Package.xml b/src/Umbraco.Tests/Services/Importing/Dictionary-Package.xml index 7c5cf87856..6fa5a3d15f 100644 --- a/src/Umbraco.Tests/Services/Importing/Dictionary-Package.xml +++ b/src/Umbraco.Tests/Services/Importing/Dictionary-Package.xml @@ -29,4 +29,8 @@ + + + + \ No newline at end of file From 33b99073f016bf8e0fb57c64882afa3c4c185d4e Mon Sep 17 00:00:00 2001 From: Morten Christensen Date: Fri, 10 Jan 2014 14:48:31 +0100 Subject: [PATCH 04/11] Moving events out of the repository scope. Adding Get Language by Iso Code method --- .../Services/ILocalizationService.cs | 11 +++++- .../Services/LocalizationService.cs | 38 +++++++++++++------ 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Core/Services/ILocalizationService.cs b/src/Umbraco.Core/Services/ILocalizationService.cs index 82a9319884..5c3bc965ec 100644 --- a/src/Umbraco.Core/Services/ILocalizationService.cs +++ b/src/Umbraco.Core/Services/ILocalizationService.cs @@ -81,9 +81,16 @@ namespace Umbraco.Core.Services /// /// Gets a by its culture code /// - /// Culture Code + /// Culture Code - also refered to as the Friendly name /// - ILanguage GetLanguageByCultureCode(string culture); + ILanguage GetLanguageByCultureCode(string cultureName); + + /// + /// Gets a by its iso code + /// + /// Iso Code of the language (ie. en-US) + /// + ILanguage GetLanguageByIsoCode(string isoCode); /// /// Gets all available languages diff --git a/src/Umbraco.Core/Services/LocalizationService.cs b/src/Umbraco.Core/Services/LocalizationService.cs index 47284eb1ad..b7c0cb3b55 100644 --- a/src/Umbraco.Core/Services/LocalizationService.cs +++ b/src/Umbraco.Core/Services/LocalizationService.cs @@ -146,10 +146,10 @@ namespace Umbraco.Core.Services { repository.AddOrUpdate(dictionaryItem); uow.Commit(); - - SavedDictionaryItem.RaiseEvent(new SaveEventArgs(dictionaryItem, false), this); } + SavedDictionaryItem.RaiseEvent(new SaveEventArgs(dictionaryItem, false), this); + Audit.Add(AuditTypes.Save, "Save DictionaryItem performed by user", userId, dictionaryItem.Id); } @@ -170,10 +170,10 @@ namespace Umbraco.Core.Services //NOTE: The recursive delete is done in the repository repository.Delete(dictionaryItem); uow.Commit(); - - DeletedDictionaryItem.RaiseEvent(new DeleteEventArgs(dictionaryItem, false), this); } + DeletedDictionaryItem.RaiseEvent(new DeleteEventArgs(dictionaryItem, false), this); + Audit.Add(AuditTypes.Delete, "Delete DictionaryItem performed by user", userId, dictionaryItem.Id); } @@ -193,13 +193,29 @@ namespace Umbraco.Core.Services /// /// Gets a by its culture code /// - /// Culture Code + /// Culture Name - also refered to as the Friendly name /// - public ILanguage GetLanguageByCultureCode(string culture) + public ILanguage GetLanguageByCultureCode(string cultureName) { using (var repository = _repositoryFactory.CreateLanguageRepository(_uowProvider.GetUnitOfWork())) { - var query = Query.Builder.Where(x => x.CultureName == culture); + var query = Query.Builder.Where(x => x.CultureName == cultureName); + var items = repository.GetByQuery(query); + + return items.FirstOrDefault(); + } + } + + /// + /// Gets a by its iso code + /// + /// Iso Code of the language (ie. en-US) + /// + public ILanguage GetLanguageByIsoCode(string isoCode) + { + using (var repository = _repositoryFactory.CreateLanguageRepository(_uowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.IsoCode == isoCode); var items = repository.GetByQuery(query); return items.FirstOrDefault(); @@ -234,10 +250,10 @@ namespace Umbraco.Core.Services { repository.AddOrUpdate(language); uow.Commit(); - - SavedLanguage.RaiseEvent(new SaveEventArgs(language, false), this); } + SavedLanguage.RaiseEvent(new SaveEventArgs(language, false), this); + Audit.Add(AuditTypes.Save, "Save Language performed by user", userId, language.Id); } @@ -257,10 +273,10 @@ namespace Umbraco.Core.Services //NOTE: There isn't any constraints in the db, so possible references aren't deleted repository.Delete(language); uow.Commit(); - - DeletedLanguage.RaiseEvent(new DeleteEventArgs(language, false), this); } + DeletedLanguage.RaiseEvent(new DeleteEventArgs(language, false), this); + Audit.Add(AuditTypes.Delete, "Delete Language performed by user", userId, language.Id); } From f86eb4b74149d926159419435c732c6ebad40f55 Mon Sep 17 00:00:00 2001 From: Morten Christensen Date: Fri, 10 Jan 2014 14:48:55 +0100 Subject: [PATCH 05/11] Adds import and export of languages along with tests --- src/Umbraco.Core/Services/PackagingService.cs | 43 ++++++++++++++++++- .../Services/Importing/PackageImportTests.cs | 19 ++++++++ .../Services/PackagingServiceTests.cs | 29 ++++++++++--- 3 files changed, 84 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Core/Services/PackagingService.cs b/src/Umbraco.Core/Services/PackagingService.cs index faf8a967a3..b68d85501c 100644 --- a/src/Umbraco.Core/Services/PackagingService.cs +++ b/src/Umbraco.Core/Services/PackagingService.cs @@ -849,7 +849,7 @@ namespace Umbraco.Core.Services return xml; } - private XElement Export(IDictionaryItem dictionaryItem, bool includeChildren) + public XElement Export(IDictionaryItem dictionaryItem, bool includeChildren) { var xml = new XElement("DictionaryItem", new XAttribute("Key", dictionaryItem.ItemKey)); foreach (var translation in dictionaryItem.Translations) @@ -947,6 +947,47 @@ namespace Umbraco.Core.Services #endregion #region Languages + + public XElement Export(IEnumerable languages) + { + var xml = new XElement("Languages"); + foreach (var language in languages) + { + xml.Add(Export(language)); + } + return xml; + } + + public XElement Export(ILanguage language) + { + var xml = new XElement("Language", + new XAttribute("Id", language.Id), + new XAttribute("CultureAlias", language.IsoCode), + new XAttribute("FriendlyName", language.CultureName)); + return xml; + } + + public IEnumerable ImportLanguages(XElement languageElementList) + { + var list = new List(); + foreach (var languageElement in languageElementList.Elements("Language")) + { + var isoCode = languageElement.Attribute("CultureAlias").Value; + var existingLanguage = _localizationService.GetLanguageByIsoCode(isoCode); + if (existingLanguage == null) + { + var langauge = new Language(isoCode) + { + CultureName = languageElement.Attribute("FriendlyName").Value + }; + _localizationService.Save(langauge); + list.Add(langauge); + } + } + + return list; + } + #endregion #region Macros diff --git a/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs b/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs index 1e13712458..5a412966f3 100644 --- a/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs +++ b/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs @@ -450,6 +450,25 @@ namespace Umbraco.Tests.Services.Importing AssertDictionaryItem("Child", expectedNorwegianChildValue, "nb-NO"); } + [Test] + public void PackagingService_Can_Import_Languages() + { + // Arrange + var newPackageXml = XElement.Parse(ImportResources.Dictionary_Package); + var LanguageItemsElement = newPackageXml.Elements("Languages").First(); + + // Act + var languages = ServiceContext.PackagingService.ImportLanguages(LanguageItemsElement); + var allLanguages = ServiceContext.LocalizationService.GetAllLanguages(); + + // Assert + Assert.That(languages.Any(x => x.HasIdentity == false), Is.False); + foreach (var language in languages) + { + Assert.That(allLanguages.Any(x => x.IsoCode == language.IsoCode), Is.True); + } + } + private void AddLanguages() { var norwegian = new Core.Models.Language("nb-NO"); diff --git a/src/Umbraco.Tests/Services/PackagingServiceTests.cs b/src/Umbraco.Tests/Services/PackagingServiceTests.cs index 2abf808d04..22c41ee669 100644 --- a/src/Umbraco.Tests/Services/PackagingServiceTests.cs +++ b/src/Umbraco.Tests/Services/PackagingServiceTests.cs @@ -1,12 +1,9 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using NUnit.Framework; -using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Tests.Services.Importing; -using Umbraco.Tests.TestHelpers.Entities; namespace Umbraco.Tests.Services { @@ -29,7 +26,7 @@ namespace Umbraco.Tests.Services public void PackagingService_Can_Export_DictionaryItems() { // Arrange - CreateTestData(); + CreateDictionaryData(); var dictionaryItem = ServiceContext.LocalizationService.GetDictionaryItemByKey("Parent"); var newPackageXml = XElement.Parse(ImportResources.Dictionary_Package); @@ -42,7 +39,27 @@ namespace Umbraco.Tests.Services Assert.That(xml.ToString(), Is.EqualTo(dictionaryItemsElement.ToString())); } - public void CreateTestData() + [Test] + public void PackagingService_Can_Export_Languages() + { + // Arrange + var languageNbNo = new Language("nb-NO") { CultureName = "Norwegian" }; + ServiceContext.LocalizationService.Save(languageNbNo); + + var languageEnGb = new Language("en-GB") { CultureName = "English (United Kingdom)" }; + ServiceContext.LocalizationService.Save(languageEnGb); + + var newPackageXml = XElement.Parse(ImportResources.Dictionary_Package); + var languageItemsElement = newPackageXml.Elements("Languages").First(); + + // Act + var xml = ServiceContext.PackagingService.Export(new[] { languageNbNo, languageEnGb }); + + // Assert + Assert.That(xml.ToString(), Is.EqualTo(languageItemsElement.ToString())); + } + + private void CreateDictionaryData() { var languageNbNo = new Language("nb-NO") { CultureName = "nb-NO" }; ServiceContext.LocalizationService.Save(languageNbNo); From 2467b5ee3bf228a51ffa13b3ddf4d2455f95e13d Mon Sep 17 00:00:00 2001 From: Morten Christensen Date: Fri, 10 Jan 2014 14:53:27 +0100 Subject: [PATCH 06/11] Removing some internal experimental stuff that isnt used --- src/Umbraco.Core/Services/PackagingService.cs | 44 +------------------ 1 file changed, 1 insertion(+), 43 deletions(-) diff --git a/src/Umbraco.Core/Services/PackagingService.cs b/src/Umbraco.Core/Services/PackagingService.cs index b68d85501c..d770642418 100644 --- a/src/Umbraco.Core/Services/PackagingService.cs +++ b/src/Umbraco.Core/Services/PackagingService.cs @@ -56,49 +56,7 @@ namespace Umbraco.Core.Services _importedContentTypes = new Dictionary(); } - - #region Generic export methods - - internal void ExportToFile(string absoluteFilePath, string nodeType, int id) - { - XElement xml = null; - - if (nodeType.Equals("content", StringComparison.InvariantCultureIgnoreCase)) - { - var content = _contentService.GetById(id); - xml = Export(content); - } - - if (nodeType.Equals("media", StringComparison.InvariantCultureIgnoreCase)) - { - var media = _mediaService.GetById(id); - xml = Export(media); - } - - if (nodeType.Equals("contenttype", StringComparison.InvariantCultureIgnoreCase)) - { - var contentType = _contentTypeService.GetContentType(id); - xml = Export(contentType); - } - - if (nodeType.Equals("mediatype", StringComparison.InvariantCultureIgnoreCase)) - { - var mediaType = _contentTypeService.GetMediaType(id); - xml = Export(mediaType); - } - - if (nodeType.Equals("datatype", StringComparison.InvariantCultureIgnoreCase)) - { - var dataType = _dataTypeService.GetDataTypeDefinitionById(id); - xml = Export(dataType); - } - - if (xml != null) - xml.Save(absoluteFilePath); - } - - #endregion - + #region Content /// From d963f5cd7d59ea3919b01c85cad4b6323441bd52 Mon Sep 17 00:00:00 2001 From: Morten Christensen Date: Fri, 10 Jan 2014 14:57:27 +0100 Subject: [PATCH 07/11] Updating a few methods that are made public in 7.0.2 --- src/Umbraco.Core/Services/PackagingService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Services/PackagingService.cs b/src/Umbraco.Core/Services/PackagingService.cs index d770642418..ca3a31870e 100644 --- a/src/Umbraco.Core/Services/PackagingService.cs +++ b/src/Umbraco.Core/Services/PackagingService.cs @@ -65,7 +65,7 @@ namespace Umbraco.Core.Services /// Content to export /// Optional parameter indicating whether to include descendents /// containing the xml representation of the Content object - internal XElement Export(IContent content, bool deep = false) + public XElement Export(IContent content, bool deep = false) { //nodeName should match Casing.SafeAliasWithForcingCheck(content.ContentType.Alias); var nodeName = UmbracoSettings.UseLegacyXmlSchema ? "node" : content.ContentType.Alias.ToSafeAliasWithForcingCheck(); @@ -676,7 +676,7 @@ namespace Umbraco.Core.Services /// /// /// - internal XElement Export(IEnumerabledataTypeDefinitions) + public XElement Export(IEnumerabledataTypeDefinitions) { var container = new XElement("DataTypes"); foreach (var d in dataTypeDefinitions) @@ -686,7 +686,7 @@ namespace Umbraco.Core.Services return container; } - internal XElement Export(IDataTypeDefinition dataTypeDefinition) + public XElement Export(IDataTypeDefinition dataTypeDefinition) { var prevalues = new XElement("PreValues"); From 955573256dd47925f42f6a6eb84522503876b9ab Mon Sep 17 00:00:00 2001 From: Morten Christensen Date: Fri, 10 Jan 2014 15:11:07 +0100 Subject: [PATCH 08/11] Updating the legacy Installer class to use the new methods in the PackagingService --- src/Umbraco.Core/Services/PackagingService.cs | 3 + .../businesslogic/Packager/Installer.cs | 118 +----------------- 2 files changed, 8 insertions(+), 113 deletions(-) diff --git a/src/Umbraco.Core/Services/PackagingService.cs b/src/Umbraco.Core/Services/PackagingService.cs index ca3a31870e..816865d807 100644 --- a/src/Umbraco.Core/Services/PackagingService.cs +++ b/src/Umbraco.Core/Services/PackagingService.cs @@ -1195,5 +1195,8 @@ namespace Umbraco.Core.Services } #endregion + + #region Stylesheets + #endregion } } \ No newline at end of file diff --git a/src/umbraco.cms/businesslogic/Packager/Installer.cs b/src/umbraco.cms/businesslogic/Packager/Installer.cs index 31bebe9bb8..102a74b2fc 100644 --- a/src/umbraco.cms/businesslogic/Packager/Installer.cs +++ b/src/umbraco.cms/businesslogic/Packager/Installer.cs @@ -300,36 +300,18 @@ namespace umbraco.cms.businesslogic.packager foreach (var dataTypeDefinition in dataTypeDefinitions) { insPack.Data.DataTypes.Add(dataTypeDefinition.Id.ToString(CultureInfo.InvariantCulture)); - //saveNeeded = true; } } - /*foreach (XmlNode n in _packageConfig.DocumentElement.SelectNodes("//DataType")) - { - cms.businesslogic.datatype.DataTypeDefinition newDtd = cms.businesslogic.datatype.DataTypeDefinition.Import(n); - - if (newDtd != null) - { - insPack.Data.DataTypes.Add(newDtd.Id.ToString()); - saveNeeded = true; - } - }*/ - - //if (saveNeeded) { insPack.Save(); saveNeeded = false; } #endregion #region Languages - foreach (XmlNode n in _packageConfig.DocumentElement.SelectNodes("//Language")) + var languageItemsElement = rootElement.Descendants("Languages").FirstOrDefault(); + if (languageItemsElement != null) { - language.Language newLang = language.Language.Import(n); - - if (newLang != null) - { - insPack.Data.Languages.Add(newLang.id.ToString(CultureInfo.InvariantCulture)); - //saveNeeded = true; - } + var insertedLanguages = packagingService.ImportLanguages(languageItemsElement); + insPack.Data.Languages.AddRange(insertedLanguages.Select(l => l.Id.ToString())); } - - //if (saveNeeded) { insPack.Save(); saveNeeded = false; } + #endregion #region Dictionary items @@ -364,56 +346,8 @@ namespace umbraco.cms.businesslogic.packager foreach (var template in templates) { insPack.Data.Templates.Add(template.Id.ToString(CultureInfo.InvariantCulture)); - //saveNeeded = true; } } - - //if (saveNeeded) { insPack.Save(); saveNeeded = false; } - - /*foreach (XmlNode n in _packageConfig.DocumentElement.SelectNodes("Templates/Template")) - { - var t = Template.Import(n, currentUser); - - insPack.Data.Templates.Add(t.Id.ToString()); - - saveNeeded = true; - } - - if (saveNeeded) { insPack.Save(); saveNeeded = false; } - - - //NOTE: SD: I'm pretty sure the only thing the below script does is ensure that the Master template Id is set - // in the database, but this is also duplicating the saving of the design content since the above Template.Import - // already does this. I've left this for now because I'm not sure the reprocussions of removing it but seems there - // is a lot of excess database calls happening here. - foreach (XmlNode n in _packageConfig.DocumentElement.SelectNodes("Templates/Template")) - { - string master = xmlHelper.GetNodeValue(n.SelectSingleNode("Master")); - Template t = Template.GetByAlias(xmlHelper.GetNodeValue(n.SelectSingleNode("Alias"))); - if (master.Trim() != "") - { - var masterTemplate = Template.GetByAlias(master); - if (masterTemplate != null) - { - t.MasterTemplate = Template.GetByAlias(master).Id; - //SD: This appears to always just save an empty template because the design isn't set yet - // this fixes an issue now that we have MVC because if there is an empty template and MVC is - // the default, it will create a View not a master page and then the system will try to route via - // MVC which means that the package will not work anymore. - // The code below that imports the templates should suffice because it's actually importing - // template data not just blank data. - - //if (UmbracoSettings.UseAspNetMasterPages) - // t.SaveMasterPageFile(t.Design); - } - } - // Master templates can only be generated when their master is known - if (UmbracoSettings.UseAspNetMasterPages) - { - t.ImportDesign(xmlHelper.GetNodeValue(n.SelectSingleNode("Design"))); - t.SaveMasterPageFile(t.Design); - } - }*/ #endregion #region DocumentTypes @@ -432,44 +366,6 @@ namespace umbraco.cms.businesslogic.packager //saveNeeded = true; } } - - //if (saveNeeded) { insPack.Save(); saveNeeded = false; } - - /*foreach (XmlNode n in _packageConfig.DocumentElement.SelectNodes("DocumentTypes/DocumentType")) - { - ImportDocumentType(n, currentUser, false); - saveNeeded = true; - } - - if (saveNeeded) { insPack.Save(); saveNeeded = false; } - - - // Add documenttype structure - foreach (XmlNode n in _packageConfig.DocumentElement.SelectNodes("DocumentTypes/DocumentType")) - { - DocumentType dt = DocumentType.GetByAlias(xmlHelper.GetNodeValue(n.SelectSingleNode("Info/Alias"))); - if (dt != null) - { - ArrayList allowed = new ArrayList(); - foreach (XmlNode structure in n.SelectNodes("Structure/DocumentType")) - { - DocumentType dtt = DocumentType.GetByAlias(xmlHelper.GetNodeValue(structure)); - if (dtt != null) - allowed.Add(dtt.Id); - } - - int[] adt = new int[allowed.Count]; - for (int i = 0; i < allowed.Count; i++) - adt[i] = (int)allowed[i]; - dt.AllowedChildContentTypeIDs = adt; - dt.Save(); - //PPH we log the document type install here. - insPack.Data.Documenttypes.Add(dt.Id.ToString()); - saveNeeded = true; - } - } - - if (saveNeeded) { insPack.Save(); saveNeeded = false; }*/ #endregion #region Stylesheets @@ -492,10 +388,6 @@ namespace umbraco.cms.businesslogic.packager var firstContentItem = content.First(); insPack.Data.ContentNodeId = firstContentItem.Id.ToString(CultureInfo.InvariantCulture); } - /*foreach (XmlElement n in _packageConfig.DocumentElement.SelectNodes("Documents/DocumentSet [@importMode = 'root']/*")) - { - insPack.Data.ContentNodeId = cms.businesslogic.web.Document.Import(-1, currentUser, n).ToString(); - }*/ #endregion #region Package Actions From 06defee41045d93b02c6337f9d838991715013ab Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 13 Jan 2014 13:05:25 +1100 Subject: [PATCH 09/11] Completed new NotificationService, moved Diff to Core --- .../Services/INotificationService.cs | 25 +- .../Services/NotificationService.cs | 313 ++++++++++- src/Umbraco.Core/Services/ServiceContext.cs | 12 + src/Umbraco.Core/Strings/Diff.cs | 510 ++++++++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../businesslogic/workflow/Diff.cs | 3 +- .../businesslogic/workflow/Notification.cs | 2 +- 7 files changed, 857 insertions(+), 9 deletions(-) create mode 100644 src/Umbraco.Core/Strings/Diff.cs diff --git a/src/Umbraco.Core/Services/INotificationService.cs b/src/Umbraco.Core/Services/INotificationService.cs index 801424d3b0..4be6d17f0d 100644 --- a/src/Umbraco.Core/Services/INotificationService.cs +++ b/src/Umbraco.Core/Services/INotificationService.cs @@ -1,7 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using System.Web; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; @@ -17,9 +19,15 @@ namespace Umbraco.Core.Services /// Sends the notifications for the specified user regarding the specified node and action. /// /// - /// + /// /// - void SendNotifications(IEntity entity, IUser user, IAction action); + /// + /// + /// + /// + void SendNotifications(IUser operatingUser, IUmbracoEntity entity, string action, string actionName, HttpContextBase http, + Func createSubject, + Func createBody); /// /// Gets the notifications for the user @@ -28,6 +36,17 @@ namespace Umbraco.Core.Services /// IEnumerable GetUserNotifications(IUser user); + /// + /// Gets the notifications for the user based on the specified node path + /// + /// + /// + /// + /// + /// Notifications are inherited from the parent so any child node will also have notifications assigned based on it's parent (ancestors) + /// + IEnumerable GetUserNotifications(IUser user, string path); + /// /// Returns the notifications for an entity /// diff --git a/src/Umbraco.Core/Services/NotificationService.cs b/src/Umbraco.Core/Services/NotificationService.cs index 31c534470b..609b6316d8 100644 --- a/src/Umbraco.Core/Services/NotificationService.cs +++ b/src/Umbraco.Core/Services/NotificationService.cs @@ -1,10 +1,19 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Mail; +using System.Text; +using System.Web; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Strings; using umbraco.interfaces; namespace Umbraco.Core.Services @@ -12,21 +21,59 @@ namespace Umbraco.Core.Services internal class NotificationService : INotificationService { private readonly IDatabaseUnitOfWorkProvider _uowProvider; + private readonly IUserService _userService; + private readonly IContentService _contentService; - public NotificationService(IDatabaseUnitOfWorkProvider provider) + public NotificationService(IDatabaseUnitOfWorkProvider provider, IUserService userService, IContentService contentService) { _uowProvider = provider; + _userService = userService; + _contentService = contentService; } /// /// Sends the notifications for the specified user regarding the specified node and action. /// /// - /// + /// /// - public void SendNotifications(IEntity entity, IUser user, IAction action) + /// + /// + /// + /// + /// + /// Currently this will only work for Content entities! + /// + public void SendNotifications(IUser operatingUser, IUmbracoEntity entity, string action, string actionName, HttpContextBase http, + Func createSubject, + Func createBody) { - throw new NotImplementedException(); + if ((entity is IContent) == false) + { + throw new NotSupportedException(); + } + var content = (IContent) entity; + + int totalUsers; + var allUsers = _userService.GetAllMembers(0, int.MaxValue, out totalUsers); + foreach (var u in allUsers) + { + if (u.IsApproved == false) continue; + var userNotifications = GetUserNotifications(u, content.Path).ToArray(); + var notificationForAction = userNotifications.FirstOrDefault(x => x.Action == action); + if (notificationForAction != null) + { + try + { + SendNotification(operatingUser, u, content, actionName, http, createSubject, createBody); + LogHelper.Debug(string.Format("Notification type: {0} sent to {1} ({2})", action, u.Name, u.Email)); + } + catch (Exception ex) + { + LogHelper.Error("An error occurred sending notification", ex); + } + } + } } /// @@ -41,6 +88,26 @@ namespace Umbraco.Core.Services return repository.GetUserNotifications(user); } + /// + /// Gets the notifications for the user based on the specified node path + /// + /// + /// + /// + /// + /// Notifications are inherited from the parent so any child node will also have notifications assigned based on it's parent (ancestors) + /// + public IEnumerable GetUserNotifications(IUser user, string path) + { + var userNotifications = GetUserNotifications(user).ToArray(); + var pathParts = path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); + var result = pathParts + .Select(part => userNotifications.FirstOrDefault(x => x.EntityId.ToString(CultureInfo.InvariantCulture) == part)) + .Where(notification => notification != null) + .ToList(); + return result; + } + /// /// Deletes notifications by entity /// @@ -99,5 +166,243 @@ namespace Umbraco.Core.Services var repository = new NotificationsRepository(uow); return repository.CreateNotification(user, entity, action); } + + #region private methods + + /// + /// Sends the notification + /// + /// + /// + /// + /// The action readable name - currently an action is just a single letter, this is the name associated with the letter + /// + /// Callback to create the mail subject + /// Callback to create the mail body + private void SendNotification(IUser performingUser, IUser mailingUser, IContent content, string actionName, HttpContextBase http, + Func createSubject, + Func createBody) + { + if (performingUser == null) throw new ArgumentNullException("performingUser"); + if (mailingUser == null) throw new ArgumentNullException("mailingUser"); + if (content == null) throw new ArgumentNullException("content"); + if (http == null) throw new ArgumentNullException("http"); + if (createSubject == null) throw new ArgumentNullException("createSubject"); + if (createBody == null) throw new ArgumentNullException("createBody"); + + // retrieve previous version of the document + var versions = _contentService.GetVersions(content.Id).ToArray(); + + int versionCount = (versions.Length > 1) ? (versions.Length - 2) : (versions.Length - 1); + var oldDoc = _contentService.GetByVersion(versions[versionCount].Version); + //var oldDoc = new Document(documentObject.Id, versions[versionCount].Version); + + // build summary + var summary = new StringBuilder(); + var props = content.Properties.ToArray(); + foreach (var p in props) + { + var newText = p.Value != null ? p.Value.ToString() : ""; + var oldText = newText; + + // check if something was changed and display the changes otherwise display the fields + if (oldDoc.Properties.Contains(p.PropertyType.Alias)) + { + var oldProperty = oldDoc.Properties[p.PropertyType.Alias]; + oldText = oldProperty.Value != null ? oldProperty.Value.ToString() : ""; + + // replace html with char equivalent + ReplaceHtmlSymbols(ref oldText); + ReplaceHtmlSymbols(ref newText); + } + + + // make sure to only highlight changes done using TinyMCE editor... other changes will be displayed using default summary + // TODO: We should probably allow more than just tinymce?? + if ((p.PropertyType.DataTypeId == Guid.Parse(Constants.PropertyEditors.TinyMCEv3) || p.PropertyType.DataTypeId == Guid.Parse(Constants.PropertyEditors.TinyMCE)) + && string.CompareOrdinal(oldText, newText) != 0) + { + summary.Append(""); + summary.Append(" Note: "); + summary.Append( + " Red for deleted characters Yellow for inserted characters"); + summary.Append(""); + summary.Append(""); + summary.Append(" New " + + p.PropertyType.Name + ""); + summary.Append("" + + ReplaceLinks(CompareText(oldText, newText, true, false, "", string.Empty), http.Request) + + ""); + summary.Append(""); + summary.Append(""); + summary.Append(" Old " + + p.PropertyType.Name + ""); + summary.Append("" + + ReplaceLinks(CompareText(newText, oldText, true, false, "", string.Empty), http.Request) + + ""); + summary.Append(""); + } + else + { + summary.Append(""); + summary.Append("" + + p.PropertyType.Name + ""); + summary.Append("" + newText + ""); + summary.Append(""); + } + summary.Append( + " "); + } + + string protocol = GlobalSettings.UseSSL ? "https" : "http"; + + + string[] subjectVars = { + http.Request.ServerVariables["SERVER_NAME"] + ":" + + http.Request.Url.Port + + IOHelper.ResolveUrl(SystemDirectories.Umbraco), + actionName, + content.Name + }; + string[] bodyVars = { + mailingUser.Name, + actionName, + content.Name, + performingUser.Name, + http.Request.ServerVariables["SERVER_NAME"] + ":" + http.Request.Url.Port + IOHelper.ResolveUrl(SystemDirectories.Umbraco), + content.Id.ToString(CultureInfo.InvariantCulture), summary.ToString(), + string.Format("{2}://{0}/{1}", + http.Request.ServerVariables["SERVER_NAME"] + ":" + http.Request.Url.Port, + //TODO: RE-enable this so we can have a nice url + /*umbraco.library.NiceUrl(documentObject.Id))*/ + content.Id + ".aspx", + protocol) + + }; + + // create the mail message + var mail = new MailMessage(UmbracoSettings.NotificationEmailSender, mailingUser.Email); + + // populate the message + mail.Subject = createSubject(mailingUser, subjectVars); + //mail.Subject = ui.Text("notifications", "mailSubject", subjectVars, mailingUser); + if (UmbracoSettings.NotificationDisableHtmlEmail) + { + mail.IsBodyHtml = false; + //mail.Body = ui.Text("notifications", "mailBody", bodyVars, mailingUser); + mail.Body = createBody(mailingUser, bodyVars); + } + else + { + mail.IsBodyHtml = true; + mail.Body = + @" + + +" + createBody(mailingUser, bodyVars); + //ui.Text("notifications", "mailBodyHtml", bodyVars, mailingUser) + ""; + } + + // nh, issue 30724. Due to hardcoded http strings in resource files, we need to check for https replacements here + // adding the server name to make sure we don't replace external links + if (GlobalSettings.UseSSL && string.IsNullOrEmpty(mail.Body) == false) + { + string serverName = http.Request.ServerVariables["SERVER_NAME"]; + mail.Body = mail.Body.Replace( + string.Format("http://{0}", serverName), + string.Format("https://{0}", serverName)); + } + + // send it + var sender = new SmtpClient(); + sender.Send(mail); + } + + private static string ReplaceLinks(string text, HttpRequestBase request) + { + string domain = GlobalSettings.UseSSL ? "https://" : "http://"; + domain += request.ServerVariables["SERVER_NAME"] + ":" + request.Url.Port + "/"; + text = text.Replace("href=\"/", "href=\"" + domain); + text = text.Replace("src=\"/", "src=\"" + domain); + return text; + } + + /// + /// Replaces the HTML symbols with the character equivalent. + /// + /// The old string. + private static void ReplaceHtmlSymbols(ref string oldString) + { + oldString = oldString.Replace(" ", " "); + oldString = oldString.Replace("’", "'"); + oldString = oldString.Replace("&", "&"); + oldString = oldString.Replace("“", "“"); + oldString = oldString.Replace("”", "”"); + oldString = oldString.Replace(""", "\""); + } + + /// + /// Compares the text. + /// + /// The old text. + /// The new text. + /// if set to true [display inserted text]. + /// if set to true [display deleted text]. + /// The inserted style. + /// The deleted style. + /// + private static string CompareText(string oldText, string newText, bool displayInsertedText, + bool displayDeletedText, string insertedStyle, string deletedStyle) + { + var sb = new StringBuilder(); + var diffs = Diff.DiffText1(oldText, newText); + + int pos = 0; + for (int n = 0; n < diffs.Length; n++) + { + Diff.Item it = diffs[n]; + + // write unchanged chars + while ((pos < it.StartB) && (pos < newText.Length)) + { + sb.Append(newText[pos]); + pos++; + } // while + + // write deleted chars + if (displayDeletedText && it.DeletedA > 0) + { + sb.Append(deletedStyle); + for (int m = 0; m < it.DeletedA; m++) + { + sb.Append(oldText[it.StartA + m]); + } // for + sb.Append(""); + } + + // write inserted chars + if (displayInsertedText && pos < it.StartB + it.InsertedB) + { + sb.Append(insertedStyle); + while (pos < it.StartB + it.InsertedB) + { + sb.Append(newText[pos]); + pos++; + } // while + sb.Append(""); + } // if + } // while + + // write rest of unchanged chars + while (pos < newText.Length) + { + sb.Append(newText[pos]); + pos++; + } // while + + return sb.ToString(); + } + + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index ffce7b42ba..c57081f51a 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -28,6 +28,7 @@ namespace Umbraco.Core.Services //private Lazy _sectionService; //private Lazy _macroService; private Lazy _memberTypeService; + private Lazy _notificationService; /// /// public ctor - will generally just be used for unit testing @@ -98,6 +99,9 @@ namespace Umbraco.Core.Services var provider = dbUnitOfWorkProvider; var fileProvider = fileUnitOfWorkProvider; + if (_notificationService == null) + _notificationService = new Lazy(() => new NotificationService(provider, _userService.Value, _contentService.Value)); + if (_serverRegistrationService == null) _serverRegistrationService = new Lazy(() => new ServerRegistrationService(provider, repositoryFactory.Value)); @@ -148,6 +152,14 @@ namespace Umbraco.Core.Services } + /// + /// Gets the + /// + internal INotificationService NotificationService + { + get { return _notificationService.Value; } + } + /// /// Gets the /// diff --git a/src/Umbraco.Core/Strings/Diff.cs b/src/Umbraco.Core/Strings/Diff.cs new file mode 100644 index 0000000000..ed381d4f6f --- /dev/null +++ b/src/Umbraco.Core/Strings/Diff.cs @@ -0,0 +1,510 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace Umbraco.Core.Strings +{ + /// + /// This Class implements the Difference Algorithm published in + /// "An O(ND) Difference Algorithm and its Variations" by Eugene Myers + /// Algorithmica Vol. 1 No. 2, 1986, p 251. + /// + /// The algorithm itself is comparing 2 arrays of numbers so when comparing 2 text documents + /// each line is converted into a (hash) number. See DiffText(). + /// + /// diff.cs: A port of the algorithm to C# + /// Copyright (c) by Matthias Hertel, http://www.mathertel.de + /// This work is licensed under a BSD style license. See http://www.mathertel.de/License.aspx + /// + internal class Diff + { + /// Data on one input file being compared. + /// + internal class DiffData + { + + /// Number of elements (lines). + internal int Length; + + /// Buffer of numbers that will be compared. + internal int[] Data; + + /// + /// Array of booleans that flag for modified data. + /// This is the result of the diff. + /// This means deletedA in the first Data or inserted in the second Data. + /// + internal bool[] Modified; + + /// + /// Initialize the Diff-Data buffer. + /// + /// reference to the buffer + internal DiffData(int[] initData) + { + Data = initData; + Length = initData.Length; + Modified = new bool[Length + 2]; + } // DiffData + + } // class DiffData + + /// details of one difference. + public struct Item + { + /// Start Line number in Data A. + public int StartA; + /// Start Line number in Data B. + public int StartB; + + /// Number of changes in Data A. + public int DeletedA; + /// Number of changes in Data B. + public int InsertedB; + } // Item + + /// + /// Shortest Middle Snake Return Data + /// + private struct Smsrd + { + internal int X, Y; + // internal int u, v; // 2002.09.20: no need for 2 points + } + + /// + /// Find the difference in 2 texts, comparing by textlines. + /// + /// A-version of the text (usualy the old one) + /// B-version of the text (usualy the new one) + /// Returns a array of Items that describe the differences. + public static Item[] DiffText(string textA, string textB) + { + return (DiffText(textA, textB, false, false, false)); + } // DiffText + + /// + /// Find the difference in 2 texts, comparing by textlines. + /// This method uses the DiffInt internally by 1st converting the string into char codes + /// then uses the diff int method + /// + /// A-version of the text (usualy the old one) + /// B-version of the text (usualy the new one) + /// Returns a array of Items that describe the differences. + public static Item[] DiffText1(string textA, string textB) + { + return DiffInt(DiffCharCodes(textA, false), DiffCharCodes(textB, false)); + } + + + /// + /// Find the difference in 2 text documents, comparing by textlines. + /// The algorithm itself is comparing 2 arrays of numbers so when comparing 2 text documents + /// each line is converted into a (hash) number. This hash-value is computed by storing all + /// textlines into a common hashtable so i can find dublicates in there, and generating a + /// new number each time a new textline is inserted. + /// + /// A-version of the text (usualy the old one) + /// B-version of the text (usualy the new one) + /// When set to true, all leading and trailing whitespace characters are stripped out before the comparation is done. + /// When set to true, all whitespace characters are converted to a single space character before the comparation is done. + /// When set to true, all characters are converted to their lowercase equivivalence before the comparation is done. + /// Returns a array of Items that describe the differences. + public static Item[] DiffText(string textA, string textB, bool trimSpace, bool ignoreSpace, bool ignoreCase) + { + // prepare the input-text and convert to comparable numbers. + var h = new Hashtable(textA.Length + textB.Length); + + // The A-Version of the data (original data) to be compared. + var dataA = new DiffData(DiffCodes(textA, h, trimSpace, ignoreSpace, ignoreCase)); + + // The B-Version of the data (modified data) to be compared. + var dataB = new DiffData(DiffCodes(textB, h, trimSpace, ignoreSpace, ignoreCase)); + + h = null; // free up hashtable memory (maybe) + + var max = dataA.Length + dataB.Length + 1; + // vector for the (0,0) to (x,y) search + var downVector = new int[2 * max + 2]; + // vector for the (u,v) to (N,M) search + var upVector = new int[2 * max + 2]; + + Lcs(dataA, 0, dataA.Length, dataB, 0, dataB.Length, downVector, upVector); + + Optimize(dataA); + Optimize(dataB); + return CreateDiffs(dataA, dataB); + } // DiffText + + + /// + /// Diffs the char codes. + /// + /// A text. + /// if set to true [ignore case]. + /// + private static int[] DiffCharCodes(string aText, bool ignoreCase) + { + if (ignoreCase) + aText = aText.ToUpperInvariant(); + + var codes = new int[aText.Length]; + + for (int n = 0; n < aText.Length; n++) + codes[n] = (int)aText[n]; + + return (codes); + } // DiffCharCodes + + /// + /// If a sequence of modified lines starts with a line that contains the same content + /// as the line that appends the changes, the difference sequence is modified so that the + /// appended line and not the starting line is marked as modified. + /// This leads to more readable diff sequences when comparing text files. + /// + /// A Diff data buffer containing the identified changes. + private static void Optimize(DiffData data) + { + var startPos = 0; + while (startPos < data.Length) + { + while ((startPos < data.Length) && (data.Modified[startPos] == false)) + startPos++; + int endPos = startPos; + while ((endPos < data.Length) && (data.Modified[endPos] == true)) + endPos++; + + if ((endPos < data.Length) && (data.Data[startPos] == data.Data[endPos])) + { + data.Modified[startPos] = false; + data.Modified[endPos] = true; + } + else + { + startPos = endPos; + } // if + } // while + } // Optimize + + + /// + /// Find the difference in 2 arrays of integers. + /// + /// A-version of the numbers (usualy the old one) + /// B-version of the numbers (usualy the new one) + /// Returns a array of Items that describe the differences. + public static Item[] DiffInt(int[] arrayA, int[] arrayB) + { + // The A-Version of the data (original data) to be compared. + var dataA = new DiffData(arrayA); + + // The B-Version of the data (modified data) to be compared. + var dataB = new DiffData(arrayB); + + var max = dataA.Length + dataB.Length + 1; + // vector for the (0,0) to (x,y) search + var downVector = new int[2 * max + 2]; + // vector for the (u,v) to (N,M) search + var upVector = new int[2 * max + 2]; + + Lcs(dataA, 0, dataA.Length, dataB, 0, dataB.Length, downVector, upVector); + return CreateDiffs(dataA, dataB); + } // Diff + + + /// + /// This function converts all textlines of the text into unique numbers for every unique textline + /// so further work can work only with simple numbers. + /// + /// the input text + /// This extern initialized hashtable is used for storing all ever used textlines. + /// ignore leading and trailing space characters + /// + /// + /// a array of integers. + private static int[] DiffCodes(string aText, IDictionary h, bool trimSpace, bool ignoreSpace, bool ignoreCase) + { + // get all codes of the text + var lastUsedCode = h.Count; + + // strip off all cr, only use lf as textline separator. + aText = aText.Replace("\r", ""); + var lines = aText.Split('\n'); + + var codes = new int[lines.Length]; + + for (int i = 0; i < lines.Length; ++i) + { + string s = lines[i]; + if (trimSpace) + s = s.Trim(); + + if (ignoreSpace) + { + s = Regex.Replace(s, "\\s+", " "); // TODO: optimization: faster blank removal. + } + + if (ignoreCase) + s = s.ToLower(); + + object aCode = h[s]; + if (aCode == null) + { + lastUsedCode++; + h[s] = lastUsedCode; + codes[i] = lastUsedCode; + } + else + { + codes[i] = (int)aCode; + } // if + } // for + return (codes); + } // DiffCodes + + + /// + /// This is the algorithm to find the Shortest Middle Snake (SMS). + /// + /// sequence A + /// lower bound of the actual range in DataA + /// upper bound of the actual range in DataA (exclusive) + /// sequence B + /// lower bound of the actual range in DataB + /// upper bound of the actual range in DataB (exclusive) + /// a vector for the (0,0) to (x,y) search. Passed as a parameter for speed reasons. + /// a vector for the (u,v) to (N,M) search. Passed as a parameter for speed reasons. + /// a MiddleSnakeData record containing x,y and u,v + private static Smsrd Sms(DiffData dataA, int lowerA, int upperA, DiffData dataB, int lowerB, int upperB, int[] downVector, int[] upVector) + { + int max = dataA.Length + dataB.Length + 1; + + int downK = lowerA - lowerB; // the k-line to start the forward search + int upK = upperA - upperB; // the k-line to start the reverse search + + int delta = (upperA - lowerA) - (upperB - lowerB); + bool oddDelta = (delta & 1) != 0; + + // The vectors in the publication accepts negative indexes. the vectors implemented here are 0-based + // and are access using a specific offset: UpOffset UpVector and DownOffset for DownVektor + int downOffset = max - downK; + int upOffset = max - upK; + + int maxD = ((upperA - lowerA + upperB - lowerB) / 2) + 1; + + // Debug.Write(2, "SMS", String.Format("Search the box: A[{0}-{1}] to B[{2}-{3}]", LowerA, UpperA, LowerB, UpperB)); + + // init vectors + downVector[downOffset + downK + 1] = lowerA; + upVector[upOffset + upK - 1] = upperA; + + for (int d = 0; d <= maxD; d++) + { + + // Extend the forward path. + Smsrd ret; + for (int k = downK - d; k <= downK + d; k += 2) + { + // Debug.Write(0, "SMS", "extend forward path " + k.ToString()); + + // find the only or better starting point + int x, y; + if (k == downK - d) + { + x = downVector[downOffset + k + 1]; // down + } + else + { + x = downVector[downOffset + k - 1] + 1; // a step to the right + if ((k < downK + d) && (downVector[downOffset + k + 1] >= x)) + x = downVector[downOffset + k + 1]; // down + } + y = x - k; + + // find the end of the furthest reaching forward D-path in diagonal k. + while ((x < upperA) && (y < upperB) && (dataA.Data[x] == dataB.Data[y])) + { + x++; y++; + } + downVector[downOffset + k] = x; + + // overlap ? + if (oddDelta && (upK - d < k) && (k < upK + d)) + { + if (upVector[upOffset + k] <= downVector[downOffset + k]) + { + ret.X = downVector[downOffset + k]; + ret.Y = downVector[downOffset + k] - k; + // ret.u = UpVector[UpOffset + k]; // 2002.09.20: no need for 2 points + // ret.v = UpVector[UpOffset + k] - k; + return (ret); + } // if + } // if + + } // for k + + // Extend the reverse path. + for (int k = upK - d; k <= upK + d; k += 2) + { + // Debug.Write(0, "SMS", "extend reverse path " + k.ToString()); + + // find the only or better starting point + int x, y; + if (k == upK + d) + { + x = upVector[upOffset + k - 1]; // up + } + else + { + x = upVector[upOffset + k + 1] - 1; // left + if ((k > upK - d) && (upVector[upOffset + k - 1] < x)) + x = upVector[upOffset + k - 1]; // up + } // if + y = x - k; + + while ((x > lowerA) && (y > lowerB) && (dataA.Data[x - 1] == dataB.Data[y - 1])) + { + x--; y--; // diagonal + } + upVector[upOffset + k] = x; + + // overlap ? + if (!oddDelta && (downK - d <= k) && (k <= downK + d)) + { + if (upVector[upOffset + k] <= downVector[downOffset + k]) + { + ret.X = downVector[downOffset + k]; + ret.Y = downVector[downOffset + k] - k; + // ret.u = UpVector[UpOffset + k]; // 2002.09.20: no need for 2 points + // ret.v = UpVector[UpOffset + k] - k; + return (ret); + } // if + } // if + + } // for k + + } // for D + + throw new ApplicationException("the algorithm should never come here."); + } // SMS + + + /// + /// This is the divide-and-conquer implementation of the longes common-subsequence (LCS) + /// algorithm. + /// The published algorithm passes recursively parts of the A and B sequences. + /// To avoid copying these arrays the lower and upper bounds are passed while the sequences stay constant. + /// + /// sequence A + /// lower bound of the actual range in DataA + /// upper bound of the actual range in DataA (exclusive) + /// sequence B + /// lower bound of the actual range in DataB + /// upper bound of the actual range in DataB (exclusive) + /// a vector for the (0,0) to (x,y) search. Passed as a parameter for speed reasons. + /// a vector for the (u,v) to (N,M) search. Passed as a parameter for speed reasons. + private static void Lcs(DiffData dataA, int lowerA, int upperA, DiffData dataB, int lowerB, int upperB, int[] downVector, int[] upVector) + { + // Debug.Write(2, "LCS", String.Format("Analyse the box: A[{0}-{1}] to B[{2}-{3}]", LowerA, UpperA, LowerB, UpperB)); + + // Fast walkthrough equal lines at the start + while (lowerA < upperA && lowerB < upperB && dataA.Data[lowerA] == dataB.Data[lowerB]) + { + lowerA++; lowerB++; + } + + // Fast walkthrough equal lines at the end + while (lowerA < upperA && lowerB < upperB && dataA.Data[upperA - 1] == dataB.Data[upperB - 1]) + { + --upperA; --upperB; + } + + if (lowerA == upperA) + { + // mark as inserted lines. + while (lowerB < upperB) + dataB.Modified[lowerB++] = true; + + } + else if (lowerB == upperB) + { + // mark as deleted lines. + while (lowerA < upperA) + dataA.Modified[lowerA++] = true; + + } + else + { + // Find the middle snakea and length of an optimal path for A and B + Smsrd smsrd = Sms(dataA, lowerA, upperA, dataB, lowerB, upperB, downVector, upVector); + // Debug.Write(2, "MiddleSnakeData", String.Format("{0},{1}", smsrd.x, smsrd.y)); + + // The path is from LowerX to (x,y) and (x,y) to UpperX + Lcs(dataA, lowerA, smsrd.X, dataB, lowerB, smsrd.Y, downVector, upVector); + Lcs(dataA, smsrd.X, upperA, dataB, smsrd.Y, upperB, downVector, upVector); // 2002.09.20: no need for 2 points + } + } // LCS() + + + /// Scan the tables of which lines are inserted and deleted, + /// producing an edit script in forward order. + /// + /// dynamic array + private static Item[] CreateDiffs(DiffData dataA, DiffData dataB) + { + ArrayList a = new ArrayList(); + Item aItem; + Item[] result; + + int lineA = 0; + int lineB = 0; + while (lineA < dataA.Length || lineB < dataB.Length) + { + if ((lineA < dataA.Length) && (!dataA.Modified[lineA]) + && (lineB < dataB.Length) && (!dataB.Modified[lineB])) + { + // equal lines + lineA++; + lineB++; + + } + else + { + // maybe deleted and/or inserted lines + int startA = lineA; + int startB = lineB; + + while (lineA < dataA.Length && (lineB >= dataB.Length || dataA.Modified[lineA])) + // while (LineA < DataA.Length && DataA.modified[LineA]) + lineA++; + + while (lineB < dataB.Length && (lineA >= dataA.Length || dataB.Modified[lineB])) + // while (LineB < DataB.Length && DataB.modified[LineB]) + lineB++; + + if ((startA < lineA) || (startB < lineB)) + { + // store a new difference-item + aItem = new Item(); + aItem.StartA = startA; + aItem.StartB = startB; + aItem.DeletedA = lineA - startA; + aItem.InsertedB = lineB - startB; + a.Add(aItem); + } // if + } // if + } // while + + result = new Item[a.Count]; + a.CopyTo(result); + + return (result); + } + + } // class Diff + + +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index de971f6269..4c26172f17 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -789,6 +789,7 @@ + diff --git a/src/umbraco.cms/businesslogic/workflow/Diff.cs b/src/umbraco.cms/businesslogic/workflow/Diff.cs index e4e4d505a9..39cbc1d2c3 100644 --- a/src/umbraco.cms/businesslogic/workflow/Diff.cs +++ b/src/umbraco.cms/businesslogic/workflow/Diff.cs @@ -18,7 +18,7 @@ namespace umbraco.cms.businesslogic.workflow /// Copyright (c) by Matthias Hertel, http://www.mathertel.de /// This work is licensed under a BSD style license. See http://www.mathertel.de/License.aspx /// - + [Obsolete("This class will be removed from the codebase in the future")] public class Diff { @@ -489,6 +489,7 @@ namespace umbraco.cms.businesslogic.workflow /// Data on one input file being compared. /// + [Obsolete("This class will be removed from the codebase in the future, logic has moved to the Core project")] internal class DiffData { diff --git a/src/umbraco.cms/businesslogic/workflow/Notification.cs b/src/umbraco.cms/businesslogic/workflow/Notification.cs index 29fc8ed319..e70077d118 100644 --- a/src/umbraco.cms/businesslogic/workflow/Notification.cs +++ b/src/umbraco.cms/businesslogic/workflow/Notification.cs @@ -71,7 +71,7 @@ namespace umbraco.cms.businesslogic.workflow } } - ///TODO: Include update with html mail notification and document contents + //TODO: Include update with html mail notification and document contents private static void SendNotification(User performingUser, User mailingUser, Document documentObject, IAction action) { // retrieve previous version of the document From 44bc365fddff47ad67f223d5fab7b9d1c39b0c67 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 13 Jan 2014 13:50:30 +1100 Subject: [PATCH 10/11] Moves all notification sending logic to the notification service, improves performance during any action when notifications are to be sent, ensures emails are sent out async to not block up the current request. --- .../Services/NotificationService.cs | 45 ++-- .../businesslogic/utilities/Diff.cs | 2 + .../businesslogic/workflow/Notification.cs | 212 +----------------- 3 files changed, 41 insertions(+), 218 deletions(-) diff --git a/src/Umbraco.Core/Services/NotificationService.cs b/src/Umbraco.Core/Services/NotificationService.cs index 609b6316d8..4ddde70e86 100644 --- a/src/Umbraco.Core/Services/NotificationService.cs +++ b/src/Umbraco.Core/Services/NotificationService.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using System.Net.Mail; using System.Text; +using System.Threading; using System.Web; using Umbraco.Core.Configuration; using Umbraco.Core.IO; @@ -53,6 +54,8 @@ namespace Umbraco.Core.Services throw new NotSupportedException(); } var content = (IContent) entity; + //we'll lazily get these if we need to send notifications + IContent[] allVersions = null; int totalUsers; var allUsers = _userService.GetAllMembers(0, int.MaxValue, out totalUsers); @@ -63,9 +66,15 @@ namespace Umbraco.Core.Services var notificationForAction = userNotifications.FirstOrDefault(x => x.Action == action); if (notificationForAction != null) { + //lazy load versions if notifications are required + if (allVersions == null) + { + allVersions = _contentService.GetVersions(entity.Id).ToArray(); + } + try { - SendNotification(operatingUser, u, content, actionName, http, createSubject, createBody); + SendNotification(operatingUser, u, content, allVersions, actionName, http, createSubject, createBody); LogHelper.Debug(string.Format("Notification type: {0} sent to {1} ({2})", action, u.Name, u.Email)); } catch (Exception ex) @@ -101,10 +110,7 @@ namespace Umbraco.Core.Services { var userNotifications = GetUserNotifications(user).ToArray(); var pathParts = path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); - var result = pathParts - .Select(part => userNotifications.FirstOrDefault(x => x.EntityId.ToString(CultureInfo.InvariantCulture) == part)) - .Where(notification => notification != null) - .ToList(); + var result = userNotifications.Where(r => pathParts.InvariantContains(r.EntityId.ToString(CultureInfo.InvariantCulture))).ToList(); return result; } @@ -175,27 +181,25 @@ namespace Umbraco.Core.Services /// /// /// + /// /// The action readable name - currently an action is just a single letter, this is the name associated with the letter /// /// Callback to create the mail subject /// Callback to create the mail body - private void SendNotification(IUser performingUser, IUser mailingUser, IContent content, string actionName, HttpContextBase http, + private void SendNotification(IUser performingUser, IUser mailingUser, IContent content, IContent[] allVersions, string actionName, HttpContextBase http, Func createSubject, Func createBody) { if (performingUser == null) throw new ArgumentNullException("performingUser"); if (mailingUser == null) throw new ArgumentNullException("mailingUser"); if (content == null) throw new ArgumentNullException("content"); + if (allVersions == null) throw new ArgumentNullException("allVersions"); if (http == null) throw new ArgumentNullException("http"); if (createSubject == null) throw new ArgumentNullException("createSubject"); if (createBody == null) throw new ArgumentNullException("createBody"); - // retrieve previous version of the document - var versions = _contentService.GetVersions(content.Id).ToArray(); - - int versionCount = (versions.Length > 1) ? (versions.Length - 2) : (versions.Length - 1); - var oldDoc = _contentService.GetByVersion(versions[versionCount].Version); - //var oldDoc = new Document(documentObject.Id, versions[versionCount].Version); + int versionCount = (allVersions.Length > 1) ? (allVersions.Length - 2) : (allVersions.Length - 1); + var oldDoc = _contentService.GetByVersion(allVersions[versionCount].Version); // build summary var summary = new StringBuilder(); @@ -313,9 +317,20 @@ namespace Umbraco.Core.Services string.Format("https://{0}", serverName)); } - // send it - var sender = new SmtpClient(); - sender.Send(mail); + + // send it asynchronously, we don't want to got up all of the request time to send emails! + ThreadPool.QueueUserWorkItem(state => + { + try + { + var sender = new SmtpClient(); + sender.Send(mail); + } + catch (Exception ex) + { + LogHelper.Error("An error occurred sending notification", ex); + } + }); } private static string ReplaceLinks(string text, HttpRequestBase request) diff --git a/src/umbraco.cms/businesslogic/utilities/Diff.cs b/src/umbraco.cms/businesslogic/utilities/Diff.cs index 62397f2495..3aa159de7e 100644 --- a/src/umbraco.cms/businesslogic/utilities/Diff.cs +++ b/src/umbraco.cms/businesslogic/utilities/Diff.cs @@ -3,6 +3,8 @@ using System.Collections; using System.Text; using System.Text.RegularExpressions; +//TODO: We've alraedy moved most of this logic to Core.Strings - need to review this as it has slightly more functionality but should be moved to core and obsoleted! + namespace umbraco.cms.businesslogic.utilities { /// /// This Class implements the Difference Algorithm published in diff --git a/src/umbraco.cms/businesslogic/workflow/Notification.cs b/src/umbraco.cms/businesslogic/workflow/Notification.cs index e70077d118..4ebd48e7f2 100644 --- a/src/umbraco.cms/businesslogic/workflow/Notification.cs +++ b/src/umbraco.cms/businesslogic/workflow/Notification.cs @@ -74,135 +74,16 @@ namespace umbraco.cms.businesslogic.workflow //TODO: Include update with html mail notification and document contents private static void SendNotification(User performingUser, User mailingUser, Document documentObject, IAction action) { - // retrieve previous version of the document - DocumentVersionList[] versions = documentObject.GetVersions(); - int versionCount = (versions.Length > 1) ? (versions.Length - 2) : (versions.Length - 1); - var oldDoc = new Document(documentObject.Id, versions[versionCount].Version); + var nService = ApplicationContext.Current.Services.NotificationService; + var pUser = ApplicationContext.Current.Services.UserService.GetById(performingUser.Id); - // build summary - var summary = new StringBuilder(); - var props = documentObject.GenericProperties; - foreach (Property p in props) - { - // check if something was changed and display the changes otherwise display the fields - Property oldProperty = oldDoc.getProperty(p.PropertyType.Alias); - string oldText = oldProperty.Value != null ? oldProperty.Value.ToString() : ""; - string newText = p.Value != null ? p.Value.ToString() : ""; - - // replace html with char equivalent - ReplaceHtmlSymbols(ref oldText); - ReplaceHtmlSymbols(ref newText); - - // make sure to only highlight changes done using TinyMCE editor... other changes will be displayed using default summary - //TODO PPH: Had to change this, as a reference to the editorcontrols is not allowed, so a string comparison is the only way, this should be a DIFF or something instead.. - if (p.PropertyType.DataTypeDefinition.DataType.ToString() == - "umbraco.editorControls.tinymce.TinyMCEDataType" && - string.CompareOrdinal(oldText, newText) != 0) - { - summary.Append(""); - summary.Append(" Note: "); - summary.Append( - " Red for deleted characters Yellow for inserted characters"); - summary.Append(""); - summary.Append(""); - summary.Append(" New " + - p.PropertyType.Name + ""); - summary.Append("" + - ReplaceLinks(CompareText(oldText, newText, true, false, - "", string.Empty)) + - ""); - summary.Append(""); - summary.Append(""); - summary.Append(" Old " + - oldProperty.PropertyType.Name + ""); - summary.Append("" + - ReplaceLinks(CompareText(newText, oldText, true, false, - "", string.Empty)) + - ""); - summary.Append(""); - } - else - { - summary.Append(""); - summary.Append("" + - p.PropertyType.Name + ""); - summary.Append("" + newText + ""); - summary.Append(""); - } - summary.Append( - " "); - } - - string protocol = GlobalSettings.UseSSL ? "https" : "http"; - - - string[] subjectVars = { - HttpContext.Current.Request.ServerVariables["SERVER_NAME"] + ":" + - HttpContext.Current.Request.Url.Port + - IOHelper.ResolveUrl(SystemDirectories.Umbraco), ui.Text(action.Alias) - , - documentObject.Text - }; - string[] bodyVars = { - mailingUser.Name, ui.Text(action.Alias), documentObject.Text, performingUser.Name, - HttpContext.Current.Request.ServerVariables["SERVER_NAME"] + ":" + - HttpContext.Current.Request.Url.Port + - IOHelper.ResolveUrl(SystemDirectories.Umbraco), - documentObject.Id.ToString(), summary.ToString(), - String.Format("{2}://{0}/{1}", - HttpContext.Current.Request.ServerVariables["SERVER_NAME"] + ":" + - HttpContext.Current.Request.Url.Port, - /*umbraco.library.NiceUrl(documentObject.Id))*/ - documentObject.Id + ".aspx", - protocol) - //TODO: PPH removed the niceURL reference... cms.dll cannot reference the presentation project... - //TODO: This should be moved somewhere else.. - }; - - // create the mail message - var mail = new MailMessage(UmbracoSettings.NotificationEmailSender, mailingUser.Email); - - // populate the message - mail.Subject = ui.Text("notifications", "mailSubject", subjectVars, mailingUser); - if (UmbracoSettings.NotificationDisableHtmlEmail) - { - mail.IsBodyHtml = false; - mail.Body = ui.Text("notifications", "mailBody", bodyVars, mailingUser); - } - else - { - mail.IsBodyHtml = true; - mail.Body = - @" - - -" + - ui.Text("notifications", "mailBodyHtml", bodyVars, mailingUser) + ""; - } - - // nh, issue 30724. Due to hardcoded http strings in resource files, we need to check for https replacements here - // adding the server name to make sure we don't replace external links - if (GlobalSettings.UseSSL && string.IsNullOrEmpty(mail.Body) == false) - { - string serverName = HttpContext.Current.Request.ServerVariables["SERVER_NAME"]; - mail.Body = mail.Body.Replace( - string.Format("http://{0}", serverName), - string.Format("https://{0}", serverName)); - } - - // send it - var sender = new SmtpClient(); - sender.Send(mail); - } - - private static string ReplaceLinks(string text) - { - string domain = GlobalSettings.UseSSL ? "https://" : "http://"; - domain += HttpContext.Current.Request.ServerVariables["SERVER_NAME"] + ":" + - HttpContext.Current.Request.Url.Port + "/"; - text = text.Replace("href=\"/", "href=\"" + domain); - text = text.Replace("src=\"/", "src=\"" + domain); - return text; + nService.SendNotifications( + pUser, documentObject.Content, action.Letter.ToString(CultureInfo.InvariantCulture), ui.Text(action.Alias), + new HttpContextWrapper(HttpContext.Current), + (user, strings) => ui.Text("notifications", "mailSubject", strings, mailingUser), + (user, strings) => UmbracoSettings.NotificationDisableHtmlEmail + ? ui.Text("notifications", "mailBody", strings, mailingUser) + : ui.Text("notifications", "mailBodyHtml", strings, mailingUser)); } /// @@ -327,80 +208,5 @@ namespace umbraco.cms.businesslogic.workflow MakeNew(user, node, c); } - /// - /// Replaces the HTML symbols with the character equivalent. - /// - /// The old string. - private static void ReplaceHtmlSymbols(ref string oldString) - { - oldString = oldString.Replace(" ", " "); - oldString = oldString.Replace("’", "'"); - oldString = oldString.Replace("&", "&"); - oldString = oldString.Replace("“", ""); - oldString = oldString.Replace("”", ""); - oldString = oldString.Replace(""", "\""); - } - - /// - /// Compares the text. - /// - /// The old text. - /// The new text. - /// if set to true [display inserted text]. - /// if set to true [display deleted text]. - /// The inserted style. - /// The deleted style. - /// - private static string CompareText(string oldText, string newText, bool displayInsertedText, - bool displayDeletedText, string insertedStyle, string deletedStyle) - { - var sb = new StringBuilder(); - Diff.Item[] diffs = Diff.DiffText1(oldText, newText); - - int pos = 0; - for (int n = 0; n < diffs.Length; n++) - { - Diff.Item it = diffs[n]; - - // write unchanged chars - while ((pos < it.StartB) && (pos < newText.Length)) - { - sb.Append(newText[pos]); - pos++; - } // while - - // write deleted chars - if (displayDeletedText && it.deletedA > 0) - { - sb.Append(deletedStyle); - for (int m = 0; m < it.deletedA; m++) - { - sb.Append(oldText[it.StartA + m]); - } // for - sb.Append(""); - } - - // write inserted chars - if (displayInsertedText && pos < it.StartB + it.insertedB) - { - sb.Append(insertedStyle); - while (pos < it.StartB + it.insertedB) - { - sb.Append(newText[pos]); - pos++; - } // while - sb.Append(""); - } // if - } // while - - // write rest of unchanged chars - while (pos < newText.Length) - { - sb.Append(newText[pos]); - pos++; - } // while - - return sb.ToString(); - } } } \ No newline at end of file From aebc30c7a399206e1eaa7045ff31310ad260f0bb Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 13 Jan 2014 14:02:50 +1100 Subject: [PATCH 11/11] Removes the version specific info from the CDF and Examine references in all proj files. --- src/Umbraco.Tests/Umbraco.Tests.csproj | 2 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 6 +++--- src/Umbraco.Web.UI/config/ClientDependency.config | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 4 ++-- src/UmbracoExamine.Azure/UmbracoExamine.Azure.csproj | 4 ++-- .../UmbracoExamine.PDF.Azure.csproj | 4 ++-- src/UmbracoExamine.PDF/UmbracoExamine.PDF.csproj | 2 +- src/UmbracoExamine/UmbracoExamine.csproj | 2 +- src/umbraco.cms/umbraco.cms.csproj | 2 +- src/umbraco.controls/umbraco.controls.csproj | 2 +- src/umbraco.editorControls/umbraco.editorControls.csproj | 2 +- src/umbraco.macroRenderings/umbraco.macroRenderings.csproj | 2 +- 12 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index db4fb63e84..ac240834d5 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -48,7 +48,7 @@ 4 - + False ..\packages\Examine.0.1.52.2941\lib\Examine.dll diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index f993e4fce4..2af0a7cf5d 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -101,15 +101,15 @@ {07fbc26b-2927-4a22-8d96-d644c667fecc} UmbracoExamine - + False ..\packages\ClientDependency.1.7.1.1\lib\ClientDependency.Core.dll - + False ..\packages\ClientDependency-Mvc.1.7.0.4\lib\ClientDependency.Core.Mvc.dll - + False ..\packages\Examine.0.1.52.2941\lib\Examine.dll diff --git a/src/Umbraco.Web.UI/config/ClientDependency.config b/src/Umbraco.Web.UI/config/ClientDependency.config index acd5d07260..6c1ff881cf 100644 --- a/src/Umbraco.Web.UI/config/ClientDependency.config +++ b/src/Umbraco.Web.UI/config/ClientDependency.config @@ -10,7 +10,7 @@ NOTES: * Compression/Combination/Minification is not enabled unless debug="false" is specified on the 'compiliation' element in the web.config * A new version will invalidate both client and server cache and create new persisted files --> - +