diff --git a/src/Orchard.Tests/DataMigration/DataMigrationCommandsTests.cs b/src/Orchard.Tests/DataMigration/DataMigrationCommandsTests.cs new file mode 100644 index 000000000..6d9fc665c --- /dev/null +++ b/src/Orchard.Tests/DataMigration/DataMigrationCommandsTests.cs @@ -0,0 +1,38 @@ +using System.Data; +using NUnit.Framework; +using Orchard.DataMigration.Schema; + +namespace Orchard.Tests.DataMigration { + [TestFixture] + public class DataMigrationCommandsTests { + + [Test] + public void AllMethodsShouldBeCalledSuccessfully() { + var schemaBuilder = new SchemaBuilder("TEST_"); + + schemaBuilder + .CreateTable("User", table => table + .ContentPartRecord() + .Column("Id", DbType.Int32, column => column.PrimaryKey()) + .Column("Firstname", DbType.String, column => column.Length(255)) + .Column("Lastname", DbType.String, column => column.Precision(0).Scale(1)) + .ForeignKey("User_Address", fk => fk.On("Id", "Address", "UserId"))) + .CreateTable("Address", table => table + .VersionedContentPartRecord() + .Column("City", DbType.String) + .Column("ZIP", DbType.Int32, column => column.Unique()) + .Column("UserId", DbType.Int32, column => column.NotNull())) + .AlterTable("User", table => table + .AddColumn("Age", DbType.Int32) + .AlterColumn("Lastname", column => column.Default("John")) + .AlterColumn("Lastname", column => column.Rename("John")) + .DropColumn("Lastname") + .CreateIndex("IDX_XYZ", "NickName") + .DropIndex("IDX_XYZ") + .AddForeignKey("FKL", fk => fk.On("Id", "A", "Id").On("Id", "B", "Id") ) + .DropForeignKey("FKL")) + .DropTable("Address") + .ExecuteSql("DROP DATABASE", statement => statement.ForDialect("SQLite").ForDialect("MsSqlServer2008")); + } + } +} diff --git a/src/Orchard.Tests/DataMigration/DataMigrationTests.cs b/src/Orchard.Tests/DataMigration/DataMigrationTests.cs index a12e2e58b..26c19c624 100644 --- a/src/Orchard.Tests/DataMigration/DataMigrationTests.cs +++ b/src/Orchard.Tests/DataMigration/DataMigrationTests.cs @@ -6,9 +6,9 @@ using NHibernate; using NUnit.Framework; using Orchard.ContentManagement.Records; using Orchard.Data; +using Orchard.Environment.Configuration; using Orchard.Environment.Extensions; using Orchard.Environment.Extensions.Folders; -using Orchard.Environment.Extensions.Loaders; using Orchard.Environment.Extensions.Models; using Orchard.Tests.ContentManagement; using Orchard.DataMigration; @@ -17,7 +17,6 @@ namespace Orchard.Tests.DataMigration { [TestFixture] public class DataMigrationTests { private IContainer _container; - private IExtensionManager _manager; private StubFolders _folders; private IDataMigrationManager _dataMigrationManager; private IRepository _repository; @@ -25,13 +24,8 @@ namespace Orchard.Tests.DataMigration { private ISessionFactory _sessionFactory; private ISession _session; - [SetUp] - public void Init() { - Init(Enumerable.Empty()); - } - - public void Init(IEnumerable dataMigrations) { - + [TestFixtureSetUp] + public void CreateDb() { var databaseFileName = System.IO.Path.GetTempFileName(); _sessionFactory = DataUtility.CreateSessionFactory( databaseFileName, @@ -39,9 +33,22 @@ namespace Orchard.Tests.DataMigration { typeof(ContentItemVersionRecord), typeof(ContentItemRecord), typeof(ContentTypeRecord)); + } + public void InitDb() { + foreach ( var record in _repository.Fetch(m => true) ) { + _repository.Delete(record); + } + _repository.Flush(); + } + + public void Init(IEnumerable dataMigrations) { + var builder = new ContainerBuilder(); _folders = new StubFolders(); + + builder.RegisterInstance(new ShellSettings { DataTablePrefix = "TEST_"}); + builder.RegisterInstance(_folders).As(); builder.RegisterType().As(); builder.RegisterType().As(); @@ -53,10 +60,11 @@ namespace Orchard.Tests.DataMigration { builder.RegisterType(type).As(); } _container = builder.Build(); - _manager = _container.Resolve(); + _container.Resolve(); _dataMigrationManager = _container.Resolve(); _repository = _container.Resolve>(); + InitDb(); } public class StubFolders : IExtensionFolders { @@ -139,6 +147,7 @@ namespace Orchard.Tests.DataMigration { return 999; } } + public class DataMigrationDependenciesModule2 : IDataMigration { public string Feature { get { return "Feature2"; } @@ -149,9 +158,52 @@ namespace Orchard.Tests.DataMigration { } } + public class DataMigrationWithSchemaBuilder : DataMigrationImpl { + public override string Feature { + get { return "Feature1"; } + } + + public int Create() { + Assert.That(SchemaBuilder, Is.Not.Null); + Assert.That(SchemaBuilder.TablePrefix, Is.EqualTo("TEST_")); + return 1; + } + } + + public class DataMigrationFeatureNeedUpdate1 : IDataMigration { + public string Feature { + get { return "Feature1"; } + } + } + + + public class DataMigrationFeatureNeedUpdate2 : IDataMigration { + public string Feature { + get { return "Feature2"; } + } + + public int Create() { + return 999; + } + } + + public class DataMigrationFeatureNeedUpdate3 : IDataMigration { + public string Feature { + get { return "Feature3"; } + } + + public int Create() { + return 999; + } + + public int UpdateFrom42() { + return 999; + } + } + [Test] public void DataMigrationShouldDoNothingIfNoDataMigrationIsProvidedForFeature() { - Init(new Type[] {typeof (DataMigrationEmpty)}); + Init(new[] {typeof (DataMigrationEmpty)}); _folders.Manifests.Add("Module2", @" name: Module2 @@ -162,13 +214,13 @@ features: Description: Feature "); - _dataMigrationManager.Upgrade("Feature1"); + _dataMigrationManager.Update("Feature1"); Assert.That(_repository.Table.Count(), Is.EqualTo(0)); } [Test] public void DataMigrationShouldDoNothingIfNoUpgradeOrCreateMethodWasFound() { - Init(new Type[] { typeof(DataMigration11) }); + Init(new[] { typeof(DataMigration11) }); _folders.Manifests.Add("Module1", @" name: Module1 @@ -179,13 +231,13 @@ features: Description: Feature "); - _dataMigrationManager.Upgrade("Feature1"); + _dataMigrationManager.Update("Feature1"); Assert.That(_repository.Table.Count(), Is.EqualTo(0)); } [Test] public void CreateShouldReturnVersionNumber() { - Init(new Type[] { typeof(DataMigration11Create) }); + Init(new[] { typeof(DataMigration11Create) }); _folders.Manifests.Add("Module1", @" name: Module1 @@ -196,7 +248,7 @@ features: Description: Feature "); - _dataMigrationManager.Upgrade("Feature1"); + _dataMigrationManager.Update("Feature1"); Assert.That(_repository.Table.Count(), Is.EqualTo(1)); Assert.That(_repository.Table.First().Current, Is.EqualTo(999)); Assert.That(_repository.Table.First().DataMigrationClass, Is.EqualTo("Orchard.Tests.DataMigration.DataMigrationTests+DataMigration11Create")); @@ -204,7 +256,7 @@ features: [Test] public void CreateCanBeFollowedByUpdates() { - Init(new Type[] {typeof (DataMigrationCreateCanBeFollowedByUpdates)}); + Init(new[] {typeof (DataMigrationCreateCanBeFollowedByUpdates)}); _folders.Manifests.Add("Module1", @" name: Module1 @@ -215,14 +267,14 @@ features: Description: Feature "); - _dataMigrationManager.Upgrade("Feature1"); + _dataMigrationManager.Update("Feature1"); Assert.That(_repository.Table.Count(), Is.EqualTo(1)); Assert.That(_repository.Table.First().Current, Is.EqualTo(666)); } [Test] public void SameMigrationClassCanEvolve() { - Init(new Type[] { typeof(DataMigrationSameMigrationClassCanEvolve) }); + Init(new[] { typeof(DataMigrationSameMigrationClassCanEvolve) }); _folders.Manifests.Add("Module1", @" name: Module1 @@ -232,12 +284,12 @@ features: Feature1: Description: Feature "); - _repository.Create(new DataMigrationRecord() { + _repository.Create(new DataMigrationRecord { Current = 42, DataMigrationClass = "Orchard.Tests.DataMigration.DataMigrationTests+DataMigrationSameMigrationClassCanEvolve" }); - _dataMigrationManager.Upgrade("Feature1"); + _dataMigrationManager.Update("Feature1"); Assert.That(_repository.Table.Count(), Is.EqualTo(1)); Assert.That(_repository.Table.First().Current, Is.EqualTo(999)); } @@ -245,7 +297,7 @@ features: [Test] public void DependenciesShouldBeUpgradedFirst() { - Init(new Type[] { typeof(DataMigrationDependenciesModule1), typeof(DataMigrationDependenciesModule2) }); + Init(new[] { typeof(DataMigrationDependenciesModule1), typeof(DataMigrationDependenciesModule2) }); _folders.Manifests.Add("Module1", @" name: Module1 @@ -265,12 +317,77 @@ features: Feature2: Description: Feature "); - _dataMigrationManager.Upgrade("Feature1"); + _dataMigrationManager.Update("Feature1"); Assert.That(_repository.Table.Count(), Is.EqualTo(2)); Assert.That(_repository.Fetch(d => d.Current == 999).Count(), Is.EqualTo(2)); Assert.That(_repository.Fetch(d => d.DataMigrationClass == "Orchard.Tests.DataMigration.DataMigrationTests+DataMigrationDependenciesModule1").Count(), Is.EqualTo(1)); Assert.That(_repository.Fetch(d => d.DataMigrationClass == "Orchard.Tests.DataMigration.DataMigrationTests+DataMigrationDependenciesModule2").Count(), Is.EqualTo(1)); } + [Test] + public void DataMigrationImplShouldGetASchemaBuilder() { + Init(new[] { typeof(DataMigrationWithSchemaBuilder) }); + + _folders.Manifests.Add("Module1", @" +name: Module1 +version: 0.1 +orchardversion: 1 +features: + Feature1: + Description: Feature +"); + + _dataMigrationManager.Update("Feature1"); + Assert.That(_repository.Table.Count(), Is.EqualTo(1)); + } + + [Test] + public void ShouldDetectFeaturesThatNeedUpdates() { + + Init(new[] { typeof(DataMigrationFeatureNeedUpdate1), typeof(DataMigrationFeatureNeedUpdate2), typeof(DataMigrationFeatureNeedUpdate3) }); + + _folders.Manifests.Add("Module1", @" +name: Module1 +version: 0.1 +orchardversion: 1 +features: + Feature1: + Description: Feature + Feature2: + Description: Feature + Feature3: + Description: Feature + Feature4: + Description: Feature +"); + + // even if there is a data migration class, as it is empty there should me no migration to do + Assert.That(_dataMigrationManager.GetFeaturesThatNeedUpdate().Contains("Feature1"), Is.False); + + // there is no available class for this feature + Assert.That(_dataMigrationManager.GetFeaturesThatNeedUpdate().Contains("Feature4"), Is.False); + + // there is a create method and no record in db, so let's create it + Assert.That(_dataMigrationManager.GetFeaturesThatNeedUpdate().Contains("Feature2"), Is.True); + + // there is an UpdateFrom42 method, so it should be fired if Current == 42 + + _repository.Create(new DataMigrationRecord { + Current = 42, + DataMigrationClass = "Orchard.Tests.DataMigration.DataMigrationTests+DataMigrationFeatureNeedUpdate3" + }); + + Assert.That(_dataMigrationManager.GetFeaturesThatNeedUpdate().Contains("Feature3"), Is.True); + + _repository.Delete(_repository.Fetch(m => m.Current == 42).First()); + _repository.Flush(); + + _repository.Create(new DataMigrationRecord { + Current = 43, + DataMigrationClass = "Orchard.Tests.DataMigration.DataMigrationTests+DataMigrationFeatureNeedUpdate3" + }); + + Assert.That(_dataMigrationManager.GetFeaturesThatNeedUpdate().Contains("Feature3"), Is.False); + } } } \ No newline at end of file diff --git a/src/Orchard.Tests/Orchard.Framework.Tests.csproj b/src/Orchard.Tests/Orchard.Framework.Tests.csproj index b6393e39a..89ab0f5a7 100644 --- a/src/Orchard.Tests/Orchard.Framework.Tests.csproj +++ b/src/Orchard.Tests/Orchard.Framework.Tests.csproj @@ -180,6 +180,7 @@ Code + diff --git a/src/Orchard.Web/Core/Common/Drivers/ItemReferenceContentFieldDriver.cs b/src/Orchard.Web/Core/Common/Drivers/ItemReferenceContentFieldDriver.cs new file mode 100644 index 000000000..2c2b3c8ba --- /dev/null +++ b/src/Orchard.Web/Core/Common/Drivers/ItemReferenceContentFieldDriver.cs @@ -0,0 +1,46 @@ +using JetBrains.Annotations; +using Orchard.ContentManagement; +using Orchard.ContentManagement.Drivers; +using Orchard.Core.Common.Fields; +using Orchard.Core.Common.ViewModels; + +namespace Orchard.Core.Common.Drivers { + [UsedImplicitly] + public class ItemReferenceContentFieldDriver : ContentFieldDriver { + public IOrchardServices Services { get; set; } + private const string TemplateName = "Fields/Common.ItemReferenceContentField"; + + public ItemReferenceContentFieldDriver(IOrchardServices services) { + Services = services; + } + + protected override string Prefix { + get { return "ItemReferenceContentField"; } + } + + protected override DriverResult Display(ItemReferenceContentField field, string displayType) { + var model = new ItemReferenceContentFieldDisplayViewModel { + Item = Services.ContentManager.Get(field.ContentItemReference.Id) + }; + + return ContentFieldTemplate(model, TemplateName, Prefix); + } + + protected override DriverResult Editor(ItemReferenceContentField field) { + var model = BuildEditorViewModel(field); + return ContentFieldTemplate(model, TemplateName, Prefix).Location("primary", "6"); + } + + protected override DriverResult Editor(ItemReferenceContentField field, IUpdateModel updater) { + var model = BuildEditorViewModel(field); + updater.TryUpdateModel(model, Prefix, null, null); + return ContentFieldTemplate(model, TemplateName, Prefix).Location("primary", "6"); + } + + private ItemReferenceContentFieldEditorViewModel BuildEditorViewModel(ItemReferenceContentField field) { + return new ItemReferenceContentFieldEditorViewModel { + Item = Services.ContentManager.Get(field.ContentItemReference.Id) + }; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Core/Common/Fields/ItemReferenceContentField.cs b/src/Orchard.Web/Core/Common/Fields/ItemReferenceContentField.cs new file mode 100644 index 000000000..bd3e9b62b --- /dev/null +++ b/src/Orchard.Web/Core/Common/Fields/ItemReferenceContentField.cs @@ -0,0 +1,8 @@ +using Orchard.ContentManagement; +using Orchard.ContentManagement.Records; + +namespace Orchard.Core.Common.Fields { + public class ItemReferenceContentField : ContentField { + public ContentItemRecord ContentItemReference { get; set; } + } +} diff --git a/src/Orchard.Web/Core/Common/Handlers/ItemReferenceContentFieldHandler.cs b/src/Orchard.Web/Core/Common/Handlers/ItemReferenceContentFieldHandler.cs new file mode 100644 index 000000000..0e2afb3bd --- /dev/null +++ b/src/Orchard.Web/Core/Common/Handlers/ItemReferenceContentFieldHandler.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using Orchard.ContentManagement.Drivers; +using Orchard.ContentManagement.Handlers; + +namespace Orchard.Core.Common.Handlers { + public class ItemReferenceContentFieldHandler : ContentFieldHandler { + public ItemReferenceContentFieldHandler(IEnumerable contentFieldDrivers) : base(contentFieldDrivers) { } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Core/Common/ViewModels/ItemReferenceContentFieldDisplayModel.cs b/src/Orchard.Web/Core/Common/ViewModels/ItemReferenceContentFieldDisplayModel.cs new file mode 100644 index 000000000..5344396c8 --- /dev/null +++ b/src/Orchard.Web/Core/Common/ViewModels/ItemReferenceContentFieldDisplayModel.cs @@ -0,0 +1,12 @@ +using Orchard.ContentManagement; + +namespace Orchard.Core.Common.ViewModels { + public class ItemReferenceContentFieldDisplayViewModel { + private ContentItem _item; + + public ContentItem Item { + get { return _item; } + set { _item = value; } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Core/Common/ViewModels/ItemReferenceContentFieldEditorViewModel.cs b/src/Orchard.Web/Core/Common/ViewModels/ItemReferenceContentFieldEditorViewModel.cs new file mode 100644 index 000000000..dd0249005 --- /dev/null +++ b/src/Orchard.Web/Core/Common/ViewModels/ItemReferenceContentFieldEditorViewModel.cs @@ -0,0 +1,12 @@ +using Orchard.ContentManagement; + +namespace Orchard.Core.Common.ViewModels { + public class ItemReferenceContentFieldEditorViewModel { + private ContentItem _item; + + public ContentItem Item { + get { return _item; } + set { _item = value; } + } + } +} diff --git a/src/Orchard.Web/Core/Common/Views/DisplayTemplates/Fields/Common.ItemReferenceContentField.ascx b/src/Orchard.Web/Core/Common/Views/DisplayTemplates/Fields/Common.ItemReferenceContentField.ascx new file mode 100644 index 000000000..ef1af1f37 --- /dev/null +++ b/src/Orchard.Web/Core/Common/Views/DisplayTemplates/Fields/Common.ItemReferenceContentField.ascx @@ -0,0 +1,3 @@ +<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> +<%@ Import Namespace="Orchard.Core.Common.ViewModels"%> +<%= Html.ItemDisplayLink(Model.Item) %> \ No newline at end of file diff --git a/src/Orchard.Web/Core/Common/Views/EditorTemplates/Fields/Common.ItemReferenceContentField.ascx b/src/Orchard.Web/Core/Common/Views/EditorTemplates/Fields/Common.ItemReferenceContentField.ascx new file mode 100644 index 000000000..ab522e34e --- /dev/null +++ b/src/Orchard.Web/Core/Common/Views/EditorTemplates/Fields/Common.ItemReferenceContentField.ascx @@ -0,0 +1,3 @@ +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +<%@ Import Namespace="Orchard.Core.Common.ViewModels"%> +<%= Html.ItemEditLink(Model.Item) %> \ No newline at end of file diff --git a/src/Orchard.Web/Core/Common/Views/EditorTemplates/Parts/Common.TextContentField.ascx b/src/Orchard.Web/Core/Common/Views/EditorTemplates/Fields/Common.TextContentField.ascx similarity index 100% rename from src/Orchard.Web/Core/Common/Views/EditorTemplates/Parts/Common.TextContentField.ascx rename to src/Orchard.Web/Core/Common/Views/EditorTemplates/Fields/Common.TextContentField.ascx diff --git a/src/Orchard.Web/Core/Contents/Controllers/AdminController.cs b/src/Orchard.Web/Core/Contents/Controllers/AdminController.cs index c2756d441..ef872853b 100644 --- a/src/Orchard.Web/Core/Contents/Controllers/AdminController.cs +++ b/src/Orchard.Web/Core/Contents/Controllers/AdminController.cs @@ -106,28 +106,97 @@ namespace Orchard.Core.Contents.Controllers { return EditType(id); } + var contentTypeDefinitionParts = viewModel.Parts.Select( + p => new ContentTypeDefinition.Part( + new ContentPartDefinition( + p.PartDefinition.Name, + p.PartDefinition.Fields.Select( + f => new ContentPartDefinition.Field( + new ContentFieldDefinition(f.FieldDefinition.Name), + f.Name, + f.Settings + ) + ), + p.PartDefinition.Settings + ), + p.Settings + ) + ).ToList(); + + if (viewModel.Fields.Any()) { + var implicitContentTypeDefinitionPart = new ContentTypeDefinition.Part( + new ContentPartDefinition( + viewModel.Name, + viewModel.Fields.Select( + f => new ContentPartDefinition.Field( + new ContentFieldDefinition(f.FieldDefinition.Name), + f.Name, + f.Settings + ) + ), + null + ), + null + ); + contentTypeDefinitionParts.Add(implicitContentTypeDefinitionPart); + } + //todo: apply the changes along the lines of but definately not resembling // for now this _might_ just get a little messy -> _contentDefinitionService.AlterTypeDefinition( new ContentTypeDefinition( viewModel.Name, viewModel.DisplayName, - viewModel.Parts.Select( - p => new ContentTypeDefinition.Part( - new ContentPartDefinition( - p.PartDefinition.Name, - p.PartDefinition.Fields.Select( - f => new ContentPartDefinition.Field( - new ContentFieldDefinition(f.FieldDefinition.Name), - f.Name, - f.Settings - ) - ), - p.PartDefinition.Settings - ), - p.Settings - ) - ), + contentTypeDefinitionParts, + viewModel.Settings + ) + ); + // little == lot + + return RedirectToAction("Index"); + } + + public ActionResult EditPart(string id) { + if (!Services.Authorizer.Authorize(Permissions.CreateContentTypes, T("Not allowed to edit a part."))) + return new HttpUnauthorizedResult(); + + var contentPartDefinition = _contentDefinitionService.GetPartDefinition(id); + + if (contentPartDefinition == null) + return new NotFoundResult(); + + return View(new EditPartViewModel(contentPartDefinition)); + } + + [HttpPost, ActionName("EditPart")] + public ActionResult EditPartPOST(string id) { + if (!Services.Authorizer.Authorize(Permissions.CreateContentTypes, T("Not allowed to edit a part."))) + return new HttpUnauthorizedResult(); + + var contentPartDefinition = _contentDefinitionService.GetPartDefinition(id); + + if (contentPartDefinition == null) + return new NotFoundResult(); + + var viewModel = new EditPartViewModel(); + TryUpdateModel(viewModel); + + if (!ModelState.IsValid) { + return EditPart(id); + } + + //todo: apply the changes along the lines of but definately not resembling + // for now this _might_ just get a little messy -> + _contentDefinitionService.AlterPartDefinition( + new ContentPartDefinition( + viewModel.Name, + viewModel.Fields.Select( + f => new ContentPartDefinition.Field( + new ContentFieldDefinition(f.FieldDefinition.Name), + f.Name, + f.Settings + ) + ), viewModel.Settings ) ); diff --git a/src/Orchard.Web/Core/Contents/Services/ContentDefinitionService.cs b/src/Orchard.Web/Core/Contents/Services/ContentDefinitionService.cs index 16052287d..b0a78fe48 100644 --- a/src/Orchard.Web/Core/Contents/Services/ContentDefinitionService.cs +++ b/src/Orchard.Web/Core/Contents/Services/ContentDefinitionService.cs @@ -49,12 +49,33 @@ namespace Orchard.Core.Contents.Services { public void AlterTypeDefinition(ContentTypeDefinition contentTypeDefinition) { _contentDefinitionManager.StoreTypeDefinition(contentTypeDefinition); + + var implicitTypePart = contentTypeDefinition.Parts.SingleOrDefault(p => p.PartDefinition.Name == contentTypeDefinition.Name); + if (implicitTypePart != null) { + AlterPartDefinition(implicitTypePart.PartDefinition); + } } public void RemoveTypeDefinition(string name) { throw new NotImplementedException(); } + public ContentPartDefinition GetPartDefinition(string name) { + return _contentDefinitionManager.GetPartDefinition(name); + } + + public void AddPartDefinition(ContentPartDefinition contentPartDefinition) { + throw new NotImplementedException(); + } + + public void AlterPartDefinition(ContentPartDefinition contentPartDefinition) { + _contentDefinitionManager.StorePartDefinition(contentPartDefinition); + } + + public void RemovePartDefinition(string name) { + throw new NotImplementedException(); + } + //gratuitously stolen from the RoutableService private static string GenerateTypeName(string displayName) { if (string.IsNullOrWhiteSpace(displayName)) diff --git a/src/Orchard.Web/Core/Contents/Services/IContentDefinitionService.cs b/src/Orchard.Web/Core/Contents/Services/IContentDefinitionService.cs index 157e53f3a..8840ee23c 100644 --- a/src/Orchard.Web/Core/Contents/Services/IContentDefinitionService.cs +++ b/src/Orchard.Web/Core/Contents/Services/IContentDefinitionService.cs @@ -8,5 +8,10 @@ namespace Orchard.Core.Contents.Services { void AddTypeDefinition(ContentTypeDefinition contentTypeDefinition); void AlterTypeDefinition(ContentTypeDefinition contentTypeDefinition); void RemoveTypeDefinition(string name); + + ContentPartDefinition GetPartDefinition(string name); + void AddPartDefinition(ContentPartDefinition contentPartDefinition); + void AlterPartDefinition(ContentPartDefinition contentPartDefinition); + void RemovePartDefinition(string name); } } \ No newline at end of file diff --git a/src/Orchard.Web/Core/Contents/Styles/admin.css b/src/Orchard.Web/Core/Contents/Styles/admin.css index d6614bb1f..ba113a16e 100644 --- a/src/Orchard.Web/Core/Contents/Styles/admin.css +++ b/src/Orchard.Web/Core/Contents/Styles/admin.css @@ -3,4 +3,44 @@ } .manage.add-to-type { margin-top:-4em; +} + +.manage-part h3, +.manage-field h3 { + border-bottom:1px solid #EAEAEA; +} +.manage-part .manage, +.manage-field .manage { + font-size:1.4em; + margin-top:-2.4em; +} +.manage-part .manage.minor { + margin-top:-1.7em; +} +.manage-part label, +.manage-field label { + font-weight:normal; +} + +/* should pull this back into the base admin theme css, w/out the .manage-part of course */ +.manage-part dl { + margin:0 0 1em; + overflow:auto; + padding:6px 0 0; +} +.manage-part dt { + font-weight:bold; +} +.manage-part dl dl { + font-size:1em; + margin:0; + padding:0; +} +.manage-part dd dt { + clear:left; + float:left; +} +.manage-part dd dd { + float:left; + padding-left:.5em; } \ No newline at end of file diff --git a/src/Orchard.Web/Core/Contents/ViewModels/EditTypeViewModel.cs b/src/Orchard.Web/Core/Contents/ViewModels/EditTypeViewModel.cs index b43cc113b..20bbb4772 100644 --- a/src/Orchard.Web/Core/Contents/ViewModels/EditTypeViewModel.cs +++ b/src/Orchard.Web/Core/Contents/ViewModels/EditTypeViewModel.cs @@ -13,13 +13,27 @@ namespace Orchard.Core.Contents.ViewModels { Name = contentTypeDefinition.Name; DisplayName = contentTypeDefinition.DisplayName; Settings = contentTypeDefinition.Settings; - Parts = contentTypeDefinition.Parts.Select(p => new EditTypePartViewModel(p)); + Fields = GetTypeFields(contentTypeDefinition); + Parts = GetTypeParts(contentTypeDefinition); } public string Name { get; set; } public string DisplayName { get; set; } public SettingsDictionary Settings { get; set; } + public IEnumerable Fields { get; set; } public IEnumerable Parts { get; set; } + + private IEnumerable GetTypeFields(ContentTypeDefinition contentTypeDefinition) { + var implicitTypePart = contentTypeDefinition.Parts.SingleOrDefault(p => p.PartDefinition.Name == Name); + + return implicitTypePart == null + ? Enumerable.Empty() + : implicitTypePart.PartDefinition.Fields.Select(f => new EditPartFieldViewModel(f)); + } + + private IEnumerable GetTypeParts(ContentTypeDefinition contentTypeDefinition) { + return contentTypeDefinition.Parts.Where(p => p.PartDefinition.Name != Name).Select(p => new EditTypePartViewModel(p)); + } } public class EditTypePartViewModel { @@ -35,7 +49,7 @@ namespace Orchard.Core.Contents.ViewModels { public SettingsDictionary Settings { get; set; } } - public class EditPartViewModel { + public class EditPartViewModel : BaseViewModel { public EditPartViewModel() { Fields = new List(); Settings = new SettingsDictionary(); @@ -69,7 +83,7 @@ namespace Orchard.Core.Contents.ViewModels { public class EditFieldViewModel { public EditFieldViewModel() { } public EditFieldViewModel(ContentFieldDefinition contentFieldDefinition) { - Name = Name; + Name = contentFieldDefinition.Name; } public string Name { get; set; } diff --git a/src/Orchard.Web/Core/Contents/Views/Admin/EditPart.ascx b/src/Orchard.Web/Core/Contents/Views/Admin/EditPart.ascx new file mode 100644 index 000000000..78589bdc0 --- /dev/null +++ b/src/Orchard.Web/Core/Contents/Views/Admin/EditPart.ascx @@ -0,0 +1,19 @@ +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +<%@ Import Namespace="Orchard.Core.Contents.ViewModels" %><% +Html.RegisterStyle("admin.css"); %> +

<%:Html.TitleForPage(T("Edit Part").ToString())%>

<% +using (Html.BeginFormAntiForgeryPost()) { %> + <%:Html.ValidationSummary() %> +
+ + <%--// has unintended consequences (renamging the part) - changing the name creates a new part of that name--%> + <%:Html.TextBoxFor(m => m.Name, new {@class = "textMedium"}) %> +
+ <%:Html.EditorFor(m => m.Settings, "Settings", "")%> +

<%:T("Fields") %>

+
<%: Html.ActionLink(T("Add").Text, "AddField", new { }, new { @class = "button" })%>
+ <%:Html.EditorFor(m => m.Fields, "Fields", "") %> +
+ +
<% +} %> diff --git a/src/Orchard.Web/Core/Contents/Views/Admin/EditType.ascx b/src/Orchard.Web/Core/Contents/Views/Admin/EditType.ascx index ffce68bcf..86c390021 100644 --- a/src/Orchard.Web/Core/Contents/Views/Admin/EditType.ascx +++ b/src/Orchard.Web/Core/Contents/Views/Admin/EditType.ascx @@ -7,8 +7,10 @@ using (Html.BeginFormAntiForgeryPost()) { %>
<%:Html.TextBoxFor(m => m.DisplayName, new {@class = "textMedium"}) %> + <%--// has unintended consequences (renamging the type) - changing the name creates a new type of that name--%> - <%:Html.TextBoxFor(m => m.Name, new {@class = "textMedium"}) %> + <%:Html.TextBoxFor(m => m.Name, new {@class = "textMedium", disabled = "disabled"}) %> + <%:Html.HiddenFor(m => m.Name) %>
<%:Html.EditorFor(m => m.Settings) %>

<%:T("Parts") %>

@@ -16,7 +18,7 @@ using (Html.BeginFormAntiForgeryPost()) { %> <%:Html.EditorFor(m => m.Parts, "Parts", "") %>

<%:T("Fields") %>

<%: Html.ActionLink(T("Add").Text, "AddField", new { }, new { @class = "button" })%>
- <%--<%:Html.EditorFor(m => m.Fields, "ContentTypeFields")%>--%> + <%:Html.EditorFor(m => m.Fields, "Fields", "")%>
<% diff --git a/src/Orchard.Web/Core/Contents/Views/Admin/Types.ascx b/src/Orchard.Web/Core/Contents/Views/Admin/Types.ascx index 027fa19ee..c6fe6cc60 100644 --- a/src/Orchard.Web/Core/Contents/Views/Admin/Types.ascx +++ b/src/Orchard.Web/Core/Contents/Views/Admin/Types.ascx @@ -2,7 +2,7 @@ <%@ Import Namespace="Orchard.Core.Contents.ViewModels" %>

<%:Html.TitleForPage(T("Content Types").ToString())%>

<%: Html.ActionLink(T("Create new type").ToString(), "CreateType", null, new { @class = "button primaryAction" })%>
-<%=Html.UnorderedList( +<%:Html.UnorderedList( Model.Types, (t,i) => Html.DisplayFor(m => t), "contentItems" diff --git a/src/Orchard.Web/Core/Contents/Views/DisplayTemplates/ContentTypeDefinition.ascx b/src/Orchard.Web/Core/Contents/Views/DisplayTemplates/ContentTypeDefinition.ascx index 7e6b634ae..15dfd2ca4 100644 --- a/src/Orchard.Web/Core/Contents/Views/DisplayTemplates/ContentTypeDefinition.ascx +++ b/src/Orchard.Web/Core/Contents/Views/DisplayTemplates/ContentTypeDefinition.ascx @@ -1,4 +1,5 @@ -<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +<%@ Import namespace="Orchard.ContentManagement.MetaData.Models" %>

<%:Model.DisplayName%>

diff --git a/src/Orchard.Web/Core/Contents/Views/DisplayTemplates/Settings.ascx b/src/Orchard.Web/Core/Contents/Views/DisplayTemplates/Settings.ascx new file mode 100644 index 000000000..24d4536cd --- /dev/null +++ b/src/Orchard.Web/Core/Contents/Views/DisplayTemplates/Settings.ascx @@ -0,0 +1,8 @@ +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +<%@ import Namespace="Orchard.ContentManagement.MetaData.Models" %> +
<% + foreach (var setting in Model) { %> +
<%:setting.Key %>
+
<%:setting.Value %>
<% + } %> +
\ No newline at end of file diff --git a/src/Orchard.Web/Core/Contents/Views/EditorTemplates/FieldOnPart.ascx b/src/Orchard.Web/Core/Contents/Views/EditorTemplates/Field.ascx similarity index 60% rename from src/Orchard.Web/Core/Contents/Views/EditorTemplates/FieldOnPart.ascx rename to src/Orchard.Web/Core/Contents/Views/EditorTemplates/Field.ascx index 24ad5a714..4ca058cba 100644 --- a/src/Orchard.Web/Core/Contents/Views/EditorTemplates/FieldOnPart.ascx +++ b/src/Orchard.Web/Core/Contents/Views/EditorTemplates/Field.ascx @@ -1,14 +1,17 @@ -<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> -<%@ Import Namespace="Orchard.ContentManagement.MetaData.Models" %> -
-

<%:Model.FieldDefinition.Name %>

-
+<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +<%@ Import Namespace="Orchard.Core.Contents.ViewModels" %> +
+

<%:Model.Name %> (<%:Model.FieldDefinition.Name %>)

+
<%--// these inline forms can't be here. should probably have some JavaScript in here to build up the forms and add the "remove" link. // get the antiforgery token from the edit type form and mark up the part in a semantic way so I can get some info from the DOM --%> <%:Html.Link("[remove]", "#forshowonlyandnotintendedtowork!") %> <%-- <% using (Html.BeginFormAntiForgeryPost(Url.Action("RemovePart", new { area = "Contents" }), FormMethod.Post, new {@class = "inline link"})) { %> - <%=Html.Hidden("name", Model.PartDefinition.Name, new { id = "" }) %> + <%:Html.Hidden("name", Model.PartDefinition.Name, new { id = "" }) %> <% } %> --%>
+ <%:Html.EditorFor(m => m.Settings, "Settings", "") %> + <%:Html.HiddenFor(m => m.Name) %> + <%:Html.HiddenFor(m => m.FieldDefinition.Name) %>
\ No newline at end of file diff --git a/src/Orchard.Web/Core/Contents/Views/EditorTemplates/Fields.ascx b/src/Orchard.Web/Core/Contents/Views/EditorTemplates/Fields.ascx new file mode 100644 index 000000000..c22a4f3bb --- /dev/null +++ b/src/Orchard.Web/Core/Contents/Views/EditorTemplates/Fields.ascx @@ -0,0 +1,12 @@ +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl>" %> +<%@ Import Namespace="Orchard.Core.Contents.ViewModels" %><% +if (Model.Any()) { %> +
<% + var fi = 0; + foreach (var field in Model) { + var f = field; + var htmlFieldName = string.Format("Fields[{0}]", fi++); %> + <%:Html.EditorFor(m => f, "Field", htmlFieldName) %><% + } %> +
<% +} %> \ No newline at end of file diff --git a/src/Orchard.Web/Core/Contents/Views/EditorTemplates/Part.Field.ascx b/src/Orchard.Web/Core/Contents/Views/EditorTemplates/Part.Field.ascx new file mode 100644 index 000000000..08660099b --- /dev/null +++ b/src/Orchard.Web/Core/Contents/Views/EditorTemplates/Part.Field.ascx @@ -0,0 +1,6 @@ +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +<%@ Import Namespace="Orchard.Core.Contents.ViewModels" %> +
<%:Model.Name %> (<%:Model.FieldDefinition.Name %>)
+
+ <%:Html.DisplayFor(m => m.Settings, "Settings", "") %> +
\ No newline at end of file diff --git a/src/Orchard.Web/Core/Contents/Views/EditorTemplates/FieldsOnPart.ascx b/src/Orchard.Web/Core/Contents/Views/EditorTemplates/Part.Fields.ascx similarity index 73% rename from src/Orchard.Web/Core/Contents/Views/EditorTemplates/FieldsOnPart.ascx rename to src/Orchard.Web/Core/Contents/Views/EditorTemplates/Part.Fields.ascx index 8b45f2edc..c93ce4f70 100644 --- a/src/Orchard.Web/Core/Contents/Views/EditorTemplates/FieldsOnPart.ascx +++ b/src/Orchard.Web/Core/Contents/Views/EditorTemplates/Part.Fields.ascx @@ -1,10 +1,10 @@ <%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl>" %> <%@ Import Namespace="Orchard.Core.Contents.ViewModels" %><% if (Model.Any()) { %> -
<% +
<% foreach (var field in Model) { var f = field; %> - <%:Html.EditorFor(m => f, "FieldOnPart") %><% + <%:Html.EditorFor(m => f, "Part.Field") %><% } %> -
<% + <% } %> \ No newline at end of file diff --git a/src/Orchard.Web/Core/Contents/Views/EditorTemplates/Part.ascx b/src/Orchard.Web/Core/Contents/Views/EditorTemplates/Part.ascx index b282d485e..214c22b7c 100644 --- a/src/Orchard.Web/Core/Contents/Views/EditorTemplates/Part.ascx +++ b/src/Orchard.Web/Core/Contents/Views/EditorTemplates/Part.ascx @@ -1,8 +1,8 @@ <%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> <%@ Import Namespace="Orchard.Core.Contents.ViewModels" %> -
+

<%:Model.PartDefinition.Name %>

-
+
<%--// these inline forms can't be here. should probably have some JavaScript in here to build up the forms and add the "remove" link. // get the antiforgery token from the edit type form and mark up the part in a semantic way so I can get some info from the DOM --%> <%:Html.Link("[remove]", "#forshowonlyandnotintendedtowork") %> @@ -11,11 +11,12 @@ <% } %> --%>
- <%-- - what is this settings for? - <%:Html.EditorFor(m => m.PartDefinition.Settings, "Settings") %>--%> - <%:Html.EditorFor(m => m.Settings, "Settings", "") %> - <%:Html.EditorFor(m => m.PartDefinition.Fields, "FieldsOnPart") %> - <%:Html.Hidden("PartDefinition.Name", Model.PartDefinition.Name) %> + <%:Html.EditorFor(m => m.Settings, "Settings", "") %><% + if (Model.PartDefinition.Settings.Any() || Model.PartDefinition.Fields.Any()) { %> +

<%:T("Global configuration") %>

+
<%:Html.ActionLink(T("Edit").Text, "EditPart", new { area = "Contents", id = Model.PartDefinition.Name }) %>
+ <%:Html.DisplayFor(m => m.PartDefinition.Settings, "Settings", "PartDefinition") %> + <%:Html.EditorFor(m => m.PartDefinition.Fields, "Part.Fields") %><% + } %> <%:Html.Hidden("PartDefinition.Name", Model.PartDefinition.Name) %>
\ No newline at end of file diff --git a/src/Orchard.Web/Core/Contents/Views/EditorTemplates/Settings.ascx b/src/Orchard.Web/Core/Contents/Views/EditorTemplates/Settings.ascx index 75432c61a..5a0f59533 100644 --- a/src/Orchard.Web/Core/Contents/Views/EditorTemplates/Settings.ascx +++ b/src/Orchard.Web/Core/Contents/Views/EditorTemplates/Settings.ascx @@ -1,4 +1,5 @@ -<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %><% +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +<%@ import Namespace="Orchard.ContentManagement.MetaData.Models" %><% if (Model.Any()) { %>
<% var si = 0; diff --git a/src/Orchard.Web/Core/Orchard.Core.csproj b/src/Orchard.Web/Core/Orchard.Core.csproj index ca9327177..566bc1913 100644 --- a/src/Orchard.Web/Core/Orchard.Core.csproj +++ b/src/Orchard.Web/Core/Orchard.Core.csproj @@ -208,6 +208,7 @@ + @@ -217,9 +218,12 @@ + + + - - + + diff --git a/src/Orchard.Web/Modules/Orchard.DevTools/Commands/ProfilingCommands.cs b/src/Orchard.Web/Modules/Orchard.DevTools/Commands/ProfilingCommands.cs index 6635ab22b..aae71f2d8 100644 --- a/src/Orchard.Web/Modules/Orchard.DevTools/Commands/ProfilingCommands.cs +++ b/src/Orchard.Web/Modules/Orchard.DevTools/Commands/ProfilingCommands.cs @@ -3,9 +3,11 @@ using Orchard.ContentManagement; using Orchard.ContentManagement.Aspects; using Orchard.Core.Common.Models; using Orchard.Core.Navigation.Models; +using Orchard.Environment.Extensions; using Orchard.Security; namespace Orchard.DevTools.Commands { + [OrchardFeature("Profiling")] public class ProfilingCommands : DefaultOrchardCommandHandler { private readonly IContentManager _contentManager; private readonly IMembershipService _membershipService; diff --git a/src/Orchard.Web/Modules/Orchard.DevTools/Commands/ScaffoldingCommands.cs b/src/Orchard.Web/Modules/Orchard.DevTools/Commands/ScaffoldingCommands.cs new file mode 100644 index 000000000..92c051d12 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.DevTools/Commands/ScaffoldingCommands.cs @@ -0,0 +1,8 @@ +using Orchard.Commands; +using Orchard.Environment.Extensions; + +namespace Orchard.DevTools.Commands { + [OrchardFeature("Scaffolding")] + public class ScaffoldingCommands : DefaultOrchardCommandHandler { + } +} diff --git a/src/Orchard.Web/Modules/Orchard.DevTools/Module.txt b/src/Orchard.Web/Modules/Orchard.DevTools/Module.txt index 40cb75a5e..1aeb0b2b3 100644 --- a/src/Orchard.Web/Modules/Orchard.DevTools/Module.txt +++ b/src/Orchard.Web/Modules/Orchard.DevTools/Module.txt @@ -7,5 +7,13 @@ orchardversion: 0.1.2010.0312 description: This module is not activated by default and should only be used in a development environment. It contains various debugging and tracing tools that can display information about your content types. features: Orchard.DevTools: - Description: An assortment of debuging tools. - Category: Developer \ No newline at end of file + Description: An assortment of debugging tools. + Category: Developer + Scaffolding: + Description: Tools to create Orchard components. + Category: Developer + Dependencies: Orchard.DevTools + Profiling: + Description: Tools to help profile Orchard. + Category: Developer + Dependencies: Orchard.DevTools \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.DevTools/Orchard.DevTools.csproj b/src/Orchard.Web/Modules/Orchard.DevTools/Orchard.DevTools.csproj index fd88a2bd9..8ed2402e5 100644 --- a/src/Orchard.Web/Modules/Orchard.DevTools/Orchard.DevTools.csproj +++ b/src/Orchard.Web/Modules/Orchard.DevTools/Orchard.DevTools.csproj @@ -71,6 +71,7 @@ + @@ -85,6 +86,11 @@ + + + + + @@ -106,6 +112,7 @@ Orchard.Core + + + + + $(ProjectDir)\..\Manifests + + + + + + + + + + + + False + True + 45979 + / + + + False + True + http://orchard.codeplex.com + False + + + + + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.DevTools/ScaffoldingTemplates/ModuleManifest.txt b/src/Orchard.Web/Modules/Orchard.DevTools/ScaffoldingTemplates/ModuleManifest.txt new file mode 100644 index 000000000..7f7d0b295 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.DevTools/ScaffoldingTemplates/ModuleManifest.txt @@ -0,0 +1,5 @@ +name: $$ModuleName$$ +antiforgery: enabled +features: + Orchard.$$ModuleName$$: + Description: Description for feature $$ModuleName$$. \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.DevTools/ScaffoldingTemplates/ModuleWebConfig.txt b/src/Orchard.Web/Modules/Orchard.DevTools/ScaffoldingTemplates/ModuleWebConfig.txt new file mode 100644 index 000000000..9445e4ba4 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.DevTools/ScaffoldingTemplates/ModuleWebConfig.txt @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Themes/TheAdmin/Styles/site.css b/src/Orchard.Web/Themes/TheAdmin/Styles/site.css index 19d122b0d..a106bbd64 100644 --- a/src/Orchard.Web/Themes/TheAdmin/Styles/site.css +++ b/src/Orchard.Web/Themes/TheAdmin/Styles/site.css @@ -147,10 +147,11 @@ h1 { font-size:2.6em; } /* 26px */ h2 { font-size:2.1em; } /* 21px */ h2 span { font-size:.57em; } /* 12px */ h3 { font-size:1.8em; } /* 18px */ +h3 span { font-size:.667em; } /* 12px */ h4 { font-size:1.6em; } /* 16px */ h5 { font-size:1.4em; } /* 14px */ -h6, p, label, /*input, select,*/ .button, +h6, p, dl, label, /*input, select,*/ .button, .message, .validation-summary-errors, table.items th, table.items td, table.items caption { font-size:1.4em; line-height:1.4em; } /* 14px */ table.items p, table.items label, table.items input, table.items .button { font-size:1em; line-height:1em; } @@ -510,7 +511,6 @@ button.remove:focus::-moz-focus-inner, .remove.button:focus::-moz-focus-inner { .manage { float:right; margin:0 0 10px 8px; - overflow:hidden; } .actions { clear:right; diff --git a/src/Orchard/DataMigration/Commands/DataMigrationCommands.cs b/src/Orchard/DataMigration/Commands/DataMigrationCommands.cs index 4faca9604..2beb4a9d9 100644 --- a/src/Orchard/DataMigration/Commands/DataMigrationCommands.cs +++ b/src/Orchard/DataMigration/Commands/DataMigrationCommands.cs @@ -18,7 +18,7 @@ namespace Orchard.DataMigration.Commands { [OrchardSwitches("Feature")] public string UpgradeDatabase() { try { - _dataMigrationManager.Upgrade(Feature); + _dataMigrationManager.Update(Feature); } catch ( Exception ex ) { Context.Output.WriteLine(T("An error occured while upgrading the database: " + ex.Message)); diff --git a/src/Orchard/DataMigration/DataMigration.cs b/src/Orchard/DataMigration/DataMigration.cs index 469ec6556..8ae3769a7 100644 --- a/src/Orchard/DataMigration/DataMigration.cs +++ b/src/Orchard/DataMigration/DataMigration.cs @@ -1,5 +1,11 @@ -namespace Orchard.DataMigration { - public abstract class DataMigration : IDataMigration { +using Orchard.DataMigration.Schema; + +namespace Orchard.DataMigration { + /// + /// Data Migration classes can inherit from this class to get a SchemaBuilder instance configured with the current tenant database prefix + /// + public abstract class DataMigrationImpl : IDataMigration { public abstract string Feature { get; } + public SchemaBuilder SchemaBuilder { get; set; } } } diff --git a/src/Orchard/DataMigration/DataMigrationCoordinator.cs b/src/Orchard/DataMigration/DataMigrationCoordinator.cs new file mode 100644 index 000000000..f34954491 --- /dev/null +++ b/src/Orchard/DataMigration/DataMigrationCoordinator.cs @@ -0,0 +1,40 @@ +using Orchard.Environment; +using Orchard.Environment.Extensions.Models; + +namespace Orchard.DataMigration { + /// + /// Responsible for executing data migration tasks when a feature is enabled for the first time + /// 1) Initial install of a module: + /// Enable a feature for the first time => run data migration up to the latest version + /// Enable a feature on 2nd time => no data migration run + /// Disable a feature => no data migration + /// 2) Installing a newer version of a module + /// Don't do any data migration by default + /// 2 cases: + /// 1) feature wasn't not enabled when new code was installed + /// 2) feature was enabled when new code was installed + /// + public class DataMigrationCoordinator : IFeatureEventHandler { + private readonly IDataMigrationManager _dataMigrationManager; + + public DataMigrationCoordinator(IDataMigrationManager dataMigrationManager) { + _dataMigrationManager = dataMigrationManager; + } + + public void Install(Feature feature) { + var featureName = feature.Descriptor.Name; + if ( !_dataMigrationManager.IsFeatureAlreadyInstalled(featureName) ) { + _dataMigrationManager.Update(featureName); + } + } + + public void Enable(Feature feature) { + } + + public void Disable(Feature feature) { + } + + public void Uninstall(Feature feature) { + } + } +} diff --git a/src/Orchard/DataMigration/DataMigrationManager.cs b/src/Orchard/DataMigration/DataMigrationManager.cs index 035c543de..8d1a66e0c 100644 --- a/src/Orchard/DataMigration/DataMigrationManager.cs +++ b/src/Orchard/DataMigration/DataMigrationManager.cs @@ -4,8 +4,10 @@ using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using Orchard.Data; +using Orchard.DataMigration.Schema; +using Orchard.Environment.Configuration; using Orchard.Environment.Extensions; -using Orchard.Environment.Extensions.Models; +using Orchard.Environment.State; using Orchard.Logging; namespace Orchard.DataMigration { @@ -17,34 +19,77 @@ namespace Orchard.DataMigration { private readonly IRepository _dataMigrationRepository; private readonly IDataMigrationGenerator _dataMigrationGenerator; private readonly IExtensionManager _extensionManager; + private readonly ShellSettings _shellSettings; public DataMigrationManager( IEnumerable dataMigrations, IRepository dataMigrationRepository, IDataMigrationGenerator dataMigrationGenerator, - IExtensionManager extensionManager) { + IExtensionManager extensionManager, + ShellSettings shellSettings) { _dataMigrations = dataMigrations; _dataMigrationRepository = dataMigrationRepository; _dataMigrationGenerator = dataMigrationGenerator; _extensionManager = extensionManager; + _shellSettings = shellSettings; Logger = NullLogger.Instance; } public ILogger Logger { get; set; } - public void Upgrade(string feature) { + public IEnumerable GetFeaturesThatNeedUpdate() { + var features = new List(); + + // compare current version and available migration methods for each migration class + foreach ( var dataMigration in _dataMigrations ) { + + // get current version for this migration + var dataMigrationRecord = GetDataMigrationRecord(dataMigration); + + var current = 0; + if (dataMigrationRecord != null) { + current = dataMigrationRecord.Current; + } + + // do we need to call Create() ? + if (current == 0) { + + // try to resolve a Create method + if ( GetCreateMethod(dataMigration) != null ) { + features.Add(dataMigration.Feature); + continue; + } + } + + var lookupTable = CreateUpgradeLookupTable(dataMigration); + + if(lookupTable.ContainsKey(current)) { + features.Add(dataMigration.Feature); + } + } + + return features; + } + + public void Update(IEnumerable features) { + foreach(var feature in features) { + Update(feature); + } + } + + public void Update(string feature){ + // proceed with dependent features first, whatever the module it's in - var dependencies = _extensionManager - .AvailableExtensions() - .SelectMany(e => e.Features) + var dependencies = ShellStateCoordinator.OrderByDependencies(_extensionManager.AvailableExtensions() + .SelectMany(ext => ext.Features)) .Where(f => String.Equals(f.Name, feature, StringComparison.OrdinalIgnoreCase)) .Where(f => f.Dependencies != null) .SelectMany( f => f.Dependencies ) .ToList(); foreach(var dependency in dependencies) { - Upgrade(dependency); + Update(dependency); } var migrations = GetDataMigrations(feature); @@ -55,11 +100,8 @@ namespace Orchard.DataMigration { var tempMigration = migration; // get current version for this migration - var dataMigrationRecord = _dataMigrationRepository.Table - .Where(dm => dm.DataMigrationClass == tempMigration.GetType().FullName) - .FirstOrDefault(); + var dataMigrationRecord = GetDataMigrationRecord(tempMigration); - var updateMethodNameRegex = new Regex(@"^UpdateFrom(?\d+)$", RegexOptions.Compiled); var current = 0; if(dataMigrationRecord != null) { current = dataMigrationRecord.Current; @@ -69,8 +111,8 @@ namespace Orchard.DataMigration { if(current == 0) { // try to resolve a Create method - var createMethod = migration.GetType().GetMethod("Create", BindingFlags.Public | BindingFlags.Instance); - if(createMethod != null && createMethod.ReturnType == typeof(int)) { + var createMethod = GetCreateMethod(migration); + if(createMethod != null) { current = (int)createMethod.Invoke(migration, new object[0]); } else { @@ -79,15 +121,7 @@ namespace Orchard.DataMigration { } } - // update methods might also be called after Create() - var lookupTable = new Dictionary(); - // construct a lookup table with all managed initial versions - foreach(var methodInfo in migration.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)) { - var match = updateMethodNameRegex.Match(methodInfo.Name); - if(match.Success) { - lookupTable.Add(int.Parse(match.Groups["version"].Value), methodInfo); - } - } + var lookupTable = CreateUpgradeLookupTable(migration); while(lookupTable.ContainsKey(current)) { try { @@ -96,29 +130,82 @@ namespace Orchard.DataMigration { } catch (Exception ex) { Logger.Error(ex, "An unexpected error orccured while applying migration on {0} from version {1}", feature, current); + throw; } } // if current is 0, it means no upgrade/create method was found or succeeded - if ( current != 0 ) { - if (dataMigrationRecord == null) { - _dataMigrationRepository.Create(new DataMigrationRecord {Current = current, DataMigrationClass = migration.GetType().FullName}); - } - else { - dataMigrationRecord.Current = current; - _dataMigrationRepository.Update(dataMigrationRecord); - } + if (current == 0) { + continue; + } + if (dataMigrationRecord == null) { + _dataMigrationRepository.Create(new DataMigrationRecord {Current = current, DataMigrationClass = migration.GetType().FullName}); + } + else { + dataMigrationRecord.Current = current; + _dataMigrationRepository.Update(dataMigrationRecord); } } } + private DataMigrationRecord GetDataMigrationRecord(IDataMigration tempMigration) { + return _dataMigrationRepository.Table + .Where(dm => dm.DataMigrationClass == tempMigration.GetType().FullName) + .FirstOrDefault(); + } + /// - /// Returns all the available IDataMigration instances for a specific module + /// Returns all the available IDataMigration instances for a specific module, and inject necessary builders /// - public IEnumerable GetDataMigrations(string feature) { - return _dataMigrations + private IEnumerable GetDataMigrations(string feature) { + var migrations = _dataMigrations .Where(dm => String.Equals(dm.Feature, feature, StringComparison.OrdinalIgnoreCase)) .ToList(); + + foreach (var migration in migrations.OfType()) { + migration.SchemaBuilder = new SchemaBuilder(_shellSettings.DataTablePrefix); + } + + return migrations; + } + + /// + /// Whether a feature has already been installed, i.e. one of its Data Migration class has already been processed + /// + public bool IsFeatureAlreadyInstalled(string feature) { + return GetDataMigrations(feature).Any(dataMigration => GetDataMigrationRecord(dataMigration) != null); + } + + /// + /// Create a list of all available Update methods from a data migration class, indexed by the version number + /// + private static Dictionary CreateUpgradeLookupTable(IDataMigration dataMigration) { + var updateMethodNameRegex = new Regex(@"^UpdateFrom(?\d+)$", RegexOptions.Compiled); + + // update methods might also be called after Create() + var lookupTable = new Dictionary(); + + // construct a lookup table with all managed initial versions + foreach ( var methodInfo in dataMigration.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance) ) { + var match = updateMethodNameRegex.Match(methodInfo.Name); + if ( match.Success ) { + lookupTable.Add(int.Parse(match.Groups["version"].Value), methodInfo); + } + } + + return lookupTable; + } + + /// + /// Returns the Create metho from a data migration class if it's found + /// + private static MethodInfo GetCreateMethod(IDataMigration dataMigration) { + var methodInfo = dataMigration.GetType().GetMethod("Create", BindingFlags.Public | BindingFlags.Instance); + if(methodInfo != null && methodInfo.ReturnType == typeof(int)) { + return methodInfo; + } + + return null; } } } diff --git a/src/Orchard/DataMigration/IDataMigrationManager.cs b/src/Orchard/DataMigration/IDataMigrationManager.cs index bf7032f67..7f887e728 100644 --- a/src/Orchard/DataMigration/IDataMigrationManager.cs +++ b/src/Orchard/DataMigration/IDataMigrationManager.cs @@ -1,5 +1,25 @@ +using System.Collections.Generic; + namespace Orchard.DataMigration { public interface IDataMigrationManager : IDependency { - void Upgrade(string feature); + /// + /// Whether a feature has already been installed, i.e. one of its Data Migration class has already been processed + /// + bool IsFeatureAlreadyInstalled(string feature); + + /// + /// Returns the features which have at least one Data Migration class with a corresponding Upgrade method to be called + /// + IEnumerable GetFeaturesThatNeedUpdate(); + + /// + /// Updates the database to the latest version for the specified feature + /// + void Update(string feature); + + /// + /// Updates the database to the latest version for the specified features + /// + void Update(IEnumerable features); } } \ No newline at end of file diff --git a/src/Orchard/DataMigration/Schema/AlterColumnCommand.cs b/src/Orchard/DataMigration/Schema/AlterColumnCommand.cs new file mode 100644 index 000000000..c3d88884a --- /dev/null +++ b/src/Orchard/DataMigration/Schema/AlterColumnCommand.cs @@ -0,0 +1,15 @@ +namespace Orchard.DataMigration.Schema { + public class AlterColumnCommand : ColumnCommand { + private string _newName; + + public AlterColumnCommand(string name) + : base(name) { + } + + + public AlterColumnCommand Rename(string name) { + _newName = name; + return this; + } + } +} diff --git a/src/Orchard/DataMigration/Schema/AlterTableCommand.cs b/src/Orchard/DataMigration/Schema/AlterTableCommand.cs new file mode 100644 index 000000000..2fee79722 --- /dev/null +++ b/src/Orchard/DataMigration/Schema/AlterTableCommand.cs @@ -0,0 +1,64 @@ +using System; +using System.Data; + +namespace Orchard.DataMigration.Schema { + public class AlterTableCommand : SchemaCommand { + public AlterTableCommand(string name) + : base(name) { + } + + public AlterTableCommand AddColumn(string name, DbType dbType, Action column = null) { + var command = new CreateColumnCommand(name); + command.Type(dbType); + + if(column != null) { + column(command); + } + + _tableCommands.Add(command); + return this; + } + + public AlterTableCommand DropColumn(string name) { + var command = new DropColumnCommand(name); + _tableCommands.Add(command); + return this; + } + + public AlterTableCommand AlterColumn(string name, Action column = null) { + var command = new AlterColumnCommand(name); + + if ( column != null ) { + column(command); + } + + _tableCommands.Add(command); + return this; + } + + public AlterTableCommand CreateIndex(string name, params string[] columnNames) { + var command = new CreateIndexCommand(name, columnNames); + _tableCommands.Add(command); + return this; + } + + public AlterTableCommand DropIndex(string name) { + var command = new DropIndexCommand(name); + _tableCommands.Add(command); + return this; + } + + public AlterTableCommand AddForeignKey(string name, Action fk) { + var command = new CreateForeignKeyCommand(name); + fk(command); + _tableCommands.Add(command); + return this; + } + + public AlterTableCommand DropForeignKey(string name) { + var command = new DropForeignKeyCommand(name); + _tableCommands.Add(command); + return this; + } + } +} diff --git a/src/Orchard/DataMigration/Schema/ColumnCommand.cs b/src/Orchard/DataMigration/Schema/ColumnCommand.cs new file mode 100644 index 000000000..46e9f32a2 --- /dev/null +++ b/src/Orchard/DataMigration/Schema/ColumnCommand.cs @@ -0,0 +1,23 @@ +using System.Data; + +namespace Orchard.DataMigration.Schema { + public class ColumnCommand : TableCommand { + private DbType _dbType; + private object _default; + public ColumnCommand(string name) : base(name) { + _dbType = DbType.Object; + _default = null; + } + + public ColumnCommand Type(DbType dbType) { + _dbType = dbType; + return this; + } + + public ColumnCommand Default(object @default) { + _default = @default; + return this; + } + + } +} diff --git a/src/Orchard/DataMigration/Schema/CreateColumnCommand.cs b/src/Orchard/DataMigration/Schema/CreateColumnCommand.cs new file mode 100644 index 000000000..86bc1aef6 --- /dev/null +++ b/src/Orchard/DataMigration/Schema/CreateColumnCommand.cs @@ -0,0 +1,60 @@ +namespace Orchard.DataMigration.Schema { + public class CreateColumnCommand : ColumnCommand { + private bool _primaryKey; + private byte? _precision; + private byte? _scale; + private int? _length; + private bool _notNull; + private bool _unique; + + + public CreateColumnCommand(string name) : base(name) { + _precision = null; + _scale = null; + _length = null; + _notNull = false; + _unique = false; + + } + + public CreateColumnCommand PrimaryKey() { + _primaryKey = true; + return this; + } + + public CreateColumnCommand Precision(byte? precision) { + _precision = precision; + return this; + } + + public CreateColumnCommand Scale(byte? scale) { + _scale = scale; + return this; + } + + public CreateColumnCommand Length(int? length) { + _length = length; + return this; + } + + public CreateColumnCommand NotNull() { + _notNull = true; + return this; + } + + public CreateColumnCommand Nullable() { + _notNull = false; + return this; + } + + public CreateColumnCommand Unique() { + _unique = true; + return this; + } + + public CreateColumnCommand NotUnique() { + _unique = false; + return this; + } + } +} diff --git a/src/Orchard/DataMigration/Schema/CreateForeignKeyCommand.cs b/src/Orchard/DataMigration/Schema/CreateForeignKeyCommand.cs new file mode 100644 index 000000000..0fe485ee5 --- /dev/null +++ b/src/Orchard/DataMigration/Schema/CreateForeignKeyCommand.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace Orchard.DataMigration.Schema { + public class CreateForeignKeyCommand : TableCommand { + protected readonly List _foreignKeyClauses; + + public CreateForeignKeyCommand(string name) + : base(name) { + _foreignKeyClauses = new List(); + } + + public CreateForeignKeyCommand On(string srcColumn, string destTable, string destColumn) { + _foreignKeyClauses.Add(new ForeignKeyClause(srcColumn, destTable, destColumn)); + return this; + } + } + + public class ForeignKeyClause { + public ForeignKeyClause(string srcColumn, string destTable, string destColumn) { + SrcColumn = srcColumn; + DestTable = destTable; + DestColumn = destColumn; + } + + public string DestColumn { get; private set; } + + public string DestTable { get; private set; } + + public string SrcColumn { get; private set; } + } +} diff --git a/src/Orchard/DataMigration/Schema/CreateIndexCommand.cs b/src/Orchard/DataMigration/Schema/CreateIndexCommand.cs new file mode 100644 index 000000000..3460c8364 --- /dev/null +++ b/src/Orchard/DataMigration/Schema/CreateIndexCommand.cs @@ -0,0 +1,10 @@ +namespace Orchard.DataMigration.Schema { + public class CreateIndexCommand : TableCommand { + public CreateIndexCommand(string name, params string[] columnNames) + : base(name) { + ColumnNames = columnNames; + } + + public string[] ColumnNames { get; private set; } + } +} diff --git a/src/Orchard/DataMigration/Schema/CreateTableCommand.cs b/src/Orchard/DataMigration/Schema/CreateTableCommand.cs new file mode 100644 index 000000000..f49342afa --- /dev/null +++ b/src/Orchard/DataMigration/Schema/CreateTableCommand.cs @@ -0,0 +1,38 @@ +using System; +using System.Data; + +namespace Orchard.DataMigration.Schema { + public class CreateTableCommand : SchemaCommand { + public CreateTableCommand(string name) + : base(name) { + } + + public CreateTableCommand Column(string name, DbType dbType, Action column = null) { + var command = new CreateColumnCommand(name); + command.Type(dbType); + + if ( column != null ) { + column(command); + } + _tableCommands.Add(command); + return this; + } + + public CreateTableCommand ContentPartRecord() { + /// TODO: Call Column() with necessary information for content part records + return this; + } + + public CreateTableCommand VersionedContentPartRecord() { + /// TODO: Call Column() with necessary information for content part records + return this; + } + + public CreateTableCommand ForeignKey(string name, Action fk) { + var command = new CreateForeignKeyCommand(name); + fk(command); + _tableCommands.Add(command); + return this; + } + } +} diff --git a/src/Orchard/DataMigration/Schema/DropColumnCommand.cs b/src/Orchard/DataMigration/Schema/DropColumnCommand.cs new file mode 100644 index 000000000..03e32b942 --- /dev/null +++ b/src/Orchard/DataMigration/Schema/DropColumnCommand.cs @@ -0,0 +1,8 @@ +namespace Orchard.DataMigration.Schema { + public class DropColumnCommand : ColumnCommand { + public DropColumnCommand(string name) + : base(name) { + + } + } +} diff --git a/src/Orchard/DataMigration/Schema/DropForeignKeyCommand.cs b/src/Orchard/DataMigration/Schema/DropForeignKeyCommand.cs new file mode 100644 index 000000000..ceb1fe530 --- /dev/null +++ b/src/Orchard/DataMigration/Schema/DropForeignKeyCommand.cs @@ -0,0 +1,8 @@ +namespace Orchard.DataMigration.Schema { + public class DropForeignKeyCommand : TableCommand { + + public DropForeignKeyCommand(string name) + : base(name) { + } + } +} diff --git a/src/Orchard/DataMigration/Schema/DropIndexCommand.cs b/src/Orchard/DataMigration/Schema/DropIndexCommand.cs new file mode 100644 index 000000000..ba48e5269 --- /dev/null +++ b/src/Orchard/DataMigration/Schema/DropIndexCommand.cs @@ -0,0 +1,8 @@ +namespace Orchard.DataMigration.Schema { + public class DropIndexCommand : TableCommand { + + public DropIndexCommand(string name) + : base(name) { + } + } +} diff --git a/src/Orchard/DataMigration/Schema/DropTableCommand.cs b/src/Orchard/DataMigration/Schema/DropTableCommand.cs new file mode 100644 index 000000000..38d21ecbd --- /dev/null +++ b/src/Orchard/DataMigration/Schema/DropTableCommand.cs @@ -0,0 +1,7 @@ +namespace Orchard.DataMigration.Schema { + public class DropTableCommand : SchemaCommand { + public DropTableCommand(string name) + : base(name) { + } + } +} diff --git a/src/Orchard/DataMigration/Schema/SchemaBuilder.cs b/src/Orchard/DataMigration/Schema/SchemaBuilder.cs new file mode 100644 index 000000000..2cd33ba34 --- /dev/null +++ b/src/Orchard/DataMigration/Schema/SchemaBuilder.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; + +namespace Orchard.DataMigration.Schema { + public class SchemaBuilder { + + private readonly List _schemaCommands; + + public SchemaBuilder() { + _schemaCommands = new List(); + } + + public SchemaBuilder(string tablePrefix) : this() { + TablePrefix = tablePrefix; + } + + public string TablePrefix { get; private set; } + + public SchemaBuilder CreateTable(string name, Action table) { + var createTable = new CreateTableCommand(name); + table(createTable); + _schemaCommands.Add(createTable); + return this; + } + + public SchemaBuilder AlterTable(string name, Action table) { + var alterTable = new AlterTableCommand(name); + table(alterTable); + _schemaCommands.Add(alterTable); + return this; + } + + public SchemaBuilder DropTable(string name) { + var deleteTable = new DropTableCommand(name); + _schemaCommands.Add(deleteTable); + return this; + } + + public SchemaBuilder ExecuteSql(string sql, Action statement) { + var sqlStatmentCommand = new SqlStatementCommand(sql); + statement(sqlStatmentCommand); + return this; + } + + } +} diff --git a/src/Orchard/DataMigration/Schema/SchemaCommand.cs b/src/Orchard/DataMigration/Schema/SchemaCommand.cs new file mode 100644 index 000000000..3f4392ad0 --- /dev/null +++ b/src/Orchard/DataMigration/Schema/SchemaCommand.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Orchard.DataMigration.Schema { + public class SchemaCommand { + protected readonly List _tableCommands; + + public SchemaCommand(string tableName) { + _tableCommands = new List(); + Name(tableName); + } + + public string TableName { get; private set; } + + public SchemaCommand Name(string name) { + TableName = name; + return this; + } + } +} diff --git a/src/Orchard/DataMigration/Schema/SqlStatementCommand.cs b/src/Orchard/DataMigration/Schema/SqlStatementCommand.cs new file mode 100644 index 000000000..86721f76f --- /dev/null +++ b/src/Orchard/DataMigration/Schema/SqlStatementCommand.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Orchard.DataMigration.Schema { + public class SqlStatementCommand : SchemaCommand { + protected readonly List _dialects; + public SqlStatementCommand(string sql) + : base("") { + Sql = sql; + _dialects = new List(); + } + + public string Sql { get; private set; } + + public SqlStatementCommand ForDialect(string dialect) { + _dialects.Add(dialect); + return this; + } + } +} diff --git a/src/Orchard/DataMigration/Schema/TableCommand.cs b/src/Orchard/DataMigration/Schema/TableCommand.cs new file mode 100644 index 000000000..5e5bb458e --- /dev/null +++ b/src/Orchard/DataMigration/Schema/TableCommand.cs @@ -0,0 +1,10 @@ +namespace Orchard.DataMigration.Schema { + public class TableCommand { + private string _name; + + public TableCommand(string name) { + _name = name; + } + + } +} diff --git a/src/Orchard/Environment/State/ShellStateCoordinator.cs b/src/Orchard/Environment/State/ShellStateCoordinator.cs index 637ae8a0d..004ce0cc4 100644 --- a/src/Orchard/Environment/State/ShellStateCoordinator.cs +++ b/src/Orchard/Environment/State/ShellStateCoordinator.cs @@ -195,7 +195,7 @@ namespace Orchard.Environment.State { } } - private static IEnumerable OrderByDependencies(IEnumerable descriptors) { + public static IEnumerable OrderByDependencies(IEnumerable descriptors) { var population = descriptors.Select(d => new Linkage { Feature = d }).ToArray(); diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index 61ae62328..01159b3b6 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -349,7 +349,19 @@ Code + + + + + + + + + + + + @@ -358,6 +370,10 @@ + + + +