Merge branch 'dev' into feature/recipesteps

This commit is contained in:
Sipke Schoorstra
2015-07-15 14:53:07 +01:00
25 changed files with 774 additions and 192 deletions

View File

@@ -88,6 +88,13 @@
</Properties>
</Component>
<Component Type="Orchard.Environment.Descriptor.ShellDescriptorCache">
<Properties>
<!-- Set Value="true" to disable shell descriptors cache (cache.dat). Recommended when using multiple instances. -->
<Property Name="Disabled" Value="false"/>
</Properties>
</Component>
<Component Type="Orchard.Services.ClientAddressAccessor">
<Properties>
<!-- Set Value="true" to read the client host address from the specified HTTP header. -->

View File

@@ -85,7 +85,7 @@
<levelMin value="ERROR" />
</filter>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %logger - %P{Tenant} - %message%newline%P{Url}%newline" />
<conversionPattern value="%date [%thread] %logger - %P{Tenant} - %message [%P{Url}]%newline" />
</layout>
</appender>

View File

@@ -1,5 +1,4 @@
(function ($) {
var LayoutDesignerHost = function (element) {
var self = this;
this.element = element;
@@ -125,5 +124,16 @@
$(function () {
var host = new LayoutDesignerHost($(".layout-designer"));
$(".layout-designer").each(function (e) {
var designer = $(this);
var dialog = designer.find(".layout-editor-help-dialog");
designer.find(".layout-editor-help-link").click(function (e) {
dialog.dialog({
modal: true,
width: 840
});
e.preventDefault();
});
});
});
})(jQuery);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,20 @@
@import "Variables.less";
.layout-editor-toolbar {
display: flex;
justify-content: space-between;
position: relative;
top: 10px;
.layout-editor-toolbar-group {
display: flex;
> li + li {
margin-left: 12px;
}
}
}
.layout-editor {
display: flex;
margin-top: 1em;
@@ -32,3 +47,53 @@
background: #e8e8e8;
}
}
.layout-editor-help-dialog {
display: none;
.help-row {
&:before, &:after {
content: " "; // 1
display: table; // 2
}
&:after {
clear: both;
}
> .help-column-full, > .help-column-half {
margin: 0.5em 0;
}
> .help-column-half {
box-sizing: border-box;
float: left;
width: 50%;
&:nth-child(2n) {
padding-right: 10px;
clear: left;
}
}
+ .help-row {
margin-top: 1em;
}
}
code {
border-radius: 4px;
background-color: #f3f4f5;
padding: 2px 4px;
font-family: monospace;
}
p {
margin-bottom: 0.5em;
line-height: 1.6em;
}
table > tbody > tr > td:first-child {
padding-right: 10px;
}
}

View File

@@ -16,6 +16,7 @@
Script.Require("jQueryUI_Sortable");
Script.Require("jQueryUI_Resizable");
Script.Require("jQueryUI_Position");
Script.Require("jQueryUI_Dialog");
Script.Require("TinyMce");
Script.Require("Layouts.LayoutEditor");
Script.Include("jquery.deserialize.js");
@@ -35,11 +36,11 @@
angular
.module("LayoutEditor")
.constant("environment", {
templateUrl: function(templateName) {
templateUrl: function (templateName) {
return "@Url.Action("Get", "Template", new { area = "Orchard.Layouts" })" + "/" + templateName;
}
});
(function() {
(function () {
var editorConfig = JSON.parse(LayoutEditor.decode("@Html.Raw(Url.Encode(Model.ConfigurationData))"));
var editorCanvasData = JSON.parse(LayoutEditor.decode("@Html.Raw(Url.Encode(Model.Data))"));
@@ -52,7 +53,8 @@
var contentType = Model.Content != null ? Model.Content.ContentItem.ContentType : default(string);
}
<div class="layout-designer"
<fieldset>
<div class="layout-designer"
data-modelstate-valid="@ViewData.ModelState.IsValid.ToString().ToLower()"
data-display-type="Design"
data-edit-url="@Url.Action("Edit", "Element", new { session = Model.SessionKey, contentId = contentId, contentType = contentType, area = "Orchard.Layouts" })"
@@ -67,11 +69,16 @@
@Html.HiddenFor(m => m.SessionKey)
@Html.HiddenFor(m => m.Data, new { @class = "layout-data-field" })
<fieldset>
<div class="layout-editor-toolbar">
<ol class="layout-editor-toolbar-group">
<li>
<label>@T("Layout")</label>
<div class="group canvas-toolbar">
<div class="pull-right">
<ol class="group">
</li>
</ol>
<ol class="layout-editor-toolbar-group">
<li>
<a class="layout-editor-help-link" href="#"><i class="fa fa-info-circle"></i> Clipboard, keyboard shortcuts, etc.</a>
</li>
@if (Model.Templates.Any()) {
var options = Model.Templates.Select(x => new SelectListItem { Text = Html.ItemDisplayText(x).ToString(), Value = x.Id.ToString(CultureInfo.InvariantCulture), Selected = x.Id == Model.TemplateId });
<li>
@@ -85,11 +92,131 @@
}
</ol>
</div>
</div>
<div class="layout-editor-holder">
<orc-layout-editor model="window.layoutEditor" ng-app="LayoutEditor" />
</div>
</fieldset>
<div class="trash"></div>
@Display.DialogTemplate(Name: "Layout")
</div>
<div class="layout-editor-help-dialog" title="Layout editor help">
<div class="help-row">
<h3>Clipboard</h3>
<div class="help-column-full">
<p>Elements (including containers) can be cut, copied and pasted using the standard clipboard shortcuts (<code>Ctrl+X</code> / <code>Ctrl+C</code> / <code>Ctrl-V</code> on Windows, <code>⌘+X</code> / <code>⌘+C</code> / <code>⌘+V</code> on Mac OS).</p>
<p>On browsers that support native clipboard events, clipboard operations can be performed across different layout editor instances, in different tabs or browser windows. Text content can also be pasted into other applications.</p>
<p>On other browsers, clipboard operations work only within the same layout editor instance.</p>
</div>
</div>
<div class="help-row">
<h3>Keyboard shortcuts</h3>
<div class="help-column-half">
<h4>Resizing columns</h4>
<table>
<tbody>
<tr>
<td><code>Alt+Left</code></td>
<td>Moves the left edge of the focused column left</td>
</tr>
<tr>
<td><code>Alt+Right</code></td>
<td>Moves the left edge of the focused column right</td>
</tr>
<tr>
<td><code>Shift+Left</code></td>
<td>Moves the right edge of the focused column left</td>
</tr>
<tr>
<td><code>Shift+Right</code></td>
<td>Moves the right edge of the focused column right</td>
</tr>
</tbody>
</table>
<p>The <code>Alt</code> and <code>Shift</code> keys can also be combined to move both edges simultaneously.</p>
</div>
<div class="help-column-half">
<h4>Focus</h4>
<table>
<tbody>
<tr>
<td><code>Up</code></td>
<td>Moves focus to the previous element (above)</td>
</tr>
<tr>
<td><code>Down</code></td>
<td>Moves focus to the next element (below)</td>
</tr>
<tr>
<td><code>Left</code></td>
<td>Moves focus to the previous column (to the left)</td>
</tr>
<tr>
<td><code>Right</code></td>
<td>Moves focus to the next column (to the right)</td>
</tr>
<tr>
<td><code>Alt+Up</code></td>
<td>Moves focus to the parent element</td>
</tr>
<tr>
<td><code>Alt+Down</code></td>
<td>Moves focus to the first child element</td>
</tr>
</tbody>
</table>
</div>
<div class="help-column-half">
<h4>Editing</h4>
<table>
<tbody>
<tr>
<td><code>Enter</code></td>
<td>Opens the content editor of the focused element</td>
</tr>
<tr>
<td><code>Space</code></td>
<td>Opens the properties popup of the focused element</td>
</tr>
<tr>
<td><code>Esc</code></td>
<td>Closes the properties popup of the focused element</td>
</tr>
<tr>
<td><code>Del</code></td>
<td>Deletes the focused element</td>
</tr>
</tbody>
</table>
</div>
<div class="help-column-half">
<h4>Moving</h4>
<table>
<tbody>
<tr>
<td><code>Ctrl+Up</code></td>
<td>Moves (reorders) the focused element up</td>
</tr>
<tr>
<td><code>Ctrl+Down</code></td>
<td>Moves (reorders) the focused element down</td>
</tr>
<tr>
<td><code>Ctrl+Left</code></td>
<td>Moves (reorders) the focused column left</td>
</tr>
<tr>
<td><code>Ctrl+Right</code></td>
<td>Moves (reorders) the focused column right</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="help-row">
<h3>Drag and drop</h3>
<section class="help-column-full">
<p>Drag any existing element to reorder within its parent.</p>
<p>Drag a new element from the toolbox and drop it within a compatible container.</p>
<p>Drag the left and right edges of a focused column to resize the column. By default any adjacent column will be attached and resized accordingly; holding down the <code>Alt</code> key while resizing unattaches from the adjacent column and instead modifies the offset between.</p>
</section>
</div>
</div>
</div>
</fieldset>

View File

@@ -58,7 +58,9 @@ namespace Orchard.Localization.Drivers {
protected override DriverResult Editor(LocalizationPart part, IUpdateModel updater, dynamic shapeHelper) {
var model = new EditLocalizationViewModel();
if (updater != null && updater.TryUpdateModel(model, TemplatePrefix, null, null)) {
// Content culture has to be set only if it's not set already.
if (updater != null && updater.TryUpdateModel(model, TemplatePrefix, null, null) && GetCulture(part) == null) {
_localizationService.SetContentCulture(part, model.SelectedCulture);
}

View File

@@ -71,11 +71,12 @@ $(function () {
var listWidth = $('#media-library-main-list').width();
var listHeight = $('#media-library-main-list').height();
var itemSize = $('.thumbnail').first().width();
var itemWidth = $('.thumbnail').first().width();
var itemHeight = $('.thumbnail').first().height();
var draftText = $("#media-library").data("draft-text");
var itemsPerRow = Math.floor(listWidth / itemSize);
var itemsPerColumn = Math.ceil(listHeight / itemSize);
var itemsPerRow = Math.floor(listWidth / itemWidth);
var itemsPerColumn = Math.ceil(listHeight / itemHeight);
var pageCount = itemsPerRow * itemsPerColumn;

View File

@@ -8,18 +8,30 @@ using Orchard.MultiTenancy.Services;
namespace Orchard.MultiTenancy.Commands {
public class TenantCommand : DefaultOrchardCommandHandler {
private readonly ITenantService _tenantService;
private readonly string[] _validDataProviderNames = new[] { "SqlCe", "SqlServer", "MySql", "PostgreSql" };
public TenantCommand(ITenantService tenantService) {
_tenantService = tenantService;
}
[OrchardSwitch]
public string Host { get; set; }
public string DataProvider { get; set; }
[OrchardSwitch]
public string DataConnectionString { get; set; }
[OrchardSwitch]
public string DataTablePrefix { get; set; }
[OrchardSwitch]
public string UrlHost { get; set; }
[OrchardSwitch]
public string UrlPrefix { get; set; }
[OrchardSwitch]
public string Themes { get; set; }
[OrchardSwitch]
public string Modules { get; set; }
[OrchardSwitch]
public bool DropDatabaseTables { get; set; }
[CommandHelp("tenant list\r\n\t" + "Display current tenants of a site")]
[CommandHelp("tenant list\r\n\t" + "Display current tenants of the site.")]
[CommandName("tenant list")]
public void List() {
Context.Output.WriteLine(T("List of tenants"));
@@ -28,61 +40,157 @@ namespace Orchard.MultiTenancy.Commands {
var tenants = _tenantService.GetTenants();
foreach (var tenant in tenants) {
Context.Output.WriteLine(T("Name: ") + tenant.Name);
Context.Output.WriteLine(T("Provider: ") + tenant.DataProvider);
Context.Output.WriteLine(T("ConnectionString: ") + tenant.DataConnectionString);
Context.Output.WriteLine(T("Data Table Prefix: ") + tenant.DataTablePrefix);
Context.Output.WriteLine(T("Request Url Host: ") + tenant.RequestUrlHost);
Context.Output.WriteLine(T("Request Url Prefix: ") + tenant.RequestUrlPrefix);
Context.Output.WriteLine(T("State: ") + tenant.State.ToString());
Context.Output.WriteLine(T("Data provider: ") + tenant.DataProvider);
Context.Output.WriteLine(T("Connection string: ") + tenant.DataConnectionString);
Context.Output.WriteLine(T("Data table prefix: ") + tenant.DataTablePrefix);
Context.Output.WriteLine(T("Request URL host: ") + tenant.RequestUrlHost);
Context.Output.WriteLine(T("Request URL prefix: ") + tenant.RequestUrlPrefix);
Context.Output.WriteLine(T("Encryption algorithm: ") + tenant.EncryptionAlgorithm);
Context.Output.WriteLine(T("Encryption key: ") + tenant.EncryptionKey);
Context.Output.WriteLine(T("Hash algorithm: ") + tenant.HashAlgorithm);
Context.Output.WriteLine(T("Hash key: ") + tenant.HashKey);
Context.Output.WriteLine(T("Themes: ") + String.Join(";", tenant.Themes));
Context.Output.WriteLine(T("Modules: ") + String.Join(";", tenant.Modules));
Context.Output.WriteLine(T("---------------------------"));
}
}
[CommandHelp("tenant add <tenantName> /Host:<hostname> /UrlPrefix:<url prefix>\r\n\t" +
"Create new tenant named <tenantName> on the site")]
[CommandName("tenant add")]
[OrchardSwitches("Host,UrlPrefix")]
public void Create(string tenantName) {
Context.Output.WriteLine(T("Creating tenant"));
[CommandHelp("tenant info <tenantName>\r\n\t" + "Display the current settings for a tenant.")]
[CommandName("tenant info")]
public void Info(string tenantName) {
var tenant = _tenantService.GetTenants().FirstOrDefault(x => x.Name == tenantName);
if (tenant == null) {
Context.Output.WriteLine(T("Could not read tenant '{0}'. No tenant with that name exists.", tenantName));
return;
}
if (string.IsNullOrWhiteSpace(tenantName) || !Regex.IsMatch(tenantName, @"^\w+$")) {
Context.Output.WriteLine(T("Tenant settings:"));
Context.Output.WriteLine(T("---------------------------"));
Context.Output.WriteLine(T("Name: ") + tenant.Name);
Context.Output.WriteLine(T("State: ") + tenant.State.ToString());
Context.Output.WriteLine(T("Data provider: ") + tenant.DataProvider);
Context.Output.WriteLine(T("Connection string: ") + tenant.DataConnectionString);
Context.Output.WriteLine(T("Data table prefix: ") + tenant.DataTablePrefix);
Context.Output.WriteLine(T("Request URL host: ") + tenant.RequestUrlHost);
Context.Output.WriteLine(T("Request URL prefix: ") + tenant.RequestUrlPrefix);
Context.Output.WriteLine(T("Encryption algorithm: ") + tenant.EncryptionAlgorithm);
Context.Output.WriteLine(T("Encryption key: ") + tenant.EncryptionKey);
Context.Output.WriteLine(T("Hash algorithm: ") + tenant.HashAlgorithm);
Context.Output.WriteLine(T("Hash key: ") + tenant.HashKey);
Context.Output.WriteLine(T("Themes: ") + String.Join(";", tenant.Themes));
Context.Output.WriteLine(T("Modules: ") + String.Join(";", tenant.Modules));
Context.Output.WriteLine(T("---------------------------"));
}
[CommandHelp("tenant add <tenantName> /DataProvider:<provider> /DataConnectionString:<connectionString> /DataTablePrefix:<prefix> /UrlHost:<hostname> /UrlPrefix:<prefix> /Themes:<themes> /Modules:<modules>\r\n\t" + "Create a new tenant named <tenantName> on the site.\r\n" + "The <themes> and <modules> parameters should be semicolon-separated lists of module names.")]
[CommandName("tenant add")]
[OrchardSwitches("DataProvider,DataConnectionString,DataTablePrefix,UrlHost,UrlPrefix,Themes,Modules")]
public void Create(string tenantName) {
Context.Output.WriteLine(T("Creating tenant '{0}'...", tenantName));
if (String.IsNullOrWhiteSpace(tenantName) || !Regex.IsMatch(tenantName, @"^\w+$")) {
Context.Output.WriteLine(T("Invalid tenant name. Must contain characters only and no spaces."));
return;
}
if (_tenantService.GetTenants().Any(tenant => string.Equals(tenant.Name, tenantName, StringComparison.OrdinalIgnoreCase))) {
Context.Output.WriteLine(T("Could not create tenant \"{0}\". A tenant with the same name already exists.", tenantName));
if (_tenantService.GetTenants().Any(tenant => String.Equals(tenant.Name, tenantName, StringComparison.OrdinalIgnoreCase))) {
Context.Output.WriteLine(T("Could not create tenant '{0}'. A tenant with the same name already exists.", tenantName));
return;
}
if (DataProvider != null && !_validDataProviderNames.Contains(DataProvider)) {
Context.Output.WriteLine(T("Invalid value '{0}' for parameter DataProvider. Expect one of the following: {1}", DataProvider, String.Join(", ", _validDataProviderNames)));
return;
}
_tenantService.CreateTenant(
new ShellSettings {
Name = tenantName,
RequestUrlHost = Host,
State = TenantState.Uninitialized,
DataProvider = DataProvider,
DataConnectionString = DataConnectionString,
DataTablePrefix = DataTablePrefix,
RequestUrlHost = UrlHost,
RequestUrlPrefix = UrlPrefix,
State = TenantState.Uninitialized
Themes = Themes.Split(';'),
Modules = Modules.Split(';')
});
}
[CommandHelp("tenant info <tenantName>\r\n\t" + "Display settings for a tenant")]
[CommandName("tenant info")]
public void Info(string tenantName) {
ShellSettings tenant = _tenantService.GetTenants().Where(x => x.Name == tenantName).FirstOrDefault();
[CommandHelp("tenant update <tenantName> /DataProvider:<SqlCe|SqlServer|MySql|PostgreSql> /DataConnectionString:<connectionString> /DataTablePrefix:<prefix> /UrlHost:<hostname> /UrlPrefix:<prefix> /Themes:<themes> /Modules:<modules>\r\n\t" + "Update the settings of the existing tenant <tenantName>.\r\n" + "The <themes> and <modules> parameters should be semicolon-separated lists of module names.")]
[CommandName("tenant update")]
[OrchardSwitches("DataProvider,DataConnectionString,DataTablePrefix,UrlHost,UrlPrefix,Themes,Modules")]
public void Edit(string tenantName) {
Context.Output.WriteLine(T("Updating tenant '{0}'...", tenantName));
var tenant = _tenantService.GetTenants().FirstOrDefault(t => String.Equals(t.Name, tenantName, StringComparison.OrdinalIgnoreCase));
if (tenant == null) {
Context.Output.Write(T("Tenant: ") + tenantName + T(" was not found"));
Context.Output.WriteLine(T("Could not update tenant '{0}'. No tenant with that name exists.", tenantName));
return;
}
else {
Context.Output.WriteLine(T("Tenant Settings:"));
Context.Output.WriteLine(T("---------------------------"));
Context.Output.WriteLine(T("Name: ") + tenant.Name);
Context.Output.WriteLine(T("Provider: ") + tenant.DataProvider);
Context.Output.WriteLine(T("ConnectionString: ") + tenant.DataConnectionString);
Context.Output.WriteLine(T("Data Table Prefix: ") + tenant.DataTablePrefix);
Context.Output.WriteLine(T("Request Url Host: ") + tenant.RequestUrlHost);
Context.Output.WriteLine(T("Request Url Prefix: ") + tenant.RequestUrlPrefix);
Context.Output.WriteLine(T("State: ") + tenant.State.ToString());
Context.Output.WriteLine(T("---------------------------"));
if (DataProvider != null && !_validDataProviderNames.Contains(DataProvider)) {
Context.Output.WriteLine(T("Invalid value '{0}' for parameter DataProvider. Expect one of the following: {1}", DataProvider, String.Join(", ", _validDataProviderNames)));
return;
}
_tenantService.UpdateTenant(
new ShellSettings {
Name = tenant.Name,
State = tenant.State,
DataProvider = DataProvider ?? tenant.DataProvider,
DataConnectionString = DataConnectionString ?? tenant.DataConnectionString,
DataTablePrefix = DataTablePrefix ?? tenant.DataTablePrefix,
RequestUrlHost = UrlHost ?? tenant.RequestUrlHost,
RequestUrlPrefix = UrlPrefix ?? tenant.RequestUrlPrefix,
Themes = Themes != null ? Themes.Split(';') : tenant.Themes,
Modules = Modules != null ? Modules.Split(';') : tenant.Modules
});
}
[CommandHelp("tenant disable <tenantName>\r\n\t" + "Disable the tenant <tenantName>.")]
[CommandName("tenant disable")]
public void Disable(string tenantName) {
Context.Output.WriteLine(T("Disabling tenant '{0}'...", tenantName));
var tenant = _tenantService.GetTenants().FirstOrDefault(t => String.Equals(t.Name, tenantName, StringComparison.OrdinalIgnoreCase));
if (tenant == null) {
Context.Output.WriteLine(T("Could not disable tenant '{0}'. No tenant with that name exists.", tenantName));
return;
}
tenant.State = TenantState.Disabled;
_tenantService.UpdateTenant(tenant);
}
[CommandHelp("tenant enable <tenantName>\r\n\t" + "Enable the tenant <tenantName>.")]
[CommandName("tenant enable")]
public void Enable(string tenantName) {
Context.Output.WriteLine(T("Enabling tenant '{0}'...", tenantName));
var tenant = _tenantService.GetTenants().FirstOrDefault(t => String.Equals(t.Name, tenantName, StringComparison.OrdinalIgnoreCase));
if (tenant == null) {
Context.Output.WriteLine(T("Could not enable tenant '{0}'. No tenant with that name exists.", tenantName));
return;
}
tenant.State = TenantState.Running;
_tenantService.UpdateTenant(tenant);
}
[CommandHelp("tenant reset <tenantName> /DropDatabaseTables:<true|false>\r\n\t" + "Reset the tenant <tenantName> to its uninitialized, optionally dropping its tables from the database.")]
[CommandName("tenant reset")]
[OrchardSwitches("DropDatabaseTables")]
public void Reset(string tenantName) {
Context.Output.WriteLine(T("Resetting tenant '{0}'...", tenantName));
var tenant = _tenantService.GetTenants().FirstOrDefault(t => String.Equals(t.Name, tenantName, StringComparison.OrdinalIgnoreCase));
if (tenant == null) {
Context.Output.WriteLine(T("Could not reset tenant '{0}'. No tenant with that name exists.", tenantName));
return;
}
_tenantService.ResetTenant(tenant, DropDatabaseTables);
}
}
}

View File

@@ -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,21 +90,21 @@ 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();
@@ -129,15 +131,17 @@ namespace Orchard.MultiTenancy.Controllers {
[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,13 +7,14 @@
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)) {
<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;
@@ -24,11 +25,11 @@
</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
@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.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.ActionLink(T("Edit").ToString(), "Edit", new { name = tenant.Name, area = "Orchard.MultiTenancy" })
</div>
</div>
</li>

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>
}

View File

@@ -3,12 +3,6 @@
namespace Orchard.Recipes {
public class Migrations : DataMigrationImpl {
public int Create() {
//SchemaBuilder.CreateTable("RecipeResultRecord", table => table
// .Column<int>("Id", c => c.PrimaryKey().Identity())
// .Column<string>("ExecutionId", c => c.WithLength(128).Unique().NotNull())
// .Column<bool>("IsCompleted", c => c.NotNull())
//);
SchemaBuilder.CreateTable("RecipeStepResultRecord", table => table
.Column<int>("Id", c => c.PrimaryKey().Identity())
.Column<string>("ExecutionId", c => c.WithLength(128).NotNull())
@@ -18,13 +12,10 @@ namespace Orchard.Recipes {
.Column<string>("ErrorMessage", c => c.Unlimited().Nullable())
);
SchemaBuilder.AlterTable("RecipeStepResultRecord", table => table
.CreateIndex("IDX_RecipeStepResultRecord_ExecutionId", "ExecutionId")
);
SchemaBuilder.AlterTable("RecipeStepResultRecord", table => table
.CreateIndex("IDX_RecipeStepResultRecord_ExecutionId_StepName", "ExecutionId", "StepName")
);
SchemaBuilder.AlterTable("RecipeStepResultRecord", table => {
table.CreateIndex("IDX_RecipeStepResultRecord_ExecutionId", "ExecutionId");
table.CreateIndex("IDX_RecipeStepResultRecord_ExecutionId_StepName", "ExecutionId", "StepName");
});
return 1;
}

View File

@@ -15,8 +15,8 @@
/* Component containers
----------------------------------*/
.ui-widget {
font-family: Verdana,Arial,sans-serif;
font-size: 1.1em;
font-family: inherit;
font-size: inherit;
}
.ui-widget .ui-widget {
font-size: 1em;
@@ -25,7 +25,7 @@
.ui-widget select,
.ui-widget textarea,
.ui-widget button {
font-family: Verdana,Arial,sans-serif;
font-family: inherit;
font-size: 1em;
}
.ui-widget-content {

File diff suppressed because one or more lines are too long

View File

@@ -153,7 +153,7 @@ namespace Orchard.ContentManagement {
/// <returns>The string representation of the value.</returns>
public static string ToString<T>(T value) {
var type = typeof(T);
if (type == typeof(string)) {
if (type == typeof(string) || type == typeof(char)) {
return Convert.ToString(value);
}
if ((!type.IsValueType || Nullable.GetUnderlyingType(type) != null) &&
@@ -250,6 +250,9 @@ namespace Orchard.ContentManagement {
if (type == typeof(double)) return (T)(object)double.NegativeInfinity;
throw new NotSupportedException(String.Format("Infinity not supported for type {0}", type.Name));
}
if (type == typeof(char) || type == typeof(char?)) {
return (T)(object)char.Parse(value);
}
if (type == typeof(int) || type == typeof(int?)) {
return (T)(object)int.Parse(value, CultureInfo.InvariantCulture);
}

View File

@@ -20,6 +20,9 @@ namespace Orchard.Data.Migration.Schema {
dbType = DbType.Boolean;
break;
default:
if(type == typeof(Guid))
dbType = DbType.Guid;
else
Enum.TryParse(Type.GetTypeCode(type).ToString(), true, out dbType);
break;
}