Adding some feature enable/disable UI

--HG--
branch : dev
This commit is contained in:
Nathan Heskew
2010-04-28 11:47:29 -07:00
parent f288e3ec08
commit 0b3dbcb161
16 changed files with 294 additions and 49 deletions

View File

@@ -20,4 +20,5 @@ Scenario: Features of installed modules are listed
Given I have installed Orchard Given I have installed Orchard
When I go to "admin/modules/features" When I go to "admin/modules/features"
Then I should see "<h2>Available Features</h2>" Then I should see "<h2>Available Features</h2>"
And I should see "<h3>Common</h3>"
And the status should be 200 OK And the status should be 200 OK

View File

@@ -104,6 +104,8 @@ this.ScenarioSetup(scenarioInfo);
#line 22 #line 22
testRunner.Then("I should see \"<h2>Available Features</h2>\""); testRunner.Then("I should see \"<h2>Available Features</h2>\"");
#line 23 #line 23
testRunner.And("I should see \"<h3>Common</h3>\"");
#line 24
testRunner.And("the status should be 200 OK"); testRunner.And("the status should be 200 OK");
#line hidden #line hidden
testRunner.CollectScenarioErrors(); testRunner.CollectScenarioErrors();

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,8 +1,13 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Web.Mvc; using System.Web.Mvc;
using Orchard.Localization; using Orchard.Localization;
using Orchard.Modules.ViewModels; using Orchard.Modules.ViewModels;
using Orchard.Mvc.AntiForgery;
using Orchard.Mvc.Results; using Orchard.Mvc.Results;
using Orchard.UI.Notify;
namespace Orchard.Modules.Controllers { namespace Orchard.Modules.Controllers {
public class AdminController : Controller { public class AdminController : Controller {
@@ -39,12 +44,85 @@ namespace Orchard.Modules.Controllers {
}); });
} }
public ActionResult Features() { public ActionResult Features(FeaturesOptions options) {
if (!Services.Authorizer.Authorize(Permissions.ManageFeatures, T("Not allowed to manage features"))) if (!Services.Authorizer.Authorize(Permissions.ManageFeatures, T("Not allowed to manage features")))
return new HttpUnauthorizedResult(); return new HttpUnauthorizedResult();
var features = _moduleService.GetAvailableFeatures(); var features = _moduleService.GetAvailableFeatures();
return View(new FeatureListViewModel {Features = features}); return View(new FeaturesViewModel {Features = features, Options = options});
}
[HttpPost, ActionName("Features")]
[FormValueRequired("submit.BulkEdit")]
public ActionResult FeaturesPOST(FeaturesOptions options, IList<string> selection) {
if (selection != null && selection.Count > 0)
{
switch (options.BulkAction)
{
case FeaturesBulkAction.None:
break;
case FeaturesBulkAction.Enable:
if (!Services.Authorizer.Authorize(Permissions.ManageFeatures, T("Not allowed to enable features")))
return new HttpUnauthorizedResult();
_moduleService.EnableFeatures(selection);
//todo: (heskew) need better messages
//todo: (heskew) hmmm...need a helper to comma-separate all but last, which would get the " and " treatment...all localized, of course
Services.Notifier.Information(T("{0} were enabled", string.Join(", ", selection.ToArray())));
break;
case FeaturesBulkAction.Disable:
if (!Services.Authorizer.Authorize(Permissions.ManageFeatures, T("Not allowed to disable features")))
return new HttpUnauthorizedResult();
_moduleService.DisableFeatures(selection);
//todo: (heskew) need better messages
Services.Notifier.Information(T("{0} were disabled", string.Join(", ", selection.ToArray())));
break;
default:
throw new ArgumentOutOfRangeException();
}
}
return RedirectToAction("Features");
}
[ValidateAntiForgeryTokenOrchard]
public ActionResult Enable(string featureName) {
if (!Services.Authorizer.Authorize(Permissions.ManageFeatures, T("Not allowed to manage features")))
return new HttpUnauthorizedResult();
if (string.IsNullOrEmpty(featureName))
return new NotFoundResult();
_moduleService.EnableFeatures(new [] {featureName});
Services.Notifier.Information(T("{0} was enabled", featureName));
return RedirectToAction("Features");
}
[ValidateAntiForgeryTokenOrchard]
public ActionResult Disable(string featureName) {
if (!Services.Authorizer.Authorize(Permissions.ManageFeatures, T("Not allowed to manage features")))
return new HttpUnauthorizedResult();
if (string.IsNullOrEmpty(featureName))
return new NotFoundResult();
_moduleService.DisableFeatures(new[] { featureName });
Services.Notifier.Information(T("{0} was disabled", featureName));
return RedirectToAction("Features");
}
private class FormValueRequiredAttribute : ActionMethodSelectorAttribute {
private readonly string _submitButtonName;
public FormValueRequiredAttribute(string submitButtonName) {
_submitButtonName = submitButtonName;
}
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) {
var value = controllerContext.HttpContext.Request.Form[_submitButtonName];
return !string.IsNullOrEmpty(value);
}
} }
} }
} }

View File

@@ -0,0 +1,8 @@
using Orchard.Environment.Extensions.Models;
namespace Orchard.Modules.Models {
public class ModuleFeature : IModuleFeature {
public FeatureDescriptor Descriptor { get; set; }
public bool IsEnabled { get; set; }
}
}

View File

@@ -63,7 +63,9 @@
<ItemGroup> <ItemGroup>
<Compile Include="AdminMenu.cs" /> <Compile Include="AdminMenu.cs" />
<Compile Include="Controllers\AdminController.cs" /> <Compile Include="Controllers\AdminController.cs" />
<Compile Include="ViewModels\FeatureListViewModel.cs" /> <Compile Include="Models\ModuleFeature.cs" />
<Compile Include="Routing\FeatureNameConstraint.cs" />
<Compile Include="ViewModels\FeaturesViewModel.cs" />
<Compile Include="Models\Module.cs" /> <Compile Include="Models\Module.cs" />
<Compile Include="Permissions.cs" /> <Compile Include="Permissions.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
@@ -85,6 +87,8 @@
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="Content\Admin\images\disabled.gif" />
<Content Include="Content\Admin\images\enabled.gif" />
<Content Include="Views\Admin\Features.ascx" /> <Content Include="Views\Admin\Features.ascx" />
<Content Include="Views\Admin\Edit.ascx" /> <Content Include="Views\Admin\Edit.ascx" />
<Content Include="Views\Web.config" /> <Content Include="Views\Web.config" />

View File

@@ -7,9 +7,11 @@ using Orchard.Mvc.Routes;
namespace Orchard.Modules { namespace Orchard.Modules {
public class Routes : IRouteProvider { public class Routes : IRouteProvider {
private readonly IModuleNameConstraint _moduleNameConstraint; private readonly IModuleNameConstraint _moduleNameConstraint;
private readonly IFeatureNameConstraint _featureNameConstraint;
public Routes(IModuleNameConstraint moduleNameConstraint) { public Routes(IModuleNameConstraint moduleNameConstraint, IFeatureNameConstraint featureNameConstraint) {
_moduleNameConstraint = moduleNameConstraint; _moduleNameConstraint = moduleNameConstraint;
_featureNameConstraint = featureNameConstraint;
} }
public IEnumerable<RouteDescriptor> GetRoutes() { public IEnumerable<RouteDescriptor> GetRoutes() {
@@ -29,6 +31,38 @@ namespace Orchard.Modules {
{"area", "Orchard.Modules"} {"area", "Orchard.Modules"}
}, },
new MvcRouteHandler()) new MvcRouteHandler())
},
new RouteDescriptor {
Route = new Route(
"Admin/Modules/Enable/{featureName}",
new RouteValueDictionary {
{"area", "Orchard.Modules"},
{"controller", "Admin"},
{"action", "Enable"}
},
new RouteValueDictionary {
{"featureName", _featureNameConstraint}
},
new RouteValueDictionary {
{"area", "Orchard.Modules"}
},
new MvcRouteHandler())
},
new RouteDescriptor {
Route = new Route(
"Admin/Modules/Disable/{featureName}",
new RouteValueDictionary {
{"area", "Orchard.Modules"},
{"controller", "Admin"},
{"action", "Disable"}
},
new RouteValueDictionary {
{"featureName", _featureNameConstraint}
},
new RouteValueDictionary {
{"area", "Orchard.Modules"}
},
new MvcRouteHandler())
} }
}; };
} }

View File

@@ -0,0 +1,28 @@
using System;
using System.Linq;
using System.Web;
using System.Web.Routing;
namespace Orchard.Modules.Routing {
public interface IFeatureNameConstraint : IRouteConstraint, ISingletonDependency {
}
public class FeatureNameConstraint : IFeatureNameConstraint {
private readonly IModuleService _moduleService;
public FeatureNameConstraint(IModuleService moduleService) {
_moduleService = moduleService;
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) {
if (routeDirection == RouteDirection.UrlGeneration)
return true;
object value;
if (values.TryGetValue(parameterName, out value))
return _moduleService.GetModuleByFeatureName(Convert.ToString(value)) != null;
return false;
}
}
}

View File

@@ -4,19 +4,23 @@ using System.Linq;
using System.Web; using System.Web;
using Orchard.Environment.Extensions; using Orchard.Environment.Extensions;
using Orchard.Environment.Extensions.Models; using Orchard.Environment.Extensions.Models;
using Orchard.Environment.Topology;
using Orchard.Environment.Topology.Models;
using Orchard.Modules.Models; using Orchard.Modules.Models;
namespace Orchard.Modules.Services { namespace Orchard.Modules.Services {
public class ModuleService : IModuleService { public class ModuleService : IModuleService {
private const string ModuleExtensionType = "module"; private const string ModuleExtensionType = "module";
private readonly IExtensionManager _extensionManager; private readonly IExtensionManager _extensionManager;
private readonly IShellDescriptorManager _shellDescriptorManager;
public ModuleService(IExtensionManager extensionManager) { public ModuleService(IExtensionManager extensionManager, IShellDescriptorManager shellDescriptorManager) {
_extensionManager = extensionManager; _extensionManager = extensionManager;
_shellDescriptorManager = shellDescriptorManager;
} }
public IModule GetModuleByName(string moduleName) { public IModule GetModuleByName(string moduleName) {
return _extensionManager.AvailableExtensions().Where(e => string.Equals(e.Name, moduleName, StringComparison.OrdinalIgnoreCase) && string.Equals(e.ExtensionType, "Module", StringComparison.OrdinalIgnoreCase)).Select( return _extensionManager.AvailableExtensions().Where(e => string.Equals(e.Name, moduleName, StringComparison.OrdinalIgnoreCase) && string.Equals(e.ExtensionType, ModuleExtensionType, StringComparison.OrdinalIgnoreCase)).Select(
descriptor => AssembleModuleFromDescriptor(descriptor)).FirstOrDefault(); descriptor => AssembleModuleFromDescriptor(descriptor)).FirstOrDefault();
} }
@@ -35,24 +39,41 @@ namespace Orchard.Modules.Services {
_extensionManager.UninstallExtension(ModuleExtensionType, moduleName); _extensionManager.UninstallExtension(ModuleExtensionType, moduleName);
} }
public IEnumerable<Feature> GetAvailableFeatures() { public IModule GetModuleByFeatureName(string featureName) {
return GetInstalledModules() return GetInstalledModules()
.Where(m => m.Features != null) .Where(
.SelectMany(m => _extensionManager.LoadFeatures(m.Features)); m =>
m.Features.FirstOrDefault(f => string.Equals(f.Name, featureName, StringComparison.OrdinalIgnoreCase)) !=
null).FirstOrDefault();
}
public IEnumerable<IModuleFeature> GetAvailableFeatures() {
var enabledFeatures = _shellDescriptorManager.GetShellDescriptor().EnabledFeatures;
return GetInstalledModules()
.SelectMany(m => _extensionManager.LoadFeatures(m.Features))
.Select(f => AssembleModuleFromDescriptor(f, enabledFeatures.FirstOrDefault(sf => string.Equals(sf.Name, f.Descriptor.Name, StringComparison.OrdinalIgnoreCase)) != null));
} }
public IEnumerable<Feature> GetAvailableFeaturesByModule(string moduleName) { public IEnumerable<Feature> GetAvailableFeaturesByModule(string moduleName) {
var module = GetModuleByName(moduleName); throw new NotImplementedException();
if (module == null || module.Features == null)
return null;
return _extensionManager.LoadFeatures(module.Features);
} }
public void EnableFeatures(IEnumerable<string> featureNames) { public void EnableFeatures(IEnumerable<string> featureNames) {
var shellDescriptor = _shellDescriptorManager.GetShellDescriptor();
var enabledFeatures = shellDescriptor.EnabledFeatures
.Union(featureNames.Select(s => new ShellFeature {Name = s}));
_shellDescriptorManager.UpdateShellDescriptor(shellDescriptor.SerialNumber, enabledFeatures, shellDescriptor.Parameters);
} }
public void DisableFeatures(IEnumerable<string> featureNames) { public void DisableFeatures(IEnumerable<string> featureNames) {
var shellDescriptor = _shellDescriptorManager.GetShellDescriptor();
var enabledFeatures = shellDescriptor.EnabledFeatures.ToList();
enabledFeatures.RemoveAll(f => featureNames.Contains(f.Name));
_shellDescriptorManager.UpdateShellDescriptor(shellDescriptor.SerialNumber, enabledFeatures, shellDescriptor.Parameters);
} }
private static IModule AssembleModuleFromDescriptor(ExtensionDescriptor extensionDescriptor) { private static IModule AssembleModuleFromDescriptor(ExtensionDescriptor extensionDescriptor) {
@@ -67,5 +88,12 @@ namespace Orchard.Modules.Services {
Features = extensionDescriptor.Features Features = extensionDescriptor.Features
}; };
} }
private static IModuleFeature AssembleModuleFromDescriptor(Feature feature, bool isEnabled) {
return new ModuleFeature {
Descriptor = feature.Descriptor,
IsEnabled = isEnabled
};
}
} }
} }

View File

@@ -1,9 +0,0 @@
using System.Collections.Generic;
using Orchard.Environment.Extensions.Models;
using Orchard.Mvc.ViewModels;
namespace Orchard.Modules.ViewModels {
public class FeatureListViewModel : BaseViewModel {
public IEnumerable<Feature> Features { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
using System.Collections.Generic;
using Orchard.Mvc.ViewModels;
namespace Orchard.Modules.ViewModels {
public class FeaturesViewModel : BaseViewModel {
public IEnumerable<IModuleFeature> Features { get; set; }
public FeaturesOptions Options { get; set; }
}
public class FeaturesOptions {
public FeaturesBulkAction BulkAction { get; set; }
}
public enum FeaturesBulkAction {
None,
Enable,
Disable
}
}

View File

@@ -1,33 +1,75 @@
<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl<FeatureListViewModel>" %> <%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl<FeaturesViewModel>" %>
<%@ Import Namespace="Orchard.Mvc.Html"%> <%@ Import Namespace="Orchard.Mvc.Html"%>
<%@ Import Namespace="Orchard.Modules.ViewModels"%> <%@ Import Namespace="Orchard.Modules.ViewModels"%>
<h1><%=Html.TitleForPage(T("Manage Features").ToString()) %></h1> <h1><%=Html.TitleForPage(T("Manage Features").ToString()) %></h1>
<h2><%=T("Available Features") %></h2> <div class="manage"><%=Html.ActionLink(T("∞").ToString(), "Features", new { }, new { @class = "button primaryAction" })%></div>
<% if (Model.Features.Count() > 0) { %> <% if (Model.Features.Count() > 0) {
<ul class="contentItems blogs"><%
foreach (var featureGroup in Model.Features.OrderBy(f => f.Descriptor.Name).GroupBy(f => f.Descriptor.Category)) { %> using (Html.BeginFormAntiForgeryPost()) { %>
<li> <%=Html.ValidationSummary()%>
<h3><%=Html.Encode(featureGroup.First().Descriptor.Category ?? T("General")) %></h3> <fieldset class="actions bulk">
<ul><% <label for="publishActions"><%=_Encoded("Actions: ")%></label>
foreach (var feature in featureGroup.Select(f => f.Descriptor)) {%> <select id="publishActions" name="<%=Html.NameOf(m => m.Options.BulkAction) %>">
<%=Html.SelectOption(Model.Options.BulkAction, FeaturesBulkAction.None, _Encoded("Choose action...").ToString())%>
<%=Html.SelectOption(Model.Options.BulkAction, FeaturesBulkAction.Enable, _Encoded("Enable").ToString())%>
<%=Html.SelectOption(Model.Options.BulkAction, FeaturesBulkAction.Disable, _Encoded("Disable").ToString())%>
</select>
<input class="button" type="submit" name="submit.BulkEdit" value="<%=_Encoded("Apply") %>" />
</fieldset>
<fieldset class="pageList">
<ul class="contentItems"><%
foreach (var featureGroup in Model.Features.OrderBy(f => f.Descriptor.Name).GroupBy(f => f.Descriptor.Category)) { %>
<li> <li>
<h4><%=Html.Encode(feature.Name) %></h4> <h2><%=Html.Encode(featureGroup.First().Descriptor.Category ?? T("General")) %></h2>
<p><%=T("From: {0}", Html.Encode(feature.Extension.DisplayName)) %></p><%
if (!string.IsNullOrEmpty(feature.Description)) { %>
<p><%=Html.Encode(feature.Description) %></p><%
}
if (feature.Dependencies.Count() > 0) {%>
<h5><%=_Encoded("Depends on:")%></h5>
<ul><% <ul><%
foreach (var dependency in feature.Dependencies) { %> foreach (var feature in featureGroup) {%>
<li><%=Html.Encode(dependency) %></li><% <li>
<div class="summary">
<div class="properties">
<input type="checkbox" name="selection" value="<%=Html.Encode(feature.Descriptor.Name) %>" />
<h3><%=Html.Encode(feature.Descriptor.Name) %></h3>
<ul class="pageStatus">
<li><%
//enabled or not
if (feature.IsEnabled) { %>
<img class="icon" src="<%=ResolveUrl("~/Modules/Orchard.Modules/Content/Admin/images/enabled.gif") %>" alt="<%=_Encoded("Enabled") %>" title="<%=_Encoded("This feature is currently enabled") %>" /><%=_Encoded("Enabled") %><%
}
else { %>
<img class="icon" src="<%=ResolveUrl("~/Modules/Orchard.Modules/Content/Admin/images/disabled.gif") %>" alt="<%=_Encoded("Disabled") %>" title="<%=_Encoded("This feature is currently disabled") %>" /><%=_Encoded("Disabled")%><%
} %>
</li><%
//dependencies
if (feature.Descriptor.Dependencies.Count() > 0) { %>
<li>&nbsp;&#124;&nbsp;
<%=_Encoded("Depends on: ") %><%
foreach (var dependency in feature.Descriptor.Dependencies) {
%><% if (dependency != feature.Descriptor.Dependencies.First()) { %><%=_Encoded(", ") %><% }
%><%=Html.Encode(dependency) %><%
} %>
</li><%
}
//dependencies == nothing <- temporary just to get some stuff in the UI
else { %>
<li>&nbsp;&#124;&nbsp;
<%=T("Depends on: <em>nothing</em>") %>
</li><%
} %>
</ul>
</div>
<div class="related"><%
if (feature.IsEnabled) { %>
<a href="<%=Html.AntiForgeryTokenGetUrl(Url.Action("Disable", new { featureName = feature.Descriptor.Name, area = "Orchard.Modules" })) %>"><%=_Encoded("Disable") %></a><%
} else { %>
<a href="<%=Html.AntiForgeryTokenGetUrl(Url.Action("Enable", new { featureName = feature.Descriptor.Name, area = "Orchard.Modules" })) %>"><%=_Encoded("Enable") %></a><%
} %>
</div>
</div>
</li><%
} %> } %>
</ul><% </ul>
}%>
</li><% </li><%
} %> } %>
</ul> </ul><%
</li><% } %>
} %> </fieldset><%
</ul><% } %>
} %>

View File

@@ -0,0 +1,8 @@
using Orchard.Environment.Extensions.Models;
namespace Orchard.Modules {
public interface IModuleFeature {
FeatureDescriptor Descriptor { get; set; }
bool IsEnabled { get; set; }
}
}

View File

@@ -8,7 +8,8 @@ namespace Orchard.Modules {
IEnumerable<IModule> GetInstalledModules(); IEnumerable<IModule> GetInstalledModules();
void InstallModule(HttpPostedFileBase file); void InstallModule(HttpPostedFileBase file);
void UninstallModule(string moduleName); void UninstallModule(string moduleName);
IEnumerable<Feature> GetAvailableFeatures(); IModule GetModuleByFeatureName(string featureName);
IEnumerable<IModuleFeature> GetAvailableFeatures();
void EnableFeatures(IEnumerable<string> featureNames); void EnableFeatures(IEnumerable<string> featureNames);
void DisableFeatures(IEnumerable<string> featureNames); void DisableFeatures(IEnumerable<string> featureNames);
} }

View File

@@ -191,6 +191,7 @@
<Compile Include="Environment\Extensions\OrchardFeatureAttribute.cs" /> <Compile Include="Environment\Extensions\OrchardFeatureAttribute.cs" />
<Compile Include="Environment\Extensions\ShellTopology.cs" /> <Compile Include="Environment\Extensions\ShellTopology.cs" />
<Compile Include="Modules\IModule.cs" /> <Compile Include="Modules\IModule.cs" />
<Compile Include="Modules\IModuleFeature.cs" />
<Compile Include="Modules\IModuleService.cs" /> <Compile Include="Modules\IModuleService.cs" />
<Compile Include="Mvc\AntiForgery\ValidateAntiForgeryTokenOrchardAttribute.cs" /> <Compile Include="Mvc\AntiForgery\ValidateAntiForgeryTokenOrchardAttribute.cs" />
<Compile Include="Mvc\Extensions\ControllerExtensions.cs" /> <Compile Include="Mvc\Extensions\ControllerExtensions.cs" />