Implemented tenant reset action.

This commit is contained in:
Daniel Stolt 2015-07-14 17:43:40 +01:00
parent 3f5919fa40
commit b6d0fa3dd2
10 changed files with 291 additions and 84 deletions

View File

@ -19,7 +19,7 @@ namespace Orchard.MultiTenancy.Controllers {
public AdminController(ITenantService tenantService, IOrchardServices orchardServices, ShellSettings shellSettings) {
_tenantService = tenantService;
_thisShellSettings = shellSettings;
Services = orchardServices;
T = NullLocalizer.Instance;
Logger = NullLogger.Instance;
@ -30,32 +30,34 @@ namespace Orchard.MultiTenancy.Controllers {
public ILogger Logger { get; set; }
public ActionResult Index() {
return View(new TenantsIndexViewModel { TenantSettings = _tenantService.GetTenants() });
return View(new TenantsIndexViewModel {
TenantSettings = _tenantService.GetTenants()
});
}
public ActionResult Add() {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Cannot create tenant")))
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("You don't have permission to create tenants.")))
return new HttpUnauthorizedResult();
if ( !EnsureDefaultTenant() )
if (!IsExecutingInDefaultTenant())
return new HttpUnauthorizedResult();
var model = new TenantAddViewModel();
var viewModel = new TenantAddViewModel();
// fetches all available themes and modules
model.Themes = _tenantService.GetInstalledThemes().Select(x => new ThemeEntry { ThemeId = x.Id, ThemeName = x.Name }).ToList();
model.Modules = _tenantService.GetInstalledModules().Select(x => new ModuleEntry { ModuleId = x.Id, ModuleName = x.Name }).ToList();
// Fetches all available themes and modules.
viewModel.Themes = _tenantService.GetInstalledThemes().Select(x => new ThemeEntry { ThemeId = x.Id, ThemeName = x.Name }).ToList();
viewModel.Modules = _tenantService.GetInstalledModules().Select(x => new ModuleEntry { ModuleId = x.Id, ModuleName = x.Name }).ToList();
return View(model);
return View(viewModel);
}
[HttpPost, ActionName("Add")]
public ActionResult AddPOST(TenantAddViewModel viewModel) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Couldn't create tenant"))) {
public ActionResult AddPost(TenantAddViewModel viewModel) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("You don't have permission to create tenants."))) {
return new HttpUnauthorizedResult();
}
if (!EnsureDefaultTenant()) {
if (!IsExecutingInDefaultTenant()) {
return new HttpUnauthorizedResult();
}
@ -63,7 +65,7 @@ namespace Orchard.MultiTenancy.Controllers {
ModelState.AddModelError("Name", T("A tenant with the same name already exists.", viewModel.Name).Text);
}
// ensure tenants name are valid
// Ensure tenants name are valid.
if (!String.IsNullOrEmpty(viewModel.Name) && !Regex.IsMatch(viewModel.Name, @"^\w+$")) {
ModelState.AddModelError("Name", T("Invalid tenant name. Must contain characters only and no spaces.").Text);
}
@ -88,56 +90,58 @@ namespace Orchard.MultiTenancy.Controllers {
return RedirectToAction("Index");
}
catch (ArgumentException exception) {
Services.Notifier.Error(T("Creating Tenant failed: {0}", exception.Message));
catch (ArgumentException ex) {
Logger.Error(ex, "Error while creating tenant.");
Services.Notifier.Error(T("Tenant creation failed with error: {0}", ex.Message));
return View(viewModel);
}
}
public ActionResult Edit(string name) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Cannot edit tenant")))
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("You don't have permission to edit tenants.")))
return new HttpUnauthorizedResult();
if ( !EnsureDefaultTenant() )
if (!IsExecutingInDefaultTenant())
return new HttpUnauthorizedResult();
var tenant = _tenantService.GetTenants().FirstOrDefault(ss => ss.Name == name);
if (tenant == null)
return HttpNotFound();
return View(new TenantEditViewModel {
Name = tenant.Name,
RequestUrlHost = tenant.RequestUrlHost,
RequestUrlPrefix = tenant.RequestUrlPrefix,
DataProvider = tenant.DataProvider,
DatabaseConnectionString = tenant.DataConnectionString,
DatabaseTablePrefix = tenant.DataTablePrefix,
State = tenant.State,
Themes = _tenantService.GetInstalledThemes().Select(x => new ThemeEntry {
ThemeId = x.Id,
ThemeName = x.Name,
Checked = tenant.Themes.Contains(x.Id)
}).ToList(),
Modules = _tenantService.GetInstalledModules().Select(x => new ModuleEntry {
ModuleId = x.Id,
ModuleName = x.Name,
Checked = tenant.Modules.Contains(x.Id)
}).ToList()
});
Name = tenant.Name,
RequestUrlHost = tenant.RequestUrlHost,
RequestUrlPrefix = tenant.RequestUrlPrefix,
DataProvider = tenant.DataProvider,
DatabaseConnectionString = tenant.DataConnectionString,
DatabaseTablePrefix = tenant.DataTablePrefix,
State = tenant.State,
Themes = _tenantService.GetInstalledThemes().Select(x => new ThemeEntry {
ThemeId = x.Id,
ThemeName = x.Name,
Checked = tenant.Themes.Contains(x.Id)
}).ToList(),
Modules = _tenantService.GetInstalledModules().Select(x => new ModuleEntry {
ModuleId = x.Id,
ModuleName = x.Name,
Checked = tenant.Modules.Contains(x.Id)
}).ToList()
});
}
[HttpPost, ActionName("Edit")]
public ActionResult EditPost(TenantEditViewModel viewModel) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Couldn't edit tenant")))
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("You don't have permission to edit tenants.")))
return new HttpUnauthorizedResult();
if ( !EnsureDefaultTenant() )
if (!IsExecutingInDefaultTenant())
return new HttpUnauthorizedResult();
var tenant = _tenantService.GetTenants().FirstOrDefault(ss => ss.Name == viewModel.Name);
if (tenant == null)
return HttpNotFound();
else if (tenant.Name == _thisShellSettings.Name)
return new HttpUnauthorizedResult();
if (!ModelState.IsValid) {
return View(viewModel);
@ -163,18 +167,19 @@ namespace Orchard.MultiTenancy.Controllers {
return RedirectToAction("Index");
}
catch (Exception exception) {
Services.Notifier.Error(T("Failed to edit tenant: {0} ", exception.Message));
catch (Exception ex) {
Logger.Error(ex, "Error while editing tenant.");
Services.Notifier.Error(T("Failed to edit tenant: {0} ", ex.Message));
return View(viewModel);
}
}
[HttpPost]
public ActionResult Disable(string name) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Couldn't disable tenant")))
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("You don't have permission to disable tenants.")))
return new HttpUnauthorizedResult();
if ( !EnsureDefaultTenant() )
if (!IsExecutingInDefaultTenant())
return new HttpUnauthorizedResult();
var tenant = _tenantService.GetTenants().FirstOrDefault(ss => ss.Name == name);
@ -189,10 +194,10 @@ namespace Orchard.MultiTenancy.Controllers {
[HttpPost]
public ActionResult Enable(string name) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Couldn't enable tenant")))
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("You don't have permission to enable tenants.")))
return new HttpUnauthorizedResult();
if ( !EnsureDefaultTenant() )
if (!IsExecutingInDefaultTenant())
return new HttpUnauthorizedResult();
var tenant = _tenantService.GetTenants().FirstOrDefault(ss => ss.Name == name);
@ -205,7 +210,55 @@ namespace Orchard.MultiTenancy.Controllers {
return RedirectToAction("Index");
}
private bool EnsureDefaultTenant() {
public ActionResult Reset(string name) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("You don't have permission to reset tenants.")))
return new HttpUnauthorizedResult();
if (!IsExecutingInDefaultTenant())
return new HttpUnauthorizedResult();
var tenant = _tenantService.GetTenants().FirstOrDefault(ss => ss.Name == name);
if (tenant == null)
return HttpNotFound();
return View(new TenantResetViewModel() {
Name = name,
DatabaseTableNames = _tenantService.GetTenantDatabaseTableNames(tenant)
});
}
[HttpPost, ActionName("Reset")]
public ActionResult ResetPost(TenantResetViewModel viewModel) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("You don't have permission to reset tenants.")))
return new HttpUnauthorizedResult();
if (!IsExecutingInDefaultTenant())
return new HttpUnauthorizedResult();
var tenant = _tenantService.GetTenants().FirstOrDefault(ss => ss.Name == viewModel.Name);
if (tenant == null)
return HttpNotFound();
else if (tenant.Name == _thisShellSettings.Name)
return new HttpUnauthorizedResult();
if (!ModelState.IsValid) {
viewModel.DatabaseTableNames = _tenantService.GetTenantDatabaseTableNames(tenant);
return View(viewModel);
}
try {
_tenantService.ResetTenant(tenant, viewModel.DropDatabaseTables);
return RedirectToAction("Index");
}
catch (Exception ex) {
Logger.Error(ex, "Error while resetting tenant.");
Services.Notifier.Error(T("Failed to reset tenant: {0} ", ex.Message));
viewModel.DatabaseTableNames = _tenantService.GetTenantDatabaseTableNames(tenant);
return View(viewModel);
}
}
private bool IsExecutingInDefaultTenant() {
return _thisShellSettings.Name == ShellSettings.DefaultName;
}
}

View File

@ -25,6 +25,7 @@
<IISExpressAnonymousAuthentication />
<IISExpressWindowsAuthentication />
<IISExpressUseClassicPipelineMode />
<UseGlobalApplicationHostFile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@ -48,6 +49,12 @@
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="Autofac">
<HintPath>..\..\..\..\lib\autofac\Autofac.dll</HintPath>
</Reference>
<Reference Include="NHibernate">
<HintPath>..\..\..\..\lib\nhibernate\NHibernate.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.DataAnnotations">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
@ -73,9 +80,11 @@
<Compile Include="Controllers\AdminController.cs" />
<Compile Include="Extensions\UrlHelperExtensions.cs" />
<Compile Include="Routes.cs" />
<Compile Include="Services\ITenantResetEventHandler.cs" />
<Compile Include="Services\ITenantService.cs" />
<Compile Include="Services\TenantService.cs" />
<Compile Include="ViewModels\ModuleEntry.cs" />
<Compile Include="ViewModels\TenantResetViewModel.cs" />
<Compile Include="ViewModels\TenantEditViewModel.cs" />
<Compile Include="ViewModels\TenantAddViewModel.cs" />
<Compile Include="ViewModels\TenantsIndexViewModel.cs" />
@ -123,6 +132,9 @@
</SubType>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="Views\Admin\Reset.cshtml" />
</ItemGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>

View File

@ -0,0 +1,10 @@
using Orchard.Events;
namespace Orchard.MultiTenancy.Services {
/// <summary>
/// An event handler interface that allows implementers to execute code when a tenant is being reset.
/// </summary>
public interface ITenantResetEventHandler : IEventHandler {
void Resetting();
}
}

View File

@ -5,23 +5,35 @@ using Orchard.Environment.Extensions.Models;
namespace Orchard.MultiTenancy.Services {
public interface ITenantService : IDependency {
/// <summary>
/// Retrieves all tenants' shell settings.
/// Retrieves ShellSettings objects for all tenants.
/// </summary>
/// <returns>All tenants' shell settings.</returns>
IEnumerable<ShellSettings> GetTenants();
/// <summary>
/// Creates a new tenant.
/// </summary>
/// <param name="settings">Shell settings of the tenant.</param>
/// <param name="settings">A ShellSettings object specifying the settings for the new tenant.</param>
void CreateTenant(ShellSettings settings);
/// <summary>
/// Updates the shell settings of a tenant.
/// Updates the settings of a tenant.
/// </summary>
/// <param name="settings">Shell settings of the tenant.</param>
/// <param name="settings">The new ShellSettings object for the tenant.</param>
void UpdateTenant(ShellSettings settings);
/// <summary>
/// Resets a tenant to its uninitialized state.
/// </summary>
/// <param name="tenantName">A ShellSettings object for the tenant to reset.</param>
/// <param name="dropDatabaseTables">A boolean indicated whether tenant database tables should be dropped also.</param>
void ResetTenant(ShellSettings settings, bool dropDatabaseTables);
/// <summary>
/// Returns a list of all known database tables in a tenant.
/// </summary>
/// <returns>A ShellSettings object for the tenant.</returns>
IEnumerable<string> GetTenantDatabaseTableNames(ShellSettings settings);
/// <summary>
/// Returns a list of all installed themes.
/// </summary>

View File

@ -1,21 +1,36 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using Orchard.Environment.Configuration;
using Orchard.Environment.Extensions.Models;
using Orchard.Environment.Extensions;
using Orchard.Environment.ShellBuilders;
using Orchard.Data.Migration.Interpreters;
using Orchard.Data.Migration.Schema;
using Orchard.Data;
using Orchard.Logging;
namespace Orchard.MultiTenancy.Services {
public class TenantService : ITenantService {
private readonly IShellSettingsManager _shellSettingsManager;
private readonly IExtensionManager _extensionManager;
private readonly IShellContextFactory _shellContextFactory;
private readonly IShellContainerFactory _shellContainerFactory;
public TenantService(
IShellSettingsManager shellSettingsManager,
IExtensionManager extensionManager) {
IExtensionManager extensionManager,
IShellContextFactory shellContextFactory,
IShellContainerFactory shellContainerFactory) {
_shellSettingsManager = shellSettingsManager;
_extensionManager = extensionManager;
_shellContextFactory = shellContextFactory;
_shellContainerFactory = shellContainerFactory;
Logger = NullLogger.Instance;
}
public ILogger Logger { get; set; }
public IEnumerable<ShellSettings> GetTenants() {
return _shellSettingsManager.LoadSettings();
}
@ -28,16 +43,32 @@ namespace Orchard.MultiTenancy.Services {
_shellSettingsManager.SaveSettings(settings);
}
/// <summary>
/// Loads only installed themes
/// </summary>
public void ResetTenant(ShellSettings settings, bool dropDatabaseTables) {
if (settings.State != TenantState.Disabled)
throw new InvalidOperationException(String.Format("Tenant state is '{0}'; must be '{1}' to perform reset action.", settings.State, TenantState.Disabled));
ExecuteOnTenantScope(settings, environment => {
ExecuteResetEventHandlers(environment);
if (dropDatabaseTables)
DropTenantDatabaseTables(environment);
});
settings.State = TenantState.Uninitialized;
_shellSettingsManager.SaveSettings(settings);
}
public IEnumerable<string> GetTenantDatabaseTableNames(ShellSettings settings) {
IEnumerable<string> result = null;
ExecuteOnTenantScope(settings, environment => {
result = GetTenantDatabaseTableNames(environment);
});
return result;
}
public IEnumerable<ExtensionDescriptor> GetInstalledThemes() {
return GetThemes(_extensionManager.AvailableExtensions());
}
/// <summary>
/// Loads only installed modules
/// </summary>
public IEnumerable<ExtensionDescriptor> GetInstalledModules() {
return _extensionManager.AvailableExtensions().Where(descriptor => DefaultExtensionTypes.IsModule(descriptor.ExtensionType));
}
@ -58,5 +89,45 @@ namespace Orchard.MultiTenancy.Services {
}
return themes;
}
private void ExecuteOnTenantScope(ShellSettings settings, Action<IWorkContextScope> action) {
var shellContext = _shellContextFactory.CreateShellContext(settings);
using (var container = _shellContainerFactory.CreateContainer(shellContext.Settings, shellContext.Blueprint)) {
using (var environment = container.CreateWorkContextScope()) {
action(environment);
}
}
}
private IEnumerable<string> GetTenantDatabaseTableNames(IWorkContextScope environment) {
var sessionFactoryHolder = environment.Resolve<ISessionFactoryHolder>();
var schemaBuilder = new SchemaBuilder(environment.Resolve<IDataMigrationInterpreter>());
var configuration = sessionFactoryHolder.GetConfiguration();
var result =
from mapping in configuration.ClassMappings
select mapping.Table.Name;
return result.ToArray();
}
private void DropTenantDatabaseTables(IWorkContextScope environment) {
var sessionFactoryHolder = environment.Resolve<ISessionFactoryHolder>();
var schemaBuilder = new SchemaBuilder(environment.Resolve<IDataMigrationInterpreter>());
var configuration = sessionFactoryHolder.GetConfiguration();
foreach (var mapping in configuration.ClassMappings) {
try {
schemaBuilder.DropTable(mapping.Table.Name);
}
catch (Exception ex) {
Logger.Warning(ex, "Failed to drop table '{0}'.", mapping.Table.Name);
}
}
}
private void ExecuteResetEventHandlers(IWorkContextScope environment) {
var handler = environment.Resolve<ITenantResetEventHandler>();
handler.Resetting();
}
}
}

View File

@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.Linq;
namespace Orchard.MultiTenancy.ViewModels {
public class TenantResetViewModel {
public TenantResetViewModel() {
DatabaseTableNames = Enumerable.Empty<string>();
}
[Required]
public string Name { get; set; }
public bool DropDatabaseTables { get; set; }
public IEnumerable<string> DatabaseTableNames { get; set; }
}
}

View File

@ -1,7 +1,8 @@
@model Orchard.Environment.Configuration.ShellSettings
@using Orchard.MultiTenancy.Extensions;
@using(Html.BeginFormAntiForgeryPost(Url.Action("enable", new {area = "Orchard.MultiTenancy"}), FormMethod.Post, new {@class = "inline link"})) {
@using(Html.BeginFormAntiForgeryPost(Url.Action("Enable", new {area = "Orchard.MultiTenancy"}), FormMethod.Post, new {@class = "inline link"})) {
@Html.HiddenFor(ss => ss.Name)
<button type="submit">@T("Resume")</button>
}
} @T(" | ")
@Html.ActionLink(T("Reset").ToString(), "Reset", new { name = Model.Name, area = "Orchard.MultiTenancy" })

View File

@ -1,7 +1,7 @@
@model Orchard.Environment.Configuration.ShellSettings
@using Orchard.MultiTenancy.Extensions;
@using(Html.BeginFormAntiForgeryPost(Url.Action("disable", new {area = "Orchard.MultiTenancy"}), FormMethod.Post, new {@class = "inline link"})) {
@using(Html.BeginFormAntiForgeryPost(Url.Action("Disable", new {area = "Orchard.MultiTenancy"}), FormMethod.Post, new {@class = "inline link"})) {
@Html.HiddenFor(ss => ss.Name)
<button type="submit">@T("Suspend")</button>
}
}

View File

@ -7,30 +7,31 @@
Layout.Title = T("List of Site's Tenants").ToString();
}
<div class="manage">@Html.ActionLink(T("Add a Tenant").ToString(), "Add", new {area = "Orchard.MultiTenancy"}, new { @class = "button primaryAction" })</div>
<div class="manage">@Html.ActionLink(T("Add a Tenant").ToString(), "Add", new { area = "Orchard.MultiTenancy" }, new { @class = "button primaryAction" })</div>
<ul class="contentItems tenants">
@foreach (var tenant in Model.TenantSettings) {
<li class="tenant @tenant.State">
<div class="summary">
<div class="properties">
<h3>@tenant.Name @if (!string.IsNullOrEmpty(tenant.RequestUrlHost)) {
var tenantClone = new ShellSettings(tenant);
foreach (var t in tenant.RequestUrlHost.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) {
tenantClone.RequestUrlHost = t;
var url = Url.Tenant(tenantClone);
<span class="tenantHost"> - @Html.Link(url, url)</span>
}
}
</h3>
</div>
<div class="related">
@if (!string.Equals(tenant.Name, "default", StringComparison.OrdinalIgnoreCase)) { //todo: (heskew) base this off the view model so logic on what can be removed and have its state changed stays in the controller
@foreach (var tenant in Model.TenantSettings) {
<li class="tenant @tenant.State">
<div class="summary">
<div class="properties">
<h3>
@tenant.Name @if (!string.IsNullOrEmpty(tenant.RequestUrlHost)) {
var tenantClone = new ShellSettings(tenant);
foreach (var t in tenant.RequestUrlHost.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) {
tenantClone.RequestUrlHost = t;
var url = Url.Tenant(tenantClone);
<span class="tenantHost"> - @Html.Link(url, url)</span>
}
}
</h3>
</div>
<div class="related">
@if (!String.Equals(tenant.Name, "default", StringComparison.OrdinalIgnoreCase)) { //todo: (heskew) base this off the view model so logic on what can be removed and have its state changed stays in the controller
var t = tenant;
@Html.DisplayFor(m => t, string.Format("ActionsFor{0}", tenant.State.ToString()), "") @T(" | ")
}
@Html.ActionLink(T("Edit").ToString(), "Edit", new {name = tenant.Name, area = "Orchard.MultiTenancy"})
@Html.DisplayFor(m => t, String.Format("ActionsFor{0}", tenant.State.ToString()), "") @T(" | ")
}
@Html.ActionLink(T("Edit").ToString(), "Edit", new { name = tenant.Name, area = "Orchard.MultiTenancy" })
</div>
</div>
</div>
</li>
</li>
}
</ul>

View File

@ -0,0 +1,30 @@
@model Orchard.MultiTenancy.ViewModels.TenantResetViewModel
@{
Layout.Title = T("Reset Tenant").ToString();
Script.Require("jQuery");
Script.Include(Url.Content("~/Themes/TheAdmin/Scripts/admin.js")).AtFoot();
}
@using (Html.BeginFormAntiForgeryPost()) {
@Html.ValidationSummary()
<fieldset>
<p>@T("This will reset the tenant <strong>{0}</strong> to its uninitialized state, allowing you to set it up again.", Model.Name)</p>
</fieldset>
<fieldset>
@Html.CheckBoxFor(m => Model.DropDatabaseTables)
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.DropDatabaseTables)">@("Also delete tenant database tables:")</label>
<ul style="margin-left: 4em; margin-top: 1em; -webkit-column-width: 24em; -moz-column-width: 24em; column-width: 24em;">
@foreach (var tableName in Model.DatabaseTableNames) {
<li><span class="hint">@tableName</span></li>
}
</ul>
</fieldset>
<fieldset>
<button class="primaryAction" type="submit">@T("Reset")</button>
</fieldset>
}