Refactored ImportExport to support extensible, configurable export steps.

This commit is contained in:
Sipke Schoorstra
2015-07-14 15:39:32 +01:00
parent a926e7b8fa
commit 04c193afd1
24 changed files with 575 additions and 357 deletions

View File

@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Orchard.Commands;
using Orchard.ContentManagement.MetaData;
using Orchard.ImportExport.Models;
using Orchard.ImportExport.Providers;
using Orchard.ImportExport.Services;
using Orchard.Security;
using Orchard.Settings;
@@ -15,18 +17,22 @@ namespace Orchard.ImportExport.Commands {
private readonly ISiteService _siteService;
private readonly IMembershipService _membershipService;
private readonly IAuthenticationService _authenticationService;
private readonly IOrchardServices _orchardServices;
public ImportExportCommands(
IImportExportService importExportService,
IContentDefinitionManager contentDefinitionManager,
ISiteService siteService,
IMembershipService membershipService,
IAuthenticationService authenticationService) {
IAuthenticationService authenticationService,
IOrchardServices orchardServices) {
_importExportService = importExportService;
_contentDefinitionManager = contentDefinitionManager;
_siteService = siteService;
_membershipService = membershipService;
_authenticationService = authenticationService;
_orchardServices = orchardServices;
}
[OrchardSwitch]
@@ -65,10 +71,10 @@ namespace Orchard.ImportExport.Commands {
}
[CommandName("export file")]
[CommandHelp("export file [/Types:<type-name-1>, ... ,<type-name-n>] [/Metadata:true|false] [/Data:true|false] [/Version:Published|Draft] [/SiteSettings:true|false] [/Steps:<custom-step-1>, ... ,<custom-step-n>]\r\n\t" + "Create an export file according to the specified options.")]
[CommandHelp("export file [/Types:<type-name-1>, ... ,<type-name-n>] [/Metadata:true|false] [/Data:true|false] [/Version:Published|Draft|Latest] [/SiteSettings:true|false] [/Steps:<custom-step-1>, ... ,<custom-step-n>]\r\n\t" + "Create an export file according to the specified options.")]
[OrchardSwitches("Types,Metadata,Data,Version,SiteSettings,Steps")]
public void ExportFile() {
// impersonate the Site owner
// Impersonate the Site owner.
var superUser = _siteService.GetSiteSettings().SuperUser;
var owner = _membershipService.GetUser(superUser);
_authenticationService.SetAuthenticatedUserForRequest(owner);
@@ -82,23 +88,39 @@ namespace Orchard.ImportExport.Commands {
var enteredTypes = (Types ?? String.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
var exportTypes = _contentDefinitionManager.ListTypeDefinitions()
.Where(contentType => enteredTypes.Contains(contentType.Name))
.Select(contentType => contentType.Name);
var exportTypes = _contentDefinitionManager
.ListTypeDefinitions()
.Where(contentType => enteredTypes.Contains(contentType.Name))
.Select(contentType => contentType.Name)
.ToList();
var enteredSteps = (Steps ?? String.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
var exportSteps = new List<IExportStepProvider>();
var exportOptions = new ExportOptions {
ExportMetadata = Metadata,
ExportData = Data,
VersionHistoryOptions = versionOption,
ExportSiteSettings = SiteSettings,
CustomSteps = enteredSteps
};
if (Metadata || Data) {
var dataStep = _orchardServices.WorkContext.Resolve<DataExportStep>();
if(Data)
dataStep.DataContentTypes = exportTypes;
if(Metadata)
dataStep.SchemaContentTypes = exportTypes;
dataStep.VersionHistoryOptions = versionOption;
exportSteps.Add(dataStep);
}
if (SiteSettings) {
var siteSettingsStep = _orchardServices.WorkContext.Resolve<SiteSettingsExportStep>();
exportSteps.Add(siteSettingsStep);
}
Context.Output.WriteLine(T("Export starting..."));
var exportFilePath = _importExportService.Export(exportTypes, exportOptions);
var exportFilePath = _importExportService.Export(exportSteps, exportOptions);
Context.Output.WriteLine(T("Export completed at {0}", exportFilePath));
}

View File

@@ -3,18 +3,17 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web.Mvc;
using Orchard.ContentManagement;
using Orchard.ContentManagement.MetaData;
using Orchard.DisplayManagement;
using Orchard.ImportExport.Models;
using Orchard.ImportExport.Services;
using Orchard.ImportExport.ViewModels;
using Orchard.Localization;
using Orchard.Recipes.Services;
using Orchard.UI.Notify;
using Orchard.ImportExport.Models;
using Orchard.Utility.Extensions;
namespace Orchard.ImportExport.Controllers {
public class AdminController : Controller {
public class AdminController : Controller, IUpdateModel {
private readonly IImportExportService _importExportService;
private readonly IContentDefinitionManager _contentDefinitionManager;
private readonly ICustomExportStep _customExportStep;
@@ -79,25 +78,18 @@ namespace Orchard.ImportExport.Controllers {
var customSteps = new List<string>();
_customExportStep.Register(customSteps);
var exportSteps = _exportStepProviders.Select(x => {
var editor = x.BuildEditor(Services.New);
editor.Provider = x;
editor.Name = x.Name;
editor.DisplayName = x.DisplayName;
return editor;
var exportSteps = _exportStepProviders.OrderBy(x => x.Position).Select(x => new ExportStepViewModel {
Name = x.Name,
DisplayName = x.DisplayName,
Description = x.Description,
Editor = x.BuildEditor(Services.New)
}).Where(x => x != null);
var viewModel = new ExportViewModel {
RecipeVersion = "1.0",
ContentTypes = new List<ContentTypeEntry>(),
CustomSteps = customSteps.Select(x => new CustomStepEntry { CustomStep = x }).ToList(),
ExportSteps = exportSteps.ToList()
};
foreach (var contentType in _contentDefinitionManager.ListTypeDefinitions().OrderBy(c => c.Name)) {
viewModel.ContentTypes.Add(new ContentTypeEntry { ContentTypeName = contentType.Name });
}
return View(viewModel);
}
@@ -107,37 +99,39 @@ namespace Orchard.ImportExport.Controllers {
return new HttpUnauthorizedResult();
var viewModel = new ExportViewModel {
ContentTypes = new List<ContentTypeEntry>(),
CustomSteps = new List<CustomStepEntry>()
CustomSteps = new List<CustomStepEntry>(),
ExportSteps = new List<ExportStepViewModel>()
};
UpdateModel(viewModel);
var contentTypesToExport = viewModel.ContentTypes.Where(c => c.IsChecked).Select(c => c.ContentTypeName).ToList();
var exportStepNames = viewModel.ExportSteps.Where(x => x.IsSelected).Select(x => x.Name);
var exportStepsQuery = from name in exportStepNames
let provider = _exportStepProviders.SingleOrDefault(x => x.Name == name)
where provider != null
select provider;
var exportSteps = exportStepsQuery.ToArray();
var customSteps = viewModel.CustomSteps.Where(c => c.IsChecked).Select(c => c.CustomStep);
var exportOptions = new ExportOptions {
ExportMetadata = viewModel.Metadata,
ExportSiteSettings = viewModel.SiteSettings,
SetupRecipe = viewModel.SetupRecipe,
RecipeName = viewModel.RecipeName,
RecipeDescription = viewModel.RecipeDescription,
RecipeWebsite = viewModel.RecipeWebsite,
RecipeTags = viewModel.RecipeTags,
RecipeVersion = viewModel.RecipeVersion,
CustomSteps = customSteps
};
if (viewModel.Data) {
exportOptions.ExportData = true;
exportOptions.VersionHistoryOptions = (VersionHistoryOptions)Enum.Parse(typeof(VersionHistoryOptions), viewModel.DataImportChoice, true);
exportOptions.ImportBatchSize = viewModel.ImportBatchSize;
foreach (var exportStep in exportSteps) {
exportStep.UpdateEditor(Services.New, this);
}
var exportFilePath = _importExportService.Export(contentTypesToExport, exportOptions);
var exportFileName = exportOptions.SetupRecipe && !String.IsNullOrWhiteSpace(exportOptions.RecipeName)
? String.Format("{0}.recipe.xml", exportOptions.RecipeName.HtmlClassify())
: "export.xml";
var exportFilePath = _importExportService.Export(exportSteps, exportOptions);
var exportFileName = "export.xml";
return File(exportFilePath, "text/xml", exportFileName);
}
bool IUpdateModel.TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties) {
return TryUpdateModel(model, prefix, includeProperties, excludeProperties);
}
void IUpdateModel.AddModelError(string key, LocalizedString errorMessage) {
ModelState.AddModelError(key, errorMessage.ToString());
}
}
}

View File

@@ -2,22 +2,12 @@
namespace Orchard.ImportExport.Models {
public class ExportOptions {
public bool ExportMetadata { get; set; }
public bool ExportData { get; set; }
public int? ImportBatchSize { get; set; }
public VersionHistoryOptions VersionHistoryOptions { get; set; }
public bool ExportSiteSettings { get; set; }
public IEnumerable<string> CustomSteps { get; set; }
public bool SetupRecipe { get; set; }
public string RecipeName { get; set; }
public string RecipeDescription { get; set; }
public string RecipeWebsite { get; set; }
public string RecipeTags { get; set; }
public string RecipeVersion { get; set; }
}
public enum VersionHistoryOptions {
Published,
Draft,
Latest
}
}

View File

@@ -71,7 +71,15 @@
<Compile Include="Models\ExportOptions.cs" />
<Compile Include="Permissions.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Providers\SetupRecipeExportStep.cs" />
<Compile Include="Providers\SiteSettingsExportStep.cs" />
<Compile Include="Providers\DataExportStep.cs" />
<Compile Include="ViewModels\ContentTypeEntry.cs" />
<Compile Include="ViewModels\CustomStepEntry.cs" />
<Compile Include="ViewModels\ExportStepViewModel.cs" />
<Compile Include="ViewModels\SetupRecipeStepViewModel.cs" />
<Compile Include="ViewModels\SiteSettingsStepViewModel.cs" />
<Compile Include="ViewModels\DataExportStepViewModel.cs" />
<Compile Include="Services\ICustomExportStep.cs" />
<Compile Include="Services\IExportEventHandler.cs" />
<Compile Include="Services\IImportExportService.cs" />
@@ -101,6 +109,7 @@
</ItemGroup>
<ItemGroup>
<Content Include="Styles\images\menu.importexport.png" />
<Content Include="Styles\exportstep-data.css" />
<Content Include="Styles\menu.importexport-admin.css" />
<Content Include="Web.config" />
</ItemGroup>
@@ -121,6 +130,12 @@
<ItemGroup>
<Content Include="Views\EditorTemplates\ExportSteps\Data.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\EditorTemplates\ExportSteps\SiteSettings.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\EditorTemplates\ExportSteps\SetupRecipe.cshtml" />
</ItemGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>

View File

@@ -1,27 +1,153 @@
using Orchard.ContentManagement;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Orchard.ContentManagement;
using Orchard.ContentManagement.MetaData;
using Orchard.ContentManagement.MetaData.Models;
using Orchard.ImportExport.Models;
using Orchard.ImportExport.Services;
using Orchard.ImportExport.ViewModels;
using Orchard.Localization;
namespace Orchard.ImportExport.Providers {
public class DataExportStep : ExportStepProvider {
private readonly IContentDefinitionManager _contentDefinitionManager;
private readonly IOrchardServices _orchardServices;
private readonly IContentDefinitionWriter _contentDefinitionWriter;
public DataExportStep(
IContentDefinitionManager contentDefinitionManager,
IOrchardServices orchardServices,
IContentDefinitionWriter contentDefinitionWriter) {
_contentDefinitionManager = contentDefinitionManager;
_orchardServices = orchardServices;
_contentDefinitionWriter = contentDefinitionWriter;
}
public override string Name {
get { return "Data"; }
}
public override LocalizedString DisplayName {
get { return T("Data"); }
get { return T("Content Items and Definitions"); }
}
public override LocalizedString Description {
get { return T("Exports content items and content item definitions."); }
}
public override int Position { get { return 10; } }
public IList<string> SchemaContentTypes { get; set; }
public IList<string> DataContentTypes { get; set; }
public int? ImportBatchSize { get; set; }
public VersionHistoryOptions VersionHistoryOptions { get; set; }
public override dynamic BuildEditor(dynamic shapeFactory) {
return UpdateEditor(shapeFactory, null);
}
public override dynamic UpdateEditor(dynamic shapeFactory, IUpdateModel updater) {
if (updater != null) {
var contentTypeViewModels = _contentDefinitionManager.ListTypeDefinitions()
.OrderBy(x => x.Name)
.Select(x => new ContentTypeEntry { Name = x.Name, DisplayName = x.DisplayName })
.ToList();
var viewModel = new DataExportStepViewModel {
ContentTypes = contentTypeViewModels
};
if (updater != null && updater.TryUpdateModel(viewModel, Prefix, null, null)) {
DataContentTypes = viewModel.ContentTypes.Where(x => x.ExportData).Select(x => x.Name).ToList();
SchemaContentTypes = viewModel.ContentTypes.Where(x => x.ExportSchema).Select(x => x.Name).ToList();
VersionHistoryOptions = viewModel.VersionHistoryOptions;
}
return shapeFactory.EditorTemplate(TemplateName: "ExportSteps/Data");
return shapeFactory.EditorTemplate(TemplateName: "ExportSteps/Data", Model: viewModel, Prefix: Prefix);
}
public override void Export(ExportContext context) {
var dataContentTypes = DataContentTypes;
var schemaContentTypes = SchemaContentTypes;
var exportVersionOptions = GetContentExportVersionOptions(VersionHistoryOptions);
var contentItems = dataContentTypes.Any()
? _orchardServices.ContentManager.Query(exportVersionOptions, dataContentTypes.ToArray()).List().ToArray()
: Enumerable.Empty<ContentItem>();
if(schemaContentTypes.Any())
context.Document.Element("Orchard").Add(ExportMetadata(schemaContentTypes));
if(contentItems.Any())
context.Document.Element("Orchard").Add(ExportData(dataContentTypes, contentItems, ImportBatchSize));
}
private XElement ExportMetadata(IEnumerable<string> contentTypes) {
var typesElement = new XElement("Types");
var partsElement = new XElement("Parts");
var typesToExport = _contentDefinitionManager.ListTypeDefinitions()
.Where(typeDefinition => contentTypes.Contains(typeDefinition.Name))
.ToList();
var partsToExport = new Dictionary<string, ContentPartDefinition>();
foreach (var contentTypeDefinition in typesToExport.OrderBy(x => x.Name)) {
foreach (var contentPartDefinition in contentTypeDefinition.Parts) {
if (partsToExport.ContainsKey(contentPartDefinition.PartDefinition.Name)) {
continue;
}
partsToExport.Add(contentPartDefinition.PartDefinition.Name, contentPartDefinition.PartDefinition);
}
typesElement.Add(_contentDefinitionWriter.Export(contentTypeDefinition));
}
foreach (var part in partsToExport.Values.OrderBy(x => x.Name)) {
partsElement.Add(_contentDefinitionWriter.Export(part));
}
return new XElement("Metadata", typesElement, partsElement);
}
private XElement ExportData(IEnumerable<string> contentTypes, IEnumerable<ContentItem> contentItems, int? batchSize) {
var data = new XElement("Data");
if (batchSize.HasValue && batchSize.Value > 0)
data.SetAttributeValue("BatchSize", batchSize);
var orderedContentItemsQuery =
from contentItem in contentItems
let identity = _orchardServices.ContentManager.GetItemMetadata(contentItem).Identity.ToString()
orderby identity
select contentItem;
var orderedContentItems = orderedContentItemsQuery.ToList();
foreach (var contentType in contentTypes.OrderBy(x => x)) {
var type = contentType;
var items = orderedContentItems.Where(i => i.ContentType == type);
foreach (var contentItem in items) {
var contentItemElement = ExportContentItem(contentItem);
if (contentItemElement != null)
data.Add(contentItemElement);
}
}
return data;
}
private VersionOptions GetContentExportVersionOptions(VersionHistoryOptions versionHistoryOptions) {
switch (versionHistoryOptions) {
case VersionHistoryOptions.Draft:
return VersionOptions.Draft;
case VersionHistoryOptions.Latest:
return VersionOptions.Latest;
case VersionHistoryOptions.Published:
default:
return VersionOptions.Published;
}
}
private XElement ExportContentItem(ContentItem contentItem) {
return _orchardServices.ContentManager.Export(contentItem);
}
}
}

View File

@@ -0,0 +1,69 @@
using Orchard.ContentManagement;
using Orchard.ImportExport.Services;
using Orchard.ImportExport.ViewModels;
using Orchard.Localization;
namespace Orchard.ImportExport.Providers {
public class SetupRecipeExportStep : ExportStepProvider {
private readonly IOrchardServices _orchardServices;
public SetupRecipeExportStep(IOrchardServices orchardServices) {
_orchardServices = orchardServices;
}
public override string Name {
get { return "SetupRecipe"; }
}
public override LocalizedString DisplayName {
get { return T("Setup Recipe"); }
}
public override LocalizedString Description {
get { return T("Turns the export file into a Setup recipe."); }
}
public override int Position { get { return -10; } }
public string RecipeName { get; set; }
public string RecipeDescription { get; set; }
public string RecipeAuthor { get; set; }
public string RecipeWebsite { get; set; }
public string RecipeTags { get; set; }
public string RecipeVersion { get; set; }
public bool IsSetupRecipe { get; set; }
public override dynamic BuildEditor(dynamic shapeFactory) {
return UpdateEditor(shapeFactory, null);
}
public override dynamic UpdateEditor(dynamic shapeFactory, IUpdateModel updater) {
var viewModel = new SetupRecipeStepViewModel {
RecipeAuthor = _orchardServices.WorkContext.CurrentUser.UserName
};
if (updater != null && updater.TryUpdateModel(viewModel, Prefix, null, null)) {
RecipeName = viewModel.RecipeName;
RecipeDescription = viewModel.RecipeDescription;
RecipeAuthor = viewModel.RecipeAuthor;
RecipeWebsite = viewModel.RecipeWebsite;
RecipeTags = viewModel.RecipeTags;
RecipeVersion = viewModel.RecipeVersion;
IsSetupRecipe = true;
}
return shapeFactory.EditorTemplate(TemplateName: "ExportSteps/SetupRecipe", Model: viewModel, Prefix: Prefix);
}
public override void Export(ExportContext context) {
var recipeElement = context.Document.Element("Orchard").Element("Recipe");
recipeElement.SetElementValue("Name", RecipeName);
recipeElement.SetElementValue("Description", RecipeDescription);
recipeElement.SetElementValue("Author", RecipeAuthor);
recipeElement.SetElementValue("WebSite", RecipeWebsite);
recipeElement.SetElementValue("Tags", RecipeTags);
recipeElement.SetElementValue("Version", RecipeVersion);
recipeElement.SetElementValue("IsSetupRecipe", IsSetupRecipe);
}
}
}

View File

@@ -0,0 +1,95 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Orchard.ContentManagement;
using Orchard.ImportExport.Services;
using Orchard.ImportExport.ViewModels;
using Orchard.Localization;
namespace Orchard.ImportExport.Providers {
public class SiteSettingsExportStep : ExportStepProvider {
private readonly IOrchardServices _orchardServices;
public SiteSettingsExportStep(IOrchardServices orchardServices) {
_orchardServices = orchardServices;
}
public override string Name {
get { return "SiteSettings"; }
}
public override LocalizedString DisplayName {
get { return T("Site Settings"); }
}
public override LocalizedString Description {
get { return T("Exports site settings."); }
}
public override int Position { get { return 20; } }
public override dynamic BuildEditor(dynamic shapeFactory) {
return UpdateEditor(shapeFactory, null);
}
public override dynamic UpdateEditor(dynamic shapeFactory, IUpdateModel updater) {
var viewModel = new SiteSettingsStepViewModel();
return shapeFactory.EditorTemplate(TemplateName: "ExportSteps/SiteSettings", Model: viewModel, Prefix: Prefix);
}
public override void Export(ExportContext context) {
context.Document.Element("Orchard").Add(ExportSiteSettings());
}
private XElement ExportSiteSettings() {
var siteContentItem = _orchardServices.WorkContext.CurrentSite.ContentItem;
var exportedElements = ExportContentItem(siteContentItem).Elements().ToList();
foreach (var contentPart in siteContentItem.Parts.OrderBy(x => x.PartDefinition.Name)) {
var exportedElement = exportedElements.FirstOrDefault(element => element.Name == contentPart.PartDefinition.Name);
// Get all simple attributes if exported element is null.
// Get exclude the simple attributes that already exist if element is not null.
var simpleAttributes =
ExportSettingsPartAttributes(contentPart)
.Where(attribute => exportedElement == null || exportedElement.Attributes().All(xAttribute => xAttribute.Name != attribute.Name))
.OrderBy(x => x.Name.LocalName)
.ToList();
if (simpleAttributes.Any()) {
if (exportedElement == null) {
exportedElement = new XElement(contentPart.PartDefinition.Name);
exportedElements.Add(exportedElement);
}
exportedElement.Add(simpleAttributes);
}
}
exportedElements = exportedElements.OrderBy(x => x.Name.LocalName).ToList();
return new XElement("Settings", exportedElements);
}
private XElement ExportContentItem(ContentItem contentItem) {
return _orchardServices.ContentManager.Export(contentItem);
}
private IEnumerable<XAttribute> ExportSettingsPartAttributes(ContentPart sitePart) {
foreach (var property in sitePart.GetType().GetProperties().OrderBy(x => x.Name)) {
var propertyType = property.PropertyType;
// Supported types (we also know they are not indexed properties).
if (propertyType == typeof(string) || propertyType == typeof(bool) || propertyType == typeof(int)) {
// Exclude read-only properties.
if (property.GetSetMethod() != null) {
var value = property.GetValue(sitePart, null);
if (value == null)
continue;
yield return new XAttribute(property.Name, value);
}
}
}
}
}
}

View File

@@ -5,6 +5,8 @@ namespace Orchard.ImportExport.Services {
public abstract class ExportStepProvider : Component, IExportStepProvider {
public abstract string Name { get; }
public abstract LocalizedString DisplayName { get; }
public abstract LocalizedString Description { get; }
public virtual int Position { get { return 0; } }
protected virtual string Prefix {
get { return GetType().Name; }
@@ -17,5 +19,7 @@ namespace Orchard.ImportExport.Services {
public virtual dynamic UpdateEditor(dynamic shapeFactory, IUpdateModel updater) {
return null;
}
public virtual void Export(ExportContext context) {}
}
}

View File

@@ -1,12 +1,10 @@
using System.Collections.Generic;
using System.Xml.Linq;
using System.Xml.Linq;
using Orchard.Events;
using Orchard.ImportExport.Models;
namespace Orchard.ImportExport.Services {
public class ExportContext {
public XDocument Document { get; set; }
public IEnumerable<string> ContentTypes { get; set; }
public ExportOptions ExportOptions { get; set; }
}

View File

@@ -5,7 +5,10 @@ namespace Orchard.ImportExport.Services {
public interface IExportStepProvider : IDependency {
string Name { get; }
LocalizedString DisplayName { get; }
LocalizedString Description { get; }
int Position { get; }
dynamic BuildEditor(dynamic shapeFactory);
dynamic UpdateEditor(dynamic shapeFactory, IUpdateModel updater);
void Export(ExportContext context);
}
}

View File

@@ -1,12 +1,10 @@
using System.Collections.Generic;
using Orchard.ContentManagement;
using Orchard.ImportExport.Models;
namespace Orchard.ImportExport.Services {
public interface IImportExportService : IDependency {
string Import(string recipeText);
string Export(IEnumerable<string> contentTypes, IEnumerable<ContentItem> contentItems, ExportOptions exportOptions);
string Export(IEnumerable<string> contentTypes, ExportOptions exportOptions);
string Export(IEnumerable<IExportStepProvider> steps, ExportOptions exportOptions);
}
}

View File

@@ -1,11 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using Orchard.ContentManagement;
using Orchard.ContentManagement.MetaData;
using Orchard.ContentManagement.MetaData.Models;
using Orchard.Environment.Descriptor;
using Orchard.FileSystems.AppData;
using Orchard.ImportExport.Models;
@@ -17,8 +14,6 @@ using Orchard.Services;
namespace Orchard.ImportExport.Services {
public class ImportExportService : IImportExportService {
private readonly IOrchardServices _orchardServices;
private readonly IContentDefinitionManager _contentDefinitionManager;
private readonly IContentDefinitionWriter _contentDefinitionWriter;
private readonly IAppDataFolder _appDataFolder;
private readonly IRecipeParser _recipeParser;
private readonly IRecipeManager _recipeManager;
@@ -29,8 +24,6 @@ namespace Orchard.ImportExport.Services {
public ImportExportService(
IOrchardServices orchardServices,
IContentDefinitionManager contentDefinitionManager,
IContentDefinitionWriter contentDefinitionWriter,
IAppDataFolder appDataFolder,
IRecipeParser recipeParser,
IRecipeManager recipeManager,
@@ -39,8 +32,6 @@ namespace Orchard.ImportExport.Services {
IEnumerable<IExportEventHandler> exportEventHandlers) {
_orchardServices = orchardServices;
_contentDefinitionManager = contentDefinitionManager;
_contentDefinitionWriter = contentDefinitionWriter;
_appDataFolder = appDataFolder;
_recipeParser = recipeParser;
_recipeManager = recipeManager;
@@ -61,37 +52,18 @@ namespace Orchard.ImportExport.Services {
return executionId;
}
public string Export(IEnumerable<string> contentTypes, ExportOptions exportOptions) {
//items need to be retrieved
IEnumerable<ContentItem> contentItems = null;
if (exportOptions.ExportData) {
contentItems = _orchardServices.ContentManager.Query(GetContentExportVersionOptions(exportOptions.VersionHistoryOptions), contentTypes.ToArray()).List().ToArray();
}
return Export(contentTypes, contentItems, exportOptions);
}
public string Export(IEnumerable<string> contentTypes, IEnumerable<ContentItem> contentItems, ExportOptions exportOptions) {
var exportDocument = CreateExportRoot(exportOptions);
public string Export(IEnumerable<IExportStepProvider> steps, ExportOptions exportOptions) {
var exportDocument = CreateExportRoot();
var context = new ExportContext {
Document = exportDocument,
ContentTypes = contentTypes,
ExportOptions = exportOptions
};
_exportEventHandlers.Invoke(x => x.Exporting(context), Logger);
if (exportOptions.ExportMetadata && (!exportOptions.ExportData || contentItems.Any())) {
exportDocument.Element("Orchard").Add(ExportMetadata(contentTypes));
}
if (exportOptions.ExportSiteSettings) {
exportDocument.Element("Orchard").Add(ExportSiteSettings());
}
if (exportOptions.ExportData && contentItems.Any()) {
exportDocument.Element("Orchard").Add(ExportData(contentTypes, contentItems, exportOptions.ImportBatchSize));
foreach (var step in steps) {
step.Export(context);
}
_exportEventHandlers.Invoke(x => x.Exported(context), Logger);
@@ -99,23 +71,12 @@ namespace Orchard.ImportExport.Services {
return WriteExportFile(exportDocument.ToString());
}
private XDocument CreateExportRoot(ExportOptions exportOptions) {
var recipeName = !String.IsNullOrWhiteSpace(exportOptions.RecipeName)
? exportOptions.RecipeName
: T("Generated by Orchard.ImportExport").ToString();
private XDocument CreateExportRoot() {
var exportRoot = new XDocument(
new XDeclaration("1.0", "", "yes"),
new XComment("Exported from Orchard"),
new XElement("Orchard",
new XElement("Recipe",
new XElement("Name", recipeName),
new XElement("Description", exportOptions.RecipeDescription),
new XElement("Author", _orchardServices.WorkContext.CurrentUser.UserName),
new XElement("WebSite", exportOptions.RecipeWebsite),
new XElement("Tags", exportOptions.RecipeTags),
new XElement("Version", exportOptions.RecipeVersion),
new XElement("IsSetupRecipe", exportOptions.SetupRecipe),
new XElement("ExportUtc", XmlConvert.ToString(_clock.UtcNow, XmlDateTimeSerializationMode.Utc))
)
)
@@ -123,119 +84,8 @@ namespace Orchard.ImportExport.Services {
return exportRoot;
}
private XElement ExportMetadata(IEnumerable<string> contentTypes) {
var typesElement = new XElement("Types");
var partsElement = new XElement("Parts");
var typesToExport = _contentDefinitionManager.ListTypeDefinitions()
.Where(typeDefinition => contentTypes.Contains(typeDefinition.Name))
.ToList();
var partsToExport = new Dictionary<string, ContentPartDefinition>();
foreach (var contentTypeDefinition in typesToExport.OrderBy(x => x.Name)) {
foreach (var contentPartDefinition in contentTypeDefinition.Parts) {
if (partsToExport.ContainsKey(contentPartDefinition.PartDefinition.Name)) {
continue;
}
partsToExport.Add(contentPartDefinition.PartDefinition.Name, contentPartDefinition.PartDefinition);
}
typesElement.Add(_contentDefinitionWriter.Export(contentTypeDefinition));
}
foreach (var part in partsToExport.Values.OrderBy(x => x.Name)) {
partsElement.Add(_contentDefinitionWriter.Export(part));
}
return new XElement("Metadata", typesElement, partsElement);
}
private XElement ExportSiteSettings() {
var siteContentItem = _orchardServices.WorkContext.CurrentSite.ContentItem;
var exportedElements = ExportContentItem(siteContentItem).Elements().ToList();
foreach (var contentPart in siteContentItem.Parts.OrderBy(x => x.PartDefinition.Name)) {
var exportedElement = exportedElements.FirstOrDefault(element => element.Name == contentPart.PartDefinition.Name);
//Get all simple attributes if exported element is null
//Get exclude the simple attributes that already exist if element is not null
var simpleAttributes =
ExportSettingsPartAttributes(contentPart)
.Where(attribute => exportedElement == null || exportedElement.Attributes().All(xAttribute => xAttribute.Name != attribute.Name))
.OrderBy(x => x.Name.LocalName)
.ToList();
if (simpleAttributes.Any()) {
if (exportedElement == null) {
exportedElement = new XElement(contentPart.PartDefinition.Name);
exportedElements.Add(exportedElement);
}
exportedElement.Add(simpleAttributes);
}
}
exportedElements = exportedElements.OrderBy(x => x.Name.LocalName).ToList();
return new XElement("Settings", exportedElements);
}
private IEnumerable<XAttribute> ExportSettingsPartAttributes(ContentPart sitePart) {
foreach (var property in sitePart.GetType().GetProperties().OrderBy(x => x.Name)) {
var propertyType = property.PropertyType;
// Supported types (we also know they are not indexed properties).
if (propertyType == typeof(string) || propertyType == typeof(bool) || propertyType == typeof(int)) {
// Exclude read-only properties.
if (property.GetSetMethod() != null) {
var value = property.GetValue(sitePart, null);
if (value == null)
continue;
yield return new XAttribute(property.Name, value);
}
}
}
}
private XElement ExportData(IEnumerable<string> contentTypes, IEnumerable<ContentItem> contentItems, int? batchSize) {
var data = new XElement("Data");
if (batchSize.HasValue && batchSize.Value > 0)
data.SetAttributeValue("BatchSize", batchSize);
var orderedContentItemsQuery =
from contentItem in contentItems
let identity = _orchardServices.ContentManager.GetItemMetadata(contentItem).Identity.ToString()
orderby identity
select contentItem;
var orderedContentItems = orderedContentItemsQuery.ToList();
foreach (var contentType in contentTypes.OrderBy(x => x)) {
var type = contentType;
var items = orderedContentItems.Where(i => i.ContentType == type);
foreach (var contentItem in items) {
var contentItemElement = ExportContentItem(contentItem);
if (contentItemElement != null)
data.Add(contentItemElement);
}
}
return data;
}
private XElement ExportContentItem(ContentItem contentItem) {
// Call export handler for the item.
return _orchardServices.ContentManager.Export(contentItem);
}
private static VersionOptions GetContentExportVersionOptions(VersionHistoryOptions versionHistoryOptions) {
if (versionHistoryOptions.HasFlag(VersionHistoryOptions.Draft)) {
return VersionOptions.Draft;
}
return VersionOptions.Published;
}
private string WriteExportFile(string exportDocument) {
var exportFile = string.Format("Export-{0}-{1}.xml", _orchardServices.WorkContext.CurrentUser.UserName, DateTime.UtcNow.Ticks);
var exportFile = String.Format("Export-{0}-{1}.xml", _orchardServices.WorkContext.CurrentUser.UserName, DateTime.UtcNow.Ticks);
if (!_appDataFolder.DirectoryExists(ExportsDirectory)) {
_appDataFolder.CreateDirectory(ExportsDirectory);
}

View File

@@ -0,0 +1,9 @@
fieldset.export-step-data table.items {
width: 500px;
margin: 0;
}
fieldset.export-step-data table.items td,
fieldset.export-step-data table.items thead tr.sub th {
padding: 0 12px;
}

View File

@@ -0,0 +1,8 @@
namespace Orchard.ImportExport.ViewModels {
public class ContentTypeEntry {
public string Name { get; set; }
public string DisplayName { get; set; }
public bool ExportSchema { get; set; }
public bool ExportData { get; set; }
}
}

View File

@@ -0,0 +1,6 @@
namespace Orchard.ImportExport.ViewModels {
public class CustomStepEntry {
public string CustomStep { get; set; }
public bool IsChecked { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using System.Collections.Generic;
using Orchard.ImportExport.Models;
namespace Orchard.ImportExport.ViewModels {
public class DataExportStepViewModel {
public DataExportStepViewModel() {
ContentTypes = new List<ContentTypeEntry>();
}
public IList<ContentTypeEntry> ContentTypes { get; set; }
public VersionHistoryOptions VersionHistoryOptions { get; set; }
public int? ImportBatchSize { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
using Orchard.Localization;
namespace Orchard.ImportExport.ViewModels {
public class ExportStepViewModel {
public string Name { get; set; }
public LocalizedString DisplayName { get; set; }
public LocalizedString Description { get; set; }
public bool IsSelected { get; set; }
public dynamic Editor { get; set; }
}
}

View File

@@ -2,29 +2,7 @@
namespace Orchard.ImportExport.ViewModels {
public class ExportViewModel {
public IList<ContentTypeEntry> ContentTypes { get; set; }
public IList<CustomStepEntry> CustomSteps { get; set; }
public IList<dynamic> ExportSteps { get; set; }
public bool Metadata { get; set; }
public bool Data { get; set; }
public int? ImportBatchSize { get; set; }
public string DataImportChoice { get; set; }
public bool SiteSettings { get; set; }
public bool SetupRecipe { get; set; }
public string RecipeName { get; set; }
public string RecipeDescription { get; set; }
public string RecipeWebsite { get; set; }
public string RecipeTags { get; set; }
public string RecipeVersion { get; set; }
}
public class ContentTypeEntry {
public string ContentTypeName { get; set; }
public bool IsChecked { get; set; }
}
public class CustomStepEntry {
public string CustomStep { get; set; }
public bool IsChecked { get; set; }
public IList<ExportStepViewModel> ExportSteps { get; set; }
}
}

View File

@@ -0,0 +1,10 @@
namespace Orchard.ImportExport.ViewModels {
public class SetupRecipeStepViewModel {
public string RecipeName { get; set; }
public string RecipeDescription { get; set; }
public string RecipeAuthor { get; set; }
public string RecipeWebsite { get; set; }
public string RecipeTags { get; set; }
public string RecipeVersion { get; set; }
}
}

View File

@@ -0,0 +1,3 @@
namespace Orchard.ImportExport.ViewModels {
public class SiteSettingsStepViewModel {}
}

View File

@@ -1,114 +1,44 @@
@using Orchard.Localization
@using Orchard.Utility.Extensions
@using Orchard.Utility.Extensions
@model Orchard.ImportExport.ViewModels.ExportViewModel
@{ Layout.Title = T("Export").ToString(); }
@using (Html.BeginFormAntiForgeryPost()) {
Html.ValidationSummary();
var exportStepIndex = 0;
foreach (var exportStep in Model.ExportSteps) {
var displayName = (LocalizedString)exportStep.DisplayName;
var stepId = String.Format("step-{0}", displayName.ToString().HtmlClassify());
<fieldset>
<legend><label><input type="checkbox" id="@stepId" />@displayName</label></legend>
<div data-controllerid="@stepId">
@Display(exportStep)
</div>
</fieldset>
}
<fieldset>
<legend>@T("Choose the types to include in the export file:")</legend>
<label><input type="checkbox" class="check-all" />@T("Select all")</label>
<ol>
@for (var contentTypeIndex = 0; contentTypeIndex < Model.ContentTypes.Count; contentTypeIndex++) {
<li>
<input type="hidden" value="@Model.ContentTypes[contentTypeIndex].ContentTypeName" name="@Html.NameOf(m => m.ContentTypes[contentTypeIndex].ContentTypeName)"/>
<input type="checkbox" value="true" name="@Html.NameOf(m => m.ContentTypes[contentTypeIndex].IsChecked)" id="@Html.NameOf(m => m.ContentTypes[contentTypeIndex].IsChecked)" />
<label class="forcheckbox" for="@Html.NameOf(m => m.ContentTypes[contentTypeIndex].IsChecked)">@Model.ContentTypes[contentTypeIndex].ContentTypeName.CamelFriendly()</label>
</li>
}
</ol>
</fieldset>
<hr />
<fieldset>
<legend>@T("Choose what to save for these types:")</legend>
<div>
@Html.EditorFor(m => m.Metadata)
@Html.LabelFor(m => m.Metadata, T("Metadata").ToString(), new { @class = "forcheckbox" })
@Html.Hint(T("Metadata is the definition of your content types: what parts and fields they have, with what settings."))
</div>
<div>
@Html.EditorFor(m => m.Data)
@Html.LabelFor(m => m.Data, T("Data").ToString(), new { @class = "forcheckbox" })
@Html.Hint(T("Data is the actual content of your site."))
</div>
<div>
@Html.LabelFor(m => m.ImportBatchSize, T("Batch Size"))
@Html.TextBoxFor(m => m.ImportBatchSize, new {@class = "text small"})
@Html.Hint(T("The batch size to use when importing the data. Leave empty to disable batched imports."))
</div>
<div>
<p>@T("Version History")</p>
@Html.RadioButtonFor(m => m.DataImportChoice, "Published", new {id = "Published", Checked = "Checked"})
<label for="Published" class="forcheckbox">@T("Only Published Versions")</label>
<br/>
@Html.RadioButtonFor(m => m.DataImportChoice, "Draft", new {id = "Draft"})
<label for="Draft" class="forcheckbox">@T("Only Drafts")</label>
</div>
<div>
@Html.EditorFor(m => m.SiteSettings)
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.SiteSettings)">@T("Site Settings")</label><br/>
@Html.Hint(T("Please verify that you are not exporting confidential information, such as passwords or application keys."))
</div>
</fieldset>
<fieldset>
<legend>@T("Setup")</legend>
<div>
@Html.CheckBoxFor(m => m.SetupRecipe)
@Html.LabelFor(m => m.SetupRecipe, T("Setup Recipe").ToString(), new {@class = "forcheckbox"})
@Html.Hint(T("Check this option if you want to use the generated recipe as a setup recipe."))
</div>
<div data-controllerid="@Html.FieldIdFor(m => m.SetupRecipe)">
<div>
@Html.LabelFor(m => m.RecipeName, T("Name"))
@Html.TextBoxFor(m => m.RecipeName, new { @class = "text medium" })
@Html.Hint(T("Optionally provide a name for the setup recipe."))
var stepName = Html.NameFor(m => m.ExportSteps[exportStepIndex].IsSelected).ToString();
var stepId = stepName.HtmlClassify();
<fieldset class="export-step export-step-@exportStep.Name.HtmlClassify()">
<legend>
<input type="hidden" name="@Html.NameFor(m => m.ExportSteps[exportStepIndex].Name)" value="@Model.ExportSteps[exportStepIndex].Name"/>
<input type="checkbox" id="@stepId" name="@stepName" value="true"/>
<label for="@stepId" class="forcheckbox">@exportStep.DisplayName</label>
</legend>
@Html.Hint(@exportStep.Description)
<div data-controllerid="@stepId">
@Display(exportStep.Editor)
</div>
<div>
@Html.LabelFor(m => m.RecipeDescription, T("Description"))
@Html.TextBoxFor(m => m.RecipeDescription, new {@class = "text large"})
@Html.Hint(T("Optionally provide a description for the setup recipe."))
</div>
<div>
@Html.LabelFor(m => m.RecipeWebsite, T("Website"))
@Html.TextBoxFor(m => m.RecipeWebsite, new {@class = "text large"})
@Html.Hint(T("Optionally provide a website URL for the setup recipe."))
</div>
<div>
@Html.LabelFor(m => m.RecipeTags, T("Tags"))
@Html.TextBoxFor(m => m.RecipeTags, new {@class = "text large"})
@Html.Hint(T("Optionally provide tags for the setup recipe."))
</div>
<div>
@Html.LabelFor(m => m.RecipeVersion, T("Version"))
@Html.TextBoxFor(m => m.RecipeVersion, new {@class = "text small"})
@Html.Hint(T("Optionally provide a version for the setup recipe."))
</div>
</div>
</fieldset>
</fieldset>
exportStepIndex++;
if (exportStepIndex < Model.ExportSteps.Count) {
<hr />
}
}
if (Model.CustomSteps.Any()) {
<fieldset>
<legend>@T("Choose the custom steps to execute in the export file:")</legend>
<ol>
@for (var customStepIndex = 0; customStepIndex < Model.CustomSteps.Count; customStepIndex++) {
<li>
<input type="hidden" value="@Model.CustomSteps[customStepIndex].CustomStep" name="@Html.NameOf(m => m.CustomSteps[customStepIndex].CustomStep)"/>
<input type="checkbox" value="true" name="@Html.NameOf(m => m.CustomSteps[customStepIndex].IsChecked)" id="@Html.NameOf(m => m.CustomSteps[customStepIndex].IsChecked)" />
<label class="forcheckbox" for="@Html.NameOf(m => m.CustomSteps[customStepIndex].IsChecked)">@Model.CustomSteps[customStepIndex].CustomStep.CamelFriendly()</label>
</li>
}
</ol>
</fieldset>
}
<fieldset>
<legend>@T("Choose the custom steps to execute in the export file:")</legend>
<ol>
@for (var customStepIndex = 0; customStepIndex < Model.CustomSteps.Count; customStepIndex++) {
<li>
<input type="hidden" value="@Model.CustomSteps[customStepIndex].CustomStep" name="@Html.NameOf(m => m.CustomSteps[customStepIndex].CustomStep)" />
<input type="checkbox" value="true" name="@Html.NameOf(m => m.CustomSteps[customStepIndex].IsChecked)" id="@Html.NameOf(m => m.CustomSteps[customStepIndex].IsChecked)" />
<label class="forcheckbox" for="@Html.NameOf(m => m.CustomSteps[customStepIndex].IsChecked)">@Model.CustomSteps[customStepIndex].CustomStep.CamelFriendly()</label>
</li>
}
</ol>
</fieldset>
}
<button type="submit" class="primaryAction">@T("Export")</button>
}

View File

@@ -1,5 +1,57 @@
<table>
<thead>
@model Orchard.ImportExport.ViewModels.DataExportStepViewModel
@{
Style.Include("exportstep-data.css");
}
<div>
<table class="items">
<thead>
<tr>
<th>@T("Content Type")</th>
<th>@T("Schema")</th>
<th>@T("Data")</th>
<th>@T("Both")</th>
</tr>
<tr class="sub">
<th>&nbsp;</th>
<th><input type="checkbox"/></th>
<th><input type="checkbox"/></th>
<th><input type="checkbox"/></th>
</tr>
</thead>
<tbody>
@{ var contentTypeIndex = 0;}
@foreach (var contentType in Model.ContentTypes) {
<tr>
<td>@contentType.DisplayName</td>
<td>
<input type="hidden" name="@Html.NameFor(m => m.ContentTypes[contentTypeIndex].Name)" value="@Model.ContentTypes[contentTypeIndex].Name"/>
<input type="checkbox" name="@Html.NameFor(m => m.ContentTypes[contentTypeIndex].ExportSchema)" value="true"/>
</td>
<td>
<input type="checkbox" name="@Html.NameFor(m => m.ContentTypes[contentTypeIndex].ExportData)" value="true"/>
</td>
<td><input type="checkbox"/></td>
</tr>
contentTypeIndex++;
}
</tbody>
</table>
@Html.Hint(T("Choose the types to include in the export file"))
</div>
</thead>
</table>
<div>
@Html.LabelFor(m => m.ImportBatchSize, T("Batch Size"))
@Html.TextBoxFor(m => m.ImportBatchSize, new { @class = "text small" })
@Html.Hint(T("The batch size to use when importing the data. Leave empty to disable batched imports."))
</div>
<div>
<p>@T("Version History")</p>
@Html.RadioButtonFor(m => m.VersionHistoryOptions, "Published", new { id = "Published", Checked = "Checked" })
<label for="Published" class="forcheckbox">@T("Only Published Versions")</label>
<br />
@Html.RadioButtonFor(m => m.VersionHistoryOptions, "Draft", new { id = "Draft" })
<label for="Draft" class="forcheckbox">@T("Only Drafts")</label>
<br />
@Html.RadioButtonFor(m => m.VersionHistoryOptions, "Latest", new { id = "Latest" })
<label for="Latest" class="forcheckbox">@T("Latest Versions")</label>
</div>

View File

@@ -0,0 +1,31 @@
@model Orchard.ImportExport.ViewModels.SetupRecipeStepViewModel
<div>
@Html.LabelFor(m => m.RecipeName, T("Name"))
@Html.TextBoxFor(m => m.RecipeName, new { @class = "text medium" })
@Html.Hint(T("Optionally provide a name for the setup recipe."))
</div>
<div>
@Html.LabelFor(m => m.RecipeDescription, T("Description"))
@Html.TextBoxFor(m => m.RecipeDescription, new {@class = "text large"})
@Html.Hint(T("Optionally provide a description for the setup recipe."))
</div>
<div>
@Html.LabelFor(m => m.RecipeAuthor, T("Author"))
@Html.TextBoxFor(m => m.RecipeAuthor, new { @class = "text large" })
@Html.Hint(T("Optionally provide the name of the author for the setup recipe."))
</div>
<div>
@Html.LabelFor(m => m.RecipeWebsite, T("Website"))
@Html.TextBoxFor(m => m.RecipeWebsite, new { @class = "text large" })
@Html.Hint(T("Optionally provide a website URL for the setup recipe."))
</div>
<div>
@Html.LabelFor(m => m.RecipeTags, T("Tags"))
@Html.TextBoxFor(m => m.RecipeTags, new { @class = "text large" })
@Html.Hint(T("Optionally provide tags for the setup recipe."))
</div>
<div>
@Html.LabelFor(m => m.RecipeVersion, T("Version"))
@Html.TextBoxFor(m => m.RecipeVersion, new { @class = "text small" })
@Html.Hint(T("Optionally provide a version for the setup recipe."))
</div>

View File

@@ -0,0 +1,2 @@
@model Orchard.ImportExport.ViewModels.SiteSettingsStepViewModel
@Html.Hint(T("Please verify that you are not exporting confidential information, such as passwords or application keys."))